reactoradar 1.2.3 → 1.4.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 +103 -223
- package/app.js +425 -81
- package/index.html +5 -1
- package/main.js +14 -12
- package/package.json +1 -1
- package/preload.js +2 -2
- package/sdk/RNDebugSDK.js +117 -4
- package/styles.css +216 -13
package/index.html
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
<span id="deviceText">Waiting for device...</span>
|
|
22
22
|
</div>
|
|
23
23
|
<div class="titlebar-actions">
|
|
24
|
-
<button class="tb-btn" id="btnClear" title="Clear active tab (⌘K clears all)">Clear</button>
|
|
25
24
|
<button class="tb-btn primary" id="btnCDP" title="Open JS Debugger (⌘D)">JS Debugger ↗</button>
|
|
26
25
|
</div>
|
|
27
26
|
</header>
|
|
@@ -40,6 +39,10 @@
|
|
|
40
39
|
<svg viewBox="0 0 20 20"><rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="11" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/><rect x="7" y="11" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
|
41
40
|
<span>Redux</span>
|
|
42
41
|
</button>
|
|
42
|
+
<button class="nav-btn" data-panel="ga4" title="GA4 Events">
|
|
43
|
+
<svg viewBox="0 0 20 20"><path d="M10 2v16M6 6l4-4 4 4M3 18h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><circle cx="5" cy="13" r="2" stroke="currentColor" stroke-width="1.2" fill="none"/><circle cx="10" cy="9" r="2" stroke="currentColor" stroke-width="1.2" fill="none"/><circle cx="15" cy="12" r="2" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
|
|
44
|
+
<span>GA4</span>
|
|
45
|
+
</button>
|
|
43
46
|
<button class="nav-btn" data-panel="storage" title="Application">
|
|
44
47
|
<svg viewBox="0 0 20 20"><ellipse cx="10" cy="6" rx="7" ry="3" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 6v4c0 1.66 3.13 3 7 3s7-1.34 7-3V6" stroke="currentColor" stroke-width="1.5" fill="none"/><path d="M3 10v4c0 1.66 3.13 3 7 3s7-1.34 7-3v-4" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
|
|
45
48
|
<span>App</span>
|
|
@@ -69,6 +72,7 @@
|
|
|
69
72
|
<div id="panel-network" class="panel"></div>
|
|
70
73
|
<div id="panel-performance" class="panel"></div>
|
|
71
74
|
<div id="panel-memory" class="panel"></div>
|
|
75
|
+
<div id="panel-ga4" class="panel"></div>
|
|
72
76
|
<div id="panel-redux" class="panel"></div>
|
|
73
77
|
<div id="panel-storage" class="panel"></div>
|
|
74
78
|
<div id="panel-react" class="panel"></div>
|
package/main.js
CHANGED
|
@@ -24,26 +24,26 @@ let reduxClients = new Set();
|
|
|
24
24
|
let storageClients = new Set();
|
|
25
25
|
let networkClients = new Set();
|
|
26
26
|
|
|
27
|
+
// ─── Set dock icon ASAP (before app ready) ──────────────────────────────────
|
|
28
|
+
const _appIcon = nativeImage.createFromPath(path.join(__dirname, 'ReactoRadar.png'));
|
|
29
|
+
|
|
27
30
|
// ─── App lifecycle ────────────────────────────────────────────────────────────
|
|
28
31
|
app.whenReady().then(async () => {
|
|
29
|
-
// Theme will be set by renderer via IPC once it reads localStorage
|
|
30
32
|
nativeTheme.themeSource = 'dark';
|
|
31
33
|
|
|
32
34
|
// Set dock icon on macOS
|
|
33
|
-
if (process.platform === 'darwin') {
|
|
34
|
-
try {
|
|
35
|
-
const iconPath = path.join(__dirname, 'ReactoRadar.png');
|
|
36
|
-
const icon = nativeImage.createFromPath(iconPath);
|
|
37
|
-
if (!icon.isEmpty()) {
|
|
38
|
-
app.dock.setIcon(icon);
|
|
39
|
-
}
|
|
40
|
-
} catch (e) {
|
|
41
|
-
console.warn('[Icon] Failed to set dock icon:', e.message);
|
|
42
|
-
}
|
|
35
|
+
if (process.platform === 'darwin' && !_appIcon.isEmpty()) {
|
|
36
|
+
try { app.dock.setIcon(_appIcon); } catch {}
|
|
43
37
|
}
|
|
44
38
|
|
|
45
39
|
await createMainWindow();
|
|
46
40
|
|
|
41
|
+
// Send version to renderer
|
|
42
|
+
const appVersion = require('./package.json').version;
|
|
43
|
+
mainWindow?.webContents.on('did-finish-load', () => {
|
|
44
|
+
mainWindow?.webContents.send('app-version', appVersion);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
47
|
// Check for updates (non-blocking)
|
|
48
48
|
checkForUpdates();
|
|
49
49
|
startBridgeServers();
|
|
@@ -86,7 +86,7 @@ async function createMainWindow() {
|
|
|
86
86
|
contextIsolation: true,
|
|
87
87
|
preload: path.join(__dirname, 'preload.js'),
|
|
88
88
|
},
|
|
89
|
-
icon:
|
|
89
|
+
icon: _appIcon,
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
|
@@ -257,6 +257,8 @@ function startBridgeServers() {
|
|
|
257
257
|
mainWindow?.webContents.send('console-event', event);
|
|
258
258
|
} else if (event.type === 'perf') {
|
|
259
259
|
mainWindow?.webContents.send('perf-event', event);
|
|
260
|
+
} else if (event.type === 'ga4') {
|
|
261
|
+
mainWindow?.webContents.send('ga4-event', event);
|
|
260
262
|
} else {
|
|
261
263
|
mainWindow?.webContents.send('network-event', event);
|
|
262
264
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactoradar",
|
|
3
3
|
"productName": "ReactoRadar",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"description": "macOS debugger for React Native — Console, Sources, Network, Performance, Memory, Redux, AsyncStorage, React tree. Supports RN 0.74+ with Hermes and New Architecture.",
|
|
6
6
|
"main": "main.js",
|
|
7
7
|
"bin": {
|
package/preload.js
CHANGED
|
@@ -9,8 +9,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
9
9
|
on: (channel, cb) => {
|
|
10
10
|
const allowed = [
|
|
11
11
|
'ports', 'cdp-targets', 'redux-event', 'storage-event', 'network-event',
|
|
12
|
-
'console-event', 'perf-event', 'redux-connected', 'storage-connected', 'network-connected',
|
|
13
|
-
'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available',
|
|
12
|
+
'console-event', 'perf-event', 'ga4-event', 'redux-connected', 'storage-connected', 'network-connected',
|
|
13
|
+
'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available', 'app-version',
|
|
14
14
|
];
|
|
15
15
|
if (allowed.includes(channel)) {
|
|
16
16
|
ipcRenderer.removeAllListeners(channel);
|
package/sdk/RNDebugSDK.js
CHANGED
|
@@ -529,10 +529,123 @@ try {
|
|
|
529
529
|
}, 2000);
|
|
530
530
|
})();
|
|
531
531
|
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
532
|
+
// ─── GA4 / Firebase Analytics Interceptor ────────────────────────────────────
|
|
533
|
+
// Intercepts @react-native-firebase/analytics logEvent calls.
|
|
534
|
+
// The analytics() function returns a new instance each time, so we patch the
|
|
535
|
+
// PROTOTYPE of the analytics module class, not individual instances.
|
|
536
|
+
(function setupGA4Interceptor() {
|
|
537
|
+
function patchAnalytics() {
|
|
538
|
+
try {
|
|
539
|
+
const analyticsModule = require('@react-native-firebase/analytics');
|
|
540
|
+
if (!analyticsModule) return false;
|
|
541
|
+
|
|
542
|
+
// Get the default export (the analytics factory function)
|
|
543
|
+
const analyticsFn = analyticsModule.default || analyticsModule;
|
|
544
|
+
if (typeof analyticsFn !== 'function') return false;
|
|
545
|
+
|
|
546
|
+
// Create one instance to get access to its prototype
|
|
547
|
+
const instance = analyticsFn();
|
|
548
|
+
if (!instance || !instance.logEvent) return false;
|
|
549
|
+
|
|
550
|
+
const proto = Object.getPrototypeOf(instance);
|
|
551
|
+
if (!proto || proto.__reactoRadarPatched) return false;
|
|
552
|
+
proto.__reactoRadarPatched = true;
|
|
553
|
+
|
|
554
|
+
// Helper to safely serialize params
|
|
555
|
+
function _safeParams(p) {
|
|
556
|
+
if (!p || typeof p !== 'object') return p || {};
|
|
557
|
+
try { return JSON.parse(JSON.stringify(p)); } catch { return {}; }
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Convert method name to event name: logAddToCart → add_to_cart
|
|
561
|
+
function _methodToEvent(name) {
|
|
562
|
+
// Remove 'log' prefix, then convert camelCase to snake_case
|
|
563
|
+
return name.replace(/^log/, '')
|
|
564
|
+
.replace(/([A-Z])/g, '_$1')
|
|
565
|
+
.toLowerCase()
|
|
566
|
+
.replace(/^_/, '');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Dynamically wrap ALL methods that start with 'log' on the prototype
|
|
570
|
+
// This catches logEvent, logPurchase, logAddToCart, logScreenView, etc.
|
|
571
|
+
// Also catches any future methods Firebase adds.
|
|
572
|
+
Object.getOwnPropertyNames(proto).forEach(methodName => {
|
|
573
|
+
if (!methodName.startsWith('log') || typeof proto[methodName] !== 'function') return;
|
|
574
|
+
|
|
575
|
+
const orig = proto[methodName];
|
|
576
|
+
|
|
577
|
+
if (methodName === 'logEvent') {
|
|
578
|
+
// logEvent has signature: (eventName, params, options?)
|
|
579
|
+
proto.logEvent = function(eventName, params, options) {
|
|
580
|
+
try { mainCh.send({ type: 'ga4', name: eventName, params: _safeParams(params), tag: 'GA4' }); } catch {}
|
|
581
|
+
return orig.call(this, eventName, params, options);
|
|
582
|
+
};
|
|
583
|
+
} else {
|
|
584
|
+
// All other log methods: logPurchase(params), logScreenView(params), etc.
|
|
585
|
+
const eventName = _methodToEvent(methodName);
|
|
586
|
+
proto[methodName] = function() {
|
|
587
|
+
try {
|
|
588
|
+
// First argument is always the params object (or undefined for logAppOpen, logTutorialBegin, etc.)
|
|
589
|
+
const params = arguments[0];
|
|
590
|
+
mainCh.send({ type: 'ga4', name: eventName, params: _safeParams(params), tag: 'GA4' });
|
|
591
|
+
} catch {}
|
|
592
|
+
return orig.apply(this, arguments);
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Also wrap set* methods to track user properties/consent
|
|
598
|
+
['setUserId', 'setUserProperty', 'setUserProperties', 'setConsent', 'setDefaultEventParameters', 'setAnalyticsCollectionEnabled'].forEach(methodName => {
|
|
599
|
+
if (!proto[methodName] || typeof proto[methodName] !== 'function') return;
|
|
600
|
+
const orig = proto[methodName];
|
|
601
|
+
proto[methodName] = function() {
|
|
602
|
+
try {
|
|
603
|
+
const params = {};
|
|
604
|
+
// Capture the arguments as key-value
|
|
605
|
+
if (arguments.length === 1) params.value = _safeParams(arguments[0]);
|
|
606
|
+
else if (arguments.length >= 2) { params.name = arguments[0]; params.value = arguments[1]; }
|
|
607
|
+
mainCh.send({ type: 'ga4', name: methodName, params, tag: 'GA4' });
|
|
608
|
+
} catch {}
|
|
609
|
+
return orig.apply(this, arguments);
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
_console.log('[RNDebugSDK] GA4 Analytics prototype interceptor active');
|
|
614
|
+
return true;
|
|
615
|
+
} catch (e) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Try immediately, then retry at increasing delays
|
|
621
|
+
if (!patchAnalytics()) {
|
|
622
|
+
[100, 500, 2000, 5000].forEach(delay => {
|
|
623
|
+
setTimeout(() => patchAnalytics(), delay);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Fallback: also patch the module's default export function to wrap returned instances
|
|
628
|
+
setTimeout(() => {
|
|
629
|
+
try {
|
|
630
|
+
const mod = require('@react-native-firebase/analytics');
|
|
631
|
+
if (!mod || mod.__reactoRadarWrapped) return;
|
|
632
|
+
const origDefault = mod.default;
|
|
633
|
+
if (typeof origDefault !== 'function') return;
|
|
634
|
+
mod.__reactoRadarWrapped = true;
|
|
635
|
+
mod.default = function() {
|
|
636
|
+
const inst = origDefault.apply(this, arguments);
|
|
637
|
+
// Ensure prototype is patched (in case new prototype was created)
|
|
638
|
+
if (inst && inst.logEvent) {
|
|
639
|
+
const p = Object.getPrototypeOf(inst);
|
|
640
|
+
if (p && !p.__reactoRadarPatched) patchAnalytics();
|
|
641
|
+
}
|
|
642
|
+
return inst;
|
|
643
|
+
};
|
|
644
|
+
// Copy static properties
|
|
645
|
+
Object.keys(origDefault).forEach(k => { mod.default[k] = origDefault[k]; });
|
|
646
|
+
} catch {}
|
|
647
|
+
}, 50);
|
|
648
|
+
})();
|
|
536
649
|
|
|
537
650
|
console.log(`[RNDebugSDK] Connected to ${HOST} | Console+Network:${PORTS.NETWORK_AND_CONSOLE} Redux:${PORTS.REDUX} Storage:${PORTS.STORAGE}`);
|
|
538
651
|
|
package/styles.css
CHANGED
|
@@ -369,6 +369,76 @@ body {
|
|
|
369
369
|
.tab:hover:not(.active) { color: var(--text); }
|
|
370
370
|
.ml-auto { margin-left: auto; }
|
|
371
371
|
|
|
372
|
+
/* Panel clear button (used in Console, Network, GA4, Redux) */
|
|
373
|
+
.panel-clear-btn {
|
|
374
|
+
padding: 3px 10px;
|
|
375
|
+
border: 1px solid var(--border2);
|
|
376
|
+
border-radius: 4px;
|
|
377
|
+
background: transparent;
|
|
378
|
+
color: var(--text-dim);
|
|
379
|
+
font-family: inherit;
|
|
380
|
+
font-size: 10px;
|
|
381
|
+
font-weight: 500;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
transition: all 0.12s;
|
|
384
|
+
flex-shrink: 0;
|
|
385
|
+
}
|
|
386
|
+
.panel-clear-btn:hover { border-color: var(--red); color: var(--red); }
|
|
387
|
+
|
|
388
|
+
/* Console level dropdown */
|
|
389
|
+
.console-level-dropdown { position: relative; }
|
|
390
|
+
.console-level-btn {
|
|
391
|
+
padding: 3px 10px;
|
|
392
|
+
border: 1px solid var(--border2);
|
|
393
|
+
border-radius: 4px;
|
|
394
|
+
background: var(--bg3);
|
|
395
|
+
color: var(--text);
|
|
396
|
+
font-family: inherit;
|
|
397
|
+
font-size: 10px;
|
|
398
|
+
font-weight: 500;
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
white-space: nowrap;
|
|
401
|
+
}
|
|
402
|
+
.console-level-btn:hover { border-color: var(--accent); }
|
|
403
|
+
.console-level-menu {
|
|
404
|
+
display: none;
|
|
405
|
+
position: absolute;
|
|
406
|
+
top: 100%;
|
|
407
|
+
right: 0;
|
|
408
|
+
margin-top: 4px;
|
|
409
|
+
background: var(--bg2);
|
|
410
|
+
border: 1px solid var(--border2);
|
|
411
|
+
border-radius: 6px;
|
|
412
|
+
padding: 6px 0;
|
|
413
|
+
min-width: 140px;
|
|
414
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.3);
|
|
415
|
+
z-index: 100;
|
|
416
|
+
}
|
|
417
|
+
.console-level-menu.open { display: block; }
|
|
418
|
+
.console-level-option {
|
|
419
|
+
display: flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
gap: 8px;
|
|
422
|
+
padding: 5px 12px;
|
|
423
|
+
font-size: 11px;
|
|
424
|
+
color: var(--text);
|
|
425
|
+
cursor: pointer;
|
|
426
|
+
transition: background 0.08s;
|
|
427
|
+
}
|
|
428
|
+
.console-level-option:hover { background: var(--bg3); }
|
|
429
|
+
.console-level-option input[type="checkbox"] {
|
|
430
|
+
width: 14px;
|
|
431
|
+
height: 14px;
|
|
432
|
+
accent-color: var(--accent);
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
}
|
|
435
|
+
.lvl-dot {
|
|
436
|
+
width: 8px;
|
|
437
|
+
height: 8px;
|
|
438
|
+
border-radius: 50%;
|
|
439
|
+
flex-shrink: 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
372
442
|
/* ── SCROLL AREA ─────────────────────────────────────────────────────────────── */
|
|
373
443
|
.scroll-area { flex: 1; overflow-y: auto; overflow-x: hidden; user-select: text; -webkit-user-select: text; }
|
|
374
444
|
.scroll-area::-webkit-scrollbar { width: 4px; }
|
|
@@ -399,22 +469,22 @@ body {
|
|
|
399
469
|
CONSOLE PANEL
|
|
400
470
|
───────────────────────────────────────────────────────────────────────────── */
|
|
401
471
|
.log-row {
|
|
402
|
-
display:
|
|
403
|
-
|
|
404
|
-
gap:
|
|
405
|
-
align-items: baseline;
|
|
472
|
+
display: flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
gap: 6px;
|
|
406
475
|
padding: 4px 14px;
|
|
407
476
|
border-bottom: 1px solid transparent;
|
|
408
477
|
transition: background 0.08s;
|
|
409
478
|
cursor: default;
|
|
479
|
+
overflow: hidden;
|
|
410
480
|
}
|
|
411
481
|
.log-row:hover { background: var(--bg3); }
|
|
412
482
|
.log-row.warn { background: rgba(245,200,66,.03); border-bottom-color: rgba(245,200,66,.06); }
|
|
413
483
|
.log-row.error { background: rgba(255,94,114,.04); border-bottom-color: rgba(255,94,114,.07); }
|
|
414
484
|
|
|
415
|
-
.log-time { color: var(--text-dim); font-size: 10px; white-space: nowrap; }
|
|
485
|
+
.log-time { color: var(--text-dim); font-size: 10px; white-space: nowrap; flex-shrink: 0; }
|
|
416
486
|
|
|
417
|
-
.lvl-badge { display: inline-block; font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
487
|
+
.lvl-badge { display: inline-block; font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0; }
|
|
418
488
|
.lvl-log { background: rgba(200,210,230,.07); color: var(--text-dim); }
|
|
419
489
|
.lvl-info { background: rgba(79,172,255,.12); color: var(--accent); }
|
|
420
490
|
.lvl-warn { background: rgba(245,200,66,.12); color: var(--yellow); }
|
|
@@ -426,25 +496,25 @@ body {
|
|
|
426
496
|
flex-direction: column;
|
|
427
497
|
cursor: pointer;
|
|
428
498
|
min-width: 0;
|
|
499
|
+
flex: 1;
|
|
429
500
|
}
|
|
430
501
|
.log-arrow {
|
|
431
502
|
display: inline-block;
|
|
432
|
-
width:
|
|
503
|
+
width: 12px;
|
|
433
504
|
font-size: 8px;
|
|
434
505
|
color: var(--text-dim);
|
|
435
506
|
flex-shrink: 0;
|
|
436
507
|
user-select: none;
|
|
437
508
|
-webkit-user-select: none;
|
|
438
|
-
margin-bottom: 1px;
|
|
439
509
|
}
|
|
440
510
|
.log-arrow.expanded { color: var(--accent); }
|
|
441
511
|
.log-preview {
|
|
442
512
|
display: flex;
|
|
443
|
-
align-items:
|
|
444
|
-
gap:
|
|
513
|
+
align-items: center;
|
|
514
|
+
gap: 6px;
|
|
445
515
|
color: var(--text);
|
|
446
516
|
font-size: 11px;
|
|
447
|
-
line-height: 1.
|
|
517
|
+
line-height: 1.4;
|
|
448
518
|
overflow: hidden;
|
|
449
519
|
white-space: nowrap;
|
|
450
520
|
user-select: text;
|
|
@@ -759,8 +829,17 @@ body {
|
|
|
759
829
|
.kv-grid { display: grid; grid-template-columns: auto 1fr; gap: 2px 12px; }
|
|
760
830
|
.kv-key { color: var(--accent); font-size: 11px; }
|
|
761
831
|
.kv-val { color: var(--text); word-break: break-all; font-size: 11px; }
|
|
762
|
-
.section-label {
|
|
763
|
-
|
|
832
|
+
.section-label {
|
|
833
|
+
font-size: 10px;
|
|
834
|
+
text-transform: uppercase;
|
|
835
|
+
letter-spacing: 1px;
|
|
836
|
+
color: var(--text-dim);
|
|
837
|
+
margin-bottom: 4px;
|
|
838
|
+
margin-top: 0;
|
|
839
|
+
padding-top: 8px;
|
|
840
|
+
border-top: 1px solid var(--border);
|
|
841
|
+
}
|
|
842
|
+
.section-label:first-child { border-top: none; padding-top: 0; }
|
|
764
843
|
|
|
765
844
|
/* Context menu */
|
|
766
845
|
.ctx-menu {
|
|
@@ -1264,6 +1343,130 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
|
|
|
1264
1343
|
───────────────────────────────────────────────────────────────────────────── */
|
|
1265
1344
|
.memory-layout { display: flex; flex-direction: column; height: 100%; }
|
|
1266
1345
|
|
|
1346
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
1347
|
+
GA4 EVENT INSPECTOR
|
|
1348
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
1349
|
+
.ga4-layout { display: flex; flex: 1; overflow: hidden; }
|
|
1350
|
+
.ga4-list-pane { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
|
1351
|
+
/* Resize handle between list and detail */
|
|
1352
|
+
.ga4-resize-handle {
|
|
1353
|
+
width: 5px;
|
|
1354
|
+
cursor: col-resize;
|
|
1355
|
+
background: var(--border);
|
|
1356
|
+
flex-shrink: 0;
|
|
1357
|
+
transition: background 0.15s;
|
|
1358
|
+
}
|
|
1359
|
+
.ga4-resize-handle:hover { background: var(--accent); opacity: 0.5; }
|
|
1360
|
+
|
|
1361
|
+
.ga4-detail-pane {
|
|
1362
|
+
width: 60%;
|
|
1363
|
+
min-width: 350px;
|
|
1364
|
+
display: flex;
|
|
1365
|
+
flex-direction: column;
|
|
1366
|
+
overflow: hidden;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
.ga4-list-header {
|
|
1370
|
+
display: flex;
|
|
1371
|
+
align-items: center;
|
|
1372
|
+
padding: 4px 10px;
|
|
1373
|
+
background: var(--bg2);
|
|
1374
|
+
border-bottom: 1px solid var(--border);
|
|
1375
|
+
font-size: 9px;
|
|
1376
|
+
font-weight: 600;
|
|
1377
|
+
color: var(--text-dim);
|
|
1378
|
+
text-transform: uppercase;
|
|
1379
|
+
letter-spacing: 0.5px;
|
|
1380
|
+
flex-shrink: 0;
|
|
1381
|
+
}
|
|
1382
|
+
.ga4-hcell { padding: 3px 6px; }
|
|
1383
|
+
.ga4-sort-btn:hover { color: var(--accent); }
|
|
1384
|
+
.ga4-sort-btn span { font-size: 8px; color: var(--accent); }
|
|
1385
|
+
|
|
1386
|
+
.ga4-row {
|
|
1387
|
+
display: flex;
|
|
1388
|
+
align-items: center;
|
|
1389
|
+
padding: 5px 10px;
|
|
1390
|
+
border-bottom: 1px solid var(--border);
|
|
1391
|
+
cursor: pointer;
|
|
1392
|
+
transition: background 0.08s;
|
|
1393
|
+
font-size: 11px;
|
|
1394
|
+
}
|
|
1395
|
+
.ga4-row:hover { background: var(--bg3); }
|
|
1396
|
+
.ga4-row.selected { background: var(--bg4); border-left: 2px solid var(--accent); }
|
|
1397
|
+
|
|
1398
|
+
.ga4-cell { padding: 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1399
|
+
.ga4-time { width: 90px; color: var(--text-dim); font-size: 10px; flex-shrink: 0; }
|
|
1400
|
+
.ga4-name { flex: 1; color: var(--text-bright); font-weight: 500; min-width: 0; }
|
|
1401
|
+
|
|
1402
|
+
/* Detail pane */
|
|
1403
|
+
.ga4-detail-header {
|
|
1404
|
+
padding: 8px 14px;
|
|
1405
|
+
font-size: 10px;
|
|
1406
|
+
font-weight: 700;
|
|
1407
|
+
color: var(--text-dim);
|
|
1408
|
+
text-transform: uppercase;
|
|
1409
|
+
letter-spacing: 1px;
|
|
1410
|
+
border-bottom: 1px solid var(--border);
|
|
1411
|
+
background: var(--bg2);
|
|
1412
|
+
flex-shrink: 0;
|
|
1413
|
+
}
|
|
1414
|
+
.ga4-detail-content { flex: 1; overflow-y: auto; padding: 10px 14px; user-select: text; -webkit-user-select: text; }
|
|
1415
|
+
.ga4-detail-info { margin-bottom: 8px; }
|
|
1416
|
+
.ga4-detail-row { display: flex; gap: 12px; padding: 3px 0; font-size: 11px; }
|
|
1417
|
+
.ga4-detail-key { color: var(--text-dim); min-width: 100px; flex-shrink: 0; }
|
|
1418
|
+
.ga4-detail-val { color: var(--text); word-break: break-all; }
|
|
1419
|
+
.ga4-detail-sep { height: 1px; background: var(--border); margin: 8px 0; }
|
|
1420
|
+
|
|
1421
|
+
.ga4-param-row {
|
|
1422
|
+
display: flex;
|
|
1423
|
+
gap: 8px;
|
|
1424
|
+
padding: 4px 0;
|
|
1425
|
+
font-size: 11px;
|
|
1426
|
+
border-bottom: 1px solid var(--border);
|
|
1427
|
+
align-items: flex-start;
|
|
1428
|
+
}
|
|
1429
|
+
.ga4-param-row:last-child { border-bottom: none; }
|
|
1430
|
+
.ga4-param-key { color: var(--accent); width: 180px; min-width: 180px; flex-shrink: 0; font-weight: 500; padding-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1431
|
+
.ga4-param-val { color: var(--text); flex: 1; min-width: 0; }
|
|
1432
|
+
.ga4-param-val .ov-node, .ga4-param-val .ov-leaf { white-space: normal; }
|
|
1433
|
+
.ga4-param-val .ov-key { white-space: nowrap; }
|
|
1434
|
+
.ga4-param-val .ov-header { flex-wrap: nowrap; }
|
|
1435
|
+
|
|
1436
|
+
/* Summary bar */
|
|
1437
|
+
.ga4-summary {
|
|
1438
|
+
display: flex;
|
|
1439
|
+
align-items: center;
|
|
1440
|
+
gap: 8px;
|
|
1441
|
+
padding: 6px 14px;
|
|
1442
|
+
background: var(--bg2);
|
|
1443
|
+
border-top: 1px solid var(--border);
|
|
1444
|
+
font-size: 10px;
|
|
1445
|
+
color: var(--text-dim);
|
|
1446
|
+
flex-shrink: 0;
|
|
1447
|
+
overflow-x: auto;
|
|
1448
|
+
overflow-y: hidden;
|
|
1449
|
+
white-space: nowrap;
|
|
1450
|
+
}
|
|
1451
|
+
.ga4-summary::-webkit-scrollbar { height: 3px; }
|
|
1452
|
+
.ga4-summary::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
|
|
1453
|
+
.ga4-summary-label { font-weight: 600; color: var(--text-mid); flex-shrink: 0; }
|
|
1454
|
+
.ga4-summary-chip {
|
|
1455
|
+
flex-shrink: 0;
|
|
1456
|
+
padding: 2px 8px;
|
|
1457
|
+
border-radius: 10px;
|
|
1458
|
+
border: 1px solid var(--border2);
|
|
1459
|
+
cursor: pointer;
|
|
1460
|
+
transition: all 0.12s;
|
|
1461
|
+
white-space: nowrap;
|
|
1462
|
+
}
|
|
1463
|
+
.ga4-summary-chip:hover { border-color: var(--accent); color: var(--accent); }
|
|
1464
|
+
.ga4-summary-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
1465
|
+
.ga4-summary-chip b { font-weight: 500; color: var(--text); }
|
|
1466
|
+
.ga4-summary-chip.active b { color: #fff; }
|
|
1467
|
+
.ga4-summary-chip .chip-count { color: var(--text-dim); margin-left: 3px; }
|
|
1468
|
+
.ga4-summary-chip.active .chip-count { color: rgba(255,255,255,0.7); }
|
|
1469
|
+
|
|
1267
1470
|
/* ── LIGHT THEME OVERRIDES ───────────────────────────────────────────────────── */
|
|
1268
1471
|
[data-theme="light"] .tb-btn.primary {
|
|
1269
1472
|
background: rgba(9,105,218,.08);
|