zero-query 1.0.9 → 1.2.0

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 (154) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -0
  5. package/cli/commands/build.js +254 -216
  6. package/cli/commands/bundle.js +1228 -1183
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -167
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +7264 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6252
  81. package/dist/zquery.min.js +8 -601
  82. package/index.d.ts +570 -365
  83. package/index.js +311 -232
  84. package/package.json +76 -69
  85. package/src/component.js +1709 -1454
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -254
  93. package/src/router.js +843 -773
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -272
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1023
  115. package/tests/compare.test.js +497 -0
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -0
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -0
  121. package/tests/electron-features.test.js +864 -0
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -145
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
@@ -1,423 +1,423 @@
1
- // ===================================================================
2
- // Shared state
3
- // ===================================================================
4
- var targetWin = null;
5
- var targetDoc = null;
6
- var channel;
7
- var morphCount = 0;
8
- var requests = [];
9
- var selectedEl = null;
10
- var rootEl = document.getElementById('root');
11
- var iframe = document.getElementById('app-frame');
12
- var mode = 'split-h'; // split-h | split-v | devtools-only | popup
13
- var expandedPaths = {}; // track expanded nodes by path to survive rebuilds
14
- var componentNames = {}; // cache of registered component tag names
15
- var changedPaths = {}; // paths mutated since last rebuild (for auto-expand)
16
- var mutatedPaths = {}; // actual mutation targets (for highlight + scroll)
17
- var observer;
18
- var morphEvents = [];
19
- var routerEvents = [];
20
-
21
- try { channel = new BroadcastChannel('__zq_devtools'); } catch(e) {}
22
-
23
- // ===================================================================
24
- // Utilities
25
- // ===================================================================
26
- function esc(s) {
27
- var d = document.createElement('span');
28
- d.textContent = s;
29
- return d.innerHTML;
30
- }
31
-
32
- function formatTime(ts) {
33
- if (!ts) return '';
34
- var d = new Date(ts);
35
- var h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
36
- var ampm = h >= 12 ? 'PM' : 'AM';
37
- h = h % 12 || 12;
38
- return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0') + ' ' + ampm;
39
- }
40
-
41
- // ===================================================================
42
- // Connection - find target window (opener popup → iframe fallback)
43
- // ===================================================================
44
- function isConnected() {
45
- try { return targetWin && (targetWin === window.opener ? !targetWin.closed : true) && targetWin.document; }
46
- catch(e) { return false; }
47
- }
48
-
49
- function detectMode() {
50
- if (window.opener) {
51
- // Opened as popup - hide iframe, use opener
52
- mode = 'popup';
53
- targetWin = window.opener;
54
- } else {
55
- // Standalone tab - embed app in iframe
56
- mode = 'split-h';
57
- targetWin = null; // will set from iframe.contentWindow
58
- }
59
- rootEl.className = mode;
60
- }
61
-
62
- detectMode();
63
-
64
- // ===================================================================
65
- // Layout toggle: split-h → split-v → devtools-only → split-h
66
- // ===================================================================
67
- var modeToggle = document.getElementById('mode-toggle');
68
-
69
- function setMode(newMode) {
70
- mode = newMode;
71
- rootEl.style.gridTemplateColumns = '';
72
- rootEl.style.gridTemplateRows = '';
73
- rootEl.className = mode;
74
- modeToggle.textContent = mode === 'split-h' ? '\u2b82' : mode === 'split-v' ? '\u2b81' : '\u25a1';
75
- // Reset toolbar drag position
76
- var tb = document.getElementById('divider-toolbar');
77
- if (tb) { tb.style.left = ''; tb.style.top = ''; }
78
- }
79
-
80
- modeToggle.addEventListener('click', function() {
81
- if (mode === 'popup') return;
82
- if (mode === 'split-h') setMode('split-v');
83
- else if (mode === 'split-v') setMode('devtools-only');
84
- else setMode('split-h');
85
- // Reset viewport presets to desktop when switching modes
86
- var vBtns = document.querySelectorAll('.viewport-btn');
87
- vBtns.forEach(function(b) { b.classList.remove('active'); });
88
- var desk = document.querySelector('.viewport-btn[data-width="0"]');
89
- if (desk) desk.classList.add('active');
90
- });
91
-
92
- // ===================================================================
93
- // Divider drag-to-resize
94
- // ===================================================================
95
- var divider = document.getElementById('divider');
96
- var toolbar = document.getElementById('divider-toolbar');
97
-
98
- // --- Toolbar drag (Y-axis in split-h, X-axis in split-v) ------------------
99
- var tbDragging = false;
100
- (function() {
101
- var startPos, startOffset;
102
- toolbar.addEventListener('mousedown', function(e) {
103
- if (e.target.closest('button') || e.target.closest('.route-label')) return;
104
- e.preventDefault();
105
- e.stopPropagation();
106
- tbDragging = true;
107
- toolbar.classList.add('dragging');
108
- iframe.style.pointerEvents = 'none';
109
- document.body.style.userSelect = 'none';
110
-
111
- var isH = mode === 'split-h';
112
- if (isH) {
113
- // Vertical column divider - drag toolbar up/down
114
- startPos = e.clientY;
115
- startOffset = parseInt(toolbar.style.top, 10) || toolbar.offsetTop;
116
- } else {
117
- // Horizontal row divider - drag toolbar left/right
118
- startPos = e.clientX;
119
- startOffset = parseInt(toolbar.style.left, 10) || toolbar.offsetLeft;
120
- }
121
-
122
- function onMove(e2) {
123
- if (isH) {
124
- var delta = e2.clientY - startPos;
125
- var max = divider.offsetHeight - toolbar.offsetHeight - 4;
126
- toolbar.style.top = Math.max(4, Math.min(max, startOffset + delta)) + 'px';
127
- } else {
128
- var delta = e2.clientX - startPos;
129
- var max = divider.offsetWidth - toolbar.offsetWidth - 4;
130
- toolbar.style.left = Math.max(4, Math.min(max, startOffset + delta)) + 'px';
131
- }
132
- }
133
- function onUp() {
134
- tbDragging = false;
135
- toolbar.classList.remove('dragging');
136
- iframe.style.pointerEvents = '';
137
- document.body.style.userSelect = '';
138
- document.removeEventListener('mousemove', onMove);
139
- document.removeEventListener('mouseup', onUp);
140
- }
141
- document.addEventListener('mousemove', onMove);
142
- document.addEventListener('mouseup', onUp);
143
- });
144
- })();
145
-
146
- divider.addEventListener('mousedown', function(e) {
147
- // Don't start drag when clicking toolbar buttons or dragging toolbar
148
- if (e.target.closest('.divider-toolbar')) return;
149
- e.preventDefault();
150
- var startX = e.clientX, startY = e.clientY;
151
- var isH = mode === 'split-h';
152
- var total = isH ? rootEl.offsetWidth : rootEl.offsetHeight;
153
- var startFrac = isH
154
- ? (iframe.offsetWidth / total)
155
- : (iframe.offsetHeight / total);
156
-
157
- // Prevent iframe from stealing mouse events during drag
158
- iframe.style.pointerEvents = 'none';
159
- document.body.style.userSelect = 'none';
160
- document.body.style.cursor = isH ? 'col-resize' : 'row-resize';
161
-
162
- function onMove(e2) {
163
- var delta = isH ? (e2.clientX - startX) / total : (e2.clientY - startY) / total;
164
- var frac = Math.min(0.85, Math.max(0.15, startFrac + delta));
165
- var pct = (frac * 100).toFixed(1) + '%';
166
- var rest = ((1 - frac) * 100).toFixed(1) + '%';
167
- if (isH) rootEl.style.gridTemplateColumns = pct + ' 4px ' + rest;
168
- else rootEl.style.gridTemplateRows = pct + ' 4px ' + rest;
169
- updateViewportActive();
170
- }
171
- function onUp() {
172
- iframe.style.pointerEvents = '';
173
- document.body.style.userSelect = '';
174
- document.body.style.cursor = '';
175
- document.removeEventListener('mousemove', onMove);
176
- document.removeEventListener('mouseup', onUp);
177
- }
178
- document.addEventListener('mousemove', onMove);
179
- document.addEventListener('mouseup', onUp);
180
- });
181
-
182
- // ===================================================================
183
- // Refresh button - reload the embedded iframe
184
- // ===================================================================
185
- document.getElementById('btn-refresh').addEventListener('click', function(e) {
186
- e.stopPropagation();
187
- if (mode === 'popup' && targetWin && !targetWin.closed) {
188
- targetWin.location.reload();
189
- } else if (iframe && iframe.contentWindow) {
190
- iframe.contentWindow.location.reload();
191
- }
192
- });
193
-
194
- // ===================================================================
195
- // Viewport preset buttons - resize browser pane to mobile/tablet/desktop
196
- // ===================================================================
197
- var viewportBtns = document.querySelectorAll('.viewport-btn');
198
-
199
- function updateViewportActive() {
200
- var w = iframe.offsetWidth;
201
- viewportBtns.forEach(function(b) {
202
- var target = parseInt(b.getAttribute('data-width'), 10);
203
- if (target === 0) {
204
- // Desktop is active when width doesn't match any preset
205
- b.classList.toggle('active', w > 780);
206
- } else {
207
- b.classList.toggle('active', Math.abs(w - target) < 20);
208
- }
209
- });
210
- }
211
-
212
- viewportBtns.forEach(function(btn) {
213
- btn.addEventListener('click', function(e) {
214
- e.stopPropagation();
215
- if (mode === 'popup') return;
216
- // Ensure we're in side-by-side mode
217
- if (mode !== 'split-h') setMode('split-h');
218
- var targetWidth = parseInt(this.getAttribute('data-width'), 10);
219
- var total = rootEl.offsetWidth;
220
-
221
- if (targetWidth === 0) {
222
- // Desktop - reset to default CSS proportions
223
- rootEl.style.gridTemplateColumns = '';
224
- } else {
225
- // Mobile/Tablet - set iframe column to exact pixel width
226
- var pct = Math.min(85, Math.max(15, (targetWidth / total) * 100));
227
- rootEl.style.gridTemplateColumns = pct.toFixed(1) + '% 4px 1fr';
228
- }
229
-
230
- viewportBtns.forEach(function(b) { b.classList.remove('active'); });
231
- this.classList.add('active');
232
- });
233
- });
234
-
235
- // ===================================================================
236
- // Route indicator - toggle label showing current path + hash
237
- // ===================================================================
238
- var routeBtn = document.getElementById('btn-route');
239
- var routeLabel = document.getElementById('route-label');
240
-
241
- function getRoutePath() {
242
- try {
243
- var win = (mode === 'popup' && targetWin) ? targetWin : (iframe && iframe.contentWindow);
244
- if (!win) return '/';
245
- var loc = win.location;
246
- return (loc.pathname || '/') + (loc.hash || '');
247
- } catch(e) { return '/'; }
248
- }
249
-
250
- routeBtn.addEventListener('click', function(e) {
251
- e.stopPropagation();
252
- var isOpen = routeLabel.classList.toggle('open');
253
- if (isOpen) {
254
- routeLabel.textContent = getRoutePath();
255
- this.classList.add('active');
256
- } else {
257
- this.classList.remove('active');
258
- }
259
- });
260
-
261
- // Keep label in sync while open
262
- setInterval(function() {
263
- if (routeLabel.classList.contains('open')) {
264
- routeLabel.textContent = getRoutePath();
265
- }
266
- }, 500);
267
-
268
- // ===================================================================
269
- // init - connect to target window (popup or iframe)
270
- // ===================================================================
271
- function init() {
272
- // If popup mode, targetWin is already set
273
- // If iframe mode, wait for iframe to load
274
- if (!isConnected()) {
275
- if (mode !== 'popup') {
276
- // Wait for iframe
277
- iframe.addEventListener('load', function() {
278
- try {
279
- targetWin = iframe.contentWindow;
280
- targetDoc = targetWin.document;
281
- // Inject dark scrollbar styles into the iframe content
282
- try {
283
- var s = targetDoc.createElement('style');
284
- s.textContent = '::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.12);border-radius:2px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.2)}html{scrollbar-color:rgba(255,255,255,0.12) transparent;scrollbar-width:thin}';
285
- targetDoc.head.appendChild(s);
286
- } catch(se) {}
287
- } catch(e) {
288
- document.getElementById('disconnected').style.display = 'flex';
289
- return;
290
- }
291
- document.getElementById('disconnected').style.display = 'none';
292
- connectToTarget();
293
- });
294
- return;
295
- }
296
- document.getElementById('disconnected').style.display = 'flex';
297
- // Retry for popup opener
298
- setTimeout(function() {
299
- targetWin = window.opener;
300
- if (isConnected()) {
301
- document.getElementById('disconnected').style.display = 'none';
302
- init();
303
- }
304
- }, 1000);
305
- return;
306
- }
307
-
308
- targetDoc = targetWin.document;
309
- connectToTarget();
310
- }
311
-
312
- // ===================================================================
313
- // connectToTarget - read existing data, start listeners, periodic sync
314
- // ===================================================================
315
- function connectToTarget() {
316
-
317
- // Read existing requests and render events from opener
318
- if (targetWin.__zqDevTools) {
319
- requests = targetWin.__zqDevTools.requests.slice();
320
- morphCount = targetWin.__zqDevTools.morphCount || 0;
321
- var stored = targetWin.__zqDevTools.morphEvents;
322
- if (stored && stored.length) {
323
- morphEvents = stored.slice();
324
- }
325
- var storedRouter = targetWin.__zqDevTools.routerEvents;
326
- if (storedRouter && storedRouter.length) {
327
- routerEvents = storedRouter.slice();
328
- }
329
- }
330
-
331
- buildDOMTree();
332
- renderNetwork();
333
- renderComponents();
334
- renderPerf();
335
- renderRouter();
336
- startObserver();
337
- updateStats();
338
-
339
- // Listen for BroadcastChannel messages
340
- if (channel) {
341
- channel.onmessage = function(e) {
342
- var msg = e.data;
343
- if (msg.type === 'http') {
344
- requests.push(msg.data);
345
- if (requests.length > 500) requests.shift();
346
- renderNetwork();
347
- updateStats();
348
- } else if (msg.type === 'morph') {
349
- morphCount++;
350
- updateStats();
351
- renderPerf();
352
- } else if (msg.type === 'morph-detail') {
353
- morphCount++;
354
- recordMorphEvent(msg.data);
355
- updateStats();
356
- renderPerf();
357
- } else if (msg.type === 'render-detail') {
358
- morphCount++;
359
- recordMorphEvent({ target: msg.data.target, elapsed: msg.data.elapsed, kind: msg.data.kind, timestamp: msg.data.timestamp });
360
- updateStats();
361
- renderPerf();
362
- } else if (msg.type === 'router') {
363
- routerEvents.push(msg.data);
364
- if (routerEvents.length > 200) routerEvents.shift();
365
- var activeTab = document.querySelector('.tab.active');
366
- if (activeTab && activeTab.dataset.tab === 'router') renderRouter();
367
- updateStats();
368
- }
369
- };
370
- }
371
-
372
- // Periodic refresh for components + perf (fast when tab is visible)
373
- setInterval(function() {
374
- if (!isConnected()) {
375
- // Retry connection - opener may be mid-mutation, not truly gone
376
- try {
377
- if (mode === 'popup' && window.opener && !window.opener.closed) {
378
- targetWin = window.opener;
379
- targetDoc = targetWin.document;
380
- document.getElementById('disconnected').style.display = 'none';
381
- } else if (iframe && iframe.contentWindow) {
382
- targetWin = iframe.contentWindow;
383
- targetDoc = targetWin.document;
384
- document.getElementById('disconnected').style.display = 'none';
385
- }
386
- } catch(e) {}
387
- if (!isConnected()) {
388
- document.getElementById('disconnected').style.display = 'flex';
389
- return;
390
- }
391
- }
392
- // Keep targetDoc fresh - the opener may have reloaded (live-reload)
393
- try {
394
- var freshDoc = targetWin.document;
395
- if (freshDoc !== targetDoc) {
396
- targetDoc = freshDoc;
397
- // Re-attach MutationObserver to the new document
398
- if (observer) { observer.disconnect(); observer = null; }
399
- startObserver();
400
- buildDOMTree();
401
- }
402
- } catch(e) {}
403
- var activeTab = document.querySelector('.tab.active');
404
- var tabName = activeTab ? activeTab.dataset.tab : '';
405
- if (tabName === 'components') renderComponents();
406
- updateStats();
407
- // Sync requests and render events from opener
408
- if (targetWin.__zqDevTools) {
409
- requests = targetWin.__zqDevTools.requests.slice();
410
- morphCount = targetWin.__zqDevTools.morphCount || 0;
411
- var stored = targetWin.__zqDevTools.morphEvents;
412
- if (stored && stored.length) {
413
- morphEvents = stored.slice();
414
- }
415
- var storedRouter = targetWin.__zqDevTools.routerEvents;
416
- if (storedRouter && storedRouter.length) {
417
- routerEvents = storedRouter.slice();
418
- }
419
- }
420
- if (tabName === 'perf') renderPerf();
421
- if (tabName === 'router') renderRouter();
422
- }, 800);
423
- }
1
+ // ===================================================================
2
+ // Shared state
3
+ // ===================================================================
4
+ var targetWin = null;
5
+ var targetDoc = null;
6
+ var channel;
7
+ var morphCount = 0;
8
+ var requests = [];
9
+ var selectedEl = null;
10
+ var rootEl = document.getElementById('root');
11
+ var iframe = document.getElementById('app-frame');
12
+ var mode = 'split-h'; // split-h | split-v | devtools-only | popup
13
+ var expandedPaths = {}; // track expanded nodes by path to survive rebuilds
14
+ var componentNames = {}; // cache of registered component tag names
15
+ var changedPaths = {}; // paths mutated since last rebuild (for auto-expand)
16
+ var mutatedPaths = {}; // actual mutation targets (for highlight + scroll)
17
+ var observer;
18
+ var morphEvents = [];
19
+ var routerEvents = [];
20
+
21
+ try { channel = new BroadcastChannel('__zq_devtools'); } catch(e) {}
22
+
23
+ // ===================================================================
24
+ // Utilities
25
+ // ===================================================================
26
+ function esc(s) {
27
+ var d = document.createElement('span');
28
+ d.textContent = s;
29
+ return d.innerHTML;
30
+ }
31
+
32
+ function formatTime(ts) {
33
+ if (!ts) return '';
34
+ var d = new Date(ts);
35
+ var h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
36
+ var ampm = h >= 12 ? 'PM' : 'AM';
37
+ h = h % 12 || 12;
38
+ return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0') + ' ' + ampm;
39
+ }
40
+
41
+ // ===================================================================
42
+ // Connection - find target window (opener popup → iframe fallback)
43
+ // ===================================================================
44
+ function isConnected() {
45
+ try { return targetWin && (targetWin === window.opener ? !targetWin.closed : true) && targetWin.document; }
46
+ catch(e) { return false; }
47
+ }
48
+
49
+ function detectMode() {
50
+ if (window.opener) {
51
+ // Opened as popup - hide iframe, use opener
52
+ mode = 'popup';
53
+ targetWin = window.opener;
54
+ } else {
55
+ // Standalone tab - embed app in iframe
56
+ mode = 'split-h';
57
+ targetWin = null; // will set from iframe.contentWindow
58
+ }
59
+ rootEl.className = mode;
60
+ }
61
+
62
+ detectMode();
63
+
64
+ // ===================================================================
65
+ // Layout toggle: split-h → split-v → devtools-only → split-h
66
+ // ===================================================================
67
+ var modeToggle = document.getElementById('mode-toggle');
68
+
69
+ function setMode(newMode) {
70
+ mode = newMode;
71
+ rootEl.style.gridTemplateColumns = '';
72
+ rootEl.style.gridTemplateRows = '';
73
+ rootEl.className = mode;
74
+ modeToggle.textContent = mode === 'split-h' ? '\u2b82' : mode === 'split-v' ? '\u2b81' : '\u25a1';
75
+ // Reset toolbar drag position
76
+ var tb = document.getElementById('divider-toolbar');
77
+ if (tb) { tb.style.left = ''; tb.style.top = ''; }
78
+ }
79
+
80
+ modeToggle.addEventListener('click', function() {
81
+ if (mode === 'popup') return;
82
+ if (mode === 'split-h') setMode('split-v');
83
+ else if (mode === 'split-v') setMode('devtools-only');
84
+ else setMode('split-h');
85
+ // Reset viewport presets to desktop when switching modes
86
+ var vBtns = document.querySelectorAll('.viewport-btn');
87
+ vBtns.forEach(function(b) { b.classList.remove('active'); });
88
+ var desk = document.querySelector('.viewport-btn[data-width="0"]');
89
+ if (desk) desk.classList.add('active');
90
+ });
91
+
92
+ // ===================================================================
93
+ // Divider drag-to-resize
94
+ // ===================================================================
95
+ var divider = document.getElementById('divider');
96
+ var toolbar = document.getElementById('divider-toolbar');
97
+
98
+ // --- Toolbar drag (Y-axis in split-h, X-axis in split-v) ------------------
99
+ var tbDragging = false;
100
+ (function() {
101
+ var startPos, startOffset;
102
+ toolbar.addEventListener('mousedown', function(e) {
103
+ if (e.target.closest('button') || e.target.closest('.route-label')) return;
104
+ e.preventDefault();
105
+ e.stopPropagation();
106
+ tbDragging = true;
107
+ toolbar.classList.add('dragging');
108
+ iframe.style.pointerEvents = 'none';
109
+ document.body.style.userSelect = 'none';
110
+
111
+ var isH = mode === 'split-h';
112
+ if (isH) {
113
+ // Vertical column divider - drag toolbar up/down
114
+ startPos = e.clientY;
115
+ startOffset = parseInt(toolbar.style.top, 10) || toolbar.offsetTop;
116
+ } else {
117
+ // Horizontal row divider - drag toolbar left/right
118
+ startPos = e.clientX;
119
+ startOffset = parseInt(toolbar.style.left, 10) || toolbar.offsetLeft;
120
+ }
121
+
122
+ function onMove(e2) {
123
+ if (isH) {
124
+ var delta = e2.clientY - startPos;
125
+ var max = divider.offsetHeight - toolbar.offsetHeight - 4;
126
+ toolbar.style.top = Math.max(4, Math.min(max, startOffset + delta)) + 'px';
127
+ } else {
128
+ var delta = e2.clientX - startPos;
129
+ var max = divider.offsetWidth - toolbar.offsetWidth - 4;
130
+ toolbar.style.left = Math.max(4, Math.min(max, startOffset + delta)) + 'px';
131
+ }
132
+ }
133
+ function onUp() {
134
+ tbDragging = false;
135
+ toolbar.classList.remove('dragging');
136
+ iframe.style.pointerEvents = '';
137
+ document.body.style.userSelect = '';
138
+ document.removeEventListener('mousemove', onMove);
139
+ document.removeEventListener('mouseup', onUp);
140
+ }
141
+ document.addEventListener('mousemove', onMove);
142
+ document.addEventListener('mouseup', onUp);
143
+ });
144
+ })();
145
+
146
+ divider.addEventListener('mousedown', function(e) {
147
+ // Don't start drag when clicking toolbar buttons or dragging toolbar
148
+ if (e.target.closest('.divider-toolbar')) return;
149
+ e.preventDefault();
150
+ var startX = e.clientX, startY = e.clientY;
151
+ var isH = mode === 'split-h';
152
+ var total = isH ? rootEl.offsetWidth : rootEl.offsetHeight;
153
+ var startFrac = isH
154
+ ? (iframe.offsetWidth / total)
155
+ : (iframe.offsetHeight / total);
156
+
157
+ // Prevent iframe from stealing mouse events during drag
158
+ iframe.style.pointerEvents = 'none';
159
+ document.body.style.userSelect = 'none';
160
+ document.body.style.cursor = isH ? 'col-resize' : 'row-resize';
161
+
162
+ function onMove(e2) {
163
+ var delta = isH ? (e2.clientX - startX) / total : (e2.clientY - startY) / total;
164
+ var frac = Math.min(0.85, Math.max(0.15, startFrac + delta));
165
+ var pct = (frac * 100).toFixed(1) + '%';
166
+ var rest = ((1 - frac) * 100).toFixed(1) + '%';
167
+ if (isH) rootEl.style.gridTemplateColumns = pct + ' 4px ' + rest;
168
+ else rootEl.style.gridTemplateRows = pct + ' 4px ' + rest;
169
+ updateViewportActive();
170
+ }
171
+ function onUp() {
172
+ iframe.style.pointerEvents = '';
173
+ document.body.style.userSelect = '';
174
+ document.body.style.cursor = '';
175
+ document.removeEventListener('mousemove', onMove);
176
+ document.removeEventListener('mouseup', onUp);
177
+ }
178
+ document.addEventListener('mousemove', onMove);
179
+ document.addEventListener('mouseup', onUp);
180
+ });
181
+
182
+ // ===================================================================
183
+ // Refresh button - reload the embedded iframe
184
+ // ===================================================================
185
+ document.getElementById('btn-refresh').addEventListener('click', function(e) {
186
+ e.stopPropagation();
187
+ if (mode === 'popup' && targetWin && !targetWin.closed) {
188
+ targetWin.location.reload();
189
+ } else if (iframe && iframe.contentWindow) {
190
+ iframe.contentWindow.location.reload();
191
+ }
192
+ });
193
+
194
+ // ===================================================================
195
+ // Viewport preset buttons - resize browser pane to mobile/tablet/desktop
196
+ // ===================================================================
197
+ var viewportBtns = document.querySelectorAll('.viewport-btn');
198
+
199
+ function updateViewportActive() {
200
+ var w = iframe.offsetWidth;
201
+ viewportBtns.forEach(function(b) {
202
+ var target = parseInt(b.getAttribute('data-width'), 10);
203
+ if (target === 0) {
204
+ // Desktop is active when width doesn't match any preset
205
+ b.classList.toggle('active', w > 780);
206
+ } else {
207
+ b.classList.toggle('active', Math.abs(w - target) < 20);
208
+ }
209
+ });
210
+ }
211
+
212
+ viewportBtns.forEach(function(btn) {
213
+ btn.addEventListener('click', function(e) {
214
+ e.stopPropagation();
215
+ if (mode === 'popup') return;
216
+ // Ensure we're in side-by-side mode
217
+ if (mode !== 'split-h') setMode('split-h');
218
+ var targetWidth = parseInt(this.getAttribute('data-width'), 10);
219
+ var total = rootEl.offsetWidth;
220
+
221
+ if (targetWidth === 0) {
222
+ // Desktop - reset to default CSS proportions
223
+ rootEl.style.gridTemplateColumns = '';
224
+ } else {
225
+ // Mobile/Tablet - set iframe column to exact pixel width
226
+ var pct = Math.min(85, Math.max(15, (targetWidth / total) * 100));
227
+ rootEl.style.gridTemplateColumns = pct.toFixed(1) + '% 4px 1fr';
228
+ }
229
+
230
+ viewportBtns.forEach(function(b) { b.classList.remove('active'); });
231
+ this.classList.add('active');
232
+ });
233
+ });
234
+
235
+ // ===================================================================
236
+ // Route indicator - toggle label showing current path + hash
237
+ // ===================================================================
238
+ var routeBtn = document.getElementById('btn-route');
239
+ var routeLabel = document.getElementById('route-label');
240
+
241
+ function getRoutePath() {
242
+ try {
243
+ var win = (mode === 'popup' && targetWin) ? targetWin : (iframe && iframe.contentWindow);
244
+ if (!win) return '/';
245
+ var loc = win.location;
246
+ return (loc.pathname || '/') + (loc.hash || '');
247
+ } catch(e) { return '/'; }
248
+ }
249
+
250
+ routeBtn.addEventListener('click', function(e) {
251
+ e.stopPropagation();
252
+ var isOpen = routeLabel.classList.toggle('open');
253
+ if (isOpen) {
254
+ routeLabel.textContent = getRoutePath();
255
+ this.classList.add('active');
256
+ } else {
257
+ this.classList.remove('active');
258
+ }
259
+ });
260
+
261
+ // Keep label in sync while open
262
+ setInterval(function() {
263
+ if (routeLabel.classList.contains('open')) {
264
+ routeLabel.textContent = getRoutePath();
265
+ }
266
+ }, 500);
267
+
268
+ // ===================================================================
269
+ // init - connect to target window (popup or iframe)
270
+ // ===================================================================
271
+ function init() {
272
+ // If popup mode, targetWin is already set
273
+ // If iframe mode, wait for iframe to load
274
+ if (!isConnected()) {
275
+ if (mode !== 'popup') {
276
+ // Wait for iframe
277
+ iframe.addEventListener('load', function() {
278
+ try {
279
+ targetWin = iframe.contentWindow;
280
+ targetDoc = targetWin.document;
281
+ // Inject dark scrollbar styles into the iframe content
282
+ try {
283
+ var s = targetDoc.createElement('style');
284
+ s.textContent = '::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.12);border-radius:2px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.2)}html{scrollbar-color:rgba(255,255,255,0.12) transparent;scrollbar-width:thin}';
285
+ targetDoc.head.appendChild(s);
286
+ } catch(se) {}
287
+ } catch(e) {
288
+ document.getElementById('disconnected').style.display = 'flex';
289
+ return;
290
+ }
291
+ document.getElementById('disconnected').style.display = 'none';
292
+ connectToTarget();
293
+ });
294
+ return;
295
+ }
296
+ document.getElementById('disconnected').style.display = 'flex';
297
+ // Retry for popup opener
298
+ setTimeout(function() {
299
+ targetWin = window.opener;
300
+ if (isConnected()) {
301
+ document.getElementById('disconnected').style.display = 'none';
302
+ init();
303
+ }
304
+ }, 1000);
305
+ return;
306
+ }
307
+
308
+ targetDoc = targetWin.document;
309
+ connectToTarget();
310
+ }
311
+
312
+ // ===================================================================
313
+ // connectToTarget - read existing data, start listeners, periodic sync
314
+ // ===================================================================
315
+ function connectToTarget() {
316
+
317
+ // Read existing requests and render events from opener
318
+ if (targetWin.__zqDevTools) {
319
+ requests = targetWin.__zqDevTools.requests.slice();
320
+ morphCount = targetWin.__zqDevTools.morphCount || 0;
321
+ var stored = targetWin.__zqDevTools.morphEvents;
322
+ if (stored && stored.length) {
323
+ morphEvents = stored.slice();
324
+ }
325
+ var storedRouter = targetWin.__zqDevTools.routerEvents;
326
+ if (storedRouter && storedRouter.length) {
327
+ routerEvents = storedRouter.slice();
328
+ }
329
+ }
330
+
331
+ buildDOMTree();
332
+ renderNetwork();
333
+ renderComponents();
334
+ renderPerf();
335
+ renderRouter();
336
+ startObserver();
337
+ updateStats();
338
+
339
+ // Listen for BroadcastChannel messages
340
+ if (channel) {
341
+ channel.onmessage = function(e) {
342
+ var msg = e.data;
343
+ if (msg.type === 'http') {
344
+ requests.push(msg.data);
345
+ if (requests.length > 500) requests.shift();
346
+ renderNetwork();
347
+ updateStats();
348
+ } else if (msg.type === 'morph') {
349
+ morphCount++;
350
+ updateStats();
351
+ renderPerf();
352
+ } else if (msg.type === 'morph-detail') {
353
+ morphCount++;
354
+ recordMorphEvent(msg.data);
355
+ updateStats();
356
+ renderPerf();
357
+ } else if (msg.type === 'render-detail') {
358
+ morphCount++;
359
+ recordMorphEvent({ target: msg.data.target, elapsed: msg.data.elapsed, kind: msg.data.kind, timestamp: msg.data.timestamp });
360
+ updateStats();
361
+ renderPerf();
362
+ } else if (msg.type === 'router') {
363
+ routerEvents.push(msg.data);
364
+ if (routerEvents.length > 200) routerEvents.shift();
365
+ var activeTab = document.querySelector('.tab.active');
366
+ if (activeTab && activeTab.dataset.tab === 'router') renderRouter();
367
+ updateStats();
368
+ }
369
+ };
370
+ }
371
+
372
+ // Periodic refresh for components + perf (fast when tab is visible)
373
+ setInterval(function() {
374
+ if (!isConnected()) {
375
+ // Retry connection - opener may be mid-mutation, not truly gone
376
+ try {
377
+ if (mode === 'popup' && window.opener && !window.opener.closed) {
378
+ targetWin = window.opener;
379
+ targetDoc = targetWin.document;
380
+ document.getElementById('disconnected').style.display = 'none';
381
+ } else if (iframe && iframe.contentWindow) {
382
+ targetWin = iframe.contentWindow;
383
+ targetDoc = targetWin.document;
384
+ document.getElementById('disconnected').style.display = 'none';
385
+ }
386
+ } catch(e) {}
387
+ if (!isConnected()) {
388
+ document.getElementById('disconnected').style.display = 'flex';
389
+ return;
390
+ }
391
+ }
392
+ // Keep targetDoc fresh - the opener may have reloaded (live-reload)
393
+ try {
394
+ var freshDoc = targetWin.document;
395
+ if (freshDoc !== targetDoc) {
396
+ targetDoc = freshDoc;
397
+ // Re-attach MutationObserver to the new document
398
+ if (observer) { observer.disconnect(); observer = null; }
399
+ startObserver();
400
+ buildDOMTree();
401
+ }
402
+ } catch(e) {}
403
+ var activeTab = document.querySelector('.tab.active');
404
+ var tabName = activeTab ? activeTab.dataset.tab : '';
405
+ if (tabName === 'components') renderComponents();
406
+ updateStats();
407
+ // Sync requests and render events from opener
408
+ if (targetWin.__zqDevTools) {
409
+ requests = targetWin.__zqDevTools.requests.slice();
410
+ morphCount = targetWin.__zqDevTools.morphCount || 0;
411
+ var stored = targetWin.__zqDevTools.morphEvents;
412
+ if (stored && stored.length) {
413
+ morphEvents = stored.slice();
414
+ }
415
+ var storedRouter = targetWin.__zqDevTools.routerEvents;
416
+ if (storedRouter && storedRouter.length) {
417
+ routerEvents = storedRouter.slice();
418
+ }
419
+ }
420
+ if (tabName === 'perf') renderPerf();
421
+ if (tabName === 'router') renderRouter();
422
+ }, 800);
423
+ }