@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/auto.cjs
CHANGED
|
@@ -9,9 +9,9 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
var __copyProps = (to, from, except, desc) => {
|
|
11
11
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let
|
|
13
|
-
if (!__hasOwnProp.call(to,
|
|
14
|
-
__defProp(to,
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
15
|
}
|
|
16
16
|
return to;
|
|
17
17
|
};
|
|
@@ -158,204 +158,6 @@ function needsPersist(raws) {
|
|
|
158
158
|
return false;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
// src/storage.ts
|
|
162
|
-
var key = (ns) => `vizu:comments:${ns}`;
|
|
163
|
-
var LocalStorageAdapter = class {
|
|
164
|
-
async load(namespace) {
|
|
165
|
-
return readArray(namespace);
|
|
166
|
-
}
|
|
167
|
-
async addComment(namespace, comment) {
|
|
168
|
-
const list = readArray(namespace);
|
|
169
|
-
list.push(comment);
|
|
170
|
-
writeArray(namespace, list);
|
|
171
|
-
}
|
|
172
|
-
async updateComment(namespace, id, patch) {
|
|
173
|
-
const list = readArray(namespace);
|
|
174
|
-
const idx = list.findIndex((c) => c.id === id);
|
|
175
|
-
if (idx === -1) return null;
|
|
176
|
-
const next = { ...list[idx], ...patch, id: list[idx].id };
|
|
177
|
-
list[idx] = next;
|
|
178
|
-
writeArray(namespace, list);
|
|
179
|
-
return next;
|
|
180
|
-
}
|
|
181
|
-
async removeComment(namespace, id) {
|
|
182
|
-
const list = readArray(namespace);
|
|
183
|
-
writeArray(namespace, list.filter((c) => c.id !== id));
|
|
184
|
-
}
|
|
185
|
-
async setAll(namespace, comments) {
|
|
186
|
-
writeArray(namespace, comments);
|
|
187
|
-
}
|
|
188
|
-
async clear(namespace) {
|
|
189
|
-
if (typeof localStorage === "undefined") return;
|
|
190
|
-
localStorage.removeItem(key(namespace));
|
|
191
|
-
}
|
|
192
|
-
async addReply(namespace, commentId, reply) {
|
|
193
|
-
const list = readArray(namespace);
|
|
194
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
195
|
-
if (idx === -1) return null;
|
|
196
|
-
const next = { ...list[idx], replies: [...list[idx].replies ?? [], reply] };
|
|
197
|
-
list[idx] = next;
|
|
198
|
-
writeArray(namespace, list);
|
|
199
|
-
return next;
|
|
200
|
-
}
|
|
201
|
-
async removeReply(namespace, commentId, replyId) {
|
|
202
|
-
const list = readArray(namespace);
|
|
203
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
204
|
-
if (idx === -1) return null;
|
|
205
|
-
const next = {
|
|
206
|
-
...list[idx],
|
|
207
|
-
replies: (list[idx].replies ?? []).filter((r) => r.id !== replyId)
|
|
208
|
-
};
|
|
209
|
-
list[idx] = next;
|
|
210
|
-
writeArray(namespace, list);
|
|
211
|
-
return next;
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
function readArray(namespace) {
|
|
215
|
-
if (typeof localStorage === "undefined") return [];
|
|
216
|
-
const raw = localStorage.getItem(key(namespace));
|
|
217
|
-
if (!raw) return [];
|
|
218
|
-
try {
|
|
219
|
-
const parsed = JSON.parse(raw);
|
|
220
|
-
return Array.isArray(parsed) ? migrateComments(parsed) : [];
|
|
221
|
-
} catch {
|
|
222
|
-
return [];
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function writeArray(namespace, comments) {
|
|
226
|
-
if (typeof localStorage === "undefined") return;
|
|
227
|
-
localStorage.setItem(key(namespace), JSON.stringify(comments));
|
|
228
|
-
}
|
|
229
|
-
var InMemoryStorageAdapter = class {
|
|
230
|
-
constructor() {
|
|
231
|
-
this.store = /* @__PURE__ */ new Map();
|
|
232
|
-
}
|
|
233
|
-
async load(namespace) {
|
|
234
|
-
return [...this.store.get(namespace) || []];
|
|
235
|
-
}
|
|
236
|
-
async addComment(namespace, comment) {
|
|
237
|
-
const list = this.store.get(namespace) ?? [];
|
|
238
|
-
this.store.set(namespace, [...list, comment]);
|
|
239
|
-
}
|
|
240
|
-
async updateComment(namespace, id, patch) {
|
|
241
|
-
const list = this.store.get(namespace) ?? [];
|
|
242
|
-
const idx = list.findIndex((c) => c.id === id);
|
|
243
|
-
if (idx === -1) return null;
|
|
244
|
-
const next = { ...list[idx], ...patch, id: list[idx].id };
|
|
245
|
-
const copy = [...list];
|
|
246
|
-
copy[idx] = next;
|
|
247
|
-
this.store.set(namespace, copy);
|
|
248
|
-
return next;
|
|
249
|
-
}
|
|
250
|
-
async removeComment(namespace, id) {
|
|
251
|
-
const list = this.store.get(namespace) ?? [];
|
|
252
|
-
this.store.set(
|
|
253
|
-
namespace,
|
|
254
|
-
list.filter((c) => c.id !== id)
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
async setAll(namespace, comments) {
|
|
258
|
-
this.store.set(namespace, [...comments]);
|
|
259
|
-
}
|
|
260
|
-
async clear(namespace) {
|
|
261
|
-
this.store.delete(namespace);
|
|
262
|
-
}
|
|
263
|
-
async addReply(namespace, commentId, reply) {
|
|
264
|
-
const list = this.store.get(namespace) ?? [];
|
|
265
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
266
|
-
if (idx === -1) return null;
|
|
267
|
-
const next = { ...list[idx], replies: [...list[idx].replies ?? [], reply] };
|
|
268
|
-
const copy = [...list];
|
|
269
|
-
copy[idx] = next;
|
|
270
|
-
this.store.set(namespace, copy);
|
|
271
|
-
return next;
|
|
272
|
-
}
|
|
273
|
-
async removeReply(namespace, commentId, replyId) {
|
|
274
|
-
const list = this.store.get(namespace) ?? [];
|
|
275
|
-
const idx = list.findIndex((c) => c.id === commentId);
|
|
276
|
-
if (idx === -1) return null;
|
|
277
|
-
const next = {
|
|
278
|
-
...list[idx],
|
|
279
|
-
replies: (list[idx].replies ?? []).filter((r) => r.id !== replyId)
|
|
280
|
-
};
|
|
281
|
-
const copy = [...list];
|
|
282
|
-
copy[idx] = next;
|
|
283
|
-
this.store.set(namespace, copy);
|
|
284
|
-
return next;
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
var NullStorageAdapter = class {
|
|
288
|
-
async load() {
|
|
289
|
-
return [];
|
|
290
|
-
}
|
|
291
|
-
async addComment() {
|
|
292
|
-
}
|
|
293
|
-
async updateComment() {
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
async removeComment() {
|
|
297
|
-
}
|
|
298
|
-
async setAll() {
|
|
299
|
-
}
|
|
300
|
-
async clear() {
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
function isV1Adapter(x) {
|
|
304
|
-
if (!x || typeof x !== "object") return false;
|
|
305
|
-
const o = x;
|
|
306
|
-
return typeof o.load === "function" && typeof o.save === "function" && typeof o.clear === "function" && typeof o.addComment !== "function";
|
|
307
|
-
}
|
|
308
|
-
function wrapV1Adapter(v1) {
|
|
309
|
-
if (!isV1Adapter(v1)) return v1;
|
|
310
|
-
let warned = false;
|
|
311
|
-
const warnOnce = () => {
|
|
312
|
-
if (warned) return;
|
|
313
|
-
warned = true;
|
|
314
|
-
if (typeof console !== "undefined") {
|
|
315
|
-
console.warn(
|
|
316
|
-
"[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."
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
return {
|
|
321
|
-
async load(namespace) {
|
|
322
|
-
warnOnce();
|
|
323
|
-
return v1.load(namespace);
|
|
324
|
-
},
|
|
325
|
-
async addComment(namespace, comment) {
|
|
326
|
-
warnOnce();
|
|
327
|
-
const list = await v1.load(namespace);
|
|
328
|
-
list.push(comment);
|
|
329
|
-
await v1.save(namespace, list);
|
|
330
|
-
},
|
|
331
|
-
async updateComment(namespace, id, patch) {
|
|
332
|
-
warnOnce();
|
|
333
|
-
const list = await v1.load(namespace);
|
|
334
|
-
const idx = list.findIndex((c) => c.id === id);
|
|
335
|
-
if (idx === -1) return null;
|
|
336
|
-
const next = { ...list[idx], ...patch, id: list[idx].id };
|
|
337
|
-
list[idx] = next;
|
|
338
|
-
await v1.save(namespace, list);
|
|
339
|
-
return next;
|
|
340
|
-
},
|
|
341
|
-
async removeComment(namespace, id) {
|
|
342
|
-
warnOnce();
|
|
343
|
-
const list = await v1.load(namespace);
|
|
344
|
-
await v1.save(
|
|
345
|
-
namespace,
|
|
346
|
-
list.filter((c) => c.id !== id)
|
|
347
|
-
);
|
|
348
|
-
},
|
|
349
|
-
async setAll(namespace, comments) {
|
|
350
|
-
warnOnce();
|
|
351
|
-
await v1.save(namespace, comments);
|
|
352
|
-
},
|
|
353
|
-
async clear(namespace) {
|
|
354
|
-
await v1.clear(namespace);
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
161
|
// src/cloud.ts
|
|
360
162
|
var DEFAULT_API_URL = "https://vizu.unhingged.com";
|
|
361
163
|
var POPUP_WIDTH = 480;
|
|
@@ -1412,8 +1214,8 @@ var Popover = class {
|
|
|
1412
1214
|
const removeBtn = target.closest('[data-vz="remove-anchor"]');
|
|
1413
1215
|
if (removeBtn) {
|
|
1414
1216
|
e.stopPropagation();
|
|
1415
|
-
const
|
|
1416
|
-
if (
|
|
1217
|
+
const key = removeBtn.getAttribute("data-fp-key");
|
|
1218
|
+
if (key) this.removeAnchor(key);
|
|
1417
1219
|
return;
|
|
1418
1220
|
}
|
|
1419
1221
|
const refEl = target.closest('[data-vz="ref"]');
|
|
@@ -1508,8 +1310,8 @@ var Popover = class {
|
|
|
1508
1310
|
}
|
|
1509
1311
|
/** Add another anchor to the in-progress comment (Shift+Click flow). */
|
|
1510
1312
|
addAnchor(fp, target) {
|
|
1511
|
-
const
|
|
1512
|
-
if (this.anchorFingerprints.some((x) => fingerprintKey(x) ===
|
|
1313
|
+
const key = fingerprintKey(fp);
|
|
1314
|
+
if (this.anchorFingerprints.some((x) => fingerprintKey(x) === key)) return false;
|
|
1513
1315
|
this.anchorFingerprints.push(fp);
|
|
1514
1316
|
if (target) this.anchorTargets.push(target);
|
|
1515
1317
|
this.callbacks.onAnchorsChanged([...this.anchorFingerprints]);
|
|
@@ -1518,9 +1320,9 @@ var Popover = class {
|
|
|
1518
1320
|
return true;
|
|
1519
1321
|
}
|
|
1520
1322
|
/** Remove an anchor by fingerprint key. Never removes the last anchor. */
|
|
1521
|
-
removeAnchor(
|
|
1323
|
+
removeAnchor(key) {
|
|
1522
1324
|
if (this.anchorFingerprints.length <= 1) return false;
|
|
1523
|
-
const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) ===
|
|
1325
|
+
const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) === key);
|
|
1524
1326
|
if (idx < 0) return false;
|
|
1525
1327
|
this.anchorFingerprints.splice(idx, 1);
|
|
1526
1328
|
this.anchorTargets.splice(idx, 1);
|
|
@@ -1630,9 +1432,9 @@ var Popover = class {
|
|
|
1630
1432
|
return;
|
|
1631
1433
|
}
|
|
1632
1434
|
const chips = this.anchorFingerprints.map((fp) => {
|
|
1633
|
-
const
|
|
1435
|
+
const key = fingerprintKey(fp);
|
|
1634
1436
|
const label = fingerprintLabel(fp);
|
|
1635
|
-
return `<span class="vz-anchor-chip" data-fp-key="${escapeHtml(
|
|
1437
|
+
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>`;
|
|
1636
1438
|
}).join("");
|
|
1637
1439
|
wrap.innerHTML = `
|
|
1638
1440
|
<div class="vz-anchor-hint">Anchored to ${this.anchorFingerprints.length} elements:</div>
|
|
@@ -2359,10 +2161,10 @@ var Sidebar = class {
|
|
|
2359
2161
|
const fps = c.fingerprints || [];
|
|
2360
2162
|
const primary = fps[0];
|
|
2361
2163
|
if (!primary) continue;
|
|
2362
|
-
const
|
|
2363
|
-
const g = groups.get(
|
|
2164
|
+
const key = fingerprintKey(primary);
|
|
2165
|
+
const g = groups.get(key);
|
|
2364
2166
|
if (g) g.comments.push(c);
|
|
2365
|
-
else groups.set(
|
|
2167
|
+
else groups.set(key, { fp: primary, comments: [c] });
|
|
2366
2168
|
}
|
|
2367
2169
|
const groupHtml = [];
|
|
2368
2170
|
let gi = 0;
|
|
@@ -2423,8 +2225,8 @@ var Sidebar = class {
|
|
|
2423
2225
|
}
|
|
2424
2226
|
const chips = this.el.querySelectorAll('[data-vz="jump-fp"]');
|
|
2425
2227
|
for (const chip of chips) {
|
|
2426
|
-
const
|
|
2427
|
-
const fp = findFingerprintByKey(visible,
|
|
2228
|
+
const key = chip.getAttribute("data-fp-key");
|
|
2229
|
+
const fp = findFingerprintByKey(visible, key || "");
|
|
2428
2230
|
chip.__fp = fp;
|
|
2429
2231
|
}
|
|
2430
2232
|
}
|
|
@@ -2512,10 +2314,10 @@ function renderOrphansHtml(orphans) {
|
|
|
2512
2314
|
</div>
|
|
2513
2315
|
`;
|
|
2514
2316
|
}
|
|
2515
|
-
function findFingerprintByKey(comments,
|
|
2317
|
+
function findFingerprintByKey(comments, key) {
|
|
2516
2318
|
for (const c of comments) {
|
|
2517
2319
|
for (const fp of c.fingerprints || []) {
|
|
2518
|
-
if (fingerprintKey(fp) ===
|
|
2320
|
+
if (fingerprintKey(fp) === key) return fp;
|
|
2519
2321
|
}
|
|
2520
2322
|
}
|
|
2521
2323
|
return null;
|
|
@@ -3610,30 +3412,17 @@ var EventBus = class {
|
|
|
3610
3412
|
// src/index.ts
|
|
3611
3413
|
var DEFAULTS = {
|
|
3612
3414
|
shortcut: "mod+shift+e",
|
|
3613
|
-
namespace: "default",
|
|
3614
3415
|
accent: "#FF6647"
|
|
3615
3416
|
};
|
|
3616
|
-
function resolveStorage(storage, cloud, defaultMode, onAuthChanged) {
|
|
3617
|
-
if (cloud) {
|
|
3618
|
-
if (storage && typeof console !== "undefined") {
|
|
3619
|
-
console.warn("[vizu] Both `cloud` and `storage` set; `cloud` wins. Drop `storage` to silence this.");
|
|
3620
|
-
}
|
|
3621
|
-
return new CloudStorageAdapter({
|
|
3622
|
-
workspace: cloud.workspace,
|
|
3623
|
-
apiUrl: cloud.apiUrl,
|
|
3624
|
-
autoSignIn: cloud.autoSignIn,
|
|
3625
|
-
onAuthChanged
|
|
3626
|
-
});
|
|
3627
|
-
}
|
|
3628
|
-
if (!storage) return defaultMode === "local" ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
|
|
3629
|
-
if (storage === "local") return new LocalStorageAdapter();
|
|
3630
|
-
if (storage === "memory") return new InMemoryStorageAdapter();
|
|
3631
|
-
if (storage === "none") return new NullStorageAdapter();
|
|
3632
|
-
if (isV1Adapter(storage)) return wrapV1Adapter(storage);
|
|
3633
|
-
return storage;
|
|
3634
|
-
}
|
|
3635
3417
|
var Vizu = class {
|
|
3636
|
-
constructor(options
|
|
3418
|
+
constructor(options, storageOverride) {
|
|
3419
|
+
/**
|
|
3420
|
+
* The cloud adapter when this instance runs in the normal cloud mode;
|
|
3421
|
+
* null only under the internal storage override (tests / storybook).
|
|
3422
|
+
* Auth-gated behavior (mount gate, write gate, sign-in kicks) branches
|
|
3423
|
+
* on this instead of `instanceof` checks against `this.storage`.
|
|
3424
|
+
*/
|
|
3425
|
+
this.cloud = null;
|
|
3637
3426
|
this.root = null;
|
|
3638
3427
|
this.highlighter = null;
|
|
3639
3428
|
this.popover = null;
|
|
@@ -3695,8 +3484,8 @@ var Vizu = class {
|
|
|
3695
3484
|
this.pendingTargets = [];
|
|
3696
3485
|
/**
|
|
3697
3486
|
* Subscribe to remote-change notifications from the active storage
|
|
3698
|
-
* adapter
|
|
3699
|
-
*
|
|
3487
|
+
* adapter, when it implements `subscribe` (none do today — kept as
|
|
3488
|
+
* the hook realtime lands on). Torn down on `destroy()`.
|
|
3700
3489
|
*/
|
|
3701
3490
|
this.unsubscribeStorage = null;
|
|
3702
3491
|
this.onKeyDown = (e) => {
|
|
@@ -3739,23 +3528,34 @@ var Vizu = class {
|
|
|
3739
3528
|
this.popover?.reposition();
|
|
3740
3529
|
});
|
|
3741
3530
|
};
|
|
3531
|
+
if (!storageOverride && !options?.workspace) {
|
|
3532
|
+
throw new Error(
|
|
3533
|
+
"[vizu] `workspace` is required \u2014 create one at https://vizu.unhingged.com and pass new Vizu({ workspace: 'your-slug' })."
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3742
3536
|
this.opts = { ...options };
|
|
3743
3537
|
this.opts.shortcut = options.shortcut ?? DEFAULTS.shortcut;
|
|
3744
|
-
this.opts.namespace = options.namespace ?? DEFAULTS.namespace;
|
|
3745
3538
|
this.opts.accent = options.accent ?? DEFAULTS.accent;
|
|
3539
|
+
this.ns = options.workspace ?? "default";
|
|
3746
3540
|
this.parsedShortcut = parseShortcut(this.opts.shortcut);
|
|
3747
3541
|
this.user = options.user ?? null;
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3542
|
+
if (storageOverride) {
|
|
3543
|
+
this.storage = storageOverride;
|
|
3544
|
+
this.cloud = null;
|
|
3545
|
+
} else {
|
|
3546
|
+
this.cloud = new CloudStorageAdapter({
|
|
3547
|
+
workspace: options.workspace,
|
|
3548
|
+
apiUrl: options.apiUrl,
|
|
3549
|
+
autoSignIn: options.autoSignIn,
|
|
3550
|
+
onAuthChanged: (info) => {
|
|
3551
|
+
if (info.user) this.setUser(info.user);
|
|
3552
|
+
if (this.hasLoadedComments) {
|
|
3553
|
+
void this.loadComments();
|
|
3554
|
+
}
|
|
3756
3555
|
}
|
|
3757
|
-
}
|
|
3758
|
-
|
|
3556
|
+
});
|
|
3557
|
+
this.storage = this.cloud;
|
|
3558
|
+
}
|
|
3759
3559
|
if (options.actions) this.actions = [...options.actions];
|
|
3760
3560
|
if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
|
|
3761
3561
|
if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
|
|
@@ -3797,8 +3597,8 @@ var Vizu = class {
|
|
|
3797
3597
|
* Resolution order:
|
|
3798
3598
|
* 1. `options.mentionable` if provided — host-supplied static or
|
|
3799
3599
|
* custom resolver. Wins over everything else.
|
|
3800
|
-
* 2.
|
|
3801
|
-
* 3. Empty array (
|
|
3600
|
+
* 2. The workspace's mentionable-members API.
|
|
3601
|
+
* 3. Empty array (internal storage override, no override).
|
|
3802
3602
|
*/
|
|
3803
3603
|
async searchMentionable() {
|
|
3804
3604
|
if (this.opts.mentionable) {
|
|
@@ -3811,38 +3611,36 @@ var Vizu = class {
|
|
|
3811
3611
|
return [];
|
|
3812
3612
|
}
|
|
3813
3613
|
}
|
|
3814
|
-
if (this.
|
|
3815
|
-
return this.storage.searchMentionable();
|
|
3816
|
-
}
|
|
3614
|
+
if (this.cloud) return this.cloud.searchMentionable();
|
|
3817
3615
|
return [];
|
|
3818
3616
|
}
|
|
3819
3617
|
/**
|
|
3820
3618
|
* Whether the full Vizu UI (pill, highlighter, markers, popover) can
|
|
3821
|
-
* be mounted right now
|
|
3822
|
-
*
|
|
3823
|
-
* auth
|
|
3824
|
-
*
|
|
3825
|
-
*
|
|
3619
|
+
* be mounted right now: not until the user is signed in to the
|
|
3620
|
+
* workspace. Re-checked from `enable()` and from `setUser()` so that
|
|
3621
|
+
* the moment auth resolves, the UI appears without the user needing
|
|
3622
|
+
* to press the shortcut again. (Under the internal storage override
|
|
3623
|
+
* there is no auth concept and the UI mounts immediately.)
|
|
3826
3624
|
*/
|
|
3827
3625
|
shouldMount() {
|
|
3828
|
-
if (!
|
|
3626
|
+
if (!this.cloud) return true;
|
|
3829
3627
|
return this.user !== null;
|
|
3830
3628
|
}
|
|
3831
3629
|
/**
|
|
3832
|
-
*
|
|
3833
|
-
*
|
|
3834
|
-
*
|
|
3835
|
-
*
|
|
3836
|
-
*
|
|
3837
|
-
*
|
|
3630
|
+
* Write gate. Every entry point that creates or modifies a comment
|
|
3631
|
+
* first checks that we have a known user. Without one, the write
|
|
3632
|
+
* would land as Anonymous (or 401 on the backend) — neither is the
|
|
3633
|
+
* right UX. So we re-trigger the sign-in popup and refuse the action.
|
|
3634
|
+
* The user signs in, this.user gets set via onAuthChanged, then they
|
|
3635
|
+
* can retry the click themselves.
|
|
3838
3636
|
*
|
|
3839
3637
|
* Returns true when the caller may proceed.
|
|
3840
3638
|
*/
|
|
3841
3639
|
requireUserOrAuth() {
|
|
3842
|
-
if (!
|
|
3640
|
+
if (!this.cloud) return true;
|
|
3843
3641
|
if (this.user) return true;
|
|
3844
|
-
if (this.
|
|
3845
|
-
void this.
|
|
3642
|
+
if (this.cloud.isAccessDenied()) return false;
|
|
3643
|
+
void this.cloud.preflightAuth().catch(() => {
|
|
3846
3644
|
});
|
|
3847
3645
|
return false;
|
|
3848
3646
|
}
|
|
@@ -3869,19 +3667,19 @@ var Vizu = class {
|
|
|
3869
3667
|
return this.enabled;
|
|
3870
3668
|
}
|
|
3871
3669
|
/**
|
|
3872
|
-
* Kick the
|
|
3670
|
+
* Kick the sign-in flow. Call this alongside `enable()` from an
|
|
3873
3671
|
* explicit user gesture — a host-rendered launcher button, a menu item —
|
|
3874
|
-
* so
|
|
3875
|
-
*
|
|
3876
|
-
*
|
|
3877
|
-
*
|
|
3672
|
+
* so users get the sign-in popup immediately instead of only after
|
|
3673
|
+
* clicking an element. The keyboard shortcut goes through this same
|
|
3674
|
+
* path. No-op when already signed in, when the workspace has already
|
|
3675
|
+
* denied access this session, or under the internal storage override.
|
|
3878
3676
|
*/
|
|
3879
3677
|
requestAuth() {
|
|
3880
|
-
if (!
|
|
3678
|
+
if (!this.cloud) return;
|
|
3881
3679
|
if (this.user) return;
|
|
3882
|
-
if (this.
|
|
3883
|
-
void this.
|
|
3884
|
-
if (err?.code === "auth_canceled" || err?.code === "popup_blocked") return;
|
|
3680
|
+
if (this.cloud.isAccessDenied()) return;
|
|
3681
|
+
void this.cloud.preflightAuth().catch((err) => {
|
|
3682
|
+
if (err?.code === "auth_canceled" || err?.code === "popup_blocked" || err?.code === "auth_required") return;
|
|
3885
3683
|
if (typeof console !== "undefined") {
|
|
3886
3684
|
console.warn("[vizu] preflight auth failed:", err);
|
|
3887
3685
|
}
|
|
@@ -3922,7 +3720,7 @@ var Vizu = class {
|
|
|
3922
3720
|
}
|
|
3923
3721
|
async setComments(comments, opts) {
|
|
3924
3722
|
this.comments = migrateComments(comments);
|
|
3925
|
-
if (opts?.persist !== false) await this.storage.setAll(this.
|
|
3723
|
+
if (opts?.persist !== false) await this.storage.setAll(this.ns, this.comments);
|
|
3926
3724
|
this.bus.emit("comments:set", { comments: this.getComments() });
|
|
3927
3725
|
this.refreshUi();
|
|
3928
3726
|
}
|
|
@@ -3937,7 +3735,7 @@ var Vizu = class {
|
|
|
3937
3735
|
const { id: _i, schemaVersion: _s, ...safe } = patch;
|
|
3938
3736
|
const next = { ...this.comments[idx], ...safe };
|
|
3939
3737
|
this.comments[idx] = next;
|
|
3940
|
-
const persisted = await this.storage.updateComment(this.
|
|
3738
|
+
const persisted = await this.storage.updateComment(this.ns, id, safe);
|
|
3941
3739
|
this.bus.emit("comment:updated", { comment: persisted ?? next });
|
|
3942
3740
|
this.refreshUi();
|
|
3943
3741
|
return next;
|
|
@@ -3979,7 +3777,7 @@ var Vizu = class {
|
|
|
3979
3777
|
const idx = this.comments.findIndex((x) => x.id === c.id);
|
|
3980
3778
|
if (idx !== -1) this.comments[idx] = { ...this.comments[idx], ...patch };
|
|
3981
3779
|
try {
|
|
3982
|
-
await this.storage.updateComment(this.
|
|
3780
|
+
await this.storage.updateComment(this.ns, c.id, patch);
|
|
3983
3781
|
} catch (err) {
|
|
3984
3782
|
if (typeof console !== "undefined") {
|
|
3985
3783
|
console.warn("[vizu] self-heal write failed for", c.id, err);
|
|
@@ -4001,8 +3799,8 @@ var Vizu = class {
|
|
|
4001
3799
|
}
|
|
4002
3800
|
/**
|
|
4003
3801
|
* True iff the active storage adapter implements `uploadAttachment`.
|
|
4004
|
-
* The popover hides its upload UI when this returns false (
|
|
4005
|
-
*
|
|
3802
|
+
* The popover hides its upload UI when this returns false (only the
|
|
3803
|
+
* internal offline adapters lack uploads).
|
|
4006
3804
|
*/
|
|
4007
3805
|
canUploadAttachments() {
|
|
4008
3806
|
return typeof this.storage.uploadAttachment === "function";
|
|
@@ -4015,10 +3813,10 @@ var Vizu = class {
|
|
|
4015
3813
|
async uploadAttachment(file) {
|
|
4016
3814
|
if (!this.storage.uploadAttachment) {
|
|
4017
3815
|
throw new Error(
|
|
4018
|
-
"[vizu] active storage adapter does not support uploadAttachment
|
|
3816
|
+
"[vizu] active storage adapter does not support uploadAttachment"
|
|
4019
3817
|
);
|
|
4020
3818
|
}
|
|
4021
|
-
return this.storage.uploadAttachment(this.
|
|
3819
|
+
return this.storage.uploadAttachment(this.ns, file);
|
|
4022
3820
|
}
|
|
4023
3821
|
/**
|
|
4024
3822
|
* Append a reply to a comment. Returns the parent comment or null if
|
|
@@ -4046,9 +3844,9 @@ var Vizu = class {
|
|
|
4046
3844
|
this.comments[idx] = updated;
|
|
4047
3845
|
try {
|
|
4048
3846
|
if (this.storage.addReply) {
|
|
4049
|
-
await this.storage.addReply(this.
|
|
3847
|
+
await this.storage.addReply(this.ns, commentId, reply);
|
|
4050
3848
|
} else {
|
|
4051
|
-
await this.storage.updateComment(this.
|
|
3849
|
+
await this.storage.updateComment(this.ns, commentId, { replies: updated.replies });
|
|
4052
3850
|
}
|
|
4053
3851
|
} catch (err) {
|
|
4054
3852
|
const cur = this.comments[idx];
|
|
@@ -4088,9 +3886,9 @@ var Vizu = class {
|
|
|
4088
3886
|
const updated = { ...this.comments[idx], replies: filtered };
|
|
4089
3887
|
this.comments[idx] = updated;
|
|
4090
3888
|
if (this.storage.removeReply) {
|
|
4091
|
-
await this.storage.removeReply(this.
|
|
3889
|
+
await this.storage.removeReply(this.ns, commentId, replyId);
|
|
4092
3890
|
} else {
|
|
4093
|
-
await this.storage.updateComment(this.
|
|
3891
|
+
await this.storage.updateComment(this.ns, commentId, { replies: filtered });
|
|
4094
3892
|
}
|
|
4095
3893
|
this.bus.emit("comment:updated", { comment: updated });
|
|
4096
3894
|
this.refreshUi();
|
|
@@ -4103,7 +3901,7 @@ var Vizu = class {
|
|
|
4103
3901
|
async clearAll() {
|
|
4104
3902
|
const previous = this.comments;
|
|
4105
3903
|
this.comments = [];
|
|
4106
|
-
await this.storage.clear(this.
|
|
3904
|
+
await this.storage.clear(this.ns);
|
|
4107
3905
|
this.bus.emit("comments:cleared", { previous });
|
|
4108
3906
|
this.refreshUi();
|
|
4109
3907
|
this.popover?.close();
|
|
@@ -4144,10 +3942,10 @@ var Vizu = class {
|
|
|
4144
3942
|
}
|
|
4145
3943
|
/* ===== Internal ===== */
|
|
4146
3944
|
async loadComments() {
|
|
4147
|
-
const raw = await this.storage.load(this.
|
|
3945
|
+
const raw = await this.storage.load(this.ns);
|
|
4148
3946
|
this.comments = migrateComments(raw);
|
|
4149
3947
|
if (needsPersist(raw)) {
|
|
4150
|
-
void this.storage.setAll(this.
|
|
3948
|
+
void this.storage.setAll(this.ns, this.comments).catch(() => {
|
|
4151
3949
|
});
|
|
4152
3950
|
}
|
|
4153
3951
|
this.bus.emit("comments:loaded", { comments: this.getComments() });
|
|
@@ -4156,7 +3954,7 @@ var Vizu = class {
|
|
|
4156
3954
|
subscribeStorage() {
|
|
4157
3955
|
if (this.unsubscribeStorage) return;
|
|
4158
3956
|
if (!this.storage.subscribe) return;
|
|
4159
|
-
this.unsubscribeStorage = this.storage.subscribe(this.
|
|
3957
|
+
this.unsubscribeStorage = this.storage.subscribe(this.ns, (e) => {
|
|
4160
3958
|
if (e.type === "added") {
|
|
4161
3959
|
const fresh = migrateComment(e.comment);
|
|
4162
3960
|
if (!this.comments.some((c) => c.id === fresh.id)) {
|
|
@@ -4308,9 +4106,9 @@ var Vizu = class {
|
|
|
4308
4106
|
this.bus.emit("element:deselected", {});
|
|
4309
4107
|
}
|
|
4310
4108
|
addToSelection(fp, el) {
|
|
4311
|
-
const
|
|
4312
|
-
if (this.pendingFingerprints.some((x) => fingerprintKey(x) ===
|
|
4313
|
-
this.pendingFingerprints = this.pendingFingerprints.filter((x) => fingerprintKey(x) !==
|
|
4109
|
+
const key = fingerprintKey(fp);
|
|
4110
|
+
if (this.pendingFingerprints.some((x) => fingerprintKey(x) === key)) {
|
|
4111
|
+
this.pendingFingerprints = this.pendingFingerprints.filter((x) => fingerprintKey(x) !== key);
|
|
4314
4112
|
this.pendingTargets = this.pendingTargets.filter((t) => t !== el);
|
|
4315
4113
|
} else {
|
|
4316
4114
|
this.pendingFingerprints.push(fp);
|
|
@@ -4334,9 +4132,9 @@ var Vizu = class {
|
|
|
4334
4132
|
return (c.status ?? "open") === this.statusFilter;
|
|
4335
4133
|
}
|
|
4336
4134
|
commentsForElement(fp) {
|
|
4337
|
-
const
|
|
4135
|
+
const key = fingerprintKey(fp);
|
|
4338
4136
|
return this.comments.filter(
|
|
4339
|
-
(c) => (c.fingerprints || []).some((f) => fingerprintKey(f) ===
|
|
4137
|
+
(c) => (c.fingerprints || []).some((f) => fingerprintKey(f) === key)
|
|
4340
4138
|
);
|
|
4341
4139
|
}
|
|
4342
4140
|
async addCommentFromPopover(text, fps, references, mentions, attachments) {
|
|
@@ -4360,7 +4158,7 @@ var Vizu = class {
|
|
|
4360
4158
|
mentions: mentions ?? []
|
|
4361
4159
|
};
|
|
4362
4160
|
this.comments.push(c);
|
|
4363
|
-
await this.storage.addComment(this.
|
|
4161
|
+
await this.storage.addComment(this.ns, c);
|
|
4364
4162
|
this.bus.emit("comment:added", { comment: c });
|
|
4365
4163
|
this.popover?.update(this.commentsForElement(fps[0]));
|
|
4366
4164
|
this.refreshUi();
|
|
@@ -4371,7 +4169,7 @@ var Vizu = class {
|
|
|
4371
4169
|
const comment = this.comments[idx];
|
|
4372
4170
|
this.comments = this.comments.filter((c) => c.id !== id);
|
|
4373
4171
|
try {
|
|
4374
|
-
await this.storage.removeComment(this.
|
|
4172
|
+
await this.storage.removeComment(this.ns, id);
|
|
4375
4173
|
} catch (err) {
|
|
4376
4174
|
this.comments.splice(idx, 0, comment);
|
|
4377
4175
|
const status = err?.status;
|
|
@@ -4469,8 +4267,8 @@ var Vizu = class {
|
|
|
4469
4267
|
this.spotlights.delete(entry);
|
|
4470
4268
|
}, 1800);
|
|
4471
4269
|
}
|
|
4472
|
-
const
|
|
4473
|
-
const outline = this.outlines.get(
|
|
4270
|
+
const key = fingerprintKey(fp);
|
|
4271
|
+
const outline = this.outlines.get(key)?.outline;
|
|
4474
4272
|
if (outline) {
|
|
4475
4273
|
setTimeout(() => {
|
|
4476
4274
|
outline.classList.remove("is-pulsing");
|
|
@@ -4493,8 +4291,8 @@ var Vizu = class {
|
|
|
4493
4291
|
const bestConf = (a, b) => a === void 0 ? b : rankConf(b) > rankConf(a) ? b : a;
|
|
4494
4292
|
for (const c of this.comments) {
|
|
4495
4293
|
for (const fp of c.fingerprints || []) {
|
|
4496
|
-
const
|
|
4497
|
-
let bucket = byElement.get(
|
|
4294
|
+
const key = fingerprintKey(fp);
|
|
4295
|
+
let bucket = byElement.get(key);
|
|
4498
4296
|
if (!bucket) {
|
|
4499
4297
|
const match = findByFingerprint(fp);
|
|
4500
4298
|
bucket = {
|
|
@@ -4503,7 +4301,7 @@ var Vizu = class {
|
|
|
4503
4301
|
confidence: match.confidence,
|
|
4504
4302
|
commentIds: []
|
|
4505
4303
|
};
|
|
4506
|
-
byElement.set(
|
|
4304
|
+
byElement.set(key, bucket);
|
|
4507
4305
|
}
|
|
4508
4306
|
bucket.commentIds.push(c.id);
|
|
4509
4307
|
perComment.set(c.id, bestConf(perComment.get(c.id), bucket.confidence));
|
|
@@ -4522,7 +4320,7 @@ var Vizu = class {
|
|
|
4522
4320
|
if (this.resolvedConfidence.get(c.id) !== "drifted") continue;
|
|
4523
4321
|
void this.selfHealComment(c, byElement);
|
|
4524
4322
|
}
|
|
4525
|
-
for (const [
|
|
4323
|
+
for (const [key, info] of byElement) {
|
|
4526
4324
|
if (info.confidence === "orphaned" || !info.target) continue;
|
|
4527
4325
|
const idsForFilter = info.commentIds.filter((id) => this.commentMatchesStatusFilter(id));
|
|
4528
4326
|
if (idsForFilter.length === 0) continue;
|
|
@@ -4530,11 +4328,11 @@ var Vizu = class {
|
|
|
4530
4328
|
outlineEl.className = "vz-comment-outline";
|
|
4531
4329
|
if (info.confidence === "drifted") outlineEl.classList.add("vz-comment-outline-drifted");
|
|
4532
4330
|
this.root.appendChild(outlineEl);
|
|
4533
|
-
this.outlines.set(
|
|
4331
|
+
this.outlines.set(key, { outline: outlineEl, target: info.target, fp: info.fp });
|
|
4534
4332
|
const marker = document.createElement("button");
|
|
4535
4333
|
marker.className = "vz-marker";
|
|
4536
4334
|
if (info.confidence === "drifted") marker.classList.add("vz-marker-drifted");
|
|
4537
|
-
marker.setAttribute("data-element-key",
|
|
4335
|
+
marker.setAttribute("data-element-key", key);
|
|
4538
4336
|
marker.setAttribute("data-confidence", info.confidence);
|
|
4539
4337
|
const total = idsForFilter.length;
|
|
4540
4338
|
if (total > 1) {
|
|
@@ -4553,9 +4351,9 @@ var Vizu = class {
|
|
|
4553
4351
|
this.openPopoverFor([info.target], [info.fp], elComments);
|
|
4554
4352
|
}
|
|
4555
4353
|
});
|
|
4556
|
-
marker.addEventListener("mouseenter", () => this.pulseRelatedElements(idsForFilter,
|
|
4354
|
+
marker.addEventListener("mouseenter", () => this.pulseRelatedElements(idsForFilter, key));
|
|
4557
4355
|
this.root.appendChild(marker);
|
|
4558
|
-
this.markers.set(
|
|
4356
|
+
this.markers.set(key, { marker, target: info.target, fp: info.fp, commentIds: idsForFilter });
|
|
4559
4357
|
if (info.target) {
|
|
4560
4358
|
this.positionMarker(marker, info.target);
|
|
4561
4359
|
this.positionOutline(outlineEl, info.target);
|
|
@@ -4578,8 +4376,8 @@ var Vizu = class {
|
|
|
4578
4376
|
if (k !== selfKey) relatedKeys.add(k);
|
|
4579
4377
|
}
|
|
4580
4378
|
}
|
|
4581
|
-
for (const
|
|
4582
|
-
const outline = this.outlines.get(
|
|
4379
|
+
for (const key of relatedKeys) {
|
|
4380
|
+
const outline = this.outlines.get(key)?.outline;
|
|
4583
4381
|
if (!outline) continue;
|
|
4584
4382
|
outline.classList.remove("is-pulsing");
|
|
4585
4383
|
void outline.offsetWidth;
|
|
@@ -4613,22 +4411,22 @@ var Vizu = class {
|
|
|
4613
4411
|
syncActiveAnchorOutlines(fps) {
|
|
4614
4412
|
if (!this.root) return;
|
|
4615
4413
|
const wanted = new Set(fps.map((fp) => fingerprintKey(fp)));
|
|
4616
|
-
for (const [
|
|
4617
|
-
if (!wanted.has(
|
|
4414
|
+
for (const [key, info] of this.activeAnchorOutlines) {
|
|
4415
|
+
if (!wanted.has(key)) {
|
|
4618
4416
|
info.el.remove();
|
|
4619
|
-
this.activeAnchorOutlines.delete(
|
|
4417
|
+
this.activeAnchorOutlines.delete(key);
|
|
4620
4418
|
}
|
|
4621
4419
|
}
|
|
4622
4420
|
for (const fp of fps) {
|
|
4623
|
-
const
|
|
4624
|
-
if (this.activeAnchorOutlines.has(
|
|
4421
|
+
const key = fingerprintKey(fp);
|
|
4422
|
+
if (this.activeAnchorOutlines.has(key)) continue;
|
|
4625
4423
|
const outline = document.createElement("div");
|
|
4626
4424
|
outline.className = "vz-comment-outline is-selected-pending";
|
|
4627
4425
|
this.root.appendChild(outline);
|
|
4628
4426
|
const target = findElementByFingerprint(fp);
|
|
4629
4427
|
if (target) this.positionOutline(outline, target);
|
|
4630
4428
|
else outline.style.display = "none";
|
|
4631
|
-
this.activeAnchorOutlines.set(
|
|
4429
|
+
this.activeAnchorOutlines.set(key, { el: outline, fp });
|
|
4632
4430
|
}
|
|
4633
4431
|
}
|
|
4634
4432
|
clearActiveAnchorOutlines() {
|
|
@@ -4687,7 +4485,12 @@ function init() {
|
|
|
4687
4485
|
if (window.__vizu) return;
|
|
4688
4486
|
const script = findScript();
|
|
4689
4487
|
const ds = script?.dataset || {};
|
|
4690
|
-
|
|
4488
|
+
if (!ds.workspace) {
|
|
4489
|
+
console.error(
|
|
4490
|
+
'[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>'
|
|
4491
|
+
);
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
4691
4494
|
const user = ds.userName ? {
|
|
4692
4495
|
id: ds.userId,
|
|
4693
4496
|
name: ds.userName,
|
|
@@ -4695,16 +4498,17 @@ function init() {
|
|
|
4695
4498
|
email: ds.userEmail
|
|
4696
4499
|
} : void 0;
|
|
4697
4500
|
const opts = {
|
|
4501
|
+
workspace: ds.workspace,
|
|
4502
|
+
apiUrl: ds.apiUrl,
|
|
4503
|
+
autoSignIn: ds.autoSignIn === "false" ? false : void 0,
|
|
4698
4504
|
shortcut: ds.shortcut || "mod+shift+e",
|
|
4699
|
-
namespace: ds.namespace || "default",
|
|
4700
4505
|
pageVersion: ds.version,
|
|
4701
4506
|
accent: ds.accent,
|
|
4702
4507
|
startEnabled: ds.startEnabled === "true" || ds.startEnabled === "" || ds.startEnabled === "1",
|
|
4703
4508
|
ignoreSelectors: ds.ignore ? ds.ignore.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
4704
|
-
storage,
|
|
4705
4509
|
user
|
|
4706
4510
|
};
|
|
4707
|
-
const v = new Vizu(opts
|
|
4511
|
+
const v = new Vizu(opts);
|
|
4708
4512
|
window.__vizu = v;
|
|
4709
4513
|
window.Vizu = Vizu;
|
|
4710
4514
|
}
|