@paged-media/plugin-sdk 0.2.36-canary.0 → 0.2.38-canary.3

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, BindingProvider, Disposable, ActiveBindingProvider, BindingPropertyRequest, BindingReadResult, BindingPropertyWrite, BindingWriteResult, BindingCollectionRequest, BindingCollectionResult, MutationInput, FontFaceAsset, BundleHost, ClipboardPayload, ConsentResult, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, JournalRecord, 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';
1
+ import { PagedBundle, BindingProvider, Disposable, ActiveBindingProvider, BindingPropertyRequest, BindingReadResult, BindingPropertyWrite, BindingWriteResult, BindingCollectionRequest, BindingCollectionResult, MutationInput, FontFaceAsset, BundleHost, ClipboardPayload, ConsentResult, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, JournalRecord, ShellSurface, WidgetSurface, WillSaveEvent, ToolSettingValue, 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
  /**
@@ -333,7 +333,19 @@ type BundleTrust = "first-party";
333
333
  * omits `saveFile` still injects a valid backend and
334
334
  * `supports("shell.saveFile@1")` reports the truth.
335
335
  */
336
- type ShellBackend = Omit<ShellSurface, "saveFile"> & Partial<Pick<ShellSurface, "saveFile">>;
336
+ type ShellBackend = Omit<ShellSurface, "saveFile" | "enterEditContext"> & Partial<Pick<ShellSurface, "saveFile" | "enterEditContext">>;
337
+ /** The host's will-save registry (protocol 66 batch): the editor awaits
338
+ * every registered listener before it serialises the document. The SDK
339
+ * wraps each bundle listener so it is awaited and its errors contained. */
340
+ interface WillSaveBackend {
341
+ register(pluginId: string, listener: (e: WillSaveEvent) => Promise<void>): Disposable;
342
+ }
343
+ /** The host's tool-options store, read side: the values its tool-options
344
+ * UI holds per tool id, plus a change subscription. */
345
+ interface ToolSettingsBackend {
346
+ get(toolId: string): Record<string, ToolSettingValue>;
347
+ subscribe(toolId: string, listener: () => void): () => void;
348
+ }
337
349
  interface CreateBundleHostOptions {
338
350
  /**
339
351
  * The host's trust assertion for this bundle (`plugin-trust-line.md`).
@@ -385,7 +397,14 @@ interface CreateBundleHostOptions {
385
397
  * the editor's UI package and is injected here. When absent,
386
398
  * `host.widgets` is the plain-textarea fallback and
387
399
  * `supports("widgets.codeEditor@1")` answers false. */
388
- widgets?: WidgetSurface;
400
+ widgets?: Partial<WidgetSurface>;
401
+ /** The host's will-save registry. When absent, `document.onWillSave`
402
+ * holds listeners that never run and `supports("document.onWillSave@1")`
403
+ * is false. */
404
+ willSave?: WillSaveBackend;
405
+ /** The host's tool-options store. When absent, `host.tools.settings`
406
+ * answers `{}` and `supports("tools.settings@1")` is false. */
407
+ toolSettings?: ToolSettingsBackend;
389
408
  /** Host-provided SCHEMA-PANEL renderer (W3.1): the editor's
390
409
  * `SchemaPanelRenderer` that walks a `PanelSchema` through the
391
410
  * catalog + subscribes to the bundle's bindings. When absent,
@@ -560,8 +579,20 @@ interface RecordedContribution {
560
579
  id: string;
561
580
  value: ToolContribution | PanelContribution | SchemaPanelContribution | CommandContribution | KeybindingContribution | OverlayContribution | EditContextContribution | ObjectTypeContribution | ImporterContribution | ExporterContribution;
562
581
  }
563
- interface HarnessOptions extends LoadHeadlessEngineOptions, Pick<CreateBundleHostOptions, "console" | "storage" | "capabilityMode" | "assetSource" | "blobStore" | "clipboard" | "secrets" | "bindingProviders"> {
582
+ interface HarnessOptions extends LoadHeadlessEngineOptions, Pick<CreateBundleHostOptions, "console" | "storage" | "capabilityMode" | "assetSource" | "blobStore" | "clipboard" | "secrets" | "bindingProviders" | "willSave" | "toolSettings"> {
564
583
  }
584
+ /** A headless will-save registry: listeners register per plugin and
585
+ * `fire()` awaits them all, the way the editor's save path does — so a
586
+ * bundle's commit-before-save is assertable without an editor. */
587
+ declare function inMemoryWillSave(): WillSaveBackend & {
588
+ fire(): Promise<void>;
589
+ count(): number;
590
+ };
591
+ /** A headless tool-options store: what the editor's tool-options UI would
592
+ * hold, settable from a test with `set`. */
593
+ declare function inMemoryToolSettings(): ToolSettingsBackend & {
594
+ set(toolId: string, key: string, value: ToolSettingValue): void;
595
+ };
565
596
  /** What `createHeadlessHost` resolves to: a real engine-backed host plus
566
597
  * the conformance affordances (load an IDML, read the contribution log,
567
598
  * load a bundle, dispose honestly). */
@@ -615,6 +646,13 @@ interface HeadlessHost {
615
646
  * that a path it does not declare comes back as a typed refusal to
616
647
  * fall through to core). */
617
648
  readonly bindingProviders: BindingProviderBackend;
649
+ /** The will-save registry this host injected: `fire()` runs every
650
+ * bundle's `document.onWillSave` listener and awaits them, as a save
651
+ * would. */
652
+ readonly willSave: ReturnType<typeof inMemoryWillSave>;
653
+ /** The tool-options store this host injected: `set()` stands in for the
654
+ * editor's tool-options UI, and `host.tools` reads it back. */
655
+ readonly toolSettings: ReturnType<typeof inMemoryToolSettings>;
618
656
  /** Load an IDML package into the headless document. Resolves to the
619
657
  * loaded page ids (or throws on a parse failure). */
620
658
  load(idml: Uint8Array): Promise<string[]>;
@@ -664,8 +702,9 @@ declare const API_VERSION = "0.2.0";
664
702
  */
665
703
  declare function satisfiesApiVersion(range: string, version?: string): boolean;
666
704
 
667
- /** The default widget catalog: a textarea CodeEditor. Replaced wholesale
668
- * when the host app injects `widgets` into `createBundleHost`. */
705
+ /** The default widget catalog: a textarea CodeEditor and a native colour
706
+ * input. Each member is replaced by the one the host app injects through
707
+ * `createBundleHost({ widgets })`; members it omits keep the fallback. */
669
708
  declare const FALLBACK_WIDGETS: WidgetSurface;
670
709
 
671
710
  /** One recorded `getFontFace` call. */
@@ -882,4 +921,4 @@ declare function contributeObjectType(host: BundleHost, contribution: ObjectType
882
921
  */
883
922
  declare function contributeBindingProvider(host: BundleHost, contextType: string, provider: BindingProvider): BindingProviderHandle;
884
923
 
885
- 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 JournalSink, 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 };
924
+ 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 JournalSink, 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, type ToolSettingsBackend, WASM_BUDGETS, WORKER_BUDGETS, type WillSaveBackend, type WorkerBackend, beginPageDrag, commitAndSelect, contributeBindingProvider, contributeEditContext, contributeObjectType, contributePanel, contributeSchemaPanel, contributeTool, createBindingProviderRegistry, createBundleHost, createDataProviderRegistry, createHeadlessHost, createRecordableAssetSource, defineBundle, endLocalFor, inMemoryToolSettings, inMemoryWillSave, loadBundle, loadBundleWasm, loadHeadlessEngine, makeSchemaPanelComponent, protocolFromVersion, pxToPt, readVendoredWireVersion, resolveCanvasWasm, resolveGate, satisfiesApiVersion, toDisposable };
package/dist/index.js CHANGED
@@ -93,8 +93,31 @@ function TextareaCodeEditor(props) {
93
93
  }
94
94
  });
