@paged-media/plugin-sdk 0.2.37-canary.0 → 0.2.39-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, 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`
@@ -210,6 +266,10 @@ var HOST_FEATURES = [
210
266
  // honesty split as the protocol-ahead mutations (mutations.ts): the
211
267
  // engine-level gate is the worker handshake, not `supports()`.
212
268
  "document.planarRegions@1",
269
+ // v67 (RFI C-68) — the engine's point snapper. Static like the region
270
+ // door: the flag says this SDK forwards `snapPoint`; an engine older
271
+ // than v67 answers the point unsnapped, never a throw.
272
+ "document.snapPoint@1",
213
273
  // DOC-03 structured whole-story read (canvas-wasm v54). Static for the same
214
274
  // reason as storage.parts@1: the pinned engine is the server, and the worker
215
275
  // handshake refuses a protocol older than the pin — so a running bundle is
@@ -464,6 +524,50 @@ function createBindingProviderRegistry() {
464
524
  }
465
525
  };
466
526
  }
527
+ function checkImage(image) {
528
+ const { width, height, rgba } = image;
529
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || rgba.length !== width * height * 4) {
530
+ throw new Error(
531
+ `contribute.sceneLayer().submitImage: ${rgba.length} bytes for a ${width}x${height} RGBA8 image (want width*height*4)`
532
+ );
533
+ }
534
+ }
535
+ function imageLayer(image) {
536
+ const [x, y, w, h2] = image.dest;
537
+ return {
538
+ items: [
539
+ {
540
+ kind: "image",
541
+ rgba: Array.from(image.rgba),
542
+ width: image.width,
543
+ height: image.height,
544
+ x,
545
+ y,
546
+ w,
547
+ h: h2
548
+ }
549
+ ]
550
+ };
551
+ }
552
+ function checkTiles(image, tiles) {
553
+ for (const t of tiles) {
554
+ 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) {
555
+ throw new Error(
556
+ `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`
557
+ );
558
+ }
559
+ }
560
+ }
561
+ function patchImage(image, tiles) {
562
+ const stride = image.width * 4;
563
+ for (const t of tiles) {
564
+ const row = t.width * 4;
565
+ for (let j = 0; j < t.height; j++) {
566
+ const dst = (t.y + j) * stride + t.x * 4;
567
+ image.rgba.set(t.rgba.subarray(j * row, j * row + row), dst);
568
+ }
569
+ }
570
+ }
467
571
  function createBundleHost(getEditor, manifest, options) {
468
572
  const store = new DisposableStore();
469
573
  const sink = options?.console ?? console;
@@ -892,6 +996,8 @@ function createBundleHost(getEditor, manifest, options) {
892
996
  'capabilities.rendering must include "sceneLayer"'
893
997
  );
894
998
  const submitted = /* @__PURE__ */ new Set();
999
+ const imaged = /* @__PURE__ */ new Map();
1000
+ const retained = /* @__PURE__ */ new Map();
895
1001
  const channel = () => getEditor().sceneLayers;
896
1002
  const surface = {
897
1003
  async submit(elementId, layer) {
@@ -907,14 +1013,83 @@ function createBundleHost(getEditor, manifest, options) {
907
1013
  },
908
1014
  async clear(elementId) {
909
1015
  submitted.delete(elementId);
1016
+ imaged.delete(elementId);
1017
+ retained.delete(elementId);
910
1018
  await channel()?.clear(elementId);
911
1019
  },
1020
+ // Protocol 66 — the binary scene-image lane, with the SDK owning
1021
+ // the fallback so a bundle calls one method on every host.
1022
+ async submitImage(elementId, image, options2) {
1023
+ const ch = channel();
1024
+ if (!ch) {
1025
+ log.warn(
1026
+ `contribute.sceneLayer().submitImage("${elementId}") ignored \u2014 the host wired no scene channel (probe supports("rendering.sceneLayer@1"))`
1027
+ );
1028
+ return;
1029
+ }
1030
+ checkImage(image);
1031
+ submitted.add(elementId);
1032
+ imaged.set(elementId, { width: image.width, height: image.height });
1033
+ if (ch.submitImage && ch.submitImageTiles) {
1034
+ retained.delete(elementId);
1035
+ await ch.submitImage(
1036
+ elementId,
1037
+ image,
1038
+ manifest.id,
1039
+ options2?.transfer ?? false
1040
+ );
1041
+ return;
1042
+ }
1043
+ const copy = {
1044
+ rgba: new Uint8Array(image.rgba),
1045
+ width: image.width,
1046
+ height: image.height,
1047
+ dest: [...image.dest]
1048
+ };
1049
+ retained.set(elementId, copy);
1050
+ await ch.submit(elementId, imageLayer(copy), manifest.id);
1051
+ },
1052
+ async submitImageTiles(elementId, tiles, options2) {
1053
+ const ch = channel();
1054
+ if (!ch) {
1055
+ log.warn(
1056
+ `contribute.sceneLayer().submitImageTiles("${elementId}") ignored \u2014 the host wired no scene channel`
1057
+ );
1058
+ return;
1059
+ }
1060
+ const size = imaged.get(elementId);
1061
+ if (!size) {
1062
+ throw new Error(
1063
+ `contribute.sceneLayer().submitImageTiles("${elementId}"): no image was submitted for this element \u2014 call submitImage first`
1064
+ );
1065
+ }
1066
+ checkTiles(size, tiles);
1067
+ if (ch.submitImage && ch.submitImageTiles) {
1068
+ await ch.submitImageTiles(
1069
+ elementId,
1070
+ tiles,
1071
+ manifest.id,
1072
+ options2?.transfer ?? false
1073
+ );
1074
+ return;
1075
+ }
1076
+ const image = retained.get(elementId);
1077
+ if (!image) {
1078
+ throw new Error(
1079
+ `contribute.sceneLayer().submitImageTiles("${elementId}"): the retained image is gone (the binary lane went away mid-session)`
1080
+ );
1081
+ }
1082
+ patchImage(image, tiles);
1083
+ await ch.submit(elementId, imageLayer(image), manifest.id);
1084
+ },
912
1085
  dispose() {
913
1086
  const ch = channel();
914
1087
  if (ch) {
915
1088
  for (const id of submitted) void ch.clear(id);
916
1089
  }
917
1090
  submitted.clear();
1091
+ imaged.clear();
1092
+ retained.clear();
918
1093
  }
919
1094
  };
920
1095
  return store.add(surface);
@@ -965,33 +1140,66 @@ function createBundleHost(getEditor, manifest, options) {
965
1140
  walk(roots, null);
966
1141
  return index;
967
1142
  };
1143
+ const gateRawWrite = (mutation, door) => {
1144
+ const denied = denyWrite(hasDoc("write"), door, "capabilities.document.write");
1145
+ if (denied !== null) return { applied: false, error: denied };
1146
+ const foreign = foreignMetadataKey(mutation);
1147
+ if (foreign !== null) {
1148
+ const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
1149
+ log.warn(error);
1150
+ return { applied: false, error };
1151
+ }
1152
+ return null;
1153
+ };
1154
+ const mutateOutcome = (reply) => {
1155
+ if (reply.kind === "mutationApplied") {
1156
+ const minted = reply.payload.minted;
1157
+ return {
1158
+ applied: true,
1159
+ createdId: reply.payload.createdId ?? null,
1160
+ pageIds: reply.payload.pageIds,
1161
+ ...minted !== void 0 ? { minted } : {}
1162
+ };
1163
+ }
1164
+ return {
1165
+ applied: false,
1166
+ error: reply.kind === "mutationFailed" ? reply.payload : reply
1167
+ };
1168
+ };
968
1169
  const document = {
969
1170
  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
- }
1171
+ const gated = gateRawWrite(mutation, "document.mutate");
1172
+ if (gated !== null) return gated;
982
1173
  try {
983
1174
  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
- }
1175
+ return mutateOutcome(reply);
1176
+ } catch (error) {
1177
+ return { applied: false, error };
1178
+ }
1179
+ },
1180
+ async mutateWithBytes(mutation, bytes, options2) {
1181
+ const gated = gateRawWrite(mutation, "document.mutateWithBytes");
1182
+ if (gated !== null) return gated;
1183
+ if (!hasBytesSlot(mutation)) {
991
1184
  return {
992
1185
  applied: false,
993
- error: reply.kind === "mutationFailed" ? reply.payload : reply
1186
+ error: "mutateWithBytes: no replaceImageBytes with an empty `bytes` slot to fill"
994
1187
  };
1188
+ }
1189
+ try {
1190
+ const editor = getEditor();
1191
+ if (typeof editor.mutateWithBytes === "function") {
1192
+ const reply2 = await editor.mutateWithBytes(
1193
+ mutation,
1194
+ bytes,
1195
+ options2?.transfer === true
1196
+ );
1197
+ return mutateOutcome(reply2);
1198
+ }
1199
+ const reply = await editor.client.mutate(
1200
+ spliceBytesSlot(mutation, Array.from(bytes))
1201
+ );
1202
+ return mutateOutcome(reply);
995
1203
  } catch (error) {
996
1204
  return { applied: false, error };
997
1205
  }
@@ -1092,6 +1300,24 @@ function createBundleHost(getEditor, manifest, options) {
1092
1300
  return refusal(`the region read door failed: ${String(error)}`);
1093
1301
  }
1094
1302
  },
1303
+ async snapPoint(query) {
1304
+ requireDocRead("document.snapPoint");
1305
+ const unsnapped = () => ({
1306
+ point: [query.point[0], query.point[1]],
1307
+ snapped: false,
1308
+ lines: [],
1309
+ tolerancePt: 0
1310
+ });
1311
+ try {
1312
+ const reply = await getEditor().client.send({
1313
+ kind: "requestSnapPoint",
1314
+ payload: { query }
1315
+ });
1316
+ return reply.kind === "snapPoint" ? reply.payload.result : unsnapped();
1317
+ } catch {
1318
+ return unsnapped();
1319
+ }
1320
+ },
1095
1321
  async tree() {
1096
1322
  requireDocRead("document.tree");
1097
1323
  const reply = await getEditor().client.send({
@@ -1185,6 +1411,24 @@ function createBundleHost(getEditor, manifest, options) {
1185
1411
  }
1186
1412
  });
1187
1413
  return store.add(toDisposable(off));
1414
+ },
1415
+ // Protocol 66 batch — the save hook. The host awaits every
1416
+ // registered listener before it serialises the document; the wrapper
1417
+ // makes a bundle's failure a logged event, never a failed save.
1418
+ onWillSave(listener) {
1419
+ const backend = options?.willSave;
1420
+ if (!backend) {
1421
+ return store.add(toDisposable(() => {
1422
+ }));
1423
+ }
1424
+ const wrapped = async (e) => {
1425
+ try {
1426
+ await listener(e);
1427
+ } catch (err) {
1428
+ log.error(`document.onWillSave listener failed: ${String(err)}`);
1429
+ }
1430
+ };
1431
+ return store.add(backend.register(manifest.id, wrapped));
1188
1432
  }
1189
1433
  };
1190
1434
  const selection = {
@@ -1474,9 +1718,44 @@ function createBundleHost(getEditor, manifest, options) {
1474
1718
  `shell.saveFile("${saveOptions.suggestedName}") ignored \u2014 the host app wired no file saver (probe with supports("shell.saveFile@1"))`
1475
1719
  );
1476
1720
  return false;
1721
+ },
1722
+ // Programmatic entry into one of THIS bundle's edit contexts — the
1723
+ // same stack push a double-click performs. A foreign or unregistered
1724
+ // type is a bug in the bundle, so it throws; a host that cannot enter
1725
+ // answers false.
1726
+ async enterEditContext(type, elementId) {
1727
+ if (!registeredContexts.has(type)) {
1728
+ throw new Error(
1729
+ `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.`
1730
+ );
1731
+ }
1732
+ if (!shellBackend?.enterEditContext) {
1733
+ log.warn(
1734
+ `shell.enterEditContext("${type}") ignored \u2014 the host app wired no context entry (probe with supports("shell.enterEditContext@1"))`
1735
+ );
1736
+ return false;
1737
+ }
1738
+ return shellBackend.enterEditContext(type, elementId);
1739
+ }
1740
+ };
1741
+ const toolSettingsBackend = options?.toolSettings;
1742
+ const tools = {
1743
+ settings(toolId) {
1744
+ assertNamespaced(toolId, "tools.settings tool");
1745
+ return { ...toolSettingsBackend?.get(toolId) ?? {} };
1746
+ },
1747
+ onDidChangeSettings(toolId, listener) {
1748
+ assertNamespaced(toolId, "tools.onDidChangeSettings tool");
1749
+ if (!toolSettingsBackend) return store.add(toDisposable(() => {
1750
+ }));
1751
+ const off = toolSettingsBackend.subscribe(
1752
+ toolId,
1753
+ () => listener({ ...toolSettingsBackend.get(toolId) })
1754
+ );
1755
+ return store.add(toDisposable(off));
1477
1756
  }
1478
1757
  };
1479
- const widgets = options?.widgets ?? FALLBACK_WIDGETS;
1758
+ const widgets = { ...FALLBACK_WIDGETS, ...options?.widgets };
1480
1759
  const assetSource = options?.assetSource;
1481
1760
  const assets = {
1482
1761
  async getFontFace(family, style) {
@@ -1673,10 +1952,20 @@ function createBundleHost(getEditor, manifest, options) {
1673
1952
  };
1674
1953
  const parts = {
1675
1954
  async write(path, bytes) {
1955
+ const full = partsFull(path);
1956
+ const binary = getEditor().parts;
1957
+ if (binary) {
1958
+ try {
1959
+ await binary.write(full, bytes, manifest.id);
1960
+ } catch (err) {
1961
+ throw new Error(`host.parts.write("${path}"): ${String(err)}`);
1962
+ }
1963
+ return;
1964
+ }
1676
1965
  const reply = await getEditor().client.send({
1677
1966
  kind: "writePagedPart",
1678
1967
  payload: {
1679
- path: partsFull(path),
1968
+ path: full,
1680
1969
  bytes: Array.from(bytes),
1681
1970
  // C-34 — DECLARE WHO IS WRITING, so the engine
1682
1971
  // confines this to `paged/<id>/` instead of trusting that we
@@ -1692,14 +1981,6 @@ function createBundleHost(getEditor, manifest, options) {
1692
1981
  // probe needed — there is no degraded path to choose between.
1693
1982
  caller: manifest.id
1694
1983
  }
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
1984
  });
1704
1985
  if (reply.kind !== "pagedPartWritten") {
1705
1986
  const err = reply.kind === "pagedPartFailed" ? reply.payload.error : `unexpected ${reply.kind}`;
@@ -1707,9 +1988,12 @@ function createBundleHost(getEditor, manifest, options) {
1707
1988
  }
1708
1989
  },
1709
1990
  async read(path) {
1991
+ const full = partsFull(path);
1992
+ const binary = getEditor().parts;
1993
+ if (binary) return binary.read(full);
1710
1994
  const reply = await getEditor().client.send({
1711
1995
  kind: "readPagedPart",
1712
- payload: { path: partsFull(path) }
1996
+ payload: { path: full }
1713
1997
  });
1714
1998
  if (reply.kind === "pagedPartRead") {
1715
1999
  return reply.payload.found ? Uint8Array.from(reply.payload.bytes) : null;
@@ -1725,6 +2009,27 @@ function createBundleHost(getEditor, manifest, options) {
1725
2009
  return reply.payload.paths.map(
1726
2010
  (p) => p.startsWith(partsBase) ? p.slice(partsBase.length) : p
1727
2011
  );
2012
+ },
2013
+ // Protocol 66 — `deletePagedPart`. The engine tombstones a part the
2014
+ // loaded file carries, so it leaves the next save rather than
2015
+ // reappearing from the source container.
2016
+ async delete(path) {
2017
+ const full = partsFull(path);
2018
+ const binary = getEditor().parts;
2019
+ if (binary) {
2020
+ try {
2021
+ return await binary.delete(full, manifest.id);
2022
+ } catch (err2) {
2023
+ throw new Error(`host.parts.delete("${path}"): ${String(err2)}`);
2024
+ }
2025
+ }
2026
+ const reply = await getEditor().client.send({
2027
+ kind: "deletePagedPart",
2028
+ payload: { path: full, caller: manifest.id }
2029
+ });
2030
+ if (reply.kind === "pagedPartDeleted") return reply.payload.existed;
2031
+ const err = reply.kind === "pagedPartFailed" ? reply.payload.error : `unexpected ${reply.kind}`;
2032
+ throw new Error(`host.parts.delete("${path}"): ${err}`);
1728
2033
  }
1729
2034
  };
1730
2035
  const nativeDocBackend = options?.nativeDocument;
@@ -1972,9 +2277,30 @@ function createBundleHost(getEditor, manifest, options) {
1972
2277
  featureSet.add("shell.saveFile@1");
1973
2278
  }
1974
2279
  }
1975
- if (options?.widgets) {
2280
+ if (options?.widgets?.CodeEditor) {
1976
2281
  featureSet.add("widgets.codeEditor@1");
1977
2282
  }
2283
+ if (options?.widgets?.ColorPicker) {
2284
+ featureSet.add("widgets.colorPicker@1");
2285
+ }
2286
+ if (options?.willSave) {
2287
+ featureSet.add("document.onWillSave@1");
2288
+ }
2289
+ if (typeof options?.shell?.enterEditContext === "function") {
2290
+ featureSet.add("shell.enterEditContext@1");
2291
+ }
2292
+ if (options?.toolSettings) {
2293
+ featureSet.add("tools.settings@1");
2294
+ }
2295
+ {
2296
+ const sl = getEditor().sceneLayers;
2297
+ if (typeof sl?.submitImage === "function" && typeof sl?.submitImageTiles === "function") {
2298
+ featureSet.add("rendering.sceneLayer.binary@1");
2299
+ }
2300
+ }
2301
+ if (typeof getEditor().mutateWithBytes === "function") {
2302
+ featureSet.add("document.mutateBinary@1");
2303
+ }
1978
2304
  if (options?.schemaPanelRenderer) {
1979
2305
  featureSet.add("schemaPanel.renderer@1");
1980
2306
  }
@@ -2040,6 +2366,7 @@ function createBundleHost(getEditor, manifest, options) {
2040
2366
  workers,
2041
2367
  secrets,
2042
2368
  clipboard,
2369
+ tools,
2043
2370
  supports: (feature) => featureSet.has(feature),
2044
2371
  get editor() {
2045
2372
  return getEditor();
@@ -2186,6 +2513,36 @@ async function loadHeadlessEngine(options = {}) {
2186
2513
  }
2187
2514
 
2188
2515
  // src/harness.ts
2516
+ function inMemoryWillSave() {
2517
+ const listeners = /* @__PURE__ */ new Set();
2518
+ return {
2519
+ register(_pluginId, listener) {
2520
+ listeners.add(listener);
2521
+ return { dispose: () => void listeners.delete(listener) };
2522
+ },
2523
+ async fire() {
2524
+ await Promise.all([...listeners].map((l) => l({ format: "paged" })));
2525
+ },
2526
+ count: () => listeners.size
2527
+ };
2528
+ }
2529
+ function inMemoryToolSettings() {
2530
+ const values = /* @__PURE__ */ new Map();
2531
+ const subs = /* @__PURE__ */ new Map();
2532
+ return {
2533
+ get: (toolId) => ({ ...values.get(toolId) ?? {} }),
2534
+ subscribe(toolId, listener) {
2535
+ const set = subs.get(toolId) ?? /* @__PURE__ */ new Set();
2536
+ set.add(listener);
2537
+ subs.set(toolId, set);
2538
+ return () => void set.delete(listener);
2539
+ },
2540
+ set(toolId, key, value) {
2541
+ values.set(toolId, { ...values.get(toolId) ?? {}, [key]: value });
2542
+ for (const l of subs.get(toolId) ?? []) l();
2543
+ }
2544
+ };
2545
+ }
2189
2546
  function inMemoryBlobStore() {
2190
2547
  const byPlugin = /* @__PURE__ */ new Map();
2191
2548
  const dir = (id) => {
@@ -2437,6 +2794,8 @@ async function createHeadlessHost(options = {}) {
2437
2794
  const clipboard = options.clipboard ?? inMemoryClipboard();
2438
2795
  const secrets = options.secrets ?? inMemorySecretStore();
2439
2796
  const bindingProviders = options.bindingProviders ?? createBindingProviderRegistry();
2797
+ const willSave = inMemoryWillSave();
2798
+ const toolSettings = inMemoryToolSettings();
2440
2799
  const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
2441
2800
  console: options.console,
2442
2801
  storage: options.storage,
@@ -2444,6 +2803,8 @@ async function createHeadlessHost(options = {}) {
2444
2803
  clipboard,
2445
2804
  secrets,
2446
2805
  bindingProviders,
2806
+ willSave: options.willSave ?? willSave,
2807
+ toolSettings: options.toolSettings ?? toolSettings,
2447
2808
  capabilityMode: mode,
2448
2809
  // W-06 — a recordable fake asset source the conformance harness
2449
2810
  // can pass so a bundle's `@font-face` byte path is exercisable
@@ -2564,6 +2925,8 @@ async function createHeadlessHost(options = {}) {
2564
2925
  return lastPreviews;
2565
2926
  },
2566
2927
  bindingProviders,
2928
+ willSave,
2929
+ toolSettings,
2567
2930
  async load(idml) {
2568
2931
  const raw = worker.loadDocumentDirect(seqCounter++, idml);
2569
2932
  const reply = JSON.parse(raw);
@@ -2951,6 +3314,8 @@ export {
2951
3314
  createRecordableAssetSource,
2952
3315
  defineBundle,
2953
3316
  endLocalFor,
3317
+ inMemoryToolSettings,
3318
+ inMemoryWillSave,
2954
3319
  loadBundle,
2955
3320
  loadBundleWasm,
2956
3321
  loadHeadlessEngine,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paged-media/plugin-sdk",
3
- "version": "0.2.37-canary.0",
3
+ "version": "0.2.39-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.37-canary.0"
14
+ "@paged-media/plugin-api": "0.2.39-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.64.0",
25
+ "@paged-media/canvas-wasm": "0.67.0",
26
26
  "@types/node": "20.19.39",
27
27
  "@types/react": "^18.3.31",
28
28
  "react": "^18.3.1",