bee-ladybug 0.5.2 → 0.5.3

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
@@ -195,13 +195,13 @@ css.attach();
195
195
 
196
196
  `F6` (pulsante **F6 CSS** accanto a F3/F4) accende la caccia. Sull'elemento sotto il puntatore l'overlay elenca:
197
197
 
198
- - regole `!important` che gli corrispondono (fogli cross-origin: `unreadable sheet: <href>`)
198
+ - regole `!important` che gli corrispondono. Un foglio cross-origin è una nota grigia, una volta sola: `unreadable sheet: <href>`
199
199
  - overflow del viewport e margini negativi
200
200
  - testo trasparente, `opacity: 0`, o colore uguale allo sfondo
201
201
  - chi intercetta il click al centro, inclusi `::before` / `::after` fissi su `html` / `body`
202
202
  - `<img>` con rapporto del file diverso di oltre il 5% dal riquadro disegnato (`getBoundingClientRect`), solo se `object-fit` calcolato è `fill` (il default)
203
203
 
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
+ `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. **Issues** raggruppa per causa: `body::after blocks clicks on 10 elements` è una riga rossa, e il click la apre sull'elenco degli elementi. In cima alla sezione, la legenda dice rosso = bug e grigio = info. Un foglio illeggibile è una riga grigia, una per foglio, e non entra tra i bug. L'overlay resta alto al massimo metà schermo, scorre al suo interno, e le sezioni Status, Issues, Inspect ed Events si chiudono. 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.
205
205
 
206
206
  Demo: `examples/css-hunt.html`.
207
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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",
@@ -42,7 +42,6 @@ export class CSSAdapter {
42
42
  this.#attached = false;
43
43
  this.#active = false;
44
44
  this.#rules = [];
45
- this.#unreadable = [];
46
45
  this.#sheetsDirty = true;
47
46
  this.#x = 0;
48
47
  this.#y = 0;
@@ -51,6 +50,8 @@ export class CSSAdapter {
51
50
  this.#pinned = null;
52
51
  this.#panelLock = false;
53
52
  this.#issues = [];
53
+ this.#notes = [];
54
+ this.#seenSheets = new Set();
54
55
  this.#issueLog = new Map();
55
56
  this.#hoverBox = null;
56
57
  this.#pinBox = null;
@@ -72,7 +73,6 @@ export class CSSAdapter {
72
73
  #attached;
73
74
  #active;
74
75
  #rules;
75
- #unreadable;
76
76
  #sheetsDirty;
77
77
  #x;
78
78
  #y;
@@ -81,6 +81,8 @@ export class CSSAdapter {
81
81
  #pinned;
82
82
  #panelLock;
83
83
  #issues;
84
+ #notes;
85
+ #seenSheets;
84
86
  #issueLog;
85
87
  #hoverBox;
86
88
  #pinBox;
@@ -159,7 +161,11 @@ export class CSSAdapter {
159
161
  });
160
162
  }
161
163
  const groups = groupIssues(rows);
162
- for (let i = groups.length - 1; i >= 0; i--) this.#stickGroup(groups[i]);
164
+ for (let i = 0; i < rows.length; i++) {
165
+ const row = rows[i];
166
+ this.#rememberCause(row.el, row.selector, row.kind);
167
+ }
168
+ this.#publishIssues();
163
169
  const shown = groups.slice(0, this.maxScan);
164
170
  const items = shown.map((group) => ({
165
171
  level: group.level,
@@ -228,6 +234,8 @@ export class CSSAdapter {
228
234
  this.#pinned = null;
229
235
  this.#panelLock = false;
230
236
  this.#issues = [];
237
+ this.#notes = [];
238
+ this.#seenSheets.clear();
231
239
  this.#issueLog.clear();
232
240
  this.#unmountBoxes();
233
241
  this.core?.publishDetail([]);
@@ -264,13 +272,8 @@ export class CSSAdapter {
264
272
  this.#sheetsDirty = false;
265
273
  const built = collectImportantRules();
266
274
  this.#rules = built.rules;
267
- this.#unreadable = built.unreadable;
268
- for (let i = 0; i < built.unreadable.length; i++) {
269
- this.core?.sendData('warn', {
270
- source: this.source,
271
- message: `unreadable sheet: ${built.unreadable[i]}`
272
- });
273
- }
275
+ for (let i = 0; i < built.unreadable.length; i++) this.#rememberSheet(built.unreadable[i]);
276
+ this.#publishIssues();
274
277
  }
275
278
 
276
279
  #inspect(x, y) {
@@ -290,13 +293,6 @@ export class CSSAdapter {
290
293
  pushInvisible(el, findings);
291
294
  pushBlockers(el, findings);
292
295
  pushImage(el, findings);
293
- for (let i = 0; i < this.#unreadable.length; i++) {
294
- findings.push({
295
- level: 'warn',
296
- text: `unreadable sheet: ${this.#unreadable[i]}`,
297
- score: 0
298
- });
299
- }
300
296
  const path = cssPath(el);