95
95
  }
96
+ function NativeColorPicker(props) {
97
+ const createElement2 = requireCreateElement(
98
+ "plugin-sdk: host.widgets.ColorPicker fell back to <input type=color>, but React is not installed. Install react, or inject a widget catalog via createBundleHost({ widgets })."
99
+ );
100
+ return createElement2("input", {
101
+ type: "color",
102
+ value: props.value,
103
+ disabled: props.disabled,
104
+ "aria-label": props.ariaLabel,
105
+ "data-color-picker-fallback": "native",
106
+ onChange: (e) => props.onChange(e.currentTarget.value),
107
+ onBlur: (e) => props.onCommit?.(e.currentTarget.value),
108
+ style: {
109
+ width: 40,
110
+ height: 24,
111
+ padding: 0,
112
+ border: "1px solid var(--pg-border)",
113
+ borderRadius: "var(--radius-sm, 4px)",
114
+ background: "var(--pg-bg)"
115
+ }
116
+ });
117
+ }
96
118
  var FALLBACK_WIDGETS = {
97
- CodeEditor: TextareaCodeEditor
119
+ CodeEditor: TextareaCodeEditor,
120
+ ColorPicker: NativeColorPicker
98
121
  };
99
122
 
100
123
  // src/schema-panel.tsx
