Commit Graph

5999 Commits

Author SHA1 Message Date
Mike Newbon 20ea49537d fix(theme-studio): shade folds space off the control above them, smoothly
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
2026-08-02 15:38:28 +02:00
Mike Newbon f1fd731165 fix(theme-studio): the shade fold breathes again when open
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
2026-08-02 15:36:18 +02:00
Mike Newbon 7520a62590 fix(theme-studio): sub-sections stay open, and the closed fold stops 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
2026-08-02 15:34:30 +02:00
Mike Newbon 33e956d7db refactor(theme-studio): the page composes, the composables behave
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
2026-08-02 15:29:30 +02:00
Mike Newbon 390e2306c2 feat(theme-studio): toolbar controls share one field, tooltipped when 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
2026-08-02 15:26:59 +02:00
Mike Newbon 7a1a636e76 refactor(theme-studio): shadow, inner shadow and borders share one component
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
2026-08-02 15:20:36 +02:00
Mike Newbon 3a42b1bc8d refactor(theme-studio): shade groups own their dirty/reset
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
2026-08-02 15:17:35 +02:00
Mike Newbon 4817b34561 refactor(theme-studio): font picker builds on the list picker
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
2026-08-02 15:13:19 +02:00
Mike Newbon 1c1c4e68b1 refactor(theme-studio): fold single-consumer wrappers back into their 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
2026-08-02 14:54:36 +02:00
Mike Newbon 071872e772 fix(theme-studio): shade editors get their own section; scope the first-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
2026-08-02 14:45:27 +02:00
Mike Newbon eb336e2d41 refactor(theme-studio): enhance ThemeStudioSection for improved layout and functionality
Updated ThemeStudioControlsStyle.vue to replace individual color and shade controls with a unified ThemeStudioSection component, allowing for better organization and reset functionality. Adjusted ThemeStudioSection.vue to manage collapsibility and spacing more effectively, ensuring a cleaner UI. Added tooltip for color mode in fullscreen view in theme.vue for improved user experience.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 14:37:29 +02:00
Mike Newbon 3bfe61dd2d refactor(theme-studio): colors button back to stacked dots and one label
Dot-beside-name spent the bar's width twice — a dot per name, and two
names side by side. The dots stack again with primary on top, and the
names read as one label after them.

Both still come from colorChips, so the dots and the label can't disagree
about black-as-primary or a custom ramp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 14:37:28 +02:00
Mike Newbon 90744fd4dd Merge branch 'v4' into docs/theme-studio-plan 2026-08-02 14:36:14 +02:00
Mike Newbon d2ec6e5390 feat(theme-studio): even out 8-bit's ramp, and name both custom neutrals
8-bit's neutral bunched at the light end — 89.7, 89.1, 87.1 before dropping
away — so the top four steps were nearly one colour. It steps evenly now,
keeping the light end short of white: a dim LCD panel, not paper.

Both presets called their ramp `custom-neutral`, which is the engine's own
name for a ramp you've edited — so the picker showed "Custom" rather than
naming them. They're `crt` and `saturated-mauve`, like carbon and grove
before them. A hyphen in a palette id is a word break, not part of the
name, so one paletteLabel decides how ids read wherever they're shown.

Defaults reads "Default variants".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 14:24:58 +02:00
Mike Newbon ed5f915340 refactor(theme-studio): split the die out of the preset menu
The die lived inside ThemeStudioPresetMenu behind a `random` prop, so the
preset field's label covered both controls. It's ThemeStudioShuffleButton
now — the roll animation, its keyframes and the face-cycling timers move
with it — and the two sit as separate fields in the toolbar. The prop goes
with it; the menu is the trigger and its listbox, nothing else.

The Colors button reads its two ramps as dot-beside-name rather than a
stack plus a joint label, and drops the semantic dots the label couldn't
name. Black-as-primary and custom ramps read as the picker names them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 14:14:30 +02:00
Mike Newbon ef44811cc6 fix(theme-studio): restore the picker size prop; toolbar opens the options
Two regressions from my own cleanup, plus the ellipsis going away.

The `size` prop on the font and icon pickers wasn't dead code — it had no
callers at the moment I removed it, because the toolbar had just given them
up. Back in the bar they rendered a size down from every control beside
them. It's a prop again, defaulting to the panels' sm.

The toolbar's icon dropdown had lost its per-set glyph strips, so it read as
a plain select rather than the listbox the panel showed. One
ThemeStudioIconPicker now serves both, which is why they could differ.

