Commit Graph
273 Commits
Author SHA1 Message Date
Miguel Angel Simon Sierra 675cbe194d fix(studio): retime flat tween boundaries and target the clicked keyframe's own tween
Two more regressions the QA triage attributed to this stack.

Dragging a flat tween's boundary diamond did nothing: the handler bailed on
`!anim.keyframes` even though resolveKeyframeRetime already resolves that case
to a position/duration resize. The empty remap now dispatches update-meta, which
moves the window without rewriting the authored flat tween into keyframes form
(what the keyframed-resize writer would do as a side effect).

The property panel's keyframe gutter guessed one animation per property group,
so clicking a keyframe authored by a sibling tween on a merged row named the
wrong tween and the writer silently removed nothing. The diamond now reports the
clicked keyframe's own animationId, with the group guess kept as the fallback for
cache rows that carry no identity.
2026-07-28 00:40:45 +02:00
Miguel Angel Simon Sierra 282cc883e5 fix(studio): address family B timeline review findings
- revert diamond selection when a rejected retime leaves the source in place
- clear project-local ease focus and expansion on player store reset
- share one static-position-hold predicate across the tween cache
- invalidate the GSAP cache even when a group timing rewrite throws
- stamp each lane keyframe's ease from its own source tween
- memoize property lanes and row offsets so memo'd diamond lanes hold
- use the editable tween duration for drag position commits
- restore the pre-t=0 pad in the all-collapsed content origin
- clamp the drag ghost and drop placeholder to the collapsed clip height
- aria-expanded on the layer disclosure, aria-pressed plus state-specific
  labels on the keyframe toggle, 24px chevron targets, focus-visible parity
2026-07-28 00:40:42 +02:00
Miguel Angel Simon Sierra 4a12eb9d9c feat(studio): wire expanded keyframe timeline lanes 2026-07-28 00:40:42 +02:00
Miguel Angel Simon Sierra 6b58678d94 fix(studio): drop dead keyframe menu wiring and close the R1 review items
The diamond context menu still declared `onChangeEase` and `onCopyProperties`
props, and `TimelineOverlays` still threaded `onChangeKeyframeEase` plus a
`keyframeCache` it never read. Nothing on any timeline branch calls them, so
they are removed along with the `onChangeKeyframeEase` callback implementation.

Also from review:
- `deleteSelectedKeyframes` only falls back to the sole keyframed animation when
  there is exactly one. A collapsed selection key carries no animation id, so
  taking the first of several deleted an arbitrary tween's keyframe.
- The duration-less retime test asserts the real 87.601% instead of
  `expect.any(Number)`, so a wrong timing basis fails it.
- `Timeline` wires the keyframe handlers' `onSelectSegment` through to the
  diamonds; it was built and then dropped, so segment ease selection never fired.
- The flat text section arms auto-focus in state rather than reading and
  clearing a ref during render, which Strict Mode's double render swallowed.
2026-07-28 00:37:31 +02:00
Miguel Angel Simon Sierra eddc5e9224 fix(studio): pass the whole keyframe identity out of the diamond menu
The context menu handed its actions loose positional arguments, so the
overlay adapters forwarded the percentage alone and dropped the property
group, tween percentage, and animation id the menu had resolved. The target
then fell back to first-match-by-percentage and deleted or retimed the wrong
animation whenever two collide at one percentage. Every action now carries a
TimelineKeyframeTarget, which leaves no adapter shape that can drop it.
2026-07-28 00:25:41 +02:00
Miguel Angel Simon Sierra 62e866eda6 fix(studio): span the clip on the remaining duration-less tween paths
Two sibling call paths still answered from GSAP's 0.5s default while the
toggle path had moved to the clip-wide fallback.

isPlayheadWithinTween now takes the selection, so the toolbar stops
promising "extends animation" on a duration-less tween whose click actually
toggles an interior keyframe. The arc drag commit resolves its replacement
duration the same way its click-path sibling does, instead of authoring
duration 0.5 and collapsing the arc window.

