Skip to main content

Customizable Owner Dashboard — Design Spec

Date: 2026-05-31 Goal: Make the owner dashboard personalizable (rearrange, hide, add, resize “to an extent”) and visually revamped (prettier, less crowded), built entirely from deps/patterns already in the repo — no new heavy libraries, no DB writes, no migration, daily load stays fast, and it cannot get into a broken state.

Decisions (locked)

  • Engine: @dnd-kit/core + @dnd-kit/sortable (already installed; used by the appointment calendar). NOT react-grid-layout.
  • Resize = column-span presets S / M / L on the existing 12-col Tailwind grid (the dashboard already uses lg:col-span-5/4/3). No free-pixel resize.
  • Persistence = per-user localStorage (same pattern as ThemeCustomizerContext), keyed by user+clinic. Zero DB writes (deliberate — keeps the Neon bill down). Cross-device sync is explicitly out of scope for v1.
  • Scope: owner/admin dashboard (AdminOverview) only. Other roles unchanged.
  • Polish via design tokens + framer-motion (already installed). No new visual language.

Architecture

  • Widget registry (widgetRegistry.tsx): each existing dental card is registered with { id, title, render, defaultSpan, minSpan, maxSpan, removable, category }. Single source of truth for “what widgets exist.”
  • Layout store (useDashboardLayout.ts): a versioned, validated array [{ id, span, hidden }] in localStorage. On load it reconciles against the registry: unknown ids dropped, newly-shipped widgets appended to an “available” state, malformed data → fall back to DEFAULT_LAYOUT. Always exposes resetToDefault(). This is what makes it unbreakable.
  • View mode (DashboardGrid.tsx): renders visible widgets in order into a static CSS 12-col grid using each widget’s spanlg:col-span-{n}. No dnd-kit in this path — the daily view stays the fast static grid it is today.
  • Edit mode (DashboardCustomizer.tsx, lazy-loaded): wraps the grid in dnd-kit SortableContext; each widget gets a drag handle, a hide toggle, and an S/M/L size control. A side “Add widgets” tray lists hidden/available widgets. A “Reset to default” button. framer-motion layout animation for smooth reflow. The dnd-kit code only enters the bundle when the user clicks Customize, so initial load is unaffected.
  • WidgetFrame.tsx: in view mode renders the widget plain; in edit mode adds the drag handle / hide / size chrome. Keeps widgets themselves untouched.
  • AdminOverview.tsx: holds the Customize toggle and renders DashboardGrid (view) or the lazy DashboardCustomizer (edit). Owns the single useQuery and passes data slices to widgets via the registry.

Visual revamp (concrete, token-level — not open-ended)

  • More breathing room: bump section gaps; group KPI row, then a quiet divider, then content rows.
  • Softer card surface: bg-card with a faint top highlight + shadow-sm (no heavy gradients), rounded-xl.
  • Stronger number typography: KPI values larger/tighter; labels smaller, muted, uppercase tracking.
  • Calmer hierarchy: KPIs are the visual anchor; secondary stats stay as the quiet chip band; Action Required keeps its accent.
  • Subtle framer-motion fade/slide-in on first paint (respects prefers-reduced-motion), no looping animations.

Unbreakable guarantees

  • Layout is pure data; DEFAULT_LAYOUT is always the fallback.
  • Schema version field; on mismatch → reset (don’t crash).
  • Registry is the authority: a widget removed from code simply won’t render; a widget added in code appears in the tray.
  • resetToDefault() always available in edit mode.
  • View mode never imports dnd-kit (perf + can’t break the daily view).

Performance

  • Daily view: static CSS grid, zero extra JS vs today.
  • dnd-kit + customizer: React.lazy behind the Customize toggle.
  • Persistence: localStorage read once on mount; write on layout change (debounced). No network, no DB.

Non-goals (v1)

  • No cross-device server sync, no per-role layout presets, no free-pixel resize, no widgets beyond the existing dental set, no changes to other dashboards.

Verification

  • ui tsc 0 errors; vite build succeeds; dnd-kit confirmed only in a lazy chunk (not the entry/dashboard chunk).
  • Unit test the layout reconcile/validate logic (pure function): unknown id dropped, new widget appended, malformed → default, version bump → reset.
  • Manual: toggle Customize → drag reorder, hide/add, S/M/L resize, reset; reload persists; corrupt the localStorage key → falls back to default without crashing.