Skip to content

docs(theme): exploring a full theme customizer - #6675

Open
mikenewbon wants to merge 235 commits into
nuxt:v4from
mikenewbon:docs/theme-studio-plan
Open

docs(theme): exploring a full theme customizer#6675
mikenewbon wants to merge 235 commits into
nuxt:v4from
mikenewbon:docs/theme-studio-plan

Conversation

@mikenewbon

@mikenewbon mikenewbon commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

This is a concept/prototype, not a finished feature — opening early for direction feedback.

What it is: a standalone /theme page (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 between main.css and app.config.ts.

docs: scope only — no library code touched.

Features

  • Palette editor — devtools-style bézier curves for lightness/chroma/hue per ramp; any existing palette reverse-fits into editable curves; per-stop pinning; taste modifiers (lightness/contrast/saturation/hue) over the fitted curve; live-streaming drag with a WCAG readout.
  • Style axes — one config-driven shadow (offset/blur/spread, opacity, colour) plus an independent inner shadow, and a border treatment (width, optional frame, colour) — each with per-mode neutral-shade sliders.
  • Semantic token shades — background/text/border tokens mapped to ramp stops, per light/dark mode.
  • Typography & scale — family, per-step weights, case/tracking/leading, separate heading treatment, radius, UI scale and spacing density.
  • 9 presets (Nuxt UI, shadcn, Neo-brutalist, Orchard, Marshmallow, 8-bit, Anthropic, Spotify, Bootstrap) as plain data docs, plus a seeded shuffle.
  • 10 preview surfaces — component grid, dashboard, chat, SaaS, landing, docs, portfolio, changelog, editor, and a contrast/a11y sheet.
  • Round-trip import — paste an exported main.css + app.config.ts back 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-bg should 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:

  • The shadow/border axes expand to static per-component class bundles, because there are no semantic --ui-shadow-* / border-width tokens to drive yet. Epic A2 of v5 #6610 proposes exactly that scale — when it lands, most of theme-engine/styles.ts should collapse into consuming it. Until then a bold preset's app.config.ts is 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.
  • Exported tabs overrides embed a library-internal selector. Stripping the pill tabs' pre-hydration shadow requires repeating the ssr() modifier chain from src/theme/tabs.ts verbatim, so it lands in the user's app.config.ts. If that internal changes — Epic C mentions possibly namespacing data-slot — those overrides silently go stale. Worth a public hook, or accepting that tabs overrides are provisional.
  • Style class bundles restore post-hydration, so a reload briefly shows the unstyled treatment before the persisted bundle reapplies.
  • Engine tests sit in the library test tree (test/utils/theme-*.spec.ts, 290 tests) and import across the root tsconfig.json exclude: ["docs"] boundary. They'd be better as a docs-scoped vitest project; happy to move them.
  • Palette fitting is ~13ms per click, fine interactively but not free.
  • Scope. This is deliberately one branch for review, but it splits cleanly if you'd prefer that: the pure engine (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 🧵

mikenewbon and others added 21 commits July 5, 2026 03:43
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>
mikenewbon and others added 2 commits July 5, 2026 04:40
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>
@mikenewbon

mikenewbon commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Planned next (roadmap for this concept):

  • Primary-ramp shade options for shadow/border colors (currently neutral shades only)
  • Better palette modifiers — pastel/tinted need tuning; likely an effect-strength slider to scale any modifier up or down
  • Split default-variant selection per component group (buttons vs cards vs inputs)
  • Improved presets, and more of them
  • Template-scale previews (SaaS / Dashboard / Chat) alongside the component bento
  • Richer border options and controls
  • Shuffle coverage for the newer axes (style treatments, palettes)
  • More neutral and primary palettes in the pickers

Demo videos coming soon.

@pkg-pr-new

pkg-pr-new Bot commented Jul 5, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxt/ui@6675

commit: d041311

mikenewbon and others added 30 commits August 2, 2026 14:45
…st-child reset

The Style panel's shade editors never caught up with the Colors panel.

Shadow, inner shadow and border shades sat as loose rows with a reset
button each. They're sections now, like the token shades: one reset for
the pair, and the colour source that drives them lives inside rather than
above, so picking Primary shade and then its shades is one place.

The border editor drew an empty trigger — a chevron with nothing beside
it — because its single token group has no label to show but was still
collapsible by depth. A section with nothing in its header is a bare
group, whatever its depth.

The first-child reset that clears a nested section's rule was matching
ANY first child, so the colour select lost its own top padding and stood
22px tall against 28px rows. It's scoped to sections now.

Fullscreen hides the header, and with it the only way to flip the mode a
theme is being judged in — so the toolbar carries the colour mode button
there, beside the preview switcher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
… hosts

IconPicker, PresetList and ControlsGeneral each had exactly one caller —
the indirection cost a file without buying reuse. 34 components → 31.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Both restated the same popover trigger (and the same ui.label gotcha). The
list picker gains a header slot and an empty label; the font picker keeps
only what's its own — the catalog search.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Five call sites folded Object.values(sliders).some(dirty)/forEach(reset) in
their templates. One component now does it, and the sliders that hide when
the colour source has no stops say so with a prop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
…mponent

They were the same 90-line shape three times: tabs, knobs, a fold-out with a
colour source and its shades. Borders was a hand-written copy of the loop the
other two ran through. The caller now brings only its knobs.

Header fold-out toggles become one component too — the ghost/tinted button
was written out four times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
… unlabelled

Colors, Font, Icons and Options were four copies of the same popover button.
The label now lives in one place, and where it's hidden — anywhere but
fullscreen — the control says its name in a tooltip instead.

The tooltip sits on the button, not around the popover: Reka binds to its one
child element, and a popover root renders nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
theme.vue carried the Esc-vs-dismissable-layer dance, the fullscreen pointer
reveal, the debounced history capture and the two-stage reset — studio
behaviour that happened to live at the top. It now reads as layout plus a
toolbar: 382 lines to 223, script 150 to 55.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
…padding the panel

Semantic's colours and the default-variant groups are already inside a fold —
a second chevron there is a fold for the sake of one. Shade folds keep theirs.

The shade fold sat inside the knobs' gap container, so a closed one still
contributed a gap at the bottom of the panel. Its toggle now reads "Adjust
shades" like the colour panel's, and the preset menu drops the props its
only caller stopped passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Moving it out of the gap container fixed the trailing gap but took the space
above its rule with it. The margin rides the fold's content instead, which a
closed fold doesn't render — so it spaces when open and takes nothing when not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
…thly

The colour panel's folds touched their select the same way the treatments
touched their knobs. Both space with padding inside the fold's own content:
a margin sits outside the animated box, so it popped in ahead of the height.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
…erit

The stock collapsible classes only clip a CLOSED fold, so an opening one
painted full-size over the rows below and snapped into place at the end.

Shadow and border colour emit whatever the treatment is — only 'none' has
nothing to tint — so the colour fold no longer hides behind Custom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Only the studio's own shadow scale (--ui-shadow-c-*) reads --ui-shadow-color;
the inherited shadows are Tailwind's, tinted by --tw-shadow. So the colour
control was live but inert under Inherit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Padding on the animated box can't collapse below itself: with border-box, a
height of 0 still renders the padding, so a fold shrank to its own 8px and
then dropped them in one frame when it unmounted. Every section's content
carried that padding. It now rides an inner box, and the animated one only
clips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Refined the UI styles in the ThemeStudioRow component by adding 'ring-inset' to the base class for better visual feedback on focus and hover states. This change enhances the overall user experience by ensuring consistent styling across interactive elements.
The die follows the Options button. Every toolbar popover now pins the
fullscreen bar — Font and Icons kept their own open state, so the bar
retracted under them.

Black leaves the shadow and border colour lists: it IS the neutral ladder's
bottom stop, reachable from Neutral shade. Neo-brutalist said 'black'
literally; it now says the same thing on the ladder, byte-for-byte identical
CSS. The font pickers go up a size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
Same reason as black: it's the shade ladder's top stop, reachable from
Neutral shade. No preset asked for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
A raw <button> meant hand-written hover, focus and selected states — and the
selected ring landed on the swatch inside rather than the control. UButton
carries all three: ghost for rest and hover, its own focus ring, and the
active subtle variant for the applied preset. The ui.base override is only
what makes it stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
UPopover has no header or footer — UCard does, and its variants already
divide their slots with rules. The popover drops its padding and gap; the
card brings both, plus the separators, and the footer disappears with its
rule when there's nothing to put in it.

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
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants