mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix(studio): code-review and live-test fixes for the variables stack
This commit is contained in:
@@ -124,7 +124,20 @@ function applyVariableDefault(document: Document, id: string, newDefault: unknow
|
||||
export function applyOverrideSet(parsed: ParsedDocument, overrides: OverrideSet): void {
|
||||
const patches: JsonPatchOp[] = [];
|
||||
const rootId = findRoot(parsed.document)?.getAttribute("data-hf-id") ?? null;
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
// Whole-declaration snapshots (varDecl.{id}) must replay BEFORE value keys
|
||||
// (var.{id}): a declaration snapshot embeds the default at fold time, while
|
||||
// var.{id} always carries the latest value — insertion order alone would let
|
||||
// an older snapshot clobber a newer value.
|
||||
const entries = Object.entries(overrides).sort(([a], [b]) => {
|
||||
const aVar = a.startsWith("var.");
|
||||
const bVar = b.startsWith("var.");
|
||||
const aDecl = a.startsWith("varDecl.");
|
||||
const bDecl = b.startsWith("varDecl.");
|
||||
if (aVar && bDecl) return 1;
|
||||
if (aDecl && bVar) return -1;
|
||||
return 0; // stable — every other key keeps its insertion order
|
||||
});
|
||||
for (const [key, value] of entries) {
|
||||
const path = keyToPath(key);
|
||||
if (!path) continue;
|
||||
if (value === null) {
|
||||
|
||||
@@ -873,37 +873,21 @@ function handleSetVariableValue(
|
||||
const modelPath = variablePath(id);
|
||||
const oldVarDefault = readVariableDefault(parsed.document, id);
|
||||
|
||||
if (isObjectVariableValue(value)) {
|
||||
// Object values (font / image): write to JSON model only — objects are not
|
||||
// valid CSS custom property values (LOCKED §7).
|
||||
writeVariableDefault(parsed.document, id, value);
|
||||
const p = valueChange(modelPath, oldVarDefault ?? null, value);
|
||||
return { forward: [p.forward], inverse: [p.inverse] };
|
||||
}
|
||||
|
||||
// Scalar values: update the JSON model (B1 — drives the runtime) and also
|
||||
// keep the CSS custom prop as secondary / compat for compositions that
|
||||
// CSS-bind directly to --{id}.
|
||||
const cssVar = `--${id}`;
|
||||
const rootId = root.getAttribute("data-hf-id");
|
||||
const oldStyles = getElementStyles(root);
|
||||
const oldCssValue = oldStyles[cssVar] ?? null;
|
||||
const newVal = String(value);
|
||||
setElementStyles(root, { [cssVar]: newVal });
|
||||
// Update the JSON model (B1 — drives the runtime) and keep the CSS custom
|
||||
// prop as secondary / compat for compositions that CSS-bind directly to
|
||||
// --{id}. Object values (font / image) are not valid CSS custom property
|
||||
// values (LOCKED §7) — cssCompatChange clears any stale scalar prop instead.
|
||||
// Emitting separate model + style patches keeps apply-patches.ts pure per
|
||||
// path type, so inverse patches restore the exact pre-call state.
|
||||
writeVariableDefault(parsed.document, id, value);
|
||||
|
||||
// Emit explicit patches for both the JSON model (canonical) and the CSS compat
|
||||
// prop. Keeping them separate means apply-patches.ts can handle each path type
|
||||
// purely (variable path → model only; style path → CSS only), so inverse patches
|
||||
// correctly restore the exact pre-call state without CSS-side-effect ambiguity.
|
||||
const modelP = valueChange(modelPath, oldVarDefault ?? null, value);
|
||||
const forward: JsonPatchOp[] = [modelP.forward];
|
||||
const inverse: JsonPatchOp[] = [modelP.inverse];
|
||||
|
||||
if (rootId) {
|
||||
const cssPatch = scalarChange(stylePath(rootId, cssVar), oldCssValue, newVal);
|
||||
forward.push(cssPatch.forward);
|
||||
inverse.push(cssPatch.inverse);
|
||||
const css = cssCompatChange(parsed, id, isObjectVariableValue(value) ? null : String(value));
|
||||
if (css) {
|
||||
forward.push(css.forward);
|
||||
inverse.push(css.inverse);
|
||||
}
|
||||
|
||||
return { forward, inverse };
|
||||
|
||||
@@ -269,14 +269,7 @@ class CompositionImpl implements Composition {
|
||||
// The CSS compat channel counts as usage: a variable consumed only via
|
||||
// var(--id) in stylesheets or inline styles must not be badged unused
|
||||
// (removing it also removes the --{id} root prop and breaks the binding).
|
||||
const cssParts: string[] = [];
|
||||
for (const styleEl of Array.from(this.parsed.document.querySelectorAll("style"))) {
|
||||
cssParts.push(styleEl.textContent ?? "");
|
||||
}
|
||||
for (const el of Array.from(this.parsed.document.querySelectorAll("[style]"))) {
|
||||
cssParts.push(el.getAttribute("style") ?? "");
|
||||
}
|
||||
const cssText = cssParts.join("\n");
|
||||
const cssText = this._collectCssText();
|
||||
// Match var(--id) only at a custom-property-name boundary: the id must be
|
||||
// followed by whitespace, a comma (fallback), or the closing paren — so id
|
||||
// "foo" is NOT counted as used by an unrelated var(--foo-header). Ids are
|
||||
@@ -293,6 +286,18 @@ class CompositionImpl implements Composition {
|
||||
};
|
||||
}
|
||||
|
||||
/** All stylesheet + inline-style text, for var(--id) consumption checks. */
|
||||
private _collectCssText(): string {
|
||||
const cssParts: string[] = [];
|
||||
for (const styleEl of Array.from(this.parsed.document.querySelectorAll("style"))) {
|
||||
cssParts.push(styleEl.textContent ?? "");
|
||||
}
|
||||
for (const el of Array.from(this.parsed.document.querySelectorAll("[style]"))) {
|
||||
cssParts.push(el.getAttribute("style") ?? "");
|
||||
}
|
||||
return cssParts.join("\n");
|
||||
}
|
||||
|
||||
setPreviewVariables(values: Record<string, unknown> | null): boolean {
|
||||
if (!this.preview?.setPreviewVariables) return false;
|
||||
this.preview.setPreviewVariables(values);
|
||||
|
||||
Reference in New Issue
Block a user