@holdyapp/sdk 1.6.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 +8 -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 +420 -36
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +420 -36
- 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 +3 -1
- package/dist/skeleton.d.ts.map +1 -1
- 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 +2 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.mjs +420 -36
- 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.ts +12 -3
- 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 +8 -2
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,86 @@ 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
|
+
|
|
549
657
|
// src/skeleton.ts
|
|
550
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]';
|
|
551
659
|
var SKELETON_MAX_ELEMENTS = 80;
|
|
@@ -592,18 +700,18 @@ function describeSkeletonElement(el, viewport) {
|
|
|
592
700
|
if (viewport && el.getBoundingClientRect) out.b = relativeBox(el.getBoundingClientRect(), viewport);
|
|
593
701
|
return out;
|
|
594
702
|
}
|
|
595
|
-
function collectSkeleton(root, path, viewport) {
|
|
703
|
+
function collectSkeleton(root, path, viewport, theme = null) {
|
|
596
704
|
const found = root.querySelectorAll(SKELETON_SELECTOR);
|
|
597
|
-
const
|
|
705
|
+
const visible2 = [];
|
|
598
706
|
for (let i = 0; i < found.length; i++) {
|
|
599
707
|
const el = found[i];
|
|
600
708
|
if (el.tagName.toLowerCase() === "input" && (el.getAttribute("type") ?? "").toLowerCase() === "hidden") continue;
|
|
601
709
|
if (!isVisible(el)) continue;
|
|
602
|
-
|
|
710
|
+
visible2.push(el);
|
|
603
711
|
}
|
|
604
|
-
const elements =
|
|
712
|
+
const elements = visible2.slice(0, SKELETON_MAX_ELEMENTS).map((el) => describeSkeletonElement(el, viewport));
|
|
605
713
|
const structural = elements.map(({ b: _b, ...rest }) => rest);
|
|
606
|
-
return { path, hash: fnv1a(JSON.stringify(structural)), count:
|
|
714
|
+
return { path, hash: fnv1a(JSON.stringify(structural)), count: visible2.length, elements, ...theme ? { theme } : {} };
|
|
607
715
|
}
|
|
608
716
|
function loadSent() {
|
|
609
717
|
try {
|
|
@@ -637,7 +745,8 @@ function installSkeletonCapture(opts) {
|
|
|
637
745
|
height: Math.max(document.documentElement.scrollHeight, window.innerHeight),
|
|
638
746
|
scrollY: window.scrollY
|
|
639
747
|
};
|
|
640
|
-
const
|
|
748
|
+
const theme = readTheme(document, (el) => window.getComputedStyle(el));
|
|
749
|
+
const skeleton = collectSkeleton(document, path, viewport, theme);
|
|
641
750
|
const key = `${path}:${skeleton.hash}`;
|
|
642
751
|
if (sent.has(key)) return;
|
|
643
752
|
sent.add(key);
|
|
@@ -723,6 +832,12 @@ function randomId() {
|
|
|
723
832
|
return `hol-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
724
833
|
}
|
|
725
834
|
}
|
|
835
|
+
function clearSessionId() {
|
|
836
|
+
try {
|
|
837
|
+
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
|
838
|
+
} catch {
|
|
839
|
+
}
|
|
840
|
+
}
|
|
726
841
|
function resolveSessionId() {
|
|
727
842
|
const existing = readStorage("session", SESSION_STORAGE_KEY);
|
|
728
843
|
if (existing) return existing;
|
|
@@ -776,6 +891,33 @@ function buildWebContext() {
|
|
|
776
891
|
};
|
|
777
892
|
}
|
|
778
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
|
+
|
|
779
921
|
// src/arm.ts
|
|
780
922
|
function fnv1a2(input) {
|
|
781
923
|
let hash = 2166136261;
|
|
@@ -805,12 +947,16 @@ var SCAN_DEBOUNCE_MS = 250;
|
|
|
805
947
|
var SEEN_KEY = "holdy_changes_seen";
|
|
806
948
|
var DISMISSED_KEY = "holdy_changes_dismissed";
|
|
807
949
|
var TIP_GAP_PX = 8;
|
|
808
|
-
var
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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;
|
|
814
960
|
function normalizeText(text) {
|
|
815
961
|
return text.replace(/\s+/g, " ").trim();
|
|
816
962
|
}
|
|
@@ -823,13 +969,42 @@ function isAnchor(value) {
|
|
|
823
969
|
if (a?.kind === "page") return true;
|
|
824
970
|
return (a?.kind === "selector" || a?.kind === "text") && typeof a.value === "string" && a.value.length > 0;
|
|
825
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
|
+
}
|
|
826
976
|
function isChange(value) {
|
|
827
977
|
const c = value;
|
|
828
|
-
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";
|
|
829
979
|
}
|
|
830
|
-
function changesForPage(changes, path, accountKey) {
|
|
831
|
-
|
|
832
|
-
|
|
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
|
+
}
|
|
833
1008
|
}
|
|
834
1009
|
function storage(kind) {
|
|
835
1010
|
try {
|
|
@@ -891,7 +1066,7 @@ function findTextNode(el, match) {
|
|
|
891
1066
|
}
|
|
892
1067
|
function installProductChanges(opts) {
|
|
893
1068
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
894
|
-
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 };
|
|
895
1070
|
}
|
|
896
1071
|
let changes = [];
|
|
897
1072
|
let lastFetch = 0;
|
|
@@ -900,6 +1075,11 @@ function installProductChanges(opts) {
|
|
|
900
1075
|
let host = null;
|
|
901
1076
|
const copies = /* @__PURE__ */ new Map();
|
|
902
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"));
|
|
903
1083
|
const log = (message) => {
|
|
904
1084
|
if (opts.debug) console.warn(`[holdy] ${message}`);
|
|
905
1085
|
};
|
|
@@ -949,21 +1129,66 @@ function installProductChanges(opts) {
|
|
|
949
1129
|
document.body.appendChild(host);
|
|
950
1130
|
return root;
|
|
951
1131
|
};
|
|
952
|
-
|
|
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) => {
|
|
953
1140
|
if (tips.has(change.id)) return true;
|
|
954
|
-
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;
|
|
955
1144
|
const shadow = ensureRoot();
|
|
956
1145
|
if (!shadow) return false;
|
|
1146
|
+
if (host) applyThemeVars(host, readTheme(document, (el) => window.getComputedStyle(el), anchor));
|
|
957
1147
|
const card = document.createElement("div");
|
|
958
|
-
card.className = "tip";
|
|
959
|
-
card.setAttribute("role", "tooltip");
|
|
1148
|
+
card.className = asking ? "tip ask" : "tip";
|
|
1149
|
+
card.setAttribute("role", asking ? "dialog" : "tooltip");
|
|
960
1150
|
const text = document.createElement("p");
|
|
961
1151
|
text.textContent = change.content.text;
|
|
962
1152
|
const close = document.createElement("button");
|
|
963
1153
|
close.type = "button";
|
|
964
1154
|
close.setAttribute("aria-label", "Fechar");
|
|
965
1155
|
close.textContent = "\xD7";
|
|
966
|
-
|
|
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());
|
|
967
1192
|
card.style.top = "-9999px";
|
|
968
1193
|
card.style.left = "-9999px";
|
|
969
1194
|
shadow.appendChild(card);
|
|
@@ -993,6 +1218,11 @@ function installProductChanges(opts) {
|
|
|
993
1218
|
card.style.left = `${Math.max(8, left)}px`;
|
|
994
1219
|
}
|
|
995
1220
|
close.addEventListener("click", () => {
|
|
1221
|
+
if (preview) {
|
|
1222
|
+
dismissedPreview = true;
|
|
1223
|
+
remove();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
996
1226
|
addToSet(storage("local"), DISMISSED_KEY, change.id);
|
|
997
1227
|
opts.track("$change_dismissed", { change_id: change.id, kind: change.kind });
|
|
998
1228
|
remove();
|
|
@@ -1003,20 +1233,143 @@ function installProductChanges(opts) {
|
|
|
1003
1233
|
window.addEventListener("resize", position);
|
|
1004
1234
|
return true;
|
|
1005
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
|
+
};
|
|
1006
1340
|
const scan = () => {
|
|
1007
1341
|
timer = null;
|
|
1008
|
-
const active = changesForPage(changes, location.pathname, opts.getAccountKey());
|
|
1342
|
+
const active = changesForPage(changes, location.pathname, opts.getAccountKey(), previewId);
|
|
1009
1343
|
const activeIds = new Set(active.map((a) => a.change.id));
|
|
1010
1344
|
for (const id of [...copies.keys()]) if (!activeIds.has(id)) restoreCopy(id);
|
|
1011
1345
|
for (const [id, remove] of [...tips]) if (!activeIds.has(id)) remove();
|
|
1012
|
-
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) {
|
|
1013
1353
|
const el = findAnchor(change.anchor);
|
|
1014
|
-
if (!el && change.anchor.kind !== "page")
|
|
1015
|
-
|
|
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") {
|
|
1016
1369
|
if (!el) continue;
|
|
1017
1370
|
const shown = arm === "change" ? applyCopy(el, change) : eligibleCopyNode(el, change) !== null;
|
|
1018
1371
|
if (shown) expose(change, arm);
|
|
1019
|
-
} else if (arm === "holdout" || showTip(el, change)) {
|
|
1372
|
+
} else if (arm === "holdout" || showTip(el, change, false)) {
|
|
1020
1373
|
expose(change, arm);
|
|
1021
1374
|
}
|
|
1022
1375
|
}
|
|
@@ -1026,8 +1379,14 @@ function installProductChanges(opts) {
|
|
|
1026
1379
|
};
|
|
1027
1380
|
const refresh = async () => {
|
|
1028
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();
|
|
1029
1388
|
try {
|
|
1030
|
-
const res = await fetch(`${opts.baseUrl}/runtime/changes`, {
|
|
1389
|
+
const res = await fetch(`${opts.baseUrl}/runtime/changes${qs ? `?${qs}` : ""}`, {
|
|
1031
1390
|
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
1032
1391
|
});
|
|
1033
1392
|
if (!res.ok) {
|
|
@@ -1052,9 +1411,28 @@ function installProductChanges(opts) {
|
|
|
1052
1411
|
void refresh();
|
|
1053
1412
|
return {
|
|
1054
1413
|
onNavigate: () => {
|
|
1055
|
-
|
|
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();
|
|
1056
1423
|
schedule();
|
|
1057
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
|
+
},
|
|
1058
1436
|
refresh,
|
|
1059
1437
|
teardown: () => {
|
|
1060
1438
|
clearInterval(interval);
|
|
@@ -1062,6 +1440,7 @@ function installProductChanges(opts) {
|
|
|
1062
1440
|
observer?.disconnect();
|
|
1063
1441
|
for (const id of [...copies.keys()]) restoreCopy(id);
|
|
1064
1442
|
for (const remove of [...tips.values()]) remove();
|
|
1443
|
+
for (const id of [...lists.keys()]) lists.delete(id);
|
|
1065
1444
|
host?.remove();
|
|
1066
1445
|
host = null;
|
|
1067
1446
|
root = null;
|
|
@@ -1138,6 +1517,10 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1138
1517
|
super.group(accountKey, traits);
|
|
1139
1518
|
this.changes?.onNavigate();
|
|
1140
1519
|
}
|
|
1520
|
+
track(name, properties, overrides) {
|
|
1521
|
+
super.track(name, properties, overrides);
|
|
1522
|
+
if (!name.startsWith("$")) this.changes?.onEvent(name);
|
|
1523
|
+
}
|
|
1141
1524
|
reset() {
|
|
1142
1525
|
this.userId = null;
|
|
1143
1526
|
this.accountKey = null;
|
|
@@ -1145,6 +1528,7 @@ var HoldyWeb = class extends HoldyCore {
|
|
|
1145
1528
|
localStorage.removeItem(ANON_STORAGE_KEY);
|
|
1146
1529
|
} catch {
|
|
1147
1530
|
}
|
|
1531
|
+
clearSessionId();
|
|
1148
1532
|
this.anonymousId = resolveAnonymousId();
|
|
1149
1533
|
this.changes?.onNavigate();
|
|
1150
1534
|
}
|