@@ -139,6 +162,34 @@ function makeSchemaPanelComponent(contribution, bindings, renderer) {
139
162
  }
140
163
 
141
164
  // src/host-impl.ts
165
+ function isBytesSlot(m) {
166
+ if (m.op !== "replaceImageBytes") return false;
167
+ const b = m.args.bytes;
168
+ return Array.isArray(b) && b.length === 0;
169
+ }
170
+ function hasBytesSlot(m) {
171
+ if (isBytesSlot(m)) return true;
172
+ if (m.op === "batch") return m.args.ops.some((op) => hasBytesSlot(op));
173
+ return false;
174
+ }
175
+ function spliceBytesSlot(m, bytes) {
176
+ let filled = false;
177
+ const visit = (node) => {
178
+ if (filled) return node;
179
+ if (isBytesSlot(node)) {
180
+ filled = true;
181
+ return { ...node, args: { ...node.args, bytes } };
182
+ }
183
+ if (node.op === "batch") {
184
+ return {
185
+ ...node,
186
+ args: { ...node.args, ops: node.args.ops.map((op) => visit(op)) }
187
+ };
188
+ }
189
+ return node;
190
+ };
191
+ return visit(m);
192
+ }
142
193
  var HOST_FEATURES = [
143
194
  "contribute.tool@1",
144
195
  "contribute.panel@1",
@@ -200,6 +251,11 @@ var HOST_FEATURES = [
200
251
  // v51) is the writer, so it is live whenever a bundle runs (the worker
201
252
  // handshake guarantees a v51-protocol worker).
202
253
  "storage.parts@1",
254
+ // Protocol 66 — `host.parts.delete`. Static for the same reason as @1:
255
+ // the pinned engine (the worker handshake refuses an older one) answers
256
+ // `deletePagedPart`, and the binary lane is an optimisation the SDK
257
+ // falls back from, not a different contract.
258
+ "storage.parts@2",
203
259
  // B-22 (protocol v57) — the planar-region read door. STATIC, and the
204
260
  // flag means exactly one thing: this SDK's document surface IMPLEMENTS
205
261
  // `planarRegions` and forwards it (retiring the `host.editor.client`
@@ -464,6 +520,50 @@ function createBindingProviderRegistry() {
464
520
  }
465
521
  };
466
522
  }
