Ansora overview dashboard
Overview screen for an outpatient clinic / front desk — a KPI strip as one card with internal dividers, a visits chart and a filterable patients table with statuses. The header actions (date range, export, light/dark toggle) ship separately as DashboardActions and belong in the shell's headerActions; the screen renders no top bar, no search field and no user menu of its own. Composes shadcn primitives (Card, Table, Button, Avatar, Select, Tabs, Chart, Badge) on top of @ansora/tokens — only the status logic is vendored (@ansora/status-pill); the primitives are installed from the public shadcn registry and pick up our look automatically.
blockdashboard
Install
pnpm dlx shadcn@4.23.0 add @ansora/dashboardExamples
Usage
Four files. Dashboard (server, dashboard.tsx) renders three of them — KpiStrip (a separate item, @ansora/kpi-strip: ONE card with internal columns separated by divide-x, not N cards with gaps), DashboardVisitsChart (client: Recharts through the shadcn chart, one series, a 7/14/30-day range picked with Select) and DashboardPatientsTable (client: a @ansora/data-table recipe inside @ansora/section-card — ColumnDef.cell renders the avatar and the StatusPill per row, Tabs sits in the card's action slot as the status filter, status through @ansora/status-pill — never green; pozornost is the only status with a saturated amber).
The fourth file is DashboardActions, and Dashboard does NOT render it — it only re-exports it, so that you can import both from one place. DashboardActions renders no <header> of its own — it is the group of controls for the right-hand side of the shell's top bar, and you pass it to @ansora/app-shell through headerActions.
Required props of Dashboard: kpis, kpisTitle, visits, patients. kpisTitle is the KPI strip's own heading (KpiStrip carries no domain content of its own) and also names its scroll region. There is no user prop — the user block belongs to @ansora/app-shell and lives in the sidebar footer. Nothing here has a default: the component knows no patient and no KPI number of its own, the same rule as nav in @ansora/app-shell and emptyLabel in @ansora/data-table.
DashboardActions takes rangeLabel, onRangeChange, onExport and onCommandPalette — all optional. The range button renders only when rangeLabel and onRangeChange are both given, the export button only with onExport, and the ⌘K / Ctrl+K listener is not registered at all without onCommandPalette: a control that does nothing, or a swallowed browser shortcut with nothing behind it, is worse than a missing one. With none of them wired all that is left is the theme toggle. The only exported type is DashboardActionsProps.
import { formatUnit } from "@ansora/core"
import { AppShell } from "@/components/ansora/app-shell"
import { Dashboard, DashboardActions } from "@/components/ansora/dashboard"
import { readSidebarState } from "@/lib/ansora/sidebar-state"
export default async function Page() {
return (
<AppShell
defaultOpen={await readSidebarState()}
nav={nav}
title="Přehled"
headerActions={<DashboardActions />}
>
<Dashboard
kpisTitle="Klíčové ukazatele"
kpis={[
{ label: "Pacienti dnes", value: 128, icon: <UsersIcon />, hint: "vs 120 včera", delta: { label: "+8", direction: "up" } },
{ label: "V čekárně", value: 9, icon: <ClockIcon />, hint: "aktuálně čeká" },
{ label: "Vyžaduje pozornost", value: 3, icon: <WarningIcon />, tone: "warn", hint: "čekací doba nad 30 min", delta: { label: "2 nad limit" } },
{ label: "Průměrná čekací doba", value: formatUnit(18, "min"), icon: <HourglassIcon />, hint: "cíl je 20 min", delta: { label: formatUnit(-4, "min"), direction: "down" } },
]}
visits={[{ date: "2026-07-18", visits: 96 } /* … */]}
patients={[
{ id: "p1", name: "Petra Nová", initials: "PN", arrivedAt: "08:14", department: "Interna", waitMinutes: 12, status: "ceka" },
{ id: "p2", name: "Jan Dvořák", initials: "JD", arrivedAt: "07:58", department: "Kardiologie", waitMinutes: 41, status: "pozornost" },
]}
/>
</AppShell>
)
}
KpiCell.value is string | number. The NUMBER branch is formatted for you with formatNumber; the STRING branch is passed through untouched, so anything with a unit is YOUR obligation to run through @ansora/core — formatUnit for a quantity, as the wait-time cell above does, formatDose for a medication dose. Never concatenate a unit with an ASCII space: "10 mg" can then wrap into "10" / "mg" and be misread. Both emit U+00A0; formatDose additionally brings the ISMP rules (leading zero, no trailing zero) and refuses a value it cannot render exactly — right for a dose, wrong for a waiting time, which is why the cell above is not one.
hint and delta are independent — most cells above use both. Icons are ReactNode, so the components stay unaware of any icon library; in a Server Component import them from "@phosphor-icons/react/ssr", never from the main entry point (that one calls React.useContext and crashes the server render).
Wiring the range, the export or the palette means handlers, so those three have to be passed from a client component; the page above stays a Server Component precisely because it passes none of them.
Recharts (3.8.0) arrives on its own through chart.json's dependencies — do not add it by hand.
The theme toggle does not go through next-themes (no extra npm dependency for a single item) — it flips the .dark class on <html>, which is what @custom-variant dark in @ansora/tokens reacts to, and remembers the choice in localStorage under the key ansora-theme. Add the blocking inline script from base docs to your layout, otherwise a reload shows one frame of the wrong theme. If you want the mode shared across pages, replace the toggle with your own provider.
Files
registry/ansora/components/dashboard.tsx→@components/ansora/dashboard.tsxregistry/ansora/components/dashboard-actions.tsx→@components/ansora/dashboard-actions.tsxregistry/ansora/components/dashboard-visits-chart.tsx→@components/ansora/dashboard-visits-chart.tsxregistry/ansora/components/dashboard-patients-table.tsx→@components/ansora/dashboard-patients-table.tsx
Dependencies
@ansora/base@ansora/status-pill@ansora/kpi-strip@ansora/section-card@ansora/data-tablehttps://ui.shadcn.com/r/styles/base-nova/button.jsonhttps://ui.shadcn.com/r/styles/base-nova/avatar.jsonhttps://ui.shadcn.com/r/styles/base-nova/select.jsonhttps://ui.shadcn.com/r/styles/base-nova/tabs.jsonhttps://ui.shadcn.com/r/styles/base-nova/chart.json
Ansora app shell
Application skeleton on top of the sidebar — collapsible navigation with a brand and a user block, a top bar with the page title, breadcrumbs and an actions slot, a skip link, and a slot for the content. A server component; the only client parts are the sidebar itself, deriving the active nav item from the path, and the user menu.
Ansora base
Tokens, icons and shared helpers. Install this first — every other item assumes it is already there.