feat(studio): inspector fields, toggles and sections on the shared primitives - #3774
Draft
miguel-heygen wants to merge 12 commits into
Draft
miguel-heygen wants to merge 12 commits into
miguel-heygen wants to merge 12 commits into
Conversation
Boots Studio from source on a free port, walks six review states, writes a PNG for each plus a markdown table of height, radius, font size and background for a fixed selector list. The table is the before/after artifact for the design-system sweep. Adds a data-testid to the two Export buttons and the inspector metric field, the three controls the table measures that carry no role or aria-label.
Studio moves from Tailwind 3.4 to 4.3.3, loaded by @tailwindcss/vite instead of PostCSS. The JS config becomes an @theme block in studio.css carrying the same studio.* and panel.* colors, with explicit @source globs so the gitignored data/projects tree is never scanned. The official upgrade tool renamed the utilities, but it rewrites bare words inside any string, so its edits to event names, CSS property names, filter function names, status enum values and prose were reverted by hand. Nothing in Studio relied on the v3 default border color, ring width or ring color, so the tool's compatibility shim was dropped rather than kept.
…erlay Adds @base-ui/react 1.7.0 to Studio and runs the KTD6 dismiss spike on both halves: a happy-dom regression test, and a real-browser run over the actual DomEditOverlay booted through the design-shots harness. Base UI registers its outside-press listeners on document in the capture phase, so the overlay's bubble-phase stopPropagation cannot swallow them. Both halves pass on the plain path, so no capture-phase wrapper is needed and useContextMenuDismiss is left untouched.
Studio's design values lived in three places: a JS Tailwind preset, an inline @theme block, and raw hex and pixel literals in markup and CSS. theme.css is now the single owner. Tailwind v4 turns it into both the utility classes and the CSS custom properties, so markup, plain CSS and canvas code all read the same number. - semantic colour palette (background levels, surfaces, borders, text levels, accent, selection, playhead, danger, media, container), radii, control heights, the micro type scale, font stacks, shadows, easings and motion durations - the stock Tailwind palette is cleared with --color-*: initial, so a colour that is not a Studio token cannot be reached from markup; the entries Studio's markup still names are re-declared as deprecated aliases and removed once the sweep converts them - motion durations ship as @Utility rules that zero themselves under prefers-reduced-motion, since Tailwind has no duration namespace - Phosphor icons get one default size and weight, read off the theme at the app root; icons that pass their own still win - published as @hyperframes/studio/theme.css; the JS preset export stays as a deprecated shim for v3 consumers, with a test that fails if the two drift apart
…pike' into feat/studio-u4-primitives-button-tabs # Conflicts: # bun.lock
Four primitives that consume design tokens only, plus the configured class merger they all share. - ui/cn.ts: one `cn` instance whose config knows the two class families theme.css invents, the `text-step-*` type scale and the `ctl` control heights, so a caller's class replaces a primitive's instead of stacking with it and letting stylesheet order decide. - Button and IconButton: native buttons on the three control heights, the radius scale, the semantic palette and the motion tokens. Four classes that resolved to nothing (rounded-button, shadow-btn-primary, bg-accent-red, ease-standard) are gone, which is why the Renders Export now has a radius. - Tooltip: Base UI's tooltip, so the portal, the viewport flip and the collision shift stop being hand-rolled. role="tooltip", the id wiring and aria-describedby stay explicit here because the library does not add them. - Tabs: Base UI's tabs, with roving tabindex, arrow keys, Home and End, and a data-tab-id the sidebar's focus restore looks up. Every interactive look is also reachable through a data-preview-state attribute that CSS alone reads, so a gallery can show hover, active and focus without a pointer. A test pairs the two lists in both directions. The class-resolution test compiles Studio's real stylesheet with every class the primitives emit and fails on any that produces no selector. Tailwind treats an undefined class as silence; that silence is what let four dead classes sit in this file.
Tailwind silently drops a class it cannot compile, so `rounded-button` renders as no radius at all and nothing goes red. Two tests close that: - the token gate compiles Studio's entry stylesheet with every class the source claims and reports `file: class` for anything that produces no selector. Tailwind is the judge, so there is no allowlist. - the hex ratchet counts colour literals per file against a committed baseline. It fails on a rise, and on a fall it prints the command that banks the lower number. The baseline is only written under a named flag, never as a side effect of a run. The gate's first run found seven names the markup has always asked for and no config has ever defined: rounded-button, shadow-btn-primary, bg-surface-hover, bg-accent-red, ease-standard, text-2xs and two panel backgrounds. Each is added to theme.css, the semantic ones by role and the rest as deprecated aliases for the sweep to remove. It also found a dead state class on the timeline clip, styled nowhere, which is deleted.
…t' into feat/studio-u9-header-tabs-sweep
The header's view-mode toggle and the inspector's tab strip become real Tabs, so both gain the roving tabindex, arrow keys and Home/End they never had; undo, redo and Inspector become IconButton and Button; Export becomes the primary Button at the medium size. Capture stays an <a download>, wearing Button's recipe, because a button cannot save a file. Colour literals in StudioHeader.tsx drop to zero: the brand gradient moves into theme.css as two tokens and the Export's hand-written text colour becomes text-bg-0. Ratchet total 475 to 470.
…imitives The five value controls exist once, on Base UI, wearing the theme tokens. Each is boxed with a visible border and a focus ring so an inspector value reads as an input rather than as its own label. Value writes happen at a commit boundary, never per keystroke or per pointer move: one drag is one onCommit and one telemetry call. Right-click and Escape abort a drag in flight and put the pre-drag value back. The switch role joins the typing-target selector list, so a Toggle keeps the keys the native checkbox it replaces owned. No consumer moves onto these yet.
The four native selects become the shared Select, so no OS popup is left in the panel and the four controls read as the same control as everything around them. Disabled options survive the move: SelectOption gains a disabled flag, which is what keeps a resolution the composition cannot reach out of the keyboard's path while its label still explains why. Export drops its own type size. cn resolved that override by dropping the size recipe's step, which is what left the panel Export a type step below the header's; the computed-style table now reads 28px / 6px / 12px for both. Its two per-row siblings become IconButton with a Tooltip carrying what the title attribute used to say. The panel's colours move to the semantic tokens.
The inspector's value control is a box now, not bare text with an underline drawn around it. CommitField wears the same fieldBase every other Studio field wears, so the metric input measures 28px high with a 6px radius and a real border where it measured 17px, no radius and no background. The label moves outside that box, which is the whole of R10: a boundary that differs from the label. SliderControl, SegmentedControl and SelectField become the shared Slider, Tabs and Select. The segmented strip gains arrow keys, Home and End from Tabs; the aria-pressed buttons it replaces answered no key but Tab. The native select is gone, so no OS popup is left in the classic panel. Each one still fires one telemetry event per commit boundary, now proven through a real section rather than the primitive alone. FlatToggle becomes the shared Toggle, and that migration found a defect in the primitive: Base UI renders the switch as a span, so Toggle's disabled:opacity-40 compiled to a rule that could never match and a disabled switch drew at full opacity with a pointer cursor. It is data-[disabled] now, with a test at the primitive that fails without it. The Layout section stops printing its own "3D Transform" label above a component that already renders a collapsible header with the same words, so the heading appears once (R9). Section and the 3D header expand on the duration-expand token, which zeroes itself under prefers-reduced-motion. Colour literals: 470 to 468.
miguel-heygen
force-pushed
the
feat/studio-u13-renders-sweep
branch
from
September 19, 2026 20:28
4fe3390 to
142a68c
Compare
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.
Lands the first inspector sweep PR (U10a: primitives, layout, text, sections, the 3D Transform heading) of the Studio design-system foundation. Stacked on the Renders sweep (#3772). Ratchet 470 to 468. Bundle +7.0 KB gzipped. Stops at a seam: the flat slider and select row keep their hand-rolled implementations and move in the next PR.
What
Lands U10a, the first of the inspector sweep PRs: the inspector's shared value
controls move onto the design system's primitives, and the doubled "3D Transform"
heading is gone.
CommitFieldis a boxed field. It wears the samefieldBaseasInput,NumberFieldandSelect, so the inspector's metric input and every otherStudio field are one control with one look.
MetricFieldandDetailFieldput their label outside that box.SliderControlbecomes the sharedSlider,SegmentedControlthe sharedTabs,SelectFieldthe sharedSelect,FlatTogglethe sharedToggle.FlatRowdrops the underline it drew around the value; the field's own box isthe boundary now, and the value tier tints it.
already renders one.
Sectionand the 3D Transform header expand on theduration-expandtoken.Also fixes a defect in the shared
Togglethat this migration surfaced: Base UIrenders the switch as a
<span role="switch">, sodisabled:opacity-40compiledto a real rule that could never match and a disabled switch drew at full opacity
with a pointer cursor. It is
data-[disabled]:now, with a test beside theprimitive that fails without the fix.
Why
Two requirements from the design-system contract.
R10 asks that inspector inputs read as inputs, with a boundary that differs from
their label. They did not: a value was bare text on the panel background, and the
metric input measured 17px high with no radius and no background. You could not
tell a value from its own label until you clicked one.
R9 asks that "3D Transform" render once for a selected element. It rendered twice:
the Layout section printed a static label directly above a component whose own
header carries the same words and is the one that collapses.
R8 and KTD7 want one implementation per control. The hand-rolled range input,
segmented strip and native
<select>were three more. The segmented strip inparticular carried
aria-pressedbuttons with no keyboard handling, so the onlyway through it was Tab, one segment at a time; on
Tabsit answers arrow keys,Home and End.
How
The public prop signatures of the migrated primitives do not change, so their
twenty-odd consumers across the inspector pick up the new look and the new
keyboard behaviour without a line of their own changing. That is what keeps this
PR small while the visible change is panel-wide.
Telemetry keeps its contract: one event per commit boundary, never per
intermediate value. Each migrated control passes the tracker the section already
holds as
onTrack, and the assertions run through a real section rather than theprimitive alone, because the section is what holds the tracker.
data-flat-row-valueis deleted rather than moved. The boxed field alreadycarries
data-testid="inspector-field", so a second hook on the same element wasone more thing to keep in step.
Test plan
Studio suite, from
packages/studio, with a capped worker pool:bunx vitest run --poolOptions.forks.maxForks=4— 440 files, 4845 tests, green.Baseline on the branch point was 440 files / 4841 tests, so the delta is the
four tests added here and no test removed or weakened.
bun run typecheck— clean.bun run --filter @hyperframes/studio build— succeeds.bunx oxlintandbunx oxfmt --checkon the changed files — clean.bunx fallow audit --fail-on-issues— exit 0 against both the base branch andthe trunk; no finding in a file this PR touches.
PASS: studio loaded with schema-valid API fixtures and no runtime errors.New tests, each proven to fail against the unfixed code:
label put back.
Sectionexpands onduration-expand. Fails with a hard-coded duration.Selecttrigger classifies the way the native<select>did onboth hotkey selector lists, asserted true-and-equal so two falses cannot agree
and prove nothing.
Togglewears its disabled look on the attribute Base UI sets.Screenshots and computed styles, before and after, from the sweep's capture
script. The row this unit is judged on:
rgb(22, 22, 24)28px and 6px are what both Export buttons already measure, so the inspector field
now sits inside the same three-height, three-radius set the sweep is aiming at.
Every other row in the table is identical before and after.
Bundle, gzipped across the app's chunks: 1,571,013 bytes before, 1,578,200 after.
A delta of 7,187 bytes (about 7.0 KB) for the first inspector consumers of
SliderandToggle, against the 100 KB budget for the set. Reported, nottrimmed.
Not covered
FlatSliderandFlatSelectRowkeep their hand-rolled implementations.Both are large pointer-capture and native-select state machines with about
thirty tests each pinned to their exact DOM, and converting them would have
taken this PR well past a reviewable size. They are the next sweep PR's, and
the flat inspector therefore still shows one underlined row (Weight) beside the
boxed ones until then.
grading, canvas menu, media, motion and overlays keep their own colours and
controls. Those are the later inspector PRs.
rgb(0, 0, 0)CSS fallback for a computed style, which is a browser defaultrather than a design value. The other three are shadow presets that have no
matching token yet; adding tokens belongs to the unit that owns the token file.
motion-path toggle appears in some captures and not others, on the base branch
as well as this one (present in one of two base runs, absent in two runs here).
Its visibility is gated on an async runtime probe of the preview iframe, so it
races the screenshot. Anyone diffing the inspector screenshots should expect it
and not read it as a regression.
measured against the trunk will report the whole stack instead.