Skip to Content

← All archived runs

Run: design-system-reconciliation

run.md

Run: design-system-reconciliation

  • branch: claude/design-system-reconciliation-pipeline-ynk5e6
  • pr: #698

01_define/output/spec.md

Spec: Design-system component reconciliation (Badge, Card, metric tile)

  • slug: design-system-reconciliation
  • personas: admin, csm, sdm, expert, vendor, customer (platform-wide primitives)
  • touches: packages/ui/src/base/badge.tsx, packages/ui/src/base/card.tsx, packages/ui/src/lib/variants.ts, apps/demo/components/ui/badge.tsx, apps/demo/components/ui/card.tsx, apps/web/components/ui/card.tsx, apps/web/components/dashboard/metric-tile.tsx, apps/web/components/ui/stat-card.tsx, apps/storybook
  • complexity: complex

Problem

Demo and web render the same primitives differently because apps/demo ships local shadcn components that have drifted from the shared @sustentus/ui package. Badge, Card and the metric tile look materially different across the two apps — different shape, weight, default intent, and variant vocabulary — so no downstream page can reach pixel parity until the primitives agree. This is stub 1 of the demo-web-parity epic (initiative: Refine the Bridge; objective: Q2 2026 — O1 vendor engagement): a consistent primitive layer is the substrate every persona dashboard is built on, so reconciling these three primitives unblocks all the per-persona parity work that follows. Per the agreed styling decision, demo is fixed up to the design system (the shared package is canonical for tokens and variants), while genuinely-useful demo variants are promoted into the shared package rather than deleted.

Proposed change

Make both apps consume one canonical implementation of each primitive, with @sustentus/ui as the single source of truth.

Badge — settle one canonical primitive in packages/ui:

  • Shape and weight follow the shared package (rounded-md, font-medium, px-2 py-0.5); demo's rounded-full/font-semibold is dropped.
  • default stays the shared solid-primary intent (canonical). Demo's neutral soft-grey chip is promoted as a new neutral variant (bg-muted / text-muted-foreground) so the neutral intent survives without changing default.
  • One variant vocabulary — the shared semantic names win. Demo's blue, purple and soft destructive are re-mapped to the shared equivalents (blueinfo, purpleprimary, soft destructive → error). destructive stays the shared solid intent; the soft-red intent is expressed by error. No back-compat aliases are kept — the ~35 demo call sites are migrated to the shared vocabulary in this run.
  • apps/demo/components/ui/badge.tsx re-exports the shared Badge instead of defining its own.

