Skip to content

feat(studio): track design-panel input usage across both inspector UIs#2467

Merged
miguel-heygen merged 4 commits into
mainfrom
feat/design-panel-input-usage-tracking
Jul 15, 2026
Merged

feat(studio): track design-panel input usage across both inspector UIs#2467
miguel-heygen merged 4 commits into
mainfrom
feat/design-panel-input-usage-tracking

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Adds per-input usage tracking to the Studio design (inspector) panel, covering both inspector UIs:

  • the classic PropertyPanel sections, and
  • the flat PropertyPanelFlat redesign (behind STUDIO_FLAT_INSPECTOR_ENABLED).

Every user-driven input change emits a single studio:design_input event tagged with { ui, section, control, name }, through the existing opt-in Studio telemetry helper. No user-facing behavior changes — this is additive instrumentation only.

Why

The design panel has a large and growing surface of inputs across many sections and now two parallel UIs, with no per-input signal for which controls are actually used. This data lets us rank inputs and identify controls that can be removed or simplified, and it keeps the two UIs comparable during the flat rollout.

Removing unused inputs is intentionally not part of this PR — it depends on real usage data this change produces.

How

  • Single choke point. A new trackDesignInput helper wraps the existing trackStudioEvent (opt-out aware, never throws). Both UIs funnel their inputs through it rather than instrumenting each leaf control.
  • Section/UI via context. A small DesignPanelInputProvider carries { ui, section }; each panel sets ui once and each section sets its slug, so commit sites only pass { control, name }. Providers nest and inherit.
  • Coalescing. Continuous controls (sliders, scrub, live-commit text) collapse repeated fires of the same input within a short window into one event, and fire on settle — so a single drag counts once.
  • Shared components. Three components used by both panels tag ui from context, so they report correctly in either mode with no double-fire.
  • Coverage guard. A render-based test asserts every emitted event has a real name and section (no empty/unknown buckets), so the data stays rankable as inputs are added.

Commits are split by unit: tracking primitive, classic panel, flat panel.

Test plan

  • Full Studio test suite green (2591 passed): bun run test in packages/studio.

  • New unit tests for the tracking helper (event shape, coalescing window, never-throw) and the section/UI context (nesting + inheritance).

  • New render-based coverage guard for both the classic and flat panels asserting no unnamed/unknown events.

  • Typecheck, lint, and format clean.

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

Not covered / follow-up

  • Removal of unused inputs (gated on collected usage data).
  • The flat inspector remains behind STUDIO_FLAT_INSPECTOR_ENABLED (default off), so it does not emit until enabled.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 843b03d85c73e40a40f95856839390ac75a666cb.

What this does

Adds per-input usage instrumentation to the Studio design panel across both the classic PropertyPanel and the flat PropertyPanelFlat UIs (behind STUDIO_FLAT_INSPECTOR_ENABLED). Every user-driven commit funnels through a single choke point — trackDesignInput({ ui, section, control, name }) at packages/studio/src/utils/designInputTracking.ts — and emits one batched studio:design_input event via the existing opt-out-aware trackStudioEvent helper. No user-facing behavior changes.

Architecture:

  • designInputTracking.ts — pure utility. trackDesignInput slugifies section+name, coalesces repeated fires of the same ${ui}:${section}:${control}:${name} key within a 600 ms window (so a drag → many pointermove commits collapse to one event), and swallows any downstream telemetry throw so telemetry never breaks the edit path.
  • DesignPanelInputContext.tsxDesignPanelInputProvider carries { ui, section } through the render tree so commit sites only pass { control, name }. Providers nest: PropertyPanel sets ui once at the top; each Section sets section and inherits ui from the parent. useTrackDesignInput() returns a stable (control, name) => void bound to the current context.
  • Primitive wiring — every commit-shaped primitive (MetricField, SliderControl, SegmentedControl, SelectField, DetailField, TextAreaField, ColorField, FontFamilyField, and the flat equivalents FlatRow/FlatSlider/FlatSegmentedRow/FlatToggle/FlatSelectRow) invokes the tracker on its commit path. Classic's Section auto-wraps in a DesignPanelInputProvider keyed on the slugified section title.

Test surface: three new test files totalling +750 lines — utility (throttle, slugify, throw-safe), context (nested provider precedence, default fallback), and a 572-line propertyPanelInputCoverage.test.tsx that exercises each primitive under both UIs and enumerates the whole classic layout section + flat layout/header/footer sections end-to-end asserting payload.name !== "" && payload.name !== "unnamed" && payload.section !== "unknown".

