Problem
Developers (and AI code generators — v0, Copilot, shadcn copy-paste) reflexively use the generic shadcn-style CSS variables (--card, --border, --muted-foreground, --primary, --destructive, …) instead of the --mieweb-* design tokens. This keeps happening despite instructions, grep audits, and code review — the generic names are the ecosystem default, so the reflex will never go away.
The failure mode is nasty because it's silent and partial: shadcn-style code ships light fallbacks (var(--card, #fff)). No brand CSS defines the generic names, so the fallback renders — but only visibly breaks in dark mode (e.g. a white card on a dark chart), only for the missed tokens. This has shipped repeatedly in echart-sim.
Today the situation is the worst of both worlds: generic names silently half-work instead of either working or failing loudly.
Proposal: scoped shadcn-compat alias layer
Ship a compatibility layer in @mieweb/ui (styles.css) that maps the ~15 shadcn semantic tokens to the mieweb tokens, scoped to the app container, not :root:
// shadcn-compat: generic ecosystem names resolve to the active brand.
// Scoped to the app root so aliases can't leak into a host page,
// and a host page's :root definitions can't leak in.
.mieweb-app {
--background: var(--mieweb-background);
--foreground: var(--mieweb-foreground);
--card: var(--mieweb-card);
--card-foreground: var(--mieweb-card-foreground);
--muted: var(--mieweb-muted);
--muted-foreground: var(--mieweb-muted-foreground);
--border: var(--mieweb-border);
--input: var(--mieweb-input);
--primary: var(--mieweb-primary-500);
--primary-foreground: var(--mieweb-primary-foreground);
--secondary: var(--mieweb-secondary);
--accent: var(--mieweb-accent);
--destructive: var(--mieweb-destructive);
--ring: var(--mieweb-ring);
--radius: var(--mieweb-radius);
// ...complete the shadcn semantic set
}
Why this design works
- Inheritance does the fan-out. CSS custom properties cascade, so one declaration on the wrapper covers every descendant
var(--card) — no per-tag prefixing.
- Dark mode is free. Aliases point at
--mieweb-*, which are re-declared under [data-theme='dark'] on <html>; var() re-resolves at each element, so the alias picks up the dark value automatically. No dark variants needed in the compat layer.
- Brand switching is free for the same reason — swap the brand
<link> and everything re-resolves.
- Collision protection both ways. Scoping to
.mieweb-app (instead of :root) means a host page's :root { --primary: … } loses to the closer ancestor, and our aliases never escape the app subtree. Important because these apps are embedded in host pages (WebChart) that may load their own Tailwind/shadcn themes.
- Fallbacks become harmless.
var(--card, #fff)'s fallback only renders if the compat layer is missing entirely — an obvious whole-page failure instead of a subtle per-token, dark-mode-only one.
Invariants / follow-ups
- Completeness is the new invariant. The failure mode shifts from "developer used a banned name" to "compat layer is missing a token." Add a small CI check: extract every
var(--x) used in library/app SCSS and assert each generic --x appears in the compat file (replaces the current grep -v mieweb audit).
- The wrapper must wrap everything, including portals/modals rendered into
document.body. Either apply the class at app boot on <html>/<body> (fine standalone, riskier embedded) or ensure portal containers also carry .mieweb-app.
- First-party library SCSS should still use
--mieweb-* directly — the compat layer is a safety net for imported/generated/consumer code, not a second first-class vocabulary.
Context
- Convention docs that keep getting violated: echart-sim
.github/copilot-instructions.md ("Never use generic shadcn-style variables…") and repeated dark-mode regressions in component PRs.
- Analogy: generic names in the leaves, one branded adapter at the root — same shape as React context vs. prop-drilling.
Problem
Developers (and AI code generators — v0, Copilot, shadcn copy-paste) reflexively use the generic shadcn-style CSS variables (
--card,--border,--muted-foreground,--primary,--destructive, …) instead of the--mieweb-*design tokens. This keeps happening despite instructions, grep audits, and code review — the generic names are the ecosystem default, so the reflex will never go away.The failure mode is nasty because it's silent and partial: shadcn-style code ships light fallbacks (
var(--card, #fff)). No brand CSS defines the generic names, so the fallback renders — but only visibly breaks in dark mode (e.g. a white card on a dark chart), only for the missed tokens. This has shipped repeatedly in echart-sim.Today the situation is the worst of both worlds: generic names silently half-work instead of either working or failing loudly.
Proposal: scoped shadcn-compat alias layer
Ship a compatibility layer in
@mieweb/ui(styles.css) that maps the ~15 shadcn semantic tokens to the mieweb tokens, scoped to the app container, not:root:Why this design works
var(--card)— no per-tag prefixing.--mieweb-*, which are re-declared under[data-theme='dark']on<html>;var()re-resolves at each element, so the alias picks up the dark value automatically. No dark variants needed in the compat layer.<link>and everything re-resolves..mieweb-app(instead of:root) means a host page's:root { --primary: … }loses to the closer ancestor, and our aliases never escape the app subtree. Important because these apps are embedded in host pages (WebChart) that may load their own Tailwind/shadcn themes.var(--card, #fff)'s fallback only renders if the compat layer is missing entirely — an obvious whole-page failure instead of a subtle per-token, dark-mode-only one.Invariants / follow-ups
var(--x)used in library/app SCSS and assert each generic--xappears in the compat file (replaces the currentgrep -v miewebaudit).document.body. Either apply the class at app boot on<html>/<body>(fine standalone, riskier embedded) or ensure portal containers also carry.mieweb-app.--mieweb-*directly — the compat layer is a safety net for imported/generated/consumer code, not a second first-class vocabulary.Context
.github/copilot-instructions.md("Never use generic shadcn-style variables…") and repeated dark-mode regressions in component PRs.