@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.
@@ -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 y = clampBubbleY(spot.y, v.h);
347
- const other = spot.side === "right" ? "left" : "right";
348
- Object.assign(bubble.style, { [spot.side]: `${spot.x}px`, [other]: "", bottom: `${y}px`, top: "" });
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({ ...spot, y }, v.h);
356
- Object.assign(panel.style, { [spot.side]: `${spot.x}px`, [other]: "", bottom: a.bottom !== undefined ? `${a.bottom}px` : "", top: a.top !== undefined ? `${a.top}px` : "", height: `${a.height}px` });
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({ ...spot, y }) : 0;
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
- layout();
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 = () => layout();
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;