docs(theme): add theme studio - #6675
Open
mikenewbon wants to merge 254 commits into
Open
Conversation
Opening the editor no longer hijacks the theme (preview only until a user edit or explicit 'Use this palette'). With a custom palette active, the standard swatches re-anchor the custom ramp to the chosen palette instead of being dead ends, and an always-visible ✕ on the trigger row drops the custom palette entirely. Adds parseCssColor (hex/oklch/rgb) so anchors can seed from tailwind JS values or the docs' CSS-only palettes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The base lightness curve was taken from tailwind's gray scale, so color ramps came out far too dark at the 800-950 end (dL up to -0.17) and the too-light wings gamut-clamped their chroma away. Rebased the curve on tailwind's color ramps, widened the chroma bell, gave neutrals their own deeper dark end, and widened the extreme-anchor guard so near-black anchors still produce distinct shades. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the hex-anchor abstraction: a palette is now three transition curves (lightness, chroma, hue) edited in a devtools-easing-style square with draggable endpoint Ys and free handles, one tab per channel. Any existing palette (tailwind or the docs' CSS-only neutrals) is reverse-fitted into curves via coordinate-descent so editing always starts from the real thing — swatch clicks while a custom palette is active refit to the chosen palette, resetting the scales. Custom neutrals also re-route the hardcoded white tokens (light --ui-bg / --ui-text-inverted, dark --ui-text-highlighted) through the ramp so a tinted neutral finally reaches the app background in light mode; the remaps are cleaned up on removal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Curve edits now apply on a 60ms throttle (leading + trailing) instead of a trailing debounce, so the whole page restyles during the drag. A temporary html class enables short background/color/border transitions (excluding the curve editor itself) so throttle ticks glide instead of stepping, and the analytics event is rate-limited to one per burst. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ition Light-mode --ui-bg now follows the neutral ramp app-wide (main.css + docs render baseline) instead of hardcoded white, so tinted neutrals show without editing anything. The engine gains LIBRARY_TOKEN_DEFAULTS — the stock @nuxt/ui token values — and export diffing compares against those rather than the docs baseline, so themes that rely on the ramp-following background still export it for stock installs. Drag transition matched to the 60ms apply throttle (was 150ms lagging behind the pointer). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…line The :root baseline rule shared specificity with the library's .dark override and won on cascade order, painting dark mode with the light 50. Scope it with :root:not(.dark). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a style axis to the studio modeled on Shadcn Studio's configurator: Shadows (none/soft/hard offset) and Borders (thin/bold), expanded into static per-component class bundles over the app.config ui path — the stopgap for the missing --ui-shadow/--ui-border-width core tokens. Hard shadows use --ui-border-inverted so they stay black-on-light and white-on-dark, with the classic press-down hover on buttons. Ships a Neo-brutalist preset (hard shadows, bold borders, radius 0, yellow primary, black borders via token remaps) and teaches shuffle to occasionally roll style treatments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
outline/subtle variants take their rings from theme compoundVariants, which render after slot-base classes — so the bold bundle's width AND inverted color were both dropped there (visible as 1px primary/error/ success rings in dark mode). The bundle now also ships extension compoundVariants for those variants on button/badge/alert, which append after the theme's and win the merge; inputs keep the slot-level override that already worked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bold now only thickens borders that already exist, keeping each element's own ring color — ghost/solid/soft variants no longer gain frames they never had. Hard shadows switch from --ui-border-inverted (white in dark mode, reading as a glow) to a new --ui-shadow-color variable: near-black over light surfaces, pure black in dark, defined in the docs CSS and emitted by generateCSS whenever the hard treatment is active so exports stand alone. Ghost/link buttons cast no shadow. The uniform outlined-everything look moves into the Neo-brutalist preset itself (per-mode border token remaps plus explicit component frames on solid/outline/subtle/soft, ghost stays flat). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… palette Border and shadow colors become first-class settings: Borders gain a Frame option (outlines solid/soft surfaces too; ghost/link stay flat) plus a color select (inverted/black/white/primary/neutral), Shadows gain graded colors (black/dark/medium gray/inverted/primary — pure black disappears against dark surfaces, which also made the previous defaults invisible in dark mode). Colors ride two CSS variables so every combination is a variable swap over static class bundles; all ring overrides move to extension compoundVariants after discovering card and input rings live at variant level and silently beat slot overrides. Studio CSS baselines are now :where()-wrapped — unlayered rules beat the library's @layer theme but zero specificity lets user theme overrides win regardless of load order (previously --ui-shadow-color choices and preset --ui-bg overrides lost the cascade to main.css). The Neo-brutalist preset adopts the reference palette (#FF3333/#FF6666 custom red ramp, yellow secondary, pure white/black surfaces) and is now pure configuration. The preview becomes a masonry column layout with five new tiles: Table, Stats, Chat, Team, Notifications. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Presets grow to nine with Art Deco (gilded amber lines via primary border color), Marshmallow (pastel pink on mauve), Ghibli (custom moss ramp on sun-warmed amber paper) and Marvel (comic red/blue with black frames and hard shadows) — translated from shadcn studio's registry values into ThemeDoc form, each composing the new style axes rather than bespoke component hacks. The sidebar gets compact: primary/neutral collapse into popover swatch rows with live color dots, radius becomes a slider, everything fits without scrolling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 😊 |
…goes outline The preset tiles now use the same listbox the view switcher does, down to the checked row's elevated tint — so the two grids in the studio's chrome read as one pattern, and the tiles get roving focus and listbox semantics for free. The view switcher keeps ghost for the site header and takes outline from the toolbar through $attrs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
`chip` is ListboxItem's own prop and types as ChipProps, so the swatch's style object landed in :style as a chip config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
…y the classes it needs Reset buttons stop swapping colour by hand: neutral, active on dirty, outline when active, still disabled when there's nothing to reset. The variant grid marks its pick the same way, keeping each cell in the variant it demos. The preset listbox drops the overrides that were carrying their own weight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Updated comments across various components to enhance clarity and readability. Changes include rephrasing for better understanding and consistency in style. This refactor aims to make the codebase more maintainable and easier for future developers to navigate.
… stops The border select and the token shades were painting different things: the select emitted one --ui-border-color for every component ring, while the four border tokens rode the neutral ramp no matter what it said. They are one control now — the select names the ramp, each token owns its stop, and rings follow the default token. That collapses the option lists everywhere. A shade source names a ramp, so the stops below it can express what Black, White, Inverted and a fixed Primary used to be. All three treatments read Neutral / Primary, with Inherit only where it still means something. A value a saved theme holds but the picker no longer lists stays selectable rather than blanking the trigger. Also: the landing hero's shimmer follows --ui-primary instead of ramp stops, so black-as-primary no longer leaves it on the last palette; ?view= keeps the preview across a reload; and the view grid folds into the switcher that was its only caller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
…ders
`?? {}` inline in the v-for widened the item to never, so the optional prop
typechecked locally and failed CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
…ndefined
Optional sliders left the v-for item as never with a `?? {}`, and possibly-
undefined behind a Partial. An empty record of the right shape is neither.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Updated the FOREGROUNDS and BACKGROUNDS arrays to include class bindings for each token, improving the styling capabilities of the theme studio. Additionally, implemented a MutationObserver to handle theme updates more effectively during component lifecycle events. This change ensures that the theme styles are applied correctly when the document head is modified.
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 🧵