bee-ladybug 0.5.0 → 0.5.1
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 +2 -2
- package/package.json +1 -1
- package/src/adapters/CSSAdapter.js +160 -34
- package/src/core/BeeLadybugCore.js +1 -1
package/README.MD
CHANGED
|
@@ -134,7 +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
|
-
| `S` | scansione
|
|
137
|
+
| `S` | scansione CSS, solo con la hunt accesa |
|
|
138
138
|
|
|
139
139
|
Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
|
|
140
140
|
|
|
@@ -200,7 +200,7 @@ css.attach();
|
|
|
200
200
|
- chi intercetta il click al centro, inclusi `::before` / `::after` fissi su `html` / `body`
|
|
201
201
|
- `<img>` con rapporto stirato oltre il 5% e `object-fit: fill`
|
|
202
202
|
|
|
203
|
-
`S` con la hunt accesa
|
|
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
204
|
|
|
205
205
|
Demo: `examples/css-hunt.html`.
|
|
206
206
|
|
package/package.json
CHANGED
|
@@ -112,43 +112,61 @@ export class CSSAdapter {
|
|
|
112
112
|
return this;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/**
|
|
115
|
+
/** Page scan. Click a row to outline one element of that group. */
|
|
116
116
|
scan() {
|
|
117
117
|
if (!this.#active || typeof document === 'undefined') return this;
|
|
118
118
|
if (this.#sheetsDirty) this.#rebuildSheets();
|
|
119
119
|
const nodes = document.body ? document.body.querySelectorAll('*') : [];
|
|
120
|
-
const
|
|
120
|
+
const overflow = [];
|
|
121
|
+
const rows = [];
|
|
121
122
|
for (let i = 0; i < nodes.length; i++) {
|
|
122
123
|
const el = nodes[i];
|
|
123
124
|
if (!el || isBeeChrome(el) || isSkippedTag(el)) continue;
|
|
125
|
+
const measured = measureViewportOverflow(el);
|
|
126
|
+
if (measured) overflow.push(measured);
|
|
124
127
|
const findings = [];
|
|
125
|
-
|
|
128
|
+
pushNegativeMargins(el, findings);
|
|
129
|
+
pushInvisible(el, findings);
|
|
130
|
+
pushBlockers(el, findings);
|
|
126
131
|
pushImage(el, findings);
|
|
127
|
-
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
pushImportant(el, this.#rules, findings);
|
|
133
|
+
for (let j = 0; j < findings.length; j++) {
|
|
134
|
+
rows.push({ el, selector: cssPath(el), ...findings[j] });
|
|
135
|
+
}
|
|
131
136
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
const causes = keepOverflowCauses(overflow);
|
|
138
|
+
for (let i = 0; i < causes.length; i++) {
|
|
139
|
+
const cause = causes[i];
|
|
140
|
+
rows.push({
|
|
141
|
+
el: cause.el,
|
|
142
|
+
selector: cssPath(cause.el),
|
|
143
|
+
kind: 'overflows viewport',
|
|
144
|
+
rank: 0,
|
|
145
|
+
level: 'error',
|
|
146
|
+
score: cause.score
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const groups = groupIssues(rows).slice(0, this.maxScan);
|
|
150
|
+
const items = groups.map((group) => ({
|
|
151
|
+
level: group.level,
|
|
152
|
+
text: formatIssue(group),
|
|
153
|
+
onSelect: () => this.#pin(group.el)
|
|
138
154
|
}));
|
|
139
155
|
this.#lastEl = null;
|
|
140
156
|
this.core?.publishDetail(items);
|
|
141
157
|
this.core?.sendData('state', {
|
|
142
158
|
source: this.source,
|
|
143
159
|
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.'
|
|
160
|
+
value: String(groups.length)
|
|
151
161
|
});
|
|
162
|
+
if (groups.length || isVerbose(this.core)) {
|
|
163
|
+
this.core?.sendData('warn', {
|
|
164
|
+
source: this.source,
|
|
165
|
+
message: groups.length
|
|
166
|
+
? `scan ${groups.length} issue(s). Click a row to outline it.`
|
|
167
|
+
: 'scan clean: no causes found.'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
152
170
|
return this;
|
|
153
171
|
}
|
|
154
172
|
|
|
@@ -267,7 +285,9 @@ export class CSSAdapter {
|
|
|
267
285
|
onSelect: () => this.#pin(el)
|
|
268
286
|
})));
|
|
269
287
|
if (!findings.length) {
|
|
270
|
-
this.core
|
|
288
|
+
if (isVerbose(this.core)) {
|
|
289
|
+
this.core?.sendData('log', { source: this.source, message: `${path} clean` });
|
|
290
|
+
}
|
|
271
291
|
return;
|
|
272
292
|
}
|
|
273
293
|
const lead = findings.find((hit) => hit.level === 'error') || findings[0];
|
|
@@ -380,7 +400,9 @@ function pushImportant(el, rules, findings) {
|
|
|
380
400
|
const prop = rule.props[j];
|
|
381
401
|
findings.push({
|
|
382
402
|
level: 'warn',
|
|
403
|
+
rank: 4,
|
|
383
404
|
score: 5,
|
|
405
|
+
kind: `!important ${rule.selector} { ${prop.property}: ${prop.value} }`,
|
|
384
406
|
text: `!important ${rule.selector} { ${prop.property}: ${prop.value} } @ ${rule.source}`
|
|
385
407
|
});
|
|
386
408
|
}
|
|
@@ -388,19 +410,75 @@ function pushImportant(el, rules, findings) {
|
|
|
388
410
|
}
|
|
389
411
|
|
|
390
412
|
function pushOverflow(el, findings) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
? `left ${Math.round(rect.left)}px`
|
|
397
|
-
: `right +${Math.round(rect.right - view)}px`;
|
|
413
|
+
const measured = measureViewportOverflow(el);
|
|
414
|
+
if (measured) {
|
|
415
|
+
const past = measured.left > 1
|
|
416
|
+
? `left ${Math.round(-measured.left)}px`
|
|
417
|
+
: `right +${Math.round(measured.right)}px`;
|
|
398
418
|
findings.push({
|
|
399
419
|
level: 'error',
|
|
400
|
-
|
|
420
|
+
rank: 0,
|
|
421
|
+
score: measured.score,
|
|
422
|
+
kind: 'overflows viewport',
|
|
401
423
|
text: `overflows viewport (${past})`
|
|
402
424
|
});
|
|
403
425
|
}
|
|
426
|
+
pushNegativeMargins(el, findings);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function measureViewportOverflow(el) {
|
|
430
|
+
if (!el || typeof el.getBoundingClientRect !== 'function') return null;
|
|
431
|
+
if (typeof document === 'undefined') return null;
|
|
432
|
+
if (el === document.documentElement || el === document.body) return null;
|
|
433
|
+
const rect = el.getBoundingClientRect();
|
|
434
|
+
const view = document.documentElement.clientWidth;
|
|
435
|
+
const left = rect.left < -1 ? -rect.left : 0;
|
|
436
|
+
const right = rect.right > view + 1 ? rect.right - view : 0;
|
|
437
|
+
if (left <= 1 && right <= 1) return null;
|
|
438
|
+
return { el, left, right, width: rect.width, view, score: Math.max(left, right) };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Keep the node that actually sticks out.
|
|
443
|
+
* Drop a parent when a descendant overflows by the same amount (±2px),
|
|
444
|
+
* and drop a box whose own width fits the viewport when a child is the one overflowing.
|
|
445
|
+
* @param {Array<{ el: Element, left: number, right: number, width: number, view: number, score: number }>} items
|
|
446
|
+
*/
|
|
447
|
+
export function keepOverflowCauses(items) {
|
|
448
|
+
return items.filter((item) => !echoedByDescendant(item, items) && !fitsOnlyBecauseOfChild(item, items));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function echoedByDescendant(item, items) {
|
|
452
|
+
for (let i = 0; i < items.length; i++) {
|
|
453
|
+
const other = items[i];
|
|
454
|
+
if (other.el === item.el || typeof item.el.contains !== 'function' || !item.el.contains(other.el)) continue;
|
|
455
|
+
const leftEcho = item.left <= 1 || Math.abs(item.left - other.left) <= 2;
|
|
456
|
+
const rightEcho = item.right <= 1 || Math.abs(item.right - other.right) <= 2;
|
|
457
|
+
const matched = (item.left > 1 && Math.abs(item.left - other.left) <= 2)
|
|
458
|
+
|| (item.right > 1 && Math.abs(item.right - other.right) <= 2);
|
|
459
|
+
if (matched && leftEcho && rightEcho) return true;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function fitsOnlyBecauseOfChild(item, items) {
|
|
465
|
+
if (item.width > item.view + 2) return false;
|
|
466
|
+
if (item.left <= 1 && item.right <= 1) return false;
|
|
467
|
+
let leftOk = item.left <= 1;
|
|
468
|
+
let rightOk = item.right <= 1;
|
|
469
|
+
let childOverflows = false;
|
|
470
|
+
for (let i = 0; i < items.length; i++) {
|
|
471
|
+
const other = items[i];
|
|
472
|
+
if (other.el === item.el || typeof item.el.contains !== 'function' || !item.el.contains(other.el)) continue;
|
|
473
|
+
if (other.left > 1 || other.right > 1) childOverflows = true;
|
|
474
|
+
if (item.left > 1 && other.left + 2 >= item.left) leftOk = true;
|
|
475
|
+
if (item.right > 1 && other.right + 2 >= item.right) rightOk = true;
|
|
476
|
+
}
|
|
477
|
+
return childOverflows && leftOk && rightOk;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function pushNegativeMargins(el, findings) {
|
|
481
|
+
if (typeof getComputedStyle !== 'function') return;
|
|
404
482
|
const style = getComputedStyle(el);
|
|
405
483
|
const sides = ['top', 'right', 'bottom', 'left'];
|
|
406
484
|
for (let i = 0; i < sides.length; i++) {
|
|
@@ -409,26 +487,64 @@ function pushOverflow(el, findings) {
|
|
|
409
487
|
if (!Number.isFinite(value) || value >= 0) continue;
|
|
410
488
|
findings.push({
|
|
411
489
|
level: 'warn',
|
|
490
|
+
rank: 5,
|
|
412
491
|
score: Math.abs(value),
|
|
492
|
+
kind: `negative margin margin-${sides[i]}: ${raw.trim()}`,
|
|
413
493
|
text: `negative margin margin-${sides[i]}: ${raw.trim()}`
|
|
414
494
|
});
|
|
415
495
|
}
|
|
416
496
|
}
|
|
417
497
|
|
|
498
|
+
const ISSUE_RANK = Object.freeze({
|
|
499
|
+
overflow: 0,
|
|
500
|
+
blocker: 1,
|
|
501
|
+
invisible: 2,
|
|
502
|
+
image: 3,
|
|
503
|
+
important: 4
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* @param {Array<{ selector: string, kind: string, rank: number, level: string, score: number, el?: Element }>} rows
|
|
508
|
+
*/
|
|
509
|
+
export function groupIssues(rows) {
|
|
510
|
+
const map = new Map();
|
|
511
|
+
for (let i = 0; i < rows.length; i++) {
|
|
512
|
+
const row = rows[i];
|
|
513
|
+
const kind = row.kind || row.text;
|
|
514
|
+
const key = `${row.selector}\0${kind}`;
|
|
515
|
+
const prev = map.get(key);
|
|
516
|
+
if (!prev) {
|
|
517
|
+
map.set(key, { ...row, kind, count: 1 });
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
prev.count += 1;
|
|
521
|
+
if (row.score > prev.score) {
|
|
522
|
+
prev.score = row.score;
|
|
523
|
+
prev.el = row.el;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return [...map.values()].sort((a, b) => a.rank - b.rank || b.score - a.score || b.count - a.count);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function formatIssue(group) {
|
|
530
|
+
const count = group.count > 1 ? ` ×${group.count}` : '';
|
|
531
|
+
return `${group.selector} ${group.kind}${count}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
418
534
|
function pushInvisible(el, findings) {
|
|
419
535
|
if (!hasOwnText(el)) return;
|
|
420
536
|
const style = getComputedStyle(el);
|
|
421
537
|
if (opacityZero(el)) {
|
|
422
|
-
findings.push(
|
|
538
|
+
findings.push(issue('invisible text (opacity 0)', ISSUE_RANK.invisible, 30));
|
|
423
539
|
}
|
|
424
540
|
const color = parseColor(style.color);
|
|
425
541
|
if (!color || color.a === 0) {
|
|
426
|
-
findings.push(
|
|
542
|
+
findings.push(issue('invisible text (transparent color)', ISSUE_RANK.invisible, 30));
|
|
427
543
|
return;
|
|
428
544
|
}
|
|
429
545
|
const behind = paintBehind(el);
|
|
430
546
|
if (behind && nearlySame(color, behind)) {
|
|
431
|
-
findings.push(
|
|
547
|
+
findings.push(issue('invisible text (color matches background)', ISSUE_RANK.invisible, 30));
|
|
432
548
|
}
|
|
433
549
|
}
|
|
434
550
|
|
|
@@ -440,12 +556,12 @@ function pushBlockers(el, findings) {
|
|
|
440
556
|
if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) return;
|
|
441
557
|
const pseudo = coveringPseudo(x, y);
|
|
442
558
|
if (pseudo) {
|
|
443
|
-
findings.push(
|
|
559
|
+
findings.push(issue(`click blocked by ${pseudo}`, ISSUE_RANK.blocker, 40));
|
|
444
560
|
}
|
|
445
561
|
const hit = document.elementFromPoint(x, y);
|
|
446
562
|
if (!hit || isBeeChrome(hit) || hit === el || el.contains(hit)) return;
|
|
447
563
|
if (pseudo && (hit === document.documentElement || hit === document.body)) return;
|
|
448
|
-
findings.push(
|
|
564
|
+
findings.push(issue(`click blocked by ${cssPath(hit)}`, ISSUE_RANK.blocker, 40));
|
|
449
565
|
}
|
|
450
566
|
|
|
451
567
|
function pushImage(el, findings) {
|
|
@@ -462,11 +578,21 @@ function pushImage(el, findings) {
|
|
|
462
578
|
if (diff <= 0.05 || fit !== 'fill') return;
|
|
463
579
|
findings.push({
|
|
464
580
|
level: 'error',
|
|
581
|
+
rank: ISSUE_RANK.image,
|
|
465
582
|
score: diff * 100,
|
|
583
|
+
kind: 'distorted image',
|
|
466
584
|
text: `distorted image ${Math.round(diff * 100)}% (object-fit: fill)`
|
|
467
585
|
});
|
|
468
586
|
}
|
|
469
587
|
|
|
588
|
+
function issue(kind, rank, score) {
|
|
589
|
+
return { level: 'error', rank, score, kind, text: kind };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function isVerbose(core) {
|
|
593
|
+
return Boolean(core) && !core.frozen && core.timeScale !== 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
470
596
|
function coveringPseudo(x, y) {
|
|
471
597
|
const hosts = [document.documentElement, document.body];
|
|
472
598
|
const names = ['::before', '::after'];
|
|
@@ -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.1';
|
|
13
13
|
|
|
14
14
|
export const CORE_DEFAULTS = Object.freeze({
|
|
15
15
|
toggleKey: 'F2',
|