Skip to content

feat(studio): variables inspector panel with live preview values#2050

Merged
jrusso1020 merged 1 commit into
mainfrom
07-07-feat_studio_variables_inspector_panel_with_live_preview_values
Jul 9, 2026
Merged

feat(studio): variables inspector panel with live preview values#2050
jrusso1020 merged 1 commit into
mainfrom
07-07-feat_studio_variables_inspector_panel_with_live_preview_values

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Fifth PR of the template-variables stack: the Studio UI. Everything here is a thin client over the SDK surface from #2046#2048 — no bespoke parsing or schema logic lives in Studio.

  • New Variables tab in the right inspector: declared schema with type badges, descriptions, usage info (unused badges, one-click Declare for script-read-but-undeclared ids, honest degradation when access is dynamic)
  • Preview with values: per-type controls (text, number w/ range, color, boolean, enum; font/image as text for v1) write ephemeral overrides that ride the preview URL as ?variables= — the server injects them exactly like render time. Overrides survive soft reloads and hard remounts, a header pill shows defaults-vs-custom with one-click reset, validation issues render as an error strip
  • Schema editing: add / edit / remove declarations and per-row "Set default", persisted through the standard single-writer path (undo history, self-write echo suppression, preview reload); can() failures surface as toasts
  • Review fixes absorbed: slider/color commit on release instead of per-drag-tick (each commit reloads the preview iframe), edit-form round-trip preserves unmodeled declaration metadata (font source, brandRole, unit, …) via mergeDeclarationEdit, clipboard copy uses the shared Safari-safe helper, panel subscribes to session change events
  • PanelTabButton extracted (the tab bar was 5 copies of the same 15-line button)

Why

The docs have long described a Studio variables panel that didn't exist. This is the trust loop: see the schema, try values against the live preview, persist the ones you like — with undo.

