bee-ladybug 0.5.1 → 0.5.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/README.MD CHANGED
@@ -134,6 +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
+ | `P` | blocca il pannello CSS sull'elemento corrente, solo con la hunt accesa |
137
138
  | `S` | scansione CSS, solo con la hunt accesa |
138
139
 
139
140
  Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
@@ -198,9 +199,9 @@ css.attach();
198
199
  - overflow del viewport e margini negativi
199
200
  - testo trasparente, `opacity: 0`, o colore uguale allo sfondo
200
201
  - chi intercetta il click al centro, inclusi `::before` / `::after` fissi su `html` / `body`
201
- - `<img>` con rapporto stirato oltre il 5% e `object-fit: fill`
202
+ - `<img>` con rapporto del file diverso di oltre il 5% dal riquadro disegnato (`getBoundingClientRect`), solo se `object-fit` calcolato è `fill` (il default)
202
203
 
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
+ `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. `P` ferma il pannello sull'elemento corrente, così si può leggere. In cima all'overlay, la lista **Issues** tiene gli avvisi rossi (click bloccato e causa di overflow): al massimo 10, il più recente in alto, e non scorre via con la console. In console un elemento pulito non viene scritto, e con la hunt accesa restano fuori anche `hover` e `mutate attr` del DOM, a meno che `F3` SLOW sia acceso. Ogni problema CSS viene scritto una sola volta per elemento ogni 5 secondi. I fogli di stile restano in cache finché un `MutationObserver` non vede un cambio a `<style>` o `<link>`. Hover è limitato a un frame.
204
205
 
205
206
  Demo: `examples/css-hunt.html`.
206
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.5.1",
3
+ "version": "0.5.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",
@@ -15,8 +15,11 @@ export const CSS_ADAPTER_DEFAULTS = Object.freeze({
15
15
  source: 'css',
16
16
  hotkey: 'F6',
17
17
  scanKey: 'KeyS',
18
+ pinKey: 'KeyP',
18
19
  label: 'F6 CSS',
19
- maxScan: 8
20
+ maxScan: 8,
21
+ issueWindow: 5000,
22
+ maxIssues: 10
20
23
  });
21
24
 
22
25
  export class CSSAdapter {
@@ -30,8 +33,11 @@ export class CSSAdapter {
30
33
  this.source = cfg.source;
31
34
  this.hotkey = cfg.hotkey;
32
35
  this.scanKey = cfg.scanKey;
36
+ this.pinKey = cfg.pinKey;
33
37
  this.label = cfg.label;
34
38
  this.maxScan = cfg.maxScan;
39
+ this.issueWindow = cfg.issueWindow;
40
+ this.maxIssues = cfg.maxIssues;
35
41
 
36
42
  this.#attached = false;
37
43
  this.#active = false;
@@ -43,6 +49,9 @@ export class CSSAdapter {
43
49
  this.#rafId = 0;
44
50
  this.#lastEl = null;
45
51
  this.#pinned = null;
52
+ this.#panelLock = false;
53
+ this.#issues = [];
54
+ this.#issueLog = new Map();
46
55
  this.#hoverBox = null;
47
56
  this.#pinBox = null;
48
57
  this.#observer = null;
@@ -70,6 +79,9 @@ export class CSSAdapter {
70
79
  #rafId;
71
80
  #lastEl;
72
81
  #pinned;
82
+ #panelLock;
83
+ #issues;
84
+ #issueLog;
73
85
  #hoverBox;
74
86
  #pinBox;
75
87
  #observer;
@@ -94,7 +106,7 @@ export class CSSAdapter {
94
106
  });
95
107
  this.core?.sendData('log', {
96
108
  source: this.source,
97
- message: 'CSSAdapter attached. F6 CSS hunt. S scans the page while hunt is on.'
109
+ message: 'CSSAdapter attached. F6 CSS hunt. S scans. P pins the panel.'
98
110
  });
99
111
  this.core?.sendData('state', {
100
112
  source: this.source,
@@ -146,8 +158,10 @@ export class CSSAdapter {
146
158
  score: cause.score
147
159
  });
148
160
  }
149
- const groups = groupIssues(rows).slice(0, this.maxScan);
150
- const items = groups.map((group) => ({
161
+ const groups = groupIssues(rows);
162
+ for (let i = groups.length - 1; i >= 0; i--) this.#stickGroup(groups[i]);
163
+ const shown = groups.slice(0, this.maxScan);
164
+ const items = shown.map((group) => ({
151
165
  level: group.level,
152
166
  text: formatIssue(group),
153
167
  onSelect: () => this.#pin(group.el)
@@ -157,13 +171,13 @@ export class CSSAdapter {
157
171
  this.core?.sendData('state', {
158
172
  source: this.source,
159
173
  key: 'css.scan',
160
- value: String(groups.length)
174
+ value: String(shown.length)
161
175
  });
162
- if (groups.length || isVerbose(this.core)) {
176
+ if (shown.length || isVerbose(this.core)) {
163
177
  this.core?.sendData('warn', {
164
178
  source: this.source,
165
- message: groups.length
166
- ? `scan ${groups.length} issue(s). Click a row to outline it.`
179
+ message: shown.length
180
+ ? `scan ${shown.length} issue(s). Click a row to outline it.`
167
181
  : 'scan clean: no causes found.'
168
182
  });
169
183
  }
@@ -193,7 +207,7 @@ export class CSSAdapter {
193
207
  this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'on' });
194
208
  this.core?.sendData('log', {
195
209
  source: this.source,
196
- message: 'CSS hunt on. Hover an element, or press S to scan.'
210
+ message: 'CSS hunt on. Hover an element, P pins the panel, S scans.'
197
211
  });
198
212
  }
199
213
 
@@ -212,17 +226,27 @@ export class CSSAdapter {
212
226
  this.#observer = null;
213
227
  this.#lastEl = null;
214
228
  this.#pinned = null;
229
+ this.#panelLock = false;
230
+ this.#issues = [];
231
+ this.#issueLog.clear();
215
232
  this.#unmountBoxes();
216
233
  this.core?.publishDetail([]);
234
+ this.core?.publishIssues([]);
217
235
  this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'off' });
218
236
  this.core?.sendData('log', { source: this.source, message: 'CSS hunt off.' });
219
237
  }
220
238
 
221
239
  #handleKey(event) {
222
240
  if (!this.#active || event.repeat) return;
223
- if (event.code !== this.scanKey) return;
224
241
  if (isTypingTarget(event.target)) return;
242
+ if (event.code === this.pinKey) {
243
+ event.preventDefault();
244
+ this.#togglePanelPin();
245
+ return;
246
+ }
247
+ if (event.code !== this.scanKey) return;
225
248
  event.preventDefault();
249
+ this.#panelLock = false;
226
250
  this.scan();
227
251
  }
228
252
 
@@ -252,6 +276,7 @@ export class CSSAdapter {
252
276
  #inspect(x, y) {
253
277
  const el = pickElement(x, y);
254
278
  placeBox(this.#hoverBox, el);
279
+ if (this.#panelLock) return;
255
280
  if (el === this.#lastEl) return;
256
281
  this.#lastEl = el;
257
282
  if (!el) {
@@ -284,17 +309,80 @@ export class CSSAdapter {
284
309
  text: `${path} ${hit.text}`,
285
310
  onSelect: () => this.#pin(el)
286
311
  })));
312
+ this.#stickFindings(el, path, findings);
287
313
  if (!findings.length) {
288
314
  if (isVerbose(this.core)) {
289
315
  this.core?.sendData('log', { source: this.source, message: `${path} clean` });
290
316
  }
291
317
  return;
292
318
  }
293
- const lead = findings.find((hit) => hit.level === 'error') || findings[0];
294
- this.core?.sendData(lead.level === 'error' ? 'error' : 'warn', {
295
- source: this.source,
296
- message: `${path} ${lead.text}`
297
- });
319
+ this.#logFindings(path, findings);
320
+ }
321
+
322
+ #togglePanelPin() {
323
+ if (this.#panelLock) {
324
+ this.#panelLock = false;
325
+ this.#pinned = null;
326
+ placeBox(this.#pinBox, null);
327
+ this.core?.sendData('state', { source: this.source, key: 'css.pin', value: 'off' });
328
+ this.core?.sendData('log', { source: this.source, message: 'CSS pin off.' });
329
+ this.#lastEl = null;
330
+ this.#schedule();
331
+ return;
332
+ }
333
+ const el = this.#lastEl;
334
+ if (!el) return;
335
+ this.#panelLock = true;
336
+ this.#pin(el);
337
+ const path = cssPath(el);
338
+ this.core?.sendData('state', { source: this.source, key: 'css.pin', value: path });
339
+ this.core?.sendData('log', { source: this.source, message: `CSS pin ${path}` });
340
+ }
341
+
342
+ #logFindings(path, findings) {
343
+ const now = nowMs();
344
+ for (let i = 0; i < findings.length; i++) {
345
+ const hit = findings[i];
346
+ const text = hit.text || hit.kind || '';
347
+ if (!text) continue;
348
+ if (!rememberIssue(this.#issueLog, `${path}\0${text}`, now, this.issueWindow)) continue;
349
+ const level = hit.level === 'error' ? 'error' : 'warn';
350
+ this.core?.sendData(level, {
351
+ source: this.source,
352
+ message: `${path} ${text}`
353
+ });
354
+ }
355
+ }
356
+
357
+ #stickFindings(el, path, findings) {
358
+ for (let i = 0; i < findings.length; i++) {
359
+ const hit = findings[i];
360
+ if (!isStickyKind(hit.kind)) continue;
361
+ if (hit.kind === 'overflows viewport' && !elementCausesOverflow(el)) continue;
362
+ this.#issues = pushStickyIssue(this.#issues, {
363
+ text: `${path} ${hit.kind}`,
364
+ el
365
+ }, this.maxIssues);
366
+ }
367
+ this.#publishIssues();
368
+ }
369
+
370
+ #stickGroup(group) {
371
+ if (!isStickyKind(group.kind)) return;
372
+ const count = group.count > 1 ? ` ×${group.count}` : '';
373
+ this.#issues = pushStickyIssue(this.#issues, {
374
+ text: `${group.selector} ${group.kind}${count}`,
375
+ el: group.el
376
+ }, this.maxIssues);
377
+ this.#publishIssues();
378
+ }
379
+
380
+ #publishIssues() {
381
+ this.core?.publishIssues(this.#issues.map((item) => ({
382
+ level: 'error',
383
+ text: item.text,
384
+ onSelect: () => this.#pin(item.el)
385
+ })));
298
386
  }
299
387
 
300
388
  #pin(el) {
@@ -568,23 +656,91 @@ function pushImage(el, findings) {
568
656
  if (!el || el.tagName !== 'IMG') return;
569
657
  const naturalW = el.naturalWidth;
570
658
  const naturalH = el.naturalHeight;
571
- const shownW = el.clientWidth;
572
- const shownH = el.clientHeight;
573
- if (!naturalW || !naturalH || !shownW || !shownH) return;
574
- const natural = naturalW / naturalH;
575
- const shown = shownW / shownH;
576
- const diff = Math.abs(natural - shown) / natural;
577
- const fit = getComputedStyle(el).objectFit;
578
- if (diff <= 0.05 || fit !== 'fill') return;
659
+ if (!naturalW || !naturalH || typeof el.getBoundingClientRect !== 'function') return;
660
+ const rect = el.getBoundingClientRect();
661
+ const fit = (typeof getComputedStyle === 'function' ? getComputedStyle(el).objectFit : '') || 'fill';
662
+ if (!isDistortedImage(naturalW, naturalH, rect.width, rect.height, fit)) return;
663
+ const gap = renderedRatioGap(naturalW, naturalH, rect.width, rect.height);
579
664
  findings.push({
580
665
  level: 'error',
581
666
  rank: ISSUE_RANK.image,
582
- score: diff * 100,
667
+ score: gap * 100,
583
668
  kind: 'distorted image',
584
- text: `distorted image ${Math.round(diff * 100)}% (object-fit: fill)`
669
+ text: `distorted image ${Math.round(gap * 100)}% (object-fit: fill)`
585
670
  });
586
671
  }
587
672
 
673
+ /**
674
+ * Rendered box versus the file. Attributes and clientWidth are ignored.
675
+ * `object-fit` defaults to fill. Flag when the ratios differ by more than 5%.
676
+ */
677
+ export function isDistortedImage(naturalW, naturalH, renderedW, renderedH, objectFit) {
678
+ const fit = objectFit || 'fill';
679
+ if (fit !== 'fill') return false;
680
+ return renderedRatioGap(naturalW, naturalH, renderedW, renderedH) > 0.05;
681
+ }
682
+
683
+ export function renderedRatioGap(naturalW, naturalH, renderedW, renderedH) {
684
+ if (!naturalW || !naturalH || !renderedW || !renderedH) return 0;
685
+ const natural = naturalW / naturalH;
686
+ const rendered = renderedW / renderedH;
687
+ return Math.abs(natural - rendered) / natural;
688
+ }
689
+
690
+ /** @returns {boolean} true when this issue should be written to the event list */
691
+ export function rememberIssue(seen, key, now, windowMs = 5000) {
692
+ const prev = seen.get(key);
693
+ if (typeof prev === 'number' && now - prev < windowMs) return false;
694
+ seen.set(key, now);
695
+ if (seen.size > 64) {
696
+ for (const [name, at] of seen) {
697
+ if (now - at >= windowMs) seen.delete(name);
698
+ }
699
+ }
700
+ return true;
701
+ }
702
+
703
+ /**
704
+ * Newest first, duplicates move to the top, capped at `max`.
705
+ * @param {Array<{ text: string, el?: Element }>} list
706
+ * @param {{ text: string, el?: Element }} entry
707
+ */
708
+ export function pushStickyIssue(list, entry, max = 10) {
709
+ const next = [entry];
710
+ for (let i = 0; i < list.length; i++) {
711
+ if (list[i].text === entry.text) continue;
712
+ next.push(list[i]);
713
+ if (next.length >= max) break;
714
+ }
715
+ return next.slice(0, max);
716
+ }
717
+
718
+ function isStickyKind(kind) {
719
+ return kind === 'overflows viewport' || (typeof kind === 'string' && kind.startsWith('click blocked'));
720
+ }
721
+
722
+ function elementCausesOverflow(el) {
723
+ const self = measureViewportOverflow(el);
724
+ if (!self) return false;
725
+ const items = [self];
726
+ if (typeof el.querySelectorAll === 'function') {
727
+ const nodes = el.querySelectorAll('*');
728
+ for (let i = 0; i < nodes.length; i++) {
729
+ const measured = measureViewportOverflow(nodes[i]);
730
+ if (measured) items.push(measured);
731
+ }
732
+ }
733
+ const causes = keepOverflowCauses(items);
734
+ for (let i = 0; i < causes.length; i++) {
735
+ if (causes[i].el === el) return true;
736
+ }
737
+ return false;
738
+ }
739
+
740
+ function nowMs() {
741
+ return typeof performance !== 'undefined' ? performance.now() : Date.now();
742
+ }
743
+
588
744
  function issue(kind, rank, score) {
589
745
  return { level: 'error', rank, score, kind, text: kind };
590
746
  }
@@ -279,10 +279,12 @@ export class WebDOMAdapter {
279
279
 
280
280
  if (key !== this.#lastHoverKey) {
281
281
  this.#lastHoverKey = key;
282
- this.core.sendData('log', {
283
- source: this.source,
284
- message: `hover ${key} ${Math.round(model.border.w)}×${Math.round(model.border.h)} ${model.display}`
285
- });
282
+ if (!domNoiseQuiet(cssHuntEnabled(this.core), this.frozen, this.timeScale)) {
283
+ this.core.sendData('log', {
284
+ source: this.source,
285
+ message: `hover ${key} ${Math.round(model.border.w)}×${Math.round(model.border.h)} ${model.display}`
286
+ });
287
+ }
286
288
  }
287
289
  }
288
290
 
@@ -313,14 +315,17 @@ export class WebDOMAdapter {
313
315
  this.#mutRemoved = 0;
314
316
  this.#mutAttr = 0;
315
317
  if (!added && !removed && !attr) return;
318
+ const quiet = domNoiseQuiet(cssHuntEnabled(this.core), this.frozen, this.timeScale);
316
319
  const parts = [];
317
320
  if (added) parts.push(`+${added}`);
318
321
  if (removed) parts.push(`-${removed}`);
319
- if (attr) parts.push(`attr ${attr}`);
320
- this.core?.sendData('warn', {
321
- source: this.source,
322
- message: `mutate ${parts.join(' ')}`
323
- });
322
+ if (attr && !quiet) parts.push(`attr ${attr}`);
323
+ if (parts.length) {
324
+ this.core?.sendData('warn', {
325
+ source: this.source,
326
+ message: `mutate ${parts.join(' ')}`
327
+ });
328
+ }
324
329
  this.core?.sendData('metric', {
325
330
  name: 'mutations',
326
331
  value: added + removed + attr,
@@ -504,3 +509,21 @@ function countElements(root) {
504
509
  if (root.nodeType === 1) return 1 + (root.getElementsByTagName?.('*').length ?? 0);
505
510
  return root.getElementsByTagName?.('*').length ?? 0;
506
511
  }
512
+
513
+ /**
514
+ * While CSS hunt is on, hover and attribute-mutation lines stay quiet
515
+ * unless F3 SLOW is running.
516
+ */
517
+ export function domNoiseQuiet(cssEnabled, frozen, timeScale) {
518
+ if (!cssEnabled) return false;
519
+ return Boolean(frozen) || timeScale === 1;
520
+ }
521
+
522
+ function cssHuntEnabled(core) {
523
+ if (!core || typeof core.getAdapters !== 'function') return false;
524
+ const list = core.getAdapters();
525
+ for (let i = 0; i < list.length; i++) {
526
+ if (list[i].name === 'css' && list[i].enabled) return true;
527
+ }
528
+ return false;
529
+ }
@@ -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.1';
12
+ export const BEE_LADYBUG_VERSION = '0.5.2';
13
13
 
14
14
  export const CORE_DEFAULTS = Object.freeze({
15
15
  toggleKey: 'F2',
@@ -99,6 +99,7 @@ export class BeeLadybugCore {
99
99
  this.#askBusy = false;
100
100
  this.#adapters = new Map();
101
101
  this.#detail = [];
102
+ this.#issues = [];
102
103
 
103
104
  this.ui = null;
104
105
  if (cfg.ui !== false && typeof document !== 'undefined') {
@@ -135,6 +136,8 @@ export class BeeLadybugCore {
135
136
  #askBusy;
136
137
  /** @type {Array<{ text: string, level?: string, onSelect?: () => void }>} */
137
138
  #detail;
139
+ /** @type {Array<{ text: string, level?: string, onSelect?: () => void }>} */
140
+ #issues;
138
141
  /** @type {Map<string, { name: string, label: string, enabled: boolean, enable: Function, disable: Function }>} */
139
142
  #adapters;
140
143
 
@@ -356,6 +359,17 @@ export class BeeLadybugCore {
356
359
  return this;
357
360
  }
358
361
 
362
+ /**
363
+ * Sticky red warnings. The core does not interpret them.
364
+ * @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
365
+ */
366
+ publishIssues(items) {
367
+ const list = Array.isArray(items) ? items.slice(0, 10) : [];
368
+ this.#issues = list;
369
+ this.ui?.setIssues(this.#issues);
370
+ return this;
371
+ }
372
+
359
373
  /**
360
374
  * @param {string} name
361
375
  */
@@ -463,6 +477,7 @@ export class BeeLadybugCore {
463
477
  this.#askBusy = false;
464
478
  this.#adapters.clear();
465
479
  this.#detail = [];
480
+ this.#issues = [];
466
481
  this.ui?.destroy();
467
482
  this.ui = null;
468
483
  return this;
@@ -263,6 +263,35 @@ const STYLES = `
263
263
  .bl-hotkeys:empty {
264
264
  display: none;
265
265
  }
