@holdyapp/sdk 1.5.0 → 1.9.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/README.md +9 -1
- package/dist/changes.d.ts +16 -2
- package/dist/changes.d.ts.map +1 -1
- package/dist/core.d.ts +1 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/friction.d.ts +2 -1
- package/dist/friction.d.ts.map +1 -1
- package/dist/holdy.js +25 -4
- package/dist/holdy.js.map +4 -4
- package/dist/index.cjs +540 -33
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +540 -33
- package/dist/index.mjs.map +4 -4
- package/dist/server.cjs +3 -0
- package/dist/server.cjs.map +2 -2
- package/dist/server.mjs +3 -0
- package/dist/server.mjs.map +2 -2
- package/dist/skeleton.d.ts +52 -0
- package/dist/skeleton.d.ts.map +1 -0
- package/dist/theme.d.ts +43 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/tip-css.d.ts +9 -0
- package/dist/tip-css.d.ts.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/web-context.d.ts +1 -0
- package/dist/web-context.d.ts.map +1 -1
- package/dist/web.d.ts +10 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.mjs +540 -33
- package/dist/web.mjs.map +4 -4
- package/package.json +1 -1
- package/src/changes.dom.test.ts +221 -0
- package/src/changes.test.ts +38 -0
- package/src/changes.ts +299 -26
- package/src/core.test.ts +15 -0
- package/src/core.ts +3 -0
- package/src/friction.dom.test.ts +54 -0
- package/src/friction.test.ts +43 -0
- package/src/friction.ts +44 -7
- package/src/skeleton.test.ts +60 -0
- package/src/skeleton.ts +182 -0
- package/src/theme.test.ts +118 -0
- package/src/theme.ts +120 -0
- package/src/tip-css.ts +32 -0
- package/src/types.ts +6 -0
- package/src/web-context.test.ts +18 -0
- package/src/web-context.ts +8 -0
- package/src/web.ts +28 -3
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ var HoldyCore = class {
|
|
|
11
11
|
flushAt;
|
|
12
12
|
debug;
|
|
13
13
|
source;
|
|
14
|
+
release;
|
|
14
15
|
queue = [];
|
|
15
16
|
timer = null;
|
|
16
17
|
flushing = null;
|
|
@@ -27,6 +28,7 @@ var HoldyCore = class {
|
|
|
27
28
|
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
28
29
|
this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT);
|
|
29
30
|
this.debug = opts.debug ?? false;
|
|
31
|
+
this.release = typeof opts.release === "string" && opts.release.trim() ? opts.release.trim().slice(0, 64) : null;
|
|
30
32
|
const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
31
33
|
if (intervalMs > 0) {
|
|
32
34
|
this.timer = setInterval(() => void this.flush(), intervalMs);
|
|
@@ -43,6 +45,7 @@ var HoldyCore = class {
|
|
|
43
45
|
const occurredAt = overrides?.occurredAt ? new Date(overrides.occurredAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
44
46
|
const context = {
|
|
45
47
|
...this.environmentContext(),
|
|
48
|
+
...this.release ? { release: this.release } : {},
|
|
46
49
|
...overrides?.context ?? {}
|
|
47
50
|
};
|
|
48
51
|
if (accountKey) context.account_external_id = accountKey;
|
|
@@ -344,11 +347,14 @@ var NavLoopDetector = class {
|
|
|
344
347
|
return null;
|
|
345
348
|
}
|
|
346
349
|
};
|
|
350
|
+
function tagOf(el) {
|
|
351
|
+
return typeof el?.tagName === "string" ? el.tagName.toLowerCase() : "";
|
|
352
|
+
}
|
|
347
353
|
function resolveInteractive(el) {
|
|
348
354
|
if (!el) return null;
|
|
349
355
|
const viaClosest = el.closest?.(INTERACTIVE_SELECTOR) ?? null;
|
|
350
356
|
if (viaClosest) return viaClosest;
|
|
351
|
-
const tag = el
|
|
357
|
+
const tag = tagOf(el);
|
|
352
358
|
if (INTERACTIVE_TAGS.has(tag)) return el;
|
|
353
359
|
const role = el.getAttribute("role");
|
|
354
360
|
if (role === "button" || role === "link" || role === "menuitem" || role === "tab") return el;
|
|
@@ -360,7 +366,7 @@ function isDeadClickTarget(el) {
|
|
|
360
366
|
if (target.disabled === true) return true;
|
|
361
367
|
if (target.getAttribute("disabled") !== null) return true;
|
|
362
368
|
if (target.getAttribute("aria-disabled") === "true") return true;
|
|
363
|
-
const tag = target
|
|
369
|
+
const tag = tagOf(target);
|
|
364
370
|
if (tag === "a") {
|
|
365
371
|
const href = target.getAttribute("href");
|
|
366
372
|
if (!href || href === "#" || href.startsWith("javascript:")) return true;
|
|
@@ -370,7 +376,7 @@ function isDeadClickTarget(el) {
|
|
|
370
376
|
function describeElement(el) {
|
|
371
377
|
const target = resolveInteractive(el) ?? el;
|
|
372
378
|
if (!target) return {};
|
|
373
|
-
const out = { element: target
|
|
379
|
+
const out = { element: tagOf(target) };
|
|
374
380
|
if (target.id) out.element_id = target.id;
|
|
375
381
|
const role = target.getAttribute("role");
|
|
376
382
|
if (role) out.element_role = role;
|
|
@@ -398,6 +404,22 @@ function errorMessage(reason) {
|
|
|
398
404
|
return String(reason);
|
|
399
405
|
}
|
|
400
406
|
}
|
|
407
|
+
var STACK_MAX_FRAMES = 8;
|
|
408
|
+
var STACK_MAX_FRAME_CHARS = 200;
|
|
409
|
+
var STACK_FRAME = /^at\s|^\S*@\S+:\d+(?::\d+)?$/;
|
|
410
|
+
function sanitizeStack(stack) {
|
|
411
|
+
if (typeof stack !== "string" || !stack) return null;
|
|
412
|
+
const frames = stack.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && STACK_FRAME.test(line)).slice(0, STACK_MAX_FRAMES).map(
|
|
413
|
+
(line) => line.replace(/(https?:\/\/[^\s)?#]+)[?#][^\s)]*?(?=(?::\d+){0,2}\)?$)/, "$1").slice(0, STACK_MAX_FRAME_CHARS)
|
|
414
|
+
);
|
|
415
|
+
return frames.length > 0 ? frames.join("\n") : null;
|
|
416
|
+
}
|
|
417
|
+
function withStack(stack) {
|
|
418
|
+
return stack ? { stack } : {};
|
|
419
|
+
}
|
|
420
|
+
function stackOf(reason) {
|
|
421
|
+
return typeof reason === "object" && reason !== null ? sanitizeStack(reason.stack) : null;
|
|
422
|
+
}
|
|
401
423
|
function isAbortError(err) {
|
|
402
424
|
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
403
425
|
}
|
|
@@ -475,13 +497,19 @@ function installFrictionCapture(opts) {
|
|
|
475
497
|
emit("js_error", {
|
|
476
498
|
message: String(ev.message ?? "").slice(0, 200),
|
|
477
499
|
...ev.filename ? { source: stripRequestUrl(ev.filename, location.href) } : {},
|
|
478
|
-
...ev.lineno ? { line: ev.lineno } : {}
|
|
500
|
+
...ev.lineno ? { line: ev.lineno } : {},
|
|
501
|
+
...ev.colno ? { column: ev.colno } : {},
|
|
502
|
+
...withStack(stackOf(ev.error))
|
|
479
503
|
});
|
|
480
504
|
};
|
|
481
505
|
window.addEventListener("error", onError);
|
|
482
506
|
cleanups.push(() => window.removeEventListener("error", onError));
|
|
483
507
|
const onRejection = (ev) => {
|
|
484
|
-
emit("js_error", {
|
|
508
|
+
emit("js_error", {
|
|
509
|
+
message: errorMessage(ev.reason).slice(0, 200),
|
|
510
|
+
unhandled_rejection: true,
|
|
511
|
+
...withStack(stackOf(ev.reason))
|
|
512
|
+
});
|
|
485
513
|
};
|
|
486
514
|
window.addEventListener("unhandledrejection", onRejection);
|
|
487
515
|
cleanups.push(() => window.removeEventListener("unhandledrejection", onRejection));
|
|
@@ -563,11 +591,11 @@ function installFrictionCapture(opts) {
|
|
|
563
591
|
}
|
|
564
592
|
const onFocusIn = (ev) => {
|
|
565
593
|
const el = ev.target;
|
|
566
|
-
if (!el || !FORM_CONTROL_TAGS.has(el
|
|
594
|
+
if (!el || !FORM_CONTROL_TAGS.has(tagOf(el))) return;
|
|
567
595
|
const form = el.form;
|
|
568
596
|
if (!form) return;
|
|
569
597
|
const entry = touched.get(form) ?? { form: formKey(form), fields: /* @__PURE__ */ new Set() };
|
|
570
|
-
const field = el.name || el.id || el
|
|
598
|
+
const field = el.name || el.id || tagOf(el);
|
|
571
599
|
entry.fields.add(field);
|
|
572
600
|
touched.set(form, entry);
|
|
573
601
|
};
|
|
@@ -600,6 +628,200 @@ function installFrictionCapture(opts) {
|
|
|
600
628
|
};
|
|
601
629
|
}
|
|
602
630
|
|
|
631
|
+
// src/theme.ts
|
|
632
|
+
var THEME_BUTTON_SELECTOR = 'button[type="submit"],[data-testid*="primary"],.btn-primary,button,[role="button"],a.button';
|
|
633
|
+
var THEME_MAX_BUTTONS = 40;
|
|
634
|
+
var THEME_KEYS = ["font", "size", "fg", "bg", "accent", "accentFg", "radius"];
|
|
635
|
+
var COLOR = /^(rgba?\([\d.,\s%/]{1,40}\)|#[0-9a-fA-F]{3,8}|(?:color|oklch|oklab|hsla?|lab|lch)\([^()]{1,60}\))$/;
|
|
636
|
+
var MIN_ACCENT_CONTRAST = 60;
|
|
637
|
+
var RGB = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/;
|
|
638
|
+
function colorDistance(a, b) {
|
|
639
|
+
const x = RGB.exec(a);
|
|
640
|
+
const y = b ? RGB.exec(b) : null;
|
|
641
|
+
if (!x) return MIN_ACCENT_CONTRAST + 1;
|
|
642
|
+
if (!y) return MIN_ACCENT_CONTRAST + 1;
|
|
643
|
+
return Math.abs(+x[1] - +y[1]) + Math.abs(+x[2] - +y[2]) + Math.abs(+x[3] - +y[3]);
|
|
644
|
+
}
|
|
645
|
+
var TRANSPARENT = /^(transparent|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0\s*\)|rgba?\(\s*\d+\s+\d+\s+\d+\s*\/\s*0\s*\))$/;
|
|
646
|
+
function themeVarName(key) {
|
|
647
|
+
return `--holdy-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
648
|
+
}
|
|
649
|
+
function cleanColor(value) {
|
|
650
|
+
const v = (value ?? "").trim();
|
|
651
|
+
return v && v.length <= 80 && !TRANSPARENT.test(v) && COLOR.test(v) ? v : void 0;
|
|
652
|
+
}
|
|
653
|
+
function cleanFont(value) {
|
|
654
|
+
const first = (value ?? "").split(",")[0].trim().replace(/^["']|["']$/g, "").trim();
|
|
655
|
+
return /^[\w\s-]{1,60}$/.test(first) ? first : void 0;
|
|
656
|
+
}
|
|
657
|
+
function cleanPx(value, min, max) {
|
|
658
|
+
const m = /^(\d+(?:\.\d+)?)px/.exec((value ?? "").trim());
|
|
659
|
+
const n = m ? Math.round(Number(m[1])) : NaN;
|
|
660
|
+
return n >= min && n <= max ? `${n}px` : void 0;
|
|
661
|
+
}
|
|
662
|
+
function visible(el) {
|
|
663
|
+
const r = el.getBoundingClientRect?.();
|
|
664
|
+
return !r || r.width > 0 && r.height > 0;
|
|
665
|
+
}
|
|
666
|
+
function readTheme(doc, getStyle, anchor = null) {
|
|
667
|
+
const body = doc.body;
|
|
668
|
+
if (!body) return null;
|
|
669
|
+
const bodyStyle = getStyle(body);
|
|
670
|
+
const theme = {};
|
|
671
|
+
const put = (key, value) => {
|
|
672
|
+
if (value) theme[key] = value;
|
|
673
|
+
};
|
|
674
|
+
put("font", cleanFont(bodyStyle.getPropertyValue("font-family")));
|
|
675
|
+
put("size", cleanPx(bodyStyle.getPropertyValue("font-size"), 10, 20));
|
|
676
|
+
put("fg", cleanColor(bodyStyle.getPropertyValue("color")));
|
|
677
|
+
put("bg", cleanColor(bodyStyle.getPropertyValue("background-color")) ?? cleanColor(getStyle(doc.documentElement).getPropertyValue("background-color")));
|
|
678
|
+
const candidates = doc.querySelectorAll(THEME_BUTTON_SELECTOR);
|
|
679
|
+
let button = null;
|
|
680
|
+
let best = MIN_ACCENT_CONTRAST;
|
|
681
|
+
for (let i = 0; i < candidates.length && i < THEME_MAX_BUTTONS; i++) {
|
|
682
|
+
const el = candidates[i];
|
|
683
|
+
if (!visible(el)) continue;
|
|
684
|
+
const s = getStyle(el);
|
|
685
|
+
const accent = cleanColor(s.getPropertyValue("background-color"));
|
|
686
|
+
if (!accent || accent === theme.bg) continue;
|
|
687
|
+
const contrast = colorDistance(accent, theme.bg);
|
|
688
|
+
if (contrast <= best) continue;
|
|
689
|
+
best = contrast;
|
|
690
|
+
put("accent", accent);
|
|
691
|
+
put("accentFg", cleanColor(s.getPropertyValue("color")));
|
|
692
|
+
button = el;
|
|
693
|
+
}
|
|
694
|
+
const radiusSource = anchor && anchor.tagName.toLowerCase() === "button" ? anchor : button;
|
|
695
|
+
if (radiusSource) put("radius", cleanPx(getStyle(radiusSource).getPropertyValue("border-top-left-radius"), 0, 24));
|
|
696
|
+
return Object.keys(theme).length > 0 ? theme : null;
|
|
697
|
+
}
|
|
698
|
+
function themeVars(theme) {
|
|
699
|
+
const out = [];
|
|
700
|
+
for (const key of THEME_KEYS) {
|
|
701
|
+
const value = theme?.[key];
|
|
702
|
+
if (value) out.push([themeVarName(key), value]);
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
function applyThemeVars(el, theme) {
|
|
707
|
+
for (const key of THEME_KEYS) el.style.removeProperty(themeVarName(key));
|
|
708
|
+
for (const [name, value] of themeVars(theme)) el.style.setProperty(name, value);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/skeleton.ts
|
|
712
|
+
var SKELETON_SELECTOR = 'a[href], button, input, select, textarea, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="checkbox"], [role="radio"], [role="switch"], [data-testid]';
|
|
713
|
+
var SKELETON_MAX_ELEMENTS = 80;
|
|
714
|
+
var SKELETON_DELAY_MS = 1500;
|
|
715
|
+
var SENT_STORAGE_KEY = "holdy_skeletons";
|
|
716
|
+
var SENT_STORAGE_MAX = 200;
|
|
717
|
+
function fnv1a(input) {
|
|
718
|
+
let hash = 2166136261;
|
|
719
|
+
for (let i = 0; i < input.length; i++) {
|
|
720
|
+
hash ^= input.charCodeAt(i);
|
|
721
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
722
|
+
}
|
|
723
|
+
return hash.toString(16).padStart(8, "0");
|
|
724
|
+
}
|
|
725
|
+
function attr(el, name, max) {
|
|
726
|
+
const value = el.getAttribute(name);
|
|
727
|
+
if (!value) return void 0;
|
|
728
|
+
const trimmed = value.replace(/\s+/g, " ").trim().slice(0, max);
|
|
729
|
+
return trimmed || void 0;
|
|
730
|
+
}
|
|
731
|
+
function isVisible(el) {
|
|
732
|
+
if (!el.getBoundingClientRect) return true;
|
|
733
|
+
const rect = el.getBoundingClientRect();
|
|
734
|
+
return rect.width > 0 && rect.height > 0;
|
|
735
|
+
}
|
|
736
|
+
function relativeBox(rect, viewport) {
|
|
737
|
+
const pct = (value, base) => base > 0 ? Math.max(0, Math.min(100, Math.round(value / base * 100))) : 0;
|
|
738
|
+
return [pct(rect.x, viewport.width), pct(rect.y + viewport.scrollY, viewport.height), pct(rect.width, viewport.width), pct(rect.height, viewport.height)];
|
|
739
|
+
}
|
|
740
|
+
function describeSkeletonElement(el, viewport) {
|
|
741
|
+
const out = { t: el.tagName.toLowerCase() };
|
|
742
|
+
const role = attr(el, "role", 40);
|
|
743
|
+
if (role) out.r = role;
|
|
744
|
+
const testid = attr(el, "data-testid", 80);
|
|
745
|
+
if (testid) out.id = testid;
|
|
746
|
+
const name = attr(el, "name", 80);
|
|
747
|
+
if (name) out.n = name;
|
|
748
|
+
const label = attr(el, "aria-label", 80);
|
|
749
|
+
if (label) out.l = label;
|
|
750
|
+
if (out.t === "input" || out.t === "button") {
|
|
751
|
+
const type = attr(el, "type", 20);
|
|
752
|
+
if (type) out.k = type.toLowerCase();
|
|
753
|
+
}
|
|
754
|
+
if (viewport && el.getBoundingClientRect) out.b = relativeBox(el.getBoundingClientRect(), viewport);
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
function collectSkeleton(root, path, viewport, theme = null) {
|
|
758
|
+
const found = root.querySelectorAll(SKELETON_SELECTOR);
|
|
759
|
+
const visible2 = [];
|
|
760
|
+
for (let i = 0; i < found.length; i++) {
|
|
761
|
+
const el = found[i];
|
|
762
|
+
if (el.tagName.toLowerCase() === "input" && (el.getAttribute("type") ?? "").toLowerCase() === "hidden") continue;
|
|
763
|
+
if (!isVisible(el)) continue;
|
|
764
|
+
visible2.push(el);
|
|
765
|
+
}
|
|
766
|
+
const elements = visible2.slice(0, SKELETON_MAX_ELEMENTS).map((el) => describeSkeletonElement(el, viewport));
|
|
767
|
+
const structural = elements.map(({ b: _b, ...rest }) => rest);
|
|
768
|
+
return { path, hash: fnv1a(JSON.stringify(structural)), count: visible2.length, elements, ...theme ? { theme } : {} };
|
|
769
|
+
}
|
|
770
|
+
function loadSent() {
|
|
771
|
+
try {
|
|
772
|
+
const raw = sessionStorage.getItem(SENT_STORAGE_KEY);
|
|
773
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
774
|
+
return new Set(Array.isArray(list) ? list.filter((x) => typeof x === "string") : []);
|
|
775
|
+
} catch {
|
|
776
|
+
return /* @__PURE__ */ new Set();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function saveSent(sent) {
|
|
780
|
+
try {
|
|
781
|
+
sessionStorage.setItem(SENT_STORAGE_KEY, JSON.stringify([...sent].slice(-SENT_STORAGE_MAX)));
|
|
782
|
+
} catch {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function installSkeletonCapture(opts) {
|
|
787
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
788
|
+
return { onNavigate: () => void 0, teardown: () => void 0 };
|
|
789
|
+
}
|
|
790
|
+
const delay = opts.delayMs ?? SKELETON_DELAY_MS;
|
|
791
|
+
const sent = loadSent();
|
|
792
|
+
let pending = null;
|
|
793
|
+
const capture = (path) => {
|
|
794
|
+
pending = null;
|
|
795
|
+
if (location.pathname !== path) return;
|
|
796
|
+
try {
|
|
797
|
+
const viewport = {
|
|
798
|
+
width: window.innerWidth,
|
|
799
|
+
height: Math.max(document.documentElement.scrollHeight, window.innerHeight),
|
|
800
|
+
scrollY: window.scrollY
|
|
801
|
+
};
|
|
802
|
+
const theme = readTheme(document, (el) => window.getComputedStyle(el));
|
|
803
|
+
const skeleton = collectSkeleton(document, path, viewport, theme);
|
|
804
|
+
const key = `${path}:${skeleton.hash}`;
|
|
805
|
+
if (sent.has(key)) return;
|
|
806
|
+
sent.add(key);
|
|
807
|
+
saveSent(sent);
|
|
808
|
+
opts.onSkeleton(skeleton);
|
|
809
|
+
} catch {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
return {
|
|
814
|
+
onNavigate: (path) => {
|
|
815
|
+
if (pending) clearTimeout(pending);
|
|
816
|
+
pending = setTimeout(() => capture(path), delay);
|
|
817
|
+
},
|
|
818
|
+
teardown: () => {
|
|
819
|
+
if (pending) clearTimeout(pending);
|
|
820
|
+
pending = null;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
603
825
|
// src/web-context.ts
|
|
604
826
|
var UTM_KEYS = [
|
|
605
827
|
"utm_source",
|
|
@@ -664,6 +886,12 @@ function randomId() {
|
|
|
664
886
|
return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
665
887
|
}
|
|
666
888
|
}
|
|
889
|
+
function clearSessionId() {
|
|
890
|
+
try {
|
|
891
|
+
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
}
|
|
667
895
|
function resolveSessionId() {
|
|
668
896
|
const existing = readStorage("session", SESSION_STORAGE_KEY);
|
|
669
897
|
if (existing) return existing;
|
|
@@ -717,8 +945,35 @@ function buildWebContext() {
|
|
|
717
945
|
};
|
|
718
946
|
}
|
|
719
947
|
|
|
948
|
+
// src/tip-css.ts
|
|
949
|
+
var TIP_CSS = `:host{all:initial;--h-bg:#111;--h-fg:#fff;--h-accent:#fff;--h-accent-fg:#111;--h-border:transparent}
|
|
950
|
+
@media (prefers-color-scheme:dark){:host{--h-bg:#fff;--h-fg:#111;--h-accent:#111;--h-accent-fg:#fff}}
|
|
951
|
+
.tip{position:fixed;box-sizing:border-box;max-width:280px;padding:10px 12px;border-radius:var(--holdy-radius,8px);background:var(--holdy-bg,var(--h-bg));color:var(--holdy-fg,var(--h-fg));border:1px solid color-mix(in srgb,var(--holdy-fg,var(--h-fg)) 14%,transparent);font:var(--holdy-size,13px)/1.45 var(--holdy-font,system-ui),system-ui,-apple-system,sans-serif;box-shadow:0 8px 24px rgba(0,0,0,.18);pointer-events:auto;display:flex;gap:10px;align-items:flex-start}
|
|
952
|
+
.tip p{margin:0;flex:1}
|
|
953
|
+
.tip button{all:unset;cursor:pointer;opacity:.7;font-size:15px;line-height:1;color:inherit}
|
|
954
|
+
.tip button:hover,.tip button:focus-visible{opacity:1}
|
|
955
|
+
.ask{flex-direction:column;width:300px;max-width:calc(100vw - 16px);gap:8px}
|
|
956
|
+
.ask .head{display:flex;gap:10px;align-items:flex-start;width:100%}
|
|
957
|
+
.ask textarea{all:unset;box-sizing:border-box;width:100%;min-height:56px;padding:8px;border-radius:calc(var(--holdy-radius,8px) * .75);background:color-mix(in srgb,var(--holdy-fg,var(--h-fg)) 8%,transparent);color:inherit;font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;cursor:text}
|
|
958
|
+
.ask textarea:focus-visible{outline:2px solid color-mix(in srgb,var(--holdy-fg,var(--h-fg)) 45%,transparent)}
|
|
959
|
+
.ask .send{align-self:flex-end;padding:5px 12px;border-radius:calc(var(--holdy-radius,8px) * .75);background:var(--holdy-accent,var(--h-accent));color:var(--holdy-accent-fg,var(--h-accent-fg));font-size:12.5px;font-weight:500;opacity:1}
|
|
960
|
+
.ask .send[disabled]{opacity:.4;cursor:default}
|
|
961
|
+
.list{flex-direction:column;width:300px;max-width:calc(100vw - 16px);gap:8px}
|
|
962
|
+
.list .head{display:flex;gap:10px;align-items:flex-start;width:100%}
|
|
963
|
+
.list .head p{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
|
|
964
|
+
.list .count{opacity:.7;font-size:12px;white-space:nowrap}
|
|
965
|
+
.list ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;width:100%}
|
|
966
|
+
.list li{display:flex;gap:8px;align-items:flex-start}
|
|
967
|
+
.list li.done{opacity:.55;text-decoration:line-through}
|
|
968
|
+
.list .mark{flex:none;width:16px;height:16px;border-radius:4px;border:1px solid color-mix(in srgb,var(--holdy-fg,var(--h-fg)) 45%,transparent);font-size:11px;line-height:16px;text-align:center}
|
|
969
|
+
.list li.done .mark{background:var(--holdy-accent,var(--h-accent));color:var(--holdy-accent-fg,var(--h-accent-fg));border-color:var(--holdy-accent,var(--h-accent))}
|
|
970
|
+
.list.folded ul{display:none}
|
|
971
|
+
.list.folded{width:auto}
|
|
972
|
+
.preview{flex:none;order:-1;width:100%;font-size:11px;opacity:.7}
|
|
973
|
+
.tip:has(.preview){flex-wrap:wrap;row-gap:4px}`;
|
|
974
|
+
|
|
720
975
|
// src/arm.ts
|
|
721
|
-
function
|
|
976
|
+
function fnv1a2(input) {
|
|
722
977
|
let hash = 2166136261;
|
|
723
978
|
for (let i = 0; i < input.length; i++) {
|
|
724
979
|
hash ^= input.charCodeAt(i);
|
|
@@ -727,7 +982,7 @@ function fnv1a(input) {
|
|
|
727
982
|
return hash >>> 0;
|
|
728
983
|
}
|
|
729
984
|
function changeBucket(changeId, accountKey) {
|
|
730
|
-
return
|
|
985
|
+
return fnv1a2(`${changeId}:${accountKey}`) % 100;
|
|
731
986
|
}
|
|
732
987
|
function changeArm(changeId, accountKey, holdoutPct) {
|
|
733
988
|
return changeBucket(changeId, accountKey) < holdoutPct ? "holdout" : "change";
|
|
@@ -746,12 +1001,16 @@ var SCAN_DEBOUNCE_MS = 250;
|
|
|
746
1001
|
var SEEN_KEY = "holdy_changes_seen";
|
|
747
1002
|
var DISMISSED_KEY = "holdy_changes_dismissed";
|
|
748
1003
|
var TIP_GAP_PX = 8;
|
|
749
|
-
var
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1004
|
+
var ANSWERED_KEY = "holdy_questions_answered";
|
|
1005
|
+
var FOLDED_KEY = "holdy_checklist_folded";
|
|
1006
|
+
var COMPLETED_KEY = "holdy_checklist_completed";
|
|
1007
|
+
var PROGRESS_REFRESH_MS = 60 * 1e3;
|
|
1008
|
+
var VOICE_EVENT = "$voice";
|
|
1009
|
+
var MAX_ANSWER_CHARS = 1e3;
|
|
1010
|
+
var THANKS_MS = 1600;
|
|
1011
|
+
var PREVIEW_PARAM = "holdy_preview";
|
|
1012
|
+
var PREVIEW_KEY = "holdy_preview";
|
|
1013
|
+
var PREVIEW_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
755
1014
|
function normalizeText(text) {
|
|
756
1015
|
return text.replace(/\s+/g, " ").trim();
|
|
757
1016
|
}
|
|
@@ -764,13 +1023,42 @@ function isAnchor(value) {
|
|
|
764
1023
|
if (a?.kind === "page") return true;
|
|
765
1024
|
return (a?.kind === "selector" || a?.kind === "text") && typeof a.value === "string" && a.value.length > 0;
|
|
766
1025
|
}
|
|
1026
|
+
function isItem(value) {
|
|
1027
|
+
const i = value;
|
|
1028
|
+
return typeof i?.definition_name === "string" && typeof i.label === "string" && Array.isArray(i.events) && i.events.every((e) => typeof e === "string");
|
|
1029
|
+
}
|
|
767
1030
|
function isChange(value) {
|
|
768
1031
|
const c = value;
|
|
769
|
-
return typeof c?.id === "string" && (c.kind === "copy" || c.kind === "guidance") && typeof c.path === "string" && isAnchor(c.anchor) && typeof c.content?.text === "string" && typeof c.holdout_pct === "number";
|
|
1032
|
+
return typeof c?.id === "string" && (c.kind === "copy" || c.kind === "guidance" || c.kind === "question" && typeof c.plan_id === "string" || c.kind === "checklist" && Array.isArray(c.content?.items) && c.content.items.length > 0 && c.content.items.every(isItem)) && typeof c.path === "string" && isAnchor(c.anchor) && typeof c.content?.text === "string" && typeof c.holdout_pct === "number";
|
|
770
1033
|
}
|
|
771
|
-
function changesForPage(changes, path, accountKey) {
|
|
772
|
-
|
|
773
|
-
|
|
1034
|
+
function changesForPage(changes, path, accountKey, previewId = null) {
|
|
1035
|
+
return changes.filter((c) => c.kind === "checklist" || matchesPathTemplate(c.path, path)).flatMap((change) => {
|
|
1036
|
+
if (change.id === previewId) return [{ change, arm: "change", preview: true }];
|
|
1037
|
+
if (!accountKey) return [];
|
|
1038
|
+
return [{ change, arm: changeArm(change.id, accountKey, change.holdout_pct), preview: false }];
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
function resolvePreviewId(search, store) {
|
|
1042
|
+
let asked = null;
|
|
1043
|
+
try {
|
|
1044
|
+
asked = new URLSearchParams(search).get(PREVIEW_PARAM);
|
|
1045
|
+
} catch {
|
|
1046
|
+
asked = null;
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
if (asked !== null) {
|
|
1050
|
+
if (PREVIEW_ID.test(asked)) {
|
|
1051
|
+
store?.setItem(PREVIEW_KEY, asked);
|
|
1052
|
+
return asked;
|
|
1053
|
+
}
|
|
1054
|
+
store?.removeItem(PREVIEW_KEY);
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
const kept = store?.getItem(PREVIEW_KEY) ?? null;
|
|
1058
|
+
return kept && PREVIEW_ID.test(kept) ? kept : null;
|
|
1059
|
+
} catch {
|
|
1060
|
+
return asked !== null && PREVIEW_ID.test(asked) ? asked : null;
|
|
1061
|
+
}
|
|
774
1062
|
}
|
|
775
1063
|
function storage(kind) {
|
|
776
1064
|
try {
|
|
@@ -832,7 +1120,7 @@ function findTextNode(el, match) {
|
|
|
832
1120
|
}
|
|
833
1121
|
function installProductChanges(opts) {
|
|
834
1122
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
835
|
-
return { onNavigate: () => void 0, refresh: async () => void 0, teardown: () => void 0 };
|
|
1123
|
+
return { onNavigate: () => void 0, onEvent: () => void 0, refresh: async () => void 0, teardown: () => void 0 };
|
|
836
1124
|
}
|
|
837
1125
|
let changes = [];
|
|
838
1126
|
let lastFetch = 0;
|
|
@@ -841,6 +1129,11 @@ function installProductChanges(opts) {
|
|
|
841
1129
|
let host = null;
|
|
842
1130
|
const copies = /* @__PURE__ */ new Map();
|
|
843
1131
|
const tips = /* @__PURE__ */ new Map();
|
|
1132
|
+
const progress = /* @__PURE__ */ new Map();
|
|
1133
|
+
const progressAt = /* @__PURE__ */ new Map();
|
|
1134
|
+
const lists = /* @__PURE__ */ new Map();
|
|
1135
|
+
let fetchedForKey = null;
|
|
1136
|
+
let previewId = resolvePreviewId(location.search, storage("session"));
|
|
844
1137
|
const log = (message) => {
|
|
845
1138
|
if (opts.debug) console.warn(`[holdy] ${message}`);
|
|
846
1139
|
};
|
|
@@ -890,21 +1183,66 @@ function installProductChanges(opts) {
|
|
|
890
1183
|
document.body.appendChild(host);
|
|
891
1184
|
return root;
|
|
892
1185
|
};
|
|
893
|
-
|
|
1186
|
+
let dismissedPreview = false;
|
|
1187
|
+
const previewTag = () => {
|
|
1188
|
+
const tag = document.createElement("span");
|
|
1189
|
+
tag.className = "preview";
|
|
1190
|
+
tag.textContent = "Pr\xE9via, s\xF3 voc\xEA v\xEA";
|
|
1191
|
+
return tag;
|
|
1192
|
+
};
|
|
1193
|
+
const showTip = (anchor, change, preview) => {
|
|
894
1194
|
if (tips.has(change.id)) return true;
|
|
895
|
-
if (readSet(storage("local"), DISMISSED_KEY).has(change.id)) return true;
|
|
1195
|
+
if (!preview && readSet(storage("local"), DISMISSED_KEY).has(change.id)) return true;
|
|
1196
|
+
const asking = change.kind === "question";
|
|
1197
|
+
if (!preview && asking && readSet(storage("local"), ANSWERED_KEY).has(change.id)) return true;
|
|
896
1198
|
const shadow = ensureRoot();
|
|
897
1199
|
if (!shadow) return false;
|
|
1200
|
+
if (host) applyThemeVars(host, readTheme(document, (el) => window.getComputedStyle(el), anchor));
|
|
898
1201
|
const card = document.createElement("div");
|
|
899
|
-
card.className = "tip";
|
|
900
|
-
card.setAttribute("role", "tooltip");
|
|
1202
|
+
card.className = asking ? "tip ask" : "tip";
|
|
1203
|
+
card.setAttribute("role", asking ? "dialog" : "tooltip");
|
|
901
1204
|
const text = document.createElement("p");
|
|
902
1205
|
text.textContent = change.content.text;
|
|
903
1206
|
const close = document.createElement("button");
|
|
904
1207
|
close.type = "button";
|
|
905
1208
|
close.setAttribute("aria-label", "Fechar");
|
|
906
1209
|
close.textContent = "\xD7";
|
|
907
|
-
|
|
1210
|
+
if (asking) {
|
|
1211
|
+
const head = document.createElement("div");
|
|
1212
|
+
head.className = "head";
|
|
1213
|
+
head.append(text, close);
|
|
1214
|
+
const input = document.createElement("textarea");
|
|
1215
|
+
input.maxLength = MAX_ANSWER_CHARS;
|
|
1216
|
+
input.setAttribute("aria-label", change.content.text);
|
|
1217
|
+
const send = document.createElement("button");
|
|
1218
|
+
send.type = "button";
|
|
1219
|
+
send.className = "send";
|
|
1220
|
+
send.textContent = "Enviar";
|
|
1221
|
+
send.disabled = true;
|
|
1222
|
+
input.addEventListener("input", () => {
|
|
1223
|
+
send.disabled = input.value.trim().length === 0;
|
|
1224
|
+
});
|
|
1225
|
+
send.addEventListener("click", () => {
|
|
1226
|
+
const answer = input.value.trim().slice(0, MAX_ANSWER_CHARS);
|
|
1227
|
+
if (!answer) return;
|
|
1228
|
+
if (!preview) {
|
|
1229
|
+
addToSet(storage("local"), ANSWERED_KEY, change.id);
|
|
1230
|
+
opts.track(VOICE_EVENT, {
|
|
1231
|
+
question: change.content.text,
|
|
1232
|
+
text: answer,
|
|
1233
|
+
action_plan_id: change.plan_id,
|
|
1234
|
+
change_id: change.id,
|
|
1235
|
+
path: location.pathname
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
card.replaceChildren(Object.assign(document.createElement("p"), { textContent: "Obrigado pela resposta." }));
|
|
1239
|
+
setTimeout(remove, THANKS_MS);
|
|
1240
|
+
});
|
|
1241
|
+
card.append(head, input, send);
|
|
1242
|
+
} else {
|
|
1243
|
+
card.append(text, close);
|
|
1244
|
+
}
|
|
1245
|
+
if (preview) card.prepend(previewTag());
|
|
908
1246
|
card.style.top = "-9999px";
|
|
909
1247
|
card.style.left = "-9999px";
|
|
910
1248
|
shadow.appendChild(card);
|
|
@@ -934,6 +1272,11 @@ function installProductChanges(opts) {
|
|
|
934
1272
|
card.style.left = `${Math.max(8, left)}px`;
|
|
935
1273
|
}
|
|
936
1274
|
close.addEventListener("click", () => {
|
|
1275
|
+
if (preview) {
|
|
1276
|
+
dismissedPreview = true;
|
|
1277
|
+
remove();
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
937
1280
|
addToSet(storage("local"), DISMISSED_KEY, change.id);
|
|
938
1281
|
opts.track("$change_dismissed", { change_id: change.id, kind: change.kind });
|
|
939
1282
|
remove();
|
|
@@ -944,20 +1287,143 @@ function installProductChanges(opts) {
|
|
|
944
1287
|
window.addEventListener("resize", position);
|
|
945
1288
|
return true;
|
|
946
1289
|
};
|
|
1290
|
+
const doneOf = (change) => progress.get(change.id) ?? /* @__PURE__ */ new Set();
|
|
1291
|
+
const allDone = (change) => {
|
|
1292
|
+
const done = doneOf(change);
|
|
1293
|
+
return (change.content.items ?? []).every((i) => done.has(i.definition_name));
|
|
1294
|
+
};
|
|
1295
|
+
const fetchProgress = async (change) => {
|
|
1296
|
+
const account = opts.getAccountKey();
|
|
1297
|
+
if (!account) return;
|
|
1298
|
+
const last = progressAt.get(change.id) ?? 0;
|
|
1299
|
+
if (Date.now() - last < PROGRESS_REFRESH_MS) return;
|
|
1300
|
+
progressAt.set(change.id, Date.now());
|
|
1301
|
+
try {
|
|
1302
|
+
const res = await fetch(
|
|
1303
|
+
`${opts.baseUrl}/runtime/checklist?change_id=${encodeURIComponent(change.id)}&account=${encodeURIComponent(account)}`,
|
|
1304
|
+
{ headers: { Authorization: `Bearer ${opts.apiKey}` } }
|
|
1305
|
+
);
|
|
1306
|
+
if (!res.ok) return;
|
|
1307
|
+
const body = await res.json();
|
|
1308
|
+
const done = Array.isArray(body.done) ? body.done.filter((d) => typeof d === "string") : [];
|
|
1309
|
+
const set = doneOf(change);
|
|
1310
|
+
for (const d of done) set.add(d);
|
|
1311
|
+
progress.set(change.id, set);
|
|
1312
|
+
lists.get(change.id)?.();
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
log(`checklist progress error: ${err.message}`);
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
const showChecklist = (change, preview) => {
|
|
1318
|
+
if (lists.has(change.id)) return true;
|
|
1319
|
+
if (!preview && readSet(storage("local"), COMPLETED_KEY).has(change.id)) return true;
|
|
1320
|
+
const shadow = ensureRoot();
|
|
1321
|
+
if (!shadow) return false;
|
|
1322
|
+
const items = change.content.items ?? [];
|
|
1323
|
+
const card = document.createElement("div");
|
|
1324
|
+
card.className = "tip list";
|
|
1325
|
+
card.setAttribute("role", "region");
|
|
1326
|
+
card.setAttribute("aria-label", change.content.text);
|
|
1327
|
+
const head = document.createElement("div");
|
|
1328
|
+
head.className = "head";
|
|
1329
|
+
const title = document.createElement("p");
|
|
1330
|
+
const strong = document.createElement("strong");
|
|
1331
|
+
strong.textContent = change.content.text;
|
|
1332
|
+
const count = document.createElement("span");
|
|
1333
|
+
count.className = "count";
|
|
1334
|
+
title.append(strong, count);
|
|
1335
|
+
const fold = document.createElement("button");
|
|
1336
|
+
fold.type = "button";
|
|
1337
|
+
head.append(title, fold);
|
|
1338
|
+
const list = document.createElement("ul");
|
|
1339
|
+
card.append(head, list);
|
|
1340
|
+
if (preview) card.prepend(previewTag());
|
|
1341
|
+
card.style.position = "fixed";
|
|
1342
|
+
card.style.right = "16px";
|
|
1343
|
+
card.style.bottom = "16px";
|
|
1344
|
+
shadow.appendChild(card);
|
|
1345
|
+
const remove = () => {
|
|
1346
|
+
card.remove();
|
|
1347
|
+
lists.delete(change.id);
|
|
1348
|
+
};
|
|
1349
|
+
const render = () => {
|
|
1350
|
+
const done = doneOf(change);
|
|
1351
|
+
const n = items.filter((i) => done.has(i.definition_name)).length;
|
|
1352
|
+
count.textContent = `${n} de ${items.length}`;
|
|
1353
|
+
const folded = readSet(storage("local"), FOLDED_KEY).has(change.id);
|
|
1354
|
+
card.classList.toggle("folded", folded);
|
|
1355
|
+
fold.textContent = folded ? "+" : "\u2013";
|
|
1356
|
+
fold.setAttribute("aria-label", folded ? "Abrir" : "Recolher");
|
|
1357
|
+
list.replaceChildren(
|
|
1358
|
+
...items.map((item) => {
|
|
1359
|
+
const li = document.createElement("li");
|
|
1360
|
+
const isDone = done.has(item.definition_name);
|
|
1361
|
+
if (isDone) li.className = "done";
|
|
1362
|
+
const mark = document.createElement("span");
|
|
1363
|
+
mark.className = "mark";
|
|
1364
|
+
mark.setAttribute("aria-hidden", "true");
|
|
1365
|
+
mark.textContent = isDone ? "\u2713" : "";
|
|
1366
|
+
const label = document.createElement("span");
|
|
1367
|
+
label.textContent = item.label;
|
|
1368
|
+
li.append(mark, label);
|
|
1369
|
+
return li;
|
|
1370
|
+
})
|
|
1371
|
+
);
|
|
1372
|
+
if (!preview && n === items.length && items.length > 0) {
|
|
1373
|
+
addToSet(storage("local"), COMPLETED_KEY, change.id);
|
|
1374
|
+
opts.track("$checklist_completed", { change_id: change.id });
|
|
1375
|
+
remove();
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
fold.addEventListener("click", () => {
|
|
1379
|
+
const store = storage("local");
|
|
1380
|
+
const set = readSet(store, FOLDED_KEY);
|
|
1381
|
+
if (set.has(change.id)) set.delete(change.id);
|
|
1382
|
+
else set.add(change.id);
|
|
1383
|
+
try {
|
|
1384
|
+
store?.setItem(FOLDED_KEY, JSON.stringify([...set].slice(-200)));
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
render();
|
|
1388
|
+
});
|
|
1389
|
+
lists.set(change.id, render);
|
|
1390
|
+
render();
|
|
1391
|
+
void fetchProgress(change);
|
|
1392
|
+
return true;
|
|
1393
|
+
};
|
|
947
1394
|
const scan = () => {
|
|
948
1395
|
timer = null;
|
|
949
|
-
const active = changesForPage(changes, location.pathname, opts.getAccountKey());
|
|
1396
|
+
const active = changesForPage(changes, location.pathname, opts.getAccountKey(), previewId);
|
|
950
1397
|
const activeIds = new Set(active.map((a) => a.change.id));
|
|
951
1398
|
for (const id of [...copies.keys()]) if (!activeIds.has(id)) restoreCopy(id);
|
|
952
1399
|
for (const [id, remove] of [...tips]) if (!activeIds.has(id)) remove();
|
|
953
|
-
for (const
|
|
1400
|
+
for (const [id, render] of [...lists]) {
|
|
1401
|
+
if (!activeIds.has(id)) {
|
|
1402
|
+
lists.delete(id);
|
|
1403
|
+
render();
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
for (const { change, arm, preview } of active) {
|
|
954
1407
|
const el = findAnchor(change.anchor);
|
|
955
|
-
if (!el && change.anchor.kind !== "page")
|
|
956
|
-
|
|
1408
|
+
if (!el && change.anchor.kind !== "page") {
|
|
1409
|
+
if (preview) log(`preview: \xE2ncora n\xE3o encontrada em ${location.pathname}`);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
if (preview) {
|
|
1413
|
+
if (change.kind === "checklist") showChecklist(change, true);
|
|
1414
|
+
else if (change.kind === "copy") {
|
|
1415
|
+
if (el) applyCopy(el, change);
|
|
1416
|
+
} else if (!dismissedPreview) showTip(el, change, true);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (change.kind === "checklist") {
|
|
1420
|
+
if (arm === "holdout" || allDone(change) || showChecklist(change, false)) expose(change, arm);
|
|
1421
|
+
if (arm === "change") void fetchProgress(change);
|
|
1422
|
+
} else if (change.kind === "copy") {
|
|
957
1423
|
if (!el) continue;
|
|
958
1424
|
const shown = arm === "change" ? applyCopy(el, change) : eligibleCopyNode(el, change) !== null;
|
|
959
1425
|
if (shown) expose(change, arm);
|
|
960
|
-
} else if (arm === "holdout" || showTip(el, change)) {
|
|
1426
|
+
} else if (arm === "holdout" || showTip(el, change, false)) {
|
|
961
1427
|
expose(change, arm);
|
|
962
1428
|
}
|
|
963
1429
|
}
|
|
@@ -967,8 +1433,14 @@ function installProductChanges(opts) {
|
|
|
967
1433
|
};
|
|
968
1434
|
const refresh = async () => {
|
|
969
1435
|
lastFetch = Date.now();
|
|
1436
|
+
const account = opts.getAccountKey();
|
|
1437
|
+
fetchedForKey = account;
|
|
1438
|
+
const params = new URLSearchParams();
|
|
1439
|
+
if (account) params.set("account", account);
|
|
1440
|
+
if (previewId) params.set("preview", previewId);
|
|
1441
|
+
const qs = params.toString();
|
|
970
1442
|
try {
|
|
971
|
-
const res = await fetch(`${opts.baseUrl}/runtime/changes`, {
|
|
1443
|
+
const res = await fetch(`${opts.baseUrl}/runtime/changes${qs ? `?${qs}` : ""}`, {
|
|
972
1444
|
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
973
1445
|
});
|
|
974
1446
|
if (!res.ok) {
|
|
@@ -993,9 +1465,28 @@ function installProductChanges(opts) {
|
|
|
993
1465
|
void refresh();
|
|
994
1466
|
return {
|
|
995
1467
|
onNavigate: () => {
|
|
996
|
-
|
|
1468
|
+
const asked = resolvePreviewId(location.search, storage("session"));
|
|
1469
|
+
const previewChanged = asked !== previewId;
|
|
1470
|
+
if (previewChanged) {
|
|
1471
|
+
previewId = asked;
|
|
1472
|
+
dismissedPreview = false;
|
|
1473
|
+
for (const id of [...copies.keys()]) restoreCopy(id);
|
|
1474
|
+
for (const remove of [...tips.values()]) remove();
|
|
1475
|
+
}
|
|
1476
|
+
if (previewChanged || Date.now() - lastFetch > NAV_REFRESH_MS || opts.getAccountKey() !== fetchedForKey) void refresh();
|
|
997
1477
|
schedule();
|
|
998
1478
|
},
|
|
1479
|
+
onEvent: (name) => {
|
|
1480
|
+
for (const change of changes) {
|
|
1481
|
+
if (change.kind !== "checklist") continue;
|
|
1482
|
+
const hit = (change.content.items ?? []).filter((i) => i.events.includes(name));
|
|
1483
|
+
if (hit.length === 0) continue;
|
|
1484
|
+
const set = doneOf(change);
|
|
1485
|
+
for (const i of hit) set.add(i.definition_name);
|
|
1486
|
+
progress.set(change.id, set);
|
|
1487
|
+
lists.get(change.id)?.();
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
999
1490
|
refresh,
|
|
1000
1491
|
teardown: () => {
|
|
1001
1492
|
clearInterval(interval);
|
|
@@ -1003,6 +1494,7 @@ function installProductChanges(opts) {
|
|
|
1003
1494
|
observer?.disconnect();
|
|
1004
1495
|
for (const id of [...copies.keys()]) restoreCopy(id);
|
|
1005
1496
|
for (const remove of [...tips.values()]) remove();
|
|
1497
|
+
for (const id of [...lists.keys()]) lists.delete(id);
|
|
1006
1498
|
host?.remove();
|
|
1007
1499
|
host = null;
|
|
1008
1500
|
root = null;
|
|
@@ -1017,6 +1509,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1017
1509
|
teardownAutoCapture = null;
|
|
1018
1510
|
friction = null;
|
|
1019
1511
|
changes = null;
|
|
1512
|
+
skeleton = null;
|
|
1020
1513
|
constructor(opts) {
|
|
1021
1514
|
super("sdk_web", opts);
|
|
1022
1515
|
this.anonymousId = resolveAnonymousId();
|
|
@@ -1034,12 +1527,19 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1034
1527
|
onFriction: (signal) => this.track(`$${signal.kind}`, signal.properties)
|
|
1035
1528
|
});
|
|
1036
1529
|
}
|
|
1530
|
+
if (opts.skeleton !== false) {
|
|
1531
|
+
this.skeleton = installSkeletonCapture({
|
|
1532
|
+
onSkeleton: (skeleton) => this.track("$page_skeleton", { ...skeleton })
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1037
1535
|
if (opts.autoCapture !== false) {
|
|
1038
1536
|
this.teardownAutoCapture = installAutoCapture({
|
|
1039
1537
|
onNavigate: (kind) => {
|
|
1040
1538
|
this.page(void 0, { navigation: kind });
|
|
1041
|
-
|
|
1539
|
+
const path = typeof location !== "undefined" ? location.pathname : "/";
|
|
1540
|
+
this.friction?.onNavigate(path);
|
|
1042
1541
|
this.changes?.onNavigate();
|
|
1542
|
+
this.skeleton?.onNavigate(path);
|
|
1043
1543
|
}
|
|
1044
1544
|
});
|
|
1045
1545
|
}
|
|
@@ -1071,6 +1571,10 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1071
1571
|
super.group(accountKey, traits);
|
|
1072
1572
|
this.changes?.onNavigate();
|
|
1073
1573
|
}
|
|
1574
|
+
track(name, properties, overrides) {
|
|
1575
|
+
super.track(name, properties, overrides);
|
|
1576
|
+
if (!name.startsWith("$")) this.changes?.onEvent(name);
|
|
1577
|
+
}
|
|
1074
1578
|
reset() {
|
|
1075
1579
|
this.userId = null;
|
|
1076
1580
|
this.accountKey = null;
|
|
@@ -1078,6 +1582,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1078
1582
|
localStorage.removeItem(ANON_STORAGE_KEY);
|
|
1079
1583
|
} catch {
|
|
1080
1584
|
}
|
|
1585
|
+
clearSessionId();
|
|
1081
1586
|
this.anonymousId = resolveAnonymousId();
|
|
1082
1587
|
this.changes?.onNavigate();
|
|
1083
1588
|
}
|
|
@@ -1089,6 +1594,8 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1089
1594
|
this.friction = null;
|
|
1090
1595
|
this.changes?.teardown();
|
|
1091
1596
|
this.changes = null;
|
|
1597
|
+
this.skeleton?.teardown();
|
|
1598
|
+
this.skeleton = null;
|
|
1092
1599
|
await super.shutdown();
|
|
1093
1600
|
}
|
|
1094
1601
|
environmentContext() {
|