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).
This commit is contained in:
Vance Ingalls
2026-07-14 16:28:33 -07:00
parent 2e44a15d19
commit 5dd9efe555
11 changed files with 550 additions and 142 deletions
@@ -33,10 +33,10 @@ const ALIGN_OPTIONS = [
];
const CASE_OPTIONS = [
{ key: "none", node: "" },
{ key: "uppercase", node: "AG" },
{ key: "lowercase", node: "ag" },
{ key: "capitalize", node: "Ag" },
{ key: "none", label: "none", node: "" },
{ key: "uppercase", label: "uppercase", node: "AG" },
{ key: "lowercase", label: "lowercase", node: "ag" },
{ key: "capitalize", label: "capitalize", node: "Ag" },
];
function FlatTextFieldEditor({
@@ -167,12 +167,23 @@ function FlatTextFieldEditor({
options={ALIGN_OPTIONS.map((option) => ({
key: option.key,
node: option.node,
label: option.label,
active:
align === option.key ||
(option.key === "left" && align === "start") ||
(option.key === "right" && align === "end"),
}))}
onChange={(next) => onSetTextFieldStyle(field.key, "text-align", next)}
onChange={(next) => {
// Re-clicking the option that's already visually active for a
// logical value (authored "start"/"end") must not rewrite it to
// the physical "left"/"right" — that destroys the logical
// semantics and is wrong for RTL content. Only write when the
// user actually picked a different alignment.
if ((next === "left" && align === "start") || (next === "right" && align === "end")) {
return;
}
onSetTextFieldStyle(field.key, "text-align", next);
}}
/>
<FlatSegmentedRow
label="Case · Style"
@@ -180,10 +191,11 @@ function FlatTextFieldEditor({
...CASE_OPTIONS.map((option) => ({
key: option.key,
node: option.node,
label: option.label,
active: textTransform === option.key,
})),
{ key: "normal", node: "A", active: fontStyle === "normal" },
{ key: "italic", node: "A", active: fontStyle === "italic" },
{ key: "normal", node: "A", label: "upright", active: fontStyle === "normal" },
{ key: "italic", node: "A", label: "italic", active: fontStyle === "italic" },
]}
spacerAfterIndex={2}
onChange={(next) => {