@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.
- package/dist/auto.cjs +127 -323
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +124 -320
- package/dist/auto.js.map +1 -1
- package/dist/index.cjs +117 -325
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -113
- package/dist/index.d.ts +50 -113
- package/dist/index.js +114 -319
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +205 -18
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +122 -2
- package/dist/internal.d.ts +122 -2
- package/dist/internal.js +199 -15
- package/dist/internal.js.map +1 -1
- package/dist/vizu.min.js +47 -47
- package/dist/vizu.min.js.map +1 -1
- package/package.json +1 -1
package/dist/internal.d.cts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
392
|
-
if (
|
|
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
|
|
488
|
-
if (this.anchorFingerprints.some((x) => fingerprintKey(x) ===
|
|
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(
|
|
535
|
+
removeAnchor(key2) {
|
|
498
536
|
if (this.anchorFingerprints.length <= 1) return false;
|
|
499
|
-
const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) ===
|
|
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
|
|
647
|
+
const key2 = fingerprintKey(fp);
|
|
610
648
|
const label = fingerprintLabel(fp);
|
|
611
|
-
return `<span class="vz-anchor-chip" data-fp-key="${escapeHtml(
|
|
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
|
|
1333
|
-
const g = groups.get(
|
|
1370
|
+
const key2 = fingerprintKey(primary);
|
|
1371
|
+
const g = groups.get(key2);
|
|
1334
1372
|
if (g) g.comments.push(c);
|
|
1335
|
-
else groups.set(
|
|
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
|
|
1397
|
-
const fp = findFingerprintByKey(visible,
|
|
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,
|
|
1523
|
+
function findFingerprintByKey(comments, key2) {
|
|
1486
1524
|
for (const c of comments) {
|
|
1487
1525
|
for (const fp of c.fingerprints || []) {
|
|
1488
|
-
if (fingerprintKey(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,
|