bee-ladybug 0.6.3 → 0.6.5
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 +9 -0
- package/package.json +1 -1
- package/src/adapters/AntAdapter.js +111 -32
- package/src/adapters/CSSAdapter.js +70 -28
- package/src/adapters/WebDOMAdapter.js +1 -0
- package/src/core/BeeLadybugCore.js +1 -1
- package/src/index.d.ts +1 -0
package/README.MD
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
|
|
2
2
|
# BeeLadybug
|
|
3
|
+
[](https://www.npmjs.com/package/bee-ladybug)
|
|
4
|
+
[](https://www.npmjs.com/package/bee-ladybug)
|
|
5
|
+
[](https://www.npmjs.com/package/bee-ladybug)
|
|
6
|
+
[](https://www.jsdelivr.com/package/npm/bee-ladybug)
|
|
7
|
+

|
|
8
|
+
[](https://badge.socket.dev/npm/package/bee-ladybug/0.6.5)
|
|
9
|
+
[](https://bundlephobia.com/package/bee-ladybug)
|
|
10
|
+
[](https://security.snyk.io/package/npm/bee-ladybug)
|
|
3
11
|
|
|
4
12
|
<p align="center">
|
|
5
13
|
<img src="https://beeenginejs.com/wp-content/uploads/2026/10/beeladybug-logo-animato-ezgif.com-effects.gif" alt="BeeEngine Logo" width="900" />
|
|
6
14
|
</p>
|
|
7
15
|
|
|
16
|
+
|
|
8
17
|
Universal telemetry and visual inspection overlay.
|
|
9
18
|
|
|
10
19
|
BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
|
package/package.json
CHANGED
|
@@ -38,6 +38,7 @@ export class AntAdapter {
|
|
|
38
38
|
this.source = cfg.source;
|
|
39
39
|
|
|
40
40
|
this.#attached = false;
|
|
41
|
+
this.#active = false;
|
|
41
42
|
this.#observer = null;
|
|
42
43
|
this.#counts = new WeakMap();
|
|
43
44
|
this.#hot = [];
|
|
@@ -45,6 +46,8 @@ export class AntAdapter {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
#attached;
|
|
49
|
+
/** False while the ANT button is off: setRoot() must not restart the observer then. */
|
|
50
|
+
#active;
|
|
48
51
|
#observer;
|
|
49
52
|
#counts;
|
|
50
53
|
#hot;
|
|
@@ -53,17 +56,19 @@ export class AntAdapter {
|
|
|
53
56
|
attach() {
|
|
54
57
|
if (this.#attached) return this;
|
|
55
58
|
this.#attached = true;
|
|
56
|
-
this.#
|
|
59
|
+
this.#turnOn();
|
|
57
60
|
this.core?.registerAdapter('ant', {
|
|
58
|
-
enable: () => this.#
|
|
61
|
+
enable: () => this.#turnOn(),
|
|
59
62
|
disable: () => this.#disable(),
|
|
60
63
|
label: 'ANT',
|
|
61
64
|
snapshot: () => this.snapshot()
|
|
62
65
|
});
|
|
63
|
-
this
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
if (this.#observer) {
|
|
67
|
+
this.core?.sendData('log', {
|
|
68
|
+
source: this.source,
|
|
69
|
+
message: 'AntAdapter attached. Watching DOM mutation rate.'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
67
72
|
return this;
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -75,6 +80,19 @@ export class AntAdapter {
|
|
|
75
80
|
return this;
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Watch another root. Counters and hot rows restart; `null` stops watching.
|
|
85
|
+
* @param {ParentNode | null} root
|
|
86
|
+
*/
|
|
87
|
+
setRoot(root) {
|
|
88
|
+
this.root = root;
|
|
89
|
+
if (!this.#attached) return this;
|
|
90
|
+
this.#reset();
|
|
91
|
+
if (!root || !this.#active) return this;
|
|
92
|
+
this.#warnNotWatching(this.#observe());
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
/**
|
|
79
97
|
* Nodes whose current window is still at or above the threshold.
|
|
80
98
|
* Hidden boxes, the WordPress admin bar, and BeeLadybug chrome are skipped.
|
|
@@ -99,49 +117,83 @@ export class AntAdapter {
|
|
|
99
117
|
return { name: 'ant', items };
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
#
|
|
103
|
-
if (!this.#attached
|
|
104
|
-
|
|
120
|
+
#turnOn() {
|
|
121
|
+
if (!this.#attached) return;
|
|
122
|
+
this.#active = true;
|
|
123
|
+
this.#warnNotWatching(this.#observe());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#disable() {
|
|
127
|
+
this.#active = false;
|
|
128
|
+
this.#reset();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#reset() {
|
|
132
|
+
if (this.#observer) this.#observer.disconnect();
|
|
133
|
+
this.#observer = null;
|
|
134
|
+
this.#counts = new WeakMap();
|
|
135
|
+
this.#hot = [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @returns {'' | 'no-root' | 'no-observer' | 'failed'} why nothing is watched, empty when it is */
|
|
139
|
+
#observe() {
|
|
140
|
+
if (this.#observer) return '';
|
|
141
|
+
if (!this.root) return 'no-root';
|
|
142
|
+
if (typeof MutationObserver === 'undefined') return 'no-observer';
|
|
105
143
|
this.#counts = new WeakMap();
|
|
106
|
-
|
|
144
|
+
const observer = new MutationObserver(this.#onMutate);
|
|
107
145
|
try {
|
|
108
|
-
|
|
146
|
+
observer.observe(this.root, {
|
|
109
147
|
childList: true,
|
|
110
148
|
subtree: true,
|
|
111
149
|
attributes: true,
|
|
112
150
|
characterData: true
|
|
113
151
|
});
|
|
114
152
|
} catch {
|
|
115
|
-
|
|
116
|
-
|
|
153
|
+
observer.disconnect();
|
|
154
|
+
return 'failed';
|
|
117
155
|
}
|
|
156
|
+
this.#observer = observer;
|
|
157
|
+
return '';
|
|
118
158
|
}
|
|
119
159
|
|
|
120
|
-
#
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
#warnNotWatching(reason) {
|
|
161
|
+
if (!reason) return;
|
|
162
|
+
const messages = {
|
|
163
|
+
'no-root': 'no root: mutation rate is not watched. Pass { root } or call setRoot().',
|
|
164
|
+
'no-observer': 'MutationObserver is missing: mutation rate is not watched.',
|
|
165
|
+
failed: 'observe() failed: mutation rate is not watched.'
|
|
166
|
+
};
|
|
167
|
+
this.core?.sendData('warn', { source: this.source, message: messages[reason] });
|
|
125
168
|
}
|
|
126
169
|
|
|
127
170
|
#ingest(records) {
|
|
128
171
|
if (!this.#attached || !this.core || !records?.length) return;
|
|
129
172
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
173
|
+
this.#prune(now);
|
|
174
|
+
// One layout read per element per batch, however many records it produced.
|
|
175
|
+
const skipped = new Map();
|
|
130
176
|
for (let i = 0; i < records.length; i++) {
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
177
|
+
const el = elementOf(records[i]?.target);
|
|
178
|
+
if (!el) continue;
|
|
179
|
+
let skip = skipped.get(el);
|
|
180
|
+
if (skip === undefined) {
|
|
181
|
+
skip = isBeeChrome(el) || isReportNoise(el);
|
|
182
|
+
skipped.set(el, skip);
|
|
183
|
+
}
|
|
184
|
+
if (skip) continue;
|
|
133
185
|
|
|
134
|
-
let slot = this.#counts.get(
|
|
186
|
+
let slot = this.#counts.get(el);
|
|
135
187
|
if (!slot || now - slot.windowStart >= this.windowMs) {
|
|
136
188
|
slot = { count: 0, windowStart: now, warned: false };
|
|
137
|
-
this.#counts.set(
|
|
189
|
+
this.#counts.set(el, slot);
|
|
138
190
|
}
|
|
139
191
|
slot.count += 1;
|
|
140
|
-
this.#track(
|
|
192
|
+
this.#track(el, slot);
|
|
141
193
|
if (slot.count < this.threshold || slot.warned) continue;
|
|
142
194
|
|
|
143
195
|
slot.warned = true;
|
|
144
|
-
const selector = nodeSelector(
|
|
196
|
+
const selector = nodeSelector(el);
|
|
145
197
|
this.core.sendData('warn', {
|
|
146
198
|
source: this.source,
|
|
147
199
|
message: `High Mutation Rate on ${selector}`,
|
|
@@ -150,21 +202,36 @@ export class AntAdapter {
|
|
|
150
202
|
}
|
|
151
203
|
}
|
|
152
204
|
|
|
153
|
-
#
|
|
154
|
-
|
|
205
|
+
#prune(now) {
|
|
206
|
+
this.#hot = this.#hot.filter((row) => now - row.windowStart < this.windowMs);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#track(el, slot) {
|
|
210
|
+
const index = this.#hot.findIndex((row) => row.node === el);
|
|
155
211
|
if (slot.count < this.threshold) {
|
|
156
212
|
if (index >= 0) this.#hot.splice(index, 1);
|
|
157
213
|
return;
|
|
158
214
|
}
|
|
159
215
|
const row = {
|
|
160
|
-
node,
|
|
161
|
-
selector: nodeSelector(
|
|
216
|
+
node: el,
|
|
217
|
+
selector: nodeSelector(el),
|
|
162
218
|
count: slot.count,
|
|
163
219
|
windowStart: slot.windowStart
|
|
164
220
|
};
|
|
165
221
|
if (index >= 0) this.#hot[index] = row;
|
|
166
222
|
else this.#hot.push(row);
|
|
167
|
-
|
|
223
|
+
while (this.#hot.length > MAX_HOT) this.#hot.splice(this.#lowestRow(), 1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Fewest mutations first; on a tie the older window goes. */
|
|
227
|
+
#lowestRow() {
|
|
228
|
+
let worst = 0;
|
|
229
|
+
for (let i = 1; i < this.#hot.length; i++) {
|
|
230
|
+
const row = this.#hot[i];
|
|
231
|
+
const low = this.#hot[worst];
|
|
232
|
+
if (row.count < low.count || (row.count === low.count && row.windowStart < low.windowStart)) worst = i;
|
|
233
|
+
}
|
|
234
|
+
return worst;
|
|
168
235
|
}
|
|
169
236
|
}
|
|
170
237
|
|
|
@@ -173,15 +240,26 @@ function positive(value, fallback) {
|
|
|
173
240
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
174
241
|
}
|
|
175
242
|
|
|
243
|
+
/** A text node counts on its parent element, so one element keeps one window. */
|
|
244
|
+
function elementOf(node) {
|
|
245
|
+
if (!node) return null;
|
|
246
|
+
if (node.nodeType === 1) return node;
|
|
247
|
+
return node.parentElement || null;
|
|
248
|
+
}
|
|
249
|
+
|
|
176
250
|
function nodeSelector(node) {
|
|
177
|
-
const el = node
|
|
251
|
+
const el = elementOf(node);
|
|
178
252
|
if (!el || !el.tagName) return 'node';
|
|
179
|
-
if (el.id) return `#${el.id}`;
|
|
253
|
+
if (el.id) return `#${cssEscape(el.id)}`;
|
|
180
254
|
const cls = el.classList && el.classList[0];
|
|
181
|
-
if (cls) return `.${cls}`;
|
|
255
|
+
if (cls) return `.${cssEscape(cls)}`;
|
|
182
256
|
return el.tagName.toLowerCase();
|
|
183
257
|
}
|
|
184
258
|
|
|
259
|
+
function cssEscape(value) {
|
|
260
|
+
return String(value).replace(/[^\w-]/g, '\\$&');
|
|
261
|
+
}
|
|
262
|
+
|
|
185
263
|
function isBeeChrome(node) {
|
|
186
264
|
let el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
187
265
|
while (el) {
|
|
@@ -189,6 +267,7 @@ function isBeeChrome(node) {
|
|
|
189
267
|
if (tag === 'BEE-LADYBUG-OVERLAY') return true;
|
|
190
268
|
if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
|
|
191
269
|
if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
|
|
270
|
+
if (el.hasAttribute?.('data-bee-ladybug-css')) return true;
|
|
192
271
|
el = el.parentElement;
|
|
193
272
|
}
|
|
194
273
|
return false;
|
|
@@ -61,9 +61,13 @@ export class CSSAdapter {
|
|
|
61
61
|
this.#hoverBox = null;
|
|
62
62
|
this.#pinBox = null;
|
|
63
63
|
this.#observer = null;
|
|
64
|
+
this.#unsubscribe = null;
|
|
65
|
+
this.#hasPointer = false;
|
|
64
66
|
this.#onMove = (event) => {
|
|
67
|
+
if (this.core && this.core.visible === false) return;
|
|
65
68
|
this.#x = event.clientX;
|
|
66
69
|
this.#y = event.clientY;
|
|
70
|
+
this.#hasPointer = true;
|
|
67
71
|
this.#schedule();
|
|
68
72
|
};
|
|
69
73
|
this.#onKey = (event) => this.#handleKey(event);
|
|
@@ -71,7 +75,7 @@ export class CSSAdapter {
|
|
|
71
75
|
this.#onMutate = (records) => {
|
|
72
76
|
if (!records.some(touchesSheet)) return;
|
|
73
77
|
this.#sheetsDirty = true;
|
|
74
|
-
this.#schedule();
|
|
78
|
+
if (this.#hasPointer) this.#schedule();
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -92,6 +96,9 @@ export class CSSAdapter {
|
|
|
92
96
|
#hoverBox;
|
|
93
97
|
#pinBox;
|
|
94
98
|
#observer;
|
|
99
|
+
#unsubscribe;
|
|
100
|
+
/** True after the first mousemove: before that (x, y) is not a real pointer. */
|
|
101
|
+
#hasPointer;
|
|
95
102
|
#onMove;
|
|
96
103
|
#onKey;
|
|
97
104
|
#onScroll;
|
|
@@ -104,6 +111,9 @@ export class CSSAdapter {
|
|
|
104
111
|
attach() {
|
|
105
112
|
if (this.#attached) return this;
|
|
106
113
|
this.#attached = true;
|
|
114
|
+
if (this.core && typeof this.core.subscribe === 'function') {
|
|
115
|
+
this.#unsubscribe = this.core.subscribe((packet) => this.#onCore(packet));
|
|
116
|
+
}
|
|
107
117
|
this.core?.registerAdapter('css', {
|
|
108
118
|
label: this.label,
|
|
109
119
|
hotkey: this.hotkey,
|
|
@@ -128,10 +138,21 @@ export class CSSAdapter {
|
|
|
128
138
|
if (!this.#attached) return this;
|
|
129
139
|
this.#disable();
|
|
130
140
|
this.#attached = false;
|
|
141
|
+
if (this.#unsubscribe) this.#unsubscribe();
|
|
142
|
+
this.#unsubscribe = null;
|
|
131
143
|
this.core?.unregisterAdapter('css');
|
|
132
144
|
return this;
|
|
133
145
|
}
|
|
134
146
|
|
|
147
|
+
#onCore(packet) {
|
|
148
|
+
if (packet.type !== 'sys' || packet.payload?.op !== 'visibility') return;
|
|
149
|
+
this.#placeBoxes();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#boxesVisible() {
|
|
153
|
+
return !(this.core && this.core.visible === false);
|
|
154
|
+
}
|
|
155
|
+
|
|
135
156
|
/** Page scan. Click a row to outline one element of that group. */
|
|
136
157
|
scan() {
|
|
137
158
|
if (!this.#active || typeof document === 'undefined') return this;
|
|
@@ -141,15 +162,15 @@ export class CSSAdapter {
|
|
|
141
162
|
const rows = [];
|
|
142
163
|
for (let i = 0; i < nodes.length; i++) {
|
|
143
164
|
const el = nodes[i];
|
|
144
|
-
if (!el || isBeeChrome(el) || isSkippedTag(el)) continue;
|
|
165
|
+
if (!el || isBeeChrome(el) || isSkippedTag(el) || isReportNoise(el)) continue;
|
|
145
166
|
const measured = measureViewportOverflow(el);
|
|
146
167
|
if (measured) overflow.push(measured);
|
|
147
168
|
const findings = [];
|
|
148
169
|
pushNegativeMargins(el, findings);
|
|
149
170
|
pushInvisible(el, findings);
|
|
150
|
-
pushBlockers(el, findings);
|
|
171
|
+
pushBlockers(el, findings, true);
|
|
151
172
|
pushImage(el, findings);
|
|
152
|
-
pushStacking(el, findings);
|
|
173
|
+
pushStacking(el, findings, true);
|
|
153
174
|
pushImportant(el, this.#rules, findings);
|
|
154
175
|
for (let j = 0; j < findings.length; j++) {
|
|
155
176
|
rows.push({ el, selector: cssPath(el), ...findings[j] });
|
|
@@ -245,7 +266,9 @@ export class CSSAdapter {
|
|
|
245
266
|
for (let i = 0; i < rows.length; i++) {
|
|
246
267
|
const row = rows[i];
|
|
247
268
|
if (describeCause(row.kind, row.selector)) {
|
|
248
|
-
|
|
269
|
+
// No cap here: MAX_SNAPSHOT applies after the severity sort, so an info row
|
|
270
|
+
// can never push a real error out.
|
|
271
|
+
causes = addCause(causes, { kind: row.kind, path: row.selector, el: row.el }, Infinity);
|
|
249
272
|
} else {
|
|
250
273
|
plain.push(row);
|
|
251
274
|
}
|
|
@@ -319,6 +342,7 @@ export class CSSAdapter {
|
|
|
319
342
|
#disable() {
|
|
320
343
|
if (!this.#active) return;
|
|
321
344
|
this.#active = false;
|
|
345
|
+
this.#hasPointer = false;
|
|
322
346
|
if (typeof window !== 'undefined') {
|
|
323
347
|
window.removeEventListener('mousemove', this.#onMove, true);
|
|
324
348
|
window.removeEventListener('keydown', this.#onKey);
|
|
@@ -340,11 +364,16 @@ export class CSSAdapter {
|
|
|
340
364
|
this.core?.publishDetail([]);
|
|
341
365
|
this.core?.publishIssues([]);
|
|
342
366
|
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'off' });
|
|
367
|
+
this.core?.sendData('state', { source: this.source, key: 'css.target', value: '—' });
|
|
368
|
+
this.core?.sendData('state', { source: this.source, key: 'css.hits', value: '0' });
|
|
369
|
+
this.core?.sendData('state', { source: this.source, key: 'css.pin', value: 'off' });
|
|
343
370
|
this.core?.sendData('log', { source: this.source, message: 'CSS hunt off.' });
|
|
344
371
|
}
|
|
345
372
|
|
|
346
373
|
#handleKey(event) {
|
|
347
374
|
if (!this.#active || event.repeat) return;
|
|
375
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
376
|
+
if (this.core && this.core.visible === false) return;
|
|
348
377
|
if (isTypingTarget(event.target)) return;
|
|
349
378
|
if (event.code === this.pinKey) {
|
|
350
379
|
event.preventDefault();
|
|
@@ -377,22 +406,23 @@ export class CSSAdapter {
|
|
|
377
406
|
|
|
378
407
|
#inspect(x, y) {
|
|
379
408
|
const el = pickElement(x, y);
|
|
380
|
-
placeBox(this.#hoverBox, el);
|
|
409
|
+
placeBox(this.#hoverBox, el, this.#boxesVisible());
|
|
381
410
|
if (this.#panelLock) return;
|
|
382
411
|
if (el === this.#lastEl) return;
|
|
383
412
|
this.#lastEl = el;
|
|
384
413
|
if (!el) {
|
|
385
414
|
this.core?.publishDetail([]);
|
|
386
415
|
this.core?.sendData('state', { source: this.source, key: 'css.target', value: '—' });
|
|
416
|
+
this.core?.sendData('state', { source: this.source, key: 'css.hits', value: '0' });
|
|
387
417
|
return;
|
|
388
418
|
}
|
|
389
419
|
const findings = [];
|
|
390
420
|
pushImportant(el, this.#rules, findings);
|
|
391
421
|
pushOverflow(el, findings);
|
|
392
422
|
pushInvisible(el, findings);
|
|
393
|
-
pushBlockers(el, findings);
|
|
423
|
+
pushBlockers(el, findings, true);
|
|
394
424
|
pushImage(el, findings);
|
|
395
|
-
pushStacking(el, findings);
|
|
425
|
+
pushStacking(el, findings, true);
|
|
396
426
|
const path = cssPath(el);
|
|
397
427
|
this.core?.sendData('state', { source: this.source, key: 'css.target', value: path });
|
|
398
428
|
this.core?.sendData('state', {
|
|
@@ -419,7 +449,7 @@ export class CSSAdapter {
|
|
|
419
449
|
if (this.#panelLock) {
|
|
420
450
|
this.#panelLock = false;
|
|
421
451
|
this.#pinned = null;
|
|
422
|
-
placeBox(this.#pinBox, null);
|
|
452
|
+
placeBox(this.#pinBox, null, this.#boxesVisible());
|
|
423
453
|
this.core?.sendData('state', { source: this.source, key: 'css.pin', value: 'off' });
|
|
424
454
|
this.core?.sendData('log', { source: this.source, message: 'CSS pin off.' });
|
|
425
455
|
this.#lastEl = null;
|
|
@@ -504,15 +534,16 @@ export class CSSAdapter {
|
|
|
504
534
|
|
|
505
535
|
#pin(el) {
|
|
506
536
|
this.#pinned = el;
|
|
507
|
-
placeBox(this.#pinBox, el);
|
|
537
|
+
placeBox(this.#pinBox, el, this.#boxesVisible());
|
|
508
538
|
if (el && typeof el.scrollIntoView === 'function') {
|
|
509
539
|
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' });
|
|
510
540
|
}
|
|
511
541
|
}
|
|
512
542
|
|
|
513
543
|
#placeBoxes() {
|
|
514
|
-
|
|
515
|
-
placeBox(this.#
|
|
544
|
+
const visible = this.#boxesVisible();
|
|
545
|
+
placeBox(this.#hoverBox, this.#lastEl, visible);
|
|
546
|
+
placeBox(this.#pinBox, this.#pinned, visible);
|
|
516
547
|
}
|
|
517
548
|
|
|
518
549
|
#mountBoxes() {
|
|
@@ -538,6 +569,8 @@ function collectImportantRules() {
|
|
|
538
569
|
const sheets = document.styleSheets;
|
|
539
570
|
for (let i = 0; i < sheets.length; i++) {
|
|
540
571
|
const sheet = sheets[i];
|
|
572
|
+
if (sheet.disabled === true) continue;
|
|
573
|
+
if (sheet.media && sheet.media.mediaText && !mediaMatches(sheet.media.mediaText)) continue;
|
|
541
574
|
let list = null;
|
|
542
575
|
try {
|
|
543
576
|
list = sheet.cssRules;
|
|
@@ -550,21 +583,21 @@ function collectImportantRules() {
|
|
|
550
583
|
return { rules, unreadable };
|
|
551
584
|
}
|
|
552
585
|
|
|
586
|
+
function mediaMatches(mediaText) {
|
|
587
|
+
try {
|
|
588
|
+
return typeof matchMedia === 'function' ? matchMedia(mediaText).matches : true;
|
|
589
|
+
} catch {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
553
594
|
function walkRules(list, source, into) {
|
|
554
595
|
if (!list) return;
|
|
555
596
|
for (let i = 0; i < list.length; i++) {
|
|
556
597
|
const rule = list[i];
|
|
557
598
|
if (!rule) continue;
|
|
558
599
|
if (rule.type === 4 && rule.media) {
|
|
559
|
-
|
|
560
|
-
try {
|
|
561
|
-
active = typeof matchMedia === 'function'
|
|
562
|
-
? matchMedia(rule.media.mediaText).matches
|
|
563
|
-
: true;
|
|
564
|
-
} catch {
|
|
565
|
-
active = true;
|
|
566
|
-
}
|
|
567
|
-
if (active) walkRules(rule.cssRules, source, into);
|
|
600
|
+
if (mediaMatches(rule.media.mediaText)) walkRules(rule.cssRules, source, into);
|
|
568
601
|
continue;
|
|
569
602
|
}
|
|
570
603
|
if (rule.type === 1 && rule.selectorText && rule.style) {
|
|
@@ -745,7 +778,9 @@ function pushInvisible(el, findings) {
|
|
|
745
778
|
findings.push(issue('invisible text (opacity 0)', ISSUE_RANK.invisible, 30));
|
|
746
779
|
}
|
|
747
780
|
const color = parseColor(style.color);
|
|
748
|
-
|
|
781
|
+
// Unknown syntax (oklch, lab, color(), color-mix): nothing to compare, nothing to report.
|
|
782
|
+
if (!color) return;
|
|
783
|
+
if (color.a === 0) {
|
|
749
784
|
findings.push(issue('invisible text (transparent color)', ISSUE_RANK.invisible, 30));
|
|
750
785
|
return;
|
|
751
786
|
}
|
|
@@ -1096,7 +1131,7 @@ function pickElement(x, y) {
|
|
|
1096
1131
|
const stack = document.elementsFromPoint(x, y);
|
|
1097
1132
|
for (let i = 0; i < stack.length; i++) {
|
|
1098
1133
|
const el = stack[i];
|
|
1099
|
-
if (!el || isBeeChrome(el)) continue;
|
|
1134
|
+
if (!el || isBeeChrome(el) || isReportNoise(el)) continue;
|
|
1100
1135
|
if (el === document.documentElement || el === document.body) continue;
|
|
1101
1136
|
return el;
|
|
1102
1137
|
}
|
|
@@ -1154,12 +1189,16 @@ function hasOwnText(el) {
|
|
|
1154
1189
|
function cssPath(el) {
|
|
1155
1190
|
if (!el || !el.tagName) return 'node';
|
|
1156
1191
|
const tag = el.tagName.toLowerCase();
|
|
1157
|
-
if (el.id) return `${tag}#${el.id}`;
|
|
1192
|
+
if (el.id) return `${tag}#${cssEscape(el.id)}`;
|
|
1158
1193
|
const classes = el.classList ? [...el.classList].slice(0, 2) : [];
|
|
1159
|
-
if (classes.length) return `${tag}.${classes.join('.')}`;
|
|
1194
|
+
if (classes.length) return `${tag}.${classes.map(cssEscape).join('.')}`;
|
|
1160
1195
|
return tag;
|
|
1161
1196
|
}
|
|
1162
1197
|
|
|
1198
|
+
function cssEscape(value) {
|
|
1199
|
+
return String(value).replace(/[^\w-]/g, '\\$&');
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1163
1202
|
function isSkippedTag(el) {
|
|
1164
1203
|
const tag = el.tagName;
|
|
1165
1204
|
return tag === 'SCRIPT' || tag === 'STYLE' || tag === 'LINK' || tag === 'META';
|
|
@@ -1179,7 +1218,10 @@ function touchesSheet(record) {
|
|
|
1179
1218
|
const name = record.target?.nodeName;
|
|
1180
1219
|
return name === 'STYLE' || name === 'LINK';
|
|
1181
1220
|
}
|
|
1182
|
-
|
|
1221
|
+
// Only what was added or removed: the target's whole subtree always holds some <style> or <link>.
|
|
1222
|
+
// A <style> that receives new text is the one target worth checking, and that check is O(1).
|
|
1223
|
+
if (record.target?.nodeName === 'STYLE') return true;
|
|
1224
|
+
const nodes = [];
|
|
1183
1225
|
if (record.addedNodes) {
|
|
1184
1226
|
for (let i = 0; i < record.addedNodes.length; i++) nodes.push(record.addedNodes[i]);
|
|
1185
1227
|
}
|
|
@@ -1220,9 +1262,9 @@ function makeBox(kind, border, fill) {
|
|
|
1220
1262
|
return node;
|
|
1221
1263
|
}
|
|
1222
1264
|
|
|
1223
|
-
function placeBox(node, el) {
|
|
1265
|
+
function placeBox(node, el, visible = true) {
|
|
1224
1266
|
if (!node) return;
|
|
1225
|
-
if (!el || typeof el.getBoundingClientRect !== 'function' || isBeeChrome(el)) {
|
|
1267
|
+
if (visible === false || !el || typeof el.getBoundingClientRect !== 'function' || isBeeChrome(el)) {
|
|
1226
1268
|
node.style.display = 'none';
|
|
1227
1269
|
return;
|
|
1228
1270
|
}
|
|
@@ -520,6 +520,7 @@ function isBeeChrome(node) {
|
|
|
520
520
|
if (tag === 'BEE-LADYBUG-OVERLAY') return true;
|
|
521
521
|
if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
|
|
522
522
|
if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
|
|
523
|
+
if (el.hasAttribute?.('data-bee-ladybug-css')) return true;
|
|
523
524
|
el = el.parentElement;
|
|
524
525
|
}
|
|
525
526
|
return false;
|
|
@@ -10,7 +10,7 @@ import { UIOverlay } from './UIOverlay.js';
|
|
|
10
10
|
import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
|
|
11
11
|
import { exportReport as writeReport } from './ReportExporter.js';
|
|
12
12
|
|
|
13
|
-
export const BEE_LADYBUG_VERSION = '0.6.
|
|
13
|
+
export const BEE_LADYBUG_VERSION = '0.6.5';
|
|
14
14
|
|
|
15
15
|
export const CORE_DEFAULTS = Object.freeze({
|
|
16
16
|
toggleKey: 'F2',
|
package/src/index.d.ts
CHANGED