docs(theme): exploring a full theme customizer#6675
Open
mikenewbon wants to merge 171 commits into
Open
Conversation
Plan for the Nuxt UI theme customizer: standalone /theme page in the main header nav, ThemeDoc + provenance resolver engine, OKLCH palette curve editor, inheritance drill-down, presets + shuffle, minimal CSS/app.config exports, bento + template previews, and a phased roadmap with upstream core-gap PRs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Standalone /theme page in the main header nav: Essentials controls (primary, neutral, radius, font, icons, color mode) sharing state with the existing popover via useTheme, a live component bento preview, seeded-taste shuffle, minimal-diff export panel (main.css + app.config), and an Open Theme Studio link in the popover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure theme-engine module (ThemeDoc schema, provenance resolver, minimal CSS/app.config export generators) with unit tests; useTheme now snapshots its state as a ThemeDoc and delegates exports to the engine. New useThemeStudio composable applies docs through the sanitized applyThemeSettings path, powering a Presets menu (Nuxt UI, Shadcn, Anthropic, Spotify) and a doc-based shuffle in the studio toolbar. Also fixes the theme-color computed crashing on custom neutral palettes (undefined tailwind lookup aborted the whole unhead flush, so injected palette styles never rendered). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dependency-free sRGB<->OKLCH math (gamut clamping, WCAG contrast) and a parametric ramp generator: anchor hex pinned exactly at shade 500 with adjustable light/dark endpoints, vibrance, chroma spread and hue drift. Inline editors under the Primary/Neutral pickers live-apply the generated ramp through the doc path with a WCAG badge and swatch preview; curve params persist across reloads and exports stay minimal (unreferenced palettes are now filtered out of the doc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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: |
- Introduced logic to restate library dark defaults for tokens overridden in light only, ensuring proper rendering in dark mode. - Updated the import process to remove dark defaults that match light overrides, preventing unnecessary token growth during round-trips. - Added tests to verify the correct behavior of CSS generation and token handling for light and dark themes. These changes improve the consistency and efficiency of theme management, particularly in handling token overrides across different modes.
- Adjusted the step value for saturation and hueShift effects to 1 for finer control. - Enhanced the tooltip text to reflect the state of modifiers more accurately, indicating when no modifiers are active. - Implemented keyboard navigation for numeric readouts, allowing users to adjust values using arrow keys and commit changes with Enter. These updates improve user interaction and precision in the Theme Studio component.
…tion - Replaced the snap function with a clamp function to ensure precise value handling within defined limits, enhancing the slider's behavior. - Updated keyboard navigation to utilize the new clamping logic for better user experience when adjusting values with arrow keys and committing changes. These changes refine the interaction model for the Theme Studio component, ensuring more accurate value adjustments.
Route the theme studio's own icons through the applied icon pack so the
whole studio skins to the selected theme instead of staying Lucide —
toolbar controls, header (Ask-AI/GitHub/theme picker), preset menu, dice,
view switcher, the ⌘K search links, every preview template, and the
component wall.
- Add per-pack icon maps kept OUT of the exported theme config:
studioIcons (chrome), studioExtraIcons (functional demo glyphs,
Lucide-default + pixel overrides), studioViewIcons (view switcher),
resolved through reactive (toReactive) composables.
- Add a pixelarticons ("Pixel") set and point the Comic preset at it.
- Remount the preview on icon-pack change (:key) so the demos, which
resolve icons into plain data at setup, re-resolve on a live swap.
Also on this branch:
- Tabs: the pill list now follows the shadow treatment (was flat); strip
the stock trigger SSR shadow via the library's own modifier chain.
- Persist custom palettes + modifiers reliably across reload (self-heal
re-derives them from the editor curves after the plugin restore).
- Two-stage reset button, custom slider values, F fullscreen shortcut.
- Fix dark-mode export: restate library dark defaults for light-only
token overrides so `:root, .light` can't win the source-order tie.
- Source Serif 4 headings for the Anthropic preset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an opt-in 19-stop ramp (the eight 100-step midpoints alongside Tailwind's standard 11) selectable per custom palette via a dropdown in the gradient square, and expose the midpoints to the token shade sliders. Value-based stop positioning keeps the standard 11 byte-identical whether fine or not. Midpoint tokens reference --color-custom-<ramp>-<mid> directly (defined by @theme static) since the runtime plugin only emits --ui-color-* for the standard stops — referencing those for a midpoint dangled and rendered transparent. Guard the transitions between modes: narrowing 19→11, clearing, or switching to a stock ramp snaps any orphaned midpoint token back to the nearest standard stop; seeding infers fine stops from a source palette's midpoint keys so an imported fine ramp keeps them on edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Changed stop labels from 'shades' to 'stops' in the palette editor for clarity. - Refactored the layout of the ThemeStudioCurveEditor and stop-count picker to improve usability and prevent overlap with other UI elements. - Adjusted the stop-count picker to be more responsive and integrated into the design flow. These changes aim to enhance the user experience in the Theme Studio by providing clearer terminology and a more intuitive interface.
- Replaced the swatchInfo computed property with a more efficient swatches property to reduce computational overhead during drag events. - Introduced a single activeShade computed property to manage the active swatch state, improving performance by minimizing re-renders. - Enhanced the activeSwatch computed property to handle detailed color parsing only when necessary, deferring costly operations. - Updated the rendering logic to utilize a single popover for swatch details, significantly improving responsiveness and reducing the number of rendered components. These changes aim to enhance the performance and user experience of the Theme Studio by streamlining swatch interactions and reducing unnecessary computations.
…mance Pins: - Lock stops to an exact OKLCH; the ramp bends through them via a localized RBF correction, so you can pin a brand colour and edit around it. Editable hex/rgb/oklch readouts, multi-pin, markers on the strip and curve. - The curve editor draws the pin-corrected curve so the line rides through pinned stops and shows their pull on neighbours; every dot sits on the value its colour actually resolves to. Performance (live curve drags): - CSSOM fast path during a drag — write ramp vars straight to :root, skipping the stylesheet reparse, reactive re-inject and per-frame localStorage write; the reactive path reconciles once on release. - content-visibility on off-screen grid tiles so a theme change only restyles what's visible (~60% fewer elements on the component wall). - Colour field rendered on a canvas (small bitmap scaled with bilinear smoothing) instead of 24 SVG gradients behind a Gaussian blur — that filter was re-rasterised on every restyle (~7ms per apply). Fixes: - Undo/redo restores the palette editor's curves/pins (composite snapshot; proxy-safe clone) instead of leaving stale curves that clobber on the next edit. - The editor now follows a preset's custom palette (e.g. 8-bit, Marshmallow neutrals) rather than resetting to blank grey defaults. - No white flash between presets (reset skips the imperative <style> clear when a re-apply follows in the same tick). - Dragging a curve endpoint carries its handle; stop picker moved to the Modifiers row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin persistence was silently failing for every pin after the first: setPin rebuilds the array from a deeply-reactive ref, so kept elements came back as Vue proxies that toRaw() doesn't unwrap, and structuredClone threw DataCloneError inside applyReactive — aborting the apply before it saved. A plainPins() helper rebuilds each pin from its scalar fields so persistence can't throw. The curve editor's endpoint handles were drawn at the raw bézier control, so a pin near the edge (or on it) pulled the corrected line away while the handle stayed put — visibly detached. Endpoints now sit on the corrected curve; a pinned endpoint is locked (the pin owns it) and dragging an unpinned one backs out the RBF pull so it still tracks the pointer. Also folds in the PR review batch: - pin RBF: spacing-adaptive sigma + hue chain-unwrap (no white collapse / long hue arc), with regression tests - self-heal no longer re-fires the primary setter every load - resetSection preserves curves for untouched custom palettes - external-sync guard compares pins and stop density, not just curves - paletteShades samples fine stops; reactive per-seed hue window - contrastRatio returns null instead of fabricating black - midpoint tokens promote through parseUiColorRef - SSR FOUC scripts and import/export parser hardened - font name re-validated at the <style> sink Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
paletteShades sampled the fine stop set against tailwind's JS colors, but a stock ramp has no midpoints (150…850) — those lookups are undefined, and parseColor threw on .trim() before the filter could drop them. The throw blew up seedFromCurrent's watcher, so opening the editor on a default (stock) colour failed and later applies stopped landing. Skip absent shades, and make parseColor treat any non-string as unparseable rather than throwing. Also fixes the contrastRatio test now that it returns number | null. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Introduced a lastDetail ref to hold the last active swatch details after it goes inactive, preventing placeholder flashes during popover animations.
The stop density was a boolean (11 or 19 stops), so the ramp couldn't be subdivided further. It's now the interval between stops — 100, 50, 25 or 10, giving 11, 19, 37 or 91 stops — carried per palette as `stopStep`. `fineStops` is still read so palettes saved before this keep their 19. Everything that special-cased the 150–850 midpoints now works off the ramp's own stop set instead: the CSS variable indirection, the orphaned token-shade snapping and the shade slider ladders. The swatch strip keeps the height it has at 11 stops so a denser ramp only narrows the tiles, and the pin badge takes the contrasting neutral end rather than a blend mode, which vanished on mid-tone stops. Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
Author
|
Original description was written at v0. Full inventory of what's actually in here now. Still Palette editor
Colours and tokens
Typography
Icons
Style axes
Presets
Preview views
Import / export
Session
Tests
Open
|
The density model replaced `fineStops: boolean` with `stopStep: ShadeStep` and dropped SHADES_FINE, but the specs kept calling the old signature. Booleans coerce at runtime so the tests still passed; only vue-tsc caught it, failing CI after lint went green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ments Entering custom shadow sets `shadow: 'hard'` but leaves shadowColor unset, and styleTokens only emitted --ui-shadow-color from shadowShade when shadowColor was explicitly 'shade'. So moving the shade slider did nothing until the colour dropdown was toggled. Treat an unset colour on a hard shadow as the shade default (a pristine theme still emits nothing). Also a conciseness pass over the comments added across the recent theme-studio work — same intent, less prose. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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. I'll add demo videos tomorrow.
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 exports stay minimal by construction (only what you changed, split correctly betweenmain.cssandapp.config.ts).Features:
--ui-shadow*/--ui-border-widthtokensThinking: 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 worth discussing for v5: semantic shadow/border tokens, a runtime global
defaultVariants.variant, and whether light--ui-bgshould follow the neutral ramp.Known prototype limitations: engine tests sit in the library test tree; style class bundles restore post-hydration (brief restyle on reload); palette fitting is ~13ms/click;
docs:scope only — zero library code touched.Built with Fable 🧵