@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 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 key2 of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key2) && key2 !== except)
14
- __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
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 key2 = removeBtn.getAttribute("data-fp-key");
1416
- if (key2) this.removeAnchor(key2);
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 key2 = fingerprintKey(fp);
1512
- if (this.anchorFingerprints.some((x) => fingerprintKey(x) === key2)) return false;
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(key2) {
1323
+ removeAnchor(key) {
1522
1324
  if (this.anchorFingerprints.length <= 1) return false;
1523
- const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) === key2);
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 key2 = fingerprintKey(fp);
1435
+ const key = fingerprintKey(fp);
1634
1436
  const label = fingerprintLabel(fp);
1635
- 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>`;
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 key2 = fingerprintKey(primary);
2363
- const g = groups.get(key2);
2164
+ const key = fingerprintKey(primary);
2165
+ const g = groups.get(key);
2364
2166
  if (g) g.comments.push(c);
2365
- else groups.set(key2, { fp: primary, comments: [c] });
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 key2 = chip.getAttribute("data-fp-key");
2427
- const fp = findFingerprintByKey(visible, key2 || "");
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, key2) {
2317
+ function findFingerprintByKey(comments, key) {
2516
2318
  for (const c of comments) {
2517
2319
  for (const fp of c.fingerprints || []) {
2518
- if (fingerprintKey(fp) === key2) return 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 = {}, _defaultStorage = "memory") {
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 (cloud adapters only). Local adapters never emit. The
3699
- * subscription is torn down on `destroy()`.
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
- this.storage = resolveStorage(
3749
- options.storage,
3750
- options.cloud,
3751
- _defaultStorage,
3752
- (info) => {
3753
- if (info.user) this.setUser(info.user);
3754
- if (this.hasLoadedComments) {
3755
- void this.loadComments();
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. Cloud adapter's API call if running cloud mode.
3801
- * 3. Empty array (non-cloud, no override).
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.storage instanceof CloudStorageAdapter) {
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. Cloud-mode workspaces hold off until the
3822
- * user is signed in; non-cloud usage (local / memory storage) has no
3823
- * auth concept and mounts immediately. Re-checked from `enable()`
3824
- * and from `setUser()` so that the moment auth resolves, the UI
3825
- * appears without the user needing to press the shortcut again.
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 (!(this.storage instanceof CloudStorageAdapter)) return true;
3626
+ if (!this.cloud) return true;
3829
3627
  return this.user !== null;
3830
3628
  }
3831
3629
  /**
3832
- * Cloud-mode write gate. Every entry point that creates or modifies a
3833
- * comment first checks that we have a known user. Without one, the
3834
- * write would land as Anonymous (or 401 on the backend) — neither is
3835
- * the right UX. So we re-trigger the sign-in popup and refuse the
3836
- * action. The user signs in, this.user gets set via onAuthChanged,
3837
- * then they can retry the click themselves.
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 (!(this.storage instanceof CloudStorageAdapter)) return true;
3640
+ if (!this.cloud) return true;
3843
3641
  if (this.user) return true;
3844
- if (this.storage.isAccessDenied()) return false;
3845
- void this.storage.preflightAuth().catch(() => {
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 cloud sign-in flow. Call this alongside `enable()` from an
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 cloud-mode users get the sign-in popup immediately instead of only
3875
- * after clicking an element. The keyboard shortcut goes through this
3876
- * same path. No-op outside cloud mode, when already signed in, or when
3877
- * the workspace has already denied access this session.
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 (!(this.storage instanceof CloudStorageAdapter)) return;
3678
+ if (!this.cloud) return;
3881
3679
  if (this.user) return;
3882
- if (this.storage.isAccessDenied()) return;
3883
- void this.storage.preflightAuth().catch((err) => {
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.opts.namespace, this.comments);
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.opts.namespace, id, safe);
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.opts.namespace, c.id, patch);
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 (local /
4005
- * memory modes don't support attachments today).
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; only the CloudStorageAdapter does in v1"
3816
+ "[vizu] active storage adapter does not support uploadAttachment"
4019
3817
  );
4020
3818
  }
4021
- return this.storage.uploadAttachment(this.opts.namespace, file);
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.opts.namespace, commentId, reply);
3847
+ await this.storage.addReply(this.ns, commentId, reply);
4050
3848
  } else {
4051
- await this.storage.updateComment(this.opts.namespace, commentId, { replies: updated.replies });
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.opts.namespace, commentId, replyId);
3889
+ await this.storage.removeReply(this.ns, commentId, replyId);
4092
3890
  } else {
4093
- await this.storage.updateComment(this.opts.namespace, commentId, { replies: filtered });
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.opts.namespace);
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.opts.namespace);
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.opts.namespace, this.comments).catch(() => {
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.opts.namespace, (e) => {
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 key2 = fingerprintKey(fp);
4312
- if (this.pendingFingerprints.some((x) => fingerprintKey(x) === key2)) {
4313
- this.pendingFingerprints = this.pendingFingerprints.filter((x) => fingerprintKey(x) !== key2);
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 key2 = fingerprintKey(fp);
4135
+ const key = fingerprintKey(fp);
4338
4136
  return this.comments.filter(
4339
- (c) => (c.fingerprints || []).some((f) => fingerprintKey(f) === key2)
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.opts.namespace, c);
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.opts.namespace, id);
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 key2 = fingerprintKey(fp);
4473
- const outline = this.outlines.get(key2)?.outline;
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 key2 = fingerprintKey(fp);
4497
- let bucket = byElement.get(key2);
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(key2, bucket);
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 [key2, info] of byElement) {
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(key2, { outline: outlineEl, target: info.target, fp: info.fp });
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", key2);
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, key2));
4354
+ marker.addEventListener("mouseenter", () => this.pulseRelatedElements(idsForFilter, key));
4557
4355
  this.root.appendChild(marker);
4558
- this.markers.set(key2, { marker, target: info.target, fp: info.fp, commentIds: idsForFilter });
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 key2 of relatedKeys) {
4582
- const outline = this.outlines.get(key2)?.outline;
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 [key2, info] of this.activeAnchorOutlines) {
4617
- if (!wanted.has(key2)) {
4414
+ for (const [key, info] of this.activeAnchorOutlines) {
4415
+ if (!wanted.has(key)) {
4618
4416
  info.el.remove();
4619
- this.activeAnchorOutlines.delete(key2);
4417
+ this.activeAnchorOutlines.delete(key);
4620
4418
  }
4621
4419
  }
4622
4420
  for (const fp of fps) {
4623
- const key2 = fingerprintKey(fp);
4624
- if (this.activeAnchorOutlines.has(key2)) continue;
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(key2, { el: outline, fp });
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
- const storage = ds.storage ? ["local", "memory", "none"].includes(ds.storage) ? ds.storage : "local" : "local";
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, "local");
4511
+ const v = new Vizu(opts);
4708
4512
  window.__vizu = v;
4709
4513
  window.Vizu = Vizu;
4710
4514
  }