@lightworkai.official/debug-capture 0.19.7 → 0.19.8

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.
@@ -14,13 +14,14 @@
14
14
  * Content that scrolls under is not: a floating button over content is what a floating button is. A host may name
15
15
  * more (`chat.avoid: "<selector>"`) or turn it off (`false`).
16
16
  *
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. 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.
23
- *
17
+ * THE NEAREST FREE PLACE (`placeSpot`). What a place costs is the climb plus the move in from its edge, and
18
+ * SWITCH_COST more on the other side. Up or down its own column; or a column further in, just past what stands in
19
+ * the way; or the other corner. A FAB lifts it above. A toolbar column as tall as the window puts it just inside the
20
+ * toolbar, at the same height. 0.19.7 only knew the two corner columns, so on a short window, with the ERP's tray
21
+ * filling the right edge and its sidebar the left, it found no free place and stayed on the tray (the owner's
22
+ * screenshot, 11-02). When a nearer place frees up it goes back: the tray shut, it is home again. A shift of less than
23
+ * STAY_MARGIN does not move it, so it does not jitter as the page settles.
24
+
24
25
  * A PAGE'S OWN PLACE. The desk's rules (ปุ่มแชท › ตำแหน่งปุ่ม): pages whose path starts so — hidden there, or
25
26
  * started in a corner at a height. The most specific rule wins; a person who moved it keeps their place.
26
27
  *
@@ -51,31 +52,28 @@ export declare function blockedRanges(obstacles: readonly Rect[], col: {
51
52
  }, viewH: number, size: number): [number, number][];
52
53
  /** The free height nearest `want` within [min, max], outside every range (open ends) — null when none. Up wins a tie. Pure. */
53
54
  export declare function nearestFreeY(want: number, ranges: readonly [number, number][], min: number, max: number): number | null;
54
- /**
55
- * Where the bubble stands: `want` on its side when free, else the nearest free height there — or the other side's when
56
- * that is cheaper (SWITCH_COST plus its own move). Nothing free anywhere: `want`, as it was. Pure.
57
- */
58
- export declare function chooseSpot(want: {
59
- side: Side;
60
- y: number;
61
- }, free: Record<Side, readonly [number, number][]>, min: number, max: number): {
55
+ export interface Spot {
62
56
  side: Side;
57
+ /** From its side's edge, px. */
58
+ x: number;
59
+ /** Up from the bottom, px. */
63
60
  y: number;
64
- };
61
+ }
62
+ /** The page's boxes across the bubble's column at `x` from `side`'s edge — read off the page, or given in the tests. */
63
+ export type ReadColumn = (side: Side, x: number) => readonly Rect[];
64
+ /** How many columns further in it looks on a side, past what stands in the way. */
65
+ export declare const MAX_SHIFTS = 3;
65
66
  /**
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.
67
+ * Where the bubble stands: `want` when it is free there; else the cheapest free place — up or down its column, a column
68
+ * further in just past what stands in the way (a toolbar column as tall as the window: just left of it, at the same
69
+ * height), or the other side's (SWITCH_COST more). What a place costs is the climb plus the move in. A place it already
70
+ * stands in (`prev`), still free and within STAY_MARGIN of the best, is kept — no jitter as the page settles. Columns
71
+ * are read only as needed (`read`), and never past the middle of the window. Nothing free anywhere: `want`. Pure.
68
72
  */
