feat(machine): add light/dark mode toggle with semantic color styling

Replace hardcoded Tailwind colors (bg-white/5, text-green-400, bg-black/30,
etc.) with theme-aware semantic classes (bg-card, text-success, text-bitcoin,
bg-destructive/20) across CashInView and CashOutView. Add colorMode support
to useTheme composable with localStorage persistence and system preference
detection. QRCode component now reacts to theme/mode changes via
MutationObserver. Debug panel includes Light/Dark/System toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-27 10:57:33 -05:00
commit f5f00108aa
5 changed files with 151 additions and 83 deletions

View file

@ -6,7 +6,7 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
const atmStore = useAtmStore() const atmStore = useAtmStore()
const { current: currentTheme, themes } = useTheme() const { current: currentTheme, themes, colorMode } = useTheme()
const debugExpanded = ref(false) const debugExpanded = ref(false)
const network = import.meta.env.VITE_NETWORK || 'regtest' const network = import.meta.env.VITE_NETWORK || 'regtest'
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
@ -122,6 +122,32 @@ function toggleLiveServices() {
{{ theme.label }} {{ theme.label }}
</Button> </Button>
</div> </div>
<div class="flex items-center gap-1">
<Button
:variant="colorMode === 'light' ? 'default' : 'outline'"
size="sm"
class="h-6 text-xs"
@click="colorMode = 'light'"
>
Light
</Button>
<Button
:variant="colorMode === 'dark' ? 'default' : 'outline'"
size="sm"
class="h-6 text-xs"
@click="colorMode = 'dark'"
>
Dark
</Button>
<Button
:variant="colorMode === 'system' ? 'default' : 'outline'"
size="sm"
class="h-6 text-xs"
@click="colorMode = 'system'"
>
System
</Button>
</div>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted } from 'vue' import { computed, ref, onMounted, onUnmounted } from 'vue'
import QrcodeVue from 'qrcode.vue' import QrcodeVue from 'qrcode.vue'
const props = defineProps<{ const props = defineProps<{
@ -13,10 +13,26 @@ const qrSize = computed(() => props.size ?? 200)
const qrBackground = ref('#f5f0e6') const qrBackground = ref('#f5f0e6')
const qrForeground = ref('#3c3836') const qrForeground = ref('#3c3836')
onMounted(() => { function readQrColors() {
const styles = getComputedStyle(document.documentElement) const styles = getComputedStyle(document.documentElement)
qrBackground.value = styles.getPropertyValue('--qr').trim() || '#f5f0e6' qrBackground.value = styles.getPropertyValue('--qr').trim() || '#f5f0e6'
qrForeground.value = styles.getPropertyValue('--qr-foreground').trim() || '#3c3836' qrForeground.value = styles.getPropertyValue('--qr-foreground').trim() || '#3c3836'
}
// Re-read colors when theme or color mode changes (data-theme or class attributes)
let observer: MutationObserver | null = null
onMounted(() => {
readQrColors()
observer = new MutationObserver(readQrColors)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme', 'class'],
})
})
onUnmounted(() => {
observer?.disconnect()
}) })
</script> </script>

View file

