@lightworkai.official/debug-capture 0.19.6 → 0.19.7
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 +1 -1
- package/dist/debug-capture.js +14 -14
- package/dist/index.mjs +22 -22
- package/dist/ui/bubblePlace.d.ts +9 -5
- package/package.json +1 -1
- package/src/ui/bubblePlace.ts +28 -9
- package/src/ui/chatBubble.ts +63 -33
package/dist/ui/bubblePlace.d.ts
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* more (`chat.avoid: "<selector>"`) or turn it off (`false`).
|
|
16
16
|
*
|
|
17
17
|
* THE NEAREST FREE PLACE. Up or down its own side first; the other side when that is nearer than a long climb
|
|
18
|
-
* (`SWITCH_COST`) — a column the height of the window sends it to the other corner, a FAB lifts it above.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* (`SWITCH_COST`) — a column the height of the window sends it to the other corner, a FAB lifts it above. When a
|
|
19
|
+
* nearer place frees up it goes back (`settleSpot`): the tray shut, it is above the toggle again. 0.19.6 stayed
|
|
20
|
+
* where it had stepped to for the rest of the page, which on the real เอกสาร page left it high on the right over
|
|
21
|
+
* the viewer's ยังไม่มีการวางตรา once the tray was shut. A shift of less than STAY_MARGIN does not move it — no
|
|
22
|
+
* jitter as the page settles.
|
|
21
23
|
*
|
|
22
24
|
* A PAGE'S OWN PLACE. The desk's rules (ปุ่มแชท › ตำแหน่งปุ่ม): pages whose path starts so — hidden there, or
|
|
23
25
|
* started in a corner at a height. The most specific rule wins; a person who moved it keeps their place.
|
|
@@ -35,6 +37,8 @@ export type Side = "right" | "left";
|
|
|
35
37
|
export declare const CLEAR = 8;
|
|
36
38
|
/** What moving to the other side costs, in px of climbing — a short lift stays on its side, a long one crosses. */
|
|
37
39
|
export declare const SWITCH_COST = 160;
|
|
40
|
+
/** A free place this much nearer (in the same px of cost) is needed to move it from one it already stands in. */
|
|
41
|
+
export declare const STAY_MARGIN = 48;
|
|
38
42
|
/** An obstacle is smaller than this share of the window — a box as big as the page is the page. */
|
|
39
43
|
export declare const OBSTACLE_MAX_AREA = 0.34;
|
|
40
44
|
/**
|
|
@@ -59,8 +63,8 @@ export declare function chooseSpot(want: {
|
|
|
59
63
|
y: number;
|
|
60
64
|
};
|
|
61
65
|
/**
|
|
62
|
-
* Where it stands now, given where it stood:
|
|
63
|
-
* (`prev` is null when the wish, the page or the person's own place changed)
|
|
66
|
+
* Where it stands now, given where it stood: the best place (`chooseSpot`), unless where it stands is still free and
|
|
67
|
+
* within STAY_MARGIN of it (`prev` is null when the wish, the page or the person's own place changed). Pure.
|
|
64
68
|
*/
|
|
65
69
|
export declare function settleSpot(prev: {
|
|
66
70
|
side: Side;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightworkai.official/debug-capture",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.7",
|
|
4
4
|
"description": "Report-a-problem widget: rolling network/console/action capture, optional rrweb session replay, annotated screenshot, and submission to a Lightwork Support host",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.mjs",
|
package/src/ui/bubblePlace.ts
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* more (`chat.avoid: "<selector>"`) or turn it off (`false`).
|
|
16
16
|
*
|
|
17
17
|
* THE NEAREST FREE PLACE. Up or down its own side first; the other side when that is nearer than a long climb
|
|
18
|
-
* (`SWITCH_COST`) — a column the height of the window sends it to the other corner, a FAB lifts it above.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* (`SWITCH_COST`) — a column the height of the window sends it to the other corner, a FAB lifts it above. When a
|
|
19
|
+
* nearer place frees up it goes back (`settleSpot`): the tray shut, it is above the toggle again. 0.19.6 stayed
|
|
20
|
+
* where it had stepped to for the rest of the page, which on the real เอกสาร page left it high on the right over
|
|
21
|
+
* the viewer's ยังไม่มีการวางตรา once the tray was shut. A shift of less than STAY_MARGIN does not move it — no
|
|
22
|
+
* jitter as the page settles.
|
|
21
23
|
*
|
|
22
24
|
* A PAGE'S OWN PLACE. The desk's rules (ปุ่มแชท › ตำแหน่งปุ่ม): pages whose path starts so — hidden there, or
|
|
23
25
|
* started in a corner at a height. The most specific rule wins; a person who moved it keeps their place.
|
|
@@ -37,6 +39,8 @@ export type Side = "right" | "left";
|
|
|
37
39
|
export const CLEAR = 8;
|
|
38
40
|
/** What moving to the other side costs, in px of climbing — a short lift stays on its side, a long one crosses. */
|
|
39
41
|
export const SWITCH_COST = 160;
|
|
42
|
+
/** A free place this much nearer (in the same px of cost) is needed to move it from one it already stands in. */
|
|
43
|
+
export const STAY_MARGIN = 48;
|
|
40
44
|
/** An obstacle is smaller than this share of the window — a box as big as the page is the page. */
|
|
41
45
|
export const OBSTACLE_MAX_AREA = 0.34;
|
|
42
46
|
|
|
@@ -86,9 +90,12 @@ export function chooseSpot(want: { side: Side; y: number }, free: Record<Side, r
|
|
|
86
90
|
return costHere <= costThere ? { side: want.side, y: here! } : { side: other, y: there! };
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
/** What a place costs against the wish: the climb, and SWITCH_COST more on the other side. */
|
|
94
|
+
const costOf = (spot: { side: Side; y: number }, want: { side: Side; y: number }) => Math.abs(spot.y - want.y) + (spot.side === want.side ? 0 : SWITCH_COST);
|
|
95
|
+
|
|
89
96
|
/**
|
|
90
|
-
* Where it stands now, given where it stood:
|
|
91
|
-
* (`prev` is null when the wish, the page or the person's own place changed)
|
|
97
|
+
* Where it stands now, given where it stood: the best place (`chooseSpot`), unless where it stands is still free and
|
|
98
|
+
* within STAY_MARGIN of it (`prev` is null when the wish, the page or the person's own place changed). Pure.
|
|
92
99
|
*/
|
|
93
100
|
export function settleSpot(
|
|
94
101
|
prev: { side: Side; y: number } | null,
|
|
@@ -97,8 +104,9 @@ export function settleSpot(
|
|
|
97
104
|
min: number,
|
|
98
105
|
max: number,
|
|
99
106
|
): { side: Side; y: number } {
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
const best = chooseSpot(want, free, min, max);
|
|
108
|
+
if (prev && nearestFreeY(prev.y, free[prev.side], min, max) === prev.y && costOf(prev, want) <= costOf(best, want) + STAY_MARGIN) return prev;
|
|
109
|
+
return best;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
/* ── A page's own place: the desk's rules ───────────────────────────────── */
|
|
@@ -157,7 +165,8 @@ const SCROLLS = /(auto|scroll|overlay)/;
|
|
|
157
165
|
*/
|
|
158
166
|
export function scanColumns(cols: readonly { left: number; right: number }[], view: { w: number; h: number }, opts: { step?: number; extra?: string | null } = {}): Rect[][] {
|
|
159
167
|
if (typeof document === "undefined" || typeof document.elementsFromPoint !== "function") return cols.map(() => []);
|
|
160
|
-
|
|
168
|
+
// 16 px: anything a hand presses is taller. Each point is a hit test, ~30 µs on the ERP's เอกสาร page.
|
|
169
|
+
const step = opts.step ?? 16;
|
|
161
170
|
const html = document.documentElement;
|
|
162
171
|
const body = document.body;
|
|
163
172
|
const styles = new Map<Element, CSSStyleDeclaration>();
|
|
@@ -205,6 +214,16 @@ export function scanColumns(cols: readonly { left: number; right: number }[], vi
|
|
|
205
214
|
return { float, scrolled, opacity };
|
|
206
215
|
};
|
|
207
216
|
const pins = new Map<Element, ReturnType<typeof pinOf>>();
|
|
217
|
+
/*
|
|
218
|
+
* The page's topmost piece at a point. elementsFromPoint hit-tests every layer down to the root — 450 of them took
|
|
219
|
+
* 30–37 ms on the ERP's เอกสาร page, against 14 ms for elementFromPoint, which stops at the first; the whole stack
|
|
220
|
+
* is asked for only where this library's own box is the first.
|
|
221
|
+
*/
|
|
222
|
+
const topAt = (x: number, y: number): Element | null => {
|
|
223
|
+
const first = document.elementFromPoint(x, y);
|
|
224
|
+
if (!first || !OURS.test(first.tagName)) return first;
|
|
225
|
+
return document.elementsFromPoint(x, y).find((e) => !OURS.test(e.tagName)) ?? null;
|
|
226
|
+
};
|
|
208
227
|
return cols.map((col) => {
|
|
209
228
|
const seen = new Set<Element>();
|
|
210
229
|
const kept = new Set<Element>();
|
|
@@ -224,7 +243,7 @@ export function scanColumns(cols: readonly { left: number; right: number }[], vi
|
|
|
224
243
|
const xs = [col.left + 6, (col.left + col.right) / 2, col.right - 6];
|
|
225
244
|
for (let y = step / 2; y < view.h; y += step) {
|
|
226
245
|
for (const x of xs) {
|
|
227
|
-
const top =
|
|
246
|
+
const top = topAt(x, y);
|
|
228
247
|
if (!top || top === html || top === body || seen.has(top)) continue;
|
|
229
248
|
seen.add(top);
|
|
230
249
|
if (opts.extra) {
|
package/src/ui/chatBubble.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { chatPlace, getConfig } from "../config";
|
|
|
18
18
|
import { hostToken, onLiveFrame } from "../live";
|
|
19
19
|
import { openSupportPortal } from "../portal";
|
|
20
20
|
import { launch } from "../reporter";
|
|
21
|
-
import { blockedRanges, placeFor, scanColumns, settleSpot, type PlaceRule, type Side } from "./bubblePlace";
|
|
21
|
+
import { blockedRanges, nearestFreeY, placeFor, scanColumns, settleSpot, type PlaceRule, type Side } from "./bubblePlace";
|
|
22
22
|
import { followHostFont, OWN_FONTS } from "./hostFont";
|
|
23
23
|
import { strings } from "./strings";
|
|
24
24
|
|
|
@@ -254,7 +254,7 @@ function wearLook(look: DeskLook | null): void {
|
|
|
254
254
|
const face = shadow?.querySelector<HTMLImageElement>(".face");
|
|
255
255
|
const url = lookFaceUrl(getConfig().host, look);
|
|
256
256
|
// Its rules and its switch may have changed where it stands.
|
|
257
|
-
queueRescan(
|
|
257
|
+
queueRescan("now");
|
|
258
258
|
if (face) {
|
|
259
259
|
if (!url) {
|
|
260
260
|
bubble.classList.remove("has-face");
|
|
@@ -311,7 +311,7 @@ function show(on: boolean): void {
|
|
|
311
311
|
// Its words in the host page's own font — read again here, after the host has drawn itself.
|
|
312
312
|
if (on) followHostFont(root);
|
|
313
313
|
layout();
|
|
314
|
-
if (on) queueRescan(
|
|
314
|
+
if (on) queueRescan("now");
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
/* ── Where it is: the config's place, or where the person put it ───────── */
|
|
@@ -356,8 +356,11 @@ const pagePath = () => window.location?.pathname ?? "/";
|
|
|
356
356
|
let shown: BubbleSpot | null = null;
|
|
357
357
|
/** What `shown` was chosen for — the page and the wish; while they hold, a free `shown` is kept (settleSpot). */
|
|
358
358
|
let shownFor = "";
|
|
359
|
-
/**
|
|
360
|
-
|
|
359
|
+
/**
|
|
360
|
+
* The heights each side's column rules out, as last read off the page — null: not read yet, or not stepping aside. A
|
|
361
|
+
* side is null when it was not read: its own place was free, so the other corner could not be better.
|
|
362
|
+
*/
|
|
363
|
+
let blocked: Record<Side, [number, number][] | null> | null = null;
|
|
361
364
|
/** Hidden on this page by the desk's rule (ซ่อนปุ่มแชทในหน้านี้). */
|
|
362
365
|
let ruleHidden = false;
|
|
363
366
|
|
|
@@ -384,22 +387,29 @@ function rescan(): void {
|
|
|
384
387
|
}
|
|
385
388
|
const v = view();
|
|
386
389
|
const x = spot.x;
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
const
|
|
390
|
+
const cols: Record<Side, { left: number; right: number }> = { right: { left: v.w - x - SIZE, right: v.w - x }, left: { left: x, right: x + SIZE } };
|
|
391
|
+
const read = (side: Side) => blockedRanges(scanColumns([cols[side]], v, { extra })[0] ?? [], cols[side], v.h, SIZE);
|
|
392
|
+
// Its own side first; the other corner is read only when the place it wants there is taken (half the hit tests).
|
|
393
|
+
const want = wanted();
|
|
394
|
+
const wantY = clampBubbleY(want.y, v.h);
|
|
395
|
+
const other: Side = want.side === "right" ? "left" : "right";
|
|
396
|
+
const here = read(want.side);
|
|
397
|
+
const there = nearestFreeY(wantY, here, MARGIN, Math.max(MARGIN, v.h - SIZE - MARGIN)) === wantY ? null : read(other);
|
|
398
|
+
const next = { [want.side]: here, [other]: there } as Record<Side, [number, number][] | null>;
|
|
392
399
|
// 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 =
|
|
400
|
+
calm = JSON.stringify([next.right, next.left]) === JSON.stringify(blocked && [blocked.right, blocked.left]) ? Math.min(calm + 1, CALM_STEPS) : 0;
|
|
401
|
+
blocked = next;
|
|
395
402
|
layout();
|
|
396
403
|
}
|
|
397
404
|
/*
|
|
398
|
-
* How often the page is read (a read is two
|
|
405
|
+
* How often the page is read (a read is a column or two of hit tests). A page that keeps changing — a clock, a
|
|
399
406
|
* 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
407
|
* (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
408
|
* 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
|
|
409
|
+
* and resets it; a transition or animation that ends in a corner's column — something there just finished appearing
|
|
410
|
+
* — resets it too and is looked at within RESCAN_MS ("soon": a read during the ERP tray's 300 ms fade-in saw it nearly
|
|
411
|
+
* clear, and the next waited out the doubling). Never while the panel is open (it does not move then), while it is
|
|
412
|
+
* dragged, or in a hidden tab.
|
|
403
413
|
*/
|
|
404
414
|
const RESCAN_MS = 400;
|
|
405
415
|
const CALM_STEPS = 3;
|
|
@@ -408,17 +418,14 @@ let rescanTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
408
418
|
let rescanDue = 0;
|
|
409
419
|
let lastRescan = 0;
|
|
410
420
|
let calm = 0;
|
|
411
|
-
function queueRescan(
|
|
421
|
+
function queueRescan(when: "now" | "soon" | "later" = "later"): void {
|
|
412
422
|
if (!root) return;
|
|
413
423
|
const now = Date.now();
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
if (rescanTimer) return;
|
|
421
|
-
const wait = urgent ? URGENT_MS : Math.max(0, RESCAN_MS * 2 ** calm - (now - lastRescan));
|
|
424
|
+
if (when !== "later") calm = 0;
|
|
425
|
+
const wait = when === "now" ? URGENT_MS : Math.max(0, RESCAN_MS * 2 ** calm - (now - lastRescan));
|
|
426
|
+
// One read waiting at a time — moved sooner when this asks sooner.
|
|
427
|
+
if (rescanTimer && rescanDue <= now + wait) return;
|
|
428
|
+
clearTimeout(rescanTimer);
|
|
422
429
|
rescanDue = now + wait;
|
|
423
430
|
rescanTimer = setTimeout(() => {
|
|
424
431
|
rescanTimer = undefined;
|
|
@@ -431,16 +438,31 @@ function queueRescan(urgent = false): void {
|
|
|
431
438
|
let watcher: MutationObserver | null = null;
|
|
432
439
|
let lastPath = "";
|
|
433
440
|
let pathTimer: ReturnType<typeof setInterval> | undefined;
|
|
434
|
-
const
|
|
441
|
+
const ours = (t: EventTarget | null) => t instanceof Element && (t === root || /^LW-/.test(t.tagName));
|
|
442
|
+
const onPageMoved = (e: Event) => {
|
|
443
|
+
if (!ours(e.target)) queueRescan();
|
|
444
|
+
};
|
|
445
|
+
/** A transition or animation ended: soon when it was in a corner's column, where the bubble may stand. */
|
|
446
|
+
const onPageSettled = (e: Event) => {
|
|
447
|
+
if (ours(e.target)) return;
|
|
448
|
+
const r = e.target instanceof Element ? e.target.getBoundingClientRect() : null;
|
|
449
|
+
const v = view();
|
|
450
|
+
const x = spot.x;
|
|
451
|
+
const inColumn = !!r && r.width > 0 && (r.left < x + SIZE || r.right > v.w - x - SIZE);
|
|
452
|
+
queueRescan(inColumn ? "soon" : "later");
|
|
453
|
+
};
|
|
435
454
|
/** What a person did, or the window itself changed: looked at at once. */
|
|
436
|
-
const onPageActed = () =>
|
|
455
|
+
const onPageActed = (e: Event) => {
|
|
456
|
+
if (!ours(e.target)) queueRescan("now");
|
|
457
|
+
};
|
|
437
458
|
const ACTED = ["popstate", "hashchange", "pointerup", "keyup"] as const;
|
|
438
459
|
function watchPage(on: boolean): void {
|
|
439
460
|
watcher?.disconnect();
|
|
440
461
|
watcher = null;
|
|
441
462
|
clearInterval(pathTimer);
|
|
442
463
|
for (const t of ACTED) window.removeEventListener(t, onPageActed, true);
|
|
443
|
-
|
|
464
|
+
window.removeEventListener("scroll", onPageMoved, true);
|
|
465
|
+
for (const t of ["transitionend", "animationend"] as const) window.removeEventListener(t, onPageSettled, true);
|
|
444
466
|
document.removeEventListener("visibilitychange", onPageActed);
|
|
445
467
|
if (!on) return;
|
|
446
468
|
if (typeof MutationObserver !== "undefined") {
|
|
@@ -456,7 +478,8 @@ function watchPage(on: boolean): void {
|
|
|
456
478
|
}
|
|
457
479
|
for (const t of ACTED) window.addEventListener(t, onPageActed, { capture: true, passive: true });
|
|
458
480
|
// A toolbar's own opening ends after the press; a sticky bar sticks as the page scrolls.
|
|
459
|
-
|
|
481
|
+
window.addEventListener("scroll", onPageMoved, { capture: true, passive: true });
|
|
482
|
+
for (const t of ["transitionend", "animationend"] as const) window.addEventListener(t, onPageSettled, { capture: true, passive: true });
|
|
460
483
|
document.addEventListener("visibilitychange", onPageActed);
|
|
461
484
|
// A single-page app moves by history.pushState, which says nothing: the path is looked at once a second.
|
|
462
485
|
lastPath = pagePath();
|
|
@@ -464,7 +487,7 @@ function watchPage(on: boolean): void {
|
|
|
464
487
|
if (pagePath() === lastPath) return;
|
|
465
488
|
lastPath = pagePath();
|
|
466
489
|
layout();
|
|
467
|
-
queueRescan(
|
|
490
|
+
queueRescan("now");
|
|
468
491
|
}, 1000);
|
|
469
492
|
}
|
|
470
493
|
|
|
@@ -477,7 +500,9 @@ function layout(): void {
|
|
|
477
500
|
// While the panel is open it stays where it was opened from — never moved under the person's eyes.
|
|
478
501
|
const wish = `${pagePath()}|${want.side}|${want.x}|${wantY}`;
|
|
479
502
|
const prev = shown && shownFor === wish ? shown : null;
|
|
480
|
-
|
|
503
|
+
// A side not read is free here: it was left unread because the wish's own place was free, and that place wins.
|
|
504
|
+
const read = blocked && avoidSetting().on ? { right: blocked.right ?? [], left: blocked.left ?? [] } : null;
|
|
505
|
+
const free = !isOpen || !shown ? (read ? settleSpot(prev, { side: want.side, y: wantY }, read, MARGIN, Math.max(MARGIN, v.h - SIZE - MARGIN)) : { side: want.side, y: wantY }) : shown;
|
|
481
506
|
shown = { side: free.side, x: want.x, y: free.y };
|
|
482
507
|
shownFor = wish;
|
|
483
508
|
const at = shown;
|
|
@@ -496,8 +521,13 @@ function layout(): void {
|
|
|
496
521
|
}
|
|
497
522
|
// The call's card and frame sit above the bubble while it is there and low on their side (ring.ts, callFrame.ts).
|
|
498
523
|
const lift = root && root.style.display !== "none" && bubble.style.display !== "none" ? liftFor(at) : 0;
|
|
499
|
-
|
|
500
|
-
|
|
524
|
+
// Written only when it differs from what <html> carries (read back, so a host that rewrites its style attribute
|
|
525
|
+
// gets it again): a property there restyles the host's whole page, and layout() runs on every read of the page.
|
|
526
|
+
const liftNow = lift ? `${lift}px` : "";
|
|
527
|
+
if (document.documentElement.style.getPropertyValue("--lw-chat-lift") !== liftNow) {
|
|
528
|
+
if (liftNow) document.documentElement.style.setProperty("--lw-chat-lift", liftNow);
|
|
529
|
+
else document.documentElement.style.removeProperty("--lw-chat-lift");
|
|
530
|
+
}
|
|
501
531
|
setLabel();
|
|
502
532
|
}
|
|
503
533
|
|
|
@@ -586,7 +616,7 @@ function followHere(on: boolean): void {
|
|
|
586
616
|
}
|
|
587
617
|
const onResize = () => {
|
|
588
618
|
layout();
|
|
589
|
-
queueRescan(
|
|
619
|
+
queueRescan("now");
|
|
590
620
|
};
|
|
591
621
|
|
|
592
622
|
function post(message: Record<string, unknown>): void {
|
|
@@ -665,7 +695,7 @@ export function closeSupportChat(refocus = true): void {
|
|
|
665
695
|
void refreshUnread();
|
|
666
696
|
// Closed: free to step aside again from whatever the page did meanwhile.
|
|
667
697
|
layout();
|
|
668
|
-
queueRescan(
|
|
698
|
+
queueRescan("now");
|
|
669
699
|
}
|
|
670
700
|
|
|
671
701
|
let stopFrames: (() => void) | null = null;
|