Templates
Dashboard
A date range, four KPI cards, a trend chart and a recent records table.
The dashboard block builds a summary page inside the AppShell template. When the date range changes, the figures swap; the chart and table do not enter again.
Preview
Section titled “Preview”Change the date range. The brand and mode switches in the page header restyle the preview too. The second preview is the first-load state.
pnpm add @bigbang/ui| Data | Type | Note |
|---|---|---|
| KPIs | { active, open, done, cycle, delta } |
The app formats them with Intl; StatCard never formats numbers. |
| Chart series | ChartSeries[] |
At most 8 series; colours from the chart.categorical-* tokens. |
| Recent records | Row[] |
DataTable shows loading, empty and error states through status. |
loading |
boolean |
Shows every region in its first-load state. |
import { Button, Chart, createDataTableColumnHelper, DataTable, Grid, Icon, SegmentedControl, StatCard, StatusBadge } from '@bigbang/ui';import type { ChartSeries, StatusKey } from '@bigbang/ui';import { useMemo, useState } from 'react';import type { Lang } from '../../patterns/lang';import { AppShellTemplate } from '../../templates/app-shell/AppShellTemplate';
const copy = { tr: { title: 'Genel bakış', description: 'Ekibinizin projeleri ve görevleri.', range: 'Tarih aralığı', ranges: { '7': '7 gün', '30': '30 gün', '90': '90 gün' }, report: 'Rapor oluştur', active: 'Aktif proje', open: 'Açık görev', done: 'Tamamlanan görev', cycle: 'Ortalama süre', days: (value: string) => `${value} gün`, period: 'önceki döneme göre', chart: 'Açılan ve kapanan görevler', chartDescription: 'Haftalık görev sayısı', opened: 'Açılan', closed: 'Kapanan', week: (index: number) => `${index}. hafta`, recent: 'Son kayıtlar', name: 'Kayıt', status: 'Durum', owner: 'Ekip', updated: 'Güncellendi', record: (index: number) => `Kayıt ${index}`, team: (index: number) => `Ekip ${index}`, }, en: { title: 'Overview', description: "Your team's projects and tasks.", range: 'Date range', ranges: { '7': '7 days', '30': '30 days', '90': '90 days' }, report: 'Create report', active: 'Active projects', open: 'Open tasks', done: 'Completed tasks', cycle: 'Average cycle time', days: (value: string) => `${value} days`, period: 'against the previous period', chart: 'Opened and closed tasks', chartDescription: 'Tasks per week', opened: 'Opened', closed: 'Closed', week: (index: number) => `Week ${index}`, recent: 'Recent records', name: 'Record', status: 'Status', owner: 'Team', updated: 'Updated', record: (index: number) => `Record ${index}`, team: (index: number) => `Team ${index}`, },};
type Range = '7' | '30' | '90';
// Round demo figures per range; an app passes its own query results.const figures: Record<Range, { active: number; open: number; done: number; cycle: number; delta: number }> = { '7': { active: 24, open: 120, done: 300, cycle: 3.5, delta: 8 }, '30': { active: 26, open: 140, done: 1200, cycle: 4, delta: 12 }, '90': { active: 30, open: 180, done: 3600, cycle: 4.5, delta: 5 },};
const opened = [40, 45, 50, 48, 60, 55, 70, 65, 80, 75, 90, 85];const closed = [30, 40, 45, 50, 50, 55, 60, 65, 70, 75, 80, 85];const statuses: StatusKey[] = ['completed', 'in-review', 'active', 'pending', 'completed'];
type Row = { id: string; name: string; status: StatusKey; owner: string; updated: string };const helper = createDataTableColumnHelper<Row>();
export type DashboardShellBlockProps = { lang?: Lang; /** Shows the loading state of every region at once, as on first load. */ loading?: boolean; defaultRange?: Range;};
/** A dashboard page: range control, four KPIs, a trend chart and the latest records. */export function DashboardShellBlock({ lang = 'tr', loading = false, defaultRange = '30' }: DashboardShellBlockProps) { const t = copy[lang]; const [range, setRange] = useState<Range>(defaultRange); const number = new Intl.NumberFormat(lang === 'tr' ? 'tr-TR' : 'en-GB'); const f = figures[range]; // Turkish puts the percent sign first. const percent = (value: number) => (lang === 'tr' ? `%${value}` : `${value}%`);
const series = useMemo<ChartSeries[]>( () => [ { id: 'opened', label: t.opened, data: opened.map((y, index) => ({ x: t.week(index + 1), y })) }, { id: 'closed', label: t.closed, data: closed.map((y, index) => ({ x: t.week(index + 1), y })) }, ], [t], );
const rows = useMemo<Row[]>(() => { const date = new Intl.DateTimeFormat(lang === 'tr' ? 'tr-TR' : 'en-GB', { day: 'numeric', month: 'short' }); return statuses.map((status, index) => ({ id: `r${index + 1}`, name: t.record(index + 1), status, owner: t.team((index % 3) + 1), updated: date.format(new Date(2026, 9, 28 - index)) })); }, [t, lang]);
const columns = useMemo( () => [ helper.accessor('name', { header: t.name, meta: { rowHeader: true } }), helper.accessor('status', { header: t.status, enableSorting: false, cell: (context) => <StatusBadge status={context.getValue()} size="sm" /> }), helper.accessor('owner', { header: t.owner }), helper.accessor('updated', { header: t.updated }), ], [t], );
return ( <AppShellTemplate lang={lang} current="home" title={t.title} description={t.description} loading={loading} actions={ <> <SegmentedControl label={t.range} size="sm" value={range} onValueChange={(value) => setRange(value as Range)} items={(['7', '30', '90'] as const).map((value) => ({ value, label: t.ranges[value] }))} /> <Button size="sm" startIcon={<Icon name="file" />}> {t.report} </Button> </> } > {/* Figures change by remount (110 ms cross-fade); amounts never count up. */} <Grid columns={{ base: 2, md: 4 }} gap="lg"> <StatCard key={`active-${range}`} label={t.active} value={number.format(f.active)} loading={loading} /> <StatCard key={`open-${range}`} label={t.open} value={number.format(f.open)} loading={loading} /> <StatCard key={`done-${range}`} label={t.done} value={number.format(f.done)} delta={{ value: percent(f.delta), direction: 'up' }} period={t.period} loading={loading} /> <StatCard key={`cycle-${range}`} label={t.cycle} value={t.days(number.format(f.cycle))} delta={{ value: percent(5), direction: 'down', sentiment: 'positive' }} period={t.period} loading={loading} /> </Grid> <Chart kind="area" title={t.chart} description={t.chartDescription} series={series} height="md" loading={loading} formatValue={(value) => number.format(value)} /> <DataTable caption={t.recent} columns={columns} data={rows} getRowId={(row) => row.id} status={loading ? 'loading' : 'ready'} density="compact" /> </AppShellTemplate> );}Structure
Section titled “Structure”- AppShell template: navigation and top bar.
- PageHeader:
h1“Overview”, a SegmentedControl date range and “Create report”. - Four StatCards: two columns below 1024 px, four above.
- A Chart area chart: opened and closed tasks over 12 weeks.
- A DataTable “Recent records” with status as a StatusBadge.
Accessibility
Section titled “Accessibility”- The page has one
h1; the chart and the table are named by their own titles andcaption. - The date range reads as a
radiogroup; one tab stop, arrow keys choose. - The chart reads as
role="img"with a summary text, and offers a “Show data as a table” fallback. - While loading, regions carry
aria-busy; the page header keeps its place. - A figure change is a 110 ms cross-fade; amounts never count up.
Components used
Section titled “Components used”StatCard, Chart, DataTable, SegmentedControl, StatusBadge, Grid, PageHeader. Patterns: Loading, Charts gallery.