@unhingged/vizu-core 0.1.1 → 0.1.3

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;
@@ -630,8 +601,6 @@ declare class Vizu {
630
601
  private selfHealedThisSession;
631
602
  private actions;
632
603
  private user;
633
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
634
- private liveCollab;
635
604
  private enabled;
636
605
  private rafScheduled;
637
606
  private bus;
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;
@@ -630,8 +601,6 @@ declare class Vizu {
630
601
  private selfHealedThisSession;
631
602
  private actions;
632
603
  private user;
633
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
634
- private liveCollab;
635
604
  private enabled;
636
605
  private rafScheduled;
637
606
  private bus;
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
 
@@ -537,7 +529,7 @@ var CloudStorageAdapter = class {
537
529
  }
538
530
  }
539
531
  sessionKey() {
540
- return `vizu:cloud-token:${this.workspace}`;
532
+ return `vizu:cloud-token:v2:${this.workspace}`;
541
533
  }
542
534
  /**
543
535
  * Resolve a valid token, opening the popup if needed.
@@ -680,10 +672,297 @@ function apiErr(status, body) {
680
672
  return err;
681
673
  }
682
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
+
683
962
  // src/highlighter.ts
684
963
  var IGNORE_SELECTOR = "[data-vizu-root], [data-vizu-ignore], script, style, link, meta, head";
685
964
  var Highlighter = class {
686
- constructor(root, extraIgnore, onClick, onCurrentChange) {
965
+ constructor(root, extraIgnore, onClick) {
687
966
  this.current = null;
688
967
  this.active = false;
689
968
  this.paused = false;
@@ -718,7 +997,6 @@ var Highlighter = class {
718
997
  this.root = root;
719
998
  this.extraIgnore = extraIgnore;
720
999
  this.onClick = onClick;
721
- this.onCurrentChange = onCurrentChange ?? null;
722
1000
  this.overlay = document.createElement("div");
723
1001
  this.overlay.className = "vz-highlight";
724
1002
  this.overlay.style.display = "none";
@@ -727,7 +1005,6 @@ var Highlighter = class {
727
1005
  setCurrent(el) {
728
1006
  if (this.current === el) return;
729
1007
  this.current = el;
730
- this.onCurrentChange?.(el);
731
1008
  }
732
1009
  start() {
733
1010
  if (this.active) return;
@@ -2772,8 +3049,6 @@ var Vizu = class {
2772
3049
  this.selfHealedThisSession = /* @__PURE__ */ new Set();
2773
3050
  this.actions = [];
2774
3051
  this.user = null;
2775
- /** Live-collab module — non-null only when `opts.liveblocks` and `opts.cloud` are both set AND the plan carries `live_collab`. Stays null otherwise. */
2776
- this.liveCollab = null;
2777
3052
  this.enabled = false;
2778
3053
  this.rafScheduled = false;
2779
3054
  this.bus = new EventBus();
@@ -2845,18 +3120,6 @@ var Vizu = class {
2845
3120
  if (options.actions) this.actions = [...options.actions];
2846
3121
  if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
2847
3122
  if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
2848
- if (options.liveblocks && options.cloud) {
2849
- const live = options.liveblocks;
2850
- const cloud = options.cloud;
2851
- void import("./live-collab-IRUNFAPE.js").then(({ LiveCollab }) => {
2852
- this.liveCollab = new LiveCollab({
2853
- publicKey: live.publicKey,
2854
- authEndpoint: live.authEndpoint,
2855
- workspaceSlug: cloud.workspace
2856
- });
2857
- void this.liveCollab.start();
2858
- });
2859
- }
2860
3123
  if (typeof window !== "undefined") {
2861
3124
  window.addEventListener("keydown", this.onKeyDown);
2862
3125
  void this.loadComments().then(() => this.subscribeStorage());
@@ -2914,8 +3177,6 @@ var Vizu = class {
2914
3177
  this.unsubscribeStorage?.();
2915
3178
  this.unsubscribeStorage = null;
2916
3179
  this.selfHealedThisSession.clear();
2917
- this.liveCollab?.destroy();
2918
- this.liveCollab = null;
2919
3180
  this.bus.clear();
2920
3181
  }
2921
3182
  /* ===== Action API ===== */
@@ -3237,11 +3498,7 @@ var Vizu = class {
3237
3498
  this.highlighter = new Highlighter(
3238
3499
  this.root,
3239
3500
  this.opts.ignoreSelectors ?? [],
3240
- (el, e) => this.onElementClick(el, e),
3241
- (el) => {
3242
- if (!this.liveCollab) return;
3243
- this.liveCollab.setLocalSelection(el ? fingerprint(el) : null);
3244
- }
3501
+ (el, e) => this.onElementClick(el, e)
3245
3502
  );
3246
3503
  this.highlighter.start();
3247
3504
  this.renderAllMarkers();