reactoradar 1.5.8 → 1.5.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.
Files changed (3) hide show
  1. package/app.js +226 -33
  2. package/package.json +1 -1
  3. package/styles.css +15 -1
package/app.js CHANGED
@@ -67,7 +67,7 @@ function renderJSON(val) {
67
67
  try {
68
68
  const str = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
69
69
  return syntaxHighlight(esc(str));
70
- } catch { return esc(String(val)); }
70
+ } catch { return esc(typeof val === 'object' ? JSON.stringify(val) : String(val)); }
71
71
  }
72
72
 
73
73
  function tryURL(url) { try { return new URL(url); } catch { return null; } }
@@ -239,7 +239,7 @@ if (window.electronAPI) {
239
239
  window.electronAPI.on('clear-all-ui', clearAll);
240
240
 
241
241
  // Cmd+F — focus the search input for the active panel
242
- window.electronAPI.on('focus-search', () => {
242
+ function _handleFind() {
243
243
  const searchMap = {
244
244
  console: 'consoleSearch',
245
245
  network: 'netSearchInput',
@@ -257,6 +257,14 @@ if (window.electronAPI) {
257
257
  const bar = $('consoleFindBar');
258
258
  if (bar) { bar.style.display = 'flex'; $('consoleFindInput')?.focus(); }
259
259
  }
260
+ }
261
+ window.electronAPI.on('focus-search', _handleFind);
262
+ // Direct keyboard fallback — Electron menu accelerators can miss in some contexts
263
+ document.addEventListener('keydown', (e) => {
264
+ if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
265
+ e.preventDefault();
266
+ _handleFind();
267
+ }
260
268
  });
261
269
 
262
270
  window.electronAPI.on('app-version', (version) => {
@@ -688,6 +696,14 @@ function createTreeNode(key, val, startCollapsed) {
688
696
  return container;
689
697
  }
690
698
 
699
+ function _safeStr(val) {
700
+ if (val === null) return 'null';
701
+ if (val === undefined) return 'undefined';
702
+ if (typeof val === 'string') return val;
703
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val);
704
+ try { return JSON.stringify(val, null, 2); } catch { return String(val); }
705
+ }
706
+
691
707
  function createPrimitiveSpan(val) {
692
708
  const s = document.createElement('span');
693
709
  if (val === null) { s.className = 'ov-null'; s.textContent = 'null'; }
@@ -695,7 +711,7 @@ function createPrimitiveSpan(val) {
695
711
  else if (typeof val === 'string') { s.className = 'ov-str'; s.textContent = `"${val}"`; }
696
712
  else if (typeof val === 'number') { s.className = 'ov-num'; s.textContent = String(val); }
697
713
  else if (typeof val === 'boolean') { s.className = 'ov-bool'; s.textContent = String(val); }
698
- else { s.textContent = String(val); }
714
+ else { s.textContent = _safeStr(val); }
699
715
  return s;
700
716
  }
701
717
 
@@ -705,7 +721,7 @@ function renderConsoleArg(arg) {
705
721
  // Backward compat: raw string
706
722
  const s = document.createElement('span');
707
723
  s.className = 'ov-str';
708
- s.textContent = String(arg);
724
+ s.textContent = _safeStr(arg);
709
725
  return s;
710
726
  }
711
727
  const { t, v } = arg;
@@ -723,7 +739,7 @@ function renderConsoleArg(arg) {
723
739
  return createTreeNode(null, v, false);
724
740
  }
725
741
  const s = document.createElement('span');
726
- s.textContent = String(v);
742
+ s.textContent = _safeStr(v);
727
743
  return s;
728
744
  }
729
745
 
@@ -1972,6 +1988,94 @@ function _findLeafChanges(oldVal, newVal, basePath, maxDepth) {
1972
1988
  return changes;
1973
1989
  }
1974
1990
 
1991
+ // Create a tree node with changed paths highlighted in a different color
1992
+ function _createHighlightedTree(key, val, changedPaths, currentPath, isOld) {
1993
+ const isArray = Array.isArray(val);
1994
+ const isObj = val !== null && typeof val === 'object';
1995
+ const myPath = key !== null ? (currentPath ? `${currentPath}.${key}` : String(key)) : currentPath;
1996
+ const isChanged = changedPaths.has(myPath);
1997
+
1998
+ if (!isObj) {
1999
+ // Leaf value
2000
+ const row = document.createElement('div');
2001
+ row.className = 'ov-leaf' + (isChanged ? ' rdx-highlight' : '');
2002
+ if (isChanged) row.style.cssText = isOld
2003
+ ? 'background:rgba(255,94,114,.12);border-radius:3px;padding:1px 4px;'
2004
+ : 'background:rgba(61,214,140,.12);border-radius:3px;padding:1px 4px;';
2005
+ if (key !== null) {
2006
+ const k = document.createElement('span');
2007
+ k.className = 'ov-key';
2008
+ k.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : '';
2009
+ k.textContent = `${key}: `;
2010
+ row.appendChild(k);
2011
+ }
2012
+ const v = document.createElement('span');
2013
+ v.className = 'ov-prim';
2014
+ if (isChanged) v.style.fontWeight = '700';
2015
+ if (val === null) { v.textContent = 'null'; v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--text-dim)'; }
2016
+ else if (typeof val === 'string') { v.textContent = `"${val}"`; v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--green)'; }
2017
+ else if (typeof val === 'number') { v.textContent = String(val); v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--accent2)'; }
2018
+ else if (typeof val === 'boolean') { v.textContent = String(val); v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--accent2)'; }
2019
+ else { v.textContent = _safeStr(val); }
2020
+ row.appendChild(v);
2021
+ return row;
2022
+ }
2023
+
2024
+ // Object/Array — check if any descendants changed
2025
+ const hasChangedDescendant = [...changedPaths].some(p => p === myPath || p.startsWith(myPath ? myPath + '.' : ''));
2026
+ const container = document.createElement('div');
2027
+ container.className = 'ov-node';
2028
+
2029
+ const header = document.createElement('div');
2030
+ header.className = 'ov-header';
2031
+
2032
+ const arrow = document.createElement('span');
2033
+ arrow.className = 'ov-arrow';
2034
+ arrow.textContent = '\u25B6';
2035
+ header.appendChild(arrow);
2036
+
2037
+ if (key !== null) {
2038
+ const k = document.createElement('span');
2039
+ k.className = 'ov-key';
2040
+ if (hasChangedDescendant) k.style.color = isOld ? 'var(--red)' : 'var(--green)';
2041
+ k.textContent = `${key}: `;
2042
+ header.appendChild(k);
2043
+ }
2044
+
2045
+ const preview = document.createElement('span');
2046
+ preview.className = 'ov-preview';
2047
+ preview.textContent = isArray ? `Array(${val.length})` : `{${Object.keys(val).length} keys}`;
2048
+ header.appendChild(preview);
2049
+
2050
+ container.appendChild(header);
2051
+
2052
+ const children = document.createElement('div');
2053
+ children.className = 'ov-children';
2054
+ // Always start collapsed — user expands what they need
2055
+ children.style.display = 'none';
2056
+
2057
+ let populated = false;
2058
+ function populate() {
2059
+ if (populated) return;
2060
+ populated = true;
2061
+ const entries = isArray ? val.map((v, i) => [i, v]) : Object.entries(val);
2062
+ entries.forEach(([k, v]) => {
2063
+ children.appendChild(_createHighlightedTree(k, v, changedPaths, myPath, isOld));
2064
+ });
2065
+ }
2066
+
2067
+ header.addEventListener('click', (e) => {
2068
+ e.stopPropagation();
2069
+ const open = children.style.display !== 'none';
2070
+ children.style.display = open ? 'none' : 'block';
2071
+ arrow.textContent = open ? '\u25B6' : '\u25BC';
2072
+ if (!open) populate();
2073
+ });
2074
+
2075
+ container.appendChild(children);
2076
+ return container;
2077
+ }
2078
+
1975
2079
  function handleReduxEvent(event) {
1976
2080
  if (event.type !== 'redux') return;
1977
2081
  const { action, nextState } = event;
@@ -2004,6 +2108,29 @@ function handleReduxEvent(event) {
2004
2108
  }
2005
2109
  }
2006
2110
 
2111
+ // Assign a consistent color to each Redux action category (e.g. ANALYTICS, CART, USER)
2112
+ const _reduxCatColors = {};
2113
+ const _reduxColorPalette = [
2114
+ 'var(--accent)', // blue
2115
+ 'var(--green)', // green
2116
+ 'var(--orange)', // orange
2117
+ 'var(--accent2)', // purple
2118
+ '#e06c75', // coral
2119
+ '#56b6c2', // teal
2120
+ '#c678dd', // magenta
2121
+ '#d19a66', // gold
2122
+ '#98c379', // lime
2123
+ '#e5c07b', // yellow
2124
+ ];
2125
+ let _reduxColorIdx = 0;
2126
+ function _reduxCategoryColor(category) {
2127
+ if (!_reduxCatColors[category]) {
2128
+ _reduxCatColors[category] = _reduxColorPalette[_reduxColorIdx % _reduxColorPalette.length];
2129
+ _reduxColorIdx++;
2130
+ }
2131
+ return _reduxCatColors[category];
2132
+ }
2133
+
2007
2134
  function renderRedux() {
2008
2135
  const content = $('reduxContent');
2009
2136
  const empty = $('reduxEmpty');
@@ -2031,12 +2158,32 @@ function renderRedux() {
2031
2158
  const header = document.createElement('div');
2032
2159
  header.className = 'rdx-entry-header';
2033
2160
  const changesBadge = a.changedKeys?.length ? `<span class="rdx-changes">${a.changedKeys.length} changed</span>` : '';
2034
- header.innerHTML = `<span class="rdx-index">#${a.index}</span><span class="rdx-type">${esc(a.type)}</span>${changesBadge}<span class="rdx-time">${ts(a.ts)}</span>`;
2161
+ // Color-code action type by category prefix (e.g. ANALYTICS/, CART/, USER/)
2162
+ const typeParts = a.type.split('/');
2163
+ let typeHtml;
2164
+ if (typeParts.length >= 2) {
2165
+ const catColor = _reduxCategoryColor(typeParts[0]);
2166
+ typeHtml = `<span class="rdx-type-cat" style="color:${catColor}">${esc(typeParts[0])}/</span><span class="rdx-type-name">${esc(typeParts.slice(1).join('/'))}</span>`;
2167
+ } else {
2168
+ typeHtml = `<span class="rdx-type">${esc(a.type)}</span>`;
2169
+ }
2170
+ header.innerHTML = `<span class="rdx-index">#${a.index}</span>${typeHtml}<span class="rdx-header-right">${changesBadge}<span class="rdx-time">${ts(a.ts)}</span></span>`;
2035
2171
  // Toggle: click to expand, click again to collapse
2036
2172
  header.addEventListener('click', () => {
2037
2173
  state.redux.selected = isSelected ? -1 : a.index;
2038
2174
  renderRedux();
2039
2175
  });
2176
+ // Right-click to copy action type
2177
+ header.addEventListener('contextmenu', (e) => {
2178
+ e.preventDefault();
2179
+ e.stopPropagation();
2180
+ showContextMenu(e, [
2181
+ { label: 'Copy Action Type', action: () => navigator.clipboard.writeText(a.type) },
2182
+ { label: 'Copy Action Payload', action: () => navigator.clipboard.writeText(JSON.stringify(a.payload, null, 2)) },
2183
+ ]);
2184
+ });
2185
+ // Allow text selection on the action type
2186
+ header.style.userSelect = 'text';
2040
2187
  entry.appendChild(header);
2041
2188
 
2042
2189
  // Expanded detail — only for explicitly selected action
@@ -2044,6 +2191,18 @@ function renderRedux() {
2044
2191
  const detail = document.createElement('div');
2045
2192
  detail.className = 'rdx-entry-detail';
2046
2193
 
2194
+ // Close button
2195
+ const closeBtn = document.createElement('button');
2196
+ closeBtn.className = 'rdx-close-btn';
2197
+ closeBtn.textContent = '✕';
2198
+ closeBtn.title = 'Close';
2199
+ closeBtn.addEventListener('click', (e) => {
2200
+ e.stopPropagation();
2201
+ state.redux.selected = -1;
2202
+ renderRedux();
2203
+ });
2204
+ detail.appendChild(closeBtn);
2205
+
2047
2206
  // Changed keys badges
2048
2207
  if (a.changedKeys?.length > 0) {
2049
2208
  const keysEl = document.createElement('div');
@@ -2062,18 +2221,14 @@ function renderRedux() {
2062
2221
  detail.appendChild(createTreeNode(null, a.payload, false));
2063
2222
  }
2064
2223
 
2065
- // Store changes — show ONLY the changed leaf values, not entire state
2224
+ // Store changes — two-column layout: Previous | Current
2066
2225
  const prevS = a.index > 0 ? states[a.index - 1] : null;
2067
2226
  const currS = states[a.index];
2068
2227
  if (currS && typeof currS === 'object' && a.changedKeys?.length > 0) {
2069
- const sLabel = document.createElement('div');
2070
- sLabel.className = 'redux-section-title';
2071
- sLabel.textContent = 'Store Changes';
2072
- detail.appendChild(sLabel);
2073
-
2074
2228
  a.changedKeys.forEach(key => {
2075
2229
  const keyWrap = document.createElement('div');
2076
2230
  keyWrap.className = 'rdx-store-diff';
2231
+
2077
2232
  const kLabel = document.createElement('div');
2078
2233
  kLabel.className = 'rdx-store-key-label';
2079
2234
  kLabel.textContent = key;
@@ -2082,29 +2237,67 @@ function renderRedux() {
2082
2237
  const oldVal = prevS ? prevS[key] : undefined;
2083
2238
  const newVal = currS[key];
2084
2239
 
2085
- // Deep diff: find only the leaf values that actually changed
2086
- const leafChanges = _findLeafChanges(oldVal, newVal, key);
2087
- if (leafChanges.length > 0) {
2088
- leafChanges.forEach(change => {
2089
- const row = document.createElement('div');
2090
- row.className = 'rdx-diff-leaf';
2091
- row.innerHTML = `<span class="rdx-diff-path">${esc(change.path)}</span>`
2092
- + `<span class="rdx-diff-old">${esc(String(change.oldVal ?? 'undefined'))}</span>`
2093
- + `<span class="rdx-diff-arrow">→</span>`
2094
- + `<span class="rdx-diff-new">${esc(String(change.newVal ?? 'undefined'))}</span>`;
2095
- keyWrap.appendChild(row);
2096
- });
2240
+ // Find which sub-keys changed (for highlighting)
2241
+ const changedPaths = new Set();
2242
+ _findLeafChanges(oldVal, newVal, '').forEach(c => changedPaths.add(c.path));
2243
+
2244
+ // Two-column grid: Previous | Current
2245
+ const grid = document.createElement('div');
2246
+ grid.className = 'rdx-diff-grid';
2247
+
2248
+ // Previous column
2249
+ const prevCol = document.createElement('div');
2250
+ prevCol.className = 'rdx-diff-col prev';
2251
+ const prevLabel = document.createElement('div');
2252
+ prevLabel.className = 'rdx-state-label prev';
2253
+ prevLabel.textContent = '- Previous';
2254
+ prevCol.appendChild(prevLabel);
2255
+ if (oldVal !== undefined) {
2256
+ prevCol.appendChild(_createHighlightedTree(null, oldVal, changedPaths, '', true));
2257
+ } else {
2258
+ const na = document.createElement('span');
2259
+ na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
2260
+ na.textContent = 'undefined';
2261
+ prevCol.appendChild(na);
2262
+ }
2263
+ grid.appendChild(prevCol);
2264
+
2265
+ // Current column
2266
+ const currCol = document.createElement('div');
2267
+ currCol.className = 'rdx-diff-col curr';
2268
+ const currLabel = document.createElement('div');
2269
+ currLabel.className = 'rdx-state-label curr';
2270
+ currLabel.textContent = '+ Current';
2271
+ currCol.appendChild(currLabel);
2272
+ if (newVal !== undefined) {
2273
+ currCol.appendChild(_createHighlightedTree(null, newVal, changedPaths, '', false));
2097
2274
  } else {
2098
- // Fallback: show old → new for the whole key
2099
- const row = document.createElement('div');
2100
- row.className = 'rdx-diff-leaf';
2101
- const oldStr = oldVal === undefined ? 'undefined' : (typeof oldVal === 'object' ? JSON.stringify(oldVal) : String(oldVal));
2102
- const newStr = newVal === undefined ? 'undefined' : (typeof newVal === 'object' ? JSON.stringify(newVal) : String(newVal));
2103
- row.innerHTML = `<span class="rdx-diff-old">${esc(oldStr)}</span>`
2104
- + `<span class="rdx-diff-arrow">→</span>`
2105
- + `<span class="rdx-diff-new">${esc(newStr)}</span>`;
2106
- keyWrap.appendChild(row);
2275
+ const na = document.createElement('span');
2276
+ na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
2277
+ na.textContent = 'undefined';
2278
+ currCol.appendChild(na);
2107
2279
  }
2280
+ grid.appendChild(currCol);
2281
+
2282
+ // Right-click to copy on each column
2283
+ prevCol.addEventListener('contextmenu', (e) => {
2284
+ e.preventDefault(); e.stopPropagation();
2285
+ showContextMenu(e, [
2286
+ { label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
2287
+ { label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
2288
+ { label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
2289
+ ]);
2290
+ });
2291
+ currCol.addEventListener('contextmenu', (e) => {
2292
+ e.preventDefault(); e.stopPropagation();
2293
+ showContextMenu(e, [
2294
+ { label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
2295
+ { label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
2296
+ { label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
2297
+ ]);
2298
+ });
2299
+
2300
+ keyWrap.appendChild(grid);
2108
2301
  detail.appendChild(keyWrap);
2109
2302
  });
2110
2303
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.5.8",
4
+ "version": "1.5.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/styles.css CHANGED
@@ -924,7 +924,10 @@ body {
924
924
  .rdx-entry-header:hover { background: var(--bg3); }
925
925
  .rdx-entry.selected .rdx-entry-header { border-left: 3px solid var(--accent); }
926
926
  .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; }
927
+ .rdx-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-bright); font-weight: 500; }
928
+ .rdx-type-cat { font-weight: 700; }
929
+ .rdx-type-name { color: var(--text-bright); font-weight: 500; }
930
+ .rdx-header-right { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
928
931
  .rdx-changes {
929
932
  font-size: 8px; font-weight: 700; padding: 1px 5px; border-radius: 8px;
930
933
  background: rgba(255,94,114,.12); color: var(--red); flex-shrink: 0;
@@ -968,6 +971,17 @@ body {
968
971
  .rdx-diff-old { color: var(--red); text-decoration: line-through; opacity: 0.8; word-break: break-all; }
969
972
  .rdx-diff-arrow { color: var(--text-dim); font-size: 10px; flex-shrink: 0; }
970
973
  .rdx-diff-new { color: var(--green); font-weight: 600; word-break: break-all; }
974
+ .rdx-state-label { font-size: 10px; font-weight: 700; padding: 4px 0 2px; letter-spacing: 0.5px; }
975
+ .rdx-state-label.prev { color: var(--red); }
976
+ .rdx-state-label.curr { color: var(--green); }
977
+ .rdx-diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 4px; }
978
+ .rdx-diff-col { min-width: 0; overflow: auto; max-height: 400px; padding: 4px; border-radius: 4px; font-size: 11px; }
979
+ .rdx-diff-col.prev { background: rgba(255,94,114,.04); border: 1px solid rgba(255,94,114,.12); }
980
+ .rdx-diff-col.curr { background: rgba(61,214,140,.04); border: 1px solid rgba(61,214,140,.12); }
981
+ .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; }
982
+ .rdx-close-btn:hover { background: var(--bg4); color: var(--text); }
983
+ .rdx-entry-detail { position: relative; }
984
+ .rdx-highlight { font-weight: 600; }
971
985
  .rdx-diff-sign { font-weight: 700; font-size: 11px; flex-shrink: 0; width: 14px; text-align: center; }
972
986
  .rdx-diff-row.removed .rdx-diff-sign { color: var(--red); }
973
987
  .rdx-diff-row.added .rdx-diff-sign { color: var(--green); }