@unhingged/vizu-core 0.1.23 → 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/auto.js
CHANGED
|
@@ -132,204 +132,6 @@ function needsPersist(raws) {
|
|
|
132
132
|
return false;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
// src/storage.ts
|
|
136
|
-
var key = (ns) => `vizu:comments:${ns}`;
|
|
137
|
-
var LocalStorageAdapter = class {
|
|
138
|
-
async load(namespace) {
|
|
139
|
-
return readArray(namespace);
|
|
140
|
-
}
|
|
141
|
-
async addComment(namespace, comment) {
|
|
142
|
-
const list = readArray(namespace);
|
|
143
|
-
list.push(comment);
|
|
144
|
-
writeArray(namespace, list);
|
|
145
|
-
}
|
|
146
|
-
async updateComment(namespace, id, patch) {
|
|
147
|
-
const list = readArray(namespace);
|
|
148
|
-
const idx = list.findIndex((c) => c.id === id);
|
|
149
|
-
if (idx === -1) return null;
|
|
150
|
-
const next = { ...list[idx], ...patch, id: list[idx].id };
|
|
151
|
-
list[idx] = next;
|
|
152
|
-
writeArray(namespace, list);
|
|
153
|
-
return next;
|
|
154
|
-
}
|
|
155
|
-
async removeComment(namespace, id) {
|
|
156
|
-
const list = readArray(namespace);
|
|
157
|
-
writeArray(namespace, list.filter((c) => c.id !== id));
|
|
158
|
-
}
|
|
159
|
-
async setAll(namespace, comments) {
|
|
160
|
-
writeArray(namespace, comments);
|
|
161
|
-
}
|
|
162
|
-
async clear(namespace) {
|
|
163
|
-
if (typeof localStorage === "undefined") return;
|
|
164
|
-
localStorage.removeItem(key(namespace));
|
|
165
|
-
}
|
|
166
|
-
async addReply(namespace, commentId, reply) {
|
|
167
|
-
const list = readArray(namespace);
|
|
168
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
169
|
-
if (idx === -1) return null;
|
|
170
|
-
const next = { ...list[idx], replies: [...list[idx].replies ?? [], reply] };
|
|
171
|
-
list[idx] = next;
|
|
172
|
-
writeArray(namespace, list);
|
|
173
|
-
return next;
|
|
174
|
-
}
|
|
175
|
-
async removeReply(namespace, commentId, replyId) {
|
|
176
|
-
const list = readArray(namespace);
|
|
177
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
178
|
-
if (idx === -1) return null;
|
|
179
|
-
const next = {
|
|
180
|
-
...list[idx],
|
|
181
|
-
replies: (list[idx].replies ?? []).filter((r) => r.id !== replyId)
|
|
182
|
-
};
|
|
183
|
-
list[idx] = next;
|
|
184
|
-
writeArray(namespace, list);
|
|
185
|
-
return next;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
function readArray(namespace) {
|
|
189
|
-
if (typeof localStorage === "undefined") return [];
|
|
190
|
-
const raw = localStorage.getItem(key(namespace));
|
|
191
|
-
if (!raw) return [];
|
|
192
|
-
try {
|
|
193
|
-
const parsed = JSON.parse(raw);
|
|
194
|
-
return Array.isArray(parsed) ? migrateComments(parsed) : [];
|
|
195
|
-
} catch {
|
|
196
|
-
return [];
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
function writeArray(namespace, comments) {
|
|
200
|
-
if (typeof localStorage === "undefined") return;
|
|
201
|
-
localStorage.setItem(key(namespace), JSON.stringify(comments));
|
|
202
|
-
}
|
|
203
|
-
var InMemoryStorageAdapter = class {
|
|
204
|
-
constructor() {
|
|
205
|
-
this.store = /* @__PURE__ */ new Map();
|
|
206
|
-
}
|
|
207
|
-
async load(namespace) {
|
|
208
|
-
return [...this.store.get(namespace) || []];
|
|
209
|
-
}
|
|
210
|
-
async addComment(namespace, comment) {
|
|
211
|
-
const list = this.store.get(namespace) ?? [];
|
|
212
|
-
this.store.set(namespace, [...list, comment]);
|
|
213
|
-
}
|
|
214
|
-
async updateComment(namespace, id, patch) {
|
|
215
|
-
const list = this.store.get(namespace) ?? [];
|
|
216
|
-
const idx = list.findIndex((c) => c.id === id);
|
|
217
|
-
if (idx === -1) return null;
|
|
218
|
-
const next = { ...list[idx], ...patch, id: list[idx].id };
|
|
219
|
-
const copy = [...list];
|
|
220
|
-
copy[idx] = next;
|
|
221
|
-
this.store.set(namespace, copy);
|
|
222
|
-
return next;
|
|
223
|
-
}
|
|
224
|
-
async removeComment(namespace, id) {
|
|
225
|
-
const list = this.store.get(namespace) ?? [];
|
|
226
|
-
this.store.set(
|
|
227
|
-
namespace,
|
|
228
|
-
list.filter((c) => c.id !== id)
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
async setAll(namespace, comments) {
|
|
232
|
-
this.store.set(namespace, [...comments]);
|
|
233
|
-
}
|
|
234
|
-
async clear(namespace) {
|
|
235
|
-
this.store.delete(namespace);
|
|
236
|
-
}
|
|
237
|
-
async addReply(namespace, commentId, reply) {
|
|
238
|
-
const list = this.store.get(namespace) ?? [];
|
|
239
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
240
|
-
if (idx === -1) return null;
|
|
241
|
-
const next = { ...list[idx], replies: [...list[idx].replies ?? [], reply] };
|
|
242
|
-
const copy = [...list];
|
|
243
|
-
copy[idx] = next;
|
|
244
|
-
this.store.set(namespace, copy);
|
|
245
|
-
return next;
|
|
246
|
-
}
|
|
247
|
-
async removeReply(namespace, commentId, replyId) {
|
|
248
|
-
const list = this.store.get(namespace) ?? [];
|
|
249
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
250
|
-
if (idx === -1) return null;
|
|
251
|
-
const next = {
|
|
252
|
-
...list[idx],
|
|
253
|
-
replies: (list[idx].replies ?? []).filter((r) => r.id !== replyId)
|
|
254
|
-
};
|
|
255
|
-
const copy = [...list];
|
|
256
|
-
copy[idx] = next;
|
|
257
|
-
this.store.set(namespace, copy);
|
|
258
|
-
return next;
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
var NullStorageAdapter = class {
|
|
262
|
-
async load() {
|
|
263
|
-
return [];
|
|
264
|
-
}
|
|
265
|
-
async addComment() {
|
|
266
|
-
}
|
|
267
|
-
async updateComment() {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
async removeComment() {
|
|
271
|
-
}
|
|
272
|
-
async setAll() {
|
|
273
|
-
}
|
|
274
|
-
async clear() {
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
function isV1Adapter(x) {
|
|
278
|
-
if (!x || typeof x !== "object") return false;
|
|
279
|
-
const o = x;
|
|
280
|
-
return typeof o.load === "function" && typeof o.save === "function" && typeof o.clear === "function" && typeof o.addComment !== "function";
|
|
281
|
-
}
|
|
282
|
-
function wrapV1Adapter(v1) {
|
|
283
|
-
if (!isV1Adapter(v1)) return v1;
|
|
284
|
-
let warned = false;
|
|
285
|
-
const warnOnce = () => {
|
|
286
|
-
if (warned) return;
|
|
287
|
-
warned = true;
|
|
288
|
-
if (typeof console !== "undefined") {
|
|
289
|
-
console.warn(
|
|
290
|
-
"[vizu] StorageAdapter v1 detected. Per-comment ops are emulated via full-array rewrites \u2014 upgrade your adapter to the v2 interface (addComment / updateComment / removeComment / setAll / load / clear) when you can."
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
return {
|
|
295
|
-
async load(namespace) {
|
|
296
|
-
warnOnce();
|
|
297
|
-
return v1.load(namespace);
|
|
298
|
-
},
|
|
299
|
-
async addComment(namespace, comment) {
|
|
300
|
-
warnOnce();
|
|
301
|
-
const list = await v1.load(namespace);
|
|
302
|
-
list.push(comment);
|
|
303
|
-
await v1.save(namespace, list);
|
|
304
|
-
},
|
|
305
|
-
async updateComment(namespace, id, patch) {
|
|
306
|
-
warnOnce();
|
|
307
|
-
const list = await v1.load(namespace);
|
|
308
|
-
const idx = list.findIndex((c) => c.id === id);
|
|
309
|
-
if (idx === -1) return null;
|
|
310
|
-
const next = { ...list[idx], ...patch, id: list[idx].id };
|
|
311
|
-
list[idx] = next;
|
|
312
|
-
await v1.save(namespace, list);
|
|
313
|
-
return next;
|
|
314
|
-
},
|
|
315
|
-
async removeComment(namespace, id) {
|
|
316
|
-
warnOnce();
|
|
317
|
-
const list = await v1.load(namespace);
|
|
318
|
-
await v1.save(
|
|
319
|
-
namespace,
|
|
320
|
-
list.filter((c) => c.id !== id)
|
|
321
|
-
);
|
|
322
|
-
},
|
|
323
|
-
async setAll(namespace, comments) {
|
|
324
|
-
warnOnce();
|
|
325
|
-
await v1.save(namespace, comments);
|
|
326
|
-
},
|
|
327
|
-
async clear(namespace) {
|
|
328
|
-
await v1.clear(namespace);
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
135
|
// src/cloud.ts
|
|
334
136
|
var DEFAULT_API_URL = "https://vizu.unhingged.com";
|
|
335
137
|
var POPUP_WIDTH = 480;
|
|
@@ -1386,8 +1188,8 @@ var Popover = class {
|
|
|
1386
1188
|
const removeBtn = target.closest('[data-vz="remove-anchor"]');
|
|
1387
1189
|
if (removeBtn) {
|
|
1388
1190
|
e.stopPropagation();
|
|
1389
|
-
const
|
|
1390
|
-
if (
|
|
1191
|
+
const key = removeBtn.getAttribute("data-fp-key");
|
|
1192
|
+
if (key) this.removeAnchor(key);
|
|
1391
1193
|
return;
|
|
1392
1194
|
}
|
|
1393
1195
|
const refEl = target.closest('[data-vz="ref"]');
|
|
@@ -1482,8 +1284,8 @@ var Popover = class {
|
|
|
1482
1284
|
}
|
|
1483
1285
|
/** Add another anchor to the in-progress comment (Shift+Click flow). */
|
|
1484
1286
|
addAnchor(fp, target) {
|
|
1485
|
-
const
|
|
1486
|
-
if (this.anchorFingerprints.some((x) => fingerprintKey(x) ===
|
|
1287
|
+
const key = fingerprintKey(fp);
|
|
1288
|
+
if (this.anchorFingerprints.some((x) => fingerprintKey(x) === key)) return false;
|
|
1487
1289
|
this.anchorFingerprints.push(fp);
|
|
1488
1290
|
if (target) this.anchorTargets.push(target);
|
|
1489
1291
|
this.callbacks.onAnchorsChanged([...this.anchorFingerprints]);
|
|
@@ -1492,9 +1294,9 @@ var Popover = class {
|
|
|
1492
1294
|
return true;
|
|
1493
1295
|
}
|
|
1494
1296
|
/** Remove an anchor by fingerprint key. Never removes the last anchor. */
|
|
1495
|
-
removeAnchor(
|
|
1297
|
+
removeAnchor(key) {
|
|
1496
1298
|
if (this.anchorFingerprints.length <= 1) return false;
|
|
1497
|
-
const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) ===
|
|
1299
|
+
const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) === key);
|
|
1498
1300
|
if (idx < 0) return false;
|
|
1499
1301
|
this.anchorFingerprints.splice(idx, 1);
|
|
1500
1302
|
this.anchorTargets.splice(idx, 1);
|
|
@@ -1604,9 +1406,9 @@ var Popover = class {
|
|
|
1604
1406
|
return;
|
|
1605
1407
|
}
|
|
1606
1408
|
const chips = this.anchorFingerprints.map((fp) => {
|
|
1607
|
-
const
|
|
1409
|
+
const key = fingerprintKey(fp);
|
|
1608
1410
|
const label = fingerprintLabel(fp);
|
|
1609
|
-
return `<span class="vz-anchor-chip" data-fp-key="${escapeHtml(
|
|
1411
|
+
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>`;
|
|
1610
1412
|
}).join("");
|
|
1611
1413
|
wrap.innerHTML = `
|
|
1612
1414
|
<div class="vz-anchor-hint">Anchored to ${this.anchorFingerprints.length} elements:</div>
|
|
@@ -2333,10 +2135,10 @@ var Sidebar = class {
|
|
|
2333
2135
|
const fps = c.fingerprints || [];
|
|
2334
2136
|
const primary = fps[0];
|
|
2335
2137
|
if (!primary) continue;
|
|
2336
|
-
const
|
|
2337
|
-
const g = groups.get(
|
|
2138
|
+
const key = fingerprintKey(primary);
|
|
2139
|
+
const g = groups.get(key);
|
|
2338
2140
|
if (g) g.comments.push(c);
|
|
2339
|
-
else groups.set(
|
|
2141
|
+
else groups.set(key, { fp: primary, comments: [c] });
|
|
2340
2142
|
}
|
|
2341
2143
|
const groupHtml = [];
|
|
2342
2144
|
let gi = 0;
|
|
@@ -2397,8 +2199,8 @@ var Sidebar = class {
|
|
|
2397
2199
|
}
|
|
2398
2200
|
const chips = this.el.querySelectorAll('[data-vz="jump-fp"]');
|
|
2399
2201
|
for (const chip of chips) {
|
|
2400
|
-
const
|
|
2401
|
-
const fp = findFingerprintByKey(visible,
|
|
2202
|
+
const key = chip.getAttribute("data-fp-key");
|
|
2203
|
+
const fp = findFingerprintByKey(visible, key || "");
|
|
2402
2204
|
chip.__fp = fp;
|
|
2403
2205
|
}
|
|
2404
2206
|
}
|
|
@@ -2486,10 +2288,10 @@ function renderOrphansHtml(orphans) {
|
|
|
2486
2288
|
</div>
|
|
2487
2289
|
`;
|
|
2488
2290
|
}
|
|
2489
|
-
function findFingerprintByKey(comments,
|
|
2291
|
+
function findFingerprintByKey(comments, key) {
|
|
2490
2292
|
for (const c of comments) {
|
|
2491
2293
|
for (const fp of c.fingerprints || []) {
|
|
2492
|
-
if (fingerprintKey(fp) ===
|
|
2294
|
+
if (fingerprintKey(fp) === key) return fp;
|
|
2493
2295
|
}
|
|
2494
2296
|
}
|
|
2495
2297
|
return null;
|
|
@@ -3584,30 +3386,17 @@ var EventBus = class {
|
|
|
3584
3386
|
// src/index.ts
|
|
3585
3387
|
var DEFAULTS = {
|
|
3586
3388
|
shortcut: "mod+shift+e",
|
|
3587
|
-
namespace: "default",
|
|
3588
3389
|
accent: "#FF6647"
|
|
3589
3390
|
};
|
|
3590
|
-
function resolveStorage(storage, cloud, defaultMode, onAuthChanged) {
|
|
3591
|
-
if (cloud) {
|
|
3592
|
-
if (storage && typeof console !== "undefined") {
|
|
3593
|
-
console.warn("[vizu] Both `cloud` and `storage` set; `cloud` wins. Drop `storage` to silence this.");
|
|
3594
|
-
}
|
|
3595
|
-
return new CloudStorageAdapter({
|
|
3596
|
-
workspace: cloud.workspace,
|
|
3597
|
-
apiUrl: cloud.apiUrl,
|
|
3598
|
-
autoSignIn: cloud.autoSignIn,
|
|
3599
|
-
onAuthChanged
|
|
3600
|
-
});
|
|
3601
|
-
}
|
|
3602
|
-
if (!storage) return defaultMode === "local" ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
|
|
3603
|
-
if (storage === "local") return new LocalStorageAdapter();
|
|
3604
|
-
if (storage === "memory") return new InMemoryStorageAdapter();
|
|
3605
|
-
if (storage === "none") return new NullStorageAdapter();
|
|
3606
|
-
if (isV1Adapter(storage)) return wrapV1Adapter(storage);
|
|
3607
|
-
return storage;
|
|
3608
|
-
}
|
|
3609
3391
|
var Vizu = class {
|
|
3610
|
-
constructor(options
|
|
3392
|
+
constructor(options, storageOverride) {
|
|
3393
|
+
/**
|
|
3394
|
+
* The cloud adapter when this instance runs in the normal cloud mode;
|
|
3395
|
+
* null only under the internal storage override (tests / storybook).
|
|
3396
|
+
* Auth-gated behavior (mount gate, write gate, sign-in kicks) branches
|
|
3397
|
+
* on this instead of `instanceof` checks against `this.storage`.
|
|
3398
|
+
*/
|
|
3399
|
+
this.cloud = null;
|
|
3611
3400
|
this.root = null;
|
|
3612
3401
|
this.highlighter = null;
|
|
3613
3402
|
this.popover = null;
|
|
@@ -3669,8 +3458,8 @@ var Vizu = class {
|
|
|
3669
3458
|
this.pendingTargets = [];
|
|
3670
3459
|
/**
|
|
3671
3460
|
* Subscribe to remote-change notifications from the active storage
|
|
3672
|
-
* adapter
|
|
3673
|
-
*
|
|
3461
|
+
* adapter, when it implements `subscribe` (none do today — kept as
|
|
3462
|
+
* the hook realtime lands on). Torn down on `destroy()`.
|
|
3674
3463
|
*/
|
|
3675
3464
|
this.unsubscribeStorage = null;
|
|
3676
3465
|
this.onKeyDown = (e) => {
|
|
@@ -3713,23 +3502,34 @@ var Vizu = class {
|
|
|
3713
3502
|
this.popover?.reposition();
|
|
3714
3503
|
});
|
|
3715
3504
|
};
|
|
3505
|
+
if (!storageOverride && !options?.workspace) {
|
|
3506
|
+
throw new Error(
|
|
3507
|
+
"[vizu] `workspace` is required \u2014 create one at https://vizu.unhingged.com and pass new Vizu({ workspace: 'your-slug' })."
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3716
3510
|
this.opts = { ...options };
|
|
3717
3511
|
this.opts.shortcut = options.shortcut ?? DEFAULTS.shortcut;
|
|
3718
|
-
this.opts.namespace = options.namespace ?? DEFAULTS.namespace;
|
|
3719
3512
|
this.opts.accent = options.accent ?? DEFAULTS.accent;
|
|
3513
|
+
this.ns = options.workspace ?? "default";
|
|
3720
3514
|
this.parsedShortcut = parseShortcut(this.opts.shortcut);
|
|
3721
3515
|
this.user = options.user ?? null;
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3516
|
+
if (storageOverride) {
|
|
3517
|
+
this.storage = storageOverride;
|
|
3518
|
+
this.cloud = null;
|
|
3519
|
+
} else {
|
|
3520
|
+
this.cloud = new CloudStorageAdapter({
|
|
3521
|
+
workspace: options.workspace,
|
|
3522
|
+
apiUrl: options.apiUrl,
|
|
3523
|
+
autoSignIn: options.autoSignIn,
|
|
3524
|
+
onAuthChanged: (info) => {
|
|
3525
|
+
if (info.user) this.setUser(info.user);
|
|
3526
|
+
if (this.hasLoadedComments) {
|
|
3527
|
+
void this.loadComments();
|
|
3528
|
+
}
|
|
3730
3529
|
}
|
|
3731
|
-
}
|
|
3732
|
-
|
|
3530
|
+
});
|
|
3531
|
+
this.storage = this.cloud;
|
|
3532
|
+
}
|
|
3733
3533
|
if (options.actions) this.actions = [...options.actions];
|
|
3734
3534
|
if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
|
|
3735
3535
|
if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
|
|
@@ -3771,8 +3571,8 @@ var Vizu = class {
|
|
|
3771
3571
|
* Resolution order:
|
|
3772
3572
|
* 1. `options.mentionable` if provided — host-supplied static or
|
|
3773
3573
|
* custom resolver. Wins over everything else.
|
|
3774
|
-
* 2.
|
|
3775
|
-
* 3. Empty array (
|
|
3574
|
+
* 2. The workspace's mentionable-members API.
|
|
3575
|
+
* 3. Empty array (internal storage override, no override).
|
|
3776
3576
|
*/
|
|
3777
3577
|
async searchMentionable() {
|
|
3778
3578
|
if (this.opts.mentionable) {
|
|
@@ -3785,38 +3585,36 @@ var Vizu = class {
|
|
|
3785
3585
|
return [];
|
|
3786
3586
|
}
|
|
3787
3587
|
}
|
|
3788
|
-
if (this.
|
|
3789
|
-
return this.storage.searchMentionable();
|
|
3790
|
-
}
|
|
3588
|
+
if (this.cloud) return this.cloud.searchMentionable();
|
|
3791
3589
|
return [];
|
|
3792
3590
|
}
|
|
3793
3591
|
/**
|
|
3794
3592
|
* Whether the full Vizu UI (pill, highlighter, markers, popover) can
|
|
3795
|
-
* be mounted right now
|
|
3796
|
-
*
|
|
3797
|
-
* auth
|
|
3798
|
-
*
|
|
3799
|
-
*
|
|
3593
|
+
* be mounted right now: not until the user is signed in to the
|
|
3594
|
+
* workspace. Re-checked from `enable()` and from `setUser()` so that
|
|
3595
|
+
* the moment auth resolves, the UI appears without the user needing
|
|
3596
|
+
* to press the shortcut again. (Under the internal storage override
|
|
3597
|
+
* there is no auth concept and the UI mounts immediately.)
|
|
3800
3598
|
*/
|
|
3801
3599
|
shouldMount() {
|
|
3802
|
-
if (!
|
|
3600
|
+
if (!this.cloud) return true;
|
|
3803
3601
|
return this.user !== null;
|
|
3804
3602
|
}
|
|
3805
3603
|
/**
|
|
3806
|
-
*
|
|
3807
|
-
*
|
|
3808
|
-
*
|
|
3809
|
-
*
|
|
3810
|
-
*
|
|
3811
|
-
*
|
|
3604
|
+
* Write gate. Every entry point that creates or modifies a comment
|
|
3605
|
+
* first checks that we have a known user. Without one, the write
|
|
3606
|
+
* would land as Anonymous (or 401 on the backend) — neither is the
|
|
3607
|
+
* right UX. So we re-trigger the sign-in popup and refuse the action.
|
|
3608
|
+
* The user signs in, this.user gets set via onAuthChanged, then they
|
|
3609
|
+
* can retry the click themselves.
|
|
3812
3610
|
*
|
|
3813
3611
|
* Returns true when the caller may proceed.
|
|
3814
3612
|
*/
|
|
3815
3613
|
requireUserOrAuth() {
|
|
3816
|
-
if (!
|
|
3614
|
+
if (!this.cloud) return true;
|
|
3817
3615
|
if (this.user) return true;
|
|
3818
|
-
if (this.
|
|
3819
|
-
void this.
|
|
3616
|
+
if (this.cloud.isAccessDenied()) return false;
|
|
3617
|
+
void this.cloud.preflightAuth().catch(() => {
|
|
3820
3618
|
});
|
|
3821
3619
|
return false;
|
|
3822
3620
|
}
|
|
@@ -3843,19 +3641,19 @@ var Vizu = class {
|
|
|
3843
3641
|
return this.enabled;
|
|
3844
3642
|
}
|
|
3845
3643
|
/**
|
|
3846
|
-
* Kick the
|
|
3644
|
+
* Kick the sign-in flow. Call this alongside `enable()` from an
|
|
3847
3645
|
* explicit user gesture — a host-rendered launcher button, a menu item —
|
|
3848
|
-
* so
|
|
3849
|
-
*
|
|
3850
|
-
*
|
|
3851
|
-
*
|
|
3646
|
+
* so users get the sign-in popup immediately instead of only after
|
|
3647
|
+
* clicking an element. The keyboard shortcut goes through this same
|
|
3648
|
+
* path. No-op when already signed in, when the workspace has already
|
|
3649
|
+
* denied access this session, or under the internal storage override.
|
|
3852
3650
|
*/
|
|
3853
3651
|
requestAuth() {
|
|
3854
|
-
if (!
|
|
3652
|
+
if (!this.cloud) return;
|
|
3855
3653
|
if (this.user) return;
|
|
3856
|
-
if (this.
|
|
3857
|
-
void this.
|
|
3858
|
-
if (err?.code === "auth_canceled" || err?.code === "popup_blocked") return;
|
|
3654
|
+
if (this.cloud.isAccessDenied()) return;
|
|
3655
|
+
void this.cloud.preflightAuth().catch((err) => {
|
|
3656
|
+
if (err?.code === "auth_canceled" || err?.code === "popup_blocked" || err?.code === "auth_required") return;
|
|
3859
3657
|
if (typeof console !== "undefined") {
|
|
3860
3658
|
console.warn("[vizu] preflight auth failed:", err);
|
|
3861
3659
|
}
|
|
@@ -3896,7 +3694,7 @@ var Vizu = class {
|
|
|
3896
3694
|
}
|
|
3897
3695
|
async setComments(comments, opts) {
|
|
3898
3696
|
this.comments = migrateComments(comments);
|
|
3899
|
-
if (opts?.persist !== false) await this.storage.setAll(this.
|
|
3697
|
+
if (opts?.persist !== false) await this.storage.setAll(this.ns, this.comments);
|
|
3900
3698
|
this.bus.emit("comments:set", { comments: this.getComments() });
|
|
3901
3699
|
this.refreshUi();
|
|
3902
3700
|
}
|
|
@@ -3911,7 +3709,7 @@ var Vizu = class {
|
|
|
3911
3709
|
const { id: _i, schemaVersion: _s, ...safe } = patch;
|
|
3912
3710
|
const next = { ...this.comments[idx], ...safe };
|
|
3913
3711
|
this.comments[idx] = next;
|
|
3914
|
-
const persisted = await this.storage.updateComment(this.
|
|
3712
|
+
const persisted = await this.storage.updateComment(this.ns, id, safe);
|
|
3915
3713
|
this.bus.emit("comment:updated", { comment: persisted ?? next });
|
|
3916
3714
|
this.refreshUi();
|
|
3917
3715
|
return next;
|
|
@@ -3953,7 +3751,7 @@ var Vizu = class {
|
|
|
3953
3751
|
const idx = this.comments.findIndex((x) => x.id === c.id);
|
|
3954
3752
|
if (idx !== -1) this.comments[idx] = { ...this.comments[idx], ...patch };
|
|
3955
3753
|
try {
|
|
3956
|
-
await this.storage.updateComment(this.
|
|
3754
|
+
await this.storage.updateComment(this.ns, c.id, patch);
|
|
3957
3755
|
} catch (err) {
|
|
3958
3756
|
if (typeof console !== "undefined") {
|
|
3959
3757
|
console.warn("[vizu] self-heal write failed for", c.id, err);
|
|
@@ -3975,8 +3773,8 @@ var Vizu = class {
|
|
|
3975
3773
|
}
|
|
3976
3774
|
/**
|
|
3977
3775
|
* True iff the active storage adapter implements `uploadAttachment`.
|
|
3978
|
-
* The popover hides its upload UI when this returns false (
|
|
3979
|
-
*
|
|
3776
|
+
* The popover hides its upload UI when this returns false (only the
|
|
3777
|
+
* internal offline adapters lack uploads).
|
|
3980
3778
|
*/
|
|
3981
3779
|
canUploadAttachments() {
|
|
3982
3780
|
return typeof this.storage.uploadAttachment === "function";
|
|
@@ -3989,10 +3787,10 @@ var Vizu = class {
|
|
|
3989
3787
|
async uploadAttachment(file) {
|
|
3990
3788
|
if (!this.storage.uploadAttachment) {
|
|
3991
3789
|
throw new Error(
|
|
3992
|
-
"[vizu] active storage adapter does not support uploadAttachment
|
|
3790
|
+
"[vizu] active storage adapter does not support uploadAttachment"
|
|
3993
3791
|
);
|
|
3994
3792
|
}
|
|
3995
|
-
return this.storage.uploadAttachment(this.
|
|
3793
|
+
return this.storage.uploadAttachment(this.ns, file);
|
|
3996
3794
|
}
|
|
3997
3795
|
/**
|
|
3998
3796
|
* Append a reply to a comment. Returns the parent comment or null if
|
|
@@ -4020,9 +3818,9 @@ var Vizu = class {
|
|
|
4020
3818
|
this.comments[idx] = updated;
|
|
4021
3819
|
try {
|
|
4022
3820
|
if (this.storage.addReply) {
|
|
4023
|
-
await this.storage.addReply(this.
|
|
3821
|
+
await this.storage.addReply(this.ns, commentId, reply);
|
|
4024
3822
|
} else {
|
|
4025
|
-
await this.storage.updateComment(this.
|
|
3823
|
+
await this.storage.updateComment(this.ns, commentId, { replies: updated.replies });
|
|
4026
3824
|
}
|
|
4027
3825
|
} catch (err) {
|
|
4028
3826
|
const cur = this.comments[idx];
|
|
@@ -4062,9 +3860,9 @@ var Vizu = class {
|
|
|
4062
3860
|
const updated = { ...this.comments[idx], replies: filtered };
|
|
4063
3861
|
this.comments[idx] = updated;
|
|
4064
3862
|
if (this.storage.removeReply) {
|
|
4065
|
-
await this.storage.removeReply(this.
|
|
3863
|
+
await this.storage.removeReply(this.ns, commentId, replyId);
|
|
4066
3864
|
} else {
|
|
4067
|
-
await this.storage.updateComment(this.
|
|
3865
|
+
await this.storage.updateComment(this.ns, commentId, { replies: filtered });
|
|
4068
3866
|
}
|
|
4069
3867
|
this.bus.emit("comment:updated", { comment: updated });
|
|
4070
3868
|
this.refreshUi();
|
|
@@ -4077,7 +3875,7 @@ var Vizu = class {
|
|
|
4077
3875
|
async clearAll() {
|
|
4078
3876
|
const previous = this.comments;
|
|
4079
3877
|
this.comments = [];
|
|
4080
|
-
await this.storage.clear(this.
|
|
3878
|
+
await this.storage.clear(this.ns);
|
|
4081
3879
|
this.bus.emit("comments:cleared", { previous });
|
|
4082
3880
|
this.refreshUi();
|
|
4083
3881
|
this.popover?.close();
|
|
@@ -4118,10 +3916,10 @@ var Vizu = class {
|
|
|
4118
3916
|
}
|
|
4119
3917
|
/* ===== Internal ===== */
|
|
4120
3918
|
async loadComments() {
|
|
4121
|
-
const raw = await this.storage.load(this.
|
|
3919
|
+
const raw = await this.storage.load(this.ns);
|
|
4122
3920
|
this.comments = migrateComments(raw);
|
|
4123
3921
|
if (needsPersist(raw)) {
|
|
4124
|
-
void this.storage.setAll(this.
|
|
3922
|
+
void this.storage.setAll(this.ns, this.comments).catch(() => {
|
|
4125
3923
|
});
|
|
4126
3924
|
}
|
|
4127
3925
|
this.bus.emit("comments:loaded", { comments: this.getComments() });
|
|
@@ -4130,7 +3928,7 @@ var Vizu = class {
|
|
|
4130
3928
|
subscribeStorage() {
|
|
4131
3929
|
if (this.unsubscribeStorage) return;
|
|
4132
3930
|
if (!this.storage.subscribe) return;
|
|
4133
|
-
this.unsubscribeStorage = this.storage.subscribe(this.
|
|
3931
|
+
this.unsubscribeStorage = this.storage.subscribe(this.ns, (e) => {
|
|
4134
3932
|
if (e.type === "added") {
|
|
4135
3933
|
const fresh = migrateComment(e.comment);
|
|
4136
3934
|
if (!this.comments.some((c) => c.id === fresh.id)) {
|
|
@@ -4282,9 +4080,9 @@ var Vizu = class {
|
|
|
4282
4080
|
this.bus.emit("element:deselected", {});
|
|
4283
4081
|
}
|
|
4284
4082
|
addToSelection(fp, el) {
|
|
4285
|
-
const
|
|
4286
|
-
if (this.pendingFingerprints.some((x) => fingerprintKey(x) ===
|
|
4287
|
-
this.pendingFingerprints = this.pendingFingerprints.filter((x) => fingerprintKey(x) !==
|
|
4083
|
+
const key = fingerprintKey(fp);
|
|
4084
|
+
if (this.pendingFingerprints.some((x) => fingerprintKey(x) === key)) {
|
|
4085
|
+
this.pendingFingerprints = this.pendingFingerprints.filter((x) => fingerprintKey(x) !== key);
|
|
4288
4086
|
this.pendingTargets = this.pendingTargets.filter((t) => t !== el);
|
|
4289
4087
|
} else {
|
|
4290
4088
|
this.pendingFingerprints.push(fp);
|
|
@@ -4308,9 +4106,9 @@ var Vizu = class {
|
|
|
4308
4106
|
return (c.status ?? "open") === this.statusFilter;
|
|
4309
4107
|
}
|
|
4310
4108
|
commentsForElement(fp) {
|
|
4311
|
-
const
|
|
4109
|
+
const key = fingerprintKey(fp);
|
|
4312
4110
|
return this.comments.filter(
|
|
4313
|
-
(c) => (c.fingerprints || []).some((f) => fingerprintKey(f) ===
|
|
4111
|
+
(c) => (c.fingerprints || []).some((f) => fingerprintKey(f) === key)
|
|
4314
4112
|
);
|
|
4315
4113
|
}
|
|
4316
4114
|
async addCommentFromPopover(text, fps, references, mentions, attachments) {
|
|
@@ -4334,7 +4132,7 @@ var Vizu = class {
|
|
|
4334
4132
|
mentions: mentions ?? []
|
|
4335
4133
|
};
|
|
4336
4134
|
this.comments.push(c);
|
|
4337
|
-
await this.storage.addComment(this.
|
|
4135
|
+
await this.storage.addComment(this.ns, c);
|
|
4338
4136
|
this.bus.emit("comment:added", { comment: c });
|
|
4339
4137
|
this.popover?.update(this.commentsForElement(fps[0]));
|
|
4340
4138
|
this.refreshUi();
|
|
@@ -4345,7 +4143,7 @@ var Vizu = class {
|
|
|
4345
4143
|
const comment = this.comments[idx];
|
|
4346
4144
|
this.comments = this.comments.filter((c) => c.id !== id);
|
|
4347
4145
|
try {
|
|
4348
|
-
await this.storage.removeComment(this.
|
|
4146
|
+
await this.storage.removeComment(this.ns, id);
|
|
4349
4147
|
} catch (err) {
|
|
4350
4148
|
this.comments.splice(idx, 0, comment);
|
|
4351
4149
|
const status = err?.status;
|
|
@@ -4443,8 +4241,8 @@ var Vizu = class {
|
|
|
4443
4241
|
this.spotlights.delete(entry);
|
|
4444
4242
|
}, 1800);
|
|
4445
4243
|
}
|
|
4446
|
-
const
|
|
4447
|
-
const outline = this.outlines.get(
|
|
4244
|
+
const key = fingerprintKey(fp);
|
|
4245
|
+
const outline = this.outlines.get(key)?.outline;
|
|
4448
4246
|
if (outline) {
|
|
4449
4247
|
setTimeout(() => {
|
|
4450
4248
|
outline.classList.remove("is-pulsing");
|
|
@@ -4467,8 +4265,8 @@ var Vizu = class {
|
|
|
4467
4265
|
const bestConf = (a, b) => a === void 0 ? b : rankConf(b) > rankConf(a) ? b : a;
|
|
4468
4266
|
for (const c of this.comments) {
|
|
4469
4267
|
for (const fp of c.fingerprints || []) {
|
|
4470
|
-
const
|
|
4471
|
-
let bucket = byElement.get(
|
|
4268
|
+
const key = fingerprintKey(fp);
|
|
4269
|
+
let bucket = byElement.get(key);
|
|
4472
4270
|
if (!bucket) {
|
|
4473
4271
|
const match = findByFingerprint(fp);
|
|
4474
4272
|
bucket = {
|
|
@@ -4477,7 +4275,7 @@ var Vizu = class {
|
|
|
4477
4275
|
confidence: match.confidence,
|
|
4478
4276
|
commentIds: []
|
|
4479
4277
|
};
|
|
4480
|
-
byElement.set(
|
|
4278
|
+
byElement.set(key, bucket);
|
|
4481
4279
|
}
|
|
4482
4280
|
bucket.commentIds.push(c.id);
|
|
4483
4281
|
perComment.set(c.id, bestConf(perComment.get(c.id), bucket.confidence));
|
|
@@ -4496,7 +4294,7 @@ var Vizu = class {
|
|
|
4496
4294
|
if (this.resolvedConfidence.get(c.id) !== "drifted") continue;
|
|
4497
4295
|
void this.selfHealComment(c, byElement);
|
|
4498
4296
|
}
|
|
4499
|
-
for (const [
|
|
4297
|
+
for (const [key, info] of byElement) {
|
|
4500
4298
|
if (info.confidence === "orphaned" || !info.target) continue;
|
|
4501
4299
|
const idsForFilter = info.commentIds.filter((id) => this.commentMatchesStatusFilter(id));
|
|
4502
4300
|
if (idsForFilter.length === 0) continue;
|
|
@@ -4504,11 +4302,11 @@ var Vizu = class {
|
|
|
4504
4302
|
outlineEl.className = "vz-comment-outline";
|
|
4505
4303
|
if (info.confidence === "drifted") outlineEl.classList.add("vz-comment-outline-drifted");
|
|
4506
4304
|
this.root.appendChild(outlineEl);
|
|
4507
|
-
this.outlines.set(
|
|
4305
|
+
this.outlines.set(key, { outline: outlineEl, target: info.target, fp: info.fp });
|
|
4508
4306
|
const marker = document.createElement("button");
|
|
4509
4307
|
marker.className = "vz-marker";
|
|
4510
4308
|
if (info.confidence === "drifted") marker.classList.add("vz-marker-drifted");
|
|
4511
|
-
marker.setAttribute("data-element-key",
|
|
4309
|
+
marker.setAttribute("data-element-key", key);
|
|
4512
4310
|
marker.setAttribute("data-confidence", info.confidence);
|
|
4513
4311
|
const total = idsForFilter.length;
|
|
4514
4312
|
if (total > 1) {
|
|
@@ -4527,9 +4325,9 @@ var Vizu = class {
|
|
|
4527
4325
|
this.openPopoverFor([info.target], [info.fp], elComments);
|
|
4528
4326
|
}
|
|
4529
4327
|
});
|
|
4530
|
-
marker.addEventListener("mouseenter", () => this.pulseRelatedElements(idsForFilter,
|
|
4328
|
+
marker.addEventListener("mouseenter", () => this.pulseRelatedElements(idsForFilter, key));
|
|
4531
4329
|
this.root.appendChild(marker);
|
|
4532
|
-
this.markers.set(
|
|
4330
|
+
this.markers.set(key, { marker, target: info.target, fp: info.fp, commentIds: idsForFilter });
|
|
4533
4331
|
if (info.target) {
|
|
4534
4332
|
this.positionMarker(marker, info.target);
|
|
4535
4333
|
this.positionOutline(outlineEl, info.target);
|
|
@@ -4552,8 +4350,8 @@ var Vizu = class {
|
|
|
4552
4350
|
if (k !== selfKey) relatedKeys.add(k);
|
|
4553
4351
|
}
|
|
4554
4352
|
}
|
|
4555
|
-
for (const
|
|
4556
|
-
const outline = this.outlines.get(
|
|
4353
|
+
for (const key of relatedKeys) {
|
|
4354
|
+
const outline = this.outlines.get(key)?.outline;
|
|
4557
4355
|
if (!outline) continue;
|
|
4558
4356
|
outline.classList.remove("is-pulsing");
|
|
4559
4357
|
void outline.offsetWidth;
|
|
@@ -4587,22 +4385,22 @@ var Vizu = class {
|
|
|
4587
4385
|
syncActiveAnchorOutlines(fps) {
|
|
4588
4386
|
if (!this.root) return;
|
|
4589
4387
|
const wanted = new Set(fps.map((fp) => fingerprintKey(fp)));
|
|
4590
|
-
for (const [
|
|
4591
|
-
if (!wanted.has(
|
|
4388
|
+
for (const [key, info] of this.activeAnchorOutlines) {
|
|
4389
|
+
if (!wanted.has(key)) {
|
|
4592
4390
|
info.el.remove();
|
|
4593
|
-
this.activeAnchorOutlines.delete(
|
|
4391
|
+
this.activeAnchorOutlines.delete(key);
|
|
4594
4392
|
}
|
|
4595
4393
|
}
|
|
4596
4394
|
for (const fp of fps) {
|
|
4597
|
-
const
|
|
4598
|
-
if (this.activeAnchorOutlines.has(
|
|
4395
|
+
const key = fingerprintKey(fp);
|
|
4396
|
+
if (this.activeAnchorOutlines.has(key)) continue;
|
|
4599
4397
|
const outline = document.createElement("div");
|
|
4600
4398
|
outline.className = "vz-comment-outline is-selected-pending";
|
|
4601
4399
|
this.root.appendChild(outline);
|
|
4602
4400
|
const target = findElementByFingerprint(fp);
|
|
4603
4401
|
if (target) this.positionOutline(outline, target);
|
|
4604
4402
|
else outline.style.display = "none";
|
|
4605
|
-
this.activeAnchorOutlines.set(
|
|
4403
|
+
this.activeAnchorOutlines.set(key, { el: outline, fp });
|
|
4606
4404
|
}
|
|
4607
4405
|
}
|
|
4608
4406
|
clearActiveAnchorOutlines() {
|
|
@@ -4661,7 +4459,12 @@ function init() {
|
|
|
4661
4459
|
if (window.__vizu) return;
|
|
4662
4460
|
const script = findScript();
|
|
4663
4461
|
const ds = script?.dataset || {};
|
|
4664
|
-
|
|
4462
|
+
if (!ds.workspace) {
|
|
4463
|
+
console.error(
|
|
4464
|
+
'[vizu] data-workspace is required. Get your install snippet at https://vizu.unhingged.com \u2014 <script src="\u2026/vizu.min.js" data-workspace="your-slug"></script>'
|
|
4465
|
+
);
|
|
4466
|
+
return;
|
|
4467
|
+
}
|
|
4665
4468
|
const user = ds.userName ? {
|
|
4666
4469
|
id: ds.userId,
|
|
4667
4470
|
name: ds.userName,
|
|
@@ -4669,16 +4472,17 @@ function init() {
|
|
|
4669
4472
|
email: ds.userEmail
|
|
4670
4473
|
} : void 0;
|
|
4671
4474
|
const opts = {
|
|
4475
|
+
workspace: ds.workspace,
|
|
4476
|
+
apiUrl: ds.apiUrl,
|
|
4477
|
+
autoSignIn: ds.autoSignIn === "false" ? false : void 0,
|
|
4672
4478
|
shortcut: ds.shortcut || "mod+shift+e",
|
|
4673
|
-
namespace: ds.namespace || "default",
|
|
4674
4479
|
pageVersion: ds.version,
|
|
4675
4480
|
accent: ds.accent,
|
|
4676
4481
|
startEnabled: ds.startEnabled === "true" || ds.startEnabled === "" || ds.startEnabled === "1",
|
|
4677
4482
|
ignoreSelectors: ds.ignore ? ds.ignore.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
4678
|
-
storage,
|
|
4679
4483
|
user
|
|
4680
4484
|
};
|
|
4681
|
-
const v = new Vizu(opts
|
|
4485
|
+
const v = new Vizu(opts);
|
|
4682
4486
|
window.__vizu = v;
|
|
4683
4487
|
window.Vizu = Vizu;
|
|
4684
4488
|
}
|