From e45d655b83d07722f4b4689301363a36ec063581 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 13 Sep 2026 19:02:28 +0300 Subject: [PATCH 1/2] docs: update the binding provider example --- .../sdk/api/components/bindable-value.md | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/docs/programmable/sdk/api/components/bindable-value.md b/packages/docs/programmable/sdk/api/components/bindable-value.md index d9dac5fa6..5f5475456 100644 --- a/packages/docs/programmable/sdk/api/components/bindable-value.md +++ b/packages/docs/programmable/sdk/api/components/bindable-value.md @@ -27,7 +27,7 @@ automatically when nested beneath `BindableValueRoot`. - `detach-on-edit` unbinds targets on the first value mutation and keeps the complete interaction in one provider undo batch. - `readonly-when-bound` blocks field editing, scrubbing, and keyboard stepping. -- `edit-variable` sends changes to `provider.setValue()` instead of changing the target value. +- `edit-variable` uses `provider.prepareEdit()` to capture a stable edit key, current value, setter, and restoration callback instead of changing the target value. Focusing a bound NumberField or opening its picker is non-destructive. The policy starts only when the user types a changed draft, steps the value, or crosses the pointer-scrub threshold. Committing @@ -38,16 +38,29 @@ where possible. ## Provider example ```ts twoslash +import type { Variable } from '@open-pencil/scene-graph' import type { BindingProvider, BindingTarget } from '@open-pencil/vue' -const values = new Map([['spacing/md', 16]]) +const variable: Variable = { + id: 'spacing/md', name: 'Spacing / Medium', type: 'FLOAT', + collectionId: 'spacing', valuesByMode: { default: 16 }, + description: '', hiddenFromPublishing: false +} +const values = new Map([[variable.id, 16]]) const bindings = new Map() +const getBindingId = (target: BindingTarget) => bindings.get(`${target.nodeId}:${target.path}`) const provider: BindingProvider = { - listVariables: () => [], - filterVariables: () => [], - getBound: () => undefined, - getState: () => 'unbound', + listVariables: () => [variable], + filterVariables: term => variable.name.toLowerCase().includes(term.toLowerCase()) ? [variable] : [], + getBindingId, + getBound: target => getBindingId(target) === variable.id ? variable : undefined, + getState: targets => { + const ids = new Set(targets.map(getBindingId)) + if (ids.size === 0 || (ids.size === 1 && ids.has(undefined))) return 'unbound' + if (ids.size > 1) return 'mixed' + return ids.has(variable.id) ? 'bound' : 'unresolved' + }, resolve: id => values.get(id), bind: (target: BindingTarget, variableId) => { bindings.set(`${target.nodeId}:${target.path}`, variableId) From 70fbbddf7d7b8636ae2c9c9f1271bedf39265ef8 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 13 Sep 2026 20:13:34 +0300 Subject: [PATCH 2/2] docs: compare resolved binding values in provider state --- .../programmable/sdk/api/components/bindable-value.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/docs/programmable/sdk/api/components/bindable-value.md b/packages/docs/programmable/sdk/api/components/bindable-value.md index 5f5475456..bd256fd9f 100644 --- a/packages/docs/programmable/sdk/api/components/bindable-value.md +++ b/packages/docs/programmable/sdk/api/components/bindable-value.md @@ -49,6 +49,7 @@ const variable: Variable = { const values = new Map([[variable.id, 16]]) const bindings = new Map() const getBindingId = (target: BindingTarget) => bindings.get(`${target.nodeId}:${target.path}`) +const resolve: BindingProvider['resolve'] = id => values.get(id) const provider: BindingProvider = { listVariables: () => [variable], @@ -59,9 +60,12 @@ const provider: BindingProvider = { const ids = new Set(targets.map(getBindingId)) if (ids.size === 0 || (ids.size === 1 && ids.has(undefined))) return 'unbound' if (ids.size > 1) return 'mixed' - return ids.has(variable.id) ? 'bound' : 'unresolved' + if (!ids.has(variable.id)) return 'unresolved' + const resolved = targets.map(target => resolve(variable.id, target)) + if (resolved.some(value => value === undefined)) return 'unresolved' + return new Set(resolved).size > 1 ? 'mixed' : 'bound' }, - resolve: id => values.get(id), + resolve, bind: (target: BindingTarget, variableId) => { bindings.set(`${target.nodeId}:${target.path}`, variableId) },