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 +3 -2
- package/package.json +1 -1
- package/src/adapters/CSSAdapter.js +181 -25
- package/src/adapters/WebDOMAdapter.js +32 -9
- package/src/core/BeeLadybugCore.js +16 -1
- package/src/core/UIOverlay.js +68 -0
- package/src/index.d.ts +8 -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.
|
|
@@ -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
|
|
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
|
@@ -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
|
|
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)
|
|
150
|
-
|
|
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(
|
|
174
|
+
value: String(shown.length)
|
|
161
175
|
});
|
|
162
|
-
if (
|
|
176
|
+
if (shown.length || isVerbose(this.core)) {
|
|
163
177
|
this.core?.sendData('warn', {
|
|
164
178
|
source: this.source,
|
|
165
|
-
message:
|
|
166
|
-
? `scan ${
|
|
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,
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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;
|
|
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:
|
|
667
|
+
score: gap * 100,
|
|
583
668
|
kind: 'distorted image',
|
|
584
|
-
text: `distorted image ${Math.round(
|
|
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.
|
|
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.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;
|
package/src/core/UIOverlay.js
CHANGED
|
@@ -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;
|