523
+ function checkImage(image) {
524
+ const { width, height, rgba } = image;
525
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
526
+ throw new Error(
527
+ `contribute.sceneLayer().submitImage: ${rgba.length} bytes for a ${width}x${height} RGBA8 image (want width*height*4)`
528
+ );
529
+ }
530
+ }
531
+ function imageLayer(image) {
532
+ const [x, y, w, h2] = image.dest;
533
+ return {
534
+ items: [
535
+ {
536
+ kind: "image",
537
+ rgba: Array.from(image.rgba),
538
+ width: image.width,
539
+ height: image.height,
540
+ x,
541
+ y,
542
+ w,
543
+ h: h2
544
+ }
545
+ ]
546
+ };
547
+ }
548
+ function checkTiles(image, tiles) {
549
+ for (const t of tiles) {
550
+ if (t.width <= 0 || t.height <= 0 || t.x < 0 || t.y < 0 || t.x + t.width > image.width || t.y + t.height > image.height || t.rgba.length !== t.width * t.height * 4) {
551
+ throw new Error(
552
+ `contribute.sceneLayer().submitImageTiles: tile ${t.x},${t.y} ${t.width}x${t.height} (${t.rgba.length} bytes) does not fit the ${image.width}x${image.height} image`
553
+ );
554
+ }
555
+ }
556
+ }
557
+ function patchImage(image, tiles) {
558
+ const stride = image.width * 4;
559
+ for (const t of tiles) {
560
+ const row = t.width * 4;
561
+ for (let j = 0; j < t.height; j++) {
562
+ const dst = (t.y + j) * stride + t.x * 4;
563
+ image.rgba.set(t.rgba.subarray(j * row, j * row + row), dst);
564
+ }
565
+ }
566
+ }
467
567
  function createBundleHost(getEditor, manifest, options) {
468
568
  const store = new DisposableStore();
469
569
  const sink = options?.console ?? console;
@@ -892,6 +992,8 @@ function createBundleHost(getEditor, manifest, options) {
892
992
  'capabilities.rendering must include "sceneLayer"'
893
993
  );
894
994
  const submitted = /* @__PURE__ */ new Set();
995
+ const imaged = /* @__PURE__ */ new Map();
996
+ const retained = /* @__PURE__ */ new Map();
895
997
  const channel = () => getEditor().sceneLayers;
896
998
  const surface = {
897
999
  async submit(elementId, layer) {
@@ -907,14 +1009,83 @@ function createBundleHost(getEditor, manifest, options) {
907
1009
  },
908
1010
  async clear(elementId) {
909
1011
  submitted.delete(elementId);
1012
+ imaged.delete(elementId);
1013
+ retained.delete(elementId);
910
1014
  await channel()?.clear(elementId);
911
1015
  },
1016
+ // Protocol 66 — the binary scene-image lane, with the SDK owning
1017
+ // the fallback so a bundle calls one method on every host.
1018
+ async submitImage(elementId, image, options2) {
1019
+ const ch = channel();
1020
+ if (!ch) {
1021
+ log.warn(
1022
+ `contribute.sceneLayer().submitImage("${elementId}") ignored \u2014 the host wired no scene channel (probe supports("rendering.sceneLayer@1"))`
1023
+ );
1024
+ return;
1025
+ }
1026
+ checkImage(image);
1027
+ submitted.add(elementId);
1028
+ imaged.set(elementId, { width: image.width, height: image.height });
1029
+ if (ch.submitImage && ch.submitImageTiles) {
1030
+ retained.delete(elementId);
1031
+ await ch.submitImage(
1032
+ elementId,
1033
+ image,
1034
+ manifest.id,
1035
+ options2?.transfer ?? false
1036
+ );
1037
+ return;
1038
+ }
1039
+ const copy = {
1040
+ rgba: new Uint8Array(image.rgba),
1041
+ width: image.width,
1042
+ height: image.height,
1043
+ dest: [...image.dest]
1044
+ };
1045
+ retained.set(elementId, copy);
1046
+ await ch.submit(elementId, imageLayer(copy), manifest.id);
1047
+ },
1048
+ async submitImageTiles(elementId, tiles, options2) {
1049
+ const ch = channel();
1050
+ if (!ch) {
1051
+ log.warn(
1052
+ `contribute.sceneLayer().submitImageTiles("${elementId}") ignored \u2014 the host wired no scene channel`
1053
+ );
1054
+ return;
1055
+ }
1056
+ const size = imaged.get(elementId);
1057
+ if (!size) {
1058
+ throw new Error(
1059
+ `contribute.sceneLayer().submitImageTiles("${elementId}"): no image was submitted for this element \u2014 call submitImage first`
1060
+ );
1061
+ }
1062
+ checkTiles(size, tiles);
1063
+ if (ch.submitImage && ch.submitImageTiles) {
1064
+ await ch.submitImageTiles(
1065
+ elementId,
1066
+ tiles,
1067
+ manifest.id,
1068
+ options2?.transfer ?? false
1069
+ );
1070
+ return;
1071
+ }
1072
+ const image = retained.get(elementId);
1073
+ if (!image) {
1074
+ throw new Error(
1075
+ `contribute.sceneLayer().submitImageTiles("${elementId}"): the retained image is gone (the binary lane went away mid-session)`
1076
+ );
1077
+ }
1078
+ patchImage(image, tiles);
1079
+ await ch.submit(elementId, imageLayer(image), manifest.id);
1080
+ },
912
1081
  dispose() {
913
1082
  const ch = channel();
914
1083
  if (ch) {
915
1084
  for (const id of submitted) void ch.clear(id);
916
1085
  }
917
1086
  submitted.clear();
1087
+ imaged.clear();
1088
+ retained.clear();
918
1089
  }
919
1090
  };
920
1091
  return store.add(surface);
@@ -965,33 +1136,66 @@ function createBundleHost(getEditor, manifest, options) {
965
1136
  walk(roots, null);
966
1137
  return index;
967
1138
  };
1139
+ const gateRawWrite = (mutation, door) => {
1140
+ const denied = denyWrite(hasDoc("write"), door, "capabilities.document.write");
1141
+ if (denied !== null) return { applied: false, error: denied };
1142
+ const foreign = foreignMetadataKey(mutation);
1143
+ if (foreign !== null) {
1144
+ const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
1145
+ log.warn(error);
1146
+ return { applied: false, error };
1147
+ }
1148
+ return null;
1149
+ };
1150
+ const mutateOutcome = (reply) => {
1151
+ if (reply.kind === "mutationApplied") {
1152
+ const minted = reply.payload.minted;
1153
+ return {
1154
+ applied: true,
1155
+ createdId: reply.payload.createdId ?? null,
1156
+ pageIds: reply.payload.pageIds,
1157
+ ...minted !== void 0 ? { minted } : {}
1158
+ };
1159
+ }
1160
+ return {
1161
+ applied: false,
1162
+ error: reply.kind === "mutationFailed" ? reply.payload : reply
1163
+ };
1164
+ };
968
1165
  const document = {
969
1166
  async mutate(mutation) {
970
- const denied = denyWrite(
971
- hasDoc("write"),
972
- "document.mutate",
973
- "capabilities.document.write"
974
- );
975
- if (denied !== null) return { applied: false, error: denied };
976
- const foreign = foreignMetadataKey(mutation);
977
- if (foreign !== null) {
978
- const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
979
- log.warn(error);
980
- return { applied: false, error };
981
- }
1167
+ const gated = gateRawWrite(mutation, "document.mutate");
1168
+ if (gated !== null) return gated;
982
1169
  try {
983
1170
  const reply = await getEditor().client.mutate(mutation);
984
- if (reply.kind === "mutationApplied") {
985
- return {
986
- applied: true,
987
- createdId: reply.payload.createdId ?? null,
988
- pageIds: reply.payload.pageIds
989
- };
990
- }
1171
+ return mutateOutcome(reply);
1172
+ } catch (error) {
1173
+ return { applied: false, error };
1174
+ }
1175
+ },
1176
+ async mutateWithBytes(mutation, bytes, options2) {
1177
+ const gated = gateRawWrite(mutation, "document.mutateWithBytes");
1178
+ if (gated !== null) return gated;
1179
+ if (!hasBytesSlot(mutation)) {
991
1180
  return {
992
1181
  applied: false,
993
- error: reply.kind === "mutationFailed" ? reply.payload : reply
1182
+ error: "mutateWithBytes: no replaceImageBytes with an empty `bytes` slot to fill"
994
1183
  };
1184
+ }
1185
+ try {
1186
+ const editor = getEditor();
1187
+ if (typeof editor.mutateWithBytes === "function") {
1188
+ const reply2 = await editor.mutateWithBytes(
1189
+ mutation,
1190
+ bytes,
1191
+ options2?.transfer === true
1192
+ );
1193
+ return mutateOutcome(reply2);
1194
+ }
1195
+ const reply = await editor.client.mutate(
1196
+ spliceBytesSlot(mutation, Array.from(bytes))
1197
+ );
1198
+ return mutateOutcome(reply);
995
1199
  } catch (error) {
996
1200
  return { applied: false, error };
997
1201
  }
@@ -1185,6 +1389,24 @@ function createBundleHost(getEditor, manifest, options) {
1185
1389
  }
1186
1390
  });
1187
1391
  return store.add(toDisposable(off));
1392
+ },
1393
+ // Protocol 66 batch — the save hook. The host awaits every
1394
+ // registered listener before it serialises the document; the wrapper
1395
+ // makes a bundle's failure a logged event, never a failed save.
1396
+ onWillSave(listener) {
1397
+ const backend = options?.willSave;
1398
+ if (!backend) {
1399
+ return store.add(toDisposable(() => {
1400
+ }));
1401
+ }
1402
+ const wrapped = async (e) => {
1403
+ try {
1404
+ await listener(e);
1405
+ } catch (err) {
1406
+ log.error(`document.onWillSave listener failed: ${String(err)}`);
1407
+ }
1408
+ };
1409
+ return store.add(backend.register(manifest.id, wrapped));
1188
1410
  }
1189
1411
  };
