Summary
Add an interactive theme builder page to the docs site: users edit design tokens, watch framework components restyle live, then download a ready-to-use settings.theme.custom.css file.
This is the follow-up to the component playground (_Playground.jsx, piloted on the Button page). The design below was worked out alongside it.
Design
Page
New docs page at /docs/theme-builder, authored as src/content/docs/theme-builder.mdx. The docs collection schema already fits ({ title, lastUpdate, version }), so no routing work. The page contains intro prose, the <ThemeBuilder client:load /> island (load, not visible: default-value probing should run immediately), and a static preview strip of raw-HTML framework components (button variants, badge, card, notice, form field, headings, links) that live token overrides restyle.
Also: add a nav entry in src/components/NavDocs.astro next to the Themes link, and cross-link from src/content/docs/themes.mdx.
Token metadata: hand-maintained manifest, defaults read from the live page
New src/scripts/themeTokens.js with curated groups. Values are omitted on purpose to avoid drift: at mount, read each token's current value via getComputedStyle(document.documentElement).getPropertyValue(name) and convert to hex for <input type="color">.
Chosen over parsing settings.theme.default.css?raw at build time because curation, grouping, labels, and control types would need a manifest anyway, and the raw values are light-dark(var()) chains that must be runtime-resolved regardless.
v1 scope (roughly 50 tokens, all primitives, matching the intended theme-file shape shown by the commented-out settings.theme.wireframe.css):
- Primary scale
--primary-50..950, neutrals --base-0..950, feedback --yes|no|maybe-100..500 (type color)
- Radii
--radius-sm..xxl (type length)
- Font stacks
--display, --text, --mono (type fontStack)
Because the semantic layer in settings.theme.default.css derives from these via light-dark(var(--base-X), var(--base-Y)), editing primitives re-themes both light and dark modes automatically. Editing semantic light-dark() pairs directly (two inputs per token, resolved via hidden probe elements with forced color-scheme) is v2.
Island: src/components/_ThemeBuilder.jsx
- Controls: color input plus hex text field per color token; text inputs for lengths and font stacks. Per-group and global reset. (OKLCH sliders from
_ColorPickerOklch.jsx are a possible v2 upgrade.)
- Live preview: assemble
:root { ... } from edited tokens only and inject via <style dangerouslySetInnerHTML> (same pattern as _GridDemo.jsx). Global on purpose: the page is dedicated, and an unlayered body <style> beats the layered framework declarations. The existing ThemeToggle keeps working since light/dark resolves downstream in light-dark().
- Output: generated CSS shown live-highlighted via
useHighlightedCode(css, "css"), a <CopyButton />, and a "Download settings.theme.custom.css" button (Blob + URL.createObjectURL + temporary <a download> + revokeObjectURL). The file matches the wireframe theme shape: header usage comment (import after mcss.css, link to /docs/themes), grouped section comments, 2-space indent, and only tokens that differ from defaults.
Supporting files
src/styles/site/component.themeBuilder.css (new), registered unlayered in src/styles/_global.css
Prior art in the repo
_GridDemo.jsx: settings state driving live <style> injection and a highlighted code panel
_ColorPickerOklch.jsx: slider-driven color editing with gamut math
_useHighlightedCode.js + scripts/shikiHighlighter.js: client-side Shiki
settings.theme.wireframe.css (commented out): the shape the downloaded theme file should match
Summary
Add an interactive theme builder page to the docs site: users edit design tokens, watch framework components restyle live, then download a ready-to-use
settings.theme.custom.cssfile.This is the follow-up to the component playground (
_Playground.jsx, piloted on the Button page). The design below was worked out alongside it.Design
Page
New docs page at
/docs/theme-builder, authored assrc/content/docs/theme-builder.mdx. The docs collection schema already fits ({ title, lastUpdate, version }), so no routing work. The page contains intro prose, the<ThemeBuilder client:load />island (load, not visible: default-value probing should run immediately), and a static preview strip of raw-HTML framework components (button variants, badge, card, notice, form field, headings, links) that live token overrides restyle.Also: add a nav entry in
src/components/NavDocs.astronext to the Themes link, and cross-link fromsrc/content/docs/themes.mdx.Token metadata: hand-maintained manifest, defaults read from the live page
New
src/scripts/themeTokens.jswith curated groups. Values are omitted on purpose to avoid drift: at mount, read each token's current value viagetComputedStyle(document.documentElement).getPropertyValue(name)and convert to hex for<input type="color">.Chosen over parsing
settings.theme.default.css?rawat build time because curation, grouping, labels, and control types would need a manifest anyway, and the raw values arelight-dark(var())chains that must be runtime-resolved regardless.v1 scope (roughly 50 tokens, all primitives, matching the intended theme-file shape shown by the commented-out
settings.theme.wireframe.css):--primary-50..950, neutrals--base-0..950, feedback--yes|no|maybe-100..500(typecolor)--radius-sm..xxl(typelength)--display,--text,--mono(typefontStack)Because the semantic layer in
settings.theme.default.cssderives from these vialight-dark(var(--base-X), var(--base-Y)), editing primitives re-themes both light and dark modes automatically. Editing semanticlight-dark()pairs directly (two inputs per token, resolved via hidden probe elements with forcedcolor-scheme) is v2.Island:
src/components/_ThemeBuilder.jsx_ColorPickerOklch.jsxare a possible v2 upgrade.):root { ... }from edited tokens only and inject via<style dangerouslySetInnerHTML>(same pattern as_GridDemo.jsx). Global on purpose: the page is dedicated, and an unlayered body<style>beats the layered framework declarations. The existing ThemeToggle keeps working since light/dark resolves downstream inlight-dark().useHighlightedCode(css, "css"), a<CopyButton />, and a "Download settings.theme.custom.css" button (Blob+URL.createObjectURL+ temporary<a download>+revokeObjectURL). The file matches the wireframe theme shape: header usage comment (import aftermcss.css, link to /docs/themes), grouped section comments, 2-space indent, and only tokens that differ from defaults.Supporting files
src/styles/site/component.themeBuilder.css(new), registered unlayered insrc/styles/_global.cssPrior art in the repo
_GridDemo.jsx: settings state driving live<style>injection and a highlighted code panel_ColorPickerOklch.jsx: slider-driven color editing with gamut math_useHighlightedCode.js+scripts/shikiHighlighter.js: client-side Shikisettings.theme.wireframe.css(commented out): the shape the downloaded theme file should match