fix(machine): add mobile responsive layout for atm.aiolabs.dev

The kiosk UI was optimized for 1920px touchscreens, breaking mobile web.
Uses Tailwind lg: breakpoints (mobile-first) so mobile works naturally
while kiosk sizing applies at 1024px+. No JS branching — pure CSS.

- Viewport: width=1920 → width=device-width
- Kiosk-only: overflow:hidden and cursor:none behind @media (min-width: 1024px)
- IdleView: stack buttons vertically on mobile, justify-around for even spacing
- CashInView/CashOutView: stack split panels on mobile (flex-col-reverse lg:flex-row)
- QRCode: SVG scales down on small screens via max-w-full
- App.vue: hide verbose badges on mobile, debug bar collapses theme/color selectors
- Connection/network badges always visible (not gated by isIdle)
- Reduce zone-glow animation intensity by half (20px/4px vs 40px/8px)
- All lg: values verified to match original kiosk rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-01 23:41:18 -05:00
commit 7a42380f0e
7 changed files with 257 additions and 149 deletions

View file

@ -55,7 +55,9 @@ function toggleLiveServices() {
</script>
<template>
<div class="flex h-dvh w-screen flex-col overflow-hidden bg-background font-sans text-foreground">
<div
class="flex min-h-dvh lg:h-dvh w-screen flex-col overflow-y-auto lg:overflow-hidden bg-background font-sans text-foreground"
>
<!-- Maintenance screen: shown when initialization fails in production -->
<div
v-if="atmStore.initError"
@ -77,8 +79,8 @@ function toggleLiveServices() {
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
<h1 class="text-[3.5rem] font-bold">ATM Unavailable</h1>
<p class="max-w-md text-center text-2xl text-muted-foreground">
<h1 class="text-2xl lg:text-[3.5rem] font-bold">ATM Unavailable</h1>
<p class="max-w-md text-center text-base lg:text-2xl text-muted-foreground">
This machine is temporarily out of service. Please try again later or use another machine.
</p>
<p v-if="atmStore.debugMode" class="max-w-lg text-center font-mono text-sm text-destructive">
@ -89,42 +91,47 @@ function toggleLiveServices() {
<template v-else>
<router-view />
<!-- Connection status + balance (top right, idle only) -->
<!-- Connection status + network (always visible, top right) -->
<div
v-if="atmStore.isIdle"
class="fixed right-6 top-6 z-50 flex flex-wrap items-center justify-end gap-2"
class="fixed right-2 top-2 lg:right-6 lg:top-6 z-50 flex flex-wrap items-center justify-end gap-1 lg:gap-2"
>
<!-- Price and balance: only on idle, hidden on mobile -->
<Badge
v-if="formattedBtcPrice"
class="bg-primary/10 font-mono text-primary border border-primary/30 text-base"
v-if="atmStore.isIdle && formattedBtcPrice"
class="hidden lg:inline-flex bg-primary/10 font-mono text-primary border border-primary/30 text-base"
>
{{ formattedBtcPrice }}
</Badge>
<Badge
v-if="atmStore.balanceSats !== null"
class="bg-bitcoin/20 font-mono text-bitcoin border border-bitcoin/30 text-base"
v-if="atmStore.isIdle && atmStore.balanceSats !== null"
class="hidden lg:inline-flex bg-bitcoin/20 font-mono text-bitcoin border border-bitcoin/30 text-base"
>
bal: {{ formatSats(atmStore.balanceSats) }} sats
</Badge>
<!-- Connection status: always visible -->
<Badge
v-if="atmStore.connectionStatus === 'connected'"
class="bg-success text-success-foreground"
class="bg-success text-success-foreground text-[10px] lg:text-xs"
>
Live
</Badge>
<Badge
v-else-if="atmStore.connectionStatus === 'connecting'"
class="bg-warning text-warning-foreground"
class="bg-warning text-warning-foreground text-[10px] lg:text-xs"
>
Connecting...
</Badge>
<Badge v-else-if="atmStore.connectionStatus === 'error'" class="bg-destructive">
<Badge
v-else-if="atmStore.connectionStatus === 'error'"
class="bg-destructive text-[10px] lg:text-xs"
>
Offline
</Badge>
<Badge v-else class="bg-muted text-muted-foreground">Mock</Badge>
<Badge v-else class="bg-muted text-muted-foreground text-[10px] lg:text-xs">Mock</Badge>
<!-- Network badge: always visible when not mainnet -->
<Badge
v-if="atmStore.network && atmStore.network !== 'mainnet'"
class="bg-warning/20 text-warning border border-warning/30"
class="bg-warning/20 text-warning border border-warning/30 text-[10px] lg:text-xs"
>
{{ atmStore.network }}
</Badge>
@ -134,12 +141,11 @@ function toggleLiveServices() {
<Button
v-if="!atmStore.allowMockFallback"
variant="outline"
size="kiosk"
class="fixed bottom-6 right-6 z-50 gap-3"
class="fixed bottom-3 right-3 lg:bottom-6 lg:right-6 z-50 h-10 px-3 py-1 text-sm rounded-lg lg:h-[7vh] lg:min-h-[70px] lg:px-8 lg:py-3 lg:text-2xl lg:rounded-xl gap-2 lg:gap-3"
@click="colorMode = colorMode === 'dark' ? 'light' : 'dark'"
>
<Sun v-if="colorMode === 'dark'" :size="28" />
<Moon v-else :size="28" />
<Sun v-if="colorMode === 'dark'" class="w-5 h-5 lg:w-7 lg:h-7" />
<Moon v-else class="w-5 h-5 lg:w-7 lg:h-7" />
{{ colorMode === 'dark' ? 'Light' : 'Dark' }}
</Button>
@ -148,7 +154,8 @@ function toggleLiveServices() {
v-if="atmStore.debugMode"
class="shrink-0 bg-popover pb-[env(safe-area-inset-bottom)] font-mono text-xs text-popover-foreground"
>
<div class="flex flex-wrap items-center gap-2 px-4 py-2 sm:gap-4">
<!-- Compact bar: always visible -->
<div class="flex flex-wrap items-center gap-2 lg:gap-4 px-3 py-1.5 lg:px-4 lg:py-2">
<Button
variant="ghost"
size="sm"
@ -157,11 +164,17 @@ function toggleLiveServices() {
>
{{ debugExpanded ? '▼' : '▲' }}
</Button>
<span class="text-success">State: {{ atmStore.currentState }}</span>
<Button variant="outline" size="sm" class="h-6 text-xs" @click="toggleLiveServices">
{{ atmStore.useLiveServices ? 'Switch to Mock' : 'Connect Live' }}
<span class="text-success text-[10px] lg:text-xs">{{ atmStore.currentState }}</span>
<Button
variant="outline"
size="sm"
class="h-5 lg:h-6 text-[10px] lg:text-xs"
@click="toggleLiveServices"
>
{{ atmStore.useLiveServices ? 'Mock' : 'Live' }}
</Button>
<div class="flex flex-wrap items-center gap-1">
<!-- Theme + color mode: hidden on mobile, shown inline on desktop -->
<div class="hidden lg:flex flex-wrap items-center gap-1">
<Button
v-for="theme in themes"
:key="theme.id"
@ -173,7 +186,7 @@ function toggleLiveServices() {
{{ theme.label }}
</Button>
</div>
<div class="flex items-center gap-1">
<div class="hidden lg:flex items-center gap-1">
<Button
:variant="colorMode === 'light' ? 'default' : 'outline'"
size="sm"
@ -202,14 +215,60 @@ function toggleLiveServices() {
<Button
variant="ghost"
size="sm"
class="ml-auto h-6 text-xs text-muted-foreground"
class="ml-auto h-5 lg:h-6 text-[10px] lg:text-xs text-muted-foreground"
@click="atmStore.toggleDebug"
>
Hide Debug
Hide
</Button>
</div>
<div v-if="debugExpanded" class="max-h-48 overflow-auto px-4 pb-4">
<pre class="text-muted-foreground">{{ JSON.stringify(atmStore.context, null, 2) }}</pre>
<!-- Expanded section: theme/color on mobile + JSON context -->
<div v-if="debugExpanded" class="px-3 pb-3 lg:px-4 lg:pb-4 space-y-2">
<!-- Theme + color mode on mobile (hidden on desktop since shown above) -->
<div class="lg:hidden space-y-2">
<div class="flex flex-wrap items-center gap-1">
<Button
v-for="theme in themes"
:key="'m-' + theme.id"
:variant="currentTheme === theme.id ? 'default' : 'outline'"
size="sm"
class="h-5 text-[10px]"
@click="currentTheme = theme.id"
>
{{ theme.label }}
</Button>
</div>
<div class="flex items-center gap-1">
<Button
:variant="colorMode === 'light' ? 'default' : 'outline'"
size="sm"
class="h-5 text-[10px]"
@click="colorMode = 'light'"
>
Light
</Button>
<Button
:variant="colorMode === 'dark' ? 'default' : 'outline'"
size="sm"
class="h-5 text-[10px]"
@click="colorMode = 'dark'"
>
Dark
</Button>
<Button
:variant="colorMode === 'system' ? 'default' : 'outline'"
size="sm"
class="h-5 text-[10px]"
@click="colorMode = 'system'"
>
System
</Button>
</div>
</div>
<div class="max-h-48 overflow-auto">
<pre class="text-muted-foreground text-[10px] lg:text-xs">{{
JSON.stringify(atmStore.context, null, 2)
}}</pre>
</div>
</div>
</div>
</template>

View file

@ -62,7 +62,7 @@ onUnmounted(() => {
/>
<!-- QR code -->
<div class="rounded-xl bg-qr p-6">
<div class="rounded-xl bg-qr p-4 lg:p-6 max-w-full [&_svg]:max-w-full [&_svg]:h-auto">
<QrcodeVue
:value="value"
:size="qrSize"

View file

@ -624,7 +624,7 @@
box-shadow: 0 0 0 0 var(--glow-color, transparent);
}
50% {
box-shadow: 0 0 40px 8px var(--glow-color, transparent);
box-shadow: 0 0 20px 4px var(--glow-color, transparent);
}
}
@utility animate-zone-glow {

View file

@ -168,24 +168,24 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
<div class="flex min-h-0 flex-1 flex-col">
<!-- Header with success flow accent -->
<header
class="flex items-center px-8 py-4"
class="flex items-center px-4 lg:px-8 py-2 lg:py-4"
style="background: color-mix(in srgb, var(--success) 10%, var(--background))"
>
<Button
v-if="showCancelButton"
variant="ghost"
size="kiosk"
class="text-muted-foreground"
class="text-muted-foreground text-sm h-auto min-h-0 px-2 py-1 lg:text-2xl lg:h-[7vh] lg:min-h-[70px] lg:px-8 lg:py-3"
@click="cancel"
>
← Cancel
</Button>
<div v-else class="w-20"></div>
<div class="flex flex-1 flex-col items-center gap-2">
<h2 class="text-[2.5rem] font-semibold">Buy Bitcoin</h2>
<div v-else class="w-16 lg:w-20"></div>
<div class="flex flex-1 flex-col items-center gap-1 lg:gap-2">
<h2 class="text-xl lg:text-[2.5rem] font-semibold">Buy Bitcoin</h2>
<StepIndicator :steps="cashInSteps" :current-step="currentStepIndex" />
</div>
<div class="w-20"></div>
<div class="w-16 lg:w-20"></div>
</header>
<!-- Content area -->
@ -202,26 +202,32 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
</div>
<!-- Inserting Bills (split-screen) -->
<div v-else-if="nestedState === 'insertingBills'" key="insertingBills" class="flex flex-1">
<!-- Left panel (52%) — amounts and controls -->
<div class="flex w-[52%] flex-col justify-center gap-6 px-[4vw] py-6">
<div
v-else-if="nestedState === 'insertingBills'"
key="insertingBills"
class="flex flex-1 flex-col lg:flex-row"
>
<!-- Left panel — amounts and controls -->
<div
class="flex w-full lg:w-[52%] flex-col justify-center gap-4 lg:gap-6 px-4 lg:px-[4vw] py-4 lg:py-6"
>
<!-- Amount display -->
<div v-if="context">
<p class="text-[6vh] font-bold text-success leading-tight">
<p class="text-3xl lg:text-[6vh] font-bold text-success leading-tight">
{{ atmStore.fiatSymbol }}{{ (context.fiatAmount / 100).toFixed(2) }}
</p>
<p class="text-[3.5vh] font-semibold text-bitcoin">
<p class="text-xl lg:text-[3.5vh] font-semibold text-bitcoin">
{{ formatSats(context.satsAmount) }} sats
</p>
</div>
<!-- Rate -->
<p v-if="context" class="text-xl text-muted-foreground">
<p v-if="context" class="text-sm lg:text-xl text-muted-foreground">
{{ atmStore.fiatSymbol }}{{ formatFiatPrice(context.exchangeRate) }}/BTC
</p>
<p
v-if="context && atmStore.fiatCode !== 'USD' && atmStore.btcPriceUsd"
class="text-lg text-muted-foreground"
class="text-sm lg:text-lg text-muted-foreground"
>
${{ formatFiatPrice(atmStore.btcPriceUsd) }} USD/BTC
</p>
@ -229,17 +235,19 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
<!-- Available balance -->
<p
v-if="context && context.availableBalance > 0"
class="text-xl"
class="text-sm lg:text-xl"
:class="balanceLimitReached ? 'text-foreground' : 'text-muted-foreground'"
>
Available: {{ formatSats(context.availableBalance) }} sats
</p>
<!-- Status -->
<p v-if="balanceLimitReached" class="text-xl text-muted-foreground">
<p v-if="balanceLimitReached" class="text-sm lg:text-xl text-muted-foreground">
Maximum amount reached — press Done to continue
</p>
<p v-else class="text-xl text-muted-foreground">💵 Insert bills into the acceptor</p>
<p v-else class="text-sm lg:text-xl text-muted-foreground">
💵 Insert bills into the acceptor
</p>
<!-- Dev: Simulate bill insertion -->
<Alert v-if="atmStore.debugMode" class="border-destructive/50 bg-destructive/10">
@ -272,8 +280,10 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
</Button>
</div>
<!-- Right panel (48%) — bill visual -->
<div class="flex w-[48%] flex-col items-center justify-center gap-6 bg-foreground/5">
<!-- Right panel — bill visual (kiosk only) -->
<div
class="hidden lg:flex w-[48%] flex-col items-center justify-center gap-6 bg-foreground/5"
>
<span class="text-[14vh] leading-none">💵</span>
<p class="text-2xl text-muted-foreground">Feed bills here</p>
<p v-if="context" class="text-lg text-muted-foreground">
@ -296,21 +306,29 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
</div>
<!-- Display QR (split-screen) -->
<div v-else-if="nestedState === 'displayingQR'" key="displayingQR" class="flex flex-1">
<!-- Left panel (52%) — info -->
<div class="flex w-[52%] flex-col items-center justify-center gap-5 px-[4vw]">
<p class="text-[2rem] font-semibold text-success">Scan to receive your sats</p>
<p class="text-[7vh] font-bold text-bitcoin leading-tight">
<div
v-else-if="nestedState === 'displayingQR'"
key="displayingQR"
class="flex flex-1 flex-col-reverse lg:flex-row"
>
<!-- Left panel — info -->
<div
class="flex w-full lg:w-[52%] flex-col items-center justify-center gap-3 lg:gap-5 px-4 lg:px-[4vw] py-4 lg:py-0"
>
<p class="text-lg lg:text-[2rem] font-semibold text-success">
Scan to receive your sats
</p>
<p class="text-3xl lg:text-[7vh] font-bold text-bitcoin leading-tight">
{{ formatSats(context?.satsAmount || 0) }} sats
</p>
<p class="text-2xl text-muted-foreground">
<p class="text-lg lg:text-2xl text-muted-foreground">
{{ atmStore.fiatSymbol }}{{ ((context?.fiatAmount || 0) / 100).toFixed(2) }}
</p>
<!-- Waiting indicator -->
<div class="flex items-center gap-3 pt-4">
<div class="flex items-center gap-3 pt-2 lg:pt-4">
<BounceDots />
<p class="text-xl text-muted-foreground">Waiting for wallet scan...</p>
<p class="text-sm lg:text-xl text-muted-foreground">Waiting for wallet scan...</p>
</div>
<!-- LNURL URI (web-ui only) -->
@ -342,8 +360,10 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
</div>
</div>
<!-- Right panel (48%) — QR code -->
<div class="flex w-[48%] flex-col items-center justify-center bg-foreground/5 p-6">
<!-- Right panel — QR code -->
<div
class="flex w-full lg:w-[48%] flex-col items-center justify-center bg-foreground/5 p-4 lg:p-6"
>
<div v-if="isGeneratingLnurl && !context?.ndebitUri">
<BounceDots class="scale-150" />
</div>
@ -364,9 +384,9 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
style="background: color-mix(in srgb, var(--destructive) 8%, var(--background))"
>
<div class="text-[8vh]">⚠️</div>
<h3 class="text-[3rem] font-bold text-destructive">Cash Already Accepted</h3>
<p class="max-w-lg text-center text-2xl text-muted-foreground">
<div class="text-5xl lg:text-[8vh]">⚠️</div>
<h3 class="text-2xl lg:text-[3rem] font-bold text-destructive">Cash Already Accepted</h3>
<p class="max-w-lg text-center text-base lg:text-2xl text-muted-foreground">
Your {{ atmStore.fiatSymbol
}}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted and
cannot be returned. Please scan the QR code to receive your Bitcoin.
@ -392,32 +412,32 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
<div
v-else-if="nestedState === 'complete'"
key="complete"
class="flex flex-1 items-center justify-center gap-10 p-8"
class="flex flex-1 flex-col lg:flex-row items-center justify-center gap-6 lg:gap-10 p-4 lg:p-8"
style="background: color-mix(in srgb, var(--success) 10%, var(--background))"
>
<!-- Left side — details -->
<div class="flex flex-col items-center gap-5">
<div class="text-[8vh]">🎉</div>
<h3 class="text-[3rem] font-bold text-success">Transaction Complete!</h3>
<div class="flex flex-col items-center gap-3 lg:gap-5">
<div class="text-5xl lg:text-[8vh]">🎉</div>
<h3 class="text-2xl lg:text-[3rem] font-bold text-success">Transaction Complete!</h3>
<!-- Transaction details -->
<div
v-if="context"
class="w-full max-w-md rounded-xl bg-background/60 px-8 py-6 space-y-3"
class="w-full max-w-md rounded-xl bg-background/60 px-4 py-4 lg:px-8 lg:py-6 space-y-2 lg:space-y-3"
>
<div class="flex justify-between text-2xl">
<div class="flex justify-between text-base lg:text-2xl">
<span class="text-muted-foreground">Inserted</span>
<span class="font-semibold"
>{{ atmStore.fiatSymbol }}{{ (context.fiatAmount / 100).toFixed(2) }}</span
>
</div>
<div class="flex justify-between text-2xl">
<div class="flex justify-between text-base lg:text-2xl">
<span class="text-muted-foreground">Rate</span>
<span
>{{ atmStore.fiatSymbol }}{{ formatFiatPrice(context.exchangeRate) }}/BTC</span
>
</div>
<div class="flex justify-between text-2xl">
<div class="flex justify-between text-base lg:text-2xl">
<span class="text-muted-foreground"
>Commission ({{ (context.feePercent * 100).toFixed(1) }}%)</span
>
@ -434,13 +454,13 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
>
</div>
<div class="border-t border-border my-2"></div>
<div class="flex justify-between text-[1.8rem] font-bold">
<div class="flex justify-between text-xl lg:text-[1.8rem] font-bold">
<span class="text-success">You received</span>
<span class="text-success">{{ formatSats(context.satsAmount) }} sats</span>
</div>
</div>
<p class="text-xl text-muted-foreground">
<p class="text-sm lg:text-xl text-muted-foreground">
Take a photo of this screen for your receipt
</p>
@ -462,12 +482,14 @@ const isProcessing = computed(() => atmStore.isPayingInvoice)
<div
v-else-if="nestedState === 'error'"
key="error"
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
class="flex flex-1 flex-col items-center justify-center gap-4 lg:gap-6 p-4 lg:p-8"
style="background: color-mix(in srgb, var(--destructive) 8%, var(--background))"
>
<div class="text-[8vh]">⚠️</div>
<h3 class="text-[3rem] font-bold text-destructive">Error</h3>
<p class="text-2xl text-muted-foreground">{{ context?.error || 'An error occurred' }}</p>
<div class="text-5xl lg:text-[8vh]">⚠️</div>
<h3 class="text-2xl lg:text-[3rem] font-bold text-destructive">Error</h3>
<p class="text-base lg:text-2xl text-muted-foreground">
{{ context?.error || 'An error occurred' }}
</p>
<Button
v-if="context && context.billsInserted.length > 0"
class="w-full max-w-md bg-success text-success-foreground"

View file

@ -117,17 +117,21 @@ function formatFiat(cents: number): string {
<div class="flex min-h-0 flex-1 flex-col">
<!-- Header with warning flow accent -->
<header
class="flex items-center px-8 py-4"
class="flex items-center px-4 lg:px-8 py-2 lg:py-4"
style="background: color-mix(in srgb, var(--warning) 10%, var(--background))"
>
<Button variant="ghost" size="kiosk" class="text-muted-foreground" @click="cancel"
<Button
variant="ghost"
size="kiosk"
class="text-muted-foreground text-sm h-auto min-h-0 px-2 py-1 lg:text-2xl lg:h-[7vh] lg:min-h-[70px] lg:px-8 lg:py-3"
@click="cancel"
>← Cancel</Button
>
<div class="flex flex-1 flex-col items-center gap-2">
<h2 class="text-[2.5rem] font-semibold">Sell Bitcoin</h2>
<div class="flex flex-1 flex-col items-center gap-1 lg:gap-2">
<h2 class="text-xl lg:text-[2.5rem] font-semibold">Sell Bitcoin</h2>
<StepIndicator :steps="cashOutSteps" :current-step="currentStepIndex" />
</div>
<div class="w-20"></div>
<div class="w-16 lg:w-20"></div>
</header>
<!-- Content area -->
@ -147,16 +151,16 @@ function formatFiat(cents: number): string {
<div
v-else-if="nestedState === 'selectingAmount'"
key="selectingAmount"
class="flex flex-1 flex-col justify-center px-[8vw] py-6 gap-6"
class="flex flex-1 flex-col justify-center px-4 lg:px-[8vw] py-4 lg:py-6 gap-4 lg:gap-6"
>
<!-- Denomination grid -->
<div class="grid grid-cols-2 gap-6">
<div class="grid grid-cols-2 gap-3 lg:gap-6">
<div
v-for="denom in availableDenominations"
:key="denom"
class="flex items-center justify-between rounded-lg bg-card px-6 py-5"
class="flex items-center justify-between rounded-lg bg-card px-3 py-3 lg:px-6 lg:py-5"
>
<span class="text-[2.5rem] font-bold text-success"
<span class="text-xl lg:text-[2.5rem] font-bold text-success"
>{{ atmStore.fiatSymbol }}{{ denom }}</span
>
<div class="flex items-center gap-3">
@ -168,7 +172,7 @@ function formatFiat(cents: number): string {
>
-
</Button>
<span class="w-16 text-center text-[2rem] font-medium">{{
<span class="w-10 lg:w-16 text-center text-lg lg:text-[2rem] font-medium">{{
selectionCounts[denom] || 0
}}</span>
<Button
@ -184,18 +188,18 @@ function formatFiat(cents: number): string {
</div>
<!-- Amount summary -->
<div class="text-center py-4">
<p class="text-xl text-muted-foreground">You will receive</p>
<p class="text-[7vh] font-bold text-success leading-tight">
<div class="text-center py-2 lg:py-4">
<p class="text-sm lg:text-xl text-muted-foreground">You will receive</p>
<p class="text-4xl lg:text-[7vh] font-bold text-success leading-tight">
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatAmount) : '0.00' }}
</p>
<p class="mt-2 text-2xl text-muted-foreground">
<p class="mt-1 lg:mt-2 text-lg lg:text-2xl text-muted-foreground">
You pay:
<Badge variant="secondary" class="ml-1 text-2xl">
<Badge variant="secondary" class="ml-1 text-lg lg:text-2xl">
{{ context ? formatSats(context.satsAmount) : 0 }} sats
</Badge>
</p>
<p v-if="context" class="mt-1 text-lg text-muted-foreground">
<p v-if="context" class="mt-1 text-sm lg:text-lg text-muted-foreground">
Rate:
<Badge variant="secondary" class="text-lg">
{{ atmStore.fiatSymbol }}{{ formatFiatPrice(context.exchangeRate) }}/BTC
@ -233,7 +237,7 @@ function formatFiat(cents: number): string {
<!-- Instructions -->
<Alert class="border-bitcoin/50 bg-bitcoin/10">
<AlertDescription>
<p class="text-xl text-muted-foreground">
<p class="text-sm lg:text-xl text-muted-foreground">
Select the cash amount you want to receive. You'll pay the equivalent in Bitcoin.
</p>
</AlertDescription>
@ -254,25 +258,27 @@ function formatFiat(cents: number): string {
<div
v-else-if="nestedState === 'displayingInvoice'"
key="displayingInvoice"
class="flex flex-1"
class="flex flex-1 flex-col-reverse lg:flex-row"
>
<!-- Left panel (52%) — info -->
<div class="flex w-[52%] flex-col items-center justify-center gap-5 px-[4vw]">
<p class="text-[2rem] font-semibold text-warning">Scan to Pay</p>
<p class="text-[7vh] font-bold text-bitcoin leading-tight">
<!-- Left panel — info -->
<div
class="flex w-full lg:w-[52%] flex-col items-center justify-center gap-3 lg:gap-5 px-4 lg:px-[4vw] py-4 lg:py-0"
>
<p class="text-lg lg:text-[2rem] font-semibold text-warning">Scan to Pay</p>
<p class="text-3xl lg:text-[7vh] font-bold text-bitcoin leading-tight">
{{ context ? formatSats(context.satsAmount) : 0 }} sats
</p>
<p class="text-2xl text-muted-foreground">
<p class="text-lg lg:text-2xl text-muted-foreground">
to receive
<Badge class="bg-success text-success-foreground text-2xl">
<Badge class="bg-success text-success-foreground text-lg lg:text-2xl">
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatAmount) : '0.00' }}
</Badge>
</p>
<!-- Waiting indicator -->
<div class="flex items-center gap-3 pt-4">
<div class="flex items-center gap-3 pt-2 lg:pt-4">
<BounceDots />
<p class="text-xl text-muted-foreground">Waiting for payment...</p>
<p class="text-sm lg:text-xl text-muted-foreground">Waiting for payment...</p>
</div>
<!-- Invoice info with copy button (web-ui only) -->
@ -300,8 +306,10 @@ function formatFiat(cents: number): string {
</Alert>
</div>
<!-- Right panel (48%) — QR code -->
<div class="flex w-[48%] flex-col items-center justify-center bg-foreground/5 p-6">
<!-- Right panel — QR code -->
<div
class="flex w-full lg:w-[48%] flex-col items-center justify-center bg-foreground/5 p-4 lg:p-6"
>
<div v-if="context?.invoice">
<QRCode :value="context.invoice" :size="380" />
</div>
@ -330,9 +338,9 @@ function formatFiat(cents: number): string {
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
style="background: color-mix(in srgb, var(--success) 10%, var(--background))"
>
<div class="text-[8vh] text-success">✅</div>
<h3 class="text-[3rem] font-bold text-success">Cash Ready!</h3>
<p v-if="context" class="text-2xl">
<div class="text-5xl lg:text-[8vh] text-success">✅</div>
<h3 class="text-2xl lg:text-[3rem] font-bold text-success">Cash Ready!</h3>
<p v-if="context" class="text-lg lg:text-2xl">
Please take your
<Badge class="bg-success text-success-foreground text-2xl"
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge
@ -351,30 +359,30 @@ function formatFiat(cents: number): string {
<div
v-else-if="nestedState === 'complete'"
key="complete"
class="flex flex-1 items-center justify-center gap-10 p-8"
class="flex flex-1 flex-col lg:flex-row items-center justify-center gap-6 lg:gap-10 p-4 lg:p-8"
style="background: color-mix(in srgb, var(--success) 10%, var(--background))"
>
<!-- Left side — details -->
<div class="flex flex-col items-center gap-5">
<div class="text-[8vh]">🎉</div>
<h3 class="text-[3rem] font-bold text-success">Transaction Complete!</h3>
<div class="flex flex-col items-center gap-3 lg:gap-5">
<div class="text-5xl lg:text-[8vh]">🎉</div>
<h3 class="text-2xl lg:text-[3rem] font-bold text-success">Transaction Complete!</h3>
<!-- Transaction details -->
<div
v-if="context"
class="w-full max-w-md rounded-xl bg-background/60 px-8 py-6 space-y-3"
class="w-full max-w-md rounded-xl bg-background/60 px-4 py-4 lg:px-8 lg:py-6 space-y-2 lg:space-y-3"
>
<div class="flex justify-between text-2xl">
<div class="flex justify-between text-base lg:text-2xl">
<span class="text-muted-foreground">You paid</span>
<span class="font-semibold">{{ formatSats(context.satsAmount) }} sats</span>
</div>
<div class="flex justify-between text-2xl">
<div class="flex justify-between text-base lg:text-2xl">
<span class="text-muted-foreground">Rate</span>
<span
>{{ atmStore.fiatSymbol }}{{ formatFiatPrice(context.exchangeRate) }}/BTC</span
>
</div>
<div class="flex justify-between text-2xl">
<div class="flex justify-between text-base lg:text-2xl">
<span class="text-muted-foreground"
>Commission ({{ (context.feePercent * 100).toFixed(1) }}%)</span
>
@ -391,7 +399,7 @@ function formatFiat(cents: number): string {
>
</div>
<div class="border-t border-border my-2"></div>
<div class="flex justify-between text-[1.8rem] font-bold">
<div class="flex justify-between text-xl lg:text-[1.8rem] font-bold">
<span class="text-success">Cash received</span>
<span class="text-success"
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</span
@ -399,7 +407,7 @@ function formatFiat(cents: number): string {
</div>
</div>
<p class="text-xl text-muted-foreground">
<p class="text-sm lg:text-xl text-muted-foreground">
Take a photo of this screen for your receipt
</p>
@ -421,12 +429,14 @@ function formatFiat(cents: number): string {
<div
v-else-if="nestedState === 'error' || nestedState === 'dispenseError'"
key="error"
class="flex flex-1 flex-col items-center justify-center gap-6 p-8"
class="flex flex-1 flex-col items-center justify-center gap-4 lg:gap-6 p-4 lg:p-8"
style="background: color-mix(in srgb, var(--destructive) 8%, var(--background))"
>
<div class="text-[8vh]">⚠️</div>
<h3 class="text-[3rem] font-bold text-destructive">Error</h3>
<p class="text-2xl text-muted-foreground">{{ context?.error || 'An error occurred' }}</p>
<div class="text-5xl lg:text-[8vh]">⚠️</div>
<h3 class="text-2xl lg:text-[3rem] font-bold text-destructive">Error</h3>
<p class="text-base lg:text-2xl text-muted-foreground">
{{ context?.error || 'An error occurred' }}
</p>
<Button
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black"
size="kiosk"

View file

@ -59,23 +59,31 @@ function handleCashOut() {
</script>
<template>
<div class="flex min-h-0 flex-1 flex-col">
<!-- Brand Zone (top ~30%) -->
<div class="flex flex-col items-center justify-center gap-2 pt-[4vh] pb-[2vh]">
<div class="flex min-h-0 flex-1 flex-col justify-around lg:justify-start">
<!-- Brand Zone -->
<div
class="flex flex-col items-center justify-center gap-1.5 lg:gap-2 pt-6 lg:pt-[4vh] pb-2 lg:pb-[2vh]"
>
<img
src="/logo.png"
alt="Lamassu"
class="h-[12vh] w-[12vh] animate-float drop-shadow-[0_0_24px_var(--primary)]"
class="h-20 w-20 lg:h-[12vh] lg:w-[12vh] animate-float drop-shadow-[0_0_24px_var(--primary)]"
/>
<h1 class="text-[5vh] font-bold text-primary leading-tight">Bitcoinmat</h1>
<div class="flex flex-wrap items-center justify-center gap-3">
<Badge variant="outline" class="border-success text-success text-lg">No KYC</Badge>
<Badge variant="outline" class="border-primary text-primary text-lg">No Registration</Badge>
<Badge variant="outline" class="border-bitcoin text-bitcoin text-lg">Just Bitcoin</Badge>
<h1 class="text-3xl lg:text-[5vh] font-bold text-primary leading-tight">Bitcoinmat</h1>
<div class="flex flex-wrap items-center justify-center gap-2 lg:gap-3">
<Badge variant="outline" class="border-success text-success text-[11px] lg:text-lg"
>No KYC</Badge
>
<Badge variant="outline" class="border-primary text-primary text-[11px] lg:text-lg"
>No Registration</Badge
>
<Badge variant="outline" class="border-bitcoin text-bitcoin text-[11px] lg:text-lg"
>Just Bitcoin</Badge
>
</div>
<!-- Balance + Commission rates -->
<div
class="mt-1 flex flex-wrap items-center justify-center gap-4 text-[2vh] text-muted-foreground"
class="mt-1 flex flex-wrap items-center justify-center gap-2 lg:gap-4 text-[11px] lg:text-[2vh] text-muted-foreground"
>
<span v-if="atmStore.balanceSats !== null">
Available:
@ -106,36 +114,39 @@ function handleCashOut() {
</div>
</div>
<!-- Touch Zones (fills remaining space) -->
<div class="flex flex-1 items-center justify-center gap-[8vw] pb-[4vh]">
<!-- Touch Zones -->
<div
class="flex flex-col lg:flex-1 lg:flex-row items-center justify-center gap-5 lg:gap-[8vw] lg:pb-[4vh]"
>
<!-- Buy Bitcoin -->
<button
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"
class="flex aspect-square w-40 lg:w-[36vh] flex-col items-center justify-center gap-1.5 lg: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"
>
<BitcoinIcon :size="100" class="shrink-0" />
<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="100" class="shrink-0 w-10 h-10 lg:w-[100px] lg:h-[100px]" />
<span class="text-base lg:text-[3.5vh] font-bold text-bitcoin">Buy Bitcoin</span>
<span class="text-[10px] lg:text-[1.8vh] text-foreground/70"
>Insert cash, receive sats</span
>
</button>
<!-- Sell Bitcoin -->
<button
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"
class="flex aspect-square w-40 lg:w-[36vh] flex-col items-center justify-center gap-1.5 lg: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"
>
<span class="text-[8vh] leading-none">💵</span>
<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-4xl lg:text-[8vh] leading-none">💵</span>
<span class="text-base lg:text-[3.5vh] font-bold text-success">Sell Bitcoin</span>
<span class="text-[10px] lg:text-[1.8vh] text-foreground/70"
>Pay invoice, receive cash</span
>
</button>
</div>
<!-- Dev QR buttons -->
<div
v-if="atmStore.debugMode"
class="absolute bottom-16 left-1/2 -translate-x-1/2 flex flex-wrap items-center justify-center gap-4"
>
<div v-if="atmStore.debugMode" class="flex flex-wrap items-center justify-center gap-2 pb-2">
<Button
v-if="lndconnectUrl"
variant="ghost"
@ -207,7 +218,7 @@ function handleCashOut() {
v-if="!atmStore.debugMode && atmStore.allowMockFallback"
variant="ghost"
size="kiosk"
class="absolute bottom-6 right-6 text-lg text-muted-foreground"
class="absolute bottom-2 right-2 lg:bottom-6 lg:right-6 h-8 min-h-0 px-2 text-xs lg:h-[7vh] lg:min-h-[70px] lg:px-8 lg:text-2xl text-muted-foreground"
@click="atmStore.toggleDebug"
>
Show Debug