docs(theme): add theme studio - #6675
Open
mikenewbon wants to merge 265 commits into
Open
Conversation
Replaces the fixed dark/medium gray options (clustered at the dark end) with a 'Neutral shade' mode: two sliders choose any of the 11 neutral ramp shades independently for light and dark mode, with live swatches. Two single sliders rather than one dual-thumb range because the two values are independent and may cross — a range slider enforces ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e style offsets Border color gains the same per-mode neutral-shade slider pair as shadows, and a new Background section drives --ui-bg per mode the same way (a token shorthand on the style axis until a full tokens editor exists). The palette editor's trigger moves into the section header as a Custom button, and 'Use this palette' is replaced by a style-offset dropdown (Fitted/Pastel/Muted/Vivid/Dazzling) applying taste transforms from the fitted base so repeated picks never compound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Page layout: the toolbar spans full width with the sidebar toggle next to the title (badge now 'Concept'), and the settings panel sits under the toolbar row. Every sidebar section collapses via its header. Fixes the preset regression: setStyle only removes the tokens the previous style emitted instead of blanket-clearing shared variable names, so a preset's --ui-bg survives style clicks; and applyDoc now derives bgShade from preset tokens where they are neutral-ramp refs, so the background sliders reflect the preset instead of stale defaults. Also restores the neutral palette editor's lost open binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Background pattern generalizes into a whitelist-driven tokenShades map: Inverted, Highlighted, Muted and Dimmed each get their own collapsible section with per-mode neutral-shade sliders (Background open by default, the rest collapsed). Presets hydrate all of them via the same token parser, and the legacy bgShade field folds in for persisted sessions. Pastel offset redefined per feedback: compress the lightness range from both ends (light end down, dark end up) while RAISING chroma — candy pastels instead of washed-out tints. Vivid/dazzling rebalanced, and the lightness remap preserves the fitted curve's shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correctness:
- Style class bundles move to their own channel (nuxt-ui-style-ui) and
compose with preset/AI overrides via class-concatenating mergeUi —
restyling can no longer destroy a preset's or the AI's component
overrides, and doc components + style expansion now merge identically
in live apply and exports (spread was dropping one side).
- resetTheme() clears ALL studio state (style prefs, style bundle,
palette params/prev, active preset) so the popover and chat reset
buttons no longer leave orphaned styles that resurrect on the next
click or leak invisible bundles into exports.
- tokenShades are per-mode optional: hydrating a dark-only preset token
no longer invents a light override that leaks into exports untouched.
- currentDoc references palettes by the alias's current value, so a
custom ramp shadowing a default name ('green') still exports.
- applyThemeSettings guards the neutral alias against the SANITIZED
palette set (a rejected palette could previously still be selected,
bricking every neutral token), and takes a track option so slider
drags stop emitting ~16 analytics events per second.
- Removing a custom palette restores the palette that preceded it
instead of the hardcoded default; neutral remaps skip tokens the
shade sliders own (no more two-writer flip-flops on --ui-bg).
- Palette editor: style offset resets to Fitted on reseed, watchIgnorable
scopes seed suppression to its own writes, self-applies no longer
round-trip through the echo watcher; theme-color meta follows the
neutral-50 baseline in light mode.
Perf: shade-only style edits skip the 7-component teardown/re-merge.
Cleanup: shared ThemeStudioShadeSlider component, SHADES/defaults single
sources, shared parseUiColorRef, token-keyed sections, dead exports and
never-shipped legacy paths (bgShade, anchor/vibrance) deleted, FOUC
plugin restores the style bundle with the same merge as the live path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New settings: - Defaults section: app-wide default variant and size, expanded into per-component defaultVariants (the runtime channel Nuxt UI already honors) only where the component supports the value — form fields have no solid variant and keep their own default. - Shadow geometry sliders (offset X/Y, blur, spread) driving CSS variables referenced by one static class shape, plus an opacity slider folded into the shadow color via color-mix with per-treatment fallbacks (solid for custom, 25% for soft). 'Hard' renamed 'Custom'. - Soft shadows now respect the color/shade options — tailwind's stock shadow color is hardcoded black, and the bare shadow-(--var) form parses as a shadow SIZE var; shadow-(color:--var) disambiguates. - Sidebar groups (Colors/Style/Tokens/General) become a UAccordion with leading chevrons, built-in separators and unpadded panels; the shade option in both color selects is now labeled 'Custom…'. Fixes: - Studio prefs and palette params are restored on load again — the useState conversion (for popover-reset clearing) lost the client restore that useLocalStorage had provided; the FOUC plugin now rehydrates both keys. The persisted style class bundle also self-heals when the generator changes, so stale expanded classes can't outlive their source. - Layout: toolbar returns beside the sidebar atop the preview column (with the sidebar toggle and Concept badge kept). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion headers Color mode moves out of the accordion to UTabs pinned at the top of the sidebar. Group panels are a single-open UAccordion (one section of the studio in focus at a time), and the sub-section collapsibles become ghost buttons with leading rotating chevrons. Shade options in the color selects read 'Custom…'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
track() accepts primitive property values only; the style object now carries nested geometry/token maps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
|
Planned next (roadmap for this concept):
Demo videos coming soon. |
commit: |
- five studio palettes beyond tailwind (sand/sage/ash neutrals, cocoa/marine primaries) defined in @theme static; exports inline their ramps since consumer builds can't resolve them - shuffle now rolls frames, border/shadow colors and app-wide default variants at tasteful weights - palette offsets get an effect-strength slider (0-200%) that lerps between the fitted base and the offset target, extrapolating past 100% for stronger takes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… per-group default variants - Tokens gains a Primary section: light/dark sliders remapping --ui-primary along the primary ramp (library defaults 500/400 added to LIBRARY_TOKEN_DEFAULTS so at-default positions export nothing) - shadow/border color selects gain 'Primary shade…' — the existing per-mode shade sliders walk the primary ramp instead of neutral - default variant select split per component group (Buttons/Cards/ Inputs); the app-wide value from presets/shuffle shows through as the fallback until a group chooses its own Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both inline their ramps from CUSTOM_PALETTES so exports stay self-contained, and double as living tests of the new axes: per-group button defaults (Cabin) and primary-shade shadows with a dark-mode primary shade remap (Harbor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member
|
No way @mikenewbon this is awesome! 🔥 I was planning to build this for v5 but let's go!!! Since this replaces the ThemePicker from the header I think we could use a different layout like HeroUI does: https://heroui.com/en/themes Will add more to the review tomorrow 😊 |
…t oklch, not hex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and spacing knobs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cising the new token axes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sidebar sections Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mantic colors Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipeline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed with their selects Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…variant set Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…at layouts, presets in sidebar Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverts collateral edits that were out of scope for a docs branch: the em dash replacements across src/runtime/types, test/bench and the AI prompt text, the devtools toggle and the .gitignore entry. Also fixes the ring-N scanner in the theme importer, whose consumed separator hid every second adjacent token, and stops the theme engine specs running in both vitest projects when they need neither.
Shadcn, Bootstrap, Anthropic and Spotify become Mono, Cobalt, Parchment and Signal, with generic lucide glyphs. Cobalt's palettes were named bs-blue/bs-gray, which put the brand in every exported main.css, so they are renamed too.
Both pickers are already popovers, so holding them in a second one was a popover inside a popover. Drops the headings treatment controls with it.
A renamed preset leaves an id naming nothing while the theme it applied still sits in the other storage keys, so the pickers read Custom against a page no preset can name.
Every setting owned a localStorage key, sixteen of them, and two of those were derived caches of the others. Restores could interleave, so the derived stores each needed a self-heal to reconcile, and eight FOUC scripts each re-read their own key and re-derived a slice of useTheme's style tags. One key writes atomically, so those states cannot disagree. The style class bundle and the custom-palette ramps are rebuilt from their source on load instead of stored, which removes two of the three heals outright. Reads and writes both live in the plugin, so no composable touches localStorage. The nine keys the shipped picker wrote migrate once per browser. Also groups the theme utils under utils/theme/, with the 646-line theme.ts split by concern and theme-engine moved to utils/theme/engine.
They only ever existed as static per-component class bundles, including a verbatim copy of the private `ssr()` modifier chain from `src/theme/tabs.ts`, plus site-wide `--shadow-*` and `--default-border-width` overrides in `main.css`. They come back in v5 on real tokens. An 8-bit export drops from 389 config lines to 68. Fonts become three independent stacks rather than a family plus a heading treatment. Preflight already wires `--font-sans` and `--font-mono`, so only the serif needs an element rule, and it is emitted only when one is set. The two runtime channels behind them collapse into one, and the engine's nine modules into five.
Carbon, Orchard and 8-bit all set a border darker than the surface it sits on, the opposite of the stock convention. That read as deliberate while the borders were 2px with hard shadows behind them; at a stock 1px hairline they became muddy seams, and 8-bit's literal black on a 21% background vanished. Light keeps the ink, which is those themes' whole identity.
Every picker was a hand-rolled popover, listbox and search input. USelectMenu already does grouped items, an async-friendly search and custom rows, so that component is gone and both callers use it directly. Typography collapses into one panel with the three stacks grouped and offering every category, so a mono heading is possible; radius and the default variants each get their own control. `--spacing` is dropped entirely, exposing tailwind's base unit as a theme knob was never good practice. Fullscreen goes too. Its job was hiding the site chrome, which the page now does unconditionally, so what remained was a mousemove listener and a manual Esc handler buying edge-to-edge width. Controls report divergence from the preset by turning primary instead of carrying a chip, gated on mount because the theme is restored before first render and hydration adopts the server's markup without patching.
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.
This is a concept/prototype, not a finished feature — opening early for direction feedback.
What it is: a standalone
/themepage (header nav) that turns the existing popover into a full theme studio. Everything is a view over one idea: a theme is a sparse set of overrides on the chain Tailwind palette → aliases → semantic tokens → component variants — so the CSS export stays minimal by construction, and the two files split correctly betweenmain.cssandapp.config.ts.docs:scope only — no library code touched.Features
main.css+app.config.tsback in and get the doc it came from; anything outside the grammar is surfaced rather than silently dropped.Thinking
The theming primitives in v4 are already excellent — this makes them visible and manipulable, Blender-style, without inventing a parallel system. Along the way it surfaces concrete core gaps, which are now tracked as open questions 5 and 6 on the v5 roadmap (#6610): a runtime global
defaultVariants.variant, and whether light--ui-bgshould follow the neutral ramp.Known limitations & open questions
These are the parts I'd most like direction on, since several depend on decisions that aren't mine to make:
--ui-shadow-*/ border-width tokens to drive yet. Epic A2 of v5 #6610 proposes exactly that scale — when it lands, most oftheme-engine/styles.tsshould collapse into consuming it. Until then a bold preset'sapp.config.tsis verbose: 8-bit is ~590 lines, neo-brutalist ~630 (the CSS half stays small, 80–90 lines). I've folded compound entries that select the same variants to cut that ~10%, but the real fix is the token.ssr()modifier chain fromsrc/theme/tabs.tsverbatim, so it lands in the user'sapp.config.ts. If that internal changes — Epic C mentions possibly namespacingdata-slot— those overrides silently go stale. Worth a public hook, or accepting that tabs overrides are provisional.test/utils/theme-*.spec.ts, 290 tests) and import across the roottsconfig.jsonexclude: ["docs"]boundary. They'd be better as a docs-scoped vitest project; happy to move them.theme-engine/+ its tests), the runtime plumbing (useTheme/plugins/theme), the studio shell, the palette/curve editors, and the preview surfaces are all separable. Say the word and I'll break it up.Built with Claude Code 🧵