feat(machine): redesign idle screen with circular touch zones

Full-canvas layout: brand zone with logo/badges/balance at top,
two circular touch zones (Buy=bitcoin orange, Sell=green) with
zone-glow animation. Show per-flow commission rates (Buy/Sell).
Move light/dark toggle to App.vue for omnipresence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-01 12:12:09 -05:00
commit 263165b495

View file

@ -3,16 +3,14 @@ import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { useAtmStore } from '@/stores/atm' import { useAtmStore } from '@/stores/atm'
import { useTheme } from '@/composables/useTheme' import { initialContext } from '@lamassu/state-machine'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import BitcoinIcon from '@/components/BitcoinIcon.vue' import BitcoinIcon from '@/components/BitcoinIcon.vue'
import QRCode from '@/components/QRCode.vue' import QRCode from '@/components/QRCode.vue'
const router = useRouter() const router = useRouter()
const atmStore = useAtmStore() const atmStore = useAtmStore()
const { colorMode } = useTheme()
const lndconnectUrl = import.meta.env.VITE_LNDCONNECT_URL || '' const lndconnectUrl = import.meta.env.VITE_LNDCONNECT_URL || ''
const showZeusQR = ref(false) const showZeusQR = ref(false)
const showLpQR = ref(false) const showLpQR = ref(false)
@ -61,75 +59,82 @@ function handleCashOut() {
</script> </script>
<template> <template>
<div <div class="flex min-h-0 flex-1 flex-col">
class="flex min-h-0 flex-1 flex-col items-center justify-center gap-4 overflow-auto p-4 sm:gap-6 md:gap-8 md:p-8" <!-- Brand Zone (top ~30%) -->
> <div class="flex flex-col items-center justify-center gap-2 pt-[4vh] pb-[2vh]">
<!-- Logo -->
<div class="flex flex-col items-center gap-1 sm:gap-2">
<img <img
src="/logo.png" src="/logo.png"
alt="Lamassu" alt="Lamassu"
class="h-20 w-20 animate-float drop-shadow-[0_0_24px_var(--primary)] sm:h-28 sm:w-28 md:h-40 md:w-40" class="h-[12vh] w-[12vh] animate-float drop-shadow-[0_0_24px_var(--primary)]"
/> />
<h1 class="text-2xl font-bold text-primary sm:text-3xl md:text-4xl">Bitcoinmat</h1> <h1 class="text-[5vh] font-bold text-primary leading-tight">Bitcoinmat</h1>
<!-- Footer badges --> <div class="flex flex-wrap items-center justify-center gap-3">
<div class="mt-2 flex flex-wrap items-center justify-center gap-2 sm:mt-4 sm:gap-4"> <Badge variant="outline" class="border-success text-success text-lg">No KYC</Badge>
<Badge variant="outline" class="border-success text-success text-xs sm:text-sm" <Badge variant="outline" class="border-primary text-primary text-lg">No Registration</Badge>
>No KYC</Badge <Badge variant="outline" class="border-bitcoin text-bitcoin text-lg">Just Bitcoin</Badge>
> </div>
<Badge variant="outline" class="border-primary text-primary text-xs sm:text-sm" <!-- Balance + Commission rates -->
>No Registration</Badge <div
> class="mt-1 flex flex-wrap items-center justify-center gap-4 text-[2vh] text-muted-foreground"
<Badge variant="outline" class="border-bitcoin text-bitcoin text-xs sm:text-sm" >
>Just Bitcoin</Badge <span v-if="atmStore.balanceSats !== null">
> Available:
<span class="font-bold text-primary"
>{{ atmStore.balanceSats.toLocaleString() }} sats</span
>
</span>
<span>
Buy:
<span class="font-bold text-bitcoin"
>{{
(
(atmStore.context?.cashInFeePercent ?? initialContext.cashInFeePercent) * 100
).toFixed(2)
}}%</span
>
</span>
<span>
Sell:
<span class="font-bold text-success"
>{{
(
(atmStore.context?.cashOutFeePercent ?? initialContext.cashOutFeePercent) * 100
).toFixed(2)
}}%</span
>
</span>
</div> </div>
</div> </div>
<!-- Balance --> <!-- Touch Zones (fills remaining space) -->
<div v-if="atmStore.balanceSats !== null" class="text-center"> <div class="flex flex-1 items-center justify-center gap-[8vw] pb-[4vh]">
<p class="text-sm text-muted-foreground">Available Balance</p>
<p class="text-xl font-bold text-primary sm:text-2xl">
{{ atmStore.balanceSats.toLocaleString() }} sats
</p>
</div>
<!-- Action Buttons -->
<div class="flex flex-col gap-4 sm:flex-row sm:gap-6 md:gap-8">
<!-- Buy Bitcoin --> <!-- Buy Bitcoin -->
<Card <button
class="h-40 w-40 cursor-pointer border-2 border-success/50 bg-success/20 transition-all hover:scale-105 hover:border-success hover:bg-success/30 active:scale-95 sm:h-52 sm:w-52 md:h-64 md:w-64" class="flex aspect-square w-[36vh] flex-col items-center justify-center gap-4 rounded-full border-2 border-bitcoin/50 bg-bitcoin/10 transition-all active:scale-[0.97] animate-zone-glow"
style="--glow-color: var(--bitcoin)"
@click="handleCashIn" @click="handleCashIn"
> >
<CardContent <BitcoinIcon :size="100" class="shrink-0" />
class="flex h-full flex-col items-center justify-center gap-2 p-4 sm:gap-3 sm:p-6" <span class="text-[3.5vh] font-bold text-bitcoin">Buy Bitcoin</span>
> <span class="text-[1.8vh] text-foreground/70">Insert cash, receive sats</span>
<BitcoinIcon :size="48" class="shrink-0 sm:hidden" /> </button>
<BitcoinIcon :size="64" class="hidden shrink-0 sm:block" />
<span class="text-lg font-bold text-success sm:text-xl">Buy Bitcoin</span>
<span class="hidden text-sm text-foreground/70 sm:block">Insert cash, receive sats</span>
</CardContent>
</Card>
<!-- Sell Bitcoin --> <!-- Sell Bitcoin -->
<Card <button
class="h-40 w-40 cursor-pointer border-2 border-warning/50 bg-warning/20 transition-all hover:scale-105 hover:border-warning hover:bg-warning/30 active:scale-95 sm:h-52 sm:w-52 md:h-64 md:w-64" class="flex aspect-square w-[36vh] flex-col items-center justify-center gap-4 rounded-full border-2 border-success/50 bg-success/10 transition-all active:scale-[0.97] animate-zone-glow"
style="--glow-color: var(--success)"
@click="handleCashOut" @click="handleCashOut"
> >
<CardContent <span class="text-[8vh] leading-none">💵</span>
class="flex h-full flex-col items-center justify-center gap-2 p-4 sm:gap-3 sm:p-6" <span class="text-[3.5vh] font-bold text-success">Sell Bitcoin</span>
> <span class="text-[1.8vh] text-foreground/70">Pay invoice, receive cash</span>
<span class="text-5xl sm:text-7xl">💵</span> </button>
<span class="text-lg font-bold text-warning sm:text-xl">Sell Bitcoin</span>
<span class="hidden text-sm text-foreground/70 sm:block">Pay invoice, receive cash</span>
</CardContent>
</Card>
</div> </div>
<!-- Dev QR buttons --> <!-- Dev QR buttons -->
<div <div
v-if="atmStore.debugMode" v-if="atmStore.debugMode"
class="flex flex-wrap items-center justify-center gap-2 sm:gap-4" class="absolute bottom-16 left-1/2 -translate-x-1/2 flex flex-wrap items-center justify-center gap-4"
> >
<Button <Button
v-if="lndconnectUrl" v-if="lndconnectUrl"
@ -159,8 +164,7 @@ function handleCashOut() {
> >
<p class="text-sm text-white/70">Scan with Zeus to connect to lnd-alice</p> <p class="text-sm text-white/70">Scan with Zeus to connect to lnd-alice</p>
<div class="rounded-2xl"> <div class="rounded-2xl">
<QRCode :value="lndconnectUrl" :size="280" class="sm:hidden" /> <QRCode :value="lndconnectUrl" :size="400" />
<QRCode :value="lndconnectUrl" :size="400" class="hidden sm:block" />
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button <Button
@ -185,10 +189,9 @@ function handleCashOut() {
> >
<p class="text-sm text-white/70">Lightning.Pub nprofile</p> <p class="text-sm text-white/70">Lightning.Pub nprofile</p>
<div class="rounded-2xl"> <div class="rounded-2xl">
<QRCode :value="lpNprofile" :size="280" class="sm:hidden" /> <QRCode :value="lpNprofile" :size="400" />
<QRCode :value="lpNprofile" :size="400" class="hidden sm:block" />
</div> </div>
<code class="max-w-[90vw] truncate text-xs text-white/50 sm:max-w-md">{{ lpNprofile }}</code> <code class="max-w-[90vw] truncate text-xs text-white/50">{{ lpNprofile }}</code>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button variant="outline" size="sm" class="text-white" @click="copyToClipboard(lpNprofile)"> <Button variant="outline" size="sm" class="text-white" @click="copyToClipboard(lpNprofile)">
{{ copied ? 'Copied!' : 'Copy' }} {{ copied ? 'Copied!' : 'Copy' }}
@ -199,22 +202,12 @@ function handleCashOut() {
</div> </div>
</div> </div>
<!-- Light/dark toggle -->
<Button
variant="ghost"
size="sm"
class="absolute bottom-4 left-4 text-xs text-muted-foreground"
@click="colorMode = colorMode === 'dark' ? 'light' : 'dark'"
>
{{ colorMode === 'dark' ? '☀' : '☾' }}
</Button>
<!-- Debug toggle (only visible when debug bar is hidden, never in production) --> <!-- Debug toggle (only visible when debug bar is hidden, never in production) -->
<Button <Button
v-if="!atmStore.debugMode && atmStore.allowMockFallback" v-if="!atmStore.debugMode && atmStore.allowMockFallback"
variant="ghost" variant="ghost"
size="sm" size="kiosk"
class="absolute bottom-4 right-4 text-xs text-muted-foreground" class="absolute bottom-6 right-6 text-lg text-muted-foreground"
@click="atmStore.toggleDebug" @click="atmStore.toggleDebug"
> >
Show Debug Show Debug