301
297
  this.core?.sendData('state', { source: this.source, key: 'css.target', value: path });
302
298
  this.core?.sendData('state', {
@@ -344,7 +340,7 @@ export class CSSAdapter {
344
340
  for (let i = 0; i < findings.length; i++) {
345
341
  const hit = findings[i];
346
342
  const text = hit.text || hit.kind || '';
347
- if (!text) continue;
343
+ if (!text || text.startsWith('unreadable sheet:')) continue;
348
344
  if (!rememberIssue(this.#issueLog, `${path}\0${text}`, now, this.issueWindow)) continue;
349
345
  const level = hit.level === 'error' ? 'error' : 'warn';
350
346
  this.core?.sendData(level, {
@@ -355,34 +351,50 @@ export class CSSAdapter {
355
351
  }
356
352
 
357
353
  #stickFindings(el, path, findings) {
354
+ let changed = false;
358
355
  for (let i = 0; i < findings.length; i++) {
359
356
  const hit = findings[i];
360
- if (!isStickyKind(hit.kind)) continue;
361
357
  if (hit.kind === 'overflows viewport' && !elementCausesOverflow(el)) continue;
362
- this.#issues = pushStickyIssue(this.#issues, {
363
- text: `${path} ${hit.kind}`,
364
- el
365
- }, this.maxIssues);
358
+ if (this.#rememberCause(el, path, hit.kind)) changed = true;
366
359
  }
367
- this.#publishIssues();
360
+ if (changed) this.#publishIssues();
368
361
  }
369
362
 
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();
363
+ #rememberCause(el, path, kind) {
364
+ const next = addCause(this.#issues, { kind, path, el }, this.maxIssues);
365
+ if (next === this.#issues) return false;
366
+ this.#issues = next;
367
+ return true;
368
+ }
369
+
370
+ #rememberSheet(href) {
371
+ const label = String(href || '');
372
+ if (!label || this.#seenSheets.has(label)) return;
373
+ this.#seenSheets.add(label);
374
+ this.#notes.push({ text: `unreadable sheet: ${label}` });
375
+ this.core?.sendData('log', {
376
+ source: this.source,
377
+ level: 'info',
378
+ message: `unreadable sheet: ${label}`
379
+ });
378
380
  }
379
381
 
380
382
  #publishIssues() {
381
- this.core?.publishIssues(this.#issues.map((item) => ({
383
+ const bugs = this.#issues.map((entry) => ({
382
384
  level: 'error',
383
- text: item.text,
384
- onSelect: () => this.#pin(item.el)
385
- })));
385
+ key: entry.key,
386
+ text: formatCause(entry),
387
+ targets: entry.targets.map((target) => ({
388
+ text: target.text,
389
+ onSelect: () => this.#pin(target.el)
390
+ }))
391
+ }));
392
+ const notes = this.#notes.map((note) => ({
393
+ level: 'info',
394
+ key: note.text,
395
+ text: note.text
396
+ }));
397
+ this.core?.publishIssues(bugs.concat(notes));
386
398
  }
387
399
 
388
400
  #pin(el) {
@@ -715,8 +727,57 @@ export function pushStickyIssue(list, entry, max = 10) {
715
727
  return next.slice(0, max);
716
728
  }
717
729
 
