Skip to content

feat(theming): OKLCH tokens, runtime presets, and web theme editor#404

Open
bntvllnt wants to merge 6 commits into
mainfrom
feat/oklch-theming-system
Open

feat(theming): OKLCH tokens, runtime presets, and web theme editor#404
bntvllnt wants to merge 6 commits into
mainfrom
feat/oklch-theming-system

Conversation

@bntvllnt

@bntvllnt bntvllnt commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

What

A complete theming system for @vllnt/ui: an OKLCH token pipeline, full-theme runtime presets, and a ui.shadcn.com/create-style web theme editor that re-themes the whole site, with a standard export flow.

Closes #403

Why

@vllnt/ui shipped a single hard-coded theme with no way to author or switch themes beyond light/dark. This adds first-class theming end-to-end.

Changes

1. OKLCH token migration (packages/design/tokens.json, theme CSS, Tailwind config + preset)

  • All color tokens converted HSL-channel → OKLCH-channel via exact conversions (values match shadcn's published OKLCH defaults).
  • Tailwind maps colors as oklch(var(--x) / <alpha-value>) (explicit alpha form), so the ~hundreds of opacity modifiers (bg-muted/30, border-border/70, …) and every component render identically — no component appearance change.
  • Reconciled a latent drift (styles.css dark --background was 3.9% vs 0% elsewhere) and swept hard-coded hsl(var(--x)) usages (inline styles, arbitrary Tailwind values, SVG fills, gradients) in 13 components to oklch(var(--x)).

2. Full-theme runtime presets (@vllnt/ui)

  • themes/presets.css — five complete themes (Blue, Green, Amber, Rose, Violet): each tints backgrounds, surfaces, text, and accent for light + dark, so switching transforms the entire UI. Applied via html[data-theme="<name>"]. Default stays the neutral black/white theme.
  • New exports: ThemeSwitcher, ThemePresetProvider (pre-paint restore), useThemePreset / setThemePreset, and setCustomTheme for user-authored themes.

3. Web theme editor (/themes)

  • Per-token OKLCH color pickers (native input ↔ OKLCH via a dependency-free oklch.ts), radius, light/dark, and a focused preview.
  • Picking a preset or editing a token re-themes the entire site live and persists across navigation (preset → data-theme; custom edit → injected stylesheet via setCustomTheme; both restored before paint). State is also encoded in the URL (shareable).
  • Export panel: a CSS :root/.dark block, a npx shadcn@latest add <origin>/r/themes?t=… command (decoded by the dynamic /r/themes route into a shadcn registry:theme), and tokens.json — copy/download.
  • The color swatches are intentionally not in the navbar; theme switching lives on /themes and persists site-wide.

Verification

  • Gates green: pnpm -F @vllnt/ui lint, tsc (both packages), pnpm build (incl. registry:build + shadcn build), pnpm test:once, check:use-client, story coverage, check:circular.
  • /r/themes builds as ƒ (Dynamic), /themes as (SSG, en + fr).
  • Browser-verified (agent-browser, desktop + mobile): default is neutral B/W; picking a preset (e.g. Blue → --background: 0.17 0.022 264) re-themes the whole site (header, sidebar, surfaces, accent); the theme persists across navigation; a token edit switches to data-theme="custom" with an injected stylesheet; Reset returns to B/W; the navbar has no color switcher; zero console errors. The /r/themes endpoint returns valid registry:theme JSON. Existing component pages render correctly (OKLCH migration is visually inert).

Notes

  • Stays on Tailwind v3 using OKLCH channel vars (not full-color), which keeps opacity modifiers working without a v4 migration; exports still emit standard full-oklch(...) for consumers.
  • apps/registry/registry/default/* and component-metadata.json are generated by registry:build and intentionally not hand-committed.

Migrate the color token pipeline from HSL-channel to OKLCH-channel and add a
shadcn-style theme creator with a standard export flow.

- tokens: convert packages/design/tokens.json and the theme CSS to OKLCH
  channels (exact conversions). Tailwind maps colors as
  `oklch(var(--x) / <alpha-value>)`, so every opacity modifier and component
  renders identically.
- @vllnt/ui: runtime theme presets applied via `data-theme` — new ThemeSwitcher,
  ThemePresetProvider, useThemePreset, and themes/presets.css (six presets).
- registry: a /themes editor with per-token OKLCH controls, radius, light/dark,
  and a live preview. Exports as a CSS `:root`/`.dark` block, a `npx shadcn add`
  command (served by the dynamic /r/themes route), or tokens.json. Theme state is
  encoded in the URL so it is shareable.
- docs: README + ARCHITECTURE updated for OKLCH and the new theming surface.

Closes #403
Comment on lines +10 to +12
const FOUC_SCRIPT = `(function(){try{var v=localStorage.getItem(${JSON.stringify(
THEME_PRESET_STORAGE_KEY,
)});if(v&&v!==${JSON.stringify(
@vllnt-pilot

vllnt-pilot Bot commented Jun 5, 2026

Copy link
Copy Markdown

Preview ready · Updated 2026-06-07T22:58:36Z

Service Status Preview
ui-registry Ready https://pr-404-ui-registry.preview.vllnt.ai
Inspect
  • Tailnet-only by default (not reachable from public internet)
  • Cert: real Let's Encrypt wildcard
  • Reply with /clean to destroy this preview now

Refine the theming system per design feedback:

- Presets are now COMPLETE themes (tinted backgrounds, surfaces, text, and
  accent), so switching transforms the entire site. The default stays the
  neutral black/white light + dark theme.
- The /themes editor applies the chosen preset or custom edits to the whole
  document live and persists across navigation (data-theme + an injected
  stylesheet, restored before paint via the FOUC script).
- Add `setCustomTheme` to @vllnt/ui for user-authored themes.
- Remove the color swatches from the navbar; theme switching now lives on the
  /themes editor.

Part of #403
Comment on lines +15 to +17
)});if(!v||v===${JSON.stringify(
DEFAULT_THEME_PRESET,
)})return;document.documentElement.setAttribute("data-theme",v);if(v===${JSON.stringify(
Comment on lines +17 to +19
)})return;document.documentElement.setAttribute("data-theme",v);if(v===${JSON.stringify(
CUSTOM_THEME_NAME,
)}){var c=localStorage.getItem(${JSON.stringify(
Comment on lines +19 to +21
)}){var c=localStorage.getItem(${JSON.stringify(
THEME_CUSTOM_CSS_STORAGE_KEY,
)});if(c){var s=document.createElement("style");s.id=${JSON.stringify(
Comment on lines +21 to +23
)});if(c){var s=document.createElement("style");s.id=${JSON.stringify(
THEME_CUSTOM_STYLE_ID,
)};s.textContent=c;document.head.appendChild(s);}}}catch(e){}})();`;
Replace the generic color presets with six hand-crafted, named full themes —
Matrix, Dracula, Synthwave, Tron, Cyberpunk, and Nord — each a complete OKLCH
palette (background, surfaces, text, and accent) for light and dark. Default
stays the neutral black/white theme. Switching a preset transforms the whole
site (e.g. Matrix = phosphor green on black).

Part of #403
bntvllnt added 3 commits June 7, 2026 15:02
- Fix live editing: editing a token value now re-themes the entire site live
  (not only on preset select). Each token gains a reliable editable OKLCH
  channel field next to the swatch; the native color input alone did not fire
  state updates dependably.
- Add six named themes with full OKLCH palettes — AI: Claude, ChatGPT, Gemini;
  2026 trends: Future Dusk, Cyber Lime, Aura — bringing the set to 13 presets
  (plus the neutral Default).

Part of #403
…d E2E

Code review of the theming branch surfaced and fixed:

Security
- Validate theme channel/radius values in decodeTheme: a shared `?t=` URL is
  untrusted, and unvalidated values flowed verbatim into an injected `<style>`
  (CSS injection). Sanitize values in setCustomTheme (@vllnt/ui) as
  defense-in-depth, and cap `?t=` length on the /r/themes route.

Correctness
- oklchChannelsToHex returns black for non-numeric input (no invalid color value).
- export-panel: handle clipboard rejection; defer blob URL revoke.
- theme-serialize: radius uses `??` not `||`.
- ThemePresetProvider: route defaultPreset through setThemePreset so the external
  store (ThemeSwitcher) stays in sync.
- Nudge dusk/gemini dark primary-foreground for WCAG contrast headroom.

Tests (enforced)
- @vllnt/ui (vitest): use-theme-preset (incl. injection sanitization),
  theme-switcher, theme-preset-provider.
- apps/registry: vitest unit (oklch, theme-serialize incl. injection rejection,
  /r/themes 200/400) + Playwright E2E (editor flow + API). Pin
  eslint-plugin-react-hooks via override to avoid lockfile drift.

Part of #403
…stem

# Conflicts:
#	apps/registry/app/[locale]/layout.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Theming system: OKLCH tokens, runtime presets, and a web theme editor

2 participants