1190
1412
  const selection = {
@@ -1474,9 +1696,44 @@ function createBundleHost(getEditor, manifest, options) {
1474
1696
  `shell.saveFile("${saveOptions.suggestedName}") ignored \u2014 the host app wired no file saver (probe with supports("shell.saveFile@1"))`
1475
1697
  );
1476
1698
  return false;
1699
+ },
1700
+ // Programmatic entry into one of THIS bundle's edit contexts — the
1701
+ // same stack push a double-click performs. A foreign or unregistered
1702
+ // type is a bug in the bundle, so it throws; a host that cannot enter
1703
+ // answers false.
1704
+ async enterEditContext(type, elementId) {
1705
+ if (!registeredContexts.has(type)) {
1706
+ throw new Error(
1707
+ `plugin-api: shell.enterEditContext("${type}") \u2014 ${manifest.id} has registered no edit context of that type. A bundle enters only its own contexts: call contribute.editContext({ type: "${type}", \u2026 }) first.`
1708
+ );
1709
+ }
1710
+ if (!shellBackend?.enterEditContext) {
1711
+ log.warn(
1712
+ `shell.enterEditContext("${type}") ignored \u2014 the host app wired no context entry (probe with supports("shell.enterEditContext@1"))`
1713
+ );
1714
+ return false;
1715
+ }
1716
+ return shellBackend.enterEditContext(type, elementId);
1477
1717
  }
