bee-ladybug 0.5.2 → 0.5.4
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 +3 -2
- package/package.json +1 -1
- package/src/adapters/CSSAdapter.js +218 -40
- package/src/core/BeeLadybugCore.js +2 -2
- package/src/core/UIOverlay.js +167 -33
- package/src/index.d.ts +2 -0
package/README.MD
CHANGED
|
@@ -195,13 +195,14 @@ css.attach();
|
|
|
195
195
|
|
|
196
196
|
`F6` (pulsante **F6 CSS** accanto a F3/F4) accende la caccia. Sull'elemento sotto il puntatore l'overlay elenca:
|
|
197
197
|
|
|
198
|
-
- regole `!important` che gli corrispondono
|
|
198
|
+
- regole `!important` che gli corrispondono. Un foglio cross-origin è una nota grigia, una volta sola: `unreadable sheet: <href>`
|
|
199
199
|
- overflow del viewport e margini negativi
|
|
200
200
|
- testo trasparente, `opacity: 0`, o colore uguale allo sfondo
|
|
201
201
|
- chi intercetta il click al centro, inclusi `::before` / `::after` fissi su `html` / `body`
|
|
202
|
+
- un antenato che crea uno stacking context (`transform`, `opacity` sotto 1, `filter`, oppure `position` non static con `z-index`). I figli si raggruppano sulla causa: `div.hero creates stacking context (transform) - z-index of 3 children is trapped`. `will-change: transform` o `opacity` è una riga grigia, con la nota sullo `z-index` sulla stessa riga
|
|
202
203
|
- `<img>` con rapporto del file diverso di oltre il 5% dal riquadro disegnato (`getBoundingClientRect`), solo se `object-fit` calcolato è `fill` (il default)
|
|
203
204
|
|
|
204
|
-
`S` con la hunt accesa percorre la pagina. L'overflow resta solo sull'elemento che lo causa: un antenato si salta se un discendente sfora della stessa quantità (±2px), oppure se la larghezza del box ci sta nel viewport e a uscire è un figlio. Righe uguali (stesso selettore e stesso problema) diventano una sola, con il conteggio (`article overflows viewport ×4`). L'ordine è: causa di overflow, click bloccato, testo invisibile, immagine distorta, `!important`. Un click sulla riga disegna il contorno rosso. `P` ferma il pannello sull'elemento corrente,
|
|
205
|
+
`S` con la hunt accesa percorre la pagina. L'overflow resta solo sull'elemento che lo causa: un antenato si salta se un discendente sfora della stessa quantità (±2px), oppure se la larghezza del box ci sta nel viewport e a uscire è un figlio. Righe uguali (stesso selettore e stesso problema) diventano una sola, con il conteggio (`article overflows viewport ×4`). L'ordine è: causa di overflow, click bloccato, testo invisibile, immagine distorta, `!important`. Un click sulla riga disegna il contorno rosso. `P` ferma il pannello sull'elemento corrente. **Issues** raggruppa per causa: `body::after blocks clicks on 10 elements` è una riga rossa, e il click la apre sull'elenco degli elementi. In cima alla sezione, la legenda dice rosso = bug e grigio = info. Un foglio illeggibile è una riga grigia, una per foglio, e non entra tra i bug. L'overlay resta alto al massimo metà schermo, scorre al suo interno, e le sezioni Status, Issues, Inspect ed Events si chiudono. In console un elemento pulito non viene scritto, e con la hunt accesa restano fuori anche `hover` e `mutate attr` del DOM, a meno che `F3` SLOW sia acceso. Ogni problema CSS viene scritto una sola volta per elemento ogni 5 secondi. I fogli di stile restano in cache finché un `MutationObserver` non vede un cambio a `<style>` o `<link>`. Hover è limitato a un frame.
|
|
205
206
|
|
|
206
207
|
Demo: `examples/css-hunt.html`.
|
|
207
208
|
|
package/package.json
CHANGED
|
@@ -42,7 +42,6 @@ export class CSSAdapter {
|
|
|
42
42
|
this.#attached = false;
|
|
43
43
|
this.#active = false;
|
|
44
44
|
this.#rules = [];
|
|
45
|
-
this.#unreadable = [];
|
|
46
45
|
this.#sheetsDirty = true;
|
|
47
46
|
this.#x = 0;
|
|
48
47
|
this.#y = 0;
|
|
@@ -51,6 +50,8 @@ export class CSSAdapter {
|
|
|
51
50
|
this.#pinned = null;
|
|
52
51
|
this.#panelLock = false;
|
|
53
52
|
this.#issues = [];
|
|
53
|
+
this.#notes = [];
|
|
54
|
+
this.#seenSheets = new Set();
|
|
54
55
|
this.#issueLog = new Map();
|
|
55
56
|
this.#hoverBox = null;
|
|
56
57
|
this.#pinBox = null;
|
|
@@ -72,7 +73,6 @@ export class CSSAdapter {
|
|
|
72
73
|
#attached;
|
|
73
74
|
#active;
|
|
74
75
|
#rules;
|
|
75
|
-
#unreadable;
|
|
76
76
|
#sheetsDirty;
|
|
77
77
|
#x;
|
|
78
78
|
#y;
|
|
@@ -81,6 +81,8 @@ export class CSSAdapter {
|
|
|
81
81
|
#pinned;
|
|
82
82
|
#panelLock;
|
|
83
83
|
#issues;
|
|
84
|
+
#notes;
|
|
85
|
+
#seenSheets;
|
|
84
86
|
#issueLog;
|
|
85
87
|
#hoverBox;
|
|
86
88
|
#pinBox;
|
|
@@ -141,6 +143,7 @@ export class CSSAdapter {
|
|
|
141
143
|
pushInvisible(el, findings);
|
|
142
144
|
pushBlockers(el, findings);
|
|
143
145
|
pushImage(el, findings);
|
|
146
|
+
pushStacking(el, findings);
|
|
144
147
|
pushImportant(el, this.#rules, findings);
|
|
145
148
|
for (let j = 0; j < findings.length; j++) {
|
|
146
149
|
rows.push({ el, selector: cssPath(el), ...findings[j] });
|
|
@@ -159,7 +162,11 @@ export class CSSAdapter {
|
|
|
159
162
|
});
|
|
160
163
|
}
|
|
161
164
|
const groups = groupIssues(rows);
|
|
162
|
-
for (let i =
|
|
165
|
+
for (let i = 0; i < rows.length; i++) {
|
|
166
|
+
const row = rows[i];
|
|
167
|
+
this.#rememberCause(row.el, row.selector, row.kind);
|
|
168
|
+
}
|
|
169
|
+
this.#publishIssues();
|
|
163
170
|
const shown = groups.slice(0, this.maxScan);
|
|
164
171
|
const items = shown.map((group) => ({
|
|
165
172
|
level: group.level,
|
|
@@ -228,6 +235,8 @@ export class CSSAdapter {
|
|
|
228
235
|
this.#pinned = null;
|
|
229
236
|
this.#panelLock = false;
|
|
230
237
|
this.#issues = [];
|
|
238
|
+
this.#notes = [];
|
|
239
|
+
this.#seenSheets.clear();
|
|
231
240
|
this.#issueLog.clear();
|
|
232
241
|
this.#unmountBoxes();
|
|
233
242
|
this.core?.publishDetail([]);
|
|
@@ -264,13 +273,8 @@ export class CSSAdapter {
|
|
|
264
273
|
this.#sheetsDirty = false;
|
|
265
274
|
const built = collectImportantRules();
|
|
266
275
|
this.#rules = built.rules;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.core?.sendData('warn', {
|
|
270
|
-
source: this.source,
|
|
271
|
-
message: `unreadable sheet: ${built.unreadable[i]}`
|
|
272
|
-
});
|
|
273
|
-
}
|
|
276
|
+
for (let i = 0; i < built.unreadable.length; i++) this.#rememberSheet(built.unreadable[i]);
|
|
277
|
+
this.#publishIssues();
|
|
274
278
|
}
|
|
275
279
|
|
|
276
280
|
#inspect(x, y) {
|
|
@@ -290,13 +294,7 @@ export class CSSAdapter {
|
|
|
290
294
|
pushInvisible(el, findings);
|
|
291
295
|
pushBlockers(el, findings);
|
|
292
296
|
pushImage(el, findings);
|
|
293
|
-
|
|
294
|
-
findings.push({
|
|
295
|
-
level: 'warn',
|
|
296
|
-
text: `unreadable sheet: ${this.#unreadable[i]}`,
|
|
297
|
-
score: 0
|
|
298
|
-
});
|
|
299
|
-
}
|
|
297
|
+
pushStacking(el, findings);
|
|
300
298
|
const path = cssPath(el);
|
|
301
299
|
this.core?.sendData('state', { source: this.source, key: 'css.target', value: path });
|
|
302
300
|
this.core?.sendData('state', {
|
|
@@ -344,7 +342,7 @@ export class CSSAdapter {
|
|
|
344
342
|
for (let i = 0; i < findings.length; i++) {
|
|
345
343
|
const hit = findings[i];
|
|
346
344
|
const text = hit.text || hit.kind || '';
|
|
347
|
-
if (!text) continue;
|
|
345
|
+
if (!text || text.startsWith('unreadable sheet:') || hit.level === 'info') continue;
|
|
348
346
|
if (!rememberIssue(this.#issueLog, `${path}\0${text}`, now, this.issueWindow)) continue;
|
|
349
347
|
const level = hit.level === 'error' ? 'error' : 'warn';
|
|
350
348
|
this.core?.sendData(level, {
|
|
@@ -355,34 +353,55 @@ export class CSSAdapter {
|
|
|
355
353
|
}
|
|
356
354
|
|
|
357
355
|
#stickFindings(el, path, findings) {
|
|
356
|
+
let changed = false;
|
|
358
357
|
for (let i = 0; i < findings.length; i++) {
|
|
359
358
|
const hit = findings[i];
|
|
360
|
-
if (!isStickyKind(hit.kind)) continue;
|
|
361
359
|
if (hit.kind === 'overflows viewport' && !elementCausesOverflow(el)) continue;
|
|
362
|
-
|
|
363
|
-
text: `${path} ${hit.kind}`,
|
|
364
|
-
el
|
|
365
|
-
}, this.maxIssues);
|
|
360
|
+
if (this.#rememberCause(el, path, hit.kind)) changed = true;
|
|
366
361
|
}
|
|
367
|
-
this.#publishIssues();
|
|
362
|
+
if (changed) this.#publishIssues();
|
|
368
363
|
}
|
|
369
364
|
|
|
370
|
-
#
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
this.#issues =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
365
|
+
#rememberCause(el, path, kind) {
|
|
366
|
+
const next = addCause(this.#issues, { kind, path, el }, this.maxIssues);
|
|
367
|
+
if (next === this.#issues) return false;
|
|
368
|
+
this.#issues = next;
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#rememberSheet(href) {
|
|
373
|
+
const label = String(href || '');
|
|
374
|
+
if (!label || this.#seenSheets.has(label)) return;
|
|
375
|
+
this.#seenSheets.add(label);
|
|
376
|
+
this.#notes.push({ text: `unreadable sheet: ${label}` });
|
|
377
|
+
this.core?.sendData('log', {
|
|
378
|
+
source: this.source,
|
|
379
|
+
level: 'info',
|
|
380
|
+
message: `unreadable sheet: ${label}`
|
|
381
|
+
});
|
|
378
382
|
}
|
|
379
383
|
|
|
380
384
|
#publishIssues() {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
385
|
+
const bugs = [];
|
|
386
|
+
const notes = this.#notes.map((note) => ({
|
|
387
|
+
level: 'info',
|
|
388
|
+
key: note.text,
|
|
389
|
+
text: note.text
|
|
390
|
+
}));
|
|
391
|
+
for (let i = 0; i < this.#issues.length; i++) {
|
|
392
|
+
const entry = this.#issues[i];
|
|
393
|
+
const item = {
|
|
394
|
+
key: entry.key,
|
|
395
|
+
text: formatCause(entry),
|
|
396
|
+
targets: entry.targets.map((target) => ({
|
|
397
|
+
text: target.text,
|
|
398
|
+
onSelect: () => this.#pin(target.el)
|
|
399
|
+
}))
|
|
400
|
+
};
|
|
401
|
+
if (entry.level === 'info') notes.push({ ...item, level: 'info' });
|
|
402
|
+
else bugs.push({ ...item, level: 'error' });
|
|
403
|
+
}
|
|
404
|
+
this.core?.publishIssues(bugs.concat(notes));
|
|
386
405
|
}
|
|
387
406
|
|
|
388
407
|
#pin(el) {
|
|
@@ -586,9 +605,11 @@ function pushNegativeMargins(el, findings) {
|
|
|
586
605
|
const ISSUE_RANK = Object.freeze({
|
|
587
606
|
overflow: 0,
|
|
588
607
|
blocker: 1,
|
|
608
|
+
stack: 1,
|
|
589
609
|
invisible: 2,
|
|
590
610
|
image: 3,
|
|
591
|
-
important: 4
|
|
611
|
+
important: 4,
|
|
612
|
+
hint: 6
|
|
592
613
|
});
|
|
593
614
|
|
|
594
615
|
/**
|
|
@@ -715,8 +736,165 @@ export function pushStickyIssue(list, entry, max = 10) {
|
|
|
715
736
|
return next.slice(0, max);
|
|
716
737
|
}
|
|
717
738
|
|
|
718
|
-
function
|
|
719
|
-
|
|
739
|
+
export function describeCause(kind, path) {
|
|
740
|
+
if (kind === 'overflows viewport') {
|
|
741
|
+
return { key: `overflow\0${path}`, type: 'overflow', cause: path, reason: '', level: 'error' };
|
|
742
|
+
}
|
|
743
|
+
const prefix = 'click blocked by ';
|
|
744
|
+
if (typeof kind === 'string' && kind.startsWith(prefix)) {
|
|
745
|
+
const cause = kind.slice(prefix.length);
|
|
746
|
+
return { key: `block\0${cause}`, type: 'block', cause, reason: '', level: 'error' };
|
|
747
|
+
}
|
|
748
|
+
const stack = typeof kind === 'string' ? kind.match(/^stacking context \(([^)]+)\) by (.+)$/) : null;
|
|
749
|
+
if (stack) {
|
|
750
|
+
return {
|
|
751
|
+
key: `stack\0${stack[2]}\0${stack[1]}`,
|
|
752
|
+
type: 'stack',
|
|
753
|
+
cause: stack[2],
|
|
754
|
+
reason: stack[1],
|
|
755
|
+
level: 'error'
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const hint = typeof kind === 'string' ? kind.match(/^will-change (.+?) by (.+)$/) : null;
|
|
759
|
+
if (hint) {
|
|
760
|
+
return {
|
|
761
|
+
key: `will\0${hint[2]}\0${hint[1]}`,
|
|
762
|
+
type: 'stack-hint',
|
|
763
|
+
cause: hint[2],
|
|
764
|
+
reason: hint[1],
|
|
765
|
+
level: 'info'
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* One row per cause. A blocker that covers many nodes stays a single row.
|
|
773
|
+
* @param {Array<{ key: string, type: string, cause: string, targets: Array<{ text: string, el?: Element }> }>} list
|
|
774
|
+
* @param {{ kind?: string, path: string, el?: Element }} entry
|
|
775
|
+
*/
|
|
776
|
+
export function addCause(list, entry, max = 10) {
|
|
777
|
+
const described = describeCause(entry.kind, entry.path);
|
|
778
|
+
if (!described) return list;
|
|
779
|
+
const target = { text: entry.path, el: entry.el };
|
|
780
|
+
const rest = [];
|
|
781
|
+
let prev = null;
|
|
782
|
+
for (let i = 0; i < list.length; i++) {
|
|
783
|
+
if (list[i].key === described.key) prev = list[i];
|
|
784
|
+
else rest.push(list[i]);
|
|
785
|
+
}
|
|
786
|
+
const targets = prev ? prev.targets.filter((item) => !sameTarget(item, target)) : [];
|
|
787
|
+
targets.unshift(target);
|
|
788
|
+
rest.unshift({
|
|
789
|
+
key: described.key,
|
|
790
|
+
type: described.type,
|
|
791
|
+
cause: described.cause,
|
|
792
|
+
reason: described.reason || '',
|
|
793
|
+
level: described.level || 'error',
|
|
794
|
+
targets
|
|
795
|
+
});
|
|
796
|
+
return rest.slice(0, max);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export function formatCause(entry) {
|
|
800
|
+
const count = entry.targets.length;
|
|
801
|
+
if (entry.type === 'block') {
|
|
802
|
+
const noun = count === 1 ? 'element' : 'elements';
|
|
803
|
+
return `${entry.cause} blocks clicks on ${count} ${noun}`;
|
|
804
|
+
}
|
|
805
|
+
if (entry.type === 'stack') {
|
|
806
|
+
const noun = count === 1 ? 'child' : 'children';
|
|
807
|
+
return `${entry.cause} creates stacking context (${entry.reason}) - z-index of ${count} ${noun} is trapped`;
|
|
808
|
+
}
|
|
809
|
+
if (entry.type === 'stack-hint') {
|
|
810
|
+
const noun = count === 1 ? 'child' : 'children';
|
|
811
|
+
return `${entry.cause} will-change: ${entry.reason} — z-index of ${count} ${noun} stays in this layer`;
|
|
812
|
+
}
|
|
813
|
+
if (count <= 1) return `${entry.cause} overflows viewport`;
|
|
814
|
+
return `${entry.cause} overflows viewport · ${count} elements`;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Properties that create a stacking context. `will-change` for transform or
|
|
819
|
+
* opacity is only a hint: it may promote a layer, it is not reported as a bug.
|
|
820
|
+
* @param {{ transform?: string, opacity?: string, filter?: string, position?: string, zIndex?: string, willChange?: string, getPropertyValue?: (name: string) => string }} style
|
|
821
|
+
*/
|
|
822
|
+
export function stackingReasons(style) {
|
|
823
|
+
const read = (prop, camel) => {
|
|
824
|
+
if (style && typeof style.getPropertyValue === 'function') {
|
|
825
|
+
const raw = style.getPropertyValue(prop);
|
|
826
|
+
if (raw) return String(raw).trim();
|
|
827
|
+
}
|
|
828
|
+
if (style && style[camel] != null && style[camel] !== '') return String(style[camel]).trim();
|
|
829
|
+
return '';
|
|
830
|
+
};
|
|
831
|
+
const bugs = [];
|
|
832
|
+
const transform = read('transform', 'transform');
|
|
833
|
+
if (transform && transform !== 'none') bugs.push('transform');
|
|
834
|
+
const opacity = parseFloat(read('opacity', 'opacity'));
|
|
835
|
+
if (Number.isFinite(opacity) && opacity < 1) bugs.push('opacity');
|
|
836
|
+
const filter = read('filter', 'filter');
|
|
837
|
+
if (filter && filter !== 'none') bugs.push('filter');
|
|
838
|
+
const position = read('position', 'position') || 'static';
|
|
839
|
+
const zIndex = read('z-index', 'zIndex') || 'auto';
|
|
840
|
+
if (position !== 'static' && zIndex !== 'auto') bugs.push('z-index');
|
|
841
|
+
const hint = willChangeHint(read('will-change', 'willChange'));
|
|
842
|
+
return { bugs, hint };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function willChangeHint(value) {
|
|
846
|
+
if (!value || value === 'auto') return '';
|
|
847
|
+
const parts = value.split(',').map((part) => part.trim().toLowerCase());
|
|
848
|
+
const hit = [];
|
|
849
|
+
for (let i = 0; i < parts.length; i++) {
|
|
850
|
+
if ((parts[i] === 'transform' || parts[i] === 'opacity') && !hit.includes(parts[i])) hit.push(parts[i]);
|
|
851
|
+
}
|
|
852
|
+
return hit.join(', ');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function pushStacking(el, findings) {
|
|
856
|
+
if (!el || typeof getComputedStyle !== 'function' || !el.parentElement) return;
|
|
857
|
+
const own = getComputedStyle(el);
|
|
858
|
+
const ownZ = own.zIndex || own.getPropertyValue?.('z-index') || 'auto';
|
|
859
|
+
if (!ownZ || ownZ === 'auto') return;
|
|
860
|
+
let node = el.parentElement;
|
|
861
|
+
while (node && node.nodeType === 1) {
|
|
862
|
+
if (isBeeChrome(node)) return;
|
|
863
|
+
const found = stackingReasons(getComputedStyle(node));
|
|
864
|
+
const ancestor = cssPath(node);
|
|
865
|
+
if (found.bugs.length) {
|
|
866
|
+
const reason = found.bugs[0];
|
|
867
|
+
findings.push({
|
|
868
|
+
level: 'error',
|
|
869
|
+
rank: ISSUE_RANK.stack,
|
|
870
|
+
score: 35,
|
|
871
|
+
kind: `stacking context (${reason}) by ${ancestor}`,
|
|
872
|
+
text: `z-index trapped by ${ancestor} (${reason})`
|
|
873
|
+
});
|
|
874
|
+
if (found.hint) findings.push(stackHint(ancestor, found.hint));
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (found.hint) {
|
|
878
|
+
findings.push(stackHint(ancestor, found.hint));
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
node = node.parentElement;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function stackHint(ancestor, hint) {
|
|
886
|
+
return {
|
|
887
|
+
level: 'info',
|
|
888
|
+
rank: ISSUE_RANK.hint,
|
|
889
|
+
score: 1,
|
|
890
|
+
kind: `will-change ${hint} by ${ancestor}`,
|
|
891
|
+
text: `will-change: ${hint} on ${ancestor}`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function sameTarget(a, b) {
|
|
896
|
+
if (a.el && b.el) return a.el === b.el;
|
|
897
|
+
return a.text === b.text;
|
|
720
898
|
}
|
|
721
899
|
|
|
722
900
|
function elementCausesOverflow(el) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { UIOverlay } from './UIOverlay.js';
|
|
10
10
|
import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
|
|
11
11
|
|
|
12
|
-
export const BEE_LADYBUG_VERSION = '0.5.
|
|
12
|
+
export const BEE_LADYBUG_VERSION = '0.5.4';
|
|
13
13
|
|
|
14
14
|
export const CORE_DEFAULTS = Object.freeze({
|
|
15
15
|
toggleKey: 'F2',
|
|
@@ -364,7 +364,7 @@ export class BeeLadybugCore {
|
|
|
364
364
|
* @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
|
|
365
365
|
*/
|
|
366
366
|
publishIssues(items) {
|
|
367
|
-
const list = Array.isArray(items) ? items
|
|
367
|
+
const list = Array.isArray(items) ? items : [];
|
|
368
368
|
this.#issues = list;
|
|
369
369
|
this.ui?.setIssues(this.#issues);
|
|
370
370
|
return this;
|
package/src/core/UIOverlay.js
CHANGED
|
@@ -35,6 +35,9 @@ const STYLES = `
|
|
|
35
35
|
top: 12px;
|
|
36
36
|
left: 12px;
|
|
37
37
|
width: 360px;
|
|
38
|
+
max-height: 50vh;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
38
41
|
background: rgba(7, 8, 12, 0.94);
|
|
39
42
|
border: 1px solid rgba(255, 59, 78, 0.55);
|
|
40
43
|
box-sizing: border-box;
|
|
@@ -62,6 +65,7 @@ const STYLES = `
|
|
|
62
65
|
display: flex;
|
|
63
66
|
align-items: center;
|
|
64
67
|
gap: 8px;
|
|
68
|
+
flex: 0 0 auto;
|
|
65
69
|
padding: 10px 12px 8px;
|
|
66
70
|
border-bottom: 1px solid rgba(255, 59, 78, 0.28);
|
|
67
71
|
}
|
|
@@ -201,7 +205,7 @@ const STYLES = `
|
|
|
201
205
|
.bl-k { color: #8b91a3; }
|
|
202
206
|
.bl-v { color: #e9edf5; }
|
|
203
207
|
.bl-console {
|
|
204
|
-
height:
|
|
208
|
+
max-height: 140px;
|
|
205
209
|
overflow: auto;
|
|
206
210
|
padding: 6px 10px 8px;
|
|
207
211
|
background: #05060a;
|
|
@@ -212,13 +216,14 @@ const STYLES = `
|
|
|
212
216
|
.bl-line { white-space: normal; word-break: break-word; }
|
|
213
217
|
.bl-line .t { color: #8b91a3; margin-right: 8px; }
|
|
214
218
|
.bl-line .s { color: #6ee7ff; margin-right: 8px; }
|
|
215
|
-
.bl-line.info .m { color: #
|
|
219
|
+
.bl-line.info .m { color: #8b91a3; }
|
|
216
220
|
.bl-line.warn .m { color: #ffd36a; }
|
|
217
221
|
.bl-line.error .m { color: #ff6b7d; }
|
|
218
222
|
.bl-line.debug .m { color: #8b91a3; }
|
|
219
223
|
.bl-foot {
|
|
220
224
|
display: flex;
|
|
221
225
|
align-items: center;
|
|
226
|
+
flex: 0 0 auto;
|
|
222
227
|
flex-wrap: wrap;
|
|
223
228
|
gap: 6px;
|
|
224
229
|
padding: 8px 10px;
|
|
@@ -263,35 +268,72 @@ const STYLES = `
|
|
|
263
268
|
.bl-hotkeys:empty {
|
|
264
269
|
display: none;
|
|
265
270
|
}
|
|
271
|
+
.bl-scroll {
|
|
272
|
+
overflow: auto;
|
|
273
|
+
min-height: 0;
|
|
274
|
+
flex: 1 1 auto;
|
|
275
|
+
}
|
|
276
|
+
.bl-section[hidden] { display: none; }
|
|
277
|
+
.bl-section-toggle {
|
|
278
|
+
display: block;
|
|
279
|
+
width: 100%;
|
|
280
|
+
margin: 0;
|
|
281
|
+
padding: 5px 10px;
|
|
282
|
+
border: 0;
|
|
283
|
+
border-top: 1px solid rgba(255, 59, 78, 0.28);
|
|
284
|
+
background: #101218;
|
|
285
|
+
color: #ffd36a;
|
|
286
|
+
font: 700 9px ui-monospace, Consolas, monospace;
|
|
287
|
+
letter-spacing: 0.14em;
|
|
288
|
+
text-align: left;
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
}
|
|
291
|
+
.bl-section-toggle::before { content: "▾ "; }
|
|
292
|
+
.bl-section[data-open="false"] > .bl-section-toggle::before { content: "▸ "; }
|
|
293
|
+
.bl-section[data-open="false"] > .bl-section-body { display: none; }
|
|
266
294
|
.bl-issues {
|
|
267
|
-
overflow: hidden;
|
|
268
295
|
background: #07080c;
|
|
269
|
-
border-top: 1px solid rgba(255, 59, 78, 0.55);
|
|
270
296
|
}
|
|
271
|
-
.bl-
|
|
272
|
-
display:
|
|
297
|
+
.bl-legend {
|
|
298
|
+
display: flex;
|
|
299
|
+
gap: 14px;
|
|
300
|
+
padding: 4px 10px 2px;
|
|
301
|
+
color: #8b91a3;
|
|
302
|
+
font-size: 10px;
|
|
273
303
|
}
|
|
274
|
-
.bl-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
304
|
+
.bl-dot {
|
|
305
|
+
display: inline-block;
|
|
306
|
+
width: 7px;
|
|
307
|
+
height: 7px;
|
|
308
|
+
margin-right: 5px;
|
|
309
|
+
border-radius: 50%;
|
|
310
|
+
vertical-align: baseline;
|
|
280
311
|
}
|
|
281
|
-
.bl-
|
|
312
|
+
.bl-dot.bug { background: #ff3b4e; }
|
|
313
|
+
.bl-dot.info { background: #8b91a3; }
|
|
314
|
+
.bl-cause > button,
|
|
315
|
+
.bl-cause-kids button,
|
|
316
|
+
.bl-note {
|
|
282
317
|
display: block;
|
|
283
318
|
width: 100%;
|
|
284
319
|
margin: 0;
|
|
285
320
|
padding: 3px 10px;
|
|
286
321
|
border: 0;
|
|
287
322
|
background: transparent;
|
|
288
|
-
color: #ff6b7d;
|
|
289
323
|
font: 11px ui-monospace, Consolas, monospace;
|
|
290
324
|
line-height: 1.4;
|
|
291
325
|
text-align: left;
|
|
326
|
+
}
|
|
327
|
+
.bl-cause > button,
|
|
328
|
+
.bl-cause-kids button {
|
|
329
|
+
color: #ff6b7d;
|
|
292
330
|
cursor: pointer;
|
|
293
331
|
}
|
|
294
|
-
.bl-
|
|
332
|
+
.bl-cause-kids[hidden] { display: none; }
|
|
333
|
+
.bl-cause-kids button { padding-left: 22px; }
|
|
334
|
+
.bl-cause > button:hover,
|
|
335
|
+
.bl-cause-kids button:hover { background: rgba(255, 59, 78, 0.08); }
|
|
336
|
+
.bl-note { color: #8b91a3; }
|
|
295
337
|
.bl-detail {
|
|
296
338
|
max-height: 132px;
|
|
297
339
|
overflow: auto;
|
|
@@ -316,6 +358,7 @@ const STYLES = `
|
|
|
316
358
|
}
|
|
317
359
|
.bl-detail button[data-level="warn"] { color: #ffd36a; }
|
|
318
360
|
.bl-detail button[data-level="error"] { color: #ff6b7d; }
|
|
361
|
+
.bl-detail button[data-level="info"] { color: #8b91a3; }
|
|
319
362
|
.bl-detail button:hover { background: rgba(255, 255, 255, 0.04); }
|
|
320
363
|
.bl-adapter {
|
|
321
364
|
appearance: none;
|
|
@@ -396,6 +439,8 @@ export class UIOverlay {
|
|
|
396
439
|
this.#hotkeySig = '';
|
|
397
440
|
this.#detail = [];
|
|
398
441
|
this.#issues = [];
|
|
442
|
+
this.#issueNotes = [];
|
|
443
|
+
this.#openCauses = new Set();
|
|
399
444
|
this.#flashAt = new Map();
|
|
400
445
|
this.#flashTimers = new Map();
|
|
401
446
|
}
|
|
@@ -411,6 +456,8 @@ export class UIOverlay {
|
|
|
411
456
|
#hotkeySig;
|
|
412
457
|
#detail;
|
|
413
458
|
#issues;
|
|
459
|
+
#issueNotes;
|
|
460
|
+
#openCauses;
|
|
414
461
|
#flashAt;
|
|
415
462
|
#flashTimers;
|
|
416
463
|
|
|
@@ -447,6 +494,10 @@ export class UIOverlay {
|
|
|
447
494
|
</button>
|
|
448
495
|
</span>
|
|
449
496
|
</div>
|
|
497
|
+
<div class="bl-scroll">
|
|
498
|
+
<section class="bl-section" data-section="status" data-open="true">
|
|
499
|
+
<button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">STATUS</button>
|
|
500
|
+
<div class="bl-section-body">
|
|
450
501
|
<div class="bl-stats">
|
|
451
502
|
<div>HOST <b data-el="hostFps">—</b></div>
|
|
452
503
|
<div>CORE <b data-el="coreFps">—</b></div>
|
|
@@ -461,12 +512,32 @@ export class UIOverlay {
|
|
|
461
512
|
</div>
|
|
462
513
|
</div>
|
|
463
514
|
<div class="bl-hud" data-el="hud"></div>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
515
|
+
</div>
|
|
516
|
+
</section>
|
|
517
|
+
<section class="bl-section" data-el="issues" data-section="issues" data-open="true" hidden>
|
|
518
|
+
<button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">ISSUES</button>
|
|
519
|
+
<div class="bl-section-body">
|
|
520
|
+
<div class="bl-legend">
|
|
521
|
+
<span><i class="bl-dot bug"></i>bug</span>
|
|
522
|
+
<span><i class="bl-dot info"></i>info</span>
|
|
523
|
+
</div>
|
|
524
|
+
<div data-el="issues-list"></div>
|
|
525
|
+
<div data-el="notes"></div>
|
|
526
|
+
</div>
|
|
527
|
+
</section>
|
|
528
|
+
<section class="bl-section" data-el="inspect" data-section="inspect" data-open="true" hidden>
|
|
529
|
+
<button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">INSPECT</button>
|
|
530
|
+
<div class="bl-section-body">
|
|
531
|
+
<div class="bl-detail" data-el="detail" hidden></div>
|
|
532
|
+
</div>
|
|
533
|
+
</section>
|
|
534
|
+
<section class="bl-section" data-section="events" data-open="true">
|
|
535
|
+
<button class="bl-section-toggle" type="button" data-section-toggle aria-expanded="true">EVENTS</button>
|
|
536
|
+
<div class="bl-section-body">
|
|
537
|
+
<div class="bl-console" data-el="console" role="log" aria-live="polite"></div>
|
|
538
|
+
</div>
|
|
539
|
+
</section>
|
|
467
540
|
</div>
|
|
468
|
-
<div class="bl-detail" data-el="detail" hidden></div>
|
|
469
|
-
<div class="bl-console" data-el="console" role="log" aria-live="polite"></div>
|
|
470
541
|
<div class="bl-foot">
|
|
471
542
|
<button class="bl-btn" data-act="slow" type="button">F3 SLOW</button>
|
|
472
543
|
<button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
|
|
@@ -494,8 +565,10 @@ export class UIOverlay {
|
|
|
494
565
|
hud: this.#root.querySelector('[data-el="hud"]'),
|
|
495
566
|
console: this.#root.querySelector('[data-el="console"]'),
|
|
496
567
|
detail: this.#root.querySelector('[data-el="detail"]'),
|
|
568
|
+
inspect: this.#root.querySelector('[data-el="inspect"]'),
|
|
497
569
|
issues: this.#root.querySelector('[data-el="issues"]'),
|
|
498
570
|
issuesList: this.#root.querySelector('[data-el="issues-list"]'),
|
|
571
|
+
notes: this.#root.querySelector('[data-el="notes"]'),
|
|
499
572
|
providers: this.#root.querySelector('[data-el="providers"]'),
|
|
500
573
|
adapters: this.#root.querySelector('[data-el="adapters"]'),
|
|
501
574
|
hotkeys: this.#root.querySelector('[data-el="hotkeys"]')
|
|
@@ -513,11 +586,40 @@ export class UIOverlay {
|
|
|
513
586
|
else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
|
|
514
587
|
});
|
|
515
588
|
|
|
516
|
-
this.#root.querySelector('.bl-
|
|
589
|
+
this.#root.querySelector('.bl-scroll')?.addEventListener('click', (event) => {
|
|
590
|
+
const toggle = event.target.closest('[data-section-toggle]');
|
|
591
|
+
if (!toggle) return;
|
|
592
|
+
const section = toggle.closest('.bl-section');
|
|
593
|
+
if (!section) return;
|
|
594
|
+
const open = section.dataset.open === 'false';
|
|
595
|
+
section.dataset.open = open ? 'true' : 'false';
|
|
596
|
+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
this.#els.issues?.addEventListener('click', (event) => {
|
|
600
|
+
if (event.target.closest('[data-section-toggle]')) return;
|
|
601
|
+
const childBtn = event.target.closest('[data-target]');
|
|
602
|
+
if (childBtn) {
|
|
603
|
+
const item = this.#issues[Number(childBtn.getAttribute('data-issue'))];
|
|
604
|
+
const child = item?.targets?.[Number(childBtn.getAttribute('data-target'))];
|
|
605
|
+
if (child && typeof child.onSelect === 'function') child.onSelect();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
517
608
|
const row = event.target.closest('[data-issue]');
|
|
518
609
|
if (!row) return;
|
|
519
610
|
const item = this.#issues[Number(row.getAttribute('data-issue'))];
|
|
520
|
-
if (item
|
|
611
|
+
if (!item) return;
|
|
612
|
+
const many = Array.isArray(item.targets) && item.targets.length > 1;
|
|
613
|
+
if (many) {
|
|
614
|
+
const key = item.key || item.text;
|
|
615
|
+
if (this.#openCauses.has(key)) this.#openCauses.delete(key);
|
|
616
|
+
else this.#openCauses.add(key);
|
|
617
|
+
this.setIssues(this.#issues.concat(this.#issueNotes));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const only = item.targets?.[0];
|
|
621
|
+
if (only && typeof only.onSelect === 'function') only.onSelect();
|
|
622
|
+
else if (typeof item.onSelect === 'function') item.onSelect();
|
|
521
623
|
});
|
|
522
624
|
|
|
523
625
|
this.#root.querySelector('.bl-detail')?.addEventListener('click', (event) => {
|
|
@@ -551,6 +653,8 @@ export class UIOverlay {
|
|
|
551
653
|
this.#hotkeySig = '';
|
|
552
654
|
this.#detail = [];
|
|
553
655
|
this.#issues = [];
|
|
656
|
+
this.#issueNotes = [];
|
|
657
|
+
this.#openCauses.clear();
|
|
554
658
|
this.#clearFlashTimers();
|
|
555
659
|
this.#flashAt.clear();
|
|
556
660
|
return this;
|
|
@@ -576,9 +680,11 @@ export class UIOverlay {
|
|
|
576
680
|
const list = Array.isArray(items) ? items : [];
|
|
577
681
|
this.#detail = list;
|
|
578
682
|
const box = this.#els.detail;
|
|
683
|
+
const section = this.#els.inspect;
|
|
579
684
|
if (!list.length) {
|
|
580
685
|
box.innerHTML = '';
|
|
581
686
|
box.hidden = true;
|
|
687
|
+
if (section) section.hidden = true;
|
|
582
688
|
return this;
|
|
583
689
|
}
|
|
584
690
|
let html = '';
|
|
@@ -587,30 +693,58 @@ export class UIOverlay {
|
|
|
587
693
|
html += `<button type="button" data-detail="${i}" data-level="${level}">${escapeHtml(list[i].text)}</button>`;
|
|
588
694
|
}
|
|
589
695
|
box.hidden = false;
|
|
696
|
+
if (section) section.hidden = false;
|
|
590
697
|
box.innerHTML = html;
|
|
591
698
|
return this;
|
|
592
699
|
}
|
|
593
700
|
|
|
594
701
|
/**
|
|
595
|
-
*
|
|
596
|
-
* @param {Array<{ text: string, level?: string, onSelect?: () => void }>} items
|
|
702
|
+
* Cause rows plus grey info notes. Bugs are red and expand; info stays grey.
|
|
703
|
+
* @param {Array<{ text: string, level?: string, key?: string, onSelect?: () => void, targets?: Array<{ text: string, onSelect?: () => void }> }>} items
|
|
597
704
|
*/
|
|
598
705
|
setIssues(items) {
|
|
599
|
-
if (!this.#els?.issues || !this.#els.issuesList) return this;
|
|
600
|
-
const list = Array.isArray(items) ? items
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
706
|
+
if (!this.#els?.issues || !this.#els.issuesList || !this.#els.notes) return this;
|
|
707
|
+
const list = Array.isArray(items) ? items : [];
|
|
708
|
+
const bugs = [];
|
|
709
|
+
const notes = [];
|
|
710
|
+
for (let i = 0; i < list.length; i++) {
|
|
711
|
+
if (list[i].level === 'info') notes.push(list[i]);
|
|
712
|
+
else bugs.push(list[i]);
|
|
713
|
+
}
|
|
714
|
+
const capped = bugs.slice(0, 10);
|
|
715
|
+
this.#issues = capped;
|
|
716
|
+
this.#issueNotes = notes;
|
|
717
|
+
const section = this.#els.issues;
|
|
718
|
+
if (!capped.length && !notes.length) {
|
|
604
719
|
this.#els.issuesList.innerHTML = '';
|
|
605
|
-
|
|
720
|
+
this.#els.notes.innerHTML = '';
|
|
721
|
+
section.hidden = true;
|
|
606
722
|
return this;
|
|
607
723
|
}
|
|
608
724
|
let html = '';
|
|
609
|
-
for (let i = 0; i <
|
|
610
|
-
|
|
725
|
+
for (let i = 0; i < capped.length; i++) {
|
|
726
|
+
const item = capped[i];
|
|
727
|
+
const targets = Array.isArray(item.targets) ? item.targets : [];
|
|
728
|
+
const many = targets.length > 1;
|
|
729
|
+
const open = many && this.#openCauses.has(item.key || item.text);
|
|
730
|
+
html += `<div class="bl-cause" data-open="${open ? 'true' : 'false'}">`;
|
|
731
|
+
html += `<button type="button" data-issue="${i}">${many ? (open ? '▾ ' : '▸ ') : ''}${escapeHtml(item.text)}</button>`;
|
|
732
|
+
if (many) {
|
|
733
|
+
html += `<div class="bl-cause-kids"${open ? '' : ' hidden'}>`;
|
|
734
|
+
for (let j = 0; j < targets.length; j++) {
|
|
735
|
+
html += `<button type="button" data-issue="${i}" data-target="${j}">${escapeHtml(targets[j].text)}</button>`;
|
|
736
|
+
}
|
|
737
|
+
html += '</div>';
|
|
738
|
+
}
|
|
739
|
+
html += '</div>';
|
|
611
740
|
}
|
|
612
|
-
box.hidden = false;
|
|
613
741
|
this.#els.issuesList.innerHTML = html;
|
|
742
|
+
let noteHtml = '';
|
|
743
|
+
for (let i = 0; i < notes.length; i++) {
|
|
744
|
+
noteHtml += `<div class="bl-note">${escapeHtml(notes[i].text)}</div>`;
|
|
745
|
+
}
|
|
746
|
+
this.#els.notes.innerHTML = noteHtml;
|
|
747
|
+
section.hidden = false;
|
|
614
748
|
return this;
|
|
615
749
|
}
|
|
616
750
|
|
package/src/index.d.ts
CHANGED