Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/sdk/reference/composition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.<id>`
override. For a variable declared mid-session, `{ base: true }` returns its
live default (its declaration is its base).

### `listVariables`

```typescript
Expand Down Expand Up @@ -753,4 +759,3 @@ comp.dispose();
Template-driven products with host-owned history.
</Card>
</CardGroup>

46 changes: 36 additions & 10 deletions packages/sdk/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;

/** Lazily-built element snapshot, invalidated on every mutation. */
private elementsCache: ElementSnapshot[] | null = null;
/** Lazily-built root snapshot (getRootElements), invalidated alongside elementsCache. */
Expand All @@ -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<string, unknown> = {},
) {
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;
}
Expand Down Expand Up @@ -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[] {
Expand Down Expand Up @@ -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.<id>`
// 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;

Expand Down
33 changes: 33 additions & 0 deletions packages/sdk/src/session.variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<id> 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");
});
});
10 changes: 9 additions & 1 deletion packages/sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<id>` 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).
Expand Down
Loading