bee-ladybug 0.4.0 → 0.5.0
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 +34 -5
- package/package.json +1 -1
- package/src/adapters/CSSAdapter.js +647 -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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
# BeeLadybug
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
<p align="center">
|
|
5
|
+
<img src="https://raw.githubusercontent.com/antonioprosperi2-svg/BeeLadybug-universal/main/docs/logo.jpg" alt="BeeLadybug" width="720">
|
|
6
|
+
</p>
|
|
5
7
|
|
|
6
8
|
Universal telemetry and visual inspection overlay.
|
|
7
9
|
|
|
@@ -37,6 +39,7 @@ bee-ladybug/
|
|
|
37
39
|
│ └── adapters/
|
|
38
40
|
│ ├── CanvasAdapter.js
|
|
39
41
|
│ ├── WebDOMAdapter.js
|
|
42
|
+
│ ├── CSSAdapter.js
|
|
40
43
|
│ ├── AntAdapter.js
|
|
41
44
|
│ ├── SpiderAdapter.js
|
|
42
45
|
│ └── python/PythonBridge.js
|
|
@@ -44,10 +47,12 @@ bee-ladybug/
|
|
|
44
47
|
│ ├── core.html
|
|
45
48
|
│ ├── canvas-sandbox.js
|
|
46
49
|
│ ├── dom.html
|
|
50
|
+
│ ├── css-hunt.html
|
|
47
51
|
│ ├── python.html
|
|
48
52
|
│ └── python/sender.py
|
|
49
53
|
├── tsconfig.json
|
|
50
54
|
├── package.json
|
|
55
|
+
├── CHANGELOG.md
|
|
51
56
|
├── LICENSE
|
|
52
57
|
├── index.html
|
|
53
58
|
└── README.md
|
|
@@ -119,7 +124,7 @@ bee.subscribe((packet) => {
|
|
|
119
124
|
});
|
|
120
125
|
```
|
|
121
126
|
|
|
122
|
-

|
|
127
|
+

|
|
123
128
|
|
|
124
129
|
## Tasti
|
|
125
130
|
|
|
@@ -128,6 +133,8 @@ bee.subscribe((packet) => {
|
|
|
128
133
|
| `F2` | mostra / nascondi overlay (e hitbox canvas) |
|
|
129
134
|
| `F3` | slow-motion del tempo fittizio (`0.25x` / `1x`) |
|
|
130
135
|
| `F4` | freeze / run del tempo fittizio |
|
|
136
|
+
| `F6` | CSS hunt, se `CSSAdapter` è attaccato |
|
|
137
|
+
| `S` | scansione overflow / immagini, solo con la hunt accesa |
|
|
131
138
|
|
|
132
139
|
Niente `F12`, niente tilde: sui layout italiani la tilde non è un tasto unico.
|
|
133
140
|
|
|
@@ -175,6 +182,28 @@ adapter.attach();
|
|
|
175
182
|
|
|
176
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.
|
|
177
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 esegue overflow e immagini su tutta la pagina e lista i casi peggiori. Un click sulla riga disegna il contorno rosso. 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
|
+
|
|
178
207
|
## Registro adapter (overlay)
|
|
179
208
|
|
|
180
209
|
Il Core non conosce ANT/SPIDER. Gli adapter si iscrivono con hook propri; l'overlay disegna un toggle per ciascuno.
|
|
@@ -249,7 +278,7 @@ python examples/python/sender.py
|
|
|
249
278
|
|
|
250
279
|
Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
|
|
251
280
|
|
|
252
|
-

|
|
281
|
+

|
|
253
282
|
|
|
254
283
|
## npm / TypeScript
|
|
255
284
|
|
|
@@ -275,7 +304,7 @@ npm login
|
|
|
275
304
|
npm publish --access public
|
|
276
305
|
```
|
|
277
306
|
|
|
278
|
-
Versione runtime e pacchetto: **0.4.
|
|
307
|
+
Versione runtime e pacchetto: **0.4.1** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
|
|
279
308
|
|
|
280
309
|
## Assistente AI (gancio, nessun vendor)
|
|
281
310
|
|
|
@@ -348,7 +377,7 @@ bee.destroy();
|
|
|
348
377
|
## Roadmap
|
|
349
378
|
|
|
350
379
|
1. **Ora** — Core, adapter, registro toggle overlay, Ant/Spider, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
|
|
351
|
-
2. **npm 0.4.
|
|
380
|
+
2. **npm 0.4.1** — `npm publish`.
|
|
352
381
|
|
|
353
382
|
## Come contribuire
|
|
354
383
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,647 @@
|
|
|
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
|
+
/** Checks 2 and 5 across the document. Click a row to outline it. */
|
|
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 offenders = [];
|
|
121
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
122
|
+
const el = nodes[i];
|
|
123
|
+
if (!el || isBeeChrome(el) || isSkippedTag(el)) continue;
|
|
124
|
+
const findings = [];
|
|
125
|
+
pushOverflow(el, findings);
|
|
126
|
+
pushImage(el, findings);
|
|
127
|
+
if (!findings.length) continue;
|
|
128
|
+
let score = 0;
|
|
129
|
+
for (let j = 0; j < findings.length; j++) score += findings[j].score;
|
|
130
|
+
offenders.push({ el, findings, score });
|
|
131
|
+
}
|
|
132
|
+
offenders.sort((a, b) => b.score - a.score);
|
|
133
|
+
const top = offenders.slice(0, this.maxScan);
|
|
134
|
+
const items = top.map((entry) => ({
|
|
135
|
+
level: 'error',
|
|
136
|
+
text: `${cssPath(entry.el)} — ${entry.findings.map((hit) => hit.text).join('; ')}`,
|
|
137
|
+
onSelect: () => this.#pin(entry.el)
|
|
138
|
+
}));
|
|
139
|
+
this.#lastEl = null;
|
|
140
|
+
this.core?.publishDetail(items);
|
|
141
|
+
this.core?.sendData('state', {
|
|
142
|
+
source: this.source,
|
|
143
|
+
key: 'css.scan',
|
|
144
|
+
value: String(top.length)
|
|
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.'
|
|
151
|
+
});
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#enable() {
|
|
156
|
+
if (this.#active || typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
157
|
+
this.#active = true;
|
|
158
|
+
this.#sheetsDirty = true;
|
|
159
|
+
this.#mountBoxes();
|
|
160
|
+
window.addEventListener('mousemove', this.#onMove, true);
|
|
161
|
+
window.addEventListener('keydown', this.#onKey);
|
|
162
|
+
window.addEventListener('scroll', this.#onScroll, true);
|
|
163
|
+
window.addEventListener('resize', this.#onScroll);
|
|
164
|
+
if (typeof MutationObserver !== 'undefined' && document.documentElement) {
|
|
165
|
+
this.#observer = new MutationObserver(this.#onMutate);
|
|
166
|
+
this.#observer.observe(document.documentElement, {
|
|
167
|
+
childList: true,
|
|
168
|
+
subtree: true,
|
|
169
|
+
attributes: true,
|
|
170
|
+
attributeFilter: ['href', 'rel', 'media', 'disabled', 'type'],
|
|
171
|
+
characterData: true
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
this.#rebuildSheets();
|
|
175
|
+
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'on' });
|
|
176
|
+
this.core?.sendData('log', {
|
|
177
|
+
source: this.source,
|
|
178
|
+
message: 'CSS hunt on. Hover an element, or press S to scan.'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#disable() {
|
|
183
|
+
if (!this.#active) return;
|
|
184
|
+
this.#active = false;
|
|
185
|
+
if (typeof window !== 'undefined') {
|
|
186
|
+
window.removeEventListener('mousemove', this.#onMove, true);
|
|
187
|
+
window.removeEventListener('keydown', this.#onKey);
|
|
188
|
+
window.removeEventListener('scroll', this.#onScroll, true);
|
|
189
|
+
window.removeEventListener('resize', this.#onScroll);
|
|
190
|
+
}
|
|
191
|
+
if (this.#rafId && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(this.#rafId);
|
|
192
|
+
this.#rafId = 0;
|
|
193
|
+
if (this.#observer) this.#observer.disconnect();
|
|
194
|
+
this.#observer = null;
|
|
195
|
+
this.#lastEl = null;
|
|
196
|
+
this.#pinned = null;
|
|
197
|
+
this.#unmountBoxes();
|
|
198
|
+
this.core?.publishDetail([]);
|
|
199
|
+
this.core?.sendData('state', { source: this.source, key: 'css.hunt', value: 'off' });
|
|
200
|
+
this.core?.sendData('log', { source: this.source, message: 'CSS hunt off.' });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#handleKey(event) {
|
|
204
|
+
if (!this.#active || event.repeat) return;
|
|
205
|
+
if (event.code !== this.scanKey) return;
|
|
206
|
+
if (isTypingTarget(event.target)) return;
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
this.scan();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#schedule() {
|
|
212
|
+
if (!this.#active || this.#rafId || typeof requestAnimationFrame !== 'function') return;
|
|
213
|
+
this.#rafId = requestAnimationFrame(() => {
|
|
214
|
+
this.#rafId = 0;
|
|
215
|
+
if (!this.#active) return;
|
|
216
|
+
if (this.#sheetsDirty) this.#rebuildSheets();
|
|
217
|
+
this.#inspect(this.#x, this.#y);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#rebuildSheets() {
|
|
222
|
+
this.#sheetsDirty = false;
|
|
223
|
+
const built = collectImportantRules();
|
|
224
|
+
this.#rules = built.rules;
|
|
225
|
+
this.#unreadable = built.unreadable;
|
|
226
|
+
for (let i = 0; i < built.unreadable.length; i++) {
|
|
227
|
+
this.core?.sendData('warn', {
|
|
228
|
+
source: this.source,
|
|
229
|
+
message: `unreadable sheet: ${built.unreadable[i]}`
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#inspect(x, y) {
|
|
235
|
+
const el = pickElement(x, y);
|
|
236
|
+
placeBox(this.#hoverBox, el);
|
|
237
|
+
if (el === this.#lastEl) return;
|
|
238
|
+
this.#lastEl = el;
|
|
239
|
+
if (!el) {
|
|
240
|
+
this.core?.publishDetail([]);
|
|
241
|
+
this.core?.sendData('state', { source: this.source, key: 'css.target', value: '—' });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const findings = [];
|
|
245
|
+
pushImportant(el, this.#rules, findings);
|
|
246
|
+
pushOverflow(el, findings);
|
|
247
|
+
pushInvisible(el, findings);
|
|
248
|
+
pushBlockers(el, findings);
|
|
249
|
+
pushImage(el, findings);
|
|
250
|
+
for (let i = 0; i < this.#unreadable.length; i++) {
|
|
251
|
+
findings.push({
|
|
252
|
+
level: 'warn',
|
|
253
|
+
text: `unreadable sheet: ${this.#unreadable[i]}`,
|
|
254
|
+
score: 0
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const path = cssPath(el);
|
|
258
|
+
this.core?.sendData('state', { source: this.source, key: 'css.target', value: path });
|
|
259
|
+
this.core?.sendData('state', {
|
|
260
|
+
source: this.source,
|
|
261
|
+
key: 'css.hits',
|
|
262
|
+
value: String(findings.length)
|
|
263
|
+
});
|
|
264
|
+
this.core?.publishDetail(findings.map((hit) => ({
|
|
265
|
+
level: hit.level,
|
|
266
|
+
text: `${path} ${hit.text}`,
|
|
267
|
+
onSelect: () => this.#pin(el)
|
|
268
|
+
})));
|
|
269
|
+
if (!findings.length) {
|
|
270
|
+
this.core?.sendData('log', { source: this.source, message: `${path} clean` });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const lead = findings.find((hit) => hit.level === 'error') || findings[0];
|
|
274
|
+
this.core?.sendData(lead.level === 'error' ? 'error' : 'warn', {
|
|
275
|
+
source: this.source,
|
|
276
|
+
message: `${path} ${lead.text}`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#pin(el) {
|
|
281
|
+
this.#pinned = el;
|
|
282
|
+
placeBox(this.#pinBox, el);
|
|
283
|
+
if (el && typeof el.scrollIntoView === 'function') {
|
|
284
|
+
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#placeBoxes() {
|
|
289
|
+
placeBox(this.#hoverBox, this.#lastEl);
|
|
290
|
+
placeBox(this.#pinBox, this.#pinned);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#mountBoxes() {
|
|
294
|
+
if (typeof document === 'undefined' || this.#hoverBox) return;
|
|
295
|
+
this.#hoverBox = makeBox('hover', '1px solid #ffd36a', 'transparent');
|
|
296
|
+
this.#pinBox = makeBox('pin', '2px solid #ff3b4e', 'rgba(255, 59, 78, 0.12)');
|
|
297
|
+
document.body.appendChild(this.#hoverBox);
|
|
298
|
+
document.body.appendChild(this.#pinBox);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#unmountBoxes() {
|
|
302
|
+
this.#hoverBox?.remove();
|
|
303
|
+
this.#pinBox?.remove();
|
|
304
|
+
this.#hoverBox = null;
|
|
305
|
+
this.#pinBox = null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function collectImportantRules() {
|
|
310
|
+
const rules = [];
|
|
311
|
+
const unreadable = [];
|
|
312
|
+
if (typeof document === 'undefined') return { rules, unreadable };
|
|
313
|
+
const sheets = document.styleSheets;
|
|
314
|
+
for (let i = 0; i < sheets.length; i++) {
|
|
315
|
+
const sheet = sheets[i];
|
|
316
|
+
let list = null;
|
|
317
|
+
try {
|
|
318
|
+
list = sheet.cssRules;
|
|
319
|
+
} catch {
|
|
320
|
+
unreadable.push(sheet.href || '<style> inline');
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
walkRules(list, sheetLabel(sheet), rules);
|
|
324
|
+
}
|
|
325
|
+
return { rules, unreadable };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function walkRules(list, source, into) {
|
|
329
|
+
if (!list) return;
|
|
330
|
+
for (let i = 0; i < list.length; i++) {
|
|
331
|
+
const rule = list[i];
|
|
332
|
+
if (!rule) continue;
|
|
333
|
+
if (rule.type === 4 && rule.media) {
|
|
334
|
+
let active = true;
|
|
335
|
+
try {
|
|
336
|
+
active = typeof matchMedia === 'function'
|
|
337
|
+
? matchMedia(rule.media.mediaText).matches
|
|
338
|
+
: true;
|
|
339
|
+
} catch {
|
|
340
|
+
active = true;
|
|
341
|
+
}
|
|
342
|
+
if (active) walkRules(rule.cssRules, source, into);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (rule.type === 1 && rule.selectorText && rule.style) {
|
|
346
|
+
const props = importantProps(rule.style);
|
|
347
|
+
if (props.length) into.push({ selector: rule.selectorText, source, props });
|
|
348
|
+
} else if (rule.cssRules) {
|
|
349
|
+
walkRules(rule.cssRules, source, into);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function importantProps(style) {
|
|
355
|
+
const props = [];
|
|
356
|
+
for (let i = 0; i < style.length; i++) {
|
|
357
|
+
const property = style.item(i);
|
|
358
|
+
if (style.getPropertyPriority(property) !== 'important') continue;
|
|
359
|
+
props.push({ property, value: style.getPropertyValue(property).trim() });
|
|
360
|
+
}
|
|
361
|
+
return props;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function sheetLabel(sheet) {
|
|
365
|
+
if (sheet.href) return sheet.href;
|
|
366
|
+
return '<style> inline';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function pushImportant(el, rules, findings) {
|
|
370
|
+
for (let i = 0; i < rules.length; i++) {
|
|
371
|
+
const rule = rules[i];
|
|
372
|
+
let matched = false;
|
|
373
|
+
try {
|
|
374
|
+
matched = el.matches(rule.selector);
|
|
375
|
+
} catch {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!matched) continue;
|
|
379
|
+
for (let j = 0; j < rule.props.length; j++) {
|
|
380
|
+
const prop = rule.props[j];
|
|
381
|
+
findings.push({
|
|
382
|
+
level: 'warn',
|
|
383
|
+
score: 5,
|
|
384
|
+
text: `!important ${rule.selector} { ${prop.property}: ${prop.value} } @ ${rule.source}`
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function pushOverflow(el, findings) {
|
|
391
|
+
if (!el.getBoundingClientRect || el === document.documentElement || el === document.body) return;
|
|
392
|
+
const rect = el.getBoundingClientRect();
|
|
393
|
+
const view = document.documentElement.clientWidth;
|
|
394
|
+
if (rect.left < -1 || rect.right > view + 1) {
|
|
395
|
+
const past = rect.left < -1
|
|
396
|
+
? `left ${Math.round(rect.left)}px`
|
|
397
|
+
: `right +${Math.round(rect.right - view)}px`;
|
|
398
|
+
findings.push({
|
|
399
|
+
level: 'error',
|
|
400
|
+
score: Math.abs(rect.left < -1 ? rect.left : rect.right - view),
|
|
401
|
+
text: `overflows viewport (${past})`
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const style = getComputedStyle(el);
|
|
405
|
+
const sides = ['top', 'right', 'bottom', 'left'];
|
|
406
|
+
for (let i = 0; i < sides.length; i++) {
|
|
407
|
+
const raw = style.getPropertyValue(`margin-${sides[i]}`);
|
|
408
|
+
const value = parseFloat(raw);
|
|
409
|
+
if (!Number.isFinite(value) || value >= 0) continue;
|
|
410
|
+
findings.push({
|
|
411
|
+
level: 'warn',
|
|
412
|
+
score: Math.abs(value),
|
|
413
|
+
text: `negative margin margin-${sides[i]}: ${raw.trim()}`
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function pushInvisible(el, findings) {
|
|
419
|
+
if (!hasOwnText(el)) return;
|
|
420
|
+
const style = getComputedStyle(el);
|
|
421
|
+
if (opacityZero(el)) {
|
|
422
|
+
findings.push({ level: 'error', score: 30, text: 'invisible text (opacity 0)' });
|
|
423
|
+
}
|
|
424
|
+
const color = parseColor(style.color);
|
|
425
|
+
if (!color || color.a === 0) {
|
|
426
|
+
findings.push({ level: 'error', score: 30, text: 'invisible text (transparent color)' });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const behind = paintBehind(el);
|
|
430
|
+
if (behind && nearlySame(color, behind)) {
|
|
431
|
+
findings.push({ level: 'error', score: 30, text: 'invisible text (color matches background)' });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function pushBlockers(el, findings) {
|
|
436
|
+
if (!el.getBoundingClientRect || typeof document.elementFromPoint !== 'function') return;
|
|
437
|
+
const rect = el.getBoundingClientRect();
|
|
438
|
+
const x = rect.left + rect.width / 2;
|
|
439
|
+
const y = rect.top + rect.height / 2;
|
|
440
|
+
if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) return;
|
|
441
|
+
const pseudo = coveringPseudo(x, y);
|
|
442
|
+
if (pseudo) {
|
|
443
|
+
findings.push({ level: 'error', score: 40, text: `click blocked by ${pseudo}` });
|
|
444
|
+
}
|
|
445
|
+
const hit = document.elementFromPoint(x, y);
|
|
446
|
+
if (!hit || isBeeChrome(hit) || hit === el || el.contains(hit)) return;
|
|
447
|
+
if (pseudo && (hit === document.documentElement || hit === document.body)) return;
|
|
448
|
+
findings.push({ level: 'error', score: 40, text: `click blocked by ${cssPath(hit)}` });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function pushImage(el, findings) {
|
|
452
|
+
if (!el || el.tagName !== 'IMG') return;
|
|
453
|
+
const naturalW = el.naturalWidth;
|
|
454
|
+
const naturalH = el.naturalHeight;
|
|
455
|
+
const shownW = el.clientWidth;
|
|
456
|
+
const shownH = el.clientHeight;
|
|
457
|
+
if (!naturalW || !naturalH || !shownW || !shownH) return;
|
|
458
|
+
const natural = naturalW / naturalH;
|
|
459
|
+
const shown = shownW / shownH;
|
|
460
|
+
const diff = Math.abs(natural - shown) / natural;
|
|
461
|
+
const fit = getComputedStyle(el).objectFit;
|
|
462
|
+
if (diff <= 0.05 || fit !== 'fill') return;
|
|
463
|
+
findings.push({
|
|
464
|
+
level: 'error',
|
|
465
|
+
score: diff * 100,
|
|
466
|
+
text: `distorted image ${Math.round(diff * 100)}% (object-fit: fill)`
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function coveringPseudo(x, y) {
|
|
471
|
+
const hosts = [document.documentElement, document.body];
|
|
472
|
+
const names = ['::before', '::after'];
|
|
473
|
+
for (let i = 0; i < hosts.length; i++) {
|
|
474
|
+
const host = hosts[i];
|
|
475
|
+
if (!host) continue;
|
|
476
|
+
for (let j = 0; j < names.length; j++) {
|
|
477
|
+
const box = pseudoBox(host, names[j]);
|
|
478
|
+
if (!box || box.pointerEvents === 'none') continue;
|
|
479
|
+
if (x < box.x || y < box.y || x > box.x + box.w || y > box.y + box.h) continue;
|
|
480
|
+
return `${host.tagName.toLowerCase()}${names[j]}`;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return '';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function pseudoBox(el, pseudo) {
|
|
487
|
+
const style = getComputedStyle(el, pseudo);
|
|
488
|
+
const content = style.content;
|
|
489
|
+
if (!content || content === 'none' || content === 'normal') return null;
|
|
490
|
+
if (style.position !== 'fixed' && style.position !== 'absolute') return null;
|
|
491
|
+
const width = parseFloat(style.width);
|
|
492
|
+
const height = parseFloat(style.height);
|
|
493
|
+
if (!width || !height || width < 2 || height < 2) return null;
|
|
494
|
+
let x = parseFloat(style.left);
|
|
495
|
+
let y = parseFloat(style.top);
|
|
496
|
+
if (!Number.isFinite(x)) x = 0;
|
|
497
|
+
if (!Number.isFinite(y)) y = 0;
|
|
498
|
+
if (style.position === 'absolute') {
|
|
499
|
+
const host = el.getBoundingClientRect();
|
|
500
|
+
x = host.left + x;
|
|
501
|
+
y = host.top + y;
|
|
502
|
+
}
|
|
503
|
+
return { x, y, w: width, h: height, pointerEvents: style.pointerEvents };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function pickElement(x, y) {
|
|
507
|
+
if (typeof document === 'undefined' || typeof document.elementsFromPoint !== 'function') return null;
|
|
508
|
+
const stack = document.elementsFromPoint(x, y);
|
|
509
|
+
for (let i = 0; i < stack.length; i++) {
|
|
510
|
+
const el = stack[i];
|
|
511
|
+
if (!el || isBeeChrome(el)) continue;
|
|
512
|
+
if (el === document.documentElement || el === document.body) continue;
|
|
513
|
+
return el;
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function paintBehind(el) {
|
|
519
|
+
const own = parseColor(getComputedStyle(el).backgroundColor);
|
|
520
|
+
if (own && own.a > 0.04) return own;
|
|
521
|
+
let node = el.parentElement;
|
|
522
|
+
while (node) {
|
|
523
|
+
const bg = parseColor(getComputedStyle(node).backgroundColor);
|
|
524
|
+
if (bg && bg.a > 0.04) return bg;
|
|
525
|
+
node = node.parentElement;
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function opacityZero(el) {
|
|
531
|
+
let node = el;
|
|
532
|
+
while (node && node.nodeType === 1) {
|
|
533
|
+
const value = parseFloat(getComputedStyle(node).opacity);
|
|
534
|
+
if (value === 0) return true;
|
|
535
|
+
node = node.parentElement;
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function parseColor(value) {
|
|
541
|
+
const text = String(value || '').trim();
|
|
542
|
+
if (text === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
|
|
543
|
+
const match = text.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+%?))?\s*\)/);
|
|
544
|
+
if (!match) return null;
|
|
545
|
+
let alpha = 1;
|
|
546
|
+
if (match[4] != null) {
|
|
547
|
+
alpha = match[4].endsWith('%') ? parseFloat(match[4]) / 100 : parseFloat(match[4]);
|
|
548
|
+
}
|
|
549
|
+
return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]), a: alpha };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function nearlySame(a, b) {
|
|
553
|
+
return Math.abs(a.r - b.r) < 2
|
|
554
|
+
&& Math.abs(a.g - b.g) < 2
|
|
555
|
+
&& Math.abs(a.b - b.b) < 2
|
|
556
|
+
&& Math.abs(a.a - b.a) < 0.05;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function hasOwnText(el) {
|
|
560
|
+
const tag = el.tagName;
|
|
561
|
+
if (tag === 'BUTTON' || tag === 'A' || tag === 'LABEL' || tag === 'INPUT' || tag === 'TEXTAREA') return true;
|
|
562
|
+
if (el.childElementCount > 0) return false;
|
|
563
|
+
return Boolean((el.textContent || '').trim());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function cssPath(el) {
|
|
567
|
+
if (!el || !el.tagName) return 'node';
|
|
568
|
+
const tag = el.tagName.toLowerCase();
|
|
569
|
+
if (el.id) return `${tag}#${el.id}`;
|
|
570
|
+
const classes = el.classList ? [...el.classList].slice(0, 2) : [];
|
|
571
|
+
if (classes.length) return `${tag}.${classes.join('.')}`;
|
|
572
|
+
return tag;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function isSkippedTag(el) {
|
|
576
|
+
const tag = el.tagName;
|
|
577
|
+
return tag === 'SCRIPT' || tag === 'STYLE' || tag === 'LINK' || tag === 'META';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function isTypingTarget(target) {
|
|
581
|
+
if (!target || !target.tagName) return false;
|
|
582
|
+
const tag = target.tagName;
|
|
583
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
584
|
+
return Boolean(target.isContentEditable);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function touchesSheet(record) {
|
|
588
|
+
if (!record) return false;
|
|
589
|
+
if (record.type === 'characterData') return record.target?.parentElement?.nodeName === 'STYLE';
|
|
590
|
+
if (record.type === 'attributes') {
|
|
591
|
+
const name = record.target?.nodeName;
|
|
592
|
+
return name === 'STYLE' || name === 'LINK';
|
|
593
|
+
}
|
|
594
|
+
const nodes = [record.target];
|
|
595
|
+
if (record.addedNodes) {
|
|
596
|
+
for (let i = 0; i < record.addedNodes.length; i++) nodes.push(record.addedNodes[i]);
|
|
597
|
+
}
|
|
598
|
+
if (record.removedNodes) {
|
|
599
|
+
for (let i = 0; i < record.removedNodes.length; i++) nodes.push(record.removedNodes[i]);
|
|
600
|
+
}
|
|
601
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
602
|
+
const node = nodes[i];
|
|
603
|
+
if (!node || node.nodeType !== 1) continue;
|
|
604
|
+
if (node.nodeName === 'STYLE' || node.nodeName === 'LINK') return true;
|
|
605
|
+
if (typeof node.querySelector === 'function' && node.querySelector('style, link')) return true;
|
|
606
|
+
}
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function isBeeChrome(node) {
|
|
611
|
+
let el = node && node.nodeType === 1 ? node : node?.parentElement;
|
|
612
|
+
while (el) {
|
|
613
|
+
if (el.tagName === 'BEE-LADYBUG-OVERLAY') return true;
|
|
614
|
+
if (el.hasAttribute?.('data-bee-ladybug-dom')) return true;
|
|
615
|
+
if (el.hasAttribute?.('data-bee-ladybug-canvas')) return true;
|
|
616
|
+
if (el.hasAttribute?.('data-bee-ladybug-css')) return true;
|
|
617
|
+
el = el.parentElement;
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function makeBox(kind, border, fill) {
|
|
623
|
+
const node = document.createElement('div');
|
|
624
|
+
node.setAttribute('data-bee-ladybug-css', kind);
|
|
625
|
+
node.style.position = 'fixed';
|
|
626
|
+
node.style.pointerEvents = 'none';
|
|
627
|
+
node.style.zIndex = '2147483644';
|
|
628
|
+
node.style.display = 'none';
|
|
629
|
+
node.style.boxSizing = 'border-box';
|
|
630
|
+
node.style.border = border;
|
|
631
|
+
node.style.background = fill;
|
|
632
|
+
return node;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function placeBox(node, el) {
|
|
636
|
+
if (!node) return;
|
|
637
|
+
if (!el || typeof el.getBoundingClientRect !== 'function' || isBeeChrome(el)) {
|
|
638
|
+
node.style.display = 'none';
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const rect = el.getBoundingClientRect();
|
|
642
|
+
node.style.display = 'block';
|
|
643
|
+
node.style.left = `${rect.left}px`;
|
|
644
|
+
node.style.top = `${rect.top}px`;
|
|
645
|
+
node.style.width = `${rect.width}px`;
|
|
646
|
+
node.style.height = `${rect.height}px`;
|
|
647
|
+
}
|
|
@@ -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.0';
|
|
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';
|