@unhingged/vizu-core 0.1.22 → 0.2.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.
@@ -162,6 +162,79 @@ interface VizuComment {
162
162
  */
163
163
  fingerprintsRefreshedAt?: number | null;
164
164
  }
165
+ /**
166
+ * Storage event emitted by adapters that support push notifications of
167
+ * remote changes.
168
+ */
169
+ type StorageEvent = {
170
+ type: 'added';
171
+ comment: VizuComment;
172
+ } | {
173
+ type: 'updated';
174
+ comment: VizuComment;
175
+ } | {
176
+ type: 'removed';
177
+ id: string;
178
+ } | {
179
+ type: 'cleared';
180
+ };
181
+ /**
182
+ * Optional authorization context an adapter may surface to the host.
183
+ * Cloud adapters set this after the user signs in; the Vizu class
184
+ * forwards it via `vizu.getAuthContext()` for hosts that need to call
185
+ * the same backend with the same session.
186
+ */
187
+ interface AuthContext {
188
+ /** Identity provider's user id (e.g. Clerk user id). */
189
+ userId: string;
190
+ /** Short-lived bearer token. Adapter is responsible for refresh. */
191
+ token: string;
192
+ /** ISO timestamp the token expires; informational. */
193
+ expiresAt?: string;
194
+ }
195
+ /**
196
+ * Storage contract implemented by {@link CloudStorageAdapter} — the only
197
+ * adapter Vizu constructs. The type stays exported because it is
198
+ * load-bearing for CloudStorageAdapter's public typing; the interface is
199
+ * per-comment so each mutation is one network call and the adapter can do
200
+ * optimistic-concurrency without re-shipping the whole list.
201
+ *
202
+ * The `namespace` parameter is the workspace slug (Vizu passes
203
+ * `options.workspace` through).
204
+ *
205
+ * Reply ops (`addReply` / `removeReply`) are optional: adapters that don't
206
+ * implement them fall back to `updateComment` with a manually-patched
207
+ * `replies` array. Wrapping logic lives in the Vizu class, not here.
208
+ */
209
+ interface StorageAdapter {
210
+ /** Load all comments for this namespace. Called once at boot, plus on `setComments`. */
211
+ load(namespace: string): Promise<VizuComment[]>;
212
+ /** Persist a new comment. */
213
+ addComment(namespace: string, comment: VizuComment): Promise<void>;
214
+ /** Patch an existing comment by id. Adapter returns the updated doc or `null` if not found. */
215
+ updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
216
+ /** Remove a comment by id. */
217
+ removeComment(namespace: string, id: string): Promise<void>;
218
+ /** Replace the entire list (used by hosts that hydrate from their own backend). */
219
+ setAll(namespace: string, comments: VizuComment[]): Promise<void>;
220
+ /** Wipe the namespace. */
221
+ clear(namespace: string): Promise<void>;
222
+ /** Append a reply to a comment. Adapter returns the parent or null if not found. */
223
+ addReply?(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
224
+ /** Remove a reply by id from a comment. Adapter returns the parent or null if not found. */
225
+ removeReply?(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
226
+ /**
227
+ * Upload a file attachment. Returns the resolved {@link Attachment} with
228
+ * a public URL the host can persist on a comment. Adapters that don't
229
+ * implement this surface report `vizu.canUploadAttachments() === false`
230
+ * and the popover hides the upload UI.
231
+ */
232
+ uploadAttachment?(namespace: string, file: File): Promise<Attachment>;
233
+ /** Optional remote-change subscription. Returns an unsubscribe function. */
234
+ subscribe?(namespace: string, handler: (event: StorageEvent) => void): () => void;
235
+ /** Optional auth context (cloud adapters). */
236
+ getAuthContext?(): AuthContext | null;
237
+ }
165
238
  /** Context handed to every action's onClick. Has the data and the side-effect helpers. */
166
239
  interface ActionContext {
167
240
  comments: VizuComment[];
@@ -243,7 +316,7 @@ interface PopoverCallbacks {
243
316
  * Fetch the workspace's mentionable users for the @ picker dropdown.
244
317
  * Called every time the user opens the picker — keeps the list fresh
245
318
  * if members were added/removed in another tab. Returns [] for hosts
246
- * that don't support mentions (local/memory storage adapters).
319
+ * that don't support mentions.
247
320
  */
248
321
  onMentionSearch?: () => Promise<MentionableUser[]>;
249
322
  }
@@ -450,4 +523,51 @@ declare class Sidebar {
450
523
 
451
524
  declare function injectStyles(): void;
452
525
 
453
- export { Highlighter, Pill, type PillCallbacks, Popover, type PopoverCallbacks, Sidebar, injectStyles };
526
+ /**
527
+ * Persist comments to the host page's localStorage. One JSON document per
528
+ * namespace. Migrations are applied on read.
529
+ *
530
+ * v2 per-comment ops are emulated via read-modify-write of the underlying
531
+ * array — fine for localStorage's <1ms cost. Cloud adapters do real
532
+ * single-document writes.
533
+ */
534
+ declare class LocalStorageAdapter implements StorageAdapter {
535
+ load(namespace: string): Promise<VizuComment[]>;
536
+ addComment(namespace: string, comment: VizuComment): Promise<void>;
537
+ updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
538
+ removeComment(namespace: string, id: string): Promise<void>;
539
+ setAll(namespace: string, comments: VizuComment[]): Promise<void>;
540
+ clear(namespace: string): Promise<void>;
541
+ addReply(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
542
+ removeReply(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
543
+ }
544
+ /**
545
+ * Stores comments in RAM only — wipes on reload. Useful when the host owns
546
+ * persistence via events (the integrator listens to `comment:added` and
547
+ * forwards to its own backend).
548
+ */
549
+ declare class InMemoryStorageAdapter implements StorageAdapter {
550
+ private store;
551
+ load(namespace: string): Promise<VizuComment[]>;
552
+ addComment(namespace: string, comment: VizuComment): Promise<void>;
553
+ updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
554
+ removeComment(namespace: string, id: string): Promise<void>;
555
+ setAll(namespace: string, comments: VizuComment[]): Promise<void>;
556
+ clear(namespace: string): Promise<void>;
557
+ addReply(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
558
+ removeReply(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
559
+ }
560
+ /**
561
+ * No-op adapter — never persists. The host hydrates via `setComments` and
562
+ * listens to events to drive its own storage.
563
+ */
564
+ declare class NullStorageAdapter implements StorageAdapter {
565
+ load(): Promise<VizuComment[]>;
566
+ addComment(): Promise<void>;
567
+ updateComment(): Promise<VizuComment | null>;
568
+ removeComment(): Promise<void>;
569
+ setAll(): Promise<void>;
570
+ clear(): Promise<void>;
571
+ }
572
+
573
+ export { Highlighter, InMemoryStorageAdapter, LocalStorageAdapter, NullStorageAdapter, Pill, type PillCallbacks, Popover, type PopoverCallbacks, Sidebar, type StorageAdapter, injectStyles };
@@ -162,6 +162,79 @@ interface VizuComment {
162
162
  */
163
163
  fingerprintsRefreshedAt?: number | null;
164
164
  }
165
+ /**
166
+ * Storage event emitted by adapters that support push notifications of
167
+ * remote changes.
168
+ */
169
+ type StorageEvent = {
170
+ type: 'added';
171
+ comment: VizuComment;
172
+ } | {
173
+ type: 'updated';
174
+ comment: VizuComment;
175
+ } | {
176
+ type: 'removed';
177
+ id: string;
178
+ } | {
179
+ type: 'cleared';
180
+ };
181
+ /**
182
+ * Optional authorization context an adapter may surface to the host.
183
+ * Cloud adapters set this after the user signs in; the Vizu class
184
+ * forwards it via `vizu.getAuthContext()` for hosts that need to call
185
+ * the same backend with the same session.
186
+ */
187
+ interface AuthContext {
188
+ /** Identity provider's user id (e.g. Clerk user id). */
189
+ userId: string;
190
+ /** Short-lived bearer token. Adapter is responsible for refresh. */
191
+ token: string;
192
+ /** ISO timestamp the token expires; informational. */
193
+ expiresAt?: string;
194
+ }
195
+ /**
196
+ * Storage contract implemented by {@link CloudStorageAdapter} — the only
197
+ * adapter Vizu constructs. The type stays exported because it is
198
+ * load-bearing for CloudStorageAdapter's public typing; the interface is
199
+ * per-comment so each mutation is one network call and the adapter can do
200
+ * optimistic-concurrency without re-shipping the whole list.
201
+ *
202
+ * The `namespace` parameter is the workspace slug (Vizu passes
203
+ * `options.workspace` through).
204
+ *
205
+ * Reply ops (`addReply` / `removeReply`) are optional: adapters that don't
206
+ * implement them fall back to `updateComment` with a manually-patched
207
+ * `replies` array. Wrapping logic lives in the Vizu class, not here.
208
+ */
209
+ interface StorageAdapter {
210
+ /** Load all comments for this namespace. Called once at boot, plus on `setComments`. */
211
+ load(namespace: string): Promise<VizuComment[]>;
212
+ /** Persist a new comment. */
213
+ addComment(namespace: string, comment: VizuComment): Promise<void>;
214
+ /** Patch an existing comment by id. Adapter returns the updated doc or `null` if not found. */
215
+ updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
216
+ /** Remove a comment by id. */
217
+ removeComment(namespace: string, id: string): Promise<void>;
218
+ /** Replace the entire list (used by hosts that hydrate from their own backend). */
219
+ setAll(namespace: string, comments: VizuComment[]): Promise<void>;
220
+ /** Wipe the namespace. */
221
+ clear(namespace: string): Promise<void>;
222
+ /** Append a reply to a comment. Adapter returns the parent or null if not found. */
223
+ addReply?(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
224
+ /** Remove a reply by id from a comment. Adapter returns the parent or null if not found. */
225
+ removeReply?(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
226
+ /**
227
+ * Upload a file attachment. Returns the resolved {@link Attachment} with
228
+ * a public URL the host can persist on a comment. Adapters that don't
229
+ * implement this surface report `vizu.canUploadAttachments() === false`
230
+ * and the popover hides the upload UI.
231
+ */
232
+ uploadAttachment?(namespace: string, file: File): Promise<Attachment>;
233
+ /** Optional remote-change subscription. Returns an unsubscribe function. */
234
+ subscribe?(namespace: string, handler: (event: StorageEvent) => void): () => void;
235
+ /** Optional auth context (cloud adapters). */
236
+ getAuthContext?(): AuthContext | null;
237
+ }
165
238
  /** Context handed to every action's onClick. Has the data and the side-effect helpers. */
166
239
  interface ActionContext {
167
240
  comments: VizuComment[];
@@ -243,7 +316,7 @@ interface PopoverCallbacks {
243
316
  * Fetch the workspace's mentionable users for the @ picker dropdown.
244
317
  * Called every time the user opens the picker — keeps the list fresh
245
318
  * if members were added/removed in another tab. Returns [] for hosts
246
- * that don't support mentions (local/memory storage adapters).
319
+ * that don't support mentions.
247
320
  */
248
321
  onMentionSearch?: () => Promise<MentionableUser[]>;
249
322
  }
@@ -450,4 +523,51 @@ declare class Sidebar {
450
523
 
451
524
  declare function injectStyles(): void;
452
525
 
453
- export { Highlighter, Pill, type PillCallbacks, Popover, type PopoverCallbacks, Sidebar, injectStyles };
526
+ /**
527
+ * Persist comments to the host page's localStorage. One JSON document per
528
+ * namespace. Migrations are applied on read.
529
+ *
530
+ * v2 per-comment ops are emulated via read-modify-write of the underlying
531
+ * array — fine for localStorage's <1ms cost. Cloud adapters do real
532
+ * single-document writes.
533
+ */
534
+ declare class LocalStorageAdapter implements StorageAdapter {
535
+ load(namespace: string): Promise<VizuComment[]>;
536
+ addComment(namespace: string, comment: VizuComment): Promise<void>;
537
+ updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
538
+ removeComment(namespace: string, id: string): Promise<void>;
539
+ setAll(namespace: string, comments: VizuComment[]): Promise<void>;
540
+ clear(namespace: string): Promise<void>;
541
+ addReply(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
542
+ removeReply(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
543
+ }
544
+ /**
545
+ * Stores comments in RAM only — wipes on reload. Useful when the host owns
546
+ * persistence via events (the integrator listens to `comment:added` and
547
+ * forwards to its own backend).
548
+ */
549
+ declare class InMemoryStorageAdapter implements StorageAdapter {
550
+ private store;
551
+ load(namespace: string): Promise<VizuComment[]>;
552
+ addComment(namespace: string, comment: VizuComment): Promise<void>;
553
+ updateComment(namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
554
+ removeComment(namespace: string, id: string): Promise<void>;
555
+ setAll(namespace: string, comments: VizuComment[]): Promise<void>;
556
+ clear(namespace: string): Promise<void>;
557
+ addReply(namespace: string, commentId: string, reply: Reply): Promise<VizuComment | null>;
558
+ removeReply(namespace: string, commentId: string, replyId: string): Promise<VizuComment | null>;
559
+ }
560
+ /**
561
+ * No-op adapter — never persists. The host hydrates via `setComments` and
562
+ * listens to events to drive its own storage.
563
+ */
564
+ declare class NullStorageAdapter implements StorageAdapter {
565
+ load(): Promise<VizuComment[]>;
566
+ addComment(): Promise<void>;
567
+ updateComment(): Promise<VizuComment | null>;
568
+ removeComment(): Promise<void>;
569
+ setAll(): Promise<void>;
570
+ clear(): Promise<void>;
571
+ }
572
+
573
+ export { Highlighter, InMemoryStorageAdapter, LocalStorageAdapter, NullStorageAdapter, Pill, type PillCallbacks, Popover, type PopoverCallbacks, Sidebar, type StorageAdapter, injectStyles };
package/dist/internal.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/types.ts
2
+ var SCHEMA_VERSION = 2;
3
+
1
4
  // src/util.ts
2
5
  function isInside(target, selector) {
3
6
  return !!target.closest(selector);
@@ -74,6 +77,41 @@ function renderAttachmentsHtml(attachments) {
74
77
  }).join("");
75
78
  return `<div class="vz-attachment-grid">${items}</div>`;
76
79
  }
80
+ function migrateComment(raw) {
81
+ if (!raw || typeof raw !== "object") return raw;
82
+ const next = { ...raw };
83
+ if (next.id == null && next._id != null) {
84
+ next.id = next._id;
85
+ }
86
+ if (!Array.isArray(next.fingerprints) || next.fingerprints.length === 0) {
87
+ if (next.fingerprint) {
88
+ next.fingerprints = [next.fingerprint];
89
+ }
90
+ }
91
+ delete next.fingerprint;
92
+ if (next.url && !next.pageUrl) {
93
+ next.pageUrl = next.url;
94
+ }
95
+ if (!("status" in next) || !isValidStatus(next.status)) next.status = "open";
96
+ if (!Array.isArray(next.replies)) next.replies = [];
97
+ if (!Array.isArray(next.attachments)) next.attachments = [];
98
+ if (!Array.isArray(next.mentions)) next.mentions = [];
99
+ if (!("fingerprintsRefreshedAt" in next)) next.fingerprintsRefreshedAt = null;
100
+ next.schemaVersion = SCHEMA_VERSION;
101
+ return next;
102
+ }
103
+ function isValidStatus(s) {
104
+ return s === "open" || s === "resolved" || s === "wontfix";
105
+ }
106
+ function migrateComments(raws) {
107
+ if (!Array.isArray(raws)) return [];
108
+ const out = [];
109
+ for (const raw of raws) {
110
+ if (!raw || typeof raw !== "object") continue;
111
+ out.push(migrateComment(raw));
112
+ }
113
+ return out;
114
+ }
77
115
 
78
116
  // src/pill.ts
79
117
  var Pill = class {
@@ -388,8 +426,8 @@ var Popover = class {
388
426
  const removeBtn = target.closest('[data-vz="remove-anchor"]');
389
427
  if (removeBtn) {
390
428
  e.stopPropagation();
391
- const key = removeBtn.getAttribute("data-fp-key");
392
- if (key) this.removeAnchor(key);
429
+ const key2 = removeBtn.getAttribute("data-fp-key");
430
+ if (key2) this.removeAnchor(key2);
393
431
  return;
394
432
  }
395
433
  const refEl = target.closest('[data-vz="ref"]');
@@ -484,8 +522,8 @@ var Popover = class {
484
522
  }
485
523
  /** Add another anchor to the in-progress comment (Shift+Click flow). */
486
524
  addAnchor(fp, target) {
487
- const key = fingerprintKey(fp);
488
- if (this.anchorFingerprints.some((x) => fingerprintKey(x) === key)) return false;
525
+ const key2 = fingerprintKey(fp);
526
+ if (this.anchorFingerprints.some((x) => fingerprintKey(x) === key2)) return false;
489
527
  this.anchorFingerprints.push(fp);
490
528
  if (target) this.anchorTargets.push(target);
491
529
  this.callbacks.onAnchorsChanged([...this.anchorFingerprints]);
@@ -494,9 +532,9 @@ var Popover = class {
494
532
  return true;
495
533
  }
496
534
  /** Remove an anchor by fingerprint key. Never removes the last anchor. */
497
- removeAnchor(key) {
535
+ removeAnchor(key2) {
498
536
  if (this.anchorFingerprints.length <= 1) return false;
499
- const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) === key);
537
+ const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) === key2);
500
538
  if (idx < 0) return false;
501
539
  this.anchorFingerprints.splice(idx, 1);
502
540
  this.anchorTargets.splice(idx, 1);
@@ -606,9 +644,9 @@ var Popover = class {
606
644
  return;
607
645
  }
608
646
  const chips = this.anchorFingerprints.map((fp) => {
609
- const key = fingerprintKey(fp);
647
+ const key2 = fingerprintKey(fp);
610
648
  const label = fingerprintLabel(fp);
611
- return `<span class="vz-anchor-chip" data-fp-key="${escapeHtml(key)}">${escapeHtml(label)}<button class="vz-anchor-chip-remove" data-vz="remove-anchor" data-fp-key="${escapeHtml(key)}" aria-label="Remove anchor">\xD7</button></span>`;
649
+ return `<span class="vz-anchor-chip" data-fp-key="${escapeHtml(key2)}">${escapeHtml(label)}<button class="vz-anchor-chip-remove" data-vz="remove-anchor" data-fp-key="${escapeHtml(key2)}" aria-label="Remove anchor">\xD7</button></span>`;
612
650
  }).join("");
613
651
  wrap.innerHTML = `
614
652
  <div class="vz-anchor-hint">Anchored to ${this.anchorFingerprints.length} elements:</div>
@@ -1329,10 +1367,10 @@ var Sidebar = class {
1329
1367
  const fps = c.fingerprints || [];
1330
1368
  const primary = fps[0];
1331
1369
  if (!primary) continue;
1332
- const key = fingerprintKey(primary);
1333
- const g = groups.get(key);
1370
+ const key2 = fingerprintKey(primary);
1371
+ const g = groups.get(key2);
1334
1372
  if (g) g.comments.push(c);
1335
- else groups.set(key, { fp: primary, comments: [c] });
1373
+ else groups.set(key2, { fp: primary, comments: [c] });
1336
1374
  }
1337
1375
  const groupHtml = [];
1338
1376
  let gi = 0;
@@ -1393,8 +1431,8 @@ var Sidebar = class {
1393
1431
  }
1394
1432
  const chips = this.el.querySelectorAll('[data-vz="jump-fp"]');
1395
1433
  for (const chip of chips) {
1396
- const key = chip.getAttribute("data-fp-key");
1397
- const fp = findFingerprintByKey(visible, key || "");
1434
+ const key2 = chip.getAttribute("data-fp-key");
1435
+ const fp = findFingerprintByKey(visible, key2 || "");
1398
1436
  chip.__fp = fp;
1399
1437
  }
1400
1438
  }
@@ -1482,10 +1520,10 @@ function renderOrphansHtml(orphans) {
1482
1520
  </div>
1483
1521
  `;
1484
1522
  }
1485
- function findFingerprintByKey(comments, key) {
1523
+ function findFingerprintByKey(comments, key2) {
1486
1524
  for (const c of comments) {
1487
1525
  for (const fp of c.fingerprints || []) {
1488
- if (fingerprintKey(fp) === key) return fp;
1526
+ if (fingerprintKey(fp) === key2) return fp;
1489
1527
  }
1490
1528
  }
1491
1529
  return null;
@@ -2517,8 +2555,154 @@ function injectStyles() {
2517
2555
  style.textContent = STYLES;
2518
2556
  (document.head || document.documentElement).appendChild(style);
2519
2557
  }
2558
+
2559
+ // src/storage.ts
2560
+ var key = (ns) => `vizu:comments:${ns}`;
2561
+ var LocalStorageAdapter = class {
2562
+ async load(namespace) {
2563
+ return readArray(namespace);
2564
+ }
2565
+ async addComment(namespace, comment) {
2566
+ const list = readArray(namespace);
2567
+ list.push(comment);
2568
+ writeArray(namespace, list);
2569
+ }
2570
+ async updateComment(namespace, id, patch) {
2571
+ const list = readArray(namespace);
2572
+ const idx = list.findIndex((c) => c.id === id);
2573
+ if (idx === -1) return null;
2574
+ const next = { ...list[idx], ...patch, id: list[idx].id };
2575
+ list[idx] = next;
2576
+ writeArray(namespace, list);
2577
+ return next;
2578
+ }
2579
+ async removeComment(namespace, id) {
2580
+ const list = readArray(namespace);
2581
+ writeArray(namespace, list.filter((c) => c.id !== id));
2582
+ }
2583
+ async setAll(namespace, comments) {
2584
+ writeArray(namespace, comments);
2585
+ }
2586
+ async clear(namespace) {
2587
+ if (typeof localStorage === "undefined") return;
2588
+ localStorage.removeItem(key(namespace));
2589
+ }
2590
+ async addReply(namespace, commentId, reply) {
2591
+ const list = readArray(namespace);
2592
+ const idx = list.findIndex((c) => c.id === commentId);
2593
+ if (idx === -1) return null;
2594
+ const next = { ...list[idx], replies: [...list[idx].replies ?? [], reply] };
2595
+ list[idx] = next;
2596
+ writeArray(namespace, list);
2597
+ return next;
2598
+ }
2599
+ async removeReply(namespace, commentId, replyId) {
2600
+ const list = readArray(namespace);
2601
+ const idx = list.findIndex((c) => c.id === commentId);
2602
+ if (idx === -1) return null;
2603
+ const next = {
2604
+ ...list[idx],
2605
+ replies: (list[idx].replies ?? []).filter((r) => r.id !== replyId)
2606
+ };
2607
+ list[idx] = next;
2608
+ writeArray(namespace, list);
2609
+ return next;
2610
+ }
2611
+ };
2612
+ function readArray(namespace) {
2613
+ if (typeof localStorage === "undefined") return [];
2614
+ const raw = localStorage.getItem(key(namespace));
2615
+ if (!raw) return [];
2616
+ try {
2617
+ const parsed = JSON.parse(raw);
2618
+ return Array.isArray(parsed) ? migrateComments(parsed) : [];
2619
+ } catch {
2620
+ return [];
2621
+ }
2622
+ }
2623
+ function writeArray(namespace, comments) {
2624
+ if (typeof localStorage === "undefined") return;
2625
+ localStorage.setItem(key(namespace), JSON.stringify(comments));
2626
+ }
2627
+ var InMemoryStorageAdapter = class {
2628
+ constructor() {
2629
+ this.store = /* @__PURE__ */ new Map();
2630
+ }
2631
+ async load(namespace) {
2632
+ return [...this.store.get(namespace) || []];
2633
+ }
2634
+ async addComment(namespace, comment) {
2635
+ const list = this.store.get(namespace) ?? [];
2636
+ this.store.set(namespace, [...list, comment]);
2637
+ }
2638
+ async updateComment(namespace, id, patch) {
2639
+ const list = this.store.get(namespace) ?? [];
2640
+ const idx = list.findIndex((c) => c.id === id);
2641
+ if (idx === -1) return null;
2642
+ const next = { ...list[idx], ...patch, id: list[idx].id };
2643
+ const copy = [...list];
2644
+ copy[idx] = next;
2645
+ this.store.set(namespace, copy);
2646
+ return next;
2647
+ }
2648
+ async removeComment(namespace, id) {
2649
+ const list = this.store.get(namespace) ?? [];
2650
+ this.store.set(
2651
+ namespace,
2652
+ list.filter((c) => c.id !== id)
2653
+ );
2654
+ }
2655
+ async setAll(namespace, comments) {
2656
+ this.store.set(namespace, [...comments]);
2657
+ }
2658
+ async clear(namespace) {
2659
+ this.store.delete(namespace);
2660
+ }
2661
+ async addReply(namespace, commentId, reply) {
2662
+ const list = this.store.get(namespace) ?? [];
2663
+ const idx = list.findIndex((c) => c.id === commentId);
2664
+ if (idx === -1) return null;
2665
+ const next = { ...list[idx], replies: [...list[idx].replies ?? [], reply] };
2666
+ const copy = [...list];
2667
+ copy[idx] = next;
2668
+ this.store.set(namespace, copy);
2669
+ return next;
2670
+ }
2671
+ async removeReply(namespace, commentId, replyId) {
2672
+ const list = this.store.get(namespace) ?? [];
2673
+ const idx = list.findIndex((c) => c.id === commentId);
2674
+ if (idx === -1) return null;
2675
+ const next = {
2676
+ ...list[idx],
2677
+ replies: (list[idx].replies ?? []).filter((r) => r.id !== replyId)
2678
+ };
2679
+ const copy = [...list];
2680
+ copy[idx] = next;
2681
+ this.store.set(namespace, copy);
2682
+ return next;
2683
+ }
2684
+ };
2685
+ var NullStorageAdapter = class {
2686
+ async load() {
2687
+ return [];
2688
+ }
2689
+ async addComment() {
2690
+ }
2691
+ async updateComment() {
2692
+ return null;
2693
+ }
2694
+ async removeComment() {
2695
+ }
2696
+ async setAll() {
2697
+ }
2698
+ async clear() {
2699
+ }
2700
+ };
2520
2701
  export {
2521
2702
  Highlighter,
2703
+ InMemoryStorageAdapter,
2704
+ LocalStorageAdapter,
2705
+ NullStorageAdapter,
2522
2706
  Pill,
2523
2707
  Popover,
2524
2708
  Sidebar,