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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;