dashboard01
A Dashboard block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call dashboard01() with no arguments for a working demo, or edit the code below live.
Props
| Prop | Type | Description |
|---|---|---|
pageTitle | string | — |
user | Sidebar07User | — |
navMain | Sidebar07NavMainItem[] | — |
documents | Sidebar07Project[] | — |
metricCards | MetricCardData[] | — |
chartData | ChartBarDailyPoint[] | — |
tableRows | DashboardTableRow[] | — |
Implementation notes
TypeScript typecheck clean, all 738 package tests pass (4 new), manual @domphy/doctor diagnose() self-check reports 0 diagnostics. Composed from existing pieces per instructions: sidebar07() reused wholesale for the app shell (aside+sticky header+backdrop), chartBarStacked() reused for the chart canvas, createDomphyTable + table() patch for the data grid. Deliberate simplifications vs. the full upstream spec (documented in the file's header comment): (1) sidebar07's shell has only one nav-main group + one secondary/projects group, so the spec's third 'pinned to bottom' secondary-utility nav group has no slot and is omitted; (2) sidebar07 owns its own sticky header (toggle+divider+breadcrumb title), which has no slot for a trailing action, so the 'ghost external link' button lives in its own thin utility row at the top of the content pane instead of being fused into that same bar; (3) chartBarStacked's props don't expose a header-aside slot, so the 7/30/90-day range toggleGroup/select control sits in a small row above the chartBarStacked card (which itself re-renders with a range-appropriate title/subtitle/trend) rather than nested inside chartBarStacked's own header; (4) the table's 'view tabs' genuinely filter rows by status via table-core's own ColumnFiltering feature (not a placeholder) with relabeled views (All Items/Done/In Progress/Not Started) instead of reproducing the upstream's differently-named tabs; (5) the row drawer's mobile-vs-desktop placement is resolved once via matchMedia at build time (drawer()'s placement prop isn't itself resize-reactive). Fully functional (not stubbed): row selection with indeterminate header checkbox, native HTML5 drag-and-drop reorder with opacity/shadow feedback, inline-editable Target/Limit numeric cells (commit on change, not per-keystroke, to avoid focus loss), reviewer dropdown, status badges, per-row kebab menu (Edit/Duplicate/Favorite/Delete), column-visibility toggling (Customize Columns), Add Section (creates+opens a new row in the drawer), and first/prev/next/last pagination with page-size select. All sample data/copy/numbers are original inventions, not reproductions of the upstream's real content.
Status: complete · Reference: shadcn/ui original
// shadcn/ui "dashboard-01" — clean-room reimplementation.
//
// A full-page admin dashboard shell: a collapsible icon-nav sidebar (reused
// wholesale from sidebar07), a slim content header, four KPI summary cards,
// a range-switchable bar chart (reused from chartBarStacked), and a rich
// drag-reorderable data table (createDomphyTable + the table() patch) with
// its own toolbar, status-filter tabs, column visibility control, inline
// numeric editing, per-row actions menu and a row-editing drawer.
//
// Composition notes (deliberate simplifications vs. the full upstream spec):
// - The sidebar is `sidebar07()` called directly with dashboard-flavored nav
// data, per the instruction to reuse that shell wholesale. sidebar07's own
// shell only exposes ONE nav-main group + ONE secondary (projects) group,
// so the spec's third "secondary utility" group (pinned to the bottom via
// margin-auto) has no slot to render into and is omitted here.
// - sidebar07 owns its own sticky header (toggle + divider + breadcrumb
// title). Since that header has no slot for an extra trailing action, the
// "ghost-styled external link button" lives in its own thin utility row at
// the top of the content pane instead of being fused into the same bar.
// - The chart region reuses the exported `chartBarStacked()` factory as
// directed, rather than the closer-fitting (but not-directed-to-use)
// `chartAreaInteractive`. Since chartBarStacked's own props don't expose a
// header-aside slot, the 7/30/90-day range control sits in a small toolbar
// row directly above the chartBarStacked card (which itself re-renders
// with a range-appropriate title/description/trend) rather than nested
// inside chartBarStacked's own header.
// - The data table's "view tabs" filter the row set by `status` using
// table-core's own column-filtering feature (genuinely functional, not a
// placeholder) rather than reproducing the upstream's differently-labeled,
// mostly-inert tabs.
// - The row drawer's mobile-vs-desktop placement is resolved once at build
// time via `matchMedia` (the `drawer()` patch's `placement` prop is not
// itself reactive to viewport resize).
//
// Implemented purely from the block's public functional/visual spec — no
// upstream shadcn/ui source was viewed or copied. Sample data, copy, and
// numbers are original inventions for this port.
import type { DomphyElement, Listener, State } from "@domphy/core";
import { behavior, rawHtml, toState } from "@domphy/core";
import type { Column, Row } from "@domphy/table";
import {
createColumnHelper,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
} from "@domphy/table";
import { createDomphyTable } from "@domphy/table/domphy";
import {
type ThemeColor,
themeColor,
themeDensity,
themeSpacing,
} from "@domphy/theme";
import {
button,
buttonGhost,
card,
drawer,
empty,
formGroup,
heading,
icon,
inputCheckbox,
inputNumber,
inputText,
label,
menu,
paragraph,
popover,
select,
small,
strong,
table as tableUI,
tabs,
toggleGroup,
} from "@domphy/ui";
import {
CHART_BAR_DAILY_END_DATE,
type ChartBarDailyPoint,
type ChartBarTwoSeriesPoint,
type ChartTrendDirection,
chartBarTrendIcon,
generateChartBarDailyData,
} from "../charts/chart-bar-shared.js";
import { chartBarStacked } from "../charts/chart-bar-stacked.js";
import {
ICON_BAR_CHART,
ICON_CHEVRON_RIGHT,
ICON_FOLDER,
ICON_GRID,
ICON_INBOX,
ICON_MESSAGE,
ICON_MORE,
ICON_PLUS,
ICON_USERS,
sidebarIcon,
} from "../sidebar/sidebar05-08-shared.js";
import {
type Sidebar07NavMainItem,
type Sidebar07Project,
type Sidebar07User,
sidebar07,
} from "../sidebar/sidebar07.js";
// ---------------------------------------------------------------------------
// Icons — hand-authored generic line glyphs, original to this file (24x24,
// stroke=currentColor unless noted). Not sourced from any third-party icon
// set. `ICON_CHEVRON_RIGHT`/`ICON_MORE`/`ICON_PLUS` are reused from the
// sidebar shared module instead of being re-authored here.
// ---------------------------------------------------------------------------
const ICON_EXTERNAL_LINK =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="1em" height="1em"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14L21 3"/></svg>';
const ICON_GRIP =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="none" width="1em" height="1em"><circle cx="9" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg>';
const ICON_CHEVRON_DOWN =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="1em" height="1em"><path d="M6 9l6 6 6-6"/></svg>';
const ICON_CHECK_CIRCLE =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="1em" height="1em"><circle cx="12" cy="12" r="9"/><path d="M8 12l3 3 5-6"/></svg>';
const ICON_SPINNER_ARC =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="1em" height="1em"><path d="M12 3a9 9 0 1 1-9 9"/></svg>';
const ICON_CIRCLE_DASHED =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="1em" height="1em"><circle cx="12" cy="12" r="9" stroke-dasharray="4 3"/></svg>';
const ICON_STAR =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="none" width="1em" height="1em"><path d="M12 2l2.9 6.6 7.1.7-5.4 4.7 1.6 7-6.2-3.7-6.2 3.7 1.6-7L2 9.3l7.1-.7z"/></svg>';
const ICON_CHEVRON_DOUBLE_RIGHT =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="1em" height="1em"><path d="M7 6l6 6-6 6"/><path d="M14 6l6 6-6 6"/></svg>';
// Visually-hidden ("sr-only") style, same recipe as this package's other
// sr-only usages (shadcn/auth/login05.ts, magicui/core/tweetCard.ts). Upstream
// shadcn's data-table cells give the row-select Checkbox / numeric inputs only
// an `aria-label` (via Radix, which isn't a native `<input>`) — this port uses
// real `<input>` elements, so each gets a matching sr-only `<label for>` too:
// `aria-label` alone satisfies accessible-name computation but not htmlhint's
// `input-requires-label` rule, which requires a real associated `<label>` tag.
const SR_ONLY_STYLE = {
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
} as const;
/** Wraps a raw inline SVG string in a themed `icon()` box, mirrored on the
* inline axis — reuses a single chevron glyph for both "next" and "prev" /
* "last" and "first" instead of authoring four near-identical SVGs. */
function flippedIcon(
svg: string,
color: ThemeColor = "neutral",
): DomphyElement<"span"> {
return {
span: rawHtml(svg),
ariaHidden: "true",
style: { transform: "scaleX(-1)" },
$: [icon({ color })],
} as unknown as DomphyElement<"span">;
}
// ---------------------------------------------------------------------------
// Sidebar data (fed into the reused `sidebar07()` shell)
// ---------------------------------------------------------------------------
const DEFAULT_NAV_MAIN: Sidebar07NavMainItem[] = [
{ title: "Dashboard", icon: ICON_GRID, href: "#", active: true },
{ title: "Analytics", icon: ICON_BAR_CHART, href: "#" },
{ title: "Team", icon: ICON_USERS, href: "#" },
{ title: "Inbox", icon: ICON_INBOX, href: "#" },
{ title: "Documentation", icon: ICON_FOLDER, href: "#" },
];
const DEFAULT_DOCUMENTS: Sidebar07Project[] = [
{ title: "Document Library", icon: ICON_FOLDER, href: "#" },
{ title: "Report Center", icon: ICON_BAR_CHART, href: "#" },
{ title: "Content Assistant", icon: ICON_MESSAGE, href: "#" },
];
const DEFAULT_USER: Sidebar07User = {
name: "Alex Rivera",
email: "alex@example.com",
};
// ---------------------------------------------------------------------------
// KPI metric cards
// ---------------------------------------------------------------------------
interface MetricCardData {
label: string;
value: string;
badgeDelta: string;
trendDirection: ChartTrendDirection;
footerHeadline: string;
footerSubtext: string;
}
const DEFAULT_METRIC_CARDS: MetricCardData[] = [
{
label: "Active Subscriptions",
value: "8,492",
badgeDelta: "+6.4%",
trendDirection: "up",
footerHeadline: "Subscriber growth is accelerating",
footerSubtext: "Compared to the previous 30 days",
},
{
label: "Monthly Recurring Revenue",
value: "$42,150",
badgeDelta: "+3.1%",
trendDirection: "up",
footerHeadline: "Revenue is trending upward",
footerSubtext: "Includes upgrades and renewals",
},
{
label: "Churn Rate",
value: "2.3%",
badgeDelta: "-0.6%",
trendDirection: "down",
footerHeadline: "Churn improved this quarter",
footerSubtext: "Fewer cancellations than last period",
},
{
label: "Avg. Response Time",
value: "4.2h",
badgeDelta: "-18%",
trendDirection: "down",
footerHeadline: "Faster responses this week",
footerSubtext: "Average first-reply time across tickets",
},
];
/** A single elevated, gradient-tinted KPI card: muted label, huge bold value,
* a top-right trend-delta pill, and a two-line trend footer. */
function metricCard(
data: MetricCardData,
key: string | number,
): DomphyElement<"div"> {
// Rendered inline with the label (not as a sibling `aside`) — `card()`'s
// grid only carves out an "aside" column next to a "title"/"desc" pair,
// and this KPI tile's label+value order is reversed from that (label
// above value, whereas card()'s "title" area always renders above
// "desc"), so both already bypass those named areas via the `content`
// wrapper below. A standalone `aside` sibling here would span the now-
// empty title/desc rows and float above `content` instead of sitting
// next to the label.
//
// Upstream section-cards.tsx uses a uniform neutral `<Badge
// variant="outline">` on every card: direction is conveyed only by the
// trend-arrow glyph, never by tinting the badge (or footer icon) per
// direction — so both stay neutral here.
const trendBadge: DomphyElement<"span"> = {
span: [
chartBarTrendIcon(data.trendDirection, "neutral"),
{ span: data.badgeDelta } as unknown as DomphyElement,
],
style: {
display: "inline-flex",
alignItems: "center",
gap: themeSpacing(1),
paddingInline: themeSpacing(2),
paddingBlock: themeSpacing(1),
borderRadius: themeSpacing(999),
border: (l: Listener) =>
`1px solid ${themeColor(l, "shift-4", "neutral")}`,
color: (l: Listener) => themeColor(l, "shift-10", "neutral"),
},
} as unknown as DomphyElement<"span">;
const content: DomphyElement<"div"> = {
div: [
{
div: [
{
small: data.label,
$: [small({ color: "neutral" })],
style: {
minWidth: 0,
flex: "1 1 auto",
// Prefer wrap over mid-word ellipsis on narrow KPI tiles.
overflowWrap: "anywhere",
lineHeight: 1.25,
},
} as unknown as DomphyElement,
{
...trendBadge,
style: {
...(trendBadge.style as object),
flex: "0 0 auto",
whiteSpace: "nowrap",
},
} as unknown as DomphyElement,
],
style: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: themeSpacing(2),
minWidth: 0,
},
} as unknown as DomphyElement,
{
h2: data.value,
$: [heading({ color: "neutral" })],
style: {
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
} as unknown as DomphyElement,
],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(1),
minWidth: 0,
},
} as unknown as DomphyElement<"div">;
const footer: DomphyElement<"footer"> = {
footer: [
{
div: [
chartBarTrendIcon(data.trendDirection, "neutral"),
{
strong: data.footerHeadline,
$: [strong({ color: "neutral" })],
style: {
minWidth: 0,
// strong() paints an inherit-tone background — keep it
// transparent so no box appears over the card's gradient tint.
backgroundColor: "transparent",
// Allow multi-line headlines instead of clipping mid-word under
// a 4-column KPI grid inside a sidebar shell.
overflowWrap: "anywhere",
},
} as unknown as DomphyElement,
],
style: {
display: "flex",
alignItems: "flex-start",
gap: themeSpacing(1.5),
minWidth: 0,
},
} as unknown as DomphyElement,
{
small: data.footerSubtext,
$: [small({ color: "neutral" })],
style: { minWidth: 0, overflowWrap: "anywhere" },
} as unknown as DomphyElement,
],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(1),
minWidth: 0,
},
} as unknown as DomphyElement<"footer">;
return {
div: [content, footer],
_key: key,
$: [card({ color: "neutral" })],
// `color` is already declared by the card() patch above — the doctor
// tool inspects only this element's own inline `style`, not patch
// contributions, so it can't see that and flags a false positive here.
_doctorDisable: "missing-color",
style: {
minWidth: 0,
overflow: "hidden",
backgroundImage: (l: Listener) =>
// Upstream: from-primary/5 — upstream's primary is near-black, so the
// tint is a barely-visible NEUTRAL wash, not a blue one.
`linear-gradient(to top, ${themeColor(l, "shift-1", "neutral")}, transparent)`,
},
} as unknown as DomphyElement<"div">;
}
function metricCardGrid(cards: MetricCardData[]): DomphyElement<"section"> {
return {
section: cards.map((data, index) =>
metricCard(data, `${data.label}-${index}`),
),
style: {
display: "grid",
// minmax(12rem, 1fr) keeps KPI tiles readable; drop to fewer columns
// before numbers/labels clip mid-glyph in a sidebar + content shell.
gridTemplateColumns: "repeat(1, minmax(0, 1fr))",
gap: themeSpacing(4),
"@media (min-width: 48em)": {
gridTemplateColumns: "repeat(2, minmax(12rem, 1fr))",
},
"@media (min-width: 80em)": {
gridTemplateColumns: "repeat(4, minmax(12rem, 1fr))",
},
},
} as unknown as DomphyElement<"section">;
}
// ---------------------------------------------------------------------------
// Chart region — range-switchable, reusing chartBarStacked()
// ---------------------------------------------------------------------------
interface ChartRangePreset {
key: string;
label: string;
days: number;
}
const CHART_RANGE_PRESETS: ChartRangePreset[] = [
{ key: "90d", label: "Last 3 months", days: 90 },
{ key: "30d", label: "Last 30 days", days: 30 },
{ key: "7d", label: "Last 7 days", days: 7 },
];
function formatShortMonthDay(dateIso: string): string {
const date = new Date(`${dateIso}T00:00:00Z`);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
timeZone: "UTC",
});
}
function buildRangeSeries(
data: ChartBarDailyPoint[],
days: number,
): ChartBarTwoSeriesPoint[] {
return data.slice(-days).map((point) => ({
label: formatShortMonthDay(point.date),
desktop: point.desktop,
mobile: point.mobile,
}));
}
function computeChartTrend(series: ChartBarTwoSeriesPoint[]): {
direction: ChartTrendDirection;
text: string;
} {
if (series.length < 2) {
return { direction: "up", text: "Not enough data in this range yet" };
}
const first = series[0].desktop + series[0].mobile;
const last =
series[series.length - 1].desktop + series[series.length - 1].mobile;
const deltaPercent = first === 0 ? 0 : ((last - first) / first) * 100;
const direction: ChartTrendDirection = deltaPercent >= 0 ? "up" : "down";
return {
direction,
text: `Trending ${direction} by ${Math.abs(deltaPercent).toFixed(1)}% in this range`,
};
}
function chartRegion(data: ChartBarDailyPoint[]): DomphyElement<"div"> {
// Upstream chart-area-interactive uses useIsMobile() (a 768px breakpoint)
// plus an effect that forces the range to "7d" on small screens. The range
// control isn't otherwise reactive to viewport resize here, so mirror that
// with a one-shot matchMedia check at build time (same approach the drawer's
// placement uses below).
const isMobileViewport =
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia("(max-width: 767px)").matches;
const rangeKey = toState(
isMobileViewport ? "7d" : CHART_RANGE_PRESETS[0].key,
);
const controlRow: DomphyElement<"div"> = {
div: [
{
small: "Date range",
$: [small({ color: "neutral" })],
} as unknown as DomphyElement,
{
div: null,
style: { "@media (max-width: 48em)": { display: "none" } },
$: [
toggleGroup({
items: CHART_RANGE_PRESETS.map((preset) => ({
label: preset.label,
key: preset.key,
})),
value: rangeKey,
// Upstream active pill is neutral (bg-background on a muted
// track), not brand blue.
accentColor: "neutral",
}),
],
} as unknown as DomphyElement,
{
select: CHART_RANGE_PRESETS.map((preset) => ({
option: preset.label,
value: preset.key,
_key: preset.key,
})),
value: (l: Listener) => rangeKey.get(l),
ariaLabel: "Select date range",
onChange: (e: Event) =>
rangeKey.set((e.target as HTMLSelectElement).value),
style: {
display: "none",
"@media (max-width: 48em)": { display: "block" },
},
$: [select()],
} as unknown as DomphyElement,
],
style: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: themeSpacing(3),
flexWrap: "wrap",
},
} as unknown as DomphyElement<"div">;
const chartHost: DomphyElement<"div"> = {
div: (l: Listener) => {
const key = rangeKey.get(l);
const preset =
CHART_RANGE_PRESETS.find((candidate) => candidate.key === key) ??
CHART_RANGE_PRESETS[0];
const series = buildRangeSeries(data, preset.days);
const trend = computeChartTrend(series);
return [
chartBarStacked({
data: series,
title: "Visitor Trends",
subtitle: `Desktop & mobile sessions — ${preset.label.toLowerCase()}`,
trendText: trend.text,
trendDirection: trend.direction,
captionText: `Showing total visitors for the ${preset.label.toLowerCase()}`,
}),
];
},
} as unknown as DomphyElement<"div">;
return {
div: [controlRow, chartHost],
style: { display: "flex", flexDirection: "column", gap: themeSpacing(3) },
} as unknown as DomphyElement<"div">;
}
// ---------------------------------------------------------------------------
// Data table region
// ---------------------------------------------------------------------------
type DashboardTableStatus = "Done" | "In Progress" | "Not Started";
interface DashboardTableRow {
id: number;
header: string;
sectionType: string;
status: DashboardTableStatus;
target: number;
limit: number;
reviewer: string;
favorite?: boolean;
}
const REVIEWER_ROSTER = [
"Maria Chen",
"Jonas Weber",
"Priya Nair",
"Liam Turner",
"Sofia Ramos",
"Noah Becker",
"Ava Kim",
"Ethan Brooks",
];
// Section categories offered by the drawer's Type/Category select (upstream's
// TableCellViewer has an editable Type field). Any row whose current value
// falls outside this roster is unioned in at render time.
const CATEGORY_ROSTER = [
"Overview",
"Analysis",
"Finance",
"Design",
"Engineering",
];
const DEFAULT_TABLE_ROWS: DashboardTableRow[] = [
{
id: 1,
header: "Project Brief",
sectionType: "Overview",
status: "Done",
target: 24,
limit: 20,
reviewer: "Maria Chen",
},
{
id: 2,
header: "Market Research",
sectionType: "Analysis",
status: "In Progress",
target: 18,
limit: 15,
reviewer: "Jonas Weber",
},
{
id: 3,
header: "Budget Forecast",
sectionType: "Finance",
status: "Done",
target: 32,
limit: 30,
reviewer: "Priya Nair",
},
{
id: 4,
header: "Risk Assessment",
sectionType: "Analysis",
status: "Not Started",
target: 12,
limit: 10,
reviewer: "Liam Turner",
},
{
id: 5,
header: "Design Mockups",
sectionType: "Design",
status: "In Progress",
target: 20,
limit: 18,
reviewer: "Sofia Ramos",
},
{
id: 6,
header: "API Specification",
sectionType: "Engineering",
status: "Done",
target: 40,
limit: 40,
reviewer: "Noah Becker",
},
{
id: 7,
header: "Marketing Plan",
sectionType: "Overview",
status: "Not Started",
target: 15,
limit: 12,
reviewer: "Ava Kim",
},
{
id: 8,
header: "Compliance Review",
sectionType: "Finance",
status: "In Progress",
target: 22,
limit: 20,
reviewer: "Ethan Brooks",
},
];
// Editable status values, shared by the status cell and the drawer's status
// select. Upstream data-table.tsx distinguishes only "Done" (a green filled
// check) from everything-else (one static loader glyph, no spin); the badge
// chrome itself is a neutral outline for every status — there is no
// per-status border/text tint and no animation.
const TABLE_STATUSES: DashboardTableStatus[] = [
"Done",
"In Progress",
"Not Started",
];
function statusBadge(status: DashboardTableStatus): DomphyElement<"span"> {
const isDone = status === "Done";
return {
span: [
{
span: rawHtml(isDone ? ICON_CHECK_CIRCLE : ICON_SPINNER_ARC),
ariaHidden: "true",
style: {
display: "inline-flex",
width: themeSpacing(4),
height: themeSpacing(4),
},
$: [icon({ color: isDone ? "success" : "neutral" })],
} as unknown as DomphyElement,
{ span: status } as unknown as DomphyElement,
],
style: {
display: "inline-flex",
alignItems: "center",
gap: themeSpacing(1),
paddingInline: themeSpacing(2),
paddingBlock: themeSpacing(0.5),
borderRadius: themeSpacing(999),
border: (l: Listener) =>
`1px solid ${themeColor(l, "shift-4", "neutral")}`,
color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
},
} as unknown as DomphyElement<"span">;
}
function categoryBadge(sectionType: string): DomphyElement<"span"> {
return {
span: sectionType,
style: {
display: "inline-flex",
paddingInline: themeSpacing(2),
paddingBlock: themeSpacing(0.5),
borderRadius: themeSpacing(2),
border: (l: Listener) =>
`1px solid ${themeColor(l, "shift-4", "neutral")}`,
color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
},
} as unknown as DomphyElement<"span">;
}
/** Builds the full interactive table card: toolbar (status-filter tabs +
* column visibility + add-section), the table itself (checkbox select, drag
* reorder, inline-editable numeric cells, reviewer dropdown, actions menu),
* a pagination footer, and the row-editing drawer. */
function tableRegion(initialRows: DashboardTableRow[]): DomphyElement<"div"> {
const rows: DashboardTableRow[] = initialRows.map((row) => ({ ...row }));
const renderTick = toState(0);
const draggingId = toState<number | null>(null);
const activeStatusFilter = toState<string>("all");
const drawerOpen = toState(false);
const drawerRow = toState<DashboardTableRow | null>(null);
const isNarrowViewport =
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia("(max-width: 40em)").matches;
const columnHelper = createColumnHelper<DashboardTableRow>();
const columns = [
columnHelper.display({ id: "drag", header: "", enableHiding: false }),
columnHelper.display({
id: "select",
header: "Select",
enableHiding: false,
}),
columnHelper.accessor("header", { id: "header", header: "Title" }),
columnHelper.accessor("sectionType", {
id: "sectionType",
header: "Category",
}),
columnHelper.accessor("status", {
id: "status",
header: "Status",
filterFn: "equalsString",
}),
columnHelper.accessor("target", { id: "target", header: "Target" }),
columnHelper.accessor("limit", { id: "limit", header: "Limit" }),
columnHelper.accessor("reviewer", { id: "reviewer", header: "Reviewer" }),
columnHelper.display({ id: "actions", header: "", enableHiding: false }),
];
const domphyTable = createDomphyTable<DashboardTableRow>({
data: rows,
columns,
getRowId: (row) => String(row.id),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: { pagination: { pageIndex: 0, pageSize: 10 } },
});
function commitRows(): void {
domphyTable.table.setOptions((prev) => ({ ...prev, data: [...rows] }));
renderTick.set(renderTick.get() + 1);
}
function openDrawerForRow(row: DashboardTableRow): void {
drawerRow.set(row);
drawerOpen.set(true);
}
function reorderRows(sourceId: number | null, targetId: number): void {
if (sourceId === null || sourceId === targetId) return;
const sourceIndex = rows.findIndex((row) => row.id === sourceId);
const targetIndex = rows.findIndex((row) => row.id === targetId);
if (sourceIndex === -1 || targetIndex === -1) return;
const [moved] = rows.splice(sourceIndex, 1);
rows.splice(targetIndex, 0, moved);
commitRows();
}
function addSection(): void {
const nextId = rows.reduce((max, row) => Math.max(max, row.id), 0) + 1;
const newRow: DashboardTableRow = {
id: nextId,
header: "New section",
sectionType: "Overview",
status: "Not Started",
target: 0,
limit: 0,
reviewer: REVIEWER_ROSTER[0],
};
rows.push(newRow);
commitRows();
openDrawerForRow(newRow);
}
function duplicateRow(row: DashboardTableRow): void {
const nextId =
rows.reduce((max, current) => Math.max(max, current.id), 0) + 1;
const index = rows.findIndex((candidate) => candidate.id === row.id);
const copy: DashboardTableRow = {
...row,
id: nextId,
header: `${row.header} (copy)`,
};
rows.splice(index + 1, 0, copy);
commitRows();
}
function toggleFavorite(row: DashboardTableRow): void {
row.favorite = !row.favorite;
commitRows();
}
function deleteRow(row: DashboardTableRow): void {
const index = rows.findIndex((candidate) => candidate.id === row.id);
if (index === -1) return;
rows.splice(index, 1);
commitRows();
}
function rowActionsContent(row: DashboardTableRow): DomphyElement<"div"> {
return {
div: null,
style: { minWidth: themeSpacing(36) },
$: [
menu({
selectable: false,
items: [
{
label: "Edit",
key: "edit",
onClick: () => openDrawerForRow(row),
},
{
label: "Duplicate",
key: "duplicate",
onClick: () => duplicateRow(row),
},
{
label: row.favorite ? "Unfavorite" : "Favorite",
key: "favorite",
onClick: () => toggleFavorite(row),
},
{
label: {
span: "Delete",
style: {
color: (l: Listener) => themeColor(l, "shift-9", "danger"),
},
} as unknown as DomphyElement,
key: "delete",
onClick: () => deleteRow(row),
},
],
}),
],
} as unknown as DomphyElement<"div">;
}
function headerCellFor(
column: Column<DashboardTableRow, unknown>,
): DomphyElement<"th"> {
if (column.id === "select") {
const selectAllId = "dashboard01-select-all";
return {
th: [
{
label: "Select all rows",
for: selectAllId,
style: SR_ONLY_STYLE,
} as unknown as DomphyElement,
{
input: null,
id: selectAllId,
type: "checkbox",
ariaLabel: "Select all rows",
checked: domphyTable.table.getIsAllPageRowsSelected(),
onChange: () => domphyTable.table.toggleAllPageRowsSelected(),
// `indeterminate` is an imperative DOM prop, not an attribute —
// re-apply it on EVERY generation via behavior() update(). The
// previous `_onMount` ran once, so the header checkbox never
// showed the partial-selection dash after the first re-render.
...behavior<{ indeterminate: boolean }>(
"dashboard-select-all-indeterminate",
(node, initialProps) => {
const input = node.domElement as HTMLInputElement | null;
if (!input) return undefined;
input.indeterminate = initialProps.indeterminate;
return {
update: (next) => {
input.indeterminate = next.indeterminate;
},
};
},
{
indeterminate:
domphyTable.table.getIsSomePageRowsSelected() &&
!domphyTable.table.getIsAllPageRowsSelected(),
},
),
_doctorDisable: "missing-color",
$: [inputCheckbox({ accentColor: "neutral" })],
} as unknown as DomphyElement,
],
_key: "select",
} as unknown as DomphyElement<"th">;
}
if (column.id === "drag") {
return {
th: [
{
span: "Reorder",
style: {
position: "absolute",
width: "1px",
height: "1px",
overflow: "hidden",
},
},
],
_key: "drag",
} as unknown as DomphyElement<"th">;
}
return {
th: String(column.columnDef.header ?? column.id),
_key: column.id,
} as unknown as DomphyElement<"th">;
}
function bodyCellFor(
column: Column<DashboardTableRow, unknown>,
row: Row<DashboardTableRow>,
): DomphyElement<"td"> {
const original = row.original;
switch (column.id) {
case "select": {
const rowSelectId = `dashboard01-select-${original.id}`;
const rowSelectLabel = `Select ${original.header}`;
return {
td: [
{
label: rowSelectLabel,
for: rowSelectId,
style: SR_ONLY_STYLE,
} as unknown as DomphyElement,
{
input: null,
id: rowSelectId,
type: "checkbox",
ariaLabel: rowSelectLabel,
checked: row.getIsSelected(),
onChange: () => row.toggleSelected(),
_doctorDisable: "missing-color",
$: [inputCheckbox({ accentColor: "neutral" })],
} as unknown as DomphyElement,
],
_key: "select",
} as unknown as DomphyElement<"td">;
}
case "drag":
return {
td: [
{
span: rawHtml(ICON_GRIP),
ariaHidden: "true",
$: [icon({ color: "neutral" })],
} as unknown as DomphyElement,
],
_key: "drag",
draggable: true,
onDragStart: () => draggingId.set(original.id),
onDragEnd: () => draggingId.set(null),
style: { cursor: "grab", width: themeSpacing(6) },
} as unknown as DomphyElement<"td">;
case "header":
return {
td: [
{
button: [
...(original.favorite
? [
{
span: rawHtml(ICON_STAR),
ariaHidden: "true",
$: [icon({ color: "warning" })],
} as unknown as DomphyElement,
]
: []),
{ span: original.header } as unknown as DomphyElement,
],
type: "button",
onClick: () => openDrawerForRow(original),
style: { paddingInline: 0 },
$: [buttonGhost({ color: "neutral" })],
} as unknown as DomphyElement,
],
_key: "header",
} as unknown as DomphyElement<"td">;
case "sectionType":
return {
td: [categoryBadge(original.sectionType)],
_key: "sectionType",
} as unknown as DomphyElement<"td">;
case "status":
return {
td: [statusBadge(original.status)],
_key: "status",
} as unknown as DomphyElement<"td">;
case "target":
case "limit": {
const numericFieldId = `dashboard01-${column.id}-${original.id}`;
const numericFieldLabel = `${column.id === "target" ? "Target" : "Limit"} for ${original.header}`;
return {
td: [
{
label: numericFieldLabel,
for: numericFieldId,
style: SR_ONLY_STYLE,
} as unknown as DomphyElement,
{
input: null,
id: numericFieldId,
type: "number",
value: original[column.id as "target" | "limit"],
ariaLabel: numericFieldLabel,
onChange: (e: Event) => {
original[column.id as "target" | "limit"] =
Number((e.target as HTMLInputElement).value) || 0;
commitRows();
},
style: { width: themeSpacing(20) },
$: [inputNumber({ color: "neutral" })],
} as unknown as DomphyElement,
],
_key: column.id,
style: { textAlign: "right" },
} as unknown as DomphyElement<"td">;
}
case "reviewer":
return {
td: [
{
select: REVIEWER_ROSTER.map((name) => ({
option: name,
value: name,
_key: name,
})),
value: original.reviewer,
ariaLabel: `Reviewer for ${original.header}`,
onChange: (e: Event) => {
original.reviewer = (e.target as HTMLSelectElement).value;
commitRows();
},
$: [select({ color: "neutral" })],
} as unknown as DomphyElement,
],
_key: "reviewer",
} as unknown as DomphyElement<"td">;
case "actions":
return {
td: [
{
button: [
{
span: rawHtml(ICON_MORE),
ariaHidden: "true",
$: [icon({ color: "neutral" })],
} as unknown as DomphyElement,
],
type: "button",
ariaLabel: `${original.header} actions`,
$: [
buttonGhost({ color: "neutral" }),
popover({
placement: "left-start",
content: rowActionsContent(original),
}),
],
} as unknown as DomphyElement,
],
_key: "actions",
style: { textAlign: "right" },
} as unknown as DomphyElement<"td">;
default:
return { td: null, _key: column.id } as unknown as DomphyElement<"td">;
}
}
function buildRow(row: Row<DashboardTableRow>): DomphyElement<"tr"> {
const original = row.original;
return {
tr: row.getVisibleCells().map((cell) => bodyCellFor(cell.column, row)),
_key: original.id,
onDragOver: (e: Event) => e.preventDefault(),
onDrop: (e: Event) => {
e.preventDefault();
reorderRows(draggingId.get(), original.id);
},
style: {
opacity: (l: Listener) =>
draggingId.get(l) === original.id ? "0.5" : "1",
boxShadow: (l: Listener) =>
draggingId.get(l) === original.id
? `0 ${themeSpacing(2)} ${themeSpacing(4)} ${themeColor(l, "shift-4", "neutral")}`
: "none",
},
} as unknown as DomphyElement<"tr">;
}
function emptyStateRow(columnCount: number): DomphyElement<"tr"> {
return {
tr: [
{
td: [
{
div: [
{
span: rawHtml(ICON_CIRCLE_DASHED),
ariaHidden: "true",
$: [icon({ color: "neutral" })],
} as unknown as DomphyElement,
{
p: "No rows in this view",
$: [paragraph({ color: "neutral" })],
} as unknown as DomphyElement,
{
small: "Try a different tab or add a new section.",
$: [small({ color: "neutral" })],
} as unknown as DomphyElement,
],
$: [empty({ color: "neutral" })],
} as unknown as DomphyElement,
],
colSpan: columnCount,
_key: "empty",
} as unknown as DomphyElement,
],
_key: "empty",
} as unknown as DomphyElement<"tr">;
}
const tableElement: DomphyElement<"table"> = {
table: (l: Listener) => {
domphyTable.version(l);
renderTick.get(l);
const visibleColumns = domphyTable.getVisibleLeafColumns();
const rowsModel = domphyTable.getRowModel().rows;
return [
{
thead: [
{
tr: visibleColumns.map((column) => headerCellFor(column)),
_key: "header-row",
},
],
_key: "thead",
} as unknown as DomphyElement,
{
tbody:
rowsModel.length > 0
? rowsModel.map((row) => buildRow(row))
: [emptyStateRow(visibleColumns.length)],
_key: "tbody",
} as unknown as DomphyElement,
];
},
$: [tableUI({ color: "neutral" })],
} as unknown as DomphyElement<"table">;
const tableScroll: DomphyElement<"div"> = {
div: [tableElement],
style: { overflowX: "auto" },
} as unknown as DomphyElement<"div">;
// ── Toolbar: status-filter tabs/select + spacer + column visibility + add ──
const statusViews: { key: string; label: string }[] = [
{ key: "all", label: "All Items" },
{ key: "Done", label: "Done" },
{ key: "In Progress", label: "In Progress" },
{ key: "Not Started", label: "Not Started" },
];
const viewSelector: DomphyElement<"div"> = {
div: [
{
div: null,
style: { "@media (max-width: 40em)": { display: "none" } },
$: [
tabs({
items: statusViews.map((view) => ({
label: view.label,
key: view.key,
content: { div: null, style: { display: "none" } },
})),
activeKey: activeStatusFilter,
// Upstream active tab indicator is neutral foreground, not blue.
accentColor: "neutral",
}),
],
} as unknown as DomphyElement,
{
select: statusViews.map((view) => ({
option: view.label,
value: view.key,
_key: view.key,
})),
value: (l: Listener) => activeStatusFilter.get(l),
ariaLabel: "Select view",
onChange: (e: Event) =>
activeStatusFilter.set((e.target as HTMLSelectElement).value),
style: {
display: "none",
"@media (max-width: 40em)": { display: "block" },
},
$: [select()],
} as unknown as DomphyElement,
],
style: { flexShrink: "0" },
} as unknown as DomphyElement<"div">;
const customizeColumnsContent: DomphyElement<"div"> = {
div: (l: Listener) => {
domphyTable.version(l);
return domphyTable
.getAllLeafColumns()
.filter((column) => column.getCanHide())
.map((column) => ({
label: [
{
input: null,
type: "checkbox",
checked: column.getIsVisible(),
onChange: () => column.toggleVisibility(),
_doctorDisable: "missing-color",
$: [inputCheckbox({ color: "neutral" })],
} as unknown as DomphyElement,
{
span: String(column.columnDef.header ?? column.id),
} as unknown as DomphyElement,
],
_key: column.id,
style: {
display: "flex",
alignItems: "center",
gap: themeSpacing(2),
paddingBlock: themeSpacing(1),
paddingInline: themeSpacing(2),
cursor: "pointer",
},
}));
},
style: { minWidth: themeSpacing(48) },
} as unknown as DomphyElement<"div">;
const toolbar: DomphyElement<"div"> = {
div: [
viewSelector,
{ div: null, style: { flex: "1" } } as unknown as DomphyElement,
{
button: [{ span: "Customize Columns" }, sidebarIcon(ICON_CHEVRON_DOWN)],
type: "button",
$: [
buttonGhost({ color: "neutral" }),
popover({
placement: "bottom-end",
content: customizeColumnsContent,
}),
],
} as unknown as DomphyElement,
{
button: [sidebarIcon(ICON_PLUS), { span: "Add Section" }],
type: "button",
onClick: () => addSection(),
// Upstream renders this as an outline (not solid blue) button.
$: [button({ color: "neutral" })],
} as unknown as DomphyElement,
],
style: {
display: "flex",
alignItems: "center",
gap: themeSpacing(3),
flexWrap: "wrap",
},
// activeStatusFilter → table column-filter bridge via behavior(): the
// subscription is re-pointed at the CURRENT generation's state + table on
// every re-render. The previous `_onMount` subscribed generation 1's
// state only, so the status select silently stopped filtering after any
// ancestor re-render.
...behavior<{
filterState: State<string>;
table: typeof domphyTable.table;
}>(
"dashboard-status-filter-bridge",
(_node, initialProps) => {
let filterState = initialProps.filterState;
let table = initialProps.table;
const applyFilter = (status: string) =>
table
.getColumn("status")
?.setFilterValue(status === "all" ? undefined : status);
let release = filterState.addListener(applyFilter);
return {
update: (next) => {
if (next.filterState === filterState && next.table === table) {
return;
}
release();
filterState = next.filterState;
table = next.table;
applyFilter(filterState.get());
release = filterState.addListener(applyFilter);
},
destroy: () => release(),
};
},
{ filterState: activeStatusFilter, table: domphyTable.table },
),
} as unknown as DomphyElement<"div">;
// ── Pagination footer ──
function rowsPerPageControl(): DomphyElement<"div"> {
return {
div: [
{
small: "Rows per page",
$: [small({ color: "neutral" })],
} as unknown as DomphyElement,
{
select: [10, 20, 30, 40, 50].map((size) => ({
option: String(size),
value: String(size),
_key: size,
})),
value: String(domphyTable.table.getState().pagination.pageSize),
ariaLabel: "Rows per page",
onChange: (e: Event) =>
domphyTable.table.setPageSize(
Number((e.target as HTMLSelectElement).value),
),
$: [select()],
} as unknown as DomphyElement,
],
_key: "pageSize",
style: { display: "flex", alignItems: "center", gap: themeSpacing(2) },
} as unknown as DomphyElement<"div">;
}
function pageNavButtons(): DomphyElement<"div"> {
const canPrevious = domphyTable.getCanPreviousPage();
const canNext = domphyTable.getCanNextPage();
const navButton = (
label: string,
iconElement: DomphyElement<"span">,
onClick: () => void,
disabled: boolean,
key: string,
): DomphyElement<"button"> =>
({
button: [iconElement],
type: "button",
ariaLabel: label,
disabled,
onClick,
_key: key,
$: [buttonGhost({ color: "neutral" })],
}) as unknown as DomphyElement<"button">;
return {
div: [
navButton(
"First page",
flippedIcon(ICON_CHEVRON_DOUBLE_RIGHT),
() => domphyTable.table.firstPage(),
!canPrevious,
"first",
),
navButton(
"Previous page",
flippedIcon(ICON_CHEVRON_RIGHT),
() => domphyTable.table.previousPage(),
!canPrevious,
"prev",
),
navButton(
"Next page",
sidebarIcon(ICON_CHEVRON_RIGHT),
() => domphyTable.table.nextPage(),
!canNext,
"next",
),
navButton(
"Last page",
sidebarIcon(ICON_CHEVRON_DOUBLE_RIGHT),
() => domphyTable.table.lastPage(),
!canNext,
"last",
),
],
_key: "nav",
style: { display: "flex", alignItems: "center", gap: themeSpacing(1) },
} as unknown as DomphyElement<"div">;
}
const paginationFooter: DomphyElement<"footer"> = {
footer: (l: Listener) => {
domphyTable.version(l);
const pageIndex = domphyTable.table.getState().pagination.pageIndex;
const pageCount = Math.max(1, domphyTable.getPageCount());
const selectedCount = domphyTable.getSelectedRowModel().rows.length;
const totalCount = domphyTable.table.getFilteredRowModel().rows.length;
return [
{
small: `${selectedCount} of ${totalCount} row(s) selected.`,
_key: "selection",
$: [small({ color: "neutral" })],
} as unknown as DomphyElement,
{
div: null,
_key: "spacer",
style: { flex: "1" },
} as unknown as DomphyElement,
rowsPerPageControl(),
{
small: `Page ${pageIndex + 1} of ${pageCount}`,
_key: "pageIndicator",
$: [small({ color: "neutral" })],
} as unknown as DomphyElement,
pageNavButtons(),
];
},
style: {
display: "flex",
alignItems: "center",
gap: themeSpacing(4),
flexWrap: "wrap",
paddingBlockStart: themeSpacing(4),
borderTop: (l: Listener) =>
`1px solid ${themeColor(l, "shift-3", "neutral")}`,
color: (l: Listener) => themeColor(l, "shift-10", "neutral"),
},
} as unknown as DomphyElement<"footer">;
// ── Row-editing drawer ──
function drawerBarRow(
label: string,
value: number,
max: number,
fillTone: `shift-${number}`,
key: string,
): DomphyElement<"div"> {
const percent = Math.round((value / max) * 100);
return {
div: [
{
small: `${label} — ${value}`,
$: [small({ color: "neutral" })],
} as unknown as DomphyElement,
{
div: [
{
div: null,
style: {
width: `${percent}%`,
height: "100%",
borderRadius: themeSpacing(2),
backgroundColor: (l: Listener) =>
themeColor(l, fillTone, "neutral"),
},
} as unknown as DomphyElement,
],
style: {
width: "100%",
height: themeSpacing(3),
borderRadius: themeSpacing(2),
overflow: "hidden",
backgroundColor: (l: Listener) =>
themeColor(l, "shift-2", "neutral"),
},
} as unknown as DomphyElement,
],
_key: key,
style: { display: "flex", flexDirection: "column", gap: themeSpacing(1) },
} as unknown as DomphyElement<"div">;
}
function drawerChart(row: DashboardTableRow): DomphyElement<"div"> {
const max = Math.max(row.target, row.limit, 1);
return {
div: [
// Upstream's mini chart tints both series off the single near-black
// primary at different opacities — two neutral ramp shades here.
drawerBarRow("Target", row.target, max, "shift-9", "target-bar"),
drawerBarRow("Limit", row.limit, max, "shift-5", "limit-bar"),
],
_key: "chart",
style: { display: "flex", flexDirection: "column", gap: themeSpacing(3) },
} as unknown as DomphyElement<"div">;
}
function drawerHeader(row: DashboardTableRow): DomphyElement<"header"> {
return {
header: [
{ h3: row.header, $: [heading()] } as unknown as DomphyElement,
{
p: `${row.sectionType} · ${row.status}`,
$: [paragraph({ color: "neutral" })],
} as unknown as DomphyElement,
],
_key: "header",
style: { display: "flex", flexDirection: "column", gap: themeSpacing(1) },
} as unknown as DomphyElement<"header">;
}
function drawerForm(row: DashboardTableRow): DomphyElement<"fieldset"> {
const fieldId = (name: string) => `dashboard01-drawer-${name}-${row.id}`;
return {
fieldset: [
{ legend: "Section details" },
{ label: "Title", for: fieldId("title"), $: [label()] },
{
input: null,
id: fieldId("title"),
value: row.header,
onChange: (e: Event) => {
row.header = (e.target as HTMLInputElement).value;
commitRows();
},
$: [inputText()],
} as unknown as DomphyElement,
{ label: "Category", for: fieldId("category"), $: [label()] },
{
select: Array.from(
new Set([...CATEGORY_ROSTER, row.sectionType]),
).map((name) => ({
option: name,
value: name,
_key: name,
})),
id: fieldId("category"),
value: row.sectionType,
onChange: (e: Event) => {
row.sectionType = (e.target as HTMLSelectElement).value;
commitRows();
},
$: [select()],
} as unknown as DomphyElement,
{ label: "Reviewer", for: fieldId("reviewer"), $: [label()] },
{
select: REVIEWER_ROSTER.map((name) => ({
option: name,
value: name,
_key: name,
})),
id: fieldId("reviewer"),
value: row.reviewer,
onChange: (e: Event) => {
row.reviewer = (e.target as HTMLSelectElement).value;
commitRows();
},
$: [select()],
} as unknown as DomphyElement,
{ label: "Status", for: fieldId("status"), $: [label()] },
{
select: TABLE_STATUSES.map((status) => ({
option: status,
value: status,
_key: status,
})),
id: fieldId("status"),
value: row.status,
onChange: (e: Event) => {
row.status = (e.target as HTMLSelectElement)
.value as DashboardTableStatus;
commitRows();
},
$: [select()],
} as unknown as DomphyElement,
{ label: "Target", for: fieldId("target"), $: [label()] },
{
input: null,
type: "number",
id: fieldId("target"),
value: row.target,
onChange: (e: Event) => {
row.target = Number((e.target as HTMLInputElement).value) || 0;
commitRows();
},
$: [inputNumber()],
} as unknown as DomphyElement,
{ label: "Limit", for: fieldId("limit"), $: [label()] },
{
input: null,
type: "number",
id: fieldId("limit"),
value: row.limit,
onChange: (e: Event) => {
row.limit = Number((e.target as HTMLInputElement).value) || 0;
commitRows();
},
$: [inputNumber()],
} as unknown as DomphyElement,
],
_key: "form",
$: [formGroup({ layout: "vertical" })],
} as unknown as DomphyElement<"fieldset">;
}
function drawerFooter(): DomphyElement<"footer"> {
// Upstream DrawerFooter renders a primary `Submit` plus an outline `Done`
// (DrawerClose). Field edits already commit live on each change, so Submit
// just flushes + closes; Done closes without further action.
return {
footer: [
{
button: "Submit",
type: "button",
onClick: () => {
commitRows();
drawerOpen.set(false);
},
_key: "submit",
// Upstream default <Button> is near-black (primary), not brand blue.
$: [button({ color: "neutral", variant: "solid" })],
} as unknown as DomphyElement,
{
button: "Done",
type: "button",
onClick: () => drawerOpen.set(false),
_key: "done",
$: [button({ color: "neutral" })],
} as unknown as DomphyElement,
],
_key: "footer",
style: {
display: "flex",
justifyContent: "flex-end",
gap: themeSpacing(2),
marginBlockStart: themeSpacing(4),
},
} as unknown as DomphyElement<"footer">;
}
const rowDrawer: DomphyElement<"dialog"> = {
dialog: (l: Listener) => {
const row = drawerRow.get(l);
if (!row) return [];
return [
drawerHeader(row),
drawerChart(row),
drawerForm(row),
drawerFooter(),
];
},
$: [
drawer({
open: drawerOpen,
placement: isNarrowViewport ? "bottom" : "end",
}),
],
} as unknown as DomphyElement<"dialog">;
return {
div: [toolbar, tableScroll, paginationFooter, rowDrawer],
style: {
display: "flex",
flexDirection: "column",
gap: themeSpacing(4),
borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 2),
border: (l: Listener) =>
`1px solid ${themeColor(l, "shift-4", "neutral")}`,
backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
color: (l: Listener) => themeColor(l, "shift-10", "neutral"),
padding: (l: Listener) => themeSpacing(themeDensity(l) * 4),
},
} as unknown as DomphyElement<"div">;
}
// ---------------------------------------------------------------------------
// Content-pane utility header row (external link ghost button)
// ---------------------------------------------------------------------------
function dashboardUtilityRow(): DomphyElement<"div"> {
return {
div: [
{
button: [{ span: "GitHub" }, sidebarIcon(ICON_EXTERNAL_LINK)],
type: "button",
ariaLabel: "Open project source",
$: [buttonGhost({ color: "neutral" })],
// `buttonGhost()`'s own resting-state shift-6 (deliberately subtle
// until hovered) measured too little contrast against the page
// background here — bumped directly on this call site rather than
// touching the shared patch's own default.
style: { color: (l: Listener) => themeColor(l, "shift-9", "neutral") },
} as unknown as DomphyElement,
],
style: {
display: "flex",
justifyContent: "flex-end",
"@media (max-width: 48em)": { display: "none" },
},
} as unknown as DomphyElement<"div">;
}
// ---------------------------------------------------------------------------
// Public factory
// ---------------------------------------------------------------------------
interface Dashboard01Props {
pageTitle?: string;
user?: Sidebar07User;
navMain?: Sidebar07NavMainItem[];
documents?: Sidebar07Project[];
metricCards?: MetricCardData[];
chartData?: ChartBarDailyPoint[];
tableRows?: DashboardTableRow[];
}
/**
* shadcn/ui "dashboard-01" — a full admin dashboard shell: the icon-nav
* sidebar (reused from `sidebar07()`), a slim content header, four KPI
* cards, a range-switchable chart (reused from `chartBarStacked()`), and a
* drag-reorderable data table with its own toolbar and row-editing drawer.
* Call with no arguments for a fully working demo.
*/
function dashboard01(props: Dashboard01Props = {}): DomphyElement<"div"> {
const {
pageTitle = "Documents",
user = DEFAULT_USER,
navMain = DEFAULT_NAV_MAIN,
documents = DEFAULT_DOCUMENTS,
metricCards = DEFAULT_METRIC_CARDS,
chartData = generateChartBarDailyData(90, CHART_BAR_DAILY_END_DATE),
tableRows = DEFAULT_TABLE_ROWS,
} = props;
const content: DomphyElement[] = [
dashboardUtilityRow(),
metricCardGrid(metricCards),
chartRegion(chartData),
tableRegion(tableRows),
];
return sidebar07({
teams: [{ name: "Acme Inc.", plan: "Workspace" }],
navMain,
projects: documents,
user,
breadcrumbItems: [{ label: pageTitle }],
children: content,
});
}
export { dashboard01 };
export type {
Dashboard01Props,
DashboardTableRow,
DashboardTableStatus,
MetricCardData,
};