@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,59 @@
1
+ /**
2
+ * Stable discriminator for cross-backend storage error handling.
3
+ * Mirrors the `StorageError` subclass hierarchy below; callers
4
+ * that prefer a `switch` over `instanceof` can use `e.code`.
5
+ */
6
+ export type StorageErrorCode = "conflict" | "not-found" | "permission" | "quota";
7
+ /**
8
+ * Base for all `StorageProvider`-thrown errors that callers may
9
+ * want to discriminate. Generic IO / network / parse errors keep
10
+ * throwing plain `Error` and are NOT captured by `instanceof
11
+ * StorageError`.
12
+ *
13
+ * Subclasses set `name` / `code` / `path` themselves. Construct via
14
+ * the subclasses (`StorageConflictError`, `StorageNotFoundError`,
15
+ * `StoragePermissionError`, `StorageQuotaError`) — callers and
16
+ * backends never instantiate `StorageError` directly.
17
+ */
18
+ export declare class StorageError extends Error {
19
+ /** Discriminator. Set by each subclass to its matching code. */
20
+ readonly code: StorageErrorCode;
21
+ /** Path the failed operation was attempted on (image or folder). */
22
+ readonly path: string;
23
+ constructor(code: StorageErrorCode, path: string, message: string);
24
+ }
25
+ /**
26
+ * Destination path collides with an existing image or folder, and
27
+ * the operation does NOT auto-uniquify. Thrown by `createFolder`
28
+ * (duplicate name), `renameImage` / `renameFolder` / `moveFolder`
29
+ * (caller-picked name collides). `saveImage` and `moveImage`
30
+ * auto-uniquify and never throw this.
31
+ */
32
+ export declare class StorageConflictError extends StorageError {
33
+ constructor(path: string, message?: string);
34
+ }
35
+ /**
36
+ * Source path for a `rename*` / `move*` no longer exists. Read
37
+ * methods (`getImage`, `getFolder`, `listImages`, `listFolders`,
38
+ * `getBreadcrumb`) and idempotent mutations (`updateImage`,
39
+ * `deleteImage`, `deleteFolder`) return `undefined` / `[]` /
40
+ * silently instead of throwing this.
41
+ */
42
+ export declare class StorageNotFoundError extends StorageError {
43
+ constructor(path: string, message?: string);
44
+ }
45
+ /**
46
+ * Backend rejected the operation for auth / ACL reasons — expired
47
+ * GitHub token, revoked Drive scope, FSA permission lapse, etc.
48
+ * Any mutating `StorageProvider` method may throw this.
49
+ */
50
+ export declare class StoragePermissionError extends StorageError {
51
+ constructor(path: string, message?: string);
52
+ }
53
+ /**
54
+ * Backend reports out-of-space or out-of-quota. Any mutating
55
+ * `StorageProvider` method may throw this.
56
+ */
57
+ export declare class StorageQuotaError extends StorageError {
58
+ constructor(path: string, message?: string);
59
+ }
@@ -0,0 +1,6 @@
1
+ export * from './errors.js';
2
+ export * from './metadata-cache.js';
3
+ export * from './path.js';
4
+ export * from './thumbnail.js';
5
+ export * from './thumbnail-cache.js';
6
+ export * from './types.js';
@@ -0,0 +1,231 @@
1
+ import { DocumentRecord, ImageRecord, StorageProvider } from './types.js';
2
+ /**
3
+ * What kind of leaf entity a `ListingEntry` represents. `folder`
4
+ * means "this child path is a subfolder of the listing's
5
+ * `folderPath`"; consumers walk it for the folder tree, not for
6
+ * `getImage` / `getDocument` lookups.
7
+ */
8
+ export type ListingEntryKind = "image" | "document" | "folder";
9
+ /**
10
+ * One entry in a folder's listing. `path` is the full path (not the
11
+ * leaf name); `version` is the opaque per-backend version string the
12
+ * cache uses to detect external mutation (`mtime` for Device/Desktop,
13
+ * blob SHA for GitHub, `modifiedTime` for Drive).
14
+ */
15
+ export interface ListingEntry {
16
+ path: string;
17
+ version: string;
18
+ kind: ListingEntryKind;
19
+ }
20
+ /**
21
+ * Path → cached record + listing + per-namespace metadata store with
22
+ * version-gated hits. Lifecycle is shared across every opt-in
23
+ * `StorageProvider`; the providers themselves talk to this directly
24
+ * (unlike thumbnails, where a manager sits between the provider and
25
+ * the cache — metadata is intrinsic to `listImages` rather than a
26
+ * side-channel).
27
+ *
28
+ * Methods are organized by concern:
29
+ *
30
+ * - **Records**: per-path `ImageRecord` / `DocumentRecord` caches,
31
+ * version-gated so a stale entry is invisible to readers.
32
+ * - **Listing**: per-folder `ListingEntry[]` cache, used by
33
+ * `listImages` / `listDocuments` / `listFolders` so the store
34
+ * can return "what we know" before deciding whether to revalidate
35
+ * against the backend.
36
+ * - **Namespace meta**: per-namespace single-value KV store. Used
37
+ * for GitHub `branchHead` SHA tracking and Drive
38
+ * `changesPageToken` integration.
39
+ * - **Backend ID**: per-namespace bidirectional path ↔
40
+ * backend-internal-id map. Used by ID-based backends (Drive)
41
+ * where the change-tracking API speaks fileIds, not paths.
42
+ * - **Bulk operations**: cross-namespace migrations (rename,
43
+ * move-folder prefix rewrite) consolidated in one place rather
44
+ * than reimplemented per backend.
45
+ * - **Invalidation**: precise (`invalidatePath`) and broad
46
+ * (`invalidatePrefix`) eviction, used on `resync()` /
47
+ * `forceRefresh()` paths.
48
+ */
49
+ export interface MetadataCache {
50
+ /**
51
+ * Returns the cached image record IFF its stored version matches
52
+ * `version`. Version mismatch returns `undefined` without
53
+ * disturbing the stored row — in a multi-tab world a peer may
54
+ * have written a NEWER version we don't yet know about, and
55
+ * silently evicting it here would discard valid data. Stale
56
+ * entries age out via LRU or are overwritten by the next `put`.
57
+ * `lastAccessedAt` is updated on hit so LRU eviction respects
58
+ * recent reads.
59
+ */
60
+ getImage(ns: string, path: string, version: string): Promise<ImageRecord | undefined>;
61
+ /**
62
+ * Write-or-overwrite at `(ns, path)`. The `version` field becomes
63
+ * the new gate for future `getImage` calls. Implementations
64
+ * should handle quota exhaustion internally (one self-eviction
65
+ * sweep + retry, `clearAll` as last resort) and only throw
66
+ * `MetadataCacheQuotaError` when even that fails.
67
+ */
68
+ putImage(ns: string, path: string, version: string, rec: ImageRecord): Promise<void>;
69
+ /** Document equivalent of {@link getImage}. */
70
+ getDocument(ns: string, path: string, version: string): Promise<DocumentRecord | undefined>;
71
+ /** Document equivalent of {@link putImage}. */
72
+ putDocument(ns: string, path: string, version: string, rec: DocumentRecord): Promise<void>;
73
+ /**
74
+ * Returns the cached listing for `folderPath` under `ns`, or
75
+ * `undefined` if no listing has been recorded. Listings are
76
+ * **not** version-gated at this layer — callers compare individual
77
+ * entry versions to decide whether to revalidate per-entry.
78
+ */
79
+ getListing(ns: string, folderPath: string): Promise<ListingEntry[] | undefined>;
80
+ /**
81
+ * Replace the cached listing at `(ns, folderPath)` with `entries`.
82
+ * Used on cold-fetch or invalidate-all paths. Incremental updates
83
+ * (single add / version bump / remove) should prefer the
84
+ * targeted helpers below.
85
+ */
86
+ putListing(ns: string, folderPath: string, entries: ListingEntry[]): Promise<void>;
87
+ /**
88
+ * Add `entry` to the listing for `(ns, folderPath)`, or replace
89
+ * the existing entry with the same `path`. No-op if no listing
90
+ * has been recorded for the folder.
91
+ */
92
+ upsertListingEntry(ns: string, folderPath: string, entry: ListingEntry): Promise<void>;
93
+ /**
94
+ * Remove the entry matching `path` from the listing for
95
+ * `(ns, folderPath)`. No-op if no listing or no matching entry.
96
+ */
97
+ removeListingEntry(ns: string, folderPath: string, path: string): Promise<void>;
98
+ /**
99
+ * Read a single per-namespace value. Returns `undefined` when not
100
+ * set. Used today for:
101
+ *
102
+ * - GitHubStore's `branchHead` (current commit SHA of the
103
+ * tracked branch). On `init()` the store compares the live
104
+ * HEAD against this value; on match it skips the recursive
105
+ * tree fetch entirely.
106
+ * - GoogleDriveStore's `changesPageToken` (Drive Changes API
107
+ * resume token). On `init()` / `resync()` the store reads
108
+ * this token, applies changes since, advances the token.
109
+ *
110
+ * Treated as opaque strings by the cache.
111
+ */
112
+ getNamespaceMeta(ns: string, key: string): Promise<string | undefined>;
113
+ /** Write-or-overwrite a per-namespace value. */
114
+ putNamespaceMeta(ns: string, key: string, value: string): Promise<void>;
115
+ /** Remove a per-namespace value. */
116
+ deleteNamespaceMeta(ns: string, key: string): Promise<void>;
117
+ /**
118
+ * Record the path ↔ backend-internal-id mapping. Used by ID-based
119
+ * backends (Drive's fileId, future S3 / API-keyed stores) where
120
+ * the change-tracking API speaks IDs, not paths. The cache
121
+ * maintains both directions so `getBackendIdByPath` and
122
+ * `getPathByBackendId` are constant-time.
123
+ */
124
+ setBackendId(ns: string, path: string, backendId: string): Promise<void>;
125
+ /** Forward lookup: path → backend ID. */
126
+ getBackendIdByPath(ns: string, path: string): Promise<string | undefined>;
127
+ /** Reverse lookup: backend ID → path. */
128
+ getPathByBackendId(ns: string, backendId: string): Promise<string | undefined>;
129
+ /**
130
+ * Move every cached artefact (record + listing entry + backend
131
+ * ID) from `oldPath` to `newPath` within the namespace.
132
+ * Generalizes the per-store rename/move handling that
133
+ * `GitHubBlobCache.migrateEntry` did for GitHubStore alone.
134
+ * Listing entries are updated in BOTH the source and destination
135
+ * folder's listings (remove from old, upsert into new).
136
+ */
137
+ migrateEntry(ns: string, oldPath: string, newPath: string): Promise<void>;
138
+ /**
139
+ * Bulk-rewrite every cached artefact whose path starts with
140
+ * `oldPrefix` to start with `newPrefix` instead. Generalizes
141
+ * `GitHubBlobCache.rewriteEntriesForPrefix`. Used by folder
142
+ * rename / folder move operations.
143
+ */
144
+ rewriteEntriesForPrefix(ns: string, oldPrefix: string, newPrefix: string): Promise<void>;
145
+ /** Targeted eviction: drop the record at `(ns, path)`. */
146
+ invalidatePath(ns: string, path: string): Promise<void>;
147
+ /**
148
+ * Drop every cached artefact whose key starts with `prefix`.
149
+ * Used by:
150
+ * - `StorageProvider.forceRefresh()` to invalidate a whole
151
+ * instance's namespace.
152
+ * - GitHubStore's branch-HEAD-mismatch handling (v1: drop the
153
+ * whole namespace, re-fetch from scratch).
154
+ * - Plugin uninstall — the host fires
155
+ * `invalidatePrefix("plugin:<pluginId>:")`.
156
+ */
157
+ invalidatePrefix(prefix: string): Promise<void>;
158
+ }
159
+ /**
160
+ * Base error type implementations may throw out of mutating methods
161
+ * when their underlying transport fails irrecoverably. Stores that
162
+ * catch this should fall back to in-memory-only operation —
163
+ * functionality stays correct for the current session, just not
164
+ * across reload.
165
+ */
166
+ export declare class MetadataCacheError extends Error {
167
+ constructor(message: string, options?: {
168
+ cause?: unknown;
169
+ });
170
+ }
171
+ /**
172
+ * Specific `MetadataCacheError` for quota-exhaustion paths.
173
+ * Implementations that distinguish quota from other I/O errors
174
+ * should throw this so the host can render quota-specific UI
175
+ * ("free up space") rather than the generic fallback.
176
+ */
177
+ export declare class MetadataCacheQuotaError extends MetadataCacheError {
178
+ constructor(message?: string, options?: {
179
+ cause?: unknown;
180
+ });
181
+ }
182
+ /**
183
+ * Capability interface — opt into the shared metadata cache by
184
+ * implementing both methods. Stores that don't implement this
185
+ * continue managing their own metadata internally
186
+ * (BrowserStore / IDBStore — already IDB-native and don't benefit
187
+ * from a second cache layer).
188
+ *
189
+ * The host wires the cache in at construction time via
190
+ * `attachMetadataCache(cache)`; the store reads its own namespace
191
+ * via `metadataNamespace()` so consumers (test helpers, `forceRefresh`
192
+ * implementations) can derive prefix-invalidation keys.
193
+ */
194
+ export interface StorageWithMetadataCache {
195
+ /**
196
+ * Return the per-instance namespace prefix used to scope this
197
+ * store's cache entries. Examples:
198
+ *
199
+ * - DeviceStore: `"device:my-screenshots"` (root folder name)
200
+ * - DesktopStore: `"desktop:default-library"`
201
+ * - GitHubStore: `"github:owner/repo:main"` (owner / repo / branch)
202
+ * - GoogleDriveStore: `"googledrive:<rootFolderId>"`
203
+ * - Plugin-registered: `"plugin:<pluginId>:..."` — the rest of
204
+ * the suffix is the plugin's choice; the `plugin:<pluginId>:`
205
+ * prefix is enforced by the host at registration time.
206
+ *
207
+ * Stable across resync. Implementations MUST NOT bake mutable
208
+ * state into the namespace (e.g. don't append a "session ID" or
209
+ * "build hash") — invalidation relies on prefix stability across
210
+ * sessions.
211
+ */
212
+ metadataNamespace(): string;
213
+ /**
214
+ * Receive the `MetadataCache` instance from the host at
215
+ * construction time. After this call, the store may issue cache
216
+ * operations from any of its public methods.
217
+ *
218
+ * Called exactly once per store instance, before any
219
+ * `StorageProvider` method. Stores that need to perform initial
220
+ * cache reads (e.g. seeding their in-memory shortcuts from
221
+ * `getNamespaceMeta`) should do so in `init()`, not here.
222
+ */
223
+ attachMetadataCache(cache: MetadataCache): void;
224
+ }
225
+ /**
226
+ * Capability predicate — narrow a `StorageProvider` to
227
+ * `StorageProvider & StorageWithMetadataCache` before invoking
228
+ * the optional methods. Mirrors the `supports*` helpers next to
229
+ * the other `StorageWith*` interfaces in `./types.ts`.
230
+ */
231
+ export declare function supportsMetadataCache(store: StorageProvider): store is StorageProvider & StorageWithMetadataCache;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Path utilities for filesystem-style storage identifiers.
3
+ *
4
+ * Paths are POSIX-style, separated by "/". Never leading or trailing "/".
5
+ * Root path is "" (empty string). Reserved segments "." and ".." are invalid.
6
+ */
7
+ /** Empty string represents the root folder. */
8
+ export declare const ROOT_PATH = "";
9
+ /**
10
+ * Validate a single path segment (folder or filename).
11
+ * Throws if the name is empty, is "." / "..", or contains reserved characters.
12
+ */
13
+ export declare function validateName(name: string): void;
14
+ /**
15
+ * Join a parent path with a single name.
16
+ * Throws if `name` contains "/".
17
+ */
18
+ export declare function joinPath(parent: string, name: string): string;
19
+ /** Return the parent folder's path. Root path's parent is "" (root). */
20
+ export declare function getParentPath(path: string): string;
21
+ /** Return the last segment (folder name or filename) from a path. */
22
+ export declare function getFilename(path: string): string;
23
+ /** Split a path into its segments. Returns [] for the root path. */
24
+ export declare function splitPath(path: string): string[];
25
+ /**
26
+ * Return all ancestor paths from root down to (but not including) `path`.
27
+ * Example: `ancestorPaths("A/B/C") = ["A", "A/B"]`.
28
+ */
29
+ export declare function ancestorPaths(path: string): string[];
30
+ /**
31
+ * Return `true` if `path` is equal to `ancestor` or is nested inside it.
32
+ * Example: `isDescendantOrSame("A/B/C", "A") === true`.
33
+ */
34
+ export declare function isDescendantOrSame(path: string, ancestor: string): boolean;
35
+ /**
36
+ * Split a filename into base + extension. Extension includes the leading ".".
37
+ * Example: `splitExt("image.tar.gz") = ["image.tar", ".gz"]`.
38
+ * Files without extension return `[name, ""]`.
39
+ */
40
+ export declare function splitExt(filename: string): [string, string];
41
+ /**
42
+ * Return a unique filename within its folder by appending " (2)", " (3)", ...
43
+ * before the extension. If `desired` is already free, returns it unchanged.
44
+ */
45
+ export declare function uniquifyFilename(desired: string, exists: (candidate: string) => boolean): string;
46
+ /** Async variant of `uniquifyFilename` for stores that need async existence checks. */
47
+ export declare function uniquifyFilenameAsync(desired: string, exists: (candidate: string) => Promise<boolean>): Promise<string>;
48
+ /** Rewrite a child path when its ancestor folder is renamed/moved. */
49
+ export declare function rewritePathPrefix(path: string, oldPrefix: string, newPrefix: string): string;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Persistent thumbnail cache contract — Tier A, DOM-free.
3
+ *
4
+ * Owns a key-versioned key/value store of pre-rendered gallery
5
+ * thumbnails (`dataUrl` + natural dimensions). Implementations
6
+ * decide where the bytes live: the first-party
7
+ * `IndexedDBThumbnailCache` in `@ingcreators/annot-web/storage/
8
+ * idb-thumbnail-cache` is the canonical browser-side option;
9
+ * tests / Node hosts can swap in an in-memory mock with the
10
+ * same surface.
11
+ *
12
+ * The unified-thumbnail-cache plan
13
+ * ([`docs/plans/_done/unified-thumbnail-cache.md`](../../../../docs/plans/_done/unified-thumbnail-cache.md))
14
+ * lifts the per-store `#ensureThumbnail` lifecycle into a single
15
+ * host-side `ThumbnailManager` that talks to one `ThumbnailCache`
16
+ * instance shared across every backend; this file is the Tier A
17
+ * piece that contract.
18
+ *
19
+ * Keys carry a host-reserved namespace prefix matching the
20
+ * `StorageMode` strings used elsewhere in the app (`browser:` /
21
+ * `device:` / `github:` / `googledrive:`); plugin-registered
22
+ * providers must use the `plugin:<pluginId>:` prefix the host
23
+ * enforces at registration time. See the plan doc for the full
24
+ * convention.
25
+ */
26
+ /**
27
+ * One cached thumbnail. JPEG-encoded `dataUrl` (already base64
28
+ * via the standard `image-thumbnail.ts` helper) plus the source
29
+ * image's natural dimensions — needed by the gallery card's
30
+ * `WxH • date` line so it doesn't have to wait on a separate
31
+ * decode round-trip after a cache hit.
32
+ */
33
+ export interface CachedThumbnail {
34
+ dataUrl: string;
35
+ width: number;
36
+ height: number;
37
+ }
38
+ /** One bulk-`get` request — the key plus the version the caller
39
+ * considers fresh. Mismatches are evicted, not returned. */
40
+ export interface ThumbnailCacheGetRequest {
41
+ key: string;
42
+ expectedVersion: string;
43
+ }
44
+ /**
45
+ * Key/value store of `CachedThumbnail` entries with version-gated
46
+ * cache hits. Lifecycle owned by the host-side `ThumbnailManager`;
47
+ * `StorageProvider` implementations don't talk to this directly,
48
+ * they expose `thumbnailKey` / `thumbnailVersion` /
49
+ * `fetchThumbnailSource` and let the manager drive the cache.
50
+ */
51
+ export interface ThumbnailCache {
52
+ /**
53
+ * Returns the cached entry IFF its stored version matches
54
+ * `expectedVersion`. Stale entries (version mismatch) are
55
+ * evicted as a side effect of `get` — the next caller sees a
56
+ * cold miss. `lastAccessedAt` is updated on hit so LRU
57
+ * eviction respects recent reads.
58
+ */
59
+ get(key: string, expectedVersion: string): Promise<CachedThumbnail | undefined>;
60
+ /**
61
+ * Bulk get — single transaction instead of one per record.
62
+ * Used by `ThumbnailManager.attach` against a `listImages`
63
+ * result so cold listings of a populated gallery don't fan
64
+ * out into N IDB round-trips.
65
+ *
66
+ * The returned `Map` keys are the request keys (so callers
67
+ * can correlate). Misses are simply absent.
68
+ */
69
+ getMany(requests: ThumbnailCacheGetRequest[]): Promise<Map<string, CachedThumbnail>>;
70
+ /**
71
+ * Write or overwrite at `key`. Updates the entry's
72
+ * `lastAccessedAt`. Implementations should handle quota
73
+ * exhaustion internally (one self-eviction sweep + retry,
74
+ * `clearAll` as last resort) and only throw
75
+ * `ThumbnailCacheQuotaError` when even that fails.
76
+ */
77
+ set(key: string, version: string, value: CachedThumbnail): Promise<void>;
78
+ /** Evict by exact key. */
79
+ delete(key: string): Promise<void>;
80
+ /**
81
+ * Drop every entry whose key starts with `prefix`. Used by:
82
+ * - `StorageProvider.resync()` to invalidate a whole instance's
83
+ * namespace without enumerating individual keys.
84
+ * - Plugin uninstall — the host fires
85
+ * `deletePrefix("plugin:<pluginId>:")`.
86
+ */
87
+ deletePrefix(prefix: string): Promise<void>;
88
+ /** Quota-recovery hatch. Drops everything. */
89
+ clearAll(): Promise<void>;
90
+ }
91
+ /**
92
+ * Base error type implementations may throw out of `set` once
93
+ * eviction + retry both fail. `ThumbnailManager` swallows this
94
+ * and falls back to in-memory only — the gallery still gets the
95
+ * thumbnail for the current session, just not across reload.
96
+ */
97
+ export declare class ThumbnailCacheError extends Error {
98
+ constructor(message: string, options?: {
99
+ cause?: unknown;
100
+ });
101
+ }
102
+ /** Specific `ThumbnailCacheError` for quota-exhaustion paths.
103
+ * Implementations that distinguish quota from other I/O errors
104
+ * should throw this so callers can apply quota-specific UI
105
+ * ("free up space") rather than the generic fallback. */
106
+ export declare class ThumbnailCacheQuotaError extends ThumbnailCacheError {
107
+ constructor(message?: string, options?: {
108
+ cause?: unknown;
109
+ });
110
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared thumbnail rendering helper.
3
+ *
4
+ * The gallery card is a fixed 16:9 tile. Scaling a short/wide source image
5
+ * up to fill that tile (as `object-fit: cover` would do) makes the image
6
+ * blurry. Instead, every storage provider's `generateThumbnail()` draws
7
+ * onto a fixed-aspect (16:9) canvas with the following rules:
8
+ *
9
+ * - Never upscale. If the source is smaller than the canvas, it stays
10
+ * at natural size and the gaps are filled with the `bgColor`.
11
+ * - For source wider than 16:9 (or exactly 16:9): fit width,
12
+ * letterbox vertically when shorter than the canvas height.
13
+ * - For source taller than 16:9 (portrait screenshots, scrollshots):
14
+ * fit width, take only the top portion so the canvas shows the
15
+ * "head" of the page — matching typical screenshot gallery UX.
16
+ *
17
+ * Works with both `HTMLCanvasElement` (main-thread web) and
18
+ * `OffscreenCanvas` (service worker / fs-store). Caller passes in the
19
+ * source's natural width/height so we don't assume an `HTMLImageElement`.
20
+ */
21
+ export interface ThumbCanvasLike {
22
+ width: number;
23
+ height: number;
24
+ }
25
+ export type ThumbImageSource = HTMLImageElement | CanvasImageSource;
26
+ /**
27
+ * Configure `canvas` size, clear the background, and draw `source` onto it
28
+ * following the rules above. The canvas ends up exactly `targetW × targetH`
29
+ * where targetW = maxWidth and targetH = round(targetW * 9/16).
30
+ */
31
+ export declare function drawToThumbCanvas(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, canvas: ThumbCanvasLike, source: ThumbImageSource, srcW: number, srcH: number, maxWidth?: number, bgColor?: string): void;