69
- export declare function settleSpot(prev: {
70
- side: Side;
71
- y: number;
72
- } | null, want: {
73
- side: Side;
74
- y: number;
75
- }, free: Record<Side, readonly [number, number][]>, min: number, max: number): {
76
- side: Side;
77
- y: number;
78
- };
73
+ export declare function placeSpot(want: Spot, read: ReadColumn, view: {
74
+ w: number;
75
+ h: number;
76
+ }, size: number, min: number, max: number, prev?: Spot | null): Spot;
79
77
  export interface PlaceRule {
80
78
  /** "/saraban/edoc-management" — the page and every page under it; `*` one segment, `**` any. */
81
79
  path: string;
@@ -93,7 +91,9 @@ export declare function placeFor(pathname: string, rules: readonly PlaceRule[] |
93
91
  * - a host's selector (`extra`): what it names, whole, wherever it is;
94
92
  * - in a small floating box (its nearest `fixed`, or `sticky` stuck at its edge): what shows there, by its own box —
95
93
  * the control it is part of, else itself;
96
- * - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller;
94
+ * - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller — a disabled one
95
+ * too, found by its own selector, as no hit test reaches it (the ERP's ผลการจำแนก panel on a phone, 11-02: its
96
+ * greyed มอบหมายผู้ดำเนินการ was under the bubble);
97
97
  * - anything else scrolls with the page, and is not.
98
98
  */
99
99
  export declare function scanColumns(cols: readonly {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightworkai.official/debug-capture",
3
- "version": "0.19.7",
3
+ "version": "0.19.8",
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",
@@ -14,13 +14,14 @@
14
14
  * Content that scrolls under is not: a floating button over content is what a floating button is. A host may name
15
15
  * more (`chat.avoid: "<selector>"`) or turn it off (`false`).
16
16
  *
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. 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.
23
- *
17
+ * THE NEAREST FREE PLACE (`placeSpot`). What a place costs is the climb plus the move in from its edge, and
18
+ * SWITCH_COST more on the other side. Up or down its own column; or a column further in, just past what stands in
19
+ * the way; or the other corner. A FAB lifts it above. A toolbar column as tall as the window puts it just inside the
20
+ * toolbar, at the same height. 0.19.7 only knew the two corner columns, so on a short window, with the ERP's tray
21
+ * filling the right edge and its sidebar the left, it found no free place and stayed on the tray (the owner's
22
+ * screenshot, 11-02). When a nearer place frees up it goes back: the tray shut, it is home again. A shift of less than
23
+ * STAY_MARGIN does not move it, so it does not jitter as the page settles.
24
+
24
25
  * A PAGE'S OWN PLACE. The desk's rules (ปุ่มแชท › ตำแหน่งปุ่ม): pages whose path starts so — hidden there, or
25
26
  * started in a corner at a height. The most specific rule wins; a person who moved it keeps their place.
26
27
  *
@@ -76,37 +77,58 @@ export function nearestFreeY(want: number, ranges: readonly [number, number][],
76
77
  });
77
78
  }
78
79
 
79
- /**
80
- * Where the bubble stands: `want` on its side when free, else the nearest free height there — or the other side's when
81
- * that is cheaper (SWITCH_COST plus its own move). Nothing free anywhere: `want`, as it was. Pure.
82
- */
83
- export function chooseSpot(want: { side: Side; y: number }, free: Record<Side, readonly [number, number][]>, min: number, max: number): { side: Side; y: number } {
84
- const other: Side = want.side === "right" ? "left" : "right";
85
- const here = nearestFreeY(want.y, free[want.side], min, max);
86
- const there = nearestFreeY(want.y, free[other], min, max);
87
- if (here === null && there === null) return want;
88
- const costHere = here === null ? Infinity : Math.abs(here - want.y);
89
- const costThere = there === null ? Infinity : SWITCH_COST + Math.abs(there - want.y);
90
- return costHere <= costThere ? { side: want.side, y: here! } : { side: other, y: there! };
80
+ export interface Spot {
81
+ side: Side;
82
+ /** From its side's edge, px. */
83
+ x: number;
84
+ /** Up from the bottom, px. */
85
+ y: number;
91
86
  }
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);
87
+ /** The page's boxes across the bubble's column at `x` from `side`'s edge — read off the page, or given in the tests. */
88
+ export type ReadColumn = (side: Side, x: number) => readonly Rect[];
89
+ /** How many columns further in it looks on a side, past what stands in the way. */
90
+ export const MAX_SHIFTS = 3;
95
91
 
96
92
  /**
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.
93
+ * Where the bubble stands: `want` when it is free there; else the cheapest free place — up or down its column, a column
94
+ * further in just past what stands in the way (a toolbar column as tall as the window: just left of it, at the same
95
+ * height), or the other side's (SWITCH_COST more). What a place costs is the climb plus the move in. A place it already
96
+ * stands in (`prev`), still free and within STAY_MARGIN of the best, is kept — no jitter as the page settles. Columns
97
+ * are read only as needed (`read`), and never past the middle of the window. Nothing free anywhere: `want`. Pure.
99
98
  */
100
- export function settleSpot(
101
- prev: { side: Side; y: number } | null,
102
- want: { side: Side; y: number },
103
- free: Record<Side, readonly [number, number][]>,
104
- min: number,
105
- max: number,
106
- ): { side: Side; y: number } {
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;
99
+ export function placeSpot(want: Spot, read: ReadColumn, view: { w: number; h: number }, size: number, min: number, max: number, prev: Spot | null = null): Spot {
100
+ const boxes = new Map<string, readonly Rect[]>();
101
+ const at = (side: Side, x: number) => {
102
+ const k = `${side}:${x}`;
103
+ if (!boxes.has(k)) boxes.set(k, read(side, x));
104
+ return boxes.get(k)!;
105
+ };
106
+ const col = (side: Side, x: number) => (side === "right" ? { left: view.w - x - size, right: view.w - x } : { left: x, right: x + size });
107
+ const ranges = (side: Side, x: number) => blockedRanges(at(side, x), col(side, x), view.h, size);
108
+ const cost = (s: Spot) => Math.abs(s.y - want.y) + Math.abs(s.x - want.x) + (s.side === want.side ? 0 : SWITCH_COST);
109
+ const freeAt = (s: Spot) => nearestFreeY(s.y, ranges(s.side, s.x), min, max) === s.y;
110
+ const keep = (best: Spot) => (prev && (prev.side !== best.side || prev.x !== best.x || prev.y !== best.y) && cost(prev) <= cost(best) + STAY_MARGIN && freeAt(prev) ? prev : best);
111
+ if (freeAt(want)) return keep(want);
112
+ let best: Spot | null = null;
113
+ const other: Side = want.side === "right" ? "left" : "right";
114
+ for (const side of [want.side, other]) {
115
+ const away = side === want.side ? 0 : SWITCH_COST;
116
+ if (best && away >= cost(best)) break;
117
+ let x = want.x;
118
+ for (let i = 0; i <= MAX_SHIFTS; i++) {
119
+ const y = nearestFreeY(want.y, ranges(side, x), min, max);
120
+ if (y !== null && (!best || cost({ side, x, y }) < cost(best))) best = { side, x, y };
121
+ // What stands where it wants to be, at this column: the next column in is just past it.
122
+ const c = col(side, x);
123
+ const band = { top: view.h - want.y - size - CLEAR, bottom: view.h - want.y + CLEAR };
124
+ const inWay = at(side, x).filter((r) => r.top < band.bottom && r.bottom > band.top && r.left < c.right && r.right > c.left);
125
+ if (!inWay.length) break;
126
+ const next = Math.round(side === "right" ? Math.max(...inWay.map((r) => view.w - r.left + CLEAR)) : Math.max(...inWay.map((r) => r.right + CLEAR)));
127
+ if (next <= x || next + size > view.w / 2 || (best && away + next - want.x >= cost(best))) break;
128
+ x = next;
129
+ }
130
+ }
131
+ return best ? keep(best) : want;
110
132
  }
111
133
 
112
134
  /* ── A page's own place: the desk's rules ───────────────────────────────── */
@@ -153,6 +175,8 @@ const OURS = /^LW-/;
153
175
  const CONTROL =
154
176
  'a[href],button,input,select,textarea,summary,label,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="switch"],[role="radio"],[role="option"],[role="slider"],[role="combobox"],[role="textbox"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
155
177
  const SCROLLS = /(auto|scroll|overlay)/;
178
+ /** A control that takes no press now — no hit test finds it (`disabled:pointer-events-none`), yet it shows, and takes one soon. */
179
+ const DISABLED = 'button:disabled,input:disabled,select:disabled,textarea:disabled,[aria-disabled="true"]';
156
180
 
157
181
  /**
158
182
  * The page's pieces the bubble must not cover, for each column — sampled every `step` px down it at three points
@@ -160,7 +184,9 @@ const SCROLLS = /(auto|scroll|overlay)/;
160
184
  * - a host's selector (`extra`): what it names, whole, wherever it is;
161
185
  * - in a small floating box (its nearest `fixed`, or `sticky` stuck at its edge): what shows there, by its own box —
162
186
  * the control it is part of, else itself;
163
- * - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller;
187
+ * - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller — a disabled one
188
+ * too, found by its own selector, as no hit test reaches it (the ERP's ผลการจำแนก panel on a phone, 11-02: its
189
+ * greyed มอบหมายผู้ดำเนินการ was under the bubble);
164
190
  * - anything else scrolls with the page, and is not.
165
191
  */
166
192
  export function scanColumns(cols: readonly { left: number; right: number }[], view: { w: number; h: number }, opts: { step?: number; extra?: string | null } = {}): Rect[][] {
@@ -240,6 +266,8 @@ export function scanColumns(cols: readonly { left: number; right: number }[], vi
240
266
  right: r.right,
241
267
  });
242
268
  };
269
+ /** The frames as big as the window met down this column (null: a page that does not scroll) — their disabled controls are looked for below. */
270
+ const frames = new Set<Element | null>();
243
271
  const xs = [col.left + 6, (col.left + col.right) / 2, col.right - 6];
244
272
  for (let y = step / 2; y < view.h; y += step) {
245
273
  for (const x of xs) {
@@ -268,10 +296,23 @@ export function scanColumns(cols: readonly { left: number; right: number }[], vi
268
296
  continue;
269
297
  }
270
298
  // The page itself — a scrolling page, or what is in a frame as big as the window: only a control that stays put.
299
+ if (!scrolled && (float || !pageScrolls)) frames.add(float);
271
300
  if (scrolled || !own || big(own.getBoundingClientRect())) continue;
272
301
  keep(own);
273
302
  }
274
303
  }
304
+ for (const frame of frames) {
305
+ for (const el of (frame ?? body)?.querySelectorAll(DISABLED) ?? []) {
306
+ const r = el.getBoundingClientRect();
307
+ if (!r.width || !r.height || r.right <= col.left || r.left >= col.right || big(r) || style(el).visibility === "hidden") continue;
308
+ const pin = pins.get(el) ?? (pins.set(el, pinOf(el)), pins.get(el)!);
309
+ if (pin.float !== frame || pin.scrolled || pin.opacity < 0.05) continue;
310
+ // Not under another layer: what is on top at its middle (in this column) is it, or the frame's piece around it.
311
+ const on = topAt(Math.min(Math.max((r.left + r.right) / 2, col.left + 6), col.right - 6), (r.top + r.bottom) / 2);
312
+ if (on && on !== el && !el.contains(on) && !on.contains(el)) continue;
313
+ keep(el);
314
+ }
315
+ }
275
316
  return out;
276
317
  });
