@triiiceratops/plugin-annotation-editor 1.0.0-rc.6 → 1.0.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/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # @triiiceratops/plugin-annotation-editor
2
+
3
+ Annotation editing for the [Triiiceratops](https://triiiceratops.org/)
4
+ IIIF viewer.
5
+
6
+ Core renders and selects the annotations a manifest publishes; it writes none.
7
+ This plugin adds the writing half: a **drawing layer** over the image with
8
+ rectangle, ellipse, polygon, point and whole-canvas tools, a panel with a body
9
+ editor and persistence-aware undo/redo, and an `AnnotationStorageAdapter` seam so
10
+ annotations persist wherever your institution keeps them. Every tool is operable
11
+ from the keyboard, for creation as well as editing.
12
+
13
+ The drawing layer is built on core's own published primitives — an overlay layer
14
+ for its DOM, `canvasToScreen`/`screenToCanvas` for projection, `subscribeFrame`
15
+ for reprojection — so no third party's object model sits between the editor and
16
+ the viewer. Annotations are persisted as W3C Web Annotations targeting canvas
17
+ coordinates: `FragmentSelector` (`xywh=`) for a rectangle, `SvgSelector` with a
18
+ `<polygon>` for an ellipse or a polygon, `PointSelector` for a point, and no
19
+ selector at all for a whole-canvas note.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pnpm add @triiiceratops/plugin-annotation-editor
25
+ ```
26
+
27
+ `triiiceratops`, `@triiiceratops/plugin-sdk` and `svelte` are peers. The plugin
28
+ declares a core floor (`coreRange: '>=1.0.0-rc.36'`) — the first core that ships
29
+ overlay-layer registration — and refuses to activate against anything older,
30
+ loudly, on core's plugin-error channel rather than mounting a button that does
31
+ nothing.
32
+
33
+ ## Registering it
34
+
35
+ ### As a module (any bundler)
36
+
37
+ ```ts
38
+ import {
39
+ AnnotationEditorPlugin,
40
+ createAnnotationEditorPlugin,
41
+ LocalStorageAdapter,
42
+ } from '@triiiceratops/plugin-annotation-editor';
43
+ ```
44
+
45
+ `AnnotationEditorPlugin` is the preconfigured plugin: every tool, rectangle
46
+ armed by default, and the built-in `LocalStorageAdapter`. Use
47
+ `createAnnotationEditorPlugin(config)` for anything else. Hand either to the
48
+ viewer the way you hand it any other plugin — the `plugins` prop in Svelte,
49
+ React and Vue, or the `.plugins` **property** (never an attribute) on the
50
+ `<triiiceratops-viewer>` custom element:
51
+
52
+ ```ts
53
+ const annotations = createAnnotationEditorPlugin({
54
+ adapter: new LocalStorageAdapter(),
55
+ user: { id: 'user-123', name: 'Jane Doe' },
56
+ });
57
+
58
+ document.querySelector('triiiceratops-viewer').plugins = [annotations];
59
+ ```
60
+
61
+ ### As a script tag (IIFE)
62
+
63
+ ```html
64
+ <script src="/assets/triiiceratops-element.iife.js"></script>
65
+ <script src="/assets/plugin-annotation-editor/iife.js"></script>
66
+
67
+ <triiiceratops-viewer id="viewer"></triiiceratops-viewer>
68
+ <script>
69
+ // Loading the script only registers a factory; activation is per-viewer.
70
+ document.getElementById('viewer').plugins = [
71
+ window.Triiiceratops.plugins.get(
72
+ '@triiiceratops/plugin-annotation-editor',
73
+ ),
74
+ ];
75
+ </script>
76
+ ```
77
+
78
+ Either script order works — the registry is bootstrapped order-independently —
79
+ and this bundle carries its own Svelte runtime, so it shares nothing private
80
+ with core. The IIFE path uses the built-in `LocalStorageAdapter`; a custom
81
+ adapter needs the module entry.
82
+
83
+ ## Drawing
84
+
85
+ Drawing is **modal**: the reader arms a tool from the panel, and for as long as
86
+ it is armed the drawing layer takes pointer events across the whole image, so a
87
+ drag draws instead of panning. One tool, one gesture — nothing ever has to guess
88
+ whether a press was a click or a drag.
89
+
90
+ | Tool | Gesture | Persisted target |
91
+ | ------------ | ---------------------------------------------- | ------------------------- |
92
+ | Rectangle | drag a bounding box | `FragmentSelector` |
93
+ | Ellipse | drag a bounding box | `SvgSelector` `<polygon>` |
94
+ | Polygon | click per vertex; Enter or double-click closes | `SvgSelector` `<polygon>` |
95
+ | Point | single click | `PointSelector` |
96
+ | Whole canvas | activate the tool | no selector |
97
+
98
+ A region smaller than a few canvas pixels in either dimension is discarded
99
+ rather than committed, so hand jitter does not litter an annotation set with
100
+ two-pixel shapes.
101
+
102
+ An **ellipse is a creation affordance only**. It persists as a 64-point polygon
103
+ inscribed in the dragged box, and edits afterwards as the polygon it is —
104
+ nothing records that it was once an ellipse. This is deliberate; see
105
+ [ADR 0022](../../docs/adr/0022-an-ellipse-persists-as-a-polygon.md).
106
+
107
+ What still works while a tool is armed:
108
+
109
+ - **Wheel zoom**, unchanged — place a vertex precisely without disarming.
110
+ - **Keyboard zoom and arrow-key panning**, unchanged.
111
+ - **Hold Space to pan by dragging.** The layer drops its own pointer events for
112
+ as long as Space is held and the viewer's ordinary panning takes over
113
+ underneath; a Space-pan never commits a shape.
114
+
115
+ Pointer-drag panning is the one thing arming suppresses, and Space is its escape
116
+ hatch.
117
+
118
+ A tool is armed only while the panel that explains it is on screen. There are
119
+ two exits and both cancel rather than commit: Escape, and **closing the
120
+ panel** — which gives the image straight back.
121
+
122
+ ## Keyboard
123
+
124
+ Creation from the keyboard is **place-then-shape**: arming a tool drops a
125
+ default-sized shape at the centre of the current view, which the ordinary
126
+ editing verbs then move and size. Creation and editing share one set of verbs.
127
+
128
+ | Key | Effect |
129
+ | --------------- | ---------------------------------------------------------------------------------------- |
130
+ | Arrows | nudge the focused handle, vertex or whole shape by 1 canvas pixel |
131
+ | Shift + arrows | the larger step, 10 canvas pixels |
132
+ | Tab / Shift+Tab | cycle the shape's handles and vertices |
133
+ | Enter | commit; for a polygon in progress, close the outline |
134
+ | Escape | cancel the keyboard shape or edit in progress, else discard any pointer draft and disarm |
135
+ | Delete | remove the selected annotation |
136
+ | `i` | insert a vertex after the focused polygon vertex |
137
+ | `x` | remove the focused polygon vertex |
138
+
139
+ Nudges are in **canvas** pixels, not screen pixels, so one press moves a vertex
140
+ the same distance across the folio at fit zoom as at 8×.
141
+
142
+ Every persisted annotation stays a focusable, labelled element while the editor
143
+ is open — those targets are core's, and the drawing layer draws only the one
144
+ annotation currently under edit, its handles and the in-progress preview.
145
+
146
+ ## Storage: the adapter seam
147
+
148
+ An adapter is **pure storage**. It knows nothing about how a shape is displayed;
149
+ the plugin's store owns display sync, caching, id reconciliation, creator
150
+ stamping, hydration and error handling around it.
151
+
152
+ ```ts
153
+ import type {
154
+ AnnotationStorageAdapter,
155
+ W3CAnnotation,
156
+ } from '@triiiceratops/plugin-annotation-editor';
157
+
158
+ const adapter: AnnotationStorageAdapter = {
159
+ id: 'my-server',
160
+ name: 'Institutional annotation server',
161
+
162
+ async load(manifestId, canvasId) {
163
+ const res = await fetch(url(manifestId, canvasId));
164
+ return res.json();
165
+ },
166
+
167
+ // Return the canonical annotation (or just its id) when your server mints
168
+ // its own IRI — the plugin reconciles the id everywhere. `void` keeps the
169
+ // client-generated one.
170
+ async create(manifestId, canvasId, annotation) {
171
+ const res = await fetch(url(manifestId, canvasId), {
172
+ method: 'POST',
173
+ body: JSON.stringify(annotation),
174
+ });
175
+ return (await res.json()) as W3CAnnotation;
176
+ },
177
+
178
+ async update(manifestId, canvasId, annotation) {
179
+ /* … */
180
+ },
181
+ async delete(manifestId, canvasId, annotationId) {
182
+ /* … */
183
+ },
184
+ };
185
+ ```
186
+
187
+ `load` may return **skeleton** entries — annotations whose bodies have not been
188
+ fetched — marked `__fullBodyLoaded: false`; implement the optional `hydrate` and
189
+ the plugin fetches a full body when one is opened. Optional `destroy` is called
190
+ on teardown.
191
+
192
+ A failed write **rolls back** the plugin's optimistic changes and then calls
193
+ `config.onPersistenceError` with the operation, the annotation id and a `retry()`
194
+ that re-runs the exact failed call. Omit the handler and the plugin logs and
195
+ shows a dismissible error line in the panel, so a failure is never invisible.
196
+
197
+ The conformance suite the built-in adapter passes is exported for yours to run
198
+ against:
199
+
200
+ ```ts
201
+ import { runAdapterContractTests } from '@triiiceratops/plugin-annotation-editor/testing';
202
+
203
+ runAdapterContractTests(() => new MyAdapter(), {
204
+ supportsIdReconciliation: true,
205
+ supportsHydrate: true,
206
+ });
207
+ ```
208
+
209
+ `LocalStorageAdapter` is the built-in default and persists under a frozen
210
+ `@triiiceratops/plugin-annotation-editor:v1` namespace, keyed by manifest and
211
+ canvas.
212
+
213
+ ## Replacing the body editor
214
+
215
+ The built-in body editor edits W3C bodies — a value, a format, a language and a
216
+ purpose from the W3C vocabulary — and leaves structured bodies it does not
217
+ understand untouched across a save. To edit your own metadata model in place,
218
+ pass `bodyEditor`: either a Svelte `component` taking an `api` prop, or a
219
+ framework-neutral `render(container, api)` returning its own cleanup.
220
+
221
+ ```ts
222
+ createAnnotationEditorPlugin({
223
+ bodyEditor: {
224
+ render(container, api) {
225
+ const input = document.createElement('textarea');
226
+ input.value = String(api.bodies[0]?.value ?? '');
227
+ input.onchange = () =>
228
+ api.save([{ type: 'TextualBody', value: input.value }]);
229
+ container.append(input);
230
+ return () => input.remove();
231
+ },
232
+ },
233
+ });
234
+ ```
235
+
236
+ `api` carries the full `annotation` in canvas space, its `bodies` normalised to
237
+ an array, the runtime `context` (manifest, canvas, user, host context),
238
+ `isHydrating`, and `save` / `cancel` / `requestDelete`. The plugin owns the
239
+ geometry and the persistence; the body editor owns only the bodies.
240
+
241
+ `extension` is the other seam, for host applications rather than for a different
242
+ body shape: gate creation (`canCreate`, `getCreateDisabledReason`), prefill a
243
+ draft (`prepareDraft`), transform on the way out (`beforeSave`), or observe
244
+ selection (`onSelectionChange`).
245
+
246
+ ## Configuration
247
+
248
+ | Option | Default | Notes |
249
+ | --------------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------- |
250
+ | `adapter` | — | Storage. `LocalStorageAdapter` on the preconfigured plugin. |
251
+ | `user` | — | `{ id, name? }`, stamped onto new annotations as creator. |
252
+ | `tools` | all five | Which tools the panel offers. |
253
+ | `defaultTool` | first in `tools` | Honoured only if it is within `tools`. |
254
+ | `defaultMotivation` | `'commenting'` | Never overwrites a motivation the host already set. |
255
+ | `target` | `'panel'` | Where the plugin chrome renders. |
256
+ | `ui` | — | `showModeToggle`, `startInCreateMode`, `showUndoRedo`, `purposes`, `allowMultipleBodies`. |
257
+ | `bodyEditor`, `extension`, `onPersistenceError` | — | See above. |
258
+ | `prepareAnnotation`, `canCreateAnnotation`, `getCreateDisabledReason` | — | Flat equivalents of the matching `extension` hooks. |
259
+
260
+ ## Styling
261
+
262
+ The drawing layer is DOM and SVG, so CSS styles it — theming follows the
263
+ viewer's `--tri-` custom properties like everything else. There is no styling
264
+ **option**; set these on the viewer or any ancestor.
265
+
266
+ | Property | Default |
267
+ | ------------------------------------ | ----------------------------------------------- |
268
+ | `--tri-annotation-draw-stroke` | `var(--tri-color-primary)` |
269
+ | `--tri-annotation-draw-stroke-width` | `2px` |
270
+ | `--tri-annotation-draw-fill` | 20% `--tri-color-primary`, mixed to transparent |
271
+ | `--tri-annotation-draw-cursor` | `crosshair` |
272
+ | `--tri-annotation-edit-move-cursor` | `move` |
273
+ | `--tri-annotation-handle-size` | `10px` |
274
+ | `--tri-annotation-handle-fill` | `var(--tri-color-primary)` |
275
+ | `--tri-annotation-handle-stroke` | `var(--tri-color-base-100)` |
276
+ | `--tri-annotation-point-fill` | `var(--tri-annotation-color)` |
277
+ | `--tri-annotation-point-stroke` | `var(--tri-annotation-color)` |
278
+
279
+ Point markers are the exception: they are core's, not this layer's. Their size is
280
+ `--tri-annotation-point-size` and their colour `--tri-annotation-color`, the same
281
+ two tokens core's read-only overlay draws and measures a marker from, so a point
282
+ looks the same open for editing as it does at rest and one declaration restyles
283
+ both. This plugin declares neither of its own, deliberately — a second setting
284
+ could only disagree with the first, and a point would change under the reader the
285
+ moment it was opened. `--tri-annotation-point-fill` and
286
+ `--tri-annotation-point-stroke` still override the colour for this layer alone,
287
+ which is the only way to make the two disagree on purpose.
288
+
289
+ ## Upgrading from `1.0.0-rc.7`
290
+
291
+ `rc.7` was the last published version, and its editing surface was
292
+ [Annotorious](https://annotorious.dev/). Annotorious and OpenSeadragon are gone;
293
+ the drawing layer is first-party. Four configuration changes, all visible at the
294
+ call site:
295
+
296
+ - **`drawingStyle` is removed.** It was typed with an Annotorious type and only
297
+ ever read inside the deleted Annotorious binding. Style the drawing layer with
298
+ the CSS custom properties above instead.
299
+ - **`requiredCapabilities` is gone.** Compatibility is now the `coreRange` floor
300
+ alone — overlay layers are not optional in core, so there was nothing to
301
+ require.
302
+ - **`user` keeps its `{ id, name }` shape** but is a locally declared type
303
+ rather than an Annotorious one. No call-site change; replace an `import type
304
+ { User } from '@annotorious/openseadragon'` with `AnnotationEditorUser` from
305
+ this package.
306
+ - **`pointStyle` is removed from this plugin's config.** It only ever styled the
307
+ Annotorious marker, which is why it goes the way `drawingStyle` does. Nothing
308
+ replaces it in configuration: a marker is theming now, and both core's
309
+ read-only marker and this editor read the same two tokens —
310
+ `--tri-annotation-point-size` and `--tri-annotation-color` — which is what
311
+ makes a point the same size and colour selected and not.
312
+
313
+ Nothing about persisted data changed. The v1 LocalStorage namespace and the W3C
314
+ annotation format are the same, so annotations written by `rc.7` load and edit
315
+ without migration — an ellipse simply was not drawable before.
316
+
317
+ Everything that was never the Annotorious binding is carried forward unchanged:
318
+ the store, the adapter seam and `LocalStorageAdapter`, display sync, stamping, id
319
+ reconciliation, hydration, persistence-aware undo/redo, the body editor and its
320
+ replacement hook, the extension hooks, the i18n catalog, and the adapter
321
+ conformance suite on the `/testing` subpath.
322
+
323
+ ## Design records
324
+
325
+ - [ADR 0020](../../docs/adr/0020-modal-drawing-swallows-pointer-events-in-the-dom.md)
326
+ — why arming a tool takes pointer events in the DOM instead of claiming input
327
+ at the gesture arbiter.
328
+ - [ADR 0021](../../docs/adr/0021-the-editing-surface-is-first-party.md) — why the
329
+ editing surface is first-party, and why the editor draws one annotation while
330
+ core draws the set.
331
+ - [ADR 0022](../../docs/adr/0022-an-ellipse-persists-as-a-polygon.md) — why an
332
+ ellipse is stored as a polygon.
@@ -11,17 +11,14 @@ export interface AnnotationDisplayState {
11
11
  clearUserAnnotations(manifestId: string, canvasId: string): void;
12
12
  }
13
13
  /**
14
- * Plugin-internal persistence core. Owns everything the manager used to do "to
15
- * storage": the annotation cache, per-annotation hydration state, create-vs-update
14
+ * Plugin-internal persistence core. Owns everything "to storage": the
15
+ * annotation cache, per-annotation hydration state, create-vs-update
16
16
  * resolution, the per-id save queue, the load-race token, and the raw adapter.
17
17
  *
18
- * `AnnotationManager` talks only to this store for persistence and keeps the
19
- * Annotorious/OpenSeadragon mechanics (selection, tools, coordinate transforms).
20
- * The store deals exclusively in **canvas-space** W3C annotations — transforms
21
- * live at the manager/store boundary.
22
- *
23
- * This class is a refactor of code previously inlined in `AnnotationManager`
24
- * (issues 01–04); behavior is intentionally unchanged (issue 05).
18
+ * The drawing layer talks only to this store for persistence and keeps the
19
+ * geometry (selection, tools, coordinate transforms). The store deals
20
+ * exclusively in **canvas-space** W3C annotations — transforms live at the
21
+ * drawing-layer/store boundary.
25
22
  */
26
23
  export declare class AnnotationStore {
27
24
  private static readonly W3C_CONTEXT;
@@ -30,16 +27,17 @@ export declare class AnnotationStore {
30
27
  private config;
31
28
  /**
32
29
  * Notified when a `create` reconciles an annotation onto a server-assigned
33
- * id (F5), so the manager can re-open it in Annotorious under the canonical
34
- * id and re-emit the active-edit-id signal. Set by the manager; the loader
35
- * leaves it unset.
30
+ * id (F5), so the drawing layer can follow its in-flight create onto the
31
+ * canonical id and open the body editor on the id the annotation was
32
+ * actually stored under. Set by the drawing layer; the loader leaves it
33
+ * unset.
36
34
  */
37
35
  onReconcileId?: (oldId: string, canonical: W3CAnnotation) => void;
38
36
  /**
39
- * Notified after an undo/redo replay so the manager can reconcile the open
40
- * Annotorious editing session with the new storage state (F6): `annotation`
41
- * is the annotation now in the cache under `affectedId`, or `null` when the
42
- * replay removed it. Set by the manager; the loader leaves it unset.
37
+ * Notified after an undo/redo replay so the drawing layer can reconcile the
38
+ * open editing session with the new storage state (F6): `annotation` is the
39
+ * annotation now in the cache under `affectedId`, or `null` when the replay
40
+ * removed it. Set by the drawing layer; the loader leaves it unset.
43
41
  */
44
42
  onReplay?: (affectedId: string, annotation: W3CAnnotation | null) => void;
45
43
  private manifestId;
@@ -61,8 +59,9 @@ export declare class AnnotationStore {
61
59
  constructor(config: AnnotationEditorConfig);
62
60
  /**
63
61
  * Point display sync at the owning viewer's display state (ADR 0001,
64
- * amended). Called by the loader (and the manager) when the viewer is known.
65
- * Idempotent — re-attaching the same viewer is harmless.
62
+ * amended). Called by the mount seam when the viewer is known, and by the
63
+ * loader for a store it drives on its own. Idempotent — re-attaching the
64
+ * same viewer is harmless.
66
65
  */
67
66
  setDisplayState(displayState: AnnotationDisplayState | null): void;
68
67
  get currentManifestId(): string | null;
@@ -88,8 +87,8 @@ export declare class AnnotationStore {
88
87
  get canRedo(): boolean;
89
88
  /**
90
89
  * Point the store at a canvas and drop the previous canvas's cache. Does
91
- * not load — the manager drives load timing (and, from issue 06, display
92
- * sync) around this call.
90
+ * not load — the caller drives load timing and display sync around this
91
+ * call.
93
92
  */
94
93
  setCanvas(manifestId: string | null, canvasId: string | null): void;
95
94
  get(id: string): W3CAnnotation | null;
@@ -109,12 +108,12 @@ export declare class AnnotationStore {
109
108
  *
110
109
  * On create the store stamps a complete W3C/IIIF annotation (F18) and, if the
111
110
  * adapter returns a canonical annotation or id, reconciles the cache/display
112
- * onto the server-assigned id and notifies the manager (F5). On update it
111
+ * onto the server-assigned id and notifies its owner (F5). On update it
113
112
  * refreshes `modified` and adopts a server-normalized copy when returned.
114
113
  *
115
114
  * Cache and display are only advanced *after* the adapter resolves, so a
116
115
  * rejected write leaves both at their pre-operation state — the rollback the
117
- * manager relies on to re-signal selection (F20). Returns `true` on success,
116
+ * drawing layer relies on to re-signal selection (F20). Returns `true` on success,
118
117
  * `false` when the adapter rejected (the failure has been reported).
119
118
  */
120
119
  persist(annotation: W3CAnnotation): Promise<boolean>;
@@ -130,7 +129,7 @@ export declare class AnnotationStore {
130
129
  * Fetch a skeleton annotation's full body from the adapter and cache it.
131
130
  * Returns the full annotation, or null when there is nothing to do (no
132
131
  * hydrate support), the fetch came back empty, the canvas changed while
133
- * awaiting (F14), or `shouldApply` vetoes committing the result (the manager
132
+ * awaiting (F14), or `shouldApply` vetoes committing the result (the caller
134
133
  * uses this to bail if the annotation is no longer being edited).
135
134
  */
136
135
  hydrate(id: string, shouldApply?: () => boolean): Promise<W3CAnnotation | null>;
@@ -183,14 +182,14 @@ export declare class AnnotationStore {
183
182
  /**
184
183
  * Commit a created annotation to the cache under its canonical id. When the
185
184
  * adapter returns a server-assigned annotation or id string, the cache key is
186
- * swapped from the local id to the canonical one, and the manager is notified
187
- * so it can re-open the annotation under the new id (F5).
185
+ * swapped from the local id to the canonical one, and `onReconcileId` fires
186
+ * so the owner can follow the annotation onto the new id (F5).
188
187
  */
189
188
  private reconcileCreate;
190
189
  /**
191
190
  * Stamp a complete, valid W3C/IIIF annotation before create without
192
191
  * clobbering host-provided values (F18). `extension.beforeSave` has already
193
- * run (in the manager) and therefore still wins — stamping only fills gaps.
192
+ * run by the caller and therefore still wins — stamping only fills gaps.
194
193
  */
195
194
  private stampForCreate;
196
195
  /** Refresh `modified` on an updated annotation (F18). */
@@ -66,7 +66,7 @@ export interface W3CAnnotation<TBody = W3CAnnotationBody> {
66
66
  /**
67
67
  * Shape an adapter's `load()`/`hydrate()` may return. Beyond a stored
68
68
  * annotation it may carry the internal skeleton markers the plugin reads exactly
69
- * once and strips before anything enters the cache or Annotorious (ticket 03):
69
+ * once and strips before anything enters the cache:
70
70
  * `__fullBodyLoaded: false` signals a skeleton whose body must be fetched via
71
71
  * `hydrate()`. These markers are NOT part of the stored annotation contract —
72
72
  * they never round-trip — so they live here rather than on {@link W3CAnnotation}.
package/dist/catalog.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import type { LocaleCatalog } from '@triiiceratops/plugin-sdk';
2
2
  /**
3
3
  * The plugin's package-owned localization catalog (CONTEXT.md **Active locale**).
4
- * These strings previously lived in core's `messages/en.json` / `messages/de.json`;
5
- * migrating the plugin out of core moves them here so the catalog ships with (and
6
- * evolves with) the plugin, and core's catalogs carry no plugin keys. `en` is the
7
- * required fallback; a missing key resolves to `en` and then to the key itself.
4
+ * The catalog ships with (and evolves with) the plugin, so core's catalogs carry
5
+ * no plugin keys. `en` is the required fallback; a missing key resolves to `en`
6
+ * and then to the key itself.
8
7
  */
9
8
  export declare const catalog: LocaleCatalog;
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * `VIEWER_STATE_KEY` carries the owning viewer's state (the live `ViewerState`,
5
5
  * or the reactive mirror `view.mount` builds so the plugin's own Svelte runtime
6
- * tracks cross-realm state changes) to the controller. It replaces core's
7
- * internal `VIEWER_STATE_KEY` (the plugin no longer imports core internals); the
8
- * controller reads it with `getContext(VIEWER_STATE_KEY)` exactly as before.
6
+ * tracks cross-realm state changes) to the controller, which reads it with
7
+ * `getContext(VIEWER_STATE_KEY)`. Package-local so the plugin never imports
8
+ * core internals.
9
9
  */
10
10
  export declare const VIEWER_STATE_KEY: unique symbol;
@@ -0,0 +1,22 @@
1
+ import type { PluginContext } from '@triiiceratops/plugin-sdk';
2
+ import type { AnnotationStore } from './AnnotationStore.svelte';
3
+ import type { DrawingSession } from './drawingSession.svelte';
4
+ import type { TFn } from './i18n.svelte';
5
+ import type { MirroredViewerState } from './viewerMirror.svelte';
6
+ /** Marks the container so a spec can find it. */
7
+ export declare const DRAWING_LAYER_CLASS = "tri-annotation-drawing-layer";
8
+ /** The layer's name within this plugin's id namespace. */
9
+ export declare const DRAWING_LAYER_NAME = "drawing";
10
+ /**
11
+ * Register the drawing layer with the owning viewer. Returns the registry's
12
+ * idempotent dispose, which is a no-op if the registration was refused.
13
+ *
14
+ * One layer, not several: cross-plugin ordering cannot be coordinated, so
15
+ * internal stacking is `z-index` on this container's own children.
16
+ */
17
+ export declare function registerDrawingLayer(context: PluginContext, surface: {
18
+ session: DrawingSession;
19
+ store: AnnotationStore;
20
+ viewerState: MirroredViewerState;
21
+ t: TFn;
22
+ }): () => void;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The arming state the panel and the drawing layer share.
3
+ *
4
+ * The two live in different component trees — the panel is mounted into core's
5
+ * plugin surface, the layer into the overlay-layer container — so the tool the
6
+ * reader armed reaches the layer through this object rather than through props.
7
+ * Reactive so the layer's `pointer-events` follow the panel's mode toggle
8
+ * without either side polling.
9
+ */
10
+ import type { W3CAnnotation } from './adapters/types';
11
+ import type { DrawingTool } from './types';
12
+ export declare class DrawingSession {
13
+ /**
14
+ * The armed tool, or `null` while the reader is in edit mode — arming is
15
+ * modal, and this is what the layer's whole-surface `pointer-events: auto`
16
+ * is gated on.
17
+ */
18
+ armedTool: DrawingTool | null;
19
+ /**
20
+ * The persisted annotation open for editing, or `null`. The panel decides
21
+ * it — a tap on a shape reaches core's edit bus, which the controller owns
22
+ * — and the drawing layer draws its shape and its handles, which is what
23
+ * makes core's own rendering of that one annotation stand down.
24
+ */
25
+ editingAnnotationId: string | null;
26
+ /**
27
+ * Set by the controller when an edit was opened by TAPPING the shape on the
28
+ * image, and cleared by the drawing layer once focus is on it.
29
+ *
30
+ * A tap leaves focus wherever it was — core's own shape is removed the
31
+ * instant the editor takes the rendering over, so the document is left
32
+ * focused on nothing. Every keyboard verb the open shape has (nudge,
33
+ * commit, delete, the vertex keys) is bound on the shape, so until focus
34
+ * reaches it a reader who taps a shape has no keyboard at all.
35
+ *
36
+ * Only the tap. Selecting the annotation from the panel's LIST opens the
37
+ * same edit, and pulling focus out of the list onto the image would take
38
+ * the reader off the control they are actually working in.
39
+ */
40
+ focusOnEdit: boolean;
41
+ /**
42
+ * Set by the controller so a committed shape opens its body editor. Not
43
+ * reactive state: it is wiring, replaced only when the controller mounts.
44
+ */
45
+ onCreated: ((annotation: W3CAnnotation) => void) | null;
46
+ /**
47
+ * The host's `extension.prepareDraft` / `prepareAnnotation` hook, applied
48
+ * to a NEW annotation before its first save. Set by the controller, which
49
+ * is where the config and the runtime context the hook is handed live;
50
+ * called by the drawing layer, which is where a shape becomes an
51
+ * annotation.
52
+ */
53
+ prepareDraft: ((annotation: W3CAnnotation) => W3CAnnotation) | null;
54
+ /**
55
+ * The host's `extension.beforeSave` hook, applied to every annotation on
56
+ * its way to the store — a create, a reshape and a body save alike, so a
57
+ * host that rewrites annotations sees all three.
58
+ */
59
+ beforeSave: ((annotation: W3CAnnotation) => Promise<W3CAnnotation>) | null;
60
+ /**
61
+ * Create a whole-canvas annotation on the store's current canvas. Set by
62
+ * the drawing layer, called by the controller: the whole-canvas tool has no
63
+ * gesture, so the panel activating it IS the create, but the create itself
64
+ * belongs beside every other one so id reconciliation and the handoff to
65
+ * the body editor are not written twice.
66
+ */
67
+ createWholeCanvasAnnotation: (() => void) | null;
68
+ /**
69
+ * Place a tool's default shape at the centre of the current view, ready for
70
+ * the ordinary editing verbs to move and size. Set by the drawing layer,
71
+ * called by the controller when a tool is activated from the KEYBOARD.
72
+ *
73
+ * Creation is place-then-shape: a keyboard user has no cursor to describe a
74
+ * region with, so the tool supplies a starting geometry rather than the
75
+ * layer growing a second, keyboard-only creation path.
76
+ */
77
+ placeDefaultShape: ((tool: DrawingTool) => void) | null;
78
+ /**
79
+ * Delete the annotation open for editing — the Delete key's counterpart to
80
+ * the panel's own delete button, and the same confirmation with it. Set by
81
+ * the controller, called by the drawing layer.
82
+ */
83
+ requestDelete: (() => void) | null;
84
+ /**
85
+ * Escape's way back out of an open edit — the only way out there is, since
86
+ * the body editor offers no cancel of its own. Set by the controller for the
87
+ * same reason as {@link requestDisarm}: the open edit is DERIVED from the
88
+ * panel's selection, so the layer clearing `editingAnnotationId` itself
89
+ * would be overwritten by the next change to that selection.
90
+ */
91
+ requestCancelEdit: (() => void) | null;
92
+ /**
93
+ * Escape's way back out of the armed state. The layer cannot simply clear
94
+ * `armedTool`: arming is DERIVED in the controller from create mode and the
95
+ * selected tool, so a write here would be overwritten by the next change to
96
+ * either, and the panel would meanwhile still show the tool lit. Disarming
97
+ * has to happen at the state the panel renders from, which is the
98
+ * controller's.
99
+ */
100
+ requestDisarm: (() => void) | null;
101
+ }