Verification

  • Choke-point discipline is clean. All 26 file edits either wire an existing primitive to the tracker OR add per-section <DesignPanelInputProvider section="X"> scoping. No inputs bypass the utility. ✓
  • Throttle semantics correct. performance.now() monotonic; per-key Map<string, number> re-checked before every fire; __resetDesignInputThrottle() test seam is only public helper. Same-key within 600 ms → 1 event; distinct keys within window → both fire; same-key after window → 2 events. Pinned by the R4 test. ✓
  • Change-guard semantics for continuous controlsSliderControl uses interactionChangedRef (set to true only on user-driven onChange, reset in commitDraft) so an external setValue(...) prop update that flows through commitDraft doesn't fire the tracker. Same pattern in TextAreaField. FlatSlider guards on stepped !== dragStartValueRef.current. ✓
  • Non-continuous controls filter no-op commits. Classic MetricField / DetailField wrap if (nextValue !== value) track(...); flat FlatRow relies on CommitField's own if (nextDraft !== valueRef.current) onCommit(nextDraft) guard, so track only ever fires on a real change. Native <select onChange> in SelectField only fires on change; SegmentedControl guards on option.value !== value; FlatSegmentedRow guards on !option.active; FlatToggle fires only in the onChange handler. ✓
  • Throw-safety. trackDesignInput wraps the whole body in try {} catch {} so a downstream telemetry throw is swallowed. Pinned by the "never throws even if the underlying tracker throws" test. ✓
  • CI: 21 SUCCESS + 9 IN_PROGRESS + 7 SKIPPED + 1 NEUTRAL at read time. No FAILURE. ✓

Concerns — most-severe first

  • 🟡 Flat motion section's onUpdateMeta fallback mis-attributes non-duration/non-position updates to "Speed". packages/studio/src/components/editor/propertyPanelFlatMotionSection.tsx:180-184:

    onUpdateMeta={(animationId, updates) => {
      if (updates.duration !== undefined) track("metric", "Length");
      else if (updates.position !== undefined) track("metric", "Starts at");
      else track("select", "Speed");
      callbacks.onUpdateMeta(animationId, updates);
    }}

    If updates carries easing, repeat, yoyo, or any other meta field beyond duration/position, we fire track("select", "Speed") — the telemetry attributes usage to a control the user didn't touch. This poisons the "which controls are actually used" ranking that the whole PR exists to produce. Fix shape: switch on the specific key(s) in updates and either name each properly or explicitly bail on the unknown path (e.g. else return) rather than fall through to a placeholder name. Non-blocking but worth closing before this ships to real usage data.

  • 🟡 E2E coverage-completeness test only exercises layout for classic and layout/header/footer for flat. propertyPanelInputCoverage.test.tsx:436-471 (classic PropertyPanel) enumerates only the layout section's text inputs; :474-560 (flat) enumerates layout + clicks the three header/footer buttons. Other sections (style, color-grading, motion, media, text, fill) rely on individual primitive tests but no end-to-end assertion that every input under those sections fires with a resolved section name. If someone renders a subtree of inputs outside a <Section> wrapper (classic) or forgets a <DesignPanelInputProvider section="X"> (flat), those inputs silently emit section: "unknown" and the coverage test doesn't fire. Suggest extending the enumeration to each section (or, cheaper, adding a whole-panel test that asserts new Set(tracked.section) === EXPECTED_SECTIONS). Non-blocking but the wiring's silent-failure mode is exactly what a coverage-completeness test should prevent.

  • 🟢 Classic SliderControl tracks on null-net interactions while other controls only track when the value actually changes. propertyPanelPrimitives.tsx:250-253if (interactionChangedRef.current) { interactionChangedRef.current = false; track("slider", trackName); } fires whenever the user grabbed the slider and released it, even if they ended at the starting value. Defensible for "did the user try this control" analysis, but inconsistent with sibling controls that fire only on real changes. Pick one convention across the primitives and document it in the utility.

  • 🟢 Classic MetricField has double change-guard — its wrapper's if (nextValue !== value) track(...) at propertyPanelPrimitives.tsx:127 guards on top of CommitField's own if (nextDraft !== valueRef.current) onCommit(nextDraft). Flat FlatRow at propertyPanelFlatPrimitives.tsx:49-56 relies only on CommitField's guard. Harmless redundancy but the asymmetry means the two UIs implement the same invariant differently — one small refactor away from "the guard lives in CommitField full stop" (drop the wrapper's guard, since CommitField's onCommit is only ever called on real changes).

