@triiiceratops/plugin-annotation-editor 1.0.0-rc.1

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,11 @@
1
+ import type { ViewerState } from 'triiiceratops';
2
+ import type { AnnotationStore } from './AnnotationStore.svelte';
3
+ /**
4
+ * Creates a reactive loader that syncs annotations from storage to the viewer's
5
+ * read-only overlay. It runs independently of the Annotation Editor UI component
6
+ * (the panel may never open), so it drives the shared store directly: point the
7
+ * store at the current canvas and load; the store injects into this viewer's
8
+ * per-viewer display state (F10, ADR 0007). When the editor panel is mounted,
9
+ * its manager shares this same store, so both paths converge on one cache.
10
+ */
11
+ export declare function createLoader(store: AnnotationStore): (viewerState: ViewerState) => void;
@@ -0,0 +1,3 @@
1
+ import type { PluginContext } from '@triiiceratops/plugin-sdk';
2
+ import type { AnnotationEditorConfig } from './types';
3
+ export declare function mountAnnotationEditor(container: HTMLElement, context: PluginContext, config: AnnotationEditorConfig): () => void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The annotation-editor plugin, authored on `@triiiceratops/plugin-sdk`.
3
+ *
4
+ * `definePlugin` returns the framework-neutral factory core activates through the
5
+ * structural seam (it carries its own `activate(host)`); core never imports this
6
+ * package or its Svelte runtime. The full domain machinery — Store, Adapter seam,
7
+ * per-viewer display sync, undo/redo, body editors, Annotorious integration — is
8
+ * carried intact and driven from the neutral `view.mount(container, context)`
9
+ * contract (see `mount.svelte.ts`). Annotorious needs the raw OSD viewer, so the
10
+ * plugin declares `requiredCapabilities: ['osd@5']` (ADR 0009).
11
+ */
12
+ import { type SdkPlugin } from '@triiiceratops/plugin-sdk';
13
+ import type { AnnotationEditorConfig } from './types';
14
+ /**
15
+ * Create an annotation-editor plugin with custom configuration.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import {
20
+ * createAnnotationEditorPlugin,
21
+ * LocalStorageAdapter,
22
+ * } from '@triiiceratops/plugin-annotation-editor';
23
+ *
24
+ * const annotationPlugin = createAnnotationEditorPlugin({
25
+ * adapter: new LocalStorageAdapter(),
26
+ * user: { id: 'user-123', name: 'Jane Doe' },
27
+ * });
28
+ * // Svelte: <TriiiceratopsViewer plugins={[annotationPlugin]} />
29
+ * // WC: viewer.plugins = [annotationPlugin];
30
+ * ```
31
+ */
32
+ export declare function createAnnotationEditorPlugin(config?: AnnotationEditorConfig): SdkPlugin;
33
+ /**
34
+ * Pre-configured annotation-editor plugin with the LocalStorage adapter. For
35
+ * advanced configuration use {@link createAnnotationEditorPlugin}.
36
+ */
37
+ export declare const AnnotationEditorPlugin: SdkPlugin;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The combined stylesheet (base Annotorious sheet + layer fixes) installed under
3
+ * the `annotorious` id at activation, shaped by {@link definePluginStyles} into
4
+ * the `STYLES` / `STYLE_ID` exports.
5
+ *
6
+ * No plugin chrome CSS lives here anymore: core owns the toolbar button and the
7
+ * docked-panel / anchored-flyout surface (epic restore-plugin-toolbar-chrome),
8
+ * so the plugin ships only the Annotorious annotation-layer styles. The panel's
9
+ * own presentation is scoped component CSS rendered inside `view.mount`.
10
+ */
11
+ export declare const STYLES: string, STYLE_ID: string;
@@ -0,0 +1,48 @@
1
+ import type { AnnotationStorageAdapter } from '../types';
2
+ /**
3
+ * Adapter authoring kit — a reusable conformance suite so adapter authors can
4
+ * verify their implementation against the contract the plugin relies on (ticket
5
+ * 10, F28 / SPEC §2.6).
6
+ *
7
+ * An adapter is pure storage: the plugin owns display sync, caching, id
8
+ * bookkeeping, timestamp/attribution stamping, and error handling. This suite
9
+ * therefore checks only storage behavior — load/create/update/delete round-trips,
10
+ * verbatim body preservation (including structured/unknown shapes), key isolation,
11
+ * and the two opt-in capabilities (server-assigned ids and hydrate).
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { runAdapterContractTests } from '@triiiceratops/plugin-annotation-editor/testing';
16
+ * import { MyAdapter } from './MyAdapter';
17
+ *
18
+ * runAdapterContractTests(() => new MyAdapter(), {
19
+ * supportsIdReconciliation: true,
20
+ * supportsHydrate: true,
21
+ * });
22
+ * ```
23
+ */
24
+ export interface AdapterContractOptions {
25
+ /**
26
+ * The adapter mints its own canonical id on `create` and returns it (as an
27
+ * annotation or an id string). When set, the suite asserts `create` returns a
28
+ * non-void value and that the returned id is honored by subsequent
29
+ * `update`/`delete` (F5).
30
+ */
31
+ supportsIdReconciliation?: boolean;
32
+ /**
33
+ * The adapter implements `hydrate`. When set, the suite asserts `hydrate`
34
+ * exists, returns the full annotation for a known id, and returns `null` for
35
+ * an unknown id.
36
+ */
37
+ supportsHydrate?: boolean;
38
+ /** Overrides the `describe` block label. */
39
+ label?: string;
40
+ }
41
+ /**
42
+ * Register the adapter conformance suite for `factory()`. Call at the top level
43
+ * of a vitest test file; `factory` is invoked fresh before each test so a
44
+ * stateful adapter starts clean, and every test uses a unique manifest/canvas
45
+ * pair so tests can't bleed into each other (important for storage-backed
46
+ * adapters like `LocalStorageAdapter`).
47
+ */
48
+ export declare function runAdapterContractTests(factory: () => AnnotationStorageAdapter, options?: AdapterContractOptions): void;
@@ -0,0 +1,158 @@
1
+ import { describe as _, beforeEach as A, it as c, expect as n } from "vitest";
2
+ function w(i) {
3
+ const u = { ...i };
4
+ return delete u.__fullBodyLoaded, delete u.__bodyPreview, u;
5
+ }
6
+ function b(i, u) {
7
+ return typeof i == "string" ? i : i && typeof i == "object" ? i.id : u;
8
+ }
9
+ function f(i, u, h) {
10
+ return {
11
+ "@context": "http://www.w3.org/ns/anno.jsonld",
12
+ id: i,
13
+ type: "Annotation",
14
+ motivation: "commenting",
15
+ body: h ?? [
16
+ { type: "TextualBody", purpose: "commenting", value: "hello" }
17
+ ],
18
+ target: {
19
+ type: "SpecificResource",
20
+ source: u,
21
+ selector: {
22
+ type: "FragmentSelector",
23
+ conformsTo: "http://www.w3.org/TR/media-frags/",
24
+ value: "xywh=10,20,30,40"
25
+ }
26
+ }
27
+ };
28
+ }
29
+ function C(i, u = {}) {
30
+ const { supportsIdReconciliation: h, supportsHydrate: g, label: x } = u;
31
+ _(x ?? "AnnotationStorageAdapter contract", () => {
32
+ let o, d, t, m = 0;
33
+ A(() => {
34
+ o = i(), m += 1, d = `contract-manifest-${m}`, t = `contract-canvas-${m}`;
35
+ });
36
+ async function p(a) {
37
+ const r = await o.create(
38
+ d,
39
+ t,
40
+ structuredClone(a)
41
+ ), e = b(r, a.id), s = (await o.load(d, t)).find((l) => l.id === e) ?? null;
42
+ if (!s) return { id: e, resolved: null };
43
+ if (s.__fullBodyLoaded === !1) {
44
+ n(typeof o.hydrate).toBe("function");
45
+ const l = await o.hydrate(d, t, e);
46
+ return { id: e, resolved: l };
47
+ }
48
+ return { id: e, resolved: s };
49
+ }
50
+ c("load returns an empty array for a canvas with no annotations", async () => {
51
+ const a = await o.load(d, t);
52
+ n(Array.isArray(a)).toBe(!0), n(a).toHaveLength(0);
53
+ }), c("create then load round-trips the annotation verbatim", async () => {
54
+ const a = f("anno-1", t), { id: r, resolved: e } = await p(a);
55
+ n(e).not.toBeNull(), n(w(e)).toEqual({
56
+ ...a,
57
+ id: r
58
+ });
59
+ }), c("create preserves structured / unknown body shapes verbatim", async () => {
60
+ const a = {
61
+ type: "Dataset",
62
+ purpose: "linking",
63
+ value: {
64
+ nested: { deep: [1, 2, 3], flag: !0 },
65
+ ref: "https://example.org/entity/42"
66
+ },
67
+ extra: null
68
+ }, r = f(
69
+ "anno-structured",
70
+ t,
71
+ a
72
+ ), { resolved: e } = await p(r);
73
+ n(e).not.toBeNull(), n(e.body).toEqual(a);
74
+ }), c("update replaces a stored annotation body", async () => {
75
+ const a = f("anno-2", t), { id: r } = await p(a), e = {
76
+ ...a,
77
+ id: r,
78
+ body: [
79
+ {
80
+ type: "TextualBody",
81
+ purpose: "commenting",
82
+ value: "edited"
83
+ }
84
+ ]
85
+ };
86
+ await o.update(
87
+ d,
88
+ t,
89
+ structuredClone(e)
90
+ );
91
+ const s = (await o.load(d, t)).find((v) => v.id === r);
92
+ n(s).toBeDefined();
93
+ const l = s?.__fullBodyLoaded === !1 ? await o.hydrate(d, t, r) : s;
94
+ n(w(l).body).toEqual(e.body);
95
+ }), c("delete removes a stored annotation", async () => {
96
+ const a = f("anno-3", t), { id: r } = await p(a);
97
+ await o.delete(d, t, r);
98
+ const e = await o.load(d, t);
99
+ n(e.find((y) => y.id === r)).toBeUndefined();
100
+ }), c("isolates annotations by manifest and canvas", async () => {
101
+ const a = f("anno-iso", t), { id: r } = await p(a), e = `${t}-other`, y = `${d}-other`;
102
+ n(await o.load(d, e)).toHaveLength(0), n(await o.load(y, t)).toHaveLength(0);
103
+ const s = await o.load(d, t);
104
+ n(s.find((l) => l.id === r)).toBeDefined();
105
+ }), h && c("returns a server-assigned id honored by update and delete", async () => {
106
+ const a = f("local-temp-id", t), r = await o.create(
107
+ d,
108
+ t,
109
+ structuredClone(a)
110
+ );
111
+ n(r == null).toBe(!1);
112
+ const e = b(r, a.id);
113
+ n(typeof e).toBe("string");
114
+ const y = {
115
+ ...a,
116
+ id: e,
117
+ body: [
118
+ {
119
+ type: "TextualBody",
120
+ purpose: "commenting",
121
+ value: "reconciled-edit"
122
+ }
123
+ ]
124
+ };
125
+ await o.update(
126
+ d,
127
+ t,
128
+ structuredClone(y)
129
+ );
130
+ let s = await o.load(d, t);
131
+ const l = s.find((B) => B.id === e);
132
+ n(l).toBeDefined();
133
+ const v = l?.__fullBodyLoaded === !1 ? await o.hydrate(
134
+ d,
135
+ t,
136
+ e
137
+ ) : l;
138
+ n(w(v).body).toEqual(y.body), await o.delete(d, t, e), s = await o.load(d, t), n(
139
+ s.find((B) => B.id === e)
140
+ ).toBeUndefined();
141
+ }), g && (c("exposes a hydrate method", () => {
142
+ n(typeof o.hydrate).toBe("function");
143
+ }), c("hydrate returns the full annotation for a known id", async () => {
144
+ const a = f("anno-hydrate", t), { id: r } = await p(a), e = await o.hydrate(d, t, r);
145
+ n(e).not.toBeNull(), n(w(e).body).toEqual(a.body);
146
+ }), c("hydrate returns null for an unknown id", async () => {
147
+ const a = await o.hydrate(
148
+ d,
149
+ t,
150
+ "no-such-annotation"
151
+ );
152
+ n(a).toBeNull();
153
+ }));
154
+ });
155
+ }
156
+ export {
157
+ C as runAdapterContractTests
158
+ };
@@ -0,0 +1,177 @@
1
+ import type { User, DrawingStyle } from '@annotorious/openseadragon';
2
+ import type { Component } from 'svelte';
3
+ import type { PluginUiTarget } from '@triiiceratops/plugin-sdk';
4
+ import type { PointStyle } from './utils/pointMarker';
5
+ import type { W3CAnnotation, AdapterLoadResult } from './adapters/types';
6
+ export type { PointStyle };
7
+ export interface AnnotationEditorRuntimeContext<HostContext = unknown, TBody = W3CAnnotationBody> {
8
+ manifestId: string | null;
9
+ canvasId: string | null;
10
+ isEditing: boolean;
11
+ selectedAnnotation: W3CAnnotation<TBody> | null;
12
+ user?: User;
13
+ hostContext: HostContext | null;
14
+ }
15
+ export interface AnnotationEditorExtension<HostContext = unknown, TBody = W3CAnnotationBody> {
16
+ getContext?: () => HostContext | null;
17
+ /**
18
+ * Subscribe to host-context changes. Call `invalidate` when canCreate or
19
+ * getCreateDisabledReason should re-evaluate; return an unsubscribe.
20
+ */
21
+ subscribe?: (invalidate: () => void) => () => void;
22
+ canCreate?: (context: AnnotationEditorRuntimeContext<HostContext, TBody>) => boolean;
23
+ getCreateDisabledReason?: (context: AnnotationEditorRuntimeContext<HostContext, TBody>) => string | null;
24
+ prepareDraft?: (annotation: W3CAnnotation<TBody>, context: AnnotationEditorRuntimeContext<HostContext, TBody>) => W3CAnnotation<TBody>;
25
+ beforeSave?: (annotation: W3CAnnotation<TBody>, context: AnnotationEditorRuntimeContext<HostContext, TBody>) => W3CAnnotation<TBody> | Promise<W3CAnnotation<TBody>>;
26
+ onSelectionChange?: (annotation: W3CAnnotation<TBody> | null, context: AnnotationEditorRuntimeContext<HostContext, TBody>) => void;
27
+ }
28
+ export interface AnnotationBodyEditorApi<HostContext = unknown, TBody = W3CAnnotationBody> {
29
+ /** Full selected annotation in canvas space. */
30
+ annotation: W3CAnnotation<TBody>;
31
+ /** Current annotation bodies normalized to an array; body shape is host-owned. */
32
+ bodies: unknown[];
33
+ context: AnnotationEditorRuntimeContext<HostContext, TBody>;
34
+ isHydrating: boolean;
35
+ save: (bodies: unknown[] | unknown) => Promise<void>;
36
+ cancel: () => void;
37
+ requestDelete: () => void;
38
+ }
39
+ export type AnnotationBodyEditor<HostContext = unknown, TBody = W3CAnnotationBody> = {
40
+ component: Component<{
41
+ api: AnnotationBodyEditorApi<HostContext, TBody>;
42
+ }>;
43
+ } | {
44
+ render: (container: HTMLElement, api: AnnotationBodyEditorApi<HostContext, TBody>) => (() => void) | void;
45
+ };
46
+ export interface AnnotationEditorUiConfig {
47
+ /** Show the Edit/Create segmented control. Defaults to `true`. */
48
+ showModeToggle?: boolean;
49
+ /** Open in create mode when creation is currently allowed. Defaults to `false`. */
50
+ startInCreateMode?: boolean;
51
+ /** Show persistence-aware undo/redo buttons. Defaults to `true`. */
52
+ showUndoRedo?: boolean;
53
+ /** Purpose choices shown by the built-in body editor. Defaults to `W3C_PURPOSES`. */
54
+ purposes?: string[];
55
+ /** Allow adding more body rows in the built-in body editor. Defaults to `true`. */
56
+ allowMultipleBodies?: boolean;
57
+ }
58
+ /**
59
+ * The storage contract a host implements to bring its own annotation server.
60
+ * It is pure storage — the plugin's `AnnotationStore` owns display sync,
61
+ * caching, id reconciliation, stamping, and error handling — so a conforming
62
+ * adapter is roughly these five functions (see `LocalStorageAdapter`).
63
+ *
64
+ * The `W3CAnnotation` / `AdapterLoadResult` shapes are defined in
65
+ * `adapters/types.ts`; they are imported here (type-only, so the cycle is
66
+ * erased at compile time) to keep the adapter contract fully typed.
67
+ */
68
+ export interface AnnotationStorageAdapter<TBody = W3CAnnotationBody> {
69
+ readonly id: string;
70
+ readonly name: string;
71
+ /**
72
+ * Return the canvas's annotations. Skeleton entries (bodies not yet loaded)
73
+ * carry `__fullBodyLoaded: false`; the plugin reads that marker once and
74
+ * strips it (see {@link AdapterLoadResult}).
75
+ */
76
+ load(manifestId: string, canvasId: string): Promise<AdapterLoadResult<TBody>[]>;
77
+ /** Fetch the full body for a previously-skeleton annotation. */
78
+ hydrate?(manifestId: string, canvasId: string, annotationId: string): Promise<AdapterLoadResult<TBody> | null>;
79
+ /**
80
+ * Persist a new annotation. Servers that mint their own annotation IRI on
81
+ * create may return the canonical annotation (or just its id string); the
82
+ * plugin then reconciles the id everywhere. Returning `void` keeps the
83
+ * client-generated id (the LocalStorageAdapter path).
84
+ */
85
+ create(manifestId: string, canvasId: string, annotation: W3CAnnotation<TBody>): Promise<W3CAnnotation<TBody> | string | void>;
86
+ /**
87
+ * Persist an update. Returning the (possibly server-normalized) annotation
88
+ * replaces the cached copy; returning `void` keeps the sent payload.
89
+ */
90
+ update(manifestId: string, canvasId: string, annotation: W3CAnnotation<TBody>): Promise<W3CAnnotation<TBody> | void>;
91
+ delete(manifestId: string, canvasId: string, annotationId: string): Promise<void>;
92
+ destroy?(): void;
93
+ }
94
+ /** The adapter operations whose failures are surfaced (F20). */
95
+ export type AnnotationPersistenceOp = 'load' | 'create' | 'update' | 'delete' | 'hydrate';
96
+ /**
97
+ * Structured description of a failed persistence operation handed to
98
+ * `config.onPersistenceError`. The plugin has already rolled back its optimistic
99
+ * cache/display changes by the time this fires; `retry()` re-runs the exact
100
+ * failed operation with the same payload (F20).
101
+ */
102
+ export interface AnnotationPersistenceError {
103
+ op: AnnotationPersistenceOp;
104
+ /** The affected annotation id, when the operation targets one. */
105
+ annotationId?: string;
106
+ manifestId: string;
107
+ canvasId: string;
108
+ /** The value the adapter threw/rejected with. */
109
+ cause: unknown;
110
+ retry: () => Promise<void>;
111
+ }
112
+ export interface AnnotationEditorConfig<TBody = W3CAnnotationBody, THostContext = unknown> {
113
+ /** Render target for the plugin chrome. Defaults to `'panel'`. */
114
+ target?: PluginUiTarget;
115
+ /** Storage adapter for persistence */
116
+ adapter?: AnnotationStorageAdapter<TBody>;
117
+ /** Current user for attribution */
118
+ user?: User;
119
+ /** Drawing style for annotations while editing */
120
+ drawingStyle?: DrawingStyle;
121
+ /**
122
+ * Marker styling for point annotations (`PointSelector`). Consumed by both
123
+ * the read-only overlay and the editor so a point looks the same selected or
124
+ * not; `radius` is in screen pixels. Defaults to a red marker of radius 5
125
+ * (spec §3.4).
126
+ */
127
+ pointStyle?: PointStyle;
128
+ /** Available drawing tools */
129
+ tools?: DrawingTool[];
130
+ /** Default drawing tool */
131
+ defaultTool?: DrawingTool;
132
+ /** Optional extension hook surface for host apps */
133
+ extension?: AnnotationEditorExtension<THostContext, TBody>;
134
+ /** Optional replacement for the built-in annotation body editor. */
135
+ bodyEditor?: AnnotationBodyEditor<THostContext, TBody>;
136
+ /** Optional UI chrome and built-in body editor knobs. */
137
+ ui?: AnnotationEditorUiConfig;
138
+ /** Optional hook to prefill a new annotation before its first save */
139
+ prepareAnnotation?: (annotation: W3CAnnotation<TBody>) => W3CAnnotation<TBody>;
140
+ /** Optional gate for whether new annotations can be created right now */
141
+ canCreateAnnotation?: () => boolean;
142
+ /** Optional status message explaining why creation is unavailable */
143
+ getCreateDisabledReason?: () => string | null;
144
+ /**
145
+ * Motivation stamped onto new annotations that don't already carry one.
146
+ * Defaults to `'commenting'`. A host-set `motivation` (or one applied by
147
+ * `extension.beforeSave`) is never overwritten.
148
+ */
149
+ defaultMotivation?: string;
150
+ /**
151
+ * Called when a persistence operation fails. The plugin has already rolled
152
+ * back its optimistic cache/display changes and re-signalled selection; the
153
+ * host decides how to surface the failure and may call `retry()` to re-run
154
+ * the exact failed operation. When omitted, the plugin logs to the console
155
+ * and shows a dismissible error line in the panel so failures are never
156
+ * invisible (F20).
157
+ */
158
+ onPersistenceError?: (error: AnnotationPersistenceError) => void;
159
+ }
160
+ export type DrawingTool = 'rectangle' | 'polygon' | 'point';
161
+ /** W3C Annotation Body */
162
+ export interface W3CAnnotationBody {
163
+ type?: string;
164
+ purpose?: string;
165
+ value?: string;
166
+ format?: string;
167
+ language?: string;
168
+ creator?: {
169
+ id?: string;
170
+ name?: string;
171
+ };
172
+ created?: string;
173
+ modified?: string;
174
+ }
175
+ /** Standard W3C purposes for autocomplete */
176
+ export declare const W3C_PURPOSES: readonly ["commenting", "tagging", "describing", "classifying", "identifying", "linking", "bookmarking", "highlighting", "questioning", "replying"];
177
+ export type W3CPurpose = (typeof W3C_PURPOSES)[number];
@@ -0,0 +1,12 @@
1
+ export type IiifTargetBounds = [number, number, number, number];
2
+ export type NormalizedIiifTarget = {
3
+ raw: unknown;
4
+ targetId: string | null;
5
+ canvasId: string | null;
6
+ selectors: any[];
7
+ xywh: IiifTargetBounds | null;
8
+ };
9
+ export declare function parseIiifXywh(value: string): IiifTargetBounds | null;
10
+ export declare function getIiifCanvasId(targetId: string): string | null;
11
+ export declare function extractIiifTargetId(target: unknown): string | null;
12
+ export declare function normalizeIiifTargets(target: unknown): NormalizedIiifTarget[];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared utility for resolving IIIF language map values.
3
+ *
4
+ * IIIF v3 uses language maps: `{ "en": ["Hello"], "fr": ["Bonjour"] }`
5
+ * Manifesto returns arrays of `{ value, locale/language }` objects.
6
+ * IIIF v2 may use plain strings.
7
+ *
8
+ * This module provides a single resolution strategy used across the viewer.
9
+ */
10
+ /**
11
+ * Resolve a IIIF language-mapped value to a single display string.
12
+ *
13
+ * Precedence: preferredLocale → 'en' → 'none'/unset → first available.
14
+ */
15
+ export declare function resolveLanguageValue(value: unknown, preferredLocale?: string): string;
16
+ /**
17
+ * Resolve a IIIF language-mapped value to all display strings
18
+ * (for multi-value properties like metadata values with multiple entries
19
+ * in a single language).
20
+ */
21
+ export declare function resolveAllLanguageValues(value: unknown, preferredLocale?: string): string[];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared point-marker styling for the annotation editor. A point looks the same
3
+ * whether it is rendered read-only (OSDViewer overlay), selected, or edited, so
4
+ * the radius/fill/stroke live in one place consumed by both the viewer overlay
5
+ * and the editor's Annotorious styling (spec §3.4).
6
+ */
7
+ export interface PointStyle {
8
+ /** Marker radius in screen (CSS) pixels. */
9
+ radius?: number;
10
+ /** Marker fill colour (any CSS colour the consumer renders). */
11
+ fill?: string;
12
+ /** Marker stroke colour. */
13
+ stroke?: string;
14
+ /** Marker stroke width in pixels. */
15
+ strokeWidth?: number;
16
+ }
17
+ /**
18
+ * Default marker radius in screen pixels. Chosen so the diameter (2 × radius)
19
+ * equals the historical `POINT_MARKER_SIZE = 10` the read-only overlay used, so
20
+ * existing viewers render unchanged when no `pointStyle` is configured.
21
+ */
22
+ export declare const DEFAULT_POINT_RADIUS = 5;
23
+ /**
24
+ * Resolve the effective marker radius (screen pixels) from a `pointStyle`
25
+ * config, falling back to {@link DEFAULT_POINT_RADIUS} when unset or invalid.
26
+ */
27
+ export declare function resolvePointRadius(pointStyle?: PointStyle | null): number;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Cross-realm reactivity bridge for the plugin's Svelte UI.
3
+ *
4
+ * Core's `ViewerState` is compiled by CORE's Svelte runtime; when the plugin (its
5
+ * OWN Svelte runtime) reads `viewerState.canvasId` inside an `$effect`, no
6
+ * dependency is registered because the two reactivity graphs don't cross. The
7
+ * plugin therefore mirrors the handful of fields its UI reacts to
8
+ * (`manifestId`, `canvasId`, `osdViewer`) into plugin-runtime `$state`, kept in
9
+ * sync through the framework-neutral `ViewerState.subscribe` fan-out. Every other
10
+ * member/method (queries, display sync, the annotation-edit bus, the style root)
11
+ * delegates straight to the real state, so a mirror is a drop-in `ViewerState`
12
+ * for the controller, loader, manager, and store.
13
+ */
14
+ import type { ViewerState } from 'triiiceratops';
15
+ export declare class ViewerStateMirror {
16
+ #private;
17
+ manifestId: string | null;
18
+ canvasId: string | null;
19
+ osdViewer: import("openseadragon").Viewer | null;
20
+ constructor(real: ViewerState);
21
+ /** The per-viewer annotation-edit bus (mutated in place by the controller). */
22
+ get annotationEditBus(): ViewerState['annotationEditBus'];
23
+ getCanvases(manifestId: string, sequenceIndex?: number): unknown[];
24
+ getUserAnnotations(manifestId: string, canvasId: string): unknown[];
25
+ setUserAnnotations(manifestId: string, canvasId: string, annotations: unknown[]): void;
26
+ clearUserAnnotations(manifestId: string, canvasId: string): void;
27
+ getStyleRoot(): Document | ShadowRoot | null;
28
+ /** Drop the bridge's `ViewerState.subscribe` registration. */
29
+ destroy(): void;
30
+ }
31
+ /** Build a mirror and expose it typed as a `ViewerState` for the plugin UI. */
32
+ export declare function createViewerStateMirror(real: ViewerState): {
33
+ mirror: ViewerState;
34
+ destroy: () => void;
35
+ };
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@triiiceratops/plugin-annotation-editor",
3
+ "version": "1.0.0-rc.1",
4
+ "type": "module",
5
+ "description": "Annotation editor plugin for the triiiceratops IIIF viewer, authored on @triiiceratops/plugin-sdk.",
6
+ "license": "MIT",
7
+ "author": "David Flood",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/d-flood/triiiceratops.git",
11
+ "directory": "packages/plugin-annotation-editor"
12
+ },
13
+ "homepage": "https://d-flood.github.io/triiiceratops/",
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "provenance": true
17
+ },
18
+ "private": false,
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "sideEffects": [
23
+ "./dist/iife.js"
24
+ ],
25
+ "files": [
26
+ "dist",
27
+ "LICENSE"
28
+ ],
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ },
37
+ "./testing": {
38
+ "types": "./dist/testing/index.d.ts",
39
+ "import": "./dist/testing/index.js"
40
+ },
41
+ "./iife": "./dist/iife.js"
42
+ },
43
+ "scripts": {
44
+ "build": "pnpm build:esm && pnpm build:iife && pnpm build:types",
45
+ "build:esm": "BUILD_FORMAT=es vite build",
46
+ "build:iife": "BUILD_FORMAT=iife vite build",
47
+ "build:types": "tsc -p tsconfig.build.json",
48
+ "check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings && tsc -p tsconfig.build.json --noEmit",
49
+ "test": "vitest run",
50
+ "clean": "rm -rf dist",
51
+ "test:coverage": "vitest run --coverage",
52
+ "lint": "eslint . --max-warnings 0",
53
+ "lint:fix": "eslint . --fix --max-warnings 0"
54
+ },
55
+ "peerDependencies": {
56
+ "triiiceratops": "workspace:^",
57
+ "@triiiceratops/plugin-sdk": "workspace:^"
58
+ },
59
+ "dependencies": {
60
+ "@annotorious/annotorious": "^3.7.19",
61
+ "@annotorious/openseadragon": "^3.7.19",
62
+ "openseadragon": "^5.0.1"
63
+ },
64
+ "devDependencies": {
65
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
66
+ "@triiiceratops/plugin-sdk": "workspace:*",
67
+ "@triiiceratops/ui": "workspace:*",
68
+ "@types/node": "^24.10.1",
69
+ "@types/openseadragon": "^5.0.1",
70
+ "@vitest/coverage-v8": "4.0.15",
71
+ "svelte": "5.45.5",
72
+ "svelte-check": "^4.3.4",
73
+ "triiiceratops": "workspace:*",
74
+ "typescript": "~5.9.3",
75
+ "vite": "^6.0.0",
76
+ "vitest": "^4.0.15"
77
+ }
78
+ }