@unhingged/vizu-core 0.1.2 → 0.1.4

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/index.d.cts CHANGED
@@ -281,25 +281,6 @@ interface VizuCloudOptions {
281
281
  */
282
282
  autoSignIn?: boolean;
283
283
  }
284
- /**
285
- * Live collaboration config. Both keys are needed — the public key
286
- * identifies the Liveblocks project to the browser SDK; the auth
287
- * endpoint mints scoped tokens that gate room access against the
288
- * workspace's role + plan.
289
- *
290
- * Wired in Slice 1 of plans/live-collab.md; cursor + follow rendering
291
- * arrives in Slice 2+.
292
- */
293
- interface VizuLiveblocksOptions {
294
- /** Liveblocks public key from the dashboard (pk_dev_… / pk_prod_…). */
295
- publicKey: string;
296
- /**
297
- * URL of the auth endpoint that mints room tokens. Same-origin path
298
- * (`/api/liveblocks-auth`) for dashboard usage; full URL for hosts
299
- * embedding @vizu/core cross-origin.
300
- */
301
- authEndpoint: string;
302
- }
303
284
  interface VizuOptions {
304
285
  shortcut?: string;
305
286
  namespace?: string;
@@ -314,16 +295,6 @@ interface VizuOptions {
314
295
  storage?: StorageOption;
315
296
  /** Cloud workspace connection. Overrides `storage`. See {@link VizuCloudOptions}. */
316
297
  cloud?: VizuCloudOptions;
317
- /**
318
- * Optional live collaboration config. When set AND the workspace's
319
- * plan carries the `live_collab` feature, @vizu/core opens a
320
- * Liveblocks room scoped to (workspaceSlug, pageUrl) and renders
321
- * presence + cursors + follow mode. Without this, all live-collab
322
- * machinery stays inert — standalone usage is unaffected.
323
- *
324
- * See plans/live-collab.md.
325
- */
326
- liveblocks?: VizuLiveblocksOptions;
327
298
  ignoreSelectors?: string[];
328
299
  accent?: string;
329
300
  startEnabled?: boolean;
@@ -498,14 +469,6 @@ declare class CloudStorageAdapter implements StorageAdapter {
498
469
  * state.
499
470
  */
500
471
  preflightAuth(): Promise<void>;
501
- /**
502
- * Synchronous accessor for the currently cached bearer token, used
503
- * by sibling modules that need to authenticate against the same
504
- * backend (live-collab cross-origin polling). Returns null when no
505
- * valid token is cached — callers fall back to credentials:'include'
506
- * for same-origin or just disable themselves.
507
- */
508
- getCachedAuthToken(): string | null;
509
472
  load(_namespace: string): Promise<VizuComment[]>;
510
473
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
511
474
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -638,8 +601,6 @@ declare class Vizu {
638
601
  private selfHealedThisSession;
639
602
  private actions;
640
603
  private user;
641
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
642
- private liveCollab;
643
604
  private enabled;
644
605
  private rafScheduled;
645
606
  private bus;
@@ -655,6 +616,17 @@ declare class Vizu {
655
616
  off<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): void;
656
617
  setUser(user: VizuUser | null): void;
657
618
  getUser(): VizuUser | null;
619
+ /**
620
+ * Cloud-mode write gate. Every entry point that creates or modifies a
621
+ * comment first checks that we have a known user. Without one, the
622
+ * write would land as Anonymous (or 401 on the backend) — neither is
623
+ * the right UX. So we re-trigger the sign-in popup and refuse the
624
+ * action. The user signs in, this.user gets set via onAuthChanged,
625
+ * then they can retry the click themselves.
626
+ *
627
+ * Returns true when the caller may proceed.
628
+ */
629
+ private requireUserOrAuth;
658
630
  enable(): void;
659
631
  disable(): void;
660
632
  toggle(): void;
package/dist/index.d.ts CHANGED
@@ -281,25 +281,6 @@ interface VizuCloudOptions {
281
281
  */
282
282
  autoSignIn?: boolean;
283
283
  }
284
- /**
285
- * Live collaboration config. Both keys are needed — the public key
286
- * identifies the Liveblocks project to the browser SDK; the auth
287
- * endpoint mints scoped tokens that gate room access against the
288
- * workspace's role + plan.
289
- *
290
- * Wired in Slice 1 of plans/live-collab.md; cursor + follow rendering
291
- * arrives in Slice 2+.
292
- */
293
- interface VizuLiveblocksOptions {
294
- /** Liveblocks public key from the dashboard (pk_dev_… / pk_prod_…). */
295
- publicKey: string;
296
- /**
297
- * URL of the auth endpoint that mints room tokens. Same-origin path
298
- * (`/api/liveblocks-auth`) for dashboard usage; full URL for hosts
299
- * embedding @vizu/core cross-origin.
300
- */
301
- authEndpoint: string;
302
- }
303
284
  interface VizuOptions {
304
285
  shortcut?: string;
305
286
  namespace?: string;
@@ -314,16 +295,6 @@ interface VizuOptions {
314
295
  storage?: StorageOption;
315
296
  /** Cloud workspace connection. Overrides `storage`. See {@link VizuCloudOptions}. */
316
297
  cloud?: VizuCloudOptions;
317
- /**
318
- * Optional live collaboration config. When set AND the workspace's
319
- * plan carries the `live_collab` feature, @vizu/core opens a
320
- * Liveblocks room scoped to (workspaceSlug, pageUrl) and renders
321
- * presence + cursors + follow mode. Without this, all live-collab
322
- * machinery stays inert — standalone usage is unaffected.
323
- *
324
- * See plans/live-collab.md.
325
- */
326
- liveblocks?: VizuLiveblocksOptions;
327
298
  ignoreSelectors?: string[];
328
299
  accent?: string;
329
300
  startEnabled?: boolean;
@@ -498,14 +469,6 @@ declare class CloudStorageAdapter implements StorageAdapter {
498
469
  * state.
499
470
  */
500
471
  preflightAuth(): Promise<void>;
501
- /**
502
- * Synchronous accessor for the currently cached bearer token, used
503
- * by sibling modules that need to authenticate against the same
504
- * backend (live-collab cross-origin polling). Returns null when no
505
- * valid token is cached — callers fall back to credentials:'include'
506
- * for same-origin or just disable themselves.
507
- */
508
- getCachedAuthToken(): string | null;
509
472
  load(_namespace: string): Promise<VizuComment[]>;
510
473
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
511
474
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -638,8 +601,6 @@ declare class Vizu {
638
601
  private selfHealedThisSession;
639
602
  private actions;
640
603
  private user;
641
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
642
- private liveCollab;
643
604
  private enabled;
644
605
  private rafScheduled;
645
606
  private bus;
@@ -655,6 +616,17 @@ declare class Vizu {
655
616
  off<E extends VizuEventName>(event: E, handler: VizuEventHandler<E>): void;
656
617
  setUser(user: VizuUser | null): void;
657
618
  getUser(): VizuUser | null;
619
+ /**
620
+ * Cloud-mode write gate. Every entry point that creates or modifies a
621
+ * comment first checks that we have a known user. Without one, the
622
+ * write would land as Anonymous (or 401 on the backend) — neither is
623
+ * the right UX. So we re-trigger the sign-in popup and refuse the
624
+ * action. The user signs in, this.user gets set via onAuthChanged,
625
+ * then they can retry the click themselves.
626
+ *
627
+ * Returns true when the caller may proceed.
628
+ */
629
+ private requireUserOrAuth;
658
630
  enable(): void;
659
631
  disable(): void;
660
632
  toggle(): void;
package/dist/index.js CHANGED
@@ -1,11 +1,3 @@
1
- import {
2
- findByFingerprint,
3
- findElementByFingerprint,
4
- fingerprint,
5
- fingerprintKey,
6
- fingerprintLabel
7
- } from "./chunk-OMIFOSQ2.js";
8
-
9
1
  // src/types.ts
10
2
  var SCHEMA_VERSION = 2;
11
3
 
@@ -365,18 +357,6 @@ var CloudStorageAdapter = class {
365
357
  async preflightAuth() {
366
358
  await this.resolveToken();
367
359
  }
368
- /**
369
- * Synchronous accessor for the currently cached bearer token, used
370
- * by sibling modules that need to authenticate against the same
371
- * backend (live-collab cross-origin polling). Returns null when no
372
- * valid token is cached — callers fall back to credentials:'include'
373
- * for same-origin or just disable themselves.
374
- */
375
- getCachedAuthToken() {
376
- if (!this.cachedToken) return null;
377
- if (this.isExpired(this.cachedToken)) return null;
378
- return this.cachedToken.token;
379
- }
380
360
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
381
361
  async load(_namespace) {
382
362
  const out = [];
@@ -692,10 +672,297 @@ function apiErr(status, body) {
692
672
  return err;
693
673
  }
694
674
 
675
+ // src/fingerprint.ts
676
+ var TEXT_SNIPPET_MAX = 80;
677
+ var ANCESTOR_DEPTH = 4;
678
+ var ANCESTOR_MAX_SHIFT = 3;
679
+ var TEXT_RATIO_THRESHOLD = 0.75;
680
+ var CLASS_JACCARD_THRESHOLD = 0.6;
681
+ function buildSelector(el) {
682
+ const parts = [];
683
+ let current = el;
684
+ let depth = 0;
685
+ while (current && current !== document.documentElement && depth < 8) {
686
+ let part = current.tagName.toLowerCase();
687
+ if (current.id) {
688
+ parts.unshift("#" + CSS.escape(current.id));
689
+ break;
690
+ }
691
+ const cls = Array.from(current.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")).slice(0, 2);
692
+ if (cls.length) part += "." + cls.map((c) => CSS.escape(c)).join(".");
693
+ const parent = current.parentElement;
694
+ if (parent) {
695
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
696
+ if (sameTag.length > 1) {
697
+ const idx = sameTag.indexOf(current);
698
+ part += ":nth-of-type(" + (idx + 1) + ")";
699
+ }
700
+ }
701
+ parts.unshift(part);
702
+ current = parent;
703
+ depth++;
704
+ }
705
+ return parts.join(" > ");
706
+ }
707
+ function fingerprint(el) {
708
+ const text = el.innerText || el.textContent || "";
709
+ const parent = el.parentElement;
710
+ let siblingIndex = 0;
711
+ if (parent) {
712
+ siblingIndex = Array.from(parent.children).indexOf(el);
713
+ }
714
+ return {
715
+ selector: buildSelector(el),
716
+ parentSelector: parent ? buildSelector(parent) : "",
717
+ tagName: el.tagName,
718
+ textSnippet: text.trim().slice(0, TEXT_SNIPPET_MAX),
719
+ siblingIndex,
720
+ attributes: {
721
+ id: el.id || void 0,
722
+ classList: Array.from(el.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")),
723
+ role: el.getAttribute("role") || void 0,
724
+ ariaLabel: el.getAttribute("aria-label") || void 0,
725
+ dataKey: el.getAttribute("data-vizu-key") || void 0
726
+ },
727
+ ancestorChain: captureAncestorChain(el, ANCESTOR_DEPTH),
728
+ algorithmVersion: 2
729
+ };
730
+ }
731
+ function captureAncestorChain(el, depth) {
732
+ const steps = [];
733
+ let current = el.parentElement;
734
+ while (current && steps.length < depth) {
735
+ const parent = current.parentElement;
736
+ let nthOfType = 1;
737
+ if (parent) {
738
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
739
+ nthOfType = sameTag.indexOf(current) + 1;
740
+ }
741
+ steps.push({ tag: current.tagName, nthOfType });
742
+ current = parent;
743
+ }
744
+ return steps.length > 0 ? { steps } : void 0;
745
+ }
746
+ function findByFingerprint(fp, root = document) {
747
+ if (fp.attributes.dataKey) {
748
+ try {
749
+ const byKey = root.querySelector(`[data-vizu-key="${CSS.escape(fp.attributes.dataKey)}"]`);
750
+ if (byKey) return { element: byKey, confidence: "exact" };
751
+ } catch {
752
+ }
753
+ return { element: null, confidence: "orphaned" };
754
+ }
755
+ if (fp.attributes.id) {
756
+ try {
757
+ const byId = root.querySelector("#" + CSS.escape(fp.attributes.id));
758
+ if (byId) return { element: byId, confidence: "exact" };
759
+ } catch {
760
+ }
761
+ }
762
+ const candidates = [];
763
+ const classes = fp.attributes.classList;
764
+ if (classes && classes.length > 0) {
765
+ const m = findByClassSignature(root, fp.tagName, classes);
766
+ if (m?.element) candidates.push({ el: m.element, rung: "class" });
767
+ }
768
+ try {
769
+ const bySelector = root.querySelector(fp.selector);
770
+ if (bySelector) candidates.push({ el: bySelector, rung: "selector" });
771
+ } catch {
772
+ }
773
+ if (fp.ancestorChain && fp.ancestorChain.steps.length > 0) {
774
+ const m = findByAncestorChain(root, fp.tagName, fp.ancestorChain.steps);
775
+ if (m?.element) candidates.push({ el: m.element, rung: "chain" });
776
+ } else {
777
+ try {
778
+ const parent = fp.parentSelector ? root.querySelector(fp.parentSelector) : root;
779
+ if (parent) {
780
+ const candidate = parent.children[fp.siblingIndex];
781
+ if (candidate && candidate.tagName === fp.tagName) {
782
+ candidates.push({ el: candidate, rung: "chain" });
783
+ }
784
+ }
785
+ } catch {
786
+ }
787
+ }
788
+ if (fp.textSnippet) {
789
+ const m = findByTextSnippet(root, fp.tagName, fp.textSnippet);
790
+ if (m?.element) candidates.push({ el: m.element, rung: "text" });
791
+ }
792
+ if (candidates.length === 0) {
793
+ return { element: null, confidence: "orphaned" };
794
+ }
795
+ const votes = /* @__PURE__ */ new Map();
796
+ for (const c of candidates) {
797
+ let s = votes.get(c.el);
798
+ if (!s) {
799
+ s = /* @__PURE__ */ new Set();
800
+ votes.set(c.el, s);
801
+ }
802
+ s.add(c.rung);
803
+ }
804
+ let best = null;
805
+ for (const [el, rungs] of votes) {
806
+ if (!best || rungs.size > best.rungs.size) best = { el, rungs };
807
+ }
808
+ if (!best) return { element: null, confidence: "orphaned" };
809
+ if (best.rungs.size >= 3) return { element: best.el, confidence: "likely" };
810
+ if (best.rungs.size >= 2) return { element: best.el, confidence: "drifted" };
811
+ if (best.rungs.has("class") || best.rungs.has("chain")) {
812
+ return { element: best.el, confidence: "drifted" };
813
+ }
814
+ return { element: null, confidence: "orphaned" };
815
+ }
816
+ function findByClassSignature(root, tagName, needle) {
817
+ const needleSet = new Set(needle.filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-")));
818
+ if (needleSet.size === 0) return null;
819
+ const all = root.querySelectorAll(tagName.toLowerCase());
820
+ let best = null;
821
+ let secondScore = 0;
822
+ for (const el of all) {
823
+ const haystack = new Set(
824
+ Array.from(el.classList).filter((c) => !c.startsWith("vz-") && !c.startsWith("vizu-"))
825
+ );
826
+ if (haystack.size === 0) continue;
827
+ const score = jaccardSimilarity(needleSet, haystack);
828
+ if (score < CLASS_JACCARD_THRESHOLD) continue;
829
+ if (!best || score > best.score) {
830
+ secondScore = best?.score ?? 0;
831
+ best = { el, score };
832
+ } else if (score > secondScore) {
833
+ secondScore = score;
834
+ }
835
+ }
836
+ if (!best) return null;
837
+ if (best.score - secondScore < 0.1) return null;
838
+ return { element: best.el, confidence: "likely" };
839
+ }
840
+ function findByAncestorChain(root, tagName, steps) {
841
+ const all = root.querySelectorAll(tagName.toLowerCase());
842
+ let best = null;
843
+ for (const el of all) {
844
+ const ancestors = collectAncestorSteps(el, steps.length + ANCESTOR_MAX_SHIFT);
845
+ const score = scoreChainAgainstAncestors(ancestors, steps, ANCESTOR_MAX_SHIFT);
846
+ if (score < 2) continue;
847
+ if (!best || score > best.score) best = { el, score };
848
+ }
849
+ if (!best) return null;
850
+ return {
851
+ element: best.el,
852
+ confidence: best.score === steps.length ? "likely" : "drifted"
853
+ };
854
+ }
855
+ function collectAncestorSteps(el, limit) {
856
+ const steps = [];
857
+ let current = el.parentElement;
858
+ while (current && steps.length < limit) {
859
+ const parent = current.parentElement;
860
+ let nth = 1;
861
+ if (parent) {
862
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
863
+ nth = sameTag.indexOf(current) + 1;
864
+ }
865
+ steps.push({ tag: current.tagName, nthOfType: nth });
866
+ current = parent;
867
+ }
868
+ return steps;
869
+ }
870
+ function findByTextSnippet(root, tagName, snippet) {
871
+ const needle = normalizeText(snippet);
872
+ if (!needle) return null;
873
+ const all = root.querySelectorAll(tagName.toLowerCase());
874
+ let best = null;
875
+ for (const el of all) {
876
+ const raw = el.innerText || el.textContent || "";
877
+ const candidate = normalizeText(raw.slice(0, TEXT_SNIPPET_MAX * 2));
878
+ if (!candidate) continue;
879
+ const score = levenshteinRatio(needle, candidate);
880
+ if (score < TEXT_RATIO_THRESHOLD) continue;
881
+ if (!best || score > best.score) {
882
+ best = { el, score };
883
+ }
884
+ }
885
+ return best ? { element: best.el, confidence: "drifted" } : null;
886
+ }
887
+ function normalizeText(input) {
888
+ return input.toLowerCase().replace(/\s+/g, " ").trim();
889
+ }
890
+ function levenshteinRatio(a, b) {
891
+ if (a === b) return 1;
892
+ if (a.length === 0 || b.length === 0) return a.length === b.length ? 1 : 0;
893
+ const maxLen = Math.max(a.length, b.length);
894
+ const distance = levenshteinDistance(a, b);
895
+ return 1 - distance / maxLen;
896
+ }
897
+ function levenshteinDistance(a, b) {
898
+ if (a.length > b.length) {
899
+ const tmp = a;
900
+ a = b;
901
+ b = tmp;
902
+ }
903
+ const prev = new Array(a.length + 1);
904
+ const curr = new Array(a.length + 1);
905
+ for (let i = 0; i <= a.length; i++) prev[i] = i;
906
+ for (let j = 1; j <= b.length; j++) {
907
+ curr[0] = j;
908
+ for (let i = 1; i <= a.length; i++) {
909
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
910
+ curr[i] = Math.min(
911
+ curr[i - 1] + 1,
912
+ // insert
913
+ prev[i] + 1,
914
+ // delete
915
+ prev[i - 1] + cost
916
+ // substitute
917
+ );
918
+ }
919
+ for (let i = 0; i <= a.length; i++) prev[i] = curr[i];
920
+ }
921
+ return prev[a.length];
922
+ }
923
+ function scoreChainAgainstAncestors(actual, captured, maxShift) {
924
+ let best = 0;
925
+ for (let offset = 0; offset <= maxShift; offset++) {
926
+ let score = 0;
927
+ for (let i = 0; i < captured.length; i++) {
928
+ const a = actual[i + offset];
929
+ if (!a) break;
930
+ if (a.tag === captured[i].tag && a.nthOfType === captured[i].nthOfType) {
931
+ score++;
932
+ }
933
+ }
934
+ if (score > best) best = score;
935
+ }
936
+ return best;
937
+ }
938
+ function jaccardSimilarity(a, b) {
939
+ if (a.size === 0 && b.size === 0) return 1;
940
+ let intersection = 0;
941
+ for (const item of a) if (b.has(item)) intersection++;
942
+ const union = a.size + b.size - intersection;
943
+ return union === 0 ? 0 : intersection / union;
944
+ }
945
+ function findElementByFingerprint(fp, root) {
946
+ return findByFingerprint(fp, root).element;
947
+ }
948
+ function fingerprintKey(fp) {
949
+ return fp.selector + "|" + fp.textSnippet;
950
+ }
951
+ function fingerprintLabel(fp) {
952
+ const tag = fp.tagName.toLowerCase();
953
+ if (fp.textSnippet) {
954
+ const snippet = fp.textSnippet.length > 24 ? fp.textSnippet.slice(0, 24) + "\u2026" : fp.textSnippet;
955
+ return tag + ": " + snippet;
956
+ }
957
+ if (fp.attributes.id) return tag + "#" + fp.attributes.id;
958
+ if (fp.attributes.classList && fp.attributes.classList.length) return tag + "." + fp.attributes.classList[0];
959
+ return tag;
960
+ }
961
+
695
962
  // src/highlighter.ts
696
963
  var IGNORE_SELECTOR = "[data-vizu-root], [data-vizu-ignore], script, style, link, meta, head";
697
964
  var Highlighter = class {
698
- constructor(root, extraIgnore, onClick, onCurrentChange) {
965
+ constructor(root, extraIgnore, onClick) {
699
966
  this.current = null;
700
967
  this.active = false;
701
968
  this.paused = false;
@@ -730,7 +997,6 @@ var Highlighter = class {
730
997
  this.root = root;
731
998
  this.extraIgnore = extraIgnore;
732
999
  this.onClick = onClick;
733
- this.onCurrentChange = onCurrentChange ?? null;
734
1000
  this.overlay = document.createElement("div");
735
1001
  this.overlay.className = "vz-highlight";
736
1002
  this.overlay.style.display = "none";
@@ -739,7 +1005,6 @@ var Highlighter = class {
739
1005
  setCurrent(el) {
740
1006
  if (this.current === el) return;
741
1007
  this.current = el;
742
- this.onCurrentChange?.(el);
743
1008
  }
744
1009
  start() {
745
1010
  if (this.active) return;
@@ -2784,8 +3049,6 @@ var Vizu = class {
2784
3049
  this.selfHealedThisSession = /* @__PURE__ */ new Set();
2785
3050
  this.actions = [];
2786
3051
  this.user = null;
2787
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
2788
- this.liveCollab = null;
2789
3052
  this.enabled = false;
2790
3053
  this.rafScheduled = false;
2791
3054
  this.bus = new EventBus();
@@ -2857,31 +3120,6 @@ var Vizu = class {
2857
3120
  if (options.actions) this.actions = [...options.actions];
2858
3121
  if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
2859
3122
  if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
2860
- if (options.cloud) {
2861
- const cloud = options.cloud;
2862
- const apiUrl = (cloud.apiUrl ?? "https://vizu.unhingged.com").replace(/\/$/, "");
2863
- void import("./live-collab-BVLNJ5QI.js").then(({ LiveCollab }) => {
2864
- this.liveCollab = new LiveCollab({
2865
- workspaceSlug: cloud.workspace,
2866
- apiUrl,
2867
- getAuthHeader: async () => {
2868
- if (typeof window === "undefined") return null;
2869
- try {
2870
- if (new URL(apiUrl).origin === window.location.origin) return null;
2871
- } catch {
2872
- return null;
2873
- }
2874
- const adapter = this.storage;
2875
- if (adapter instanceof CloudStorageAdapter) {
2876
- const token = adapter.getCachedAuthToken();
2877
- if (token) return { Authorization: `Bearer ${token}` };
2878
- }
2879
- return null;
2880
- }
2881
- });
2882
- void this.liveCollab.start();
2883
- });
2884
- }
2885
3123
  if (typeof window !== "undefined") {
2886
3124
  window.addEventListener("keydown", this.onKeyDown);
2887
3125
  void this.loadComments().then(() => this.subscribeStorage());
@@ -2905,6 +3143,23 @@ var Vizu = class {
2905
3143
  getUser() {
2906
3144
  return this.user;
2907
3145
  }
3146
+ /**
3147
+ * Cloud-mode write gate. Every entry point that creates or modifies a
3148
+ * comment first checks that we have a known user. Without one, the
3149
+ * write would land as Anonymous (or 401 on the backend) — neither is
3150
+ * the right UX. So we re-trigger the sign-in popup and refuse the
3151
+ * action. The user signs in, this.user gets set via onAuthChanged,
3152
+ * then they can retry the click themselves.
3153
+ *
3154
+ * Returns true when the caller may proceed.
3155
+ */
3156
+ requireUserOrAuth() {
3157
+ if (!(this.storage instanceof CloudStorageAdapter)) return true;
3158
+ if (this.user) return true;
3159
+ void this.storage.preflightAuth().catch(() => {
3160
+ });
3161
+ return false;
3162
+ }
2908
3163
  /* ===== Lifecycle ===== */
2909
3164
  enable() {
2910
3165
  if (this.enabled) return;
@@ -2939,8 +3194,6 @@ var Vizu = class {
2939
3194
  this.unsubscribeStorage?.();
2940
3195
  this.unsubscribeStorage = null;
2941
3196
  this.selfHealedThisSession.clear();
2942
- this.liveCollab?.destroy();
2943
- this.liveCollab = null;
2944
3197
  this.bus.clear();
2945
3198
  }
2946
3199
  /* ===== Action API ===== */
@@ -3075,6 +3328,7 @@ var Vizu = class {
3075
3328
  * patch for adapters that don't implement reply ops.
3076
3329
  */
3077
3330
  async addReply(commentId, text, mentions) {
3331
+ if (!this.requireUserOrAuth()) return null;
3078
3332
  const idx = this.comments.findIndex((c) => c.id === commentId);
3079
3333
  if (idx === -1) return null;
3080
3334
  const reply = {
@@ -3141,6 +3395,7 @@ var Vizu = class {
3141
3395
  /* ===== Multi-select API ===== */
3142
3396
  /** Toggle multi-select mode. When on, plain clicks accumulate into the selection. */
3143
3397
  toggleMultiSelectMode() {
3398
+ if (!this.requireUserOrAuth()) return;
3144
3399
  this.multiSelectMode = !this.multiSelectMode;
3145
3400
  if (!this.multiSelectMode) this.clearSelection();
3146
3401
  this.pill?.setMultiSelectState(this.multiSelectMode, this.pendingFingerprints.length);
@@ -3262,11 +3517,7 @@ var Vizu = class {
3262
3517
  this.highlighter = new Highlighter(
3263
3518
  this.root,
3264
3519
  this.opts.ignoreSelectors ?? [],
3265
- (el, e) => this.onElementClick(el, e),
3266
- (el) => {
3267
- if (!this.liveCollab) return;
3268
- this.liveCollab.setLocalSelection(el ? fingerprint(el) : null);
3269
- }
3520
+ (el, e) => this.onElementClick(el, e)
3270
3521
  );
3271
3522
  this.highlighter.start();
3272
3523
  this.renderAllMarkers();
@@ -3306,6 +3557,7 @@ var Vizu = class {
3306
3557
  p.onSelect(fp2);
3307
3558
  return;
3308
3559
  }
3560
+ if (!this.requireUserOrAuth()) return;
3309
3561
  const fp = fingerprint(el);
3310
3562
  if (this.popover?.isOpen() && e.shiftKey) {
3311
3563
  this.popover.addAnchor(fp, el);
@@ -3353,6 +3605,7 @@ var Vizu = class {
3353
3605
  );
3354
3606
  }
3355
3607
  async addCommentFromPopover(text, fps, references, mentions, attachments) {
3608
+ if (!this.requireUserOrAuth()) return;
3356
3609
  if (fps.length === 0) return;
3357
3610
  const pageUrl = typeof location !== "undefined" ? location.href : void 0;
3358
3611
  const c = {