Verdict framing

Clean instrumentation PR with a well-designed single choke point, careful change-guarded commit sites, and a thoughtful coalescing window. The motion-section mis-attribution and the coverage-test scope gap are both about "the data this PR produces might not be trustworthy for the removal analysis it's built to enable," so worth closing before rollout — otherwise all fine. LGTM once those two 🟡 items are addressed.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additive to Rames's review at 843b03d8.

Right-vs-close: RIGHT direction — should ship after closing the parallel-branch gap below. The data-first order (instrument first, then remove inputs) is correct, and useTrackDesignInput()-bound-to-context + a single trackDesignInput choke-point with per-key 600 ms coalescing is exactly the architecture that keeps this cheap to extend as inputs are added.

Audited: designInputTracking.ts (end-to-end), DesignPanelInputContext.tsx, PropertyPanel.tsx classic header/body region, PropertyPanelFlat.tsx, PropertyPanelFlatHeader/Footer.tsx, propertyPanelPrimitives.tsx, propertyPanelFlatPrimitives.tsx, GsapAnimationSection.tsx, propertyPanelFlatMotionSection.tsx, InspectorHeaderActions.tsx (at HEAD), GestureRecordControl.tsx (at HEAD), all SliderControl/SegmentedControl consumers via code-search (7 files). Trusting: the two new test files at line-level (spot-verified key cases: throttle, throw-safety, nested-provider precedence, layout-section coverage assertions).

Position vs Rames: I concur on both 🟡 items and both 🟢s. The gaps below are additive.

🟡 blocker — classic-panel header + gesture-record button are the flat-panel siblings, and they're silent

The flat panel tracks 5 chrome buttons — PropertyPanelFlatHeader.tsx (Ungroup / Element visibility / Copy element info / Clear selection) + PropertyPanelFlatFooter.tsx (Ask agent, Gesture recording). Their classic-panel equivalents are:

  • packages/studio/src/components/editor/PropertyPanel.tsx:318-334 — inline <button onClick={() => void onToggleElementHidden(...)}> — untracked.
  • packages/studio/src/components/editor/InspectorHeaderActions.tsx:20-63 — Ungroup / Copy element info / Clear selection buttons — untracked (no useTrackDesignInput import, no wrapper).
  • packages/studio/src/components/editor/GestureRecordControl.tsx:36-50GestureRecordPanelButton renders <button onClick={onToggleRecording}> — untracked. (Also lives in the classic panel body without any <DesignPanelInputProvider section="…"> around it.)

Consequence: once data lands, dashboards for Ungroup / Visibility / Copy / Clear / Gesture-record will show as flat-only, and any comparability claim between the two UIs on those actions is definitionally wrong. That's the exact "flat gets instrumented, classic sibling silently skips" shape this PR should not ship — the whole point per the PR body is "it keeps the two UIs comparable during the flat rollout."

Fix pattern already exists in the PR — three edits:

  1. Add useTrackDesignInput() inside InspectorHeaderActions and wire the three buttons the same way PropertyPanelFlatHeader does (track("button", "Ungroup"); onUngroup?.(); track("button", "Copy element info"); onCopy(); track("button", "Clear selection"); onClear()).
  2. Do the same for the inline visibility toggle in PropertyPanel.tsx:318-334 (track("toggle", "Element visibility")) and for GestureRecordPanelButton (track("button", "Gesture recording")).
  3. Add <DesignPanelInputProvider section="header"> around the classic header row (PropertyPanel.tsx:309-344) and section="footer" (or similar) around the gesture-record button — otherwise these emit as section: "unknown" and the R3 "coverage signal" marker becomes noise instead of a gap-detector.

🟡 important — Rames's Speed misattribution repeats in GsapAnimationSection.tsx:40-46 (classic panel)

The pattern Rames flagged at propertyPanelFlatMotionSection.tsx:181-184 is duplicated verbatim in the classic panel:

// GsapAnimationSection.tsx:40-46 (classic)
const updateMeta = (animationId, updates) => {
  if (updates.duration !== undefined) track("metric", "Length");
  else if (updates.position !== undefined) track("metric", "Starts at");
  else track("select", "Speed");
  onUpdateMeta(animationId, updates);
};

