mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
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:
@@ -156,6 +156,13 @@ function extractTemplateInnerHtml(rawComp: string): string | null {
|
||||
return template ? template.innerHTML : null;
|
||||
}
|
||||
|
||||
/** Attribute values read from the DOM are decoded — re-escape on rebuild or
|
||||
* quote-bearing values (data-composition-variables is a JSON array) shred
|
||||
* the wrapper's markup into bogus attributes. */
|
||||
function escapeAttrValue(value: string): string {
|
||||
return value.replace(/&/g, "&").replace(/"/g, """);
|
||||
}
|
||||
|
||||
function extractElementAttrs(el: Element): string {
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < el.attributes.length; i++) {
|
||||
@@ -163,7 +170,7 @@ function extractElementAttrs(el: Element): string {
|
||||
if (attr.value === "") {
|
||||
parts.push(attr.name);
|
||||
} else {
|
||||
parts.push(`${attr.name}="${attr.value}"`);
|
||||
parts.push(`${attr.name}="${escapeAttrValue(attr.value)}"`);
|
||||
}
|
||||
}
|
||||
return parts.join(" ");
|
||||
|
||||
@@ -613,3 +613,27 @@ describe("preview ?variables= injection", () => {
|
||||
expect(html).toContain('window.__hfVariables={"accent":"#f00"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe("sub-composition preview attribute integrity", () => {
|
||||
it("preserves quote-bearing html attributes (data-composition-variables JSON)", async () => {
|
||||
const projectDir = createProjectDir();
|
||||
const decls = JSON.stringify([
|
||||
{ id: "title", type: "string", label: "Title", default: "Hello" },
|
||||
]);
|
||||
writeFileSync(
|
||||
join(projectDir, "card.html"),
|
||||
`<!doctype html><html data-composition-variables='${decls}'><head></head><body><div class="clip" data-start="0" data-duration="2">x</div></body></html>`,
|
||||
);
|
||||
const app = new Hono();
|
||||
registerPreviewRoutes(app, createAdapter(projectDir));
|
||||
|
||||
const res = await app.request("http://localhost/projects/demo/preview/comp/card.html");
|
||||
expect(res.status).toBe(200);
|
||||
const html = await res.text();
|
||||
const attr = /data-composition-variables="([^"]*)"/.exec(html)?.[1] ?? "";
|
||||
// Entities decode back to the exact declared JSON — a lost/shredded
|
||||
// attribute here silently breaks getVariables() on the comp route.
|
||||
const decoded = attr.replace(/"/g, '"').replace(/&/g, "&");
|
||||
expect(JSON.parse(decoded)).toEqual(JSON.parse(decls));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user