@ingcreators/annot-core 0.1.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +201 -0
  3. package/README.md +56 -0
  4. package/dist/auto-capture-options.d.ts +78 -0
  5. package/dist/editor/arrow-markers.d.ts +142 -0
  6. package/dist/editor/bake-translate.d.ts +192 -0
  7. package/dist/editor/font-registry.d.ts +58 -0
  8. package/dist/editor/gradient-utils.d.ts +23 -0
  9. package/dist/editor/history-core.d.ts +48 -0
  10. package/dist/editor/icons/brand-icons.d.ts +53 -0
  11. package/dist/editor/icons/material-symbols.d.ts +105 -0
  12. package/dist/editor/icons/registry.d.ts +179 -0
  13. package/dist/editor/icons/render.d.ts +22 -0
  14. package/dist/editor/icons/sanitize.d.ts +60 -0
  15. package/dist/editor/index.d.ts +13 -0
  16. package/dist/editor/path-utils.d.ts +32 -0
  17. package/dist/editor/property-schema.d.ts +263 -0
  18. package/dist/editor/rich-text-mapper.d.ts +8 -0
  19. package/dist/editor/selection-geometry.d.ts +66 -0
  20. package/dist/editor/shape-utils.d.ts +27 -0
  21. package/dist/editor/svg-format.d.ts +68 -0
  22. package/dist/editor/svg-id-utils.d.ts +37 -0
  23. package/dist/editor/svg-to-annotation-shapes.d.ts +34 -0
  24. package/dist/editor/text-utils.d.ts +212 -0
  25. package/dist/editor/tool-lifecycle.d.ts +56 -0
  26. package/dist/editor/tool-options.d.ts +126 -0
  27. package/dist/editor/tool-panel-adapter.d.ts +105 -0
  28. package/dist/editor/tool-preset-serde.d.ts +43 -0
  29. package/dist/editor/tool-registry.d.ts +320 -0
  30. package/dist/editor/tool-style-reader.d.ts +36 -0
  31. package/dist/editor/tool-style-writer.d.ts +33 -0
  32. package/dist/editor/toolbar-icons.d.ts +84 -0
  33. package/dist/editor/transform-utils.d.ts +127 -0
  34. package/dist/editor/viewport-math.d.ts +69 -0
  35. package/dist/encode/index.d.ts +4 -0
  36. package/dist/encode/options.d.ts +79 -0
  37. package/dist/encode/png8.d.ts +10 -0
  38. package/dist/headless.d.ts +29 -0
  39. package/dist/icons/index.d.ts +15 -0
  40. package/dist/icons/types.d.ts +108 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +3164 -0
  43. package/dist/storage/errors.d.ts +59 -0
  44. package/dist/storage/index.d.ts +6 -0
  45. package/dist/storage/metadata-cache.d.ts +231 -0
  46. package/dist/storage/path.d.ts +49 -0
  47. package/dist/storage/thumbnail-cache.d.ts +110 -0
  48. package/dist/storage/thumbnail.d.ts +31 -0
  49. package/dist/storage/types.d.ts +677 -0
  50. package/dist/utils/assert.d.ts +31 -0
  51. package/dist/utils/constants.d.ts +10 -0
  52. package/dist/utils/dash-utils.d.ts +6 -0
  53. package/dist/utils/desktop-bridge.d.ts +349 -0
  54. package/dist/utils/filename.d.ts +48 -0
  55. package/dist/utils/id.d.ts +21 -0
  56. package/dist/utils/index.d.ts +5 -0
  57. package/dist/utils/types.d.ts +20 -0
  58. package/dist/xmp/xmp-browser.d.ts +39 -0
  59. package/dist/zip/zip-builder.d.ts +8 -0
  60. package/dist/zip/zip-bytes.d.ts +22 -0
  61. package/package.json +58 -0
  62. package/styles/editor.css +1912 -0
  63. package/styles/fonts.css +46 -0
  64. package/styles/property-panel.css +779 -0
  65. package/styles/toolbar.css +673 -0