Test plan

  • Typecheck + studio suite (1392) + browser build
  • Driven end-to-end in a real browser session: all five control types commit and update the preview; pill/reset/override-equality-drop; Set default persists to disk (incl. --{id} CSS compat prop) and undo restores the file verbatim; add/remove write valid schema to disk (see fix(studio): code-review and live-test fixes for the variables stack #2052 for the full test log)

🤖 Generated with Claude Code

@jrusso1020 jrusso1020 marked this pull request as ready for review July 8, 2026 05:15
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from c17a78a to dc052d3 Compare July 8, 2026 06:08
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 5870b13 to c843527 Compare July 8, 2026 06:08
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from c843527 to dc24770 Compare July 8, 2026 08:00
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from dc052d3 to f2a1662 Compare July 8, 2026 08:00
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from dc24770 to 2c0283f Compare July 8, 2026 16:17
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from f2a1662 to 0c4c77a Compare July 8, 2026 16:17
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 2c0283f to e0f52e3 Compare July 8, 2026 16:24
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch 2 times, most recently from a51906a to abb0112 Compare July 8, 2026 17:10
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from e0f52e3 to aa20eba Compare July 8, 2026 17:10
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from abb0112 to 7017acf Compare July 8, 2026 18:42
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from aa20eba to 59929fb Compare July 8, 2026 21:38
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from 7017acf to 5905f87 Compare July 8, 2026 21:38

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM with minor nits — well-decomposed panel architecture.

Highlights:

  • PanelTabButton extraction kills 5x duplication in StudioRightPanel
  • previewVariablesStore correctly normalizes empty {}null and uses getState() for non-reactive callsites (Player mount, refreshPlayer)
  • mergeDeclarationEdit preserving unmodeled declaration keys on same-type edits is exactly right — without it, editing a font variable's label would silently drop source/brandRole/etc.
  • Color picker + range slider both draft locally and commit on release, preventing preview iframe thrash

Minor nits (not blocking):

  1. NumberControl commitRaw (VariablesValueControls.tsx): when Number.isFinite(n) is false, the raw string is committed as the preview value. The validation strip catches it, but a toast on invalid input would give clearer feedback than a red banner that appears after commit.

  2. Four parallel useMemo hooks with the same manually-silenced dependency array (VariablesPanel.tsx): pragmatic given the imperative SDK, but if the dep list ever drifts between them, one could stale-cache while others re-derive. A single composite memo might be more resilient long-term.

Neither is worth blocking the PR.

— Miga

miguel-heygen
miguel-heygen previously approved these changes Jul 9, 2026

@miguel-heygen miguel-heygen 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.

Stamped after Miga's stack review. GitHub state checked: mergeable, no red checks visible; #2046 base conflict still needs resolution separately.

@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 5905f87. Stack context: studio-UI lane (#2050/#2051/#2052/#2055/#2071) — 5-of-12 in template-variables stack.

Layering on Miga's review — Miga covered mergeDeclarationEdit correctness, PanelTabButton extraction, and the draft-and-commit pattern well. A few things Miga didn't touch:

🟠 Hold / concerns

  • 1162 lines of new studio UI with zero new test files. The body's test plan is entirely "driven end-to-end in a real browser session," and the studio-1392 count is the pre-existing suite. The riskiest single-shot logic is mergeDeclarationEdit — a same-type edit must round-trip every unmodeled key (source, brandRole, unit, default_source, maxLength, …) or Studio silently strips schema metadata every Save. A five-line unit test asserting mergeDeclarationEdit(orig, edited).unit === orig.unit would pin that contract for the future without exercising React. Similarly for declarationFromDraft (number/enum edge cases), sanitizeId-adjacent form validation, and commitPreviewValue's equality-drop behavior. Manual E2E is legitimate for the pixels, but the logic underneath deserves guardrails.

  • handleRemove asymmetry with handleAdd / handleUpdate. Add and Update both sdkSession.can(...) first and surface check.message via showToast before dispatching; Remove skips the pre-check and lets whatever fires from runSchemaEdit's catch surface as a generic error toast. More load-bearing: dropPreviewOverride(id) fires eagerly and unconditionally, non-awaited alongside the void runSchemaEdit(...) — so if the SDK rejects the removal (or if persistence fails downstream), the preview override vanishes but the declaration is still on disk. User sees "hm, my custom value is gone but the row is still here."

    const handleRemove = useCallback(
      (id: string) => {
        void runSchemaEdit(`Remove variable "${id}"`, (s) => s.removeVariableDeclaration(id));
        dropPreviewOverride(id);  // fires even if the schema edit throws
      },
      ...
    );

    Either move dropPreviewOverride inside a .then(changed => { if (changed) dropPreviewOverride(id); }), or make runSchemaEdit return a Promise the caller can chain on.

🟡 Nits / questions

  • useVariablesPersistpath = activeCompPath ?? "index.html" fallback, then downstream compositionPath: activeCompPath (raw, may be null). Internal inconsistency: the write is targeting index.html but the persist metadata carries null. Anything in persistSdkSerialize that keys off compositionPath will see a different value than the file actually being written. Likely benign in practice given #2052's master-view session is opened on index.html anyway, but worth aligning.

  • No virtualization on the declaration list. Realistic templates cap around a dozen variables, but agent-generated templates or brand-system compositions could push into the 50-100+ range. At that point, the PreviewValueControl + DeclarationForm per-row weight starts to matter (each row keeps its own draft state, and every sdkSession.on("change") triggers a top-level re-derive via revision). Not a v1 blocker; note for later.

  • Per-commit iframe reload. commitPreviewValuereloadPreview() on every text-blur / color-blur / range-release / boolean-toggle. Correct for correctness (server injects ?variables= on load), but on a template with many variables the user scrubbing values pays a full iframe reload per commit. A soft debounce (200-300ms) on reloadPreview while the user is still committing sequentially would smooth this without loosening the "what you preview is what you render" invariant.

  • NumberControl's commitRaw stores the raw string when Number.isFinite is false (Miga flagged the UX; adding the mechanics side): the preview values dict then carries {n: "abc"}, which the validation strip catches AND getVariableValues still forwards through ?variables= to the server. If the composition script does getVariables().n * 2, it now gets NaN. Coercing to null (or refusing the commit) would be cleaner than tolerating and surfacing.

  • Session-change subscription in useEffect: sdkSession.on("change", ...) — is the returned value guaranteed to be an unsubscribe? The pattern return sdkSession.on("change", cb) assumes on returns an off-fn. If it doesn't (say it returns void or the listener id), the effect cleanup does nothing and every unmount/remount of the panel adds another listener. Quick verify at the SDK's on signature.

🟢 Good

  • The sdkSession.on("change", () => setRevision((r) => r + 1)) subscription means Variables panel stays coherent through undo/redo, cross-panel writes (bind card in #2055, promote from Design in #2071), and any future writer — nothing has to know about the panel to trigger its re-derive. Clean.
  • dropPreviewOverride on remove and on Set-default is the right instinct — no stale preview keys pointing at removed vars or at values that now equal the declared default.
  • The two-level guard in commitPreviewValue (equality-drop against declDefault) means the "custom" pill only lights up for genuinely-different values. Small UX detail, big trust signal.
  • The EMPTY_STATE copy is genuinely helpful (data-composition-variables, getVariables(), --variables) — three concrete pointers instead of "nothing declared."

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.

R1 — hyperframes #2050 at 5905f874

🟢 LGTM on the panel architecture, concurring with Miga; concur with Rames-D's two 🟠 Holds — both substantive.

Verified independently:

  • Schema is 100% SDK-delegated. Panel calls getVariableDeclarations(), getVariableUsage(), validateVariableValues() on the SDK session — no bespoke parsing. Divergence risk zero.
  • mergeDeclarationEdit(original, edited) preserves unmodeled keys on same-type edits. Round-trip test locks the "silent metadata loss prevention" invariant.
  • Preview value UX per-type. Text drafts + commits on blur/Enter; number drag-commit-on-release (avoids iframe thrash); color drafts and commits on blur; boolean/enum commit immediately. Invalid number commits raw string, ValidationStrip flags red.
  • Live preview. applyPreviewVariablesToUrl() injects ?variables=<json> into Player mount + refresh — iframe reload, not incremental. URL is SSOT that #2049 validates.
  • Unused / usage badges. Only render when !scanIncomplete. False-positive unused structurally impossible on the scanner-visibility axis (though see #2048 concurrence on CSS partial-match).
  • PanelTabButton extraction kills 5x duplication.
  • ?tab=variables deep-link support added to VALID_TABS.
  • CI green, Studio 1392 passes.

Concurring with Miga's LGTM-with-minor-nits.

Concurring with Rames-D's 🟠 Holds:

  1. 1162 lines of studio UI with zero new test files. Manual E2E is legitimate for pixels, but the logic underneath deserves guardrails. Highest-value additions Rames-D named:

    • mergeDeclarationEdit(orig, edited) — a same-type edit must round-trip every unmodeled key (source, brandRole, unit, default_source, maxLength). A five-line unit test asserting .unit === orig.unit would pin the invariant that Miga and I both flagged as elegant. Regression risk on this contract is disproportionate to the test cost.
    • declarationFromDraft — number / enum edge cases.
    • sanitizeId-adjacent form validation.
    • commitPreviewValue equality-drop behavior.
  2. handleRemove asymmetry with handleAdd / handleUpdate. Confirmed at head: Add and Update both sdkSession.can(...) first and surface check.message via showToast; Remove skips the pre-check. Load-bearing: dropPreviewOverride(id) fires eagerly and unconditionally, non-awaited alongside void runSchemaEdit(...) — if the SDK rejects the removal or persistence fails, the preview override vanishes but the declaration is still on disk. UX symptom: "my custom value is gone but the row is still here." Concur on either (a) chain dropPreviewOverride in .then(changed => changed && dropPreviewOverride(id)) or (b) make runSchemaEdit return a Promise the caller can chain on. Either closes the gap.

Unique observation:

O1 — Preview values cleared on comp/project change lands in #2052. Without that cleanup (which #2052 adds), overrides could leak into another composition's preview. Called out here because the invariant is only-completed-by-the-stack — but see also Rames-D's #2052 Hold on the "index.html" fallback which introduces a different class of leak on the same code path.

R1 by Via

@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 59929fb to 476b923 Compare July 9, 2026 06:49
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from 5905f87 to 6096572 Compare July 9, 2026 06:49
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 476b923 to fbc61e4 Compare July 9, 2026 07:57
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from 6096572 to 5e68161 Compare July 9, 2026 07:57
@jrusso1020

Copy link
Copy Markdown
Collaborator Author

Addressed R1 🟠 Holds.

  • handleRemove no longer drops the preview override unconditionally — runSchemaEdit now returns whether the edit applied, and dropPreviewOverride(id) runs only in .then(changed => …). Also added the missing can() pre-check for symmetry with Add/Update.
  • Added VariablesDeclarationForm.test.ts unit tests: mergeDeclarationEdit (unmodeled-key passthrough on same-type edits, metadata drop on type change) and declarationFromDraft (per-type parse/validation + round-trip).

@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from fbc61e4 to e909dcc Compare July 9, 2026 13:17
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch 2 times, most recently from de151ad to bcdf263 Compare July 9, 2026 18:06
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from e909dcc to 265f388 Compare July 9, 2026 18:06

@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.

R2 at bcdf263.

Prior R1 🟠 (1162 studio lines with zero new test files; handleRemove drops preview override eagerly before the SDK write) resolved at both anchors:

  • handleRemove ordering at VariablesPanel.tsx:398-416: runSchemaEdit(...).then((changed) => { if (changed) dropPreviewOverride(id); }) — override is now dropped only after the SDK write returns a truthy changed signal. A rejected/failed edit leaves both on disk consistently.
  • mergeDeclarationEdit test at VariablesDeclarationForm.test.ts:16-64: both branches pinned — unmodeled-key passthrough on same-type edit + old-type-metadata-drop on type change. Plus 7 more declarationFromDraft tests covering all 6 variable types + a round-trip.

No residuals from my side. CI green.

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.

🟢 LGTM concurring with Rames-D — R2 at bcdf263. Both R1 🟠 holds resolved.

Hold-1 (handleRemove asymmetry — dropPreviewOverride fires eagerly before non-awaited SDK edit; a rejected/failed edit leaves the declaration on disk with the override gone):

  • packages/studio/src/components/panels/VariablesPanel.tsx:347-365 at head — now reads void runSchemaEdit(...).then((changed) => { if (changed) dropPreviewOverride(id); }). The eager unconditional call is gone; the override drops only after the SDK write returns a truthy changed signal. This is the exact .then(changed => changed && dropPreviewOverride(id)) shape from Rames-D's R1 ask.
  • Comment at lines 355-357 documents the fix intent directly.

Hold-2 (1162 studio-UI lines with zero new test files):

  • New packages/studio/src/components/panels/VariablesDeclarationForm.test.ts locks the two highest-value asks from Rames-D's R1:
    • mergeDeclarationEdit unmodeled-key round-trip: source / default_name / brandRole survive on same-type edits; type-change correctly drops old-type metadata (min/max cleared).
    • declarationFromDraft edges: coverage across all 6 variable types + full draft→decl→draft round-trip.
  • sanitizeId and commitPreviewValue equality-drop from the R1 test wishlist are still uncovered — the two highest-value asks landed; these two remaining ones are fine as optional follow-ups.

No new issues introduced.

No residuals from my side.

R2 by Via

@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 265f388 to afd70d5 Compare July 9, 2026 19:34
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from bcdf263 to 75a6e89 Compare July 9, 2026 19:34
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from afd70d5 to 7631965 Compare July 9, 2026 19:46
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from 75a6e89 to 04310c9 Compare July 9, 2026 19:46

@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.

🟢 LGTM at R3 — 75a6e89.

Both R2 holds preserved through the rebase-onto-#2098. R2 verdict stands.

Verified at head:

  • packages/studio/src/components/panels/VariablesPanel.tsx:360if (changed) dropPreviewOverride(id); inside .then on the schema-edit result. Shape at head is runSchemaEdit(...).then((changed) => { if (changed) dropPreviewOverride(id); }) — semantically identical to R2's inlined .then(changed => changed && dropPreviewOverride(id)), style-only difference.
  • dropPreviewOverride in useCallback dep array at :364 preserved.
  • mergeDeclarationEdit import at :16, call site at :139 — VariablesDeclarationForm.test.ts still wired.

handleRemove ordering fix + mergeDeclarationEdit round-trip test both survive.

R3 by Via

@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from 04310c9 to 98a0e84 Compare July 9, 2026 20:16
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 7631965 to 901a425 Compare July 9, 2026 20:16
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch from 98a0e84 to b34ad85 Compare July 9, 2026 20:31
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 901a425 to 2e0b884 Compare July 9, 2026 20:31
Base automatically changed from 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding to main July 9, 2026 20:42
@jrusso1020 jrusso1020 dismissed miguel-heygen’s stale review July 9, 2026 20:42

The base branch was changed.

@jrusso1020 jrusso1020 merged commit 51b5967 into main Jul 9, 2026
25 of 31 checks passed
@jrusso1020 jrusso1020 deleted the 07-07-feat_studio_variables_inspector_panel_with_live_preview_values branch July 9, 2026 20:42
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.

5 participants