refactor(web): rebuild dashboard UI on shadcn-svelte primitives#97
Problem
The dashboard had no component layer: buttons, cards, checkboxes, and four independent segmented-control implementations were hand-rolled inline and had drifted apart. Dark mode was 279 per-callsite dark: variants. The three breakdown chart cards (IPChart/ProtocolChart/SpectrumStatsChart, ~2,800 lines) were near-clones sharing 19 identically named functions. A few Svelte 4 idioms survived (createEventDispatcher, a writable store), and chart drag handlers wrote $state on every raw mousemove.
Solution
Five commits, each independently green:
- shadcn-svelte foundation — 9 vendored primitives in
src/lib/components/ui/, semantic theme tokens (bg-card,text-muted-foreground, …) mapped onto the existing palette (dark surfaces#0a0a0a/#111111/#1a1a1a/#2d2d2d, primary blue-600). Dark mode is now defined once in CSS variables. - Call-site sweep — primitives + tokens across routes, filters, alerts, and netflow components; one shared typed
SegmentedControl(with tooltip-on-disabled) replaces all four hand-rolled variants; zerodark:variants remain in app code. - Chart consolidation — shared
ChartCardshell (header/controls/resizable well/loading-error-empty states/drag overlays) plus one parameterizedBreakdownChart+ per-kind config replaces the trio.createEventDispatcherretired app-wide for callback props,rangeSelectionmoved to a runes class, drag interactions rAF-batched. Chart.js configs preserved verbatim — a future chart-library swap now touches one file. - Cleanup — legacy
--color-dark-*tokens removed. - Fix — app-wide
Tooltip.Providerin the root layout (bits-ui tooltips throw during SSR without one; found in browser verification, invisible to the home-only e2e test).
Net: authored frontend code shrinks ~2,900 lines; ~1,300 lines of vendored primitives added.
Review fast path
Flows to exercise: home → dataset dashboard → toggle metric/granularity/chart-type controls, drag-select a range on a chart, drill down, reorder chart cards, alerts page (tabs + table), theme toggle, netflow files pages.
Setup: a local dataset DB on the current schema (bucket_coverage table). Older local v2/v3 databases render the chart cards' error states instead — that's pre-existing schema drift, not this PR.
Expected: visually equivalent to before — same layout, density, and palette in both themes. No data-contract or API changes.
Decisions worth knowing:
- Native
<select>and<input type="date">kept (restyled with tokens) rather than adopting bits-ui Select — simpler and keeps native behavior. - bits-ui checkboxes render
button[role=checkbox]instead of native inputs; aria labels and visible text preserved throughout. - The four chart-state branches (loading / error / no-metrics / empty) are centralized in
ChartCardwith parameterized copy.
Verification
bun run format/lint/typecheckclean (svelte-check: 0 errors, 0 warnings)bun run test:web: 90/90 passingbun run test:e2e: passing- Production build (
vite build, Cloudflare adapter) succeeds - Manually verified home + dashboard in a real browser against live data (light mode)
Remaining manual check: a dark-mode visual pass (tokens make it a single-source change, but eyes on it are cheap insurance).
Directed by Claude Fable 5 (t3 code harness); implementation by gpt-5.6-sol agents via codex exec.
Checks
Added 7 commits
- refactor(web): adopt shadcn primitives and semantic tokens across dashboard4e2c98f
- feat(web): add shadcn-svelte foundation with semantic theme tokensa5918f4
- refactor(web): consolidate chart trio into shared ChartCard and BreakdownCharta8d043c
- fix(web): add app-wide tooltip provider required by bits-ui tooltipsbd85d0c
- chore(web): drop unused legacy dark palette tokensc1e0cdd
- fix(web): port coverage clamping into consolidated BreakdownChartf9400dd
- fix(web): convert coverage strip card shell to semantic theme tokens85aba36