266
+ .bl-issues {
267
+ overflow: hidden;
268
+ background: #07080c;
269
+ border-top: 1px solid rgba(255, 59, 78, 0.55);
270
+ }
271
+ .bl-issues[hidden] {
272
+ display: none;
273
+ }
274
+ .bl-issues-label {
275
+ padding: 5px 10px 0;
276
+ color: #ff3b4e;
277
+ font-size: 9px;
278
+ font-weight: 700;
279
+ letter-spacing: 0.14em;
280
+ }
281
+ .bl-issues button {
282
+ display: block;
283
+ width: 100%;
284
+ margin: 0;
285
+ padding: 3px 10px;
286
+ border: 0;
287
+ background: transparent;
288
+ color: #ff6b7d;
289
+ font: 11px ui-monospace, Consolas, monospace;
290
+ line-height: 1.4;
291
+ text-align: left;
292
+ cursor: pointer;
293
+ }
294
+ .bl-issues button:hover { background: rgba(255, 59, 78, 0.08); }
266
295
  .bl-detail {
267
296
  max-height: 132px;
268
297
  overflow: auto;
@@ -366,6 +395,7 @@ export class UIOverlay {
366
395
  this.#adapterSig = '';
367
396
  this.#hotkeySig = '';
368
397
  this.#detail = [];
398
+ this.#issues = [];
369
399
  this.#flashAt = new Map();
370
400
  this.#flashTimers = new Map();
371
401
  }
@@ -380,6 +410,7 @@ export class UIOverlay {
380
410
  #adapterSig;
381
411
  #hotkeySig;
382
412
  #detail;
413
+ #issues;
383
414
  #flashAt;
384
415
  #flashTimers;
385
416
 
@@ -430,6 +461,10 @@ export class UIOverlay {
430
461
  </div>
431
462
  </div>
432
463
  <div class="bl-hud" data-el="hud"></div>
464
+ <div class="bl-issues" data-el="issues" hidden>
465
+ <div class="bl-issues-label">ISSUES</div>
466
+ <div data-el="issues-list"></div>
467
+ </div>
433
468
  <div class="bl-detail" data-el="detail" hidden></div>
434
469
  <div class="bl-console" data-el="console" role="log" aria-live="polite"></div>
435
470
  <div class="bl-foot">
@@ -459,6 +494,8 @@ export class UIOverlay {
459
494
  hud: this.#root.querySelector('[data-el="hud"]'),
460
495
  console: this.#root.querySelector('[data-el="console"]'),
461
496
  detail: this.#root.querySelector('[data-el="detail"]'),
497
+ issues: this.#root.querySelector('[data-el="issues"]'),
498
+ issuesList: this.#root.querySelector('[data-el="issues-list"]'),
462
499
  providers: this.#root.querySelector('[data-el="providers"]'),
463
500
  adapters: this.#root.querySelector('[data-el="adapters"]'),
464
501
  hotkeys: this.#root.querySelector('[data-el="hotkeys"]')
@@ -476,6 +513,13 @@ export class UIOverlay {
476
513
  else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
477
514
  });
478
515
 
516
+ this.#root.querySelector('.bl-issues')?.addEventListener('click', (event) => {
517
+ const row = event.target.closest('[data-issue]');
518
+ if (!row) return;
519
+ const item = this.#issues[Number(row.getAttribute('data-issue'))];
520
+ if (item && typeof item.onSelect === 'function') item.onSelect();
521
+ });
522
+
479
523
  this.#root.querySelector('.bl-detail')?.addEventListener('click', (event) => {
480
524
  const row = event.target.closest('[data-detail]');
481
525
  if (!row) return;
@@ -506,6 +550,7 @@ export class UIOverlay {
506
550
  this.#adapterSig = '';
507
551
  this.#hotkeySig = '';
508
552
  this.#detail = [];
553
+ this.#issues = [];
509
554
  this.#clearFlashTimers();
510
555
  this.#flashAt.clear();
511
556
  return this;
@@ -546,6 +591,29 @@ export class UIOverlay {
546
591
  return this;
547
592
  }
548
593
 
594
+ /**
595
+ * Red warnings pinned above the scrolling console. Newest first.
596
+ * @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
597
+ */
598
+ setIssues(items) {
599
+ if (!this.#els?.issues || !this.#els.issuesList) return this;
600
+ const list = Array.isArray(items) ? items.slice(0, 10) : [];
601
+ this.#issues = list;
602
+ const box = this.#els.issues;
603
+ if (!list.length) {
604
+ this.#els.issuesList.innerHTML = '';
605
+ box.hidden = true;
606
+ return this;
607
+ }
608
+ let html = '';
609
+ for (let i = 0; i < list.length; i++) {
610
+ html += `<button type="button" data-issue="${i}">${escapeHtml(list[i].text)}</button>`;
611
+ }
612
+ box.hidden = false;
613
+ this.#els.issuesList.innerHTML = html;
614
+ return this;
615
+ }
616
+
549
617
  onPacket(packet) {
550
618
  if (!this.#els || !packet) return;
551
619
  if (packet.type === 'sys') return;
package/src/index.d.ts CHANGED
@@ -178,6 +178,7 @@ export class BeeLadybugCore {
178
178
  toggleAdapter(name: string): this;
179
179
  setAdapterEnabled(name: string, enabled: boolean): this;
180
180
  publishDetail(items: DetailItem[]): this;
181
+ publishIssues(items: DetailItem[]): this;
181
182
  }
182
183
 
183
184
  export interface DetailItem {
@@ -197,6 +198,7 @@ export class UIOverlay {
197
198
  onClear(channel?: string): void;
198
199
  render(): void;
199
200
  setDetail(items: DetailItem[]): this;
201
+ setIssues(items: DetailItem[]): this;
200
202
  }
201
203
 
202
204
  export interface CanvasEntity {
@@ -287,8 +289,11 @@ export interface CSSAdapterOptions {
287
289
  source?: string;
288
290
  hotkey?: string;
289
291
  scanKey?: string;
292
+ pinKey?: string;
290
293
  label?: string;
291
294
  maxScan?: number;
295
+ issueWindow?: number;
296
+ maxIssues?: number;
292
297
  }
293
298
 
294
299
  export const CSS_ADAPTER_DEFAULTS: Required<CSSAdapterOptions>;
@@ -298,8 +303,11 @@ export class CSSAdapter {
298
303
  source: string;
299
304
  hotkey: string;
300
305
  scanKey: string;
306
+ pinKey: string;
301
307
  label: string;
302
308
  maxScan: number;
309
+ issueWindow: number;
310
+ maxIssues: number;
303
311
  readonly active: boolean;
304
312
  constructor(core: BeeLadybugCore, options?: CSSAdapterOptions);
305
313
  attach(): this;