Same silent mis-attribution: updates.ease / updates.repeat / any other meta key falls through to "Speed" in classic too. Fix in both files at once — the flat: true/flat: false branches share the bug because the pattern was copied. Under the extrapolation-as-blocker rule, one-shot fix both sites so the ranking stays trustworthy on both UIs.

🟢 nits

  • SelectField in propertyPanelPrimitives.tsx:349-352 is the odd control out — it fires track("select", label) unconditionally in onChange, while every other classic control (SegmentedControl, MetricField, DetailField, ColorField, FontFamilyField) guards on nextValue !== value. Native <select> doesn't fire change when picking the same value, so in practice it's a no-op, but the primitive-level asymmetry means a future reviewer copying this pattern to a component whose onChange DOES fire on re-selection (e.g. custom combobox) inherits a silent double-count.
  • slugifyDesignInput("---") returns "", which then becomes "unnamed" / "unknown". Not a bug — the R3 marker is the intended fallback — but callers passing a pre-slugified section (e.g. slugifyDesignInput(g.title) at PropertyPanelFlat.tsx:284) that resolves to "" will silently emit section: "unknown". Cheap defense: if (!slugifyDesignInput(g.title)) console.warn(...) in dev builds so the coverage guard isn't the only gate.

CI + envelope

Producer: integration tests is red on packages/producer/src/services/distributed/renderChunk.test.ts:204expect(encodeStageMs).toBeGreaterThan(0) observed 0. Unrelated to studio scope (this PR touches only packages/studio/**); appears to be a runner-timing flake on the encode-stage measurement in the byte-identical-retry test. Rerun should clear it; if not, worth a separate investigation, not a blocker on this PR's merits. Envelope clean (no AI-attribution trailers, expected on a Miguel-authored PR).

Verdict: COMMENT (post-merge: With fixes). RIGHT direction — the primitive + context design is the shape I'd want to see, and Rames's two 🟡s + my parallel-branch gap all resolve to trivial edits. Once the classic-panel header/gesture-record instrumentation lands and the Speed misattribution is fixed at both call sites, ship.

Reasoning: No correctness bug on the wired paths; the blocker-tier finding is a coverage gap where the classic-side chrome buttons parallel to the flat-side instrumented ones are silent, poisoning the exact "two UIs comparable" claim in the PR body.

— Via

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at aff6f1c94856f742d4dddf4a77fc5dbb03c212eb — R1 (843b03d8) → R2 delta is +147 / -61 across 8 files.

R2 delta

Closes both 🟡 findings + Via's classic-chrome gap + my 🟢 SliderControl-convention question. Well-executed round.

1. 🟡 (mine) Motion-section meta mis-attribution → closed via shared trackAnimationMetaUpdate(track, updates) helper at gsapAnimationCallbacks.ts:38-63. Instead of a single else → "Speed" fallback, the helper iterates Object.keys(updates) and looks each up in an explicit ANIMATION_META_LABELS table (duration→Length, position→Starts at, ease→Speed, easeEach→Speed). Unknown keys fall through to track("select", key) — attributed by their own name rather than poisoning a placeholder control's usage count. Wired in both classic (GsapAnimationSection.tsx:56) and flat (propertyPanelFlatMotionSection.tsx:184). Bonus: also closes a bug my review missed — the R1 chain silently swallowed the keyframed easeEach key alongside all the other non-duration/non-position fields, and the R2 table catches it explicitly. ✓

2. 🟡 (Via) Classic chrome parity → closed in InspectorHeaderActions.tsx:22-101 + GestureRecordControl.tsx:33-42. Visibility toggle, Ungroup, Copy, Clear, and gesture-record button all now emit tracker events. PropertyPanel.tsx:308-341 wraps them in the matching section="header" / section="footer" DesignPanelInputProvider scopes, so the payloads mirror PropertyPanelFlatHeader/Footer exactly. Consolidating the visibility toggle into InspectorHeaderActions (removing the parallel branch in PropertyPanel.tsx) is a good structural side-effect — one place to update the header buttons and it keeps the file under the 600-line gate as Miguel noted. ✓

3. 🟡 (mine) Coverage-completeness scope → closed in propertyPanelInputCoverage.test.tsx:449-511. The classic test now:

  • Fires every body text input across the whole panel (host.querySelectorAll('input[type="text"]')) instead of just the layout section's inputs — so any section rendered without a <DesignPanelInputProvider section="X"> wrapper surfaces immediately as section: "unknown".
  • Exercises header (Clear) + footer (Record gesture) chrome — the classic siblings of what the flat test already checks.
  • Asserts sections.has("header") && sections.has("footer") && sections.size > 2 — a body section plus both chrome regions, i.e. ≥3 distinct sections — plus the existing section !== "unknown" && !== "" and name !== "unnamed" && !== "" guards.

The mock-explanation comment (Copy skipped: clipboard unavailable in JSDOM; visibility store-gated on live selection) is honest about the two chrome buttons that aren't reached — both defensible mock constraints, not test gaps. ✓

4. 🟢 (mine) SliderControl convention → documented, not "fixed", in designInputTracking.ts:5-15. Added an explicit doc block:

  • Discrete controls fire on real committed change
  • Continuous controls (slider/scrub) fire once per interaction, because "net change at settle" is unreliable — mid-drag commits have already advanced the value, so interactionChangedRef is the honest "did the user work this control" signal

Right call. Fixing SliderControl to gate on net-change would have silenced legitimate scrub events on the way back to the origin — a legitimate ergonomic usage of the slider. The MetricField double-guard is left as-is because it's the scrub path, same reasoning. ✓

Verification

  • Helper table shape is closed under future meta fields. Object.keys(updates) enumerates only the keys actually present in the payload — no wildcard fallthrough, no risk of a new field silently attributing to Speed. ✓
  • Symmetric wiring across both UIs. Same helper used by classic (GsapAnimationSection) and flat (propertyPanelFlatMotionSection) callsites — refactor collapses the two duplicate if/else chains to a single call. ✓
  • Chrome-tracker payloads match flat. section="header" for Copy/Clear/Ungroup/visibility, section="footer" for gesture-record — same shape as PropertyPanelFlatHeader/Footer. Dashboard cross-UI join keys align. ✓
  • Coverage guard would actually fire. A section rendered without a provider wrapper would emit section: "" (default context) which the test asserts against. So the invariant is enforceable, not aspirational. ✓
  • CI at read time: 30 SUCCESS + 2 Windows IN_PROGRESS + skips only. Producer: integration tests now SUCCESS — Via's flagged renderChunk flake settled on this run.

Concerns

None. Both 🟡 items closed at the correct scope, Via's parallel-branch gap closed with better structural refactoring than a minimal patch would have produced, and the SliderControl convention documented rather than silently fixed-in-a-way-that-breaks-legitimate-fires.

Verdict framing

Clean R2 — every finding addressed at the right depth. LGTM from my side; standing on the two Windows shards.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 at aff6f1c94856f742d4dddf4a77fc5dbb03c212eb (Δ from R1 843b03d8 = +147/-61 across 8 files). Additive to Rames's R2.

Right-vs-close: RIGHT — ship. Every finding closed at the right depth; my R1 parallel-branch gap and both of Rames's 🟡 items landed with structural refactors rather than minimal patches, and F4 is documented in the utility next to the code it governs.

Audited at head: gsapAnimationCallbacks.ts (helper end-to-end), GsapAnimationSection.tsx:53, propertyPanelFlatMotionSection.tsx:184, InspectorHeaderActions.tsx (full R2), GestureRecordControl.tsx (full R2), PropertyPanel.tsx (header + footer wrappers, 586 lines — under the 600 gate), DesignPanelInputContext.tsx (default value verified), PropertyPanelFlatHeader.tsx + PropertyPanelFlatFooter.tsx (label parity), propertyPanelInputCoverage.test.tsx (assertion mechanics), designInputTracking.ts (convention doc), AnimationCard.tsx (onUpdateMeta call sites — easeEach is a real key at :287/:289/:297/:298). All 4 R2 commits (386c2be5aff6f1c9) — envelope clean, no AI-attribution trailers.

F1 — Speed misattribution (both UIs) → ✅ closed

trackAnimationMetaUpdate(track, updates) in gsapAnimationCallbacks.ts:38-63 replaces the if/else if/else → "Speed" catch-all with a table lookup over Object.keys(updates). ANIMATION_META_LABELS maps duration→Length, position→Starts at, ease→Speed, easeEach→Speed; unknown keys fall through to track("select", key) — attributed by the actual key name, not poisoning Speed. Wired at GsapAnimationSection.tsx:53 and propertyPanelFlatMotionSection.tsx:184, collapsing the two duplicated if/else chains to one call. Verified: AnimationCard.tsx commits onUpdateMeta with exactly one key per call ({duration}, {position}, {[easeKey]: value} where easeKey is "ease" or "easeEach"), so iterating Object.keys cannot double-fire on realistic payloads. The easeEach → Speed entry is required — AnimationCard.tsx:281/297 sets easeKey = animation.keyframes ? "easeEach" : "ease", so on keyframed animations the R1 chain silently emitted "Speed" and the R2 table now attributes it explicitly.

F2 — Classic chrome parity → ✅ closed

InspectorHeaderActions.tsx now emits track("toggle", "Element visibility"), track("button", "Ungroup"), track("button", "Copy element info"), track("button", "Clear selection") — labels match PropertyPanelFlatHeader.tsx exactly, so cross-UI dashboards join. Inline visibility toggle in PropertyPanel.tsx is folded into InspectorHeaderActions (props: selectedElementId, selectedElementHidden, visibilityLabel, onToggleHidden), eliminating the parallel branch. GestureRecordControl.tsx emits track("button", "Gesture recording") matching PropertyPanelFlatFooter.tsx. Both regions wrapped in <DesignPanelInputProvider section="header"> / section="footer"> at PropertyPanel.tsx:308/333. No other classic-panel chrome affordance is untracked — the classic panel has no "Ask agent" equivalent, so there's no residual sibling gap. Payload shape parity verified label-by-label.

F3 — E2E coverage widening → ✅ closed, assertion is real (not aspirational)

propertyPanelInputCoverage.test.tsx:449-511 now fires host.querySelectorAll('input[type="text"]') across the whole panel (not just layout), plus Clear (header) and Record gesture (footer). The assertion is enforceable: DesignPanelInputContext.tsx:14-17 defaults section to "unknown", so any subtree rendered without a <DesignPanelInputProvider> wrapper emits payload.section === "unknown" and the expect(payload.section).not.toBe("unknown") assertion trips. sections.has("header") && sections.has("footer") && sections.size > 2 further asserts ≥3 distinct sections with both chrome regions present — silences the "silent-failure" mode I flagged at R1. The mock-explanation comment (Copy skipped — clipboard unavailable in JSDOM; visibility toggle store-gated on a live selection) is honest about the two chrome buttons the unit mock can't reach; both are defensible test-harness constraints, not coverage gaps.

F4 — Convention doc → ✅ closed

designInputTracking.ts:7-16 documents the fire convention verbatim: discrete controls fire on real committed value change (guarded by commit sites), continuous controls fire once per user interaction that produced commits (because mid-drag commits have already advanced the value, so net-change-at-settle is unreliable — interactionChangedRef is the honest "did the user work this control" signal). Right call to document rather than "fix" — gating SliderControl on net-change would silence legitimate scrub-back-to-origin as noise.

Extrapolation

The trackAnimationMetaUpdate extract-to-helper pattern (iterate real keys, lookup in a table, unknown-key falls back to its own name rather than a placeholder) generalizes cleanly to any future dispatch-shaped callback where a payload can carry multiple meta fields. No other such call sites exist in this PR — but if onUpdateProperty-style batch-update callbacks appear later (e.g. transform tuple {x, y, rotation} in one commit), reuse this shape rather than an if/else if chain. Worth noting because the "one control's usage count poisoned by another" bug family reproduces every time else → placeholder is used as a catch-all.

CI + envelope

CI at read time: 32 SUCCESS + 7 SKIPPED + 1 pending. No FAILURE. Producer flake I flagged at R1 (renderChunk.test.ts byte-identical-retry) settled on the R2 run — Rames noted the same. Envelope clean (Miguel-authored, no AI trailers). PR is MERGEABLE / BLOCKED — needs approval.

Verdict: APPROVE. Both 🟡 items and both 🟢s closed at the right scope; the classic-side chrome refactor consolidates the visibility toggle into InspectorHeaderActions for a cleaner structural outcome than a minimal patch would have produced. Concur with Rames's R2 assessment.

— Via

@miguel-heygen miguel-heygen merged commit 7cc10a9 into main Jul 15, 2026
40 checks passed
@miguel-heygen miguel-heygen deleted the feat/design-panel-input-usage-tracking branch July 15, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants