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.
@@ -0,0 +1,282 @@
1
+ // ─── Sources Panel ─────────────────────────────────────────────────────────
2
+ function initSourcesPanel() {
3
+ const panel = $('panel-sources');
4
+ panel.innerHTML = `
5
+ <div class="panel-toolbar">
6
+ <span class="panel-label">Sources</span>
7
+ <div class="ml-auto" style="display:flex;gap:6px">
8
+ <button class="tb-btn" id="btnOpenSourcesExt" title="Open in separate DevTools window">Breakpoints ↗</button>
9
+ </div>
10
+ </div>
11
+ <div class="sources-layout">
12
+ <div class="sources-sidebar" id="sourcesSidebar">
13
+ <div class="panel-toolbar" style="height:32px">
14
+ <input id="sourcesSearch" class="net-search-input" style="width:100%" placeholder="Search files..." />
15
+ </div>
16
+ <div class="scroll-area sources-file-list" id="sourcesFileList">
17
+ <div class="empty-state" id="sourcesEmpty">
18
+ <div class="icon" style="font-size:28px;opacity:.2">&lt;/&gt;</div>
19
+ <div class="label">Waiting for Metro...</div>
20
+ <div class="hint">Source files will load when Metro is running</div>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ <div class="sources-editor" id="sourcesEditor">
25
+ <div class="panel-toolbar" style="height:32px">
26
+ <span id="sourcesFileName" style="font-size:10px;color:var(--accent)"></span>
27
+ <span id="sourcesLineInfo" style="font-size:10px;color:var(--text-dim);margin-left:auto"></span>
28
+ </div>
29
+ <div class="scroll-area sources-code" id="sourcesCode">
30
+ <span style="color:var(--text-dim);padding:20px;display:block">Select a file to view its source</span>
31
+ </div>
32
+ </div>
33
+ </div>`;
34
+
35
+ // Open JS Debugger for breakpoints
36
+ $('btnOpenSourcesExt').addEventListener('click', () => {
37
+ window.electronAPI?.openCDPTarget(null);
38
+ });
39
+
40
+ // Search filter for file tree
41
+ $('sourcesSearch').addEventListener('input', (e) => {
42
+ const term = e.target.value.toLowerCase().trim();
43
+ document.querySelectorAll('#sourcesFileList .src-tree-file').forEach(row => {
44
+ const filepath = row.dataset.file || '';
45
+ const match = !term || filepath.toLowerCase().includes(term);
46
+ row.style.display = match ? '' : 'none';
47
+ });
48
+ // Show/hide folder nodes based on whether they have visible children
49
+ document.querySelectorAll('#sourcesFileList .src-tree-folder').forEach(folder => {
50
+ const visibleFiles = folder.querySelectorAll('.src-tree-file:not([style*="display: none"])');
51
+ folder.style.display = (!term || visibleFiles.length > 0) ? '' : 'none';
52
+ // Auto-expand folders when searching
53
+ if (term && visibleFiles.length > 0) {
54
+ const children = folder.querySelector('.src-tree-children');
55
+ const arrow = folder.querySelector('.src-tree-arrow');
56
+ if (children) children.style.display = 'block';
57
+ if (arrow) { arrow.textContent = '\u25BC'; arrow.classList.add('expanded'); }
58
+ }
59
+ });
60
+ });
61
+
62
+ // Fetch the source map / bundle modules list from Metro
63
+ fetchSourceFileList();
64
+ }
65
+
66
+ async function fetchSourceFileList() {
67
+ if (!window.electronAPI?.getSourceFileList) {
68
+ console.log('[Sources] electronAPI.getSourceFileList not available, retrying...');
69
+ setTimeout(fetchSourceFileList, 5000);
70
+ return;
71
+ }
72
+ try {
73
+ console.log('[Sources] Fetching file list from Metro...');
74
+ const result = await window.electronAPI.getSourceFileList();
75
+ console.log('[Sources] Got result:', result?.files?.length, 'files, root:', result?.root?.slice(-30));
76
+ if (result?.files && result.files.length > 0) {
77
+ state._sourcesRoot = result.root;
78
+ // Limit to 500 files max to avoid DOM overload
79
+ const files = result.files.length > 500 ? result.files.slice(0, 500) : result.files;
80
+ renderSourceFileList(files);
81
+ console.log('[Sources] Rendered', files.length, 'files');
82
+ } else {
83
+ console.log('[Sources] No files, retrying in 5s...');
84
+ setTimeout(fetchSourceFileList, 5000);
85
+ }
86
+ } catch (e) {
87
+ console.log('[Sources] Error:', e?.message || e);
88
+ setTimeout(fetchSourceFileList, 5000);
89
+ }
90
+ }
91
+
92
+ function renderSourceFileList(files) {
93
+ const list = $('sourcesFileList');
94
+ const empty = $('sourcesEmpty');
95
+ if (!list) return;
96
+ if (!files.length) return;
97
+ if (empty) empty.style.display = 'none';
98
+ list.querySelectorAll('.src-tree-node').forEach(e => e.remove());
99
+
100
+ // Build folder tree from file paths
101
+ const tree = {};
102
+ files.forEach(filepath => {
103
+ const parts = filepath.split('/').filter(Boolean);
104
+ let node = tree;
105
+ parts.forEach((part, i) => {
106
+ if (i === parts.length - 1) {
107
+ // File leaf
108
+ node[part] = filepath; // string = file
109
+ } else {
110
+ // Folder
111
+ if (!node[part] || typeof node[part] === 'string') node[part] = {};
112
+ node = node[part];
113
+ }
114
+ });
115
+ });
116
+
117
+ // Render tree recursively
118
+ const frag = document.createDocumentFragment();
119
+
120
+ // Project folders first, node_modules last
121
+ const topKeys = Object.keys(tree).sort((a, b) => {
122
+ if (a === 'node_modules') return 1;
123
+ if (b === 'node_modules') return -1;
124
+ return a.localeCompare(b);
125
+ });
126
+
127
+ topKeys.forEach(key => {
128
+ frag.appendChild(buildSourceTreeNode(key, tree[key], 0));
129
+ });
130
+ list.appendChild(frag);
131
+ }
132
+
133
+ function buildSourceTreeNode(name, value, depth) {
134
+ if (typeof value === 'string') {
135
+ // File leaf
136
+ const row = document.createElement('div');
137
+ row.className = 'src-tree-node src-tree-file';
138
+ row.dataset.file = value;
139
+ row.style.paddingLeft = (12 + depth * 16) + 'px';
140
+ const isNM = value.includes('node_modules');
141
+ const ext = name.split('.').pop();
142
+ const iconColor = ext === 'tsx' || ext === 'ts' ? '#3178c6'
143
+ : ext === 'jsx' || ext === 'js' ? '#f0db4f'
144
+ : ext === 'json' ? '#a0a0a0'
145
+ : ext === 'css' ? '#264de4'
146
+ : 'var(--text-dim)';
147
+ row.innerHTML = `<span class="src-file-icon" style="color:${iconColor}">●</span><span class="src-file-name" style="color:${isNM ? 'var(--text-dim)' : 'var(--text-bright)'}">${esc(name)}</span>`;
148
+ row.addEventListener('click', () => {
149
+ const fileList = $('sourcesFileList');
150
+ fileList.querySelectorAll('.src-tree-file').forEach(el => el.classList.remove('selected'));
151
+ row.classList.add('selected');
152
+ loadSourceFile(value);
153
+ });
154
+ // Search filter support
155
+ const searchInput = $('sourcesSearch');
156
+ if (searchInput && searchInput.value) {
157
+ const term = searchInput.value.toLowerCase();
158
+ if (!name.toLowerCase().includes(term) && !value.toLowerCase().includes(term)) {
159
+ row.style.display = 'none';
160
+ }
161
+ }
162
+ return row;
163
+ }
164
+
165
+ // Folder node
166
+ const container = document.createElement('div');
167
+ container.className = 'src-tree-node src-tree-folder';
168
+
169
+ const header = document.createElement('div');
170
+ header.className = 'src-tree-folder-header';
171
+ header.style.paddingLeft = (8 + depth * 16) + 'px';
172
+
173
+ const arrow = document.createElement('span');
174
+ arrow.className = 'src-tree-arrow';
175
+ arrow.textContent = '\u25B6';
176
+
177
+ const folderName = document.createElement('span');
178
+ folderName.className = 'src-folder-name';
179
+ const isNM = name === 'node_modules';
180
+ folderName.style.color = isNM ? 'var(--text-dim)' : 'var(--text)';
181
+ folderName.textContent = name;
182
+
183
+ header.appendChild(arrow);
184
+ header.appendChild(folderName);
185
+ container.appendChild(header);
186
+
187
+ const children = document.createElement('div');
188
+ children.className = 'src-tree-children';
189
+ // Start all folders collapsed
190
+ children.style.display = 'none';
191
+
192
+ // Sort: folders first, then files
193
+ const entries = Object.entries(value).sort((a, b) => {
194
+ const aIsFolder = typeof a[1] === 'object';
195
+ const bIsFolder = typeof b[1] === 'object';
196
+ if (aIsFolder !== bIsFolder) return aIsFolder ? -1 : 1;
197
+ return a[0].localeCompare(b[0]);
198
+ });
199
+
200
+ let populated = false;
201
+ function populate() {
202
+ if (populated) return;
203
+ populated = true;
204
+ entries.forEach(([childName, childValue]) => {
205
+ children.appendChild(buildSourceTreeNode(childName, childValue, depth + 1));
206
+ });
207
+ }
208
+
209
+ // Folders start collapsed — populate lazily on first expand
210
+ header.addEventListener('click', () => {
211
+ const isOpen = children.style.display !== 'none';
212
+ if (!isOpen) {
213
+ populate();
214
+ children.style.display = 'block';
215
+ arrow.textContent = '\u25BC';
216
+ arrow.classList.add('expanded');
217
+ } else {
218
+ children.style.display = 'none';
219
+ arrow.textContent = '\u25B6';
220
+ arrow.classList.remove('expanded');
221
+ }
222
+ });
223
+
224
+ container.appendChild(children);
225
+ return container;
226
+ }
227
+
228
+ async function loadSourceFile(filepath) {
229
+ const codeEl = $('sourcesCode');
230
+ const nameEl = $('sourcesFileName');
231
+ const lineEl = $('sourcesLineInfo');
232
+ if (!codeEl) return;
233
+ if (nameEl) nameEl.textContent = filepath.split('/').pop();
234
+ if (lineEl) lineEl.textContent = filepath;
235
+ codeEl.innerHTML = '<span style="color:var(--text-dim)">Loading...</span>';
236
+
237
+ let source = null;
238
+ const root = state._sourcesRoot || '';
239
+ const fullPath = root ? `${root}/${filepath}` : filepath;
240
+
241
+ // Strategy 1: Read from disk via IPC (most reliable)
242
+ if (window.electronAPI?.readSourceFile) {
243
+ source = await window.electronAPI.readSourceFile(fullPath);
244
+ }
245
+
246
+ // Strategy 2: Fetch from Metro
247
+ if (!source) {
248
+ try {
249
+ const port = getStoredMetroPort();
250
+ const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
251
+ if (resp.ok) source = await resp.text();
252
+ } catch {}
253
+ }
254
+
255
+ if (!source) {
256
+ codeEl.innerHTML = `<span style="color:var(--text-dim);padding:20px;display:block">Could not load: ${esc(filepath)}</span>`;
257
+ return;
258
+ }
259
+
260
+ // Render with line numbers
261
+ const lines = source.split('\n');
262
+ if (lineEl) lineEl.textContent = `${filepath} (${lines.length} lines)`;
263
+ codeEl.innerHTML = '';
264
+ const pre = document.createElement('pre');
265
+ pre.className = 'source-pre';
266
+ lines.forEach((line, i) => {
267
+ const lineDiv = document.createElement('div');
268
+ lineDiv.className = 'source-line';
269
+ lineDiv.innerHTML = `<span class="source-line-num">${i + 1}</span><span class="source-line-code">${syntaxHighlight(esc(line))}</span>`;
270
+ pre.appendChild(lineDiv);
271
+ });
272
+ codeEl.appendChild(pre);
273
+ }
274
+
275
+ // Called from cdp-targets IPC handler (no longer opens external window)
276
+
277
+ // Called from cdp-targets IPC handler (shared, no duplicate registration)
278
+ // Sources panel uses Metro source map for file tree — CDP targets are only
279
+ // used for the "Breakpoints" button, not for the file list.
280
+ function updateSourcesPanel(targets) {
281
+ // No-op: file list is populated by fetchSourceFileList from Metro source map
282
+ }
@@ -0,0 +1,185 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // ASYNC STORAGE PANEL — extracted from app.js
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ function initStoragePanel() {
5
+ const panel = $('panel-storage');
6
+ panel.innerHTML = `
7
+ <div class="panel-toolbar">
8
+ <span class="panel-label">AsyncStorage</span>
9
+ <span class="badge" id="sBadge">0</span>
10
+ <div class="ml-auto">
11
+ <input id="storageSearch" class="net-search-input" placeholder="Filter keys..." />
12
+ </div>
13
+ </div>
14
+ <div class="storage-layout" id="storageLayout">
15
+ <div class="storage-keys" id="storageKeysPane">
16
+ <div class="panel-toolbar" style="height:32px">
17
+ <span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Keys</span>
18
+ </div>
19
+ <div class="scroll-area storage-keys-list" id="storageKeyList">
20
+ <div class="empty-state" id="storageEmpty">
21
+ <div class="icon">💾</div>
22
+ <div class="label">No storage data</div>
23
+ <div class="hint">AsyncStorage data will appear here</div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <div class="storage-resize-handle" id="storageResizeHandle"></div>
28
+ <div class="storage-value-view">
29
+ <div class="storage-value-toolbar">
30
+ <span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Value</span>
31
+ <span id="storageSelectedKey" style="font-size:11px;color:var(--accent);margin-left:8px"></span>
32
+ </div>
33
+ <div class="storage-value-body" id="storageValueBody">
34
+ <span style="color:var(--text-dim)">Select a key to view its value</span>
35
+ </div>
36
+ </div>
37
+ </div>`;
38
+
39
+ $('storageSearch').addEventListener('input', (e) => {
40
+ state.storage.searchFilter = e.target.value.toLowerCase().trim();
41
+ renderStorage();
42
+ });
43
+
44
+ // Drag resize handle for key list width
45
+ const handle = $('storageResizeHandle');
46
+ const layout = $('storageLayout');
47
+ const keysPane = $('storageKeysPane');
48
+ if (handle && layout && keysPane) {
49
+ let dragging = false;
50
+ let startX = 0;
51
+ let startW = 0;
52
+ handle.addEventListener('mousedown', (e) => {
53
+ e.preventDefault();
54
+ dragging = true;
55
+ startX = e.clientX;
56
+ startW = keysPane.offsetWidth;
57
+ document.body.style.cursor = 'col-resize';
58
+ document.body.style.userSelect = 'none';
59
+ });
60
+ document.addEventListener('mousemove', (e) => {
61
+ if (!dragging) return;
62
+ const newW = Math.max(120, Math.min(600, startW + (e.clientX - startX)));
63
+ layout.style.gridTemplateColumns = `${newW}px 4px 1fr`;
64
+ });
65
+ document.addEventListener('mouseup', () => {
66
+ if (!dragging) return;
67
+ dragging = false;
68
+ document.body.style.cursor = '';
69
+ document.body.style.userSelect = '';
70
+ });
71
+ }
72
+ }
73
+
74
+ let _storageRAF = null;
75
+
76
+ function handleStorageEvent(event) {
77
+ if (event.type !== 'storage') return;
78
+ if (!isTabEnabled('storage')) return;
79
+ const { key, value, action } = event;
80
+ if (action === 'set' || action === 'snapshot') {
81
+ if (action === 'snapshot' && typeof key === 'object') {
82
+ // Skip if data hasn't changed
83
+ const newKeys = Object.keys(key).slice().sort().join(',');
84
+ const oldKeys = state.storage.keys.slice().sort().join(',');
85
+ if (newKeys === oldKeys) {
86
+ // Check if values changed
87
+ let same = true;
88
+ for (const [k, v] of Object.entries(key)) {
89
+ if (state.storage.entries[k] !== v) { same = false; break; }
90
+ }
91
+ if (same) return; // No changes, skip re-render
92
+ }
93
+ Object.entries(key).forEach(([k, v]) => {
94
+ state.storage.entries[k] = v;
95
+ if (!state.storage.keys.includes(k)) state.storage.keys.push(k);
96
+ });
97
+ } else {
98
+ if (state.storage.entries[key] === value) return; // No change
99
+ state.storage.entries[key] = value;
100
+ if (!state.storage.keys.includes(key)) state.storage.keys.push(key);
101
+ }
102
+ } else if (action === 'remove') {
103
+ if (!(key in state.storage.entries)) return; // Already removed
104
+ delete state.storage.entries[key];
105
+ state.storage.keys = state.storage.keys.filter(k => k !== key);
106
+ if (state.storage.selected === key) state.storage.selected = null;
107
+ }
108
+ $('sBadge').textContent = state.storage.keys.length;
109
+ // Debounce render via rAF
110
+ if (!_storageRAF) {
111
+ _storageRAF = requestAnimationFrame(() => {
112
+ _storageRAF = null;
113
+ renderStorage();
114
+ });
115
+ }
116
+ }
117
+
118
+ function renderStorage() {
119
+ const list = $('storageKeyList');
120
+ const empty = $('storageEmpty');
121
+ if (!list) return;
122
+
123
+ const { searchFilter } = state.storage;
124
+ const visible = state.storage.keys.filter(k =>
125
+ !searchFilter || k.toLowerCase().includes(searchFilter)
126
+ );
127
+
128
+ empty.style.display = visible.length ? 'none' : 'flex';
129
+ list.querySelectorAll('.storage-key-row').forEach(e => e.remove());
130
+
131
+ const frag = document.createDocumentFragment();
132
+ visible.forEach(k => {
133
+ const div = document.createElement('div');
134
+ const val = state.storage.entries[k] || '';
135
+ div.className = 'storage-key-row entry' + (k === state.storage.selected ? ' selected' : '');
136
+ div.innerHTML = `
137
+ <span class="key-name">${highlight(esc(k), searchFilter)}</span>
138
+ <span class="key-size">${formatSize(val.length)}</span>`;
139
+ div.onclick = () => { state.storage.selected = k; renderStorage(); renderStorageValue(); };
140
+ frag.appendChild(div);
141
+ });
142
+ list.appendChild(frag);
143
+ renderStorageValue();
144
+ }
145
+
146
+ function renderStorageValue() {
147
+ let body = $('storageValueBody');
148
+ const keyLabel = $('storageSelectedKey');
149
+ if (!body) return;
150
+ const { selected, entries } = state.storage;
151
+ if (!selected) {
152
+ body.innerHTML = '<span style="color:var(--text-dim)">Select a key</span>';
153
+ if (keyLabel) keyLabel.textContent = '';
154
+ return;
155
+ }
156
+ if (keyLabel) keyLabel.textContent = selected;
157
+ // Clone-replace to remove stale event listeners
158
+ const fresh = body.cloneNode(false);
159
+ body.parentNode.replaceChild(fresh, body);
160
+ body = fresh;
161
+
162
+ let val = entries[selected];
163
+ // Try to parse JSON strings into objects for tree display
164
+ if (typeof val === 'string') {
165
+ try { val = JSON.parse(val); } catch {}
166
+ }
167
+
168
+ if (val && typeof val === 'object') {
169
+ body.appendChild(createTreeNode(null, val, false));
170
+ body.addEventListener('contextmenu', (e) => {
171
+ e.preventDefault();
172
+ showContextMenu(e, [
173
+ { label: 'Copy Value', action: () => navigator.clipboard.writeText(JSON.stringify(val, null, 2)) },
174
+ { label: 'Copy Key', action: () => navigator.clipboard.writeText(selected) },
175
+ ]);
176
+ });
177
+ } else {
178
+ body.innerHTML = renderJSON(val);
179
+ }
180
+ }
181
+
182
+ function formatSize(bytes) {
183
+ if (bytes < 1024) return `${bytes}b`;
184
+ return `${(bytes/1024).toFixed(1)}kb`;
185
+ }