@paged-media/plugin-api 0.2.27-canary.1 → 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.
@@ -0,0 +1,331 @@
1
+ import type { Disposable, MutationOutcome } from "./host";
2
+ import type { MutationInput } from "./mutations";
3
+ import type { CollectionName, ElementId, PropertyPath, Value } from "./wire";
4
+ /**
5
+ * WHAT a property resolution addresses. Three variants, one per shape
6
+ * the proof consumers need — and no more:
7
+ *
8
+ * · `selection` — the host's CURRENT selection in the given scope.
9
+ * The default for a panel binding, and the ONLY variant a
10
+ * `WidgetValueBinding` produces today (its `scope` field maps here
11
+ * 1:1). The provider resolves it IN ITS OWN REALM: it already knows
12
+ * which raster layer / which cell / which DOCX run is selected, so
13
+ * the host never has to name a thing it cannot address.
14
+ * · `element` — a core-addressable target the host names explicitly.
15
+ * `ElementId` ALREADY models both halves the consumers need: the
16
+ * element kinds (`rectangle`, `textFrame`, `group`, …) AND the
17
+ * range kinds (`storyRange` carries `{story_id, start, end}`;
18
+ * `tableCell` carries the cell address). Range-scoped addressing is
19
+ * therefore admitted by core's own type — this contract does not
20
+ * invent a parallel one.
21
+ * · `row` — a row this provider itself handed out through
22
+ * `readCollection`. The id is the PROVIDER's vocabulary, opaque to
23
+ * the host, which is what lets plugin-image key rows by its own
24
+ * stable layer ids while the editor keys them by `selfId`.
25
+ *
26
+ * There is deliberately NO document-scoped target variant: the
27
+ * document-scoped lanes (`readCollection`, `applyMutation`) take no
28
+ * target at all, because a swatch list is not "the value of a path for
29
+ * a thing".
30
+ */
31
+ export type BindingTarget = {
32
+ kind: "selection";
33
+ /** Which selection surface — mirrors `WidgetValueBinding.scope`.
34
+ * `"element"` is the page-item selection; `"content"` is the text
35
+ * caret / range selection (the Character-panel lane). */
36
+ scope: "element" | "content";
37
+ } | {
38
+ kind: "element";
39
+ id: ElementId;
40
+ } | {
41
+ kind: "row";
42
+ /** The collection the row came from — a provider only ever gets a
43
+ * row target for a collection it declared. */
44
+ collection: CollectionName;
45
+ /** The row's id, in the shape the provider's own rows carry. */
46
+ id: string;
47
+ };
48
+ /** A property READ: one typed path at one target. */
49
+ export interface BindingPropertyRequest {
50
+ path: PropertyPath;
51
+ target: BindingTarget;
52
+ }
53
+ /** A property WRITE: the read request plus the typed payload. The
54
+ * payload is a core `Value` — see the vocabulary rule in this file's
55
+ * header; a provider that cannot express its own storage as a `Value`
56
+ * declines rather than inventing one. */
57
+ export interface BindingPropertyWrite extends BindingPropertyRequest {
58
+ value: Value;
59
+ }
60
+ /** A collection READ. DOCUMENT-SCOPED: no target, because a swatch list
61
+ * or a layer list belongs to the document, not to a selection. */
62
+ export interface BindingCollectionRequest {
63
+ collection: CollectionName;
64
+ }
65
+ /**
66
+ * A provider's answer to a property READ. FOUR states, and the
67
+ * distinctions are load-bearing — collapsing any two of them produces a
68
+ * user-visible lie:
69
+ *
70
+ * · `value` — a definite value.
71
+ * · `mixed` — the target spans SEVERAL values (a multi-format
72
+ * character range, a multi-row selection). The panel must show
73
+ * "mixed", never pick a winner. This mirrors core's OWN convention
74
+ * exactly: `PropertyEntry.value` is `Value | null`, and the wire's
75
+ * own comment says `None` signals "mixed / indeterminate — a
76
+ * `StoryRange` whose `CharacterRun`s carry conflicting values".
77
+ * · `absent` — this provider OWNS the target, but the path does not
78
+ * apply to it (a raster text layer with no leading concept). The
79
+ * panel blanks / disables the row; it does NOT fall through to
80
+ * core, because core has no opinion about a thing it does not hold.
81
+ * · `decline` — NOT MINE right now. Resolution continues down the
82
+ * active-provider stack and then falls through to core.
83
+ *
84
+ * `absent` vs `decline` is the pair most easily conflated and the most
85
+ * expensive to conflate: fall through on an owned-but-inapplicable path
86
+ * and the panel shows the CORE text frame's leading while the user is
87
+ * editing a raster layer.
88
+ */
89
+ export type BindingRead = BindingResolved | BindingDecline;
90
+ /** The answers that CLAIM the target (everything but `decline`). */
91
+ export type BindingResolved = {
92
+ kind: "value";
93
+ value: Value;
94
+ } | {
95
+ kind: "mixed";
96
+ } | {
97
+ kind: "absent";
98
+ reason?: string;
99
+ };
100
+ /** "Not mine" — keep looking (the next active provider, then core). */
101
+ export interface BindingDecline {
102
+ kind: "decline";
103
+ reason?: string;
104
+ }
105
+ /**
106
+ * A provider's answer to a WRITE (property or structural).
107
+ *
108
+ * `applied` carries a {@link MutationOutcome} — the SAME type
109
+ * `host.document.mutate` answers — deliberately, so the host panel has
110
+ * ONE code path for the provider write and the core fall-through write.
111
+ * A provider that simply forwards to `host.document.mutate` returns its
112
+ * outcome unchanged; a provider writing its own storage answers
113
+ * `{ applied: true, createdId: null, pageIds: [] }` — an empty
114
+ * `pageIds` is the honest "no engine page changed", not a placeholder.
115
+ * A refused write (a locked layer) is `{ applied: false, error }`, not
116
+ * a `decline`: the provider owned it and said no.
117
+ *
118
+ * THE UNDO RULE, which the type states and cannot enforce: a provider's
119
+ * write MUST land through a door that participates in undo — either
120
+ * `host.document.mutate` (the document stack) or its active context's
121
+ * OWN op-log when that context owns undo (ADR-012 Tier 1: a context
122
+ * declaring `onUndo`/`onRedo` has Cmd-Z routed to it while active, and
123
+ * commit-exit re-lowers the net change as one atomic document step).
124
+ * Either way the user's Cmd-Z works. A provider that mutates outside
125
+ * both IS the side channel this contract exists to prevent; the
126
+ * contract cannot police a callback running in the plugin's realm, so
127
+ * this is stated as a requirement on the implementer, honestly.
128
+ */
129
+ export type BindingWrite = {
130
+ kind: "applied";
131
+ outcome: MutationOutcome;
132
+ } | BindingDecline;
133
+ /** A provider's answer to a collection READ. `rows` MUST carry CORE's
134
+ * row shape for that collection (`LayerSummary` for `"layers"`,
135
+ * `SwatchSummary` for `"swatches"`, …) — that is the vocabulary rule
136
+ * applied to collections, and it is what lets one host list render
137
+ * provider rows and core rows with the same renderer. Extra fields are
138
+ * ignored by the host; a field the provider's model has no counterpart
139
+ * for carries the provider's honest default, and the honest way to
140
+ * suppress a control it cannot serve is to leave that path OUT of
141
+ * `provides.paths` (see {@link BindingProviderScope}). */
142
+ export type BindingCollection = {
143
+ kind: "rows";
144
+ rows: readonly unknown[];
145
+ } | BindingDecline;
146
+ /** The op names a provider may intercept — core's own mutation
147
+ * vocabulary, unchanged (`MutationInput` = the vendored `Mutation`
148
+ * union plus the protocol-ahead ops). No new verbs enter here. */
149
+ export type BindingOp = MutationInput["op"];
150
+ /**
151
+ * WHAT a provider declares it serves. The declaration IS the gate: the
152
+ * registry never consults a provider about a path / collection / op it
153
+ * did not declare, so a provider cannot accidentally intercept the
154
+ * whole panel surface, and the host can read the declaration (through
155
+ * `activeProviders()`) to decide what a panel may offer at all.
156
+ *
157
+ * Every member is optional and every member is a CLOSED core union —
158
+ * see the vocabulary rule in this file's header.
159
+ */
160
+ export interface BindingProviderScope {
161
+ /** Typed property paths this provider READS. (Also the write set unless
162
+ * {@link BindingProviderScope.writablePaths} narrows it.) */
163
+ paths?: readonly PropertyPath[];
164
+ /**
165
+ * The subset of {@link BindingProviderScope.paths} this provider accepts
166
+ * WRITES for. OMITTED means "all of them" — the behaviour before this
167
+ * member existed, so no existing provider changes.
168
+ *
169
+ * WHY IT EXISTS, and why the first two proof consumers did not need it.
170
+ * The property-WRITE lane was the one lane no consumer had exercised:
171
+ * Layers writes STRUCTURALLY (`layerMove` / `layerSetVisible` through
172
+ * `applyMutation`) and Swatches likewise (`editSwatch`), and both of
173
+ * those are DECLARED in `ops` — so a host panel can ask
174
+ * "may this control work?" synchronously and DISABLE it, which is
175
+ * exactly what ADR-023's Swatches slice does. The property lane had no
176
+ * such declaration: `writeProperty` is an optional CALLBACK, and a
177
+ * callback's absence is not visible through `activeProviders()`.
178
+ *
179
+ * The consequence was a contradiction inside this very file. The op
180
+ * lane's rule is "an undeclared op must not reach core — the panel is
181
+ * showing somebody else's rows"; the path lane's rule (below, now
182
+ * corrected) said the opposite: that an absent `writeProperty` lets
183
+ * writes fall through to core. Fall-through there is the WRITE-side
184
+ * form of the `absent` lie — the panel is showing a sheet cell's font
185
+ * size and the commit lands on whatever core text the caret last
186
+ * touched.
187
+ *
188
+ * So a provider that can READ a path but not WRITE it says so HERE, the
189
+ * host reads the declaration, and the control renders read-only instead
190
+ * of being fake-interactive. Forced by paged.sheet as the
191
+ * Character/Paragraph provider: the sheet engine has no cell-style
192
+ * write API at all (all spreadsheet semantics live in Rust, and adding
193
+ * one is a Rust change), so it reads the whole surface and writes none
194
+ * of it — `writablePaths: []`.
195
+ */
196
+ writablePaths?: readonly PropertyPath[];
197
+ /** Named core collections whose ROWS this provider serves. */
198
+ collections?: readonly CollectionName[];
199
+ /** Mutation ops this provider takes first refusal on — the STRUCTURAL
200
+ * edits a host panel would otherwise send straight to core
201
+ * (`layerMove`, `createSwatch`, …). A `"batch"` declaration takes the
202
+ * WHOLE batch: the registry never decomposes one to route children
203
+ * separately (that would split one undo step across two authorities). */
204
+ ops?: readonly BindingOp[];
205
+ }
206
+ /**
207
+ * The resolver a bundle registers through
208
+ * `host.contribute.bindingProvider(contextType, provider)`.
209
+ *
210
+ * LIFETIME — the whole design rests on this: a provider is consulted
211
+ * ONLY while the edit context named at registration is ACTIVE. It
212
+ * borrows `contributeEditContext`'s activation rather than inventing a
213
+ * parallel notion of "who is active", because the shell's context stack
214
+ * ALREADY is that notion (it owns the stack, the breadcrumb, the
215
+ * tool/panel swap and the write-scope narrowing). Concretely the SDK
216
+ * adapter wraps the context's own `onEnter`/`onExit` hooks, so
217
+ * activation is derived from the shell's stack rather than reported
218
+ * alongside it and cannot drift from it.
219
+ *
220
+ * The consequence is deliberate and worth stating rather than
221
+ * discovering: a provider is never consulted while its context is
222
+ * inactive — INCLUDING for the document-scoped lanes. A plugin's colour
223
+ * vocabulary shows in the host Swatches panel while you are inside that
224
+ * plugin's frame and not after you leave it. That is the retargeting
225
+ * behaviour ADR-023 set out to copy (there is ONE Character panel and it
226
+ * retargets), applied consistently; a plugin that needs its resources
227
+ * visible document-wide keeps its own panel.
228
+ *
229
+ * Every lane is OPTIONAL. Declaring a lane in `provides` without
230
+ * implementing its callback is an authoring bug, not a stance, and the
231
+ * adapter refuses it loudly at registration.
232
+ */
233
+ export interface BindingProvider {
234
+ /** What this provider answers for. See {@link BindingProviderScope}. */
235
+ provides: BindingProviderScope;
236
+ /** Read one typed path at one target. Required iff `provides.paths`
237
+ * is non-empty. */
238
+ readProperty?(request: BindingPropertyRequest): BindingRead | Promise<BindingRead>;
239
+ /** Write one typed path at one target. OPTIONAL even with declared
240
+ * paths — a read-only provider (a computed readout) is legitimate.
241
+ *
242
+ * CORRECTED 2026-08-05 (the Character/Paragraph consumer). This used
243
+ * to say its absence means "writes fall through to core, which is the
244
+ * honest behaviour". It is not honest: a provider that CLAIMED the
245
+ * read owns the target, and sending the commit to core writes
246
+ * whatever core's selection happens to be — the write-side form of
247
+ * the `absent` lie the whole seam exists to prevent. A read-only
248
+ * provider now DECLARES itself with
249
+ * {@link BindingProviderScope.writablePaths}, and the host renders
250
+ * those controls read-only rather than routing their writes anywhere.
251
+ * A claimed path whose write the registry refuses is DROPPED, never
252
+ * re-aimed at core. */
253
+ writeProperty?(request: BindingPropertyWrite): BindingWrite | Promise<BindingWrite>;
254
+ /** Serve the rows of a declared collection. Required iff
255
+ * `provides.collections` is non-empty. */
256
+ readCollection?(request: BindingCollectionRequest): BindingCollection | Promise<BindingCollection>;
257
+ /** Take first refusal on a declared structural op. Required iff
258
+ * `provides.ops` is non-empty. */
259
+ applyMutation?(mutation: MutationInput): BindingWrite | Promise<BindingWrite>;
260
+ }
261
+ /**
262
+ * What `host.contribute.bindingProvider` returns. Disposing it removes
263
+ * the provider WITHOUT touching the edit context it borrowed activation
264
+ * from — which is what makes ADR-023 phase D a migration rather than a
265
+ * deletion: a duplicate panel retires behind the seam one at a time,
266
+ * and rolls back by disposing one handle.
267
+ */
268
+ export interface BindingProviderHandle extends Disposable {
269
+ /**
270
+ * Announce that the values this provider serves have CHANGED, so host
271
+ * panels bound through it re-read.
272
+ *
273
+ * This exists because the host's usual refresh signal —
274
+ * `host.document.onDidChange` — fires on ENGINE mutations, and a
275
+ * provider's state frequently changes without one (a raster layer
276
+ * toggled inside plugin-image's own wasm layer graph moves no engine
277
+ * page). Without this the host panel goes quietly stale, which is the
278
+ * class of lie this platform refuses. Cheap and coarse by design:
279
+ * "re-read", not a per-path diff — panels are small and a diff
280
+ * protocol here would be a second damage-tracking system.
281
+ */
282
+ invalidate(): void;
283
+ }
284
+ /** The registry's answer to a property read. `resolved: false` ⇒ the
285
+ * host reads core (`document.elementProperties` / its own panel path). */
286
+ export type BindingReadResult = {
287
+ resolved: true;
288
+ provider: string;
289
+ read: BindingResolved;
290
+ } | {
291
+ resolved: false;
292
+ reason: string;
293
+ };
294
+ /** The registry's answer to a write (property or structural).
295
+ * `handled: false` ⇒ the host writes core (`document.mutate`). */
296
+ export type BindingWriteResult = {
297
+ handled: true;
298
+ provider: string;
299
+ outcome: MutationOutcome;
300
+ } | {
301
+ handled: false;
302
+ reason: string;
303
+ };
304
+ /** The registry's answer to a collection read. `resolved: false` ⇒ the
305
+ * host reads core (`document.collection(name)`). */
306
+ export type BindingCollectionResult = {
307
+ resolved: true;
308
+ provider: string;
309
+ rows: readonly unknown[];
310
+ } | {
311
+ resolved: false;
312
+ reason: string;
313
+ };
314
+ /**
315
+ * One entry of the ACTIVE provider stack, as the host sees it —
316
+ * innermost (most recently entered) FIRST, which is the precedence
317
+ * order. A host panel reads this to decide what it may OFFER at all:
318
+ * a control whose path no active provider declares, over a row an
319
+ * active provider owns, is a control that cannot work.
320
+ */
321
+ export interface ActiveBindingProvider {
322
+ /** The owning bundle's manifest id. */
323
+ plugin: string;
324
+ /** The edit-context type whose activation this provider borrows. */
325
+ contextType: string;
326
+ /** The element the context was entered on (the shell's scope root),
327
+ * or `null` when the host reported none. */
328
+ elementId: string | null;
329
+ /** What it declares it serves. */
330
+ provides: BindingProviderScope;
331
+ }
@@ -1 +1 @@
1
- export type { ShellRegistries, ToolRegistry, PanelRegistry, CommandRegistry, KeybindingRegistry, OverlayRegistry, EditContextRegistry, ObjectTypeRegistry, ImporterRegistry, ExporterRegistry, ToolContribution, ToolId, ToolGroupId, ToolSectionId, ToolOptionsSpec, ToolOptionField, CursorSpec, CssCursorToken, GestureHandler, CanvasPointerEvent, OverlayContext, OverlayPrimitive, DeactivateReason, PanelContribution, PanelProps, PanelApi, OverlayContribution, OverlayProps, OverlayPageRect, CommandContribution, KeybindingContribution, ImporterContribution, ImportRequest, ExporterContribution, ExportResult, DockEdge, VisibilityPredicate, PagedEditor, PagedClient, ImageResourceClaim, ResourceTilesNeeded, ToolPreviewShape, ToolPreviewPolyline, ToolPreviewPath, MarqueeRectPageLocal, } from "./editor";
1
+ export type { ShellRegistries, ToolRegistry, PanelRegistry, CommandRegistry, KeybindingRegistry, OverlayRegistry, EditContextRegistry, ObjectTypeRegistry, ImporterRegistry, ExporterRegistry, ToolContribution, ToolId, ToolGroupId, ToolSectionId, ToolOptionsSpec, ToolOptionField, CursorSpec, CssCursorToken, GestureHandler, CanvasPointerEvent, OverlayContext, OverlayPrimitive, DeactivateReason, PanelContribution, PanelProps, PanelApi, OverlayContribution, OverlayProps, OverlayPageRect, CommandContribution, KeybindingContribution, ImporterContribution, ImportRequest, ExporterContribution, ExportResult, DockEdge, VisibilityPredicate, PagedEditor, PagedClient, ImageResourceClaim, ResourceTilesNeeded, ToolPreviewShape, ToolPreviewPolyline, ToolPreviewPath, ToolPreviewText, MarqueeRectPageLocal, } from "./editor";
package/dist/editor.d.ts CHANGED
@@ -363,7 +363,43 @@ export interface ToolPreviewPath {
363
363
  /** Dashed stroke instead of the default solid (preview vocabulary). */
364
364
  dashed?: boolean;
365
365
  }
