@paged-media/plugin-sdk 0.2.27-canary.0 → 0.2.28-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,10 +48,31 @@ function toDisposable(fn) {
42
48
  };
43
49
  }
44
50
 
51
+ // src/react-optional.ts
52
+ var createElement = null;
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
+ );
68
+ }
69
+
45
70
  // src/widgets-fallback.tsx
46
- import { createElement } from "react";
47
71
  function TextareaCodeEditor(props) {
48
- 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", {
49
76
  value: props.value,
50
77
  readOnly: props.readOnly,
51
78
  spellCheck: false,
@@ -71,7 +98,11 @@ var FALLBACK_WIDGETS = {
71
98
  };
72
99
 
73
100
  // src/schema-panel.tsx
74
- import { createElement as createElement2 } from "react";
101
+ function h() {
102
+ return requireCreateElement(
103
+ "plugin-sdk: rendering a schema panel needs React, which is not installed."
104
+ );
105
+ }
75
106
  function resolveGate(gate, lookup) {
76
107
  if (gate === void 0) return true;
77
108
  if (typeof gate === "boolean") return gate;
@@ -82,7 +113,7 @@ function makeSchemaPanelComponent(contribution, bindings, renderer) {
82
113
  const { schema } = contribution;
83
114
  if (renderer) {
84
115
  let SchemaPanel2 = function(_props) {
85
- return createElement2(Renderer, { schema, bindings });
116
+ return h()(Renderer, { schema, bindings });
86
117
  };
87
118
  var SchemaPanel = SchemaPanel2;
88
119
  const Renderer = renderer;
@@ -90,7 +121,7 @@ function makeSchemaPanelComponent(contribution, bindings, renderer) {
90
121
  return SchemaPanel2;
91
122
  }
92
123
  function SchemaPanelSeam(_props) {
93
- return createElement2(
124
+ return h()(
94
125
  "div",
95
126
  {
96
127
  "data-schema-panel-seam": schema.id,
@@ -118,6 +149,14 @@ var HOST_FEATURES = [
118
149
  "contribute.overlay@1",
119
150
  "contribute.editContext@1",
120
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",
121
160
  "contribute.importer@1",
122
161
  "contribute.exporter@1",
123
162
  "document.mutate@1",
@@ -131,6 +170,12 @@ var HOST_FEATURES = [
131
170
  "document.elementProperties@1",
132
171
  "document.placeholders@1",
133
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",
134
179
  "document.onDidChange@1",
135
180
  "document.getMetadata@1",
136
181
  "document.setMetadata@1",
@@ -139,6 +184,13 @@ var HOST_FEATURES = [
139
184
  "overlay.toolPreview@1",
140
185
  "storage@1",
141
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",
142
194
  // C-5 / I-04 (core v42): the placed-image bytes read is engine-served
143
195
  // through the wire (no injected source), so it is unconditionally
144
196
  // implemented at the pinned canvas-wasm — unlike assets.fonts@1, which
@@ -147,7 +199,17 @@ var HOST_FEATURES = [
147
199
  // .paged container parts door (file-format.md) — the engine (canvas-wasm
148
200
  // v51) is the writer, so it is live whenever a bundle runs (the worker
149
201
  // handshake guarantees a v51-protocol worker).
150
- "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"
151
213
  ];
152
214
  var PluginApiNotImplemented = class extends Error {
153
215
  constructor(member, pointer) {
@@ -158,15 +220,15 @@ var PluginApiNotImplemented = class extends Error {
158
220
  }
159
221
  };
160
222
  var PluginCapabilityError = class extends Error {
161
- /** The host door that was called (e.g. `"contribute.tool"`). */
162
- door;
163
- /** The manifest declaration that would authorize it (e.g.
164
- * `'contributes.tools[] must include "media.paged.web.tool.pen"'`). */
165
- missingDeclaration;
166
223
  constructor(door, missingDeclaration, pluginId) {
167
224
  super(
168
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.`
169
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");
170
232
  this.name = "PluginCapabilityError";
171
233
  this.door = door;
172
234
  this.missingDeclaration = missingDeclaration;
@@ -259,6 +321,144 @@ function createDataProviderRegistry() {
259
321
  }
260
322
  };
261
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
+ }
262
462
  function createBundleHost(getEditor, manifest, options) {
263
463
  const store = new DisposableStore();
264
464
  const sink = options?.console ?? console;
@@ -312,6 +512,43 @@ function createBundleHost(getEditor, manifest, options) {
312
512
  }
313
513
  return reason;
314
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
+ };
315
552
  const contribute = {
316
553
  tool(c) {
317
554
  assertNamespaced(c.id, "tool");
@@ -404,9 +641,30 @@ function createBundleHost(getEditor, manifest, options) {
404
641
  "contribute.editContext",
405
642
  `contributes.editContexts[] must declare { type: "${c.type}" }`
406
643
  );
644
+ registeredContexts.add(c.type);
407
645
  const stamped = {
408
646
  ...c,
409
- 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
+ } : {}
410
668
  };
411
669
  const reg = getEditor().registries.editContexts;
412
670
  const recorded = options?.onEditContextRegistered?.(stamped);
@@ -452,6 +710,69 @@ function createBundleHost(getEditor, manifest, options) {
452
710
  return store.add(toDisposable(() => {
453
711
  }));
454
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
+ },
455
776
  // K-2 / S-06 — document IO. Mirrors `command`: namespaced id the
456
777
  // manifest must list, routed to the shell registry. The registry is
457
778
  // OPTIONAL on the handle (a host that hasn't wired it stays
@@ -550,6 +871,24 @@ function createBundleHost(getEditor, manifest, options) {
550
871
  door,
551
872
  "capabilities.document.openNative must be declared"
552
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
+ };
553
892
  const document = {
554
893
  async mutate(mutation) {
555
894
  const denied = denyWrite(
@@ -653,6 +992,30 @@ function createBundleHost(getEditor, manifest, options) {
653
992
  return null;
654
993
  }
655
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
+ },
656
1019
  async tree() {
657
1020
  requireDocRead("document.tree");
658
1021
  const reply = await getEditor().client.send({
@@ -660,6 +1023,30 @@ function createBundleHost(getEditor, manifest, options) {
660
1023
  });
661
1024
  return reply.kind === "sceneTree" ? reply.payload.roots : [];
662
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
+ },
663
1050
  async getMetadata(id) {
664
1051
  requireDocRead("document.getMetadata");
665
1052
  const key = metadataKey(manifest);
@@ -701,6 +1088,12 @@ function createBundleHost(getEditor, manifest, options) {
701
1088
  });
702
1089
  return reply.kind === "frameChainResult" ? reply.payload.links : [];
703
1090
  },
1091
+ async storyContent(_storyId) {
1092
+ throw new PluginApiNotImplemented(
1093
+ "document.storyContent",
1094
+ "awaiting @paged-media/canvas-wasm v54 publish + sync-wire; core op RequestStoryContent already exists"
1095
+ );
1096
+ },
704
1097
  onDidChange(listener) {
705
1098
  requireDocRead("document.onDidChange");
706
1099
  const off = getEditor().client.subscribe((msg) => {
@@ -761,6 +1154,15 @@ function createBundleHost(getEditor, manifest, options) {
761
1154
  ascender: sizePt * 0.8,
762
1155
  descender: -sizePt * 0.2
763
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;
764
1166
  }
765
1167
  };
766
1168
  const overlay = {
@@ -771,6 +1173,27 @@ function createBundleHost(getEditor, manifest, options) {
771
1173
  'capabilities.rendering must include "overlay"'
772
1174
  );
773
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);
774
1197
  }
775
1198
  };
776
1199
  const backing = options?.storage ?? defaultStorageBacking();
@@ -939,19 +1362,40 @@ function createBundleHost(getEditor, manifest, options) {
939
1362
  return store.add(toDisposable(() => bindingListeners.delete(listener)));
940
1363
  }
941
1364
  };
942
- const shell = options?.shell ?? {
1365
+ const shellBackend = options?.shell;
1366
+ const shell = {
943
1367
  openPanel(panelId) {
1368
+ if (shellBackend) {
1369
+ shellBackend.openPanel(panelId);
1370
+ return;
1371
+ }
944
1372
  log.warn(
945
1373
  `shell.openPanel("${panelId}") ignored \u2014 the host app provided no shell actions (probe with supports("shell.openPanel@1"))`
946
1374
  );
947
1375
  },
948
- closePanel() {
1376
+ closePanel(panelId) {
1377
+ shellBackend?.closePanel(panelId);
949
1378
  },
950
- async pickFile() {
1379
+ async pickFile(pickOptions) {
1380
+ if (shellBackend) return shellBackend.pickFile(pickOptions);
951
1381
  log.warn(
952
1382
  `shell.pickFile() ignored \u2014 the host app provided no shell actions (probe with supports("shell.pickFile@1"))`
953
1383
  );
954
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;
955
1399
  }
956
1400
  };
957
1401
  const widgets = options?.widgets ?? FALLBACK_WIDGETS;
@@ -1410,12 +1854,18 @@ function createBundleHost(getEditor, manifest, options) {
1410
1854
  if (getEditor().sceneLayers) {
1411
1855
  featureSet.add("rendering.sceneLayer@1");
1412
1856
  }
1857
+ if (typeof getEditor().overlaySignals?.setToolPreviews === "function") {
1858
+ featureSet.add("overlay.multiPreview@1");
1859
+ }
1413
1860
  if (getEditor().images) {
1414
1861
  featureSet.add("rendering.resourceProvider@1");
1415
1862
  }
1416
1863
  if (options?.shell) {
1417
1864
  featureSet.add("shell.openPanel@1");
1418
1865
  featureSet.add("shell.pickFile@1");
1866
+ if (typeof options.shell.saveFile === "function") {
1867
+ featureSet.add("shell.saveFile@1");
1868
+ }
1419
1869
  }
1420
1870
  if (options?.widgets) {
1421
1871
  featureSet.add("widgets.codeEditor@1");
@@ -1435,12 +1885,18 @@ function createBundleHost(getEditor, manifest, options) {
1435
1885
  if (options?.dataProviders) {
1436
1886
  featureSet.add("dataProviders@1");
1437
1887
  }
1888
+ if (options?.bindingProviders) {
1889
+ featureSet.add("bindings.provider@1");
1890
+ }
1438
1891
  if (options?.blobStore) {
1439
1892
  featureSet.add("storage.blob@1");
1440
1893
  }
1441
1894
  if (options?.clipboard) {
1442
1895
  featureSet.add("clipboard@1");
1443
1896
  }
1897
+ if (options?.textCaret) {
1898
+ featureSet.add("text.caret@1");
1899
+ }
1444
1900
  if (options?.nativeDocument) {
1445
1901
  featureSet.add("document.readNative@1");
1446
1902
  featureSet.add("document.openNative@1");
@@ -1690,7 +2146,7 @@ function inMemorySecretStore() {
1690
2146
  };
1691
2147
  }
1692
2148
  var seqCounter = 1;
1693
- function makeEngineEditor(worker, recorder, onToolPreview) {
2149
+ function makeEngineEditor(worker, recorder, onToolPreview, onToolPreviews) {
1694
2150
  const protocol = worker.protocolVersion;
1695
2151
  const listeners = /* @__PURE__ */ new Set();
1696
2152
  const fanOut = (reply) => {
@@ -1738,6 +2194,7 @@ function makeEngineEditor(worker, recorder, onToolPreview) {
1738
2194
  };
1739
2195
  let elementSelection = [];
1740
2196
  let toolPreview = null;
2197
+ let toolPreviews = null;
1741
2198
  const client = {
1742
2199
  async mutate(mutation) {
1743
2200
  const reply = dispatch("mutate", mutation);
@@ -1827,6 +2284,18 @@ function makeEngineEditor(worker, recorder, onToolPreview) {
1827
2284
  setToolPreview(value) {
1828
2285
  toolPreview = value;
1829
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);
1830
2299
  }
1831
2300
  },
1832
2301
  // No tool spine + no content caret headlessly — both are inert
@@ -1844,21 +2313,31 @@ async function createHeadlessHost(options = {}) {
1844
2313
  const worker = engine.worker;
1845
2314
  const contributions = [];
1846
2315
  let lastPreview = null;
1847
- const editor = makeEngineEditor(worker, contributions, (value) => {
1848
- lastPreview = value;
1849
- });
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
+ );
1850
2327
  let active = null;
1851
2328
  let currentHost = null;
1852
2329
  let disposed = false;
1853
2330
  const blobStore = options.blobStore ?? inMemoryBlobStore();
1854
2331
  const clipboard = options.clipboard ?? inMemoryClipboard();
1855
2332
  const secrets = options.secrets ?? inMemorySecretStore();
2333
+ const bindingProviders = options.bindingProviders ?? createBindingProviderRegistry();
1856
2334
  const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
1857
2335
  console: options.console,
1858
2336
  storage: options.storage,
1859
2337
  blobStore,
1860
2338
  clipboard,
1861
2339
  secrets,
2340
+ bindingProviders,
1862
2341
  capabilityMode: mode,
1863
2342
  // W-06 — a recordable fake asset source the conformance harness
1864
2343
  // can pass so a bundle's `@font-face` byte path is exercisable
@@ -1975,6 +2454,10 @@ async function createHeadlessHost(options = {}) {
1975
2454
  lastToolPreview() {
1976
2455
  return lastPreview;
1977
2456
  },
2457
+ lastToolPreviews() {
2458
+ return lastPreviews;
2459
+ },
2460
+ bindingProviders,
1978
2461
  async load(idml) {
1979
2462
  const raw = worker.loadDocumentDirect(seqCounter++, idml);
1980
2463
  const reply = JSON.parse(raw);
@@ -2272,6 +2755,9 @@ function contributeEditContext(host, contribution) {
2272
2755
  function contributeObjectType(host, contribution) {
2273
2756
  return host.contribute.objectType(contribution);
2274
2757
  }
2758
+ function contributeBindingProvider(host, contextType, provider) {
2759
+ return host.contribute.bindingProvider(contextType, provider);
2760
+ }
2275
2761
  export {
2276
2762
  API_VERSION,
2277
2763
  ASSET_BUDGETS,
@@ -2287,11 +2773,13 @@ export {
2287
2773
  WORKER_BUDGETS,
2288
2774
  beginPageDrag,
2289
2775
  commitAndSelect,
2776
+ contributeBindingProvider,
2290
2777
  contributeEditContext,
2291
2778
  contributeObjectType,
2292
2779
  contributePanel,
2293
2780
  contributeSchemaPanel,
2294
2781
  contributeTool,
2782
+ createBindingProviderRegistry,
2295
2783
  createBundleHost,
2296
2784
  createDataProviderRegistry,
2297
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.0",
3
+ "version": "0.2.28-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.0"
14
+ "@paged-media/plugin-api": "0.2.28-canary.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "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
  }