reactoradar 1.5.9 → 1.6.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/bin/setup.js CHANGED
@@ -85,23 +85,79 @@ function detectPlatform() {
85
85
  const xcrun = tryExec('xcrun simctl list devices booted 2>/dev/null');
86
86
  const hasIOSSim = xcrun.includes('Booted');
87
87
 
88
- return { hasAndroidEmu, hasAndroidDevice, hasIOSSim };
88
+ // Check if iOS real device is connected via USB
89
+ let hasIOSDevice = false;
90
+ // Method 1: idevice_id (from libimobiledevice — brew install libimobiledevice)
91
+ const idevice = tryExec('idevice_id -l 2>/dev/null');
92
+ if (idevice && idevice.trim().length > 0) hasIOSDevice = true;
93
+ // Method 2: system_profiler (slower but always available on macOS)
94
+ if (!hasIOSDevice) {
95
+ const profiler = tryExec('system_profiler SPUSBDataType 2>/dev/null');
96
+ if (profiler && (profiler.includes('iPhone') || profiler.includes('iPad'))) hasIOSDevice = true;
97
+ }
98
+
99
+ return { hasAndroidEmu, hasAndroidDevice, hasIOSSim, hasIOSDevice };
100
+ }
101
+
102
+ function getLanIP() {
103
+ // Get the Mac's LAN IP address for real device connections
104
+ const os = require('os');
105
+ const interfaces = os.networkInterfaces();
106
+ for (const name of Object.keys(interfaces)) {
107
+ for (const iface of interfaces[name]) {
108
+ // Skip internal/loopback and IPv6
109
+ if (iface.internal || iface.family !== 'IPv4') continue;
110
+ // Prefer en0 (Wi-Fi) or en1
111
+ if (iface.address && iface.address.startsWith('192.168.') || iface.address.startsWith('10.') || iface.address.startsWith('172.')) {
112
+ return iface.address;
113
+ }
114
+ }
115
+ }
116
+ return null;
89
117
  }
90
118
 
