bee-ladybug 0.6.0 → 0.6.2

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.0",
3
+ "version": "0.6.2",
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",
@@ -219,9 +219,9 @@ export class CSSAdapter {
219
219
  const findings = [];
220
220
  pushNegativeMargins(el, findings);
221
221
  pushInvisible(el, findings);
222
- pushBlockers(el, findings);
222
+ pushBlockers(el, findings, true);
223
223
  pushImage(el, findings);
224
- pushStacking(el, findings);
224
+ pushStacking(el, findings, true);
225
225
  pushImportant(el, built.rules, findings);
226
226
  for (let j = 0; j < findings.length; j++) {
227
227
  rows.push({ el, selector: cssPath(el), ...findings[j] });
@@ -755,7 +755,7 @@ function pushInvisible(el, findings) {
755
755
  }
756
756
  }
757
757
 
758
- function pushBlockers(el, findings) {
758
+ function pushBlockers(el, findings, skipNoise = false) {
759
759
  if (!el.getBoundingClientRect || typeof document.elementFromPoint !== 'function') return;
760
760
  const rect = el.getBoundingClientRect();
761
761
  const x = rect.left + rect.width / 2;
@@ -768,6 +768,7 @@ function pushBlockers(el, findings) {
768
768
  const hit = document.elementFromPoint(x, y);
769
769
  if (!hit || isBeeChrome(hit) || hit === el || el.contains(hit)) return;
770
770
  if (pseudo && (hit === document.documentElement || hit === document.body)) return;
771
+ if (skipNoise && isReportNoise(hit)) return;
771
772
  findings.push(issue(`click blocked by ${cssPath(hit)}`, ISSUE_RANK.blocker, 40));
772
773
  }
773
774
 
@@ -950,7 +951,7 @@ function willChangeHint(value) {
950
951
  return hit.join(', ');
951
952
  }
952
953
 
953
- function pushStacking(el, findings) {
954
+ function pushStacking(el, findings, skipNoise = false) {
954
955
  if (!el || typeof getComputedStyle !== 'function' || !el.parentElement) return;
955
956
  const own = getComputedStyle(el);
956
957
  const ownZ = own.zIndex || own.getPropertyValue?.('z-index') || 'auto';
@@ -958,6 +959,7 @@ function pushStacking(el, findings) {
958
959
  let node = el.parentElement;
959
960
  while (node && node.nodeType === 1) {
960
961
  if (isBeeChrome(node)) return;
962
+ if (skipNoise && isReportNoise(node)) return;
961
963
  const found = stackingReasons(getComputedStyle(node));
962
964
  const ancestor = cssPath(node);
963
965
  if (found.bugs.length) {
@@ -70,6 +70,8 @@ export class CanvasAdapter {
70
70
  this.#fps = 0;
71
71
  this.#fpsSamples = [];
72
72
  this.#wasColliding = new Set();
73
+ this.#names = new Map();
74
+ this.#overlayStyle = null;
73
75
  this.#onResize = () => this.#syncOverlay();
74
76
  this.#onTick = () => {
75
77
  this.#rafId = requestAnimationFrame(this.#onTick);
@@ -93,6 +95,10 @@ export class CanvasAdapter {
93
95
  #fps;
94
96
  #fpsSamples;
95
97
  #wasColliding;
98
+ /** id -> display name, refreshed on every pump. */
99
+ #names;
100
+ /** Last style values written to the overlay. */
101
+ #overlayStyle;
96
102
  #onResize;
97
103
  #onTick;
98
104
 
@@ -126,6 +132,12 @@ export class CanvasAdapter {
126
132
  source: this.source,
127
133
  message: `CanvasAdapter attached (${this.entities.length} entities).`
128
134
  });
135
+ if (this.overlayEnabled && !this.canvas) {
136
+ this.core?.sendData('warn', {
137
+ source: this.source,
138
+ message: 'no canvas: hitboxes are not drawn. Pass { canvas } or call setCanvas().'
139
+ });
140
+ }
129
141
  this.core?.sendData('state', {
130
142
  source: this.source,
131
143
  key: 'adapter',
@@ -146,6 +158,11 @@ export class CanvasAdapter {
146
158
  window.removeEventListener('resize', this.#onResize);
147
159
  window.removeEventListener('scroll', this.#onResize, true);
148
160
  }
161
+ this.#wasColliding = new Set();
162
+ this.#names = new Map();
163
+ this.#lastPump = 0;
164
+ this.#fps = 0;
165
+ this.#fpsSamples = [];
149
166
  return this;
150
167
  }
151
168
 
@@ -163,10 +180,26 @@ export class CanvasAdapter {
163
180
  return this;
164
181
  }
165
182
 
183
+ /**
184
+ * Stop the internal loop and make a manual `pump()` valid again.
185
+ * Mirror of `startAutoPump()`.
186
+ */
187
+ stopAutoPump() {
188
+ this.autoPump = false;
189
+ this.#stopAutoPumpLoop();
190
+ return this;
191
+ }
192
+
166
193
  /** @param {HTMLCanvasElement | null} canvas */
167
194
  setCanvas(canvas) {
168
195
  this.canvas = canvas;
169
- this.#syncOverlay();
196
+ if (!this.#attached) return this;
197
+ if (!canvas) {
198
+ this.#unmountOverlay();
199
+ } else if (this.overlayEnabled) {
200
+ if (this.#overlay) this.#syncOverlay();
201
+ else this.#mountOverlay();
202
+ }
170
203
  return this;
171
204
  }
172
205
 
@@ -190,7 +223,7 @@ export class CanvasAdapter {
190
223
  severity: box.hit ? 'warn' : 'ok',
191
224
  selector: box.name,
192
225
  rule: 'hitbox',
193
- value: `${box.x},${box.y} ${box.w}x${box.h}${box.hit ? ' colliding' : ''}`,
226
+ value: `${round1(box.x)},${round1(box.y)} ${round1(box.w)}x${round1(box.h)}${box.hit ? ' colliding' : ''}`,
194
227
  source: this.source
195
228
  }));
196
229
  const fps = this.#fpsItem();
@@ -327,7 +360,9 @@ export class CanvasAdapter {
327
360
 
328
361
  #emitCollisionEdges(boxes) {
329
362
  const now = new Set();
363
+ const names = new Map();
330
364
  for (let i = 0; i < boxes.length; i++) {
365
+ names.set(boxes[i].id, boxes[i].name);
331
366
  if (!boxes[i].hit) continue;
332
367
  now.add(boxes[i].id);
333
368
  if (!this.#wasColliding.has(boxes[i].id)) {
@@ -339,13 +374,14 @@ export class CanvasAdapter {
339
374
  }
340
375
  for (const id of this.#wasColliding) {
341
376
  if (now.has(id)) continue;
342
- const name = idFromKey(id);
377
+ const name = names.get(id) ?? this.#names.get(id) ?? id;
343
378
  this.core.sendData('log', {
344
379
  source: this.source,
345
380
  message: `${name} left collision`
346
381
  });
347
382
  }
348
383
  this.#wasColliding = now;
384
+ this.#names = names;
349
385
  }
350
386
 
351
387
  #drawHitboxes(boxes, hostCtx) {
@@ -386,7 +422,7 @@ export class CanvasAdapter {
386
422
  }
387
423
 
388
424
  #mountOverlay() {
389
- if (typeof document === 'undefined' || this.#overlay) return;
425
+ if (typeof document === 'undefined' || this.#overlay || !this.canvas) return;
390
426
  const canvas = document.createElement('canvas');
391
427
  canvas.setAttribute('data-bee-ladybug-canvas', '');
392
428
  canvas.style.pointerEvents = 'none';
@@ -395,12 +431,14 @@ export class CanvasAdapter {
395
431
  canvas.style.margin = '0';
396
432
  document.body.appendChild(canvas);
397
433
  this.#overlay = canvas;
434
+ this.#overlayStyle = null;
398
435
  this.#syncOverlay();
399
436
  }
400
437
 
401
438
  #unmountOverlay() {
402
439
  if (this.#overlay?.parentNode) this.#overlay.parentNode.removeChild(this.#overlay);
403
440
  this.#overlay = null;
441
+ this.#overlayStyle = null;
404
442
  }
405
443
 
406
444
  #syncOverlay() {
@@ -408,14 +446,22 @@ export class CanvasAdapter {
408
446
  const overlay = this.#overlay;
409
447
  if (!host || !overlay) return;
410
448
  const rect = host.getBoundingClientRect();
411
- overlay.width = host.width;
412
- overlay.height = host.height;
413
- overlay.style.left = `${rect.left}px`;
414
- overlay.style.top = `${rect.top}px`;
415
- overlay.style.width = `${rect.width}px`;
416
- overlay.style.height = `${rect.height}px`;
449
+ // Assigning width/height clears and reallocates the bitmap: only when they change.
450
+ if (overlay.width !== host.width) overlay.width = host.width;
451
+ if (overlay.height !== host.height) overlay.height = host.height;
417
452
  const hidden = !this.#reporting || (this.core && this.core.visible === false);
418
- overlay.style.display = hidden ? 'none' : 'block';
453
+ const next = {
454
+ left: `${rect.left}px`,
455
+ top: `${rect.top}px`,
456
+ width: `${rect.width}px`,
457
+ height: `${rect.height}px`,
458
+ display: hidden ? 'none' : 'block'
459
+ };
460
+ const prev = this.#overlayStyle;
461
+ for (const key of Object.keys(next)) {
462
+ if (!prev || prev[key] !== next[key]) overlay.style[key] = next[key];
463
+ }
464
+ this.#overlayStyle = next;
419
465
  }
420
466
 
421
467
  #enable() {
@@ -487,11 +533,6 @@ function isPrimary(entity) {
487
533
  return key === 'player' || key === 'hero' || entity.primary === true;
488
534
  }
489
535
 
490
- function idFromKey(id) {
491
- const parts = String(id).split(':');
492
- return parts[parts.length - 1] || id;
493
- }
494
-
495
536
  function round1(value) {
496
537
  return Math.round(Number(value) * 10) / 10;
497
538
  }
@@ -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.0';
13
+ export const BEE_LADYBUG_VERSION = '0.6.2';
14
14
 
15
15
  export const CORE_DEFAULTS = Object.freeze({
16
16
  toggleKey: 'F2',
@@ -74,11 +74,19 @@ export function isReportNoise(node) {
74
74
  while (current && typeof current === 'object') {
75
75
  if (seen.has(current)) break;
76
76
  seen.add(current);
77
- if (current.id === 'wpadminbar') return true;
78
- if (hasAbClass(current.className)) return true;
77
+ if (current.id === 'wpadminbar' || elementId(current) === 'wpadminbar') return true;
78
+ if (hasAbClass(classText(current))) return true;
79
79
  if (current.tagName === 'BEE-LADYBUG-OVERLAY') return true;
80
80
  if (hasBeeAttribute(current)) return true;
81
- current = current.parentElement || null;
81
+ if (typeof current.closest === 'function') {
82
+ try {
83
+ const bar = current.closest('#wpadminbar');
84
+ if (bar) return true;
85
+ } catch {
86
+ /* closest is optional */
87
+ }
88
+ }
89
+ current = current.parentElement || (current.parentNode && current.parentNode.nodeType === 1 ? current.parentNode : null);
82
90
  }
83
91
 
84
92
  const style = readStyle(el);
@@ -103,7 +111,7 @@ function buildReport(core) {
103
111
  const series = readSeries(core);
104
112
  const latest = readLatest(core);
105
113
  const adapters = readAdapters(core);
106
- const summary = countFindings(adapters);
114
+ const summary = countFindings(adapters, logs);
107
115
 
108
116
  return {
109
117
  version: state.version,
@@ -219,7 +227,7 @@ function readAdapters(core) {
219
227
  const list = result && Array.isArray(result.items) ? result.items : [];
220
228
  sections.push({
221
229
  name,
222
- items: list.filter((row) => row && typeof row === 'object').map((row) => makeItem(row, name))
230
+ items: groupIdentical(list.filter((row) => row && typeof row === 'object').map((row) => makeItem(row, name)))
223
231
  });
224
232
  }
225
233
  return sections;
@@ -240,14 +248,45 @@ function makeItem(row, fallbackSource = '') {
240
248
  };
241
249
  }
242
250
 
251
+ /**
252
+ * Identical selector + rule + value collapse to one row. The count is part of the value.
253
+ * @param {ReportItem[]} items
254
+ * @returns {ReportItem[]}
255
+ */
256
+ function groupIdentical(items) {
257
+ const order = [];
258
+ const map = new Map();
259
+ for (let i = 0; i < items.length; i++) {
260
+ const item = items[i];
261
+ const key = `${item.selector}\0${item.rule}\0${item.value}`;
262
+ const prev = map.get(key);
263
+ if (!prev) {
264
+ const row = { ...item, count: 1 };
265
+ map.set(key, row);
266
+ order.push(row);
267
+ continue;
268
+ }
269
+ prev.count += 1;
270
+ }
271
+ return order.map((row) => ({
272
+ severity: row.severity,
273
+ selector: row.selector,
274
+ rule: row.rule,
275
+ value: row.count > 1 ? `${row.value} ×${row.count}` : row.value,
276
+ source: row.source
277
+ }));
278
+ }
279
+
243
280
  /**
244
281
  * @param {Array<{ items: ReportItem[] }>} adapters
282
+ * @param {Array<{ type: string, level: string | null }>} logs
245
283
  */
246
- function countFindings(adapters) {
284
+ function countFindings(adapters, logs) {
247
285
  const summary = { error: 0, warn: 0, ok: 0 };
248
286
  for (const section of adapters) {
249
287
  for (const item of section.items) summary[item.severity] += 1;
250
288
  }
289
+ for (let i = 0; i < logs.length; i++) summary[logSeverity(logs[i])] += 1;
251
290
  return summary;
252
291
  }
253
292
 
@@ -267,7 +306,7 @@ function normalizeFormat(format) {
267
306
  function normalizeSeverity(value) {
268
307
  const text = String(value || '').toLowerCase();
269
308
  if (text === 'error') return 'error';
270
- if (text === 'warn' || text === 'warning') return 'warn';
309
+ if (text === 'warn' || text === 'warning' || text === 'warm') return 'warn';
271
310
  return 'ok';
272
311
  }
273
312
 
@@ -332,8 +371,10 @@ function csvCell(value) {
332
371
  * @returns {ReportSeverity}
333
372
  */
334
373
  function logSeverity(log) {
335
- if (log.level === 'error' || log.type === 'error') return 'error';
336
- if (log.level === 'warn' || log.type === 'warn') return 'warn';
374
+ const level = String(log.level || '').toLowerCase();
375
+ if (level === 'warn' || level === 'warning' || level === 'warm') return 'warn';
376
+ if (level === 'error' || log.type === 'error') return 'error';
377
+ if (log.type === 'warn') return 'warn';
337
378
  return 'ok';
338
379
  }
339
380
 
@@ -490,11 +531,22 @@ function saveIfMounted(core, report, ext, mime, text) {
490
531
  if (typeof document === 'undefined' || !document.body) return;
491
532
  if (typeof URL === 'undefined' || typeof URL.createObjectURL !== 'function' || typeof Blob === 'undefined') return;
492
533
 
493
- const blob = new Blob([text], { type: `${mime};charset=utf-8` });
534
+ const filename = `report-${report.fileStamp}.${ext}`;
535
+ const type = `${mime};charset=utf-8`;
536
+ let blob;
537
+ try {
538
+ blob = typeof File === 'function'
539
+ ? new File([text], filename, { type })
540
+ : new Blob([text], { type });
541
+ } catch {
542
+ if (typeof Blob !== 'function') return;
543
+ blob = new Blob([text], { type });
544
+ }
494
545
  const url = URL.createObjectURL(blob);
495
546
  const link = document.createElement('a');
496
547
  link.href = url;
497
- link.download = `report-${report.fileStamp}.${ext}`;
548
+ link.download = filename;
549
+ link.setAttribute('download', filename);
498
550
  link.rel = 'noopener';
499
551
  document.body.appendChild(link);
500
552
  link.click();
@@ -580,6 +632,34 @@ function escapeHtml(value) {
580
632
  .replace(/"/g, '&quot;');
581
633
  }
582
634
 
635
+ function classText(node) {
636
+ if (!node || typeof node !== 'object') return '';
637
+ const parts = [];
638
+ if (typeof node.className === 'string' && node.className) parts.push(node.className);
639
+ else if (node.className && typeof node.className === 'object' && 'baseVal' in node.className && node.className.baseVal) {
640
+ parts.push(String(node.className.baseVal));
641
+ }
642
+ if (node.classList && typeof node.classList.length === 'number') {
643
+ try {
644
+ parts.push([...node.classList].join(' '));
645
+ } catch {
646
+ /* classList may not be iterable */
647
+ }
648
+ }
649
+ if (typeof node.getAttribute === 'function') {
650
+ const attr = node.getAttribute('class');
651
+ if (attr) parts.push(attr);
652
+ }
653
+ return parts.join(' ');
654
+ }
655
+
656
+ function elementId(node) {
657
+ if (!node || typeof node !== 'object') return '';
658
+ if (typeof node.id === 'string' && node.id) return node.id;
659
+ if (typeof node.getAttribute === 'function') return node.getAttribute('id') || '';
660
+ return '';
661
+ }
662
+
583
663
  /**
584
664
  * @param {unknown} className
585
665
  */
package/src/index.d.ts CHANGED
@@ -334,6 +334,7 @@ export class CanvasAdapter {
334
334
  attach(): this;
335
335
  detach(): this;
336
336
  startAutoPump(): this;
337
+ stopAutoPump(): this;
337
338
  setCanvas(canvas: HTMLCanvasElement | null): this;
338
339
  setEntities(entities: CanvasEntity[]): this;
339
340
  pump(ctx?: CanvasRenderingContext2D): this;