fix(studio): code-review and live-test fixes for the variables stack

This commit is contained in:
James
2026-07-09 13:31:03 -07:00
parent 010d49327e
commit bc0e0b314b
9 changed files with 174 additions and 61 deletions
+14 -1
View File
@@ -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) {
+10 -26
View File
@@ -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 };
+13 -8
View File
@@ -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);