277
318
  }
@@ -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, nearestFreeY, placeFor, scanColumns, settleSpot, type PlaceRule, type Side } from "./bubblePlace";
21
+ import { blockedRanges, placeFor, placeSpot, scanColumns, type PlaceRule, type Rect, type Side } from "./bubblePlace";
22
22
  import { followHostFont, OWN_FONTS } from "./hostFont";
23
23
  import { strings } from "./strings";
24
24
 
@@ -354,13 +354,27 @@ function savePlace(): void {
354
354
  const pagePath = () => window.location?.pathname ?? "/";
355
355
  /** Where it stands now — `spot` (or the page's rule), moved clear of the page's floating controls. */
356
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). */
357
+ /** What `shown` was chosen for — the page and the wish; while they hold, a free `shown` is kept (placeSpot's prev). */
358
358
  let shownFor = "";
359
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.
360
+ * The page's boxes across each column read since the last look, for the window size they were read at — null: not
361
+ * looked at yet, or not stepping aside. `placeSpot` asks for columns as it needs them; one not read is read then.
362
362
  */
363
- let blocked: Record<Side, [number, number][] | null> | null = null;
363
+ let columns: { w: number; h: number; boxes: Map<string, Rect[]> } | null = null;
364
+ /** What the last placing saw at home and chose — a look that changes neither is "nothing new" (queueRescan). */
365
+ let placedSig = "";
366
+ function readColumn(side: Side, x: number): Rect[] {
367
+ const v = view();
368
+ if (!columns || columns.w !== v.w || columns.h !== v.h) columns = { w: v.w, h: v.h, boxes: new Map() };
369
+ const k = `${side}:${x}`;
370
+ let boxes = columns.boxes.get(k);
371
+ if (!boxes) {
372
+ const col = side === "right" ? { left: v.w - x - SIZE, right: v.w - x } : { left: x, right: x + SIZE };
373
+ boxes = scanColumns([col], v, { extra: avoidSetting().extra })[0] ?? [];
374
+ columns.boxes.set(k, boxes);
375
+ }
376
+ return boxes;
377
+ }
364
378
  /** Hidden on this page by the desk's rule (ซ่อนปุ่มแชทในหน้านี้). */
