@paged-media/plugin-sdk 0.2.27-canary.1 → 0.2.29-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PagedBundle, FontFaceAsset, BundleHost, ClipboardPayload, ConsentResult, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, Disposable, ShellSurface, WidgetSurface, SchemaPanelRenderer, SchemaPanelContribution, EditContextContribution, ObjectTypeContribution, Diagnostic, PagedEditor, PluginManifest, ToolContribution, PanelContribution, CommandContribution, KeybindingContribution, OverlayContribution, ImporterContribution, ExporterContribution, ToolPreviewShape, BindingsSurface, PanelProps, SchemaGate, WasmArtifact, CanvasPointerEvent, Mutation, MutationOutcome } from '@paged-media/plugin-api';
1
+ import { PagedBundle, BindingProvider, Disposable, ActiveBindingProvider, BindingPropertyRequest, BindingReadResult, BindingPropertyWrite, BindingWriteResult, BindingCollectionRequest, BindingCollectionResult, MutationInput, FontFaceAsset, BundleHost, ClipboardPayload, ConsentResult, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, ShellSurface, WidgetSurface, SchemaPanelRenderer, SchemaPanelContribution, EditContextContribution, ObjectTypeContribution, Diagnostic, TextCaret, PagedEditor, PluginManifest, ToolContribution, PanelContribution, CommandContribution, KeybindingContribution, OverlayContribution, ImporterContribution, ExporterContribution, ToolPreviewShape, BindingsSurface, PanelProps, SchemaGate, WasmArtifact, CanvasPointerEvent, Mutation, MutationOutcome, BindingProviderHandle } from '@paged-media/plugin-api';
2
2
  import { ComponentType } from 'react';
3
3
 
4
4
  /**
@@ -192,6 +192,23 @@ interface ClipboardBackend {
192
192
  read(): Promise<ClipboardPayload | null>;
193
193
  write(payload: ClipboardPayload): Promise<void>;
194
194
  }
195
+ /**
196
+ * The reader the editor injects to back `host.text.caret()` (C-9): a
197
+ * synchronous read of the user's ACTIVE text caret from the editor's
198
+ * text-editing/content-selection state (the caret lives in EDITOR
199
+ * state, not the engine). The SDK adapter owns nothing here but the
200
+ * pass-through + the honest no-backend default — `read()` answers the
201
+ * caret in the engine text-op offset convention (`ContentSelection`
202
+ * story-local offsets; see `TextCaret`), `null` when no caret is
203
+ * active. The editor's backend also answers `null` for a
204
+ * cell-qualified caret (cell-local offsets must not leak as
205
+ * story-local — the documented v1 gap). When absent,
206
+ * `host.text.caret()` is always `null` and `supports("text.caret@1")`
207
+ * is false.
208
+ */
209
+ interface TextCaretBackend {
210
+ read(): TextCaret | null;
211
+ }
195
212
  /**
196
213
  * The backend the editor injects to back `host.nativeDocument` (ADR-021 /
197
214
  * document-model direction): the isolate-safe replacement for an importer/
@@ -225,6 +242,58 @@ interface DataProviderBackend {
225
242
  * providers and consumers (different plugin hosts) meet through it. Reference
226
243
  * implementation — an RPC/isolate host swaps a proxy with the same contract. */
227
244
  declare function createDataProviderRegistry(): DataProviderBackend;
245
+ /** One provider as the registry holds it (the adapter's registration
246
+ * payload — the bundle identity the contract's answers carry as
247
+ * `provider`, plus the borrowed context type). */
248
+ interface RegisteredBindingProvider {
249
+ plugin: string;
250
+ contextType: string;
251
+ provider: BindingProvider;
252
+ }
253
+ /**
254
+ * The shared binding-provider registry (ADR-023 phase A). TWO audiences,
255
+ * deliberately on one object (like {@link DataProviderBackend}):
256
+ *
257
+ * · the SDK ADAPTER calls `register` / `setContextActive` — a bundle
258
+ * never touches these; it reaches the registry only through
259
+ * `host.contribute.bindingProvider`;
260
+ * · the HOST APP (the editor's shared panels, phase C) calls
261
+ * `activeProviders` / `readProperty` / `writeProperty` /
262
+ * `readCollection` / `applyMutation` / `onDidChange`.
263
+ *
264
+ * FALL-THROUGH IS THE HOST'S JOB, not the registry's: every resolution
265
+ * answers either a claim or a typed `resolved: false` / `handled: false`
266
+ * refusal, and the host reads/writes CORE on the refusal. The registry
267
+ * holds no editor handle and therefore cannot — and must not — fall
268
+ * through on the host's behalf; keeping the two separable is what lets
269
+ * the isolate implementation be an RPC proxy of exactly this shape.
270
+ */
271
+ interface BindingProviderBackend {
272
+ /** Register a provider. Disposing removes it without touching the
273
+ * edit context whose activation it borrows. */
274
+ register(entry: RegisteredBindingProvider): Disposable;
275
+ /** Report an edit context entering (`active: true`) or leaving. Called
276
+ * by the adapter from the shell's own `onEnter`/`onExit`. */
277
+ setContextActive(plugin: string, contextType: string, elementId: string | null, active: boolean): void;
278
+ /** A provider announced `invalidate()` — fan out to host subscribers. */
279
+ notifyChanged(plugin: string, contextType: string): void;
280
+ /** The active provider stack, INNERMOST FIRST (= precedence order). */
281
+ activeProviders(): readonly ActiveBindingProvider[];
282
+ readProperty(request: BindingPropertyRequest): Promise<BindingReadResult>;
283
+ writeProperty(request: BindingPropertyWrite): Promise<BindingWriteResult>;
284
+ readCollection(request: BindingCollectionRequest): Promise<BindingCollectionResult>;
285
+ /** Offer a structural mutation to the active providers before sending
286
+ * it to core (`layerMove`, `createSwatch`, …). */
287
+ applyMutation(mutation: MutationInput): Promise<BindingWriteResult>;
288
+ /** Fires when the active stack changes OR a provider invalidates —
289
+ * the host panel's re-read signal. */
290
+ onDidChange(listener: () => void): Disposable;
291
+ }
292
+ /** Build a shared binding-provider registry (ADR-023 phase A). The editor
293
+ * holds ONE and injects it into every `createBundleHost` call as
294
+ * `options.bindingProviders`. Reference implementation — an
295
+ * RPC/isolate host swaps a proxy with the same contract. */
296
+ declare function createBindingProviderRegistry(): BindingProviderBackend;
228
297
  /**
229
298
  * Trust posture the HOST asserts for a bundle at the load path
230
299
  * (`plugin-trust-line.md`). Same-realm execution — full `BundleHost`
@@ -241,6 +310,16 @@ declare function createDataProviderRegistry(): DataProviderBackend;
241
310
  * still does not load until the isolate/RPC host gate is built.
242
311
  */
