feat(theming): OKLCH tokens, runtime presets, and web theme editor#404
Open
bntvllnt wants to merge 6 commits into
Open
feat(theming): OKLCH tokens, runtime presets, and web theme editor#404bntvllnt wants to merge 6 commits into
bntvllnt wants to merge 6 commits into
Conversation
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( |
|
Preview ready · Updated 2026-06-07T22:58:36Z
Inspect
|
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
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A complete theming system for
@vllnt/ui: an OKLCH token pipeline, full-theme runtime presets, and aui.shadcn.com/create-style web theme editor that re-themes the whole site, with a standard export flow.Closes #403
Why
@vllnt/uishipped 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)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.styles.cssdark--backgroundwas3.9%vs0%elsewhere) and swept hard-codedhsl(var(--x))usages (inline styles, arbitrary Tailwind values, SVG fills, gradients) in 13 components tooklch(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 viahtml[data-theme="<name>"]. Default stays the neutral black/white theme.ThemeSwitcher,ThemePresetProvider(pre-paint restore),useThemePreset/setThemePreset, andsetCustomThemefor user-authored themes.3. Web theme editor (
/themes)oklch.ts), radius, light/dark, and a focused preview.data-theme; custom edit → injected stylesheet viasetCustomTheme; both restored before paint). State is also encoded in the URL (shareable).:root/.darkblock, anpx shadcn@latest add <origin>/r/themes?t=…command (decoded by the dynamic/r/themesroute into a shadcnregistry:theme), and tokens.json — copy/download./themesand persists site-wide.Verification
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/themesbuilds asƒ(Dynamic),/themesas●(SSG, en + fr).--background: 0.17 0.022 264) re-themes the whole site (header, sidebar, surfaces, accent); the theme persists across navigation; a token edit switches todata-theme="custom"with an injected stylesheet; Reset returns to B/W; the navbar has no color switcher; zero console errors. The/r/themesendpoint returns validregistry:themeJSON. Existing component pages render correctly (OKLCH migration is visually inert).Notes
oklch(...)for consumers.apps/registry/registry/default/*andcomponent-metadata.jsonare generated byregistry:buildand intentionally not hand-committed.