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:
parent
0406a21b0e
commit
f5f00108aa
5 changed files with 151 additions and 83 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue