Skip to content

refactor(website): adopt light-dark() and fix scaffold-guideline drift #1216

Description

@vivek7405

Problem

The marketing site (website/) is the framework's flagship dogfood app, but it does not follow the styling and layout guidance WebJs ships to its own users. Three concrete divergences, all verified against the current tree at 4822e0d1:

1. The palettes duplicate every dark value instead of using light-dark().

.agents/skills/webjs/references/styling.md (the "Light and dark, defined once (DRY)" section, L112-142) and packages/cli/lib/create.js (L1245-1330, the generated app/layout.ts token block) both teach one rule: write each colour token ONCE with the native light-dark(LIGHT, DARK) function and let color-scheme pick the side. The website does the opposite, in two places:

  • website/app/layout.ts L293-323: the dark palette is written TWICE, once under @media (prefers-color-scheme: dark) { :root:not([data-theme='light']) } and again verbatim under :root[data-theme='dark']. 16 tokens, ~30 lines of pure duplication. The two blocks are already byte-equal, so any future edit has to be made in both or the OS-dark and toggle-dark paths silently diverge.
  • website/public/input.css L152-218: the .ui-preview component-preview palette writes 19 tokens THREE times (base, @media (prefers-color-scheme: dark), :root[data-theme='dark']), ~66 lines.

The prerequisite for light-dark() is already in place: website/app/layout.ts L248 declares color-scheme: light dark on :root, L324 sets :root[data-theme='light'] { color-scheme: light } and L310 sets :root[data-theme='dark'] { color-scheme: dark }. So the three-way selector logic is redundant with a color-scheme cascade that already exists.

2. website/components/ui/ holds 11 accidentally-committed dead files.

