reactoradar 1.6.7 → 1.6.10

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
@@ -181,6 +181,7 @@ function findStoreFile(projectDir) {
181
181
  ];
182
182
  const storeNames = ['store.ts', 'store.js', 'store.tsx', 'store.jsx', 'index.ts', 'index.js', 'index.tsx'];
183
183
 
184
+ // 1. Check common store file locations
184
185
  for (const dir of searchDirs) {
185
186
  for (const name of storeNames) {
186
187
  const p = path.join(projectDir, dir, name);
@@ -193,18 +194,32 @@ function findStoreFile(projectDir) {
193
194
  }
194
195
  }
195
196
 
196
- // Deep search: recursively find any file with configureStore/createStore
197
+ // 2. Check App.tsx / App.js (common pattern: store created in root component)
198
+ const rootAppFiles = ['src/App.tsx', 'src/App.js', 'App.tsx', 'App.js', 'app/App.tsx', 'app/App.js'];
199
+ for (const f of rootAppFiles) {
200
+ const p = path.join(projectDir, f);
201
+ if (fileExists(p)) {
202
+ const content = fs.readFileSync(p, 'utf8');
203
+ if (content.includes('configureStore') || content.includes('createStore')) {
204
+ return f;
205
+ }
206
+ }
207
+ }
208
+
209
+ // 3. Deep search: recursively find files with actual createStore()/configureStore() calls
210
+ // Skip files that merely reference configureStore as a parameter/type name
197
211
  try {
198
212
  const glob = (dir, depth) => {
199
213
  if (depth > 4) return null;
200
214
  let entries;
201
215
  try { entries = fs.readdirSync(path.join(projectDir, dir), { withFileTypes: true }); } catch { return null; }
202
216
  for (const e of entries) {
203
- if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
217
+ if (e.name === 'node_modules' || e.name.startsWith('.') || e.name === 'debug') continue;
204
218
  const rel = path.join(dir, e.name);
205
- if (e.isFile() && /\.(ts|js|tsx|jsx)$/.test(e.name) && /store/i.test(e.name)) {
219
+ if (e.isFile() && /\.(ts|js|tsx|jsx)$/.test(e.name)) {
206
220
  const content = fs.readFileSync(path.join(projectDir, rel), 'utf8');
207
- if (content.includes('configureStore') || content.includes('createStore')) {
221
+ // Must contain actual call: configureStore({ or createStore( — not just the word as a type/param
222
+ if (/configureStore\s*\(/.test(content) || /createStore\s*\(/.test(content)) {
208
223
  return rel;
209
224
  }
210
225
  }
@@ -265,12 +280,20 @@ async function install(projectDir) {
265
280
  process.exit(1);
266
281
  }
267
282
 
268
- // Read SDK, patch HOST
283
+ // Read SDK, patch HOST_OVERRIDE for real device support
284
+ // The SDK auto-detects Android emulator vs iOS simulator at runtime.
285
+ // For real devices, we set HOST_OVERRIDE to the Mac's LAN IP.
269
286
  let sdkContent = fs.readFileSync(sdkSrc, 'utf8');
270
- sdkContent = sdkContent.replace(
271
- /const HOST = '[^']+';/,
272
- `const HOST = '${host}';`
273
- );
287
+ const isRealDevice = reason.includes('device') || reason.includes('LAN IP');
288
+ if (isRealDevice && host !== '127.0.0.1' && host !== '10.0.2.2') {
289
+ sdkContent = sdkContent.replace(
290
+ /const HOST_OVERRIDE = null;/,
291
+ `const HOST_OVERRIDE = '${host}';`
292
+ );
293
+ log('HOST_OVERRIDE set to', C.bold + host + C.reset, '(real device LAN IP)');
294
+ } else {
295
+ log('HOST auto-detect enabled', C.dim + '(Android: 10.0.2.2, iOS: 127.0.0.1)' + C.reset);
296
+ }
274
297
  try {
275
298
  fs.writeFileSync(sdkDest, sdkContent);
276
299
  } catch (e) {
@@ -383,12 +406,51 @@ ${SDK_MARKER_END}
383
406
  warn('Could not auto-patch', storeFile, '— wire Redux manually');
384
407
  }
385
408
  }
386
- } else if (storeContent.includes('createStore')) {
387
- warn('Legacy createStore found at', C.bold + storeFile + C.reset);
388
- console.log(C.dim + ' Add manually:' + C.reset);
389
- console.log(C.dim + ` import { reduxEnhancer } from '${relSDK}';` + C.reset);
390
- console.log(C.dim + ' const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);' + C.reset);
391
- }
409
+ } else if (storeContent.includes('createStore')) {
410
+ // Legacy createStore try to auto-patch by adding reduxMiddleware to middleware array
411
+ let patched = storeContent;
412
+ let didPatch = false;
413
+
414
+ // Pattern 1: middleware array exists — push reduxMiddleware into it
415
+ // e.g. const middleware = []; or const middleware = [sagaMiddleware];
416
+ if (/(?:const|let|var)\s+middleware\s*=\s*\[/.test(storeContent) && !storeContent.includes('reduxMiddleware') && !storeContent.includes('RNDebugSDK')) {
417
+ // Add the require + push after the middleware array declaration
418
+ patched = patched.replace(
419
+ /((?:const|let|var)\s+middleware\s*=\s*\[[^\]]*\];?)/,
420
+ `$1\n if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); if (reduxMiddleware) middleware.push(reduxMiddleware); } catch {} }`
421
+ );
422
+ if (patched !== storeContent) {
423
+ didPatch = true;
424
+ }
425
+ }
426
+
427
+ // Pattern 2: applyMiddleware(...middleware) exists but no middleware array — inject inline
428
+ if (!didPatch && storeContent.includes('applyMiddleware') && !storeContent.includes('reduxMiddleware') && !storeContent.includes('RNDebugSDK')) {
429
+ // Find the if (__DEV__) block near middleware setup and add there
430
+ // Or add before the createStore call
431
+ const createStoreMatch = storeContent.match(/createStore\s*\(/);
432
+ if (createStoreMatch) {
433
+ const idx = storeContent.indexOf(createStoreMatch[0]);
434
+ const insertPoint = storeContent.lastIndexOf('\n', idx);
435
+ if (insertPoint > 0) {
436
+ patched = storeContent.slice(0, insertPoint) +
437
+ `\n if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); if (reduxMiddleware) middleware.push(reduxMiddleware); } catch {} }` +
438
+ storeContent.slice(insertPoint);
439
+ if (patched !== storeContent) didPatch = true;
440
+ }
441
+ }
442
+ }
443
+
444
+ if (didPatch) {
445
+ fs.writeFileSync(storePath, patched);
446
+ log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired (legacy createStore)');
447
+ } else {
448
+ warn('Legacy createStore found at', C.bold + storeFile + C.reset);
449
+ console.log(C.dim + ' Could not auto-patch. Add manually:' + C.reset);
450
+ console.log(C.dim + ` if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); middleware.push(reduxMiddleware); } catch {} }` + C.reset);
451
+ console.log(C.dim + ' Add this BEFORE the createStore() call in your middleware setup.' + C.reset);
452
+ }
453
+ }
392
454
  } else {
393
455
  warn('Redux detected but store file not found automatically');
394
456
  console.log(C.dim + ' Add to your store setup:' + C.reset);
package/init.js CHANGED
@@ -5,7 +5,8 @@
5
5
 
6
6
  // ─── CDP Button ───────────────────────────────────────────────────────────────
7
7
  $('btnCDP')?.addEventListener('click', () => {
8
- window.electronAPI?.openCDPTarget(null);
8
+ // Tell main process to open the CDP DevTools window with the best available target
9
+ window.electronAPI?.openCDPTarget(null); // null = use latest known target
9
10
  });
10
11
 
11
12
  // ─── Screenshot Button ────────────────────────────────────────────────────────
@@ -37,7 +38,6 @@ if (window.electronAPI) {
37
38
  window.electronAPI.on('network-event', handleNetworkEvent);
38
39
  window.electronAPI.on('storage-event', handleStorageEvent);
39
40
 
40
- window.electronAPI.on('console-event', addConsoleLog);
41
41
  window.electronAPI.on('ga4-event', handleGA4Event);
42
42
 
43
43
  window.electronAPI.on('perf-event', event => {
@@ -66,6 +66,7 @@ if (window.electronAPI) {
66
66
 
67
67
  // Cmd+F — focus the search input for the active panel
68
68
  function _handleFind() {
69
+ // If network detail is open, focus the detail search
69
70
  if (state.activePanel === 'network' && state.network.selectedId) {
70
71
  const wrap = $('detailSearchWrap');
71
72
  const input = $('detailSearchInput');
@@ -88,12 +89,14 @@ if (window.electronAPI) {
88
89
  const el = $(inputId);
89
90
  if (el) { el.focus(); el.select(); }
90
91
  }
92
+ // Also show/focus Console bottom find bar
91
93
  if (state.activePanel === 'console') {
92
94
  const bar = $('consoleFindBar');
93
95
  if (bar) { bar.style.display = 'flex'; $('consoleFindInput')?.focus(); }
94
96
  }
95
97
  }
96
98
  window.electronAPI.on('focus-search', _handleFind);
99
+ // Direct keyboard fallback — Electron menu accelerators can miss in some contexts
97
100
  document.addEventListener('keydown', (e) => {
98
101
  if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
99
102
  e.preventDefault();
@@ -104,6 +107,7 @@ if (window.electronAPI) {
104
107
  window.electronAPI.on('app-version', (version, isPackaged) => {
105
108
  state._appVersion = version;
106
109
  state._isPackaged = !!isPackaged;
110
+ // Update anywhere the version is displayed
107
111
  document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
108
112
  });
109
113
 
@@ -121,21 +125,27 @@ if (window.electronAPI) {
121
125
  window.electronAPI?.openCDPTarget(null);
122
126
  });
123
127
 
128
+ // Theme toggle from menu shortcut (Cmd+Shift+T)
124
129
  window.electronAPI.on('theme-changed', theme => {
125
130
  document.documentElement.setAttribute('data-theme', theme);
126
131
  setStoredTheme(theme);
127
132
  document.querySelectorAll('#themeSwitcher .theme-card')
128
133
  .forEach(b => b.classList.toggle('active', b.dataset.theme === theme));
129
134
  });
135
+
136
+ // Console event IPC
137
+ window.electronAPI.on('console-event', addConsoleLog);
130
138
  }
131
139
 
132
140
  // ─── Memory Monitor ──────────────────────────────────────────────────────────
141
+ // Check memory usage periodically and warn user before it causes blank screen
133
142
  let _memoryWarningShown = false;
134
143
  setInterval(() => {
135
144
  if (!window.performance || !performance.memory) return;
136
145
  const used = performance.memory.usedJSHeapSize;
137
146
  const limit = performance.memory.jsHeapSizeLimit;
138
147
  const pct = used / limit;
148
+ // Warn at 70% usage
139
149
  if (pct > 0.7 && !_memoryWarningShown) {
140
150
  _memoryWarningShown = true;
141
151
  const banner = document.createElement('div');
@@ -147,6 +157,7 @@ setInterval(() => {
147
157
  + `<button class="memory-warn-btn" id="memWarnDismiss">Dismiss</button>`;
148
158
  document.body.prepend(banner);
149
159
  $('memWarnClear')?.addEventListener('click', () => {
160
+ // Clear all panel data
150
161
  state.console.logs = []; _consolePending = [];
151
162
  _lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
152
163
  $('cBadge').textContent = '0'; renderConsole();
@@ -158,17 +169,19 @@ setInterval(() => {
158
169
  });
159
170
  $('memWarnDismiss')?.addEventListener('click', () => { banner.remove(); });
160
171
  }
172
+ // Reset flag when memory drops
161
173
  if (pct < 0.5) _memoryWarningShown = false;
162
- }, 30000);
174
+ }, 30000); // Check every 30 seconds
175
+
176
+ // Apply saved theme + font size + font family + app name on load
163
177
 
164
- // ─────────────────────────────────────────────────────────────────────────────
165
- // Apply saved settings on load
166
- // ─────────────────────────────────────────────────────────────────────────────
167
178
  applyTheme(getStoredTheme());
168
179
  applyFontSize(getStoredFontSize());
169
180
  applyFontFamily(getStoredFontFamily());
170
181
  applyAppName(getStoredAppName());
171
182
  applyTabVisibility();
183
+
184
+ // Send stored metro port to backend
172
185
  window.electronAPI?.setMetroPort(getStoredMetroPort());
173
186
 
174
187
  // ─────────────────────────────────────────────────────────────────────────────
@@ -182,6 +195,5 @@ initMemoryPanel();
182
195
  initReduxPanel();
183
196
  initStoragePanel();
184
197
  initReactPanel();
185
- initSourcesPanel();
186
198
  initNativeLogsPanel();
187
199
  initSettingsPanel();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.6.7",
4
+ "version": "1.6.10",
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/panels/console.js CHANGED
@@ -1,5 +1,4 @@
1
- // ─── Console Panel ─────────────────────────────────────────────────────────
2
-
1
+ // ─── Console Panel + Shared Renderers ─────────────────────────────────────
3
2
  // Load saved log level filters from localStorage
4
3
  function getStoredLogLevels() {
5
4
  try {
@@ -14,6 +13,7 @@ function setStoredLogLevels(levels) {
14
13
 
15
14
  function initConsolePanel() {
16
15
  const panel = $('panel-console');
16
+ if (!panel) return;
17
17
  const levels = getStoredLogLevels();
18
18
  state.console.levelFilters = levels;
19
19
  state.console.showRedux = !!levels.redux;
@@ -321,10 +321,10 @@ function flushConsoleBatch() {
321
321
  }
322
322
  }
323
323
 
324
- // NOTE: console-event IPC listener is registered in init.js (not here)
325
- // to keep all IPC registrations in one place and avoid duplicate listener issues.
326
324
 
327
- // ─── Shared Object Tree Renderer (Chrome DevTools-like) ──────────────────────
325
+ // NOTE: console-event IPC listener is registered in init.js
326
+
327
+ // ─── Object Tree Renderer (Chrome DevTools-like) ─────────────────────────────
328
328
  // Builds interactive, collapsible DOM nodes for objects/arrays.
329
329
 
330
330
  // Collect all entries for an object: own data properties + prototype getter values.
@@ -709,7 +709,7 @@ function buildLogRow(l) {
709
709
  return div;
710
710
  }
711
711
 
712
- // ─── Shared Context Menu ─────────────────────────────────────────────────────
712
+ // ─── Shared context menu helper ──────────────────────────────────────────────
713
713
  function showContextMenu(e, items) {
714
714
  document.querySelectorAll('.ctx-menu').forEach(el => el.remove());
715
715
  const menu = document.createElement('div');
@@ -786,3 +786,4 @@ function renderConsole() {
786
786
  list.appendChild(frag);
787
787
  list.scrollTop = list.scrollHeight;
788
788
  }
789
+
package/panels/ga4.js CHANGED
@@ -1,10 +1,12 @@
1
+ // ─── GA4 Events Panel ──────────────────────────────────────────────────────
1
2
  // ─────────────────────────────────────────────────────────────────────────────
2
- // GA4 EVENT INSPECTOR — extracted from app.js
3
+ // GA4 EVENT INSPECTOR
3
4
  // ─────────────────────────────────────────────────────────────────────────────
4
5
  const ga4State = { events: [], selected: -1, searchFilter: '', sortDir: 'desc' };
5
6
 
6
7
  function initGA4Panel() {
7
8
  const panel = $('panel-ga4');
9
+ if (!panel) return;
8
10
  panel.innerHTML = `
9
11
  <div class="panel-toolbar">
10
12
  <span class="panel-label">GA4 Events</span>
@@ -326,3 +328,4 @@ function renderGA4Summary() {
326
328
  summary.appendChild(chip);
327
329
  });
328
330
  }
331
+
package/panels/native.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // ─── Native Logs Panel ─────────────────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // NATIVE LOGS PANEL
4
+ // ─────────────────────────────────────────────────────────────────────────────
2
5
  const _nativeState = { logs: [], connected: false, platform: null, levelFilter: 'all', searchFilter: '' };
3
6
  const MAX_NATIVE_LOGS = 2000;
4
7
 
@@ -254,3 +257,4 @@ function _renderNativeLogs() {
254
257
  list.innerHTML = '';
255
258
  _nativeState.logs.forEach(log => _appendNativeLog(log));
256
259
  }
260
+
package/panels/network.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // ─── Network Panel ─────────────────────────────────────────────────────────
2
-
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // NETWORK PANEL (Chrome DevTools-style)
4
+ // ─────────────────────────────────────────────────────────────────────────────
3
5
  const NET_COLS = [
4
6
  { key: 'name', label: 'Name', width: 380, min: 150 },
5
7
  { key: 'status', label: 'Status', width: 60, min: 40 },
@@ -12,6 +14,7 @@ const NET_COLS = [
12
14
 
13
15
  function initNetworkPanel() {
14
16
  const panel = $('panel-network');
17
+ if (!panel) return;
15
18
  panel.innerHTML = `
16
19
  <div class="panel-toolbar">
17
20
  <span class="panel-label">Network</span>
@@ -966,3 +969,4 @@ function buildCurlCommand(r) {
966
969
  }
967
970
  return cmd;
968
971
  }
972
+
@@ -1,8 +1,12 @@
1
- // ─── Performance + Memory Panel ────────────────────────────────────────────
1
+ // ─── Performance + Memory Panels ───────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // PERFORMANCE PANEL — FPS, render timing, JS thread
4
+ // ─────────────────────────────────────────────────────────────────────────────
2
5
  const perfState = { fps: [], jsThread: [], uiThread: [], recording: false, data: [] };
3
6
 
4
7
  function initPerformancePanel() {
5
8
  const panel = $('panel-performance');
9
+ if (!panel) return;
6
10
  panel.innerHTML = `
7
11
  <div class="panel-toolbar">
8
12
  <span class="panel-label">Performance</span>
@@ -130,9 +134,12 @@ function handlePerfEvent(event) {
130
134
  }
131
135
  }
132
136
 
133
- // ─── Memory Panel ────────────────────────────────────────────────────────────
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+ // MEMORY PANEL — Heap snapshot summary via Hermes CDP
139
+ // ─────────────────────────────────────────────────────────────────────────────
134
140
  function initMemoryPanel() {
135
141
  const panel = $('panel-memory');
142
+ if (!panel) return;
136
143
  panel.innerHTML = `
137
144
  <div class="panel-toolbar">
138
145
  <span class="panel-label">Memory</span>
package/panels/react.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // ─── React Tree Panel ──────────────────────────────────────────────────────
2
2
  function initReactPanel() {
3
3
  const panel = $('panel-react');
4
+ if (!panel) return;
4
5
  panel.innerHTML = `
5
6
  <div class="panel-toolbar">
6
7
  <span class="panel-label">React Tree</span>
@@ -19,3 +20,4 @@ function initReactPanel() {
19
20
  window.electronAPI?.openReactDevTools();
20
21
  });
21
22
  }
23
+
package/panels/redux.js CHANGED
@@ -1,8 +1,10 @@
1
+ // ─── Redux Panel ───────────────────────────────────────────────────────────
1
2
  // ─────────────────────────────────────────────────────────────────────────────
2
- // REDUX PANEL — extracted from app.js
3
+ // REDUX PANEL
3
4
  // ─────────────────────────────────────────────────────────────────────────────
4
5
  function initReduxPanel() {
5
6
  const panel = $('panel-redux');
7
+ if (!panel) return;
6
8
  panel.innerHTML = `
7
9
  <div class="panel-toolbar">
8
10
  <span class="panel-label">Redux</span>
@@ -436,3 +438,4 @@ function renderRedux() {
436
438
  selEl.scrollIntoView({ block: 'nearest', behavior: 'auto' });
437
439
  }
438
440
  }
441
+
@@ -1,6 +1,4 @@
1
- // ─── Settings Panel ────────────────────────────────────────────────────────
2
-
3
- // ─── Theme helpers ───────────────────────────────────────────────────────────
1
+ // ─── Settings Panel + Shared Utilities ─────────────────────────────────────
4
2
  function getStoredTheme() {
5
3
  try { return localStorage.getItem('rn-debug-theme') || 'dark'; } catch { return 'dark'; }
6
4
  }
@@ -303,6 +301,7 @@ function applyFontSize(size) {
303
301
 
304
302
  function initSettingsPanel() {
305
303
  const panel = $('panel-settings');
304
+ if (!panel) return;
306
305
  const current = getStoredTheme();
307
306
  const currentSize = getStoredFontSize();
308
307
  panel.innerHTML = `
@@ -594,13 +593,12 @@ function _loadVersionHistory() {
594
593
  } catch { /* skip bad date */ }
595
594
  }
596
595
 
597
- // Build action buttons based on install type
596
+ // Build action button based on install type
598
597
  let actionHtml = '';
599
598
  if (isCurrent) {
600
599
  actionHtml = '<span class="version-installed">Installed</span>';
601
600
  } else if (isPackaged) {
602
- // Show download dropdown with .dmg and .zip links
603
- actionHtml = '<div class="version-dl-wrap"><button class="version-install-btn" title="Download this version">Download ▾</button></div>';
601
+ actionHtml = '<button class="version-install-btn" title="Download .dmg for this version">Download</button>';
604
602
  } else {
605
603
  actionHtml = `<button class="version-npm-btn" title="Copy npm install command">npx @${esc(r.version)}</button>`;
606
604
  }
@@ -615,57 +613,14 @@ function _loadVersionHistory() {
615
613
  <button class="version-notes-btn" title="View release notes">Notes</button>
616
614
  </div>`;
617
615
 
618
- // DMG/ZIP download dropdown
619
- const dlWrap = row.querySelector('.version-dl-wrap');
616
+ // DMG download button — opens the .dmg asset or release page
620
617
  const installBtn = row.querySelector('.version-install-btn');
621
- if (installBtn && dlWrap) {
622
- installBtn.addEventListener('click', (e) => {
623
- e.stopPropagation();
624
- // Remove any existing dropdown
625
- document.querySelectorAll('.version-dl-menu').forEach(m => m.remove());
626
-
627
- const menu = document.createElement('div');
628
- menu.className = 'version-dl-menu';
629
-
630
- // .dmg link
631
- if (r.dmgUrl) {
632
- const dmgItem = document.createElement('div');
633
- dmgItem.className = 'version-dl-item';
634
- dmgItem.innerHTML = `<span class="version-dl-icon">💿</span><div><div class="version-dl-name">.dmg Installer</div><div class="version-dl-hint">macOS installer (Apple Silicon)</div></div>`;
635
- dmgItem.addEventListener('click', () => { window.electronAPI.openExternal(r.dmgUrl); menu.remove(); });
636
- menu.appendChild(dmgItem);
637
- }
638
-
639
- // .zip link
640
- if (r.zipUrl) {
641
- const zipItem = document.createElement('div');
642
- zipItem.className = 'version-dl-item';
643
- zipItem.innerHTML = `<span class="version-dl-icon">📦</span><div><div class="version-dl-name">.zip Archive</div><div class="version-dl-hint">Portable zip archive</div></div>`;
644
- zipItem.addEventListener('click', () => { window.electronAPI.openExternal(r.zipUrl); menu.remove(); });
645
- menu.appendChild(zipItem);
646
- }
647
-
648
- // GitHub release page fallback
649
- if (r.htmlUrl) {
650
- const ghItem = document.createElement('div');
651
- ghItem.className = 'version-dl-item';
652
- ghItem.innerHTML = `<span class="version-dl-icon">🔗</span><div><div class="version-dl-name">GitHub Release</div><div class="version-dl-hint">View all assets on GitHub</div></div>`;
653
- ghItem.addEventListener('click', () => { window.electronAPI.openExternal(r.htmlUrl); menu.remove(); });
654
- menu.appendChild(ghItem);
655
- }
656
-
657
- // No assets fallback
658
- if (!r.dmgUrl && !r.zipUrl && !r.htmlUrl) {
659
- menu.innerHTML = '<div style="padding:8px 12px;color:var(--text-dim);font-size:10px">No downloads available</div>';
618
+ if (installBtn) {
619
+ installBtn.addEventListener('click', () => {
620
+ const url = r.dmgUrl || r.htmlUrl || '';
621
+ if (url) {
622
+ window.electronAPI.openExternal(url);
660
623
  }
661
-
662
- dlWrap.appendChild(menu);
663
-
664
- // Close on outside click
665
- setTimeout(() => {
666
- const close = (ev) => { if (!menu.contains(ev.target) && ev.target !== installBtn) { menu.remove(); document.removeEventListener('click', close); } };
667
- document.addEventListener('click', close);
668
- }, 0);
669
624
  });
670
625
  }
671
626
 
@@ -707,7 +662,8 @@ function _loadVersionHistory() {
707
662
  });
708
663
  }
709
664
 
710
- // ─── Update Banner ───────────────────────────────────────────────────────────
665
+
666
+ // ─── Update Banner & Changelog (shared, used by IPC handlers) ──────────────
711
667
  // Reusable — called from IPC handler AND from initSettingsPanel
712
668
  function _applyUpdateBanner() {
713
669
  const info = state._updateAvailable;
@@ -830,3 +786,6 @@ async function _showChangelog(version) {
830
786
  if (body) body.innerHTML = '<div style="color:var(--red);padding:20px;text-align:center">Could not fetch release notes</div>';
831
787
  }
832
788
  }
789
+
790
+
791
+
package/panels/sources.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // ─── Sources Panel ─────────────────────────────────────────────────────────
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+ // SOURCES PANEL (placeholder — use JS Debugger button for breakpoints)
4
+ // ─────────────────────────────────────────────────────────────────────────────
2
5
  function initSourcesPanel() {
3
6
  const panel = $('panel-sources');
7
+ if (!panel) return;
4
8
  panel.innerHTML = `
5
9
  <div class="panel-toolbar">
6
10
  <span class="panel-label">Sources</span>
@@ -280,3 +284,6 @@ async function loadSourceFile(filepath) {
280
284
  function updateSourcesPanel(targets) {
281
285
  // No-op: file list is populated by fetchSourceFileList from Metro source map
282
286
  }
287
+
288
+ // ─────────────────────────────────────────────────────────────────────────────
289
+ // PERFORMANCE PANEL — FPS, render timing, JS thread
package/panels/storage.js CHANGED
@@ -1,8 +1,10 @@
1
+ // ─── AsyncStorage Panel ────────────────────────────────────────────────────
1
2
  // ─────────────────────────────────────────────────────────────────────────────
2
- // ASYNC STORAGE PANEL — extracted from app.js
3
+ // ASYNC STORAGE PANEL
3
4
  // ─────────────────────────────────────────────────────────────────────────────
4
5
  function initStoragePanel() {
5
6
  const panel = $('panel-storage');
7
+ if (!panel) return;
6
8
  panel.innerHTML = `
7
9
  <div class="panel-toolbar">
8
10
  <span class="panel-label">AsyncStorage</span>
@@ -183,3 +185,7 @@ function formatSize(bytes) {
183
185
  if (bytes < 1024) return `${bytes}b`;
184
186
  return `${(bytes/1024).toFixed(1)}kb`;
185
187
  }
188
+
189
+ // ─────────────────────────────────────────────────────────────────────────────
190
+ // REACT TREE PANEL
191
+ // ─────────────────────────────────────────────────────────────────────────────