The ellipsis goes: the control itself opens the options popover, and its
label reports the current family or set. That also retires the odd pairing
of a base-family shortcut sitting inches from the same picker inside.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 14:03:33 +02:00
Mike Newbon cce6a87c43 feat(theme-studio): toolbar owns font and icons, behind form fields
Experiment: the toolbar names what each control is.

Font and icons come back to the bar, each as a field group — the family
or set as the shortcut, and an ellipsis holding the whole section that
used to live in the Options panel. Two new components carry those:
ThemeStudioFontOptions (both rows, their weight/case/tracking/leading
buttons, the specimen) and ThemeStudioIconOptions (the picker over its
preview spread). The panel keeps Scale and drops the two sections, so
each control still has one home rather than two.

Options loses its text and moves to the end, past the things people
reach for; its glyph and the field label say what it is. Colors gains
the semantic ramps beside primary and neutral — seven dots, primary
stacked on top, so the bar reports every colour it owns unopened.

Labels only render in fullscreen, where the bar floats free and has room.
Hiding them also means zeroing the container's gap for the label, or the
compact bar keeps a stray 4px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 13:58:00 +02:00
Mike Newbon 6d0d3ec00c refactor(theme-studio): review pass — dedupe, dead code, stale strings
Adversarial pass over today's work.

Bugs:
- The two-stage reset still offered "Reset to Nuxt UI theme" after the stock
  preset was renamed to Default. It now reads its name from the preset, so a
  rename can't leave it behind again.
- A section with no `helpTo` still wrapped a UTooltip around nothing.
- A bare group's reset announced "Reset undefined to preset".

Dedupe:
- The preset chip maths lived twice, in ThemeStudioPresetList and again in the
  header popover's tiles. One themeChipStyle, so a tile and its row can't
  drift apart.

Dead code:
- The header popover loaded Google Font faces for rows the preset grid
  replaced, holding `fonts` alive to do it.
- Its doc comment still described the six rows it no longer has.

Comments trimmed again across the sections and rows — the header blocks say
what a thing is in one line, and what's left explains a decision rather than
the code beneath it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 13:25:34 +02:00
Mike Newbon 40d4c876a7 refactor(theme-studio): depth drives section behaviour; one spacing scheme
Sections stop being told how to behave. ThemeStudioSection injects its own
depth and provides depth+1, so the top level folds open, the layer under it
folds shut, and the leaves are plain groups. `collapsible`, `defaultOpen`
and `separator` become overrides rather than declarations, and every
`:collapsible="false"` at a call site goes away. They're pinned to undefined
through withDefaults — an absent optional boolean casts to false, which
would read as an override and flatten the lot.

Spacing follows the same idea: the section owns its padding, its content
gap and the rule above it, so no caller sets either. Space above a rule
comes from the parent's gap and below it from the section's padding —
setting both is what was double-spacing the semantic aliases. Only the
first layer insets from the panel edge; a section's first child drops its
rule, having nothing above it to separate from, exactly as the panel does.

Two wrappers were swallowing the gap their section children needed: the
Defaults panel wrapped its groups in a bare flex column, and TokenShades
sits inside a collapsible rather than a section, so nothing supplied one.

Reset moves onto the sections. A section can now reset something the doc
has no slice for, so a shade token resets its light and dark together
instead of a button per row, and primary's accent pair gets a real section
— header, rule and reset — instead of loose rows in a div. Token labels
carry their group ("Background muted"), and primary's reads "Primary
shades".

The shade editors fold like the palette editor above them rather than
popping in, action buttons match the sm header buttons beside them, and the
header popover gets the preset grid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 13:18:30 +02:00
Mike Newbon c5af27e9f7 feat(theme-studio): shade rows become a row variant; sections nest properly
Shades were a magic prop combo — pass `chip` and `mode` to a slider row
and it silently became something else. They're now `control="shade"`,
sitting beside slider/switch/select/custom as a peer.

The bespoke slider styling goes: shade rows ride the stock slider like
every other row, since the swatch in the label already carries the
colour. The readout stops being a dead `<span>` of the stop number and
becomes a select over the ladder — a shade is one of a fixed set, so
picking beats typing an index. It wears the numeric readout's treatment:
bare until you reach for it, ring on hover, no chevron. Its menu is
freed from the trigger's width, which would otherwise make a 40px-wide
dropdown.

The shade editor's own headings were the last bare spans in the panels,
and they nested wrong: a contentless section as a heading left an empty
content div and made the tokens its siblings. Groups now OWN their
tokens. A static section with nothing to put in its header — no label,
help, reset or actions — draws no header at all, so the single-group
case nests without repeating the section name above it.