website/AGENTS.md states that components/ui/ is "intentionally EMPTY here, left free for webjs ui add to own, exactly as the scaffold expects". It is not empty: commit 235d0fb0 (#1213) added 11 files / 766 lines there. Every one is BYTE-IDENTICAL to its counterpart in the gitignored generated mirror website/modules/ui/components/ (verified with diff on card.ts, kbd.ts, table.ts), and NONE is imported by anything in the app (grep for each basename across app lib modules components returns zero non-self hits).

They leaked because website/.gitignore ignores /modules/ui/components/, /lib/utils/cn.ts, and /lib/utils/dom.ts but NOT /components/ui/.

3. Doc drift in website/AGENTS.md.

The "Announcement banner" section describes a strip the root layout renders "just above the sticky header". No such markup exists in app/layout.ts any more, and L329 explicitly comments --header-h: 59px; /* #610 fixed header offset (no banner) */. The same section calls the header "sticky" when #610 deliberately made it position: fixed.

Design / approach

1. Collapse both palettes onto light-dark().

For app/layout.ts, fold each duplicated dark colour back into its :root declaration as light-dark(<light>, <dark>) and delete both dark blocks, keeping only the color-scheme switches. Per the styling reference's "Edge cases" paragraph, light-dark() is COLOUR-only, so the non-colour and composite tokens must keep an explicit override pair:

  • --glow-strength (0.16 / 0.08) is a bare number, not a colour.
  • --shadow-sm / --shadow are full shadow values (geometry + colour), so light-dark() cannot wrap them whole. Either keep the override pair or split the colour out into its own light-dark() token and build the shadow from it (preferred: it puts the per-theme part back under the single-definition rule).
  • --accent-tint, --cta-surface, --accent-surface, --accent-border are DERIVED via color-mix() from --accent-live; once --accent-live is a light-dark() token they track both themes for free and their overrides can go.

For public/input.css, the .ui-preview block is a scoped container rather than :root, which is fine: color-scheme inherits, so .ui-preview picks up whatever :root resolved and light-dark() inside it selects the matching side with no extra selectors.

2. Delete website/components/ui/*.ts and add /components/ui/ to website/.gitignore, restoring the state website/AGENTS.md already documents and keeping the directory free for webjs ui add.

3. Correct the two stale paragraphs in website/AGENTS.md.

Implementation notes (for the implementing agent)

Where to edit

  • website/app/layout.ts L247-324: the :root token block, the @media (prefers-color-scheme: dark) block, the :root[data-theme='dark'] block, and the two color-scheme switches. This is inside a template literal in a page/layout, which is legal (a layout never hydrates, so interpolating into <style> is allowed; invariant 9's backtick ban still applies inside the html tag).
  • website/public/input.css L152-218: the .ui-preview palette and its two dark blocks. Do NOT touch the @custom-variant dark block at L23-28 or the @theme / @theme inline blocks at L30-137; the custom variant is already correctly keyed off [data-theme] rather than the kit's .dark, which is what keeps the previews in step with the page theme.
  • website/components/ui/*.ts: delete all 11 files.
  • website/.gitignore: add /components/ui/.
  • website/AGENTS.md: the "Announcement banner" section and the components/ layout block.

Landmines / gotchas

  • website/scripts/copy-registry.mjs L44-63 carries a one-time LEGACY migration that DELETES files from components/ui/ whose contents match /from '(\.\/lib\/|\.\.\/\.\.\/lib\/ui\/)/. The 11 committed files have NO imports at all, so they do not match and survive every dev cycle. Deleting them by hand is therefore required; the script will not do it. Leave that migration block alone.
  • website/public/tailwind.css is a GITIGNORED compiled artifact. Edit public/input.css only, then let webjs.dev.before / webjs.dev.regenerate recompile (see website/AGENTS.md "Run"). Never hand-edit tailwind.css.
  • The .ui-preview palette carries a comment (L144-150) saying it mirrors what webjs ui init writes into a user's globals.css, i.e. packages/ui/packages/registry/themes/index.css. That file is NOT actually mirrored verbatim today: it uses @custom-variant dark (&:is(.dark *)) plus :root / .dark blocks (L55, L66, L102), while the website keys off [data-theme]. If .ui-preview moves to light-dark() and the registry theme does not, update that comment so it stops claiming a byte-for-byte mirror it never was. Converting the registry theme itself is a SCAFFOLD surface change and belongs in a separate issue (it would need the webjs-scaffold-sync skill and a generate-boot-check pass).
  • Verify in a REAL browser in all three toggle states (system with a dark OS, forced light, forced dark) plus the /ui gallery previews. Per the styling reference: "Light mode passing proves nothing about dark." The <theme-toggle> at website/components/theme-toggle.ts writes data-theme on <html>; the no-FOUC bootstrap that reads it back is the inline script at app/layout.ts L128-134.
  • light-dark() is CSS Color 5 / Baseline 2024. All four current targets (the site's supported browsers) have it, but oklch() values inside it are fine since light-dark() accepts any <color>.

Invariants to respect

  • AGENTS.md invariant 7 (light-DOM tag-prefix) does not apply to the layout's <style>: those rules belong to a layout, not a component. Do not "fix" them.
  • AGENTS.md invariant 11 (prose punctuation and WebJs brand casing) applies to every markdown line touched in website/AGENTS.md.
  • The root layout is the ONE file allowed to write its own shell (invariant 8). Do not move the token block anywhere else.

Tests + docs surfaces

  • website/test/ssr/* is the existing SSR suite; website/test/ssr/ui-gallery.test.ts already pins seed: false. Add an SSR assertion that the rendered root layout emits light-dark( and NO @media (prefers-color-scheme: dark) palette block, with the counterfactual (revert the layout, the test reds).
  • Add a test that website/components/ui/ stays empty, so the accidental re-commit cannot recur silently.
  • Run npx webjs check in website/ (currently clean) and npm test.
  • No framework packages/*/src change here, so the doc gate does not apply; website/AGENTS.md is the doc surface for this one.

Acceptance criteria

  • website/app/layout.ts declares every per-theme COLOUR token once via light-dark(); the @media (prefers-color-scheme: dark) and :root[data-theme='dark'] palette duplicates are gone, leaving only color-scheme switches plus the documented non-colour overrides
  • website/public/input.css .ui-preview declares its 19 tokens once via light-dark(); its two dark blocks are gone
  • Non-colour per-theme tokens (--glow-strength, the shadows) are handled per the styling reference's edge-case rule, not silently dropped
  • website/components/ui/ is empty and /components/ui/ is gitignored
  • website/AGENTS.md no longer documents a non-existent announcement banner, and describes the header as fixed (dogfood: mobile navbar flickers on forward nav (backdrop-blur sticky header) #610) rather than sticky
  • Rendered colours are unchanged in all three theme states, verified in a real browser on the home page, a /docs page, and a /ui component page
  • A counterfactual proves the new SSR test actually fires
  • npx webjs check and npm test pass in website/

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions