@lightworkai.official/debug-capture 0.19.5 → 0.19.6
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 +12 -2
- package/dist/config.d.ts +11 -2
- package/dist/debug-capture.js +16 -16
- package/dist/index.mjs +23 -23
- package/dist/ui/bubblePlace.d.ts +104 -0
- package/dist/ui/chatBubble.d.ts +5 -0
- package/package.json +1 -1
- package/src/config.ts +11 -2
- package/src/ui/bubblePlace.ts +258 -0
- package/src/ui/chatBubble.ts +165 -9
package/src/ui/chatBubble.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* sign-in the panel asks (`hello`); the host's token goes to it by postMessage, to the support host's origin only
|
|
11
11
|
* แจ้งปัญหา… in the panel: the panel folds and this library's report opens — its screen, its page — linked to the chat
|
|
12
12
|
* the corner the call's card and frame sit above the bubble (`--lw-chat-lift`)
|
|
13
|
+
* its place out of the page's own floating controls, and where the desk says for a page (0.19.6, ui/bubblePlace)
|
|
13
14
|
*
|
|
14
15
|
* Shown only for a signed-in person (`identity` answers a token): a chat is someone's, and is read back.
|
|
15
16
|
*/
|
|
@@ -17,6 +18,7 @@ import { chatPlace, getConfig } from "../config";
|
|
|
17
18
|
import { hostToken, onLiveFrame } from "../live";
|
|
18
19
|
import { openSupportPortal } from "../portal";
|
|
19
20
|
import { launch } from "../reporter";
|
|
21
|
+
import { blockedRanges, placeFor, scanColumns, settleSpot, type PlaceRule, type Side } from "./bubblePlace";
|
|
20
22
|
import { followHostFont, OWN_FONTS } from "./hostFont";
|
|
21
23
|
import { strings } from "./strings";
|
|
22
24
|
|
|
@@ -214,7 +216,7 @@ function setLabel(): void {
|
|
|
214
216
|
bubble.classList.toggle("unlabelled", !l);
|
|
215
217
|
bubble.classList.toggle("labelled", !!l?.shown);
|
|
216
218
|
bubble.classList.toggle("new", !!l?.isNew);
|
|
217
|
-
bubble.classList.toggle("at-left", spot.side === "left");
|
|
219
|
+
bubble.classList.toggle("at-left", (shown ?? spot).side === "left");
|
|
218
220
|
label.querySelector(".text")!.textContent = l?.text ?? "";
|
|
219
221
|
}
|
|
220
222
|
|
|
@@ -227,6 +229,10 @@ export interface DeskLook {
|
|
|
227
229
|
color?: string;
|
|
228
230
|
/** A path on the support host; null: the chat glyph on the colour. */
|
|
229
231
|
faceUrl?: string | null;
|
|
232
|
+
/** Step aside from the page's own floating controls (0.19.6) — the desk's switch; on unless false. */
|
|
233
|
+
avoid?: boolean;
|
|
234
|
+
/** A page's own place (0.19.6): hidden there, or started in a corner at a height — the desk's rules. */
|
|
235
|
+
places?: PlaceRule[];
|
|
230
236
|
}
|
|
231
237
|
let deskLook: DeskLook | null = null;
|
|
232
238
|
/** The face's address for a desk's look: its own path on the support host, the shipped one, or none (the glyph). Pure. */
|
|
@@ -247,6 +253,8 @@ function wearLook(look: DeskLook | null): void {
|
|
|
247
253
|
else root.style.removeProperty("--lw-chat-color");
|
|
248
254
|
const face = shadow?.querySelector<HTMLImageElement>(".face");
|
|
249
255
|
const url = lookFaceUrl(getConfig().host, look);
|
|
256
|
+
// Its rules and its switch may have changed where it stands.
|
|
257
|
+
queueRescan(true);
|
|
250
258
|
if (face) {
|
|
251
259
|
if (!url) {
|
|
252
260
|
bubble.classList.remove("has-face");
|
|
@@ -303,6 +311,7 @@ function show(on: boolean): void {
|
|
|
303
311
|
// Its words in the host page's own font — read again here, after the host has drawn itself.
|
|
304
312
|
if (on) followHostFont(root);
|
|
305
313
|
layout();
|
|
314
|
+
if (on) queueRescan(true);
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
/* ── Where it is: the config's place, or where the person put it ───────── */
|
|
@@ -339,24 +348,154 @@ function savePlace(): void {
|
|
|
339
348
|
}
|
|
340
349
|
}
|
|
341
350
|
|
|
351
|
+
/* ── Out of the page's way, and a page's own place (0.19.6, ui/bubblePlace) ─────── */
|
|
352
|
+
|
|
353
|
+
/** The host page's path — through `window`, as every other read here is. */
|
|
354
|
+
const pagePath = () => window.location?.pathname ?? "/";
|
|
355
|
+
/** Where it stands now — `spot` (or the page's rule), moved clear of the page's floating controls. */
|
|
356
|
+
let shown: BubbleSpot | null = null;
|
|
357
|
+
/** What `shown` was chosen for — the page and the wish; while they hold, a free `shown` is kept (settleSpot). */
|
|
358
|
+
let shownFor = "";
|
|
359
|
+
/** The heights each side's column rules out, as last read off the page — null: not read yet, or not stepping aside. */
|
|
360
|
+
let blocked: Record<Side, [number, number][]> | null = null;
|
|
361
|
+
/** Hidden on this page by the desk's rule (ซ่อนปุ่มแชทในหน้านี้). */
|
|
362
|
+
let ruleHidden = false;
|
|
363
|
+
|
|
364
|
+
function avoidSetting(): { on: boolean; extra: string | null } {
|
|
365
|
+
const c = getConfig().chat;
|
|
366
|
+
const own = c && c !== true ? c.avoid : undefined;
|
|
367
|
+
if (own === false || deskLook?.avoid === false) return { on: false, extra: null };
|
|
368
|
+
return { on: true, extra: typeof own === "string" && own.trim() ? own.trim() : null };
|
|
369
|
+
}
|
|
370
|
+
/** Where it wants to be on this page: put by hand — kept; else the desk's rule for the page; else the config's place. */
|
|
371
|
+
function wanted(): BubbleSpot {
|
|
372
|
+
const rule = placeFor(pagePath(), deskLook?.places);
|
|
373
|
+
ruleHidden = rule?.show === "hide";
|
|
374
|
+
if (movedByHand || !rule || rule.show === "hide") return spot;
|
|
375
|
+
return { side: rule.show, x: spot.x, y: rule.y };
|
|
376
|
+
}
|
|
377
|
+
/** Read the page's floating controls across both corners' columns — then place again. */
|
|
378
|
+
function rescan(): void {
|
|
379
|
+
if (!bubble) return;
|
|
380
|
+
const { on, extra } = avoidSetting();
|
|
381
|
+
if (!on) {
|
|
382
|
+
blocked = null;
|
|
383
|
+
return layout();
|
|
384
|
+
}
|
|
385
|
+
const v = view();
|
|
386
|
+
const x = spot.x;
|
|
387
|
+
const colR = { left: v.w - x - SIZE, right: v.w - x };
|
|
388
|
+
const colL = { left: x, right: x + SIZE };
|
|
389
|
+
const [foundR = [], foundL = []] = scanColumns([colR, colL], v, { extra });
|
|
390
|
+
const right = blockedRanges(foundR, colR, v.h, SIZE);
|
|
391
|
+
const left = blockedRanges(foundL, colL, v.h, SIZE);
|
|
392
|
+
// Nothing new where it could stand: the page is looked at less often while that holds (queueRescan).
|
|
393
|
+
calm = JSON.stringify([right, left]) === JSON.stringify(blocked && [blocked.right, blocked.left]) ? Math.min(calm + 1, CALM_STEPS) : 0;
|
|
394
|
+
blocked = { right, left };
|
|
395
|
+
layout();
|
|
396
|
+
}
|
|
397
|
+
/*
|
|
398
|
+
* How often the page is read (a read is two columns of hit tests, a few ms). A page that keeps changing — a clock, a
|
|
399
|
+
* live list — said "look again" ten times a second, and a read every 400 ms cost it half a second of script in ten
|
|
400
|
+
* (0.19.5: 20 ms). So: a read that finds nothing new doubles the wait for the next one a change asks for (400 ms up to
|
|
401
|
+
* 3.2 s); a person's own press or key, a new page, a resize or the tab coming back is looked at at once (URGENT_MS),
|
|
402
|
+
* and resets it. Never while the panel is open (it does not move then), while it is dragged, or in a hidden tab.
|
|
403
|
+
*/
|
|
404
|
+
const RESCAN_MS = 400;
|
|
405
|
+
const CALM_STEPS = 3;
|
|
406
|
+
const URGENT_MS = 60;
|
|
407
|
+
let rescanTimer: ReturnType<typeof setTimeout> | undefined;
|
|
408
|
+
let rescanDue = 0;
|
|
409
|
+
let lastRescan = 0;
|
|
410
|
+
let calm = 0;
|
|
411
|
+
function queueRescan(urgent = false): void {
|
|
412
|
+
if (!root) return;
|
|
413
|
+
const now = Date.now();
|
|
414
|
+
if (urgent) {
|
|
415
|
+
calm = 0;
|
|
416
|
+
if (rescanTimer && rescanDue - now <= URGENT_MS) return;
|
|
417
|
+
clearTimeout(rescanTimer);
|
|
418
|
+
rescanTimer = undefined;
|
|
419
|
+
}
|
|
420
|
+
if (rescanTimer) return;
|
|
421
|
+
const wait = urgent ? URGENT_MS : Math.max(0, RESCAN_MS * 2 ** calm - (now - lastRescan));
|
|
422
|
+
rescanDue = now + wait;
|
|
423
|
+
rescanTimer = setTimeout(() => {
|
|
424
|
+
rescanTimer = undefined;
|
|
425
|
+
if (!root || isOpen || pressed?.moved || (typeof document !== "undefined" && document.hidden)) return;
|
|
426
|
+
lastRescan = Date.now();
|
|
427
|
+
rescan();
|
|
428
|
+
}, wait);
|
|
429
|
+
}
|
|
430
|
+
/** The page's own changes worth a look: a toolbar opening, a page arriving — never this library's own boxes. */
|
|
431
|
+
let watcher: MutationObserver | null = null;
|
|
432
|
+
let lastPath = "";
|
|
433
|
+
let pathTimer: ReturnType<typeof setInterval> | undefined;
|
|
434
|
+
const onPageMoved = () => queueRescan();
|
|
435
|
+
/** What a person did, or the window itself changed: looked at at once. */
|
|
436
|
+
const onPageActed = () => queueRescan(true);
|
|
437
|
+
const ACTED = ["popstate", "hashchange", "pointerup", "keyup"] as const;
|
|
438
|
+
function watchPage(on: boolean): void {
|
|
439
|
+
watcher?.disconnect();
|
|
440
|
+
watcher = null;
|
|
441
|
+
clearInterval(pathTimer);
|
|
442
|
+
for (const t of ACTED) window.removeEventListener(t, onPageActed, true);
|
|
443
|
+
for (const t of ["scroll", "transitionend", "animationend"] as const) window.removeEventListener(t, onPageMoved, true);
|
|
444
|
+
document.removeEventListener("visibilitychange", onPageActed);
|
|
445
|
+
if (!on) return;
|
|
446
|
+
if (typeof MutationObserver !== "undefined") {
|
|
447
|
+
watcher = new MutationObserver((records) => {
|
|
448
|
+
const theirs = records.some((r) => {
|
|
449
|
+
const t = r.target as Element;
|
|
450
|
+
if (t === document.documentElement && r.type === "attributes") return false;
|
|
451
|
+
return !(t.nodeType === 1 && /^LW-/.test(t.tagName)) && t !== root;
|
|
452
|
+
});
|
|
453
|
+
if (theirs) queueRescan();
|
|
454
|
+
});
|
|
455
|
+
watcher.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"] });
|
|
456
|
+
}
|
|
457
|
+
for (const t of ACTED) window.addEventListener(t, onPageActed, { capture: true, passive: true });
|
|
458
|
+
// A toolbar's own opening ends after the press; a sticky bar sticks as the page scrolls.
|
|
459
|
+
for (const t of ["scroll", "transitionend", "animationend"] as const) window.addEventListener(t, onPageMoved, { capture: true, passive: true });
|
|
460
|
+
document.addEventListener("visibilitychange", onPageActed);
|
|
461
|
+
// A single-page app moves by history.pushState, which says nothing: the path is looked at once a second.
|
|
462
|
+
lastPath = pagePath();
|
|
463
|
+
pathTimer = setInterval(() => {
|
|
464
|
+
if (pagePath() === lastPath) return;
|
|
465
|
+
lastPath = pagePath();
|
|
466
|
+
layout();
|
|
467
|
+
queueRescan(true);
|
|
468
|
+
}, 1000);
|
|
469
|
+
}
|
|
470
|
+
|
|
342
471
|
/** Everything placed from `spot` and `panelAt`, inside the window as it is now — and the call's card clear of the bubble. */
|
|
343
472
|
function layout(): void {
|
|
344
473
|
if (!bubble || !panel) return;
|
|
345
474
|
const v = view();
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
475
|
+
const want = wanted();
|
|
476
|
+
const wantY = clampBubbleY(want.y, v.h);
|
|
477
|
+
// While the panel is open it stays where it was opened from — never moved under the person's eyes.
|
|
478
|
+
const wish = `${pagePath()}|${want.side}|${want.x}|${wantY}`;
|
|
479
|
+
const prev = shown && shownFor === wish ? shown : null;
|
|
480
|
+
const free = !isOpen || !shown ? (blocked && avoidSetting().on ? settleSpot(prev, { side: want.side, y: wantY }, blocked, MARGIN, Math.max(MARGIN, v.h - SIZE - MARGIN)) : { side: want.side, y: wantY }) : shown;
|
|
481
|
+
shown = { side: free.side, x: want.x, y: free.y };
|
|
482
|
+
shownFor = wish;
|
|
483
|
+
const at = shown;
|
|
484
|
+
const y = at.y;
|
|
485
|
+
const other = at.side === "right" ? "left" : "right";
|
|
486
|
+
bubble.style.display = ruleHidden && !isOpen ? "none" : "";
|
|
487
|
+
Object.assign(bubble.style, { [at.side]: `${at.x}px`, [other]: "", bottom: `${y}px`, top: "" });
|
|
349
488
|
if (panelAt) {
|
|
350
489
|
// The height it had when it was picked up — a drag moves it, never resizes it under the hand.
|
|
351
490
|
const box = { w: Math.min(PANEL_W, v.w - 32), h: Math.min(panelAt.h, v.h - 2 * MARGIN) };
|
|
352
491
|
const p = clampPanel(panelAt, box, v);
|
|
353
492
|
Object.assign(panel.style, { left: `${p.x}px`, top: `${p.y}px`, right: "", bottom: "", height: `${box.h}px` });
|
|
354
493
|
} else {
|
|
355
|
-
const a = anchorPanel(
|
|
356
|
-
Object.assign(panel.style, { [
|
|
494
|
+
const a = anchorPanel(at, v.h);
|
|
495
|
+
Object.assign(panel.style, { [at.side]: `${at.x}px`, [other]: "", bottom: a.bottom !== undefined ? `${a.bottom}px` : "", top: a.top !== undefined ? `${a.top}px` : "", height: `${a.height}px` });
|
|
357
496
|
}
|
|
358
497
|
// The call's card and frame sit above the bubble while it is there and low on their side (ring.ts, callFrame.ts).
|
|
359
|
-
const lift = root && root.style.display !== "none" ? liftFor(
|
|
498
|
+
const lift = root && root.style.display !== "none" && bubble.style.display !== "none" ? liftFor(at) : 0;
|
|
360
499
|
if (lift) document.documentElement.style.setProperty("--lw-chat-lift", `${lift}px`);
|
|
361
500
|
else document.documentElement.style.removeProperty("--lw-chat-lift");
|
|
362
501
|
setLabel();
|
|
@@ -398,7 +537,9 @@ function onBubbleUp(e: PointerEvent): void {
|
|
|
398
537
|
panelAt = null;
|
|
399
538
|
savePlace();
|
|
400
539
|
}
|
|
401
|
-
|
|
540
|
+
// Let go on a toolbar: to the nearest free place, read off the page as it is now.
|
|
541
|
+
if (avoidSetting().on) rescan();
|
|
542
|
+
else layout();
|
|
402
543
|
}
|
|
403
544
|
|
|
404
545
|
/* ── The panel, dragged by its head — the press is in the frame (as the call's, ui/callFrame) ─── */
|
|
@@ -443,7 +584,10 @@ function followHere(on: boolean): void {
|
|
|
443
584
|
};
|
|
444
585
|
for (const t of ["pointermove", "pointerup", "pointercancel"] as const) window.addEventListener(t, hostFollow, true);
|
|
445
586
|
}
|
|
446
|
-
const onResize = () =>
|
|
587
|
+
const onResize = () => {
|
|
588
|
+
layout();
|
|
589
|
+
queueRescan(true);
|
|
590
|
+
};
|
|
447
591
|
|
|
448
592
|
function post(message: Record<string, unknown>): void {
|
|
449
593
|
frame?.contentWindow?.postMessage({ source: HOST, ...message }, hostOrigin());
|
|
@@ -519,6 +663,9 @@ export function closeSupportChat(refocus = true): void {
|
|
|
519
663
|
post({ type: "hidden" });
|
|
520
664
|
if (refocus) bubble?.focus();
|
|
521
665
|
void refreshUnread();
|
|
666
|
+
// Closed: free to step aside again from whatever the page did meanwhile.
|
|
667
|
+
layout();
|
|
668
|
+
queueRescan(true);
|
|
522
669
|
}
|
|
523
670
|
|
|
524
671
|
let stopFrames: (() => void) | null = null;
|
|
@@ -571,6 +718,7 @@ export function mountSupportChat(): void {
|
|
|
571
718
|
document.addEventListener("keydown", onKey);
|
|
572
719
|
window.addEventListener("focus", onFocus);
|
|
573
720
|
window.addEventListener("resize", onResize);
|
|
721
|
+
watchPage(true);
|
|
574
722
|
// The desk wrote, or opened a เรื่อง, or read: the badge asks again — while the panel is open, the panel tells it.
|
|
575
723
|
let settle: ReturnType<typeof setTimeout> | undefined;
|
|
576
724
|
stopFrames = onLiveFrame(() => {
|
|
@@ -595,6 +743,14 @@ export function unmountSupportChat(): void {
|
|
|
595
743
|
document.removeEventListener("keydown", onKey);
|
|
596
744
|
window.removeEventListener("focus", onFocus);
|
|
597
745
|
window.removeEventListener("resize", onResize);
|
|
746
|
+
watchPage(false);
|
|
747
|
+
clearTimeout(rescanTimer);
|
|
748
|
+
rescanTimer = undefined;
|
|
749
|
+
calm = 0;
|
|
750
|
+
shown = null;
|
|
751
|
+
shownFor = "";
|
|
752
|
+
blocked = null;
|
|
753
|
+
ruleHidden = false;
|
|
598
754
|
followHere(false);
|
|
599
755
|
root?.remove();
|
|
600
756
|
root = shadow = panel = frame = bubble = badge = label = null;
|