docs(theme): extract the theme studio into a portable layer - #6779
Draft
mikenewbon wants to merge 192 commits into
Draft
docs(theme): extract the theme studio into a portable layer#6779mikenewbon wants to merge 192 commits into
mikenewbon wants to merge 192 commits into
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>
- 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>
…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>
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>
Replace the parallel soft/hard shadow machinery with a single studio-driven Custom config (offset/blur/spread + opacity + colour), scaled per Tailwind size by geometry with uniform alpha so the opacity slider reads true. - Inherit leaves native Tailwind shadows untouched; Custom drives a gated --shadow-* ramp and None strips it, so content (prose images) now follows the theme. Exports emit the ramp as @theme tokens for the same reach. - soft/hard fold into 'custom' (tolerated as legacy aliases, migrated on load). Presets migrated; bold ones set explicit opacity. - Components: cards/alerts move up to shadow-lg; the button keeps its bespoke press-effect; overlays ride the ramp. - Controls lead with the primary sliders; colour + shade sliders fold out last behind a toggle. Border shades relocate from Colors→Neutral into the Borders section via a shared ThemeStudioTokenShades component. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the theme engine, composables, plugin, controls components and the gated shadow/border CSS out of the docs into a new workspace package, @nuxt/ui-theme-studio, consumed as a Nuxt layer. The docs extend it and keep only their demo surfaces (ThemeStudioView*, pages/theme.vue). Add a drop-in <ThemeStudioButton> — one trigger opening the full editor (presets / colors / general / style tabs, reset, import/export) with props to restrict or extend presets, choose sections, and replace the trigger via slot. ThemeStudioPresetList is extracted from PresetMenu so both hosts share the listbox. Portability work proven against playgrounds/nuxt as a second consumer: - the layer plugin owns the theme's useHead style/link/theme-color injection (no app.vue wiring needed; docs slimmed accordingly) - no-op useAnalytics in the layer, overridden by the app's own (docs keep their Vercel-backed one via app-over-layer priority) - useFrameworks, useGoogleFonts and the fonts.json server route move into the layer; @vueuse/nuxt is registered by the layer - theme.css carries @source "./app" so consumers scan layer components Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an Integrations page covering the layer setup (extends + theme.css), every ThemeStudioButton prop with examples, the composables, and the useAnalytics override pattern — flagged experimental while unpublished. Add a playground page exercising the button's configurations: default, restricted + custom presets, sections-only, and a slot-replaced trigger. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… examples Add mode="dev" | "user" to ThemeStudioButton for the two audiences of an embedded editor: the developer theming their app (everything on) and an end user personalizing a product (curated presets, colors as plain dropdowns). A new useStudioFeatures provide/inject carries the host's feature set to every control: palette (curve editor), shades (per-token sliders in colors and style panels), semantic (alias section) and help (docs links, now off by default in the drop-in — they point at ui.nuxt.com paths). Each is an individual prop overriding its mode default; share defaults off in user mode. The full studio page provides nothing and keeps everything on. Docs: live component-examples on the integration page (default, user mode, custom trigger) plus a persona explainer and a mode/prop matrix. Playground: a user-mode variant on the theme-studio page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… gating The studio now adopts the host app's theme as its zero point. A one-shot CSS baseline probe (utils/theme themeCssBaseline, with the studio's own override tags muted) seeds radius/font-size/spacing/font; app.config's authored colors are captured before the persisted-state restore. Untouched knobs emit no style overrides so the host's CSS stays in charge, dirty flags measure against the detected baseline, and Reset returns to the app's look — not Nuxt UI stock. ThemeStudioButton gains a `change` event re-emitting the new reactive useTheme().themeDoc on every edit, so hosts can persist themes to account data and restore them with useThemeStudio().applyDoc(); the docs show the save/restore pattern. Fix a real regression found here: absent optional boolean props cast to false, so every `?? dev` feature default resolved off — Export had vanished from the dev editor. Pinned to undefined via withDefaults. User mode tightens per feedback: `components` (Button/Card/Input Defaults) and `scale` (base font size, spacing, default size) gate off, the Scale section retitles to Radius when only radius remains, and the Style panel drops out of the default sections. The popover caps to the collision-aware available height so mid-page triggers scroll instead of overflowing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ness fixes
- Optional button press choreography (shadowPress) with a Style-panel
switch; off for Marshmallow — soft blurred configs keep buttons still
- Pill tabs ride the theme: full-size drop shadow (matching buttons),
inner shadow, and a border ring under any active border treatment;
frame no longer double-outlines the indicator
- Strip the ramp shadow from the switch thumb under Custom
- Orchard/Marshmallow gain 1px custom borders; 1px widths now export
(and so round-trip) explicitly
- Two-stage toolbar reset unjammed: font fingerprints treat explicit
stock values as absent, and emptied style tags clear via a :root {}
sentinel (unhead skips empty innerHTML patches)
- Recompute the persisted style-ui bundle on load so fragment changes
shipped in code reach existing sessions
- Prune dead legacy shadow vars (final-soft, hard-lg/sm), keeping
import tolerance for old exports
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tailwindcss 4.3.3 (already on v4, so PR merge runs resolve it) emits CSS Color 4 missing components for achromatic stops — zinc-50 is now oklch(98.5% 0 none) — which parseColor's numeric-only regex rejected, crashing the zinc round-trip in CI. none reads as 0 for chroma/hue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… legacy vocabulary Comment pass across the studio and engine (~800 comment lines removed, keeping the one-line whys). Renames for readability: the press-effect shadows are --ui-shadow-press/-half/-color (were hard/final-hard), the inner color-mix is --ui-inner-shadow-mix, and the border recolor variable is --ui-border-color (was frame-color). Shadow inherit is now spelled by absence and the legacy 'none'/'soft'/'hard'/'bold'/'frame' values are gone along with the localStorage migrations — the studio is unshipped, so there is no persisted state to translate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vue-tsc (which CI runs over test/, unlike nuxi typecheck) caught the literals the vocabulary removal left behind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… dead code The curve editor was pointer-only — its four handles are now focusable role=slider elements: arrows nudge, shift takes a 10x step, home/end jump to the window edges, and a run of keypresses commits once like a drag does. Also: the nine non-default preview views load as async chunks instead of bundling ~3.5k lines into the /theme route; the shade-slider logic that was duplicated verbatim across ColorSection and TokenShades moves to useTokenShades(); the theme-studio-dragging class and its timeout are gone (no CSS ever defined it); and the color-parse test asserts idempotence rather than byte-equality with tailwind's palette, which a patch bump breaks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ariants Successive treatments (a frame's ring, then a border colour) emitted one compound entry each for the same variant, so an exported app.config.ts listed 'solid' and 'soft' twice and leaned on tailwind-merge to resolve them at runtime. Folding them at composition time renders identically and shrinks the exports: 8-bit 656 -> 592 lines, neo-brutalist 693 -> 629, marshmallow 289 -> 229. The two tests that pinned the old shape now assert the invariant (one entry per selector, colour last so it wins the merge) instead of exact class strings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # docs/app/assets/css/main.css # playgrounds/nuxt/package.json # test/utils/theme-engine.spec.ts # theme-studio/app/components/theme-studio/ThemeStudioColorSection.vue # theme-studio/app/components/theme-studio/ThemeStudioPresetMenu.vue # theme-studio/app/composables/useTheme.ts
… title and color mode Storage: every localStorage key now derives from runtimeConfig.public.themeStudio.storageKey (default 'nuxt-ui', so existing state is untouched). Two Nuxt UI apps on one origin no longer clobber each other's theme, and a host can scope per user. The prefix is sanitized in one place and interpolated into the FOUC scripts, so the first-paint reader and the writers can never disagree; useState keys and DOM ids stay unprefixed, since neither can collide across origins. API: the six feature booleans collapse into one `features` object — the shape provideStudioFeatures already took — which removes the six-way undefined pinning that worked around Vue's absent-boolean casting. `sections` reads `panels` (it sat next to per-panel section gates called `semantic`/`components`), and `presets` is `false | string[]`. The footer is now a header: optional `title`, a `colorMode` light/dark switch (on by default in user mode) reusing UColorModeSwitch, and the reset and import/export buttons. It hides entirely when empty. The `change` event fires immediately, so a host also learns the theme restored from localStorage — previously a pre-signin edit never synced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… mode Two affordances escaped the user/dev split. The weight, case, tracking and leading controls beside each font picker rendered unconditionally, so mode="user" could not stop an end user setting heading weight 100 with 0.25em tracking — exactly what palette/shades exist to prevent. They now sit behind a `typography` feature; the family pickers always stay. `reset: false` only ever hid the header button while every section kept its own reset-to-preset, so a host that removed reset still shipped nine of them. The prop now flows through `features.reset` to the sections. Docs corrected: the user-mode prose claimed typography survived, and the prop table listed the old per-prop API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five things a consumer inherited wrongly: - The first-paint script hardcoded 16/0.25 as "untouched" and fell back to 'Public Sans', so a host with its own scale silently skipped a stored value, and any unrelated typography pref repainted a brand font as Public Sans until hydration. It now applies whatever is stored (matching the host's own baseline is a visual no-op) and only writes --font-sans when a family was actually chosen. Google Fonts is likewise only fetched for user-picked families. - A persisted icon pack *assigned* appConfig.ui.icons, wiping the host's own overrides. Keys already diverging from the stock lucide map now outrank the pack. - Help links were host-relative /docs/* paths that 404 outside this site. They resolve against features.helpBase, which ThemeStudioButton points at ui.nuxt.com (and opens in a new tab); the docs keep internal navigation. - package.json listed @nuxt/ui and tailwindcss as dependencies rather than peers (a second copy of the library on install), and lacked the ./package.json export Nuxt needs to resolve a node_modules layer, plus a files list. Added build.transpile so the raw-TS subpath exports survive a consumer's SSR. - ThemeStudioPicker is docs chrome keyed to route.path === '/theme' but shipped in the layer and auto-registered in every consumer app; moved back to docs/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root vue-tsc was type-checking theme-studio/ under the library's tsconfig, which has no Nuxt auto-imports — so every `computed`/`ref`/`useTheme` in the layer read as TS2304. The layer is a Nuxt app tree and is already covered by `nuxt typecheck docs` (docs/.nuxt/tsconfig.json includes theme-studio/app/**), so it belongs in the same exclusion list as docs and cli. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
commit: |
…d install resolves The playground CI job runs `pnpm install --ignore-workspace` inside playgrounds/nuxt to prove it works as an external consumer of the published preview build. A `workspace:*` dep on the unpublished layer cannot resolve there — @nuxt/ui survives only because the preceding step rewrites it to a pkg.pr.new URL. `link:../../theme-studio` is a filesystem protocol, so it resolves with or without a workspace; verified by replaying the job's install locally with @nuxt/ui swapped for a registry version (1161 packages resolved), and the monorepo install still symlinks the layer as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Walks through the palette editor: why per-stop blending traded precision against coherence, why curves in OKLCH fixed it, the non-destructive modifiers and stop-density options on top, and pins for the colours that aren't a taste decision. Closes on the embeddable editor and what the curve model is actually good for. Flagged work in progress in the article itself - the copy is still being worked and the four palette-editor screenshots are placeholders standing in for short clips. 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.
Important
Stacked on #6675 — merge that first. GitHub can't take a fork branch as a base, so this PR targets
v4and its diff therefore includes all of #6675. The change actually up for review here is much smaller:→
docs/theme-studio-plan...feat/theme-studio-module— 70 files, +1,302/−387.Draft, because it only makes sense once there's a direction on #6675.
What it does: extracts the theme studio from
docs/intotheme-studio/, a portable Nuxt layer, and adds one drop-in component on top of it.That is the whole integration, beyond
extends: ['@nuxt/ui-theme-studio']and importing itstheme.css. It answers two different needs from one engine:main.css+app.config.ts.mode="user"curates it down to approved presets, plain colour dropdowns, font families, radius and a light/dark switch. No palette surgery, no CSS export of your design system.Persistence is automatic (localStorage) and every edit emits the full
ThemeDoc, so a host can sync a user's theme to account data and apply it back on load.Notable in this delta
app.config.ts; radius, font, size and spacing start from their stylesheet's computed values. Untouched settings emit no override, and Reset returns to their look, not Nuxt UI's stock.runtimeConfig.public.themeStudio.storageKey(defaultnuxt-ui, so nothing changes for existing users). Two Nuxt UI apps on one origin no longer clobber each other's theme, and a host can scope per user. The prefix is sanitized once and interpolated into the first-paint scripts, so the FOUC reader and the writers can't diverge.useStudioFeaturesis a provide/inject bag with sensible defaults, so the docs studio needs no host at all whileThemeStudioButtondeclares a curated set for everything below it.Known limitations
@nuxt/ui-theme-studioisprivateand lives in this monorepo as a workspace package. Peer deps, the exports map andbuild.transpileare in place for publishing, but that's a separate decision.modeis a coarse first cut.--ui-shadow-*scale proposed in Epic A2 of v5 #6610. Same caveat as docs(theme): exploring a full theme customizer #6675 — the layer inherits it.@nuxt/fonts; worth solving before this is publishable.useCookie('nuxt-ui-framework')is origin-wide, so it has the collision problem the storage prefix just fixed. Left alone deliberately — it only drives the docs' Nuxt/Vue export toggle.Built with Claude Code 🧵