Vorlage · React · HTML
Business finance
Ein Finanz-Dashboard für Unternehmen in Tiles: Einnahmen gegen Zahlungen, ein Wachstumsring gegen das Ziel, eine Aktien-Sparkline, Aktivität und eine Checkliste zur Verifizierung.
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/business-finance.jsonnpx shadcn@latest add @manniche/business-financebusiness-finance.tsx
// A business finance screen in Tiles: income against payments, a growth ring, a stock sparkline, activity and a
// verification checklist.
import { cn } from '@/lib/utils'
import { BigNumber, DeltaPill } from '@/registry/manniche/chart-kit/chart-kit'
import type { ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile, TileLegend } from '@/registry/manniche/data-tile/data-tile'
import { AreaChart, type AreaPoint } from '@/registry/manniche/area-chart/area-chart'
import { Donut } from '@/registry/manniche/donut/donut'
import { Sparkline } from '@/registry/manniche/sparkline/sparkline'
import { TransactionList, type Transaction } from '@/registry/manniche/transaction-list/transaction-list'
export type VerificationStep = { id: string; label: string; done: boolean; hint?: string }
export type BusinessFinanceProps = {
data: {
/** Income per period, with `previous` holding payments for the same period. */
cashflow: AreaPoint[]
income: number
incomeChange: number
/** Growth toward the target, 0 to 1. */
growth: number
growthTarget: string
stock: { label: string; value: number }[]
stockName: string
activity: Transaction[]
verification: VerificationStep[]
}
format?: ValueFormat
title?: string
note?: string
today?: string
/** The current time. Used for `today` when that is not given. */
now?: Date
labels?: { income?: string; payments?: string; growth?: string; /** Under the share in the growth ring. Default "of target". */ ofTarget?: string; stock?: string; activity?: string; verification?: string; stepsDone?: (done: number, total: number) => string; done?: string; todo?: string }
className?: string
}
const DEFAULT_FORMAT: ValueFormat = { currency: 'EUR', decimals: 0 }
/** Business finance: cash flow, growth ring, stock sparkline, activity and a verification checklist. */
// "YYYY-MM-DD" of `now` in local time, for TransactionList's "Today" and "Yesterday".
const dayOf = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
export function BusinessFinance({ data, format = DEFAULT_FORMAT, title = 'Business finance', note = 'Example data.', today, now, labels, className }: BusinessFinanceProps) {
const day = today ?? (now ? dayOf(now) : undefined)
const l = {
income: 'Income',
payments: 'Payments',
growth: 'Growth',
ofTarget: 'of target',
stock: 'Stock',
activity: 'Activity',
verification: 'Verification',
stepsDone: (d: number, t: number) => `${d} of ${t} steps done`,
done: 'Done',
todo: 'To do',
...labels,
}
const doneCount = data.verification.filter((s) => s.done).length
const pct = Math.round(data.growth * 100)
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">
<h1 className="text-[22px] leading-tight font-semibold tracking-tight @3xl:col-span-6 @6xl:col-span-12">{title}</h1>
<DataTile
title={`${l.income} / ${l.payments}`}
className="@3xl:col-span-6 @6xl:col-span-8"
footer={<span>{note}</span>}
action={<TileLegend items={[{ label: l.income, color: 'var(--chart-1)' }, { label: l.payments, color: 'var(--chart-3)', shape: 'dashed' }]} />}
>
<div className="flex flex-wrap items-center gap-3">
<BigNumber value={data.income} format={format} size="lg" />
<DeltaPill value={data.incomeChange} format={{ decimals: 1, suffix: '%', sign: true }} />
</div>
<AreaChart className="mt-4" data={data.cashflow} label={`${l.income} and ${l.payments}`} format={format} compare labels={{ previous: l.payments }} />
</DataTile>
<DataTile title={l.growth} inverted className="@3xl:col-span-3 @6xl:col-span-4">
{/* One segment against a whole of 100, held highlighted so the centre reads the share of the target rather
than "Total 100%". */}
<Donut
data={[{ label: l.growth, value: pct }]}
total={100}
activeIndex={0}
label={`${l.growth}, ${pct} percent of ${data.growthTarget}`}
format={{ suffix: '%' }}
labels={{ ofTotal: l.ofTarget }}
/>
</DataTile>
<DataTile title={l.stock} className="@3xl:col-span-3 @6xl:col-span-4">
<p className="text-sm font-medium text-muted-foreground">{data.stockName}</p>
<Sparkline className="mt-3" data={data.stock} label={`${l.stock}, ${data.stockName}`} height={72} />
</DataTile>
<DataTile title={l.verification} className="@3xl:col-span-3 @6xl:col-span-4" footer={<span>{l.stepsDone(doneCount, data.verification.length)}</span>}>
<ol className="space-y-3">
{data.verification.map((s) => (
<li key={s.id} className="flex items-start gap-3 text-sm">
<span
aria-hidden
className={cn('mt-0.5 grid size-5 shrink-0 place-items-center rounded-full border text-[11px]', s.done ? 'border-transparent bg-foreground text-background' : 'border-border text-transparent')}
>
✓
</span>
<span>
<span className="font-medium">{s.label}</span>
<span className="sr-only"> ({s.done ? l.done : l.todo})</span>
{s.hint && <span className="block text-xs text-muted-foreground">{s.hint}</span>}
</span>
</li>
))}
</ol>
</DataTile>
<DataTile title={l.activity} className="@3xl:col-span-6 @6xl:col-span-4">
<TransactionList data={data.activity} label={l.activity} today={day} format={{ ...format, decimals: 2 }} limit={4} />
</DataTile>
</div>
</div>
)
}
export default BusinessFinancebusiness-finance-demo.tsx
import { BusinessFinance } from '@/components/business-finance'
// Example data for the demo, not a real business. "Today" is Thursday 8 October 2026.
const months = ['Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct']
const income = [18200, 19400, 21000, 20100, 22800, 24100, 25600]
const payments = [14100, 15200, 15900, 16800, 17100, 18400, 19000]
const data = {
cashflow: months.map((label, i) => ({ label, value: income[i], previous: payments[i] })),
income: 25600,
incomeChange: 6.2,
growth: 0.68,
growthTarget: 'Yearly target',
stockName: 'Example Holdings (fictional)',
stock: [41, 43, 42, 46, 45, 49, 48, 52, 51, 55].map((value, i) => ({ value, label: `Week ${i + 1}` })),
activity: [
{ id: 'a1', name: 'Invoice 1042, Birch & Co', category: 'Invoice', date: '2026-10-08T10:12', amount: 2400 },
{ id: 'a2', name: 'Office rent', category: 'Rent', date: '2026-10-07T07:00', amount: -1350 },
{ id: 'a3', name: 'Supplier, Harbour Print', category: 'Supplies', date: '2026-10-06T15:30', amount: -412.5, status: 'pending' as const },
{ id: 'a4', name: 'Invoice 1041, Lumen Studio', category: 'Invoice', date: '2026-10-05T09:02', amount: 1800 },
],
verification: [
{ id: 'v1', label: 'Company details', done: true },
{ id: 'v2', label: 'Owner identity', done: true },
{ id: 'v3', label: 'Business address', done: true, hint: 'Confirmed by letter' },
{ id: 'v4', label: 'Bank statement', done: false, hint: 'Upload the last three months' },
],
}
export default function BusinessFinanceDemo() {
return <BusinessFinance data={data} today="2026-10-08" />
}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/business-finance.json