20 KiB
Frontend Redesign Plan — Aligning with docs/design Mockups
This plan describes how to evolve packages/web from the current minimal Tailwind UI to match the editorial, warm-neutral design defined in docs/design/MeshiTrack.html and the screenshots under docs/design/screenshots/.
The mockups are a React (UMD + Babel) prototype using a CSS-token system. We will port the visual language and information architecture into the existing Next.js App Router + Tailwind v4 project, without dragging the prototype's runtime (React UMD, global window.* modules, inline data) into production.
1. Goals & Non-Goals
Goals
- Match the visual language of the mockups: typography (Fraunces / Inter Tight / JetBrains Mono), warm-neutral palette, sage brand, generous whitespace, soft shadows, rounded cards.
- Reproduce the app shell: persistent left sidebar with grouped navigation + sticky topbar with title/subtitle, search, theme toggle, notifications.
- Reproduce the page archetypes shown in the mockups: editorial "hero" header, multi-card grid with consistent
mt-cardstyle, status pills, supply bars, mini SVG charts. - Support light/dark themes and an accent switcher (sage / cobalt / terracotta / graphite) via CSS custom properties.
- Keep accessibility, responsive layout, and SSR-friendliness intact.
Non-Goals
- Do not import the prototype's
data.jsxmock data into production. - Do not adopt the prototype's
window.*global module pattern or Babel-in-browser runtime. - Do not redesign data flows or API contracts; only the presentation layer changes.
- Do not block on building every page in the mockup; food-domain pages (Phases 5-9) will reuse the system once the medicine pages are migrated.
2. Gap Analysis
| Area | Current state (packages/web) |
Target (mockups) | |
|---|---|---|---|
| Theme tokens | Tailwind v4 @theme with a green ramp only |
Full token set: warm neutrals, brand sage, status, viz palette, radii, shadows, fonts; light + dark | |
| Typography | System sans only | Fraunces (display), Inter Tight (UI), JetBrains Mono (numerics/keys), tabular-nums utility | |
| Sidebar | Flat list, 3 links, no groups, no badges | Grouped (Medicines section), icons, active state, badges, footer with avatar/role |
|
| TopBar | Household chip + avatar | Breadcrumbs, title + subtitle, search with ⌘K, notifications, theme toggle |
|
| Cards | Ad-hoc Tailwind cards | Reusable Card, CardHeader, Pill, Button (primary/ghost), supply bar, ring chart, sparkbar |
|
| Dashboard | 2 link cards | Hero greeting + ring, Today's schedule, Running low, Spending bars, Days-of-supply, Pending orders, Recent activity | |
| Cabinet / Schedule / Regimens / Organizer / Library / Refills / Purchases / Prices / Stores / Activity | Mostly placeholder grids/tables | Editorial layouts with status chips, swatches, supply bars (see screenshots) | |
| Theming | None | `data-theme="light | dark"on<html>`, accent CSS vars |
| Icons | None defined centrally | Inline SVG icon set (Icon.jsx in mockup) |
3. Target Architecture
3.1 Design tokens (Tailwind v4 @theme + CSS vars)
- Replace the current
@themeblock in packages/web/src/styles/globals.css with the full token set from docs/design/styles/tokens.css:- Color tokens exposed both as CSS vars (
--bg,--ink,--brand, ...) and as Tailwind theme colors (--color-bg,--color-ink, ...) so utilities likebg-bg,text-ink,border-borderwork. - Radii (
--r-xs..--r-xl) and shadows (--shadow-sm/md/lg). - Font family vars + load Fraunces / Inter Tight / JetBrains Mono via
next/font/googlein packages/web/src/app/layout.tsx and bind them to the--font-*vars.
- Color tokens exposed both as CSS vars (
- Add
[data-theme='dark']overrides inglobals.css(copy from tokens.css). - Add small global utility classes used by the mockup:
.mono,.serif,.num(tabular-nums) — keep names so mockup snippets can be lifted with minimal edits.
3.2 Theme + accent provider
- New
ThemeProvider(client component) that:- Persists
theme(light|dark) andaccent(sage|cobalt|terracotta|graphite) inlocalStorage. - Sets
data-themeondocument.documentElementand writes the accent's--brand,--brand-deep,--brand-soft,--brand-soft-ink,--viz-1CSS vars (logic ported fromdocs/design/MeshiTrack.htmluseEffect). - Avoids FOUC by emitting a tiny inline script in
app/layout.tsxthat readslocalStorageand appliesdata-themebefore hydration.
- Persists
- Expose a
useTheme()hook for the topbar toggle.
3.3 Component library (packages/web/src/components/ui/)
Port the mockup's reusable primitives as typed React components:
Icon— single component over an inline SVG map (dashboard,cabinet,clock,list,calendar,pill,refresh,truck,tag,store,trend,settings,search,bell,sun,moon,chev,arrow,check,injection,capsule).Card,CardHeader(title + sub + right-side action slot).Buttonwithvariant: 'primary' | 'ghost' | 'danger'and optionalsize.Pill(status chip) withtone: 'ok' | 'warn' | 'danger' | 'info' | 'neutral'.SupplyBar,Ring(SVG progress ring),SparkBars(monthly bars),Sparkline.Kbd,IconButton,Avatar,SearchInput,Breadcrumbs.
These live in packages/web/src/components/ui/ and are pure presentational components (no data fetching, no 'use client' unless they need state — most do not).
3.4 App shell
Rewrite the layout primitives:
- packages/web/src/components/layout/Sidebar.tsx:
- Brand block (logo + wordmark + household sub-line —
household.nameinstead of mock "Red Panda Den"). - Grouped nav from a
NAVarray mirroring the mockup's structure but driven bynext/navigation'susePathnamefor active state. - Footer with
Avatar+displayName+ role fromuseApi(). - Badges (e.g. low-supply count, pending refills) wired to real SWR fetchers (Phase 2 — initially render the slot but optional).
- Brand block (logo + wordmark + household sub-line —
- packages/web/src/components/layout/TopBar.tsx:
- Accept
title,subtitle,crumbs,actionsprops. Provided either via a<PageHeader>component each route renders, or via a Zustand/Context "page header" store updated from each page (recommended: a server-componentPageHeaderslot rendered above the page body — simpler, no client state). - Search input (visual only at first;
⌘Kpalette is a follow-up). - Theme toggle wired to
useTheme(). - Notifications bell (visual only initially).
- Accept
- packages/web/src/app/(dashboard)/layout.tsx:
- Switch root container to the mockup's grid:
grid-template-columns: 248px 1fr; min-height: 100vh; background: var(--bg). - Inner
<main>becomesmt-pagewith the mockup's padding (32pxdesktop, responsive down).
- Switch root container to the mockup's grid:
3.5 Page layout pattern
Each route renders:
<PageHeader title="..." subtitle="..." crumbs={[...]} actions={...} />
<PageContent>...cards / grid...</PageContent>
PageHeader is a server component that renders the topbar's middle area (title block + breadcrumbs). The sticky outer TopBar reads children via React's slots pattern: simplest is to make TopBar itself accept title/subtitle/crumbs and have pages set them via a thin PageHeaderContext (client provider) or via Next's template.tsx + parallel routes. Choose the Context approach for minimal churn.
4. Page-by-Page Mapping
For each route, port the layout from the mockup's JSX while replacing mock data with the existing service hooks.
| Route | Current file | Mockup source | Notes |
|---|---|---|---|
/dashboard |
dashboard/page.tsx | Dashboard.jsx | Hero + Ring, Today's schedule, Running low, Spending, Supply, Pending orders, Activity. Many cards depend on data we don't yet expose; render skeletons / "Coming soon" states for missing endpoints. |
/medicines/cabinet |
medicines/cabinet/ |
Cabinet.jsx | Grid of medicine cards with swatch, supply bar, status pill. |
/medicines/regimens |
medicines/regimens/ |
RegimensPage in OtherPages.jsx |
Per-regimen rows with schedule chips. |
/medicines/organizer |
medicines/organizer/ |
Schedule.jsx OrganizerPage |
7-day x slots grid. |
/medicines/library |
medicines/library/ |
LibraryPage |
Filterable medicine table. |
/medicines/activity |
medicines/activity/ |
ActivityPage |
Activity feed + spend summary. |
/refills (new) |
— | RefillsPage |
Add route under (dashboard)/refills/. |
/purchases |
purchases/ |
PurchasesPage |
List with status pills. |
/medicine-prices |
medicine-prices/ |
PricesPage |
Per-store price comparison. |
/stores |
stores/ |
StoresPage |
Store cards. |
/settings |
settings/ |
SettingsPage |
Household + account sections. |
Also add a Schedule & Log page (/schedule or /medicines/schedule) per mockup — currently missing.
Update medicines/page.tsx (and dashboard/page.tsx if kept) to use Card/CardHeader instead of ad-hoc Tailwind classes.
5. Phased Rollout
Treat this as 5 PR-sized increments, each independently shippable.
Phase A — Foundations (no visible page changes yet)
- Port tokens.css into
globals.css(CSS vars + Tailwind v4@thememappings + dark overrides). - Wire fonts via
next/font/googleinapp/layout.tsx; bind to--font-*vars. - Add
ThemeProvider+ FOUC-safe inline script. - Add
Iconcomponent and the icon set. - Add UI primitives:
Card,CardHeader,Button,Pill,IconButton,Avatar,Kbd,SearchInput,Breadcrumbs,Ring,SupplyBar,SparkBars. - Vitest unit tests + RTL smoke tests for each primitive.
Phase B — App shell
- Rewrite
Sidebarwith grouped nav, icons, active-route detection viausePathname, footer. - Rewrite
TopBarwith title/subtitle/crumbs slots, search, theme toggle, notifications. - Add
PageHeaderContext(or a header slot mechanism) so pages can set the topbar content. - Update
(dashboard)/layout.tsxgrid +<main>styling. - Snapshot/RTL tests for shell.
Phase C — Dashboard
- Re-implement
/dashboardwith hero + ring + cards using real data where available, skeletons elsewhere. - Wire "Running low" to existing cabinet endpoint; "Pending orders" to purchases; "Activity" to activity endpoint.
- Tests: render with seeded SWR cache.
Phase D — Medicine pages
- Port Cabinet, Library, Regimens, Organizer, Activity, Schedule & Log one at a time.
- Each PR: lift JSX from the mockup, replace mock data with
services/*SWR hooks, add tests.
Phase E — Commerce + settings
- Port Refills (new route), Purchases, Prices, Stores, Settings.
- Add accent picker UI in Settings.
6. Tailwind v4 Token Mapping (concrete snippet)
In packages/web/src/styles/globals.css:
@import 'tailwindcss';
@theme {
--font-sans: 'Inter Tight', ui-sans-serif, system-ui, sans-serif;
--font-display: 'Fraunces', ui-serif, Georgia, serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
--color-bg: var(--bg);
--color-bg-elev: var(--bg-elev);
--color-bg-inset: var(--bg-inset);
--color-border: var(--border);
--color-border-strong: var(--border-strong);
--color-ink: var(--ink);
--color-ink-strong: var(--ink-strong);
--color-ink-muted: var(--ink-muted);
--color-ink-faint: var(--ink-faint);
--color-brand: var(--brand);
--color-brand-deep: var(--brand-deep);
--color-brand-soft: var(--brand-soft);
--color-danger: var(--danger);
--color-warn: var(--warn);
--color-ok: var(--ok);
--color-info: var(--info);
--radius-xs: var(--r-xs);
--radius-sm: var(--r-sm);
--radius-md: var(--r-md);
--radius-lg: var(--r-lg);
--radius-xl: var(--r-xl);
}
:root {
/* paste the :root block from docs/design/styles/tokens.css */
}
[data-theme='dark'] {
/* paste the dark block */
}
This lets us write bg-bg-elev text-ink-strong border-border rounded-lg shadow-md while still being able to use the raw var(--brand) inside inline SVGs (matching the mockup's pattern).
7. Risks & Mitigations
| Risk | Mitigation |
|---|---|
| FOUC on theme switch / SSR mismatch | Inline script in <head> reads localStorage and sets data-theme before React hydrates. Mark theme-dependent UI as client-only when needed. |
Mockup uses inline style={{...var(--viz-X)...}} heavily |
Keep that pattern in components; tokens are CSS vars so this works fine. |
| Real data shape differs from mock data | For each card, define a small typed view-model and a transformer from API DTO; render skeletons when data is unavailable. |
Tailwind v4 @theme + custom CSS vars interplay |
Verified above: declare CSS vars in :root/[data-theme=dark], then map them to --color-* inside @theme so utilities pick them up. |
| Test snapshots churn | Co-locate component tests, prefer RTL queries by role/text over snapshots; use snapshots only for the icon set. |
| Scope creep | Strictly follow the phased rollout; each phase is independently mergeable. |
8. Out-of-Scope Follow-ups
⌘Kcommand palette (search input is visual-only initially).- Real notification feed.
- Per-user accent persistence in DB (initially
localStorageonly). - Mobile drawer for the sidebar (mockup is desktop-first; add a
< 900pxbreakpoint that collapses to a top hamburger). - Animations beyond the existing CSS transitions.
9. Definition of Done (per phase)
- All new components have unit tests; coverage for
packages/webdoes not regress. npm run lintandnpm run typecheckpass at the workspace root.- Visual parity verified against the relevant screenshot in
docs/design/screenshots/. - No usage of
any; all view-models typed; all client-only files marked'use client'. - No regressions in existing E2E tests.