@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.cjs
CHANGED
|
@@ -41,6 +41,7 @@ var HoldyCore = class {
|
|
|
41
41
|
flushAt;
|
|
42
42
|
debug;
|
|
43
43
|
source;
|
|
44
|
+
release;
|
|
44
45
|
queue = [];
|
|
45
46
|
timer = null;
|
|
46
47
|
flushing = null;
|
|
@@ -57,6 +58,7 @@ var HoldyCore = class {
|
|
|
57
58
|
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
58
59
|
this.flushAt = Math.max(1, opts.flushAt ?? DEFAULT_FLUSH_AT);
|
|
59
60
|
this.debug = opts.debug ?? false;
|
|
61
|
+
this.release = typeof opts.release === "string" && opts.release.trim() ? opts.release.trim().slice(0, 64) : null;
|
|
60
62
|
const intervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
61
63
|
if (intervalMs > 0) {
|
|
62
64
|
this.timer = setInterval(() => void this.flush(), intervalMs);
|
|
@@ -73,6 +75,7 @@ var HoldyCore = class {
|
|
|
73
75
|
const occurredAt = overrides?.occurredAt ? new Date(overrides.occurredAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
74
76
|
const context = {
|
|
75
77
|
...this.environmentContext(),
|
|
78
|
+
...this.release ? { release: this.release } : {},
|
|
76
79
|
...overrides?.context ?? {}
|
|
77
80
|
};
|
|
78
81
|
if (accountKey) context.account_external_id = accountKey;
|
|
@@ -374,11 +377,14 @@ var NavLoopDetector = class {
|
|
|
374
377
|
return null;
|
|
375
378
|
}
|
|
376
379
|
};
|
|
380
|
+
function tagOf(el) {
|
|
381
|
+
return typeof el?.tagName === "string" ? el.tagName.toLowerCase() : "";
|
|
382
|
+
}
|
|
377
383
|
function resolveInteractive(el) {
|
|
378
384
|
if (!el) return null;
|
|
379
385
|
const viaClosest = el.closest?.(INTERACTIVE_SELECTOR) ?? null;
|
|
380
386
|
if (viaClosest) return viaClosest;
|
|
381
|
-
const tag = el
|
|
387
|
+
const tag = tagOf(el);
|
|
382
388
|
if (INTERACTIVE_TAGS.has(tag)) return el;
|
|
383
389
|
const role = el.getAttribute("role");
|
|
384
390
|
if (role === "button" || role === "link" || role === "menuitem" || role === "tab") return el;
|
|
@@ -390,7 +396,7 @@ function isDeadClickTarget(el) {
|
|
|
390
396
|
if (target.disabled === true) return true;
|
|
391
397
|
if (target.getAttribute("disabled") !== null) return true;
|
|
392
398
|
if (target.getAttribute("aria-disabled") === "true") return true;
|
|
393
|
-
const tag = target
|
|
399
|
+
const tag = tagOf(target);
|
|
394
400
|
if (tag === "a") {
|
|
395
401
|
const href = target.getAttribute("href");
|
|
396
402
|
if (!href || href === "#" || href.startsWith("javascript:")) return true;
|
|
@@ -400,7 +406,7 @@ function isDeadClickTarget(el) {
|
|
|
400
406
|
function describeElement(el) {
|
|
401
407
|
const target = resolveInteractive(el) ?? el;
|
|
402
408
|
if (!target) return {};
|
|
403
|
-
const out = { element: target
|
|
409
|
+
const out = { element: tagOf(target) };
|
|
404
410
|
if (target.id) out.element_id = target.id;
|
|
405
411
|
const role = target.getAttribute("role");
|
|
406
412
|
if (role) out.element_role = role;
|
|
@@ -428,6 +434,22 @@ function errorMessage(reason) {
|
|
|
428
434
|
return String(reason);
|
|
429
435
|
}
|
|
430
436
|
}
|
|
437
|
+
var STACK_MAX_FRAMES = 8;
|
|
438
|
+
var STACK_MAX_FRAME_CHARS = 200;
|
|
439
|
+
var STACK_FRAME = /^at\s|^\S*@\S+:\d+(?::\d+)?$/;
|
|
440
|
+
function sanitizeStack(stack) {
|
|
441
|
+
if (typeof stack !== "string" || !stack) return null;
|
|
442
|
+
const frames = stack.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && STACK_FRAME.test(line)).slice(0, STACK_MAX_FRAMES).map(
|
|
443
|
+
(line) => line.replace(/(https?:\/\/[^\s)?#]+)[?#][^\s)]*?(?=(?::\d+){0,2}\)?$)/, "$1").slice(0, STACK_MAX_FRAME_CHARS)
|
|
444
|
+
);
|
|
445
|
+
return frames.length > 0 ? frames.join("\n") : null;
|
|
446
|
+
}
|
|
447
|
+
function withStack(stack) {
|
|
448
|
+
return stack ? { stack } : {};
|
|
449
|
+
}
|
|
450
|
+
function stackOf(reason) {
|
|
451
|
+
return typeof reason === "object" && reason !== null ? sanitizeStack(reason.stack) : null;
|
|
452
|
+
}
|
|
431
453
|
function isAbortError(err) {
|
|
432
454
|
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
433
455
|
}
|
|
@@ -505,13 +527,19 @@ function installFrictionCapture(opts) {
|
|
|
505
527
|
emit("js_error", {
|
|
506
528
|
message: String(ev.message ?? "").slice(0, 200),
|
|
507
529
|
...ev.filename ? { source: stripRequestUrl(ev.filename, location.href) } : {},
|
|
508
|
-
...ev.lineno ? { line: ev.lineno } : {}
|
|
530
|
+
...ev.lineno ? { line: ev.lineno } : {},
|
|
531
|
+
...ev.colno ? { column: ev.colno } : {},
|
|
532
|
+
...withStack(stackOf(ev.error))
|
|
509
533
|
});
|
|
510
534
|
};
|
|
511
535
|
window.addEventListener("error", onError);
|
|
512
536
|
cleanups.push(() => window.removeEventListener("error", onError));
|
|
513
537
|
const onRejection = (ev) => {
|
|
514
|
-
emit("js_error", {
|
|
538
|
+
emit("js_error", {
|
|
539
|
+
message: errorMessage(ev.reason).slice(0, 200),
|
|
540
|
+
unhandled_rejection: true,
|
|
541
|
+
...withStack(stackOf(ev.reason))
|
|
542
|
+
});
|
|
515
543
|
};
|
|
516
544
|
window.addEventListener("unhandledrejection", onRejection);
|
|
517
545
|
cleanups.push(() => window.removeEventListener("unhandledrejection", onRejection));
|
|
@@ -593,11 +621,11 @@ function installFrictionCapture(opts) {
|
|
|
593
621
|
}
|
|
594
622
|
const onFocusIn = (ev) => {
|
|
595
623
|
const el = ev.target;
|
|
596
|
-
if (!el || !FORM_CONTROL_TAGS.has(el
|
|
624
|
+
if (!el || !FORM_CONTROL_TAGS.has(tagOf(el))) return;
|
|
597
625
|
const form = el.form;
|
|
598
626
|
if (!form) return;
|
|
599
627
|
const entry = touched.get(form) ?? { form: formKey(form), fields: /* @__PURE__ */ new Set() };
|
|
600
|
-
const field = el.name || el.id || el
|
|
628
|
+
const field = el.name || el.id || tagOf(el);
|
|
601
629
|
entry.fields.add(field);
|
|
602
630
|
touched.set(form, entry);
|
|
603
631
|
};
|
|
@@ -630,6 +658,200 @@ function installFrictionCapture(opts) {
|
|
|
630
658
|
};
|
|
631
659
|
}
|
|
632
660
|
|
|
661
|
+
// src/theme.ts
|
|
662
|
+
var THEME_BUTTON_SELECTOR = 'button[type="submit"],[data-testid*="primary"],.btn-primary,button,[role="button"],a.button';
|
|
663
|
+
var THEME_MAX_BUTTONS = 40;
|
|
664
|
+
var THEME_KEYS = ["font", "size", "fg", "bg", "accent", "accentFg", "radius"];
|
|
665
|
+
var COLOR = /^(rgba?\([\d.,\s%/]{1,40}\)|#[0-9a-fA-F]{3,8}|(?:color|oklch|oklab|hsla?|lab|lch)\([^()]{1,60}\))$/;
|
|
666
|
+
var MIN_ACCENT_CONTRAST = 60;
|
|
667
|
+
var RGB = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/;
|
|
668
|
+
function colorDistance(a, b) {
|
|
669
|
+
const x = RGB.exec(a);
|
|
670
|
+
const y = b ? RGB.exec(b) : null;
|
|
671
|
+
if (!x) return MIN_ACCENT_CONTRAST + 1;
|
|
672
|
+
if (!y) return MIN_ACCENT_CONTRAST + 1;
|
|
673
|
+
return Math.abs(+x[1] - +y[1]) + Math.abs(+x[2] - +y[2]) + Math.abs(+x[3] - +y[3]);
|
|
674
|
+
}
|
|
675
|
+
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*\))$/;
|
|
676
|
+
function themeVarName(key) {
|
|
677
|
+
return `--holdy-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
678
|
+
}
|
|
679
|
+
function cleanColor(value) {
|
|
680
|
+
const v = (value ?? "").trim();
|
|
681
|
+
return v && v.length <= 80 && !TRANSPARENT.test(v) && COLOR.test(v) ? v : void 0;
|
|
682
|
+
}
|
|
683
|
+
function cleanFont(value) {
|
|
684
|
+
const first = (value ?? "").split(",")[0].trim().replace(/^["']|["']$/g, "").trim();
|
|
685
|
+
return /^[\w\s-]{1,60}$/.test(first) ? first : void 0;
|
|
686
|
+
}
|
|
687
|
+
function cleanPx(value, min, max) {
|
|
688
|
+
const m = /^(\d+(?:\.\d+)?)px/.exec((value ?? "").trim());
|
|
689
|
+
const n = m ? Math.round(Number(m[1])) : NaN;
|
|
690
|
+
return n >= min && n <= max ? `${n}px` : void 0;
|
|
691
|
+
}
|
|
692
|
+
function visible(el) {
|
|
693
|
+
const r = el.getBoundingClientRect?.();
|
|
694
|
+
return !r || r.width > 0 && r.height > 0;
|
|
695
|
+
}
|
|
696
|
+
function readTheme(doc, getStyle, anchor = null) {
|
|
697
|
+
const body = doc.body;
|
|
698
|
+
if (!body) return null;
|
|
699
|
+
const bodyStyle = getStyle(body);
|
|
700
|
+
const theme = {};
|
|
701
|
+
const put = (key, value) => {
|
|
702
|
+
if (value) theme[key] = value;
|
|
703
|
+
};
|
|
704
|
+
put("font", cleanFont(bodyStyle.getPropertyValue("font-family")));
|
|
705
|
+
put("size", cleanPx(bodyStyle.getPropertyValue("font-size"), 10, 20));
|
|
706
|
+
put("fg", cleanColor(bodyStyle.getPropertyValue("color")));
|
|
707
|
+
put("bg", cleanColor(bodyStyle.getPropertyValue("background-color")) ?? cleanColor(getStyle(doc.documentElement).getPropertyValue("background-color")));
|
|
708
|
+
const candidates = doc.querySelectorAll(THEME_BUTTON_SELECTOR);
|
|
709
|
+
let button = null;
|
|
710
|
+
let best = MIN_ACCENT_CONTRAST;
|
|
711
|
+
for (let i = 0; i < candidates.length && i < THEME_MAX_BUTTONS; i++) {
|
|
712
|
+
const el = candidates[i];
|
|
713
|
+
if (!visible(el)) continue;
|
|
714
|
+
const s = getStyle(el);
|
|
715
|
+
const accent = cleanColor(s.getPropertyValue("background-color"));
|
|
716
|
+
if (!accent || accent === theme.bg) continue;
|
|
717
|
+
const contrast = colorDistance(accent, theme.bg);
|
|
718
|
+
if (contrast <= best) continue;
|
|
719
|
+
best = contrast;
|
|
720
|
+
put("accent", accent);
|
|
721
|
+
put("accentFg", cleanColor(s.getPropertyValue("color")));
|
|
722
|
+
button = el;
|
|
723
|
+
}
|
|
724
|
+
const radiusSource = anchor && anchor.tagName.toLowerCase() === "button" ? anchor : button;
|
|
725
|
+
if (radiusSource) put("radius", cleanPx(getStyle(radiusSource).getPropertyValue("border-top-left-radius"), 0, 24));
|
|
726
|
+
return Object.keys(theme).length > 0 ? theme : null;
|
|
727
|
+
}
|
|
728
|
+
function themeVars(theme) {
|
|
729
|
+
const out = [];
|
|
730
|
+
for (const key of THEME_KEYS) {
|
|
731
|
+
const value = theme?.[key];
|
|
732
|
+
if (value) out.push([themeVarName(key), value]);
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
function applyThemeVars(el, theme) {
|
|
737
|
+
for (const key of THEME_KEYS) el.style.removeProperty(themeVarName(key));
|
|
738
|
+
for (const [name, value] of themeVars(theme)) el.style.setProperty(name, value);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/skeleton.ts
|
|
742
|
+
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]';
|
|
743
|
+
var SKELETON_MAX_ELEMENTS = 80;
|
|
744
|
+
var SKELETON_DELAY_MS = 1500;
|
|
745
|
+
var SENT_STORAGE_KEY = "holdy_skeletons";
|
|
746
|
+
var SENT_STORAGE_MAX = 200;
|
|
747
|
+
function fnv1a(input) {
|
|
748
|
+
let hash = 2166136261;
|
|
749
|
+
for (let i = 0; i < input.length; i++) {
|
|
750
|
+
hash ^= input.charCodeAt(i);
|
|
751
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
752
|
+
}
|
|
753
|
+
return hash.toString(16).padStart(8, "0");
|
|
754
|
+
}
|
|
755
|
+
function attr(el, name, max) {
|
|
756
|
+
const value = el.getAttribute(name);
|
|
757
|
+
if (!value) return void 0;
|
|
758
|
+
const trimmed = value.replace(/\s+/g, " ").trim().slice(0, max);
|
|
759
|
+
return trimmed || void 0;
|
|
760
|
+
}
|
|
761
|
+
function isVisible(el) {
|
|
762
|
+
if (!el.getBoundingClientRect) return true;
|
|
763
|
+
const rect = el.getBoundingClientRect();
|
|
764
|
+
return rect.width > 0 && rect.height > 0;
|
|
765
|
+
}
|
|
766
|
+
function relativeBox(rect, viewport) {
|
|
767
|
+
const pct = (value, base) => base > 0 ? Math.max(0, Math.min(100, Math.round(value / base * 100))) : 0;
|
|
768
|
+
return [pct(rect.x, viewport.width), pct(rect.y + viewport.scrollY, viewport.height), pct(rect.width, viewport.width), pct(rect.height, viewport.height)];
|
|
769
|
+
}
|
|
770
|
+
function describeSkeletonElement(el, viewport) {
|
|
771
|
+
const out = { t: el.tagName.toLowerCase() };
|
|
772
|
+
const role = attr(el, "role", 40);
|
|
773
|
+
if (role) out.r = role;
|
|
774
|
+
const testid = attr(el, "data-testid", 80);
|
|
775
|
+
if (testid) out.id = testid;
|
|
776
|
+
const name = attr(el, "name", 80);
|
|
777
|
+
if (name) out.n = name;
|
|
778
|
+
const label = attr(el, "aria-label", 80);
|
|
779
|
+
if (label) out.l = label;
|
|
780
|
+
if (out.t === "input" || out.t === "button") {
|
|
781
|
+
const type = attr(el, "type", 20);
|
|
782
|
+
if (type) out.k = type.toLowerCase();
|
|
783
|
+
}
|
|
784
|
+
if (viewport && el.getBoundingClientRect) out.b = relativeBox(el.getBoundingClientRect(), viewport);
|
|
785
|
+
return out;
|
|
786
|
+
}
|
|
787
|
+
function collectSkeleton(root, path, viewport, theme = null) {
|
|
788
|
+
const found = root.querySelectorAll(SKELETON_SELECTOR);
|
|
789
|
+
const visible2 = [];
|
|
790
|
+
for (let i = 0; i < found.length; i++) {
|
|
791
|
+
const el = found[i];
|
|
792
|
+
if (el.tagName.toLowerCase() === "input" && (el.getAttribute("type") ?? "").toLowerCase() === "hidden") continue;
|
|
793
|
+
if (!isVisible(el)) continue;
|
|
794
|
+
visible2.push(el);
|
|
795
|
+
}
|
|
796
|
+
const elements = visible2.slice(0, SKELETON_MAX_ELEMENTS).map((el) => describeSkeletonElement(el, viewport));
|
|
797
|
+
const structural = elements.map(({ b: _b, ...rest }) => rest);
|
|
798
|
+
return { path, hash: fnv1a(JSON.stringify(structural)), count: visible2.length, elements, ...theme ? { theme } : {} };
|
|
799
|
+
}
|
|
800
|
+
function loadSent() {
|
|
801
|
+
try {
|
|
802
|
+
const raw = sessionStorage.getItem(SENT_STORAGE_KEY);
|
|
803
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
804
|
+
return new Set(Array.isArray(list) ? list.filter((x) => typeof x === "string") : []);
|
|
805
|
+
} catch {
|
|
806
|
+
return /* @__PURE__ */ new Set();
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function saveSent(sent) {
|
|
810
|
+
try {
|
|
811
|
+
sessionStorage.setItem(SENT_STORAGE_KEY, JSON.stringify([...sent].slice(-SENT_STORAGE_MAX)));
|
|
812
|
+
} catch {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function installSkeletonCapture(opts) {
|
|
817
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
818
|
+
return { onNavigate: () => void 0, teardown: () => void 0 };
|
|
819
|
+
}
|
|
820
|
+
const delay = opts.delayMs ?? SKELETON_DELAY_MS;
|
|
821
|
+
const sent = loadSent();
|
|
822
|
+
let pending = null;
|
|
823
|
+
const capture = (path) => {
|
|
824
|
+
pending = null;
|
|
825
|
+
if (location.pathname !== path) return;
|
|
826
|
+
try {
|
|
827
|
+
const viewport = {
|
|
828
|
+
width: window.innerWidth,
|
|
829
|
+
height: Math.max(document.documentElement.scrollHeight, window.innerHeight),
|
|
830
|
+
scrollY: window.scrollY
|
|
831
|
+
};
|
|
832
|
+
const theme = readTheme(document, (el) => window.getComputedStyle(el));
|
|
833
|
+
const skeleton = collectSkeleton(document, path, viewport, theme);
|
|
834
|
+
const key = `${path}:${skeleton.hash}`;
|
|
835
|
+
if (sent.has(key)) return;
|
|
836
|
+
sent.add(key);
|
|
837
|
+
saveSent(sent);
|
|
838
|
+
opts.onSkeleton(skeleton);
|
|
839
|
+
} catch {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
return {
|
|
844
|
+
onNavigate: (path) => {
|
|
845
|
+
if (pending) clearTimeout(pending);
|
|
846
|
+
pending = setTimeout(() => capture(path), delay);
|
|
847
|
+
},
|
|
848
|
+
teardown: () => {
|
|
849
|
+
if (pending) clearTimeout(pending);
|
|
850
|
+
pending = null;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
633
855
|
// src/web-context.ts
|
|
634
856
|
var UTM_KEYS = [
|
|
635
857
|
"utm_source",
|
|
@@ -694,6 +916,12 @@ function randomId() {
|
|
|
694
916
|
return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
695
917
|
}
|
|
696
918
|
}
|
|
919
|
+
function clearSessionId() {
|
|
920
|
+
try {
|
|
921
|
+
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
924
|
+
}
|
|
697
925
|
function resolveSessionId() {
|
|
698
926
|
const existing = readStorage("session", SESSION_STORAGE_KEY);
|
|
699
927
|
if (existing) return existing;
|
|
@@ -747,8 +975,35 @@ function buildWebContext() {
|
|
|
747
975
|
};
|
|
748
976
|
}
|
|
749
977
|
|
|
978
|
+
// src/tip-css.ts
|
|
979
|
+
var TIP_CSS = `:host{all:initial;--h-bg:#111;--h-fg:#fff;--h-accent:#fff;--h-accent-fg:#111;--h-border:transparent}
|
|
980
|
+
@media (prefers-color-scheme:dark){:host{--h-bg:#fff;--h-fg:#111;--h-accent:#111;--h-accent-fg:#fff}}
|
|
981
|
+
.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}
|
|
982
|
+
.tip p{margin:0;flex:1}
|
|
983
|
+
.tip button{all:unset;cursor:pointer;opacity:.7;font-size:15px;line-height:1;color:inherit}
|
|
984
|
+
.tip button:hover,.tip button:focus-visible{opacity:1}
|
|
985
|
+
.ask{flex-direction:column;width:300px;max-width:calc(100vw - 16px);gap:8px}
|
|
986
|
+
.ask .head{display:flex;gap:10px;align-items:flex-start;width:100%}
|
|
987
|
+
.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}
|
|
988
|
+
.ask textarea:focus-visible{outline:2px solid color-mix(in srgb,var(--holdy-fg,var(--h-fg)) 45%,transparent)}
|
|
989
|
+
.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}
|
|
990
|
+
.ask .send[disabled]{opacity:.4;cursor:default}
|
|
991
|
+
.list{flex-direction:column;width:300px;max-width:calc(100vw - 16px);gap:8px}
|
|
992
|
+
.list .head{display:flex;gap:10px;align-items:flex-start;width:100%}
|
|
993
|
+
.list .head p{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
|
|
994
|
+
.list .count{opacity:.7;font-size:12px;white-space:nowrap}
|
|
995
|
+
.list ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;width:100%}
|
|
996
|
+
.list li{display:flex;gap:8px;align-items:flex-start}
|
|
997
|
+
.list li.done{opacity:.55;text-decoration:line-through}
|
|
998
|
+
.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}
|
|
999
|
+
.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))}
|
|
1000
|
+
.list.folded ul{display:none}
|
|
1001
|
+
.list.folded{width:auto}
|
|
1002
|
+
.preview{flex:none;order:-1;width:100%;font-size:11px;opacity:.7}
|
|
1003
|
+
.tip:has(.preview){flex-wrap:wrap;row-gap:4px}`;
|
|
1004
|
+
|
|
750
1005
|
// src/arm.ts
|
|
751
|
-
function
|
|
1006
|
+
function fnv1a2(input) {
|
|
752
1007
|
let hash = 2166136261;
|
|
753
1008
|
for (let i = 0; i < input.length; i++) {
|
|
754
1009
|
hash ^= input.charCodeAt(i);
|
|
@@ -757,7 +1012,7 @@ function fnv1a(input) {
|
|
|
757
1012
|
return hash >>> 0;
|
|
758
1013
|
}
|
|
759
1014
|
function changeBucket(changeId, accountKey) {
|
|
760
|
-
return
|
|
1015
|
+
return fnv1a2(`${changeId}:${accountKey}`) % 100;
|
|
761
1016
|
}
|
|
762
1017
|
function changeArm(changeId, accountKey, holdoutPct) {
|
|
763
1018
|
return changeBucket(changeId, accountKey) < holdoutPct ? "holdout" : "change";
|
|
@@ -776,12 +1031,16 @@ var SCAN_DEBOUNCE_MS = 250;
|
|
|
776
1031
|
var SEEN_KEY = "holdy_changes_seen";
|
|
777
1032
|
var DISMISSED_KEY = "holdy_changes_dismissed";
|
|
778
1033
|
var TIP_GAP_PX = 8;
|
|
779
|
-
var
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1034
|
+
var ANSWERED_KEY = "holdy_questions_answered";
|
|
1035
|
+
var FOLDED_KEY = "holdy_checklist_folded";
|
|
1036
|
+
var COMPLETED_KEY = "holdy_checklist_completed";
|
|
1037
|
+
var PROGRESS_REFRESH_MS = 60 * 1e3;
|
|
1038
|
+
var VOICE_EVENT = "$voice";
|
|
1039
|
+
var MAX_ANSWER_CHARS = 1e3;
|
|
1040
|
+
var THANKS_MS = 1600;
|
|
1041
|
+
var PREVIEW_PARAM = "holdy_preview";
|
|
1042
|
+
var PREVIEW_KEY = "holdy_preview";
|
|
1043
|
+
var PREVIEW_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
785
1044
|
function normalizeText(text) {
|
|
786
1045
|
return text.replace(/\s+/g, " ").trim();
|
|
787
1046
|
}
|
|
@@ -794,13 +1053,42 @@ function isAnchor(value) {
|
|
|
794
1053
|
if (a?.kind === "page") return true;
|
|
795
1054
|
return (a?.kind === "selector" || a?.kind === "text") && typeof a.value === "string" && a.value.length > 0;
|
|
796
1055
|
}
|
|
1056
|
+
function isItem(value) {
|
|
1057
|
+
const i = value;
|
|
1058
|
+
return typeof i?.definition_name === "string" && typeof i.label === "string" && Array.isArray(i.events) && i.events.every((e) => typeof e === "string");
|
|
1059
|
+
}
|
|
797
1060
|
function isChange(value) {
|
|
798
1061
|
const c = value;
|
|
799
|
-
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";
|
|
1062
|
+
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";
|
|
800
1063
|
}
|
|
801
|
-
function changesForPage(changes, path, accountKey) {
|
|
802
|
-
|
|
803
|
-
|
|
1064
|
+
function changesForPage(changes, path, accountKey, previewId = null) {
|
|
1065
|
+
return changes.filter((c) => c.kind === "checklist" || matchesPathTemplate(c.path, path)).flatMap((change) => {
|
|
1066
|
+
if (change.id === previewId) return [{ change, arm: "change", preview: true }];
|
|
1067
|
+
if (!accountKey) return [];
|
|
1068
|
+
return [{ change, arm: changeArm(change.id, accountKey, change.holdout_pct), preview: false }];
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
function resolvePreviewId(search, store) {
|
|
1072
|
+
let asked = null;
|
|
1073
|
+
try {
|
|
1074
|
+
asked = new URLSearchParams(search).get(PREVIEW_PARAM);
|
|
1075
|
+
} catch {
|
|
1076
|
+
asked = null;
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
if (asked !== null) {
|
|
1080
|
+
if (PREVIEW_ID.test(asked)) {
|
|
1081
|
+
store?.setItem(PREVIEW_KEY, asked);
|
|
1082
|
+
return asked;
|
|
1083
|
+
}
|
|
1084
|
+
store?.removeItem(PREVIEW_KEY);
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
const kept = store?.getItem(PREVIEW_KEY) ?? null;
|
|
1088
|
+
return kept && PREVIEW_ID.test(kept) ? kept : null;
|
|
1089
|
+
} catch {
|
|
1090
|
+
return asked !== null && PREVIEW_ID.test(asked) ? asked : null;
|
|
1091
|
+
}
|
|
804
1092
|
}
|
|
805
1093
|
function storage(kind) {
|
|
806
1094
|
try {
|
|
@@ -862,7 +1150,7 @@ function findTextNode(el, match) {
|
|
|
862
1150
|
}
|
|
863
1151
|
function installProductChanges(opts) {
|
|
864
1152
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
865
|
-
return { onNavigate: () => void 0, refresh: async () => void 0, teardown: () => void 0 };
|
|
1153
|
+
return { onNavigate: () => void 0, onEvent: () => void 0, refresh: async () => void 0, teardown: () => void 0 };
|
|
866
1154
|
}
|
|
867
1155
|
let changes = [];
|
|
868
1156
|
let lastFetch = 0;
|
|
@@ -871,6 +1159,11 @@ function installProductChanges(opts) {
|
|
|
871
1159
|
let host = null;
|
|
872
1160
|
const copies = /* @__PURE__ */ new Map();
|
|
873
1161
|
const tips = /* @__PURE__ */ new Map();
|
|
1162
|
+
const progress = /* @__PURE__ */ new Map();
|
|
1163
|
+
const progressAt = /* @__PURE__ */ new Map();
|
|
1164
|
+
const lists = /* @__PURE__ */ new Map();
|
|
1165
|
+
let fetchedForKey = null;
|
|
1166
|
+
let previewId = resolvePreviewId(location.search, storage("session"));
|
|
874
1167
|
const log = (message) => {
|
|
875
1168
|
if (opts.debug) console.warn(`[holdy] ${message}`);
|
|
876
1169
|
};
|
|
@@ -920,21 +1213,66 @@ function installProductChanges(opts) {
|
|
|
920
1213
|
document.body.appendChild(host);
|
|
921
1214
|
return root;
|
|
922
1215
|
};
|
|
923
|
-
|
|
1216
|
+
let dismissedPreview = false;
|
|
1217
|
+
const previewTag = () => {
|
|
1218
|
+
const tag = document.createElement("span");
|
|
1219
|
+
tag.className = "preview";
|
|
1220
|
+
tag.textContent = "Pr\xE9via, s\xF3 voc\xEA v\xEA";
|
|
1221
|
+
return tag;
|
|
1222
|
+
};
|
|
1223
|
+
const showTip = (anchor, change, preview) => {
|
|
924
1224
|
if (tips.has(change.id)) return true;
|
|
925
|
-
if (readSet(storage("local"), DISMISSED_KEY).has(change.id)) return true;
|
|
1225
|
+
if (!preview && readSet(storage("local"), DISMISSED_KEY).has(change.id)) return true;
|
|
1226
|
+
const asking = change.kind === "question";
|
|
1227
|
+
if (!preview && asking && readSet(storage("local"), ANSWERED_KEY).has(change.id)) return true;
|
|
926
1228
|
const shadow = ensureRoot();
|
|
927
1229
|
if (!shadow) return false;
|
|
1230
|
+
if (host) applyThemeVars(host, readTheme(document, (el) => window.getComputedStyle(el), anchor));
|
|
928
1231
|
const card = document.createElement("div");
|
|
929
|
-
card.className = "tip";
|
|
930
|
-
card.setAttribute("role", "tooltip");
|
|
1232
|
+
card.className = asking ? "tip ask" : "tip";
|
|
1233
|
+
card.setAttribute("role", asking ? "dialog" : "tooltip");
|
|
931
1234
|
const text = document.createElement("p");
|
|
932
1235
|
text.textContent = change.content.text;
|
|
933
1236
|
const close = document.createElement("button");
|
|
934
1237
|
close.type = "button";
|
|
935
1238
|
close.setAttribute("aria-label", "Fechar");
|
|
936
1239
|
close.textContent = "\xD7";
|
|
937
|
-
|
|
1240
|
+
if (asking) {
|
|
1241
|
+
const head = document.createElement("div");
|
|
1242
|
+
head.className = "head";
|
|
1243
|
+
head.append(text, close);
|
|
1244
|
+
const input = document.createElement("textarea");
|
|
1245
|
+
input.maxLength = MAX_ANSWER_CHARS;
|
|
1246
|
+
input.setAttribute("aria-label", change.content.text);
|
|
1247
|
+
const send = document.createElement("button");
|
|
1248
|
+
send.type = "button";
|
|
1249
|
+
send.className = "send";
|
|
1250
|
+
send.textContent = "Enviar";
|
|
1251
|
+
send.disabled = true;
|
|
1252
|
+
input.addEventListener("input", () => {
|
|
1253
|
+
send.disabled = input.value.trim().length === 0;
|
|
1254
|
+
});
|
|
1255
|
+
send.addEventListener("click", () => {
|
|
1256
|
+
const answer = input.value.trim().slice(0, MAX_ANSWER_CHARS);
|
|
1257
|
+
if (!answer) return;
|
|
1258
|
+
if (!preview) {
|
|
1259
|
+
addToSet(storage("local"), ANSWERED_KEY, change.id);
|
|
1260
|
+
opts.track(VOICE_EVENT, {
|
|
1261
|
+
question: change.content.text,
|
|
1262
|
+
text: answer,
|
|
1263
|
+
action_plan_id: change.plan_id,
|
|
1264
|
+
change_id: change.id,
|
|
1265
|
+
path: location.pathname
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
card.replaceChildren(Object.assign(document.createElement("p"), { textContent: "Obrigado pela resposta." }));
|
|
1269
|
+
setTimeout(remove, THANKS_MS);
|
|
1270
|
+
});
|
|
1271
|
+
card.append(head, input, send);
|
|
1272
|
+
} else {
|
|
1273
|
+
card.append(text, close);
|
|
1274
|
+
}
|
|
1275
|
+
if (preview) card.prepend(previewTag());
|
|
938
1276
|
card.style.top = "-9999px";
|
|
939
1277
|
card.style.left = "-9999px";
|
|
940
1278
|
shadow.appendChild(card);
|
|
@@ -964,6 +1302,11 @@ function installProductChanges(opts) {
|
|
|
964
1302
|
card.style.left = `${Math.max(8, left)}px`;
|
|
965
1303
|
}
|
|
966
1304
|
close.addEventListener("click", () => {
|
|
1305
|
+
if (preview) {
|
|
1306
|
+
dismissedPreview = true;
|
|
1307
|
+
remove();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
967
1310
|
addToSet(storage("local"), DISMISSED_KEY, change.id);
|
|
968
1311
|
opts.track("$change_dismissed", { change_id: change.id, kind: change.kind });
|
|
969
1312
|
remove();
|
|
@@ -974,20 +1317,143 @@ function installProductChanges(opts) {
|
|
|
974
1317
|
window.addEventListener("resize", position);
|
|
975
1318
|
return true;
|
|
976
1319
|
};
|
|
1320
|
+
const doneOf = (change) => progress.get(change.id) ?? /* @__PURE__ */ new Set();
|
|
1321
|
+
const allDone = (change) => {
|
|
1322
|
+
const done = doneOf(change);
|
|
1323
|
+
return (change.content.items ?? []).every((i) => done.has(i.definition_name));
|
|
1324
|
+
};
|
|
1325
|
+
const fetchProgress = async (change) => {
|
|
1326
|
+
const account = opts.getAccountKey();
|
|
1327
|
+
if (!account) return;
|
|
1328
|
+
const last = progressAt.get(change.id) ?? 0;
|
|
1329
|
+
if (Date.now() - last < PROGRESS_REFRESH_MS) return;
|
|
1330
|
+
progressAt.set(change.id, Date.now());
|
|
1331
|
+
try {
|
|
1332
|
+
const res = await fetch(
|
|
1333
|
+
`${opts.baseUrl}/runtime/checklist?change_id=${encodeURIComponent(change.id)}&account=${encodeURIComponent(account)}`,
|
|
1334
|
+
{ headers: { Authorization: `Bearer ${opts.apiKey}` } }
|
|
1335
|
+
);
|
|
1336
|
+
if (!res.ok) return;
|
|
1337
|
+
const body = await res.json();
|
|
1338
|
+
const done = Array.isArray(body.done) ? body.done.filter((d) => typeof d === "string") : [];
|
|
1339
|
+
const set = doneOf(change);
|
|
1340
|
+
for (const d of done) set.add(d);
|
|
1341
|
+
progress.set(change.id, set);
|
|
1342
|
+
lists.get(change.id)?.();
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
log(`checklist progress error: ${err.message}`);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
const showChecklist = (change, preview) => {
|
|
1348
|
+
if (lists.has(change.id)) return true;
|
|
1349
|
+
if (!preview && readSet(storage("local"), COMPLETED_KEY).has(change.id)) return true;
|
|
1350
|
+
const shadow = ensureRoot();
|
|
1351
|
+
if (!shadow) return false;
|
|
1352
|
+
const items = change.content.items ?? [];
|
|
1353
|
+
const card = document.createElement("div");
|
|
1354
|
+
card.className = "tip list";
|
|
1355
|
+
card.setAttribute("role", "region");
|
|
1356
|
+
card.setAttribute("aria-label", change.content.text);
|
|
1357
|
+
const head = document.createElement("div");
|
|
1358
|
+
head.className = "head";
|
|
1359
|
+
const title = document.createElement("p");
|
|
1360
|
+
const strong = document.createElement("strong");
|
|
1361
|
+
strong.textContent = change.content.text;
|
|
1362
|
+
const count = document.createElement("span");
|
|
1363
|
+
count.className = "count";
|
|
1364
|
+
title.append(strong, count);
|
|
1365
|
+
const fold = document.createElement("button");
|
|
1366
|
+
fold.type = "button";
|
|
1367
|
+
head.append(title, fold);
|
|
1368
|
+
const list = document.createElement("ul");
|
|
1369
|
+
card.append(head, list);
|
|
1370
|
+
if (preview) card.prepend(previewTag());
|
|
1371
|
+
card.style.position = "fixed";
|
|
1372
|
+
card.style.right = "16px";
|
|
1373
|
+
card.style.bottom = "16px";
|
|
1374
|
+
shadow.appendChild(card);
|
|
1375
|
+
const remove = () => {
|
|
1376
|
+
card.remove();
|
|
1377
|
+
lists.delete(change.id);
|
|
1378
|
+
};
|
|
1379
|
+
const render = () => {
|
|
1380
|
+
const done = doneOf(change);
|
|
1381
|
+
const n = items.filter((i) => done.has(i.definition_name)).length;
|
|
1382
|
+
count.textContent = `${n} de ${items.length}`;
|
|
1383
|
+
const folded = readSet(storage("local"), FOLDED_KEY).has(change.id);
|
|
1384
|
+
card.classList.toggle("folded", folded);
|
|
1385
|
+
fold.textContent = folded ? "+" : "\u2013";
|
|
1386
|
+
fold.setAttribute("aria-label", folded ? "Abrir" : "Recolher");
|
|
1387
|
+
list.replaceChildren(
|
|
1388
|
+
...items.map((item) => {
|
|
1389
|
+
const li = document.createElement("li");
|
|
1390
|
+
const isDone = done.has(item.definition_name);
|
|
1391
|
+
if (isDone) li.className = "done";
|
|
1392
|
+
const mark = document.createElement("span");
|
|
1393
|
+
mark.className = "mark";
|
|
1394
|
+
mark.setAttribute("aria-hidden", "true");
|
|
1395
|
+
mark.textContent = isDone ? "\u2713" : "";
|
|
1396
|
+
const label = document.createElement("span");
|
|
1397
|
+
label.textContent = item.label;
|
|
1398
|
+
li.append(mark, label);
|
|
1399
|
+
return li;
|
|
1400
|
+
})
|
|
1401
|
+
);
|
|
1402
|
+
if (!preview && n === items.length && items.length > 0) {
|
|
1403
|
+
addToSet(storage("local"), COMPLETED_KEY, change.id);
|
|
1404
|
+
opts.track("$checklist_completed", { change_id: change.id });
|
|
1405
|
+
remove();
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
fold.addEventListener("click", () => {
|
|
1409
|
+
const store = storage("local");
|
|
1410
|
+
const set = readSet(store, FOLDED_KEY);
|
|
1411
|
+
if (set.has(change.id)) set.delete(change.id);
|
|
1412
|
+
else set.add(change.id);
|
|
1413
|
+
try {
|
|
1414
|
+
store?.setItem(FOLDED_KEY, JSON.stringify([...set].slice(-200)));
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
render();
|
|
1418
|
+
});
|
|
1419
|
+
lists.set(change.id, render);
|
|
1420
|
+
render();
|
|
1421
|
+
void fetchProgress(change);
|
|
1422
|
+
return true;
|
|
1423
|
+
};
|
|
977
1424
|
const scan = () => {
|
|
978
1425
|
timer = null;
|
|
979
|
-
const active = changesForPage(changes, location.pathname, opts.getAccountKey());
|
|
1426
|
+
const active = changesForPage(changes, location.pathname, opts.getAccountKey(), previewId);
|
|
980
1427
|
const activeIds = new Set(active.map((a) => a.change.id));
|
|
981
1428
|
for (const id of [...copies.keys()]) if (!activeIds.has(id)) restoreCopy(id);
|
|
982
1429
|
for (const [id, remove] of [...tips]) if (!activeIds.has(id)) remove();
|
|
983
|
-
for (const
|
|
1430
|
+
for (const [id, render] of [...lists]) {
|
|
1431
|
+
if (!activeIds.has(id)) {
|
|
1432
|
+
lists.delete(id);
|
|
1433
|
+
render();
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
for (const { change, arm, preview } of active) {
|
|
984
1437
|
const el = findAnchor(change.anchor);
|
|
985
|
-
if (!el && change.anchor.kind !== "page")
|
|
986
|
-
|
|
1438
|
+
if (!el && change.anchor.kind !== "page") {
|
|
1439
|
+
if (preview) log(`preview: \xE2ncora n\xE3o encontrada em ${location.pathname}`);
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
if (preview) {
|
|
1443
|
+
if (change.kind === "checklist") showChecklist(change, true);
|
|
1444
|
+
else if (change.kind === "copy") {
|
|
1445
|
+
if (el) applyCopy(el, change);
|
|
1446
|
+
} else if (!dismissedPreview) showTip(el, change, true);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (change.kind === "checklist") {
|
|
1450
|
+
if (arm === "holdout" || allDone(change) || showChecklist(change, false)) expose(change, arm);
|
|
1451
|
+
if (arm === "change") void fetchProgress(change);
|
|
1452
|
+
} else if (change.kind === "copy") {
|
|
987
1453
|
if (!el) continue;
|
|
988
1454
|
const shown = arm === "change" ? applyCopy(el, change) : eligibleCopyNode(el, change) !== null;
|
|
989
1455
|
if (shown) expose(change, arm);
|
|
990
|
-
} else if (arm === "holdout" || showTip(el, change)) {
|
|
1456
|
+
} else if (arm === "holdout" || showTip(el, change, false)) {
|
|
991
1457
|
expose(change, arm);
|
|
992
1458
|
}
|
|
993
1459
|
}
|
|
@@ -997,8 +1463,14 @@ function installProductChanges(opts) {
|
|
|
997
1463
|
};
|
|
998
1464
|
const refresh = async () => {
|
|
999
1465
|
lastFetch = Date.now();
|
|
1466
|
+
const account = opts.getAccountKey();
|
|
1467
|
+
fetchedForKey = account;
|
|
1468
|
+
const params = new URLSearchParams();
|
|
1469
|
+
if (account) params.set("account", account);
|
|
1470
|
+
if (previewId) params.set("preview", previewId);
|
|
1471
|
+
const qs = params.toString();
|
|
1000
1472
|
try {
|
|
1001
|
-
const res = await fetch(`${opts.baseUrl}/runtime/changes`, {
|
|
1473
|
+
const res = await fetch(`${opts.baseUrl}/runtime/changes${qs ? `?${qs}` : ""}`, {
|
|
1002
1474
|
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
1003
1475
|
});
|
|
1004
1476
|
if (!res.ok) {
|
|
@@ -1023,9 +1495,28 @@ function installProductChanges(opts) {
|
|
|
1023
1495
|
void refresh();
|
|
1024
1496
|
return {
|
|
1025
1497
|
onNavigate: () => {
|
|
1026
|
-
|
|
1498
|
+
const asked = resolvePreviewId(location.search, storage("session"));
|
|
1499
|
+
const previewChanged = asked !== previewId;
|
|
1500
|
+
if (previewChanged) {
|
|
1501
|
+
previewId = asked;
|
|
1502
|
+
dismissedPreview = false;
|
|
1503
|
+
for (const id of [...copies.keys()]) restoreCopy(id);
|
|
1504
|
+
for (const remove of [...tips.values()]) remove();
|
|
1505
|
+
}
|
|
1506
|
+
if (previewChanged || Date.now() - lastFetch > NAV_REFRESH_MS || opts.getAccountKey() !== fetchedForKey) void refresh();
|
|
1027
1507
|
schedule();
|
|
1028
1508
|
},
|
|
1509
|
+
onEvent: (name) => {
|
|
1510
|
+
for (const change of changes) {
|
|
1511
|
+
if (change.kind !== "checklist") continue;
|
|
1512
|
+
const hit = (change.content.items ?? []).filter((i) => i.events.includes(name));
|
|
1513
|
+
if (hit.length === 0) continue;
|
|
1514
|
+
const set = doneOf(change);
|
|
1515
|
+
for (const i of hit) set.add(i.definition_name);
|
|
1516
|
+
progress.set(change.id, set);
|
|
1517
|
+
lists.get(change.id)?.();
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1029
1520
|
refresh,
|
|
1030
1521
|
teardown: () => {
|
|
1031
1522
|
clearInterval(interval);
|
|
@@ -1033,6 +1524,7 @@ function installProductChanges(opts) {
|
|
|
1033
1524
|
observer?.disconnect();
|
|
1034
1525
|
for (const id of [...copies.keys()]) restoreCopy(id);
|
|
1035
1526
|
for (const remove of [...tips.values()]) remove();
|
|
1527
|
+
for (const id of [...lists.keys()]) lists.delete(id);
|
|
1036
1528
|
host?.remove();
|
|
1037
1529
|
host = null;
|
|
1038
1530
|
root = null;
|
|
@@ -1047,6 +1539,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1047
1539
|
teardownAutoCapture = null;
|
|
1048
1540
|
friction = null;
|
|
1049
1541
|
changes = null;
|
|
1542
|
+
skeleton = null;
|
|
1050
1543
|
constructor(opts) {
|
|
1051
1544
|
super("sdk_web", opts);
|
|
1052
1545
|
this.anonymousId = resolveAnonymousId();
|
|
@@ -1064,12 +1557,19 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1064
1557
|
onFriction: (signal) => this.track(`$${signal.kind}`, signal.properties)
|
|
1065
1558
|
});
|
|
1066
1559
|
}
|
|
1560
|
+
if (opts.skeleton !== false) {
|
|
1561
|
+
this.skeleton = installSkeletonCapture({
|
|
1562
|
+
onSkeleton: (skeleton) => this.track("$page_skeleton", { ...skeleton })
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1067
1565
|
if (opts.autoCapture !== false) {
|
|
1068
1566
|
this.teardownAutoCapture = installAutoCapture({
|
|
1069
1567
|
onNavigate: (kind) => {
|
|
1070
1568
|
this.page(void 0, { navigation: kind });
|
|
1071
|
-
|
|
1569
|
+
const path = typeof location !== "undefined" ? location.pathname : "/";
|
|
1570
|
+
this.friction?.onNavigate(path);
|
|
1072
1571
|
this.changes?.onNavigate();
|
|
1572
|
+
this.skeleton?.onNavigate(path);
|
|
1073
1573
|
}
|
|
1074
1574
|
});
|
|
1075
1575
|
}
|
|
@@ -1101,6 +1601,10 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1101
1601
|
super.group(accountKey, traits);
|
|
1102
1602
|
this.changes?.onNavigate();
|
|
1103
1603
|
}
|
|
1604
|
+
track(name, properties, overrides) {
|
|
1605
|
+
super.track(name, properties, overrides);
|
|
1606
|
+
if (!name.startsWith("$")) this.changes?.onEvent(name);
|
|
1607
|
+
}
|
|
1104
1608
|
reset() {
|
|
1105
1609
|
this.userId = null;
|
|
1106
1610
|
this.accountKey = null;
|
|
@@ -1108,6 +1612,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1108
1612
|
localStorage.removeItem(ANON_STORAGE_KEY);
|
|
1109
1613
|
} catch {
|
|
1110
1614
|
}
|
|
1615
|
+
clearSessionId();
|
|
1111
1616
|
this.anonymousId = resolveAnonymousId();
|
|
1112
1617
|
this.changes?.onNavigate();
|
|
1113
1618
|
}
|
|
@@ -1119,6 +1624,8 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1119
1624
|
this.friction = null;
|
|
1120
1625
|
this.changes?.teardown();
|
|
1121
1626
|
this.changes = null;
|
|
1627
|
+
this.skeleton?.teardown();
|
|
1628
|
+
this.skeleton = null;
|
|
1122
1629
|
await super.shutdown();
|
|
1123
1630
|
}
|
|
1124
1631
|
environmentContext() {
|