Vorlage · React · HTML
Wallet dashboard
Ein Wallet-Dashboard in Tiles: Kontostand, ein Fächer aus drei Zahlungskarten, Cashflow-Balken, ein Ausgaben-Donut und Abos.
Vorschau
React
Installiere die Vorlage mit shadcn. Sie landet als normale Komponenten in deinem Projekt, die du selbst anpasst.
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
Die ganze Seite als eine Datei mit eingebautem CSS und ohne JavaScript. Umschalter, Tabs und Filter funktionieren allein mit HTML und CSS.
Die Farbe in deiner eigenen App
Wenn shadcn bereits eingerichtet ist, ergeben diese Variablen dieselbe Farbe. Hell in :root, dunkel in .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);
}Installieren
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/wallet-dashboard.json