@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +201 -0
- package/README.md +56 -0
- package/dist/auto-capture-options.d.ts +78 -0
- package/dist/editor/arrow-markers.d.ts +142 -0
- package/dist/editor/bake-translate.d.ts +192 -0
- package/dist/editor/font-registry.d.ts +58 -0
- package/dist/editor/gradient-utils.d.ts +23 -0
- package/dist/editor/history-core.d.ts +48 -0
- package/dist/editor/icons/brand-icons.d.ts +53 -0
- package/dist/editor/icons/material-symbols.d.ts +105 -0
- package/dist/editor/icons/registry.d.ts +179 -0
- package/dist/editor/icons/render.d.ts +22 -0
- package/dist/editor/icons/sanitize.d.ts +60 -0
- package/dist/editor/index.d.ts +13 -0
- package/dist/editor/path-utils.d.ts +32 -0
- package/dist/editor/property-schema.d.ts +263 -0
- package/dist/editor/rich-text-mapper.d.ts +8 -0
- package/dist/editor/selection-geometry.d.ts +66 -0
- package/dist/editor/shape-utils.d.ts +27 -0
- package/dist/editor/svg-format.d.ts +68 -0
- package/dist/editor/svg-id-utils.d.ts +37 -0
- package/dist/editor/svg-to-annotation-shapes.d.ts +34 -0
- package/dist/editor/text-utils.d.ts +212 -0
- package/dist/editor/tool-lifecycle.d.ts +56 -0
- package/dist/editor/tool-options.d.ts +126 -0
- package/dist/editor/tool-panel-adapter.d.ts +105 -0
- package/dist/editor/tool-preset-serde.d.ts +43 -0
- package/dist/editor/tool-registry.d.ts +320 -0
- package/dist/editor/tool-style-reader.d.ts +36 -0
- package/dist/editor/tool-style-writer.d.ts +33 -0
- package/dist/editor/toolbar-icons.d.ts +84 -0
- package/dist/editor/transform-utils.d.ts +127 -0
- package/dist/editor/viewport-math.d.ts +69 -0
- package/dist/encode/index.d.ts +4 -0
- package/dist/encode/options.d.ts +79 -0
- package/dist/encode/png8.d.ts +10 -0
- package/dist/headless.d.ts +29 -0
- package/dist/icons/index.d.ts +15 -0
- package/dist/icons/types.d.ts +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3164 -0
- package/dist/storage/errors.d.ts +59 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/metadata-cache.d.ts +231 -0
- package/dist/storage/path.d.ts +49 -0
- package/dist/storage/thumbnail-cache.d.ts +110 -0
- package/dist/storage/thumbnail.d.ts +31 -0
- package/dist/storage/types.d.ts +677 -0
- package/dist/utils/assert.d.ts +31 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/dash-utils.d.ts +6 -0
- package/dist/utils/desktop-bridge.d.ts +349 -0
- package/dist/utils/filename.d.ts +48 -0
- package/dist/utils/id.d.ts +21 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/types.d.ts +20 -0
- package/dist/xmp/xmp-browser.d.ts +39 -0
- package/dist/zip/zip-builder.d.ts +8 -0
- package/dist/zip/zip-bytes.d.ts +22 -0
- package/package.json +58 -0
- package/styles/editor.css +1912 -0
- package/styles/fonts.css +46 -0
- package/styles/property-panel.css +779 -0
- 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;
|