365
379
  let ruleHidden = false;
366
380
 
@@ -377,29 +391,19 @@ function wanted(): BubbleSpot {
377
391
  if (movedByHand || !rule || rule.show === "hide") return spot;
378
392
  return { side: rule.show, x: spot.x, y: rule.y };
379
393
  }
380
- /** Read the page's floating controls across both corners' columns — then place again. */
394
+ /** Look at the page afresh — its columns read again as placing asks for them — and place again. */
381
395
  function rescan(): void {
382
396
  if (!bubble) return;
383
- const { on, extra } = avoidSetting();
384
- if (!on) {
385
- blocked = null;
397
+ if (!avoidSetting().on) {
398
+ columns = null;
386
399
  return layout();
387
400
  }
401
+ const before = placedSig;
388
402
  const v = view();
389
- const x = spot.x;
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>;
399
- // Nothing new where it could stand: the page is looked at less often while that holds (queueRescan).
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;
403
+ columns = { w: v.w, h: v.h, boxes: new Map() };
402
404
  layout();
405
+ // Nothing new where it could stand: the page is looked at less often while that holds (queueRescan).
406
+ calm = placedSig === before ? Math.min(calm + 1, CALM_STEPS) : 0;
403
407
  }
404
408
  /*
405
409
  * How often the page is read (a read is a column or two of hit tests). A page that keeps changing — a clock, a
@@ -474,7 +478,7 @@ function watchPage(on: boolean): void {
474
478
  });
475
479
  if (theirs) queueRescan();
476
480
  });
477
- watcher.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"] });
481
+ watcher.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-expanded", "disabled", "aria-disabled"] });
478
482
  }
479
483
  for (const t of ACTED) window.addEventListener(t, onPageActed, { capture: true, passive: true });
480
484
  // A toolbar's own opening ends after the press; a sticky bar sticks as the page scrolls.
@@ -500,11 +504,12 @@ function layout(): void {
500
504
  // While the panel is open it stays where it was opened from — never moved under the person's eyes.
501
505
  const wish = `${pagePath()}|${want.side}|${want.x}|${wantY}`;
502
506
  const prev = shown && shownFor === wish ? shown : null;
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;
506
- shown = { side: free.side, x: want.x, y: free.y };
507
+ const home = { side: want.side, x: want.x, y: wantY };
508
+ const placing = !!columns && avoidSetting().on;
509
+ const free = !isOpen || !shown ? (placing ? placeSpot(home, readColumn, v, SIZE, MARGIN, Math.max(MARGIN, v.h - SIZE - MARGIN), prev) : home) : shown;
510
+ shown = { side: free.side, x: free.x, y: free.y };
507
511
  shownFor = wish;
512
+ if (placing) placedSig = JSON.stringify([blockedRanges(readColumn(home.side, home.x), home.side === "right" ? { left: v.w - home.x - SIZE, right: v.w - home.x } : { left: home.x, right: home.x + SIZE }, v.h, SIZE), shown]);
508
513
  const at = shown;
509
514
  const y = at.y;
510
515
  const other = at.side === "right" ? "left" : "right";
@@ -779,7 +784,8 @@ export function unmountSupportChat(): void {
779
784
  calm = 0;
780
785
  shown = null;
781
786
  shownFor = "";
782
- blocked = null;
787
+ columns = null;
788
+ placedSig = "";
783
789
  ruleHidden = false;
784
790
  followHere(false);
785
791
  root?.remove();