zero-query 0.6.3 → 0.8.6

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 (72) hide show
  1. package/README.md +39 -29
  2. package/cli/commands/build.js +113 -4
  3. package/cli/commands/bundle.js +392 -29
  4. package/cli/commands/create.js +1 -1
  5. package/cli/commands/dev/devtools/index.js +56 -0
  6. package/cli/commands/dev/devtools/js/components.js +49 -0
  7. package/cli/commands/dev/devtools/js/core.js +409 -0
  8. package/cli/commands/dev/devtools/js/elements.js +413 -0
  9. package/cli/commands/dev/devtools/js/network.js +166 -0
  10. package/cli/commands/dev/devtools/js/performance.js +73 -0
  11. package/cli/commands/dev/devtools/js/router.js +105 -0
  12. package/cli/commands/dev/devtools/js/source.js +132 -0
  13. package/cli/commands/dev/devtools/js/stats.js +35 -0
  14. package/cli/commands/dev/devtools/js/tabs.js +79 -0
  15. package/cli/commands/dev/devtools/panel.html +95 -0
  16. package/cli/commands/dev/devtools/styles.css +244 -0
  17. package/cli/commands/dev/index.js +29 -4
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +428 -2
  20. package/cli/commands/dev/server.js +42 -5
  21. package/cli/commands/dev/watcher.js +59 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +16 -23
  24. package/cli/scaffold/{scripts → app}/components/about.js +4 -4
  25. package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
  26. package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
  27. package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
  28. package/cli/scaffold/app/components/home.js +137 -0
  29. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  30. package/cli/scaffold/{scripts → app}/store.js +6 -6
  31. package/cli/scaffold/assets/.gitkeep +0 -0
  32. package/cli/scaffold/{styles/styles.css → global.css} +4 -2
  33. package/cli/scaffold/index.html +12 -11
  34. package/cli/utils.js +111 -6
  35. package/dist/zquery.dist.zip +0 -0
  36. package/dist/zquery.js +1122 -158
  37. package/dist/zquery.min.js +3 -16
  38. package/index.d.ts +129 -1290
  39. package/index.js +15 -10
  40. package/package.json +7 -6
  41. package/src/component.js +172 -49
  42. package/src/core.js +359 -18
  43. package/src/diff.js +256 -58
  44. package/src/expression.js +33 -3
  45. package/src/reactive.js +37 -5
  46. package/src/router.js +243 -7
  47. package/tests/component.test.js +886 -0
  48. package/tests/core.test.js +977 -0
  49. package/tests/diff.test.js +525 -0
  50. package/tests/errors.test.js +162 -0
  51. package/tests/expression.test.js +482 -0
  52. package/tests/http.test.js +289 -0
  53. package/tests/reactive.test.js +339 -0
  54. package/tests/router.test.js +649 -0
  55. package/tests/store.test.js +379 -0
  56. package/tests/utils.test.js +512 -0
  57. package/types/collection.d.ts +383 -0
  58. package/types/component.d.ts +217 -0
  59. package/types/errors.d.ts +103 -0
  60. package/types/http.d.ts +81 -0
  61. package/types/misc.d.ts +179 -0
  62. package/types/reactive.d.ts +76 -0
  63. package/types/router.d.ts +161 -0
  64. package/types/ssr.d.ts +49 -0
  65. package/types/store.d.ts +107 -0
  66. package/types/utils.d.ts +142 -0
  67. package/cli/commands/dev.old.js +0 -520
  68. package/cli/scaffold/scripts/components/home.js +0 -137
  69. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  70. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  71. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  72. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
