bee-ladybug 0.5.1 → 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 +4 -3
- package/package.json +1 -1
- package/src/adapters/CSSAdapter.js +256 -39
- package/src/adapters/WebDOMAdapter.js +32 -9
- package/src/core/BeeLadybugCore.js +16 -1
- package/src/core/UIOverlay.js +205 -4
- package/src/index.d.ts +10 -0
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.
|
|
@@ -194,13 +195,13 @@ css.attach();
|
|
|
194
195
|
|
|
195
196
|
`F6` (pulsante **F6 CSS** accanto a F3/F4) accende la caccia. Sull'elemento sotto il puntatore l'overlay elenca:
|
|
196
197
|
|
|
197
|
-
- regole `!important` che gli corrispondono
|
|
198
|
+
- regole `!important` che gli corrispondono. Un foglio cross-origin è una nota grigia, una volta sola: `unreadable sheet: <href>`
|
|
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
|
|
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. **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.
|
|
204
205
|
|
|
205
206
|
Demo: `examples/css-hunt.html`.
|
|
206
207
|
|
package/package.json
CHANGED
|
@@ -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,19 +33,26 @@ 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;
|
|
38
44
|
this.#rules = [];
|
|
39
|
-
this.#unreadable = [];
|
|
40
45
|
this.#sheetsDirty = true;
|
|
41
46
|
this.#x = 0;
|
|
42
47
|
this.#y = 0;
|
|
43
48
|
this.#rafId = 0;
|
|
44
49
|
this.#lastEl = null;
|
|
45
50
|
this.#pinned = null;
|
|
51
|
+
this.#panelLock = false;
|
|
52
|
+
this.#issues = [];
|
|
53
|
+
this.#notes = [];
|
|
54
|
+
this.#seenSheets = new Set();
|
|
55
|
+
this.#issueLog = new Map();
|
|
46
56
|
this.#hoverBox = null;
|
|
47
57
|
this.#pinBox = null;
|
|
48
58
|
this.#observer = null;
|
|
@@ -63,13 +73,17 @@ export class CSSAdapter {
|
|
|
63
73
|
#attached;
|
|
64
74
|
#active;
|
|
65
75
|
#rules;
|
|
66
|
-
#unreadable;
|
|
67
76
|
#sheetsDirty;
|
|
68
77
|
#x;
|
|
69
78
|
#y;
|
|
70
79
|
#rafId;
|
|
71
80
|
#lastEl;
|
|
72
81
|
#pinned;
|
|
82
|
+
#panelLock;
|
|
83
|
+
#issues;
|
|
84
|
+
#notes;
|
|
85
|
+
#seenSheets;
|
|
86
|
+
#issueLog;
|
|
73
87
|
#hoverBox;
|
|
74
88
|
#pinBox;
|
|
75
89
|
#observer;
|
|
@@ -94,7 +108,7 @@ export class CSSAdapter {
|
|
|
94
108
|
});
|
|
95
109
|
this.core?.sendData('log', {
|
|
96
110
|
source: this.source,
|
|
97
|
-
message: 'CSSAdapter attached. F6 CSS hunt. S scans
|
|
111
|
+
message: 'CSSAdapter attached. F6 CSS hunt. S scans. P pins the panel.'
|
|
98
112
|
});
|
|
99
113
|
this.core?.sendData('state', {
|
|
100
114
|
source: this.source,
|
|
@@ -146,8 +160,14 @@ export class CSSAdapter {
|
|
|
146
160
|
score: cause.score
|
|
147
161
|
});
|
|
148
162
|
}
|
|
149
|
-
const groups = groupIssues(rows)
|
|
150
|
-
|
|
163
|
+
const groups = groupIssues(rows);
|
|
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();
|
|
169
|
+
const shown = groups.slice(0, this.maxScan);
|
|
170
|
+
const items = shown.map((group) => ({
|
|
151
171
|
level: group.level,
|
|
152
172
|
text: formatIssue(group),
|
|
153
173
|
onSelect: () => this.#pin(group.el)
|
|
@@ -157,13 +177,13 @@ export class CSSAdapter {
|
|
|
157
177
|
this.core?.sendData('state', {
|
|
158
178
|
source: this.source,
|
|
159
179
|
key: 'css.scan',
|
|
160
|
-
value: String(
|
|
180
|
+
value: String(shown.length)
|
|
161
181
|
});
|
|
162
|
-
if (
|
|
182
|
+
if (shown.length || isVerbose(this.core)) {
|
|
163
183
|
this.core?.sendData('warn', {
|
|
164
184
|
source: this.source,
|
|
165
|
-
message:
|
|
166
|
-
? `scan ${
|
|
185
|
+
message: shown.length
|
|
186
|
+
? `scan ${shown.length} issue(s). Click a row to outline it.`
|
|
167
187
|
: 'scan clean: no causes found.'
|
|
168
188
|
});
|
|
169
189
|
}
|
|
@@ -193,7 +213,7 @@ export class CSSAdapter {
|
|
|
193
213
|
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'on' });
|
|
194
214
|
this.core?.sendData('log', {
|
|
195
215
|
source: this.source,
|
|
196
|
-
message: 'CSS hunt on. Hover an element,
|
|
216
|
+
message: 'CSS hunt on. Hover an element, P pins the panel, S scans.'
|
|
197
217
|
});
|
|
198
218
|
}
|
|
199
219
|
|
|
@@ -212,17 +232,29 @@ export class CSSAdapter {
|
|
|
212
232
|
this.#observer = null;
|
|
213
233
|
this.#lastEl = null;
|
|
214
234
|
this.#pinned = null;
|
|
235
|
+
this.#panelLock = false;
|
|
236
|
+
this.#issues = [];
|
|
237
|
+
this.#notes = [];
|
|
238
|
+
this.#seenSheets.clear();
|
|
239
|
+
this.#issueLog.clear();
|
|
215
240
|
this.#unmountBoxes();
|
|
216
241
|
this.core?.publishDetail([]);
|
|
242
|
+
this.core?.publishIssues([]);
|
|
217
243
|
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'off' });
|
|
218
244
|
this.core?.sendData('log', { source: this.source, message: 'CSS hunt off.' });
|
|
219
245
|
}
|
|
220
246
|
|
|
221
247
|
#handleKey(event) {
|
|
222
248
|
if (!this.#active || event.repeat) return;
|
|
223
|
-
if (event.code !== this.scanKey) return;
|
|
224
249
|
if (isTypingTarget(event.target)) return;
|
|
250
|
+
if (event.code === this.pinKey) {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
this.#togglePanelPin();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (event.code !== this.scanKey) return;
|
|
225
256
|
event.preventDefault();
|
|
257
|
+
this.#panelLock = false;
|
|
226
258
|
this.scan();
|
|
227
259
|
}
|
|
228
260
|
|
|
@@ -240,18 +272,14 @@ export class CSSAdapter {
|
|
|
240
272
|
this.#sheetsDirty = false;
|
|
241
273
|
const built = collectImportantRules();
|
|
242
274
|
this.#rules = built.rules;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
this.core?.sendData('warn', {
|
|
246
|
-
source: this.source,
|
|
247
|
-
message: `unreadable sheet: ${built.unreadable[i]}`
|
|
248
|
-
});
|
|
249
|
-
}
|
|
275
|
+
for (let i = 0; i < built.unreadable.length; i++) this.#rememberSheet(built.unreadable[i]);
|
|
276
|
+
this.#publishIssues();
|
|
250
277
|
}
|
|
251
278
|
|
|
252
279
|
#inspect(x, y) {
|
|
253
280
|
const el = pickElement(x, y);
|
|
254
281
|
placeBox(this.#hoverBox, el);
|
|
282
|
+
if (this.#panelLock) return;
|
|
255
283
|
if (el === this.#lastEl) return;
|
|
256
284
|
this.#lastEl = el;
|
|
257
285
|
if (!el) {
|
|
@@ -265,13 +293,6 @@ export class CSSAdapter {
|
|
|
265
293
|
pushInvisible(el, findings);
|
|
266
294
|
pushBlockers(el, findings);
|
|
267
295
|
pushImage(el, findings);
|
|
268
|
-
for (let i = 0; i < this.#unreadable.length; i++) {
|
|
269
|
-
findings.push({
|
|
270
|
-
level: 'warn',
|
|
271
|
-
text: `unreadable sheet: ${this.#unreadable[i]}`,
|
|
272
|
-
score: 0
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
296
|
const path = cssPath(el);
|
|
276
297
|
this.core?.sendData('state', { source: this.source, key: 'css.target', value: path });
|
|
277
298
|
this.core?.sendData('state', {
|
|
@@ -284,19 +305,98 @@ export class CSSAdapter {
|
|
|
284
305
|
text: `${path} ${hit.text}`,
|
|
285
306
|
onSelect: () => this.#pin(el)
|
|
286
307
|
})));
|
|
308
|
+
this.#stickFindings(el, path, findings);
|
|
287
309
|
if (!findings.length) {
|
|
288
310
|
if (isVerbose(this.core)) {
|
|
289
311
|
this.core?.sendData('log', { source: this.source, message: `${path} clean` });
|
|
290
312
|
}
|
|
291
313
|
return;
|
|
292
314
|
}
|
|
293
|
-
|
|
294
|
-
|
|
315
|
+
this.#logFindings(path, findings);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#togglePanelPin() {
|
|
319
|
+
if (this.#panelLock) {
|
|
320
|
+
this.#panelLock = false;
|
|
321
|
+
this.#pinned = null;
|
|
322
|
+
placeBox(this.#pinBox, null);
|
|
323
|
+
this.core?.sendData('state', { source: this.source, key: 'css.pin', value: 'off' });
|
|
324
|
+
this.core?.sendData('log', { source: this.source, message: 'CSS pin off.' });
|
|
325
|
+
this.#lastEl = null;
|
|
326
|
+
this.#schedule();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const el = this.#lastEl;
|
|
330
|
+
if (!el) return;
|
|
331
|
+
this.#panelLock = true;
|
|
332
|
+
this.#pin(el);
|
|
333
|
+
const path = cssPath(el);
|
|
334
|
+
this.core?.sendData('state', { source: this.source, key: 'css.pin', value: path });
|
|
335
|
+
this.core?.sendData('log', { source: this.source, message: `CSS pin ${path}` });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#logFindings(path, findings) {
|
|
339
|
+
const now = nowMs();
|
|
340
|
+
for (let i = 0; i < findings.length; i++) {
|
|
341
|
+
const hit = findings[i];
|
|
342
|
+
const text = hit.text || hit.kind || '';
|
|
343
|
+
if (!text || text.startsWith('unreadable sheet:')) continue;
|
|
344
|
+
if (!rememberIssue(this.#issueLog, `${path}\0${text}`, now, this.issueWindow)) continue;
|
|
345
|
+
const level = hit.level === 'error' ? 'error' : 'warn';
|
|
346
|
+
this.core?.sendData(level, {
|
|
347
|
+
source: this.source,
|
|
348
|
+
message: `${path} ${text}`
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#stickFindings(el, path, findings) {
|
|
354
|
+
let changed = false;
|
|
355
|
+
for (let i = 0; i < findings.length; i++) {
|
|
356
|
+
const hit = findings[i];
|
|
357
|
+
if (hit.kind === 'overflows viewport' && !elementCausesOverflow(el)) continue;
|
|
358
|
+
if (this.#rememberCause(el, path, hit.kind)) changed = true;
|
|
359
|
+
}
|
|
360
|
+
if (changed) this.#publishIssues();
|
|
361
|
+
}
|
|
362
|
+
|
|
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', {
|
|
295
376
|
source: this.source,
|
|
296
|
-
|
|
377
|
+
level: 'info',
|
|
378
|
+
message: `unreadable sheet: ${label}`
|
|
297
379
|
});
|
|
298
380
|
}
|
|
299
381
|
|
|
382
|
+
#publishIssues() {
|
|
383
|
+
const bugs = this.#issues.map((entry) => ({
|
|
384
|
+
level: 'error',
|
|
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));
|
|
398
|
+
}
|
|
399
|
+
|
|
300
400
|
#pin(el) {
|
|
301
401
|
this.#pinned = el;
|
|
302
402
|
placeBox(this.#pinBox, el);
|
|
@@ -568,23 +668,140 @@ function pushImage(el, findings) {
|
|
|
568
668
|
if (!el || el.tagName !== 'IMG') return;
|
|
569
669
|
const naturalW = el.naturalWidth;
|
|
570
670
|
const naturalH = el.naturalHeight;
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
const diff = Math.abs(natural - shown) / natural;
|
|
577
|
-
const fit = getComputedStyle(el).objectFit;
|
|
578
|
-
if (diff <= 0.05 || fit !== 'fill') return;
|
|
671
|
+
if (!naturalW || !naturalH || typeof el.getBoundingClientRect !== 'function') return;
|
|
672
|
+
const rect = el.getBoundingClientRect();
|
|
673
|
+
const fit = (typeof getComputedStyle === 'function' ? getComputedStyle(el).objectFit : '') || 'fill';
|
|
674
|
+
if (!isDistortedImage(naturalW, naturalH, rect.width, rect.height, fit)) return;
|
|
675
|
+
const gap = renderedRatioGap(naturalW, naturalH, rect.width, rect.height);
|
|
579
676
|
findings.push({
|
|
580
677
|
level: 'error',
|
|
581
678
|
rank: ISSUE_RANK.image,
|
|
582
|
-
score:
|
|
679
|
+
score: gap * 100,
|
|
583
680
|
kind: 'distorted image',
|
|
584
|
-
text: `distorted image ${Math.round(
|
|
681
|
+
text: `distorted image ${Math.round(gap * 100)}% (object-fit: fill)`
|
|
585
682
|
});
|
|
586
683
|
}
|
|
587
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Rendered box versus the file. Attributes and clientWidth are ignored.
|
|
687
|
+
* `object-fit` defaults to fill. Flag when the ratios differ by more than 5%.
|
|
688
|
+
*/
|
|
689
|
+
export function isDistortedImage(naturalW, naturalH, renderedW, renderedH, objectFit) {
|
|
690
|
+
const fit = objectFit || 'fill';
|
|
691
|
+
if (fit !== 'fill') return false;
|
|
692
|
+
return renderedRatioGap(naturalW, naturalH, renderedW, renderedH) > 0.05;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function renderedRatioGap(naturalW, naturalH, renderedW, renderedH) {
|
|
696
|
+
if (!naturalW || !naturalH || !renderedW || !renderedH) return 0;
|
|
697
|
+
const natural = naturalW / naturalH;
|
|
698
|
+
const rendered = renderedW / renderedH;
|
|
699
|
+
return Math.abs(natural - rendered) / natural;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** @returns {boolean} true when this issue should be written to the event list */
|
|
703
|
+
export function rememberIssue(seen, key, now, windowMs = 5000) {
|
|
704
|
+
const prev = seen.get(key);
|
|
705
|
+
if (typeof prev === 'number' && now - prev < windowMs) return false;
|
|
706
|
+
seen.set(key, now);
|
|
707
|
+
if (seen.size > 64) {
|
|
708
|
+
for (const [name, at] of seen) {
|
|
709
|
+
if (now - at >= windowMs) seen.delete(name);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Newest first, duplicates move to the top, capped at `max`.
|
|
717
|
+
* @param {Array<{ text: string, el?: Element }>} list
|
|
718
|
+
* @param {{ text: string, el?: Element }} entry
|
|
719
|
+
*/
|
|
720
|
+
export function pushStickyIssue(list, entry, max = 10) {
|
|
721
|
+
const next = [entry];
|
|
722
|
+
for (let i = 0; i < list.length; i++) {
|
|
723
|
+
if (list[i].text === entry.text) continue;
|
|
724
|
+
next.push(list[i]);
|
|
725
|
+
if (next.length >= max) break;
|
|
726
|
+
}
|
|
727
|
+
return next.slice(0, max);
|
|
728
|
+
}
|
|
729
|
+
|
|
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;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function elementCausesOverflow(el) {
|
|
784
|
+
const self = measureViewportOverflow(el);
|
|
785
|
+
if (!self) return false;
|
|
786
|
+
const items = [self];
|
|
787
|
+
if (typeof el.querySelectorAll === 'function') {
|
|
788
|
+
const nodes = el.querySelectorAll('*');
|
|
789
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
790
|
+
const measured = measureViewportOverflow(nodes[i]);
|
|
791
|
+
if (measured) items.push(measured);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const causes = keepOverflowCauses(items);
|
|
795
|
+
for (let i = 0; i < causes.length; i++) {
|
|
796
|
+
if (causes[i].el === el) return true;
|
|
797
|
+
}
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function nowMs() {
|
|
802
|
+
return typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
803
|
+
}
|
|
804
|
+
|
|
588
805
|
function issue(kind, rank, score) {
|
|
589
806
|
return { level: 'error', rank, score, kind, text: kind };
|
|
590
807
|
}
|
|
@@ -279,10 +279,12 @@ export class WebDOMAdapter {
|
|
|
279
279
|
|
|
280
280
|
if (key !== this.#lastHoverKey) {
|
|
281
281
|
this.#lastHoverKey = key;
|
|
282
|
-
this.core.
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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.
|
|
12
|
+
export const BEE_LADYBUG_VERSION = '0.5.3';
|
|
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 : [];
|
|
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;
|
package/src/core/UIOverlay.js
CHANGED
|
@@ -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:
|
|
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: #
|
|
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,6 +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; }
|
|
294
|
+
.bl-issues {
|
|
295
|
+
background: #07080c;
|
|
296
|
+
}
|
|
297
|
+
.bl-legend {
|
|
298
|
+
display: flex;
|
|
299
|
+
gap: 14px;
|
|
300
|
+
padding: 4px 10px 2px;
|
|
301
|
+
color: #8b91a3;
|
|
302
|
+
font-size: 10px;
|
|
303
|
+
}
|
|
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;
|
|
311
|
+
}
|
|
312
|
+
.bl-dot.bug { background: #ff3b4e; }
|
|
313
|
+
.bl-dot.info { background: #8b91a3; }
|
|
314
|
+
.bl-cause > button,
|
|
315
|
+
.bl-cause-kids button,
|
|
316
|
+
.bl-note {
|
|
317
|
+
display: block;
|
|
318
|
+
width: 100%;
|
|
319
|
+
margin: 0;
|
|
320
|
+
padding: 3px 10px;
|
|
321
|
+
border: 0;
|
|
322
|
+
background: transparent;
|
|
323
|
+
font: 11px ui-monospace, Consolas, monospace;
|
|
324
|
+
line-height: 1.4;
|
|
325
|
+
text-align: left;
|
|
326
|
+
}
|
|
327
|
+
.bl-cause > button,
|
|
328
|
+
.bl-cause-kids button {
|
|
329
|
+
color: #ff6b7d;
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
}
|
|
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; }
|
|
266
337
|
.bl-detail {
|
|
267
338
|
max-height: 132px;
|
|
268
339
|
overflow: auto;
|
|
@@ -366,6 +437,9 @@ export class UIOverlay {
|
|
|
366
437
|
this.#adapterSig = '';
|
|
367
438
|
this.#hotkeySig = '';
|
|
368
439
|
this.#detail = [];
|
|
440
|
+
this.#issues = [];
|
|
441
|
+
this.#issueNotes = [];
|
|
442
|
+
this.#openCauses = new Set();
|
|
369
443
|
this.#flashAt = new Map();
|
|
370
444
|
this.#flashTimers = new Map();
|
|
371
445
|
}
|
|
@@ -380,6 +454,9 @@ export class UIOverlay {
|
|
|
380
454
|
#adapterSig;
|
|
381
455
|
#hotkeySig;
|
|
382
456
|
#detail;
|
|
457
|
+
#issues;
|
|
458
|
+
#issueNotes;
|
|
459
|
+
#openCauses;
|
|
383
460
|
#flashAt;
|
|
384
461
|
#flashTimers;
|
|
385
462
|
|
|
@@ -416,6 +493,10 @@ export class UIOverlay {
|
|
|
416
493
|
</button>
|
|
417
494
|
</span>
|
|
418
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">
|
|
419
500
|
<div class="bl-stats">
|
|
420
501
|
<div>HOST <b data-el="hostFps">—</b></div>
|
|
421
502
|
<div>CORE <b data-el="coreFps">—</b></div>
|
|
@@ -430,8 +511,32 @@ export class UIOverlay {
|
|
|
430
511
|
</div>
|
|
431
512
|
</div>
|
|
432
513
|
<div class="bl-hud" data-el="hud"></div>
|
|
433
|
-
|
|
434
|
-
|
|
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>
|
|
539
|
+
</div>
|
|
435
540
|
<div class="bl-foot">
|
|
436
541
|
<button class="bl-btn" data-act="slow" type="button">F3 SLOW</button>
|
|
437
542
|
<button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
|
|
@@ -459,6 +564,10 @@ export class UIOverlay {
|
|
|
459
564
|
hud: this.#root.querySelector('[data-el="hud"]'),
|
|
460
565
|
console: this.#root.querySelector('[data-el="console"]'),
|
|
461
566
|
detail: this.#root.querySelector('[data-el="detail"]'),
|
|
567
|
+
inspect: this.#root.querySelector('[data-el="inspect"]'),
|
|
568
|
+
issues: this.#root.querySelector('[data-el="issues"]'),
|
|
569
|
+
issuesList: this.#root.querySelector('[data-el="issues-list"]'),
|
|
570
|
+
notes: this.#root.querySelector('[data-el="notes"]'),
|
|
462
571
|
providers: this.#root.querySelector('[data-el="providers"]'),
|
|
463
572
|
adapters: this.#root.querySelector('[data-el="adapters"]'),
|
|
464
573
|
hotkeys: this.#root.querySelector('[data-el="hotkeys"]')
|
|
@@ -476,6 +585,42 @@ export class UIOverlay {
|
|
|
476
585
|
else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
|
|
477
586
|
});
|
|
478
587
|
|
|
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
|
+
}
|
|
607
|
+
const row = event.target.closest('[data-issue]');
|
|
608
|
+
if (!row) return;
|
|
609
|
+
const item = this.#issues[Number(row.getAttribute('data-issue'))];
|
|
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();
|
|
622
|
+
});
|
|
623
|
+
|
|
479
624
|
this.#root.querySelector('.bl-detail')?.addEventListener('click', (event) => {
|
|
480
625
|
const row = event.target.closest('[data-detail]');
|
|
481
626
|
if (!row) return;
|
|
@@ -506,6 +651,9 @@ export class UIOverlay {
|
|
|
506
651
|
this.#adapterSig = '';
|
|
507
652
|
this.#hotkeySig = '';
|
|
508
653
|
this.#detail = [];
|
|
654
|
+
this.#issues = [];
|
|
655
|
+
this.#issueNotes = [];
|
|
656
|
+
this.#openCauses.clear();
|
|
509
657
|
this.#clearFlashTimers();
|
|
510
658
|
this.#flashAt.clear();
|
|
511
659
|
return this;
|
|
@@ -531,9 +679,11 @@ export class UIOverlay {
|
|
|
531
679
|
const list = Array.isArray(items) ? items : [];
|
|
532
680
|
this.#detail = list;
|
|
533
681
|
const box = this.#els.detail;
|
|
682
|
+
const section = this.#els.inspect;
|
|
534
683
|
if (!list.length) {
|
|
535
684
|
box.innerHTML = '';
|
|
536
685
|
box.hidden = true;
|
|
686
|
+
if (section) section.hidden = true;
|
|
537
687
|
return this;
|
|
538
688
|
}
|
|
539
689
|
let html = '';
|
|
@@ -542,10 +692,61 @@ export class UIOverlay {
|
|
|
542
692
|
html += `<button type="button" data-detail="${i}" data-level="${level}">${escapeHtml(list[i].text)}</button>`;
|
|
543
693
|
}
|
|
544
694
|
box.hidden = false;
|
|
695
|
+
if (section) section.hidden = false;
|
|
545
696
|
box.innerHTML = html;
|
|
546
697
|
return this;
|
|
547
698
|
}
|
|
548
699
|
|
|
700
|
+
/**
|
|
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
|
|
703
|
+
*/
|
|
704
|
+
setIssues(items) {
|
|
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) {
|
|
718
|
+
this.#els.issuesList.innerHTML = '';
|
|
719
|
+
this.#els.notes.innerHTML = '';
|
|
720
|
+
section.hidden = true;
|
|
721
|
+
return this;
|
|
722
|
+
}
|
|
723
|
+
let html = '';
|
|
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>';
|
|
739
|
+
}
|
|
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;
|
|
747
|
+
return this;
|
|
748
|
+
}
|
|
749
|
+
|
|
549
750
|
onPacket(packet) {
|
|
550
751
|
if (!this.#els || !packet) return;
|
|
551
752
|
if (packet.type === 'sys') return;
|
package/src/index.d.ts
CHANGED
|
@@ -178,12 +178,15 @@ 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 {
|
|
184
185
|
text: string;
|
|
185
186
|
level?: 'info' | 'warn' | 'error';
|
|
186
187
|
onSelect?: () => void;
|
|
188
|
+
key?: string;
|
|
189
|
+
targets?: Array<{ text: string; onSelect?: () => void }>;
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
export class UIOverlay {
|
|
@@ -197,6 +200,7 @@ export class UIOverlay {
|
|
|
197
200
|
onClear(channel?: string): void;
|
|
198
201
|
render(): void;
|
|
199
202
|
setDetail(items: DetailItem[]): this;
|
|
203
|
+
setIssues(items: DetailItem[]): this;
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
export interface CanvasEntity {
|
|
@@ -287,8 +291,11 @@ export interface CSSAdapterOptions {
|
|
|
287
291
|
source?: string;
|
|
288
292
|
hotkey?: string;
|
|
289
293
|
scanKey?: string;
|
|
294
|
+
pinKey?: string;
|
|
290
295
|
label?: string;
|
|
291
296
|
maxScan?: number;
|
|
297
|
+
issueWindow?: number;
|
|
298
|
+
maxIssues?: number;
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
export const CSS_ADAPTER_DEFAULTS: Required<CSSAdapterOptions>;
|
|
@@ -298,8 +305,11 @@ export class CSSAdapter {
|
|
|
298
305
|
source: string;
|
|
299
306
|
hotkey: string;
|
|
300
307
|
scanKey: string;
|
|
308
|
+
pinKey: string;
|
|
301
309
|
label: string;
|
|
302
310
|
maxScan: number;
|
|
311
|
+
issueWindow: number;
|
|
312
|
+
maxIssues: number;
|
|
303
313
|
readonly active: boolean;
|
|
304
314
|
constructor(core: BeeLadybugCore, options?: CSSAdapterOptions);
|
|
305
315
|
attach(): this;
|