718
- function isStickyKind(kind) {
719
- return kind === 'overflows viewport' || (typeof kind === 'string' && kind.startsWith('click blocked'));
730
+ export function describeCause(kind, path) {
731
+ if (kind === 'overflows viewport') {
732
+ return { key: `overflow\0${path}`, type: 'overflow', cause: path };
733
+ }
734
+ const prefix = 'click blocked by ';
735
+ if (typeof kind === 'string' && kind.startsWith(prefix)) {
736
+ const cause = kind.slice(prefix.length);
737
+ return { key: `block\0${cause}`, type: 'block', cause };
738
+ }
739
+ return null;
740
+ }
741
+
742
+ /**
743
+ * One row per cause. A blocker that covers many nodes stays a single row.
744
+ * @param {Array<{ key: string, type: string, cause: string, targets: Array<{ text: string, el?: Element }> }>} list
745
+ * @param {{ kind?: string, path: string, el?: Element }} entry
746
+ */
747
+ export function addCause(list, entry, max = 10) {
748
+ const described = describeCause(entry.kind, entry.path);
749
+ if (!described) return list;
750
+ const target = { text: entry.path, el: entry.el };
751
+ const rest = [];
752
+ let prev = null;
753
+ for (let i = 0; i < list.length; i++) {
754
+ if (list[i].key === described.key) prev = list[i];
755
+ else rest.push(list[i]);
756
+ }
757
+ const targets = prev ? prev.targets.filter((item) => !sameTarget(item, target)) : [];
758
+ targets.unshift(target);
759
+ rest.unshift({
760
+ key: described.key,
761
+ type: described.type,
762
+ cause: described.cause,
763
+ targets
764
+ });
765
+ return rest.slice(0, max);
766
+ }
767
+
768
+ export function formatCause(entry) {
769
+ const count = entry.targets.length;
770
+ if (entry.type === 'block') {
771
+ const noun = count === 1 ? 'element' : 'elements';
772
+ return `${entry.cause} blocks clicks on ${count} ${noun}`;
773
+ }
774
+ if (count <= 1) return `${entry.cause} overflows viewport`;
775
+ return `${entry.cause} overflows viewport · ${count} elements`;
776
+ }
777
+
778
+ function sameTarget(a, b) {
779
+ if (a.el && b.el) return a.el === b.el;
780
+ return a.text === b.text;
720
781
  }
721
782
 
722
783
  function elementCausesOverflow(el) {
@@ -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.2';
12
+ export const BEE_LADYBUG_VERSION = '0.5.3';
13
13
 
14
14
  export const CORE_DEFAULTS = Object.freeze({
15
15
  toggleKey: 'F2',
@@ -364,7 +364,7 @@ export class BeeLadybugCore {
364
364
  * @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
365
365
  */
366
366
  publishIssues(items) {
367
- const list = Array.isArray(items) ? items.slice(0, 10) : [];
367
+ const list = Array.isArray(items) ? items : [];
368
368
  this.#issues = list;
369
369
  this.ui?.setIssues(this.#issues);
370
370
  return this;
@@ -35,6 +35,9 @@ const STYLES = `
35
35
  top: 12px;
36
36
  left: 12px;
37
37
  width: 360px;
38
+ max-height: 50vh;
39
+ display: flex;
40
+ flex-direction: column;
38
41
  background: rgba(7, 8, 12, 0.94);
39
42
  border: 1px solid rgba(255, 59, 78, 0.55);
40
43
  box-sizing: border-box;
@@ -62,6 +65,7 @@ const STYLES = `
62
65
  display: flex;
63
66
  align-items: center;
64
67
  gap: 8px;
68
+ flex: 0 0 auto;
65
69
  padding: 10px 12px 8px;
66
70
  border-bottom: 1px solid rgba(255, 59, 78, 0.28);
67
71
  }
@@ -201,7 +205,7 @@ const STYLES = `
201
205
  .bl-k { color: #8b91a3; }
202
206
  .bl-v { color: #e9edf5; }
203
207
  .bl-console {
204
- height: 168px;
208
+ max-height: 140px;
205
209
  overflow: auto;
206
210
  padding: 6px 10px 8px;
207
211
  background: #05060a;
@@ -212,13 +216,14 @@ const STYLES = `
212
216
  .bl-line { white-space: normal; word-break: break-word; }
213
217
  .bl-line .t { color: #8b91a3; margin-right: 8px; }
214
218
  .bl-line .s { color: #6ee7ff; margin-right: 8px; }
215
- .bl-line.info .m { color: #d8dce6; }
219
+ .bl-line.info .m { color: #8b91a3; }
216
220
  .bl-line.warn .m { color: #ffd36a; }
217
221
  .bl-line.error .m { color: #ff6b7d; }
218
222
  .bl-line.debug .m { color: #8b91a3; }
219
223
  .bl-foot {
220
224
  display: flex;
221
225
  align-items: center;
226
+ flex: 0 0 auto;
222
227
  flex-wrap: wrap;
223
228
  gap: 6px;
224
229
  padding: 8px 10px;
@@ -263,35 +268,72 @@ const STYLES = `
263
268
  .bl-hotkeys:empty {
264
269
  display: none;
265
270
  }
271
+ .bl-scroll {
272
+ overflow: auto;
273
+ min-height: 0;
274
+ flex: 1 1 auto;
275
+ }
276
+ .bl-section[hidden] { display: none; }
277
+ .bl-section-toggle {
278
+ display: block;
279
+ width: 100%;
280
+ margin: 0;
281
+ padding: 5px 10px;
282
+ border: 0;
283
+ border-top: 1px solid rgba(255, 59, 78, 0.28);
284
+ background: #101218;
285
+ color: #ffd36a;
286
+ font: 700 9px ui-monospace, Consolas, monospace;
287
+ letter-spacing: 0.14em;
288
+ text-align: left;
289
+ cursor: pointer;
290
+ }
291
+ .bl-section-toggle::before { content: "▾ "; }
292
+ .bl-section[data-open="false"] > .bl-section-toggle::before { content: "▸ "; }
293
+ .bl-section[data-open="false"] > .bl-section-body { display: none; }
266
294
  .bl-issues {
267
- overflow: hidden;
268
295
  background: #07080c;
269
- border-top: 1px solid rgba(255, 59, 78, 0.55);
270
296
  }
271
- .bl-issues[hidden] {
272
- display: none;
297
+ .bl-legend {
298
+ display: flex;
299
+ gap: 14px;
300
+ padding: 4px 10px 2px;
301
+ color: #8b91a3;
302
+ font-size: 10px;
273
303
  }
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;
304
+ .bl-dot {
305
+ display: inline-block;
306
+ width: 7px;
307
+ height: 7px;
308
+ margin-right: 5px;
309
+ border-radius: 50%;
310
+ vertical-align: baseline;
280
311
  }
281
- .bl-issues button {
312
+ .bl-dot.bug { background: #ff3b4e; }
313
+ .bl-dot.info { background: #8b91a3; }
314
+ .bl-cause > button,
315
+ .bl-cause-kids button,
316
+ .bl-note {
282
317
  display: block;
283
318
  width: 100%;
284
319
  margin: 0;
285
320
  padding: 3px 10px;
286
321
  border: 0;
287
322
  background: transparent;
288
- color: #ff6b7d;
289
323
  font: 11px ui-monospace, Consolas, monospace;
290
324
  line-height: 1.4;
291
325
  text-align: left;
326
+ }
327
+ .bl-cause > button,
328
+ .bl-cause-kids button {
329
+ color: #ff6b7d;
292
330
  cursor: pointer;
293
331
  }
294
- .bl-issues button:hover { background: rgba(255, 59, 78, 0.08); }
332
+ .bl-cause-kids[hidden] { display: none; }
333
+ .bl-cause-kids button { padding-left: 22px; }
334
+ .bl-cause > button:hover,
335
+ .bl-cause-kids button:hover { background: rgba(255, 59, 78, 0.08); }
336
+ .bl-note { color: #8b91a3; }
295
337
  .bl-detail {
296
338
  max-height: 132px;
297
339
  overflow: auto;
@@ -396,6 +438,8 @@ export class UIOverlay {
396
438
  this.#hotkeySig = '';
397
439
  this.#detail = [];
398
440
  this.#issues = [];
441
+ this.#issueNotes = [];
442
+ this.#openCauses = new Set();
399
443
  this.#flashAt = new Map();
400
444
  this.#flashTimers = new Map();
401
445
  }
@@ -411,6 +455,8 @@ export class UIOverlay {
411
455
  #hotkeySig;
412
456
  #detail;
413
457
  #issues;
458
+ #issueNotes;
459
+ #openCauses;
414
460
  #flashAt;
415
461
  #flashTimers;
416
462
 
@@ -447,6 +493,10 @@ export class UIOverlay {
447
493
  </button>
448
494
  </span>
449
495
  </div>
496
+ <div class="bl-scroll">
497
+ <section class="bl-section" data-section="status" data-open="true">
498
+ <button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">STATUS</button>
499
+ <div class="bl-section-body">
450
500
  <div class="bl-stats">
451
501
  <div>HOST <b data-el="hostFps">—</b></div>
452
502
  <div>CORE <b data-el="coreFps">—</b></div>
@@ -461,12 +511,32 @@ export class UIOverlay {
461
511
  </div>
462
512
  </div>
463
513
  <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>
514
+ </div>
515
+ </section>
516
+ <section class="bl-section" data-el="issues" data-section="issues" data-open="true" hidden>
517
+ <button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">ISSUES</button>
518
+ <div class="bl-section-body">
519
+ <div class="bl-legend">
520
+ <span><i class="bl-dot bug"></i>bug</span>
521
+ <span><i class="bl-dot info"></i>info</span>
522
+ </div>
523
+ <div data-el="issues-list"></div>
524
+ <div data-el="notes"></div>
525
+ </div>
526
+ </section>
527
+ <section class="bl-section" data-el="inspect" data-section="inspect" data-open="true" hidden>
528
+ <button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">INSPECT</button>
529
+ <div class="bl-section-body">
530
+ <div class="bl-detail" data-el="detail" hidden></div>
531
+ </div>
532
+ </section>
533
+ <section class="bl-section" data-section="events" data-open="true">
534
+ <button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">EVENTS</button>
535
+ <div class="bl-section-body">
536
+ <div class="bl-console" data-el="console" role="log" aria-live="polite"></div>
537
+ </div>
538
+ </section>
467
539
  </div>
468
- <div class="bl-detail" data-el="detail" hidden></div>
469
- <div class="bl-console" data-el="console" role="log" aria-live="polite"></div>
470
540
  <div class="bl-foot">
471
541
  <button class="bl-btn" data-act="slow" type="button">F3 SLOW</button>
472
542
  <button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
@@ -494,8 +564,10 @@ export class UIOverlay {
494
564
  hud: this.#root.querySelector('[data-el="hud"]'),
495
565
  console: this.#root.querySelector('[data-el="console"]'),
496
566
  detail: this.#root.querySelector('[data-el="detail"]'),
567
+ inspect: this.#root.querySelector('[data-el="inspect"]'),
497
568
  issues: this.#root.querySelector('[data-el="issues"]'),
498
569
  issuesList: this.#root.querySelector('[data-el="issues-list"]'),
570
+ notes: this.#root.querySelector('[data-el="notes"]'),
499
571
  providers: this.#root.querySelector('[data-el="providers"]'),
500
572
  adapters: this.#root.querySelector('[data-el="adapters"]'),
501
573
  hotkeys: this.#root.querySelector('[data-el="hotkeys"]')
@@ -513,11 +585,40 @@ export class UIOverlay {
513
585
  else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
514
586
  });
515
587
 
516
- this.#root.querySelector('.bl-issues')?.addEventListener('click', (event) => {
588
+ this.#root.querySelector('.bl-scroll')?.addEventListener('click', (event) => {
589
+ const toggle = event.target.closest('[data-section-toggle]');
590
+ if (!toggle) return;
591
+ const section = toggle.closest('.bl-section');
592
+ if (!section) return;
593
+ const open = section.dataset.open === 'false';
594
+ section.dataset.open = open ? 'true' : 'false';
595
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
596
+ });
597
+
598
+ this.#els.issues?.addEventListener('click', (event) => {
599
+ if (event.target.closest('[data-section-toggle]')) return;
600
+ const childBtn = event.target.closest('[data-target]');
601
+ if (childBtn) {
602
+ const item = this.#issues[Number(childBtn.getAttribute('data-issue'))];
603
+ const child = item?.targets?.[Number(childBtn.getAttribute('data-target'))];
604
+ if (child && typeof child.onSelect === 'function') child.onSelect();
605
+ return;
606
+ }
517
607
  const row = event.target.closest('[data-issue]');
518
608
  if (!row) return;
519
609
  const item = this.#issues[Number(row.getAttribute('data-issue'))];
520
- if (item && typeof item.onSelect === 'function') item.onSelect();
610
+ if (!item) return;
611
+ const many = Array.isArray(item.targets) && item.targets.length > 1;
612
+ if (many) {
613
+ const key = item.key || item.text;
614
+ if (this.#openCauses.has(key)) this.#openCauses.delete(key);
615
+ else this.#openCauses.add(key);
616
+ this.setIssues(this.#issues.concat(this.#issueNotes));
617
+ return;
618
+ }
619
+ const only = item.targets?.[0];
620
+ if (only && typeof only.onSelect === 'function') only.onSelect();
621
+ else if (typeof item.onSelect === 'function') item.onSelect();
521
622
  });
522
623
 
523
624
  this.#root.querySelector('.bl-detail')?.addEventListener('click', (event) => {
@@ -551,6 +652,8 @@ export class UIOverlay {
551
652
  this.#hotkeySig = '';
552
653
  this.#detail = [];
553
654
  this.#issues = [];
655
+ this.#issueNotes = [];
656
+ this.#openCauses.clear();
554
657
  this.#clearFlashTimers();
555
658
  this.#flashAt.clear();
556
659
  return this;
@@ -576,9 +679,11 @@ export class UIOverlay {
576
679
  const list = Array.isArray(items) ? items : [];
577
680
  this.#detail = list;
578
681
  const box = this.#els.detail;
682
+ const section = this.#els.inspect;
579
683
  if (!list.length) {
580
684
  box.innerHTML = '';
581
685
  box.hidden = true;
686
+ if (section) section.hidden = true;
582
687
  return this;
583
688
  }
584
689
  let html = '';
@@ -587,30 +692,58 @@ export class UIOverlay {
587
692
  html += `<button type="button" data-detail="${i}" data-level="${level}">${escapeHtml(list[i].text)}</button>`;
588
693
  }
589
694
  box.hidden = false;
695
+ if (section) section.hidden = false;
590
696
  box.innerHTML = html;
591
697
  return this;
592
698
  }
593
699
 
594
700
  /**
595
- * Red warnings pinned above the scrolling console. Newest first.
596
- * @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
701
+ * Cause rows plus grey info notes. Bugs are red and expand; info stays grey.
702
+ * @param {Array<{ text: string, level?: string, key?: string, onSelect?: () => void, targets?: Array<{ text: string, onSelect?: () => void }> }>} items
597
703
  */
598
704
  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) {
705
+ if (!this.#els?.issues || !this.#els.issuesList || !this.#els.notes) return this;
706
+ const list = Array.isArray(items) ? items : [];
707
+ const bugs = [];
708
+ const notes = [];
709
+ for (let i = 0; i < list.length; i++) {
710
+ if (list[i].level === 'info') notes.push(list[i]);
711
+ else bugs.push(list[i]);
712
+ }
713
+ const capped = bugs.slice(0, 10);
714
+ this.#issues = capped;
715
+ this.#issueNotes = notes;
716
+ const section = this.#els.issues;
717
+ if (!capped.length && !notes.length) {
604
718
  this.#els.issuesList.innerHTML = '';
605
- box.hidden = true;
719
+ this.#els.notes.innerHTML = '';
720
+ section.hidden = true;
606
721
  return this;
607
722
  }
608
723
  let html = '';
609
- for (let i = 0; i < list.length; i++) {
610
- html += `<button type="button" data-issue="${i}">${escapeHtml(list[i].text)}</button>`;
724
+ for (let i = 0; i < capped.length; i++) {
725
+ const item = capped[i];
726
+ const targets = Array.isArray(item.targets) ? item.targets : [];
727
+ const many = targets.length > 1;
728
+ const open = many && this.#openCauses.has(item.key || item.text);
729
+ html += `<div class="bl-cause" data-open="${open ? 'true' : 'false'}">`;
730
+ html += `<button type="button" data-issue="${i}">${many ? (open ? '▾ ' : '▸ ') : ''}${escapeHtml(item.text)}</button>`;
731
+ if (many) {
732
+ html += `<div class="bl-cause-kids"${open ? '' : ' hidden'}>`;
733
+ for (let j = 0; j < targets.length; j++) {
734
+ html += `<button type="button" data-issue="${i}" data-target="${j}">${escapeHtml(targets[j].text)}</button>`;
735
+ }
736
+ html += '</div>';
737
+ }
738
+ html += '</div>';
611
739
  }
612
- box.hidden = false;
613
740
  this.#els.issuesList.innerHTML = html;
741
+ let noteHtml = '';
742
+ for (let i = 0; i < notes.length; i++) {
743
+ noteHtml += `<div class="bl-note">${escapeHtml(notes[i].text)}</div>`;
744
+ }
745
+ this.#els.notes.innerHTML = noteHtml;
746
+ section.hidden = false;
614
747
  return this;
615
748
  }
616
749
 
package/src/index.d.ts CHANGED
@@ -185,6 +185,8 @@ export interface DetailItem {
185
185
  text: string;
186
186
  level?: 'info' | 'warn' | 'error';
187
187
  onSelect?: () => void;
188
+ key?: string;
189
+ targets?: Array<{ text: string; onSelect?: () => void }>;
188
190
  }
189
191
 
190
192
  export class UIOverlay {