bee-ladybug 0.4.1 → 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 +27 -0
- package/package.json +1 -1
- package/src/adapters/CSSAdapter.js +773 -0
- package/src/core/BeeLadybugCore.js +46 -4
- package/src/core/UIOverlay.js +92 -2
- package/src/index.d.ts +35 -0
- package/src/index.js +1 -0
package/README.MD
CHANGED
|
@@ -39,6 +39,7 @@ bee-ladybug/
|
|
|
39
39
|
│ └── adapters/
|
|
40
40
|
│ ├── CanvasAdapter.js
|
|
41
41
|
│ ├── WebDOMAdapter.js
|
|
42
|
+
│ ├── CSSAdapter.js
|
|
42
43
|
│ ├── AntAdapter.js
|
|
43
44
|
│ ├── SpiderAdapter.js
|
|
44
45
|
│ └── python/PythonBridge.js
|
|
@@ -46,10 +47,12 @@ bee-ladybug/
|
|
|
46
47
|
│ ├── core.html
|
|
47
48
|
│ ├── canvas-sandbox.js
|
|
48
49
|
│ ├── dom.html
|
|
50
|
+
│ ├── css-hunt.html
|
|
49
51
|
│ ├── python.html
|
|
50
52
|
│ └── python/sender.py
|
|
51
53
|
├── tsconfig.json
|
|
52
54
|
├── package.json
|
|
55
|
+
├── CHANGELOG.md
|
|
53
56
|
├── LICENSE
|
|
54
57
|
├── index.html
|
|
55
58
|
└── README.md
|
|
@@ -130,6 +133,8 @@ bee.subscribe((packet) => {
|
|
|
130
133
|
| `F2` | mostra / nascondi overlay (e hitbox canvas) |
|
|
131
134
|
| `F3` | slow-motion del tempo fittizio (`0.25x` / `1x`) |
|
|
132
135
|
| `F4` | freeze / run del tempo fittizio |
|
|
136
|
+
| `F6` | CSS hunt, se `CSSAdapter` è attaccato |
|
|
137
|
+
| `S` | scansione CSS, solo con la hunt accesa |
|
|
133
138
|
|
|
134
139
|
Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
|
|
135
140
|
|
|
@@ -177,6 +182,28 @@ adapter.attach();
|
|
|
177
182
|
|
|
178
183
|
Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
|
|
179
184
|
|
|
185
|
+
## CSS hunt (F6)
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import { BeeLadybugCore, CSSAdapter } from './src/index.js';
|
|
189
|
+
|
|
190
|
+
const bee = new BeeLadybugCore();
|
|
191
|
+
const css = new CSSAdapter(bee);
|
|
192
|
+
css.attach();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`F6` (pulsante **F6 CSS** accanto a F3/F4) accende la caccia. Sull'elemento sotto il puntatore l'overlay elenca:
|
|
196
|
+
|
|
197
|
+
- regole `!important` che gli corrispondono (fogli cross-origin: `unreadable sheet: <href>`)
|
|
198
|
+
- overflow del viewport e margini negativi
|
|
199
|
+
- testo trasparente, `opacity: 0`, o colore uguale allo sfondo
|
|
200
|
+
- chi intercetta il click al centro, inclusi `::before` / `::after` fissi su `html` / `body`
|
|
201
|
+
- `<img>` con rapporto stirato oltre il 5% e `object-fit: fill`
|
|
202
|
+
|
|
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
|
+
|
|
205
|
+
Demo: `examples/css-hunt.html`.
|
|
206
|
+
|
|
180
207
|
## Registro adapter (overlay)
|
|
181
208
|
|
|
182
209
|
Il Core non conosce ANT/SPIDER. Gli adapter si iscrivono con hook propri; l'overlay disegna un toggle per ciascuno.
|
package/package.json
CHANGED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSSAdapter — CSS hunt.
|
|
3
|
+
*
|
|
4
|
+
* Hover an element to list !important hits, viewport overflow, negative
|
|
5
|
+
* margins, invisible text, click blockers and stretched images. The core
|
|
6
|
+
* only receives packets and detail rows; it does not parse CSS.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const css = new CSSAdapter(core);
|
|
10
|
+
* css.attach();
|
|
11
|
+
* // F6 toggles hunt. S scans the page while hunt is on.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const CSS_ADAPTER_DEFAULTS = Object.freeze({
|
|
15
|
+
source: 'css',
|
|
16
|
+
hotkey: 'F6',
|
|
17
|
+
scanKey: 'KeyS',
|
|
18
|
+
label: 'F6 CSS',
|
|
19
|
+
maxScan: 8
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export class CSSAdapter {
|
|
23
|
+
/**
|
|
24
|
+
* @param {import('../core/BeeLadybugCore.js').BeeLadybugCore} core
|
|
25
|
+
* @param {Partial<typeof CSS_ADAPTER_DEFAULTS>} [options]
|
|
26
|
+
*/
|
|
27
|
+
constructor(core, options = {}) {
|
|
28
|
+
const cfg = { ...CSS_ADAPTER_DEFAULTS, ...options };
|
|
29
|
+
this.core = core;
|
|
30
|
+
this.source = cfg.source;
|
|
31
|
+
this.hotkey = cfg.hotkey;
|
|
32
|
+
this.scanKey = cfg.scanKey;
|
|
33
|
+
this.label = cfg.label;
|
|
34
|
+
this.maxScan = cfg.maxScan;
|
|
35
|
+
|
|
36
|
+
this.#attached = false;
|
|
37
|
+
this.#active = false;
|
|
38
|
+
this.#rules = [];
|
|
39
|
+
this.#unreadable = [];
|
|
40
|
+
this.#sheetsDirty = true;
|
|
41
|
+
this.#x = 0;
|
|
42
|
+
this.#y = 0;
|
|
43
|
+
this.#rafId = 0;
|
|
44
|
+
this.#lastEl = null;
|
|
45
|
+
this.#pinned = null;
|
|
46
|
+
this.#hoverBox = null;
|
|
47
|
+
this.#pinBox = null;
|
|
48
|
+
this.#observer = null;
|
|
49
|
+
this.#onMove = (event) => {
|
|
50
|
+
this.#x = event.clientX;
|
|
51
|
+
this.#y = event.clientY;
|
|
52
|
+
this.#schedule();
|
|
53
|
+
};
|
|
54
|
+
this.#onKey = (event) => this.#handleKey(event);
|
|
55
|
+
this.#onScroll = () => this.#placeBoxes();
|
|
56
|
+
this.#onMutate = (records) => {
|
|
57
|
+
if (!records.some(touchesSheet)) return;
|
|
58
|
+
this.#sheetsDirty = true;
|
|
59
|
+
this.#schedule();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#attached;
|
|
64
|
+
#active;
|
|
65
|
+
#rules;
|
|
66
|
+
#unreadable;
|
|
67
|
+
#sheetsDirty;
|
|
68
|
+
#x;
|
|
69
|
+
#y;
|
|
70
|
+
#rafId;
|
|
71
|
+
#lastEl;
|
|
72
|
+
#pinned;
|
|
73
|
+
#hoverBox;
|
|
74
|
+
#pinBox;
|
|
75
|
+
#observer;
|
|
76
|
+
#onMove;
|
|
77
|
+
#onKey;
|
|
78
|
+
#onScroll;
|
|
79
|
+
#onMutate;
|
|
80
|
+
|
|
81
|
+
get active() {
|
|
82
|
+
return this.#active;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
attach() {
|
|
86
|
+
if (this.#attached) return this;
|
|
87
|
+
this.#attached = true;
|
|
88
|
+
this.core?.registerAdapter('css', {
|
|
89
|
+
label: this.label,
|
|
90
|
+
hotkey: this.hotkey,
|
|
91
|
+
enabled: false,
|
|
92
|
+
enable: () => this.#enable(),
|
|
93
|
+
disable: () => this.#disable()
|
|
94
|
+
});
|
|
95
|
+
this.core?.sendData('log', {
|
|
96
|
+
source: this.source,
|
|
97
|
+
message: 'CSSAdapter attached. F6 CSS hunt. S scans the page while hunt is on.'
|
|
98
|
+
});
|
|
99
|
+
this.core?.sendData('state', {
|
|
100
|
+
source: this.source,
|
|
101
|
+
key: 'css.hunt',
|
|
102
|
+
value: 'off'
|
|
103
|
+
});
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
detach() {
|
|
108
|
+
if (!this.#attached) return this;
|
|
109
|
+
this.#disable();
|
|
110
|
+
this.#attached = false;
|
|
111
|
+
this.core?.unregisterAdapter('css');
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Page scan. Click a row to outline one element of that group. */
|
|
116
|
+
scan() {
|
|
117
|
+
if (!this.#active || typeof document === 'undefined') return this;
|
|
118
|
+
if (this.#sheetsDirty) this.#rebuildSheets();
|
|
119
|
+
const nodes = document.body ? document.body.querySelectorAll('*') : [];
|
|
120
|
+
const overflow = [];
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
123
|
+
const el = nodes[i];
|
|
124
|
+
if (!el || isBeeChrome(el) || isSkippedTag(el)) continue;
|
|
125
|
+
const measured = measureViewportOverflow(el);
|
|
126
|
+
if (measured) overflow.push(measured);
|
|
127
|
+
const findings = [];
|
|
128
|
+
pushNegativeMargins(el, findings);
|
|
129
|
+
pushInvisible(el, findings);
|
|
130
|
+
pushBlockers(el, findings);
|
|
131
|
+
pushImage(el, findings);
|
|
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
|
+
}
|
|
136
|
+
}
|
|
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)
|
|
154
|
+
}));
|
|
155
|
+
this.#lastEl = null;
|
|
156
|
+
this.core?.publishDetail(items);
|
|
157
|
+
this.core?.sendData('state', {
|
|
158
|
+
source: this.source,
|
|
159
|
+
key: 'css.scan',
|
|
160
|
+
value: String(groups.length)
|
|
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
|
+
}
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#enable() {
|
|
174
|
+
if (this.#active || typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
175
|
+
this.#active = true;
|
|
176
|
+
this.#sheetsDirty = true;
|
|
177
|
+
this.#mountBoxes();
|
|
178
|
+
window.addEventListener('mousemove', this.#onMove, true);
|
|
179
|
+
window.addEventListener('keydown', this.#onKey);
|
|
180
|
+
window.addEventListener('scroll', this.#onScroll, true);
|
|
181
|
+
window.addEventListener('resize', this.#onScroll);
|
|
182
|
+
if (typeof MutationObserver !== 'undefined' && document.documentElement) {
|
|
183
|
+
this.#observer = new MutationObserver(this.#onMutate);
|
|
184
|
+
this.#observer.observe(document.documentElement, {
|
|
185
|
+
childList: true,
|
|
186
|
+
subtree: true,
|
|
187
|
+
attributes: true,
|
|
188
|
+
attributeFilter: ['href', 'rel', 'media', 'disabled', 'type'],
|
|
189
|
+
characterData: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
this.#rebuildSheets();
|
|
193
|
+
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'on' });
|
|
194
|
+
this.core?.sendData('log', {
|
|
195
|
+
source: this.source,
|
|
196
|
+
message: 'CSS hunt on. Hover an element, or press S to scan.'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#disable() {
|
|
201
|
+
if (!this.#active) return;
|
|
202
|
+
this.#active = false;
|
|
203
|
+
if (typeof window !== 'undefined') {
|
|
204
|
+
window.removeEventListener('mousemove', this.#onMove, true);
|
|
205
|
+
window.removeEventListener('keydown', this.#onKey);
|
|
206
|
+
window.removeEventListener('scroll', this.#onScroll, true);
|
|
207
|
+
window.removeEventListener('resize', this.#onScroll);
|
|
208
|
+
}
|
|
209
|
+
if (this.#rafId && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(this.#rafId);
|
|
210
|
+
this.#rafId = 0;
|
|
211
|
+
if (this.#observer) this.#observer.disconnect();
|
|
212
|
+
this.#observer = null;
|
|
213
|
+
this.#lastEl = null;
|
|
214
|
+
this.#pinned = null;
|
|
215
|
+
this.#unmountBoxes();
|
|
216
|
+
this.core?.publishDetail([]);
|
|
217
|
+
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'off' });
|
|
218
|
+
this.core?.sendData('log', { source: this.source, message: 'CSS hunt off.' });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#handleKey(event) {
|
|
222
|
+
if (!this.#active || event.repeat) return;
|
|
223
|
+
if (event.code !== this.scanKey) return;
|
|
224
|
+
if (isTypingTarget(event.target)) return;
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
this.scan();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#schedule() {
|
|
230
|
+
if (!this.#active || this.#rafId || typeof requestAnimationFrame !== 'function') return;
|
|
231
|
+
this.#rafId = requestAnimationFrame(() => {
|
|
232
|
+
this.#rafId = 0;
|
|
233
|
+
if (!this.#active) return;
|
|
234
|
+
if (this.#sheetsDirty) this.#rebuildSheets();
|
|
235
|
+
this.#inspect(this.#x, this.#y);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#rebuildSheets() {
|
|
240
|
+
this.#sheetsDirty = false;
|
|
241
|
+
const built = collectImportantRules();
|
|
242
|
+
this.#rules = built.rules;
|
|
243
|
+
this.#unreadable = built.unreadable;
|
|
244
|
+
for (let i = 0; i < built.unreadable.length; i++) {
|
|
245
|
+
this.core?.sendData('warn', {
|
|
246
|
+
source: this.source,
|
|
247
|
+
message: `unreadable sheet: ${built.unreadable[i]}`
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#inspect(x, y) {
|
|
253
|
+
const el = pickElement(x, y);
|
|
254
|
+
placeBox(this.#hoverBox, el);
|
|
255
|
+
if (el === this.#lastEl) return;
|
|
256
|
+
this.#lastEl = el;
|
|
257
|
+
if (!el) {
|
|
258
|
+
this.core?.publishDetail([]);
|
|
259
|
+
this.core?.sendData('state', { source: this.source, key: 'css.target', value: '—' });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const findings = [];
|
|
263
|
+
pushImportant(el, this.#rules, findings);
|
|
264
|
+
pushOverflow(el, findings);
|
|
265
|
+
pushInvisible(el, findings);
|
|
266
|
+
pushBlockers(el, findings);
|
|
267
|
+
pushImage(el, findings);
|
|
268
|
+
for (let i = 0; i < this.#unreadable.length; i++) {
|
|
269
|
+
findings.push({
|
|
270
|
+
level: 'warn',
|
|
271
|
+
text: `unreadable sheet: ${this.#unreadable[i]}`,
|
|
272
|
+
score: 0
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const path = cssPath(el);
|
|
276
|
+
this.core?.sendData('state', { source: this.source, key: 'css.target', value: path });
|
|
277
|
+
this.core?.sendData('state', {
|
|
278
|
+
source: this.source,
|
|
279
|
+
key: 'css.hits',
|
|
280
|
+
value: String(findings.length)
|
|
281
|
+
});
|
|
282
|
+
this.core?.publishDetail(findings.map((hit) => ({
|
|
283
|
+
level: hit.level,
|
|
284
|
+
text: `${path} ${hit.text}`,
|
|
285
|
+
onSelect: () => this.#pin(el)
|
|
286
|
+
})));
|
|
287
|
+
if (!findings.length) {
|
|
288
|
+
if (isVerbose(this.core)) {
|
|
289
|
+
this.core?.sendData('log', { source: this.source, message: `${path} clean` });
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const lead = findings.find((hit) => hit.level === 'error') || findings[0];
|
|
294
|
+
this.core?.sendData(lead.level === 'error' ? 'error' : 'warn', {
|
|
295
|
+
source: this.source,
|
|
296
|
+
message: `${path} ${lead.text}`
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#pin(el) {
|
|
301
|
+
this.#pinned = el;
|
|
302
|
+
placeBox(this.#pinBox, el);
|
|
303
|
+
if (el && typeof el.scrollIntoView === 'function') {
|
|
304
|
+
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#placeBoxes() {
|
|
309
|
+
placeBox(this.#hoverBox, this.#lastEl);
|
|
310
|
+
placeBox(this.#pinBox, this.#pinned);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#mountBoxes() {
|
|
314
|
+
if (typeof document === 'undefined' || this.#hoverBox) return;
|
|
315
|
+
this.#hoverBox = makeBox('hover', '1px solid #ffd36a', 'transparent');
|
|
316
|
+
this.#pinBox = makeBox('pin', '2px solid #ff3b4e', 'rgba(255, 59, 78, 0.12)');
|
|
317
|
+
document.body.appendChild(this.#hoverBox);
|
|
318
|
+
document.body.appendChild(this.#pinBox);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#unmountBoxes() {
|
|
322
|
+
this.#hoverBox?.remove();
|
|
323
|
+
this.#pinBox?.remove();
|
|
324
|
+
this.#hoverBox = null;
|
|
325
|
+
this.#pinBox = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function collectImportantRules() {
|
|
330
|
+
const rules = [];
|
|
331
|
+
const unreadable = [];
|
|
332
|
+
if (typeof document === 'undefined') return { rules, unreadable };
|
|
333
|
+
const sheets = document.styleSheets;
|
|
334
|
+
for (let i = 0; i < sheets.length; i++) {
|
|
335
|
+
const sheet = sheets[i];
|
|
336
|
+
let list = null;
|
|
337
|
+
try {
|
|
338
|
+
list = sheet.cssRules;
|
|
339
|
+
} catch {
|
|
340
|
+
unreadable.push(sheet.href || '<style> inline');
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
walkRules(list, sheetLabel(sheet), rules);
|
|
344
|
+
}
|
|
345
|
+
return { rules, unreadable };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function walkRules(list, source, into) {
|
|
349
|
+
if (!list) return;
|
|
350
|
+
for (let i = 0; i < list.length; i++) {
|
|
351
|
+
const rule = list[i];
|
|
352
|
+
if (!rule) continue;
|
|
353
|
+
if (rule.type === 4 && rule.media) {
|
|
354
|
+
let active = true;
|
|
355
|
+
try {
|
|
356
|
+
active = typeof matchMedia === 'function'
|
|
357
|
+
? matchMedia(rule.media.mediaText).matches
|
|
358
|
+
: true;
|
|
359
|
+
} catch {
|
|
360
|
+
active = true;
|
|
361
|
+
}
|
|
362
|
+
if (active) walkRules(rule.cssRules, source, into);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (rule.type === 1 && rule.selectorText && rule.style) {
|
|
366
|
+
const props = importantProps(rule.style);
|
|
367
|
+
if (props.length) into.push({ selector: rule.selectorText, source, props });
|
|
368
|
+
} else if (rule.cssRules) {
|
|
369
|
+
walkRules(rule.cssRules, source, into);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function importantProps(style) {
|
|
375
|
+
const props = [];
|
|
376
|
+
for (let i = 0; i < style.length; i++) {
|
|
377
|
+
const property = style.item(i);
|
|
378
|
+
if (style.getPropertyPriority(property) !== 'important') continue;
|
|
379
|
+
props.push({ property, value: style.getPropertyValue(property).trim() });
|
|
380
|
+
}
|
|
381
|
+
return props;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function sheetLabel(sheet) {
|
|
385
|
+
if (sheet.href) return sheet.href;
|
|
386
|
+
return '<style> inline';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function pushImportant(el, rules, findings) {
|
|
390
|
+
for (let i = 0; i < rules.length; i++) {
|
|
391
|
+
const rule = rules[i];
|
|
392
|
+
let matched = false;
|
|
393
|
+
try {
|
|
394
|
+
matched = el.matches(rule.selector);
|
|
395
|
+
} catch {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (!matched) continue;
|
|
399
|
+
for (let j = 0; j < rule.props.length; j++) {
|
|
400
|
+
const prop = rule.props[j];
|
|
401
|
+
findings.push({
|
|
402
|
+
level: 'warn',
|
|
403
|
+
rank: 4,
|
|
404
|
+
score: 5,
|
|
405
|
+
kind: `!important ${rule.selector} { ${prop.property}: ${prop.value} }`,
|
|
406
|
+
text: `!important ${rule.selector} { ${prop.property}: ${prop.value} } @ ${rule.source}`
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function pushOverflow(el, findings) {
|
|
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`;
|
|
418
|
+
findings.push({
|
|
419
|
+
level: 'error',
|
|
420
|
+
rank: 0,
|
|
421
|
+
score: measured.score,
|
|
422
|
+
kind: 'overflows viewport',
|
|
423
|
+
text: `overflows viewport (${past})`
|
|
424
|
+
});
|
|
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;
|
|
482
|
+
const style = getComputedStyle(el);
|
|
483
|
+
const sides = ['top', 'right', 'bottom', 'left'];
|
|
484
|
+
for (let i = 0; i < sides.length; i++) {
|
|
485
|
+
const raw = style.getPropertyValue(`margin-${sides[i]}`);
|
|
486
|
+
const value = parseFloat(raw);
|
|
487
|
+
if (!Number.isFinite(value) || value >= 0) continue;
|
|
488
|
+
findings.push({
|
|
489
|
+
level: 'warn',
|
|
490
|
+
rank: 5,
|
|
491
|
+
score: Math.abs(value),
|
|
492
|
+
kind: `negative margin margin-${sides[i]}: ${raw.trim()}`,
|
|
493
|
+
text: `negative margin margin-${sides[i]}: ${raw.trim()}`
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
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
|
+
|
|
534
|
+
function pushInvisible(el, findings) {
|
|
535
|
+
if (!hasOwnText(el)) return;
|
|
536
|
+
const style = getComputedStyle(el);
|
|
537
|
+
if (opacityZero(el)) {
|
|
538
|
+
findings.push(issue('invisible text (opacity 0)', ISSUE_RANK.invisible, 30));
|
|
539
|
+
}
|
|
540
|
+
const color = parseColor(style.color);
|
|
541
|
+
if (!color || color.a === 0) {
|
|
542
|
+
findings.push(issue('invisible text (transparent color)', ISSUE_RANK.invisible, 30));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const behind = paintBehind(el);
|
|
546
|
+
if (behind && nearlySame(color, behind)) {
|
|
547
|
+
findings.push(issue('invisible text (color matches background)', ISSUE_RANK.invisible, 30));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function pushBlockers(el, findings) {
|
|
552
|
+
if (!el.getBoundingClientRect || typeof document.elementFromPoint !== 'function') return;
|
|
553
|
+
const rect = el.getBoundingClientRect();
|
|
554
|
+
const x = rect.left + rect.width / 2;
|
|
555
|
+
const y = rect.top + rect.height / 2;
|
|
556
|
+
if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) return;
|
|
557
|
+
const pseudo = coveringPseudo(x, y);
|
|
558
|
+
if (pseudo) {
|
|
559
|
+
findings.push(issue(`click blocked by ${pseudo}`, ISSUE_RANK.blocker, 40));
|
|
560
|
+
}
|
|
561
|
+
const hit = document.elementFromPoint(x, y);
|
|
562
|
+
if (!hit || isBeeChrome(hit) || hit === el || el.contains(hit)) return;
|
|
563
|
+
if (pseudo && (hit === document.documentElement || hit === document.body)) return;
|
|
564
|
+
findings.push(issue(`click blocked by ${cssPath(hit)}`, ISSUE_RANK.blocker, 40));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function pushImage(el, findings) {
|
|
568
|
+
if (!el || el.tagName !== 'IMG') return;
|
|
569
|
+
const naturalW = el.naturalWidth;
|
|
570
|
+
const naturalH = el.naturalHeight;
|
|
571
|
+
const shownW = el.clientWidth;
|
|
572
|
+
const shownH = el.clientHeight;
|
|
573
|
+
if (!naturalW || !naturalH || !shownW || !shownH) return;
|
|
574
|
+
const natural = naturalW / naturalH;
|
|
575
|
+
const shown = shownW / shownH;
|
|
576
|
+
const diff = Math.abs(natural - shown) / natural;
|
|
577
|
+
const fit = getComputedStyle(el).objectFit;
|
|
578
|
+
if (diff <= 0.05 || fit !== 'fill') return;
|
|
579
|
+
findings.push({
|
|
580
|
+
level: 'error',
|
|
581
|
+
rank: ISSUE_RANK.image,
|
|
582
|
+
score: diff * 100,
|
|
583
|
+
kind: 'distorted image',
|
|
584
|
+
text: `distorted image ${Math.round(diff * 100)}% (object-fit: fill)`
|
|
585
|
+
});
|
|
586
|
+
}
|
|
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
|
+
|
|
596
|
+
function coveringPseudo(x, y) {
|
|
597
|
+
const hosts = [document.documentElement, document.body];
|
|
598
|
+
const names = ['::before', '::after'];
|
|
599
|
+
for (let i = 0; i < hosts.length; i++) {
|
|
600
|
+
const host = hosts[i];
|
|
601
|
+
if (!host) continue;
|
|
602
|
+
for (let j = 0; j < names.length; j++) {
|
|
603
|
+
const box = pseudoBox(host, names[j]);
|
|
604
|
+
if (!box || box.pointerEvents === 'none') continue;
|
|
605
|
+
if (x < box.x || y < box.y || x > box.x + box.w || y > box.y + box.h) continue;
|
|
606
|
+
return `${host.tagName.toLowerCase()}${names[j]}`;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return '';
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function pseudoBox(el, pseudo) {
|
|
613
|
+
const style = getComputedStyle(el, pseudo);
|
|
614
|
+
const content = style.content;
|
|
615
|
+
if (!content || content === 'none' || content === 'normal') return null;
|
|
616
|
+
if (style.position !== 'fixed' && style.position !== 'absolute') return null;
|
|
617
|
+
const width = parseFloat(style.width);
|
|
618
|
+
const height = parseFloat(style.height);
|
|
619
|
+
if (!width || !height || width < 2 || height < 2) return null;
|
|
620
|
+
let x = parseFloat(style.left);
|
|
621
|
+
let y = parseFloat(style.top);
|
|
622
|
+
if (!Number.isFinite(x)) x = 0;
|
|
623
|
+
if (!Number.isFinite(y)) y = 0;
|
|
624
|
+
if (style.position === 'absolute') {
|
|
625
|
+
const host = el.getBoundingClientRect();
|
|
626
|
+
x = host.left + x;
|
|
627
|
+
y = host.top + y;
|
|
628
|
+
}
|
|
629
|
+
return { x, y, w: width, h: height, pointerEvents: style.pointerEvents };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function pickElement(x, y) {
|
|
633
|
+
if (typeof document === 'undefined' || typeof document.elementsFromPoint !== 'function') return null;
|
|
634
|
+
const stack = document.elementsFromPoint(x, y);
|
|
635
|
+
for (let i = 0; i < stack.length; i++) {
|
|
636
|
+
const el = stack[i];
|
|
637
|
+
if (!el || isBeeChrome(el)) continue;
|
|
638
|
+
if (el === document.documentElement || el === document.body) continue;
|
|
639
|
+
return el;
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function paintBehind(el) {
|
|
645
|
+
const own = parseColor(getComputedStyle(el).backgroundColor);
|
|
646
|
+
if (own && own.a > 0.04) return own;
|
|
647
|
+
let node = el.parentElement;
|
|
648
|
+
while (node) {
|
|
649
|
+
const bg = parseColor(getComputedStyle(node).backgroundColor);
|
|
650
|
+
if (bg && bg.a > 0.04) return bg;
|
|
651
|
+
node = node.parentElement;
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function opacityZero(el) {
|
|
657
|
+
let node = el;
|
|
658
|
+
while (node && node.nodeType === 1) {
|
|
659
|
+
const value = parseFloat(getComputedStyle(node).opacity);
|
|
660
|
+
if (value === 0) return true;
|
|
661
|
+
node = node.parentElement;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function parseColor(value) {
|
|
667
|
+
const text = String(value || '').trim();
|
|
668
|
+
if (text === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
|
|
669
|
+
const match = text.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+%?))?\s*\)/);
|
|
670
|
+
if (!match) return null;
|
|
671
|
+
let alpha = 1;
|
|
672
|
+
if (match[4] != null) {
|
|
673
|
+
alpha = match[4].endsWith('%') ? parseFloat(match[4]) / 100 : parseFloat(match[4]);
|
|
674
|
+
}
|
|
675
|
+
return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]), a: alpha };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function nearlySame(a, b) {
|
|
679
|
+
return Math.abs(a.r - b.r) < 2
|
|
680
|
+
&& Math.abs(a.g - b.g) < 2
|
|
681
|
+
&& Math.abs(a.b - b.b) < 2
|
|
682
|
+
&& Math.abs(a.a - b.a) < 0.05;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function hasOwnText(el) {
|
|
686
|
+
const tag = el.tagName;
|
|
687
|
+
if (tag === 'BUTTON' || tag === 'A' || tag === 'LABEL' || tag === 'INPUT' || tag === 'TEXTAREA') return true;
|
|
688
|
+
if (el.childElementCount > 0) return false;
|
|
689
|
+
return Boolean((el.textContent || '').trim());
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function cssPath(el) {
|
|
693
|
+
if (!el || !el.tagName) return 'node';
|
|
694
|
+
const tag = el.tagName.toLowerCase();
|
|
695
|
+
if (el.id) return `${tag}#${el.id}`;
|
|
696
|
+
const classes = el.classList ? [...el.classList].slice(0, 2) : [];
|
|
697
|
+
if (classes.length) return `${tag}.${classes.join('.')}`;
|
|
698
|
+
return tag;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function isSkippedTag(el) {
|
|
702
|
+
const tag = el.tagName;
|
|
703
|
+
return tag === 'SCRIPT' || tag === 'STYLE' || tag === 'LINK' || tag === 'META';
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function isTypingTarget(target) {
|
|
707
|
+
if (!target || !target.tagName) return false;
|
|
708
|
+
const tag = target.tagName;
|
|
709
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
710
|
+
return Boolean(target.isContentEditable);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function touchesSheet(record) {
|
|
714
|
+
if (!record) return false;
|
|
715
|
+
if (record.type === 'characterData') return record.target?.parentElement?.nodeName === 'STYLE';
|
|
716
|
+
if (record.type === 'attributes') {
|
|
717
|
+
const name = record.target?.nodeName;
|
|
718
|
+
return name === 'STYLE' || name === 'LINK';
|
|
719
|
+
}
|
|
720
|
+
const nodes = [record.target];
|
|
721
|
+
if (record.addedNodes) {
|
|
722
|
+
for (let i = 0; i < record.addedNodes.length; i++) nodes.push(record.addedNodes[i]);
|
|
723
|
+
}
|
|
724
|
+
if (record.removedNodes) {
|
|
725
|
+
for (let i = 0; i < record.removedNodes.length; i++) nodes.push(record.removedNodes[i]);
|
|
726
|
+
}
|
|
727
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
728
|
+
const node = nodes[i];
|
|
729
|
+
if (!node || node.nodeType !== 1) continue;
|
|
730
|
+
if (node.nodeName === 'STYLE' || node.nodeName === 'LINK') return true;
|
|
731
|
+
if (typeof node.querySelector === 'function' && node.querySelector('style, link')) return true;
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function isBeeChrome(node) {
|
|
737
|
+
let el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
738
|
+
while (el) {
|
|
739
|
+
if (el.tagName === 'BEE-LADYBUG-OVERLAY') return true;
|
|
740
|
+
if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
|
|
741
|
+
if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
|
|
742
|
+
if (el.hasAttribute?.('data-bee-ladybug-css')) return true;
|
|
743
|
+
el = el.parentElement;
|
|
744
|
+
}
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function makeBox(kind, border, fill) {
|
|
749
|
+
const node = document.createElement('div');
|
|
750
|
+
node.setAttribute('data-bee-ladybug-css', kind);
|
|
751
|
+
node.style.position = 'fixed';
|
|
752
|
+
node.style.pointerEvents = 'none';
|
|
753
|
+
node.style.zIndex = '2147483644';
|
|
754
|
+
node.style.display = 'none';
|
|
755
|
+
node.style.boxSizing = 'border-box';
|
|
756
|
+
node.style.border = border;
|
|
757
|
+
node.style.background = fill;
|
|
758
|
+
return node;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function placeBox(node, el) {
|
|
762
|
+
if (!node) return;
|
|
763
|
+
if (!el || typeof el.getBoundingClientRect !== 'function' || isBeeChrome(el)) {
|
|
764
|
+
node.style.display = 'none';
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const rect = el.getBoundingClientRect();
|
|
768
|
+
node.style.display = 'block';
|
|
769
|
+
node.style.left = `${rect.left}px`;
|
|
770
|
+
node.style.top = `${rect.top}px`;
|
|
771
|
+
node.style.width = `${rect.width}px`;
|
|
772
|
+
node.style.height = `${rect.height}px`;
|
|
773
|
+
}
|
|
@@ -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.
|
|
12
|
+
export const BEE_LADYBUG_VERSION = '0.5.1';
|
|
13
13
|
|
|
14
14
|
export const CORE_DEFAULTS = Object.freeze({
|
|
15
15
|
toggleKey: 'F2',
|
|
@@ -98,6 +98,7 @@ export class BeeLadybugCore {
|
|
|
98
98
|
this.#activeAssistant = null;
|
|
99
99
|
this.#askBusy = false;
|
|
100
100
|
this.#adapters = new Map();
|
|
101
|
+
this.#detail = [];
|
|
101
102
|
|
|
102
103
|
this.ui = null;
|
|
103
104
|
if (cfg.ui !== false && typeof document !== 'undefined') {
|
|
@@ -132,6 +133,8 @@ export class BeeLadybugCore {
|
|
|
132
133
|
/** @type {string | null} */
|
|
133
134
|
#activeAssistant;
|
|
134
135
|
#askBusy;
|
|
136
|
+
/** @type {Array<{ text: string, level?: string, onSelect?: () => void }>} */
|
|
137
|
+
#detail;
|
|
135
138
|
/** @type {Map<string, { name: string, label: string, enabled: boolean, enable: Function, disable: Function }>} */
|
|
136
139
|
#adapters;
|
|
137
140
|
|
|
@@ -289,7 +292,7 @@ export class BeeLadybugCore {
|
|
|
289
292
|
* Register a toggleable diagnostic adapter. The core stores hooks only —
|
|
290
293
|
* it does not import or special-case adapter modules.
|
|
291
294
|
* @param {string} name
|
|
292
|
-
* @param {{ enable: () => void, disable: () => void, label?: string }} hooks
|
|
295
|
+
* @param {{ enable: () => void, disable: () => void, label?: string, hotkey?: string, enabled?: boolean }} hooks
|
|
293
296
|
*/
|
|
294
297
|
registerAdapter(name, hooks = {}) {
|
|
295
298
|
const key = String(name || '').trim();
|
|
@@ -301,14 +304,24 @@ export class BeeLadybugCore {
|
|
|
301
304
|
return this;
|
|
302
305
|
}
|
|
303
306
|
const label = String(hooks.label || key).trim() || key;
|
|
307
|
+
const hotkey = typeof hooks.hotkey === 'string' ? hooks.hotkey : '';
|
|
308
|
+
const enabled = hooks.enabled !== false;
|
|
304
309
|
this.#adapters.set(key, {
|
|
305
310
|
name: key,
|
|
306
311
|
label,
|
|
307
|
-
|
|
312
|
+
hotkey,
|
|
313
|
+
enabled,
|
|
308
314
|
enable: hooks.enable,
|
|
309
315
|
disable: hooks.disable
|
|
310
316
|
});
|
|
311
|
-
this.sendData('sys', {
|
|
317
|
+
this.sendData('sys', {
|
|
318
|
+
op: 'adapter',
|
|
319
|
+
name: key,
|
|
320
|
+
registered: true,
|
|
321
|
+
label,
|
|
322
|
+
hotkey,
|
|
323
|
+
enabled
|
|
324
|
+
});
|
|
312
325
|
return this;
|
|
313
326
|
}
|
|
314
327
|
|
|
@@ -328,10 +341,21 @@ export class BeeLadybugCore {
|
|
|
328
341
|
return [...this.#adapters.values()].map((entry) => ({
|
|
329
342
|
name: entry.name,
|
|
330
343
|
label: entry.label,
|
|
344
|
+
hotkey: entry.hotkey || '',
|
|
331
345
|
enabled: entry.enabled
|
|
332
346
|
}));
|
|
333
347
|
}
|
|
334
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Clickable rows inside the overlay. The core does not interpret them.
|
|
351
|
+
* @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
|
|
352
|
+
*/
|
|
353
|
+
publishDetail(items) {
|
|
354
|
+
this.#detail = Array.isArray(items) ? items : [];
|
|
355
|
+
this.ui?.setDetail(this.#detail);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
|
|
335
359
|
/**
|
|
336
360
|
* @param {string} name
|
|
337
361
|
*/
|
|
@@ -438,6 +462,7 @@ export class BeeLadybugCore {
|
|
|
438
462
|
this.#activeAssistant = null;
|
|
439
463
|
this.#askBusy = false;
|
|
440
464
|
this.#adapters.clear();
|
|
465
|
+
this.#detail = [];
|
|
441
466
|
this.ui?.destroy();
|
|
442
467
|
this.ui = null;
|
|
443
468
|
return this;
|
|
@@ -720,10 +745,27 @@ export class BeeLadybugCore {
|
|
|
720
745
|
if (code === this.freezeKey) {
|
|
721
746
|
event.preventDefault();
|
|
722
747
|
this.toggleFreeze();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (isTypingTarget(event.target)) return;
|
|
752
|
+
|
|
753
|
+
for (const entry of this.#adapters.values()) {
|
|
754
|
+
if (!entry.hotkey || code !== entry.hotkey) continue;
|
|
755
|
+
event.preventDefault();
|
|
756
|
+
this.toggleAdapter(entry.name);
|
|
757
|
+
return;
|
|
723
758
|
}
|
|
724
759
|
}
|
|
725
760
|
}
|
|
726
761
|
|
|
762
|
+
function isTypingTarget(target) {
|
|
763
|
+
if (!target || typeof target !== 'object') return false;
|
|
764
|
+
const tag = target.tagName;
|
|
765
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
766
|
+
return Boolean(target.isContentEditable);
|
|
767
|
+
}
|
|
768
|
+
|
|
727
769
|
function asObject(payload) {
|
|
728
770
|
if (payload == null) return {};
|
|
729
771
|
if (typeof payload !== 'object') return { value: payload, message: String(payload) };
|
package/src/core/UIOverlay.js
CHANGED
|
@@ -255,6 +255,39 @@ const STYLES = `
|
|
|
255
255
|
.bl-adapters:empty {
|
|
256
256
|
display: none;
|
|
257
257
|
}
|
|
258
|
+
.bl-hotkeys {
|
|
259
|
+
display: inline-flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
gap: 6px;
|
|
262
|
+
}
|
|
263
|
+
.bl-hotkeys:empty {
|
|
264
|
+
display: none;
|
|
265
|
+
}
|
|
266
|
+
.bl-detail {
|
|
267
|
+
max-height: 132px;
|
|
268
|
+
overflow: auto;
|
|
269
|
+
background: #07080c;
|
|
270
|
+
border-top: 1px solid rgba(255, 59, 78, 0.22);
|
|
271
|
+
}
|
|
272
|
+
.bl-detail[hidden] {
|
|
273
|
+
display: none;
|
|
274
|
+
}
|
|
275
|
+
.bl-detail button {
|
|
276
|
+
display: block;
|
|
277
|
+
width: 100%;
|
|
278
|
+
margin: 0;
|
|
279
|
+
padding: 4px 10px;
|
|
280
|
+
border: 0;
|
|
281
|
+
background: transparent;
|
|
282
|
+
color: #d8dce6;
|
|
283
|
+
font: 11px ui-monospace, Consolas, monospace;
|
|
284
|
+
line-height: 1.4;
|
|
285
|
+
text-align: left;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
}
|
|
288
|
+
.bl-detail button[data-level="warn"] { color: #ffd36a; }
|
|
289
|
+
.bl-detail button[data-level="error"] { color: #ff6b7d; }
|
|
290
|
+
.bl-detail button:hover { background: rgba(255, 255, 255, 0.04); }
|
|
258
291
|
.bl-adapter {
|
|
259
292
|
appearance: none;
|
|
260
293
|
background: #16181f;
|
|
@@ -331,6 +364,8 @@ export class UIOverlay {
|
|
|
331
364
|
this.#mounted = false;
|
|
332
365
|
this.#providerSig = '';
|
|
333
366
|
this.#adapterSig = '';
|
|
367
|
+
this.#hotkeySig = '';
|
|
368
|
+
this.#detail = [];
|
|
334
369
|
this.#flashAt = new Map();
|
|
335
370
|
this.#flashTimers = new Map();
|
|
336
371
|
}
|
|
@@ -343,6 +378,8 @@ export class UIOverlay {
|
|
|
343
378
|
#mounted;
|
|
344
379
|
#providerSig;
|
|
345
380
|
#adapterSig;
|
|
381
|
+
#hotkeySig;
|
|
382
|
+
#detail;
|
|
346
383
|
#flashAt;
|
|
347
384
|
#flashTimers;
|
|
348
385
|
|
|
@@ -393,11 +430,13 @@ export class UIOverlay {
|
|
|
393
430
|
</div>
|
|
394
431
|
</div>
|
|
395
432
|
<div class="bl-hud" data-el="hud"></div>
|
|
433
|
+
<div class="bl-detail" data-el="detail" hidden></div>
|
|
396
434
|
<div class="bl-console" data-el="console" role="log" aria-live="polite"></div>
|
|
397
435
|
<div class="bl-foot">
|
|
398
436
|
<button class="bl-btn" data-act="slow" type="button">F3 SLOW</button>
|
|
399
437
|
<button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
|
|
400
438
|
<button class="bl-btn" data-act="live" type="button">1x LIVE</button>
|
|
439
|
+
<span class="bl-hotkeys" data-el="hotkeys"></span>
|
|
401
440
|
<button class="bl-btn" data-act="ask" type="button">ASK AI</button>
|
|
402
441
|
<span class="bl-providers" data-el="providers"></span>
|
|
403
442
|
<span class="bl-adapters" data-el="adapters"></span>
|
|
@@ -419,8 +458,10 @@ export class UIOverlay {
|
|
|
419
458
|
graphStats: this.#root.querySelector('[data-el="graphStats"]'),
|
|
420
459
|
hud: this.#root.querySelector('[data-el="hud"]'),
|
|
421
460
|
console: this.#root.querySelector('[data-el="console"]'),
|
|
461
|
+
detail: this.#root.querySelector('[data-el="detail"]'),
|
|
422
462
|
providers: this.#root.querySelector('[data-el="providers"]'),
|
|
423
|
-
adapters: this.#root.querySelector('[data-el="adapters"]')
|
|
463
|
+
adapters: this.#root.querySelector('[data-el="adapters"]'),
|
|
464
|
+
hotkeys: this.#root.querySelector('[data-el="hotkeys"]')
|
|
424
465
|
};
|
|
425
466
|
|
|
426
467
|
this.#root.querySelector('.bl-foot').addEventListener('click', (event) => {
|
|
@@ -435,6 +476,13 @@ export class UIOverlay {
|
|
|
435
476
|
else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
|
|
436
477
|
});
|
|
437
478
|
|
|
479
|
+
this.#root.querySelector('.bl-detail')?.addEventListener('click', (event) => {
|
|
480
|
+
const row = event.target.closest('[data-detail]');
|
|
481
|
+
if (!row) return;
|
|
482
|
+
const item = this.#detail[Number(row.getAttribute('data-detail'))];
|
|
483
|
+
if (item && typeof item.onSelect === 'function') item.onSelect();
|
|
484
|
+
});
|
|
485
|
+
|
|
438
486
|
this.#els.console.addEventListener('scroll', () => {
|
|
439
487
|
const el = this.#els.console;
|
|
440
488
|
this.#stickBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
|
|
@@ -456,6 +504,8 @@ export class UIOverlay {
|
|
|
456
504
|
this.#mounted = false;
|
|
457
505
|
this.#providerSig = '';
|
|
458
506
|
this.#adapterSig = '';
|
|
507
|
+
this.#hotkeySig = '';
|
|
508
|
+
this.#detail = [];
|
|
459
509
|
this.#clearFlashTimers();
|
|
460
510
|
this.#flashAt.clear();
|
|
461
511
|
return this;
|
|
@@ -473,6 +523,29 @@ export class UIOverlay {
|
|
|
473
523
|
return this;
|
|
474
524
|
}
|
|
475
525
|
|
|
526
|
+
/**
|
|
527
|
+
* @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
|
|
528
|
+
*/
|
|
529
|
+
setDetail(items) {
|
|
530
|
+
if (!this.#els?.detail) return this;
|
|
531
|
+
const list = Array.isArray(items) ? items : [];
|
|
532
|
+
this.#detail = list;
|
|
533
|
+
const box = this.#els.detail;
|
|
534
|
+
if (!list.length) {
|
|
535
|
+
box.innerHTML = '';
|
|
536
|
+
box.hidden = true;
|
|
537
|
+
return this;
|
|
538
|
+
}
|
|
539
|
+
let html = '';
|
|
540
|
+
for (let i = 0; i < list.length; i++) {
|
|
541
|
+
const level = list[i].level === 'error' || list[i].level === 'warn' ? list[i].level : 'info';
|
|
542
|
+
html += `<button type="button" data-detail="${i}" data-level="${level}">${escapeHtml(list[i].text)}</button>`;
|
|
543
|
+
}
|
|
544
|
+
box.hidden = false;
|
|
545
|
+
box.innerHTML = html;
|
|
546
|
+
return this;
|
|
547
|
+
}
|
|
548
|
+
|
|
476
549
|
onPacket(packet) {
|
|
477
550
|
if (!this.#els || !packet) return;
|
|
478
551
|
if (packet.type === 'sys') return;
|
|
@@ -508,6 +581,7 @@ export class UIOverlay {
|
|
|
508
581
|
this.#renderHud(state.hud);
|
|
509
582
|
this.#syncButtons(state);
|
|
510
583
|
this.#syncProviders(state);
|
|
584
|
+
this.#syncHotkeys(state);
|
|
511
585
|
this.#syncAdapters(state);
|
|
512
586
|
|
|
513
587
|
if (this.#stickBottom) {
|
|
@@ -584,10 +658,26 @@ export class UIOverlay {
|
|
|
584
658
|
wrap.innerHTML = html;
|
|
585
659
|
}
|
|
586
660
|
|
|
661
|
+
#syncHotkeys(state) {
|
|
662
|
+
const wrap = this.#els.hotkeys;
|
|
663
|
+
if (!wrap) return;
|
|
664
|
+
const list = (Array.isArray(state.adapters) ? state.adapters : []).filter((entry) => entry.hotkey);
|
|
665
|
+
const sig = list.map((entry) => `${entry.name}\0${entry.label}\0${entry.enabled ? 1 : 0}`).join('\n');
|
|
666
|
+
if (sig === this.#hotkeySig) return;
|
|
667
|
+
this.#hotkeySig = sig;
|
|
668
|
+
let html = '';
|
|
669
|
+
for (let i = 0; i < list.length; i++) {
|
|
670
|
+
const entry = list[i];
|
|
671
|
+
const hot = entry.enabled ? 'true' : 'false';
|
|
672
|
+
html += `<button class="bl-btn" data-act="adapter" data-name="${escapeHtml(entry.name)}" data-hot="${hot}" type="button">${escapeHtml(entry.label)}</button>`;
|
|
673
|
+
}
|
|
674
|
+
wrap.innerHTML = html;
|
|
675
|
+
}
|
|
676
|
+
|
|
587
677
|
#syncAdapters(state) {
|
|
588
678
|
const wrap = this.#els.adapters;
|
|
589
679
|
if (!wrap) return;
|
|
590
|
-
const list = Array.isArray(state.adapters) ? state.adapters : [];
|
|
680
|
+
const list = (Array.isArray(state.adapters) ? state.adapters : []).filter((entry) => !entry.hotkey);
|
|
591
681
|
const sig = list.map((entry) => `${entry.name}\0${entry.label}\0${entry.enabled ? 1 : 0}`).join('\n');
|
|
592
682
|
if (sig === this.#adapterSig) return;
|
|
593
683
|
this.#adapterSig = sig;
|
package/src/index.d.ts
CHANGED
|
@@ -112,11 +112,14 @@ export interface AdapterHooks {
|
|
|
112
112
|
enable(): void;
|
|
113
113
|
disable(): void;
|
|
114
114
|
label?: string;
|
|
115
|
+
hotkey?: string;
|
|
116
|
+
enabled?: boolean;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
export interface AdapterInfo {
|
|
118
120
|
name: string;
|
|
119
121
|
label: string;
|
|
122
|
+
hotkey: string;
|
|
120
123
|
enabled: boolean;
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -174,6 +177,13 @@ export class BeeLadybugCore {
|
|
|
174
177
|
getAdapters(): AdapterInfo[];
|
|
175
178
|
toggleAdapter(name: string): this;
|
|
176
179
|
setAdapterEnabled(name: string, enabled: boolean): this;
|
|
180
|
+
publishDetail(items: DetailItem[]): this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface DetailItem {
|
|
184
|
+
text: string;
|
|
185
|
+
level?: 'info' | 'warn' | 'error';
|
|
186
|
+
onSelect?: () => void;
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
export class UIOverlay {
|
|
@@ -186,6 +196,7 @@ export class UIOverlay {
|
|
|
186
196
|
onPacket(packet: BeePacket): void;
|
|
187
197
|
onClear(channel?: string): void;
|
|
188
198
|
render(): void;
|
|
199
|
+
setDetail(items: DetailItem[]): this;
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
export interface CanvasEntity {
|
|
@@ -272,6 +283,30 @@ export class WebDOMAdapter {
|
|
|
272
283
|
pump(): this;
|
|
273
284
|
}
|
|
274
285
|
|
|
286
|
+
export interface CSSAdapterOptions {
|
|
287
|
+
source?: string;
|
|
288
|
+
hotkey?: string;
|
|
289
|
+
scanKey?: string;
|
|
290
|
+
label?: string;
|
|
291
|
+
maxScan?: number;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export const CSS_ADAPTER_DEFAULTS: Required<CSSAdapterOptions>;
|
|
295
|
+
|
|
296
|
+
export class CSSAdapter {
|
|
297
|
+
core: BeeLadybugCore | null;
|
|
298
|
+
source: string;
|
|
299
|
+
hotkey: string;
|
|
300
|
+
scanKey: string;
|
|
301
|
+
label: string;
|
|
302
|
+
maxScan: number;
|
|
303
|
+
readonly active: boolean;
|
|
304
|
+
constructor(core: BeeLadybugCore, options?: CSSAdapterOptions);
|
|
305
|
+
attach(): this;
|
|
306
|
+
detach(): this;
|
|
307
|
+
scan(): this;
|
|
308
|
+
}
|
|
309
|
+
|
|
275
310
|
export interface AntAdapterOptions {
|
|
276
311
|
root?: ParentNode | null;
|
|
277
312
|
threshold?: number;
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { BeeLadybugCore, BEE_LADYBUG_VERSION, CORE_DEFAULTS, WELL_KNOWN_TYPES, f
|
|
|
2
2
|
export { UIOverlay } from './core/UIOverlay.js';
|
|
3
3
|
export { CanvasAdapter, CANVAS_ADAPTER_DEFAULTS } from './adapters/CanvasAdapter.js';
|
|
4
4
|
export { WebDOMAdapter, WEB_DOM_ADAPTER_DEFAULTS } from './adapters/WebDOMAdapter.js';
|
|
5
|
+
export { CSSAdapter, CSS_ADAPTER_DEFAULTS } from './adapters/CSSAdapter.js';
|
|
5
6
|
export { AntAdapter, ANT_ADAPTER_DEFAULTS } from './adapters/AntAdapter.js';
|
|
6
7
|
export { SpiderAdapter, SPIDER_ADAPTER_DEFAULTS } from './adapters/SpiderAdapter.js';
|
|
7
8
|
export { PythonBridge, PYTHON_BRIDGE_DEFAULTS } from './adapters/python/PythonBridge.js';
|