@ -1,6 +1,7 @@
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
export type ThemeId = 'gruvbox' | 'catppuccin' | 'cyberpunk' | 'dracula' | 'nord' | 'tokyo-night' export type ThemeId = 'gruvbox' | 'catppuccin' | 'cyberpunk' | 'dracula' | 'nord' | 'tokyo-night'
export type ColorMode = 'light' | 'dark' | 'system'
export interface ThemeOption { export interface ThemeOption {
id: ThemeId id: ThemeId
@ -16,13 +17,15 @@ export const themes: ThemeOption[] = [
{ id: 'cyberpunk', label: 'Cyberpunk' }, { id: 'cyberpunk', label: 'Cyberpunk' },
] ]
const STORAGE_KEY = 'lamassu-theme' const THEME_KEY = 'lamassu-theme'
const MODE_KEY = 'lamassu-color-mode'
const current = ref<ThemeId>(loadTheme()) const current = ref<ThemeId>(loadTheme())
const colorMode = ref<ColorMode>(loadColorMode())
function loadTheme(): ThemeId { function loadTheme(): ThemeId {
try { try {
const stored = localStorage.getItem(STORAGE_KEY) const stored = localStorage.getItem(THEME_KEY)
if (stored && themes.some((t) => t.id === stored)) { if (stored && themes.some((t) => t.id === stored)) {
return stored as ThemeId return stored as ThemeId
} }
@ -32,6 +35,18 @@ function loadTheme(): ThemeId {
return 'gruvbox' return 'gruvbox'
} }
function loadColorMode(): ColorMode {
try {
const stored = localStorage.getItem(MODE_KEY)
if (stored === 'light' || stored === 'dark' || stored === 'system') {
return stored
}
} catch {
// localStorage unavailable
}
return 'dark'
}
function applyTheme(id: ThemeId) { function applyTheme(id: ThemeId) {
const el = document.documentElement const el = document.documentElement
if (id === 'gruvbox') { if (id === 'gruvbox') {
@ -41,14 +56,42 @@ function applyTheme(id: ThemeId) {
} }
} }
function applyColorMode(mode: ColorMode) {
const el = document.documentElement
if (mode === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
el.classList.toggle('dark', prefersDark)
} else {
el.classList.toggle('dark', mode === 'dark')
}
}
// Listen for system preference changes when mode is 'system'
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', () => {
if (colorMode.value === 'system') {
applyColorMode('system')
}
})
// Apply on load // Apply on load
applyTheme(current.value) applyTheme(current.value)
applyColorMode(colorMode.value)
// Persist and apply on change // Persist and apply on change
watch(current, (id) => { watch(current, (id) => {
applyTheme(id) applyTheme(id)
try { try {
localStorage.setItem(STORAGE_KEY, id) localStorage.setItem(THEME_KEY, id)
} catch {
// localStorage unavailable
}
})
watch(colorMode, (mode) => {
applyColorMode(mode)
try {
localStorage.setItem(MODE_KEY, mode)
} catch { } catch {
// localStorage unavailable // localStorage unavailable
} }
@ -58,5 +101,6 @@ export function useTheme() {
return { return {
current, current,
themes, themes,
colorMode,
} }
} }

View file

@ -207,7 +207,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<template> <template>
<div class="flex min-h-0 flex-1 flex-col"> <div class="flex min-h-0 flex-1 flex-col">
<!-- Header --> <!-- Header -->
<header class="flex items-center bg-black/30 px-4 pb-3 pt-12 sm:px-8 sm:py-4"> <header class="flex items-center bg-foreground/10 px-4 pb-3 pt-12 sm:px-8 sm:py-4">
<Button v-if="showCancelButton" variant="ghost" class="text-muted-foreground" @click="cancel"> <Button v-if="showCancelButton" variant="ghost" class="text-muted-foreground" @click="cancel">
← Cancel ← Cancel
</Button> </Button>
@ -220,7 +220,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<ScrollArea class="min-h-0 flex-1"> <ScrollArea class="min-h-0 flex-1">
<main class="flex min-h-0 flex-1 items-start justify-center p-4 sm:items-center sm:p-8"> <main class="flex min-h-0 flex-1 items-start justify-center p-4 sm:items-center sm:p-8">
<!-- Fetching Rate --> <!-- Fetching Rate -->
<Card v-if="nestedState === 'fetchingRate'" class="w-full max-w-80 border-0 bg-white/5"> <Card v-if="nestedState === 'fetchingRate'" class="w-full max-w-80 border-0 bg-card">
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<Skeleton class="h-10 w-10 rounded-full" /> <Skeleton class="h-10 w-10 rounded-full" />
<p class="text-muted-foreground">Fetching exchange rate...</p> <p class="text-muted-foreground">Fetching exchange rate...</p>
@ -228,10 +228,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</Card> </Card>
<!-- Inserting Bills --> <!-- Inserting Bills -->
<Card <Card v-else-if="nestedState === 'insertingBills'" class="w-full max-w-96 border-0 bg-card">
v-else-if="nestedState === 'insertingBills'"
class="w-full max-w-96 border-0 bg-white/5"
>
<CardHeader class="text-center"> <CardHeader class="text-center">
<CardTitle>Insert Cash</CardTitle> <CardTitle>Insert Cash</CardTitle>
<CardDescription v-if="context"> <CardDescription v-if="context">
@ -240,16 +237,16 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</CardHeader> </CardHeader>
<CardContent class="space-y-6"> <CardContent class="space-y-6">
<!-- Bill Status --> <!-- Bill Status -->
<div v-if="context" class="rounded-lg bg-white/5 p-4"> <div v-if="context" class="rounded-lg bg-card p-4">
<div class="mb-2 flex justify-between"> <div class="mb-2 flex justify-between">
<span class="text-muted-foreground">Inserted:</span> <span class="text-muted-foreground">Inserted:</span>
<span class="text-2xl font-bold text-green-400" <span class="text-2xl font-bold text-success"
>{{ atmStore.fiatSymbol }}{{ context.fiatAmount / 100 }}</span >{{ atmStore.fiatSymbol }}{{ context.fiatAmount / 100 }}</span
> >
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-muted-foreground">You'll receive:</span> <span class="text-muted-foreground">You'll receive:</span>
<span class="font-semibold text-orange-400" <span class="font-semibold text-bitcoin"
>{{ formatSats(context.satsAmount) }} sats</span >{{ formatSats(context.satsAmount) }} sats</span
> >
</div> </div>
@ -275,9 +272,9 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</p> </p>
<!-- Dev: Simulate bill insertion --> <!-- Dev: Simulate bill insertion -->
<Alert v-if="atmStore.debugMode" class="border-red-500/50 bg-red-500/10"> <Alert v-if="atmStore.debugMode" class="border-destructive/50 bg-destructive/10">
<AlertDescription> <AlertDescription>
<p class="mb-2 text-xs text-red-400">Dev: Simulate bills</p> <p class="mb-2 text-xs text-destructive">Dev: Simulate bills</p>
<div class="flex flex-wrap justify-center gap-2"> <div class="flex flex-wrap justify-center gap-2">
<Button <Button
v-for="denom in [1, 5, 10, 20, 50, 100]" v-for="denom in [1, 5, 10, 20, 50, 100]"
@ -308,7 +305,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<!-- Generating NDebit --> <!-- Generating NDebit -->
<Card <Card
v-else-if="nestedState === 'generatingNdebit'" v-else-if="nestedState === 'generatingNdebit'"
class="w-full max-w-80 border-0 bg-white/5" class="w-full max-w-80 border-0 bg-card"
> >
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<span class="animate-hourglass text-5xl">⏳</span> <span class="animate-hourglass text-5xl">⏳</span>
@ -319,13 +316,13 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<!-- Display QR (unified lightning + clink) --> <!-- Display QR (unified lightning + clink) -->
<Card <Card
v-else-if="nestedState === 'displayingQR'" v-else-if="nestedState === 'displayingQR'"
class="w-full max-w-[420px] border-0 bg-white/5" class="w-full max-w-[420px] border-0 bg-card"
> >
<CardContent class="pt-6 space-y-5"> <CardContent class="pt-6 space-y-5">
<!-- Amount Display --> <!-- Amount Display -->
<div class="text-center"> <div class="text-center">
<p class="text-sm text-muted-foreground">Withdraw</p> <p class="text-sm text-muted-foreground">Withdraw</p>
<p class="text-3xl font-bold text-orange-400"> <p class="text-3xl font-bold text-bitcoin">
{{ formatSats(context?.satsAmount || 0) }} sats {{ formatSats(context?.satsAmount || 0) }} sats
</p> </p>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
@ -371,12 +368,12 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
v-if="isGeneratingLnurl && !context?.ndebitUri" v-if="isGeneratingLnurl && !context?.ndebitUri"
class="h-[240px] w-[240px] rounded-xl" class="h-[240px] w-[240px] rounded-xl"
/> />
<div v-else-if="currentQrValue" class="rounded-xl overflow-hidden bg-white p-3"> <div v-else-if="currentQrValue" class="rounded-xl overflow-hidden">
<QRCode :value="currentQrValue" :size="216" /> <QRCode :value="currentQrValue" :size="216" />
</div> </div>
<div <div
v-else v-else
class="flex h-[240px] w-[240px] items-center justify-center rounded-xl bg-white/10" class="flex h-[240px] w-[240px] items-center justify-center rounded-xl bg-muted"
> >
<p class="text-sm text-muted-foreground">Generating...</p> <p class="text-sm text-muted-foreground">Generating...</p>
</div> </div>
@ -423,14 +420,14 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</div> </div>
<!-- Alternative: Manual Options (web-ui only, no keyboard on kiosk) --> <!-- Alternative: Manual Options (web-ui only, no keyboard on kiosk) -->
<details v-if="!isElectron" class="rounded-lg bg-white/5 p-3"> <details v-if="!isElectron" class="rounded-lg bg-card p-3">
<summary class="cursor-pointer text-sm text-muted-foreground"> <summary class="cursor-pointer text-sm text-muted-foreground">
Alternative: Manual payment options Alternative: Manual payment options
</summary> </summary>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
<!-- CLINK Debit by pubkey --> <!-- CLINK Debit by pubkey -->
<div class="space-y-2"> <div class="space-y-2">
<p class="text-xs font-medium text-blue-400">Enter your npub/pubkey:</p> <p class="text-xs font-medium text-accent">Enter your npub/pubkey:</p>
<div class="flex gap-2"> <div class="flex gap-2">
<Input <Input
v-model="pubkeyInput" v-model="pubkeyInput"
@ -451,7 +448,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<!-- Invoice paste --> <!-- Invoice paste -->
<div class="space-y-2"> <div class="space-y-2">
<p class="text-xs font-medium text-orange-400">Or paste your invoice:</p> <p class="text-xs font-medium text-bitcoin">Or paste your invoice:</p>
<div class="flex gap-2"> <div class="flex gap-2">
<Input <Input
v-model="invoiceInput" v-model="invoiceInput"
@ -470,7 +467,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</div> </div>
</div> </div>
<p v-if="atmStore.paymentError" class="text-xs text-red-400"> <p v-if="atmStore.paymentError" class="text-xs text-destructive">
Error: {{ atmStore.paymentError }} Error: {{ atmStore.paymentError }}
</p> </p>
</div> </div>
@ -491,12 +488,9 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</Card> </Card>
<!-- Ask for Receipt --> <!-- Ask for Receipt -->
<Card <Card v-else-if="nestedState === 'askForReceipt'" class="w-full max-w-80 border-0 bg-card">
v-else-if="nestedState === 'askForReceipt'"
class="w-full max-w-80 border-0 bg-white/5"
>
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<div class="text-6xl text-green-400">✅</div> <div class="text-6xl text-success">✅</div>
<CardTitle>Payment Sent!</CardTitle> <CardTitle>Payment Sent!</CardTitle>
<p class="text-muted-foreground">Would you like a receipt?</p> <p class="text-muted-foreground">Would you like a receipt?</p>
<div class="flex h-32 w-32 items-center justify-center rounded-lg bg-white"> <div class="flex h-32 w-32 items-center justify-center rounded-lg bg-white">
@ -507,10 +501,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</Card> </Card>
<!-- Sending Receipt --> <!-- Sending Receipt -->
<Card <Card v-else-if="nestedState === 'sendingReceipt'" class="w-full max-w-80 border-0 bg-card">
v-else-if="nestedState === 'sendingReceipt'"
class="w-full max-w-80 border-0 bg-white/5"
>
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<span class="animate-hourglass text-5xl">⏳</span> <span class="animate-hourglass text-5xl">⏳</span>
<p class="text-muted-foreground">Sending receipt...</p> <p class="text-muted-foreground">Sending receipt...</p>
@ -518,13 +509,10 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</Card> </Card>
<!-- Complete --> <!-- Complete -->
<Card <Card v-else-if="nestedState === 'complete'" class="w-full max-w-80 border-0 bg-success/20">
v-else-if="nestedState === 'complete'"
class="w-full max-w-80 border-0 bg-green-900/30"
>
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">🎉</div> <div class="text-6xl">🎉</div>
<CardTitle class="text-green-400">Transaction Complete!</CardTitle> <CardTitle class="text-success">Transaction Complete!</CardTitle>
<p v-if="context"> <p v-if="context">
You received You received
<Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge> <Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge>
@ -536,37 +524,40 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
<!-- Confirm Abandon Warning --> <!-- Confirm Abandon Warning -->
<Card <Card
v-else-if="nestedState === 'confirmAbandon'" v-else-if="nestedState === 'confirmAbandon'"
class="w-full max-w-80 border-0 bg-red-900/30" class="w-full max-w-80 border-0 bg-destructive/20"
> >
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">⚠️</div> <div class="text-6xl">⚠️</div>
<CardTitle class="text-red-400">Cash Already Accepted</CardTitle> <CardTitle class="text-destructive">Cash Already Accepted</CardTitle>
<p class="text-muted-foreground"> <p class="text-muted-foreground">
Your {{ atmStore.fiatSymbol Your {{ atmStore.fiatSymbol
}}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted }}{{ context ? (context.fiatAmount / 100).toLocaleString() : '' }} has been accepted
and cannot be returned. Please scan the QR code to receive your Bitcoin. and cannot be returned. Please scan the QR code to receive your Bitcoin.
</p> </p>
<Button <Button
class="w-full bg-gradient-to-r from-green-500 to-emerald-400 text-black" class="w-full bg-success text-success-foreground"
@click="atmStore.send({ type: 'RETRY' })" @click="atmStore.send({ type: 'RETRY' })"
> >
Show QR Code Again Show QR Code Again
</Button> </Button>
<Button variant="ghost" class="w-full text-red-400" @click="cancel"> <Button variant="ghost" class="w-full text-destructive" @click="cancel">
Abandon (lose cash) Abandon (lose cash)
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<!-- Error --> <!-- Error -->
<Card v-else-if="nestedState === 'error'" class="w-full max-w-80 border-0 bg-red-900/30"> <Card
v-else-if="nestedState === 'error'"
class="w-full max-w-80 border-0 bg-destructive/20"
>
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">⚠️</div> <div class="text-6xl">⚠️</div>
<CardTitle class="text-red-400">Error</CardTitle> <CardTitle class="text-destructive">Error</CardTitle>
<p class="text-muted-foreground">{{ context?.error || 'An error occurred' }}</p> <p class="text-muted-foreground">{{ context?.error || 'An error occurred' }}</p>
<Button <Button
v-if="context && context.billsInserted.length > 0" v-if="context && context.billsInserted.length > 0"
class="w-full bg-gradient-to-r from-green-500 to-emerald-400 text-black" class="w-full bg-success text-success-foreground"
@click="atmStore.send({ type: 'RETRY' })" @click="atmStore.send({ type: 'RETRY' })"
> >
Try Again Try Again
@ -581,7 +572,7 @@ const isProcessing = computed(() => atmStore.isPayingInvoice || atmStore.isReque
</Card> </Card>
<!-- Fallback --> <!-- Fallback -->
<Card v-else class="w-full max-w-80 border-0 bg-white/5"> <Card v-else class="w-full max-w-80 border-0 bg-card">
<CardContent class="p-8 text-center"> <CardContent class="p-8 text-center">
<p class="text-muted-foreground">State: {{ nestedState }}</p> <p class="text-muted-foreground">State: {{ nestedState }}</p>
</CardContent> </CardContent>

View file

@ -100,7 +100,7 @@ function formatFiat(cents: number): string {
<template> <template>
<div class="flex min-h-0 flex-1 flex-col"> <div class="flex min-h-0 flex-1 flex-col">
<!-- Header --> <!-- Header -->
<header class="flex items-center bg-black/30 px-4 pb-3 pt-12 sm:px-8 sm:py-4"> <header class="flex items-center bg-foreground/10 px-4 pb-3 pt-12 sm:px-8 sm:py-4">
<Button variant="ghost" class="text-muted-foreground" @click="cancel">← Cancel</Button> <Button variant="ghost" class="text-muted-foreground" @click="cancel">← Cancel</Button>
<h2 class="flex-1 text-center text-lg font-semibold sm:text-xl">Sell Bitcoin</h2> <h2 class="flex-1 text-center text-lg font-semibold sm:text-xl">Sell Bitcoin</h2>
<div class="w-20"></div> <div class="w-20"></div>
@ -110,7 +110,7 @@ function formatFiat(cents: number): string {
<ScrollArea class="min-h-0 flex-1"> <ScrollArea class="min-h-0 flex-1">
<main class="flex min-h-0 flex-1 items-start justify-center p-4 sm:items-center sm:p-8"> <main class="flex min-h-0 flex-1 items-start justify-center p-4 sm:items-center sm:p-8">
<!-- Fetching Rate --> <!-- Fetching Rate -->
<Card v-if="nestedState === 'fetchingRate'" class="w-full max-w-80 border-0 bg-white/5"> <Card v-if="nestedState === 'fetchingRate'" class="w-full max-w-80 border-0 bg-card">
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<Skeleton class="h-10 w-10 rounded-full" /> <Skeleton class="h-10 w-10 rounded-full" />
<p class="text-muted-foreground">Fetching exchange rate...</p> <p class="text-muted-foreground">Fetching exchange rate...</p>
@ -120,7 +120,7 @@ function formatFiat(cents: number): string {
<!-- Selecting Amount - Main cash-out screen --> <!-- Selecting Amount - Main cash-out screen -->
<Card <Card
v-else-if="nestedState === 'selectingAmount'" v-else-if="nestedState === 'selectingAmount'"
class="w-full max-w-[520px] border-0 bg-white/5" class="w-full max-w-[520px] border-0 bg-card"
> >
<CardHeader class="text-center"> <CardHeader class="text-center">
<CardTitle>Select Cash Amount</CardTitle> <CardTitle>Select Cash Amount</CardTitle>
@ -137,9 +137,9 @@ function formatFiat(cents: number): string {
<div <div
v-for="denom in availableDenominations" v-for="denom in availableDenominations"
:key="denom" :key="denom"
class="flex items-center justify-between rounded-lg bg-white/5 px-4 py-3 sm:p-4" class="flex items-center justify-between rounded-lg bg-card px-4 py-3 sm:p-4"
> >
<span class="text-xl font-bold text-green-400 sm:text-2xl" <span class="text-xl font-bold text-success sm:text-2xl"
>{{ atmStore.fiatSymbol }}{{ denom }}</span >{{ atmStore.fiatSymbol }}{{ denom }}</span
> >
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@ -169,9 +169,9 @@ function formatFiat(cents: number): string {
</div> </div>
<!-- Selected amount summary --> <!-- Selected amount summary -->
<div class="rounded-lg bg-white/10 p-6 text-center"> <div class="rounded-lg bg-muted/50 p-6 text-center">
<p class="text-sm text-muted-foreground">You will receive</p> <p class="text-sm text-muted-foreground">You will receive</p>
<p class="text-4xl font-bold text-green-400"> <p class="text-4xl font-bold text-success">
{{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatAmount) : '0.00' }} {{ atmStore.fiatSymbol }}{{ context ? formatFiat(context.fiatAmount) : '0.00' }}
</p> </p>
<p class="mt-2 text-sm text-muted-foreground"> <p class="mt-2 text-sm text-muted-foreground">
@ -202,7 +202,7 @@ function formatFiat(cents: number): string {
</div> </div>
<!-- Instructions --> <!-- Instructions -->
<Alert class="border-orange-500/50 bg-orange-500/10"> <Alert class="border-bitcoin/50 bg-bitcoin/10">
<AlertDescription> <AlertDescription>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Select the cash amount you want to receive. You'll pay the equivalent in Bitcoin. Select the cash amount you want to receive. You'll pay the equivalent in Bitcoin.
@ -215,7 +215,7 @@ function formatFiat(cents: number): string {
<!-- Generating Invoice --> <!-- Generating Invoice -->
<Card <Card
v-else-if="nestedState === 'generatingInvoice'" v-else-if="nestedState === 'generatingInvoice'"
class="w-full max-w-80 border-0 bg-white/5" class="w-full max-w-80 border-0 bg-card"
> >
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<Skeleton class="h-10 w-10 rounded-full" /> <Skeleton class="h-10 w-10 rounded-full" />
@ -226,14 +226,14 @@ function formatFiat(cents: number): string {
<!-- Displaying Invoice - Show QR for payment --> <!-- Displaying Invoice - Show QR for payment -->
<Card <Card
v-else-if="nestedState === 'displayingInvoice'" v-else-if="nestedState === 'displayingInvoice'"
class="w-full max-w-[480px] border-0 bg-white/5" class="w-full max-w-[480px] border-0 bg-card"
> >
<CardHeader class="text-center"> <CardHeader class="text-center">
<CardTitle>Scan to Pay</CardTitle> <CardTitle>Scan to Pay</CardTitle>
<CardDescription v-if="context"> <CardDescription v-if="context">
Pay <Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge> to Pay <Badge variant="secondary">{{ formatSats(context.satsAmount) }} sats</Badge> to
receive receive
<Badge class="bg-green-600" <Badge class="bg-success"
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge >{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge
> >
</CardDescription> </CardDescription>
@ -249,14 +249,14 @@ function formatFiat(cents: number): string {
<!-- Payment status --> <!-- Payment status -->
<div class="flex flex-col items-center gap-4 py-4"> <div class="flex flex-col items-center gap-4 py-4">
<div class="h-12 w-12 animate-pulse rounded-full bg-orange-500/20"> <div class="h-12 w-12 animate-pulse rounded-full bg-bitcoin/20">
<div class="flex h-full items-center justify-center text-2xl">⏳</div> <div class="flex h-full items-center justify-center text-2xl">⏳</div>
</div> </div>
<p class="text-center text-muted-foreground">Waiting for payment...</p> <p class="text-center text-muted-foreground">Waiting for payment...</p>
</div> </div>
<!-- Invoice info with copy button (web-ui only) --> <!-- Invoice info with copy button (web-ui only) -->
<div v-if="context?.invoice && !isElectron" class="rounded-lg bg-white/5 p-3"> <div v-if="context?.invoice && !isElectron" class="rounded-lg bg-card p-3">
<p class="mb-2 truncate text-center font-mono text-xs text-muted-foreground"> <p class="mb-2 truncate text-center font-mono text-xs text-muted-foreground">
{{ context.invoice.slice(0, 50) }}... {{ context.invoice.slice(0, 50) }}...
</p> </p>
@ -268,9 +268,9 @@ function formatFiat(cents: number): string {
</div> </div>
<!-- Dev controls --> <!-- Dev controls -->
<Alert v-if="atmStore.debugMode" class="border-red-500/50 bg-red-500/10"> <Alert v-if="atmStore.debugMode" class="border-destructive/50 bg-destructive/10">
<AlertDescription class="text-center"> <AlertDescription class="text-center">
<p class="mb-2 text-xs text-red-400">Dev: Use alice-pay or button below</p> <p class="mb-2 text-xs text-destructive">Dev: Use alice-pay or button below</p>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -284,10 +284,7 @@ function formatFiat(cents: number): string {
</Card> </Card>
<!-- Dispensing Cash --> <!-- Dispensing Cash -->
<Card <Card v-else-if="nestedState === 'dispensingCash'" class="w-full max-w-80 border-0 bg-card">
v-else-if="nestedState === 'dispensingCash'"
class="w-full max-w-80 border-0 bg-white/5"
>
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<Skeleton class="h-10 w-10 rounded-full" /> <Skeleton class="h-10 w-10 rounded-full" />
<p class="text-muted-foreground">Dispensing cash...</p> <p class="text-muted-foreground">Dispensing cash...</p>
@ -298,14 +295,14 @@ function formatFiat(cents: number): string {
<!-- Cash Ready --> <!-- Cash Ready -->
<Card <Card
v-else-if="nestedState === 'waitingForCashTaken'" v-else-if="nestedState === 'waitingForCashTaken'"
class="w-full max-w-80 border-0 bg-green-900/30" class="w-full max-w-80 border-0 bg-success/20"
> >
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl text-green-400">✅</div> <div class="text-6xl text-success">✅</div>
<CardTitle class="text-green-400">Cash Ready!</CardTitle> <CardTitle class="text-success">Cash Ready!</CardTitle>
<p v-if="context"> <p v-if="context">
Please take your Please take your
<Badge class="bg-green-600 text-lg" <Badge class="bg-success text-lg"
>{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge >{{ atmStore.fiatSymbol }}{{ formatFiat(context.fiatAmount) }}</Badge
> >
</p> </p>
@ -320,10 +317,7 @@ function formatFiat(cents: number): string {
</Card> </Card>
<!-- Ask for Receipt --> <!-- Ask for Receipt -->
<Card <Card v-else-if="nestedState === 'askForReceipt'" class="w-full max-w-80 border-0 bg-card">
v-else-if="nestedState === 'askForReceipt'"
class="w-full max-w-80 border-0 bg-white/5"
>
<CardContent class="flex flex-col items-center gap-4 p-8"> <CardContent class="flex flex-col items-center gap-4 p-8">
<CardTitle>Would you like a receipt?</CardTitle> <CardTitle>Would you like a receipt?</CardTitle>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
@ -337,13 +331,10 @@ function formatFiat(cents: number): string {
</Card> </Card>
<!-- Complete --> <!-- Complete -->
<Card <Card v-else-if="nestedState === 'complete'" class="w-full max-w-80 border-0 bg-success/20">
v-else-if="nestedState === 'complete'"
class="w-full max-w-80 border-0 bg-green-900/30"
>
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">🎉</div> <div class="text-6xl">🎉</div>
<CardTitle class="text-green-400">Transaction Complete!</CardTitle> <CardTitle class="text-success">Transaction Complete!</CardTitle>
<p class="text-muted-foreground">Thank you!</p> <p class="text-muted-foreground">Thank you!</p>
</CardContent> </CardContent>
</Card> </Card>
@ -351,11 +342,11 @@ function formatFiat(cents: number): string {
<!-- Error --> <!-- Error -->
<Card <Card
v-else-if="nestedState === 'error' || nestedState === 'dispenseError'" v-else-if="nestedState === 'error' || nestedState === 'dispenseError'"
class="w-full max-w-80 border-0 bg-red-900/30" class="w-full max-w-80 border-0 bg-destructive/20"
> >
<CardContent class="flex flex-col items-center gap-4 p-8 text-center"> <CardContent class="flex flex-col items-center gap-4 p-8 text-center">
<div class="text-6xl">⚠️</div> <div class="text-6xl">⚠️</div>
<CardTitle class="text-red-400">Error</CardTitle> <CardTitle class="text-destructive">Error</CardTitle>
<p class="text-muted-foreground">{{ context?.error || 'An error occurred' }}</p> <p class="text-muted-foreground">{{ context?.error || 'An error occurred' }}</p>
<Button <Button
class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black" class="bg-gradient-to-r from-orange-500 to-yellow-400 text-black"
@ -367,7 +358,7 @@ function formatFiat(cents: number): string {
</Card> </Card>
<!-- Fallback --> <!-- Fallback -->
<Card v-else class="w-full max-w-80 border-0 bg-white/5"> <Card v-else class="w-full max-w-80 border-0 bg-card">
<CardContent class="p-8 text-center"> <CardContent class="p-8 text-center">
<p class="text-muted-foreground">State: {{ nestedState }}</p> <p class="text-muted-foreground">State: {{ nestedState }}</p>
</CardContent> </CardContent>