bee-ladybug 0.5.0 → 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 +4 -3
- package/package.json +1 -1
- package/src/adapters/CSSAdapter.js +335 -53
- 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,7 +134,8 @@ 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
|
-
| `
|
|
137
|
+
| `P` | blocca il pannello CSS sull'elemento corrente, solo con la hunt accesa |
|
|
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.
|
|
140
141
|
|
|
@@ -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
|
|
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,
|
|
@@ -112,43 +124,63 @@ export class CSSAdapter {
|
|
|
112
124
|
return this;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
/**
|
|
127
|
+
/** Page scan. Click a row to outline one element of that group. */
|
|
116
128
|
scan() {
|
|
117
129
|
if (!this.#active || typeof document === 'undefined') return this;
|
|
118
130
|
if (this.#sheetsDirty) this.#rebuildSheets();
|
|
119
131
|
const nodes = document.body ? document.body.querySelectorAll('*') : [];
|
|
120
|
-
const
|
|
132
|
+
const overflow = [];
|
|
133
|
+
const rows = [];
|
|
121
134
|
for (let i = 0; i < nodes.length; i++) {
|
|
122
135
|
const el = nodes[i];
|
|
123
136
|
if (!el || isBeeChrome(el) || isSkippedTag(el)) continue;
|
|
137
|
+
const measured = measureViewportOverflow(el);
|
|
138
|
+
if (measured) overflow.push(measured);
|
|
124
139
|
const findings = [];
|
|
125
|
-
|
|
140
|
+
pushNegativeMargins(el, findings);
|
|
141
|
+
pushInvisible(el, findings);
|
|
142
|
+
pushBlockers(el, findings);
|
|
126
143
|
pushImage(el, findings);
|
|
127
|
-
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
pushImportant(el, this.#rules, findings);
|
|
145
|
+
for (let j = 0; j < findings.length; j++) {
|
|
146
|
+
rows.push({ el, selector: cssPath(el), ...findings[j] });
|
|
147
|
+
}
|
|
131
148
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
const causes = keepOverflowCauses(overflow);
|
|
150
|
+
for (let i = 0; i < causes.length; i++) {
|
|
151
|
+
const cause = causes[i];
|
|
152
|
+
rows.push({
|
|
153
|
+
el: cause.el,
|
|
154
|
+
selector: cssPath(cause.el),
|
|
155
|
+
kind: 'overflows viewport',
|
|
156
|
+
rank: 0,
|
|
157
|
+
level: 'error',
|
|
158
|
+
score: cause.score
|
|
159
|
+
});
|
|
160
|
+
}
|
|
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) => ({
|
|
165
|
+
level: group.level,
|
|
166
|
+
text: formatIssue(group),
|
|
167
|
+
onSelect: () => this.#pin(group.el)
|
|
138
168
|
}));
|
|
139
169
|
this.#lastEl = null;
|
|
140
170
|
this.core?.publishDetail(items);
|
|
141
171
|
this.core?.sendData('state', {
|
|
142
172
|
source: this.source,
|
|
143
173
|
key: 'css.scan',
|
|
144
|
-
value: String(
|
|
145
|
-
});
|
|
146
|
-
this.core?.sendData('warn', {
|
|
147
|
-
source: this.source,
|
|
148
|
-
message: top.length
|
|
149
|
-
? `scan ${top.length} offender(s). Click a row to outline it.`
|
|
150
|
-
: 'scan clean: no overflow or distorted images.'
|
|
174
|
+
value: String(shown.length)
|
|
151
175
|
});
|
|
176
|
+
if (shown.length || isVerbose(this.core)) {
|
|
177
|
+
this.core?.sendData('warn', {
|
|
178
|
+
source: this.source,
|
|
179
|
+
message: shown.length
|
|
180
|
+
? `scan ${shown.length} issue(s). Click a row to outline it.`
|
|
181
|
+
: 'scan clean: no causes found.'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
152
184
|
return this;
|
|
153
185
|
}
|
|
154
186
|
|
|
@@ -175,7 +207,7 @@ export class CSSAdapter {
|
|
|
175
207
|
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'on' });
|
|
176
208
|
this.core?.sendData('log', {
|
|
177
209
|
source: this.source,
|
|
178
|
-
message: 'CSS hunt on. Hover an element,
|
|
210
|
+
message: 'CSS hunt on. Hover an element, P pins the panel, S scans.'
|
|
179
211
|
});
|
|
180
212
|
}
|
|
181
213
|
|
|
@@ -194,17 +226,27 @@ export class CSSAdapter {
|
|
|
194
226
|
this.#observer = null;
|
|
195
227
|
this.#lastEl = null;
|
|
196
228
|
this.#pinned = null;
|
|
229
|
+
this.#panelLock = false;
|
|
230
|
+
this.#issues = [];
|
|
231
|
+
this.#issueLog.clear();
|
|
197
232
|
this.#unmountBoxes();
|
|
198
233
|
this.core?.publishDetail([]);
|
|
234
|
+
this.core?.publishIssues([]);
|
|
199
235
|
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'off' });
|
|
200
236
|
this.core?.sendData('log', { source: this.source, message: 'CSS hunt off.' });
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
#handleKey(event) {
|
|
204
240
|
if (!this.#active || event.repeat) return;
|
|
205
|
-
if (event.code !== this.scanKey) return;
|
|
206
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;
|
|
207
248
|
event.preventDefault();
|
|
249
|
+
this.#panelLock = false;
|
|
208
250
|
this.scan();
|
|
209
251
|
}
|
|
210
252
|
|
|
@@ -234,6 +276,7 @@ export class CSSAdapter {
|
|
|
234
276
|
#inspect(x, y) {
|
|
235
277
|
const el = pickElement(x, y);
|
|
236
278
|
placeBox(this.#hoverBox, el);
|
|
279
|
+
if (this.#panelLock) return;
|
|
237
280
|
if (el === this.#lastEl) return;
|
|
238
281
|
this.#lastEl = el;
|
|
239
282
|
if (!el) {
|
|
@@ -266,15 +309,80 @@ export class CSSAdapter {
|
|
|
266
309
|
text: `${path} ${hit.text}`,
|
|
267
310
|
onSelect: () => this.#pin(el)
|
|
268
311
|
})));
|
|
312
|
+
this.#stickFindings(el, path, findings);
|
|
269
313
|
if (!findings.length) {
|
|
270
|
-
this.core
|
|
314
|
+
if (isVerbose(this.core)) {
|
|
315
|
+
this.core?.sendData('log', { source: this.source, message: `${path} clean` });
|
|
316
|
+
}
|
|
271
317
|
return;
|
|
272
318
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
})));
|
|
278
386
|
}
|
|
279
387
|
|
|
280
388
|
#pin(el) {
|
|
@@ -380,7 +488,9 @@ function pushImportant(el, rules, findings) {
|
|
|
380
488
|
const prop = rule.props[j];
|
|
381
489
|
findings.push({
|
|
382
490
|
level: 'warn',
|
|
491
|
+
rank: 4,
|
|
383
492
|
score: 5,
|
|
493
|
+
kind: `!important ${rule.selector} { ${prop.property}: ${prop.value} }`,
|
|
384
494
|
text: `!important ${rule.selector} { ${prop.property}: ${prop.value} } @ ${rule.source}`
|
|
385
495
|
});
|
|
386
496
|
}
|
|
@@ -388,19 +498,75 @@ function pushImportant(el, rules, findings) {
|
|
|
388
498
|
}
|
|
389
499
|
|
|
390
500
|
function pushOverflow(el, findings) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
? `left ${Math.round(rect.left)}px`
|
|
397
|
-
: `right +${Math.round(rect.right - view)}px`;
|
|
501
|
+
const measured = measureViewportOverflow(el);
|
|
502
|
+
if (measured) {
|
|
503
|
+
const past = measured.left > 1
|
|
504
|
+
? `left ${Math.round(-measured.left)}px`
|
|
505
|
+
: `right +${Math.round(measured.right)}px`;
|
|
398
506
|
findings.push({
|
|
399
507
|
level: 'error',
|
|
400
|
-
|
|
508
|
+
rank: 0,
|
|
509
|
+
score: measured.score,
|
|
510
|
+
kind: 'overflows viewport',
|
|
401
511
|
text: `overflows viewport (${past})`
|
|
402
512
|
});
|
|
403
513
|
}
|
|
514
|
+
pushNegativeMargins(el, findings);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function measureViewportOverflow(el) {
|
|
518
|
+
if (!el || typeof el.getBoundingClientRect !== 'function') return null;
|
|
519
|
+
if (typeof document === 'undefined') return null;
|
|
520
|
+
if (el === document.documentElement || el === document.body) return null;
|
|
521
|
+
const rect = el.getBoundingClientRect();
|
|
522
|
+
const view = document.documentElement.clientWidth;
|
|
523
|
+
const left = rect.left < -1 ? -rect.left : 0;
|
|
524
|
+
const right = rect.right > view + 1 ? rect.right - view : 0;
|
|
525
|
+
if (left <= 1 && right <= 1) return null;
|
|
526
|
+
return { el, left, right, width: rect.width, view, score: Math.max(left, right) };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Keep the node that actually sticks out.
|
|
531
|
+
* Drop a parent when a descendant overflows by the same amount (±2px),
|
|
532
|
+
* and drop a box whose own width fits the viewport when a child is the one overflowing.
|
|
533
|
+
* @param {Array<{ el: Element, left: number, right: number, width: number, view: number, score: number }>} items
|
|
534
|
+
*/
|
|
535
|
+
export function keepOverflowCauses(items) {
|
|
536
|
+
return items.filter((item) => !echoedByDescendant(item, items) && !fitsOnlyBecauseOfChild(item, items));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function echoedByDescendant(item, items) {
|
|
540
|
+
for (let i = 0; i < items.length; i++) {
|
|
541
|
+
const other = items[i];
|
|
542
|
+
if (other.el === item.el || typeof item.el.contains !== 'function' || !item.el.contains(other.el)) continue;
|
|
543
|
+
const leftEcho = item.left <= 1 || Math.abs(item.left - other.left) <= 2;
|
|
544
|
+
const rightEcho = item.right <= 1 || Math.abs(item.right - other.right) <= 2;
|
|
545
|
+
const matched = (item.left > 1 && Math.abs(item.left - other.left) <= 2)
|
|
546
|
+
|| (item.right > 1 && Math.abs(item.right - other.right) <= 2);
|
|
547
|
+
if (matched && leftEcho && rightEcho) return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function fitsOnlyBecauseOfChild(item, items) {
|
|
553
|
+
if (item.width > item.view + 2) return false;
|
|
554
|
+
if (item.left <= 1 && item.right <= 1) return false;
|
|
555
|
+
let leftOk = item.left <= 1;
|
|
556
|
+
let rightOk = item.right <= 1;
|
|
557
|
+
let childOverflows = false;
|
|
558
|
+
for (let i = 0; i < items.length; i++) {
|
|
559
|
+
const other = items[i];
|
|
560
|
+
if (other.el === item.el || typeof item.el.contains !== 'function' || !item.el.contains(other.el)) continue;
|
|
561
|
+
if (other.left > 1 || other.right > 1) childOverflows = true;
|
|
562
|
+
if (item.left > 1 && other.left + 2 >= item.left) leftOk = true;
|
|
563
|
+
if (item.right > 1 && other.right + 2 >= item.right) rightOk = true;
|
|
564
|
+
}
|
|
565
|
+
return childOverflows && leftOk && rightOk;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function pushNegativeMargins(el, findings) {
|
|
569
|
+
if (typeof getComputedStyle !== 'function') return;
|
|
404
570
|
const style = getComputedStyle(el);
|
|
405
571
|
const sides = ['top', 'right', 'bottom', 'left'];
|
|
406
572
|
for (let i = 0; i < sides.length; i++) {
|
|
@@ -409,26 +575,64 @@ function pushOverflow(el, findings) {
|
|
|
409
575
|
if (!Number.isFinite(value) || value >= 0) continue;
|
|
410
576
|
findings.push({
|
|
411
577
|
level: 'warn',
|
|
578
|
+
rank: 5,
|
|
412
579
|
score: Math.abs(value),
|
|
580
|
+
kind: `negative margin margin-${sides[i]}: ${raw.trim()}`,
|
|
413
581
|
text: `negative margin margin-${sides[i]}: ${raw.trim()}`
|
|
414
582
|
});
|
|
415
583
|
}
|
|
416
584
|
}
|
|
417
585
|
|
|
586
|
+
const ISSUE_RANK = Object.freeze({
|
|
587
|
+
overflow: 0,
|
|
588
|
+
blocker: 1,
|
|
589
|
+
invisible: 2,
|
|
590
|
+
image: 3,
|
|
591
|
+
important: 4
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* @param {Array<{ selector: string, kind: string, rank: number, level: string, score: number, el?: Element }>} rows
|
|
596
|
+
*/
|
|
597
|
+
export function groupIssues(rows) {
|
|
598
|
+
const map = new Map();
|
|
599
|
+
for (let i = 0; i < rows.length; i++) {
|
|
600
|
+
const row = rows[i];
|
|
601
|
+
const kind = row.kind || row.text;
|
|
602
|
+
const key = `${row.selector}\0${kind}`;
|
|
603
|
+
const prev = map.get(key);
|
|
604
|
+
if (!prev) {
|
|
605
|
+
map.set(key, { ...row, kind, count: 1 });
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
prev.count += 1;
|
|
609
|
+
if (row.score > prev.score) {
|
|
610
|
+
prev.score = row.score;
|
|
611
|
+
prev.el = row.el;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return [...map.values()].sort((a, b) => a.rank - b.rank || b.score - a.score || b.count - a.count);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function formatIssue(group) {
|
|
618
|
+
const count = group.count > 1 ? ` ×${group.count}` : '';
|
|
619
|
+
return `${group.selector} ${group.kind}${count}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
418
622
|
function pushInvisible(el, findings) {
|
|
419
623
|
if (!hasOwnText(el)) return;
|
|
420
624
|
const style = getComputedStyle(el);
|
|
421
625
|
if (opacityZero(el)) {
|
|
422
|
-
findings.push(
|
|
626
|
+
findings.push(issue('invisible text (opacity 0)', ISSUE_RANK.invisible, 30));
|
|
423
627
|
}
|
|
424
628
|
const color = parseColor(style.color);
|
|
425
629
|
if (!color || color.a === 0) {
|
|
426
|
-
findings.push(
|
|
630
|
+
findings.push(issue('invisible text (transparent color)', ISSUE_RANK.invisible, 30));
|
|
427
631
|
return;
|
|
428
632
|
}
|
|
429
633
|
const behind = paintBehind(el);
|
|
430
634
|
if (behind && nearlySame(color, behind)) {
|
|
431
|
-
findings.push(
|
|
635
|
+
findings.push(issue('invisible text (color matches background)', ISSUE_RANK.invisible, 30));
|
|
432
636
|
}
|
|
433
637
|
}
|
|
434
638
|
|
|
@@ -440,33 +644,111 @@ function pushBlockers(el, findings) {
|
|
|
440
644
|
if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) return;
|
|
441
645
|
const pseudo = coveringPseudo(x, y);
|
|
442
646
|
if (pseudo) {
|
|
443
|
-
findings.push(
|
|
647
|
+
findings.push(issue(`click blocked by ${pseudo}`, ISSUE_RANK.blocker, 40));
|
|
444
648
|
}
|
|
445
649
|
const hit = document.elementFromPoint(x, y);
|
|
446
650
|
if (!hit || isBeeChrome(hit) || hit === el || el.contains(hit)) return;
|
|
447
651
|
if (pseudo && (hit === document.documentElement || hit === document.body)) return;
|
|
448
|
-
findings.push(
|
|
652
|
+
findings.push(issue(`click blocked by ${cssPath(hit)}`, ISSUE_RANK.blocker, 40));
|
|
449
653
|
}
|
|
450
654
|
|
|
451
655
|
function pushImage(el, findings) {
|
|
452
656
|
if (!el || el.tagName !== 'IMG') return;
|
|
453
657
|
const naturalW = el.naturalWidth;
|
|
454
658
|
const naturalH = el.naturalHeight;
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
const diff = Math.abs(natural - shown) / natural;
|
|
461
|
-
const fit = getComputedStyle(el).objectFit;
|
|
462
|
-
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);
|
|
463
664
|
findings.push({
|
|
464
665
|
level: 'error',
|
|
465
|
-
|
|
466
|
-
|
|
666
|
+
rank: ISSUE_RANK.image,
|
|
667
|
+
score: gap * 100,
|
|
668
|
+
kind: 'distorted image',
|
|
669
|
+
text: `distorted image ${Math.round(gap * 100)}% (object-fit: fill)`
|
|
467
670
|
});
|
|
468
671
|
}
|
|
469
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
|
+
|
|
744
|
+
function issue(kind, rank, score) {
|
|
745
|
+
return { level: 'error', rank, score, kind, text: kind };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function isVerbose(core) {
|
|
749
|
+
return Boolean(core) && !core.frozen && core.timeScale !== 1;
|
|
750
|
+
}
|
|
751
|
+
|
|
470
752
|
function coveringPseudo(x, y) {
|
|
471
753
|
const hosts = [document.documentElement, document.body];
|
|
472
754
|
const names = ['::before', '::after'];
|
|
@@ -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;
|