@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/web.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;
|
|
@@ -290,11 +293,14 @@ var NavLoopDetector = class {
|
|
|
290
293
|
return null;
|
|
291
294
|
}
|
|
292
295
|
};
|
|
296
|
+
function tagOf(el) {
|
|
297
|
+
return typeof el?.tagName === "string" ? el.tagName.toLowerCase() : "";
|
|
298
|
+
}
|
|
293
299
|
function resolveInteractive(el) {
|
|
294
300
|
if (!el) return null;
|
|
295
301
|
const viaClosest = el.closest?.(INTERACTIVE_SELECTOR) ?? null;
|
|
296
302
|
if (viaClosest) return viaClosest;
|
|
297
|
-
const tag = el
|
|
303
|
+
const tag = tagOf(el);
|
|
298
304
|
if (INTERACTIVE_TAGS.has(tag)) return el;
|
|
299
305
|
const role = el.getAttribute("role");
|
|
300
306
|
if (role === "button" || role === "link" || role === "menuitem" || role === "tab") return el;
|
|
@@ -306,7 +312,7 @@ function isDeadClickTarget(el) {
|
|
|
306
312
|
if (target.disabled === true) return true;
|
|
307
313
|
if (target.getAttribute("disabled") !== null) return true;
|
|
308
314
|
if (target.getAttribute("aria-disabled") === "true") return true;
|
|
309
|
-
const tag = target
|
|
315
|
+
const tag = tagOf(target);
|
|
310
316
|
if (tag === "a") {
|
|
311
317
|
const href = target.getAttribute("href");
|
|
312
318
|
if (!href || href === "#" || href.startsWith("javascript:")) return true;
|
|
@@ -316,7 +322,7 @@ function isDeadClickTarget(el) {
|
|
|
316
322
|
function describeElement(el) {
|
|
317
323
|
const target = resolveInteractive(el) ?? el;
|
|
318
324
|
if (!target) return {};
|
|
319
|
-
const out = { element: target
|
|
325
|
+
const out = { element: tagOf(target) };
|
|
320
326
|
if (target.id) out.element_id = target.id;
|
|
321
327
|
const role = target.getAttribute("role");
|
|
322
328
|
if (role) out.element_role = role;
|
|
@@ -344,6 +350,22 @@ function errorMessage(reason) {
|
|
|
344
350
|
return String(reason);
|
|
345
351
|
}
|
|
346
352
|
}
|
|
353
|
+
var STACK_MAX_FRAMES = 8;
|
|
354
|
+
var STACK_MAX_FRAME_CHARS = 200;
|
|
355
|
+
var STACK_FRAME = /^at\s|^\S*@\S+:\d+(?::\d+)?$/;
|
|
356
|
+
function sanitizeStack(stack) {
|
|
357
|
+
if (typeof stack !== "string" || !stack) return null;
|
|
358
|
+
const frames = stack.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && STACK_FRAME.test(line)).slice(0, STACK_MAX_FRAMES).map(
|
|
359
|
+
(line) => line.replace(/(https?:\/\/[^\s)?#]+)[?#][^\s)]*?(?=(?::\d+){0,2}\)?$)/, "$1").slice(0, STACK_MAX_FRAME_CHARS)
|
|
360
|
+
);
|
|
361
|
+
return frames.length > 0 ? frames.join("\n") : null;
|
|
362
|
+
}
|
|
363
|
+
function withStack(stack) {
|
|
364
|
+
return stack ? { stack } : {};
|
|
365
|
+
}
|
|
366
|
+
function stackOf(reason) {
|
|
367
|
+
return typeof reason === "object" && reason !== null ? sanitizeStack(reason.stack) : null;
|
|
368
|
+
}
|
|
347
369
|
function isAbortError(err) {
|
|
348
370
|
return typeof err === "object" && err !== null && err.name === "AbortError";
|
|
349
371
|
}
|
|
@@ -421,13 +443,19 @@ function installFrictionCapture(opts) {
|
|
|
421
443
|
emit("js_error", {
|
|
422
444
|
message: String(ev.message ?? "").slice(0, 200),
|
|
423
445
|
...ev.filename ? { source: stripRequestUrl(ev.filename, location.href) } : {},
|
|
424
|
-
...ev.lineno ? { line: ev.lineno } : {}
|
|
446
|
+
...ev.lineno ? { line: ev.lineno } : {},
|
|
447
|
+
...ev.colno ? { column: ev.colno } : {},
|
|
448
|
+
...withStack(stackOf(ev.error))
|
|
425
449
|
});
|
|
426
450
|
};
|
|
427
451
|
window.addEventListener("error", onError);
|
|
428
452
|
cleanups.push(() => window.removeEventListener("error", onError));
|
|
429
453
|
const onRejection = (ev) => {
|
|
430
|
-
emit("js_error", {
|
|
454
|
+
emit("js_error", {
|
|
455
|
+
message: errorMessage(ev.reason).slice(0, 200),
|
|
456
|
+
unhandled_rejection: true,
|
|
457
|
+
...withStack(stackOf(ev.reason))
|
|
458
|
+
});
|
|
431
459
|
};
|
|
432
460
|
window.addEventListener("unhandledrejection", onRejection);
|
|
433
461
|
cleanups.push(() => window.removeEventListener("unhandledrejection", onRejection));
|
|
@@ -509,11 +537,11 @@ function installFrictionCapture(opts) {
|
|
|
509
537
|
}
|
|
510
538
|
const onFocusIn = (ev) => {
|
|
511
539
|
const el = ev.target;
|
|
512
|
-
if (!el || !FORM_CONTROL_TAGS.has(el
|
|
540
|
+
if (!el || !FORM_CONTROL_TAGS.has(tagOf(el))) return;
|
|
513
541
|
const form = el.form;
|
|
514
542
|
if (!form) return;
|
|
515
543
|
const entry = touched.get(form) ?? { form: formKey(form), fields: /* @__PURE__ */ new Set() };
|
|
516
|
-
const field = el.name || el.id || el
|
|
544
|
+
const field = el.name || el.id || tagOf(el);
|
|
517
545
|
entry.fields.add(field);
|
|
518
546
|
touched.set(form, entry);
|
|
519
547
|
};
|
|
@@ -546,6 +574,200 @@ function installFrictionCapture(opts) {
|
|
|
546
574
|
};
|
|
547
575
|
}
|
|
548
576
|
|
|
577
|
+
// src/theme.ts
|
|
578
|
+
var THEME_BUTTON_SELECTOR = 'button[type="submit"],[data-testid*="primary"],.btn-primary,button,[role="button"],a.button';
|
|
579
|
+
var THEME_MAX_BUTTONS = 40;
|
|
580
|
+
var THEME_KEYS = ["font", "size", "fg", "bg", "accent", "accentFg", "radius"];
|
|
581
|
+
var COLOR = /^(rgba?\([\d.,\s%/]{1,40}\)|#[0-9a-fA-F]{3,8}|(?:color|oklch|oklab|hsla?|lab|lch)\([^()]{1,60}\))$/;
|
|
582
|
+
var MIN_ACCENT_CONTRAST = 60;
|
|
583
|
+
var RGB = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/;
|
|
584
|
+
function colorDistance(a, b) {
|
|
585
|
+
const x = RGB.exec(a);
|
|
586
|
+
const y = b ? RGB.exec(b) : null;
|
|
587
|
+
if (!x) return MIN_ACCENT_CONTRAST + 1;
|
|
588
|
+
if (!y) return MIN_ACCENT_CONTRAST + 1;
|
|
589
|
+
return Math.abs(+x[1] - +y[1]) + Math.abs(+x[2] - +y[2]) + Math.abs(+x[3] - +y[3]);
|
|
590
|
+
}
|
|
591
|
+
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*\))$/;
|
|
592
|
+
function themeVarName(key) {
|
|
593
|
+
return `--holdy-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
594
|
+
}
|
|
595
|
+
function cleanColor(value) {
|
|
596
|
+
const v = (value ?? "").trim();
|
|
597
|
+
return v && v.length <= 80 && !TRANSPARENT.test(v) && COLOR.test(v) ? v : void 0;
|
|
598
|
+
}
|
|
599
|
+
function cleanFont(value) {
|
|
600
|
+
const first = (value ?? "").split(",")[0].trim().replace(/^["']|["']$/g, "").trim();
|
|
601
|
+
return /^[\w\s-]{1,60}$/.test(first) ? first : void 0;
|
|
602
|
+
}
|
|
603
|
+
function cleanPx(value, min, max) {
|
|
604
|
+
const m = /^(\d+(?:\.\d+)?)px/.exec((value ?? "").trim());
|
|
605
|
+
const n = m ? Math.round(Number(m[1])) : NaN;
|
|
606
|
+
return n >= min && n <= max ? `${n}px` : void 0;
|
|
607
|
+
}
|
|
608
|
+
function visible(el) {
|
|
609
|
+
const r = el.getBoundingClientRect?.();
|
|
610
|
+
return !r || r.width > 0 && r.height > 0;
|
|
611
|
+
}
|
|
612
|
+
function readTheme(doc, getStyle, anchor = null) {
|
|
613
|
+
const body = doc.body;
|
|
614
|
+
if (!body) return null;
|
|
615
|
+
const bodyStyle = getStyle(body);
|
|
616
|
+
const theme = {};
|
|
617
|
+
const put = (key, value) => {
|
|
618
|
+
if (value) theme[key] = value;
|
|
619
|
+
};
|
|
620
|
+
put("font", cleanFont(bodyStyle.getPropertyValue("font-family")));
|
|
621
|
+
put("size", cleanPx(bodyStyle.getPropertyValue("font-size"), 10, 20));
|
|
622
|
+
put("fg", cleanColor(bodyStyle.getPropertyValue("color")));
|
|
623
|
+
put("bg", cleanColor(bodyStyle.getPropertyValue("background-color")) ?? cleanColor(getStyle(doc.documentElement).getPropertyValue("background-color")));
|
|
624
|
+
const candidates = doc.querySelectorAll(THEME_BUTTON_SELECTOR);
|
|
625
|
+
let button = null;
|
|
626
|
+
let best = MIN_ACCENT_CONTRAST;
|
|
627
|
+
for (let i = 0; i < candidates.length && i < THEME_MAX_BUTTONS; i++) {
|
|
628
|
+
const el = candidates[i];
|
|
629
|
+
if (!visible(el)) continue;
|
|
630
|
+
const s = getStyle(el);
|
|
631
|
+
const accent = cleanColor(s.getPropertyValue("background-color"));
|
|
632
|
+
if (!accent || accent === theme.bg) continue;
|
|
633
|
+
const contrast = colorDistance(accent, theme.bg);
|
|
634
|
+
if (contrast <= best) continue;
|
|
635
|
+
best = contrast;
|
|
636
|
+
put("accent", accent);
|
|
637
|
+
put("accentFg", cleanColor(s.getPropertyValue("color")));
|
|
638
|
+
button = el;
|
|
639
|
+
}
|
|
640
|
+
const radiusSource = anchor && anchor.tagName.toLowerCase() === "button" ? anchor : button;
|
|
641
|
+
if (radiusSource) put("radius", cleanPx(getStyle(radiusSource).getPropertyValue("border-top-left-radius"), 0, 24));
|
|
642
|
+
return Object.keys(theme).length > 0 ? theme : null;
|
|
643
|
+
}
|
|
644
|
+
function themeVars(theme) {
|
|
645
|
+
const out = [];
|
|
646
|
+
for (const key of THEME_KEYS) {
|
|
647
|
+
const value = theme?.[key];
|
|
648
|
+
if (value) out.push([themeVarName(key), value]);
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
function applyThemeVars(el, theme) {
|
|
653
|
+
for (const key of THEME_KEYS) el.style.removeProperty(themeVarName(key));
|
|
654
|
+
for (const [name, value] of themeVars(theme)) el.style.setProperty(name, value);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/skeleton.ts
|
|
658
|
+
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]';
|
|
659
|
+
var SKELETON_MAX_ELEMENTS = 80;
|
|
660
|
+
var SKELETON_DELAY_MS = 1500;
|
|
661
|
+
var SENT_STORAGE_KEY = "holdy_skeletons";
|
|
662
|
+
var SENT_STORAGE_MAX = 200;
|
|
663
|
+
function fnv1a(input) {
|
|
664
|
+
let hash = 2166136261;
|
|
665
|
+
for (let i = 0; i < input.length; i++) {
|
|
666
|
+
hash ^= input.charCodeAt(i);
|
|
667
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
668
|
+
}
|
|
669
|
+
return hash.toString(16).padStart(8, "0");
|
|
670
|
+
}
|
|
671
|
+
function attr(el, name, max) {
|
|
672
|
+
const value = el.getAttribute(name);
|
|
673
|
+
if (!value) return void 0;
|
|
674
|
+
const trimmed = value.replace(/\s+/g, " ").trim().slice(0, max);
|
|
675
|
+
return trimmed || void 0;
|
|
676
|
+
}
|
|
677
|
+
function isVisible(el) {
|
|
678
|
+
if (!el.getBoundingClientRect) return true;
|
|
679
|
+
const rect = el.getBoundingClientRect();
|
|
680
|
+
return rect.width > 0 && rect.height > 0;
|
|
681
|
+
}
|
|
682
|
+
function relativeBox(rect, viewport) {
|
|
683
|
+
const pct = (value, base) => base > 0 ? Math.max(0, Math.min(100, Math.round(value / base * 100))) : 0;
|
|
684
|
+
return [pct(rect.x, viewport.width), pct(rect.y + viewport.scrollY, viewport.height), pct(rect.width, viewport.width), pct(rect.height, viewport.height)];
|
|
685
|
+
}
|
|
686
|
+
function describeSkeletonElement(el, viewport) {
|
|
687
|
+
const out = { t: el.tagName.toLowerCase() };
|
|
688
|
+
const role = attr(el, "role", 40);
|
|
689
|
+
if (role) out.r = role;
|
|
690
|
+
const testid = attr(el, "data-testid", 80);
|
|
691
|
+
if (testid) out.id = testid;
|
|
692
|
+
const name = attr(el, "name", 80);
|
|
693
|
+
if (name) out.n = name;
|
|
694
|
+
const label = attr(el, "aria-label", 80);
|
|
695
|
+
if (label) out.l = label;
|
|
696
|
+
if (out.t === "input" || out.t === "button") {
|
|
697
|
+
const type = attr(el, "type", 20);
|
|
698
|
+
if (type) out.k = type.toLowerCase();
|
|
699
|
+
}
|
|
700
|
+
if (viewport && el.getBoundingClientRect) out.b = relativeBox(el.getBoundingClientRect(), viewport);
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
function collectSkeleton(root, path, viewport, theme = null) {
|
|
704
|
+
const found = root.querySelectorAll(SKELETON_SELECTOR);
|
|
705
|
+
const visible2 = [];
|
|
706
|
+
for (let i = 0; i < found.length; i++) {
|
|
707
|
+
const el = found[i];
|
|
708
|
+
if (el.tagName.toLowerCase() === "input" && (el.getAttribute("type") ?? "").toLowerCase() === "hidden") continue;
|
|
709
|
+
if (!isVisible(el)) continue;
|
|
710
|
+
visible2.push(el);
|
|
711
|
+
}
|
|
712
|
+
const elements = visible2.slice(0, SKELETON_MAX_ELEMENTS).map((el) => describeSkeletonElement(el, viewport));
|
|
713
|
+
const structural = elements.map(({ b: _b, ...rest }) => rest);
|
|
714
|
+
return { path, hash: fnv1a(JSON.stringify(structural)), count: visible2.length, elements, ...theme ? { theme } : {} };
|
|
715
|
+
}
|
|
716
|
+
function loadSent() {
|
|
717
|
+
try {
|
|
718
|
+
const raw = sessionStorage.getItem(SENT_STORAGE_KEY);
|
|
719
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
720
|
+
return new Set(Array.isArray(list) ? list.filter((x) => typeof x === "string") : []);
|
|
721
|
+
} catch {
|
|
722
|
+
return /* @__PURE__ */ new Set();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function saveSent(sent) {
|
|
726
|
+
try {
|
|
727
|
+
sessionStorage.setItem(SENT_STORAGE_KEY, JSON.stringify([...sent].slice(-SENT_STORAGE_MAX)));
|
|
728
|
+
} catch {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function installSkeletonCapture(opts) {
|
|
733
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
734
|
+
return { onNavigate: () => void 0, teardown: () => void 0 };
|
|
735
|
+
}
|
|
736
|
+
const delay = opts.delayMs ?? SKELETON_DELAY_MS;
|
|
737
|
+
const sent = loadSent();
|
|
738
|
+
let pending = null;
|
|
739
|
+
const capture = (path) => {
|
|
740
|
+
pending = null;
|
|
741
|
+
if (location.pathname !== path) return;
|
|
742
|
+
try {
|
|
743
|
+
const viewport = {
|
|
744
|
+
width: window.innerWidth,
|
|
745
|
+
height: Math.max(document.documentElement.scrollHeight, window.innerHeight),
|
|
746
|
+
scrollY: window.scrollY
|
|
747
|
+
};
|
|
748
|
+
const theme = readTheme(document, (el) => window.getComputedStyle(el));
|
|
749
|
+
const skeleton = collectSkeleton(document, path, viewport, theme);
|
|
750
|
+
const key = `${path}:${skeleton.hash}`;
|
|
751
|
+
if (sent.has(key)) return;
|
|
752
|
+
sent.add(key);
|
|
753
|
+
saveSent(sent);
|
|
754
|
+
opts.onSkeleton(skeleton);
|
|
755
|
+
} catch {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
return {
|
|
760
|
+
onNavigate: (path) => {
|
|
761
|
+
if (pending) clearTimeout(pending);
|
|
762
|
+
pending = setTimeout(() => capture(path), delay);
|
|
763
|
+
},
|
|
764
|
+
teardown: () => {
|
|
765
|
+
if (pending) clearTimeout(pending);
|
|
766
|
+
pending = null;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
549
771
|
// src/web-context.ts
|
|
550
772
|
var UTM_KEYS = [
|
|
551
773
|
"utm_source",
|
|
@@ -610,6 +832,12 @@ function randomId() {
|
|
|
610
832
|
return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
611
833
|
}
|
|
612
834
|
}
|
|
835
|
+
function clearSessionId() {
|
|
836
|
+
try {
|
|
837
|
+
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
|
838
|
+
} catch {
|
|
839
|
+
}
|
|
840
|
+
}
|
|
613
841
|
function resolveSessionId() {
|
|
614
842
|
const existing = readStorage("session", SESSION_STORAGE_KEY);
|
|
615
843
|
if (existing) return existing;
|
|
@@ -663,8 +891,35 @@ function buildWebContext() {
|
|
|
663
891
|
};
|
|
664
892
|
}
|
|
665
893
|
|
|
894
|
+
// src/tip-css.ts
|
|
895
|
+
var TIP_CSS = `:host{all:initial;--h-bg:#111;--h-fg:#fff;--h-accent:#fff;--h-accent-fg:#111;--h-border:transparent}
|
|
896
|
+
@media (prefers-color-scheme:dark){:host{--h-bg:#fff;--h-fg:#111;--h-accent:#111;--h-accent-fg:#fff}}
|
|
897
|
+
.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}
|
|
898
|
+
.tip p{margin:0;flex:1}
|
|
899
|
+
.tip button{all:unset;cursor:pointer;opacity:.7;font-size:15px;line-height:1;color:inherit}
|
|
900
|
+
.tip button:hover,.tip button:focus-visible{opacity:1}
|
|
901
|
+
.ask{flex-direction:column;width:300px;max-width:calc(100vw - 16px);gap:8px}
|
|
902
|
+
.ask .head{display:flex;gap:10px;align-items:flex-start;width:100%}
|
|
903
|
+
.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}
|
|
904
|
+
.ask textarea:focus-visible{outline:2px solid color-mix(in srgb,var(--holdy-fg,var(--h-fg)) 45%,transparent)}
|
|
905
|
+
.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}
|
|
906
|
+
.ask .send[disabled]{opacity:.4;cursor:default}
|
|
907
|
+
.list{flex-direction:column;width:300px;max-width:calc(100vw - 16px);gap:8px}
|
|
908
|
+
.list .head{display:flex;gap:10px;align-items:flex-start;width:100%}
|
|
909
|
+
.list .head p{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
|
|
910
|
+
.list .count{opacity:.7;font-size:12px;white-space:nowrap}
|
|
911
|
+
.list ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;width:100%}
|
|
912
|
+
.list li{display:flex;gap:8px;align-items:flex-start}
|
|
913
|
+
.list li.done{opacity:.55;text-decoration:line-through}
|
|
914
|
+
.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}
|
|
915
|
+
.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))}
|
|
916
|
+
.list.folded ul{display:none}
|
|
917
|
+
.list.folded{width:auto}
|
|
918
|
+
.preview{flex:none;order:-1;width:100%;font-size:11px;opacity:.7}
|
|
919
|
+
.tip:has(.preview){flex-wrap:wrap;row-gap:4px}`;
|
|
920
|
+
|
|
666
921
|
// src/arm.ts
|
|
667
|
-
function
|
|
922
|
+
function fnv1a2(input) {
|
|
668
923
|
let hash = 2166136261;
|
|
669
924
|
for (let i = 0; i < input.length; i++) {
|
|
670
925
|
hash ^= input.charCodeAt(i);
|
|
@@ -673,7 +928,7 @@ function fnv1a(input) {
|
|
|
673
928
|
return hash >>> 0;
|
|
674
929
|
}
|
|
675
930
|
function changeBucket(changeId, accountKey) {
|
|
676
|
-
return
|
|
931
|
+
return fnv1a2(`${changeId}:${accountKey}`) % 100;
|
|
677
932
|
}
|
|
678
933
|
function changeArm(changeId, accountKey, holdoutPct) {
|
|
679
934
|
return changeBucket(changeId, accountKey) < holdoutPct ? "holdout" : "change";
|
|
@@ -692,12 +947,16 @@ var SCAN_DEBOUNCE_MS = 250;
|
|
|
692
947
|
var SEEN_KEY = "holdy_changes_seen";
|
|
693
948
|
var DISMISSED_KEY = "holdy_changes_dismissed";
|
|
694
949
|
var TIP_GAP_PX = 8;
|
|
695
|
-
var
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
950
|
+
var ANSWERED_KEY = "holdy_questions_answered";
|
|
951
|
+
var FOLDED_KEY = "holdy_checklist_folded";
|
|
952
|
+
var COMPLETED_KEY = "holdy_checklist_completed";
|
|
953
|
+
var PROGRESS_REFRESH_MS = 60 * 1e3;
|
|
954
|
+
var VOICE_EVENT = "$voice";
|
|
955
|
+
var MAX_ANSWER_CHARS = 1e3;
|
|
956
|
+
var THANKS_MS = 1600;
|
|
957
|
+
var PREVIEW_PARAM = "holdy_preview";
|
|
958
|
+
var PREVIEW_KEY = "holdy_preview";
|
|
959
|
+
var PREVIEW_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
701
960
|
function normalizeText(text) {
|
|
702
961
|
return text.replace(/\s+/g, " ").trim();
|
|
703
962
|
}
|
|
@@ -710,13 +969,42 @@ function isAnchor(value) {
|
|
|
710
969
|
if (a?.kind === "page") return true;
|
|
711
970
|
return (a?.kind === "selector" || a?.kind === "text") && typeof a.value === "string" && a.value.length > 0;
|
|
712
971
|
}
|
|
972
|
+
function isItem(value) {
|
|
973
|
+
const i = value;
|
|
974
|
+
return typeof i?.definition_name === "string" && typeof i.label === "string" && Array.isArray(i.events) && i.events.every((e) => typeof e === "string");
|
|
975
|
+
}
|
|
713
976
|
function isChange(value) {
|
|
714
977
|
const c = value;
|
|
715
|
-
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";
|
|
978
|
+
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";
|
|
716
979
|
}
|
|
717
|
-
function changesForPage(changes, path, accountKey) {
|
|
718
|
-
|
|
719
|
-
|
|
980
|
+
function changesForPage(changes, path, accountKey, previewId = null) {
|
|
981
|
+
return changes.filter((c) => c.kind === "checklist" || matchesPathTemplate(c.path, path)).flatMap((change) => {
|
|
982
|
+
if (change.id === previewId) return [{ change, arm: "change", preview: true }];
|
|
983
|
+
if (!accountKey) return [];
|
|
984
|
+
return [{ change, arm: changeArm(change.id, accountKey, change.holdout_pct), preview: false }];
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
function resolvePreviewId(search, store) {
|
|
988
|
+
let asked = null;
|
|
989
|
+
try {
|
|
990
|
+
asked = new URLSearchParams(search).get(PREVIEW_PARAM);
|
|
991
|
+
} catch {
|
|
992
|
+
asked = null;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
if (asked !== null) {
|
|
996
|
+
if (PREVIEW_ID.test(asked)) {
|
|
997
|
+
store?.setItem(PREVIEW_KEY, asked);
|
|
998
|
+
return asked;
|
|
999
|
+
}
|
|
1000
|
+
store?.removeItem(PREVIEW_KEY);
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
const kept = store?.getItem(PREVIEW_KEY) ?? null;
|
|
1004
|
+
return kept && PREVIEW_ID.test(kept) ? kept : null;
|
|
1005
|
+
} catch {
|
|
1006
|
+
return asked !== null && PREVIEW_ID.test(asked) ? asked : null;
|
|
1007
|
+
}
|
|
720
1008
|
}
|
|
721
1009
|
function storage(kind) {
|
|
722
1010
|
try {
|
|
@@ -778,7 +1066,7 @@ function findTextNode(el, match) {
|
|
|
778
1066
|
}
|
|
779
1067
|
function installProductChanges(opts) {
|
|
780
1068
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
781
|
-
return { onNavigate: () => void 0, refresh: async () => void 0, teardown: () => void 0 };
|
|
1069
|
+
return { onNavigate: () => void 0, onEvent: () => void 0, refresh: async () => void 0, teardown: () => void 0 };
|
|
782
1070
|
}
|
|
783
1071
|
let changes = [];
|
|
784
1072
|
let lastFetch = 0;
|
|
@@ -787,6 +1075,11 @@ function installProductChanges(opts) {
|
|
|
787
1075
|
let host = null;
|
|
788
1076
|
const copies = /* @__PURE__ */ new Map();
|
|
789
1077
|
const tips = /* @__PURE__ */ new Map();
|
|
1078
|
+
const progress = /* @__PURE__ */ new Map();
|
|
1079
|
+
const progressAt = /* @__PURE__ */ new Map();
|
|
1080
|
+
const lists = /* @__PURE__ */ new Map();
|
|
1081
|
+
let fetchedForKey = null;
|
|
1082
|
+
let previewId = resolvePreviewId(location.search, storage("session"));
|
|
790
1083
|
const log = (message) => {
|
|
791
1084
|
if (opts.debug) console.warn(`[holdy] ${message}`);
|
|
792
1085
|
};
|
|
@@ -836,21 +1129,66 @@ function installProductChanges(opts) {
|
|
|
836
1129
|
document.body.appendChild(host);
|
|
837
1130
|
return root;
|
|
838
1131
|
};
|
|
839
|
-
|
|
1132
|
+
let dismissedPreview = false;
|
|
1133
|
+
const previewTag = () => {
|
|
1134
|
+
const tag = document.createElement("span");
|
|
1135
|
+
tag.className = "preview";
|
|
1136
|
+
tag.textContent = "Pr\xE9via, s\xF3 voc\xEA v\xEA";
|
|
1137
|
+
return tag;
|
|
1138
|
+
};
|
|
1139
|
+
const showTip = (anchor, change, preview) => {
|
|
840
1140
|
if (tips.has(change.id)) return true;
|
|
841
|
-
if (readSet(storage("local"), DISMISSED_KEY).has(change.id)) return true;
|
|
1141
|
+
if (!preview && readSet(storage("local"), DISMISSED_KEY).has(change.id)) return true;
|
|
1142
|
+
const asking = change.kind === "question";
|
|
1143
|
+
if (!preview && asking && readSet(storage("local"), ANSWERED_KEY).has(change.id)) return true;
|
|
842
1144
|
const shadow = ensureRoot();
|
|
843
1145
|
if (!shadow) return false;
|
|
1146
|
+
if (host) applyThemeVars(host, readTheme(document, (el) => window.getComputedStyle(el), anchor));
|
|
844
1147
|
const card = document.createElement("div");
|
|
845
|
-
card.className = "tip";
|
|
846
|
-
card.setAttribute("role", "tooltip");
|
|
1148
|
+
card.className = asking ? "tip ask" : "tip";
|
|
1149
|
+
card.setAttribute("role", asking ? "dialog" : "tooltip");
|
|
847
1150
|
const text = document.createElement("p");
|
|
848
1151
|
text.textContent = change.content.text;
|
|
849
1152
|
const close = document.createElement("button");
|
|
850
1153
|
close.type = "button";
|
|
851
1154
|
close.setAttribute("aria-label", "Fechar");
|
|
852
1155
|
close.textContent = "\xD7";
|
|
853
|
-
|
|
1156
|
+
if (asking) {
|
|
1157
|
+
const head = document.createElement("div");
|
|
1158
|
+
head.className = "head";
|
|
1159
|
+
head.append(text, close);
|
|
1160
|
+
const input = document.createElement("textarea");
|
|
1161
|
+
input.maxLength = MAX_ANSWER_CHARS;
|
|
1162
|
+
input.setAttribute("aria-label", change.content.text);
|
|
1163
|
+
const send = document.createElement("button");
|
|
1164
|
+
send.type = "button";
|
|
1165
|
+
send.className = "send";
|
|
1166
|
+
send.textContent = "Enviar";
|
|
1167
|
+
send.disabled = true;
|
|
1168
|
+
input.addEventListener("input", () => {
|
|
1169
|
+
send.disabled = input.value.trim().length === 0;
|
|
1170
|
+
});
|
|
1171
|
+
send.addEventListener("click", () => {
|
|
1172
|
+
const answer = input.value.trim().slice(0, MAX_ANSWER_CHARS);
|
|
1173
|
+
if (!answer) return;
|
|
1174
|
+
if (!preview) {
|
|
1175
|
+
addToSet(storage("local"), ANSWERED_KEY, change.id);
|
|
1176
|
+
opts.track(VOICE_EVENT, {
|
|
1177
|
+
question: change.content.text,
|
|
1178
|
+
text: answer,
|
|
1179
|
+
action_plan_id: change.plan_id,
|
|
1180
|
+
change_id: change.id,
|
|
1181
|
+
path: location.pathname
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
card.replaceChildren(Object.assign(document.createElement("p"), { textContent: "Obrigado pela resposta." }));
|
|
1185
|
+
setTimeout(remove, THANKS_MS);
|
|
1186
|
+
});
|
|
1187
|
+
card.append(head, input, send);
|
|
1188
|
+
} else {
|
|
1189
|
+
card.append(text, close);
|
|
1190
|
+
}
|
|
1191
|
+
if (preview) card.prepend(previewTag());
|
|
854
1192
|
card.style.top = "-9999px";
|
|
855
1193
|
card.style.left = "-9999px";
|
|
856
1194
|
shadow.appendChild(card);
|
|
@@ -880,6 +1218,11 @@ function installProductChanges(opts) {
|
|
|
880
1218
|
card.style.left = `${Math.max(8, left)}px`;
|
|
881
1219
|
}
|
|
882
1220
|
close.addEventListener("click", () => {
|
|
1221
|
+
if (preview) {
|
|
1222
|
+
dismissedPreview = true;
|
|
1223
|
+
remove();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
883
1226
|
addToSet(storage("local"), DISMISSED_KEY, change.id);
|
|
884
1227
|
opts.track("$change_dismissed", { change_id: change.id, kind: change.kind });
|
|
885
1228
|
remove();
|
|
@@ -890,20 +1233,143 @@ function installProductChanges(opts) {
|
|
|
890
1233
|
window.addEventListener("resize", position);
|
|
891
1234
|
return true;
|
|
892
1235
|
};
|
|
1236
|
+
const doneOf = (change) => progress.get(change.id) ?? /* @__PURE__ */ new Set();
|
|
1237
|
+
const allDone = (change) => {
|
|
1238
|
+
const done = doneOf(change);
|
|
1239
|
+
return (change.content.items ?? []).every((i) => done.has(i.definition_name));
|
|
1240
|
+
};
|
|
1241
|
+
const fetchProgress = async (change) => {
|
|
1242
|
+
const account = opts.getAccountKey();
|
|
1243
|
+
if (!account) return;
|
|
1244
|
+
const last = progressAt.get(change.id) ?? 0;
|
|
1245
|
+
if (Date.now() - last < PROGRESS_REFRESH_MS) return;
|
|
1246
|
+
progressAt.set(change.id, Date.now());
|
|
1247
|
+
try {
|
|
1248
|
+
const res = await fetch(
|
|
1249
|
+
`${opts.baseUrl}/runtime/checklist?change_id=${encodeURIComponent(change.id)}&account=${encodeURIComponent(account)}`,
|
|
1250
|
+
{ headers: { Authorization: `Bearer ${opts.apiKey}` } }
|
|
1251
|
+
);
|
|
1252
|
+
if (!res.ok) return;
|
|
1253
|
+
const body = await res.json();
|
|
1254
|
+
const done = Array.isArray(body.done) ? body.done.filter((d) => typeof d === "string") : [];
|
|
1255
|
+
const set = doneOf(change);
|
|
1256
|
+
for (const d of done) set.add(d);
|
|
1257
|
+
progress.set(change.id, set);
|
|
1258
|
+
lists.get(change.id)?.();
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
log(`checklist progress error: ${err.message}`);
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
const showChecklist = (change, preview) => {
|
|
1264
|
+
if (lists.has(change.id)) return true;
|
|
1265
|
+
if (!preview && readSet(storage("local"), COMPLETED_KEY).has(change.id)) return true;
|
|
1266
|
+
const shadow = ensureRoot();
|
|
1267
|
+
if (!shadow) return false;
|
|
1268
|
+
const items = change.content.items ?? [];
|
|
1269
|
+
const card = document.createElement("div");
|
|
1270
|
+
card.className = "tip list";
|
|
1271
|
+
card.setAttribute("role", "region");
|
|
1272
|
+
card.setAttribute("aria-label", change.content.text);
|
|
1273
|
+
const head = document.createElement("div");
|
|
1274
|
+
head.className = "head";
|
|
1275
|
+
const title = document.createElement("p");
|
|
1276
|
+
const strong = document.createElement("strong");
|
|
1277
|
+
strong.textContent = change.content.text;
|
|
1278
|
+
const count = document.createElement("span");
|
|
1279
|
+
count.className = "count";
|
|
1280
|
+
title.append(strong, count);
|
|
1281
|
+
const fold = document.createElement("button");
|
|
1282
|
+
fold.type = "button";
|
|
1283
|
+
head.append(title, fold);
|
|
1284
|
+
const list = document.createElement("ul");
|
|
1285
|
+
card.append(head, list);
|
|
1286
|
+
if (preview) card.prepend(previewTag());
|
|
1287
|
+
card.style.position = "fixed";
|
|
1288
|
+
card.style.right = "16px";
|
|
1289
|
+
card.style.bottom = "16px";
|
|
1290
|
+
shadow.appendChild(card);
|
|
1291
|
+
const remove = () => {
|
|
1292
|
+
card.remove();
|
|
1293
|
+
lists.delete(change.id);
|
|
1294
|
+
};
|
|
1295
|
+
const render = () => {
|
|
1296
|
+
const done = doneOf(change);
|
|
1297
|
+
const n = items.filter((i) => done.has(i.definition_name)).length;
|
|
1298
|
+
count.textContent = `${n} de ${items.length}`;
|
|
1299
|
+
const folded = readSet(storage("local"), FOLDED_KEY).has(change.id);
|
|
1300
|
+
card.classList.toggle("folded", folded);
|
|
1301
|
+
fold.textContent = folded ? "+" : "\u2013";
|
|
1302
|
+
fold.setAttribute("aria-label", folded ? "Abrir" : "Recolher");
|
|
1303
|
+
list.replaceChildren(
|
|
1304
|
+
...items.map((item) => {
|
|
1305
|
+
const li = document.createElement("li");
|
|
1306
|
+
const isDone = done.has(item.definition_name);
|
|
1307
|
+
if (isDone) li.className = "done";
|
|
1308
|
+
const mark = document.createElement("span");
|
|
1309
|
+
mark.className = "mark";
|
|
1310
|
+
mark.setAttribute("aria-hidden", "true");
|
|
1311
|
+
mark.textContent = isDone ? "\u2713" : "";
|
|
1312
|
+
const label = document.createElement("span");
|
|
1313
|
+
label.textContent = item.label;
|
|
1314
|
+
li.append(mark, label);
|
|
1315
|
+
return li;
|
|
1316
|
+
})
|
|
1317
|
+
);
|
|
1318
|
+
if (!preview && n === items.length && items.length > 0) {
|
|
1319
|
+
addToSet(storage("local"), COMPLETED_KEY, change.id);
|
|
1320
|
+
opts.track("$checklist_completed", { change_id: change.id });
|
|
1321
|
+
remove();
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
fold.addEventListener("click", () => {
|
|
1325
|
+
const store = storage("local");
|
|
1326
|
+
const set = readSet(store, FOLDED_KEY);
|
|
1327
|
+
if (set.has(change.id)) set.delete(change.id);
|
|
1328
|
+
else set.add(change.id);
|
|
1329
|
+
try {
|
|
1330
|
+
store?.setItem(FOLDED_KEY, JSON.stringify([...set].slice(-200)));
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
render();
|
|
1334
|
+
});
|
|
1335
|
+
lists.set(change.id, render);
|
|
1336
|
+
render();
|
|
1337
|
+
void fetchProgress(change);
|
|
1338
|
+
return true;
|
|
1339
|
+
};
|
|
893
1340
|
const scan = () => {
|
|
894
1341
|
timer = null;
|
|
895
|
-
const active = changesForPage(changes, location.pathname, opts.getAccountKey());
|
|
1342
|
+
const active = changesForPage(changes, location.pathname, opts.getAccountKey(), previewId);
|
|
896
1343
|
const activeIds = new Set(active.map((a) => a.change.id));
|
|
897
1344
|
for (const id of [...copies.keys()]) if (!activeIds.has(id)) restoreCopy(id);
|
|
898
1345
|
for (const [id, remove] of [...tips]) if (!activeIds.has(id)) remove();
|
|
899
|
-
for (const
|
|
1346
|
+
for (const [id, render] of [...lists]) {
|
|
1347
|
+
if (!activeIds.has(id)) {
|
|
1348
|
+
lists.delete(id);
|
|
1349
|
+
render();
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
for (const { change, arm, preview } of active) {
|
|
900
1353
|
const el = findAnchor(change.anchor);
|
|
901
|
-
if (!el && change.anchor.kind !== "page")
|
|
902
|
-
|
|
1354
|
+
if (!el && change.anchor.kind !== "page") {
|
|
1355
|
+
if (preview) log(`preview: \xE2ncora n\xE3o encontrada em ${location.pathname}`);
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (preview) {
|
|
1359
|
+
if (change.kind === "checklist") showChecklist(change, true);
|
|
1360
|
+
else if (change.kind === "copy") {
|
|
1361
|
+
if (el) applyCopy(el, change);
|
|
1362
|
+
} else if (!dismissedPreview) showTip(el, change, true);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (change.kind === "checklist") {
|
|
1366
|
+
if (arm === "holdout" || allDone(change) || showChecklist(change, false)) expose(change, arm);
|
|
1367
|
+
if (arm === "change") void fetchProgress(change);
|
|
1368
|
+
} else if (change.kind === "copy") {
|
|
903
1369
|
if (!el) continue;
|
|
904
1370
|
const shown = arm === "change" ? applyCopy(el, change) : eligibleCopyNode(el, change) !== null;
|
|
905
1371
|
if (shown) expose(change, arm);
|
|
906
|
-
} else if (arm === "holdout" || showTip(el, change)) {
|
|
1372
|
+
} else if (arm === "holdout" || showTip(el, change, false)) {
|
|
907
1373
|
expose(change, arm);
|
|
908
1374
|
}
|
|
909
1375
|
}
|
|
@@ -913,8 +1379,14 @@ function installProductChanges(opts) {
|
|
|
913
1379
|
};
|
|
914
1380
|
const refresh = async () => {
|
|
915
1381
|
lastFetch = Date.now();
|
|
1382
|
+
const account = opts.getAccountKey();
|
|
1383
|
+
fetchedForKey = account;
|
|
1384
|
+
const params = new URLSearchParams();
|
|
1385
|
+
if (account) params.set("account", account);
|
|
1386
|
+
if (previewId) params.set("preview", previewId);
|
|
1387
|
+
const qs = params.toString();
|
|
916
1388
|
try {
|
|
917
|
-
const res = await fetch(`${opts.baseUrl}/runtime/changes`, {
|
|
1389
|
+
const res = await fetch(`${opts.baseUrl}/runtime/changes${qs ? `?${qs}` : ""}`, {
|
|
918
1390
|
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
919
1391
|
});
|
|
920
1392
|
if (!res.ok) {
|
|
@@ -939,9 +1411,28 @@ function installProductChanges(opts) {
|
|
|
939
1411
|
void refresh();
|
|
940
1412
|
return {
|
|
941
1413
|
onNavigate: () => {
|
|
942
|
-
|
|
1414
|
+
const asked = resolvePreviewId(location.search, storage("session"));
|
|
1415
|
+
const previewChanged = asked !== previewId;
|
|
1416
|
+
if (previewChanged) {
|
|
1417
|
+
previewId = asked;
|
|
1418
|
+
dismissedPreview = false;
|
|
1419
|
+
for (const id of [...copies.keys()]) restoreCopy(id);
|
|
1420
|
+
for (const remove of [...tips.values()]) remove();
|
|
1421
|
+
}
|
|
1422
|
+
if (previewChanged || Date.now() - lastFetch > NAV_REFRESH_MS || opts.getAccountKey() !== fetchedForKey) void refresh();
|
|
943
1423
|
schedule();
|
|
944
1424
|
},
|
|
1425
|
+
onEvent: (name) => {
|
|
1426
|
+
for (const change of changes) {
|
|
1427
|
+
if (change.kind !== "checklist") continue;
|
|
1428
|
+
const hit = (change.content.items ?? []).filter((i) => i.events.includes(name));
|
|
1429
|
+
if (hit.length === 0) continue;
|
|
1430
|
+
const set = doneOf(change);
|
|
1431
|
+
for (const i of hit) set.add(i.definition_name);
|
|
1432
|
+
progress.set(change.id, set);
|
|
1433
|
+
lists.get(change.id)?.();
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
945
1436
|
refresh,
|
|
946
1437
|
teardown: () => {
|
|
947
1438
|
clearInterval(interval);
|
|
@@ -949,6 +1440,7 @@ function installProductChanges(opts) {
|
|
|
949
1440
|
observer?.disconnect();
|
|
950
1441
|
for (const id of [...copies.keys()]) restoreCopy(id);
|
|
951
1442
|
for (const remove of [...tips.values()]) remove();
|
|
1443
|
+
for (const id of [...lists.keys()]) lists.delete(id);
|
|
952
1444
|
host?.remove();
|
|
953
1445
|
host = null;
|
|
954
1446
|
root = null;
|
|
@@ -963,6 +1455,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
963
1455
|
teardownAutoCapture = null;
|
|
964
1456
|
friction = null;
|
|
965
1457
|
changes = null;
|
|
1458
|
+
skeleton = null;
|
|
966
1459
|
constructor(opts) {
|
|
967
1460
|
super("sdk_web", opts);
|
|
968
1461
|
this.anonymousId = resolveAnonymousId();
|
|
@@ -980,12 +1473,19 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
980
1473
|
onFriction: (signal) => this.track(`$${signal.kind}`, signal.properties)
|
|
981
1474
|
});
|
|
982
1475
|
}
|
|
1476
|
+
if (opts.skeleton !== false) {
|
|
1477
|
+
this.skeleton = installSkeletonCapture({
|
|
1478
|
+
onSkeleton: (skeleton) => this.track("$page_skeleton", { ...skeleton })
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
983
1481
|
if (opts.autoCapture !== false) {
|
|
984
1482
|
this.teardownAutoCapture = installAutoCapture({
|
|
985
1483
|
onNavigate: (kind) => {
|
|
986
1484
|
this.page(void 0, { navigation: kind });
|
|
987
|
-
|
|
1485
|
+
const path = typeof location !== "undefined" ? location.pathname : "/";
|
|
1486
|
+
this.friction?.onNavigate(path);
|
|
988
1487
|
this.changes?.onNavigate();
|
|
1488
|
+
this.skeleton?.onNavigate(path);
|
|
989
1489
|
}
|
|
990
1490
|
});
|
|
991
1491
|
}
|
|
@@ -1017,6 +1517,10 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1017
1517
|
super.group(accountKey, traits);
|
|
1018
1518
|
this.changes?.onNavigate();
|
|
1019
1519
|
}
|
|
1520
|
+
track(name, properties, overrides) {
|
|
1521
|
+
super.track(name, properties, overrides);
|
|
1522
|
+
if (!name.startsWith("$")) this.changes?.onEvent(name);
|
|
1523
|
+
}
|
|
1020
1524
|
reset() {
|
|
1021
1525
|
this.userId = null;
|
|
1022
1526
|
this.accountKey = null;
|
|
@@ -1024,6 +1528,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1024
1528
|
localStorage.removeItem(ANON_STORAGE_KEY);
|
|
1025
1529
|
} catch {
|
|
1026
1530
|
}
|
|
1531
|
+
clearSessionId();
|
|
1027
1532
|
this.anonymousId = resolveAnonymousId();
|
|
1028
1533
|
this.changes?.onNavigate();
|
|
1029
1534
|
}
|
|
@@ -1035,6 +1540,8 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1035
1540
|
this.friction = null;
|
|
1036
1541
|
this.changes?.teardown();
|
|
1037
1542
|
this.changes = null;
|
|
1543
|
+
this.skeleton?.teardown();
|
|
1544
|
+
this.skeleton = null;
|
|
1038
1545
|
await super.shutdown();
|
|
1039
1546
|
}
|
|
1040
1547
|
environmentContext() {
|