reactoradar 1.4.1 → 1.5.2

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/app.js CHANGED
@@ -6,13 +6,13 @@ const state = {
6
6
  activePanel: 'console',
7
7
  ports: {},
8
8
 
9
- console: { logs: [], levelFilters: { log: true, info: true, warn: true, error: true, debug: true }, searchFilter: '' },
9
+ console: { logs: [], levelFilters: { log: true, info: true, warn: true, error: true, debug: true }, searchFilter: '', showRedux: false },
10
10
 
11
11
  network: {
12
12
  requests: {},
13
13
  order: [],
14
14
  statusFilter: 'all',
15
- typeFilter: 'all',
15
+ typeFilter: 'fetch',
16
16
  searchFilter: '',
17
17
  throttle: 'none',
18
18
  enabled: true,
@@ -26,6 +26,7 @@ const state = {
26
26
  states: [],
27
27
  selected: -1,
28
28
  searchFilter: '',
29
+ sortDir: 'asc',
29
30
  },
30
31
 
31
32
  storage: {
@@ -233,10 +234,31 @@ if (window.electronAPI) {
233
234
 
234
235
  window.electronAPI.on('clear-all-ui', clearAll);
235
236
 
237
+ // Cmd+F — focus the search input for the active panel
238
+ window.electronAPI.on('focus-search', () => {
239
+ const searchMap = {
240
+ console: 'consoleSearch',
241
+ network: 'netSearchInput',
242
+ ga4: 'ga4Search',
243
+ redux: 'reduxSearch',
244
+ storage: 'storageSearch',
245
+ };
246
+ const inputId = searchMap[state.activePanel];
247
+ if (inputId) {
248
+ const el = $(inputId);
249
+ if (el) { el.focus(); el.select(); }
250
+ }
251
+ // Also show/focus Console bottom find bar
252
+ if (state.activePanel === 'console') {
253
+ const bar = $('consoleFindBar');
254
+ if (bar) { bar.style.display = 'flex'; $('consoleFindInput')?.focus(); }
255
+ }
256
+ });
257
+
236
258
  window.electronAPI.on('app-version', (version) => {
237
259
  state._appVersion = version;
238
- const el = $('aboutVersion');
239
- if (el) el.textContent = 'v' + version;
260
+ // Update anywhere the version is displayed
261
+ document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
240
262
  });
241
263
 
242
264
  window.electronAPI.on('update-available', ({ current, latest }) => {
@@ -299,7 +321,7 @@ function getStoredLogLevels() {
299
321
  const saved = localStorage.getItem('rn-debug-log-levels');
300
322
  if (saved) return JSON.parse(saved);
301
323
  } catch {}
302
- return { log: true, info: true, warn: true, error: true, debug: true };
324
+ return { log: true, info: true, warn: true, error: true, debug: true, redux: false };
303
325
  }
304
326
  function setStoredLogLevels(levels) {
305
327
  try { localStorage.setItem('rn-debug-log-levels', JSON.stringify(levels)); } catch {}
@@ -309,6 +331,7 @@ function initConsolePanel() {
309
331
  const panel = $('panel-console');
310
332
  const levels = getStoredLogLevels();
311
333
  state.console.levelFilters = levels;
334
+ state.console.showRedux = !!levels.redux;
312
335
 
313
336
  panel.innerHTML = `
314
337
  <div class="panel-toolbar">
@@ -325,6 +348,8 @@ function initConsolePanel() {
325
348
  <label class="console-level-option"><input type="checkbox" data-level="warn" ${levels.warn ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--yellow)"></span>Warn</label>
326
349
  <label class="console-level-option"><input type="checkbox" data-level="error" ${levels.error ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--red)"></span>Error</label>
327
350
  <label class="console-level-option"><input type="checkbox" data-level="debug" ${levels.debug ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--accent2)"></span>Debug</label>
351
+ <div style="border-top:1px solid var(--border);margin:4px 0"></div>
352
+ <label class="console-level-option"><input type="checkbox" data-level="redux" ${levels.redux ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--green)"></span>Redux Actions</label>
328
353
  </div>
329
354
  </div>
330
355
  </div>
@@ -335,6 +360,13 @@ function initConsolePanel() {
335
360
  <div class="label">No logs yet</div>
336
361
  <div class="hint">Add RNDebugSDK.js to your app</div>
337
362
  </div>
363
+ </div>
364
+ <div class="console-find-bar" id="consoleFindBar" style="display:none">
365
+ <input id="consoleFindInput" class="console-find-input" placeholder="Find in logs... (Cmd+F)" />
366
+ <span id="consoleFindCount" class="console-find-count"></span>
367
+ <button class="console-find-btn" id="consoleFindPrev" title="Previous">▲</button>
368
+ <button class="console-find-btn" id="consoleFindNext" title="Next">▼</button>
369
+ <button class="console-find-btn" id="consoleFindClose" title="Close (Esc)">✕</button>
338
370
  </div>`;
339
371
 
340
372
  // Search filter
@@ -362,6 +394,7 @@ function initConsolePanel() {
362
394
  const level = checkbox.dataset.level;
363
395
  if (level) {
364
396
  state.console.levelFilters[level] = checkbox.checked;
397
+ if (level === 'redux') state.console.showRedux = checkbox.checked;
365
398
  setStoredLogLevels(state.console.levelFilters);
366
399
  updateLevelBtnText();
367
400
  renderConsole();
@@ -376,20 +409,67 @@ function initConsolePanel() {
376
409
  $('cBadge').textContent = '0';
377
410
  renderConsole();
378
411
  });
412
+
413
+ // Find bar (Cmd+F)
414
+ let _findMatches = [];
415
+ let _findIdx = -1;
416
+
417
+ function doFind(term) {
418
+ // Clear previous highlights
419
+ document.querySelectorAll('.console-find-highlight').forEach(el => {
420
+ el.replaceWith(el.textContent);
421
+ });
422
+ _findMatches = [];
423
+ _findIdx = -1;
424
+ if (!term) { $('consoleFindCount').textContent = ''; return; }
425
+
426
+ const rows = document.querySelectorAll('#consoleList .log-row');
427
+ rows.forEach(row => {
428
+ const text = row.textContent.toLowerCase();
429
+ if (text.includes(term.toLowerCase())) _findMatches.push(row);
430
+ });
431
+ $('consoleFindCount').textContent = _findMatches.length ? `${_findMatches.length} found` : 'No matches';
432
+ if (_findMatches.length) { _findIdx = 0; _findMatches[0].scrollIntoView({ block: 'nearest' }); _findMatches[0].style.outline = '1px solid var(--accent)'; }
433
+ }
434
+
435
+ function findNav(dir) {
436
+ if (!_findMatches.length) return;
437
+ if (_findMatches[_findIdx]) _findMatches[_findIdx].style.outline = '';
438
+ _findIdx = (_findIdx + dir + _findMatches.length) % _findMatches.length;
439
+ _findMatches[_findIdx].scrollIntoView({ block: 'nearest' });
440
+ _findMatches[_findIdx].style.outline = '1px solid var(--accent)';
441
+ $('consoleFindCount').textContent = `${_findIdx + 1}/${_findMatches.length}`;
442
+ }
443
+
444
+ $('consoleFindInput').addEventListener('input', (e) => doFind(e.target.value));
445
+ $('consoleFindPrev').addEventListener('click', () => findNav(-1));
446
+ $('consoleFindNext').addEventListener('click', () => findNav(1));
447
+ $('consoleFindClose').addEventListener('click', () => {
448
+ $('consoleFindBar').style.display = 'none';
449
+ if (_findMatches[_findIdx]) _findMatches[_findIdx].style.outline = '';
450
+ _findMatches = []; _findIdx = -1;
451
+ $('consoleFindInput').value = '';
452
+ $('consoleFindCount').textContent = '';
453
+ });
454
+ $('consoleFindInput').addEventListener('keydown', (e) => {
455
+ if (e.key === 'Escape') $('consoleFindClose').click();
456
+ if (e.key === 'Enter') findNav(e.shiftKey ? -1 : 1);
457
+ });
379
458
  }
380
459
 
381
460
  function updateLevelBtnText() {
382
461
  const levels = state.console.levelFilters;
383
- const allOn = Object.values(levels).every(v => v);
384
- const allOff = Object.values(levels).every(v => !v);
462
+ const logLevels = { log: levels.log, info: levels.info, warn: levels.warn, error: levels.error, debug: levels.debug };
463
+ const allOn = Object.values(logLevels).every(v => v);
464
+ const allOff = Object.values(logLevels).every(v => !v);
385
465
  const btn = $('consoleLevelBtn');
386
466
  if (!btn) return;
387
- if (allOn) btn.textContent = 'All Levels ▾';
388
- else if (allOff) btn.textContent = 'None ';
389
- else {
390
- const active = Object.entries(levels).filter(([, v]) => v).map(([k]) => k.charAt(0).toUpperCase() + k.slice(1));
391
- btn.textContent = active.join(', ') + '';
392
- }
467
+ let text = '';
468
+ if (allOn) text = 'All Levels';
469
+ else if (allOff) text = 'None';
470
+ else text = Object.entries(logLevels).filter(([, v]) => v).map(([k]) => k.charAt(0).toUpperCase() + k.slice(1)).join(', ');
471
+ if (levels.redux) text += (text ? ' + ' : '') + 'Redux';
472
+ btn.textContent = text + ' ▾';
393
473
  }
394
474
 
395
475
  // Console is fed via IPC (network-event handled in IPC section above)
@@ -873,8 +953,8 @@ function initNetworkPanel() {
873
953
  <div class="net-filter-bar" id="netFilterBar">
874
954
  <input id="netSearchInput" class="net-search-input" placeholder="Filter URLs..." />
875
955
  <div class="net-type-filters" id="netTypeFilters">
876
- <button class="net-type-btn active" data-type="all">All</button>
877
- <button class="net-type-btn" data-type="fetch">Fetch/XHR</button>
956
+ <button class="net-type-btn" data-type="all">All</button>
957
+ <button class="net-type-btn active" data-type="fetch">Fetch/XHR</button>
878
958
  <button class="net-type-btn" data-type="js">JS</button>
879
959
  <button class="net-type-btn" data-type="css">CSS</button>
880
960
  <button class="net-type-btn" data-type="img">Img</button>
@@ -1726,6 +1806,7 @@ function initReduxPanel() {
1726
1806
  <input id="reduxSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter actions..." />
1727
1807
  <div class="ml-auto" style="display:flex;align-items:center;gap:8px">
1728
1808
  <button class="panel-clear-btn" id="reduxClear" title="Clear redux">Clear</button>
1809
+ <button class="panel-clear-btn" id="reduxSort" title="Toggle sort order">Time ▲</button>
1729
1810
  <div class="time-travel-bar" style="border:none;padding:0;margin:0">
1730
1811
  <button class="tt-btn" onclick="reduxJumpTo(state.redux.selected-1)">◀</button>
1731
1812
  <span class="tt-label" id="ttLabel">—/—</span>
@@ -1753,6 +1834,12 @@ function initReduxPanel() {
1753
1834
  $('rBadge').textContent = '0';
1754
1835
  renderRedux();
1755
1836
  });
1837
+
1838
+ $('reduxSort').addEventListener('click', () => {
1839
+ state.redux.sortDir = state.redux.sortDir === 'desc' ? 'asc' : 'desc';
1840
+ $('reduxSort').textContent = state.redux.sortDir === 'desc' ? 'Time \u25BC' : 'Time \u25B2';
1841
+ renderRedux();
1842
+ });
1756
1843
  }
1757
1844
 
1758
1845
  window.reduxJumpTo = idx => {
@@ -1786,11 +1873,24 @@ function handleReduxEvent(event) {
1786
1873
  allKeys.forEach(k => { if (!_deepEqual(prevState[k], nextState[k])) changedKeys.push(k); });
1787
1874
  }
1788
1875
 
1789
- state.redux.actions.push({ type: action?.type || '?', payload: action, ts: event.ts, index: idx, changedKeys });
1876
+ const actionEntry = { type: action?.type || '?', payload: action, ts: event.ts, index: idx, changedKeys };
1877
+ state.redux.actions.push(actionEntry);
1790
1878
  state.redux.states.push(nextState);
1791
1879
  state.redux.selected = idx;
1792
1880
  $('rBadge').textContent = state.redux.actions.length;
1793
1881
  renderRedux();
1882
+
1883
+ // Also add to console logs if Redux is enabled in console dropdown
1884
+ if (state.console.showRedux) {
1885
+ const msg = `[Redux] ${actionEntry.type}` + (changedKeys.length ? ` (changed: ${changedKeys.join(', ')})` : '');
1886
+ addConsoleLog({
1887
+ level: 'redux',
1888
+ message: msg,
1889
+ args: [{ t: 'string', v: `[Redux] ${actionEntry.type}` }, { t: 'object', v: action }],
1890
+ ts: event.ts,
1891
+ _isRedux: true,
1892
+ });
1893
+ }
1794
1894
  }
1795
1895
 
1796
1896
  function renderRedux() {
@@ -1798,8 +1898,9 @@ function renderRedux() {
1798
1898
  const empty = $('reduxEmpty');
1799
1899
  if (!content) return;
1800
1900
 
1801
- const { actions, states, selected, searchFilter } = state.redux;
1802
- const visible = searchFilter ? actions.filter(a => a.type.toLowerCase().includes(searchFilter)) : actions;
1901
+ const { actions, states, selected, searchFilter, sortDir } = state.redux;
1902
+ let visible = searchFilter ? actions.filter(a => a.type.toLowerCase().includes(searchFilter)) : [...actions];
1903
+ if (sortDir === 'desc') visible = [...visible].reverse();
1803
1904
 
1804
1905
  empty.style.display = visible.length ? 'none' : 'flex';
1805
1906
  content.querySelectorAll('.rdx-entry').forEach(e => e.remove());
@@ -1893,8 +1994,13 @@ function renderRedux() {
1893
1994
  });
1894
1995
 
1895
1996
  content.appendChild(frag);
1896
- const selEl = content.querySelector('.rdx-entry.selected');
1897
- if (selEl) selEl.scrollIntoView({ block: 'nearest' });
1997
+ // Auto-scroll: if asc (latest at bottom), scroll to bottom; otherwise scroll selected into view
1998
+ if (state.redux.sortDir === 'asc') {
1999
+ content.scrollTop = content.scrollHeight;
2000
+ } else {
2001
+ const selEl = content.querySelector('.rdx-entry.selected');
2002
+ if (selEl) selEl.scrollIntoView({ block: 'nearest' });
2003
+ }
1898
2004
  }
1899
2005
 
1900
2006
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ReactoRadar Icon Generator
4
+ Generates all macOS iconset sizes from icon.svg, then compiles icon.icns.
5
+
6
+ Requirements:
7
+ pip install cairosvg pillow
8
+
9
+ Usage:
10
+ python3 generate_icons.py
11
+ """
12
+
13
+ import os
14
+ import shutil
15
+ import struct
16
+ import subprocess
17
+ import sys
18
+ import zlib
19
+ from pathlib import Path
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Config
23
+ # ---------------------------------------------------------------------------
24
+ SCRIPT_DIR = Path(__file__).parent.resolve()
25
+ SVG_SRC = SCRIPT_DIR / "icon.svg"
26
+ ICONSET = SCRIPT_DIR / "icon.iconset"
27
+ ICNS_OUT = SCRIPT_DIR / "icon.icns"
28
+ PNG_OUT = SCRIPT_DIR / "icon.png" # 1024×1024, for Electron dev mode
29
+
30
+ SIZES = [
31
+ # (filename, pixels)
32
+ ("icon_16x16.png", 16),
33
+ ("icon_16x16@2x.png", 32),
34
+ ("icon_32x32.png", 32),
35
+ ("icon_32x32@2x.png", 64),
36
+ ("icon_128x128.png", 128),
37
+ ("icon_128x128@2x.png", 256),
38
+ ("icon_256x256.png", 256),
39
+ ("icon_256x256@2x.png", 512),
40
+ ("icon_512x512.png", 512),
41
+ ("icon_512x512@2x.png", 1024),
42
+ ]
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Helpers
46
+ # ---------------------------------------------------------------------------
47
+
48
+ def render_svg_to_png(svg_path: Path, out_path: Path, size: int) -> None:
49
+ """Render SVG → PNG at the given square pixel size."""
50
+ try:
51
+ import cairosvg
52
+ cairosvg.svg2png(
53
+ url=str(svg_path),
54
+ write_to=str(out_path),
55
+ output_width=size,
56
+ output_height=size,
57
+ )
58
+ except ImportError:
59
+ # Fallback: Inkscape CLI
60
+ result = subprocess.run(
61
+ ["inkscape", "--export-type=png",
62
+ f"--export-filename={out_path}",
63
+ f"--export-width={size}",
64
+ f"--export-height={size}",
65
+ str(svg_path)],
66
+ capture_output=True, text=True
67
+ )
68
+ if result.returncode != 0:
69
+ print(f" inkscape error: {result.stderr}", file=sys.stderr)
70
+ raise RuntimeError("SVG rendering failed")
71
+
72
+
73
+ def build_icns_with_iconutil(iconset_dir: Path, icns_path: Path) -> bool:
74
+ """Use macOS iconutil if available."""
75
+ if shutil.which("iconutil"):
76
+ result = subprocess.run(
77
+ ["iconutil", "-c", "icns", str(iconset_dir), "-o", str(icns_path)],
78
+ capture_output=True, text=True
79
+ )
80
+ if result.returncode == 0:
81
+ return True
82
+ print(f" iconutil error: {result.stderr}", file=sys.stderr)
83
+ return False
84
+
85
+
86
+ def _png_bytes(path: Path) -> bytes:
87
+ return path.read_bytes()
88
+
89
+
90
+ def _icns_chunk(tag: bytes, data: bytes) -> bytes:
91
+ """Pack one ICNS chunk: 4-byte OSType + 4-byte length (includes header) + data."""
92
+ length = 8 + len(data)
93
+ return tag + struct.pack(">I", length) + data
94
+
95
+
96
+ # ICNS OSType tags for each size
97
+ _ICNS_TAGS = {
98
+ 16: b"icp4",
99
+ 32: b"icp5",
100
+ 64: b"icp6",
101
+ 128: b"ic07",
102
+ 256: b"ic08",
103
+ 512: b"ic09",
104
+ 1024: b"ic10",
105
+ }
106
+
107
+
108
+ def build_icns_pure_python(iconset_dir: Path, icns_path: Path) -> None:
109
+ """Pure-Python ICNS writer (fallback when iconutil unavailable)."""
110
+ chunks = b""
111
+ # Deduplicate: use only the @2x names (they have the higher-res pixels)
112
+ # Actually we want one entry per unique pixel size, highest quality first.
113
+ seen = {}
114
+ for fname, size in SIZES:
115
+ if size not in seen:
116
+ seen[size] = iconset_dir / fname
117
+
118
+ for size in sorted(seen):
119
+ tag = _ICNS_TAGS.get(size)
120
+ if tag is None:
121
+ continue
122
+ png_path = seen[size]
123
+ if not png_path.exists():
124
+ print(f" warning: {png_path.name} missing, skipping")
125
+ continue
126
+ chunks += _icns_chunk(tag, _png_bytes(png_path))
127
+
128
+ total = 8 + len(chunks)
129
+ icns_path.write_bytes(b"icns" + struct.pack(">I", total) + chunks)
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Main
134
+ # ---------------------------------------------------------------------------
135
+
136
+ def main():
137
+ print("=== ReactoRadar Icon Generator ===\n")
138
+
139
+ if not SVG_SRC.exists():
140
+ print(f"ERROR: {SVG_SRC} not found.", file=sys.stderr)
141
+ sys.exit(1)
142
+
143
+ # 1. Create iconset directory
144
+ ICONSET.mkdir(exist_ok=True)
145
+ print(f"Output directory: {ICONSET}\n")
146
+
147
+ # 2. Render each size
148
+ for fname, size in SIZES:
149
+ out = ICONSET / fname
150
+ print(f" Rendering {fname:30s} ({size:4d}px) ...", end=" ", flush=True)
151
+ render_svg_to_png(SVG_SRC, out, size)
152
+ kb = out.stat().st_size // 1024
153
+ print(f"done ({kb} KB)")
154
+
155
+ # 3. Copy 1024×1024 as icon.png (Electron dev-mode asset)
156
+ shutil.copy2(ICONSET / "icon_512x512@2x.png", PNG_OUT)
157
+ print(f"\nCopied icon.png ({PNG_OUT.stat().st_size // 1024} KB)")
158
+
159
+ # 4. Build ICNS
160
+ print(f"\nBuilding {ICNS_OUT.name} ...")
161
+ if not build_icns_with_iconutil(ICONSET, ICNS_OUT):
162
+ print(" iconutil not available – using pure-Python writer")
163
+ build_icns_pure_python(ICONSET, ICNS_OUT)
164
+ print(f" Written: {ICNS_OUT} ({ICNS_OUT.stat().st_size // 1024} KB)")
165
+
166
+ print("\n✅ All done!\n")
167
+ print("Files generated:")
168
+ print(f" {ICONSET}/ ← all PNG sizes")
169
+ print(f" {ICNS_OUT} ← for Electron builder (assets/icon.icns)")
170
+ print(f" {PNG_OUT} ← for Electron dev mode (assets/icon.png)")
171
+ print(f" {SVG_SRC} ← master source")
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
package/main.js CHANGED
@@ -38,10 +38,18 @@ app.whenReady().then(async () => {
38
38
 
39
39
  await createMainWindow();
40
40
 
41
- // Send version to renderer
42
- const appVersion = require('./package.json').version;
41
+ // Send version to renderer — try package.json, fallback to app.getVersion()
42
+ let appVersion;
43
+ try { appVersion = require('./package.json').version; } catch { appVersion = app.getVersion(); }
44
+ // Send multiple times to ensure renderer catches it
43
45
  mainWindow?.webContents.on('did-finish-load', () => {
44
- mainWindow?.webContents.send('app-version', appVersion);
46
+ [200, 1000, 3000].forEach(delay => {
47
+ setTimeout(() => {
48
+ if (mainWindow && !mainWindow.isDestroyed()) {
49
+ mainWindow.webContents.send('app-version', appVersion);
50
+ }
51
+ }, delay);
52
+ });
45
53
  });
46
54
 
47
55
  // Check for updates (non-blocking)
@@ -505,6 +513,12 @@ function buildMenu() {
505
513
  { role: 'copy' },
506
514
  { role: 'paste' },
507
515
  { role: 'selectAll' },
516
+ { type: 'separator' },
517
+ {
518
+ label: 'Find',
519
+ accelerator: 'Cmd+F',
520
+ click: () => { mainWindow?.webContents.send('focus-search'); },
521
+ },
508
522
  ],
509
523
  },
510
524
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.4.1",
4
+ "version": "1.5.2",
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
@@ -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',
13
+ 'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available', 'app-version', 'focus-search',
14
14
  ];
15
15
  if (allowed.includes(channel)) {
16
16
  ipcRenderer.removeAllListeners(channel);
package/styles.css CHANGED
@@ -369,6 +369,41 @@ body {
369
369
  .tab:hover:not(.active) { color: var(--text); }
370
370
  .ml-auto { margin-left: auto; }
371
371
 
372
+ /* Console find bar (Cmd+F) */
373
+ .console-find-bar {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 6px;
377
+ padding: 4px 10px;
378
+ background: var(--bg2);
379
+ border-top: 1px solid var(--border);
380
+ flex-shrink: 0;
381
+ }
382
+ .console-find-input {
383
+ flex: 1;
384
+ padding: 3px 8px;
385
+ border: 1px solid var(--border2);
386
+ border-radius: 4px;
387
+ background: var(--bg3);
388
+ color: var(--text);
389
+ font-family: inherit;
390
+ font-size: 11px;
391
+ outline: none;
392
+ }
393
+ .console-find-input:focus { border-color: var(--accent); }
394
+ .console-find-count { font-size: 10px; color: var(--text-dim); flex-shrink: 0; min-width: 40px; }
395
+ .console-find-btn {
396
+ border: none;
397
+ background: transparent;
398
+ color: var(--text-dim);
399
+ font-size: 11px;
400
+ cursor: pointer;
401
+ padding: 2px 6px;
402
+ border-radius: 3px;
403
+ }
404
+ .console-find-btn:hover { background: var(--bg3); color: var(--text); }
405
+ .console-find-highlight { background: rgba(245,200,66,.3); border-radius: 2px; }
406
+
372
407
  /* Panel clear button (used in Console, Network, GA4, Redux) */
373
408
  .panel-clear-btn {
374
409
  padding: 3px 10px;
@@ -490,6 +525,7 @@ body {
490
525
  .lvl-warn { background: rgba(245,200,66,.12); color: var(--yellow); }
491
526
  .lvl-error { background: rgba(255,94,114,.15); color: var(--red); }
492
527
  .lvl-debug { background: rgba(155,127,255,.12); color: var(--accent2); }
528
+ .lvl-redux { background: rgba(61,214,140,.12); color: var(--green); }
493
529
 
494
530
  .log-body-wrap {
495
531
  display: flex;