feat(studio): bind selected element properties to variables

Ninth PR of the template-variables stack: the promote-a-property gesture.
Select an element on the canvas/timeline, open the Variables tab, and the
panel offers per-property bind actions.

- "Bind selected" card in the Variables panel, built from the selection:
  image/media source (img/video/audio), text, text color, background, and
  font. Each action declares a variable whose default is the element's
  CURRENT value (promoting never changes the render — computed rgb colors
  convert to hex, the first computed font family becomes the font default)
  and writes the declarative binding the runtime resolves: data-var-src /
  data-var-text attributes or `<prop>: var(--id)` styles. Declare + bind
  run as one batched schema edit (one undo step); binding to an
  already-declared id skips the declare and just binds.
- guarded to selections from the composition the session models — a
  selection in another source file never writes bindings into this one.
- core: extract readVariablesForElement into runtime/variableScope.ts,
  shared by color grading and the declarative bindings (was duplicated).
- fix(studio-server): buildSubCompositionHtml's extractElementAttrs
  rebuilt html/body attributes without HTML-escaping values, shredding
  quote-bearing attributes — data-composition-variables (a JSON array)
  came out as mangled bogus attributes, so getVariables() silently
  returned {} on every /preview/comp/* page (no declared defaults, no
  runtime bindings). Pre-existing bug surfaced by live-testing this
  feature; regression test added.

Verified end-to-end in a live session: select headline → Bind text color
→ declaration + var(--headline-color) written to disk → override in the
panel → runtime applies the custom prop and the element renders the
override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James
2026-07-09 13:31:03 -07:00
co-authored by Claude Fable 5
parent f7ee0768ae
commit 267b289bb8
7 changed files with 358 additions and 4 deletions
@@ -97,7 +97,12 @@ export function useInspectorState(
STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive,
// Keep the selection box + motion path drawn even when the Inspector is
// collapsed — closing the panel shouldn't visually deselect the element.
shouldShowSelectedDomBounds: inspectorPanelActive && !isPlaying && !isGestureRecording,
// The Variables tab also works against the canvas selection (bind card),
// so the selection outline stays visible there too.
shouldShowSelectedDomBounds:
(inspectorPanelActive || rightPanelTab === "variables") &&
!isPlaying &&
!isGestureRecording,
};
}, [rightPanelTab, rightInspectorPanes, rightCollapsed, isPlaying, isGestureRecording]);
}