1478
1718
  };
1479
- const widgets = options?.widgets ?? FALLBACK_WIDGETS;
1719
+ const toolSettingsBackend = options?.toolSettings;
1720
+ const tools = {
1721
+ settings(toolId) {
1722
+ assertNamespaced(toolId, "tools.settings tool");
1723
+ return { ...toolSettingsBackend?.get(toolId) ?? {} };
1724
+ },
1725
+ onDidChangeSettings(toolId, listener) {
1726
+ assertNamespaced(toolId, "tools.onDidChangeSettings tool");
1727
+ if (!toolSettingsBackend) return store.add(toDisposable(() => {
1728
+ }));
1729
+ const off = toolSettingsBackend.subscribe(
1730
+ toolId,
1731
+ () => listener({ ...toolSettingsBackend.get(toolId) })
1732
+ );
1733
+ return store.add(toDisposable(off));
1734
+ }
1735
+ };
1736
+ const widgets = { ...FALLBACK_WIDGETS, ...options?.widgets };
1480
1737
  const assetSource = options?.assetSource;
1481
1738
  const assets = {
1482
1739
  async getFontFace(family, style) {
@@ -1673,10 +1930,20 @@ function createBundleHost(getEditor, manifest, options) {
1673
1930
  };
1674
1931
  const parts = {
1675
1932
  async write(path, bytes) {
1933
+ const full = partsFull(path);
1934
+ const binary = getEditor().parts;
1935
+ if (binary) {
1936
+ try {
1937
+ await binary.write(full, bytes, manifest.id);
1938
+ } catch (err) {
1939
+ throw new Error(`host.parts.write("${path}"): ${String(err)}`);
1940
+ }
1941
+ return;
1942
+ }
1676
1943
  const reply = await getEditor().client.send({
1677
1944
  kind: "writePagedPart",
1678
1945
  payload: {
1679
- path: partsFull(path),
1946
+ path: full,
1680
1947
  bytes: Array.from(bytes),
1681
1948
  // C-34 — DECLARE WHO IS WRITING, so the engine
1682
1949
  // confines this to `paged/<id>/` instead of trusting that we
@@ -1692,14 +1959,6 @@ function createBundleHost(getEditor, manifest, options) {
1692
1959
  // probe needed — there is no degraded path to choose between.
1693
1960
  caller: manifest.id
1694
1961
  }
1695
- // THE CAST, named rather than left to be discovered. `wire.d.ts`
1696
- // is vendored from the PUBLISHED `@paged-media/canvas-wasm`
1697
- // (0.61.0), whose `writePagedPart` has no `caller` — the field
1698
- // exists in core today and reaches these types only when the
1699
- // next canvas-wasm publishes and `sync-wire` runs. Same escape-hatch
1700
- // shape the repo already uses elsewhere: a cast pointing at a
1701
- // contract that EXISTS AND IS COMMITTED, not at a hope. It
1702
- // deletes itself at the repin.
1703
1962
  });
1704
1963
  if (reply.kind !== "pagedPartWritten") {
1705
1964
  const err = reply.kind === "pagedPartFailed" ? reply.payload.error : `unexpected ${reply.kind}`;
@@ -1707,9 +1966,12 @@ function createBundleHost(getEditor, manifest, options) {
1707
1966
  }
1708
1967
  },
1709
1968
  async read(path) {
1969
+ const full = partsFull(path);
1970
+ const binary = getEditor().parts;
1971
+ if (binary) return binary.read(full);
1710
1972
  const reply = await getEditor().client.send({
1711
1973
  kind: "readPagedPart",
1712
- payload: { path: partsFull(path) }
1974
+ payload: { path: full }
1713
1975
  });
1714
1976
  if (reply.kind === "pagedPartRead") {
1715
1977
  return reply.payload.found ? Uint8Array.from(reply.payload.bytes) : null;
@@ -1725,6 +1987,27 @@ function createBundleHost(getEditor, manifest, options) {
1725
1987
  return reply.payload.paths.map(
1726
1988
  (p) => p.startsWith(partsBase) ? p.slice(partsBase.length) : p
1727
1989
  );
1990
+ },
1991
+ // Protocol 66 — `deletePagedPart`. The engine tombstones a part the
1992
+ // loaded file carries, so it leaves the next save rather than
1993
+ // reappearing from the source container.
1994
+ async delete(path) {
1995
+ const full = partsFull(path);
1996
+ const binary = getEditor().parts;
1997
+ if (binary) {
1998
+ try {
1999
+ return await binary.delete(full, manifest.id);
2000
+ } catch (err2) {
2001
+ throw new Error(`host.parts.delete("${path}"): ${String(err2)}`);
2002
+ }
2003
+ }
2004
+ const reply = await getEditor().client.send({
2005
+ kind: "deletePagedPart",
2006
+ payload: { path: full, caller: manifest.id }
2007
+ });
2008
+ if (reply.kind === "pagedPartDeleted") return reply.payload.existed;
2009
+ const err = reply.kind === "pagedPartFailed" ? reply.payload.error : `unexpected ${reply.kind}`;
2010
+ throw new Error(`host.parts.delete("${path}"): ${err}`);
1728
2011
  }
1729
2012
  };
1730
2013
  const nativeDocBackend = options?.nativeDocument;
@@ -1972,9 +2255,30 @@ function createBundleHost(getEditor, manifest, options) {
1972
2255
  featureSet.add("shell.saveFile@1");
1973
2256
  }
1974
2257
  }
1975
- if (options?.widgets) {
2258
+ if (options?.widgets?.CodeEditor) {
1976
2259
  featureSet.add("widgets.codeEditor@1");
1977
2260
  }
2261
+ if (options?.widgets?.ColorPicker) {
2262
+ featureSet.add("widgets.colorPicker@1");
2263
+ }
2264
+ if (options?.willSave) {
2265
+ featureSet.add("document.onWillSave@1");
2266
+ }
2267
+ if (typeof options?.shell?.enterEditContext === "function") {
2268
+ featureSet.add("shell.enterEditContext@1");
2269
+ }
2270
+ if (options?.toolSettings) {
2271
+ featureSet.add("tools.settings@1");
2272
+ }
2273
+ {
2274
+ const sl = getEditor().sceneLayers;
2275
+ if (typeof sl?.submitImage === "function" && typeof sl?.submitImageTiles === "function") {
2276
+ featureSet.add("rendering.sceneLayer.binary@1");
2277
+ }
2278
+ }
2279
+ if (typeof getEditor().mutateWithBytes === "function") {
2280
+ featureSet.add("document.mutateBinary@1");
2281
+ }
1978
2282
  if (options?.schemaPanelRenderer) {
1979
2283
  featureSet.add("schemaPanel.renderer@1");
1980
2284
  }
@@ -2040,6 +2344,7 @@ function createBundleHost(getEditor, manifest, options) {
2040
2344
  workers,
2041
2345
  secrets,
2042
2346
  clipboard,
2347
+ tools,
2043
2348
  supports: (feature) => featureSet.has(feature),
2044
2349
  get editor() {
2045
2350
  return getEditor();
@@ -2186,6 +2491,36 @@ async function loadHeadlessEngine(options = {}) {
2186
2491
  }
2187
2492
 
2188
2493
  // src/harness.ts
2494
+ function inMemoryWillSave() {
2495
+ const listeners = /* @__PURE__ */ new Set();
2496
+ return {
2497
+ register(_pluginId, listener) {
2498
+ listeners.add(listener);
2499
+ return { dispose: () => void listeners.delete(listener) };
2500
+ },
2501
+ async fire() {
2502
+ await Promise.all([...listeners].map((l) => l({ format: "paged" })));
2503
+ },
2504
+ count: () => listeners.size
2505
+ };
2506
+ }
2507
+ function inMemoryToolSettings() {
2508
+ const values = /* @__PURE__ */ new Map();
2509
+ const subs = /* @__PURE__ */ new Map();
2510
+ return {
2511
+ get: (toolId) => ({ ...values.get(toolId) ?? {} }),
2512
+ subscribe(toolId, listener) {
2513
+ const set = subs.get(toolId) ?? /* @__PURE__ */ new Set();
2514
+ set.add(listener);
2515
+ subs.set(toolId, set);
2516
+ return () => void set.delete(listener);
2517
+ },
2518
+ set(toolId, key, value) {
2519
+ values.set(toolId, { ...values.get(toolId) ?? {}, [key]: value });
2520
+ for (const l of subs.get(toolId) ?? []) l();
2521
+ }
2522
+ };
2523
+ }
2189
2524
  function inMemoryBlobStore() {
2190
2525
  const byPlugin = /* @__PURE__ */ new Map();
2191
2526
  const dir = (id) => {
@@ -2437,6 +2772,8 @@ async function createHeadlessHost(options = {}) {
2437
2772
  const clipboard = options.clipboard ?? inMemoryClipboard();
2438
2773
  const secrets = options.secrets ?? inMemorySecretStore();
2439
2774
  const bindingProviders = options.bindingProviders ?? createBindingProviderRegistry();
2775
+ const willSave = inMemoryWillSave();
2776
+ const toolSettings = inMemoryToolSettings();
2440
2777
  const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
2441
2778
  console: options.console,
2442
2779
  storage: options.storage,
@@ -2444,6 +2781,8 @@ async function createHeadlessHost(options = {}) {
2444
2781
  clipboard,
2445
2782
  secrets,
2446
2783
  bindingProviders,
2784
+ willSave: options.willSave ?? willSave,
2785
+ toolSettings: options.toolSettings ?? toolSettings,
2447
2786
  capabilityMode: mode,
2448
2787
  // W-06 — a recordable fake asset source the conformance harness
2449
2788
  // can pass so a bundle's `@font-face` byte path is exercisable
@@ -2564,6 +2903,8 @@ async function createHeadlessHost(options = {}) {
2564
2903
  return lastPreviews;
2565
2904
  },
2566
2905
  bindingProviders,
2906
+ willSave,
2907
+ toolSettings,
2567
2908
  async load(idml) {
2568
2909
  const raw = worker.loadDocumentDirect(seqCounter++, idml);
2569
2910
  const reply = JSON.parse(raw);
@@ -2951,6 +3292,8 @@ export {
2951
3292
  createRecordableAssetSource,
2952
3293
  defineBundle,
2953
3294
  endLocalFor,
3295
+ inMemoryToolSettings,
3296
+ inMemoryWillSave,
2954
3297
  loadBundle,
2955
3298
  loadBundleWasm,
2956
3299
  loadHeadlessEngine,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paged-media/plugin-sdk",
3
- "version": "0.2.36-canary.0",
3
+ "version": "0.2.38-canary.3",
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.36-canary.0"
14
+ "@paged-media/plugin-api": "0.2.38-canary.3"
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.63.0",
25
+ "@paged-media/canvas-wasm": "0.66.0",
26
26
  "@types/node": "20.19.39",
27
27
  "@types/react": "^18.3.31",
28
28
  "react": "^18.3.1",