@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/auto.cjs +682 -1848
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +310 -57
- package/dist/auto.js.map +1 -1
- package/dist/index.cjs +681 -1847
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -39
- package/dist/index.d.ts +11 -39
- package/dist/index.js +310 -57
- package/dist/index.js.map +1 -1
- package/dist/vizu.min.js +55 -234
- package/dist/vizu.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-OMIFOSQ2.js +0 -295
- package/dist/chunk-OMIFOSQ2.js.map +0 -1
- package/dist/live-collab-BVLNJ5QI.js +0 -1098
- package/dist/live-collab-BVLNJ5QI.js.map +0 -1
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
|
|
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 = {
|