Font and icon previews move below their controls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 12:03:18 +02:00
Mike Newbon 879bd1e175 refactor(theme-studio): one ThemeStudioRow for every settings row
Rows had grown three hand-rolled shapes: the slider row (label column,
control, readout), switch rows (a bare div with justify-between), and
select rows (another bare div with a w-13 span). Each had its own idea of
height, which is why matching them meant patching py-2.75 onto one control
and min-h-7 onto another wrapper.

ThemeStudioSliderRow becomes ThemeStudioRow, with `control` choosing what
sits in the row: slider (the old behaviour, readout and reset intact),
switch, select, or custom for the variant grids and pickers that bring
their own control. The shell owns the label column, the gap, and min-h-7 —
one 28px line, matching a size-sm button, whatever the control's own
height is. All 12 rows in the Options panel now measure 28px.

Switches keep their label-left/control-right treatment through the row's
`spread` layout rather than separate markup, so a sentence-length label
still gets its full width.

`icon` and `controlIcon` are separate props on purpose: one replaces the
label in the column (the palette editor's Effect row), the other is the
control's own glyph (the Size select). Overloading one prop would have
silently swallowed a label.

Section headers follow the same idea: a static section renders the SAME
button as a collapsible one, just `as="span"` with no chevron and no
hover, instead of a span that had to re-guess the button's padding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 11:39:12 +02:00
Mike Newbon fefea9eeb2 chore: ignore Playwright MCP scratch output
Screenshots and page snapshots from browser-driven checks land in
.playwright-mcp at the repo root; they're throwaway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 03:39:00 +02:00
Mike Newbon 179d17c208 refactor(theme-studio): Defaults groups reuse the static section primitive
The three groups inside Defaults were hand-rolled markup — a bare label
span and its own spacing — while the aliases nested under Semantic go
through ThemeStudioSection in static mode. Same font and colour, but the
section indents its label px-2 and spaces content pt-2, so the two nested
groups didn't line up with each other.

They now use the same primitive, so a nested group looks like a nested
group wherever it appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 03:37:51 +02:00
Mike Newbon a2ba282aff fix(theme-studio): adjust UContainer class for fullscreen handling
Updated the UContainer class in theme.vue to ensure proper padding when fullscreen is enabled. The change removes redundant padding classes while maintaining responsive design across different screen sizes.
2026-08-02 03:28:53 +02:00
Mike Newbon 0ccf22b27d refactor(theme-studio): preset button wears its preset; toolbar sheds the pickers
The preset trigger now shows the applied preset's own glyph — the same
one its row wears in the listbox, so 8-bit gets the space invader — and
keeps the swatch book for a theme with no preset behind it (and before
mount, where the pack is still client-only). It's also the only trigger
with an accented leading icon now; font, icons and Options go back to
inheriting, so the accent points at one thing.

Font and icons leave the /theme toolbar. They stay in the header popover,
where the quick knobs live, and in the Options panel's own Font and Icons
sections — the toolbar was carrying a third copy. Their `size` prop goes
with them: nothing passed it once the toolbar stopped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 03:27:54 +02:00
Mike Newbon 7a8dddfab6 feat(theme-studio): Options panel with a pack-aware icon, accented triggers
"Style" becomes "Options" — the panel holds type, scale and per-component
defaults as well as the treatments — and gains a leading glyph. That glyph
joins the studioIcons table so it skins with the applied pack like the
rest of the chrome; all nine names verified against the Iconify API.

Picker triggers accent their leading icon with text-primary, the
treatment upstream's theme picker already used, so the toolbar's four
controls read as a set. Colors keeps its swatch dots instead.

Also the real truncation fix: `ui.label` was never reaching these
triggers at all. UButton only applies it to the span it renders for the
`label` PROP, and both pickers pass their text through the default slot,
so the classes landed on nothing and a long name — "Material Symbols" —
wrapped to two lines. They now carry flex-1/min-w-0/truncate on their own
span, which is why ThemeStudioFontPicker appeared to work all along.

Includes the toolbar width tuning from the working tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 03:22:54 +02:00
Mike Newbon eba3087663 fix(theme-studio): gate the icon pack at its source; quieter picker triggers
The pack's own brand glyph on the icon picker still came back Lucide: it
renders `icons.find(...)` off `icon`, which is a third thing derived from
client-only state on the first render — same unpatched class mismatch as
the chrome and appConfig before it. Rather than gate a third call site,
`icon` now reports the stock pack until mounted, so every reader inherits
that. The per-call gate in useStudioIcons goes away as redundant, and
`_iconSet` stays ungated so dirty checks still see the real value.

Picker triggers get quieter: the font trigger drops its inline family
(the page already renders in the picked font, so it inherits), and the
"(Default)" tag leaves the variant, size and colour triggers. The tag
earns its place in a list, where it names which option is stock; on a
trigger it only pads the value you just picked. Every list keeps it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 03:12:32 +02:00
Mike Newbon 55884e6f93 fix(theme-studio): apply the saved icon pack to appConfig after hydration
The previous commit fixed only the studio's own chrome. The semantic
icons — search, sun, moon, upload, every appConfig.ui.icons key — still
came back Lucide on reload, because the plugin's restore has the same
flaw one level up: it assigned appConfig.ui.icons during plugin setup,
so the first client render disagreed with the server's stock pack, and
Vue won't patch a mismatched class.

Deferring the assignment to onNuxtReady makes it an ordinary reactive
update, which does repaint. Colors don't need this — their FOUC scripts
rewrite the style tags before first paint — but icon names compile into
element classes, which no pre-paint script can reach.

Measured on /theme with tabler saved: reload went from 36 tabler / 46
lucide to 46 / 36, matching the live state before the reload, and the
page's hydration warnings dropped from 13 to 3. The Lucide names that
remain are content-authored literals and packs with no override for a
given extra key, which fall back to Lucide by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 03:06:04 +02:00
Mike Newbon c80708963d fix(theme-studio): truncate picker labels, and apply the saved icon pack
The picker triggers set `truncate` on the button's label slot, but the
label is a flex child with the default min-width:auto — it can't shrink
below its content, so the text pushed the button wide instead of
clipping. min-w-0 lets it. ThemeStudioFontPicker only appeared to work
because its inner span truncates against whatever width the outer
resolved to.

Separately: a saved icon pack survived a reload everywhere except the
studio's own chrome, which came back Lucide while the dropdown correctly
named the saved pack. The pack is client-only state, so useStudioIcons
put the FIRST client render at odds with the server's Lucide, and Vue
only warns about a mismatched class — it doesn't patch it. The chrome
kept the server's glyphs until something else forced a re-render.
Resolving after mount agrees with SSR first, so the swap is a real
update. Predates this branch's feedback pass; confirmed against 21938e33f.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 03:00:33 +02:00
Mike Newbon a89ad23a83 refactor(theme-studio): one settings panel, type and icons on the toolbar
General and Style were one long list chopped in half, so they merge into
Style: type, icons and scale, then the shadow/border treatments, with the
per-component defaults trailing behind. SECTION_GROUPS is the single
source for that split, so groupDirty and the toolbar follow from it.

Only top-level sections fold now. ThemeStudioSection takes `collapsible`
— same header and content, disabled shell — and the aliases nested under
Semantic use it, as do the token-shade groups, which sat three folds deep
behind a section, a fold and an "Adjust shades" toggle for one slider.

Button/Card/Input Defaults become one Defaults fold over all three, which
needed sectionKey (and sectionDirty/resetSection) to take several keys.
Reset splices every slice in one applyDoc, so it stays one history entry.

Type and icons ride the toolbar rather than a panel — the two things
feedback called hardest to reach, and the pickers preview themselves, so
seeing them costs no clicks. Both take a `size` so the toolbar can run
them at the bar's own size. The Colors trigger wears overlapping primary
and neutral dots, reporting its colours without being opened.

The light/dark tabs leave the toolbar: the header owns color mode now.
That button gets data-keep-panels so flipping the mode doesn't dismiss
the theme popover beside it — checking a theme in both modes is the
whole point of having them adjacent.

The header popover regains font, icons and radius on the studio's own
w-13 label column, plus a reset in its title row and an Export that
appears once there's something to export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 02:53:09 +02:00
Mike Newbon b661e0b9e3 fix(theme-studio): drop the shuffle die from the header picker
The die sat on the Preset row, leaving that control ~40px narrower than
the Primary and Neutral selects under it — left edges aligned, right
edges didn't, which was the ragged half of the alignment feedback. It
stays in the /theme toolbar, where there's room and where shuffling is
part of the job.

Behind a `random` prop rather than a wrapper, pinned true through
withDefaults: an absent optional boolean casts to false, so the toolbar
would have silently lost its die.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-02 02:27:18 +02:00
Mike Newbon 0a3476d656 fix(theme-studio): header and picker feedback pass
Five things from Benjamin's review of the studio PR:

The header GitHub link is a brand mark, so it goes back to
i-simple-icons-github instead of skinning with the active icon pack —
it was the one glyph in the header that stopped matching the rest of
the docs. The `github` key leaves the studioIcons table entirely; the
preview demos keep their own themed github icon, which SHOULD follow
the pack.

The light/dark toggle comes out of the theme popover and back into the
header as a UColorModeButton, where it was before the studio took it
over. No kbds on its tooltip: ctrl+shift+D is registered on /theme
only, not site-wide.

In the popover, Preset now leads its own row like Primary and Neutral
below it, so the three controls share a left edge, and the studio link
slims to size sm and reads 'Edit theme' rather than 'Open Theme
Studio'.

The stock preset is renamed Nuxt UI -> Default (id unchanged, so
persisted state still resolves) and shows as selected while the theme
is untouched: an unedited theme IS that preset, so both pickers now
resolve through useThemeStudio().selectedPreset rather than pointing at
nothing until you pick something.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-01 21:18:35 +02:00
Mike Newbon aeb5b56900 refactor(theme-studio): split the presets listbox out of the menu
ThemeStudioPresetMenu did two jobs: the trigger button that names the
active preset (plus the shuffle die), and the listbox of theme chips
inside its popover. The listbox moves to ThemeStudioPresetList, so it
can be placed on its own - the header picker wants the presets without
the toolbar trigger wrapped round them.

Fixes a hydration mismatch carried over in the process: the label was
already resolved after mount, but the listbox's selected row read the
client-only persisted preset straight off, so the server rendered no
selection and the client rendered one. It now waits for mount too.

ThemeStudioPresetMenu's public API is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-01 20:13:02 +02:00
Mike Newbon 21938e33f0 docs(blog): add a WIP theme studio article
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 where the curve model might go next.

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>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBnE22R8HET4djqk1v5F41
2026-08-01 20:12:55 +02:00
Benjamin Canac 4200e80c03 perf(components): memoize tv slot invocations with simple args (#6737) 2026-07-31 16:41:07 +02:00
Benjamin Canac 702f481fb5 docs(input-rating): remove stray defaultValue line in size 2026-07-31 16:33:44 +02:00
Benjamin Canac 5afbd5c9ae fix(Modal): emit transition events from overlay when scrollable (#6786) 2026-07-31 15:51:17 +02:00
Benjamin Canac 8aa804167e chore(github): add CodSpeed workflow (#6785) 2026-07-31 15:44:32 +02:00
Benjamin Canac 735b264e4d fix(theme): unify motion easing and respect prefers-reduced-motion (#6742) 2026-07-31 14:43:28 +02:00
Yoshihiro Yamaguchi 10bfba4d63 chore(deps): update devdependency @nuxt/test-utils to ^4.1.0 (#6784) 2026-07-31 14:31:07 +02:00
Benjamin Canac 6bd1dfc86c perf(Button/Select/SelectMenu/InputMenu): narrow reactive dependencies (#6736) 2026-07-30 17:17:27 +02:00
Benjamin Canac 4ee416f8d4 docs(chat): rework ai endpoint around ToolLoopAgent (#6781) 2026-07-30 16:55:22 +02:00
Luke Nelson c4b786c952 chore(github): improve workflows (#6764)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
2026-07-30 12:08:09 +02:00
Mike Newbon ef6760a060 refactor(theme-studio): fold compound variants that select the same variants
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>
2026-07-29 22:51:59 +02:00
Mike Newbon b770a3401d refactor(theme-studio): keyboard-operable curves, lazy previews, drop 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>
2026-07-29 22:37:46 +02:00
Mike Newbon e4baec5283 Merge branch 'v4' into docs/theme-studio-plan 2026-07-29 22:08:06 +02:00
Mike Newbon b6a25be8d2 test(theme-studio): drop legacy shadow/border values from specs
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>
2026-07-29 21:37:13 +02:00
Mike Newbon 454582e012 refactor(theme-studio): trim comments, rename press/border vars, drop 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>
2026-07-29 21:33:31 +02:00
Benjamin Canac 706cfd0db7 fix(ContentToc): prevent list from collapsing (#6778) 2026-07-29 12:00:00 +02:00
Lazizbek Ergashev a158e26b78 docs(installation): note vue-tsc build race with auto-import declarations (#6777) 2026-07-29 11:21:30 +02:00