The flat text section drops its two marker-clearing effects: the autofocus
marker is a ref now, read and cleared by the render that consumes it.
2026-07-28 00:13:46 +02:00
Miguel Angel Simon Sierra d57039882f fix(studio): harden keyframe editing semantics 2026-07-28 00:13:46 +02:00
Miguel Angel Simon Sierra ba8df2661c fix(studio): resolve keyframe fallbacks against the clicked element
Three review follow-ups on the editor-callback consolidation.

The keyframe-target resolve now takes the clicked element's key and reads
that element's keyframe cache. The diamond context menu and move-to-playhead
pass no explicit target, so they fell through to the cache of whatever
element happened to be selected: opening the menu on a non-selected
element's diamond resolved against the wrong keyframes.

PropertyPanelFlat opens the Motion group by adjusting state during render
instead of in an effect, so the AnimationCard mounts on the same commit the
focus request arrives on rather than a frame later.

Both animation sections pass a module-level focus consumer instead of a
fresh inline arrow, so AnimationCard's focus effect stops re-running on
every parent render.
2026-07-27 19:52:07 +02:00
Miguel Angel Simon Sierra 2d1b905a56 feat(studio): consolidate timeline editor callbacks 2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra 8bb31b3949 feat(studio): add keyframe track headers 2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra e36fb385bc feat(studio): add timeline keyframe retiming interactions 2026-07-27 19:52:05 +02:00
Miguel Angel Simon Sierra e4d7bde64f feat(studio): add keyframe timeline state 2026-07-25 21:40:44 +02:00
Miguel Angel Simon Sierra c253dec23b feat(studio): add keyframe ease editor 2026-07-25 14:12:17 +02:00
ukimsanov d5c7d3ee16 fix(studio): preserve composed media treatments 2026-07-24 18:42:37 -07:00
ukimsanov 39c2341c4d feat(studio): add media treatment inspector 2026-07-24 18:42:37 -07:00
ukimsanov 70213c5a85 feat(core): define media treatment capabilities 2026-07-24 08:56:24 -07:00
ukimsanov 270179d94b fix(studio): harden flat keyframe retiming 2026-07-20 19:26:10 -07:00
ukimsanov f25a136927 fix(studio): retime flat tween keyframe diamonds 2026-07-20 18:56:14 -07:00
James 562544f68d refactor(core): own edit protocol contract 2026-07-18 13:24:57 -04:00
James 38c2d06933 refactor(studio): gate SDK operation families 2026-07-18 11:34:22 -04:00
Miguel Ángel e64a22893a fix(studio): preserve alpha proxy playback (#2625)
* fix(studio): preserve alpha proxy playback

* test(cli): pin VP8 alpha proxy pre-resolution

* fix(studio): emit textarea field-sizing CSS

* test(studio): verify textarea CSS generation
2026-07-17 21:56:27 -04:00
Miguel Ángel 2b65b4efce fix(studio): harden composition timeline reliability (#2615)
* fix(studio): preserve composition playback continuity

* feat(studio): drag compositions into the timeline

* fix(studio): collapse expanded composition move aliases

* fix(studio): make timeline cuts atomic

* fix(studio): group inspector gesture history

* test(studio): cover masked text selection

* fix(studio): harden composition timeline reliability

* fix(studio): satisfy CI source gates

* fix(studio): harden composition mutation requests
2026-07-17 14:15:30 -04:00
Vance Ingalls a4167ede07 feat(studio): default the flat inspector on
Flip STUDIO_FLAT_INSPECTOR_ENABLED's default from false to true — the
bug-fix pass on the flat inspector is complete (right-aligned values,
Stroke width/style split, promote-badge overlap, Layout/Style section
gating for non-visual elements like audio). VITE_STUDIO_FLAT_INSPECTOR_ENABLED=false
still opts back into the legacy panel.

Updates the two tests that asserted the old false default: the flag's
own default test, and the "classic PropertyPanel input coverage" suite,
which relied on that default to reach the legacy panel and now mocks it
explicitly (mirroring the adjacent "flat" suite's existing pattern).
2026-07-17 00:37:39 -07:00
Vance Ingalls 8a793ba2b2 fix(studio): gate Layout and Style panel sections on the element having a rendered box
Both panels showed Layout (X/Y/W/H/Rotation/Z-index) unconditionally —
no gate existed for it at all — and Style was gated only on
canEditStyles (a permission check), never on the element's tag. Neither
gate accounted for `<audio>`, which never paints a visual frame, so a
music track's inspector showed a full set of position/size/fill/shadow
controls with zero visual effect.

Add `layout`/`style` applicability to resolveEditingSections (core),
keyed on tag !== "audio", and gate both panels' Layout section and the
existing Style gate on it. Media/Motion/Grade/Text were already
correctly gated (verified via a research pass across both panels) and
are untouched.
2026-07-16 01:47:46 -07:00
Vance Ingalls 14dbfca5a7 fix(studio): give flat Text section rows more vertical breathing room
6px between rows left little clearance above a row's value for the
promote-to-variable badge (now positioned above the row). Widen the
row gap to 10px.
2026-07-16 01:17:04 -07:00
Vance Ingalls 5f045b6bdf fix(studio): move the promote-to-variable badge above the row instead of a padding gutter
Shrinking the wrapped control's width to make room for the badge (previous
commit) fixed the overlap but pushed the value left unnecessarily. Move the
badge to sit above the row instead, clearing the value without touching its
layout.
2026-07-16 01:11:30 -07:00
Vance Ingalls 9fa1b26570 fix(studio): reserve a gutter for the promote-to-variable badge
PromotableControl absolutely-positions its "◇ var" / "◆ {id}" badge over
the wrapped control without reserving any space, so on rows where the
value renders flush to the right edge (flat Font/Color rows) the badge
sat directly on top of the value text instead of beside it. Add a
right-padding gutter on the wrapper sized to each badge state, and cap
the bound chip to a fixed max-width so it always fits inside its gutter.
2026-07-16 01:06:11 -07:00
Vance Ingalls d5a259a3d2 fix(studio): make flat inspector's Stroke width free of style-name typing
Stroke width committed border-width and border-style together from one
free-text field, so setting a style meant typing an exact CSS keyword
(e.g. "dashed") with no indication of which ones were valid — the row
also duplicated the discoverable Stroke style select directly below it.
Stroke width now only commits border-width; style changes go exclusively
through the existing select.
2026-07-16 00:48:53 -07:00
Vance Ingalls b32f9a3e86 fix(studio): right-align FlatRow's value input in the flat inspector
FlatRow lays out label…gap…value across a `justify-between` row, but the
shared CommitField input it wraps had no text-align, so its text hugged
the LEFT edge of the value's own (often much wider) right-hand box —
looking left-aligned relative to the row, out of step with FlatSelectRow
and FlatSlider, which already right-align.

Added an optional `align` prop to CommitField (default "left", preserving
the legacy panel's MetricField/DetailField layouts where label-then-value
sits inline and left reads naturally) and pass `align="right"` from
FlatRow. Left the Motion Timing row's Start/End/Duration cells alone —
those stack label-above-value in a grid, a different pattern from the
inline label…value row this fix targets.

New tests: FlatRow's input has `text-right` (not `text-left`); the legacy
MetricField's input keeps `text-left` (not `text-right`), pinning
CommitField's default so the shared component doesn't drift for the
panel that didn't ask for this.

Full studio suite (2645 tests) green; typecheck/oxlint/oxfmt clean.
2026-07-16 00:48:53 -07:00
Vance Ingalls 2b51c52634 fix(studio): give flat-inspector value fields a resting-state input affordance
Every editable value in the flat inspector (FlatRow's CommitField, the
Motion Timing row's Start/End/Duration cells, and every raw <select> —
Style/Text dropdowns, Grade's Custom LUT and Copy-grade-to scope) rendered
its underline/border only on hover (`border-transparent
group-hover:border-...` or no border at all). At rest a value looked like
plain static text, with nothing distinguishing it from a label — testers
reported not being able to tell which fields were editable.

Give each a dim-but-visible resting border (`border-panel-border-input/50`,
or `border-panel-accent/30` for the explicitCustom tier) that brightens on
hover/focus, instead of a fully transparent one. Purely visual — no
behavior change.

Full studio suite (2641 tests) green; typecheck/oxlint/oxfmt clean.
2026-07-15 20:54:07 -07:00
Miguel Ángel 7cc10a9922 feat(studio): track design-panel input usage across both inspector UIs (#2467)
* feat(studio): add design-panel input usage tracking primitive

* feat(studio): track input usage in classic inspector panel

* feat(studio): track input usage in flat inspector panel

* fix(studio): attribute animation meta by field, track classic chrome, widen coverage guard
2026-07-15 01:33:27 -04:00
Vance Ingalls e820e18092 fix(studio): atomic timing pin, expanded-list Hide All, repeated-host matching, pointercancel revert
Fixes real bugs from two independent re-reviews (#2225 @ 65954c3804,
#2416 @ beaf4ffbf6):

- FlatTimingRow's pinRange committed a pinned start+duration range through
  TWO sequential onSetAttribute calls. Each resolves domEditSelection fresh
  from current hook state, so a selection change between the two awaits
  could misdirect the second write at the newly-selected element instead of
  the one being edited, and a failure of just the second call left the pair
  half-applied (inconsistent inferred/explicit state). Added
  commitDataAttributes/handleDomAttributesCommit (mirroring
  onCommitAnimatedProperties's same-shaped fix for GSAP property batches):
  one PatchOperation[] persist call against an explicit, caller-supplied
  selection — not the "current" one — threaded through as the new optional
  onSetAttributes prop. pinRange uses it when provided, falls back to the
  old sequential behavior otherwise.

- Hide All silently dropped nested sub-composition children: a selection
  inside a sub-comp with no timeline-store entry of its own resolves to a
  virtual `sourceFile#domId` key (the fallback branch exists so the
  expansion hook can later resolve it via clipParentMap), but
  toggleTimelineElementHidden only searched the RAW store list, which never
  contains that key. useTimelineElementVisibilityEditing now resolves
  against useExpandedTimelineElements() instead, matching the track-based
  toggle's existing approach — the expanded list synthesizes a real,
  patchable TimelineElement (matching key/domId/sourceFile) for each visible
  child whenever its host is currently expanded.

- Two composition hosts importing the same sub-composition collapsed to
  the first one: findMatchingTimelineElementId ORed domId/selector/
  compositionSrc matches with equal priority in a single per-element scan,
  so `.find()` could stop at an EARLIER, unrelated host that merely shared
  the compositionSrc, before the scan ever reached the correct domId/
  selector match further down the list. Restructured to try domId, then
  selector, across the WHOLE list first; compositionSrc-only matching is
  now a true last resort for when neither identifies a specific element.

- FlatSlider's native pointercancel handler (a platform-level gesture abort
  — scroll/touch takeover, pen leaving range) manually duplicated the
  pointer-capture release logic instead of calling cancelDrag, so it never
  reverted to the pre-drag value — leaving whatever intermediate position
  the pointer last reached committed, unlike the Escape/right-click paths
  added in the previous round. Now calls cancelDrag directly.

- useColorGradingController's flushPendingPersist read identityKeyRef.current
  fresh at flush time rather than a value snapshotted when the edit was
  scheduled. Defensive fix: added pendingPersistIdentityRef, set alongside
  pendingPersistValueRef in commitColorGrading, read by flushPendingPersist
  instead of the live ref — closes the gap regardless of how unlikely the
  actual race is given the identity-cleanup effect's existing eager-flush
  behavior.

Two prior findings re-verified as already fixed further up this same
Graphite stack (not re-fixed here, per established stack-order handling):
metadata-cache negative-caching (267cdfce1) and cross-file
selectionIdentityKey (6f40e03a1), both landing after #2225's reviewed head.

StudioRightPanel.tsx crossed the 600-line file-size gate after wiring the
new onSetAttributes prop through; extracted the inspector split-pane resize
handlers (previously inlined) into their own useInspectorSplitResize hook.

New regression tests: repeated-composition-host resolution, atomic vs.
fallback pinRange commit paths, pointercancel revert. Full studio suite
still at the known pre-existing 55-failure baseline, zero new regressions.
Typecheck/oxlint/oxfmt clean.
2026-07-14 16:28:33 -07:00
Vance Ingalls 28304be74a fix(studio): cancel FlatSlider drags on Escape and right-click
A fresh full-stack re-review checked 15 PR heads independently. Cross-
checked all 9 remaining claims against the actual current tip:

- #2120 (id/selector key qualification, Hide All no-op/race, variable
  parity), #2121 (opacity-zero fallback), #2122 (GSAP preview sibling
  resolution, scrub-label), #2124/#2126 (negative metadata cache), and
  the keyboard-access half of #2121/#2186 were all already fixed by a
  later commit in this same stack (65954c380, PR #2225) — the reviewed
  heads predate it. Verified each in the current source rather than
  taking the isolated-head review at face value.

- #2186's "no ESC/right-click cancel during drag" was the one claim that
  held up: FlatSlider had keyboard arrow-key support but no way to abort
  an in-progress pointer drag. Escape now reverts to the pre-drag value
  and releases pointer capture; a right-click (contextmenu) during a drag
  does the same instead of committing whatever position the pointer last
  reached while the native context menu opens over the slider. Both go
  through commitDraft (not just a visual reset) since the drag's leading-
  edge commit in onPointerDown may already have applied an intermediate
  value that needs actually undoing, not just hiding.

propertyPanelFlatPrimitives.tsx crossed the 600-line file-size gate after
this change; extracted FlatSelectRow into its own file, matching the
FlatToggle/FlatMaskInsetRows precedent from earlier in this stack.

New regression tests for Escape-cancel and contextmenu-cancel. Full
studio suite still at the known pre-existing 55-failure baseline, zero
new regressions. Typecheck/oxlint/oxfmt clean.
2026-07-14 16:28:33 -07:00
Vance Ingalls fc70d5f331 fix(studio): guard slider release reentrancy, scope Grade persist to schedule-time callback
Fixes the four blockers from the #2416 re-review at head d6a40c38b:

- FlatSlider's onPointerUp calls releasePointerCapture() explicitly, which
  fires lostpointercapture SYNCHRONOUSLY in real browsers — the prior
  unconditional onLostPointerCapture resync ran mid-onPointerUp, flipping
  draggingRef false before onPointerUp's own check, silently dropping every
  normal drag-release's final commitDraft(). happy-dom doesn't replicate the
  synchronous cascade, so this shipped without a failing test. Added an
  explicitReleaseRef flag set right before each deliberate
  releasePointerCapture() call so onLostPointerCapture can tell "our own
  release, caller's logic already handles it" apart from a genuine external
  capture loss. Added a regression test that monkey-patches
  releasePointerCapture to reproduce the real-browser ordering.

- persistColorGradingValue read onSetAttributeLiveRef.current (reassigned
  every render) instead of the callback live when the debounced edit was
  scheduled — a timer for element A firing after a re-render for element B
  would wrongly call B's callback with A's data. Removed the ref; the
  callback is now an explicit parameter captured by commitColorGrading's own
  closure (added to its useCallback deps) and threaded through to
  persistColorGradingValue and flushPendingPersist.

- flushPendingPersist passed () => true as its isLatestAttempt checker,
  bypassing the per-commit version guard entirely. Now calls
  bumpDomEditCommitVersion(gradingVersionRef) like a regular debounced
  commit, so a newer edit landing before the flushed write settles still
  wins the race.

- The selection-identity cleanup effect stopped clearing statusTimersRef
  during an earlier refactor — stale RUNTIME_STATUS_REFRESH_DELAYS timers
  for an outgoing element could fire after switching selection and stamp
  the new element's runtimeStatus with the old element's answer. Restored
  the clear in the same effect cleanup.

Also gave the Custom LUT and "Copy grade to" scope <select> controls
aria-labels — both had their visible text in a sibling span/text node, so
neither had an accessible name.

Full studio suite still at the known pre-existing 55-failure baseline
(variablePromoteIntegration, useGsapPropertyDebounce, sdkCutover(Parity),
sdkResolverShadow), zero new regressions. Typecheck, oxlint, oxfmt clean.
2026-07-14 16:28:33 -07:00
Vance Ingalls e07f1405f6 fix(studio): version-scope Grade persist, flush pending edits via effect cleanup not render
Fixes three of the adversarial findings from the third #2416 tip
re-review:

- Grade rollback was identity-scoped but not attempt-scoped: two edits on
  the SAME element (e.g. drag Exposure, then Contrast, before Exposure's
  persist settles) could have the earlier edit's late completion stamp
  confirmedGradingRef with its now-superseded value, or revert `grading`
  out from under the newer optimistic edit. Added a monotonic per-commit
  version via the existing bumpDomEditCommitVersion primitive (the same
  one the DOM-attribute commit runner uses for the identical race) —
  persistColorGradingValue now checks both identity AND "is this still the
  latest attempt for this element" before applying any effect.

- The render-phase identity-reset block consumed shared mutable state
  (clearing the pending-persist timer, reading and nulling
  pendingPersistValueRef) directly during render. Adjusting STATE during
  render this way is React's documented pattern and safe to repeat, but
  consuming a ref this way is not: if React discarded/interrupted that
  specific render before it committed, the timer would already be
  cancelled and the pending value already nulled, with no corresponding
  effect ever running to compensate, silently losing the edit. Replaced
  with the idiomatic pattern for "clean up a per-identity resource when it
  changes" — a useEffect keyed on identityKey whose CLEANUP performs the
  cancellation/flush. A cleanup only ever runs for the effect instance
  that actually committed, closing the gap entirely. The render-phase
  block now only performs pure, idempotent state resets.

- FlatSelectRow's Preset row passes label="" (the visible "Preset" text is
  a sibling span, to avoid rendering it twice) which left the underlying
  <select> with no accessible name at all. Added a dedicated `ariaLabel`
  prop, distinct from the visible `label`, so a caller can supply a name
  without a duplicate visible label.

Also hardened FlatSlider's lostpointercapture handling: it now resyncs
the draft directly from a latestValueRef immediately, instead of only
clearing the dragging flag and waiting for the separate [value]-keyed
effect to notice — closing a narrow ordering gap where a value change
arriving while still dragging, followed by capture loss with no further
render, could otherwise leave the knob stuck.

propertyPanelFlatPrimitives.tsx crossed the 600-line file-size gate after
these changes; extracted FlatToggle (and its tests) into their own files,
matching the FlatMaskInsetRows precedent from an earlier commit in this
stack.

New/updated regression tests: same-element version race, Preset select's
aria-label. Full studio suite still at the known pre-existing 55-failure
baseline, zero regressions.
2026-07-14 16:28:33 -07:00
Vance Ingalls 539e027b60 fix(studio): wire Grade rollback through the real commit path, scope async completions
Fixes two of the three adversarial findings from the second #2416 tip
re-review; the third is a pre-existing runtime-protocol gap, explained in
the PR thread rather than patched here.

- The Grade rollback added in the previous commit could never fire through
  the real Studio callback: runDomEditCommit (the shared commit runner used
  by every data-attribute commit, not just Grade) catches persist failures
  internally and always resolves, reporting outcome only via its own
  onError side effect. A caller awaiting the promise never sees a
  rejection, so the revert-on-reject logic was dead code against the
  actual app. Added an optional onSettled(ok) callback to
  DomEditCommitRunnerConfig (purely additive — every existing caller that
  doesn't pass it is unaffected) and threaded it through
  commitDataAttribute -> handleDomAttributeLiveCommit -> the
  onSetAttributeLive prop type (now accepts an optional 3rd argument)  ->
  useColorGradingController, which now drives the revert from the real
  signal. The promise-rejection path stays as a fallback for any other
  implementation of onSetAttributeLive that rejects instead.

- Selection flushing performed a real side effect (writing the outgoing
  element's pending edit) during the render-phase identity-reset block.
  Adjusting STATE during render (comparing against a ref) is React's
  documented pattern, but it doesn't license actual I/O — React can invoke
  render more than once per commit, which could double-fire or misorder
  the write. The reset block now only enqueues the flush (a pure ref
  write); a new effect keyed on the identity performs it after commit.

- Async persist completions (both the onSettled callback and its promise-
  rejection fallback) now capture the identity key the attempt was made
  for and check it against the CURRENT identity before touching
  confirmedGradingRef/grading/runtimeStatus. Without this, a persist that
  settles after selection has moved on to a THIRD element could clobber
  that element's freshly-reset state with a result that belongs to an
  element no longer selected.

Not fixed here: the runtime Grade target (HfColorGradingTarget, used by
core's resolveTarget to find the DOM element inside the preview iframe)
has no source-file/composition-scope discriminator, matching the same gap
selectionIdentityKey had before this stack — but fixing it means changing
a wire-protocol type shared across core/player/studio and the legacy
ColorGradingSection too. hfId (checked first, before id/selector) is
minted uniquely per element at parse time in the common case, so this is
a narrow residual risk for hfId-less same-selector elements across
different source files, not a regression introduced by this stack.
Flagged as a follow-up in the PR thread.

New/updated regression tests: real onSettled(false) path (distinct from
the promise-rejection fallback), and a stale in-flight persist settling
after selection has moved on twice more. Full studio suite still at the
known pre-existing 55-failure baseline, zero regressions.
2026-07-14 16:28:33 -07:00
Vance Ingalls 1deb0dc970 fix(studio): resolve the 4 cumulative blockers from the #2416 tip re-review
Fixes the Deepwork tip re-review's four remaining blockers plus its
additive findings:

- selectionIdentityKey: add sourceFile as a 5th identity component. The
  same local id/selector can legitimately recur across different
  composition files (host vs. an inlined sub-composition, or two unrelated
  sub-comps) — without sourceFile, those collided onto the same identity
  key and reused stale controller state across a selection change that
  should have reset it.
- useColorGradingController: flush (not discard) a pending Grade edit when
  selection changes before the 350ms debounce fires. The prior fix
  correctly stopped it from landing on the WRONG (new) target, but
  cancelling outright silently dropped the user's in-flight edit instead of
  writing it to the element it was authored for — using the
  onSetAttributeLive closure captured for the outgoing render, which
  (via commitDataAttribute's own useCallback deps) is still bound to the
  outgoing selection.
- useColorGradingController: revert to the last confirmed-good grading when
  a persist rejects, instead of leaving the optimistic (never-actually-
  saved) value showing indefinitely. Tracks a separate
  confirmedGradingRef, updated only on a successful persist.
- FlatSelectRow: disable the reset button when the row itself is disabled
  (it previously ignored disabled entirely, same class of bug as the
  FlatSlider reset button fixed earlier) and give the underlying <select>
  an aria-label from the row's label text.
- FlatSlider: handle lostpointercapture the same as pointercancel — capture
  can be lost without either firing first (another element steals it, or
  the browser reclaims it for a scroll/touch gesture), which previously
  left the dragging flag stuck and the knob permanently unable to sync to
  external value changes.

New regression tests for all of the above; full studio suite still at the
known pre-existing baseline (55 failures unrelated to this stack).
2026-07-14 16:28:33 -07:00
Vance Ingalls 5dd9efe555 fix(studio): resolve 8 confirmed adversarial-review findings across the flat-inspector stack
Fixes issues raised in the Deepwork re-review of #2120-#2190 that weren't
covered by #2225's earlier fix pass:

- useColorGradingController: reset grading/compare/mediaMetadata state (and
  cancel pending persist/status timers) when selection changes to a
  different element — this hook is called unconditionally on every render
  (unlike legacy ColorGradingSection, remounted via a selectionIdentityKey
  React key), so switching selection reused the previous element's state.
- useColorGradingController: stop permanently caching a non-OK
  /media/metadata response as null — a transient server error poisoned the
  HDR banner for that asset for the whole page lifetime.
- FlatSelectRow: preserve a valid authored value outside the preset list
  (e.g. mix-blend-mode: difference, an arbitrary object-position) instead of
  silently misrepresenting it as the first preset — touching the control
  would overwrite real persisted state.
- FlatSlider: the throttled trailing commit now reads onCommit through a
  ref updated every render instead of closing over it at schedule time — a
  caller whose onCommit spreads other current state (Grade's per-detail
  commits) could otherwise have a delayed commit revert whatever the user
  changed on a different control in the same 40ms window.
- FlatSlider: flush a still-queued trailing commit on unmount instead of
  dropping it, and disable the reset button when the slider itself is
  disabled.
- FlatSlider: add touch-action: none to the track so touch drags don't
  compete with page scroll.
- FlatColorGradingAccessory: clean up the compare-hold's window listeners
  on unmount, not only on release — switching selection mid-hold used to
  leak them.
- Align (flat Text): re-clicking the option already visually active for a
  logical start/end value no longer rewrites it to the physical left/right,
  preserving RTL semantics.
- FlatSegmentedRow: give every option an accessible name and aria-pressed
  state — two visually-identical glyph buttons (upright/italic "A") had no
  way to be told apart by assistive tech.
- PropertyPanelFlat: the panel body falls back to its own scroll when the
  collapsed group headers alone exceed the available height, so groups
  can't become permanently unreachable in a short pane.

New regression tests for all of the above; full studio suite at the known
pre-existing baseline (55 failures unrelated to this stack).
2026-07-14 16:28:33 -07:00
Vance Ingalls 127b8e4658 style(studio): distinguish open-section body from header background
Add panel.bg-inset token (#121214) and inset shadow to open-group body
for visual separation from the header row.
2026-07-14 15:51:58 -07:00
Vance Ingalls 6062d3b31c fix(studio): resolve flat-inspector review defects 2026-07-14 15:51:57 -07:00
Vance Ingalls a1fccfa748 fix(studio): throttle FlatSlider commits instead of debouncing them
A pure debounce resets its timer on every pointermove, so a real drag
(events faster than 40ms apart) never commits until the pointer pauses
or lifts — killing live preview updates mid-drag. Throttle with a
leading-edge commit + trailing flush instead.
2026-07-14 15:51:57 -07:00
Vance Ingalls 567b0aa017 fix(studio): debounce flat slider drag commits to prevent rapid-fire writes 2026-07-14 15:51:57 -07:00
Vance IngallsandClaude Sonnet 5 c699778689 fix(studio): support drag on FlatSlider, not just click-to-set
Only onPointerDown was wired, so dragging the knob/track only ever
committed the initial click position — nothing tracked the pointer
after that. Uses the Pointer Capture API (setPointerCapture on
pointerdown, onPointerMove while captured, release on pointerup) so
the value follows the cursor continuously during a drag, matching how
the legacy native <input type="range"> control behaves for free.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 15:51:57 -07:00
Vance IngallsandClaude Sonnet 5 ce07dfcd4d fix(studio): widen FlatSlider's click/drag hit area vertically
The track's visible line was only 2px tall, and pointerdown was bound
directly to that thin element, making it hard to grab. The hit area is
now 20px tall (a wrapping div) with the visible line rendered as a
thin decorative child, centered inside it — the ratio math only reads
left/width so click accuracy is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 15:51:56 -07:00
Vance Ingalls 2aefec3e68 fix(studio): animate the implicitly-closed group too, add regression test 2026-07-14 15:51:56 -07:00
Vance Ingalls a0f2a65bc2 feat(studio): animate flat inspector group expand/collapse
Adds a fast (120ms) CSS entrance animation for flat inspector accordion
group headers/body, gated to the group actually toggling (not derived
from remounting alone) to avoid a Chromium reflow quirk that otherwise
replays the animation on untouched collapsed siblings.
2026-07-14 15:51:56 -07:00
Vance Ingalls 2285b399e5 refactor(studio): remove section pinning from the flat inspector 2026-07-14 15:51:56 -07:00
Vance IngallsandClaude Sonnet 5 528f2be242 refactor(studio): scroll only the open group's body, keep headers fixed
Collapsed group headers render in fixed, non-scrolling document flow
above and below the open group; only the open group's own content
scrolls, in a dedicated region. Also fixes the flat inspector footer's
missing background.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 15:51:55 -07:00
Vance IngallsandClaude Sonnet 5 f6fa9b4ec3 fix(studio): restore capitalize, end-align, live-commit size, and autofocus in flat Text
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 15:51:55 -07:00
Vance Ingalls 8a25f21cd6 fix(studio): restore zero-strength revive, keyboard hold, and HDR detail in flat Grade 2026-07-14 15:51:55 -07:00