243
312
  type BundleTrust = "first-party";
313
+ /**
314
+ * What the HOST APP injects as `shell` (K-10). Structurally the
315
+ * bundle-facing {@link ShellSurface}, except that doors added AFTER a
316
+ * host app adopted the option are optional here — today `saveFile`,
317
+ * which the editor gained one release after `pickFile`. A host that
318
+ * implements the full surface is assignable unchanged; a host that
319
+ * omits `saveFile` still injects a valid backend and
320
+ * `supports("shell.saveFile@1")` reports the truth.
321
+ */
322
+ type ShellBackend = Omit<ShellSurface, "saveFile"> & Partial<Pick<ShellSurface, "saveFile">>;
244
323
  interface CreateBundleHostOptions {
245
324
  /**
246
325
  * The host's trust assertion for this bundle (`plugin-trust-line.md`).
@@ -262,12 +341,23 @@ interface CreateBundleHostOptions {
262
341
  * through it. When absent, `host.dataProviders` discover() is empty +
263
342
  * register() is a no-op and `supports("dataProviders@1")` is false. */
264
343
  dataProviders?: DataProviderBackend;
344
+ /** Host-provided SHARED BINDING-PROVIDER registry (ADR-023 phase A): the
345
+ * editor creates ONE (`createBindingProviderRegistry`) and injects the SAME
346
+ * instance into every plugin host, so its own panels can ask "who resolves
347
+ * this path / collection / op right now?" across all loaded bundles. When
348
+ * absent, `host.contribute.bindingProvider` warns + returns an inert handle
349
+ * (nothing would ever consult the provider) and
350
+ * `supports("bindings.provider@1")` is false. */
351
+ bindingProviders?: BindingProviderBackend;
265
352
  /** Console sink override (tests). */
266
353
  console?: Pick<Console, "debug" | "info" | "warn" | "error">;
267
354
  /** Shell actions the HOST APP owns (the cockpit's panel placement).
268
355
  * When absent, `host.shell` warns and no-ops, and
269
- * `supports("shell.openPanel@1")` answers false. */
270
- shell?: ShellSurface;
356
+ * `supports("shell.openPanel@1")` answers false. Typed as
357
+ * {@link ShellBackend}, not `ShellSurface`: doors the host app has
358
+ * not adopted yet are OPTIONAL here and each answers its own honest
359
+ * no-backend value through the wrapper. */
360
+ shell?: ShellBackend;
271
361
  /** Host-provided panel widgets (W-04): the real code editor lives in
272
362
  * the editor's UI package and is injected here. When absent,
273
363
  * `host.widgets` is the plain-textarea fallback and
@@ -316,6 +406,12 @@ interface CreateBundleHostOptions {
316
406
  * When absent, reads answer empty and writes reject (the honest
317
407
  * no-store door). */
318
408
  blobStore?: BlobStore;
409
+ /** Host-provided TEXT-CARET reader (C-9). When present,
410
+ * `host.text.caret()` reads the user's active text caret through it
411
+ * and `supports("text.caret@1")` answers true. When absent, `caret()`
412
+ * always answers `null` (the honest no-reader door — a read door like
413
+ * `text.measureString`, no capability gate). */
414
+ textCaret?: TextCaretBackend;
319
415
  /** Host-provided CLIPBOARD backend (K-6 / S-14). When present,
320
416
  * `host.clipboard.read/write` go through it (capability-gated on
321
417
  * `capabilities.clipboard`) and `supports("clipboard@1")` answers true.
@@ -441,7 +537,7 @@ interface RecordedContribution {
441
537
  id: string;
442
538
  value: ToolContribution | PanelContribution | SchemaPanelContribution | CommandContribution | KeybindingContribution | OverlayContribution | EditContextContribution | ObjectTypeContribution | ImporterContribution | ExporterContribution;
443
539
  }
444
- interface HarnessOptions extends LoadHeadlessEngineOptions, Pick<CreateBundleHostOptions, "console" | "storage" | "capabilityMode" | "assetSource" | "blobStore" | "clipboard" | "secrets"> {
540
+ interface HarnessOptions extends LoadHeadlessEngineOptions, Pick<CreateBundleHostOptions, "console" | "storage" | "capabilityMode" | "assetSource" | "blobStore" | "clipboard" | "secrets" | "bindingProviders"> {
445
541
  }
446
542
  /** What `createHeadlessHost` resolves to: a real engine-backed host plus
447
543
  * the conformance affordances (load an IDML, read the contribution log,
@@ -481,6 +577,21 @@ interface HeadlessHost {
481
577
  * Béziers) rather than a flattened polyline. `null` until set, and
482
578
  * reset to `null` when the bundle clears its preview. */
483
579
  lastToolPreview(): ToolPreviewShape | null;
580
+ /** K-9 — the last LIST a bundle pushed through
581
+ * `host.overlay.setToolPreviews`, or `null` when it cleared / never
582
+ * used the multi-shape door. The harness wires the multi-shape sink,
583
+ * so `supports("overlay.multiPreview@1")` is true headlessly and a
584
+ * bundle's "geometry AND label at once" claim is assertable without a
585
+ * browser. `lastToolPreview()` tracks the list's FIRST shape. */
586
+ lastToolPreviews(): readonly ToolPreviewShape[] | null;
587
+ /** ADR-023 phase A — the SHARED binding-provider registry this host
588
+ * injected. It is the HOST side of the seam, so a conformance test
589
+ * plays the part the editor's shared panel plays in phase C: enter
590
+ * the bundle's edit context, then `readProperty` / `readCollection` /
591
+ * `applyMutation` through here and assert the bundle answered (and
592
+ * that a path it does not declare comes back as a typed refusal to
593
+ * fall through to core). */
594
+ readonly bindingProviders: BindingProviderBackend;
484
595
  /** Load an IDML package into the headless document. Resolves to the
485
596
  * loaded page ids (or throws on a parse failure). */
486
597
  load(idml: Uint8Array): Promise<string[]>;
@@ -709,5 +820,20 @@ declare function contributeEditContext(host: BundleHost, contribution: EditConte
709
820
  * matching element enters its `editContextType` instead of group
710
821
  * descent. Capability-gated on `contributes.objectTypes`. */
711
822
  declare function contributeObjectType(host: BundleHost, contribution: ObjectTypeContribution): Disposable;
823
+ /**
824
+ * ADR-023 phase A — register a BINDING PROVIDER: while `contextType` is
825
+ * the ACTIVE edit context, this bundle resolves what the HOST's own
826
+ * panels bind to (typed paths, named collections, structural ops; reads
827
+ * and writes both), so one host-owned panel serves many content types
828
+ * instead of each plugin minting its own copy.
829
+ *
830
+ * Lives here rather than in its own module deliberately: a provider
831
+ * borrows its edit context's ACTIVATION, so the two doors belong to one
832
+ * lifecycle and reading them side by side is the point. `contextType`
833
+ * must be a type this bundle already passed to
834
+ * {@link contributeEditContext} — the ordering is enforced, not
835
+ * conventional.
836
+ */
837
+ declare function contributeBindingProvider(host: BundleHost, contextType: string, provider: BindingProvider): BindingProviderHandle;
712
838
 
713
- export { API_VERSION, ASSET_BUDGETS, BLOB_BUDGETS, type BlobStore, type BundleAssetProvider, type BundleAssetSource, type BundleHostHandle, type BundleTrust, CANVAS_WASM_PKG, CLICK_DRAG_THRESHOLD_PX, type ClipboardBackend, type ConsentBackend, type CreateBundleHostOptions, type DataProviderBackend, type DiagnosticsSink, DisposableStore, FALLBACK_WIDGETS, HOST_FEATURES, type HarnessOptions, type HeadlessCanvasWorker, type HeadlessHost, type HeadlessHostHandle, type LoadBundleWasmOptions, type LoadHeadlessEngineOptions, type LoadedBundle, type LoadedBundleWasm, type LoadedEngine, type NativeDocumentBackend, type PageDrag, PluginApiNotImplemented, PluginCapabilityError, type RecordableAssetSource, type RecordedContribution, type RecordedFontFaceRequest, type SecretStoreBackend, type SeededFace, type SpawnedWorker, type StorageBacking, WASM_BUDGETS, WORKER_BUDGETS, type WorkerBackend, beginPageDrag, commitAndSelect, contributeEditContext, contributeObjectType, contributePanel, contributeSchemaPanel, contributeTool, createBundleHost, createDataProviderRegistry, createHeadlessHost, createRecordableAssetSource, defineBundle, endLocalFor, loadBundle, loadBundleWasm, loadHeadlessEngine, makeSchemaPanelComponent, protocolFromVersion, pxToPt, readVendoredWireVersion, resolveCanvasWasm, resolveGate, satisfiesApiVersion, toDisposable };
839
+ export { API_VERSION, ASSET_BUDGETS, BLOB_BUDGETS, type BindingProviderBackend, type BlobStore, type BundleAssetProvider, type BundleAssetSource, type BundleHostHandle, type BundleTrust, CANVAS_WASM_PKG, CLICK_DRAG_THRESHOLD_PX, type ClipboardBackend, type ConsentBackend, type CreateBundleHostOptions, type DataProviderBackend, type DiagnosticsSink, DisposableStore, FALLBACK_WIDGETS, HOST_FEATURES, type HarnessOptions, type HeadlessCanvasWorker, type HeadlessHost, type HeadlessHostHandle, type LoadBundleWasmOptions, type LoadHeadlessEngineOptions, type LoadedBundle, type LoadedBundleWasm, type LoadedEngine, type NativeDocumentBackend, type PageDrag, PluginApiNotImplemented, PluginCapabilityError, type RecordableAssetSource, type RecordedContribution, type RecordedFontFaceRequest, type RegisteredBindingProvider, type SecretStoreBackend, type SeededFace, type ShellBackend, type SpawnedWorker, type StorageBacking, type TextCaretBackend, WASM_BUDGETS, WORKER_BUDGETS, type WorkerBackend, beginPageDrag, commitAndSelect, contributeBindingProvider, contributeEditContext, contributeObjectType, contributePanel, contributeSchemaPanel, contributeTool, createBindingProviderRegistry, createBundleHost, createDataProviderRegistry, createHeadlessHost, createRecordableAssetSource, defineBundle, endLocalFor, loadBundle, loadBundleWasm, loadHeadlessEngine, makeSchemaPanelComponent, protocolFromVersion, pxToPt, readVendoredWireVersion, resolveCanvasWasm, resolveGate, satisfiesApiVersion, toDisposable };
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
1
5
  // src/define-bundle.ts
2
6
  function defineBundle(bundle) {
3
7
  return bundle;
@@ -5,8 +9,10 @@ function defineBundle(bundle) {
5
9
 
6
10
  // src/disposables.ts
7
11
  var DisposableStore = class {
8
- items = [];
9
- disposed = false;
12
+ constructor() {
13
+ __publicField(this, "items", []);
14
+ __publicField(this, "disposed", false);
15
+ }
10
16
  get isDisposed() {
11
17
  return this.disposed;
12
18
  }
@@ -42,19 +48,31 @@ function toDisposable(fn) {
42
48
  };
43
49
  }
44
50
 
45
- // src/widgets-fallback.tsx
51
+ // src/react-optional.ts
46
52
  var createElement = null;
47
- try {
48
- ({ createElement } = await import("react"));
49
- } catch {
53
+ var state = "pending";
54
+ void import("react").then(
55
+ (react) => {
56
+ createElement = react.createElement;
57
+ state = "ready";
58
+ },
59
+ () => {
60
+ state = "absent";
61
+ }
62
+ );
63
+ function requireCreateElement(absent) {
64
+ if (createElement) return createElement;
65
+ throw new Error(
66
+ state === "pending" ? `${absent} (React had not finished resolving when this rendered.)` : absent
67
+ );
50
68
  }
69
+
70
+ // src/widgets-fallback.tsx
51
71
  function TextareaCodeEditor(props) {
52
- if (!createElement) {
53
- throw new Error(
54
- "plugin-sdk: host.widgets.CodeEditor fell back to the built-in textarea, but React is not installed. Install react, or inject a widget catalog via createBundleHost({ widgets })."
55
- );
56
- }
57
- return createElement("textarea", {
72
+ const createElement2 = requireCreateElement(
73
+ "plugin-sdk: host.widgets.CodeEditor fell back to the built-in textarea, but React is not installed. Install react, or inject a widget catalog via createBundleHost({ widgets })."
74
+ );
75
+ return createElement2("textarea", {
58
76
  value: props.value,
59
77
  readOnly: props.readOnly,
60
78
  spellCheck: false,
@@ -80,18 +98,10 @@ var FALLBACK_WIDGETS = {
80
98
  };
81
99
 
82
100
  // src/schema-panel.tsx
83
- var createElement2 = null;
84
- try {
85
- ({ createElement: createElement2 } = await import("react"));
86
- } catch {
87
- }
88
101
  function h() {
89
- if (!createElement2) {
90
- throw new Error(
91
- "plugin-sdk: rendering a schema panel needs React, which is not installed."
92
- );
93
- }
94
- return createElement2;
102
+ return requireCreateElement(
103
+ "plugin-sdk: rendering a schema panel needs React, which is not installed."
104
+ );
95
105
  }
96
106
  function resolveGate(gate, lookup) {
97
107
  if (gate === void 0) return true;
@@ -139,6 +149,14 @@ var HOST_FEATURES = [
139
149
  "contribute.overlay@1",
140
150
  "contribute.editContext@1",
141
151
  "contribute.objectType@1",
152
+ // ADR-023 phase A — the binding-provider DOOR exists in this SDK (a
153
+ // bundle may call `host.contribute.bindingProvider` without a
154
+ // TypeError). STATIC, and it deliberately does NOT claim that anything
155
+ // will consult the provider — that is the separate, DYNAMIC
156
+ // `bindings.provider@1`, which is true only when the host wired a
157
+ // shared registry. Same static-door / dynamic-backing split as
158
+ // `contribute.schemaPanel@1` + `schemaPanel.renderer@1`.
159
+ "contribute.bindingProvider@1",
142
160
  "contribute.importer@1",
143
161
  "contribute.exporter@1",
144
162
  "document.mutate@1",
@@ -152,6 +170,12 @@ var HOST_FEATURES = [
152
170
  "document.elementProperties@1",
153
171
  "document.placeholders@1",
154
172
  "document.tree@1",
173
+ // C-16 — the per-element parentage read. STATIC like `document.tree@1`
174
+ // and for the same reason: it is served ENTIRELY by this SDK from the
175
+ // scene tree the existing `requestSceneTree` wire query already
176
+ // returns, so there is no engine version to gate on (no core door was
177
+ // needed — DESIGN.md §4.3d).
178
+ "document.parentOf@1",
155
179
  "document.onDidChange@1",
156
180
  "document.getMetadata@1",
157
181
  "document.setMetadata@1",
@@ -160,6 +184,13 @@ var HOST_FEATURES = [
160
184
  "overlay.toolPreview@1",
161
185
  "storage@1",
162
186
  "diagnostics@1",
187
+ // The overlay TEXT primitive (RFI "overlay carries shapes only" —
188
+ // consumer: paged.draw's Measure readout). A pure pass-through on the
189
+ // same tool-preview channel; the renderer that knows the variant ships
190
+ // in the same host release that bumps this SDK, so the flag is static
191
+ // like the channel's own `overlay.toolPreview@1`. The headless harness
192
+ // records the shape verbatim.
193
+ "overlay.text@1",
163
194
  // C-5 / I-04 (core v42): the placed-image bytes read is engine-served
164
195
  // through the wire (no injected source), so it is unconditionally
165
196
  // implemented at the pinned canvas-wasm — unlike assets.fonts@1, which
@@ -168,7 +199,17 @@ var HOST_FEATURES = [
168
199
  // .paged container parts door (file-format.md) — the engine (canvas-wasm
169
200
  // v51) is the writer, so it is live whenever a bundle runs (the worker
170
201
  // handshake guarantees a v51-protocol worker).
171
- "storage.parts@1"
202
+ "storage.parts@1",
203
+ // B-22 (protocol v57) — the planar-region read door. STATIC, and the
204
+ // flag means exactly one thing: this SDK's document surface IMPLEMENTS
205
+ // `planarRegions` and forwards it (retiring the `host.editor.client`
206
+ // escape hatch). It does NOT claim the host's engine carries v57 — the
207
+ // vendored wire is still 0.51, so that answer is per-call: a pre-v57
208
+ // engine (or any host that doesn't answer the query) comes back as
209
+ // `{ found: false, reason }`, never a fake empty face list. Same
210
+ // honesty split as the protocol-ahead mutations (mutations.ts): the
211
+ // engine-level gate is the worker handshake, not `supports()`.
212
+ "document.planarRegions@1"
172
213
  ];
173
214
  var PluginApiNotImplemented = class extends Error {
174
215
  constructor(member, pointer) {
@@ -179,15 +220,15 @@ var PluginApiNotImplemented = class extends Error {
179
220
  }
180
221
  };
181
222
  var PluginCapabilityError = class extends Error {
182
- /** The host door that was called (e.g. `"contribute.tool"`). */
183
- door;
184
- /** The manifest declaration that would authorize it (e.g.
185
- * `'contributes.tools[] must include "media.paged.web.tool.pen"'`). */
186
- missingDeclaration;
187
223
  constructor(door, missingDeclaration, pluginId) {
188
224
  super(
189
225
  `plugin-api: ${pluginId} called ${door} without declaring it \u2014 ${missingDeclaration}. Manifest capabilities are ENFORCED (trust-line W0.11): declare the use or the host refuses it.`
190
226
  );
227
+ /** The host door that was called (e.g. `"contribute.tool"`). */
228
+ __publicField(this, "door");
229
+ /** The manifest declaration that would authorize it (e.g.
230
+ * `'contributes.tools[] must include "media.paged.web.tool.pen"'`). */
231
+ __publicField(this, "missingDeclaration");
191
232
  this.name = "PluginCapabilityError";
192
233
  this.door = door;
193
234
  this.missingDeclaration = missingDeclaration;
@@ -280,6 +321,144 @@ function createDataProviderRegistry() {
280
321
  }
281
322
  };
282
323
  }
324
+ function createBindingProviderRegistry() {
325
+ const providers = /* @__PURE__ */ new Map();
326
+ const stack = [];
327
+ const listeners = /* @__PURE__ */ new Set();
328
+ const key = (plugin, contextType) => `${plugin}\0${contextType}`;
329
+ const emit = () => {
330
+ for (const l of [...listeners]) l();
331
+ };
332
+ const active = () => {
333
+ const out = [];
334
+ for (let i = stack.length - 1; i >= 0; i--) {
335
+ const frame = stack[i];
336
+ for (const entry of providers.get(key(frame.plugin, frame.contextType)) ?? []) {
337
+ out.push({ frame, entry });
338
+ }
339
+ }
340
+ return out;
341
+ };
342
+ const declaresPath = (e, path) => e.provider.provides.paths?.includes(path) ?? false;
343
+ const declaresWritablePath = (e, path) => {
344
+ const w = e.provider.provides.writablePaths;
345
+ if (w === void 0) return declaresPath(e, path);
346
+ return declaresPath(e, path) && w.includes(path);
347
+ };
348
+ const declaresCollection = (e, collection) => e.provider.provides.collections?.includes(collection) ?? false;
349
+ return {
350
+ register(entry) {
351
+ const k = key(entry.plugin, entry.contextType);
352
+ const list = providers.get(k) ?? [];
353
+ list.push(entry);
354
+ providers.set(k, list);
355
+ emit();
356
+ return toDisposable(() => {
357
+ const cur = providers.get(k);
358
+ if (!cur) return;
359
+ const i = cur.indexOf(entry);
360
+ if (i >= 0) cur.splice(i, 1);
361
+ if (cur.length === 0) providers.delete(k);
362
+ emit();
363
+ });
364
+ },
365
+ setContextActive(plugin, contextType, elementId, isActive) {
366
+ if (isActive) {
367
+ stack.push({ plugin, contextType, elementId });
368
+ } else {
369
+ for (let i = stack.length - 1; i >= 0; i--) {
370
+ if (stack[i].plugin === plugin && stack[i].contextType === contextType) {
371
+ stack.splice(i, 1);
372
+ break;
373
+ }
374
+ }
375
+ }
376
+ emit();
377
+ },
378
+ notifyChanged() {
379
+ emit();
380
+ },
381
+ activeProviders() {
382
+ return active().map(({ frame, entry }) => ({
383
+ plugin: frame.plugin,
384
+ contextType: frame.contextType,
385
+ elementId: frame.elementId,
386
+ provides: entry.provider.provides
387
+ }));
388
+ },
389
+ async readProperty(request) {
390
+ const rowScoped = request.target.kind === "row" ? request.target.collection : null;
391
+ for (const { entry } of active()) {
392
+ if (rowScoped !== null && !declaresCollection(entry, rowScoped)) continue;
393
+ if (!declaresPath(entry, request.path)) {
394
+ if (rowScoped === null) continue;
395
+ return {
396
+ resolved: true,
397
+ provider: entry.plugin,
398
+ read: {
399
+ kind: "absent",
400
+ reason: `${entry.plugin} owns collection "${rowScoped}" but does not serve "${request.path}" for its rows`
401
+ }
402
+ };
403
+ }
404
+ if (!entry.provider.readProperty) continue;
405
+ const read = await entry.provider.readProperty(request);
406
+ if (read.kind === "decline") continue;
407
+ return { resolved: true, provider: entry.plugin, read };
408
+ }
409
+ return {
410
+ resolved: false,
411
+ reason: `no active binding provider claimed "${request.path}"`
412
+ };
413
+ },
414
+ async writeProperty(request) {
415
+ const rowScoped = request.target.kind === "row" ? request.target.collection : null;
416
+ for (const { entry } of active()) {
417
+ if (rowScoped !== null && !declaresCollection(entry, rowScoped)) continue;
418
+ if (!declaresWritablePath(entry, request.path)) continue;
419
+ if (!entry.provider.writeProperty) continue;
420
+ const write = await entry.provider.writeProperty(request);
421
+ if (write.kind === "decline") continue;
422
+ return { handled: true, provider: entry.plugin, outcome: write.outcome };
423
+ }
424
+ return {
425
+ handled: false,
426
+ reason: `no active binding provider claimed a write to "${request.path}"`
427
+ };
428
+ },
429
+ async readCollection(request) {
430
+ for (const { entry } of active()) {
431
+ if (!declaresCollection(entry, request.collection)) continue;
432
+ if (!entry.provider.readCollection) continue;
433
+ const read = await entry.provider.readCollection(request);
434
+ if (read.kind === "decline") continue;
435
+ return { resolved: true, provider: entry.plugin, rows: read.rows };
436
+ }
437
+ return {
438
+ resolved: false,
439
+ reason: `no active binding provider claimed collection "${request.collection}"`
440
+ };
441
+ },
442
+ async applyMutation(mutation) {
443
+ for (const { entry } of active()) {
444
+ if (!(entry.provider.provides.ops?.includes(mutation.op) ?? false))
445
+ continue;
446
+ if (!entry.provider.applyMutation) continue;
447
+ const write = await entry.provider.applyMutation(mutation);
448
+ if (write.kind === "decline") continue;
449
+ return { handled: true, provider: entry.plugin, outcome: write.outcome };
450
+ }
451
+ return {
452
+ handled: false,
453
+ reason: `no active binding provider claimed the "${mutation.op}" op`
454
+ };
455
+ },
456
+ onDidChange(listener) {
457
+ listeners.add(listener);
458
+ return toDisposable(() => listeners.delete(listener));
459
+ }
460
+ };
461
+ }
283
462
  function createBundleHost(getEditor, manifest, options) {
284
463
  const store = new DisposableStore();
285
464
  const sink = options?.console ?? console;
@@ -333,6 +512,43 @@ function createBundleHost(getEditor, manifest, options) {
333
512
  }
334
513
  return reason;
335
514
  };
515
+ const registeredContexts = /* @__PURE__ */ new Set();
516
+ const bindingRegistry = options?.bindingProviders;
517
+ const providerLaneGaps = (p) => {
518
+ const gaps = [];
519
+ if ((p.provides.paths?.length ?? 0) > 0 && !p.readProperty)
520
+ gaps.push("provides.paths declared without readProperty()");
521
+ if ((p.provides.collections?.length ?? 0) > 0 && !p.readCollection)
522
+ gaps.push("provides.collections declared without readCollection()");
523
+ if ((p.provides.ops?.length ?? 0) > 0 && !p.applyMutation)
524
+ gaps.push("provides.ops declared without applyMutation()");
525
+ const dupes = (label, xs) => {
526
+ const seen = /* @__PURE__ */ new Set();
527
+ for (const x of xs ?? []) {
528
+ if (seen.has(x)) gaps.push(`provides.${label} lists "${x}" twice`);
529
+ seen.add(x);
530
+ }
531
+ };
532
+ dupes("paths", p.provides.paths);
533
+ dupes("collections", p.provides.collections);
534
+ dupes("ops", p.provides.ops);
535
+ dupes("writablePaths", p.provides.writablePaths);
536
+ for (const w of p.provides.writablePaths ?? []) {
537
+ if (!(p.provides.paths ?? []).includes(w)) {
538
+ gaps.push(
539
+ `provides.writablePaths lists "${w}", which is not in provides.paths`
540
+ );
541
+ }
542
+ }
543
+ if (p.provides.writablePaths !== void 0 && !p.writeProperty) {
544
+ if (p.provides.writablePaths.length > 0)
545
+ gaps.push("provides.writablePaths declared without writeProperty()");
546
+ }
547
+ if ((p.provides.paths?.length ?? 0) === 0 && (p.provides.collections?.length ?? 0) === 0 && (p.provides.ops?.length ?? 0) === 0) {
548
+ gaps.push("provides declares nothing \u2014 the provider could never answer");
549
+ }
550
+ return gaps;
551
+ };
336
552
  const contribute = {
337
553
  tool(c) {
338
554
  assertNamespaced(c.id, "tool");
@@ -425,9 +641,30 @@ function createBundleHost(getEditor, manifest, options) {
425
641
  "contribute.editContext",
426
642
  `contributes.editContexts[] must declare { type: "${c.type}" }`
427
643
  );
644
+ registeredContexts.add(c.type);
428
645
  const stamped = {
429
646
  ...c,
430
- metadataKey: metadataKey(manifest)
647
+ metadataKey: metadataKey(manifest),
648
+ ...bindingRegistry ? {
649
+ onEnter: (ctx) => {
650
+ bindingRegistry.setContextActive(
651
+ manifest.id,
652
+ c.type,
653
+ typeof ctx.id?.id === "string" ? ctx.id.id : null,
654
+ true
655
+ );
656
+ c.onEnter?.(ctx);
657
+ },
658
+ onExit: (ctx) => {
659
+ c.onExit?.(ctx);
660
+ bindingRegistry.setContextActive(
661
+ manifest.id,
662
+ c.type,
663
+ typeof ctx.id?.id === "string" ? ctx.id.id : null,
664
+ false
665
+ );
666
+ }
667
+ } : {}
431
668
  };
432
669
  const reg = getEditor().registries.editContexts;
433
670
  const recorded = options?.onEditContextRegistered?.(stamped);
@@ -473,6 +710,69 @@ function createBundleHost(getEditor, manifest, options) {
473
710
  return store.add(toDisposable(() => {
474
711
  }));
475
712
  },
713
+ // ADR-023 phase A — the BINDING-PROVIDER door. Three gates, in
714
+ // order, all loud:
715
+ // 1. the CAPABILITY gate, borrowed whole from the edit context the
716
+ // provider attaches to (`contributes.editContexts[]` must
717
+ // declare the type). No new manifest field and no new
718
+ // capability: the authority a provider exercises IS the
719
+ // authority the active context already holds over the selection
720
+ // it owns, so a second declaration would be ceremony, not a
721
+ // gate. (Precedent: `EditContextContribution.toolIds` swaps the
722
+ // whole tool rail and is not separately declared either.)
723
+ // 2. the ORDERING rule — the context must already be registered by
724
+ // THIS bundle. A provider on an unregistered type can never
725
+ // activate; accepting it would be the fake-interactive failure
726
+ // the platform refuses.
727
+ // 3. the DECLARATION shape — a lane declared without its callback,
728
+ // a repeated entry, or an empty `provides` is an authoring bug
729
+ // that would make the provider silently unreachable.
730
+ // Then the honest no-backend door: no registry wired → warn + an
731
+ // inert handle (probe `supports("bindings.provider@1")`).
732
+ bindingProvider(contextType, provider) {
733
+ requireDeclared(
734
+ declaresType(declared?.editContexts, contextType),
735
+ "contribute.bindingProvider",
736
+ `contributes.editContexts[] must declare { type: "${contextType}" } (a binding provider borrows its edit context's declaration)`
737
+ );
738
+ if (!registeredContexts.has(contextType)) {
739
+ throw new Error(
740
+ `plugin-api: contribute.bindingProvider("${contextType}") \u2014 no such edit context is registered by ${manifest.id}. A provider BORROWS its context's activation (ADR-023): call contribute.editContext({ type: "${contextType}", \u2026 }) first, or the provider could never be consulted.`
741
+ );
742
+ }
743
+ const gaps = providerLaneGaps(provider);
744
+ if (gaps.length > 0) {
745
+ throw new Error(
746
+ `plugin-api: contribute.bindingProvider("${contextType}") \u2014 ${gaps.join("; ")}. A declared lane the registry can never call is a silently dead provider, so the declaration is refused.`
747
+ );
748
+ }
749
+ if (!bindingRegistry) {
750
+ log.warn(
751
+ `contribute.bindingProvider("${contextType}") registered nothing \u2014 the host wired no binding-provider registry, so no panel will ever consult it (probe supports("bindings.provider@1"))`
752
+ );
753
+ return store.add({
754
+ invalidate() {
755
+ },
756
+ dispose() {
757
+ }
758
+ });
759
+ }
760
+ const entry = {
761
+ plugin: manifest.id,
762
+ contextType,
763
+ provider
764
+ };
765
+ const reg = store.add(bindingRegistry.register(entry));
766
+ const handle = {
767
+ invalidate() {
768
+ bindingRegistry.notifyChanged(manifest.id, contextType);
769
+ },
770
+ dispose() {
771
+ reg.dispose();
772
+ }
773
+ };
774
+ return handle;
775
+ },
476
776
  // K-2 / S-06 — document IO. Mirrors `command`: namespaced id the
477
777
  // manifest must list, routed to the shell registry. The registry is
478
778
  // OPTIONAL on the handle (a host that hasn't wired it stays
@@ -571,6 +871,24 @@ function createBundleHost(getEditor, manifest, options) {
571
871
  door,
572
872
  "capabilities.document.openNative must be declared"
573
873
  );
874
+ let parentIndex = null;
875
+ let parentIndexWired = false;
876
+ const buildParentIndex = (roots) => {
877
+ const index = /* @__PURE__ */ new Map();
878
+ const walk = (nodes, parent) => {
879
+ for (const node of nodes) {
880
+ const id = node.id ?? null;
881
+ if (id && typeof id.id === "string") {
882
+ if (parent) index.set(id.id, parent);
883
+ walk(node.children ?? [], id);
884
+ } else {
885
+ walk(node.children ?? [], parent);
886
+ }
887
+ }
888
+ };
889
+ walk(roots, null);
890
+ return index;
891
+ };
574
892
  const document = {
575
893
  async mutate(mutation) {
576
894
  const denied = denyWrite(
@@ -674,6 +992,30 @@ function createBundleHost(getEditor, manifest, options) {
674
992
  return null;
675
993
  }
676
994
  },
995
+ async planarRegions(elementIds, point) {
996
+ requireDocRead("document.planarRegions");
997
+ const refusal = (reason) => ({
998
+ found: false,
999
+ faces: [],
1000
+ inputCount: elementIds.length,
1001
+ complete: false,
1002
+ reason
1003
+ });
1004
+ try {
1005
+ const reply = await getEditor().client.send({
1006
+ kind: "requestPlanarRegions",
1007
+ payload: point ? { elementIds, point } : { elementIds }
1008
+ });
1009
+ if (reply.kind !== "planarRegions") {
1010
+ return refusal(
1011
+ "this host did not answer requestPlanarRegions \u2014 its engine predates the planar-region read door (protocol v57)"
1012
+ );
1013
+ }
1014
+ return reply.payload?.result ?? refusal("the engine answered planarRegions with no result");
1015
+ } catch (error) {
1016
+ return refusal(`the region read door failed: ${String(error)}`);
1017
+ }
1018
+ },
677
1019
  async tree() {
678
1020
  requireDocRead("document.tree");
679
1021
  const reply = await getEditor().client.send({
@@ -681,6 +1023,30 @@ function createBundleHost(getEditor, manifest, options) {
681
1023
  });
682
1024
  return reply.kind === "sceneTree" ? reply.payload.roots : [];
683
1025
  },
1026
+ async parentOf(id) {
1027
+ requireDocRead("document.parentOf");
1028
+ if (typeof id.id !== "string") return null;
1029
+ if (!parentIndexWired) {
1030
+ parentIndexWired = true;
1031
+ const off = getEditor().client.subscribe((msg) => {
1032
+ if (msg.kind === "mutationApplied" || msg.kind === "undoApplied" || msg.kind === "redoApplied" || // A newly opened document invalidates everything; the kind
1033
+ // is matched defensively (not every host posts it).
1034
+ msg.kind === "documentLoaded") {
1035
+ parentIndex = null;
1036
+ }
1037
+ });
1038
+ store.add(toDisposable(off));
1039
+ }
1040
+ if (!parentIndex) {
1041
+ const reply = await getEditor().client.send({
1042
+ kind: "requestSceneTree"
1043
+ });
1044
+ parentIndex = buildParentIndex(
1045
+ reply.kind === "sceneTree" ? reply.payload.roots : []
1046
+ );
1047
+ }
1048
+ return parentIndex.get(id.id) ?? null;
1049
+ },
684
1050
  async getMetadata(id) {
685
1051
  requireDocRead("document.getMetadata");
686
1052
  const key = metadataKey(manifest);
@@ -788,6 +1154,15 @@ function createBundleHost(getEditor, manifest, options) {
788
1154
  ascender: sizePt * 0.8,
789
1155
  descender: -sizePt * 0.2
790
1156
  };
1157
+ },
1158
+ // C-9 — the caret read door. A read of EDITOR state (the
1159
+ // text-editing caret), injected by the host app like clipboard /
1160
+ // consent; no capability gate (a read door, like measureString /
1161
+ // viewport). No backend → the honest null (probe
1162
+ // supports("text.caret@1") to distinguish "no caret" from
1163
+ // "no reader").
1164
+ caret() {
1165
+ return options?.textCaret?.read() ?? null;
791
1166
  }
792
1167
  };
793
1168
  const overlay = {
@@ -798,6 +1173,27 @@ function createBundleHost(getEditor, manifest, options) {
798
1173
  'capabilities.rendering must include "overlay"'
799
1174
  );
800
1175
  getEditor().overlaySignals.setToolPreview(shape);
1176
+ },
1177
+ // K-9 — the MULTI-shape write on the SAME slot + the SAME gate. When
1178
+ // the host wired a multi-shape sink we hand the list over verbatim
1179
+ // (the adapter never re-shapes a preview — the renderer owns layout).
1180
+ // When it did not, the honest degradation is the pre-K-9 behaviour a
1181
+ // bundle used to hand-code: the FIRST shape through the single slot,
1182
+ // never a throw. `null`/empty clears, so a bundle's teardown path is
1183
+ // the same either way.
1184
+ setToolPreviews(shapes) {
1185
+ requireDeclared(
1186
+ hasRendering("overlay"),
1187
+ "overlay.setToolPreviews",
1188
+ 'capabilities.rendering must include "overlay"'
1189
+ );
1190
+ const signals = getEditor().overlaySignals;
1191
+ const list = shapes && shapes.length > 0 ? shapes : null;
1192
+ if (signals.setToolPreviews) {
1193
+ signals.setToolPreviews(list);
1194
+ return;
1195
+ }
1196
+ signals.setToolPreview(list ? list[0] : null);
801
1197
  }
802
1198
  };
803
1199
  const backing = options?.storage ?? defaultStorageBacking();
@@ -966,19 +1362,40 @@ function createBundleHost(getEditor, manifest, options) {
966
1362
  return store.add(toDisposable(() => bindingListeners.delete(listener)));
967
1363
  }
968
1364
  };
969
- const shell = options?.shell ?? {
1365
+ const shellBackend = options?.shell;
1366
+ const shell = {
970
1367
  openPanel(panelId) {
1368
+ if (shellBackend) {
1369
+ shellBackend.openPanel(panelId);
1370
+ return;
1371
+ }
971
1372
  log.warn(
972
1373
  `shell.openPanel("${panelId}") ignored \u2014 the host app provided no shell actions (probe with supports("shell.openPanel@1"))`
973
1374
  );
974
1375
  },
975
- closePanel() {
1376
+ closePanel(panelId) {
1377
+ shellBackend?.closePanel(panelId);
976
1378
  },
977
- async pickFile() {
1379
+ async pickFile(pickOptions) {
1380
+ if (shellBackend) return shellBackend.pickFile(pickOptions);
978
1381
  log.warn(
979
1382
  `shell.pickFile() ignored \u2014 the host app provided no shell actions (probe with supports("shell.pickFile@1"))`
980
1383
  );
981
1384
  return [];
1385
+ },
1386
+ // K-10 — the write half. No saver wired (headless, or a host app
1387
+ // older than the door) answers FALSE, never a throw: "the bytes were
1388
+ // not delivered" is a result a bundle can act on (keep the Export
1389
+ // Center path, disable the menu item), which a rejection would turn
1390
+ // into an unhandled promise in a click handler.
1391
+ async saveFile(saveOptions) {
1392
+ if (shellBackend?.saveFile) {
1393
+ return shellBackend.saveFile(saveOptions);
1394
+ }
1395
+ log.warn(
1396
+ `shell.saveFile("${saveOptions.suggestedName}") ignored \u2014 the host app wired no file saver (probe with supports("shell.saveFile@1"))`
1397
+ );
1398
+ return false;
982
1399
  }
983
1400
  };
984
1401
  const widgets = options?.widgets ?? FALLBACK_WIDGETS;
@@ -1437,12 +1854,18 @@ function createBundleHost(getEditor, manifest, options) {
1437
1854
  if (getEditor().sceneLayers) {
1438
1855
  featureSet.add("rendering.sceneLayer@1");
1439
1856
  }
1857
+ if (typeof getEditor().overlaySignals?.setToolPreviews === "function") {
1858
+ featureSet.add("overlay.multiPreview@1");
1859
+ }
1440
1860
  if (getEditor().images) {
1441
1861
  featureSet.add("rendering.resourceProvider@1");
1442
1862
  }
1443
1863
  if (options?.shell) {
1444
1864
  featureSet.add("shell.openPanel@1");
1445
1865
  featureSet.add("shell.pickFile@1");
1866
+ if (typeof options.shell.saveFile === "function") {
1867
+ featureSet.add("shell.saveFile@1");
1868
+ }
1446
1869
  }
1447
1870
  if (options?.widgets) {
1448
1871
  featureSet.add("widgets.codeEditor@1");
@@ -1462,12 +1885,18 @@ function createBundleHost(getEditor, manifest, options) {
1462
1885
  if (options?.dataProviders) {
1463
1886
  featureSet.add("dataProviders@1");
1464
1887
  }
1888
+ if (options?.bindingProviders) {
1889
+ featureSet.add("bindings.provider@1");
1890
+ }
1465
1891
  if (options?.blobStore) {
1466
1892
  featureSet.add("storage.blob@1");
1467
1893
  }
1468
1894
  if (options?.clipboard) {
1469
1895
  featureSet.add("clipboard@1");
1470
1896
  }
1897
+ if (options?.textCaret) {
1898
+ featureSet.add("text.caret@1");
1899
+ }
1471
1900
  if (options?.nativeDocument) {
1472
1901
  featureSet.add("document.readNative@1");
1473
1902
  featureSet.add("document.openNative@1");
@@ -1717,7 +2146,7 @@ function inMemorySecretStore() {
1717
2146
  };
1718
2147
  }
1719
2148
  var seqCounter = 1;
1720
- function makeEngineEditor(worker, recorder, onToolPreview) {
2149
+ function makeEngineEditor(worker, recorder, onToolPreview, onToolPreviews) {
1721
2150
  const protocol = worker.protocolVersion;
1722
2151
  const listeners = /* @__PURE__ */ new Set();
1723
2152
  const fanOut = (reply) => {
@@ -1765,6 +2194,7 @@ function makeEngineEditor(worker, recorder, onToolPreview) {
1765
2194
  };
1766
2195
  let elementSelection = [];
1767
2196
  let toolPreview = null;
2197
+ let toolPreviews = null;
1768
2198
  const client = {
1769
2199
  async mutate(mutation) {
1770
2200
  const reply = dispatch("mutate", mutation);
@@ -1854,6 +2284,18 @@ function makeEngineEditor(worker, recorder, onToolPreview) {
1854
2284
  setToolPreview(value) {
1855
2285
  toolPreview = value;
1856
2286
  onToolPreview(toolPreview);
2287
+ },
2288
+ // K-9 — the MULTI-shape sink. Wiring it headlessly is what makes
2289
+ // `supports("overlay.multiPreview@1")` true in the harness, so a
2290
+ // bundle's "geometry AND label at once" claim is conformance-
2291
+ // assertable without a browser. One slot: the list also updates
2292
+ // `lastToolPreview()` (to its first shape) so an older assertion
2293
+ // still reads something true rather than a stale value.
2294
+ setToolPreviews(value) {
2295
+ toolPreviews = value && value.length > 0 ? value : null;
2296
+ toolPreview = toolPreviews ? toolPreviews[0] : null;
2297
+ onToolPreviews(toolPreviews);
2298
+ onToolPreview(toolPreview);
1857
2299
  }
1858
2300
  },
1859
2301
  // No tool spine + no content caret headlessly — both are inert
@@ -1871,21 +2313,31 @@ async function createHeadlessHost(options = {}) {
1871
2313
  const worker = engine.worker;
1872
2314
  const contributions = [];
1873
2315
  let lastPreview = null;
1874
- const editor = makeEngineEditor(worker, contributions, (value) => {
1875
- lastPreview = value;
1876
- });
2316
+ let lastPreviews = null;
2317
+ const editor = makeEngineEditor(
2318
+ worker,
2319
+ contributions,
2320
+ (value) => {
2321
+ lastPreview = value;
2322
+ },
2323
+ (value) => {
2324
+ lastPreviews = value;
2325
+ }
2326
+ );
1877
2327
  let active = null;
1878
2328
  let currentHost = null;
1879
2329
  let disposed = false;
1880
2330
  const blobStore = options.blobStore ?? inMemoryBlobStore();
1881
2331
  const clipboard = options.clipboard ?? inMemoryClipboard();
1882
2332
  const secrets = options.secrets ?? inMemorySecretStore();
2333
+ const bindingProviders = options.bindingProviders ?? createBindingProviderRegistry();
1883
2334
  const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
1884
2335
  console: options.console,
1885
2336
  storage: options.storage,
1886
2337
  blobStore,
1887
2338
  clipboard,
1888
2339
  secrets,
2340
+ bindingProviders,
1889
2341
  capabilityMode: mode,
1890
2342
  // W-06 — a recordable fake asset source the conformance harness
1891
2343
  // can pass so a bundle's `@font-face` byte path is exercisable
@@ -2002,6 +2454,10 @@ async function createHeadlessHost(options = {}) {
2002
2454
  lastToolPreview() {
2003
2455
  return lastPreview;
2004
2456
  },
2457
+ lastToolPreviews() {
2458
+ return lastPreviews;
2459
+ },
2460
+ bindingProviders,
2005
2461
  async load(idml) {
2006
2462
  const raw = worker.loadDocumentDirect(seqCounter++, idml);
2007
2463
  const reply = JSON.parse(raw);
@@ -2299,6 +2755,9 @@ function contributeEditContext(host, contribution) {
2299
2755
  function contributeObjectType(host, contribution) {
2300
2756
  return host.contribute.objectType(contribution);
2301
2757
  }
2758
+ function contributeBindingProvider(host, contextType, provider) {
2759
+ return host.contribute.bindingProvider(contextType, provider);
2760
+ }
2302
2761
  export {
2303
2762
  API_VERSION,
2304
2763
  ASSET_BUDGETS,
@@ -2314,11 +2773,13 @@ export {
2314
2773
  WORKER_BUDGETS,
2315
2774
  beginPageDrag,
2316
2775
  commitAndSelect,
2776
+ contributeBindingProvider,
2317
2777
  contributeEditContext,
2318
2778
  contributeObjectType,
2319
2779
  contributePanel,
2320
2780
  contributeSchemaPanel,
2321
2781
  contributeTool,
2782
+ createBindingProviderRegistry,
2322
2783
  createBundleHost,
2323
2784
  createDataProviderRegistry,
2324
2785
  createHeadlessHost,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paged-media/plugin-sdk",
3
- "version": "0.2.27-canary.1",
3
+ "version": "0.2.29-canary.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@paged-media/plugin-api": "0.2.27-canary.1"
14
+ "@paged-media/plugin-api": "0.2.29-canary.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "react": "^18.3.0"
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "devDependencies": {
25
- "@paged-media/canvas-wasm": "0.51.0",
25
+ "@paged-media/canvas-wasm": "0.61.0",
26
26
  "@types/node": "20.19.39",
27
27
  "@types/react": "^18.3.12",
28
28
  "react": "^18.3.0",
@@ -46,6 +46,6 @@
46
46
  "scripts": {
47
47
  "typecheck": "tsc --noEmit -p tsconfig.json",
48
48
  "test": "vitest run",
49
- "build": "tsup src/index.ts --format esm --dts --out-dir dist --clean"
49
+ "build": "tsup src/index.ts --format esm --target es2020 --dts --out-dir dist --clean"
50
50
  }
51
51
  }