Card — promote demo's additive surface and metric-tile pieces into the shared Card:

  • Add token-based surface variants to packages/ui/src/base/card.tsx: success, warning, error (demo's danger, renamed to the shared vocabulary), info, highlighted, and muted. default keeps the current shared Card styling unchanged, so existing web cards do not regress.
  • Provide the muted metric-tile title treatment (demo's small, medium-weight, muted CardTitle) as an opt-in on the shared Card, alongside CardValue, so both apps compose metric tiles from one primitive. The default CardTitle treatment is unchanged.
  • Restore CardValue, CardDescription and CardAction in web's barrel (apps/web/components/ui/card.tsx) so web imports them from the barrel, not ad hoc.
  • apps/demo/components/ui/card.tsx re-exports the shared Card family; demo's danger call site is migrated to error.

Metric tile — standardise on the shared Card-based tile (muted CardTitle + CardValue) and retire web's two divergent primitives:

  • apps/web/components/ui/stat-card.tsx is dead code (no live render site) — remove it.
  • apps/web/components/dashboard/metric-tile.tsx is used only by apps/web/components/dashboard/expert/performance-section.tsx — migrate that consumer to the shared tile pattern, then remove metric-tile.tsx.

Storybook@sustentus/ui's Default stories cover the reconciled Badge and Card variants (including the new neutral Badge variant and the Card surface variants + metric-tile pattern), per the shadcn-component skill.

Acceptance criteria

  • A Badge rendered with the same variant looks identical in demo and web across shape, weight, and intent — including default (solid primary), the promoted neutral chip, and the solid destructive.
  • The shared Badge exposes one variant vocabulary (default, neutral, secondary, outline, primary, destructive, success, warning, error, info); demo's blue, purple and soft-destructive call sites are migrated to info/primary/error, with no back-compat aliases remaining in @sustentus/ui.
  • A Card with variant="warning" (and the other promoted surfaces: success, error, info, highlighted, muted) renders identically in demo and web.
  • The metric-tile title/value pattern (muted CardTitle + CardValue) renders identically in both apps, and is built from the single shared Card primitive.
  • One metric-tile primitive is used across both apps: web's stat-card.tsx and metric-tile.tsx are removed and their consumer migrated to the shared pattern.
  • @sustentus/ui Storybook Default stories cover the reconciled Badge and Card variants.
  • apps/demo/components/ui/{badge,card}.tsx re-export the shared implementations rather than defining local variants.
  • No visual regression on existing web pages that already use the shared Badge/Card primitives (the shared default Card and CardTitle treatments are unchanged).
  • pnpm lint, pnpm format:check and the affected app/package builds pass.

Out of scope

  • Per-page adoption of the reconciled tiles across persona dashboards (handled by the persona-parity stubs later in the epic).
  • The DetailPageShell foundation (separate stub).
  • Demo colour-token cleanup (separate stub).
  • Progress primitive reconciliation — the stub notes touch demo/components/ui/progress.tsx, but this run is scoped to Badge, Card and the metric tile; Progress is left for a later stub unless it falls out trivially as a re-export.
  • Re-spacing demo pages whose layout shifts because demo's Card now inherits the shared Card's padding/gap — those page-level adjustments belong to the persona-parity stubs.

Open questions

  • none

02_build/output/notes.md

Build notes: design-system-reconciliation

  • commits: feat: shared Badge neutral + Card surface variants & muted title · feat: demo re-exports shared Badge/Card + call-site vocabulary migration · refactor: retire web stat-card/metric-tile, migrate consumer to shared tile pattern · docs(storybook): cover reconciled Badge/Card variants

What changed

  • packages/ui/src/lib/variants.ts — added the neutral Badge intent (bg-muted/text-muted-foreground); added cardVariantConfig (semantic surfaces default/muted/success/warning/error/info/highlighted, promoted from demo). default is empty so the base Card look is unchanged.
  • packages/ui/src/base/card.tsxCard now takes a variant prop (cva over cardVariantConfig; default preserves the current styling). CardTitle gains a variant (default = the unchanged semibold heading; muted = the promoted metric-tile label text-muted-foreground text-sm font-medium). Badge needed no code change — it reads badgeVariantConfig, so neutral is picked up automatically.
  • apps/demo/components/ui/{badge,card}.tsx — now thin re-exports of the shared @sustentus/ui implementations instead of local cva definitions (added CardDescription/CardAction to the card re-export for completeness).
  • Demo call-site migration to the single shared vocabulary (~21 sites): Badge blueinfo (×9), purpleprimary (×1), soft destructiveerror (×10 — Badge only; ActionButton/Alert/ AlertBanner destructives left untouched); Card dangererror (×1). No back-compat aliases left in the shared package.
  • Metric tileapps/web/components/ui/stat-card.tsx (dead code, no live consumer) and apps/web/components/dashboard/metric-tile.tsx removed. Its sole consumer (expert/performance-section.tsx) migrated to a local Tile composed from the shared CardTitle variant="muted" + CardValue. The demo metric tiles (admin/stats-row.tsx, admin/readiness-tiles.tsx) set their tile titles to variant="muted" so both apps render the metric-tile pattern identically.
  • apps/web/components/ui/card.tsx — restored CardValue/CardDescription/CardAction in the barrel (previously imported ad hoc from @sustentus/ui).
  • apps/storybook/src/base/{Badge,Card}.stories.tsx — the single Default stories now expose the full reconciled Badge vocabulary and the Card surface variant + CardTitle muted treatment via controls (per one-Default-story convention).

Acceptance criteria status

  • Badge same-variant identical across demo/web — demo re-exports the shared Badge (shape/weight/ intent, default solid primary, neutral chip, solid destructive).
  • One shared Badge vocabulary; demo blue/purple/soft-destructive migrated to info/primary/error; no aliases in @sustentus/ui.
  • Card promoted surfaces (warning/success/error/info/highlighted/muted) identical in both apps — demo re-exports the shared Card.
  • Metric-tile title/value pattern (muted CardTitle + CardValue) identical in both apps, built from the shared Card primitive.
  • One metric-tile primitive — web stat-card.tsx + metric-tile.tsx removed, consumer migrated.
  • Storybook Default stories cover the reconciled Badge and Card variants.
  • apps/demo/components/ui/{badge,card}.tsx re-export the shared implementations.
  • No visual regression on web pages using the shared primitives — shared Card default and the default CardTitle treatment are byte-for-byte equivalent (cva default appends nothing new).
  • pnpm lint / pnpm format:check / builds pass — verified by CI + the Vercel preview, read back at Release (not run locally; the factory owns mechanical checks).

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. @sustentus/ui dist/ is gitignored and rebuilt by Turbo before the consuming apps build, so demo/ web/storybook compile against the updated source.

Notes for review

  • Intentional demo heading shift (deferred polish). Demo's local CardTitle was muted for all cards; the shared CardTitle default is semibold. Now that demo re-exports it, demo section headings adopt the shared semibold treatment, and demo cards inherit the shared Card's px-6/py-6 spacing. This is the intended "fix demo up" direction; per-page re-spacing/polish is explicitly out of scope for this foundation stub (handled by the persona-parity stubs). Metric-tile labels specifically are kept muted via variant="muted".
  • Owner decisions honoured: neutral chip promoted as a neutral variant (not remapped to secondary); call sites migrated to one vocabulary (no back-compat aliases).
  • Progress reconciliation was left out of scope (spec), as were per-page tile adoption and demo colour-token cleanup.

03_release/output/investor-update.md

One design system now powers every Sustentus dashboard

Who it's for: All six dashboard personas (admin, CSM, SDM, expert, vendor, customer). What shipped: Demo and production now render one shared Badge, Card and metric tile instead of drifting local copies. Why it matters: A single UI foundation lets every persona dashboard reach visual parity faster — groundwork for Refine the Bridge (product-market fit with vendor partners).

One variant vocabulary now spans both apps; the divergent primitives are retired.

Dig deeper: <merged-PR URL> · <changelog entry URL>

03_release/output/release.md

Release: design-system-reconciliation

  • pr: #698 · merged: pending Ready-to-merge tick
  • CI: green — Quality Project (format · lint · typecheck), all app builds, and every Vercel preview (demo/web/storybook/marketing) pass on the head commit. Two typecheck rounds were fixed on-branch: computed/typed demo variants (ternaries, mapping objects, the workspace BadgeVariant union) and a Record<BadgeVariant> dot-colour map, all migrated to the shared vocabulary; one over-migrated local StatBox call was reverted to its own danger variant.
  • technical docs: no apps/docs technical impact (the technical/packages/ui page documents Badge/Card at a high level, not per-variant). Corrected CONVENTIONS.md §apps/demo in this PR — it now records that badge/card are re-exported from @sustentus/ui (only progress stays local).
  • business docs: no business docs impact — production (web) behaviour is unchanged; this is a platform-primitive foundation change.
  • release notes: investor-only — no end-user changelog (internal/foundation change, no new user-facing capability). Investor draft in this PR at 03_release/output/investor-update.md.
  • sent: pending merge

Review summary

  • Diff reviewed against CONVENTIONS.md (complexity: complex). Scoped to the reconciliation: shared Badge/Card gain token-based variants (no hardcoded palette added), demo re-exports them, the two divergent web tile primitives are deleted, and the sole consumer is migrated. No dead code, no debug/TODO left. — resolved on branch.
  • Intentional, accepted: demo section headings adopt the shared semibold CardTitle and demo cards inherit the shared Card spacing; metric-tile labels stay muted via variant="muted". Per-page re-spacing is out of scope (persona-parity stubs). — accepted.

Acceptance check (vs spec)

  • Badge same-variant identical across demo/web — demo re-exports the shared Badge.
  • One shared Badge vocabulary; demo blue/purple/soft-destructive migrated; no aliases.
  • Card promoted surfaces render identically in both apps — demo re-exports the shared Card.
  • Metric-tile title/value pattern (muted CardTitle + CardValue) identical, from one primitive.
  • One metric-tile primitive — web stat-card.tsx + metric-tile.tsx removed, consumer migrated.
  • Storybook Default stories cover the reconciled Badge and Card variants.
  • apps/demo/components/ui/{badge,card}.tsx re-export the shared implementations.
  • No visual regression on web pages using the shared primitives — defaults unchanged.
  • pnpm lint / pnpm format:check / builds pass — CI green on the head commit.