@@ -0,0 +1,677 @@
1
+ /**
2
+ * Shared storage types used by Extension, Web app, and Desktop.
3
+ *
4
+ * Images and folders are identified by their filesystem-style path.
5
+ * Root folder is represented by the empty string "".
6
+ */
7
+ export interface ImageRecord {
8
+ /** Primary key: full path, e.g. "Screenshots/Mobile/image-123.png". */
9
+ path: string;
10
+ /** Parent folder path. Derived from `path` but stored for efficient indexing. */
11
+ folderPath: string;
12
+ originalDataUrl: string;
13
+ thumbnailDataUrl: string;
14
+ annotationsSvg: string;
15
+ width: number;
16
+ height: number;
17
+ sourceUrl: string;
18
+ tags: Record<string, string>;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ /** Optional DOM structure metadata captured alongside the screenshot
22
+ * by the browser extension. Enables "click the Submit button →
23
+ * auto-annotate" features in the editor. Undefined when the
24
+ * capture came from a non-DOM source (desktop screenshot, paste,
25
+ * etc.). Shape may evolve; see `PageMetadata.version`. */
26
+ pageMetadata?: PageMetadata;
27
+ }
28
+ /**
29
+ * In-place updates allowed via `updateImage`. Note that `folderPath`
30
+ * is intentionally NOT in this set — moving an image to a different
31
+ * folder goes through {@link StorageProvider.moveImage}, which has
32
+ * a clearer contract (returns the new path; `updateImage` doesn't).
33
+ *
34
+ * `thumbnailDataUrl` is also intentionally absent — thumbnail bytes
35
+ * are owned by the host-side `ThumbnailManager` (see
36
+ * [`docs/plans/_done/unified-thumbnail-cache.md`](../../../../docs/plans/_done/unified-thumbnail-cache.md));
37
+ * callers seed the cache via `tm.write(provider, path, dataUrl,
38
+ * dims)` rather than going through `updateImage`.
39
+ *
40
+ * `originalDataUrl` is included so callers that mutate the
41
+ * underlying bitmap — currently the redact-burn-into-image
42
+ * `EditorShell.applyAllRedactions` path
43
+ * ([`_done/redact-burn-into-image.md`](../../../../docs/plans/_done/redact-burn-into-image.md))
44
+ * and the destructive crop path (`EditorShell.applyCrop`) — can
45
+ * persist the new bytes alongside the matching annotation SVG.
46
+ * Backends that re-encode the file on save (XMP-based stores:
47
+ * DeviceStore, DesktopStore, GoogleDriveStore, GitHubStore) MUST
48
+ * honor a non-undefined `updates.originalDataUrl` by feeding it
49
+ * into the file rebuild instead of the storage's cached / on-disk
50
+ * value. Backends that store the bitmap separately (BrowserStore
51
+ * via IDB) just `Object.assign` it onto the record, which the
52
+ * next put writes back. Including this field on a normal
53
+ * annotation save (no bitmap mutation) is unnecessary and — for
54
+ * network-backed stores — wasteful, so the field is OPT-IN: leave
55
+ * it undefined unless the bitmap actually changed.
56
+ *
57
+ * `width` / `height` are included for the same reason — the
58
+ * destructive crop path replaces the bitmap with a smaller one
59
+ * AND records the new pixel dimensions so the next reload
60
+ * reconstructs the canvas at the cropped size. Like
61
+ * `originalDataUrl` they are OPT-IN: only set when the bitmap
62
+ * dimensions actually changed.
63
+ */
64
+ export type ImageRecordUpdate = Partial<Pick<ImageRecord, "annotationsSvg" | "tags" | "updatedAt" | "originalDataUrl" | "width" | "height">>;
65
+ export interface PageMetadata {
66
+ /** Schema version — bump on breaking changes so consumers can
67
+ * gracefully handle old records. Current: 1. */
68
+ version: 1;
69
+ /** Source URL at capture time (also stored on ImageRecord.sourceUrl
70
+ * but duplicated here so metadata is self-contained). */
71
+ url: string;
72
+ /** Viewport size at capture time, in CSS pixels (not device pixels).
73
+ * Used to compute the scale factor when mapping bboxes onto the
74
+ * screenshot (which is in device pixels). */
75
+ viewport: {
76
+ width: number;
77
+ height: number;
78
+ };
79
+ /** `window.devicePixelRatio` at capture time. */
80
+ devicePixelRatio: number;
81
+ /** For scroll / per-page captures: the scroll offset at capture
82
+ * time. Single-viewport captures use `{ x: 0, y: 0 }`. Bboxes in
83
+ * `elements` are in document coordinates; subtract this offset +
84
+ * add again per-segment for stitched captures. */
85
+ scrollOffset: {
86
+ x: number;
87
+ y: number;
88
+ };
89
+ /** The rectangle of the document that the SCREENSHOT actually
90
+ * covers, in CSS pixels, document coordinates. Used by the editor
91
+ * to (a) FILTER `elements` to those visible in the screenshot
92
+ * and (b) MAP element bboxes from doc coords → screenshot coords
93
+ * via `(elBbox - captureRect.origin) * devicePixelRatio`.
94
+ * - visible capture: equals viewport at capture time (scrollX/Y +
95
+ * viewport size)
96
+ * - area capture: the user-selected sub-region, offset by scroll
97
+ * - scroll / per-page: full document (or per-segment) */
98
+ captureRect: {
99
+ x: number;
100
+ y: number;
101
+ width: number;
102
+ height: number;
103
+ };
104
+ /** ISO timestamp of when the metadata snapshot was taken (usually
105
+ * within a few ms of the screenshot). */
106
+ capturedAt: string;
107
+ /** Detected elements. Filtered to interactive / labeled items (see
108
+ * `interactiveRole` predicate in the content script). */
109
+ elements: PageElement[];
110
+ }
111
+ export interface PageElement {
112
+ /** Stable id within this capture — references across annotations. */
113
+ id: string;
114
+ /** Tag name in lowercase (e.g. "button", "a", "input"). */
115
+ tag: string;
116
+ /** ARIA role or implicit role ("button", "link", "textbox", ...). */
117
+ role?: string;
118
+ /** Visible text — textContent for button/link, label text for input. */
119
+ text?: string;
120
+ /** `aria-label` — explicit accessibility label, prefer over `text`
121
+ * when present since it's the a11y ground truth. */
122
+ ariaLabel?: string;
123
+ /** For form inputs: the input type (text / email / submit / …). */
124
+ inputType?: string;
125
+ /** For inputs: placeholder text. */
126
+ placeholder?: string;
127
+ /** For links: the href destination. */
128
+ href?: string;
129
+ /** Element id attribute (if any) — useful for stable selection. */
130
+ domId?: string;
131
+ /** Bounding box in DOCUMENT (not viewport) coordinates at capture
132
+ * time, in CSS pixels. To draw on the screenshot: multiply by
133
+ * devicePixelRatio. [x, y, width, height]. */
134
+ bbox: [number, number, number, number];
135
+ /** CSS selector (best effort) for re-locating the element later. */
136
+ selector?: string;
137
+ /** True if element was (partially) visible at capture time. */
138
+ visible: boolean;
139
+ }
140
+ export interface FolderRecord {
141
+ /** Primary key: full path, e.g. "Screenshots/Mobile". */
142
+ path: string;
143
+ /** Parent folder path. Derived from `path` but stored for efficient indexing. */
144
+ parentPath: string;
145
+ /** Last segment of `path`. */
146
+ name: string;
147
+ createdAt: string;
148
+ }
149
+ /**
150
+ * Abstract storage provider — implemented by:
151
+ * - IndexedDB (browser-extension, web-annotation local mode)
152
+ * - Extension API bridge (web-annotation when extension is installed)
153
+ * - File System Access API (web-annotation direct FS mode)
154
+ * - Google Drive API (web-annotation Drive mode)
155
+ *
156
+ * The interface is **capability-narrowed**: only methods every backend
157
+ * implements live here. Optional behaviours (resync, token refresh,
158
+ * force-refresh of cached state) live on separate `StorageWith*`
159
+ * interfaces below; use the matching `supports*()` type predicate to
160
+ * narrow before calling.
161
+ *
162
+ * ## Error contract
163
+ *
164
+ * Read methods (`getImage`, `getFolder`, `listImages`, `listFolders`,
165
+ * `getBreadcrumb`) return `undefined` / `[]` for missing paths
166
+ * rather than throwing — "missing" is not a discriminable error
167
+ * here, so callers handle the absence directly. Update / delete
168
+ * methods are idempotent on missing source: `updateImage`,
169
+ * `deleteImage`, and `deleteFolder` return silently when the target
170
+ * path doesn't exist. `saveImage` and `moveImage` auto-uniquify
171
+ * collisions with " (2)", " (3)" suffixes and never throw on
172
+ * conflict.
173
+ *
174
+ * Mutation methods that DO throw discriminate failures via the
175
+ * `StorageError` hierarchy in
176
+ * `@ingcreators/annot-core/storage/errors`:
177
+ *
178
+ * - `StorageConflictError` — a path collision the backend
179
+ * can't auto-resolve. `createFolder` throws on duplicate name;
180
+ * `renameImage`, `renameFolder`, and `moveFolder` throw on
181
+ * destination collision (they don't auto-uniquify because the
182
+ * caller picked the new name explicitly, so the conflict is
183
+ * surfaced for them to retry / prompt).
184
+ * - `StorageNotFoundError` — a `rename*` / `move*` couldn't
185
+ * find its source path. The idempotent methods above don't
186
+ * throw this.
187
+ * - `StoragePermissionError` — backend rejected the op for auth
188
+ * / ACL reasons (expired token, revoked scope, FSA permission
189
+ * lapse). Any mutating method may throw this.
190
+ * - `StorageQuotaError` — backend reports out-of-space or
191
+ * out-of-quota. Any mutating method may throw this.
192
+ *
193
+ * Other failure modes (network errors, parse errors, generic IO
194
+ * errors) surface as plain `Error` and are NOT captured by
195
+ * `instanceof StorageError`. Backend-specific subclasses
196
+ * (`GitHubRateLimitError`, `DriveAuthError`, etc.) live inside each
197
+ * backend file and don't extend the shared `StorageError` hierarchy
198
+ * — they remain backend-internal concerns.
199
+ *
200
+ * Callers that want to react to a structured failure should
201
+ * `instanceof`-check the subclass; never substring-match on
202
+ * `e.message`.
203
+ */
204
+ export interface StorageProvider {
205
+ /**
206
+ * Save a new image. The `record` carries every field of the
207
+ * saved entity except its path (which the store assigns). When
208
+ * `opts.filename` is provided the store uses it as the suggested
209
+ * leaf name; when omitted the store picks one
210
+ * (e.g. `image-<timestamp>.png`). On collision the store
211
+ * uniquifies with " (2)", " (3)" suffixes — `saveImage` NEVER
212
+ * throws `StorageConflictError`; the returned path IS the
213
+ * post-uniquification path the caller should hand to subsequent
214
+ * reads / writes.
215
+ *
216
+ * @returns the actual path assigned (post-uniquification).
217
+ * @throws `StoragePermissionError` when the backend rejects the
218
+ * write for auth / ACL reasons (expired GitHub token, revoked
219
+ * Drive scope, FSA permission lapse).
220
+ * @throws `StorageQuotaError` when the backend reports
221
+ * out-of-space or out-of-quota.
222
+ * @throws `Error` for unstructured backend / IO / network
223
+ * failures.
224
+ */
225
+ saveImage(record: Omit<ImageRecord, "path">, opts?: {
226
+ filename?: string;
227
+ }): Promise<string>;
228
+ /**
229
+ * Read a single image by path. Missing is not an error here —
230
+ * callers get `undefined` and handle it directly without
231
+ * `try` / `catch`.
232
+ *
233
+ * @returns the image record, or `undefined` if no image exists
234
+ * at that path.
235
+ * @throws `Error` for backend / IO / parse failures (e.g. an
236
+ * on-disk file is corrupt, a network request fails).
237
+ */
238
+ getImage(path: string): Promise<ImageRecord | undefined>;
239
+ /**
240
+ * List images directly inside `folderPath`. Use `""` for the
241
+ * root folder. Does NOT recurse into subfolders.
242
+ *
243
+ * @returns image records for that folder. Returns `[]` when the
244
+ * folder is empty or missing — missing folders are not an
245
+ * error here.
246
+ * @throws `Error` for backend / IO / parse failures.
247
+ */
248
+ listImages(folderPath: string): Promise<ImageRecord[]>;
249
+ /**
250
+ * In-place update of an existing image's annotations / tags /
251
+ * thumbnail / `updatedAt`. Path stays the same — to relocate
252
+ * the image, use {@link moveImage} or {@link renameImage}.
253
+ *
254
+ * Idempotent on missing source: returns silently when no image
255
+ * exists at `path`. Callers that must distinguish "updated" from
256
+ * "no-such-image" should `getImage` first.
257
+ *
258
+ * @throws `StoragePermissionError` for backend auth / ACL
259
+ * rejection.
260
+ * @throws `StorageQuotaError` when the backend reports
261
+ * out-of-space.
262
+ * @throws `Error` for unstructured backend / IO failures.
263
+ */
264
+ updateImage(path: string, updates: ImageRecordUpdate): Promise<void>;
265
+ /**
266
+ * Move an image to `newFolderPath`. Filename is preserved; only
267
+ * the parent-folder portion of the path changes. The destination
268
+ * folder must already exist (callers create it first via
269
+ * {@link createFolder}). On collision (a file with the same name
270
+ * already exists at the destination) the store auto-uniquifies
271
+ * with " (2)", " (3)" suffixes — `moveImage` NEVER throws
272
+ * `StorageConflictError`. No-op (returns the original path) when
273
+ * `newFolderPath` matches the current folder.
274
+ *
275
+ * @returns the new path.
276
+ * @throws `StorageNotFoundError` when no image exists at `path`.
277
+ * @throws `StoragePermissionError` for backend auth / ACL
278
+ * rejection.
279
+ * @throws `Error` for unstructured backend / IO failures.
280
+ */
281
+ moveImage(path: string, newFolderPath: string): Promise<string>;
282
+ /**
283
+ * Rename an image in place. Folder portion of the path stays the
284
+ * same; only the leaf filename changes. No-op (returns the
285
+ * original path) when `newName` matches the current filename.
286
+ *
287
+ * Unlike {@link moveImage}, `renameImage` does NOT auto-uniquify
288
+ * — the caller picked the new name explicitly, so a conflict is
289
+ * surfaced for them to handle (e.g. show a "name taken, choose
290
+ * another?" prompt).
291
+ *
292
+ * @returns the new path.
293
+ * @throws `StorageNotFoundError` when no image exists at `path`.
294
+ * @throws `StorageConflictError` when an image already exists at
295
+ * the renamed path.
296
+ * @throws `StoragePermissionError` for backend auth / ACL
297
+ * rejection.
298
+ * @throws `Error` for unstructured backend / IO failures.
299
+ */
300
+ renameImage(path: string, newName: string): Promise<string>;
301
+ /**
302
+ * Delete an image.
303
+ *
304
+ * Idempotent on missing source: returns silently when no image
305
+ * exists at `path`. Callers that must distinguish "deleted" from
306
+ * "no-such-image" should `getImage` first.
307
+ *
308
+ * @throws `StoragePermissionError` for backend auth / ACL
309
+ * rejection.
310
+ * @throws `Error` for unstructured backend / IO failures.
311
+ */
312
+ deleteImage(path: string): Promise<void>;
313
+ /**
314
+ * Create a folder named `name` directly under `parentPath`. Use
315
+ * `""` for `parentPath` to create at the root.
316
+ *
317
+ * Unlike {@link saveImage}, `createFolder` does NOT auto-uniquify
318
+ * — the caller picked the name explicitly, so a conflict is
319
+ * surfaced for them to handle.
320
+ *
321
+ * @returns the new folder's full path.
322
+ * @throws `StorageConflictError` when a folder named `name`
323
+ * already exists under `parentPath`.
324
+ * @throws `StoragePermissionError` for backend auth / ACL
325
+ * rejection.
326
+ * @throws `Error` for unstructured backend / IO failures,
327
+ * including `parentPath` not existing on backends that
328
+ * validate it.
329
+ */
330
+ createFolder(parentPath: string, name: string): Promise<string>;
331
+ /**
332
+ * List subfolders directly inside `parentPath`. Use `""` for the
333
+ * root folder. Does NOT recurse.
334
+ *
335
+ * @returns folder records sorted alphabetically by name. Returns
336
+ * `[]` when the folder is empty or missing — missing folders
337
+ * are not an error here.
338
+ * @throws `Error` for backend / IO failures.
339
+ */
340
+ listFolders(parentPath: string): Promise<FolderRecord[]>;
341
+ /**
342
+ * Read a folder record by path. The root folder (`""`) is NOT a
343
+ * record — `getFolder("")` returns `undefined` by contract.
344
+ *
345
+ * @returns the folder record, or `undefined` if no folder exists
346
+ * at that path. Missing paths are not an error here.
347
+ * @throws `Error` for backend / IO failures.
348
+ */
349
+ getFolder(path: string): Promise<FolderRecord | undefined>;
350
+ /**
351
+ * Rename a folder in place. Parent stays the same; only the leaf
352
+ * name changes. All descendant image and folder paths are
353
+ * rewritten to share the new prefix. No-op (returns the original
354
+ * path) when `newName` matches the current name.
355
+ *
356
+ * Does NOT auto-uniquify (same rationale as {@link renameImage}).
357
+ *
358
+ * @returns the new folder path.
359
+ * @throws `StorageNotFoundError` when no folder exists at `path`.
360
+ * @throws `StorageConflictError` when a folder with the new name
361
+ * already exists alongside the source.
362
+ * @throws `StoragePermissionError` for backend auth / ACL
363
+ * rejection.
364
+ * @throws `Error` for unstructured backend / IO failures.
365
+ */
366
+ renameFolder(path: string, newName: string): Promise<string>;
367
+ /**
368
+ * Move a folder to a new parent. Leaf name stays the same; only
369
+ * the parent portion of the path changes. All descendant image
370
+ * and folder paths are rewritten to share the new prefix. No-op
371
+ * (returns the original path) when `newParentPath` matches the
372
+ * current parent.
373
+ *
374
+ * Does NOT auto-uniquify (same rationale as {@link renameImage}).
375
+ *
376
+ * @returns the new folder path.
377
+ * @throws `StorageNotFoundError` when no folder exists at `path`.
378
+ * @throws `StorageConflictError` when a folder with the same
379
+ * leaf name already exists under `newParentPath`.
380
+ * @throws `StoragePermissionError` for backend auth / ACL
381
+ * rejection.
382
+ * @throws `Error` for unstructured backend / IO failures.
383
+ */
384
+ moveFolder(path: string, newParentPath: string): Promise<string>;
385
+ /**
386
+ * Recursively delete a folder and every image / subfolder under
387
+ * it.
388
+ *
389
+ * Idempotent on missing source: returns silently when no folder
390
+ * exists at `path`.
391
+ *
392
+ * @throws `StoragePermissionError` for backend auth / ACL
393
+ * rejection.
394
+ * @throws `Error` for unstructured backend / IO failures.
395
+ */
396
+ deleteFolder(path: string): Promise<void>;
397
+ /**
398
+ * Read the chain of folder records from the root (exclusive)
399
+ * down to `path` (inclusive). Used for breadcrumb UIs.
400
+ *
401
+ * @returns one record per existing ancestor, in root-to-leaf
402
+ * order. Returns `[]` for the root (`""`). Missing intermediate
403
+ * ancestors are silently skipped — `getBreadcrumb` never
404
+ * throws on a missing path.
405
+ * @throws `Error` for backend / IO failures.
406
+ */
407
+ getBreadcrumb(path: string): Promise<FolderRecord[]>;
408
+ }
409
+ /**
410
+ * Re-scan underlying storage for external changes. Useful for stores
411
+ * whose backing state can mutate behind our back — local filesystems
412
+ * (changes from another editor) and network-backed stores (changes
413
+ * pushed from another client).
414
+ */
415
+ export interface StorageWithResync {
416
+ resync(): Promise<void>;
417
+ }
418
+ /**
419
+ * Force-refresh cached state from the source of truth, bypassing any
420
+ * local cache. Stronger than `resync()`: where `resync` typically
421
+ * picks up incremental changes, `forceRefresh` invalidates everything
422
+ * the store knows about and re-fetches.
423
+ */
424
+ export interface StorageWithForceRefresh {
425
+ forceRefresh(): Promise<void>;
426
+ }
427
+ /**
428
+ * Token-management hooks for network-backed stores. Bundles two
429
+ * conceptually-related operations the host may need to invoke:
430
+ *
431
+ * - `setToken(token)` directly injects a fresh access token —
432
+ * used after the host performs a silent refresh outside the
433
+ * store's own 401 path.
434
+ * - `setTokenRefresher(fn)` registers the host's 401 recovery
435
+ * callback. The refresher resolves to a new token string, or
436
+ * `null` if the user dismissed the auth banner / declined to
437
+ * re-auth. The store retries the failed request once with the
438
+ * new token and gives up if `null` came back.
439
+ *
440
+ * Today implemented by the network-backed built-ins (`GoogleDriveStore`,
441
+ * `GitHubStore`); local stores (`BrowserStore`, `DeviceStore`, the
442
+ * extension proxy) skip it. Plugin stores opt in by implementing
443
+ * this interface and calling the registered refresher from their
444
+ * own 401 path.
445
+ */
446
+ export interface StorageWithTokenRefresher {
447
+ setToken(token: string): void;
448
+ setTokenRefresher(refresher: () => Promise<string | null>): void;
449
+ }
450
+ /**
451
+ * Initialisation hook for stores whose lifecycle includes work that
452
+ * can't run in the constructor — typically because it does I/O the
453
+ * caller wants to await separately. `DeviceStore` uses this to load
454
+ * its on-disk index, run crash-recovery scans, and reconcile against
455
+ * the file tree. Stores whose construction is fully synchronous
456
+ * (Browser, GitHub, Drive) skip this capability.
457
+ */
458
+ export interface StorageWithInit {
459
+ init(): Promise<void>;
460
+ }
461
+ /**
462
+ * Rate-limit telemetry. Backends that surface a quota window (e.g.
463
+ * GitHub's `X-RateLimit-Remaining`) implement this so the host can
464
+ * render an advisory banner before requests start hard-failing.
465
+ *
466
+ * `getRateLimit()` is a synchronous read of the most recent values
467
+ * the store has observed; `setRateLimitListener` registers a
468
+ * push-notification callback the store fires when the budget drops
469
+ * below an internal threshold (at most once per reset window).
470
+ */
471
+ export interface StorageWithRateLimit {
472
+ getRateLimit(): {
473
+ remaining: number | null;
474
+ resetAt: number | null;
475
+ };
476
+ setRateLimitListener(listener: (info: {
477
+ remaining: number;
478
+ resetAt: number | null;
479
+ }) => void): void;
480
+ }
481
+ /**
482
+ * Opt into the unified thumbnail cache (the host-side
483
+ * `ThumbnailManager` + `ThumbnailCache` infrastructure introduced
484
+ * by [`docs/plans/_done/unified-thumbnail-cache.md`](../../../../docs/plans/_done/unified-thumbnail-cache.md)).
485
+ *
486
+ * Stores that implement all three methods participate in the shared
487
+ * cache: the host owns the prefetch lifecycle (in-flight dedup,
488
+ * persistence, LRU eviction, `annot-thumbnail-ready` dispatch);
489
+ * the store just answers "what's the stable key", "what's the
490
+ * version", and "where do the source bytes live".
491
+ *
492
+ * Stores that don't implement this interface continue to populate
493
+ * `record.thumbnailDataUrl` themselves (legacy inline-thumbnail
494
+ * shape). The host's `ThumbnailManager.attach` is a no-op for
495
+ * non-participating providers, so adoption is per-store.
496
+ *
497
+ * Built-in prefix conventions match the `StorageMode` strings used
498
+ * in URL handoff and storage selection — `browser:` / `device:` /
499
+ * `github:` / `googledrive:`. Plugin-registered providers must use
500
+ * the `plugin:<pluginId>:` prefix the host enforces at registration.
501
+ */
502
+ export interface StorageWithThumbnailCache {
503
+ /**
504
+ * Stable per-image identifier independent of path renames /
505
+ * collision-suffixing. Returning `undefined` opts that path out
506
+ * of the unified cache (the host treats it as a non-participating
507
+ * record for that one item).
508
+ */
509
+ thumbnailKey(path: string): string | undefined;
510
+ /**
511
+ * Opaque "version" — must change whenever the file's bytes
512
+ * change. Cache hits require a version match; mismatches trigger
513
+ * eviction + re-prefetch. Stores that don't observe external
514
+ * mutation may return `""` constant.
515
+ */
516
+ thumbnailVersion(path: string): string;
517
+ /**
518
+ * Cache-miss source fetcher. Returns the bytes the manager runs
519
+ * through `generateThumbnailFromBlob`. Implementations should
520
+ * pick the cheapest path that yields a renderable image — Drive
521
+ * and GitHub bypass the full record decode and just stream the
522
+ * raw bytes.
523
+ *
524
+ * @returns the source `Blob`, or `undefined` if the file no
525
+ * longer exists (deleted between listing and fetch).
526
+ */
527
+ fetchThumbnailSource(path: string): Promise<Blob | undefined>;
528
+ }
529
+ /**
530
+ * Persistent document records — the storage shape for `.annot.html`
531
+ * files (multi-image manuals authored via the doc shell). Sibling
532
+ * to `ImageRecord`; backends opt in via `StorageWithDocuments`.
533
+ *
534
+ * Phase 6a of [`docs/plans/_done/annot-html-document.md`](../../../../docs/plans/_done/annot-html-document.md).
535
+ * The whole plan series introducing the format lives there; the
536
+ * Tier A surface (capability + record type + predicate) lands
537
+ * before any single backend implements it so a consumer that
538
+ * narrows a `StorageProvider` to `StorageWithDocuments` gets the
539
+ * documented type-shape regardless of which implementations have
540
+ * caught up.
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * if (supportsDocuments(store)) {
545
+ * const docs = await store.listDocuments("");
546
+ * for (const doc of docs) {
547
+ * console.log(doc.title, doc.imageCount, "blocks:", doc.blockCount);
548
+ * }
549
+ * }
550
+ * ```
551
+ */
552
+ export interface DocumentRecord {
553
+ /** Primary key: full path, e.g. "Manuals/onboarding.annot.html". */
554
+ path: string;
555
+ /** Parent folder path. Derived from `path` but stored for
556
+ * efficient indexing. */
557
+ folderPath: string;
558
+ /** The on-disk `.annot.html` source. Self-contained: inlined
559
+ * CSS / fonts / images per the format spec. */
560
+ bytes: string;
561
+ /** Thumbnail of the document — strategy is implementation-
562
+ * defined. `BrowserStore` (Phase 6a) uses the first
563
+ * `ImageBlock`'s SVG rendered to a small bitmap; backends that
564
+ * opt into `StorageWithThumbnailCache` may answer this via the
565
+ * unified cache instead. Empty string when no preview is
566
+ * available. */
567
+ thumbnailDataUrl: string;
568
+ /** Document title, mirroring the JSON sidecar's `title` field
569
+ * (the format-spec contract enforces `<title>` ↔ `meta.title`
570
+ * equality on save). Cached here so listing UIs don't have to
571
+ * parse every document's bytes to render a name. */
572
+ title: string;
573
+ /** Number of image blocks in the document. Cached for the
574
+ * same reason as `title`. */
575
+ imageCount: number;
576
+ /** Number of top-level blocks. Cached for the same reason. */
577
+ blockCount: number;
578
+ createdAt: string;
579
+ updatedAt: string;
580
+ }
581
+ /**
582
+ * In-place updates allowed via `updateDocument`. Note that
583
+ * `path` and `folderPath` are intentionally NOT in this set —
584
+ * relocating a document goes through {@link
585
+ * StorageProvider.moveImage} / {@link StorageProvider.renameImage}
586
+ * (the path-keyed model already covers any leaf file).
587
+ *
588
+ * The cached metadata fields (`title`, `imageCount`, `blockCount`,
589
+ * `thumbnailDataUrl`) are part of the update set so callers that
590
+ * re-derive them on save (e.g. by re-parsing the new `bytes`) can
591
+ * keep the index consistent without a separate roundtrip.
592
+ */
593
+ export type DocumentRecordUpdate = Partial<Pick<DocumentRecord, "bytes" | "thumbnailDataUrl" | "title" | "imageCount" | "blockCount" | "updatedAt">>;
594
+ /**
595
+ * Opt into the multi-image document storage surface — the storage
596
+ * half of the `.annot.html` document format
597
+ * ([`docs/plans/_done/annot-html-document.md`](../../../../docs/plans/_done/annot-html-document.md)).
598
+ *
599
+ * Stores that implement this interface gain four document-shaped
600
+ * methods. Delete / move / rename reuse the image-side equivalents
601
+ * because the path-keyed model already covers any leaf file —
602
+ * `deleteImage("a/b.annot.html")` deletes a document just as it
603
+ * deletes an image; the discriminator is the file extension and
604
+ * the receiving backend's storage layout (separate IDB store /
605
+ * separate object key prefix / etc.). Stores MAY override that
606
+ * behaviour internally if their layout demands it; consumers see
607
+ * a uniform path-keyed surface.
608
+ *
609
+ * Stores that don't implement this interface narrow out via
610
+ * `supportsDocuments(store) === false`; document UI code branches
611
+ * on the predicate before showing document-related affordances.
612
+ */
613
+ export interface StorageWithDocuments {
614
+ /**
615
+ * Save a new document. The `record` carries every field of the
616
+ * saved entity except its path (which the store assigns). When
617
+ * `opts.filename` is provided the store uses it as the suggested
618
+ * leaf name; when omitted the store picks one (e.g.
619
+ * `document-<timestamp>.annot.html`). On collision the store
620
+ * uniquifies with " (2)", " (3)" suffixes — `saveDocument`
621
+ * NEVER throws `StorageConflictError`; the returned path IS the
622
+ * post-uniquification path.
623
+ *
624
+ * @returns the actual path assigned (post-uniquification).
625
+ * @throws `StoragePermissionError` when the backend rejects the
626
+ * write for auth / ACL reasons.
627
+ * @throws `StorageQuotaError` when the backend reports
628
+ * out-of-space or out-of-quota.
629
+ * @throws `Error` for unstructured backend / IO / network
630
+ * failures.
631
+ */
632
+ saveDocument(record: Omit<DocumentRecord, "path">, opts?: {
633
+ filename?: string;
634
+ }): Promise<string>;
635
+ /**
636
+ * Read a single document by path.
637
+ *
638
+ * @returns the document record, or `undefined` if no document
639
+ * exists at that path.
640
+ * @throws `Error` for backend / IO / parse failures.
641
+ */
642
+ getDocument(path: string): Promise<DocumentRecord | undefined>;
643
+ /**
644
+ * List documents directly inside `folderPath`. Use `""` for the
645
+ * root folder. Does NOT recurse into subfolders.
646
+ *
647
+ * @returns document records for that folder. Returns `[]` when
648
+ * the folder is empty or missing.
649
+ * @throws `Error` for backend / IO / parse failures.
650
+ */
651
+ listDocuments(folderPath: string): Promise<DocumentRecord[]>;
652
+ /**
653
+ * In-place update of an existing document's bytes / cached
654
+ * metadata / `updatedAt`. Path stays the same — to relocate
655
+ * the document, use {@link StorageProvider.moveImage} or
656
+ * {@link StorageProvider.renameImage} (path-keyed semantics
657
+ * apply uniformly).
658
+ *
659
+ * Idempotent on missing source: returns silently when no
660
+ * document exists at `path`. Callers that must distinguish
661
+ * "updated" from "no-such-document" should `getDocument` first.
662
+ *
663
+ * @throws `StoragePermissionError` for backend auth / ACL
664
+ * rejection.
665
+ * @throws `StorageQuotaError` when the backend reports
666
+ * out-of-space.
667
+ * @throws `Error` for unstructured backend / IO failures.
668
+ */
669
+ updateDocument(path: string, updates: DocumentRecordUpdate): Promise<void>;
670
+ }
671
+ export declare function supportsResync(store: StorageProvider): store is StorageProvider & StorageWithResync;
672
+ export declare function supportsForceRefresh(store: StorageProvider): store is StorageProvider & StorageWithForceRefresh;
673
+ export declare function supportsTokenRefresher(store: StorageProvider): store is StorageProvider & StorageWithTokenRefresher;
674
+ export declare function supportsInit(store: StorageProvider): store is StorageProvider & StorageWithInit;
675
+ export declare function supportsRateLimit(store: StorageProvider): store is StorageProvider & StorageWithRateLimit;
676
+ export declare function supportsThumbnailCache(store: StorageProvider): store is StorageProvider & StorageWithThumbnailCache;
677
+ export declare function supportsDocuments(store: StorageProvider): store is StorageProvider & StorageWithDocuments;