reactoradar 1.6.6 → 1.6.7
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/AGENTS.md +341 -0
- package/init.js +187 -0
- package/package.json +4 -2
- package/panels/console.js +788 -0
- package/panels/ga4.js +328 -0
- package/panels/native.js +256 -0
- package/panels/network.js +968 -0
- package/panels/performance.js +181 -0
- package/panels/react.js +21 -0
- package/panels/redux.js +438 -0
- package/panels/settings.js +832 -0
- package/panels/sources.js +282 -0
- package/panels/storage.js +185 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ─── Performance + Memory Panel ────────────────────────────────────────────
|
|
2
|
+
const perfState = { fps: [], jsThread: [], uiThread: [], recording: false, data: [] };
|
|
3
|
+
|
|
4
|
+
function initPerformancePanel() {
|
|
5
|
+
const panel = $('panel-performance');
|
|
6
|
+
panel.innerHTML = `
|
|
7
|
+
<div class="panel-toolbar">
|
|
8
|
+
<span class="panel-label">Performance</span>
|
|
9
|
+
<div class="ml-auto" style="display:flex;gap:6px">
|
|
10
|
+
<button class="tb-btn" id="btnPerfRecord">Record</button>
|
|
11
|
+
<button class="tb-btn" id="btnPerfClear">Clear</button>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="perf-layout">
|
|
15
|
+
<div class="perf-meters">
|
|
16
|
+
<div class="perf-meter">
|
|
17
|
+
<div class="perf-meter-label">FPS</div>
|
|
18
|
+
<div class="perf-meter-value" id="perfFPS">—</div>
|
|
19
|
+
<canvas class="perf-canvas" id="perfFPSCanvas" width="200" height="60"></canvas>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="perf-meter">
|
|
22
|
+
<div class="perf-meter-label">JS Thread</div>
|
|
23
|
+
<div class="perf-meter-value" id="perfJS">—</div>
|
|
24
|
+
<canvas class="perf-canvas" id="perfJSCanvas" width="200" height="60"></canvas>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="perf-meter">
|
|
27
|
+
<div class="perf-meter-label">UI Thread</div>
|
|
28
|
+
<div class="perf-meter-value" id="perfUI">—</div>
|
|
29
|
+
<canvas class="perf-canvas" id="perfUICanvas" width="200" height="60"></canvas>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="scroll-area perf-timeline" id="perfTimeline">
|
|
33
|
+
<div class="empty-state" id="perfEmpty">
|
|
34
|
+
<div class="icon" style="font-size:28px;opacity:.2">📊</div>
|
|
35
|
+
<div class="label">No performance data</div>
|
|
36
|
+
<div class="hint">Click "Record" to start capturing performance metrics</div>
|
|
37
|
+
<div class="hint">The SDK sends FPS + thread usage automatically when connected</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>`;
|
|
41
|
+
|
|
42
|
+
$('btnPerfRecord').addEventListener('click', () => {
|
|
43
|
+
perfState.recording = !perfState.recording;
|
|
44
|
+
$('btnPerfRecord').textContent = perfState.recording ? 'Stop' : 'Record';
|
|
45
|
+
$('btnPerfRecord').classList.toggle('primary', perfState.recording);
|
|
46
|
+
if (perfState.recording) {
|
|
47
|
+
// Tell SDK to start sending perf data
|
|
48
|
+
window.electronAPI?.setNetworkCapture(true); // reuse channel
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
$('btnPerfClear').addEventListener('click', () => {
|
|
53
|
+
perfState.fps = [];
|
|
54
|
+
perfState.jsThread = [];
|
|
55
|
+
perfState.uiThread = [];
|
|
56
|
+
perfState.data = [];
|
|
57
|
+
$('perfFPS').textContent = '—';
|
|
58
|
+
$('perfJS').textContent = '—';
|
|
59
|
+
$('perfUI').textContent = '—';
|
|
60
|
+
clearPerfCanvas('perfFPSCanvas');
|
|
61
|
+
clearPerfCanvas('perfJSCanvas');
|
|
62
|
+
clearPerfCanvas('perfUICanvas');
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clearPerfCanvas(id) {
|
|
67
|
+
const canvas = $(id);
|
|
68
|
+
if (!canvas) return;
|
|
69
|
+
const ctx = canvas.getContext('2d');
|
|
70
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function drawPerfGraph(canvasId, data, maxVal, color) {
|
|
74
|
+
const canvas = $(canvasId);
|
|
75
|
+
if (!canvas || !data.length) return;
|
|
76
|
+
const ctx = canvas.getContext('2d');
|
|
77
|
+
const w = canvas.width, h = canvas.height;
|
|
78
|
+
ctx.clearRect(0, 0, w, h);
|
|
79
|
+
|
|
80
|
+
// Grid lines
|
|
81
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
|
|
82
|
+
ctx.lineWidth = 1;
|
|
83
|
+
for (let y = 0; y < h; y += h/4) {
|
|
84
|
+
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Data line
|
|
88
|
+
ctx.strokeStyle = color;
|
|
89
|
+
ctx.lineWidth = 1.5;
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
const step = w / Math.max(data.length - 1, 1);
|
|
92
|
+
data.forEach((v, i) => {
|
|
93
|
+
const x = i * step;
|
|
94
|
+
const y = h - (v / maxVal) * h;
|
|
95
|
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
96
|
+
});
|
|
97
|
+
ctx.stroke();
|
|
98
|
+
|
|
99
|
+
// Fill under
|
|
100
|
+
ctx.lineTo(w, h);
|
|
101
|
+
ctx.lineTo(0, h);
|
|
102
|
+
ctx.closePath();
|
|
103
|
+
ctx.fillStyle = color.replace('1)', '0.1)');
|
|
104
|
+
ctx.fill();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle performance events from SDK (always updates meters, graphs only when recording)
|
|
108
|
+
function handlePerfEvent(event) {
|
|
109
|
+
if (!isTabEnabled('performance') && !isTabEnabled('memory')) return;
|
|
110
|
+
if (event.fps != null) {
|
|
111
|
+
perfState.fps.push(event.fps);
|
|
112
|
+
if (perfState.fps.length > 100) perfState.fps.shift();
|
|
113
|
+
const fpsEl = $('perfFPS');
|
|
114
|
+
if (fpsEl) fpsEl.textContent = event.fps + ' fps';
|
|
115
|
+
drawPerfGraph('perfFPSCanvas', perfState.fps, 60, 'rgba(61,214,140,1)');
|
|
116
|
+
}
|
|
117
|
+
if (event.jsThread != null) {
|
|
118
|
+
perfState.jsThread.push(event.jsThread);
|
|
119
|
+
if (perfState.jsThread.length > 100) perfState.jsThread.shift();
|
|
120
|
+
const jsEl = $('perfJS');
|
|
121
|
+
if (jsEl) jsEl.textContent = event.jsThread.toFixed(1) + 'ms';
|
|
122
|
+
drawPerfGraph('perfJSCanvas', perfState.jsThread, 32, 'rgba(79,172,255,1)');
|
|
123
|
+
}
|
|
124
|
+
if (event.uiThread != null) {
|
|
125
|
+
perfState.uiThread.push(event.uiThread);
|
|
126
|
+
if (perfState.uiThread.length > 100) perfState.uiThread.shift();
|
|
127
|
+
const uiEl = $('perfUI');
|
|
128
|
+
if (uiEl) uiEl.textContent = event.uiThread.toFixed(1) + 'ms';
|
|
129
|
+
drawPerfGraph('perfUICanvas', perfState.uiThread, 32, 'rgba(155,127,255,1)');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Memory Panel ────────────────────────────────────────────────────────────
|
|
134
|
+
function initMemoryPanel() {
|
|
135
|
+
const panel = $('panel-memory');
|
|
136
|
+
panel.innerHTML = `
|
|
137
|
+
<div class="panel-toolbar">
|
|
138
|
+
<span class="panel-label">Memory</span>
|
|
139
|
+
<div class="ml-auto" style="display:flex;gap:6px">
|
|
140
|
+
<button class="tb-btn primary" id="btnHeapSnapshot">Take Heap Snapshot</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="memory-layout">
|
|
144
|
+
<div class="perf-meters" style="padding:14px">
|
|
145
|
+
<div class="perf-meter">
|
|
146
|
+
<div class="perf-meter-label">JS Heap Used</div>
|
|
147
|
+
<div class="perf-meter-value" id="memHeapUsed">—</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="perf-meter">
|
|
150
|
+
<div class="perf-meter-label">JS Heap Total</div>
|
|
151
|
+
<div class="perf-meter-value" id="memHeapTotal">—</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="perf-meter">
|
|
154
|
+
<div class="perf-meter-label">Native Memory</div>
|
|
155
|
+
<div class="perf-meter-value" id="memNative">—</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="scroll-area" id="memoryContent">
|
|
159
|
+
<div class="empty-state" id="memoryEmpty">
|
|
160
|
+
<div class="icon" style="font-size:28px;opacity:.2">🧠</div>
|
|
161
|
+
<div class="label">No memory data</div>
|
|
162
|
+
<div class="hint">Click "Take Heap Snapshot" to capture memory usage</div>
|
|
163
|
+
<div class="hint">Requires Hermes CDP connection (press Cmd+D first)</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>`;
|
|
167
|
+
|
|
168
|
+
$('btnHeapSnapshot').addEventListener('click', () => {
|
|
169
|
+
// Request heap snapshot via CDP - this opens the DevTools window
|
|
170
|
+
// which has built-in Memory profiler
|
|
171
|
+
window.electronAPI?.openCDPTarget(null);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle memory events from SDK
|
|
176
|
+
function handleMemoryEvent(event) {
|
|
177
|
+
const hu = $('memHeapUsed'), ht = $('memHeapTotal'), mn = $('memNative');
|
|
178
|
+
if (event.heapUsed != null && hu) hu.textContent = formatSize(event.heapUsed);
|
|
179
|
+
if (event.heapTotal != null && ht) ht.textContent = formatSize(event.heapTotal);
|
|
180
|
+
if (event.native != null && mn) mn.textContent = formatSize(event.native);
|
|
181
|
+
}
|
package/panels/react.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ─── React Tree Panel ──────────────────────────────────────────────────────
|
|
2
|
+
function initReactPanel() {
|
|
3
|
+
const panel = $('panel-react');
|
|
4
|
+
panel.innerHTML = `
|
|
5
|
+
<div class="panel-toolbar">
|
|
6
|
+
<span class="panel-label">React Tree</span>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="react-panel-inner">
|
|
9
|
+
<div class="react-connect-hint" id="reactHint">
|
|
10
|
+
<div class="icon" style="font-size:40px;opacity:.2">⚛️</div>
|
|
11
|
+
<div class="label">React DevTools</div>
|
|
12
|
+
<div class="hint">Opens as a separate window connected to your app via port 8097</div>
|
|
13
|
+
<div class="hint" style="margin-top:8px;color:var(--yellow)">Note: The RN inspector overlay won't work while React DevTools is connected. Close the DevTools window to use the built-in inspector.</div>
|
|
14
|
+
<button class="btn-launch" id="btnReactDT" style="margin-top:12px">Open React DevTools ↗</button>
|
|
15
|
+
</div>
|
|
16
|
+
</div>`;
|
|
17
|
+
|
|
18
|
+
$('btnReactDT').addEventListener('click', () => {
|
|
19
|
+
window.electronAPI?.openReactDevTools();
|
|
20
|
+
});
|
|
21
|
+
}
|
package/panels/redux.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// REDUX PANEL — extracted from app.js
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
function initReduxPanel() {
|
|
5
|
+
const panel = $('panel-redux');
|
|
6
|
+
panel.innerHTML = `
|
|
7
|
+
<div class="panel-toolbar">
|
|
8
|
+
<span class="panel-label">Redux</span>
|
|
9
|
+
<span class="badge" id="rBadge">0</span>
|
|
10
|
+
<input id="reduxSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter actions..." />
|
|
11
|
+
<div class="ml-auto" style="display:flex;align-items:center;gap:8px">
|
|
12
|
+
<button class="panel-clear-btn" id="reduxClear" title="Clear redux">Clear</button>
|
|
13
|
+
<button class="panel-clear-btn" id="reduxSort" title="Toggle sort order">Time ▲</button>
|
|
14
|
+
<div class="time-travel-bar" style="border:none;padding:0;margin:0">
|
|
15
|
+
<button class="tt-btn" onclick="reduxJumpTo(state.redux.selected-1)">◀</button>
|
|
16
|
+
<span class="tt-label" id="ttLabel">—/—</span>
|
|
17
|
+
<button class="tt-btn" onclick="reduxJumpTo(state.redux.selected+1)">▶</button>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="scroll-area" id="reduxContent">
|
|
22
|
+
<div class="empty-state" id="reduxEmpty">
|
|
23
|
+
<div class="icon">🔲</div>
|
|
24
|
+
<div class="label">No actions dispatched</div>
|
|
25
|
+
<div class="hint">Connect Redux store to RNDebugSDK</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>`;
|
|
28
|
+
|
|
29
|
+
$('reduxSearch').addEventListener('input', (e) => {
|
|
30
|
+
state.redux.searchFilter = e.target.value.toLowerCase().trim();
|
|
31
|
+
renderRedux();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
$('reduxClear').addEventListener('click', () => {
|
|
35
|
+
state.redux.actions = [];
|
|
36
|
+
state.redux.states = [];
|
|
37
|
+
state.redux.selected = -1;
|
|
38
|
+
$('rBadge').textContent = '0';
|
|
39
|
+
renderRedux();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
$('reduxSort').addEventListener('click', () => {
|
|
43
|
+
state.redux.sortDir = state.redux.sortDir === 'desc' ? 'asc' : 'desc';
|
|
44
|
+
$('reduxSort').textContent = state.redux.sortDir === 'desc' ? 'Time \u25BC' : 'Time \u25B2';
|
|
45
|
+
renderRedux();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
window.reduxJumpTo = idx => {
|
|
50
|
+
const { actions } = state.redux;
|
|
51
|
+
if (!actions.length) return;
|
|
52
|
+
idx = Math.max(0, Math.min(actions.length - 1, idx));
|
|
53
|
+
state.redux.selected = idx;
|
|
54
|
+
renderRedux();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Fast deep equality check for Redux state comparison
|
|
58
|
+
function _deepEqual(a, b) {
|
|
59
|
+
if (a === b) return true;
|
|
60
|
+
if (a == null || b == null) return false;
|
|
61
|
+
if (typeof a !== typeof b) return false;
|
|
62
|
+
if (typeof a !== 'object') return false;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
65
|
+
} catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Find leaf-level changes between two values (for Redux store diff)
|
|
69
|
+
function _findLeafChanges(oldVal, newVal, basePath, maxDepth) {
|
|
70
|
+
const changes = [];
|
|
71
|
+
if (maxDepth === undefined) maxDepth = 5;
|
|
72
|
+
|
|
73
|
+
function walk(a, b, path, depth) {
|
|
74
|
+
if (depth > maxDepth) {
|
|
75
|
+
if (!_deepEqual(a, b)) changes.push({ path, oldVal: a, newVal: b });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (a === b) return;
|
|
79
|
+
if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b)) {
|
|
80
|
+
changes.push({ path, oldVal: a, newVal: b });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
84
|
+
allKeys.forEach(k => {
|
|
85
|
+
if (!_deepEqual(a[k], b[k])) {
|
|
86
|
+
const childPath = path ? `${path}.${k}` : k;
|
|
87
|
+
if (a[k] != null && b[k] != null && typeof a[k] === 'object' && typeof b[k] === 'object' && !Array.isArray(a[k])) {
|
|
88
|
+
walk(a[k], b[k], childPath, depth + 1);
|
|
89
|
+
} else {
|
|
90
|
+
changes.push({ path: childPath, oldVal: a[k], newVal: b[k] });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
walk(oldVal, newVal, '', 0);
|
|
97
|
+
return changes;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create a tree node with changed paths highlighted in a different color
|
|
101
|
+
function _createHighlightedTree(key, val, changedPaths, currentPath, isOld) {
|
|
102
|
+
const isArray = Array.isArray(val);
|
|
103
|
+
const isObj = val !== null && typeof val === 'object';
|
|
104
|
+
const myPath = key !== null ? (currentPath ? `${currentPath}.${key}` : String(key)) : currentPath;
|
|
105
|
+
const isChanged = changedPaths.has(myPath);
|
|
106
|
+
|
|
107
|
+
if (!isObj) {
|
|
108
|
+
// Leaf value
|
|
109
|
+
const row = document.createElement('div');
|
|
110
|
+
row.className = 'ov-leaf' + (isChanged ? ' rdx-highlight' : '');
|
|
111
|
+
if (isChanged) row.style.cssText = isOld
|
|
112
|
+
? 'background:rgba(255,94,114,.12);border-radius:3px;padding:1px 4px;'
|
|
113
|
+
: 'background:rgba(61,214,140,.12);border-radius:3px;padding:1px 4px;';
|
|
114
|
+
if (key !== null) {
|
|
115
|
+
const k = document.createElement('span');
|
|
116
|
+
k.className = 'ov-key';
|
|
117
|
+
k.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : '';
|
|
118
|
+
k.textContent = `${key}: `;
|
|
119
|
+
row.appendChild(k);
|
|
120
|
+
}
|
|
121
|
+
const v = document.createElement('span');
|
|
122
|
+
v.className = 'ov-prim';
|
|
123
|
+
if (isChanged) v.style.fontWeight = '700';
|
|
124
|
+
if (val === null) { v.textContent = 'null'; v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--text-dim)'; }
|
|
125
|
+
else if (typeof val === 'string') { v.textContent = `"${val}"`; v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--green)'; }
|
|
126
|
+
else if (typeof val === 'number') { v.textContent = String(val); v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--accent2)'; }
|
|
127
|
+
else if (typeof val === 'boolean') { v.textContent = String(val); v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--accent2)'; }
|
|
128
|
+
else { v.textContent = _safeStr(val); }
|
|
129
|
+
row.appendChild(v);
|
|
130
|
+
return row;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Object/Array — check if any descendants changed
|
|
134
|
+
const hasChangedDescendant = [...changedPaths].some(p => p === myPath || p.startsWith(myPath ? myPath + '.' : ''));
|
|
135
|
+
const container = document.createElement('div');
|
|
136
|
+
container.className = 'ov-node';
|
|
137
|
+
|
|
138
|
+
const header = document.createElement('div');
|
|
139
|
+
header.className = 'ov-header';
|
|
140
|
+
|
|
141
|
+
const arrow = document.createElement('span');
|
|
142
|
+
arrow.className = 'ov-arrow';
|
|
143
|
+
arrow.textContent = '\u25B6';
|
|
144
|
+
header.appendChild(arrow);
|
|
145
|
+
|
|
146
|
+
if (key !== null) {
|
|
147
|
+
const k = document.createElement('span');
|
|
148
|
+
k.className = 'ov-key';
|
|
149
|
+
if (hasChangedDescendant) k.style.color = isOld ? 'var(--red)' : 'var(--green)';
|
|
150
|
+
k.textContent = `${key}: `;
|
|
151
|
+
header.appendChild(k);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const preview = document.createElement('span');
|
|
155
|
+
preview.className = 'ov-preview';
|
|
156
|
+
preview.textContent = isArray ? `Array(${val.length})` : `{${Object.keys(val).length} keys}`;
|
|
157
|
+
header.appendChild(preview);
|
|
158
|
+
|
|
159
|
+
container.appendChild(header);
|
|
160
|
+
|
|
161
|
+
const children = document.createElement('div');
|
|
162
|
+
children.className = 'ov-children';
|
|
163
|
+
// Always start collapsed — user expands what they need
|
|
164
|
+
children.style.display = 'none';
|
|
165
|
+
|
|
166
|
+
let populated = false;
|
|
167
|
+
function populate() {
|
|
168
|
+
if (populated) return;
|
|
169
|
+
populated = true;
|
|
170
|
+
const entries = isArray ? val.map((v, i) => [i, v]) : Object.entries(val);
|
|
171
|
+
entries.forEach(([k, v]) => {
|
|
172
|
+
children.appendChild(_createHighlightedTree(k, v, changedPaths, myPath, isOld));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
header.addEventListener('click', (e) => {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
const open = children.style.display !== 'none';
|
|
179
|
+
children.style.display = open ? 'none' : 'block';
|
|
180
|
+
arrow.textContent = open ? '\u25B6' : '\u25BC';
|
|
181
|
+
if (!open) populate();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
container.appendChild(children);
|
|
185
|
+
return container;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleReduxEvent(event) {
|
|
189
|
+
if (event.type !== 'redux') return;
|
|
190
|
+
// Skip processing if Redux tab is disabled (saves memory)
|
|
191
|
+
if (!isTabEnabled('redux')) return;
|
|
192
|
+
const { action, nextState } = event;
|
|
193
|
+
const idx = state.redux.actions.length;
|
|
194
|
+
|
|
195
|
+
const prevState = state.redux.states.length > 0 ? state.redux.states[state.redux.states.length - 1] : null;
|
|
196
|
+
const changedKeys = [];
|
|
197
|
+
if (prevState && nextState && typeof prevState === 'object' && typeof nextState === 'object') {
|
|
198
|
+
const allKeys = new Set([...Object.keys(prevState), ...Object.keys(nextState)]);
|
|
199
|
+
allKeys.forEach(k => { if (!_deepEqual(prevState[k], nextState[k])) changedKeys.push(k); });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const actionEntry = { type: action?.type || '?', payload: action, ts: event.ts, index: idx, changedKeys };
|
|
203
|
+
state.redux.actions.push(actionEntry);
|
|
204
|
+
state.redux.states.push(nextState);
|
|
205
|
+
// Cap Redux history to prevent memory leak (full state stored per action)
|
|
206
|
+
const MAX_REDUX_HISTORY = 500;
|
|
207
|
+
if (state.redux.actions.length > MAX_REDUX_HISTORY) {
|
|
208
|
+
const trim = state.redux.actions.length - MAX_REDUX_HISTORY;
|
|
209
|
+
state.redux.actions.splice(0, trim);
|
|
210
|
+
state.redux.states.splice(0, trim);
|
|
211
|
+
// Re-index remaining actions
|
|
212
|
+
state.redux.actions.forEach((a, i) => a.index = i);
|
|
213
|
+
if (state.redux.selected >= 0) state.redux.selected = Math.max(0, state.redux.selected - trim);
|
|
214
|
+
}
|
|
215
|
+
// Don't auto-select — keep all collapsed until user clicks
|
|
216
|
+
$('rBadge').textContent = state.redux.actions.length;
|
|
217
|
+
renderRedux();
|
|
218
|
+
|
|
219
|
+
// Always add Redux actions to console logs — visibility controlled by showRedux filter
|
|
220
|
+
{
|
|
221
|
+
const msg = `[Redux] ${actionEntry.type}` + (changedKeys.length ? ` (changed: ${changedKeys.join(', ')})` : '');
|
|
222
|
+
addConsoleLog({
|
|
223
|
+
level: 'redux',
|
|
224
|
+
message: msg,
|
|
225
|
+
args: [{ t: 'string', v: `[Redux] ${actionEntry.type}` }, { t: 'object', v: action }],
|
|
226
|
+
ts: event.ts,
|
|
227
|
+
_isRedux: true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Assign a consistent color to each Redux action category (e.g. ANALYTICS, CART, USER)
|
|
233
|
+
const _reduxCatColors = {};
|
|
234
|
+
const _reduxColorPalette = [
|
|
235
|
+
'var(--accent)', // blue
|
|
236
|
+
'var(--green)', // green
|
|
237
|
+
'var(--orange)', // orange
|
|
238
|
+
'var(--accent2)', // purple
|
|
239
|
+
'#e06c75', // coral
|
|
240
|
+
'#56b6c2', // teal
|
|
241
|
+
'#c678dd', // magenta
|
|
242
|
+
'#d19a66', // gold
|
|
243
|
+
'#98c379', // lime
|
|
244
|
+
'#e5c07b', // yellow
|
|
245
|
+
];
|
|
246
|
+
let _reduxColorIdx = 0;
|
|
247
|
+
function _reduxCategoryColor(category) {
|
|
248
|
+
if (!_reduxCatColors[category]) {
|
|
249
|
+
_reduxCatColors[category] = _reduxColorPalette[_reduxColorIdx % _reduxColorPalette.length];
|
|
250
|
+
_reduxColorIdx++;
|
|
251
|
+
}
|
|
252
|
+
return _reduxCatColors[category];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderRedux() {
|
|
256
|
+
const content = $('reduxContent');
|
|
257
|
+
const empty = $('reduxEmpty');
|
|
258
|
+
if (!content) return;
|
|
259
|
+
|
|
260
|
+
const { actions, states, selected, searchFilter, sortDir } = state.redux;
|
|
261
|
+
let visible = searchFilter ? actions.filter(a => a.type.toLowerCase().includes(searchFilter)) : [...actions];
|
|
262
|
+
if (sortDir === 'desc') visible = [...visible].reverse();
|
|
263
|
+
|
|
264
|
+
empty.style.display = visible.length ? 'none' : 'flex';
|
|
265
|
+
content.querySelectorAll('.rdx-entry').forEach(e => e.remove());
|
|
266
|
+
if (!visible.length) return;
|
|
267
|
+
|
|
268
|
+
const ttLabel = $('ttLabel');
|
|
269
|
+
if (ttLabel) ttLabel.textContent = selected >= 0 ? `${selected + 1}/${actions.length}` : `—/${actions.length}`;
|
|
270
|
+
|
|
271
|
+
const frag = document.createDocumentFragment();
|
|
272
|
+
visible.forEach(a => {
|
|
273
|
+
const isSelected = a.index === selected;
|
|
274
|
+
|
|
275
|
+
const entry = document.createElement('div');
|
|
276
|
+
entry.className = 'rdx-entry' + (isSelected ? ' selected' : '');
|
|
277
|
+
|
|
278
|
+
// Row header — always visible
|
|
279
|
+
const header = document.createElement('div');
|
|
280
|
+
header.className = 'rdx-entry-header';
|
|
281
|
+
const changesBadge = a.changedKeys?.length ? `<span class="rdx-changes">${a.changedKeys.length} changed</span>` : '';
|
|
282
|
+
// Color-code action type by category prefix (e.g. ANALYTICS/, CART/, USER/)
|
|
283
|
+
const typeParts = a.type.split('/');
|
|
284
|
+
let typeHtml;
|
|
285
|
+
if (typeParts.length >= 2) {
|
|
286
|
+
const catColor = _reduxCategoryColor(typeParts[0]);
|
|
287
|
+
typeHtml = `<span class="rdx-type-cat" style="color:${catColor}">${esc(typeParts[0])}/</span><span class="rdx-type-name">${esc(typeParts.slice(1).join('/'))}</span>`;
|
|
288
|
+
} else {
|
|
289
|
+
typeHtml = `<span class="rdx-type">${esc(a.type)}</span>`;
|
|
290
|
+
}
|
|
291
|
+
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>`;
|
|
292
|
+
// Toggle: click to expand, click again to collapse
|
|
293
|
+
header.addEventListener('click', () => {
|
|
294
|
+
state.redux.selected = isSelected ? -1 : a.index;
|
|
295
|
+
renderRedux();
|
|
296
|
+
});
|
|
297
|
+
// Right-click to copy action type
|
|
298
|
+
header.addEventListener('contextmenu', (e) => {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
e.stopPropagation();
|
|
301
|
+
showContextMenu(e, [
|
|
302
|
+
{ label: 'Copy Action Type', action: () => navigator.clipboard.writeText(a.type) },
|
|
303
|
+
{ label: 'Copy Action Payload', action: () => navigator.clipboard.writeText(JSON.stringify(a.payload, null, 2)) },
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
// Allow text selection on the action type
|
|
307
|
+
header.style.userSelect = 'text';
|
|
308
|
+
entry.appendChild(header);
|
|
309
|
+
|
|
310
|
+
// Expanded detail — only for explicitly selected action
|
|
311
|
+
if (isSelected) {
|
|
312
|
+
const detail = document.createElement('div');
|
|
313
|
+
detail.className = 'rdx-entry-detail';
|
|
314
|
+
|
|
315
|
+
// Close button
|
|
316
|
+
const closeBtn = document.createElement('button');
|
|
317
|
+
closeBtn.className = 'rdx-close-btn';
|
|
318
|
+
closeBtn.textContent = '✕';
|
|
319
|
+
closeBtn.title = 'Close';
|
|
320
|
+
closeBtn.addEventListener('click', (e) => {
|
|
321
|
+
e.stopPropagation();
|
|
322
|
+
state.redux.selected = -1;
|
|
323
|
+
renderRedux();
|
|
324
|
+
});
|
|
325
|
+
detail.appendChild(closeBtn);
|
|
326
|
+
|
|
327
|
+
// Changed keys badges
|
|
328
|
+
if (a.changedKeys?.length > 0) {
|
|
329
|
+
const keysEl = document.createElement('div');
|
|
330
|
+
keysEl.className = 'redux-changed-keys';
|
|
331
|
+
keysEl.innerHTML = `<span class="redux-changed-label">Changed:</span> ${a.changedKeys.map(k =>
|
|
332
|
+
`<span class="redux-changed-key">${esc(k)}</span>`).join(' ')}`;
|
|
333
|
+
detail.appendChild(keysEl);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Payload
|
|
337
|
+
if (a.payload) {
|
|
338
|
+
const pLabel = document.createElement('div');
|
|
339
|
+
pLabel.className = 'redux-section-title';
|
|
340
|
+
pLabel.textContent = 'Action Payload';
|
|
341
|
+
detail.appendChild(pLabel);
|
|
342
|
+
detail.appendChild(createTreeNode(null, a.payload, false));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Store changes — two-column layout: Previous | Current
|
|
346
|
+
const prevS = a.index > 0 ? states[a.index - 1] : null;
|
|
347
|
+
const currS = states[a.index];
|
|
348
|
+
if (currS && typeof currS === 'object' && a.changedKeys?.length > 0) {
|
|
349
|
+
a.changedKeys.forEach(key => {
|
|
350
|
+
const keyWrap = document.createElement('div');
|
|
351
|
+
keyWrap.className = 'rdx-store-diff';
|
|
352
|
+
|
|
353
|
+
const kLabel = document.createElement('div');
|
|
354
|
+
kLabel.className = 'rdx-store-key-label';
|
|
355
|
+
kLabel.textContent = key;
|
|
356
|
+
keyWrap.appendChild(kLabel);
|
|
357
|
+
|
|
358
|
+
const oldVal = prevS ? prevS[key] : undefined;
|
|
359
|
+
const newVal = currS[key];
|
|
360
|
+
|
|
361
|
+
// Find which sub-keys changed (for highlighting)
|
|
362
|
+
const changedPaths = new Set();
|
|
363
|
+
_findLeafChanges(oldVal, newVal, '').forEach(c => changedPaths.add(c.path));
|
|
364
|
+
|
|
365
|
+
// Two-column grid: Previous | Current
|
|
366
|
+
const grid = document.createElement('div');
|
|
367
|
+
grid.className = 'rdx-diff-grid';
|
|
368
|
+
|
|
369
|
+
// Previous column
|
|
370
|
+
const prevCol = document.createElement('div');
|
|
371
|
+
prevCol.className = 'rdx-diff-col prev';
|
|
372
|
+
const prevLabel = document.createElement('div');
|
|
373
|
+
prevLabel.className = 'rdx-state-label prev';
|
|
374
|
+
prevLabel.textContent = '- Previous';
|
|
375
|
+
prevCol.appendChild(prevLabel);
|
|
376
|
+
if (oldVal !== undefined) {
|
|
377
|
+
prevCol.appendChild(_createHighlightedTree(null, oldVal, changedPaths, '', true));
|
|
378
|
+
} else {
|
|
379
|
+
const na = document.createElement('span');
|
|
380
|
+
na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
|
|
381
|
+
na.textContent = 'undefined';
|
|
382
|
+
prevCol.appendChild(na);
|
|
383
|
+
}
|
|
384
|
+
grid.appendChild(prevCol);
|
|
385
|
+
|
|
386
|
+
// Current column
|
|
387
|
+
const currCol = document.createElement('div');
|
|
388
|
+
currCol.className = 'rdx-diff-col curr';
|
|
389
|
+
const currLabel = document.createElement('div');
|
|
390
|
+
currLabel.className = 'rdx-state-label curr';
|
|
391
|
+
currLabel.textContent = '+ Current';
|
|
392
|
+
currCol.appendChild(currLabel);
|
|
393
|
+
if (newVal !== undefined) {
|
|
394
|
+
currCol.appendChild(_createHighlightedTree(null, newVal, changedPaths, '', false));
|
|
395
|
+
} else {
|
|
396
|
+
const na = document.createElement('span');
|
|
397
|
+
na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
|
|
398
|
+
na.textContent = 'undefined';
|
|
399
|
+
currCol.appendChild(na);
|
|
400
|
+
}
|
|
401
|
+
grid.appendChild(currCol);
|
|
402
|
+
|
|
403
|
+
// Right-click to copy on each column
|
|
404
|
+
prevCol.addEventListener('contextmenu', (e) => {
|
|
405
|
+
e.preventDefault(); e.stopPropagation();
|
|
406
|
+
showContextMenu(e, [
|
|
407
|
+
{ label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
|
|
408
|
+
{ label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
|
|
409
|
+
{ label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
|
|
410
|
+
]);
|
|
411
|
+
});
|
|
412
|
+
currCol.addEventListener('contextmenu', (e) => {
|
|
413
|
+
e.preventDefault(); e.stopPropagation();
|
|
414
|
+
showContextMenu(e, [
|
|
415
|
+
{ label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
|
|
416
|
+
{ label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
|
|
417
|
+
{ label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
|
|
418
|
+
]);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
keyWrap.appendChild(grid);
|
|
422
|
+
detail.appendChild(keyWrap);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
entry.appendChild(detail);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
frag.appendChild(entry);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
content.appendChild(frag);
|
|
433
|
+
// Scroll selected entry into view
|
|
434
|
+
const selEl = content.querySelector('.rdx-entry.selected');
|
|
435
|
+
if (selEl) {
|
|
436
|
+
selEl.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
|
437
|
+
}
|
|
438
|
+
}
|