bee-ladybug 0.6.3 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
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",
@@ -189,6 +189,7 @@ function isBeeChrome(node) {
189
189
  if (tag === 'BEE-LADYBUG-OVERLAY') return true;
190
190
  if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
191
191
  if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
192
+ if (el.hasAttribute?.('data-bee-ladybug-css')) return true;
192
193
  el = el.parentElement;
193
194
  }
194
195
  return false;
@@ -61,9 +61,13 @@ export class CSSAdapter {
61
61
  this.#hoverBox = null;
62
62
  this.#pinBox = null;
63
63
  this.#observer = null;
64
+ this.#unsubscribe = null;
65
+ this.#hasPointer = false;
64
66
  this.#onMove = (event) => {
67
+ if (this.core && this.core.visible === false) return;
65
68
  this.#x = event.clientX;
66
69
  this.#y = event.clientY;
70
+ this.#hasPointer = true;
67
71
  this.#schedule();
68
72
  };
69
73
  this.#onKey = (event) => this.#handleKey(event);
@@ -71,7 +75,7 @@ export class CSSAdapter {
71
75
  this.#onMutate = (records) => {
72
76
  if (!records.some(touchesSheet)) return;
73
77
  this.#sheetsDirty = true;
74
- this.#schedule();
78
+ if (this.#hasPointer) this.#schedule();
75
79
  };
76
80
  }
77
81
 
