@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.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 key2 = removeBtn.getAttribute("data-fp-key");
1390
- if (key2) this.removeAnchor(key2);
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 key2 = fingerprintKey(fp);
1486
- if (this.anchorFingerprints.some((x) => fingerprintKey(x) === key2)) return false;
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(key2) {
1297
+ removeAnchor(key) {
1496
1298
  if (this.anchorFingerprints.length <= 1) return false;
1497
- const idx = this.anchorFingerprints.findIndex((x) => fingerprintKey(x) === key2);
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 key2 = fingerprintKey(fp);
1409
+ const key = fingerprintKey(fp);
1608
1410
  const label = fingerprintLabel(fp);
1609
- 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>`;
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 key2 = fingerprintKey(primary);
2337
- const g = groups.get(key2);
2138
+ const key = fingerprintKey(primary);
2139
+ const g = groups.get(key);
2338
2140
  if (g) g.comments.push(c);
2339
- else groups.set(key2, { fp: primary, comments: [c] });
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 key2 = chip.getAttribute("data-fp-key");
2401
- const fp = findFingerprintByKey(visible, key2 || "");
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, key2) {
2291
+ function findFingerprintByKey(comments, key) {
2490
2292
  for (const c of comments) {
2491
2293
  for (const fp of c.fingerprints || []) {
2492
- if (fingerprintKey(fp) === key2) return 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 = {}, _defaultStorage = "memory") {
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 (cloud adapters only). Local adapters never emit. The
3673
- * subscription is torn down on `destroy()`.
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
- this.storage = resolveStorage(
3723
- options.storage,
3724
- options.cloud,
3725
- _defaultStorage,
3726
- (info) => {
3727
- if (info.user) this.setUser(info.user);
3728
- if (this.hasLoadedComments) {
3729
- void this.loadComments();
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. Cloud adapter's API call if running cloud mode.
3775
- * 3. Empty array (non-cloud, no override).
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.storage instanceof CloudStorageAdapter) {
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. Cloud-mode workspaces hold off until the
3796
- * user is signed in; non-cloud usage (local / memory storage) has no
3797
- * auth concept and mounts immediately. Re-checked from `enable()`
3798
- * and from `setUser()` so that the moment auth resolves, the UI
3799
- * appears without the user needing to press the shortcut again.
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 (!(this.storage instanceof CloudStorageAdapter)) return true;
3600
+ if (!this.cloud) return true;
3803
3601
  return this.user !== null;
3804
3602
  }
3805
3603
  /**
3806
- * Cloud-mode write gate. Every entry point that creates or modifies a
3807
- * comment first checks that we have a known user. Without one, the
3808
- * write would land as Anonymous (or 401 on the backend) — neither is
3809
- * the right UX. So we re-trigger the sign-in popup and refuse the
3810
- * action. The user signs in, this.user gets set via onAuthChanged,
3811
- * then they can retry the click themselves.
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 (!(this.storage instanceof CloudStorageAdapter)) return true;
3614
+ if (!this.cloud) return true;
3817
3615
  if (this.user) return true;
3818
- if (this.storage.isAccessDenied()) return false;
3819
- void this.storage.preflightAuth().catch(() => {
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 cloud sign-in flow. Call this alongside `enable()` from an
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 cloud-mode users get the sign-in popup immediately instead of only
3849
- * after clicking an element. The keyboard shortcut goes through this
3850
- * same path. No-op outside cloud mode, when already signed in, or when
3851
- * the workspace has already denied access this session.
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 (!(this.storage instanceof CloudStorageAdapter)) return;
3652
+ if (!this.cloud) return;
3855
3653
  if (this.user) return;
3856
- if (this.storage.isAccessDenied()) return;
3857
- void this.storage.preflightAuth().catch((err) => {
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.opts.namespace, this.comments);
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.opts.namespace, id, safe);
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.opts.namespace, c.id, patch);
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 (local /
3979
- * memory modes don't support attachments today).
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; only the CloudStorageAdapter does in v1"
3790
+ "[vizu] active storage adapter does not support uploadAttachment"
3993
3791
  );
3994
3792
  }
3995
- return this.storage.uploadAttachment(this.opts.namespace, file);
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.opts.namespace, commentId, reply);
3821
+ await this.storage.addReply(this.ns, commentId, reply);
4024
3822
  } else {
4025
- await this.storage.updateComment(this.opts.namespace, commentId, { replies: updated.replies });
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.opts.namespace, commentId, replyId);
3863
+ await this.storage.removeReply(this.ns, commentId, replyId);
4066
3864
  } else {
4067
- await this.storage.updateComment(this.opts.namespace, commentId, { replies: filtered });
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.opts.namespace);
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.opts.namespace);
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.opts.namespace, this.comments).catch(() => {
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.opts.namespace, (e) => {
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 key2 = fingerprintKey(fp);
4286
- if (this.pendingFingerprints.some((x) => fingerprintKey(x) === key2)) {
4287
- this.pendingFingerprints = this.pendingFingerprints.filter((x) => fingerprintKey(x) !== key2);
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 key2 = fingerprintKey(fp);
4109
+ const key = fingerprintKey(fp);
4312
4110
  return this.comments.filter(
4313
- (c) => (c.fingerprints || []).some((f) => fingerprintKey(f) === key2)
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.opts.namespace, c);
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.opts.namespace, id);
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 key2 = fingerprintKey(fp);
4447
- const outline = this.outlines.get(key2)?.outline;
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 key2 = fingerprintKey(fp);
4471
- let bucket = byElement.get(key2);
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(key2, bucket);
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 [key2, info] of byElement) {
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(key2, { outline: outlineEl, target: info.target, fp: info.fp });
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", key2);
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, key2));
4328
+ marker.addEventListener("mouseenter", () => this.pulseRelatedElements(idsForFilter, key));
4531
4329
  this.root.appendChild(marker);
4532
- this.markers.set(key2, { marker, target: info.target, fp: info.fp, commentIds: idsForFilter });
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 key2 of relatedKeys) {
4556
- const outline = this.outlines.get(key2)?.outline;
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 [key2, info] of this.activeAnchorOutlines) {
4591
- if (!wanted.has(key2)) {
4388
+ for (const [key, info] of this.activeAnchorOutlines) {
4389
+ if (!wanted.has(key)) {
4592
4390
  info.el.remove();
4593
- this.activeAnchorOutlines.delete(key2);
4391
+ this.activeAnchorOutlines.delete(key);
4594
4392
  }
4595
4393
  }
4596
4394
  for (const fp of fps) {
4597
- const key2 = fingerprintKey(fp);
4598
- if (this.activeAnchorOutlines.has(key2)) continue;
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(key2, { el: outline, fp });
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
- const storage = ds.storage ? ["local", "memory", "none"].includes(ds.storage) ? ds.storage : "local" : "local";
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, "local");
4485
+ const v = new Vizu(opts);
4682
4486
  window.__vizu = v;
4683
4487
  window.Vizu = Vizu;
4684
4488
  }