From db5e062211fbad324b67bcc672d4e92cc4e2d351 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 15 Jul 2026 14:50:33 -0700 Subject: [PATCH] feat(sdk): getVariableValue({ base: true }) reads pre-override declared defaults --- docs/sdk/reference/composition.mdx | 9 ++++- packages/sdk/src/session.ts | 46 +++++++++++++++++----- packages/sdk/src/session.variables.test.ts | 33 ++++++++++++++++ packages/sdk/src/types.ts | 10 ++++- 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/docs/sdk/reference/composition.mdx b/docs/sdk/reference/composition.mdx index 53413b26e4..66d5c72f15 100644 --- a/docs/sdk/reference/composition.mdx +++ b/docs/sdk/reference/composition.mdx @@ -117,7 +117,7 @@ comp.setVariableValue("heroImage", { url: "/assets/hero.jpg", fit: "cover" }); ### `getVariableValue` ```typescript -getVariableValue(id: string): string | number | boolean | FontValue | ImageValue | undefined +getVariableValue(id: string, opts?: { base?: boolean }): string | number | boolean | FontValue | ImageValue | undefined ``` Return a declared variable's current `default` value, or `undefined` if the id is undeclared or has no value set. @@ -126,6 +126,12 @@ Return a declared variable's current `default` value, or `undefined` if the id i const color = comp.getVariableValue("brandColor"); // "#6C5CE7" ``` +Pass `{ base: true }` to read the default **as authored at open** — before the +T3 override-set or any `setVariableValue` folded into the declaration. This is +the value to restore when a host replays an undo that removes a `var.` +override. For a variable declared mid-session, `{ base: true }` returns its +live default (its declaration is its base). + ### `listVariables` ```typescript @@ -753,4 +759,3 @@ comp.dispose(); Template-driven products with host-owned history. - diff --git a/packages/sdk/src/session.ts b/packages/sdk/src/session.ts index d8cdc11280..2190870608 100644 --- a/packages/sdk/src/session.ts +++ b/packages/sdk/src/session.ts @@ -88,6 +88,13 @@ class CompositionImpl implements Composition { /** Accumulated override-set — T3 embedded mode fold contract. */ private overrides: OverrideSet; + /** + * Declared variable defaults as authored at open — captured BEFORE the + * T3 override-set (and any later setVariableValue) folded into the + * declarations. Serves getVariableValue({ base: true }). + */ + private readonly baseVariableDefaults: Record; + /** Lazily-built element snapshot, invalidated on every mutation. */ private elementsCache: ElementSnapshot[] | null = null; /** Lazily-built root snapshot (getRootElements), invalidated alongside elementsCache. */ @@ -114,11 +121,16 @@ class CompositionImpl implements Composition { /** Override-set state at outermost batch entry — restored if the batch throws. */ private batchOverridesSnapshot: OverrideSet = {}; - constructor(parsed: ParsedDocument, opts: OpenCompositionOptions) { + constructor( + parsed: ParsedDocument, + opts: OpenCompositionOptions, + baseVariableDefaults: Record = {}, + ) { this.parsed = parsed; this.persist = opts.persist; this.preview = opts.preview; this.overrides = { ...(opts.overrides ?? {}) }; + this.baseVariableDefaults = baseVariableDefaults; this.previewSelectionUnsubscribe = this.preview?.on("selection", (ids) => this.updateSelection(ids)) ?? null; } @@ -169,14 +181,20 @@ class CompositionImpl implements Composition { // ── #2098 CRUD conveniences — thin aliases over the canonical surface below. // They delegate so the per-file declaration-element scope (template/fragment // sub-comps included) is resolved in exactly one place. - getVariableValue(id: string): string | number | boolean | FontValue | ImageValue | undefined { - return this.getVariableValues()[id] as - | string - | number - | boolean - | FontValue - | ImageValue - | undefined; + getVariableValue( + id: string, + opts?: { base?: boolean }, + ): string | number | boolean | FontValue | ImageValue | undefined { + // { base: true } reads the declaration default as authored at open — + // BEFORE the override-set / any setVariableValue folded into it. This + // is the value an undo-to-base must restore; the live default IS the + // override after a fold. Ids unseen at open (declared mid-session) fall + // through to the live default — their declaration is their base. + const source = + opts?.base && id in this.baseVariableDefaults + ? this.baseVariableDefaults + : this.getVariableValues(); + return source[id] as string | number | boolean | FontValue | ImageValue | undefined; } listVariables(): CompositionVariable[] { @@ -877,11 +895,19 @@ export async function openComposition( // the query API derives element snapshots from it lazily. const parsed = parseMutable(html); + // Pre-override declared defaults — applyOverrideSet below folds `var.` + // overrides destructively into the declarations, so this is the last moment + // the authored base values are readable. getVariableValue({ base: true }) + // serves them for the rest of the session (undo-to-base restores). + const baseVariableDefaults = readDeclaredDefaults( + declarationElement(parsed.document, parsed.wrapped), + ); + // T3 embedded: replay the stored override-set onto the base in one pass, // so the session exposes the user's exact edited state — not the template. if (opts?.overrides) applyOverrideSet(parsed, opts.overrides); - const session = new CompositionImpl(parsed, opts ?? {}); + const session = new CompositionImpl(parsed, opts ?? {}, baseVariableDefaults); const isEmbedded = opts?.overrides !== undefined; diff --git a/packages/sdk/src/session.variables.test.ts b/packages/sdk/src/session.variables.test.ts index 05880900cf..1a3eb15735 100644 --- a/packages/sdk/src/session.variables.test.ts +++ b/packages/sdk/src/session.variables.test.ts @@ -138,3 +138,36 @@ describe("validateVariableValues", () => { expect(comp.validateVariableValues({ title: "still-a-string" })).toEqual([]); }); }); + +describe("getVariableValue base defaults", () => { + it("returns the pre-override declared default with { base: true }", async () => { + // openComposition({overrides}) folds var. into the declaration — + // the live default becomes the override. base:true must still see + // the authored value. + const comp = await openComposition(BASE_HTML, { + overrides: { "var.accent": "#FF0000" }, + }); + expect(comp.getVariableValue("accent")).toBe("#FF0000"); + expect(comp.getVariableValue("accent", { base: true })).toBe("#00C3FF"); + }); + + it("keeps the base default stable across live setVariableValue dispatches", async () => { + const comp = await openComposition(BASE_HTML); + comp.setVariableValue("accent", "#123456"); + expect(comp.getVariableValue("accent")).toBe("#123456"); + expect(comp.getVariableValue("accent", { base: true })).toBe("#00C3FF"); + }); + + it("returns undefined for an undeclared id with { base: true }", async () => { + const comp = await openComposition(BASE_HTML); + expect(comp.getVariableValue("never-declared", { base: true })).toBeUndefined(); + }); + + it("falls back to the live default for a variable declared mid-session", async () => { + // A variable that did not exist at open has no pre-override snapshot — + // its current declaration IS its base. + const comp = await openComposition(BASE_HTML); + comp.declareVariable({ id: "brand-title", type: "string", label: "Title", default: "Hi" }); + expect(comp.getVariableValue("brand-title", { base: true })).toBe("Hi"); + }); +}); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 7673eb55c6..d43c603e7e 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -434,8 +434,16 @@ export interface Composition { /** * Current `default` value for a declared variable, or undefined if * undeclared/unset. Convenience over getVariableValues() (kept from #2098). + * + * `{ base: true }` returns the default as authored at open — BEFORE the + * T3 override-set or any setVariableValue folded into the declaration. + * Hosts replaying an undo that removes a `var.` override should restore + * this value. Ids declared mid-session fall through to the live default. */ - getVariableValue(id: string): string | number | boolean | FontValue | ImageValue | undefined; + getVariableValue( + id: string, + opts?: { base?: boolean }, + ): string | number | boolean | FontValue | ImageValue | undefined; /** * Every declared variable's full schema (id/type/label/default/…), or [] when * none. Alias of getVariableDeclarations() (kept from #2098's surface).