design-system-reconciliationrun.md01_define/output/spec.mdDemo 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.
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:
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.blue, purple and soft
destructive are re-mapped to the shared equivalents (blue → info, purple → primary, 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:
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.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.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.
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.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.Card with variant="warning" (and the other promoted surfaces: success, error, info,
highlighted, muted) renders identically in demo and web.CardTitle + CardValue) renders identically in
both apps, and is built from the single shared Card primitive.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.Badge/Card
primitives (the shared default Card and CardTitle treatments are unchanged).pnpm lint, pnpm format:check and the affected app/package builds pass.DetailPageShell foundation (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.02_build/output/notes.mdneutral + 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 variantspackages/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.tsx — Card 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).blue→info (×9),
purple→primary (×1), soft destructive→error (×10 — Badge only; ActionButton/Alert/
AlertBanner destructives left untouched); Card danger→error (×1). No back-compat aliases left
in the shared package.apps/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).default solid primary, neutral chip, solid destructive).blue/purple/soft-destructive migrated to
info/primary/error; no aliases in @sustentus/ui.warning/success/error/info/highlighted/muted) identical in
both apps — demo re-exports the shared Card.CardTitle + CardValue) identical in both apps, built
from the shared Card primitive.stat-card.tsx + metric-tile.tsx removed, consumer migrated.Default stories cover the reconciled Badge and Card variants.apps/demo/components/ui/{badge,card}.tsx re-export the shared implementations.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).@sustentus/ui dist/ is gitignored and rebuilt by Turbo before the consuming apps build, so demo/
web/storybook compile against the updated source.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".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.mdWho 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.mdBadgeVariant 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/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).03_release/output/investor-update.md.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.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.blue/purple/soft-destructive migrated; no aliases.CardTitle + CardValue) identical, from one primitive.stat-card.tsx + metric-tile.tsx removed, consumer migrated.Default stories cover the reconciled Badge and Card variants.apps/demo/components/ui/{badge,card}.tsx re-export the shared implementations.pnpm lint / pnpm format:check / builds pass — CI green on the head commit.