91
119
  function pickHost(platform) {
92
- if (platform.hasIOSSim && !platform.hasAndroidEmu && !platform.hasAndroidDevice) {
120
+ const hasRealDevice = platform.hasAndroidDevice || platform.hasIOSDevice;
121
+ const hasSimOrEmu = platform.hasIOSSim || platform.hasAndroidEmu;
122
+
123
+ // Real device (iOS or Android) — needs LAN IP or adb reverse
124
+ if (platform.hasIOSDevice && !hasSimOrEmu) {
125
+ const lanIP = getLanIP();
126
+ if (lanIP) return { host: lanIP, reason: `iOS device detected via USB (LAN IP: ${lanIP})` };
127
+ return { host: '127.0.0.1', reason: 'iOS device detected but LAN IP not found — ensure Mac and device are on same WiFi' };
128
+ }
129
+ if (platform.hasAndroidDevice && !hasSimOrEmu) {
130
+ return { host: '10.0.2.2', reason: 'Android device detected (using adb reverse)' };
131
+ }
132
+
133
+ // Simulator/Emulator only
134
+ if (platform.hasIOSSim && !platform.hasAndroidEmu && !hasRealDevice) {
93
135
  return { host: '127.0.0.1', reason: 'iOS Simulator detected' };
94
136
  }
95
- if (platform.hasAndroidEmu && !platform.hasIOSSim) {
137
+ if (platform.hasAndroidEmu && !platform.hasIOSSim && !hasRealDevice) {
96
138
  return { host: '10.0.2.2', reason: 'Android Emulator detected' };
97
139
  }
98
- if (platform.hasAndroidDevice) {
99
- return { host: '10.0.2.2', reason: 'Android device detected (using adb reverse)' };
140
+
141
+ // Mixed: real device + simulator/emulator
142
+ if (hasRealDevice && hasSimOrEmu) {
143
+ // If iOS device + iOS sim: prefer sim (127.0.0.1), user can switch for device
144
+ if (platform.hasIOSSim) {
145
+ return { host: '127.0.0.1', reason: 'iOS Sim + real device detected (defaulting to Sim — change HOST for device)' };
146
+ }
147
+ // Android device + emu: adb reverse handles both
148
+ return { host: '10.0.2.2', reason: 'Android Emu + device detected (adb reverse handles both)' };
100
149
  }
150
+
151
+ // Both sim + emu
101
152
  if (platform.hasIOSSim && platform.hasAndroidEmu) {
102
153
  return { host: '127.0.0.1', reason: 'Both iOS Sim + Android Emu detected (defaulting to iOS, Android uses adb reverse)' };
103
154
  }
104
- // Nothing running — default to localhost
155
+
156
+ // Nothing running — try to detect LAN IP for real device use later
157
+ const lanIP = getLanIP();
158
+ if (lanIP) {
159
+ return { host: '127.0.0.1', reason: `No running devices (default). For real device use HOST: ${lanIP}` };
160
+ }
105
161
  return { host: '127.0.0.1', reason: 'No running devices detected (default)' };
106
162
  }
107
163
 
@@ -422,6 +478,26 @@ ${SDK_MARKER_END}
422
478
  console.log(C.dim + ' adb reverse tcp:9090 tcp:9090 && adb reverse tcp:9091 tcp:9091 && adb reverse tcp:9092 tcp:9092' + C.reset);
423
479
  console.log();
424
480
  }
481
+ if (platform.hasIOSDevice) {
482
+ const lanIP = getLanIP();
483
+ console.log(C.bold + ' iOS Real Device:' + C.reset);
484
+ if (lanIP) {
485
+ console.log(' HOST is set to ' + C.cyan + lanIP + C.reset + ' (your Mac\'s LAN IP)');
486
+ console.log(' Ensure your device is on the ' + C.bold + 'same WiFi network' + C.reset + ' as this Mac');
487
+ } else {
488
+ console.log(C.yellow + ' Could not detect LAN IP. Manually set HOST in src/debug/RNDebugSDK.js' + C.reset);
489
+ console.log(' to your Mac\'s IP (e.g., 192.168.1.x). Find it with: ' + C.cyan + 'ifconfig en0' + C.reset);
490
+ }
491
+ console.log();
492
+ }
493
+ // Show LAN IP tip even when no device is connected (for future reference)
494
+ if (!platform.hasIOSDevice && !platform.hasAndroidDevice) {
495
+ const lanIP = getLanIP();
496
+ if (lanIP) {
497
+ console.log(C.dim + ' For real device debugging, change HOST in src/debug/RNDebugSDK.js to: ' + C.cyan + lanIP + C.reset);
498
+ console.log();
499
+ }
500
+ }
425
501
  console.log(C.dim + ' To remove: npx reactoradar remove' + C.reset);
426
502
  console.log();
427
503
  }
package/index.html CHANGED
@@ -21,6 +21,10 @@
21
21
  <span id="deviceText">Waiting for device...</span>
22
22
  </div>
23
23
  <div class="titlebar-actions">
24
+ <button class="tb-btn" id="btnScreenshot" title="Screenshot (⌘S)">
25
+ <svg width="14" height="14" viewBox="0 0 20 20" fill="none" style="vertical-align:middle;margin-right:3px"><rect x="2" y="4" width="16" height="13" rx="2" stroke="currentColor" stroke-width="1.5"/><circle cx="10" cy="11" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M7 4V3a1 1 0 011-1h4a1 1 0 011 1v1" stroke="currentColor" stroke-width="1.5"/></svg>
26
+ Screenshot
27
+ </button>
24
28
  <button class="tb-btn primary" id="btnCDP" title="Open JS Debugger (⌘D)">JS Debugger ↗</button>
25
29
  </div>
26
30
  </header>
package/main.js CHANGED
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const { app, BrowserWindow, ipcMain, Menu, shell, nativeTheme, nativeImage } = require('electron');
3
+ const { app, BrowserWindow, ipcMain, Menu, shell, nativeTheme, nativeImage, dialog } = require('electron');
4
4
  const path = require('path');
5
5
  const http = require('http');
6
6
  const https = require('https');
7
7
  const { WebSocketServer, WebSocket } = require('ws');
8
+ let autoUpdater = null;
9
+ try { autoUpdater = require('electron-updater').autoUpdater; } catch {}
8
10
 
9
11
  // ─── Ports ────────────────────────────────────────────────────────────────────
10
12
  const PORTS = {
@@ -154,6 +156,40 @@ function _semverCompare(a, b) {
154
156
 
155
157
  function checkForUpdates() {
156
158
  const currentVersion = require('./package.json').version;
159
+
160
+ // ─── Electron Auto-Updater (for .dmg installs) ────────────────────────────
161
+ // Downloads and installs updates from GitHub Releases automatically.
162
+ if (autoUpdater && app.isPackaged) {
163
+ autoUpdater.autoDownload = true;
164
+ autoUpdater.autoInstallOnAppQuit = true;
165
+
166
+ autoUpdater.on('update-available', (info) => {
167
+ console.log(`[AutoUpdate] New version available: ${info.version}`);
168
+ const payload = { current: currentVersion, latest: info.version, autoUpdate: true };
169
+ [500, 2000].forEach(delay => setTimeout(() => _send('update-available', payload), delay));
170
+ });
171
+
172
+ autoUpdater.on('update-downloaded', (info) => {
173
+ console.log(`[AutoUpdate] Update downloaded: ${info.version}`);
174
+ _send('update-downloaded', { version: info.version });
175
+ });
176
+
177
+ autoUpdater.on('error', (err) => {
178
+ console.warn('[AutoUpdate] Error:', err?.message);
179
+ });
180
+
181
+ // Check after a short delay to not block startup
182
+ setTimeout(() => {
183
+ try { autoUpdater.checkForUpdates(); } catch {}
184
+ }, 5000);
185
+ // Also check periodically (every 2 hours)
186
+ setInterval(() => {
187
+ try { autoUpdater.checkForUpdates(); } catch {}
188
+ }, 2 * 60 * 60 * 1000);
189
+ return;
190
+ }
191
+
192
+ // ─── Fallback: npm registry check (for npx users) ─────────────────────────
157
193
  https.get('https://registry.npmjs.org/reactoradar/latest', (res) => {
158
194
  let data = '';
159
195
  res.on('data', d => data += d);
@@ -161,20 +197,15 @@ function checkForUpdates() {
161
197
  try {
162
198
  const latest = JSON.parse(data).version;
163
199
  if (latest && _semverCompare(latest, currentVersion) > 0) {
164
- // Send with retries to ensure renderer catches it after did-finish-load
165
- const payload = { current: currentVersion, latest };
200
+ const payload = { current: currentVersion, latest, autoUpdate: false };
166
201
  [500, 2000, 5000].forEach(delay => {
167
- setTimeout(() => {
168
- if (mainWindow && !mainWindow.isDestroyed()) {
169
- _send('update-available', payload);
170
- }
171
- }, delay);
202
+ setTimeout(() => _send('update-available', payload), delay);
172
203
  });
173
204
  console.log(`[Update] New version available: ${latest} (current: ${currentVersion})`);
174
205
  }
175
206
  } catch {}
176
207
  });
177
- }).on('error', () => {}); // Silently fail — update check is optional
208
+ }).on('error', () => {});
178
209
  }
