bee-ladybug 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.MD CHANGED
@@ -134,7 +134,7 @@ bee.subscribe((packet) => {
134
134
  | `F3` | slow-motion del tempo fittizio (`0.25x` / `1x`) |
135
135
  | `F4` | freeze / run del tempo fittizio |
136
136
  | `F6` | CSS hunt, se `CSSAdapter` è attaccato |
137
- | `S` | scansione overflow / immagini, solo con la hunt accesa |
137
+ | `S` | scansione CSS, solo con la hunt accesa |
138
138
 
139
139
  Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
140
140
 
@@ -200,7 +200,7 @@ css.attach();
200
200
  - chi intercetta il click al centro, inclusi `::before` / `::after` fissi su `html` / `body`
201
201
  - `<img>` con rapporto stirato oltre il 5% e `object-fit: fill`
202
202
 
203
- `S` con la hunt accesa esegue overflow e immagini su tutta la pagina e lista i casi peggiori. Un click sulla riga disegna il contorno rosso. I fogli di stile restano in cache finché un `MutationObserver` non vede un cambio a `<style>` o `<link>`. Hover è limitato a un frame.
203
+ `S` con la hunt accesa percorre la pagina. L'overflow resta solo sull'elemento che lo causa: un antenato si salta se un discendente sfora della stessa quantità (±2px), oppure se la larghezza del box ci sta nel viewport e a uscire è un figlio. Righe uguali (stesso selettore e stesso problema) diventano una sola, con il conteggio (`article overflows viewport ×4`). L'ordine è: causa di overflow, click bloccato, testo invisibile, immagine distorta, `!important`. Un click sulla riga disegna il contorno rosso. In console un elemento pulito non viene scritto, a meno che `F3` SLOW sia acceso. I fogli di stile restano in cache finché un `MutationObserver` non vede un cambio a `<style>` o `<link>`. Hover è limitato a un frame.
204
204
 
205
205
  Demo: `examples/css-hunt.html`.
206
206
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Universal telemetry and visual inspection overlay for Canvas, DOM, Python and AI flows.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -112,43 +112,61 @@ export class CSSAdapter {
112
112
  return this;
113
113
  }
114
114
 
115
- /** Checks 2 and 5 across the document. Click a row to outline it. */
115
+ /** Page scan. Click a row to outline one element of that group. */
116
116
  scan() {
117
117
  if (!this.#active || typeof document === 'undefined') return this;
118
118
  if (this.#sheetsDirty) this.#rebuildSheets();
119
119
  const nodes = document.body ? document.body.querySelectorAll('*') : [];
120
- const offenders = [];
120
+ const overflow = [];
121
+ const rows = [];
121
122
  for (let i = 0; i < nodes.length; i++) {
122
123
  const el = nodes[i];
123
124
  if (!el || isBeeChrome(el) || isSkippedTag(el)) continue;
125
+ const measured = measureViewportOverflow(el);
126
+ if (measured) overflow.push(measured);
124
127
  const findings = [];
125
- pushOverflow(el, findings);
128
+ pushNegativeMargins(el, findings);
129
+ pushInvisible(el, findings);
130
+ pushBlockers(el, findings);
126
131
  pushImage(el, findings);
127
- if (!findings.length) continue;
128
- let score = 0;
129
- for (let j = 0; j < findings.length; j++) score += findings[j].score;
130
- offenders.push({ el, findings, score });
132
+ pushImportant(el, this.#rules, findings);
133
+ for (let j = 0; j < findings.length; j++) {
134
+ rows.push({ el, selector: cssPath(el), ...findings[j] });
135
+ }
131
136
  }
132
- offenders.sort((a, b) => b.score - a.score);
133
- const top = offenders.slice(0, this.maxScan);
134
- const items = top.map((entry) => ({
135
- level: 'error',
136
- text: `${cssPath(entry.el)} — ${entry.findings.map((hit) => hit.text).join('; ')}`,
137
- onSelect: () => this.#pin(entry.el)
137
+ const causes = keepOverflowCauses(overflow);
138
+ for (let i = 0; i < causes.length; i++) {
139
+ const cause = causes[i];
140
+ rows.push({
141
+ el: cause.el,
142
+ selector: cssPath(cause.el),
143
+ kind: 'overflows viewport',
144
+ rank: 0,
145
+ level: 'error',
146
+ score: cause.score
147
+ });
148
+ }
149
+ const groups = groupIssues(rows).slice(0, this.maxScan);
150
+ const items = groups.map((group) => ({
151
+ level: group.level,
152
+ text: formatIssue(group),
153
+ onSelect: () => this.#pin(group.el)
138
154
  }));
139
155
  this.#lastEl = null;
140
156
  this.core?.publishDetail(items);
141
157
  this.core?.sendData('state', {
142
158
  source: this.source,
143
159
  key: 'css.scan',
144
- value: String(top.length)
145
- });
146
- this.core?.sendData('warn', {
147
- source: this.source,
148
- message: top.length
149
- ? `scan ${top.length} offender(s). Click a row to outline it.`
150
- : 'scan clean: no overflow or distorted images.'
160
+ value: String(groups.length)
151
161
  });
162
+ if (groups.length || isVerbose(this.core)) {
163
+ this.core?.sendData('warn', {
164
+ source: this.source,
165
+ message: groups.length
166
+ ? `scan ${groups.length} issue(s). Click a row to outline it.`
167
+ : 'scan clean: no causes found.'
168
+ });
169
+ }
152
170
  return this;
153
171
  }
154
172
 
@@ -267,7 +285,9 @@ export class CSSAdapter {
267
285
  onSelect: () => this.#pin(el)
268
286
  })));
269
287
  if (!findings.length) {
270
- this.core?.sendData('log', { source: this.source, message: `${path} clean` });
288
+ if (isVerbose(this.core)) {
289
+ this.core?.sendData('log', { source: this.source, message: `${path} clean` });
290
+ }
271
291
  return;
272
292
  }
273
293
  const lead = findings.find((hit) => hit.level === 'error') || findings[0];
@@ -380,7 +400,9 @@ function pushImportant(el, rules, findings) {
380
400
  const prop = rule.props[j];
381
401
  findings.push({
382
402
  level: 'warn',
403
+ rank: 4,
383
404
  score: 5,
405
+ kind: `!important ${rule.selector} { ${prop.property}: ${prop.value} }`,
384
406
  text: `!important ${rule.selector} { ${prop.property}: ${prop.value} } @ ${rule.source}`
385
407
  });
386
408
  }
@@ -388,19 +410,75 @@ function pushImportant(el, rules, findings) {
388
410
  }
389
411
 
390
412
  function pushOverflow(el, findings) {
391
- if (!el.getBoundingClientRect || el === document.documentElement || el === document.body) return;
392
- const rect = el.getBoundingClientRect();
393
- const view = document.documentElement.clientWidth;
394
- if (rect.left < -1 || rect.right > view + 1) {
395
- const past = rect.left < -1
396
- ? `left ${Math.round(rect.left)}px`
397
- : `right +${Math.round(rect.right - view)}px`;
413
+ const measured = measureViewportOverflow(el);
414
+ if (measured) {
415
+ const past = measured.left > 1
416
+ ? `left ${Math.round(-measured.left)}px`
417
+ : `right +${Math.round(measured.right)}px`;
398
418
  findings.push({
399
419
  level: 'error',
400
- score: Math.abs(rect.left < -1 ? rect.left : rect.right - view),
420
+ rank: 0,
421
+ score: measured.score,
422
+ kind: 'overflows viewport',
401
423
  text: `overflows viewport (${past})`
402
424
  });
403
425
  }
426
+ pushNegativeMargins(el, findings);
427
+ }
428
+
429
+ function measureViewportOverflow(el) {
430
+ if (!el || typeof el.getBoundingClientRect !== 'function') return null;
431
+ if (typeof document === 'undefined') return null;
432
+ if (el === document.documentElement || el === document.body) return null;
433
+ const rect = el.getBoundingClientRect();
434
+ const view = document.documentElement.clientWidth;
435
+ const left = rect.left < -1 ? -rect.left : 0;
436
+ const right = rect.right > view + 1 ? rect.right - view : 0;
437
+ if (left <= 1 && right <= 1) return null;
438
+ return { el, left, right, width: rect.width, view, score: Math.max(left, right) };
439
+ }
440
+
441
+ /**
442
+ * Keep the node that actually sticks out.
443
+ * Drop a parent when a descendant overflows by the same amount (±2px),
444
+ * and drop a box whose own width fits the viewport when a child is the one overflowing.
445
+ * @param {Array<{ el: Element, left: number, right: number, width: number, view: number, score: number }>} items
446
+ */
447
+ export function keepOverflowCauses(items) {
448
+ return items.filter((item) => !echoedByDescendant(item, items) && !fitsOnlyBecauseOfChild(item, items));
449
+ }
450
+
451
+ function echoedByDescendant(item, items) {
452
+ for (let i = 0; i < items.length; i++) {
453
+ const other = items[i];
454
+ if (other.el === item.el || typeof item.el.contains !== 'function' || !item.el.contains(other.el)) continue;
455
+ const leftEcho = item.left <= 1 || Math.abs(item.left - other.left) <= 2;
456
+ const rightEcho = item.right <= 1 || Math.abs(item.right - other.right) <= 2;
457
+ const matched = (item.left > 1 && Math.abs(item.left - other.left) <= 2)
458
+ || (item.right > 1 && Math.abs(item.right - other.right) <= 2);
459
+ if (matched && leftEcho && rightEcho) return true;
460
+ }
461
+ return false;
462
+ }
463
+
464
+ function fitsOnlyBecauseOfChild(item, items) {
465
+ if (item.width > item.view + 2) return false;
466
+ if (item.left <= 1 && item.right <= 1) return false;
467
+ let leftOk = item.left <= 1;
468
+ let rightOk = item.right <= 1;
469
+ let childOverflows = false;
470
+ for (let i = 0; i < items.length; i++) {
471
+ const other = items[i];
472
+ if (other.el === item.el || typeof item.el.contains !== 'function' || !item.el.contains(other.el)) continue;
473
+ if (other.left > 1 || other.right > 1) childOverflows = true;
474
+ if (item.left > 1 && other.left + 2 >= item.left) leftOk = true;
475
+ if (item.right > 1 && other.right + 2 >= item.right) rightOk = true;
476
+ }
477
+ return childOverflows && leftOk && rightOk;
478
+ }
479
+
480
+ function pushNegativeMargins(el, findings) {
481
+ if (typeof getComputedStyle !== 'function') return;
404
482
  const style = getComputedStyle(el);
405
483
  const sides = ['top', 'right', 'bottom', 'left'];
406
484
  for (let i = 0; i < sides.length; i++) {
@@ -409,26 +487,64 @@ function pushOverflow(el, findings) {
409
487
  if (!Number.isFinite(value) || value >= 0) continue;
410
488
  findings.push({
411
489
  level: 'warn',
490
+ rank: 5,
412
491
  score: Math.abs(value),
492
+ kind: `negative margin margin-${sides[i]}: ${raw.trim()}`,
413
493
  text: `negative margin margin-${sides[i]}: ${raw.trim()}`
414
494
  });
415
495
  }
416
496
  }
417
497
 
498
+ const ISSUE_RANK = Object.freeze({
499
+ overflow: 0,
500
+ blocker: 1,
501
+ invisible: 2,
502
+ image: 3,
503
+ important: 4
504
+ });
505
+
506
+ /**
507
+ * @param {Array<{ selector: string, kind: string, rank: number, level: string, score: number, el?: Element }>} rows
508
+ */
509
+ export function groupIssues(rows) {
510
+ const map = new Map();
511
+ for (let i = 0; i < rows.length; i++) {
512
+ const row = rows[i];
513
+ const kind = row.kind || row.text;
514
+ const key = `${row.selector}\0${kind}`;
515
+ const prev = map.get(key);
516
+ if (!prev) {
517
+ map.set(key, { ...row, kind, count: 1 });
518
+ continue;
519
+ }
520
+ prev.count += 1;
521
+ if (row.score > prev.score) {
522
+ prev.score = row.score;
523
+ prev.el = row.el;
524
+ }
525
+ }
526
+ return [...map.values()].sort((a, b) => a.rank - b.rank || b.score - a.score || b.count - a.count);
527
+ }
528
+
529
+ export function formatIssue(group) {
530
+ const count = group.count > 1 ? ` ×${group.count}` : '';
531
+ return `${group.selector} ${group.kind}${count}`;
532
+ }
533
+
418
534
  function pushInvisible(el, findings) {
419
535
  if (!hasOwnText(el)) return;
420
536
  const style = getComputedStyle(el);
421
537
  if (opacityZero(el)) {
422
- findings.push({ level: 'error', score: 30, text: 'invisible text (opacity 0)' });
538
+ findings.push(issue('invisible text (opacity 0)', ISSUE_RANK.invisible, 30));
423
539
  }
424
540
  const color = parseColor(style.color);
425
541
  if (!color || color.a === 0) {
426
- findings.push({ level: 'error', score: 30, text: 'invisible text (transparent color)' });
542
+ findings.push(issue('invisible text (transparent color)', ISSUE_RANK.invisible, 30));
427
543
  return;
428
544
  }
429
545
  const behind = paintBehind(el);
430
546
  if (behind && nearlySame(color, behind)) {
431
- findings.push({ level: 'error', score: 30, text: 'invisible text (color matches background)' });
547
+ findings.push(issue('invisible text (color matches background)', ISSUE_RANK.invisible, 30));
432
548
  }
433
549
  }
434
550
 
@@ -440,12 +556,12 @@ function pushBlockers(el, findings) {
440
556
  if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) return;
441
557
  const pseudo = coveringPseudo(x, y);
442
558
  if (pseudo) {
443
- findings.push({ level: 'error', score: 40, text: `click blocked by ${pseudo}` });
559
+ findings.push(issue(`click blocked by ${pseudo}`, ISSUE_RANK.blocker, 40));
444
560
  }
445
561
  const hit = document.elementFromPoint(x, y);
446
562
  if (!hit || isBeeChrome(hit) || hit === el || el.contains(hit)) return;
447
563
  if (pseudo && (hit === document.documentElement || hit === document.body)) return;
448
- findings.push({ level: 'error', score: 40, text: `click blocked by ${cssPath(hit)}` });
564
+ findings.push(issue(`click blocked by ${cssPath(hit)}`, ISSUE_RANK.blocker, 40));
449
565
  }
450
566
 
451
567
  function pushImage(el, findings) {
@@ -462,11 +578,21 @@ function pushImage(el, findings) {
462
578
  if (diff <= 0.05 || fit !== 'fill') return;
463
579
  findings.push({
464
580
  level: 'error',
581
+ rank: ISSUE_RANK.image,
465
582
  score: diff * 100,
583
+ kind: 'distorted image',
466
584
  text: `distorted image ${Math.round(diff * 100)}% (object-fit: fill)`
467
585
  });
468
586
  }
469
587
 
588
+ function issue(kind, rank, score) {
589
+ return { level: 'error', rank, score, kind, text: kind };
590
+ }
591
+
592
+ function isVerbose(core) {
593
+ return Boolean(core) && !core.frozen && core.timeScale !== 1;
594
+ }
595
+
470
596
  function coveringPseudo(x, y) {
471
597
  const hosts = [document.documentElement, document.body];
472
598
  const names = ['::before', '::after'];
@@ -9,7 +9,7 @@
9
9
  import { UIOverlay } from './UIOverlay.js';
10
10
  import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
11
11
 
12
- export const BEE_LADYBUG_VERSION = '0.5.0';
12
+ export const BEE_LADYBUG_VERSION = '0.5.1';
13
13
 
14
14
  export const CORE_DEFAULTS = Object.freeze({
15
15
  toggleKey: 'F2',