366
- export type ToolPreviewShape = MarqueeRectPageLocal | ToolPreviewPolyline | ToolPreviewPath;
366
+ /**
367
+ * A text tool-preview (the overlay TEXT primitive). Closes the RFI
368
+ * gap "the overlay channel carries shapes only, no text primitive" —
369
+ * consumer: paged.draw's Measure tool readout (an on-canvas
370
+ * length/angle HUD was impossible with shapes alone); future
371
+ * consumers: the Dimension tool, crop HUDs, Ruler markers.
372
+ *
373
+ * `x`/`y` are the text anchor position in page-local pt — the SAME
374
+ * space every other preview variant uses. The host renders the label
375
+ * at a constant SCREEN size (like its page captions), so `size` is a
376
+ * screen-px font size (default host-chosen, ~11), not a document pt
377
+ * size that would scale with zoom. `anchor` is SVG `text-anchor`
378
+ * semantics relative to `x`. `background` opts into a small backing
379
+ * plate behind the label for legibility over page content.
380
+ *
381
+ * PLAIN TEXT ONLY — the host sanitizes (control characters stripped,
382
+ * no markup interpreted). Probe `supports("overlay.text@1")`.
383
+ */
384
+ export interface ToolPreviewText {
385
+ /** Explicit discriminant — the first in this vocabulary (the older
386
+ * variants discriminate structurally; new primitives get a `kind`). */
387
+ kind: "text";
388
+ pageId: PageId;
389
+ /** Text anchor x, page-local pt. */
390
+ x: number;
391
+ /** Text BASELINE y, page-local pt. */
392
+ y: number;
393
+ /** The label. Plain text; the host sanitizes + may truncate. */
394
+ text: string;
395
+ /** Font size in screen px (constant under zoom). Host default ~11. */
396
+ size?: number;
397
+ /** Horizontal anchoring relative to `x` (SVG text-anchor). Default "start". */
398
+ anchor?: "start" | "middle" | "end";
399
+ /** Render a small backing plate behind the label for legibility. */
400
+ background?: boolean;
401
+ }
402
+ export type ToolPreviewShape = MarqueeRectPageLocal | ToolPreviewPolyline | ToolPreviewPath | ToolPreviewText;
367
403
  export interface PagedClient {
368
404
  mutate(mutation: Mutation): Promise<WorkerToMain>;
369
405
  undo(): Promise<WorkerToMain>;
@@ -430,6 +466,15 @@ export interface PagedEditor {
430
466
  };
431
467
  overlaySignals: {
432
468
  setToolPreview(value: ToolPreviewShape | null): void;
469
+ /** K-9 — the MULTI-shape tool-preview sink. `undefined` when the host
470
+ * build's overlay renderer predates the multi-shape channel (older
471
+ * editor / a headless host that wires only the single slot); the
472
+ * `host.overlay.setToolPreviews` door then degrades to the first
473
+ * shape on `setToolPreview` and
474
+ * `supports("overlay.multiPreview@1")` is false. Same slot as
475
+ * `setToolPreview` — this REPLACES the slot's content with the
476
+ * list; `null` clears it. */
477
+ setToolPreviews?(value: readonly ToolPreviewShape[] | null): void;
433
478
  };
434
479
  tool: {
435
480
  setBaseTool(id: ToolId): void;
package/dist/host.d.ts CHANGED
@@ -1,8 +1,10 @@
1
- import type { CollectionName, DocumentMeta, ElementGeometryItem, ElementId, HitFilter, HitResult, Mutation, PageId, ElementProperties, PathAnchorsResult, SceneTreeNode, SelectionMode } from "./wire";
1
+ import type { CollectionName, DocumentMeta, ElementGeometryItem, ElementId, HitFilter, HitResult, Mutation, PageId, ElementProperties, PathAnchorsResult, PathAnchorTriple, SceneTreeNode, SelectionMode } from "./wire";
2
2
  import type { CommandContribution, ExporterContribution, ImporterContribution, KeybindingContribution, OverlayContribution, PagedEditor, PanelContribution, ToolContribution, ToolPreviewShape } from "./editor";
3
3
  import type { SceneLayer } from "./wire";
4
4
  import type { AssetSurface } from "./assets";
5
+ import type { BindingProvider, BindingProviderHandle } from "./binding-provider";
5
6
  import type { ClipboardSurface } from "./clipboard";
7
+ import type { MutationInput } from "./mutations";
6
8
  import type { PluginManifest } from "./manifest";
7
9
  import type { SchemaPanelContribution } from "./panel-schema";
8
10
  import type { WidgetSurface } from "./widgets";
@@ -74,7 +76,25 @@ export interface EditContextContribution {
74
76
  * names. Empty = no restriction (all tools stay available). */
75
77
  toolIds?: string[];
76
78
  /** Panel ids the cockpit emphasizes / raises on enter (the panel-set
77
- * swap). The shell opens/raises these; it does not hide others. */
79
+ * swap).
80
+ *
81
+ * These are the context's OWN panels. There is deliberately no sibling
82
+ * field naming the SHARED host panels a context serves (ADR-023), and
83
+ * the reason is worth stating because the gap looks like an omission:
84
+ * a plugin naming `paged.layers` here would be host-panel IDENTITY in
85
+ * plugin code — the exact coupling binding providers removed from the
86
+ * value lane — and it could drift from `provides`, which is the only
87
+ * declaration that decides who actually answers. The host infers
88
+ * "serves" from `BindingProviderScope` instead (DESIGN.md §18.12).
89
+ *
90
+ * WHAT THE HOST GUARANTEES, corrected 2026-08-05: it OPENS these. It
91
+ * raises them too, EXCEPT when raising would displace a panel the
92
+ * entering context's own binding providers serve — because in a dock
93
+ * that shows one panel at a time, "raise mine" IS "hide the shared
94
+ * one", and hiding it at the instant it retargets defeats the whole
95
+ * point. (This comment used to promise "it does not hide others"
96
+ * unconditionally; that was never true in such a dock.) A withheld
97
+ * raise still opens the tab, so the surface is reachable either way. */
78
98
  panelIds?: string[];
79
99
  /** Called when the context becomes active (after the stack push + the
80
100
  * scope narrowing). The element entered on is passed so the hook can
@@ -252,6 +272,35 @@ export interface ContributionSurface {
252
272
  * `contributes.exporters[]`.
253
273
  */
254
274
  exporter(contribution: ExporterContribution): Disposable;
275
+ /**
276
+ * Register a BINDING PROVIDER (ADR-023 phase A): while the edit
277
+ * context named by `contextType` is ACTIVE, this bundle resolves what
278
+ * the HOST's own panels bind to — typed property paths, named
279
+ * collections, and first refusal on structural mutation ops. Reads and
280
+ * writes both.
281
+ *
282
+ * This is the inversion the panel-duplication problem needs: instead
283
+ * of minting a fourth Layers panel, a bundle serves the ONE host
284
+ * Layers panel's values for its own content type. See
285
+ * binding-provider.ts for the three-lane shape, the core-vocabulary-
286
+ * only rule, and the precedence model.
287
+ *
288
+ * LIFETIME is borrowed, not declared: `contextType` must be a type
289
+ * this SAME bundle already registered through `contribute.editContext`
290
+ * (an unregistered type is refused loudly — a provider that can never
291
+ * activate is a bug, not a stance). The capability gate is that
292
+ * context's: `contributes.editContexts[]` must declare the type. There
293
+ * is deliberately NO separate manifest field and NO separate
294
+ * capability — the authority a provider exercises is the authority the
295
+ * active context already holds over the selection it owns.
296
+ *
297
+ * Always present. When the host wires no shared registry, the door
298
+ * warns + returns an inert handle (nothing will ever consult the
299
+ * provider) and `supports("bindings.provider@1")` is false; probe
300
+ * `supports("contribute.bindingProvider@1")` for whether this SDK has
301
+ * the door at all.
302
+ */
303
+ bindingProvider(contextType: string, provider: BindingProvider): BindingProviderHandle;
255
304
  /**
256
305
  * Open a SCENE-LAYER surface (C-1): submit vector content that renders
257
306
  * INSIDE a frame, in frame-content coordinates — core applies the
@@ -516,8 +565,75 @@ export interface RunContent {
516
565
  position?: string | null;
517
566
  tracking?: number | null;
518
567
  }
568
+ /**
569
+ * B-22 (protocol v57) — one FACE of a planar arrangement: a connected
570
+ * region of the plane whose containment signature is constant. This is
571
+ * the level BELOW element hit-testing (the areas overlapping paths
572
+ * divide the plane into), which is what a Shape Builder needs.
573
+ *
574
+ * Coordinates are in the RAW path space `pathAnchors` reports —
575
+ * per-element `itemTransform`s are NOT composed in (the arrangement runs
576
+ * on the anchors as stored, exactly like `pathfinderBoolean`). A consumer
577
+ * that draws faces on canvas maps them with the frontmost input's
578
+ * `itemTransform`, the same chain the anchor overlays use.
579
+ */
580
+ export interface PlanarFace {
581
+ /** Stable id (`"<signature>#<component>"`, e.g. `"0-1#0"`) — stable
582
+ * across calls with the same inputs, which is what lets a hovered
583
+ * face's id ride straight into the `pathfinderFaces` mutation
584
+ * (mutations.ts) without a second query. */
585
+ id: string;
586
+ /** Indices into the REQUEST's `elementIds` whose interior contains
587
+ * this face. */
588
+ signature: number[];
589
+ /** The face outline (closed). A face with holes carries them as extra
590
+ * contours. */
591
+ anchors: PathAnchorTriple[];
592
+ /** Per-contour boundaries into `anchors`. */
593
+ subpathStarts: number[];
594
+ /** Unsigned area, outer contour minus holes. */
595
+ area: number;
596
+ /** A point strictly inside the face — what a hover highlight or a fill
597
+ * drop keys off without re-deriving containment. */
598
+ inside: [number, number];
599
+ }
600
+ /**
601
+ * B-22 (protocol v57) — the full result of `document.planarRegions`.
602
+ *
603
+ * Deliberately NOT a bare face array: the door has THREE distinct
604
+ * answers a caller must be able to tell apart, and flattening them
605
+ * would make a refusal indistinguishable from "these paths divide into
606
+ * nothing":
607
+ * · `found: true` — `faces` is the answer (possibly empty: a point
608
+ * query outside every input);
609
+ * · `found: false` + `reason` — a REFUSAL (no document, an id that
610
+ * doesn't resolve, more inputs than the kernel's cap, or a host
611
+ * whose engine predates the door). Never a truncated answer;
612
+ * surface the engine's own words, never an empty face list.
613
+ * · `complete: false` — the listed faces are all REAL but do not tile
614
+ * the union of the inputs (the enumeration missed a sliver). Always
615
+ * `true` for a point query — one face is not a tiling claim.
616
+ */
617
+ export interface PlanarRegionsResult {
618
+ /** `false` ⇒ the query could not be answered at all; read `reason`. */
619
+ found: boolean;
620
+ /** Populated when `found`. With a `point` in the request this holds at
621
+ * most one face (empty ⇒ the point is outside every input). */
622
+ faces: PlanarFace[];
623
+ /** How many inputs the arrangement was built from. */
624
+ inputCount: number;
625
+ /** `true` when the resolved faces tile the union of the inputs. */
626
+ complete: boolean;
627
+ /** Why `found` is false — the engine's own words (or the host
628
+ * adapter's, when no v57 engine answered). Absent on success. */
629
+ reason?: string | null;
630
+ }
519
631
  export interface DocumentSurface {
520
- mutate(mutation: Mutation): Promise<MutationOutcome>;
632
+ /** The single write door. Accepts `MutationInput` — the vendored
633
+ * `Mutation` union PLUS the protocol-ahead ops the vendored wire
634
+ * hasn't absorbed yet (see mutations.ts). Widening an accepted
635
+ * input is additive: every `Mutation` still passes. */
636
+ mutate(mutation: MutationInput): Promise<MutationOutcome>;
521
637
  undo(): Promise<void>;
522
638
  redo(): Promise<void>;
523
639
  collection<T>(name: CollectionName): Promise<readonly T[]>;
@@ -528,6 +644,33 @@ export interface DocumentSurface {
528
644
  * against). Retires the v0 `host.editor.client.send` escape hatch
529
645
  * the draw fill panel used. `null` for an unknown element. */
530
646
  elementProperties(id: ElementId): Promise<ElementProperties | null>;
647
+ /**
648
+ * B-22 (protocol v57) — the planar-REGION read door: resolve the faces
649
+ * of the arrangement `elementIds` forms (the areas the overlapping
650
+ * paths divide the plane into, one level below element hit-testing).
651
+ * Retires the v0 `host.editor.client.send({ kind:
652
+ * "requestPlanarRegions" })` escape hatch paged.draw's Shape Builder
653
+ * used.
654
+ *
655
+ * With `point` (in the RAW path space `pathAnchors` reports) it answers
656
+ * ONLY the face under it — the hover query, which costs N
657
+ * point-in-path tests plus one region materialisation instead of a
658
+ * full enumeration. Without it, every face comes back.
659
+ *
660
+ * Pure READ, gated on `capabilities.document.read`. Face ids are stable
661
+ * for the same inputs, so a hovered id is handed straight to the
662
+ * `pathfinderFaces` mutation.
663
+ *
664
+ * Answers the FULL {@link PlanarRegionsResult} — never a bare face
665
+ * array — because a refusal (`found: false` + `reason`: input cap,
666
+ * unresolvable id, or a host whose engine predates the door) must not
667
+ * be readable as "no regions". A host that cannot answer comes back as
668
+ * a refusal with a reason — never a throw (the undeclared-capability
669
+ * gate is the one exception, as on every read door). Probe
670
+ * `supports("document.planarRegions@1")` for whether the facade
671
+ * forwards at all.
672
+ */
673
+ planarRegions(elementIds: ElementId[], point?: [number, number]): Promise<PlanarRegionsResult>;
531
674
  /** D-01 (protocol v43) — enumerate every plugin-tagged placeholder
532
675
  * field in the document, in story order. Offsets are FRESH-READ
533
676
  * addresses (a placeholder is its own tagged run; re-enumerate
@@ -538,6 +681,31 @@ export interface DocumentSurface {
538
681
  hitTest(pageId: PageId, point: [number, number], filter?: HitFilter): Promise<HitResult | null>;
539
682
  elementGeometry(ids: ElementId[]): Promise<ElementGeometryItem[]>;
540
683
  tree(): Promise<SceneTreeNode[]>;
684
+ /**
685
+ * C-16 — the per-element PARENTAGE read: the nearest ANCESTOR that is
686
+ * itself an addressable element (a group today — the only element kind
687
+ * that nests page items), or `null` when there is none.
688
+ *
689
+ * Consumer: paged.draw's `selectParentGroup` command
690
+ * (`draw-bundle/src/commands/select-parent-group.ts`), which climbs to
691
+ * the containing group on every press. Without this door it re-read
692
+ * `tree()` and walked the WHOLE scene tree per invocation — O(document)
693
+ * per keystroke, a cost its own module header records as a named gap.
694
+ *
695
+ * `null` is the honest answer for three distinct cases a caller need
696
+ * not distinguish: the element is top-level (its container is a Page /
697
+ * Spread, which is NOT an element and never a selection target), the
698
+ * id does not resolve, or the address is a story/table range (those
699
+ * never nest in groups).
700
+ *
701
+ * A pure READ gated on `capabilities.document.read`, resolved from the
702
+ * SAME scene-tree the `tree()` door reads — no extra engine query and
703
+ * no wire op of its own (DESIGN.md §4.3d). The host keeps a parent
704
+ * index and rebuilds it when the document changes, so the O(document)
705
+ * walk happens once per EDIT rather than once per press. It is
706
+ * therefore exactly as fresh as `tree()` and no fresher.
707
+ */
708
+ parentOf(id: ElementId): Promise<ElementId | null>;
541
709
  /** Read a text frame's thread topology (protocol v38, C-2/S-05) — the
542
710
  * ordered chain of frames a story flows through, tail-overflow flagged.
543
711
  * Empty when the story has no frame or no document is loaded. A
@@ -629,6 +797,23 @@ export interface TextMetrics {
629
797
  /** Face descender at `sizePt` (negative below the baseline). */
630
798
  descender: number;
631
799
  }
800
+ /**
801
+ * C-9 — the user's text caret: the story + insertion offset a
802
+ * text-inserting plugin should target (paged.data first-insert
803
+ * placement — without this door a freshly-placed variable field lands
804
+ * at story start, offset 0).
805
+ *
806
+ * `offset` is a story-local content offset in the SAME convention the
807
+ * engine's text mutations consume (`insertText.offset`,
808
+ * `deleteRange.start/end` — the `ContentSelection` addressing: run
809
+ * bytes plus one synthetic `\n` per inter-paragraph boundary), so the
810
+ * value can be passed straight to `host.document.mutate`.
811
+ */
812
+ export interface TextCaret {
813
+ storyId: string;
814
+ /** Insertion offset (see the offset-convention note above). */
815
+ offset: number;
816
+ }
632
817
  /**
633
818
  * Text measurement against the loaded document's fonts (S-13). A read
634
819
  * door — no capability gate (like {@link ViewportSurface}); it wraps the
@@ -640,12 +825,51 @@ export interface TextMetrics {
640
825
  */
641
826
  export interface TextSurface {
642
827
  measureString(family: string, style: string | null, text: string, sizePt: number): Promise<TextMetrics>;
828
+ /**
829
+ * C-9 — read the user's text caret (the active `ContentSelection`),
830
+ * or `null` when no text caret is active OR the host injects no
831
+ * caret reader (probe `supports("text.caret@1")` to tell the two
832
+ * apart). For a collapsed selection this is the caret offset; for a
833
+ * RANGE selection it answers the range START (where a replace
834
+ * inserts). Honest v1 gap: a caret inside a TABLE CELL
835
+ * (cell-qualified selection) answers `null` — cell-local offsets
836
+ * would be misread as story-local body offsets.
837
+ */
838
+ caret(): TextCaret | null;
643
839
  }
644
840
  /** The v0 overlay channel: the shared tool-preview signal (polyline /
645
- * rect). Retained plugin scene layers are the P2 channel — reserved,
646
- * not faked. */
841
+ * rect / path — and, with `supports("overlay.text@1")`, the TEXT
842
+ * primitive `ToolPreviewText` for on-canvas readouts). Retained
843
+ * plugin scene layers are the P2 channel — reserved, not faked. */
647
844
  export interface OverlaySurface {
648
845
  setToolPreview(shape: ToolPreviewShape | null): void;
846
+ /**
847
+ * K-9 — publish MANY preview shapes at once (DESIGN.md §4.5a). The
848
+ * single-slot `setToolPreview` is last-write-wins, so a tool could
849
+ * show geometry OR a label, never both: paged.draw's **Measure** trades
850
+ * the frozen line for the frozen readout at pointer-up, and its region
851
+ * **Shape Builder** can highlight one hovered face but not shade the
852
+ * whole collected set. Both trades are named in that bundle's code;
853
+ * this door retires them.
854
+ *
855
+ * ONE SLOT, two writers: this REPLACES whatever the tool-preview slot
856
+ * holds with `shapes` (rendered in array order, first = bottom-most);
857
+ * `null` — or an empty array — clears it, exactly like
858
+ * `setToolPreview(null)`. It is not a second overlay layer (that would
859
+ * resurrect z-ordering and double the teardown paths).
860
+ *
861
+ * Shapes may address DIFFERENT pages: each carries its own `pageId`
862
+ * and the host resolves the page rect per shape.
863
+ *
864
+ * Same `capabilities.rendering` ∋ `"overlay"` gate as
865
+ * `setToolPreview` — one channel, one gate. Probe
866
+ * `supports("overlay.multiPreview@1")`: it is DYNAMIC (unlike the
867
+ * static `overlay.text@1`), true only when the host wired a real
868
+ * multi-shape sink. When it is false the door still works and never
869
+ * throws — it forwards the FIRST shape through the single slot, which
870
+ * is precisely the pre-K-9 behaviour a bundle used to hand-code.
871
+ */
872
+ setToolPreviews(shapes: readonly ToolPreviewShape[] | null): void;
649
873
  }
650
874
  /**
651
875
  * Shell actions the HOST APP injects at `loadBundle` time (the
@@ -653,6 +877,11 @@ export interface OverlaySurface {
653
877
  * function over the editor handle). When the host app provides no
654
878
  * implementation, calls warn and no-op — probe with
655
879
  * `host.supports("shell.openPanel@1")`.
880
+ *
881
+ * The two FILE doors are a pair: `pickFile` reads bytes IN (K-5 / S-11),
882
+ * `saveFile` hands bytes OUT (K-10). Both are byte-level by design —
883
+ * no DOM `File`/`Blob` ever crosses the contract — and both answer an
884
+ * honest "nothing happened" value rather than throwing.
656
885
  */
657
886
  export interface ShellSurface {
658
887
  /** Open a REGISTERED panel as the active dock tab (the
@@ -665,6 +894,42 @@ export interface ShellSurface {
665
894
  * the user cancels OR no picker is wired (the honest no-picker door);
666
895
  * probe `host.supports("shell.pickFile@1")` for the latter. */
667
896
  pickFile(options?: FilePickerOptions): Promise<readonly PickedFile[]>;
897
+ /**
898
+ * K-10 — the WRITE half of the picker door: hand the host bytes to
899
+ * deliver to the user's filesystem under `suggestedName` (DESIGN.md
900
+ * §4.5c). The mirror of `pickFile`: bytes cross, never a DOM `File`
901
+ * or a `Blob`, so a bundle stays isolate-ready.
902
+ *
903
+ * Consumer: **paged.image**, which can compute an adjusted
904
+ * PSD/PNG/JPEG but — with `pickFile` READ-only — could only deliver it
905
+ * through the Export Center's exporter registry, so "Save adjusted
906
+ * copy…" was unofferable from the bundle's own panel.
907
+ *
908
+ * Answers `true` when the host ACCEPTED the bytes and handed them to
909
+ * its save path, `false` when it did not — no saver wired (probe
910
+ * `supports("shell.saveFile@1")` to know that up front), the user
911
+ * declined, or the host refused. Never throws: a refused save is a
912
+ * result, like a refused mutation. Honest ceiling: a host backed by
913
+ * the browser's anchor-download cannot observe a user cancel, so
914
+ * `true` means "delivered to the browser's download path", not "a file
915
+ * exists on disk"; a File System Access backing can answer a real
916
+ * `false`.
917
+ */
918
+ saveFile(options: SaveFileOptions): Promise<boolean>;
919
+ }
920
+ /** What `ShellSurface.saveFile` delivers (K-10) — the inverse of
921
+ * {@link PickedFile}, field-for-field, so bytes picked from disk can be
922
+ * handed straight back after an edit. */
923
+ export interface SaveFileOptions {
924
+ /** File name incl. extension the host proposes to the user (it may
925
+ * sanitize or de-duplicate it — the name is a suggestion, not a
926
+ * path; a bundle can never target a location). */
927
+ suggestedName: string;
928
+ /** The bytes to write. */
929
+ bytes: Uint8Array;
930
+ /** MIME type for the saved file (same field name as `PickedFile`).
931
+ * Absent ⇒ the host uses `application/octet-stream`. */
932
+ mimeType?: string;
668
933
  }
669
934
  /** Filter + multiplicity for `ShellSurface.pickFile` (K-5 / S-11). */
670
935
  export interface FilePickerOptions {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export type { PluginId, PluginManifest, PluginCapabilities, PluginContributions, NetworkCapability, DataProvidersCapability, StorageCapability, WasmArtifact, WasmPurpose, WorkersCapability, SecretsCapability, GpuCapability, } from "./manifest";
2
2
  export type { BundleHandle, PagedBundle } from "./bundle";
3
- export type { BundleHost, ContributionSurface, SceneLayerSurface, ImagesSurface, ImageResourceClaimOptions, TileBytes, WorkersSurface, BundleWorker, SpawnWorkerOptions, SecretsSurface, SecretMaterial, DocumentSurface, SelectionSurface, ViewportSurface, TextSurface, TextMetrics, FrameChainLink, StoryContent, ParagraphContent, RunContent, OverlaySurface, ShellSurface, FilePickerOptions, PickedFile, StorageSurface, BlobSurface, BlobUsage, PartsSurface, NativeDocumentSurface, NetworkSurface, ConsentResult, DataProvidersSurface, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, ProviderSchema, ProviderField, ProviderRecordSet, DiagnosticsSurface, BindingsSurface, Diagnostic, DocumentChangeEvent, MutationOutcome, Disposable, PluginLogger, EditContextContribution, ObjectTypeContribution, EditContextCandidate, EnteredEditContext, ContentPointerEvent, EditContextDescriptor, ObjectTypeDescriptor, PluginMetadataEnvelope, ObjectTypeBaker, BakeContext, } from "./host";
4
- export type { PanelSchema, PanelSchemaSection, PanelSchemaRow, SchemaPanelContribution, SchemaPanelRenderer, SchemaPanelRendererProps, WidgetValueBinding, BindingRef, SchemaGate, } from "./panel-schema";
3
+ export type { BundleHost, ContributionSurface, SceneLayerSurface, ImagesSurface, ImageResourceClaimOptions, TileBytes, WorkersSurface, BundleWorker, SpawnWorkerOptions, SecretsSurface, SecretMaterial, DocumentSurface, SelectionSurface, ViewportSurface, TextSurface, TextMetrics, TextCaret, FrameChainLink, PlanarFace, PlanarRegionsResult, StoryContent, ParagraphContent, RunContent, OverlaySurface, ShellSurface, FilePickerOptions, PickedFile, SaveFileOptions, StorageSurface, BlobSurface, BlobUsage, PartsSurface, NativeDocumentSurface, NetworkSurface, ConsentResult, DataProvidersSurface, DataProviderRegistration, DataProviderHandle, DataProviderInfo, DataProviderSnapshot, ProviderSchema, ProviderField, ProviderRecordSet, DiagnosticsSurface, BindingsSurface, Diagnostic, DocumentChangeEvent, MutationOutcome, Disposable, PluginLogger, EditContextContribution, ObjectTypeContribution, EditContextCandidate, EnteredEditContext, ContentPointerEvent, EditContextDescriptor, ObjectTypeDescriptor, PluginMetadataEnvelope, ObjectTypeBaker, BakeContext, } from "./host";
4
+ export type { PanelSchema, PanelSchemaSection, PanelSchemaRow, SchemaPanelContribution, SchemaPanelRenderer, SchemaPanelRendererProps, WidgetValueBinding, BindingRef, SchemaGate, WidgetCollectionBinding, SchemaRowAction, SchemaListAction, SchemaListSpec, } from "./panel-schema";
5
5
  export type { WidgetSurface, CodeEditorProps, CodeEditorDiagnostic, CodeEditorLanguage, } from "./widgets";
6
+ export type { BindingTarget, BindingPropertyRequest, BindingPropertyWrite, BindingCollectionRequest, BindingRead, BindingResolved, BindingDecline, BindingWrite, BindingCollection, BindingOp, BindingProvider, BindingProviderScope, BindingProviderHandle, BindingReadResult, BindingWriteResult, BindingCollectionResult, ActiveBindingProvider, } from "./binding-provider";
6
7
  export type { AssetSurface, AssetKind, FontFaceAsset, FontFaceFormat, } from "./assets";
7
8
  export type { ProviderTileWire } from "./wire";
8
9
  export type { ClipboardSurface, ClipboardPayload, TabularClipboard, } from "./clipboard";
@@ -1 +1,275 @@
1
+ import type { ElementId, Mutation } from "./wire";
1
2
  export type { ElementId, PageId, NodeId, NodeSpec, Mutation, Operation, PropertyPath, Value, PathAnchorSpec, PathAnchorTriple, PathAnchorsResult, PathPointAddress, PathPointRole, PathfinderKind, HitFilter, HitResult, CollectionName, DocumentMeta, ElementGeometryItem, SceneTreeNode, SelectionMode, ContentSelection, MainToWorker, MainToWorkerKind, WorkerToMain, SceneLayer, SceneItem, ScenePathSeg, ScenePaint, GestureType, GestureHandle, GestureModifiers, SwatchSpec, GradientSpec, GradientStopSpec, SwatchSummary, GradientSummary, LayerSummary, } from "./wire";
3
+ /** v56 (Wave B) — close an OPEN subpath of a path element: the inverse
4
+ * gesture of `pathOpenAt`'s scissors cut. `subpath` picks the contour
5
+ * by index; omit for the default (the single/last open subpath).
6
+ * Coincident endpoints — the duplicated pair a cut leaves — merge back
7
+ * into one anchor; endpoints apart gain the implicit closing edge. */
8
+ export type ClosePathMutation = {
9
+ op: "closePath";
10
+ args: {
11
+ elementId: ElementId;
12
+ subpath?: number | null;
13
+ };
14
+ };
15
+ /** v56 (Wave B) — weld two OPEN single-contour path elements into one
16
+ * (InDesign's Join): connect the NEAREST endpoints of `elementId` and
17
+ * `otherId`, then delete `otherId`. Both endpoint pairs coincident
18
+ * closes the result into a ring. Non-path / closed / multi-contour
19
+ * inputs are rejected with an honest error (no-op). One undo step
20
+ * restores BOTH elements. */
21
+ export type JoinPathsMutation = {
22
+ op: "joinPaths";
23
+ args: {
24
+ elementId: ElementId;
25
+ otherId: ElementId;
26
+ };
27
+ };
28
+ /** v56 (B-18) — InDesign paste-into: nest an existing TOP-LEVEL page
29
+ * item inside a container Rectangle / Oval / Polygon. The child keeps
30
+ * its document-space geometry (nothing moves on canvas) and renders
31
+ * clipped by the container's outline. Grouped / already-nested
32
+ * children and non-container hosts are rejected with an honest error.
33
+ * One undo pops the child back to its exact stacking slot. */
34
+ export type PasteIntoMutation = {
35
+ op: "pasteInto";
36
+ args: {
37
+ containerId: ElementId;
38
+ childId: ElementId;
39
+ };
40
+ };
41
+ /** v56 (B-18) — the inverse gesture: pop a pasted-in child back to top
42
+ * level (stacks on top), world transform preserved. One undo re-nests
43
+ * it at the same child index. */
44
+ export type ReleaseFromMutation = {
45
+ op: "releaseFrom";
46
+ args: {
47
+ childId: ElementId;
48
+ };
49
+ };
50
+ /** v57 (B-22) — every face of the arrangement becomes its own object,
51
+ * keeping the attributes of the topmost input covering it. */
52
+ export type PathfinderDivideMutation = {
53
+ op: "pathfinderDivide";
54
+ args: {
55
+ elementIds: ElementId[];
56
+ };
57
+ };
58
+ /** v57 (B-22) — each input is clipped to the part nothing above it
59
+ * covers, and loses its stroke. Inputs entirely hidden are deleted. */
60
+ export type PathfinderTrimMutation = {
61
+ op: "pathfinderTrim";
62
+ args: {
63
+ elementIds: ElementId[];
64
+ };
65
+ };
66
+ /** v57 (B-22) — Trim, then coalesce: inputs that share a fill colour
67
+ * merge into one object. */
68
+ export type PathfinderMergeMutation = {
69
+ op: "pathfinderMerge";
70
+ args: {
71
+ elementIds: ElementId[];
72
+ };
73
+ };
74
+ /** v57 (B-22) — keep only what falls inside the TOPMOST input, coloured
75
+ * by the objects beneath it; the topmost input is consumed as the
76
+ * cookie cutter and disappears. */
77
+ export type PathfinderCropMutation = {
78
+ op: "pathfinderCrop";
79
+ args: {
80
+ elementIds: ElementId[];
81
+ };
82
+ };
83
+ /** v57 (B-22) — fills become strokes: every arrangement edge (split at
84
+ * each crossing) becomes an open `GraphicLine` stroked with the fill of
85
+ * the input that contributed it. All inputs are consumed. */
86
+ export type PathfinderOutlineMutation = {
87
+ op: "pathfinderOutline";
88
+ args: {
89
+ elementIds: ElementId[];
90
+ };
91
+ };
92
+ /** v57 (B-22) — the BACKMOST object minus every object in front of it. */
93
+ export type PathfinderMinusBackMutation = {
94
+ op: "pathfinderMinusBack";
95
+ args: {
96
+ elementIds: ElementId[];
97
+ };
98
+ };
99
+ /** v57 (B-22) — whether `pathfinderFaces` KEEPS the named faces or
100
+ * REMOVES them (Shape Builder's drag vs alt-drag). */
101
+ export type FaceSelectMode = "keep" | "remove";
102
+ /** v57 (B-22) — Shape Builder's click/drag output: unite the named faces
103
+ * of the arrangement into one result element. `faces` carries the
104
+ * STABLE ids `host.document.planarRegions` reported for the SAME
105
+ * `elementIds` (ids are only stable per input set); `mode` picks
106
+ * whether they are the faces kept or the faces removed. An unknown id
107
+ * is REFUSED, not ignored — a stale face set fails loudly. */
108
+ export type PathfinderFacesMutation = {
109
+ op: "pathfinderFaces";
110
+ args: {
111
+ elementIds: ElementId[];
112
+ faces: string[];
113
+ mode: FaceSelectMode;
114
+ };
115
+ };
116
+ /** v58 (C-28) — how the mask artwork's coverage is read. `luminosity`
117
+ * is Illustrator's default and PDF's `/S /Luminosity`; `alpha` reads
118
+ * the artwork's alpha channel instead. Unpainted area means HIDDEN for
119
+ * both (PDF's black backdrop). Omitted = `luminosity`. */
120
+ export type OpacityMaskType = "luminosity" | "alpha";
121
+ /** v58 (C-28) — **Make Opacity Mask**: `maskId`'s artwork becomes a soft
122
+ * mask over `targetId`. The mask item leaves the page's z-order (its
123
+ * slot is captured) and one undo pops it back exactly.
124
+ *
125
+ * RENDERS EVERYWHERE. An earlier revision of this comment warned that
126
+ * the mask was honoured by the CPU rasterizer and PDF export but NOT by
127
+ * the Vello/WebGPU backend the editor canvas uses, so a plugin must not
128
+ * imply WYSIWYG. **That is no longer true, and it was never as
129
+ * fundamental as it read**: the claim rested on the belief that Vello
130
+ * lacked a coverage-buffer API, when the pinned version has
131
+ * `Scene::push_luminance_mask_layer` (an unrelated 0.3.0 entry in
132
+ * `Cargo.lock`, belonging to a spike crate, was misread as the resolved
133
+ * version). Alpha masks need no mask layer at all — `Compose::DestIn`
134
+ * IS `dst · src.a`.
135
+ *
136
+ * Vello output is now byte-identical to the CPU rasterizer across
137
+ * luminosity, alpha and both inverted forms. Safe to build WYSIWYG UI
138
+ * over. */
139
+ export type ApplyOpacityMaskMutation = {
140
+ op: "applyOpacityMask";
141
+ args: {
142
+ targetId: ElementId;
143
+ maskId: ElementId;
144
+ maskType?: OpacityMaskType | null;
145
+ invert?: boolean | null;
146
+ };
147
+ };
148
+ /** v58 (C-28) — drop the mask relation; the artwork returns to top level
149
+ * with its geometry untouched. One undo re-applies the mask with its
150
+ * original mode/invert at the same z slot. */
151
+ export type ReleaseOpacityMaskMutation = {
152
+ op: "releaseOpacityMask";
153
+ args: {
154
+ targetId: ElementId;
155
+ };
156
+ };
157
+ /** v58 (C-29) — **Type on a Path**: flow an existing story along an
158
+ * existing path element. The engine could already RENDER type on a path
159
+ * loaded from an IDML; until v58 nothing could create one.
160
+ *
161
+ * Hosts are Rectangle / GraphicLine / Polygon. A TextFrame is refused
162
+ * WITH a reason: its glyphs are emitted by the story pass, so accepting
163
+ * it would produce a visible lie. `pathTypeAlignment` (Baseline /
164
+ * Center honoured), `flipPathEffect` and the `startBracket`/`endBracket`
165
+ * range are all live. `PathEffect` is deliberately NOT exposed — only
166
+ * `RainbowPathEffect` actually renders. */
167
+ export type AttachTextToPathMutation = {
168
+ op: "attachTextToPath";
169
+ args: {
170
+ elementId: ElementId;
171
+ storyId: string;
172
+ pathTypeAlignment?: string | null;
173
+ flipPathEffect?: string | null;
174
+ startBracket?: number | null;
175
+ endBracket?: number | null;
176
+ };
177
+ };
178
+ /** v58 (C-29) — unlink the text from the path; the exact inverse. */
179
+ export type DetachTextFromPathMutation = {
180
+ op: "detachTextFromPath";
181
+ args: {
182
+ elementId: ElementId;
183
+ };
184
+ };
185
+ /** v59 (Arrange) — where `reorderElement` puts its target inside the
186
+ * sibling list it already belongs to. `0` is the BACKMOST slot (painted
187
+ * first). Prefer the four verbs: they are evaluated against the order
188
+ * the engine holds at apply time, so a concurrent insert or delete
189
+ * cannot make them restack the wrong item. `{ index }` is absolute and
190
+ * therefore CAN go stale — an out-of-range index is refused with an
191
+ * honest error, never clamped. */
192
+ export type ZOrderTarget = "front" | "back" | "forward" | "backward" | {
193
+ index: number;
194
+ };
195
+ /** v59 (Arrange) — **the z-order door**: restack an element within the
196
+ * sibling list it already belongs to. Nothing on the wire could change
197
+ * stacking before v59, which is why so many bundles carry "inserted
198
+ * items land on top of the z-order" as an accepted limit — that
199
+ * sentence is retired by this op.
200
+ *
201
+ * The sibling list is DERIVED from where the element already is: a
202
+ * top-level item restacks in the spread's z table, a group member
203
+ * inside its group, a pasted-in child inside its container. There is no
204
+ * parent argument, so a reorder can never move an element between those
205
+ * scopes — for that use `createGroup` / `dissolveGroup` /
206
+ * `pasteInto` / `releaseFrom`. An opacity-mask artwork is painted from
207
+ * no list at all and is refused. One undo restores the previous order
208
+ * exactly (the engine's inverse carries the slot the item came from).
209
+ *
210
+ * CREATE-THEN-ARRANGE in one undo step — the Live Paint shape (fill a
211
+ * face, then put the fill UNDER the strokes bounding it) — is a batch
212
+ * of `[insert…, bindCreated { handle }, reorderElement
213
+ * { elementId: "$h:<handle>", to: "back" }]`. The `bindCreated` is
214
+ * required: the bare `$created` sentinel is understood by
215
+ * `setPluginMetadata` / `setElementProperty` only, while the generic
216
+ * handle resolver — the one that rewrites any id position of any op —
217
+ * is armed by a `bindCreated` child being present.
218
+ *
219
+ * HONEST LIMIT 1 — Arrange is WITHIN a layer. The renderer sorts by
220
+ * `ItemLayer` before it paints, so bring-to-front cannot lift an
221
+ * element above one on a higher layer. That is InDesign's model:
222
+ * crossing layers is a property write, not an Arrange.
223
+ *
224
+ * HONEST LIMIT 2 — the new order shows on canvas and survives a
225
+ * `.paged` save, but NOT an `.idml` export: the IDML writer re-emits
226
+ * existing source elements byte-for-byte and only places new ones, so a
227
+ * reorder reverts on an export/reopen round trip. Do not present
228
+ * Arrange as durable across IDML interchange yet. */
229
+ export type ReorderElementMutation = {
230
+ op: "reorderElement";
231
+ args: {
232
+ elementId: ElementId;
233
+ to: ZOrderTarget;
234
+ };
235
+ };
236
+ /** The protocol-ahead ops, as one union — the delta between the
237
+ * vendored `Mutation` (protocol 51) and core's protocol 59. Empties
238
+ * itself on the next `sync-wire.mjs` run. */
239
+ /** v57 (C-15) — name the id a creating sibling is about to mint, so a
240
+ * LATER child of the same batch can address it as `"$h:<handle>"`.
241
+ *
242
+ * This is what collapses a two-batch flow into ONE undo step. Before it,
243
+ * a bundle that inserted geometry and then painted or grouped it had to
244
+ * issue two batches, because a batch op could not reference an id minted
245
+ * in the same batch — paged.draw's appearance bake, pattern bake and
246
+ * re-plan, compound-path release, symbol place, image trace and
247
+ * live-paint fill all pay two undo steps for exactly that reason.
248
+ *
249
+ * Three rules, each of which was measured against the engine rather than
250
+ * assumed, because getting any of them wrong fails in a confusing way:
251
+ *
252
+ * 1. **Order matters.** The bind must come AFTER the creating child.
253
+ * Placed before, the batch is refused BY NAME (an honest error, not a
254
+ * silent no-op).
255
+ * 2. **It is its own op, not a field.** Passing a handle inside a
256
+ * creating op's own `args` is SILENTLY IGNORED — which is what made
257
+ * an early probe read as "the engine doesn't support this".
258
+ * 3. **Scope is the declaring batch**, visible to its own later children
259
+ * including nested batches, never outward. A `$h:` appearing in TEXT
260
+ * content is content and is never rewritten.
261
+ *
262
+ * Generalises the v34 `$created` sentinel — which only two mutation
263
+ * kinds understood — to any number of live names addressable from any
264
+ * mutation kind. */
265
+ export type BindCreatedMutation = {
266
+ op: "bindCreated";
267
+ args: {
268
+ handle: string;
269
+ };
270
+ };
271
+ export type PendingMutation = BindCreatedMutation | ClosePathMutation | JoinPathsMutation | PasteIntoMutation | ReleaseFromMutation | PathfinderDivideMutation | PathfinderTrimMutation | PathfinderMergeMutation | PathfinderCropMutation | PathfinderOutlineMutation | PathfinderMinusBackMutation | PathfinderFacesMutation | ApplyOpacityMaskMutation | ReleaseOpacityMaskMutation | AttachTextToPathMutation | DetachTextFromPathMutation | ReorderElementMutation;
272
+ /** What `host.document.mutate` ACCEPTS: the vendored op union plus the
273
+ * protocol-ahead ops. Widening an accepted-input type is additive —
274
+ * every existing `Mutation` call site still compiles. */
275
+ export type MutationInput = Mutation | PendingMutation;
@@ -25,6 +25,71 @@ export interface BindingRef {
25
25
  * (and a missing name reads as `false` — a visible seam, never a
26
26
  * throw). */
27
27
  export type SchemaGate = boolean | BindingRef;
28
+ /** Where a `list` row's items come from. Two sources, mirroring the
29
+ * two live-collection lanes the host already has:
30
+ * · `documentCollection` — a named engine collection (the same
31
+ * `host.document.collection(name)` lane the editor's own panels
32
+ * read);
33
+ * · `binding` — an ARRAY the plugin publishes through
34
+ * `host.bindings.publish(name, rows)` (the §12.2 published-
35
+ * bindings door, now carrying rows instead of a boolean). */
36
+ export type WidgetCollectionBinding = {
37
+ kind: "documentCollection";
38
+ collection: string;
39
+ } | {
40
+ kind: "binding";
41
+ bind: string;
42
+ };
43
+ /** G3 — a schema row ACTION. Either dispatches a registered command
44
+ * with the row id as payload, or the `applyEntity` write kind: apply
45
+ * the row's entity id (style / swatch self-id) to the current
46
+ * selection through the SAME `setElementProperty` mutation channel the
47
+ * scalar widgets commit on. `valueType` picks the wire payload
48
+ * (`text` for applied-style paths, `colorRef` for swatch/gradient
49
+ * paths) — the collection-select leaf's convention. Note the ceiling
50
+ * holds: an action writes ONE typed PropertyPath, it does not compute. */
51
+ export type SchemaRowAction = {
52
+ kind: "command";
53
+ command: string;
54
+ } | {
55
+ kind: "applyEntity";
56
+ /** Selection surface to write to (defaults to `"element"`). */
57
+ scope?: "element" | "content";
58
+ /** The applied-entity PropertyPath (e.g. `frameFillColor`,
59
+ * `appliedParagraphStyle`). */
60
+ path: PropertyPath;
61
+ /** Wire payload variant; defaults to `"text"`. */
62
+ valueType?: "text" | "colorRef";
63
+ };
64
+ /** One per-row action button on a `list` widget. */
65
+ export interface SchemaListAction {
66
+ /** Button label (sentence case, no emoji — brand content rules). */
67
+ label: string;
68
+ action: SchemaRowAction;
69
+ /** Gate the button on a published binding (absent = enabled;
70
+ * `applyEntity` actions additionally disable while the target
71
+ * selection is empty — the honest no-write-path rule). */
72
+ enabled?: SchemaGate;
73
+ }
74
+ /** The `list` widget spec (widget id `paged.list`). Renders rows from a
75
+ * collection binding; publishes the clicked row's id back through
76
+ * `selectionBinding` so other rows/sections can gate on it — which is
77
+ * how a list participates in §12.2 without a DSL. */
78
+ export interface SchemaListSpec {
79
+ items: WidgetCollectionBinding;
80
+ /** Dot-path into a row object for the primary label ("name"). */
81
+ labelField: string;
82
+ /** Optional secondary line (mono), e.g. "kind". */
83
+ secondaryField?: string;
84
+ /** Dot-path carrying the row's stable id; defaults to "selfId" (the
85
+ * summary-shape convention every document collection uses). */
86
+ idField?: string;
87
+ /** Published binding name that receives the selected row id on click
88
+ * (string). Absent = the list keeps private selection. */
89
+ selectionBinding?: string;
90
+ /** Per-row action buttons. */
91
+ actions?: SchemaListAction[];
92
+ }
28
93
  export interface PanelSchemaRow {
29
94
  /** Catalog widget id (a curated primitive-leaf id the host
30
95
  * registered). An unknown id renders a visible "unknown widget"
@@ -38,6 +103,9 @@ export interface PanelSchemaRow {
38
103
  * (`literal | selectionProperty`). Absent = a layout-only / display
39
104
  * leaf (label, section). */
40
105
  value?: WidgetValueBinding;
106
+ /** ADDITIVE (schema v1.1) — present iff `widget` is the list widget
107
+ * (`paged.list`). Scalar rows ignore it; a v1 schema never sets it. */
108
+ list?: SchemaListSpec;
41
109
  /** Show the row only when this gate is truthy. Absent = always. */
42
110
  visible?: SchemaGate;
43
111
  /** Enable the row's control only when this gate is truthy. Absent =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paged-media/plugin-api",
3
- "version": "0.2.27-canary.1",
3
+ "version": "0.2.28-canary.0",
4
4
  "description": "The Paged plugin contract: manifest, bundle lifecycle, the BundleHost surface, and the contribution + engine wire types. Type-only.",
5
5
  "license": "MPL-2.0 OR LicenseRef-PMEL",
6
6
  "type": "module",