Every setting owned a localStorage key, sixteen of them, and two of those
were derived caches of the others. Restores could interleave, so the derived
stores each needed a self-heal to reconcile, and eight FOUC scripts each
re-read their own key and re-derived a slice of useTheme's style tags.
One key writes atomically, so those states cannot disagree. The style class
bundle and the custom-palette ramps are rebuilt from their source on load
instead of stored, which removes two of the three heals outright. Reads and
writes both live in the plugin, so no composable touches localStorage.
The nine keys the shipped picker wrote migrate once per browser.
Also groups the theme utils under utils/theme/, with the 646-line theme.ts
split by concern and theme-engine moved to utils/theme/engine.
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
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.
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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.
macOS ships its Search-with-Google Service on ⌘⇧L, so pages never
receive that chord (verified with a capture listener — Meta and Shift
arrive, L never does). Bare Shift+D is a screen-reader landmark key.
⌘⇧D is a modifier chord pages can intercept (VS Code web proves it) and
only shadows Chrome's non-reserved bookmark-all-tabs.
The new import spec used property access on an untyped slots object —
the root vue-tsc pass in CI rejects it; compare with toEqual instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bugs:
- bootstrap ramps: all six 950 stops were copies of the 50 stop (regex
slip during hex→oklch conversion); real shade-color(90%) values restored
- bootstrap palettes renamed bs-* — plain blue/gray shadowed the tailwind
ramps app-wide (header preset apply recolored every text-blue-* in docs)
and broke curve-editor seeding; light secondary now the true $gray-600
- shuffle() now applies the neutral white-literal remaps selectPalette
uses — a shuffled tinted neutral exported a white-background theme
- resetTheme takes { track: false } so preset/import/shuffle/undo no
longer pollute the Theme Reset analytics metric
- import: legacy ring-N scrubber gated on detection (ate user classes on
config-only pastes; broke width-1 frame round-trips); non-custom-property
declarations and nested at-rule blocks now land in `skipped` instead of
importing as tokens; parseColor rejects truncated/alpha hex that fed NaN
through the curve fitter
- ai.post.ts advertised sand/sage/ash neutrals that don't exist
- heading line-height defaults unified on 1.25 (slider claimed 1.5 while
the page rendered ~1.25); AI ui.icons payloads skipped on the live path
- pinned swatch state reset on palette switch; curve editor drags only on
the primary button; swatch details are a popover (keyboard-reachable
copy/pin, pin survives outside clicks); Shift+D → ⌘⇧L (Notion
convention, WCAG 2.1.4-safe); keep-panels scoped behind a prop so the
header preset menu keeps stock modal dismiss
- Custom shadow opens seeded to the stock shadow-lg look (y6 blur12 @10%)
Bloat & noise:
- ThemeStudioControls: dead hydrationKey remount machinery removed;
typography rows and shadow sections deduped through config arrays
(~170 lines); five dead utility classes; orphaned docblocks rehomed
- 16 no-op preset token lines deleted; new tests forbid tokens equal to
library defaults and 950 stops lighter than 50
- keepPanels shared from utils/theme.ts; plugin imports the storage-key
constants; black-as-primary FOUC script gets tagPriority
- THEME_STUDIO_PLAN.md removed from the PR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ThemeDoc.font grows tailwind weight-step overrides (normal/medium/
semibold/bold — live variables components dereference, so they restyle
the whole UI), base uppercase/italic/tracking/leading, and a prose
heading treatment (font, weight, italic, underline, case, tracking,
leading) emitted as body and h1–h6 rules; import parses it all back,
round-trip tested
- Preview rides one nuxt-ui-font-prefs channel (state, storage, FOUC
script, plugin restore); Google Fonts links cover both families at
300–800 (also the Comic Neue thinness fix)
- The Font panel becomes a type editor: live specimen atop the fold,
Base and Headings rows with font selects previewing their own faces,
and grouped icon toolbars — face [B I(U)], case [AB], rhythm [↔ ↕] —
with sliders tucked in popovers; Icons fold leads with a 20-icon
spread of the selected set; text size returns to Scale
- Comic Neue joins the font list
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Updated shadow style options to include 'Inherit' and 'flat', improving clarity in shadow management.
- Refined inner shadow handling to streamline options and ensure consistent behavior across components.
- Removed unused local storage references for palette management, simplifying state handling.
- Adjusted UI elements in ThemeStudioControls for better interaction feedback and visual consistency.
These changes improve the overall user experience and maintainability of the theme studio.
- The pickers separate strictly per the module's v4 shade recipes:
primary and semantic aliases offer only chromatic ramps, neutral only
gray ramps — the neutral-as-primary path (picker row, alias redirect,
mirror recipe, FOUC restore, import fold-back) comes out, parked for
v5 where the variant redirect becomes possible
- Fold triggers light up while open (subtle active variant) across
sections, token groups and the modifiers panel; the General panel's
button/card/input default sections start collapsed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Every settings section folds behind a ghost trigger button with a
LEADING chevron (the disclosure cue that tells folds from selects);
help links and the edit/shades toggles sit beside it as their own
buttons — stopped clicks, with actions forcing a collapsed section
open. Semantic regains its help link, closed by default
- Palette modifiers drop the preset dropdown for an always-there fold:
an eye-labelled strength row plus the photo-editing quartet
(lightness, contrast about the ramp midpoint, saturation as scale
with an additive floor so grays respond, hue), all idempotent from
the fitted base, with a reset beside the trigger
- Swatch clicks always really select: an active custom palette is
cleared (curves, params, remaps) instead of silently reseeding, so
the menu names the choice instead of a stale Custom
- Grid tiles become real UCards, inheriting panel default variants and
style treatments; SIZE_SUPPORT widens to every xs–xl component (tabs,
toggles, sliders, menus, calendar, field/form groups) with a
round-trip test
- Toolbar: history/reset move to the right cluster, fullscreen caps the
row, the color-mode switch caps the left cluster; slider readouts
truncate with a title fallback and shave leading zeros; studio-wide
text-xs/gap alignment; shade-slider fills keep a hairline inset ring
so they never vanish into the panel background
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Primary offers a single full-width Neutral option mapping to the neutral
ALIAS (#6608 semantics): the primary scale mirrors whatever neutral
resolves to, live, with FOUC restore, export recipe and import round-trip
- Drop the docs-only ramps (sand/sage/ash/cocoa/marine) from main.css, the
pickers and the engine; restore tailwind v4's own extra neutrals
(taupe/mauve/mist/olive) — CSS-variable resolved, they're not in the
tailwindcss/colors JS export
- Colors panel: shared ThemeStudioColorSection renders every alias (header
row hosts the edit + adjust-shades toggles), semantic rows sit under one
header, neutral's shade groups fold individually (closed by default)
- Palette modifiers: chroma presets gain a flat offset so gray ramps
respond, plus a Custom mode layering lightness/contrast/saturation/
chroma-offset/hue sliders over the fitted base
- Shade sliders keep a hairline inset ring so fills matching the panel
background stay visible
- Toolbar buttons move to the default size (header picker stays sm);
shared slider rows collapse into ThemeStudioSliderRow
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
History: undo/redo move the baseline synchronously (a second click
inside the debounce window dropped a state and duplicated redo
entries); align() invalidates the stacks when the theme changed while
no watcher was mounted; unmount flushes the pending capture instead of
dropping it.
Fullscreen: Esc leaves defineShortcuts (whose unconditional
preventDefault stopped every Reka layer on the page from closing) for
a plain listener that defers to visible dismissable layers — one press
closes the popover, the next exits. The toolbar reveal is proximity-
driven instead of an invisible click-eating overlay, and pins open
while any of its panels is open (their content portals to body, so
hover/focus-within alone retracted the bar under its own popovers).
Palette: the hue window stretches at seed to cover seam-crossing fits
(grabbing an out-of-window handle snapped the color); pointercancel no
longer throws through releasePointerCapture and strands the dragging
class; generatePalette clamps lightness (gamut clamp only searches
chroma); fitCurve survives empty input.
Engine: import puts style variables it cannot express back into the
token channel instead of silently destroying them (with a regression
test); picking Default on a per-group variant under an app-wide
variant now explodes the app-wide value into the other groups so it
actually wins; shadeSlider tolerates single-mode shade objects;
resetTheme clears the in-memory palette params it already cleared from
storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The settings panel dissolves into one toolbar: presets, per-group
popovers (Colors/General/Style/Shades — ThemeStudioControls renders one
group per instance now), view picker, fullscreen, import/export and
history actions. Component defaults split into Button/Card/Input
sections, buttons and inputs gain a default-color axis (emitted as
defaultVariants.color, extracted group-wise on import, round-trip
tested), and Size joins Scale. Fullscreen hides all chrome — site
header included via shared state — with a peek-on-hover exit and Esc.
Icon-only import gets a tooltip; colors keep the grouped Semantic
section with ghost edit pencils.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ringFragments and its five width maps (~200 lines), the frame width
enumeration, the widthAsVariables export/import split and every
var-length border/ring class in the preview markup are gone: the docs
build compiles --default-border-width/--default-ring-width onto the
studio variable, so live preview and exports use the same mechanism.
Legacy pastes get their width tokens scrubbed and frame literals
renormalized so old exports still import cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dashboard sidebar's edge lives in its side variant, which tv applies
after config slot appends — only a compound override lands on top.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop shadow section renamed to match. Studio chrome: Reset joins
Import/Export in the sidebar footer, mode picks are light/dark/system
buttons, the sidebar ring follows the theme border width, and the
collapsible help icons get their sizing back.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /theme page is gone — the studio opens as a fullscreen modal from the
header popover, which slims down to presets + shuffle, primary/background
dropdowns, radius, color mode and the copy/export shortcuts.
Hard shadows now ride composed --ui-shadow-hard* variables via
shadow-(--var) utilities: tailwind-merge couldn't parse the old
shadow-[calc(...)] literals as shadows, so stock shadow-sm/lg classes on
overlays (popover, tooltip, dropdown) survived the merge and won on CSS
order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>