responsive ui + wallet redesign + 401 fix + round cards

This commit is contained in:
2026-08-01 19:55:11 +08:00
parent 5ac8c93027
commit 23ef77ece6
40 changed files with 3184 additions and 684 deletions
+1 -1
View File
@@ -59,7 +59,7 @@
}
</script>
<div class="mx-auto max-w-xl">
<div class="mx-auto max-w-lg">
<div class="flex flex-col gap-4">
{#if !w.ready}
<p class="text-center text-muted-foreground text-sm mt-8">
+1 -1
View File
@@ -59,7 +59,7 @@
}
</script>
<div class="mx-auto max-w-xl">
<div class="mx-auto max-w-lg">
<Card>
<CardHeader>
<CardTitle>Send XRP</CardTitle>
+1 -1
View File
@@ -35,7 +35,7 @@
})
</script>
<div class="mx-auto max-w-xl">
<div class="mx-auto max-w-lg">
<Card>
<CardHeader>
<CardTitle>Transaction Details</CardTitle>
+112 -175
View File
@@ -5,15 +5,14 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '$lib/components/ui/card/index.js'
import ArrowDown from '@lucide/svelte/icons/arrow-down'
import ArrowUp from '@lucide/svelte/icons/arrow-up'
import SettingsIcon from '@lucide/svelte/icons/settings'
import QrCodeIcon from '@lucide/svelte/icons/qr-code'
import ExternalLink from '@lucide/svelte/icons/external-link'
import Send from '@lucide/svelte/icons/send'
import Loader from '$lib/components/loader.svelte'
import QrCode from '$lib/components/qr.svelte'
import * as Dialog from '$lib/components/ui/dialog/index.js'
@@ -26,14 +25,11 @@
const walletId = $derived(route.params.id)
let qrDialogOpen = $state(false)
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined =
$state()
let autoRefreshTimer: ReturnType<typeof setInterval> | undefined = $state()
$effect(() => {
if (w.seed && !autoRefreshTimer) {
autoRefreshTimer = setInterval(() => {
w.refresh()
}, 30000)
autoRefreshTimer = setInterval(() => w.refresh(), 30000)
} else if (!w.seed && autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = undefined
@@ -55,181 +51,122 @@
})
</script>
<div class="mx-auto max-w-xl">
<div class="flex flex-col gap-4">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>
{w.currentWallet?.name ?? 'XRP Wallet'}
</CardTitle>
<img src="/icons/xrp.png" alt="" class="size-6 rounded-full" />
</div>
<CardDescription>XRP Ledger</CardDescription>
</CardHeader>
<CardContent>
<div class="mb-4 space-y-1">
<div class="flex items-start gap-3">
<div class="min-w-0 flex-1 space-y-1">
<p class="font-medium text-xs">Address</p>
<p class="font-mono text-xs break-all">{w.address}</p>
<div class="flex gap-1.5 mt-1">
<Dialog.Root bind:open={qrDialogOpen}>
<Dialog.Trigger>
<Button
variant="outline"
size="icon-sm"
class="text-muted-foreground"
aria-label="Show QR code">
<QrCodeIcon size={16} />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<div
class="flex flex-col items-center gap-3 py-4">
<QrCode text={w.address} size={256} />
<p
class="font-mono text-xs break-all text-center max-w-64">
{w.address}
</p>
</div>
</Dialog.Content>
</Dialog.Root>
<CopyButton
text={w.address}
variant="outline"
size="icon-sm"
class="text-muted-foreground">
</CopyButton>
<Button
variant="outline"
size="icon-sm"
class="text-muted-foreground"
aria-label="View on explorer"
onclick={() =>
electrobun.rpc?.request.openExternal({
url: w.explorerAddressUrl,
})}>
<ExternalLink size={16} />
</Button>
</div>
</div>
</div>
<p class="mt-2 font-medium text-xs">Balance</p>
{#if w.loadingBalance}
<div class="py-1"><Loader /></div>
{:else}
<div class="flex items-baseline gap-2">
<p class="font-mono text-lg">{w.balance} XRP</p>
{#if w.price}
<p class="font-mono text-sm text-muted-foreground">
≈ ${(
parseFloat(w.balance) * parseFloat(w.price)
).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
{/if}
</div>
{#if !w.funded}
<p class="text-xs text-muted-foreground">
Account not activated — it needs at least 10 XRP to
meet the base reserve.
</p>
{/if}
{#if w.fee}
<p class="text-xs text-muted-foreground mt-1">
Network fee: ~{w.fee} XRP
</p>
{/if}
{/if}
</div>
<div class="space-y-6">
<div class="flex flex-col items-center py-2">
{#if w.loadingBalance}
<div class="h-12 w-40 rounded-lg bg-muted animate-pulse"></div>
{:else}
<p class="text-sm text-muted-foreground">Balance</p>
<p class="mt-1 font-mono text-4xl font-semibold tabular-nums tracking-tight">
{w.balance} <span class="text-muted-foreground text-2xl">XRP</span>
</p>
{#if w.price}
<p class="mt-1 text-sm text-muted-foreground tabular-nums">
≈ ${(parseFloat(w.balance) * parseFloat(w.price)).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</p>
{/if}
{#if !w.funded}
<p class="mt-2 text-xs text-muted-foreground">
Account not activated — needs at least 10 XRP
</p>
{/if}
{/if}
</div>
<div class="flex gap-2">
<Button
variant="outline"
onclick={() => navigate('/xrp/send')}>
Send
</Button>
<Button
variant="outline"
onclick={() => navigate('/settings')}>
<SettingsIcon size={16} />
</Button>
<Button
variant="outline"
onclick={async () => {
await w.logout()
navigate('/')
}}>
Logout
</Button>
</div>
</CardContent>
</Card>
<div class="flex items-center justify-center">
<Button onclick={() => navigate('/xrp/send')} class="gap-2">
<Send size={16} />
Send
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Transactions</CardTitle>
</CardHeader>
<CardContent>
{#if w.loadingTransactions}
<div class="flex justify-center py-4">
<Loader />
</div>
{:else if w.transactions.length === 0}
<p class="text-muted-foreground text-xs">
No transactions found
</p>
{:else}
<div class="max-h-96 space-y-1 overflow-y-auto">
{#each w.transactions as tx}
<button
onclick={() =>
navigate('/xrp/tx/:hash', {
params: { hash: tx.hash },
})}
class="flex w-full items-start gap-2 rounded-md bg-muted px-3 py-2 text-xs text-left cursor-pointer hover:bg-muted/80 transition-colors">
<div
class="shrink-0 mt-0.5 {tx.direction === 'in'
? 'text-green-500'
: 'text-muted-foreground'}">
<div class="mx-auto max-w-md">
<div class="flex items-center justify-between rounded-lg border border-border px-4 py-3">
<div class="min-w-0 flex-1">
<p class="text-xs text-muted-foreground mb-0.5">Address</p>
<p class="font-mono text-xs break-all">{w.address}</p>
</div>
<div class="flex shrink-0 gap-1 ml-2">
<Dialog.Root bind:open={qrDialogOpen}>
<Dialog.Trigger>
<Button variant="ghost" size="icon-sm" class="text-muted-foreground" aria-label="QR code">
<QrCodeIcon size={14} />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<div class="flex flex-col items-center gap-3 py-4">
<QrCode text={w.address} size={256} />
<p class="font-mono text-xs break-all text-center max-w-64">{w.address}</p>
</div>
</Dialog.Content>
</Dialog.Root>
<CopyButton text={w.address} variant="ghost" size="icon-sm" class="text-muted-foreground" />
<Button
variant="ghost"
size="icon-sm"
class="text-muted-foreground"
aria-label="View on explorer"
onclick={() => electrobun.rpc?.request.openExternal({ url: w.explorerAddressUrl })}>
<ExternalLink size={14} />
</Button>
</div>
</div>
{#if w.fee}
<p class="mt-2 text-center text-xs text-muted-foreground">
Network fee: ~{w.fee} XRP
</p>
{/if}
</div>
<Card>
<CardHeader>
<CardTitle class="text-sm">Transactions</CardTitle>
</CardHeader>
<CardContent>
{#if w.loadingTransactions}
<div class="flex justify-center py-8"><Loader /></div>
{:else if w.transactions.length === 0}
<p class="text-center text-sm text-muted-foreground py-8">No transactions yet</p>
{:else}
<div class="space-y-1">
{#each w.transactions as tx}
<button
onclick={() => navigate('/xrp/tx/:hash', { params: { hash: tx.hash } })}
class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left hover:bg-muted transition-colors">
<div class="flex size-8 shrink-0 items-center justify-center rounded-full {tx.direction === 'in' ? 'bg-muted' : 'bg-muted'}">
<div class={tx.direction === 'in' ? 'text-green-500' : 'text-muted-foreground'}>
{#if tx.direction === 'in'}
<ArrowDown size={16} />
{:else}
<ArrowUp size={16} />
{/if}
</div>
<div class="min-w-0 flex-1 space-y-0.5">
<p class="font-medium">
{tx.direction === 'in' ? 'Received' : 'Sent'}
{#if !tx.confirmed}
<span class="text-muted-foreground">
(unconfirmed)
</span>
{/if}
</p>
<p class="font-mono text-muted-foreground">
{tx.amount} XRP
</p>
{#if tx.timestamp && tx.timestamp !== '0'}
<p class="text-muted-foreground">
{new Date(
Number(tx.timestamp) * 1000,
).toLocaleString()}
</p>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium">
{tx.direction === 'in' ? 'Received' : 'Sent'}
{#if !tx.confirmed}
<span class="text-muted-foreground text-xs ml-1">(unconfirmed)</span>
{/if}
<p
class="text-muted-foreground/50 truncate font-mono">
</p>
<p class="text-xs text-muted-foreground">
{#if tx.timestamp && tx.timestamp !== '0'}
{new Date(Number(tx.timestamp) * 1000).toLocaleString()}
{:else}
{tx.hash.slice(0, 12)}...
</p>
</div>
</button>
{/each}
</div>
{/if}
</CardContent>
</Card>
</div>
</div>
{/if}
</p>
</div>
<p class="shrink-0 font-mono text-sm tabular-nums {tx.direction === 'in' ? 'text-green-500' : ''}">
{tx.direction === 'in' ? '+' : '-'}{tx.amount} XRP
</p>
</button>
{/each}
</div>
{/if}
</CardContent>
</Card>
</div>