@lotics/cli 0.205.2 → 0.206.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.
@@ -22,16 +22,25 @@ var __loticsProbe = (() => {
22
22
  var probe_page_exports = {};
23
23
  __export(probe_page_exports, {
24
24
  PROBES: () => PROBES,
25
+ contentHeight: () => contentHeight,
26
+ doors: () => doors,
27
+ editorSelector: () => editorSelector,
28
+ focused: () => focused,
29
+ openDoor: () => openDoor,
25
30
  openRecord: () => openRecord,
26
31
  openScreen: () => openScreen,
32
+ overlaysOpen: () => overlaysOpen,
33
+ paintState: () => paintState,
27
34
  recordRows: () => recordRows,
35
+ rings: () => rings,
28
36
  run: () => run,
29
37
  screens: () => screens,
38
+ surfaceReport: () => surfaceReport,
30
39
  textLeafCount: () => textLeafCount
31
40
  });
32
41
 
33
42
  // src/probe_collectors.ts
34
- function moneyColumns() {
43
+ function moneyColumns(root = document.body) {
35
44
  const money = /^[-+−]?\s?(?:(?:[$€]|USD|EUR|VND)\s?[\d.,]+\s?(?:[KMB]|tr|tỷ)?|[\d.,]+\s?(?:tr|tỷ)?\s?(?:₫|đ|VND|USD|EUR|[$€]))$/i;
36
45
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
37
46
  const bandGap = 48;
@@ -59,7 +68,7 @@ var __loticsProbe = (() => {
59
68
  }
60
69
  node.childNodes.forEach(walk);
61
70
  };
62
- walk(document.body);
71
+ walk(root);
63
72
  const bands = [];
64
73
  for (const leaf of [...leaves].sort((a, b) => a.top - b.top)) {
65
74
  const band = bands[bands.length - 1];
@@ -96,7 +105,7 @@ var __loticsProbe = (() => {
96
105
  }
97
106
  return { total: leaves.length, columns };
98
107
  }
99
- function reportedValues() {
108
+ function reportedValues(root = document.body) {
100
109
  const numeric = /^[-+−]?\d[\d.,]*\s?(?:%|[A-Za-z₫đ$€]{1,4})?$/;
101
110
  const counted = /(?:%|[A-Za-z₫đ$€]{1,4})$/;
102
111
  const delta = /^[↑↓→▲▼]\s*[-+−]?\d[\d.,]*\s?%$/;
@@ -107,7 +116,7 @@ var __loticsProbe = (() => {
107
116
  const nameReach = 360;
108
117
  const values = [];
109
118
  const labels = [];
110
- let deltas = 0;
119
+ const deltaLeaves = [];
111
120
  const walk = (node) => {
112
121
  if (node.nodeType === Node.TEXT_NODE) {
113
122
  const text = (node.textContent || "").trim();
@@ -116,11 +125,11 @@ var __loticsProbe = (() => {
116
125
  if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
117
126
  const rect = parent.getBoundingClientRect();
118
127
  if (rect.width === 0 && rect.height === 0) return;
128
+ const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
119
129
  if (delta.test(text)) {
120
- deltas += 1;
130
+ deltaLeaves.push(leaf);
121
131
  return;
122
132
  }
123
- const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
124
133
  if (!numeric.test(text) || dateLike.test(text)) {
125
134
  labels.push(leaf);
126
135
  return;
@@ -131,7 +140,7 @@ var __loticsProbe = (() => {
131
140
  }
132
141
  node.childNodes.forEach(walk);
133
142
  };
134
- walk(document.body);
143
+ walk(root);
135
144
  const clusters = [];
136
145
  for (const leaf of [...values].sort((a, b) => a.left - b.left || a.top - b.top)) {
137
146
  const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
@@ -143,6 +152,11 @@ var __loticsProbe = (() => {
143
152
  clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
144
153
  }
145
154
  }
155
+ const nameOf2 = (left, right, top, inkLeft) => {
156
+ const above = labels.filter((label) => label.top < top && top - label.top <= nameReach && label.left < right && label.right > left).sort((a, b) => b.top - a.top)[0];
157
+ const beside = labels.filter((label) => Math.abs(label.top - top) <= 8 && label.right <= inkLeft).sort((a, b) => b.right - a.right)[0];
158
+ return (above ?? beside)?.text.slice(0, 40) ?? "";
159
+ };
146
160
  const columns = [];
147
161
  let bare = 0;
148
162
  let quantities = 0;
@@ -151,56 +165,229 @@ var __loticsProbe = (() => {
151
165
  const ordinal = items.length >= 3 && items.every((item, index) => item.text === String(index + 1));
152
166
  if (ordinal) continue;
153
167
  const top = items[0];
154
- const above = labels.filter((label) => label.top < top.top && top.top - label.top <= nameReach && label.left < cluster.right && label.right > cluster.left).sort((a, b) => b.top - a.top)[0];
155
- const beside = labels.filter((label) => Math.abs(label.top - top.top) <= 8 && label.right <= top.left).sort((a, b) => b.right - a.right)[0];
156
168
  const scaled = items.filter((item) => counted.test(item.text)).length;
157
- bare += items.length;
169
+ bare += items.length - scaled;
158
170
  quantities += scaled;
159
171
  columns.push({
160
- slot: (above ?? beside)?.text.slice(0, 40) ?? "",
172
+ slot: nameOf2(cluster.left, cluster.right, top.top, top.left),
161
173
  size: items.length,
162
174
  quantities: scaled,
163
175
  samples: items.slice(0, 4).map((item) => item.text)
164
176
  });
165
177
  }
166
178
  columns.sort((a, b) => b.size - a.size);
167
- const drawn = Array.from(document.querySelectorAll('[role="meter"], [role="progressbar"], canvas')).filter((el) => {
179
+ const drawn = Array.from(root.querySelectorAll('[role="meter"], [role="progressbar"], canvas')).filter((el) => {
168
180
  const r = el.getBoundingClientRect();
169
181
  return r.width > 0 && r.height > 0;
170
182
  });
171
183
  const reads = (el) => {
172
184
  const role = el.getAttribute("role");
173
185
  if (role !== "meter" && role !== "progressbar") return true;
174
- const stated = (name) => {
186
+ const stated2 = (name) => {
175
187
  const raw = el.getAttribute(name);
176
188
  return raw === null || raw.trim() === "" ? Number.NaN : Number(raw);
177
189
  };
178
190
  const bound = (name, fallback) => {
179
- const value = stated(name);
191
+ const value = stated2(name);
180
192
  return Number.isFinite(value) ? value : fallback;
181
193
  };
182
- const now = stated("aria-valuenow");
194
+ const now = stated2("aria-valuenow");
183
195
  if (!Number.isFinite(now)) return false;
184
196
  const floor = bound("aria-valuemin", 0);
185
197
  const top = bound("aria-valuemax", 100);
186
198
  return top > floor && now < top;
187
199
  };
188
- const pictures = Array.from(document.querySelectorAll('[role="img"], svg, img')).filter((el) => {
200
+ const pictures = Array.from(root.querySelectorAll('[role="img"], svg, img')).filter((el) => {
189
201
  if (el.getAttribute("aria-hidden") === "true") return false;
190
202
  const r = el.getBoundingClientRect();
191
203
  return r.width >= pictureMin && r.height >= pictureMin;
192
204
  });
193
- const encoded = drawn.filter(reads).length + pictures.length + deltas;
205
+ const markMin = 4;
206
+ const notData = 'a, button, input, nav, [role="banner"], [role="button"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"]';
207
+ const marked = (el) => {
208
+ if (el.closest(notData) !== null) return false;
209
+ const r = el.getBoundingClientRect();
210
+ return r.width >= markMin && r.height >= markMin && (r.width < pictureMin || r.height < pictureMin);
211
+ };
212
+ const glyphs = Array.from(root.querySelectorAll('[role="img"]')).filter(
213
+ (el) => el.getAttribute("aria-hidden") !== "true" && (el.getAttribute("aria-label") || "").trim() !== "" && marked(el)
214
+ );
215
+ const already = [...drawn, ...pictures, ...glyphs];
216
+ const swatches = Array.from(root.querySelectorAll("*")).filter((el) => {
217
+ if ((el.textContent || "").trim() !== "") return false;
218
+ if (already.some((device) => device === el || device.contains(el))) return false;
219
+ if (!marked(el)) return false;
220
+ const ground = getComputedStyle(el).backgroundColor;
221
+ return ground !== "" && ground !== "transparent" && !/,\s*0(?:\.0+)?\)\s*$/.test(ground);
222
+ });
223
+ const tells = (marks, sign) => new Set(marks.map(sign)).size > 1 ? marks.length : 0;
224
+ const categorical = tells(swatches, (el) => getComputedStyle(el).backgroundColor) + tells(glyphs, (el) => el.getAttribute("aria-label") || "");
225
+ const tally = /* @__PURE__ */ new Map();
226
+ const note = (kind, box) => {
227
+ const slot = nameOf2(box.left, box.right, box.top, box.left);
228
+ const key = `${kind}\0${slot}`;
229
+ const seen = tally.get(key);
230
+ if (seen === void 0) tally.set(key, { kind, slot, count: 1 });
231
+ else seen.count += 1;
232
+ };
233
+ const noteElement = (kind, el) => {
234
+ const r = el.getBoundingClientRect();
235
+ note(kind, { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top) });
236
+ };
237
+ for (const el of drawn) noteElement(el.tagName === "CANVAS" ? "chart" : "meter", el);
238
+ for (const el of pictures) noteElement("picture", el);
239
+ for (const el of swatches) noteElement("swatch", el);
240
+ for (const el of glyphs) noteElement("glyph", el);
241
+ for (const leaf of deltaLeaves) note("delta", leaf);
242
+ const deviceKinds = [...tally.values()].sort(
243
+ (a, b) => b.count - a.count || (a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0)
244
+ );
245
+ const slotMax = pictureMin * 4;
246
+ const isSlot = (el) => {
247
+ if ((el.textContent || "").trim() !== "") return false;
248
+ if (el.closest(notData) !== null) return false;
249
+ const r = el.getBoundingClientRect();
250
+ if (r.width < pictureMin || r.height < pictureMin) return false;
251
+ return r.width <= slotMax && r.height <= slotMax;
252
+ };
253
+ const holdsPicture = (el) => {
254
+ if (/^(IMG|CANVAS|VIDEO)$/.test(el.tagName)) return true;
255
+ for (const media of el.querySelectorAll("img, canvas, video")) {
256
+ const r = media.getBoundingClientRect();
257
+ if (r.width > 0 && r.height > 0) return true;
258
+ }
259
+ for (const vector of el.querySelectorAll("svg")) {
260
+ const r = vector.getBoundingClientRect();
261
+ if (r.width >= pictureMin && r.height >= pictureMin) return true;
262
+ }
263
+ return false;
264
+ };
265
+ const slotBoxes = Array.from(root.querySelectorAll("*")).filter(
266
+ (el) => isSlot(el) && !Array.from(el.children).some(isSlot)
267
+ );
268
+ const slotClusters = [];
269
+ for (const el of slotBoxes) {
270
+ const r = el.getBoundingClientRect();
271
+ const box = { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top) };
272
+ const cluster = slotClusters.find((c) => box.left < c.right && box.right > c.left);
273
+ if (cluster !== void 0) {
274
+ cluster.items.push(el);
275
+ cluster.left = Math.min(cluster.left, box.left);
276
+ cluster.right = Math.max(cluster.right, box.right);
277
+ } else {
278
+ slotClusters.push({ left: box.left, right: box.right, items: [el] });
279
+ }
280
+ }
281
+ const markSlots = slotClusters.filter((cluster) => cluster.items.length >= 3).map((cluster) => {
282
+ const tops = cluster.items.map((el) => Math.round(el.getBoundingClientRect().top));
283
+ return {
284
+ slot: nameOf2(cluster.left, cluster.right, Math.min(...tops), cluster.left),
285
+ size: cluster.items.length,
286
+ filled: cluster.items.filter(holdsPicture).length
287
+ };
288
+ }).sort((a, b) => b.size - a.size);
289
+ const encoded = drawn.filter(reads).length + pictures.length + deltaLeaves.length + categorical;
194
290
  return {
195
- total: bare + encoded,
291
+ total: bare + quantities + encoded,
196
292
  bare,
197
293
  quantities,
198
294
  encoded,
199
- devices: drawn.length + pictures.length + deltas,
200
- columns
295
+ devices: drawn.length + pictures.length + deltaLeaves.length + swatches.length + glyphs.length,
296
+ deviceKinds,
297
+ columns,
298
+ markSlots
201
299
  };
202
300
  }
203
- function internalIds() {
301
+ function stageWordColumns(root = document.body) {
302
+ const tabs = [];
303
+ for (const tab of Array.from(root.querySelectorAll('[role="tablist"] [role="tab"]'))) {
304
+ const label = (tab.textContent || "").trim();
305
+ if (label !== "" && !tabs.includes(label)) tabs.push(label);
306
+ }
307
+ if (tabs.length < 2) return { total: 0, wordsOnly: [] };
308
+ const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
309
+ const notContent = 'nav, [role="banner"], [role="tab"], [role="tablist"]';
310
+ const markMin = 4;
311
+ const pictureMin = 32;
312
+ const nameReach = 360;
313
+ const unpainted = (ground) => ground === "" || ground === "transparent" || /,\s*0(?:\.0+)?\)\s*$/.test(ground);
314
+ const leaves = [];
315
+ const labels = [];
316
+ const walk = (node) => {
317
+ if (node.nodeType === Node.TEXT_NODE) {
318
+ const text = (node.textContent || "").trim();
319
+ const parent = node.parentElement;
320
+ if (!text || !parent || skipTag.test(parent.tagName)) return;
321
+ if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
322
+ const rect = parent.getBoundingClientRect();
323
+ if (rect.width === 0 && rect.height === 0) return;
324
+ const box = {
325
+ text,
326
+ left: Math.round(rect.left),
327
+ right: Math.round(rect.right),
328
+ top: Math.round(rect.top)
329
+ };
330
+ labels.push(box);
331
+ if (tabs.includes(text) && !parent.closest(notContent)) leaves.push({ el: parent, ...box });
332
+ return;
333
+ }
334
+ node.childNodes.forEach(walk);
335
+ };
336
+ walk(root);
337
+ const clusters = [];
338
+ for (const leaf of [...leaves].sort((a, b) => a.left - b.left || a.top - b.top)) {
339
+ const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
340
+ if (cluster !== void 0) {
341
+ cluster.items.push(leaf);
342
+ cluster.left = Math.min(cluster.left, leaf.left);
343
+ cluster.right = Math.max(cluster.right, leaf.right);
344
+ } else {
345
+ clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
346
+ }
347
+ }
348
+ const nameOf2 = (left, right, top) => {
349
+ const above = labels.filter((label) => label.top < top && top - label.top <= nameReach && label.left < right && label.right > left).sort((a, b) => b.top - a.top)[0];
350
+ const beside = labels.filter((label) => Math.abs(label.top - top) <= 8 && label.right <= left).sort((a, b) => b.right - a.right)[0];
351
+ return (above ?? beside)?.text.slice(0, 40) ?? "";
352
+ };
353
+ const cellOf = (leaf, left, right) => {
354
+ let cell = leaf.el;
355
+ let node = leaf.el.parentElement;
356
+ while (node !== null) {
357
+ const r = node.getBoundingClientRect();
358
+ if (Math.round(r.left) < left - pictureMin || Math.round(r.right) > right + pictureMin) break;
359
+ cell = node;
360
+ node = node.parentElement;
361
+ }
362
+ return cell;
363
+ };
364
+ const holdsMark = (cell, word) => Array.from(cell.querySelectorAll("*")).some((el) => {
365
+ if (el === word || el.contains(word)) return false;
366
+ const r = el.getBoundingClientRect();
367
+ if (r.width < markMin || r.height < markMin) return false;
368
+ if (el.getAttribute("aria-hidden") === "true") return false;
369
+ if (el.getAttribute("role") === "img") return (el.getAttribute("aria-label") || "").trim() !== "";
370
+ if (r.width >= pictureMin && r.height >= pictureMin) return false;
371
+ if ((el.textContent || "").trim() !== "") return false;
372
+ return !unpainted(getComputedStyle(el).backgroundColor);
373
+ });
374
+ const columns = [];
375
+ const wordsOnly = [];
376
+ for (const cluster of clusters) {
377
+ if (cluster.items.length < 3) continue;
378
+ const cells = cluster.items.map((leaf) => ({ leaf, cell: cellOf(leaf, cluster.left, cluster.right) }));
379
+ const column = {
380
+ slot: nameOf2(cluster.left, cluster.right, Math.min(...cluster.items.map((item) => item.top))),
381
+ size: cluster.items.length,
382
+ values: tabs.filter((tab) => cluster.items.some((item) => item.text === tab))
383
+ };
384
+ columns.push(column);
385
+ const marked = cells.some(({ cell, leaf }) => holdsMark(cell, leaf.el)) || new Set(cells.map(({ cell }) => getComputedStyle(cell).backgroundColor)).size > 1;
386
+ if (!marked) wordsOnly.push(column);
387
+ }
388
+ return { total: columns.length, wordsOnly };
389
+ }
390
+ function internalIds(root = document.body) {
204
391
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
205
392
  const id = /\b(?:opt|fld|tbl|rec|wfl|mbr|app|wsp)_[A-Za-z0-9]{6,}\b/g;
206
393
  const ids = /* @__PURE__ */ new Set();
@@ -217,11 +404,11 @@ var __loticsProbe = (() => {
217
404
  }
218
405
  node.childNodes.forEach(walk);
219
406
  };
220
- walk(document.body);
407
+ walk(root);
221
408
  return { total, ids: [...ids] };
222
409
  }
223
- function tabStrips() {
224
- const all = Array.from(document.querySelectorAll('[role="tablist"]')).filter((el) => {
410
+ function tabStrips(root = document.body) {
411
+ const all = Array.from(root.querySelectorAll('[role="tablist"]')).filter((el) => {
225
412
  const r = el.getBoundingClientRect();
226
413
  return r.width > 0 && r.height > 0;
227
414
  });
@@ -232,13 +419,13 @@ var __loticsProbe = (() => {
232
419
  strips: counted.map((el) => Array.from(el.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim()))
233
420
  };
234
421
  }
235
- function truncatedLeaves() {
422
+ function truncatedLeaves(root = document.body) {
236
423
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
237
- const clippedAway = /^inset\(\s*50%/;
424
+ const clippedAway2 = /^inset\(\s*50%/;
238
425
  const breakable = /[\t\n\v\f\r \u200b\u3000]/;
239
426
  const cut = [];
240
427
  let total = 0;
241
- for (const el of Array.from(document.querySelectorAll("*"))) {
428
+ for (const el of Array.from(root.querySelectorAll("*"))) {
242
429
  if (skipTag.test(el.tagName) || el.children.length > 0) continue;
243
430
  const text = (el.textContent || "").trim();
244
431
  if (!text) continue;
@@ -246,7 +433,7 @@ var __loticsProbe = (() => {
246
433
  const style = getComputedStyle(el);
247
434
  const box = el.getBoundingClientRect();
248
435
  if (box.width <= 1 && box.height <= 1) continue;
249
- if (clippedAway.test(style.getPropertyValue("clip-path"))) continue;
436
+ if (clippedAway2.test(style.getPropertyValue("clip-path"))) continue;
250
437
  total += 1;
251
438
  if (style.overflow === "visible") continue;
252
439
  const states = (property) => {
@@ -262,7 +449,7 @@ var __loticsProbe = (() => {
262
449
  }
263
450
  return { total, cut };
264
451
  }
265
- function bannedGlyphs() {
452
+ function bannedGlyphs(root = document.body) {
266
453
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
267
454
  const found = [];
268
455
  let total = 0;
@@ -278,10 +465,10 @@ var __loticsProbe = (() => {
278
465
  }
279
466
  node.childNodes.forEach(walk);
280
467
  };
281
- walk(document.body);
468
+ walk(root);
282
469
  return { total, found };
283
470
  }
284
- function compactMoneyWords() {
471
+ function compactMoneyWords(root = document.body) {
285
472
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
286
473
  const any = /\d[\d.,]*\s?(?:[KMB](?![a-z])|tr(?![a-z])|tỷ)/i;
287
474
  const dongInEnglishWords = /\d[\d.,]*\s?[KMB]\s?(?:₫|đ|VND)|(?:₫|VND)\s?\d[\d.,]*\s?[KMB](?![a-z])/i;
@@ -300,11 +487,11 @@ var __loticsProbe = (() => {
300
487
  }
301
488
  node.childNodes.forEach(walk);
302
489
  };
303
- walk(document.body);
490
+ walk(root);
304
491
  return { total, foreign };
305
492
  }
306
- function scanStackedPairs() {
307
- const px = (el) => parseFloat(getComputedStyle(el).fontSize);
493
+ function scanStackedPairs(root = document.body) {
494
+ const px2 = (el) => parseFloat(getComputedStyle(el).fontSize);
308
495
  const weight = (el) => parseInt(getComputedStyle(el).fontWeight, 10) || 400;
309
496
  const isLeaf = (el) => el.children.length === 0 && (el.textContent || "").trim().length > 0;
310
497
  const leafOf = (el, depth = 3) => {
@@ -313,9 +500,16 @@ var __loticsProbe = (() => {
313
500
  return leafOf(el.children[0], depth - 1);
314
501
  };
315
502
  const sameColumn = 8;
503
+ const heading = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
504
+ const collection = 'ul, ol, dl, table, [role="list"], [role="grid"], [role="table"]';
505
+ const pairShaped = (box) => box.children.length === 2 && leafOf(box.children[0]) !== null && leafOf(box.children[1]) !== null;
506
+ const names = (line, box) => {
507
+ const id = line.getAttribute("id") || "";
508
+ return id !== "" && (box.getAttribute("aria-labelledby") || "").split(/\s+/).includes(id);
509
+ };
316
510
  let total = 0;
317
511
  const overDemoted = [];
318
- for (const el of Array.from(document.querySelectorAll("div"))) {
512
+ for (const el of Array.from(root.querySelectorAll("div"))) {
319
513
  const kids = Array.from(el.children);
320
514
  if (kids.length !== 2) continue;
321
515
  const [first, second] = kids;
@@ -328,19 +522,22 @@ var __loticsProbe = (() => {
328
522
  if (b.top < a.bottom - 2) continue;
329
523
  if (Math.abs(b.left - a.left) > sameColumn) continue;
330
524
  if (a.height === 0 || b.height === 0) continue;
525
+ if (first.matches(heading) || first.querySelector(heading) !== null) continue;
526
+ if ([first, subject].some((line) => [el, second].some((box) => names(line, box)))) continue;
527
+ if (second.matches(collection) || Array.from(second.children).some(pairShaped)) continue;
331
528
  total += 1;
332
- if (px(supporting) < px(subject) && weight(subject) > weight(supporting)) {
529
+ if (px2(supporting) < px2(subject) && weight(subject) > weight(supporting)) {
333
530
  overDemoted.push({
334
531
  subject: (subject.textContent || "").trim().slice(0, 60),
335
532
  supporting: (supporting.textContent || "").trim().slice(0, 60),
336
- sizes: [px(subject), px(supporting)],
533
+ sizes: [px2(subject), px2(supporting)],
337
534
  weights: [weight(subject), weight(supporting)]
338
535
  });
339
536
  }
340
537
  }
341
538
  return { total, overDemoted };
342
539
  }
343
- function scanRowMarks() {
540
+ function scanRowMarks(root = document.body) {
344
541
  const marks = [];
345
542
  const seen = [];
346
543
  const record = (el, text) => {
@@ -350,7 +547,7 @@ var __loticsProbe = (() => {
350
547
  marks.push({ text, w: Math.round(r.width), y: Math.round(r.y), x: Math.round(r.x) });
351
548
  };
352
549
  const inContent = (el) => el.closest('[role="banner"]') === null;
353
- for (const el of Array.from(document.querySelectorAll('[data-mark="identity"]'))) {
550
+ for (const el of Array.from(root.querySelectorAll('[data-mark="identity"]'))) {
354
551
  const r = el.getBoundingClientRect();
355
552
  if (r.height === 0 || !inContent(el)) continue;
356
553
  record(el, (el.textContent || "").trim() || "\u25A3");
@@ -362,7 +559,7 @@ var __loticsProbe = (() => {
362
559
  if (depth === 0) return false;
363
560
  return Array.from(el.children).some((child) => grounded(child, depth - 1));
364
561
  };
365
- for (const el of Array.from(document.querySelectorAll("*"))) {
562
+ for (const el of Array.from(root.querySelectorAll("*"))) {
366
563
  const r = el.getBoundingClientRect();
367
564
  if (r.width < 12 || r.width > 56) continue;
368
565
  if (Math.abs(r.width - r.height) > 1) continue;
@@ -390,63 +587,1079 @@ var __loticsProbe = (() => {
390
587
  return { total: marks.length, mismatches };
391
588
  }
392
589
 
590
+ // src/probe_collectors/naming.ts
591
+ var HOLDS_A_VALUE = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="listbox"], [role="spinbutton"], [role="slider"]';
592
+ var HEADING = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
593
+ function words(el) {
594
+ return el === null ? "" : (el.textContent || "").replace(/\s+/g, " ").trim();
595
+ }
596
+ function pointedAt(el, attribute) {
597
+ return (el.getAttribute(attribute) || "").split(/\s+/).filter((id) => id !== "").map((id) => el.ownerDocument.getElementById(id)).filter((target) => target !== null);
598
+ }
599
+ function stated(el) {
600
+ const pointed = pointedAt(el, "aria-labelledby").map(words).filter((name) => name !== "").join(" ");
601
+ if (pointed !== "") return pointed;
602
+ return (el.getAttribute("aria-label") || "").trim();
603
+ }
604
+ function labelling(el) {
605
+ const id = el.getAttribute("id") || "";
606
+ const explicit = id === "" ? void 0 : Array.from(el.ownerDocument.querySelectorAll("label[for]")).find((label2) => label2.getAttribute("for") === id);
607
+ const label = explicit ?? el.closest("label");
608
+ if (label === null || label === void 0) return "";
609
+ const said = [];
610
+ const walk = (node) => {
611
+ if (node === el) return;
612
+ if (node.nodeType === Node.TEXT_NODE) {
613
+ said.push(node.textContent || "");
614
+ return;
615
+ }
616
+ node.childNodes.forEach(walk);
617
+ };
618
+ walk(label);
619
+ return said.join(" ").replace(/\s+/g, " ").trim();
620
+ }
621
+ function regionName(region) {
622
+ const said = stated(region);
623
+ return said !== "" ? said : words(region.querySelector(HEADING));
624
+ }
625
+ function nameOf(el) {
626
+ const said = stated(el);
627
+ if (said !== "") return said;
628
+ if (el.tagName === "DETAILS") {
629
+ const summary = words(el.querySelector("summary"));
630
+ if (summary !== "") return summary;
631
+ }
632
+ const labelled = labelling(el);
633
+ if (labelled !== "") return labelled;
634
+ if (!el.matches(HOLDS_A_VALUE)) {
635
+ const own = words(el);
636
+ if (own !== "") return own;
637
+ }
638
+ const tooltip = (el.getAttribute("title") || "").trim();
639
+ if (tooltip !== "") return tooltip;
640
+ for (const region of pointedAt(el, "aria-controls")) {
641
+ const named = regionName(region);
642
+ if (named !== "") return named;
643
+ }
644
+ return "";
645
+ }
646
+
647
+ // src/probe_collectors/on_screen.ts
648
+ var CLIP_RECT = /^rect\(\s*(-?[\d.]+)px[,\s]+(-?[\d.]+)px[,\s]+(-?[\d.]+)px[,\s]+(-?[\d.]+)px\s*\)$/;
649
+ var INSET = /^inset\(([^)]*?)(?:\s+round\s+[^)]*)?\)$/;
650
+ var inset = (value, extent) => value.endsWith("%") ? parseFloat(value) / 100 * extent : parseFloat(value);
651
+ var meets = (near, far, extent) => extent > 0 && inset(near, extent) + inset(far, extent) >= extent;
652
+ function clippedAway(el) {
653
+ const style = getComputedStyle(el);
654
+ const box = el.getBoundingClientRect();
655
+ const rect = CLIP_RECT.exec(style.clip);
656
+ if (rect !== null && (Number(rect[2]) - Number(rect[4]) <= 0 || Number(rect[3]) - Number(rect[1]) <= 0)) return true;
657
+ const path = INSET.exec(style.clipPath);
658
+ if (path === null || path[1].trim() === "") return false;
659
+ const [top, right = top, bottom = top, left = right] = path[1].trim().split(/\s+/);
660
+ return meets(left, right, box.width) || meets(top, bottom, box.height);
661
+ }
662
+ function onScreen(el) {
663
+ if (el.closest("[inert]") !== null) return false;
664
+ if (typeof el.checkVisibility === "function" && !el.checkVisibility()) return false;
665
+ const shown = getComputedStyle(el).visibility;
666
+ if (shown === "hidden" || shown === "collapse") return false;
667
+ for (let up = el; up !== null; up = up.parentElement) {
668
+ if (clippedAway(up)) return false;
669
+ }
670
+ const box = el.getBoundingClientRect();
671
+ return box.width > 0 && box.height > 0;
672
+ }
673
+
674
+ // src/probe_collectors/scope.ts
675
+ var OVERLAY = '[aria-modal="true"], dialog[open], [role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
676
+ var AWAY = '[aria-hidden="true"], [inert]';
677
+ function measuredSurface() {
678
+ const away = Array.from(document.querySelectorAll(AWAY));
679
+ const open = Array.from(document.querySelectorAll(OVERLAY)).filter((el) => {
680
+ if (!onScreen(el)) return false;
681
+ if (away.some((hidden) => hidden.contains(el))) return false;
682
+ if (el.matches('[aria-modal="true"], dialog[open]')) return true;
683
+ return away.some((hidden) => hidden.parentElement === document.body && !hidden.contains(el));
684
+ });
685
+ const inner = open.filter((el) => !open.some((other) => other !== el && el.contains(other)));
686
+ return inner[inner.length - 1] ?? document.body;
687
+ }
688
+ function within(root, selector) {
689
+ const inside = Array.from(root.querySelectorAll(selector));
690
+ return root.matches(selector) ? [root, ...inside] : inside;
691
+ }
692
+
693
+ // src/probe_collectors/clutter.ts
694
+ var overlaySurface = '[role="dialog"], [role="alertdialog"], dialog[open]';
695
+ var DRAWS = 'svg, img, picture, canvas, video, [role="img"], input, textarea, select, button, [role="button"], [role="textbox"], [role="combobox"], [role="checkbox"], [role="switch"], [role="meter"], [role="progressbar"]';
696
+ function clutterCensus(root = document.body) {
697
+ const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
698
+ const chrome = 'nav, [role="banner"]';
699
+ const recordSurface = '.lotics-record-page, [role="dialog"]';
700
+ const noteFloor = 3;
701
+ const formFloor = 3;
702
+ const hueStep = 30;
703
+ const chromaFloor = 0.15;
704
+ const sameColour = 24;
705
+ const sameEdge = 8;
706
+ const words3 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
707
+ const surfaces = within(root, recordSurface).filter(onScreen);
708
+ const called2 = (el) => el.matches(recordSurface) ? words3(el.querySelector(".lotics-record-page__heading, h1, h2")) || "the record" : "the screen";
709
+ const rules = [];
710
+ const primary = '[data-variant="primary"]';
711
+ const acts = within(root, primary).filter(
712
+ (el) => onScreen(el) && el.closest(chrome) === null && (el.parentElement === null || el.parentElement.closest(primary) === null)
713
+ );
714
+ const page = within(root, '[role="main"], main')[0] ?? root;
715
+ const held = /* @__PURE__ */ new Map();
716
+ for (const act of acts) {
717
+ const owner = act.closest(recordSurface) ?? page;
718
+ const run2 = held.get(owner);
719
+ if (run2 === void 0) held.set(owner, [act]);
720
+ else run2.push(act);
721
+ }
722
+ let surplusActs = 0;
723
+ const loud = [];
724
+ for (const [owner, run2] of held) {
725
+ if (run2.length <= 1) continue;
726
+ surplusActs += run2.length - 1;
727
+ if (loud.length < 3) loud.push(`${called2(owner)}: ${run2.length} primary acts \u2014 ${run2.map((act) => `"${words3(act)}"`).join(", ")}`);
728
+ }
729
+ rules.push({
730
+ law: "hierarchy.md",
731
+ rule: "One primary act per surface",
732
+ total: acts.length,
733
+ count: surplusActs,
734
+ offenders: loud
735
+ });
736
+ const money = /^[-+−]?\s?(?:(?:[$€]|USD|EUR|VND)\s?[\d.,]+\s?(?:[KMB]|tr|tỷ)?|[\d.,]+\s?(?:tr|tỷ)?\s?(?:₫|đ|VND|USD|EUR|[$€]))$/i;
737
+ const dated = /^\d{1,4}[/.-]\d{1,2}(?:[/.-]\d{1,4})?$/;
738
+ const coded = /^(?=[^0-9]*[0-9])(?=[^A-Z]*[A-Z])[A-Z0-9][A-Z0-9/_.-]{4,}$/;
739
+ const places = /* @__PURE__ */ new Map();
740
+ const shown = /* @__PURE__ */ new Map();
741
+ for (let index = 0; index < surfaces.length; index++) {
742
+ const walk = (node) => {
743
+ if (node.nodeType === Node.TEXT_NODE) {
744
+ const text = (node.textContent || "").trim();
745
+ const parent = node.parentElement;
746
+ if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
747
+ if (!onScreen(parent)) return;
748
+ if (!money.test(text) && !dated.test(text) && !coded.test(text)) return;
749
+ const key = `${index}\0${text}`;
750
+ shown.set(key, text);
751
+ const edges = places.get(key);
752
+ const left = Math.round(parent.getBoundingClientRect().left);
753
+ if (edges === void 0) places.set(key, [left]);
754
+ else edges.push(left);
755
+ return;
756
+ }
757
+ node.childNodes.forEach(walk);
758
+ };
759
+ walk(surfaces[index]);
760
+ }
761
+ let twice = 0;
762
+ const repeated = [];
763
+ for (const [key, edges] of places) {
764
+ const columns = [];
765
+ for (const left of edges) if (!columns.some((edge) => Math.abs(edge - left) <= sameEdge)) columns.push(left);
766
+ if (columns.length < 2) continue;
767
+ twice += 1;
768
+ if (repeated.length < 3) repeated.push(`"${shown.get(key)}" drawn in ${columns.length} places`);
769
+ }
770
+ rules.push({
771
+ law: "hierarchy.md",
772
+ rule: "One fact, one place",
773
+ total: places.size,
774
+ count: twice,
775
+ offenders: repeated
776
+ });
777
+ const absent = /^[—–-]$/;
778
+ let facts = 0;
779
+ let standing = 0;
780
+ const open = [];
781
+ for (const surface of surfaces) {
782
+ if (surface.querySelector(".lotics-record-facts__more") === null) continue;
783
+ for (const cell of Array.from(surface.querySelectorAll(".lotics-record-facts__fact"))) {
784
+ const value = cell.querySelector(".lotics-record-facts__value");
785
+ if (value === null || !onScreen(cell)) continue;
786
+ facts += 1;
787
+ if (!absent.test((value.textContent || "").trim())) continue;
788
+ standing += 1;
789
+ if (open.length < 3) open.push(`"${words3(cell.querySelector(".lotics-record-facts__label")) || words3(cell)}" is unstated and drawn`);
790
+ }
791
+ }
792
+ rules.push({
793
+ law: "hierarchy.md",
794
+ rule: "How a record derives them",
795
+ total: facts,
796
+ count: standing,
797
+ offenders: open
798
+ });
799
+ let boxes = 0;
800
+ let overReserved = 0;
801
+ const reserved = [];
802
+ for (const box of within(root, 'textarea, [role="textbox"], [contenteditable="true"]')) {
803
+ if (!onScreen(box) || box.closest(chrome) !== null) continue;
804
+ const style = getComputedStyle(box);
805
+ const line = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5;
806
+ if (!Number.isFinite(line) || line <= 0) continue;
807
+ const pad = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
808
+ boxes += 1;
809
+ const lines = Math.round((box.getBoundingClientRect().height - pad) / line);
810
+ const written = Math.round((box.scrollHeight - pad) / line);
811
+ if (lines <= noteFloor || written > 1) continue;
812
+ overReserved += 1;
813
+ if (reserved.length < 3) {
814
+ const named = nameOf(box).slice(0, 40);
815
+ reserved.push(`"${named === "" ? "(unnamed)" : named}": ${lines} lines reserved for 1`);
816
+ }
817
+ }
818
+ for (const surface of within(root, overlaySurface).filter(onScreen)) {
819
+ const box = surface.getBoundingClientRect();
820
+ if (box.height <= 0) continue;
821
+ boxes += 1;
822
+ const bands = [];
823
+ const ink = (el) => {
824
+ if (!onScreen(el)) return;
825
+ const rect = el.getBoundingClientRect();
826
+ const top = Math.max(rect.top, box.top);
827
+ const bottom = Math.min(rect.bottom, box.bottom);
828
+ if (bottom > top) bands.push([top, bottom]);
829
+ };
830
+ const walkInk = (node) => {
831
+ if (node.nodeType === Node.TEXT_NODE) {
832
+ const parent = node.parentElement;
833
+ if (parent !== null && (node.textContent || "").trim() !== "" && !skipTag.test(parent.tagName)) ink(parent);
834
+ return;
835
+ }
836
+ node.childNodes.forEach(walkInk);
837
+ };
838
+ walkInk(surface);
839
+ for (const drawn of Array.from(surface.querySelectorAll(DRAWS))) ink(drawn);
840
+ bands.sort((a, b) => a[0] - b[0]);
841
+ let filled = 0;
842
+ let empty = 0;
843
+ let edge = null;
844
+ for (const [top, bottom] of bands) {
845
+ if (edge !== null && top <= edge) {
846
+ if (bottom > edge) filled += bottom - edge;
847
+ } else {
848
+ if (edge !== null) empty = Math.max(empty, top - edge);
849
+ filled += bottom - top;
850
+ }
851
+ edge = edge === null ? bottom : Math.max(edge, bottom);
852
+ }
853
+ if (empty <= filled) continue;
854
+ overReserved += 1;
855
+ if (reserved.length < 3) {
856
+ reserved.push(`${called2(surface)}: ${Math.round(empty)}px of nothing, ${Math.round(filled)}px drawn`);
857
+ }
858
+ }
859
+ rules.push({
860
+ law: "hierarchy.md",
861
+ rule: "Whitespace is structure",
862
+ total: boxes,
863
+ count: overReserved,
864
+ offenders: reserved
865
+ });
866
+ const fields = within(root, ".lotics-form-field").filter(onScreen);
867
+ const forms = /* @__PURE__ */ new Map();
868
+ for (const field of fields) {
869
+ const owner = field.closest('form, [role="form"]') ?? field.parentElement;
870
+ if (owner === null) continue;
871
+ const run2 = forms.get(owner);
872
+ if (run2 === void 0) forms.set(owner, [field]);
873
+ else run2.push(field);
874
+ }
875
+ let inForms = 0;
876
+ let overMarked = 0;
877
+ const marks = [];
878
+ for (const run2 of forms.values()) {
879
+ if (run2.length < formFloor) continue;
880
+ inForms += run2.length;
881
+ const carried = run2.filter((field) => field.querySelector(".lotics-form-field__mark") !== null);
882
+ if (carried.length * 2 <= run2.length) continue;
883
+ overMarked += carried.length;
884
+ if (marks.length < 3) {
885
+ marks.push(
886
+ `${carried.length} of ${run2.length} fields marked \u2014 ` + carried.slice(0, 3).map((field) => `"${words3(field.querySelector(".lotics-form-field__label")) || words3(field)}"`).join(", ")
887
+ );
888
+ }
889
+ }
890
+ rules.push({
891
+ law: "hierarchy.md",
892
+ rule: "Markers on the minority side",
893
+ total: inForms,
894
+ count: overMarked,
895
+ offenders: marks
896
+ });
897
+ const theme = getComputedStyle(document.documentElement);
898
+ const rgbOf = (value) => {
899
+ const channels = value.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
900
+ if (channels !== null) return [Number(channels[1]), Number(channels[2]), Number(channels[3])];
901
+ const hex = value.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
902
+ if (hex === null) return null;
903
+ const digits = hex[1];
904
+ const pair = (at) => parseInt(digits.length === 3 ? digits[at].repeat(2) : digits.slice(at * 2, at * 2 + 2), 16);
905
+ return [pair(0), pair(1), pair(2)];
906
+ };
907
+ const valence = [];
908
+ for (const token of ["--lotics-ink-danger", "--lotics-ink-warning", "--lotics-ink-success", "--lotics-ink-inactive"]) {
909
+ const ink = rgbOf(theme.getPropertyValue(token));
910
+ if (ink !== null) valence.push(ink);
911
+ }
912
+ const hues = /* @__PURE__ */ new Map();
913
+ let chromatic = 0;
914
+ if (valence.length > 0) {
915
+ const stateInk = '[role="status"], [role="alert"], [aria-invalid="true"], [aria-disabled="true"]';
916
+ const walk = (node) => {
917
+ if (node.nodeType === Node.TEXT_NODE) {
918
+ const text = (node.textContent || "").trim();
919
+ const parent = node.parentElement;
920
+ if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
921
+ if (!onScreen(parent) || parent.closest(chrome) !== null || parent.closest(stateInk) !== null) return;
922
+ const ink = rgbOf(getComputedStyle(parent).color);
923
+ if (ink === null) return;
924
+ if (valence.some((tone) => Math.abs(tone[0] - ink[0]) + Math.abs(tone[1] - ink[1]) + Math.abs(tone[2] - ink[2]) <= sameColour)) return;
925
+ const red = ink[0] / 255;
926
+ const green = ink[1] / 255;
927
+ const blue = ink[2] / 255;
928
+ const top = Math.max(red, green, blue);
929
+ const bottom = Math.min(red, green, blue);
930
+ const light = (top + bottom) / 2;
931
+ const chroma = top - bottom;
932
+ const saturation = chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * light - 1));
933
+ if (saturation < chromaFloor || light < 0.12 || light > 0.92) return;
934
+ const angle = top === red ? (green - blue) / chroma % 6 * 60 : top === green ? ((blue - red) / chroma + 2) * 60 : ((red - green) / chroma + 4) * 60;
935
+ const bucket = Math.round((angle + 360) % 360 / hueStep) * hueStep % 360;
936
+ chromatic += 1;
937
+ const tally = hues.get(bucket);
938
+ if (tally === void 0) hues.set(bucket, { count: 1, sample: text.slice(0, 40) });
939
+ else tally.count += 1;
940
+ return;
941
+ }
942
+ node.childNodes.forEach(walk);
943
+ };
944
+ walk(root);
945
+ }
946
+ const ranked = [...hues.entries()].sort((a, b) => b[1].count - a[1].count);
947
+ const second = ranked.slice(1);
948
+ rules.push({
949
+ law: "hierarchy.md",
950
+ rule: "Colour carries meaning only",
951
+ total: chromatic,
952
+ count: second.reduce((sum, [, tally]) => sum + tally.count, 0),
953
+ offenders: second.slice(0, 3).map(([bucket, tally]) => `a second hue at ${bucket}\xB0 on ${tally.count} runs \u2014 "${tally.sample}"`)
954
+ });
955
+ return rules;
956
+ }
957
+
958
+ // src/probe_collectors/ink_and_air.ts
959
+ var HEADING2 = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
960
+ var GRAPHIC = 'svg, img, picture, canvas, [role="img"], [role="figure"]';
961
+ var CONTROL = 'a[href], button, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="switch"]';
962
+ var NAVIGATION = 'nav, [role="navigation"], [role="banner"]';
963
+ var EDITOR = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="spinbutton"]';
964
+ var FIELD = EDITOR + ', [role="checkbox"], [role="switch"], [role="radio"], [role="slider"]';
965
+ var GROUP = '[role="group"], fieldset';
966
+ var TEXT_ENTRY = /* @__PURE__ */ new Set([
967
+ "text",
968
+ "search",
969
+ "url",
970
+ "tel",
971
+ "email",
972
+ "password",
973
+ "number",
974
+ "date",
975
+ "datetime-local",
976
+ "month",
977
+ "week",
978
+ "time"
979
+ ]);
980
+ var EDGES = ["top", "right", "bottom", "left"];
981
+ var HAIR = 0.5;
982
+ var words2 = (el) => el === null ? "" : (el.textContent || "").replace(/\s+/g, " ").trim();
983
+ var called = (el) => {
984
+ const named = nameOf(el).slice(0, 40);
985
+ return named === "" ? "(unnamed)" : named;
986
+ };
987
+ var px = (value) => {
988
+ const measure = parseFloat(value);
989
+ return Number.isFinite(measure) ? measure : 0;
990
+ };
991
+ var opaque = (colour) => {
992
+ if (colour === "") return true;
993
+ if (/^transparent$/i.test(colour)) return false;
994
+ const alpha = /^rgba\(\s*[\d.]+[,\s]+[\d.]+[,\s]+[\d.]+[,\s/]+([\d.]+)\s*\)/i.exec(colour);
995
+ return alpha === null || Number(alpha[1]) > 0;
996
+ };
997
+ var ringOf = (shadow) => {
998
+ if (shadow === "" || shadow === "none") return 0;
999
+ let widest = 0;
1000
+ for (const segment of shadow.split(/,(?![^(]*\))/)) {
1001
+ if (!opaque((/rgba?\([^)]*\)/i.exec(segment) ?? [""])[0])) continue;
1002
+ const lengths = (segment.match(/-?[\d.]+px/g) ?? []).map((length) => parseFloat(length));
1003
+ if (lengths.length < 3) continue;
1004
+ const [x, y, blur, spread = 0] = lengths;
1005
+ if (Math.abs(x) > HAIR || Math.abs(y) > HAIR) continue;
1006
+ widest = Math.max(widest, spread > 0 ? spread : blur);
1007
+ }
1008
+ return widest;
1009
+ };
1010
+ var sidesOf = (style) => EDGES.filter((edge) => {
1011
+ const line = style.getPropertyValue(`border-${edge}-style`);
1012
+ if (line === "" || line === "none" || line === "hidden") return false;
1013
+ if (px(style.getPropertyValue(`border-${edge}-width`)) <= 0) return false;
1014
+ return opaque(style.getPropertyValue(`border-${edge}-color`));
1015
+ }).length;
1016
+ var closed = (style) => sidesOf(style) === EDGES.length || ringOf(style.boxShadow) > 0;
1017
+ var editorsIn = (root) => within(root, EDITOR).filter((el) => {
1018
+ if (!onScreen(el)) return false;
1019
+ if (el.tagName !== "INPUT") return true;
1020
+ return TEXT_ENTRY.has((el.getAttribute("type") || "text").toLowerCase());
1021
+ });
1022
+ function inkAndAirCensus(root = document.body) {
1023
+ const rules = [];
1024
+ const rank = (el) => {
1025
+ const stated2 = parseInt(el.getAttribute("aria-level") || "", 10);
1026
+ if (Number.isFinite(stated2)) return stated2;
1027
+ const tag = /^H([1-6])$/.exec(el.tagName);
1028
+ return tag === null ? 2 : Number(tag[1]);
1029
+ };
1030
+ const headings = within(root, HEADING2).filter(
1031
+ (el) => onScreen(el) && el.closest(NAVIGATION) === null && words2(el) !== ""
1032
+ );
1033
+ const shallowest = headings.reduce((top, el) => Math.min(top, rank(el)), Infinity);
1034
+ const pageTitle = headings.find((el) => rank(el) === shallowest);
1035
+ const headerRow = (heading) => {
1036
+ const own = words2(heading);
1037
+ let row = heading;
1038
+ for (let up = heading.parentElement; up !== null && up !== root; up = up.parentElement) {
1039
+ if (words2(up) !== own) break;
1040
+ row = up;
1041
+ }
1042
+ return row;
1043
+ };
1044
+ const graphicIn = (row) => Array.from(row.querySelectorAll(GRAPHIC)).find((mark) => onScreen(mark) && mark.closest(CONTROL) === null) ?? null;
1045
+ let sections = 0;
1046
+ let badged = 0;
1047
+ const badges = [];
1048
+ for (const heading of headings) {
1049
+ if (heading === pageTitle) continue;
1050
+ sections += 1;
1051
+ const mark = graphicIn(headerRow(heading));
1052
+ if (mark === null) continue;
1053
+ badged += 1;
1054
+ if (badges.length < 3) {
1055
+ badges.push(`"${words2(heading).slice(0, 40)}": the heading row draws <${mark.tagName.toLowerCase()}>`);
1056
+ }
1057
+ }
1058
+ rules.push({
1059
+ law: "hierarchy.md",
1060
+ rule: "Ink and air state what a thing is \u2192 A section's heading",
1061
+ total: sections,
1062
+ count: badged,
1063
+ offenders: badges
1064
+ });
1065
+ const editors = editorsIn(root);
1066
+ const fieldsIn = (box) => within(box, FIELD).filter(onScreen);
1067
+ const labelsIn = (box) => within(box, "label").filter((el) => onScreen(el) && words2(el) !== "");
1068
+ const oneControl = (el) => el.matches(GROUP) || (el.getAttribute("aria-label") || "").trim() !== "" || (el.getAttribute("aria-labelledby") || "").trim() !== "";
1069
+ const around = (outer, inner) => outer.left <= inner.left + HAIR && outer.top <= inner.top + HAIR && outer.right + HAIR >= inner.right && outer.bottom + HAIR >= inner.bottom;
1070
+ let open = 0;
1071
+ let units = 0;
1072
+ const bare = [];
1073
+ const claimed = /* @__PURE__ */ new Set();
1074
+ for (const editor of editors) {
1075
+ if (claimed.has(editor)) continue;
1076
+ let unit = editor;
1077
+ let parts = fieldsIn(editor);
1078
+ let framed = closed(getComputedStyle(unit));
1079
+ for (let up = unit.parentElement; !framed && up !== null && up !== root; up = up.parentElement) {
1080
+ if (labelsIn(up).length > 0) break;
1081
+ const held = fieldsIn(up);
1082
+ if (held.some((field) => !parts.includes(field))) {
1083
+ if (!oneControl(up) || held.some((field) => claimed.has(field))) break;
1084
+ unit = up;
1085
+ parts = held;
1086
+ framed = closed(getComputedStyle(up));
1087
+ continue;
1088
+ }
1089
+ if (!around(up.getBoundingClientRect(), unit.getBoundingClientRect())) continue;
1090
+ framed = closed(getComputedStyle(up));
1091
+ }
1092
+ for (const part of parts) claimed.add(part);
1093
+ units += 1;
1094
+ if (framed) continue;
1095
+ open += 1;
1096
+ if (bare.length < 3) {
1097
+ bare.push(`"${called(unit)}": ${sidesOf(getComputedStyle(unit))} of 4 sides painted at rest`);
1098
+ }
1099
+ }
1100
+ rules.push({
1101
+ law: "hierarchy.md",
1102
+ rule: "Ink and air state what a thing is \u2192 An editor at rest",
1103
+ total: units,
1104
+ count: open,
1105
+ offenders: bare
1106
+ });
1107
+ const drawn = (el) => Array.from(el.children).filter(onScreen);
1108
+ const captionOf = (group) => {
1109
+ const first = drawn(group)[0];
1110
+ if (first === void 0) return null;
1111
+ if (first.matches(HEADING2)) return first;
1112
+ const inside = first.querySelector(HEADING2);
1113
+ return inside !== null && onScreen(inside) && words2(inside) !== "" ? inside : null;
1114
+ };
1115
+ const rowGapOf = (group) => {
1116
+ const rows = drawn(group).slice(1);
1117
+ let gap = null;
1118
+ for (let at = 1; at < rows.length; at++) {
1119
+ const above = rows[at - 1].getBoundingClientRect();
1120
+ const below = rows[at].getBoundingClientRect();
1121
+ const air = below.top - above.bottom;
1122
+ if (air < 0) continue;
1123
+ gap = gap === null ? air : Math.min(gap, air);
1124
+ }
1125
+ return gap;
1126
+ };
1127
+ let pairs = 0;
1128
+ let level = 0;
1129
+ const merged = [];
1130
+ for (const parent of [root, ...Array.from(root.querySelectorAll("*"))]) {
1131
+ const groups = drawn(parent);
1132
+ for (let at = 1; at < groups.length; at++) {
1133
+ const first = groups[at - 1];
1134
+ const second = groups[at];
1135
+ const opens = captionOf(first);
1136
+ const caption = captionOf(second);
1137
+ if (opens === null || caption === null) continue;
1138
+ const gap = rowGapOf(first) ?? rowGapOf(second);
1139
+ if (gap === null) continue;
1140
+ pairs += 1;
1141
+ const above = caption.getBoundingClientRect().top - first.getBoundingClientRect().bottom;
1142
+ if (above > gap) continue;
1143
+ level += 1;
1144
+ if (merged.length < 3) {
1145
+ merged.push(
1146
+ `"${words2(caption).slice(0, 40)}": ${Math.round(above)}px above the caption, ${Math.round(gap)}px between the rows it follows`
1147
+ );
1148
+ }
1149
+ }
1150
+ }
1151
+ rules.push({
1152
+ law: "hierarchy.md",
1153
+ rule: "Ink and air state what a thing is \u2192 Two named groups",
1154
+ total: pairs,
1155
+ count: level,
1156
+ offenders: merged
1157
+ });
1158
+ return rules;
1159
+ }
1160
+ function focusRingCensus(root = document.body) {
1161
+ const clipperOf = (el) => {
1162
+ for (let up = el.parentElement; up !== null; up = up.parentElement) {
1163
+ const style = getComputedStyle(up);
1164
+ const across = style.overflowX || style.overflow;
1165
+ const down = style.overflowY || style.overflow;
1166
+ if (across !== "" && across !== "visible" || down !== "" && down !== "visible") return up;
1167
+ }
1168
+ return null;
1169
+ };
1170
+ const shift = (before, range, short, over) => {
1171
+ const wanted = over > 0 ? over : short > 0 ? -short : 0;
1172
+ return Math.max(0, Math.min(range, before + wanted));
1173
+ };
1174
+ let rings2 = 0;
1175
+ let cut = 0;
1176
+ const clipped = [];
1177
+ for (const editor of editorsIn(root)) {
1178
+ const style = getComputedStyle(editor);
1179
+ const line = style.outlineStyle;
1180
+ const outline = line === "" || line === "none" ? 0 : px(style.outlineWidth) + px(style.outlineOffset);
1181
+ const extent = Math.max(outline, ringOf(style.boxShadow));
1182
+ if (extent <= 0) continue;
1183
+ rings2 += 1;
1184
+ const clipper = clipperOf(editor);
1185
+ if (clipper === null) continue;
1186
+ const scrolled = { top: clipper.scrollTop, left: clipper.scrollLeft };
1187
+ const first = editor.getBoundingClientRect();
1188
+ const port = clipper.getBoundingClientRect();
1189
+ clipper.scrollTop = shift(
1190
+ scrolled.top,
1191
+ clipper.scrollHeight - clipper.clientHeight,
1192
+ port.top - (first.top - extent),
1193
+ first.bottom + extent - port.bottom
1194
+ );
1195
+ clipper.scrollLeft = shift(
1196
+ scrolled.left,
1197
+ clipper.scrollWidth - clipper.clientWidth,
1198
+ port.left - (first.left - extent),
1199
+ first.right + extent - port.right
1200
+ );
1201
+ const box = editor.getBoundingClientRect();
1202
+ const clip = clipper.getBoundingClientRect();
1203
+ const outside = [
1204
+ { edge: "top", by: clip.top - (box.top - extent) },
1205
+ { edge: "right", by: box.right + extent - clip.right },
1206
+ { edge: "bottom", by: box.bottom + extent - clip.bottom },
1207
+ { edge: "left", by: clip.left - (box.left - extent) }
1208
+ ].filter((side) => side.by > HAIR);
1209
+ clipper.scrollTop = scrolled.top;
1210
+ clipper.scrollLeft = scrolled.left;
1211
+ if (outside.length === 0) continue;
1212
+ cut += 1;
1213
+ if (clipped.length < 3) {
1214
+ const worst = Math.round(Math.max(...outside.map((side) => side.by)));
1215
+ clipped.push(
1216
+ `"${called(editor)}": a ${extent}px focus ring cut by up to ${worst}px, on the ` + outside.map((side) => side.edge).join(", ")
1217
+ );
1218
+ }
1219
+ }
1220
+ return [
1221
+ {
1222
+ law: "hierarchy.md",
1223
+ rule: "Ink and air state what a thing is \u2192 A focus ring",
1224
+ total: rings2,
1225
+ count: cut,
1226
+ offenders: clipped
1227
+ }
1228
+ ];
1229
+ }
1230
+
1231
+ // src/probe_collectors/right_form.ts
1232
+ var DAY_RUN_PART = ".lotics-itinerary";
1233
+ var SIDE = 'aside, [role="complementary"], nav, [role="navigation"]';
1234
+ var IN_PAGE = 'button, summary, [role="button"], [role="link"], [role="tab"], [role="menuitem"], a[href^="#"]';
1235
+ var HEADING3 = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
1236
+ var RECORD_ROWS = 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="list"], ul, ol';
1237
+ var PARKED = [
1238
+ {
1239
+ is: 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="spinbutton"], [role="slider"], [role="checkbox"], [role="radio"], [role="switch"], form, [role="form"]',
1240
+ called: "a field"
1241
+ },
1242
+ { is: 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="listbox"]', called: "a list of rows" },
1243
+ { is: 'article, [role="article"], [role="feed"], [role="log"]', called: "a conversation" },
1244
+ { is: 'img, svg, picture, canvas, [role="img"], [role="figure"]', called: "a picture" },
1245
+ { is: "a[href]", called: "a link out of the page" }
1246
+ ];
1247
+ function rightFormCensus(root = document.body) {
1248
+ const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
1249
+ const recordSurface = '.lotics-record-page, [role="dialog"]';
1250
+ const columnFloor = 3;
1251
+ const twoTrackWidth = 1016;
1252
+ const trackHeight = 240;
1253
+ const words3 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
1254
+ const surfaces = within(root, recordSurface).filter(onScreen);
1255
+ const called2 = (el) => words3(el.querySelector(".lotics-record-page__heading, h1, h2")) || "the record";
1256
+ const readingInside = (side) => {
1257
+ for (const kind of PARKED) {
1258
+ for (const el of Array.from(side.querySelectorAll(kind.is))) {
1259
+ if (!onScreen(el) || el.closest(IN_PAGE) !== null) continue;
1260
+ const named = nameOf(el).slice(0, 40) || words3(el);
1261
+ return named === "" ? kind.called : `${kind.called} ("${named}")`;
1262
+ }
1263
+ }
1264
+ return null;
1265
+ };
1266
+ const rules = [];
1267
+ const choosers = within(root, 'select, [role="listbox"]').filter(onScreen);
1268
+ let twoOption = 0;
1269
+ const pairs = [];
1270
+ for (const chooser of choosers) {
1271
+ const options = Array.from(
1272
+ chooser.tagName === "SELECT" ? chooser.querySelectorAll("option") : chooser.querySelectorAll('[role="option"]')
1273
+ ).map((option) => (option.textContent || "").trim()).filter((label) => label !== "");
1274
+ if (options.length !== 2) continue;
1275
+ twoOption += 1;
1276
+ if (pairs.length < 3) {
1277
+ const field = chooser.closest(".lotics-form-field");
1278
+ const named = nameOf(chooser).slice(0, 40) || words3(field === null ? null : field.querySelector(".lotics-form-field__label")) || "(unnamed)";
1279
+ pairs.push(`"${named}": a two-option select \u2014 ${options.map((option) => `"${option}"`).join(" / ")}`);
1280
+ }
1281
+ }
1282
+ rules.push({
1283
+ law: "data_entry.md",
1284
+ rule: "Choosing a CHOICE control \u2014 by option count, not by taste",
1285
+ total: choosers.length,
1286
+ count: twoOption,
1287
+ offenders: pairs
1288
+ });
1289
+ let dayRuns = false;
1290
+ for (const sheet of Array.from(document.styleSheets)) {
1291
+ try {
1292
+ for (const rule of Array.from(sheet.cssRules)) {
1293
+ if (rule.cssText.includes(DAY_RUN_PART)) {
1294
+ dayRuns = true;
1295
+ break;
1296
+ }
1297
+ }
1298
+ } catch {
1299
+ continue;
1300
+ }
1301
+ if (dayRuns) break;
1302
+ }
1303
+ const dated = /^\d{1,4}[/.-]\d{1,2}(?:[/.-]\d{1,4})?$/;
1304
+ const dates = [];
1305
+ const headings = [];
1306
+ if (dayRuns) {
1307
+ const walk = (node) => {
1308
+ if (node.nodeType === Node.TEXT_NODE) {
1309
+ const text = (node.textContent || "").trim();
1310
+ const parent = node.parentElement;
1311
+ if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
1312
+ if (!onScreen(parent)) return;
1313
+ const box = parent.getBoundingClientRect();
1314
+ const leaf = { text, left: Math.round(box.left), right: Math.round(box.right), top: Math.round(box.top) };
1315
+ if (dated.test(text)) dates.push(leaf);
1316
+ else headings.push(leaf);
1317
+ return;
1318
+ }
1319
+ node.childNodes.forEach(walk);
1320
+ };
1321
+ walk(root);
1322
+ }
1323
+ const stacks = [];
1324
+ for (const leaf of [...dates].sort((a, b) => a.left - b.left || a.top - b.top)) {
1325
+ const stack = stacks.find((column) => leaf.left < column.right && leaf.right > column.left);
1326
+ if (stack !== void 0) {
1327
+ stack.items.push(leaf);
1328
+ stack.left = Math.min(stack.left, leaf.left);
1329
+ stack.right = Math.max(stack.right, leaf.right);
1330
+ } else {
1331
+ stacks.push({ left: leaf.left, right: leaf.right, items: [leaf] });
1332
+ }
1333
+ }
1334
+ const columns = stacks.filter((stack) => stack.items.length >= columnFloor);
1335
+ let grouped = 0;
1336
+ const runs = [];
1337
+ for (const column of columns) {
1338
+ const days = new Set(column.items.map((leaf) => leaf.text));
1339
+ if (days.size >= column.items.length) continue;
1340
+ grouped += 1;
1341
+ if (runs.length < 3) {
1342
+ const top = Math.min(...column.items.map((leaf) => leaf.top));
1343
+ const over = headings.filter((label) => label.top < top && top - label.top <= 360 && label.left < column.right && label.right > column.left).sort((a, b) => b.top - a.top)[0];
1344
+ runs.push(
1345
+ `${over === void 0 ? "(unnamed)" : over.text}: ${column.items.length} rows over ${days.size} days, the date repeated per row`
1346
+ );
1347
+ }
1348
+ }
1349
+ rules.push({
1350
+ law: "templates.md",
1351
+ rule: "Which device answers the question \u2192 Read a trip's stops in order",
1352
+ total: columns.length,
1353
+ count: grouped,
1354
+ offenders: runs
1355
+ });
1356
+ const namesItself = (region) => {
1357
+ const heading = Array.from(region.querySelectorAll(HEADING3)).find((el) => el.closest("summary") === null);
1358
+ return heading !== void 0 || region.querySelector(RECORD_ROWS) !== null;
1359
+ };
1360
+ let folds = 0;
1361
+ let sections = 0;
1362
+ const hidden = [];
1363
+ for (const surface of surfaces) {
1364
+ for (const fold of Array.from(surface.querySelectorAll(".lotics-accordion__trigger, details, [aria-expanded]"))) {
1365
+ if (!onScreen(fold) || fold.closest('[role="complementary"]') !== null) continue;
1366
+ if (fold.hasAttribute("aria-haspopup") || fold.matches(HOLDS_A_VALUE)) continue;
1367
+ folds += 1;
1368
+ const opened = fold.tagName === "DETAILS" ? [fold] : pointedAt(fold, "aria-controls");
1369
+ if (!fold.matches(".lotics-accordion__trigger") && !opened.some(namesItself)) continue;
1370
+ sections += 1;
1371
+ if (hidden.length < 3) {
1372
+ const named = nameOf(fold).slice(0, 40);
1373
+ hidden.push(`${called2(surface)}: "${named === "" ? "(unnamed)" : named}" is a section behind a fold`);
1374
+ }
1375
+ }
1376
+ }
1377
+ rules.push({
1378
+ law: "templates.md",
1379
+ rule: "Which device answers the question \u2192 See what NAMES this record",
1380
+ total: folds,
1381
+ count: sections,
1382
+ offenders: hidden
1383
+ });
1384
+ let read = 0;
1385
+ let split = 0;
1386
+ const tracks = [];
1387
+ if (window.innerWidth >= twoTrackWidth) {
1388
+ for (const surface of surfaces) {
1389
+ read += 1;
1390
+ const box = surface.getBoundingClientRect();
1391
+ const owners = [surface, ...Array.from(surface.querySelectorAll("*"))];
1392
+ let found = null;
1393
+ for (const owner of owners) {
1394
+ const columnsHere = Array.from(owner.children).filter((child) => {
1395
+ if (!onScreen(child)) return false;
1396
+ if (child.matches(SIDE) || child.querySelector(SIDE) !== null) return false;
1397
+ const rect = child.getBoundingClientRect();
1398
+ return rect.height >= trackHeight && rect.height >= box.height * 0.5 && rect.width >= box.width * 0.2;
1399
+ });
1400
+ for (let a = 0; a < columnsHere.length && found === null; a++) {
1401
+ for (let b = a + 1; b < columnsHere.length && found === null; b++) {
1402
+ const left = columnsHere[a].getBoundingClientRect();
1403
+ const right = columnsHere[b].getBoundingClientRect();
1404
+ const apart = left.right <= right.left + 8 || right.right <= left.left + 8;
1405
+ const overlap = Math.min(left.bottom, right.bottom) - Math.max(left.top, right.top);
1406
+ const together = Math.max(left.right, right.right) - Math.min(left.left, right.left);
1407
+ if (apart && overlap >= Math.min(left.height, right.height) * 0.5 && together >= box.width * 0.7) {
1408
+ found = [columnsHere[a], columnsHere[b]];
1409
+ }
1410
+ }
1411
+ }
1412
+ if (found !== null) break;
1413
+ }
1414
+ if (found !== null) {
1415
+ split += 1;
1416
+ if (tracks.length < 3) {
1417
+ const left = Math.round(found[0].getBoundingClientRect().width);
1418
+ const right = Math.round(found[1].getBoundingClientRect().width);
1419
+ tracks.push(`${called2(surface)}: two columns side by side, ${left}px and ${right}px`);
1420
+ }
1421
+ continue;
1422
+ }
1423
+ for (const side of Array.from(surface.querySelectorAll(SIDE))) {
1424
+ if (!onScreen(side)) continue;
1425
+ const rect = side.getBoundingClientRect();
1426
+ if (rect.width > box.width * 0.6) continue;
1427
+ const column = Array.from(surface.querySelectorAll("*")).some((other) => {
1428
+ if (other === side || side.contains(other) || other.contains(side) || !onScreen(other)) return false;
1429
+ const beside = other.getBoundingClientRect();
1430
+ const apart = beside.left >= rect.right - 8 || beside.right <= rect.left + 8;
1431
+ const overlap = Math.min(beside.bottom, rect.bottom) - Math.max(beside.top, rect.top);
1432
+ return apart && beside.width >= box.width * 0.4 && overlap >= rect.height * 0.5;
1433
+ });
1434
+ if (!column) continue;
1435
+ const parked = readingInside(side);
1436
+ if (parked === null) continue;
1437
+ split += 1;
1438
+ if (tracks.length < 3) tracks.push(`${called2(surface)}: ${parked} beside the reading column`);
1439
+ }
1440
+ }
1441
+ }
1442
+ rules.push({
1443
+ law: "hierarchy.md",
1444
+ rule: "Depth \u2014 four surfaces, four jobs",
1445
+ total: read,
1446
+ count: split,
1447
+ offenders: tracks
1448
+ });
1449
+ return rules;
1450
+ }
1451
+
1452
+ // src/probe_collectors/surface.ts
1453
+ function surfaceChrome(root = document.body) {
1454
+ const round = (n) => Math.round(n * 10) / 10;
1455
+ const anchor = (el) => el === void 0 ? null : { text: (el.textContent || "").trim().slice(0, 80), left: round(el.getBoundingClientRect().left) };
1456
+ const nav = within(root, "nav")[0] ?? null;
1457
+ const navFirst = nav === null ? void 0 : Array.from(nav.querySelectorAll('a[href], [role="link"]')).filter(
1458
+ (item) => onScreen(item) && (item.textContent || "").trim() !== ""
1459
+ )[0];
1460
+ const headings = within(root, "h1, h2, h3, h4, h5, h6").filter(
1461
+ (el) => onScreen(el) && el.closest('nav, [role="banner"]') === null && (el.textContent || "").trim() !== ""
1462
+ );
1463
+ const title = headings[0];
1464
+ const rank = (el) => Number(el.tagName.slice(1));
1465
+ const heading = title === void 0 ? void 0 : headings.slice(1).find((el) => rank(el) > rank(title));
1466
+ const primaries = within(root, '[data-variant="primary"]').filter(
1467
+ (el) => onScreen(el) && el.closest('nav, [role="banner"]') === null
1468
+ );
1469
+ const folds = within(root, '[aria-expanded="false"]').filter(
1470
+ (el) => onScreen(el) && el.getAttribute("aria-haspopup") === null
1471
+ );
1472
+ const foldNames = folds.map((el) => nameOf(el).slice(0, 60));
1473
+ const folded = folds.reduce((sum, el) => {
1474
+ const digits = /\d+/.exec((el.textContent || "").trim());
1475
+ return sum + (digits === null ? 0 : Number(digits[0]));
1476
+ }, 0);
1477
+ return {
1478
+ nav: anchor(navFirst),
1479
+ title: anchor(title),
1480
+ heading: anchor(heading),
1481
+ primary: {
1482
+ count: primaries.length,
1483
+ names: primaries.map((el) => nameOf(el).slice(0, 40))
1484
+ },
1485
+ facts: { visible: labelValuePairs(root), folded, folds: foldNames }
1486
+ };
1487
+ }
1488
+ function labelValuePairs(root) {
1489
+ const px2 = (el) => parseFloat(getComputedStyle(el).fontSize);
1490
+ const isLeaf = (el) => el.children.length === 0 && (el.textContent || "").trim().length > 0;
1491
+ const leafOf = (el, depth = 3) => {
1492
+ if (isLeaf(el)) return el;
1493
+ if (depth === 0 || el.children.length === 0) return null;
1494
+ return leafOf(el.children[0], depth - 1);
1495
+ };
1496
+ let pairs = 0;
1497
+ for (const el of Array.from(root.querySelectorAll("div"))) {
1498
+ const kids = Array.from(el.children);
1499
+ if (kids.length !== 2) continue;
1500
+ if (el.closest('nav, [role="banner"]') !== null) continue;
1501
+ const label = leafOf(kids[0]);
1502
+ const value = leafOf(kids[1]);
1503
+ if (label === null || value === null) continue;
1504
+ const a = kids[0].getBoundingClientRect();
1505
+ const b = kids[1].getBoundingClientRect();
1506
+ if (a.height === 0 || b.height === 0) continue;
1507
+ if (px2(label) < px2(value)) pairs += 1;
1508
+ }
1509
+ return pairs;
1510
+ }
1511
+ function repeatedValues(root = document.body) {
1512
+ const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
1513
+ const counts = /* @__PURE__ */ new Map();
1514
+ let total = 0;
1515
+ for (const el of Array.from(root.querySelectorAll("*"))) {
1516
+ if (skipTag.test(el.tagName) || el.children.length > 0) continue;
1517
+ const text = (el.textContent || "").trim();
1518
+ if (text.length < 3) continue;
1519
+ if (!/\d/.test(text) || !/[^\d\s]/.test(text)) continue;
1520
+ if (!onScreen(el)) continue;
1521
+ total += 1;
1522
+ counts.set(text, (counts.get(text) || 0) + 1);
1523
+ }
1524
+ const repeated = Array.from(counts.entries()).filter(([, times]) => times > 1).sort((a, b) => b[1] - a[1]).map(([text, times]) => ({ text: text.slice(0, 60), times }));
1525
+ return { total, repeated };
1526
+ }
1527
+ function wrappedMoney(root = document.body) {
1528
+ const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
1529
+ const money = /[$€£¥₫₹]|\bUS\$/;
1530
+ const wrapped = [];
1531
+ let total = 0;
1532
+ for (const el of Array.from(root.querySelectorAll("*"))) {
1533
+ if (skipTag.test(el.tagName) || el.children.length > 0) continue;
1534
+ const text = (el.textContent || "").trim();
1535
+ if (text === "" || !money.test(text) || !/\d/.test(text)) continue;
1536
+ if (!onScreen(el)) continue;
1537
+ total += 1;
1538
+ const range = document.createRange();
1539
+ range.selectNodeContents(el);
1540
+ const lines = Array.from(range.getClientRects()).filter((rect) => rect.width > 0).length;
1541
+ if (lines > 1) wrapped.push({ text: text.slice(0, 50), lines });
1542
+ }
1543
+ return { total, wrapped };
1544
+ }
1545
+
393
1546
  // src/probe_page.ts
394
1547
  var PROBES = {
395
1548
  money_edges: {
1549
+ law: "reviewing.md",
396
1550
  section: "6",
397
1551
  fix: "One right edge per money column: give every figure in it the same alignment and the same unit."
398
1552
  },
399
1553
  reported_values: {
1554
+ law: "reviewing.md",
400
1555
  section: "9",
401
1556
  fix: "Encode the column the screen is ABOUT \u2014 a mark, a meter, a chart or a picture \u2014 and leave the rest as text; a meter whose maximum is the bound it alarms at reads the same on every row, so it encodes nothing."
402
1557
  },
1558
+ empty_marks: {
1559
+ law: "reviewing.md",
1560
+ section: "9",
1561
+ fix: "Fill the picture column or drop it: a mark slot drawn empty on every row states nothing and takes a column's width."
1562
+ },
1563
+ stage_words: {
1564
+ law: "reviewing.md",
1565
+ section: "9",
1566
+ fix: "A state is the one thing a register encodes with COLOUR: give the stage cell its option's swatch, or the glyph its verdict wears. The words stay; the mark is what the reader scans."
1567
+ },
403
1568
  internal_ids: {
1569
+ law: "reviewing.md",
404
1570
  section: "8",
405
1571
  fix: "A key is not a name: render the record's own label, and keep the id in the link."
406
1572
  },
407
1573
  tab_strips: {
1574
+ law: "reviewing.md",
408
1575
  section: "8a",
409
1576
  fix: "One navigation strip per screen. A second is a filter wearing navigation's clothes \u2014 draw it as one."
410
1577
  },
411
1578
  truncation: {
1579
+ law: "reviewing.md",
412
1580
  section: "8g",
413
1581
  fix: "Shed a trailing column, or widen this one until the value fits \u2014 a date, a code or a figure cut inside itself reads as a different one."
414
1582
  },
415
1583
  banned_glyph: {
1584
+ law: "reviewing.md",
416
1585
  section: "10",
417
1586
  fix: "No punctuation doing a word's job \u2014 separate the two facts with layout, or write the word."
418
1587
  },
419
1588
  compact_money_words: {
1589
+ law: "reviewing.md",
420
1590
  section: "10",
421
1591
  fix: "Abbreviate in the currency's own words: tr/t\u1EF7 beside \u20AB, K/M/B beside $ and \u20AC."
422
1592
  },
423
1593
  stacked_pairs: {
1594
+ law: "reviewing.md",
424
1595
  section: "1",
425
1596
  fix: "Demote once, not twice: either the size steps down or the subject gets the weight."
426
1597
  },
427
1598
  row_marks: {
1599
+ law: "reviewing.md",
428
1600
  section: "9",
429
1601
  fix: "One rung per row: a mark's size follows what the row is ABOUT, so the supporting mark takes the smaller one."
430
1602
  },
431
1603
  render: {
1604
+ law: "reviewing.md",
432
1605
  section: "8",
433
1606
  fix: "Open the screen and read it: nothing below this was measured, so a clean run over the rest means nothing."
434
1607
  },
435
1608
  settle: {
1609
+ law: "reviewing.md",
436
1610
  section: "9c",
437
1611
  fix: "A screen that never stops changing has a render that re-renders or a request that never lands \u2014 find which."
1612
+ },
1613
+ /**
1614
+ * The three CENSUSES. They differ from every probe above in shape: each one
1615
+ * makes SEVERAL measurements and raises one finding per measurement that
1616
+ * fired, so the signature here is the census's own frame and each detail line
1617
+ * names the rule it is. A screen is cluttered, in the wrong form, or drawn as
1618
+ * something it is not as a WHOLE, which is why none of them reduces to one
1619
+ * number.
1620
+ */
1621
+ clutter: {
1622
+ law: "hierarchy.md",
1623
+ section: "Budgets a reviewer can count",
1624
+ fix: "Move a fact down a tier, fold it, or take it off the screen \u2014 a budget failing is a tiering question, never a permission to shrink something."
1625
+ },
1626
+ right_form: {
1627
+ law: "templates.md",
1628
+ section: "Which device answers the question",
1629
+ fix: "Draw what the JOB names: the device index gives the entry and the wrong device beside it, and a shape is picked by what the reader must do rather than by what the data is."
1630
+ },
1631
+ ink_and_air: {
1632
+ law: "hierarchy.md",
1633
+ section: "Ink and air state what a thing is",
1634
+ fix: "Draw the thing as what it is: words for a heading, a closed frame for an editor, room for its focus ring, and more air between two named groups than between the rows inside either."
438
1635
  }
439
1636
  };
440
1637
  function signature(probe) {
441
1638
  return PROBES[probe];
442
1639
  }
1640
+ function censusFindings(probe, rules) {
1641
+ const fired = rules.filter((rule) => rule.count > 0);
1642
+ if (fired.length === 0) return [];
1643
+ return [
1644
+ {
1645
+ probe,
1646
+ ...signature(probe),
1647
+ total: rules.reduce((sum, rule) => sum + rule.total, 0),
1648
+ count: fired.reduce((sum, rule) => sum + rule.count, 0),
1649
+ detail: fired.map(
1650
+ (rule) => `${rule.law} \xA7${rule.rule} \u2014 ${rule.count} of ${rule.total}` + (rule.offenders.length === 0 ? "" : `: ${rule.offenders.join(" | ")}`)
1651
+ )
1652
+ }
1653
+ ];
1654
+ }
443
1655
  var MONEY_EDGES_ALLOWED = 2;
444
1656
  var MONEY_COLUMN_MIN = 3;
445
1657
  var BARE_VALUES_FLOOR = 8;
446
1658
  var QUANTITY_COLUMN_MIN = 2;
447
1659
  function run(options) {
448
1660
  const findings = [];
449
- const money = moneyColumns();
1661
+ const surface = measuredSurface();
1662
+ const money = moneyColumns(surface);
450
1663
  const ragged = money.columns.filter((column) => column.size >= MONEY_COLUMN_MIN && column.edges.length > MONEY_EDGES_ALLOWED);
451
1664
  if (ragged.length > 0) {
452
1665
  findings.push({
@@ -457,25 +1670,53 @@ var __loticsProbe = (() => {
457
1670
  detail: ragged.slice(0, 5).map((column) => `x=${column.x}: ${column.edges.length} right edges over ${column.size} figures \u2014 ${column.samples.join(" | ")}`)
458
1671
  });
459
1672
  }
460
- const values = reportedValues();
1673
+ const values = reportedValues(surface);
461
1674
  const measurable = values.columns.some((column) => column.quantities >= QUANTITY_COLUMN_MIN) || values.devices > 0;
462
- if (values.bare >= BARE_VALUES_FLOOR && values.encoded === 0 && measurable) {
1675
+ const emptyPictureColumns = values.markSlots.filter((column) => column.filled === 0);
1676
+ const silentColumns = values.columns.filter(
1677
+ (column) => column.quantities === 0 && column.size >= QUANTITY_COLUMN_MIN
1678
+ );
1679
+ const drawnAndSilent = values.devices > 0 && values.encoded === 0 && silentColumns.length > 0 && values.total >= BARE_VALUES_FLOOR;
1680
+ const wordsOnly = values.bare >= BARE_VALUES_FLOOR && values.encoded === 0 && measurable;
1681
+ if (emptyPictureColumns.length > 0) {
1682
+ findings.push({
1683
+ probe: "empty_marks",
1684
+ ...signature("empty_marks"),
1685
+ total: values.markSlots.reduce((sum, column) => sum + column.size, 0),
1686
+ count: emptyPictureColumns.reduce((sum, column) => sum + column.size, 0),
1687
+ detail: emptyPictureColumns.slice(0, 5).map(
1688
+ (column) => `${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} picture slots, none holding a picture`
1689
+ )
1690
+ });
1691
+ } else if (drawnAndSilent || wordsOnly) {
463
1692
  findings.push({
464
1693
  probe: "reported_values",
465
1694
  ...signature("reported_values"),
466
1695
  total: values.total,
467
1696
  count: values.bare,
468
1697
  detail: [
469
- values.devices === 0 ? `${values.bare} values as bare text, nothing drawn as a mark, meter, chart or picture` : `${values.bare} values as bare text; ${values.devices} drawn, none of them reading a value`,
1698
+ values.devices === 0 ? `${values.bare} unnamed figures as bare text, nothing drawn as a mark, meter, chart or picture` : `${values.devices} drawn, none of them reading a value; ${values.bare} unnamed figures as bare text`,
470
1699
  ...values.columns.slice(0, 5).map((column) => ` ${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} \u2014 ${column.samples.join(", ")}`)
471
1700
  ]
472
1701
  });
473
1702
  }
474
- const ids = internalIds();
1703
+ const ids = internalIds(surface);
475
1704
  if (ids.ids.length > 0) {
476
1705
  findings.push({ probe: "internal_ids", ...signature("internal_ids"), total: ids.total, count: ids.ids.length, detail: ids.ids.slice(0, 5) });
477
1706
  }
478
- const strips = tabStrips();
1707
+ const stageWords = stageWordColumns(surface);
1708
+ if (stageWords.wordsOnly.length > 0) {
1709
+ findings.push({
1710
+ probe: "stage_words",
1711
+ ...signature("stage_words"),
1712
+ total: stageWords.total,
1713
+ count: stageWords.wordsOnly.reduce((sum, column) => sum + column.size, 0),
1714
+ detail: stageWords.wordsOnly.slice(0, 5).map(
1715
+ (column) => `${column.slot === "" ? "(unnamed)" : column.slot}: the stage column is words alone \u2014 ${column.size} rows of ${column.values.join(" / ")}, no swatch or glyph`
1716
+ )
1717
+ });
1718
+ }
1719
+ const strips = tabStrips(surface);
479
1720
  if (strips.counted > 1) {
480
1721
  findings.push({
481
1722
  probe: "tab_strips",
@@ -486,7 +1727,7 @@ var __loticsProbe = (() => {
486
1727
  });
487
1728
  }
488
1729
  if (options.truncation) {
489
- const cut = truncatedLeaves();
1730
+ const cut = truncatedLeaves(surface);
490
1731
  if (cut.cut.length > 0) {
491
1732
  findings.push({
492
1733
  probe: "truncation",
@@ -497,11 +1738,11 @@ var __loticsProbe = (() => {
497
1738
  });
498
1739
  }
499
1740
  }
500
- const glyphs = bannedGlyphs();
1741
+ const glyphs = bannedGlyphs(surface);
501
1742
  if (glyphs.found.length > 0) {
502
1743
  findings.push({ probe: "banned_glyph", ...signature("banned_glyph"), total: glyphs.total, count: glyphs.found.length, detail: glyphs.found.slice(0, 5) });
503
1744
  }
504
- const compact = compactMoneyWords();
1745
+ const compact = compactMoneyWords(surface);
505
1746
  if (compact.foreign.length > 0) {
506
1747
  findings.push({
507
1748
  probe: "compact_money_words",
@@ -511,7 +1752,7 @@ var __loticsProbe = (() => {
511
1752
  detail: compact.foreign.slice(0, 5).map((text) => `"${text}" abbreviates in the other currency's words`)
512
1753
  });
513
1754
  }
514
- const pairs = scanStackedPairs();
1755
+ const pairs = scanStackedPairs(surface);
515
1756
  if (pairs.overDemoted.length > 0) {
516
1757
  findings.push({
517
1758
  probe: "stacked_pairs",
@@ -521,7 +1762,7 @@ var __loticsProbe = (() => {
521
1762
  detail: pairs.overDemoted.slice(0, 5).map((pair) => `"${pair.subject}" ${pair.sizes[0]}px/${pair.weights[0]} over "${pair.supporting}" ${pair.sizes[1]}px/${pair.weights[1]}`)
522
1763
  });
523
1764
  }
524
- const marks = scanRowMarks();
1765
+ const marks = scanRowMarks(surface);
525
1766
  if (marks.mismatches.length > 0) {
526
1767
  findings.push({
527
1768
  probe: "row_marks",
@@ -531,20 +1772,46 @@ var __loticsProbe = (() => {
531
1772
  detail: marks.mismatches.slice(0, 5).map((row) => `y=${row.y}: ${row.marks.join(", ")}`)
532
1773
  });
533
1774
  }
1775
+ findings.push(...censusFindings("clutter", clutterCensus(surface)));
1776
+ findings.push(...censusFindings("right_form", rightFormCensus(surface)));
1777
+ findings.push(...censusFindings("ink_and_air", [...inkAndAirCensus(surface), ...options.rings ?? []]));
534
1778
  return {
535
1779
  findings,
536
1780
  census: {
537
- leaves: textLeafCount(),
1781
+ leaves: textLeafCount(surface),
538
1782
  money: money.total,
539
1783
  bare: values.bare,
540
1784
  quantities: values.quantities,
541
1785
  encoded: values.encoded,
542
1786
  devices: values.devices,
1787
+ device_kinds: values.deviceKinds.map(
1788
+ (device) => `${device.kind}\xD7${device.slot === "" ? "(unnamed)" : device.slot}: ${device.count}`
1789
+ ),
543
1790
  strips: strips.counted
544
1791
  }
545
1792
  };
546
1793
  }
547
- function textLeafCount() {
1794
+ function rings() {
1795
+ return focusRingCensus(measuredSurface());
1796
+ }
1797
+ function editorSelector() {
1798
+ return EDITOR;
1799
+ }
1800
+ function focused() {
1801
+ const held = document.activeElement;
1802
+ if (held === null || held === document.body) return null;
1803
+ const named = nameOf(held).slice(0, 40);
1804
+ return named === "" ? held.tagName.toLowerCase() : named;
1805
+ }
1806
+ function paintState() {
1807
+ const root = document.getElementById("root");
1808
+ return {
1809
+ leaves: textLeafCount(),
1810
+ loading: document.readyState === "loading",
1811
+ booted: root !== null && root.hasChildNodes()
1812
+ };
1813
+ }
1814
+ function textLeafCount(root = document.body) {
548
1815
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
549
1816
  let count = 0;
550
1817
  const walk = (node) => {
@@ -555,7 +1822,7 @@ var __loticsProbe = (() => {
555
1822
  }
556
1823
  node.childNodes.forEach(walk);
557
1824
  };
558
- walk(document.body);
1825
+ walk(root);
559
1826
  return count;
560
1827
  }
561
1828
  function navItems() {
@@ -586,10 +1853,12 @@ var __loticsProbe = (() => {
586
1853
  }
587
1854
  function recordLinks() {
588
1855
  const here = location.pathname + location.search;
589
- const doors = Array.from(document.querySelectorAll('a[href], [data-opens="page"]'));
590
- return doors.filter((door) => {
1856
+ const doors2 = Array.from(
1857
+ document.querySelectorAll('a[href], [data-opens="page"], button, [role="button"], [role="link"]')
1858
+ ).filter((door) => door.matches('a[href], [data-opens="page"]') || isEmptyDoor(door));
1859
+ return doors2.filter((door) => {
591
1860
  if (door.closest('nav, [role="banner"], [role="tablist"]') !== null) return false;
592
- if (doors.some((other) => other !== door && other.contains(door))) return false;
1861
+ if (doors2.some((other) => other !== door && other.contains(door))) return false;
593
1862
  const rect = door.getBoundingClientRect();
594
1863
  if (rect.width === 0 || rect.height === 0) return false;
595
1864
  const href = door.getAttribute("href");
@@ -599,7 +1868,7 @@ var __loticsProbe = (() => {
599
1868
  });
600
1869
  }
601
1870
  function recordRows() {
602
- return recordLinks().map((door) => (door.textContent || "").trim() || door.getAttribute("href") || "");
1871
+ return recordLinks().map(doorName);
603
1872
  }
604
1873
  function openRecord(index) {
605
1874
  const door = recordLinks()[index];
@@ -607,5 +1876,119 @@ var __loticsProbe = (() => {
607
1876
  door.click();
608
1877
  return true;
609
1878
  }
1879
+ var OVERLAY_ROLES = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
1880
+ var NEVER_PRESS = /delete|remove|archive|discard|revoke|unlink|detach|cancel|reset|clear|approve|reject|send|publish|\bpay|submit|confirm|void|deactivate|disable|merge|overwrite|\bsign\b|issue|export|download|print|\bsave\b|\bapply\b/i;
1881
+ var NEVER_PRESS_VI_WORDS = [
1882
+ "x[o\xF3][a\xE1]",
1883
+ "h[\u1EE7u][\u1EF7y]",
1884
+ "g[\u1EE1o]",
1885
+ "l[\u01B0u]u",
1886
+ "duy[\u1EC7e]t",
1887
+ "t[\u1EEBu] ch[\u1ED1o]i",
1888
+ "g[\u1EEDu]i",
1889
+ "thanh to[\xE1a]n",
1890
+ "ph[\xE1a]t h[\xE0a]nh",
1891
+ "x[\xE1a]c nh[\u1EADa]n",
1892
+ "k[\xFDy]",
1893
+ "ghi s[\u1ED5o]",
1894
+ "kh[\xF3o]a",
1895
+ "kho[\xE1a]",
1896
+ "ho[\xE0a]n",
1897
+ "ch[\u1ED1o]t",
1898
+ "t[\xE1a]ch",
1899
+ "g[\u1ED9o]p",
1900
+ "xu[\u1EA5a]t",
1901
+ "t[\u1EA3i]i v[\u1EC1e]",
1902
+ "in",
1903
+ "\u0111[\u0103a]ng xu[\u1EA5a]t"
1904
+ ];
1905
+ var NEVER_PRESS_VI = new RegExp(
1906
+ `(?<![\\p{L}\\p{N}])(?:${NEVER_PRESS_VI_WORDS.join("|")})(?![\\p{L}\\p{N}])`,
1907
+ "iu"
1908
+ );
1909
+ function doorName(el) {
1910
+ return (nameOf(el) || el.getAttribute("href") || "").slice(0, 60);
1911
+ }
1912
+ function isChrome(el) {
1913
+ return el.closest('nav, [role="banner"], [role="tablist"]') !== null;
1914
+ }
1915
+ function isEmptyDoor(el) {
1916
+ if (el.childElementCount > 0) return false;
1917
+ if ((el.textContent || "").trim() !== "") return false;
1918
+ return (el.getAttribute("aria-label") || "").trim() !== "";
1919
+ }
1920
+ function doorElements() {
1921
+ const open = Array.from(document.querySelectorAll(OVERLAY_ROLES)).filter(onScreen);
1922
+ const reachable = (el) => onScreen(el) && !isChrome(el) && el.getAttribute("aria-disabled") !== "true" && !(el instanceof HTMLButtonElement && el.disabled) && // NEVER A CONTROL INSIDE AN OPEN OVERLAY. A dialog's own buttons are the
1923
+ // ones that commit it, and the walk photographs a dialog rather than
1924
+ // answering it.
1925
+ !open.some((overlay) => overlay.contains(el));
1926
+ const records = recordLinks().filter(
1927
+ (el) => reachable(el) && el.getAttribute("aria-expanded") === null && el.getAttribute("aria-haspopup") === null
1928
+ );
1929
+ const menus = Array.from(document.querySelectorAll("[aria-haspopup]")).filter(
1930
+ (el) => reachable(el) && el.getAttribute("aria-expanded") !== "true"
1931
+ );
1932
+ const folds = Array.from(document.querySelectorAll('[aria-expanded="false"]')).filter(
1933
+ (el) => reachable(el) && el.getAttribute("aria-haspopup") === null
1934
+ );
1935
+ const acts = Array.from(
1936
+ document.querySelectorAll('[data-variant="primary"], [data-variant="secondary"]')
1937
+ ).filter((el) => {
1938
+ if (!reachable(el)) return false;
1939
+ if (el.tagName !== "BUTTON" && el.getAttribute("role") !== "button") return false;
1940
+ if (el.getAttribute("aria-haspopup") !== null || el.getAttribute("aria-expanded") !== null) return false;
1941
+ if (el.getAttribute("type") === "submit") return false;
1942
+ if (el.closest("form") !== null) return false;
1943
+ const tone = el.getAttribute("data-tone");
1944
+ if (tone === "danger" || tone === "warning") return false;
1945
+ const name = `${doorName(el)} ${(el.textContent || "").trim()}`;
1946
+ return !NEVER_PRESS.test(name) && !NEVER_PRESS_VI.test(name);
1947
+ });
1948
+ return [
1949
+ // One of each state-changing kind — the first row, the acts menu, the first
1950
+ // fold. A second of any is the same surface again; the acts a screen offers
1951
+ // are not, so every one of those is walked.
1952
+ ...records.slice(0, 1).map((el) => ({ kind: "record", el })),
1953
+ ...menus.slice(0, 1).map((el) => ({ kind: "menu", el })),
1954
+ ...folds.slice(0, 1).map((el) => ({ kind: "fold", el })),
1955
+ ...acts.map((el) => ({ kind: "dialog", el }))
1956
+ ];
1957
+ }
1958
+ function doors() {
1959
+ return doorElements().map((door) => ({ kind: door.kind, name: doorName(door.el) }));
1960
+ }
1961
+ function openDoor(index) {
1962
+ const door = doorElements()[index];
1963
+ if (door === void 0) return false;
1964
+ door.el.click();
1965
+ return true;
1966
+ }
1967
+ function overlaysOpen() {
1968
+ return Array.from(document.querySelectorAll(OVERLAY_ROLES)).filter(onScreen).length;
1969
+ }
1970
+ function contentHeight() {
1971
+ let tallest = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
1972
+ for (const el of Array.from(document.querySelectorAll("*"))) {
1973
+ if (el.scrollHeight <= el.clientHeight) continue;
1974
+ if (!onScreen(el)) continue;
1975
+ tallest = Math.max(tallest, Math.ceil(el.getBoundingClientRect().top) + el.scrollHeight);
1976
+ }
1977
+ for (const overlay of Array.from(document.querySelectorAll(OVERLAY_ROLES))) {
1978
+ if (!onScreen(overlay)) continue;
1979
+ tallest = Math.max(tallest, Math.ceil(overlay.getBoundingClientRect().top) + overlay.scrollHeight);
1980
+ }
1981
+ return tallest;
1982
+ }
1983
+ function surfaceReport() {
1984
+ const surface = measuredSurface();
1985
+ const cut = truncatedLeaves(surface);
1986
+ return {
1987
+ ...surfaceChrome(surface),
1988
+ repeated: repeatedValues(surface).repeated,
1989
+ wrapped_money: wrappedMoney(surface).wrapped,
1990
+ clipped: cut.cut
1991
+ };
1992
+ }
610
1993
  return __toCommonJS(probe_page_exports);
611
1994
  })();