@@ -92,6 +96,9 @@ export class CSSAdapter {
92
96
  #hoverBox;
93
97
  #pinBox;
94
98
  #observer;
99
+ #unsubscribe;
100
+ /** True after the first mousemove: before that (x, y) is not a real pointer. */
101
+ #hasPointer;
95
102
  #onMove;
96
103
  #onKey;
97
104
  #onScroll;
@@ -104,6 +111,9 @@ export class CSSAdapter {
104
111
  attach() {
105
112
  if (this.#attached) return this;
106
113
  this.#attached = true;
114
+ if (this.core && typeof this.core.subscribe === 'function') {
115
+ this.#unsubscribe = this.core.subscribe((packet) => this.#onCore(packet));
116
+ }
107
117
  this.core?.registerAdapter('css', {
108
118
  label: this.label,
109
119
  hotkey: this.hotkey,
@@ -128,10 +138,21 @@ export class CSSAdapter {
128
138
  if (!this.#attached) return this;
129
139
  this.#disable();
130
140
  this.#attached = false;
141
+ if (this.#unsubscribe) this.#unsubscribe();
142
+ this.#unsubscribe = null;
131
143
  this.core?.unregisterAdapter('css');
132
144
  return this;
133
145
  }
134
146
 
147
+ #onCore(packet) {
148
+ if (packet.type !== 'sys' || packet.payload?.op !== 'visibility') return;
149
+ this.#placeBoxes();
150
+ }
151
+
152
+ #boxesVisible() {
153
+ return !(this.core && this.core.visible === false);
154
+ }
155
+
135
156
  /** Page scan. Click a row to outline one element of that group. */
136
157
  scan() {
137
158
  if (!this.#active || typeof document === 'undefined') return this;
@@ -141,15 +162,15 @@ export class CSSAdapter {
141
162
  const rows = [];
142
163
  for (let i = 0; i < nodes.length; i++) {
143
164
  const el = nodes[i];
144
- if (!el || isBeeChrome(el) || isSkippedTag(el)) continue;
165
+ if (!el || isBeeChrome(el) || isSkippedTag(el) || isReportNoise(el)) continue;
145
166
  const measured = measureViewportOverflow(el);
146
167
  if (measured) overflow.push(measured);
147
168
  const findings = [];
148
169
  pushNegativeMargins(el, findings);
149
170
  pushInvisible(el, findings);
150
- pushBlockers(el, findings);
171
+ pushBlockers(el, findings, true);
151
172
  pushImage(el, findings);
152
- pushStacking(el, findings);
173
+ pushStacking(el, findings, true);
153
174
  pushImportant(el, this.#rules, findings);
154
175
  for (let j = 0; j < findings.length; j++) {
155
176
  rows.push({ el, selector: cssPath(el), ...findings[j] });
@@ -245,7 +266,9 @@ export class CSSAdapter {
245
266
  for (let i = 0; i < rows.length; i++) {
246
267
  const row = rows[i];
247
268
  if (describeCause(row.kind, row.selector)) {
248
- causes = addCause(causes, { kind: row.kind, path: row.selector, el: row.el }, MAX_SNAPSHOT);
269
+ // No cap here: MAX_SNAPSHOT applies after the severity sort, so an info row
270
+ // can never push a real error out.
271
+ causes = addCause(causes, { kind: row.kind, path: row.selector, el: row.el }, Infinity);
249
272
  } else {
250
273
  plain.push(row);
251
274
  }
@@ -319,6 +342,7 @@ export class CSSAdapter {
319
342
  #disable() {
320
343
  if (!this.#active) return;
321
344
  this.#active = false;
345
+ this.#hasPointer = false;
322
346
  if (typeof window !== 'undefined') {
323
347
  window.removeEventListener('mousemove', this.#onMove, true);
324
348
  window.removeEventListener('keydown', this.#onKey);
@@ -340,11 +364,16 @@ export class CSSAdapter {
340
364
  this.core?.publishDetail([]);
341
365
  this.core?.publishIssues([]);
342
366
  this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'off' });
367
+ this.core?.sendData('state', { source: this.source, key: 'css.target', value: '—' });
368
+ this.core?.sendData('state', { source: this.source, key: 'css.hits', value: '0' });
369
+ this.core?.sendData('state', { source: this.source, key: 'css.pin', value: 'off' });
343
370
  this.core?.sendData('log', { source: this.source, message: 'CSS hunt off.' });
344
371
  }
345
372
 
346
373
  #handleKey(event) {
347
374
  if (!this.#active || event.repeat) return;
375
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
376
+ if (this.core && this.core.visible === false) return;
348
377
  if (isTypingTarget(event.target)) return;
349
378
  if (event.code === this.pinKey) {
350
379
  event.preventDefault();
@@ -377,22 +406,23 @@ export class CSSAdapter {
377
406
 
378
407
  #inspect(x, y) {
379
408
  const el = pickElement(x, y);
380
- placeBox(this.#hoverBox, el);
409
+ placeBox(this.#hoverBox, el, this.#boxesVisible());
381
410
  if (this.#panelLock) return;
382
411
  if (el === this.#lastEl) return;
383
412
  this.#lastEl = el;
384
413
  if (!el) {
385
414
  this.core?.publishDetail([]);
386
415
  this.core?.sendData('state', { source: this.source, key: 'css.target', value: '—' });
416
+ this.core?.sendData('state', { source: this.source, key: 'css.hits', value: '0' });
387
417
  return;
388
418
  }
389
419
  const findings = [];
390
420
  pushImportant(el, this.#rules, findings);
391
421
  pushOverflow(el, findings);
392
422
  pushInvisible(el, findings);
393
- pushBlockers(el, findings);
423
+ pushBlockers(el, findings, true);
394
424
  pushImage(el, findings);
395
- pushStacking(el, findings);
425
+ pushStacking(el, findings, true);
396
426
  const path = cssPath(el);
397
427
  this.core?.sendData('state', { source: this.source, key: 'css.target', value: path });
398
428
  this.core?.sendData('state', {
@@ -419,7 +449,7 @@ export class CSSAdapter {
419
449
  if (this.#panelLock) {
420
450
  this.#panelLock = false;
421
451
  this.#pinned = null;
422
- placeBox(this.#pinBox, null);
452
+ placeBox(this.#pinBox, null, this.#boxesVisible());
423
453
  this.core?.sendData('state', { source: this.source, key: 'css.pin', value: 'off' });
424
454
  this.core?.sendData('log', { source: this.source, message: 'CSS pin off.' });
425
455
  this.#lastEl = null;
@@ -504,15 +534,16 @@ export class CSSAdapter {
504
534
 
505
535
  #pin(el) {
506
536
  this.#pinned = el;
507
- placeBox(this.#pinBox, el);
537
+ placeBox(this.#pinBox, el, this.#boxesVisible());
508
538
  if (el && typeof el.scrollIntoView === 'function') {
509
539
  el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' });
510
540
  }
511
541
  }
512
542
 
513
543
  #placeBoxes() {
514
- placeBox(this.#hoverBox, this.#lastEl);
515
- placeBox(this.#pinBox, this.#pinned);
544
+ const visible = this.#boxesVisible();
545
+ placeBox(this.#hoverBox, this.#lastEl, visible);
546
+ placeBox(this.#pinBox, this.#pinned, visible);
516
547
  }
517
548
 
518
549
  #mountBoxes() {
@@ -538,6 +569,8 @@ function collectImportantRules() {
538
569
  const sheets = document.styleSheets;
539
570
  for (let i = 0; i < sheets.length; i++) {
540
571
  const sheet = sheets[i];
572
+ if (sheet.disabled === true) continue;
573
+ if (sheet.media && sheet.media.mediaText && !mediaMatches(sheet.media.mediaText)) continue;
541
574
  let list = null;
542
575
  try {
543
576
  list = sheet.cssRules;
@@ -550,21 +583,21 @@ function collectImportantRules() {
550
583
  return { rules, unreadable };
551
584
  }
552
585
 
586
+ function mediaMatches(mediaText) {
587
+ try {
588
+ return typeof matchMedia === 'function' ? matchMedia(mediaText).matches : true;
589
+ } catch {
590
+ return true;
591
+ }
592
+ }
593
+
553
594
  function walkRules(list, source, into) {
554
595
  if (!list) return;
555
596
  for (let i = 0; i < list.length; i++) {
556
597
  const rule = list[i];
557
598
  if (!rule) continue;
558
599
  if (rule.type === 4 && rule.media) {
559
- let active = true;
560
- try {
561
- active = typeof matchMedia === 'function'
562
- ? matchMedia(rule.media.mediaText).matches
563
- : true;
564
- } catch {
565
- active = true;
566
- }
567
- if (active) walkRules(rule.cssRules, source, into);
600
+ if (mediaMatches(rule.media.mediaText)) walkRules(rule.cssRules, source, into);
568
601
  continue;
569
602
  }
570
603
  if (rule.type === 1 && rule.selectorText && rule.style) {
@@ -745,7 +778,9 @@ function pushInvisible(el, findings) {
745
778
  findings.push(issue('invisible text (opacity 0)', ISSUE_RANK.invisible, 30));
746
779
  }
747
780
  const color = parseColor(style.color);
748
- if (!color || color.a === 0) {
781
+ // Unknown syntax (oklch, lab, color(), color-mix): nothing to compare, nothing to report.
782
+ if (!color) return;
783
+ if (color.a === 0) {
749
784
  findings.push(issue('invisible text (transparent color)', ISSUE_RANK.invisible, 30));
750
785
  return;
751
786
  }
@@ -1096,7 +1131,7 @@ function pickElement(x, y) {
1096
1131
  const stack = document.elementsFromPoint(x, y);
1097
1132
  for (let i = 0; i < stack.length; i++) {
1098
1133
  const el = stack[i];
1099
- if (!el || isBeeChrome(el)) continue;
1134
+ if (!el || isBeeChrome(el) || isReportNoise(el)) continue;
1100
1135
  if (el === document.documentElement || el === document.body) continue;
1101
1136
  return el;
1102
1137
  }
@@ -1154,12 +1189,16 @@ function hasOwnText(el) {
1154
1189
  function cssPath(el) {
1155
1190
  if (!el || !el.tagName) return 'node';
1156
1191
  const tag = el.tagName.toLowerCase();
1157
- if (el.id) return `${tag}#${el.id}`;
1192
+ if (el.id) return `${tag}#${cssEscape(el.id)}`;
1158
1193
  const classes = el.classList ? [...el.classList].slice(0, 2) : [];
1159
- if (classes.length) return `${tag}.${classes.join('.')}`;
1194
+ if (classes.length) return `${tag}.${classes.map(cssEscape).join('.')}`;
1160
1195
  return tag;
1161
1196
  }
1162
1197
 
1198
+ function cssEscape(value) {
1199
+ return String(value).replace(/[^\w-]/g, '\\$&');
1200
+ }
1201
+
1163
1202
  function isSkippedTag(el) {
1164
1203
  const tag = el.tagName;
1165
1204
  return tag === 'SCRIPT' || tag === 'STYLE' || tag === 'LINK' || tag === 'META';
@@ -1179,7 +1218,10 @@ function touchesSheet(record) {
1179
1218
  const name = record.target?.nodeName;
1180
1219
  return name === 'STYLE' || name === 'LINK';
1181
1220
  }
1182
- const nodes = [record.target];
1221
+ // Only what was added or removed: the target's whole subtree always holds some <style> or <link>.
1222
+ // A <style> that receives new text is the one target worth checking, and that check is O(1).
1223
+ if (record.target?.nodeName === 'STYLE') return true;
1224
+ const nodes = [];
1183
1225
  if (record.addedNodes) {
1184
1226
  for (let i = 0; i < record.addedNodes.length; i++) nodes.push(record.addedNodes[i]);
1185
1227
  }
@@ -1220,9 +1262,9 @@ function makeBox(kind, border, fill) {
1220
1262
  return node;
1221
1263
  }
1222
1264
 
1223
- function placeBox(node, el) {
1265
+ function placeBox(node, el, visible = true) {
1224
1266
  if (!node) return;
1225
- if (!el || typeof el.getBoundingClientRect !== 'function' || isBeeChrome(el)) {
1267
+ if (visible === false || !el || typeof el.getBoundingClientRect !== 'function' || isBeeChrome(el)) {
1226
1268
  node.style.display = 'none';
1227
1269
  return;
1228
1270
  }
@@ -520,6 +520,7 @@ function isBeeChrome(node) {
520
520
  if (tag === 'BEE-LADYBUG-OVERLAY') return true;
521
521
  if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
522
522
  if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
523
+ if (el.hasAttribute?.('data-bee-ladybug-css')) return true;
523
524
  el = el.parentElement;
524
525
  }
525
526
  return false;
@@ -10,7 +10,7 @@ import { UIOverlay } from './UIOverlay.js';
10
10
  import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
11
11
  import { exportReport as writeReport } from './ReportExporter.js';
12
12
 
13
- export const BEE_LADYBUG_VERSION = '0.6.3';
13
+ export const BEE_LADYBUG_VERSION = '0.6.4';
14
14
 
15
15
  export const CORE_DEFAULTS = Object.freeze({
16
16
  toggleKey: 'F2',