Skabelon · React · HTML
Wallet dashboard
Et tegnebogsdashboard i Tiles: saldo, en vifte af tre betalingskort, søjler for pengestrøm, en forbrugsdonut og abonnementer.
Forhåndsvisning
React
Installér skabelonen med shadcn. Den lægges i dit projekt som almindelige komponenter, som du selv retter i.
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/wallet-dashboard.jsonnpx shadcn@latest add @manniche/wallet-dashboardwallet-dashboard.tsx
// A wallet screen in Tiles: balance, a fanned stack of payment cards, cash flow, a spending donut and subscriptions.
// Compose-only: every chart is a Manniche primitive; the screen owns layout and copy.
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { BigNumber, DeltaPill, Pills } from '@/registry/manniche/chart-kit/chart-kit'
import { formatValue, type ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile } from '@/registry/manniche/data-tile/data-tile'
import { BarChart, type BarPoint } from '@/registry/manniche/bar-chart/bar-chart'
import { CardStack, type StackCard } from '@/registry/manniche/card-stack/card-stack'
import { Donut, type DonutDatum } from '@/registry/manniche/donut/donut'
export type WalletSubscription = { id: string; name: string; amount: number; renews: string }
export type WalletPeriod = { id: string; label: string; long: string; flow: BarPoint[]; spending: DonutDatum[] }
export type WalletDashboardProps = {
data: {
balance: number
change: number
cards: StackCard[]
periods: WalletPeriod[]
subscriptions: WalletSubscription[]
}
format?: ValueFormat
title?: string
note?: string
labels?: {
balance?: string
cards?: string
cashFlow?: string
spending?: string
subscriptions?: string
period?: string
perMonth?: string
renews?: (date: string) => string
monthlyTotal?: string
}
/** Reserved for date-aware content; the screen itself is static. */
now?: Date
className?: string
}
const DEFAULT_FORMAT: ValueFormat = { currency: 'EUR', decimals: 2 }
/** Wallet overview: balance, card stack, cash flow, spending by category and recurring payments. */
export function WalletDashboard({ data, format = DEFAULT_FORMAT, title = 'Wallet', note = 'Example data.', labels, className }: WalletDashboardProps) {
const l = {
balance: 'Total balance',
cards: 'Cards',
cashFlow: 'Cash flow',
spending: 'Spending',
subscriptions: 'Subscriptions',
period: 'Period',
perMonth: 'a month',
renews: (d: string) => `Renews ${d}`,
monthlyTotal: 'Monthly total',
...labels,
}
const [periodId, setPeriodId] = useState(data.periods[0]?.id ?? '')
const period = data.periods.find((p) => p.id === periodId) ?? data.periods[0]
const monthly = data.subscriptions.reduce((s, x) => s + x.amount, 0)
return (
<div className={cn('@container w-full font-sans text-foreground', className)}>
<div className="grid gap-4 @3xl:grid-cols-6 @6xl:grid-cols-12">
<div className="@3xl:col-span-6 @6xl:col-span-12">
<h1 className="text-[22px] leading-tight font-semibold tracking-tight">{title}</h1>
</div>
<DataTile title={l.balance} className="@3xl:col-span-3 @6xl:col-span-4" footer={<span>{note}</span>}>
<BigNumber value={data.balance} format={format} size="lg" />
<div className="mt-3">
<DeltaPill value={data.change} format={{ decimals: 1, suffix: '%', sign: true }} />
</div>
</DataTile>
<DataTile title={l.cards} className="@3xl:col-span-3 @6xl:col-span-4">
<CardStack data={data.cards} label={l.cards} format={format} />
</DataTile>
<DataTile
title={l.cashFlow}
className="@3xl:col-span-6 @6xl:col-span-4"
action={<Pills label={l.period} options={data.periods.map(({ id, label }) => ({ id, label }))} value={periodId} onChange={setPeriodId} />}
>
{period && <BarChart data={period.flow} label={`${l.cashFlow}, ${period.long}`} format={{ ...format, decimals: 0 }} />}
</DataTile>
<DataTile title={l.spending} className="@3xl:col-span-3 @6xl:col-span-6">
{period && <Donut data={period.spending} label={`${l.spending}, ${period.long}`} format={format} />}
</DataTile>
<DataTile
title={l.subscriptions}
className="@3xl:col-span-3 @6xl:col-span-6"
footer={
<span>
{l.monthlyTotal}: <strong className="tabular-nums">{formatValue(monthly, { ...format, decimals: 2 })}</strong>
</span>
}
>
{/* The tile's heading names the list; a hidden <li> as its label would be counted as an extra item. */}
<ul aria-label={l.subscriptions} className="divide-y divide-border">
{data.subscriptions.map((s) => (
<li key={s.id} className="flex items-center gap-3 py-3 text-sm">
<span aria-hidden className="grid size-9 shrink-0 place-items-center rounded-full bg-muted text-xs font-semibold text-muted-foreground">
{s.name.slice(0, 2).toUpperCase()}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate font-medium">{s.name}</span>
<span className="block text-xs text-muted-foreground">{l.renews(s.renews)}</span>
</span>
<span className="font-medium tabular-nums">
{formatValue(s.amount, { ...format, decimals: 2 })}
<span className="sr-only"> {l.perMonth}</span>
</span>
</li>
))}
</ul>
</DataTile>
</div>
</div>
)
}
export default WalletDashboardwallet-dashboard-demo.tsx
import { WalletDashboard } from '@/components/wallet-dashboard'
// Example data for the demo, not real accounts.
const data = {
balance: 8318.06,
change: 3.2,
cards: [
{ id: 'everyday', name: 'Everyday', kind: 'Debit', ending: '4821', amount: 2091.2 },
{ id: 'travel', name: 'Travel', kind: 'Multi-currency', ending: '0937', amount: 612.08 },
{ id: 'savings', name: 'Savings', kind: 'Pot', ending: '1162', amount: 5614.78 },
],
periods: [
{
id: 'week',
label: 'Week',
long: 'last 7 days',
flow: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((label, i) => ({ label, value: [120, 340, -80, 210, 560, -150, 90][i] })),
spending: [
{ label: 'Groceries', value: 90.84 },
{ label: 'Transport', value: 47.1 },
{ label: 'Eating out', value: 74.02 },
{ label: 'Bills', value: 84.12 },
],
},
{
id: 'month',
label: 'Month',
long: 'last 30 days',
flow: ['Wk 1', 'Wk 2', 'Wk 3', 'Wk 4'].map((label, i) => ({ label, value: [820, 640, 1180, 910][i] })),
spending: [
{ label: 'Groceries', value: 414.96 },
{ label: 'Transport', value: 179.82 },
{ label: 'Eating out', value: 235.14 },
{ label: 'Shopping', value: 152.15 },
{ label: 'Bills', value: 401.13 },
],
},
],
subscriptions: [
{ id: 'music', name: 'Northwind Music', amount: 10.99, renews: '12 Oct' },
{ id: 'cloud', name: 'Harbour Cloud', amount: 2.99, renews: '15 Oct' },
{ id: 'news', name: 'Green Leaf Daily', amount: 7.5, renews: '21 Oct' },
],
}
export default function WalletDashboardDemo() {
return <WalletDashboard data={data} />
}HTML
Hele siden som én fil med al CSS indbygget og intet JavaScript. Knapper, faner og filtre virker med HTML og CSS alene.
Farven i din egen app
Har du allerede shadcn sat op, giver disse variabler samme farve. Lys i :root, mørk i .dark.
:root {
--primary: oklch(0.52 0.2 262);
--primary-foreground: oklch(0.99 0 0);
--accent: oklch(0.95 0.034 262);
--accent-foreground: oklch(0.36 0.140 262);
--ring: oklch(0.52 0.2 262);
}
.dark {
--primary: oklch(0.74 0.150 262);
--primary-foreground: oklch(0.18 0.024 262);
--accent: oklch(0.29 0.050 262);
--accent-foreground: oklch(0.9 0.050 262);
--ring: oklch(0.74 0.150 262);
}Installér
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/wallet-dashboard.json