@@ -0,0 +1,413 @@
1
+ // ===================================================================
2
+ // DOM Tree
3
+ // ===================================================================
4
+ function getNodePath(node) {
5
+ var parts = [];
6
+ var cur = node;
7
+ while (cur && cur.nodeType === 1) {
8
+ var tag = cur.tagName.toLowerCase();
9
+ var idx = 0;
10
+ var sib = cur.previousElementSibling;
11
+ while (sib) { if (sib.tagName === cur.tagName) idx++; sib = sib.previousElementSibling; }
12
+ parts.unshift(tag + (idx ? ':' + idx : ''));
13
+ cur = cur.parentElement;
14
+ }
15
+ return parts.join('>');
16
+ }
17
+
18
+ function refreshComponentNames() {
19
+ componentNames = {};
20
+ try {
21
+ if (targetWin && targetWin.$ && typeof targetWin.$.components === 'function') {
22
+ var reg = targetWin.$.components();
23
+ var keys = Object.keys(reg);
24
+ for (var i = 0; i < keys.length; i++) componentNames[keys[i].toLowerCase()] = true;
25
+ }
26
+ } catch(e) {}
27
+ }
28
+
29
+ function buildDOMTree() {
30
+ refreshComponentNames();
31
+ var tree = document.getElementById('dom-tree');
32
+ tree.innerHTML = '';
33
+ if (!targetDoc) return;
34
+ var root = targetDoc.documentElement;
35
+ tree.appendChild(buildNode(root, 0));
36
+ }
37
+
38
+ function buildNode(node, depth) {
39
+ var wrap = document.createElement('div');
40
+ wrap.className = 'tree-node';
41
+
42
+ if (node.nodeType === 3) {
43
+ var text = node.textContent.trim();
44
+ if (!text) return wrap;
45
+ var row = document.createElement('div');
46
+ row.className = 'tree-row';
47
+ var truncated = text.length > 60;
48
+ row.innerHTML = '<span class="tree-toggle leaf"></span><span class="tree-text">' +
49
+ esc(truncated ? text.slice(0, 60) + '...' : text) + '</span>' +
50
+ (truncated ? '<span class="tree-peek" title="View full text">&#x1f441;</span>' : '');
51
+ if (truncated) {
52
+ row.__sourceText = text;
53
+ row.__sourceLabel = 'Text content';
54
+ }
55
+ wrap.appendChild(row);
56
+ return wrap;
57
+ }
58
+ if (node.nodeType === 8) {
59
+ var row = document.createElement('div');
60
+ row.className = 'tree-row';
61
+ row.innerHTML = '<span class="tree-toggle leaf"></span><span class="tree-comment">&lt;!-- ' +
62
+ esc(node.textContent.trim().slice(0, 40)) + ' --&gt;</span>';
63
+ wrap.appendChild(row);
64
+ return wrap;
65
+ }
66
+ if (node.nodeType !== 1) return wrap;
67
+
68
+ // Skip devtools-injected overlay
69
+ if (node.id === '__zq_error_overlay' || node.id === '__zq_devbar') return wrap;
70
+
71
+ var tag = node.tagName.toLowerCase();
72
+ var nodePath = getNodePath(node);
73
+ var hasChildren = false;
74
+ var childNodes = node.childNodes;
75
+ // style/script content is shown inline via the peek button — treat as leaf
76
+ if (tag !== 'style' && tag !== 'script') {
77
+ for (var i = 0; i < childNodes.length; i++) {
78
+ var cn = childNodes[i];
79
+ if (cn.nodeType === 1 || (cn.nodeType === 3 && cn.textContent.trim())) { hasChildren = true; break; }
80
+ }
81
+ }
82
+
83
+ // Detect special nodes
84
+ var isScopedStyle = tag === 'style' && node.hasAttribute('data-zq-scope');
85
+ var isComponent = !isScopedStyle && (componentNames[tag] || node.hasAttribute('data-zq-component'));
86
+ var isRouterView = tag === 'div' && (node.id === 'app' || node.hasAttribute('data-zq-router'));
87
+ var isEntryPoint = tag === 'body' || isRouterView;
88
+
89
+ // Build row
90
+ var row = document.createElement('div');
91
+ row.className = 'tree-row';
92
+ row.__targetNode = node;
93
+
94
+ var toggleCls = hasChildren ? 'tree-toggle' : 'tree-toggle leaf';
95
+ var html = '<span class="' + toggleCls + '">&#9654;</span>';
96
+ html += '<span class="tree-tag">&lt;' + tag + '</span>';
97
+
98
+ // Show key attributes inline (skip internal zq attrs on scoped styles)
99
+ var attrs = node.attributes;
100
+ var shown = 0;
101
+ for (var i = 0; i < attrs.length && shown < 4; i++) {
102
+ var a = attrs[i];
103
+ if (a.name === 'style' || a.name === 'class' && a.value.length > 40) continue;
104
+ if (isScopedStyle && /^data-zq-/.test(a.name)) continue;
105
+ html += ' <span class="tree-attr-name">' + esc(a.name) + '</span>';
106
+ if (a.value) html += '=<span class="tree-attr-val">&quot;' + esc(a.value.length > 30 ? a.value.slice(0, 30) + '...' : a.value) + '&quot;</span>';
107
+ shown++;
108
+ }
109
+ html += '<span class="tree-tag">&gt;</span>';
110
+
111
+ // Badges for components, router, entry-point, scoped css
112
+ if (isScopedStyle) {
113
+ var scopeComp = node.getAttribute('data-zq-component');
114
+ html += '<span class="tree-badge scoped">scoped css' + (scopeComp ? ' · ' + esc(scopeComp) : '') + '</span>';
115
+ } else if (isComponent) html += '<span class="tree-badge comp">component</span>';
116
+ if (isRouterView) html += '<span class="tree-badge router">router</span>';
117
+ else if (isEntryPoint) html += '<span class="tree-badge entry">entry</span>';
118
+
119
+ // Expand / collapse action buttons (visible on hover)
120
+ if (hasChildren) {
121
+ html += '<span class="tree-actions">';
122
+ html += '<button class="tree-action" data-act="expand" title="Expand all">&#43;</button>';
123
+ html += '<button class="tree-action" data-act="collapse" title="Collapse all">&#8722;</button>';
124
+ html += '</span>';
125
+ }
126
+
127
+ // Inline text for leaf elements; add peek button for style/script with any content, or long text
128
+ var hasInlineContent = false;
129
+ if (tag === 'style' || tag === 'script') {
130
+ var fullText = node.textContent.trim();
131
+ if (fullText) {
132
+ var preview = fullText.length > 50 ? fullText.slice(0, 50) + '...' : fullText;
133
+ html += '<span class="tree-text">' + esc(preview) + '</span>';
134
+ html += '<span class="tree-peek" title="View full source">&#x1f441;</span>';
135
+ hasInlineContent = true;
136
+ }
137
+ } else if (!hasChildren || (childNodes.length === 1 && childNodes[0].nodeType === 3)) {
138
+ var text = node.textContent.trim();
139
+ if (text && text.length < 60) {
140
+ html += '<span class="tree-text">' + esc(text) + '</span>';
141
+ html += '<span class="tree-tag">&lt;/' + tag + '&gt;</span>';
142
+ } else if (text && text.length >= 60) {
143
+ html += '<span class="tree-text">' + esc(text.slice(0, 50) + '...') + '</span>';
144
+ html += '<span class="tree-peek" title="View full text">&#x1f441;</span>';
145
+ hasInlineContent = true;
146
+ }
147
+ }
148
+
149
+ row.innerHTML = html;
150
+ // Attach source data for peek overlay
151
+ if (hasInlineContent) {
152
+ row.__sourceText = node.textContent.trim();
153
+ row.__sourceIsCSS = (tag === 'style');
154
+ if (tag === 'style') {
155
+ var scopeComp = node.getAttribute('data-zq-component');
156
+ row.__sourceLabel = '<span class="tree-tag">&lt;style&gt;</span>' + (scopeComp ? ' <span class="tree-badge scoped">scoped css \u00b7 ' + esc(scopeComp) + '</span>' : '');
157
+ } else if (tag === 'script') {
158
+ row.__sourceLabel = '<span class="tree-tag">&lt;script&gt;</span>';
159
+ } else {
160
+ row.__sourceLabel = '<span class="tree-tag">&lt;' + tag + '&gt;</span> text content';
161
+ }
162
+ }
163
+ wrap.appendChild(row);
164
+
165
+ // Children container
166
+ if (hasChildren) {
167
+ var childDiv = document.createElement('div');
168
+ childDiv.className = 'tree-children';
169
+
170
+ // Restore expanded state or auto-expand; entry/router always expand, changed paths auto-expand (capped at depth 5)
171
+ var alwaysOpen = isEntryPoint || isRouterView;
172
+ var changedNow = changedPaths[nodePath] && depth < 6;
173
+ var isExpanded = alwaysOpen || (expandedPaths.hasOwnProperty(nodePath) ? expandedPaths[nodePath] : (depth < 3 || changedNow));
174
+ if (isExpanded) {
175
+ childDiv.classList.add('open');
176
+ row.querySelector('.tree-toggle').classList.add('open');
177
+ }
178
+
179
+ // Flash only actual mutation targets, not ancestors
180
+ if (mutatedPaths[nodePath]) {
181
+ row.classList.add('morph-changed');
182
+ row.setAttribute('data-changed', '1');
183
+ }
184
+
185
+ for (var i = 0; i < childNodes.length; i++) {
186
+ var child = buildNode(childNodes[i], depth + 1);
187
+ if (child.innerHTML) childDiv.appendChild(child);
188
+ }
189
+ wrap.appendChild(childDiv);
190
+ }
191
+
192
+ // Toggle click (on the arrow only)
193
+ var toggleEl = row.querySelector('.tree-toggle');
194
+ if (hasChildren) {
195
+ toggleEl.addEventListener('click', function(e) {
196
+ e.stopPropagation();
197
+ var children = row.nextElementSibling;
198
+ if (children && children.classList.contains('tree-children')) {
199
+ var opening = !children.classList.contains('open');
200
+ children.classList.toggle('open');
201
+ toggleEl.classList.toggle('open');
202
+ expandedPaths[nodePath] = opening;
203
+ }
204
+ });
205
+ }
206
+
207
+ // Expand / collapse action buttons
208
+ row.addEventListener('click', function(e) {
209
+ var actBtn = e.target.closest('.tree-action');
210
+ if (!actBtn) return;
211
+ e.stopPropagation();
212
+ var childContainer = row.nextElementSibling;
213
+ if (!childContainer || !childContainer.classList.contains('tree-children')) return;
214
+ var opening = actBtn.dataset.act === 'expand';
215
+ if (opening) { childContainer.classList.add('open'); toggleEl.classList.add('open'); }
216
+ else { childContainer.classList.remove('open'); toggleEl.classList.remove('open'); }
217
+ expandedPaths[nodePath] = opening;
218
+ var maxDepth = opening ? 4 : Infinity;
219
+ function toggleNested(container, level) {
220
+ if (level >= maxDepth) return;
221
+ var children = container.children;
222
+ for (var j = 0; j < children.length; j++) {
223
+ var nested = children[j].querySelector(':scope > .tree-children');
224
+ if (nested) {
225
+ if (opening) nested.classList.add('open');
226
+ else nested.classList.remove('open');
227
+ var arrow = nested.previousElementSibling ? nested.previousElementSibling.querySelector('.tree-toggle') : null;
228
+ if (arrow) { if (opening) arrow.classList.add('open'); else arrow.classList.remove('open'); }
229
+ toggleNested(nested, level + 1);
230
+ }
231
+ }
232
+ }
233
+ toggleNested(childContainer, 0);
234
+ });
235
+
236
+ // Row click — select element (not toggle)
237
+ row.addEventListener('click', function(e) {
238
+ // Don't select when clicking toggle arrow, badge, action buttons, or peek
239
+ if (e.target.closest('.tree-toggle') || e.target.closest('.tree-badge') || e.target.closest('.tree-action') || e.target.closest('.tree-peek')) return;
240
+ document.querySelectorAll('.tree-row.selected').forEach(function(r) { r.classList.remove('selected'); });
241
+ row.classList.add('selected');
242
+ selectedEl = node;
243
+ showDetail(node);
244
+ try { highlightElement(node); } catch(err) {}
245
+ });
246
+
247
+ // Double-click to expand/collapse
248
+ row.addEventListener('dblclick', function(e) {
249
+ if (!hasChildren) return;
250
+ e.preventDefault();
251
+ var children = row.nextElementSibling;
252
+ if (children && children.classList.contains('tree-children')) {
253
+ var opening = !children.classList.contains('open');
254
+ children.classList.toggle('open');
255
+ toggleEl.classList.toggle('open');
256
+ expandedPaths[nodePath] = opening;
257
+ }
258
+ });
259
+
260
+ return wrap;
261
+ }
262
+
263
+ // ===================================================================
264
+ // Element highlighting
265
+ // ===================================================================
266
+ function highlightElement(el) {
267
+ if (!targetWin || !targetDoc) return;
268
+ // Remove previous highlight
269
+ var prev = targetDoc.getElementById('__zq_highlight');
270
+ if (prev) prev.remove();
271
+
272
+ var rect = el.getBoundingClientRect();
273
+ if (!rect.width && !rect.height) return;
274
+
275
+ var box = targetDoc.createElement('div');
276
+ box.id = '__zq_highlight';
277
+ box.style.cssText = 'position:fixed;z-index:2147483645;pointer-events:none;' +
278
+ 'border:2px solid rgba(88,166,255,0.8);background:rgba(88,166,255,0.1);' +
279
+ 'transition:all .15s ease;' +
280
+ 'top:' + rect.top + 'px;left:' + rect.left + 'px;' +
281
+ 'width:' + rect.width + 'px;height:' + rect.height + 'px;';
282
+ targetDoc.body.appendChild(box);
283
+ setTimeout(function() { if (box.parentNode) box.remove(); }, 2000);
284
+ }
285
+
286
+ // ===================================================================
287
+ // Detail panel
288
+ // ===================================================================
289
+ function showDetail(node) {
290
+ var detail = document.getElementById('dom-detail');
291
+ detail.style.display = 'block';
292
+ var html = '<button class="detail-close" id="detail-close" title="Close">&times;</button>';
293
+
294
+ // Tag and ID
295
+ html += '<div class="detail-section"><h4>Element</h4>';
296
+ html += '<div class="detail-row"><span class="detail-key">Tag</span><span class="detail-val">' + node.tagName.toLowerCase() + '</span></div>';
297
+ if (node.id) html += '<div class="detail-row"><span class="detail-key">ID</span><span class="detail-val">' + esc(node.id) + '</span></div>';
298
+ if (node.className) html += '<div class="detail-row"><span class="detail-key">Classes</span><span class="detail-val">' + esc(node.className) + '</span></div>';
299
+ html += '</div>';
300
+
301
+ // Attributes
302
+ if (node.attributes.length) {
303
+ html += '<div class="detail-section"><h4>Attributes</h4>';
304
+ for (var i = 0; i < node.attributes.length; i++) {
305
+ var a = node.attributes[i];
306
+ var val = esc(a.value);
307
+ if (/^(src|href|action|data-src|poster|srcset)$/i.test(a.name) && a.value) {
308
+ val = '<a href="' + esc(a.value) + '" target="_blank" rel="noopener" title="Open in new tab">' + val + '</a>';
309
+ }
310
+ html += '<div class="detail-row"><span class="detail-key">' + esc(a.name) + '</span><span class="detail-val">' + val + '</span></div>';
311
+ }
312
+ html += '</div>';
313
+ }
314
+
315
+ // Dimensions
316
+ var rect = node.getBoundingClientRect();
317
+ html += '<div class="detail-section"><h4>Box Model</h4>';
318
+ html += '<div class="detail-row"><span class="detail-key">Size</span><span class="detail-val">' + Math.round(rect.width) + ' \u00d7 ' + Math.round(rect.height) + '</span></div>';
319
+ html += '<div class="detail-row"><span class="detail-key">Position</span><span class="detail-val">(' + Math.round(rect.left) + ', ' + Math.round(rect.top) + ')</span></div>';
320
+ html += '</div>';
321
+
322
+ // Component state
323
+ try {
324
+ if (targetWin.$ && targetWin.$.getInstance) {
325
+ var inst = targetWin.$.getInstance(node);
326
+ if (inst && inst.state) {
327
+ html += '<div class="detail-section"><h4>Component State</h4>';
328
+ var stateKeys = Object.keys(inst.state);
329
+ for (var i = 0; i < stateKeys.length; i++) {
330
+ var k = stateKeys[i];
331
+ var v = inst.state[k];
332
+ var display = typeof v === 'object' ? JSON.stringify(v, null, 1) : String(v);
333
+ html += '<div class="detail-row"><span class="detail-key">' + esc(k) + '</span><span class="detail-val">' + esc(display) + '</span></div>';
334
+ }
335
+ html += '</div>';
336
+ }
337
+ }
338
+ } catch(err) {}
339
+
340
+ detail.innerHTML = html;
341
+ document.getElementById('detail-close').addEventListener('click', function() {
342
+ detail.style.display = 'none';
343
+ document.querySelectorAll('.tree-row.selected').forEach(function(r) { r.classList.remove('selected'); });
344
+ selectedEl = null;
345
+ });
346
+ }
347
+
348
+ // ===================================================================
349
+ // MutationObserver — watch target document for live DOM changes
350
+ // ===================================================================
351
+ function startObserver() {
352
+ if (!targetDoc || observer) return;
353
+ observer = new MutationObserver(function(mutations) {
354
+ // Collect paths of mutated nodes so we can auto-expand + highlight them
355
+ var dominated = false;
356
+ for (var i = 0; i < mutations.length; i++) {
357
+ var m = mutations[i];
358
+ var target = m.target.nodeType === 1 ? m.target : m.target.parentElement;
359
+ if (!target) continue;
360
+ // Skip mutations caused by the devtools highlight overlay
361
+ if (target.id === '__zq_highlight' || target.id === '__zq_error_overlay' || target.id === '__zq_devbar') continue;
362
+ var isHighlightMutation = false;
363
+ if (m.addedNodes) { for (var k = 0; k < m.addedNodes.length; k++) { if (m.addedNodes[k].id === '__zq_highlight') { isHighlightMutation = true; break; } } }
364
+ if (!isHighlightMutation && m.removedNodes) { for (var k = 0; k < m.removedNodes.length; k++) { if (m.removedNodes[k].id === '__zq_highlight') { isHighlightMutation = true; break; } } }
365
+ if (isHighlightMutation) continue;
366
+ dominated = true;
367
+ var tp = getNodePath(target);
368
+ changedPaths[tp] = true;
369
+ mutatedPaths[tp] = true;
370
+ // Also mark added nodes
371
+ if (m.addedNodes) {
372
+ for (var j = 0; j < m.addedNodes.length; j++) {
373
+ if (m.addedNodes[j].nodeType === 1) {
374
+ var ap = getNodePath(m.addedNodes[j]);
375
+ changedPaths[ap] = true;
376
+ mutatedPaths[ap] = true;
377
+ }
378
+ }
379
+ }
380
+ // Mark ancestor paths so they auto-expand to reveal changes
381
+ var cur = target.parentElement;
382
+ while (cur && cur.nodeType === 1) {
383
+ var p = getNodePath(cur);
384
+ if (!expandedPaths.hasOwnProperty(p)) changedPaths[p] = true;
385
+ cur = cur.parentElement;
386
+ }
387
+ }
388
+
389
+ if (!dominated) return; // All mutations were from devtools highlight — skip rebuild
390
+
391
+ // Debounce tree rebuild
392
+ clearTimeout(startObserver._timer);
393
+ startObserver._timer = setTimeout(function() {
394
+ buildDOMTree();
395
+ updateStats();
396
+
397
+ // Scroll to deepest (last in DOM order) changed row
398
+ var allChanged = document.querySelectorAll('.tree-row[data-changed]');
399
+ if (allChanged.length) {
400
+ allChanged[allChanged.length - 1].scrollIntoView({ block: 'center', behavior: 'smooth' });
401
+ }
402
+ // Clear changed paths after applying
403
+ changedPaths = {};
404
+ mutatedPaths = {};
405
+ }, 150);
406
+ });
407
+ observer.observe(targetDoc.documentElement, {
408
+ childList: true,
409
+ subtree: true,
410
+ attributes: true,
411
+ characterData: true
412
+ });
413
+ }
@@ -0,0 +1,166 @@
1
+ // ===================================================================
2
+ // Network log
3
+ // ===================================================================
4
+ function renderNetwork() {
5
+ var tbody = document.getElementById('net-body');
6
+ var html = '';
7
+ for (var i = requests.length - 1; i >= 0; i--) {
8
+ var r = requests[i];
9
+ var statusCls = r.status < 300 ? 'ok' : r.status < 400 ? 'redirect' : 'err';
10
+ var preview = '';
11
+ if (r.bodyPreview) {
12
+ try { preview = JSON.stringify(JSON.parse(r.bodyPreview), null, 0); } catch(e) { preview = r.bodyPreview; }
13
+ if (preview.length > 120) preview = preview.slice(0, 120) + '...';
14
+ }
15
+ html += '<tr data-idx="' + i + '">';
16
+ html += '<td><span class="net-method ' + r.method + '">' + r.method + '</span></td>';
17
+ html += '<td><span class="net-status ' + statusCls + '">' + r.status + '</span></td>';
18
+ html += '<td class="net-url" title="' + esc(r.url) + '">' + esc(r.url) + '</td>';
19
+ html += '<td class="net-time">' + r.elapsed + 'ms</td>';
20
+ html += '<td class="timestamp">' + formatTime(r.timestamp) + '</td>';
21
+ html += '<td style="color:var(--text2);font-size:10px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(preview) + '</td>';
22
+ html += '</tr>';
23
+ }
24
+ if (!html) html = '<tr><td colspan="6" style="text-align:center;padding:24px;color:var(--text2)">No requests yet</td></tr>';
25
+ tbody.innerHTML = html;
26
+ }
27
+
28
+ // Click to expand/collapse (attached once, outside renderNetwork)
29
+ document.getElementById('net-body').addEventListener('click', function(e) {
30
+ var tr = e.target.closest('tr');
31
+ if (!tr || !tr.dataset.idx) return;
32
+ var idx = parseInt(tr.dataset.idx);
33
+ var existing = tr.nextElementSibling;
34
+ if (existing && existing.classList.contains('net-body-row')) {
35
+ existing.remove();
36
+ tr.classList.remove('expanded');
37
+ return;
38
+ }
39
+ tr.classList.add('expanded');
40
+ var r = requests[idx];
41
+ if (!r) return;
42
+ var detail = document.createElement('tr');
43
+ detail.className = 'net-body-row';
44
+ var td = document.createElement('td');
45
+ td.colSpan = 6;
46
+ var body = r.bodyPreview || '';
47
+ var parsed = null;
48
+ try { parsed = JSON.parse(body); } catch(e2) {}
49
+ if (parsed !== null && typeof parsed === 'object') {
50
+ var treeWrap = document.createElement('div');
51
+ treeWrap.className = 'json-tree';
52
+ treeWrap.appendChild(buildJsonNode(parsed, true));
53
+ td.appendChild(treeWrap);
54
+ } else {
55
+ var pre = document.createElement('pre');
56
+ pre.style.cssText = 'margin:0;max-height:240px;overflow:auto';
57
+ pre.textContent = body;
58
+ td.appendChild(pre);
59
+ }
60
+ detail.appendChild(td);
61
+ tr.after(detail);
62
+ });
63
+
64
+ function buildJsonNode(val, isRoot) {
65
+ var frag = document.createDocumentFragment();
66
+ if (val === null) {
67
+ var s = document.createElement('span');
68
+ s.className = 'json-null';
69
+ s.textContent = 'null';
70
+ frag.appendChild(s);
71
+ return frag;
72
+ }
73
+ if (typeof val === 'string') {
74
+ var s = document.createElement('span');
75
+ s.className = 'json-str';
76
+ s.textContent = '"' + (val.length > 300 ? val.slice(0, 300) + '...' : val) + '"';
77
+ frag.appendChild(s);
78
+ return frag;
79
+ }
80
+ if (typeof val === 'number') {
81
+ var s = document.createElement('span');
82
+ s.className = 'json-num';
83
+ s.textContent = String(val);
84
+ frag.appendChild(s);
85
+ return frag;
86
+ }
87
+ if (typeof val === 'boolean') {
88
+ var s = document.createElement('span');
89
+ s.className = 'json-bool';
90
+ s.textContent = String(val);
91
+ frag.appendChild(s);
92
+ return frag;
93
+ }
94
+ var isArr = Array.isArray(val);
95
+ var keys = isArr ? val : Object.keys(val);
96
+ var len = isArr ? val.length : keys.length;
97
+ var open = isArr ? '[' : '{';
98
+ var close = isArr ? ']' : '}';
99
+
100
+ // Toggle arrow + opening bracket
101
+ var toggle = document.createElement('span');
102
+ toggle.className = 'json-toggle';
103
+ toggle.textContent = '\u25BE ';
104
+ frag.appendChild(toggle);
105
+
106
+ var bracket = document.createElement('span');
107
+ bracket.className = 'json-bracket';
108
+ bracket.textContent = open;
109
+ frag.appendChild(bracket);
110
+
111
+ var count = document.createElement('span');
112
+ count.className = 'json-count';
113
+ count.textContent = len + (isArr ? ' items' : ' keys');
114
+ count.style.display = 'none';
115
+ frag.appendChild(count);
116
+
117
+ // Children
118
+ var children = document.createElement('div');
119
+ children.className = 'json-children';
120
+ var entries = isArr ? val : keys;
121
+ for (var i = 0; i < entries.length; i++) {
122
+ var line = document.createElement('div');
123
+ if (!isArr) {
124
+ var k = document.createElement('span');
125
+ k.className = 'json-key';
126
+ k.textContent = '"' + entries[i] + '"';
127
+ line.appendChild(k);
128
+ var colon = document.createTextNode(': ');
129
+ line.appendChild(colon);
130
+ line.appendChild(buildJsonNode(val[entries[i]], false));
131
+ } else {
132
+ line.appendChild(buildJsonNode(entries[i], false));
133
+ }
134
+ if (i < entries.length - 1) {
135
+ var comma = document.createElement('span');
136
+ comma.className = 'json-comma';
137
+ comma.textContent = ',';
138
+ line.appendChild(comma);
139
+ }
140
+ children.appendChild(line);
141
+ }
142
+ frag.appendChild(children);
143
+
144
+ var closeBracket = document.createElement('span');
145
+ closeBracket.className = 'json-bracket';
146
+ closeBracket.textContent = close;
147
+ frag.appendChild(closeBracket);
148
+
149
+ // Collapse large nodes by default (except root)
150
+ if (!isRoot && len > 5) {
151
+ children.classList.add('collapsed');
152
+ toggle.textContent = '\u25B8 ';
153
+ count.style.display = '';
154
+ closeBracket.style.display = 'none';
155
+ }
156
+
157
+ toggle.addEventListener('click', function() {
158
+ var collapsed = children.classList.contains('collapsed');
159
+ children.classList.toggle('collapsed');
160
+ toggle.textContent = collapsed ? '\u25BE ' : '\u25B8 ';
161
+ count.style.display = collapsed ? 'none' : '';
162
+ closeBracket.style.display = collapsed ? '' : 'none';
163
+ });
164
+
165
+ return frag;
166
+ }
@@ -0,0 +1,73 @@
1
+ // ===================================================================
2
+ // Performance
3
+ // ===================================================================
4
+ function recordMorphEvent(data) {
5
+ if (!data.timestamp) data.timestamp = Date.now();
6
+ morphEvents.push(data);
7
+ if (morphEvents.length > 200) morphEvents.shift();
8
+ }
9
+
10
+ function renderPerf() {
11
+ var content = document.getElementById('perf-content');
12
+ var html = '';
13
+
14
+ // Summary
15
+ html += '<div class="perf-card">';
16
+ html += '<div class="perf-title">Render Operations</div>';
17
+ html += '<div class="detail-row"><span class="detail-key">Total</span><span class="detail-val">' + morphCount + '</span></div>';
18
+
19
+ if (morphEvents.length) {
20
+ var times = morphEvents.map(function(e) { return e.elapsed || 0; });
21
+ var avg = times.reduce(function(a, b) { return a + b; }, 0) / times.length;
22
+ var max = Math.max.apply(null, times);
23
+ var min = Math.min.apply(null, times);
24
+ html += '<div class="detail-row"><span class="detail-key">Avg time</span><span class="detail-val">' + avg.toFixed(2) + 'ms</span></div>';
25
+ html += '<div class="detail-row"><span class="detail-key">Min / Max</span><span class="detail-val">' + min.toFixed(2) + 'ms / ' + max.toFixed(2) + 'ms</span></div>';
26
+ }
27
+ html += '</div>';
28
+
29
+ // DOM stats
30
+ if (targetDoc) {
31
+ var allEls = targetDoc.querySelectorAll('*').length;
32
+ html += '<div class="perf-card">';
33
+ html += '<div class="perf-title">DOM Statistics</div>';
34
+ html += '<div class="detail-row"><span class="detail-key">Elements</span><span class="detail-val">' + allEls + '</span></div>';
35
+ html += '<div class="detail-row"><span class="detail-key">Tree depth</span><span class="detail-val">' + measureDepth(targetDoc.body) + '</span></div>';
36
+
37
+ // Component count
38
+ if (targetWin.$ && typeof targetWin.$.components === 'function') {
39
+ html += '<div class="detail-row"><span class="detail-key">Components</span><span class="detail-val">' + Object.keys(targetWin.$.components()).length + ' registered</span></div>';
40
+ }
41
+ html += '</div>';
42
+ }
43
+
44
+ // Recent morph events
45
+ if (morphEvents.length) {
46
+ html += '<div class="perf-card">';
47
+ html += '<div class="perf-title">Recent Renders</div>';
48
+ for (var i = morphEvents.length - 1; i >= Math.max(0, morphEvents.length - 20); i--) {
49
+ var ev = morphEvents[i];
50
+ var kind = ev.kind || 'morph';
51
+ var badge = kind === 'route' ? '<span style="color:#f5c542;font-weight:600;margin-right:6px">[route]</span>'
52
+ : kind === 'mount' ? '<span style="color:#66d9ef;font-weight:600;margin-right:6px">[mount]</span>'
53
+ : '<span style="color:#c792ea;font-weight:600;margin-right:6px">[morph]</span>';
54
+ var pct = Math.min(100, (ev.elapsed / 16.67) * 100);
55
+ var color = pct < 50 ? 'var(--green)' : pct < 100 ? 'var(--yellow)' : 'var(--red)';
56
+ html += '<div class="perf-label"><span>' + badge + esc(ev.target || '?') + '</span><span class="timestamp">' + formatTime(ev.timestamp) + '</span><span style="min-width:60px;text-align:right">' + (ev.elapsed || 0).toFixed(2) + 'ms</span></div>';
57
+ html += '<div class="perf-bar"><div class="perf-fill" style="width:' + Math.max(2, pct) + '%;background:' + color + '"></div></div>';
58
+ }
59
+ html += '</div>';
60
+ }
61
+
62
+ content.innerHTML = html;
63
+ }
64
+
65
+ function measureDepth(el) {
66
+ if (!el || !el.children || !el.children.length) return 0;
67
+ var max = 0;
68
+ for (var i = 0; i < el.children.length; i++) {
69
+ var d = measureDepth(el.children[i]);
70
+ if (d > max) max = d;
71
+ }
72
+ return max + 1;
73
+ }