179
210
 
180
211
  // ─── CDP DevTools Window (JS breakpoints, Sources, Console) ──────────────────
@@ -522,6 +553,27 @@ function setupIPC() {
522
553
  }
523
554
  });
524
555
 
556
+ ipcMain.on('install-update', () => {
557
+ if (autoUpdater) {
558
+ autoUpdater.quitAndInstall(false, true);
559
+ }
560
+ });
561
+
562
+ ipcMain.on('capture-screenshot', async () => {
563
+ try {
564
+ if (!mainWindow || mainWindow.isDestroyed()) return;
565
+ const image = await mainWindow.webContents.capturePage();
566
+ const png = image.toPNG();
567
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
568
+ const filePath = path.join(app.getPath('downloads'), `ReactoRadar-${ts}.png`);
569
+ require('fs').writeFileSync(filePath, png);
570
+ shell.showItemInFolder(filePath);
571
+ console.log(`[Screenshot] Saved to ${filePath}`);
572
+ } catch (e) {
573
+ console.error('[Screenshot] Failed:', e.message);
574
+ }
575
+ });
576
+
525
577
  ipcMain.on('set-theme', (_, theme) => {
526
578
  nativeTheme.themeSource = theme === 'light' ? 'light' : 'dark';
527
579
  const bg = theme === 'light' ? '#f5f6f8' : '#0a0b0e';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.5.9",
4
+ "version": "1.6.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": {
@@ -50,6 +50,7 @@
50
50
  "pack": "electron-builder --mac --dir"
51
51
  },
52
52
  "dependencies": {
53
+ "electron-updater": "^6.8.3",
53
54
  "react-devtools-core": "^5.3.1",
54
55
  "ws": "^8.17.0"
55
56
  },
@@ -59,8 +60,15 @@
59
60
  "electron-devtools-installer": "^3.2.0"
60
61
  },
61
62
  "build": {
62
- "appId": "com.yourteam.rn-debugger",
63
+ "appId": "com.reactoradar.app",
63
64
  "productName": "ReactoRadar",
65
+ "publish": [
66
+ {
67
+ "provider": "github",
68
+ "owner": "sharanagouda",
69
+ "repo": "react-native-debugger"
70
+ }
71
+ ],
64
72
  "mac": {
65
73
  "category": "public.app-category.developer-tools",
66
74
  "icon": "ReactoRadar.icns",
package/preload.js CHANGED
@@ -10,7 +10,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
10
10
  const allowed = [
11
11
  'ports', 'cdp-targets', 'redux-event', 'storage-event', 'network-event',
12
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', 'focus-search',
13
+ 'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available', 'update-downloaded', 'app-version', 'focus-search',
14
14
  ];
15
15
  if (allowed.includes(channel)) {
16
16
  ipcRenderer.removeAllListeners(channel);
@@ -29,4 +29,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
29
29
  setMetroPort: (port) => ipcRenderer.send('set-metro-port', port),
30
30
  readSourceFile: (filepath) => ipcRenderer.invoke('read-source-file', filepath),
31
31
  openExternal: (url) => ipcRenderer.send('open-external', url),
32
+ installUpdate: () => ipcRenderer.send('install-update'),
33
+ captureScreenshot: () => ipcRenderer.send('capture-screenshot'),
32
34
  });
package/styles.css CHANGED
@@ -180,7 +180,7 @@ html, body, #app { height: 100%; overflow: hidden; }
180
180
  body {
181
181
  background: var(--bg);
182
182
  color: var(--text);
183
- font-family: 'JetBrains Mono', monospace;
183
+ font-family: 'SFMono-Regular', 'SF Mono', Menlo, Monaco, monospace;
184
184
  font-size: 12px;
185
185
  -webkit-font-smoothing: antialiased;
186
186
  user-select: text;
@@ -773,7 +773,27 @@ body {
773
773
  .net-initiator { color: var(--text-dim); font-size: 10px; }
774
774
  .net-size { color: var(--text-dim); font-size: 10px; text-align: right; }
775
775
  .net-time { color: var(--text-dim); font-size: 10px; text-align: right; }
776
- .net-time.slow { color: var(--orange); }
776
+ .net-time.slow { color: var(--orange); font-weight: 700; }
777
+ .net-time.very-slow { color: var(--red); font-weight: 700; }
778
+ .net-row.slow { background: rgba(255,165,0,.04); }
779
+ .net-row.slow:hover { background: rgba(255,165,0,.08); }
780
+ .net-row.very-slow { background: rgba(255,94,114,.04); }
781
+ .net-row.very-slow:hover { background: rgba(255,94,114,.08); }
782
+ .net-row.slow .net-path { color: var(--orange); }
783
+ .net-row.very-slow .net-path { color: var(--red); }
784
+
785
+ /* Status filter buttons */
786
+ .net-status-filters { display: flex; gap: 4px; margin-left: 8px; }
787
+ .net-status-btn { font-size: 10px; padding: 2px 8px; border-radius: 3px; border: 1px solid var(--border); background: transparent; color: var(--text-dim); cursor: pointer; }
788
+ .net-status-btn:hover { background: var(--bg3); }
789
+ .net-status-btn.active { background: var(--bg4); color: var(--text); border-color: var(--accent); }
790
+ .net-slow-btn.active { border-color: var(--orange); color: var(--orange); }
791
+
792
+ /* Stats bar at bottom of network panel */
793
+ .net-stats-bar { display: flex; align-items: center; gap: 6px; padding: 4px 12px; font-size: 10px; color: var(--text-dim); background: var(--bg2); border-top: 1px solid var(--border); flex-shrink: 0; user-select: text; }
794
+ .net-stats-sep { color: var(--border); }
795
+ .net-stats-bar .warn { color: var(--orange); font-weight: 700; }
796
+ .net-stats-bar .err { color: var(--red); font-weight: 700; }
777
797
 
778
798
  /* Waterfall bar */
779
799
  .net-waterfall { position: relative; height: 14px; }
@@ -924,7 +944,10 @@ body {
924
944
  .rdx-entry-header:hover { background: var(--bg3); }
925
945
  .rdx-entry.selected .rdx-entry-header { border-left: 3px solid var(--accent); }
926
946
  .rdx-index { font-size: 9px; color: var(--text-dim); min-width: 20px; text-align: right; flex-shrink: 0; }
927
- .rdx-type { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-bright); font-weight: 500; }
947
+ .rdx-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-bright); font-weight: 500; }
948
+ .rdx-type-cat { font-weight: 700; }
949
+ .rdx-type-name { color: var(--text-bright); font-weight: 500; }
950
+ .rdx-header-right { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
928
951
  .rdx-changes {
929
952
  font-size: 8px; font-weight: 700; padding: 1px 5px; border-radius: 8px;
930
953
  background: rgba(255,94,114,.12); color: var(--red); flex-shrink: 0;
@@ -968,12 +991,16 @@ body {
968
991
  .rdx-diff-old { color: var(--red); text-decoration: line-through; opacity: 0.8; word-break: break-all; }
969
992
  .rdx-diff-arrow { color: var(--text-dim); font-size: 10px; flex-shrink: 0; }
970
993
  .rdx-diff-new { color: var(--green); font-weight: 600; word-break: break-all; }
971
- .rdx-state-label { font-size: 10px; font-weight: 700; padding: 4px 8px 2px; margin-top: 4px; letter-spacing: 0.5px; }
994
+ .rdx-state-label { font-size: 10px; font-weight: 700; padding: 4px 0 2px; letter-spacing: 0.5px; }
972
995
  .rdx-state-label.prev { color: var(--red); }
973
996
  .rdx-state-label.curr { color: var(--green); }
974
- .rdx-state-tree { padding: 2px 0 4px 4px; border-left: 2px solid var(--border); margin-left: 4px; }
975
- .rdx-state-tree.prev { border-left-color: rgba(255,94,114,.3); }
976
- .rdx-state-tree.curr { border-left-color: rgba(61,214,140,.3); }
997
+ .rdx-diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 4px; }
998
+ .rdx-diff-col { min-width: 0; overflow: auto; max-height: 400px; padding: 4px; border-radius: 4px; font-size: 11px; }
999
+ .rdx-diff-col.prev { background: rgba(255,94,114,.04); border: 1px solid rgba(255,94,114,.12); }
1000
+ .rdx-diff-col.curr { background: rgba(61,214,140,.04); border: 1px solid rgba(61,214,140,.12); }
1001
+ .rdx-close-btn { position: absolute; top: 6px; right: 8px; background: var(--bg3); border: 1px solid var(--border); color: var(--text-dim); font-size: 11px; width: 22px; height: 22px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 1; }
1002
+ .rdx-close-btn:hover { background: var(--bg4); color: var(--text); }
1003
+ .rdx-entry-detail { position: relative; }
977
1004
  .rdx-highlight { font-weight: 600; }
978
1005
  .rdx-diff-sign { font-weight: 700; font-size: 11px; flex-shrink: 0; width: 14px; text-align: center; }
979
1006
  .rdx-diff-row.removed .rdx-diff-sign { color: var(--red); }
@@ -1133,6 +1160,23 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
1133
1160
  padding: 24px;
1134
1161
  max-width: 480px;
1135
1162
  }
1163
+ .settings-two-col {
1164
+ display: grid;
1165
+ grid-template-columns: 1fr 1fr;
1166
+ gap: 24px;
1167
+ padding: 20px 24px;
1168
+ }
1169
+ .settings-col-left, .settings-col-right {
1170
+ min-width: 0;
1171
+ }
1172
+ .settings-shortcut-grid {
1173
+ display: grid;
1174
+ grid-template-columns: auto 1fr;
1175
+ gap: 6px 12px;
1176
+ font-size: 11px;
1177
+ }
1178
+ .sc-key { color: var(--text-mid); font-weight: 600; background: var(--bg3); padding: 2px 6px; border-radius: 3px; text-align: center; font-size: 10px; }
1179
+ .sc-label { color: var(--text-dim); }
1136
1180
  .settings-section {
1137
1181
  margin-bottom: 28px;
1138
1182
  }
@@ -1448,9 +1492,9 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
1448
1492
  .ga4-row:hover { background: var(--bg3); }
1449
1493
  .ga4-row.selected { background: var(--bg4); border-left: 2px solid var(--accent); }
1450
1494
 
1451
- .ga4-cell { padding: 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1452
- .ga4-time { width: 90px; color: var(--text-dim); font-size: 10px; flex-shrink: 0; }
1453
- .ga4-name { flex: 1; color: var(--text-bright); font-weight: 500; min-width: 0; }
1495
+ .ga4-cell { padding: 0 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1496
+ .ga4-time { width: 100px; color: var(--text-dim); font-size: inherit; flex-shrink: 0; padding-right: 12px; }
1497
+ .ga4-name { flex: 1; color: var(--text-bright); font-weight: 600; min-width: 0; font-size: 1.1em; }
1454
1498
 
1455
1499
  /* Detail pane */
1456
1500
  .ga4-detail-header {
@@ -1468,7 +1512,7 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
1468
1512
  .ga4-detail-info { margin-bottom: 8px; }
1469
1513
  .ga4-detail-row { display: flex; gap: 12px; padding: 3px 0; font-size: 11px; }
1470
1514
  .ga4-detail-key { color: var(--text-dim); min-width: 100px; flex-shrink: 0; }
1471
- .ga4-detail-val { color: var(--text); word-break: break-all; }
1515
+ .ga4-detail-val { color: var(--text); word-break: break-word; overflow-wrap: break-word; white-space: pre-wrap; line-height: 1.5; }
1472
1516
  .ga4-detail-sep { height: 1px; background: var(--border); margin: 8px 0; }
1473
1517
 
1474
1518
  .ga4-param-row {
@@ -1481,10 +1525,11 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
1481
1525
  }
1482
1526
  .ga4-param-row:last-child { border-bottom: none; }
1483
1527
  .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; }
1484
- .ga4-param-val { color: var(--text); flex: 1; min-width: 0; }
1485
- .ga4-param-val .ov-node, .ga4-param-val .ov-leaf { white-space: normal; }
1528
+ .ga4-param-val { color: var(--text); flex: 1; min-width: 0; word-break: break-word; overflow-wrap: break-word; white-space: pre-wrap; line-height: 1.5; }
1529
+ .ga4-param-val .ov-node, .ga4-param-val .ov-leaf { white-space: pre-wrap; word-break: break-word; }
1486
1530
  .ga4-param-val .ov-key { white-space: nowrap; }
1487
1531
  .ga4-param-val .ov-header { flex-wrap: nowrap; }
1532
+ .ga4-param-val .ov-str { word-break: break-word; overflow-wrap: break-word; white-space: pre-wrap; }
1488
1533
 
1489
1534
  /* Summary bar */
1490
1535
  .ga4-summary {
@@ -1562,3 +1607,62 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
1562
1607
  [data-theme="light"] .net-cell {
1563
1608
  border-right-color: #bcc1d0;
1564
1609
  }
1610
+
1611
+ /* ── Toast Notifications ──────────────────────────────────────────────────── */
1612
+ .toast-container { position: fixed; bottom: 16px; right: 16px; z-index: 9999; display: flex; flex-direction: column; gap: 6px; max-width: 360px; pointer-events: none; }
1613
+ .toast { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; font-size: 11px; background: var(--bg3); border: 1px solid var(--border); color: var(--text); box-shadow: 0 4px 16px rgba(0,0,0,.3); pointer-events: all; animation: toastIn 0.2s ease-out; }
1614
+ .toast-error { border-left: 3px solid var(--red); }
1615
+ .toast-warn { border-left: 3px solid var(--orange); }
1616
+ .toast-info { border-left: 3px solid var(--accent); }
1617
+ .toast-msg { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1618
+ .toast-action { color: var(--accent); font-weight: 600; cursor: pointer; font-size: 10px; flex-shrink: 0; }
1619
+ .toast-action:hover { text-decoration: underline; }
1620
+ .toast-close { color: var(--text-dim); cursor: pointer; font-size: 10px; flex-shrink: 0; margin-left: 4px; }
1621
+ .toast-close:hover { color: var(--text); }
1622
+ @keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
1623
+
1624
+ /* ── Context Menu Separator ────────────────────────────────────────────────── */
1625
+ .ctx-sep { height: 1px; background: var(--border); margin: 4px 6px; }
1626
+
1627
+ /* ── Hidden URLs Dropdown ─────────────────────────────────────────────────── */
1628
+ .net-hidden-wrap { display: inline-flex; }
1629
+ .net-hidden-btn { font-size: 10px !important; color: var(--text-dim) !important; }
1630
+ .net-hidden-dropdown { position: absolute; top: 100%; right: 0; z-index: 100; min-width: 320px; max-width: 480px; max-height: 300px; overflow-y: auto; background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,.3); padding: 6px 0; }
1631
+ .net-hidden-title { display: flex; align-items: center; justify-content: space-between; padding: 4px 10px 6px; font-size: 10px; font-weight: 600; color: var(--text-dim); border-bottom: 1px solid var(--border); margin-bottom: 4px; }
1632
+ .net-hidden-clear { font-size: 9px; background: transparent; border: 1px solid var(--border); color: var(--red); border-radius: 3px; padding: 2px 6px; cursor: pointer; }
1633
+ .net-hidden-clear:hover { background: rgba(255,94,114,.1); }
1634
+ .net-hidden-row { display: flex; align-items: center; gap: 8px; padding: 4px 10px; font-size: 10px; }
1635
+ .net-hidden-row:hover { background: var(--bg3); }
1636
+ .net-hidden-url { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-mid); }
1637
+ .net-hidden-unhide { font-size: 9px; background: transparent; border: 1px solid var(--border); color: var(--accent); border-radius: 3px; padding: 2px 6px; cursor: pointer; flex-shrink: 0; }
1638
+ .net-hidden-unhide:hover { background: rgba(61,136,255,.1); }
1639
+
1640
+ /* ── Support Button ────────────────────────────────────────────────────────── */
1641
+ .support-btn { background: linear-gradient(135deg, #ff813f, #ff5e72); color: #fff; border: none; padding: 8px 20px; border-radius: 8px; font-size: 12px; font-weight: 700; cursor: pointer; transition: all 0.15s; letter-spacing: 0.3px; }
1642
+ .support-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(255,94,114,.3); }
1643
+ .support-btn:active { transform: translateY(0); }
1644
+
1645
+ /* ── Tab Visibility Grid (Settings) ────────────────────────────────────────── */
1646
+ .tab-visibility-grid { display: flex; flex-direction: column; gap: 3px; max-width: 240px; }
1647
+ .tab-vis-item { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 4px; border: 1px solid var(--border); cursor: pointer; transition: all 0.12s; user-select: none; }
1648
+ .tab-vis-item.active { background: var(--bg3); border-color: var(--accent); }
1649
+ .tab-vis-item.inactive { opacity: 0.5; background: transparent; }
1650
+ .tab-vis-item:hover { background: var(--bg3); }
1651
+ .tab-vis-check { accent-color: var(--accent); cursor: pointer; width: 13px; height: 13px; }
1652
+ .tab-vis-icon { font-size: 11px; width: 16px; text-align: center; }
1653
+ .tab-vis-label { font-size: 10px; font-weight: 500; color: var(--text); flex: 1; }
1654
+ .tab-vis-required { font-size: 7px; color: var(--text-dim); background: var(--bg4); padding: 1px 4px; border-radius: 3px; }
1655
+ .tab-vis-item.dragging { opacity: 0.4; border-style: dashed; }
1656
+ .tab-vis-item.drag-over { border-color: var(--accent); border-width: 2px; }
1657
+ .tab-vis-drag { cursor: grab; color: var(--text-dim); font-size: 10px; padding: 0 2px; }
1658
+ .tab-vis-drag:active { cursor: grabbing; }
1659
+
1660
+ /* ── Memory Warning Banner ─────────────────────────────────────────────────── */
1661
+ .memory-warning { display: flex; align-items: center; gap: 10px; padding: 6px 14px; background: rgba(255,165,0,.15); border-bottom: 1px solid rgba(255,165,0,.3); color: var(--orange); font-size: 11px; font-weight: 500; z-index: 9998; }
1662
+ .memory-warn-btn { font-size: 10px; padding: 3px 10px; border-radius: 4px; border: 1px solid var(--orange); background: transparent; color: var(--orange); cursor: pointer; font-weight: 600; }
1663
+ .memory-warn-btn:hover { background: rgba(255,165,0,.15); }
1664
+ .memory-warn-btn:first-of-type { background: var(--orange); color: #000; }
1665
+ .memory-warn-btn:first-of-type:hover { opacity: 0.9; }
1666
+
1667
+ /* ── Console Log Grouping ─────────────────────────────────────────────────── */
1668
+ .log-group-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: #fff; font-size: 9px; font-weight: 700; flex-shrink: 0; margin-right: 4px; }