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,858 +1,858 @@
1
- /**
2
- * cli/commands/dev/overlay.js - Client-side error overlay + SSE live-reload
3
- *
4
- * Returns an HTML <script> snippet that is injected before </body> in
5
- * every HTML response served by the dev server. Responsibilities:
6
- *
7
- * 1. Error Overlay - full-screen dark overlay with code frames, stack
8
- * traces, and ZQueryError metadata. Dismissable via Esc or ×.
9
- * 2. Runtime error hooks - window.onerror, unhandledrejection, AND
10
- * the zQuery $.onError() hook so framework-level errors are
11
- * surfaced in the overlay automatically.
12
- * 3. SSE connection - listens for reload / css / error:syntax /
13
- * error:clear events from the dev server watcher.
14
- */
15
-
16
- 'use strict';
17
-
18
- // The snippet is a self-contained IIFE - no external dependencies.
19
- // It must work in all browsers that support EventSource (IE11 excluded).
20
-
21
- const OVERLAY_SCRIPT = `<script>
22
- (function(){
23
- // =====================================================================
24
- // Error overlay
25
- // =====================================================================
26
- var overlayEl = null;
27
-
28
- var OVERLAY_CSS =
29
- 'position:fixed;top:0;left:0;width:100%;height:100%;' +
30
- 'background:rgba(0,0,0,0.92);color:#fff;z-index:2147483647;' +
31
- 'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;' +
32
- 'font-size:13px;overflow-y:auto;padding:0;margin:0;box-sizing:border-box;';
33
-
34
- var HEADER_CSS =
35
- 'padding:20px 24px 12px;border-bottom:1px solid rgba(255,255,255,0.1);' +
36
- 'display:flex;align-items:flex-start;justify-content:space-between;';
37
-
38
- var BADGE_CSS =
39
- 'display:inline-block;padding:3px 8px;border-radius:4px;font-size:11px;' +
40
- 'font-weight:700;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;';
41
-
42
- // Map ZQueryError code prefixes to colours so devs can see at a glance
43
- // which subsystem produced the error.
44
- var CODE_COLORS = {
45
- 'ZQ_REACTIVE': '#9b59b6',
46
- 'ZQ_SIGNAL': '#9b59b6',
47
- 'ZQ_EFFECT': '#9b59b6',
48
- 'ZQ_EXPR': '#2980b9',
49
- 'ZQ_COMP': '#16a085',
50
- 'ZQ_ROUTER': '#d35400',
51
- 'ZQ_STORE': '#8e44ad',
52
- 'ZQ_HTTP': '#2c3e50',
53
- 'ZQ_DEV': '#e74c3c',
54
- 'ZQ_INVALID': '#7f8c8d',
55
- };
56
-
57
- function badgeColor(data) {
58
- if (data.code) {
59
- var keys = Object.keys(CODE_COLORS);
60
- for (var i = 0; i < keys.length; i++) {
61
- if (data.code.indexOf(keys[i]) === 0) return CODE_COLORS[keys[i]];
62
- }
63
- }
64
- if (data.type && /syntax|parse/i.test(data.type)) return '#e74c3c';
65
- return '#e67e22';
66
- }
67
-
68
- function esc(s) {
69
- var d = document.createElement('div');
70
- d.appendChild(document.createTextNode(s));
71
- return d.innerHTML;
72
- }
73
-
74
- function createOverlay(data) {
75
- removeOverlay();
76
- var wrap = document.createElement('div');
77
- wrap.id = '__zq_error_overlay';
78
- wrap.setAttribute('style', OVERLAY_CSS);
79
- wrap.setAttribute('tabindex', '-1');
80
-
81
- var color = badgeColor(data);
82
- var html = '';
83
-
84
- // ----- header row -----
85
- html += '<div style="' + HEADER_CSS + '">';
86
- html += '<div>';
87
-
88
- // Error code badge (if present)
89
- if (data.code) {
90
- html += '<span style="' + BADGE_CSS + 'background:' + color + ';margin-right:6px;">' + esc(data.code) + '</span>';
91
- }
92
- // Type badge
93
- html += '<span style="' + BADGE_CSS + 'background:' + (data.code ? 'rgba(255,255,255,0.1)' : color) + ';">' + esc(data.type || 'Error') + '</span>';
94
-
95
- // Message
96
- html += '<div style="font-size:18px;font-weight:600;line-height:1.4;color:#ff6b6b;margin-top:4px;">';
97
- html += esc(data.message || 'Unknown error');
98
- html += '</div></div>';
99
-
100
- // Close button
101
- html += '<button id="__zq_close" style="' +
102
- 'background:none;border:1px solid rgba(255,255,255,0.2);color:#999;' +
103
- 'font-size:20px;cursor:pointer;border-radius:6px;width:32px;height:32px;' +
104
- 'display:flex;align-items:center;justify-content:center;flex-shrink:0;' +
105
- 'margin-left:16px;transition:all 0.15s;"' +
106
- ' onmouseover="this.style.color=\\'#fff\\';this.style.borderColor=\\'rgba(255,255,255,0.5)\\'"' +
107
- ' onmouseout="this.style.color=\\'#999\\';this.style.borderColor=\\'rgba(255,255,255,0.2)\\'"' +
108
- '>&times;</button>';
109
- html += '</div>';
110
-
111
- // ----- file location -----
112
- if (data.file) {
113
- html += '<div style="padding:10px 24px;color:#8be9fd;font-size:13px;">';
114
- html += '<span style="color:#888;">File: </span>' + esc(data.file);
115
- if (data.line) html += '<span style="color:#888;">:</span>' + data.line;
116
- if (data.column) html += '<span style="color:#888;">:</span>' + data.column;
117
- html += '</div>';
118
- }
119
-
120
- // ----- ZQueryError context (key/value pairs) -----
121
- if (data.context && typeof data.context === 'object' && Object.keys(data.context).length) {
122
- html += '<div style="padding:8px 24px;display:flex;flex-wrap:wrap;gap:8px;">';
123
- var ctxKeys = Object.keys(data.context);
124
- for (var ci = 0; ci < ctxKeys.length; ci++) {
125
- var k = ctxKeys[ci], v = data.context[k];
126
- html += '<span style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);' +
127
- 'padding:3px 10px;border-radius:4px;font-size:12px;">' +
128
- '<span style="color:#888;">' + esc(k) + ': </span>' +
129
- '<span style="color:#f1fa8c;">' + esc(typeof v === 'object' ? JSON.stringify(v) : String(v)) + '</span>' +
130
- '</span>';
131
- }
132
- html += '</div>';
133
- }
134
-
135
- // ----- code frame -----
136
- if (data.frame) {
137
- html += '<pre style="' +
138
- 'margin:0;padding:16px 24px;background:rgba(255,255,255,0.04);' +
139
- 'border-top:1px solid rgba(255,255,255,0.06);' +
140
- 'border-bottom:1px solid rgba(255,255,255,0.06);' +
141
- 'overflow-x:auto;line-height:1.6;font-size:13px;">';
142
- var lines = data.frame.split('\\n');
143
- for (var fi = 0; fi < lines.length; fi++) {
144
- var fl = lines[fi];
145
- if (fl.charAt(0) === '>') {
146
- html += '<span style="color:#ff6b6b;font-weight:600;">' + esc(fl) + '</span>\\n';
147
- } else if (fl.indexOf('^') !== -1 && fl.trim().replace(/[\\s|^]/g, '') === '') {
148
- html += '<span style="color:#e74c3c;font-weight:700;">' + esc(fl) + '</span>\\n';
149
- } else {
150
- html += '<span style="color:#999;">' + esc(fl) + '</span>\\n';
151
- }
152
- }
153
- html += '</pre>';
154
- }
155
-
156
- // ----- stack trace -----
157
- if (data.stack) {
158
- html += '<div style="padding:16px 24px;">';
159
- html += '<div style="color:#888;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;">Stack Trace</div>';
160
- html += '<pre style="margin:0;color:#bbb;font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-word;">';
161
- html += esc(data.stack);
162
- html += '</pre></div>';
163
- }
164
-
165
- // ----- tip -----
166
- html += '<div style="padding:16px 24px;color:#555;font-size:11px;border-top:1px solid rgba(255,255,255,0.06);">';
167
- html += 'Fix the error and save \\u2014 the overlay will clear automatically. Press <kbd style="' +
168
- 'background:rgba(255,255,255,0.1);padding:1px 6px;border-radius:3px;font-size:11px;' +
169
- '">Esc</kbd> to dismiss.';
170
- html += '</div>';
171
-
172
- wrap.innerHTML = html;
173
- document.body.appendChild(wrap);
174
- overlayEl = wrap;
175
-
176
- var closeBtn = document.getElementById('__zq_close');
177
- if (closeBtn) closeBtn.addEventListener('click', removeOverlay);
178
- wrap.addEventListener('keydown', function(e) { if (e.key === 'Escape') removeOverlay(); });
179
- wrap.focus();
180
- }
181
-
182
- function removeOverlay() {
183
- if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl);
184
- overlayEl = null;
185
- }
186
-
187
- // =====================================================================
188
- // Console helper
189
- // =====================================================================
190
- function logToConsole(data) {
191
- var label = data.code ? data.code + ' ' : '';
192
- var msg = '\\n%c zQuery DevError %c ' + label + data.type + ': ' + data.message;
193
- if (data.file) msg += '\\n at ' + data.file + (data.line ? ':' + data.line : '') + (data.column ? ':' + data.column : '');
194
- console.error(msg, 'background:#e74c3c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;', 'color:inherit;');
195
- if (data.frame) console.error(data.frame);
196
- }
197
-
198
- function cleanStack(stack) {
199
- return stack.split('\\n')
200
- .filter(function(l) { return l.indexOf('__zq_') === -1 && l.indexOf('EventSource') === -1; })
201
- .map(function(l) { return l.replace(location.origin, ''); })
202
- .join('\\n');
203
- }
204
-
205
- // =====================================================================
206
- // Runtime error hooks
207
- // =====================================================================
208
- window.addEventListener('error', function(e) {
209
- if (!e.filename) return;
210
- var err = e.error || {};
211
- var data = {
212
- code: err.code || '',
213
- type: (err.constructor && err.constructor.name) || 'Error',
214
- message: e.message || String(err),
215
- file: e.filename.replace(location.origin, ''),
216
- line: e.lineno || 0,
217
- column: e.colno || 0,
218
- context: err.context || null,
219
- stack: err.stack ? cleanStack(err.stack) : ''
220
- };
221
- createOverlay(data);
222
- logToConsole(data);
223
- });
224
-
225
- window.addEventListener('unhandledrejection', function(e) {
226
- var err = e.reason || {};
227
- var data = {
228
- code: err.code || '',
229
- type: err.name === 'ZQueryError' ? 'ZQueryError' : 'Unhandled Promise Rejection',
230
- message: err.message || String(err),
231
- context: err.context || null,
232
- stack: err.stack ? cleanStack(err.stack) : ''
233
- };
234
- createOverlay(data);
235
- logToConsole(data);
236
- });
237
-
238
- // =====================================================================
239
- // Hook into zQuery's $.onError() when the library is loaded
240
- // =====================================================================
241
- function hookZQueryErrors() {
242
- // $.onError is set by the framework - wait for it
243
- if (typeof $ !== 'undefined' && typeof $.onError === 'function') {
244
- $.onError(function(zqErr) {
245
- var data = {
246
- code: zqErr.code || '',
247
- type: 'ZQueryError',
248
- message: zqErr.message,
249
- context: zqErr.context || null,
250
- stack: zqErr.stack ? cleanStack(zqErr.stack) : ''
251
- };
252
- createOverlay(data);
253
- logToConsole(data);
254
- });
255
- return;
256
- }
257
- // Retry until the library has loaded (max ~5s)
258
- if (hookZQueryErrors._tries < 50) {
259
- hookZQueryErrors._tries++;
260
- setTimeout(hookZQueryErrors, 100);
261
- }
262
- }
263
- hookZQueryErrors._tries = 0;
264
- // Defer so the page's own scripts load first
265
- if (document.readyState === 'loading') {
266
- document.addEventListener('DOMContentLoaded', hookZQueryErrors);
267
- } else {
268
- setTimeout(hookZQueryErrors, 0);
269
- }
270
-
271
- // =====================================================================
272
- // SSE connection (live-reload + server-pushed errors)
273
- // =====================================================================
274
- var es, reconnectTimer;
275
-
276
- function connect() {
277
- es = new EventSource('/__zq_reload');
278
-
279
- es.addEventListener('reload', function() {
280
- removeOverlay();
281
- location.reload();
282
- });
283
-
284
- es.addEventListener('css', function(e) {
285
- var changedPath = (e.data || '').replace(/^\\/+/, '');
286
- var matched = false;
287
-
288
- // 1) Try cache-busting matching <link rel="stylesheet"> tags
289
- var sheets = document.querySelectorAll('link[rel="stylesheet"]');
290
- sheets.forEach(function(l) {
291
- var href = l.getAttribute('href');
292
- if (!href) return;
293
- var clean = href.replace(/[?&]_zqr=\\d+/, '').replace(/^\\/+/, '');
294
- if (changedPath && clean.indexOf(changedPath) === -1) return;
295
- matched = true;
296
- var sep = href.indexOf('?') >= 0 ? '&' : '?';
297
- l.setAttribute('href', href.replace(/[?&]_zqr=\\d+/, '') + sep + '_zqr=' + Date.now());
298
- });
299
-
300
- // 2) Try hot-swapping scoped <style data-zq-style-urls> elements
301
- // These come from component styleUrl - the CSS was fetched, scoped,
302
- // and injected as an inline <style>. We re-fetch and re-scope it.
303
- if (!matched) {
304
- var scopedEls = document.querySelectorAll('style[data-zq-style-urls]');
305
- scopedEls.forEach(function(el) {
306
- var urls = el.getAttribute('data-zq-style-urls') || '';
307
- var hit = urls.split(' ').some(function(u) {
308
- return u && u.replace(/^\\/+/, '').indexOf(changedPath) !== -1;
309
- });
310
- if (!hit) return;
311
- matched = true;
312
-
313
- var scopeAttr = el.getAttribute('data-zq-scope') || '';
314
- var inlineStyles = el.getAttribute('data-zq-inline') || '';
315
-
316
- // Re-fetch all style URLs (cache-busted)
317
- var urlList = urls.split(' ').filter(Boolean);
318
- var prevCSS = el.textContent; // preserve current styles as rollback
319
- Promise.all(urlList.map(function(u) {
320
- return fetch(u + (u.indexOf('?') >= 0 ? '&' : '?') + '_zqr=' + Date.now())
321
- .then(function(r) {
322
- if (!r.ok) throw new Error('CSS fetch failed: ' + r.status);
323
- return r.text();
324
- });
325
- })).then(function(results) {
326
- var raw = (inlineStyles ? inlineStyles + '\\n' : '') + results.join('\\n');
327
- // Re-scope CSS with the same scope attribute
328
- if (scopeAttr) {
329
- var inAt = 0;
330
- raw = raw.replace(/([^{}]+)\\{|\\}/g, function(m, sel) {
331
- if (m === '}') { if (inAt > 0) inAt--; return m; }
332
- var t = sel.trim();
333
- if (t.charAt(0) === '@') { inAt++; return m; }
334
- if (inAt > 0 && /^[\\d%\\s,fromto]+$/.test(t.replace(/\\s/g, ''))) return m;
335
- return sel.split(',').map(function(s) { return '[' + scopeAttr + '] ' + s.trim(); }).join(', ') + ' {';
336
- });
337
- }
338
- el.textContent = raw;
339
- }).catch(function() {
340
- // Restore previous CSS on failure to prevent blank page
341
- el.textContent = prevCSS;
342
- });
343
- });
344
- }
345
-
346
- // 3) Nothing matched - fall back to full reload
347
- if (!matched) { location.reload(); }
348
- });
349
-
350
- es.addEventListener('error:syntax', function(e) {
351
- try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
352
- });
353
-
354
- es.addEventListener('error:runtime', function(e) {
355
- try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
356
- });
357
-
358
- es.addEventListener('error:clear', function() {
359
- removeOverlay();
360
- });
361
-
362
- es.onerror = function() {
363
- es.close();
364
- clearTimeout(reconnectTimer);
365
- reconnectTimer = setTimeout(connect, 2000);
366
- };
367
- }
368
-
369
- connect();
370
-
371
- // =====================================================================
372
- // Fetch / $.http Interceptor - pretty console logging
373
- // =====================================================================
374
- var __zqChannel;
375
- try { __zqChannel = new BroadcastChannel('__zq_devtools'); } catch(e) {}
376
-
377
- var __zqRequests = [];
378
- var __zqMorphEvents = [];
379
- var __zqMorphCount = 0;
380
- var __zqRenderCount = 0;
381
- var __zqReqId = 0;
382
- var _origFetch = window.fetch;
383
-
384
- window.fetch = function(input, init) {
385
- var url = typeof input === 'string' ? input : (input && input.url ? input.url : String(input));
386
- var method = ((init && init.method) || (input && input.method) || 'GET').toUpperCase();
387
- var id = ++__zqReqId;
388
- var start = performance.now();
389
-
390
- // Skip internal dev-server requests
391
- if (url.indexOf('__zq_') !== -1 || url.indexOf('/_devtools') !== -1) {
392
- return _origFetch.apply(this, arguments);
393
- }
394
-
395
- return _origFetch.apply(this, arguments).then(function(response) {
396
- var elapsed = Math.round(performance.now() - start);
397
- var status = response.status;
398
- var cloned = response.clone();
399
-
400
- cloned.text().then(function(bodyText) {
401
- var entry = {
402
- id: id, method: method, url: url, status: status,
403
- elapsed: elapsed, bodyPreview: bodyText.slice(0, 5000),
404
- timestamp: Date.now()
405
- };
406
- __zqRequests.push(entry);
407
- if (__zqRequests.length > 500) __zqRequests.shift();
408
- updateDevBar();
409
-
410
- // Pretty console log
411
- var isOk = status >= 200 && status < 300;
412
- var color = isOk ? '#2ecc71' : status < 400 ? '#f39c12' : '#e74c3c';
413
-
414
- console.groupCollapsed(
415
- '%c ' + method + ' %c' + status + '%c ' + url + ' %c' + elapsed + 'ms',
416
- 'background:' + color + ';color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
417
- 'color:' + color + ';font-weight:700;margin-left:8px',
418
- 'color:inherit;margin-left:4px',
419
- 'color:#888;margin-left:8px;font-size:11px'
420
- );
421
-
422
- // Response body
423
- try {
424
- var parsed = JSON.parse(bodyText);
425
- console.log('%c Response ', 'background:#1e1e2e;color:#8be9fd;padding:2px 6px;border-radius:2px;font-weight:600', parsed);
426
- } catch(pe) {
427
- if (bodyText.length > 0) {
428
- console.log('%c Response ', 'background:#1e1e2e;color:#8be9fd;padding:2px 6px;border-radius:2px;font-weight:600',
429
- bodyText.length > 500 ? bodyText.slice(0, 500) + '... (' + bodyText.length + ' chars)' : bodyText);
430
- }
431
- }
432
-
433
- // Headers
434
- try {
435
- console.log('%c Headers ', 'background:#1e1e2e;color:#bd93f9;padding:2px 6px;border-radius:2px;font-weight:600',
436
- Object.fromEntries(response.headers.entries()));
437
- } catch(he) {}
438
-
439
- // Request body (if sent)
440
- if (init && init.body) {
441
- try {
442
- console.log('%c Request ', 'background:#1e1e2e;color:#f1fa8c;padding:2px 6px;border-radius:2px;font-weight:600',
443
- JSON.parse(init.body));
444
- } catch(re) {
445
- console.log('%c Request ', 'background:#1e1e2e;color:#f1fa8c;padding:2px 6px;border-radius:2px;font-weight:600',
446
- String(init.body).slice(0, 500));
447
- }
448
- }
449
-
450
- console.groupEnd();
451
-
452
- // Broadcast to devtools
453
- if (__zqChannel) {
454
- try { __zqChannel.postMessage({ type: 'http', data: entry }); } catch(ce) {}
455
- }
456
- }).catch(function() {});
457
-
458
- return response;
459
- }, function(err) {
460
- var elapsed = Math.round(performance.now() - start);
461
- console.groupCollapsed(
462
- '%c ' + method + ' %cERR%c ' + url + ' %c' + elapsed + 'ms',
463
- 'background:#e74c3c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
464
- 'color:#e74c3c;font-weight:700;margin-left:8px',
465
- 'color:inherit;margin-left:4px',
466
- 'color:#888;margin-left:8px;font-size:11px'
467
- );
468
- console.error(err);
469
- console.groupEnd();
470
-
471
- var entry = { id: id, method: method, url: url, status: 0, elapsed: elapsed, bodyPreview: err.message, timestamp: Date.now() };
472
- __zqRequests.push(entry);
473
- updateDevBar();
474
- if (__zqChannel) {
475
- try { __zqChannel.postMessage({ type: 'http', data: entry }); } catch(ce) {}
476
- }
477
-
478
- throw err;
479
- });
480
- };
481
-
482
- // =====================================================================
483
- // Morph instrumentation - hook via window.__zqMorphHook (set by diff.js)
484
- // =====================================================================
485
- window.__zqMorphHook = function(el, elapsed) {
486
- __zqMorphCount++;
487
- updateDevBar();
488
-
489
- var evt = { target: el.id || el.tagName.toLowerCase(), elapsed: elapsed, kind: 'morph', timestamp: Date.now() };
490
- __zqMorphEvents.push(evt);
491
- if (__zqMorphEvents.length > 200) __zqMorphEvents.shift();
492
-
493
- // Console timing for slow morphs (> 4ms)
494
- if (elapsed > 4) {
495
- console.log(
496
- '%c morph %c' + elapsed.toFixed(2) + 'ms%c ' + (el.id || el.tagName.toLowerCase()),
497
- 'background:#9b59b6;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
498
- 'color:' + (elapsed > 16 ? '#e74c3c' : '#f39c12') + ';font-weight:700;margin-left:8px',
499
- 'color:#888;margin-left:4px'
500
- );
501
- }
502
-
503
- // Broadcast to devtools
504
- if (__zqChannel) {
505
- try {
506
- __zqChannel.postMessage({
507
- type: 'morph-detail',
508
- data: evt
509
- });
510
- } catch(ce) {}
511
- }
512
- };
513
-
514
- // =====================================================================
515
- // Render instrumentation - hook for first-renders & route swaps
516
- // =====================================================================
517
- window.__zqRenderHook = function(el, elapsed, kind, name) {
518
- __zqRenderCount++;
519
- __zqMorphCount++; // count renders in the morph total for the toolbar
520
- updateDevBar();
521
-
522
- var evt = { target: name || el.id || el.tagName.toLowerCase(), elapsed: elapsed, kind: kind, timestamp: Date.now() };
523
- __zqMorphEvents.push(evt);
524
- if (__zqMorphEvents.length > 200) __zqMorphEvents.shift();
525
-
526
- // Console log for route/mount renders
527
- var label = kind === 'route' ? ' route ' : ' mount ';
528
- var bg = kind === 'route' ? '#d29922' : '#3fb950';
529
- console.log(
530
- '%c' + label + '%c' + elapsed.toFixed(2) + 'ms%c ' + (name || el.id || el.tagName.toLowerCase()),
531
- 'background:' + bg + ';color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
532
- 'color:' + (elapsed > 16 ? '#e74c3c' : '#888') + ';font-weight:700;margin-left:8px',
533
- 'color:#888;margin-left:4px'
534
- );
535
-
536
- // Broadcast to devtools
537
- if (__zqChannel) {
538
- try {
539
- __zqChannel.postMessage({
540
- type: 'render-detail',
541
- data: evt
542
- });
543
- } catch(ce) {}
544
- }
545
- };
546
-
547
- // =====================================================================
548
- // Router instrumentation - history state tracking for devtools
549
- // =====================================================================
550
- var __zqRouterEvents = [];
551
-
552
- var _origPushState = history.pushState;
553
- history.pushState = function(state, title, url) {
554
- _origPushState.apply(this, arguments);
555
- var isSubstate = state && state.__zq === 'substate';
556
- var evt = {
557
- action: isSubstate ? 'substate' : 'navigate',
558
- url: String(url || location.href).replace(location.origin, ''),
559
- key: isSubstate ? state.key : null,
560
- data: isSubstate ? state.data : null,
561
- timestamp: Date.now()
562
- };
563
- __zqRouterEvents.push(evt);
564
- if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
565
- if (__zqChannel) {
566
- try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
567
- }
568
- updateDevBar();
569
- };
570
-
571
- var _origReplaceState = history.replaceState;
572
- history.replaceState = function(state, title, url) {
573
- _origReplaceState.apply(this, arguments);
574
- var isSubstate = state && state.__zq === 'substate';
575
- var evt = {
576
- action: 'replace',
577
- url: String(url || location.href).replace(location.origin, ''),
578
- key: isSubstate ? state.key : null,
579
- data: isSubstate ? state.data : null,
580
- timestamp: Date.now()
581
- };
582
- __zqRouterEvents.push(evt);
583
- if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
584
- if (__zqChannel) {
585
- try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
586
- }
587
- updateDevBar();
588
- };
589
-
590
- window.addEventListener('popstate', function(e) {
591
- var state = e.state;
592
- var isSubstate = state && state.__zq === 'substate';
593
- var evt = {
594
- action: isSubstate ? 'pop-substate' : 'pop',
595
- url: location.pathname + location.hash,
596
- key: isSubstate ? state.key : null,
597
- data: isSubstate ? state.data : null,
598
- timestamp: Date.now()
599
- };
600
- __zqRouterEvents.push(evt);
601
- if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
602
- if (__zqChannel) {
603
- try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
604
- }
605
- updateDevBar();
606
- });
607
-
608
- window.addEventListener('hashchange', function() {
609
- var evt = {
610
- action: 'hashchange',
611
- url: location.hash,
612
- timestamp: Date.now()
613
- };
614
- __zqRouterEvents.push(evt);
615
- if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
616
- if (__zqChannel) {
617
- try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
618
- }
619
- updateDevBar();
620
- });
621
-
622
- // =====================================================================
623
- // Dev Toolbar - expandable floating bar with stats
624
- // =====================================================================
625
- var devBar;
626
- var __zqBarExpanded = false;
627
- try { __zqBarExpanded = localStorage.getItem('__zq_bar_expanded') === '1'; } catch(e) {}
628
- var __zqRouteColors = {
629
- navigate: { bg: 'rgba(63,185,80,0.12)', fg: '#3fb950' },
630
- replace: { bg: 'rgba(210,153,34,0.12)', fg: '#d29922' },
631
- pop: { bg: 'rgba(248,81,73,0.12)', fg: '#f85149' },
632
- 'pop-substate':{ bg: 'rgba(248,81,73,0.12)', fg: '#f85149' },
633
- substate: { bg: 'rgba(168,130,255,0.12)', fg: '#a882ff' },
634
- hashchange: { bg: 'rgba(88,166,255,0.12)', fg: '#58a6ff' }
635
- };
636
- var __zqRouteDefault = { bg: 'rgba(227,155,55,0.12)', fg: '#e39b37' };
637
- var __zqRouteFadeTimer = null;
638
-
639
- function createDevBar() {
640
- devBar = document.createElement('div');
641
- devBar.id = '__zq_devbar';
642
- devBar.setAttribute('style',
643
- 'position:fixed;bottom:12px;right:12px;z-index:2147483646;' +
644
- 'display:flex;align-items:center;gap:4px;' +
645
- 'background:rgba(22,27,34,0.92);border:1px solid rgba(48,54,61,0.8);' +
646
- 'border-radius:8px;padding:4px 6px;backdrop-filter:blur(8px);' +
647
- 'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;' +
648
- 'font-size:11px;color:#8b949e;user-select:none;cursor:default;' +
649
- 'box-shadow:0 4px 12px rgba(0,0,0,0.4);transition:all .25s cubic-bezier(.22,1,.36,1);'
650
- );
651
-
652
- var statStyle = 'padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;cursor:pointer;white-space:nowrap;';
653
- var expandedStyle = statStyle + 'display:none;transform:scale(0);opacity:0;transform-origin:left center;will-change:transform,opacity;transition:transform .25s cubic-bezier(.22,1,.36,1),opacity .2s ease;';
654
-
655
- devBar.innerHTML =
656
- '<span style="color:#58a6ff;font-weight:700;padding:0 4px;font-size:10px;letter-spacing:.5px">zQ</span>' +
657
- // Expanded-only stats
658
- '<span id="__zq_bar_route" class="__zq_ex" title="Current route" style="' + expandedStyle +
659
- 'background:rgba(227,155,55,0.12);color:#e39b37;outline-offset:1px;transition:transform .25s cubic-bezier(.22,1,.36,1),opacity .2s ease,background .3s ease,color .3s ease,outline-color .6s ease;">/</span>' +
660
- '<span id="__zq_bar_comps" class="__zq_ex" title="Registered components" style="' + expandedStyle +
661
- 'background:rgba(168,130,255,0.1);color:#a882ff;">0 comps</span>' +
662
- // Always-visible stats
663
- '<span id="__zq_bar_morphs" title="Render operations" style="' + statStyle +
664
- 'background:rgba(188,140,255,0.1);color:#bc8cff;">0 render</span>' +
665
- '<span id="__zq_bar_reqs" title="Network requests" style="' + statStyle +
666
- 'background:rgba(88,166,255,0.1);color:#58a6ff;">0 req</span>' +
667
- // Expanded-only stats
668
- '<span id="__zq_bar_els" class="__zq_ex" title="DOM elements" style="' + expandedStyle +
669
- 'background:rgba(63,185,80,0.1);color:#3fb950;">0 els</span>' +
670
- // Toggle expand/collapse
671
- '<button id="__zq_bar_toggle" title="Expand toolbar" style="' +
672
- 'padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;' +
673
- 'background:rgba(88,166,255,0.08);color:#58a6ff;border:1px solid rgba(88,166,255,0.2);' +
674
- 'cursor:pointer;font-family:inherit;transition:all .15s;line-height:1;' +
675
- '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 6 9 12 15 18"/></svg></button>' +
676
- '<button id="__zq_bar_close" title="Close toolbar" style="' +
677
- 'padding:0 4px;color:#484f58;cursor:pointer;font-size:14px;border:none;' +
678
- 'background:none;font-family:inherit;line-height:1;' +
679
- '">&times;</button>';
680
-
681
- document.body.appendChild(devBar);
682
-
683
- // If previously expanded, restore that state immediately
684
- if (__zqBarExpanded) {
685
- var items = devBar.querySelectorAll('.__zq_ex');
686
- var btn = document.getElementById('__zq_bar_toggle');
687
- if (btn) {
688
- btn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>';
689
- btn.title = 'Collapse toolbar';
690
- }
691
- for (var i = 0; i < items.length; i++) {
692
- items[i].style.display = 'inline';
693
- items[i].style.transform = 'scale(1)';
694
- items[i].style.opacity = '1';
695
- }
696
- }
697
- updateDevBar();
698
-
699
- // Live-poll stats so numbers stay current without waiting for events
700
- setInterval(updateDevBar, 1000);
701
-
702
- // Check if we're inside a devtools split-view iframe
703
- function isInSplitFrame() {
704
- try { return window.parent !== window && window.parent.document.getElementById('app-frame'); }
705
- catch(e) { return false; }
706
- }
707
-
708
- // Switch tab in devtools (works for both split iframe and popup)
709
- function switchDevTab(tab) {
710
- if (__zqChannel) {
711
- __zqChannel.postMessage({ type: 'switch-tab', tab: tab });
712
- }
713
- }
714
-
715
- // Open devtools popup
716
- var __zqPopup = null;
717
- function openDevToolsPopup(tab) {
718
- if (__zqPopup && !__zqPopup.closed) {
719
- switchDevTab(tab);
720
- __zqPopup.focus();
721
- return;
722
- }
723
- var w = 1080, h = 800;
724
- var left = window.screenX + window.outerWidth - w - 20;
725
- var top = window.screenY + 60;
726
- var url = '/_devtools' + (tab ? '#' + tab : '');
727
- __zqPopup = window.open(url, '__zq_devtools',
728
- 'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top +
729
- ',resizable=yes,scrollbars=yes');
730
- }
731
-
732
- function openTab(tab) {
733
- if (isInSplitFrame()) { switchDevTab(tab); } else { openDevToolsPopup(tab); }
734
- }
735
-
736
- // Stat click handlers → open relevant devtools tab
737
- document.getElementById('__zq_bar_route').addEventListener('click', function() { openTab('router'); });
738
- document.getElementById('__zq_bar_comps').addEventListener('click', function() { openTab('components'); });
739
- document.getElementById('__zq_bar_morphs').addEventListener('click', function() { openTab('perf'); });
740
- document.getElementById('__zq_bar_reqs').addEventListener('click', function() { openTab('network'); });
741
- document.getElementById('__zq_bar_els').addEventListener('click', function() { openTab('dom'); });
742
-
743
- // Expand / collapse toggle
744
- document.getElementById('__zq_bar_toggle').addEventListener('click', function() {
745
- __zqBarExpanded = !__zqBarExpanded;
746
- try { localStorage.setItem('__zq_bar_expanded', __zqBarExpanded ? '1' : '0'); } catch(e) {}
747
- var items = devBar.querySelectorAll('.__zq_ex');
748
- var btn = this;
749
- if (__zqBarExpanded) {
750
- btn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>';
751
- btn.title = 'Collapse toolbar';
752
- for (var i = 0; i < items.length; i++) {
753
- items[i].style.display = 'inline';
754
- items[i].offsetWidth; // reflow
755
- items[i].style.transform = 'scale(1)';
756
- items[i].style.opacity = '1';
757
- }
758
- updateDevBar();
759
- } else {
760
- btn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 6 9 12 15 18"/></svg>';
761
- btn.title = 'Expand toolbar';
762
- for (var i = 0; i < items.length; i++) {
763
- items[i].style.transform = 'scale(0)';
764
- items[i].style.opacity = '0';
765
- }
766
- setTimeout(function() {
767
- if (!__zqBarExpanded) {
768
- var items = devBar.querySelectorAll('.__zq_ex');
769
- for (var i = 0; i < items.length; i++) items[i].style.display = 'none';
770
- }
771
- }, 250);
772
- }
773
- });
774
-
775
- // Toggle hover
776
- document.getElementById('__zq_bar_toggle').addEventListener('mouseover', function() {
777
- this.style.background = 'rgba(88,166,255,0.18)';
778
- });
779
- document.getElementById('__zq_bar_toggle').addEventListener('mouseout', function() {
780
- this.style.background = 'rgba(88,166,255,0.08)';
781
- });
782
-
783
- // Close button
784
- document.getElementById('__zq_bar_close').addEventListener('click', function() {
785
- devBar.style.display = 'none';
786
- });
787
- }
788
-
789
- function updateDevBar() {
790
- if (!devBar) return;
791
- var reqEl = document.getElementById('__zq_bar_reqs');
792
- var morphEl = document.getElementById('__zq_bar_morphs');
793
- if (reqEl) reqEl.textContent = __zqRequests.length + ' req';
794
- if (morphEl) morphEl.textContent = __zqMorphCount + ' render';
795
-
796
- if (__zqBarExpanded) {
797
- var routeEl = document.getElementById('__zq_bar_route');
798
- var compsEl = document.getElementById('__zq_bar_comps');
799
- var elsEl = document.getElementById('__zq_bar_els');
800
- if (routeEl) {
801
- var lastEvt = __zqRouterEvents.length ? __zqRouterEvents[__zqRouterEvents.length - 1] : null;
802
- var action = lastEvt ? lastEvt.action : '';
803
- var path = location.pathname + location.hash;
804
- if (path.length > 16) path = path.substring(0, 14) + '…';
805
- var colors = __zqRouteColors[action] || __zqRouteDefault;
806
- routeEl.style.background = colors.bg;
807
- routeEl.style.color = colors.fg;
808
- if (action) {
809
- var label = action === 'pop-substate' ? 'pop' : action;
810
- routeEl.textContent = label + ' ' + path;
811
- routeEl.title = action + ' → ' + location.pathname + location.hash;
812
- } else {
813
- routeEl.textContent = path;
814
- }
815
- // Flash brightness on fresh events
816
- if (lastEvt && (Date.now() - lastEvt.timestamp) < 2000) {
817
- routeEl.style.outline = '1px solid ' + colors.fg;
818
- clearTimeout(__zqRouteFadeTimer);
819
- __zqRouteFadeTimer = setTimeout(function() {
820
- var el = document.getElementById('__zq_bar_route');
821
- if (el) el.style.outline = 'none';
822
- }, 1800);
823
- } else {
824
- routeEl.style.outline = 'none';
825
- }
826
- }
827
- if (compsEl) {
828
- var count = 0;
829
- try {
830
- if (window.$ && $.components) count = Object.keys($.components()).length;
831
- else if (window.$ && $._components) count = Object.keys($._components).length;
832
- } catch(e) {}
833
- compsEl.textContent = count + ' comps';
834
- }
835
- if (elsEl) {
836
- elsEl.textContent = document.querySelectorAll('*').length + ' els';
837
- }
838
- }
839
- }
840
-
841
- // Expose for devtools popup
842
- window.__zqDevTools = {
843
- get requests() { return __zqRequests; },
844
- get morphEvents() { return __zqMorphEvents; },
845
- get morphCount() { return __zqMorphCount; },
846
- get renderCount() { return __zqRenderCount; },
847
- get routerEvents() { return __zqRouterEvents; }
848
- };
849
-
850
- if (document.readyState === 'loading') {
851
- document.addEventListener('DOMContentLoaded', createDevBar);
852
- } else {
853
- createDevBar();
854
- }
855
- })();
856
- </script>`;
857
-
858
- module.exports = OVERLAY_SCRIPT;
1
+ /**
2
+ * cli/commands/dev/overlay.js - Client-side error overlay + SSE live-reload
3
+ *
4
+ * Returns an HTML <script> snippet that is injected before </body> in
5
+ * every HTML response served by the dev server. Responsibilities:
6
+ *
7
+ * 1. Error Overlay - full-screen dark overlay with code frames, stack
8
+ * traces, and ZQueryError metadata. Dismissable via Esc or ×.
9
+ * 2. Runtime error hooks - window.onerror, unhandledrejection, AND
10
+ * the zQuery $.onError() hook so framework-level errors are
11
+ * surfaced in the overlay automatically.
12
+ * 3. SSE connection - listens for reload / css / error:syntax /
13
+ * error:clear events from the dev server watcher.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ // The snippet is a self-contained IIFE - no external dependencies.
19
+ // It must work in all browsers that support EventSource (IE11 excluded).
20
+
21
+ const OVERLAY_SCRIPT = `<script>
22
+ (function(){
23
+ // =====================================================================
24
+ // Error overlay
25
+ // =====================================================================
26
+ var overlayEl = null;
27
+
28
+ var OVERLAY_CSS =
29
+ 'position:fixed;top:0;left:0;width:100%;height:100%;' +
30
+ 'background:rgba(0,0,0,0.92);color:#fff;z-index:2147483647;' +
31
+ 'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;' +
32
+ 'font-size:13px;overflow-y:auto;padding:0;margin:0;box-sizing:border-box;';
33
+
34
+ var HEADER_CSS =
35
+ 'padding:20px 24px 12px;border-bottom:1px solid rgba(255,255,255,0.1);' +
36
+ 'display:flex;align-items:flex-start;justify-content:space-between;';
37
+
38
+ var BADGE_CSS =
39
+ 'display:inline-block;padding:3px 8px;border-radius:4px;font-size:11px;' +
40
+ 'font-weight:700;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;';
41
+
42
+ // Map ZQueryError code prefixes to colours so devs can see at a glance
43
+ // which subsystem produced the error.
44
+ var CODE_COLORS = {
45
+ 'ZQ_REACTIVE': '#9b59b6',
46
+ 'ZQ_SIGNAL': '#9b59b6',
47
+ 'ZQ_EFFECT': '#9b59b6',
48
+ 'ZQ_EXPR': '#2980b9',
49
+ 'ZQ_COMP': '#16a085',
50
+ 'ZQ_ROUTER': '#d35400',
51
+ 'ZQ_STORE': '#8e44ad',
52
+ 'ZQ_HTTP': '#2c3e50',
53
+ 'ZQ_DEV': '#e74c3c',
54
+ 'ZQ_INVALID': '#7f8c8d',
55
+ };
56
+
57
+ function badgeColor(data) {
58
+ if (data.code) {
59
+ var keys = Object.keys(CODE_COLORS);
60
+ for (var i = 0; i < keys.length; i++) {
61
+ if (data.code.indexOf(keys[i]) === 0) return CODE_COLORS[keys[i]];
62
+ }
63
+ }
64
+ if (data.type && /syntax|parse/i.test(data.type)) return '#e74c3c';
65
+ return '#e67e22';
66
+ }
67
+
68
+ function esc(s) {
69
+ var d = document.createElement('div');
70
+ d.appendChild(document.createTextNode(s));
71
+ return d.innerHTML;
72
+ }
73
+
74
+ function createOverlay(data) {
75
+ removeOverlay();
76
+ var wrap = document.createElement('div');
77
+ wrap.id = '__zq_error_overlay';
78
+ wrap.setAttribute('style', OVERLAY_CSS);
79
+ wrap.setAttribute('tabindex', '-1');
80
+
81
+ var color = badgeColor(data);
82
+ var html = '';
83
+
84
+ // ----- header row -----
85
+ html += '<div style="' + HEADER_CSS + '">';
86
+ html += '<div>';
87
+
88
+ // Error code badge (if present)
89
+ if (data.code) {
90
+ html += '<span style="' + BADGE_CSS + 'background:' + color + ';margin-right:6px;">' + esc(data.code) + '</span>';
91
+ }
92
+ // Type badge
93
+ html += '<span style="' + BADGE_CSS + 'background:' + (data.code ? 'rgba(255,255,255,0.1)' : color) + ';">' + esc(data.type || 'Error') + '</span>';
94
+
95
+ // Message
96
+ html += '<div style="font-size:18px;font-weight:600;line-height:1.4;color:#ff6b6b;margin-top:4px;">';
97
+ html += esc(data.message || 'Unknown error');
98
+ html += '</div></div>';
99
+
100
+ // Close button
101
+ html += '<button id="__zq_close" style="' +
102
+ 'background:none;border:1px solid rgba(255,255,255,0.2);color:#999;' +
103
+ 'font-size:20px;cursor:pointer;border-radius:6px;width:32px;height:32px;' +
104
+ 'display:flex;align-items:center;justify-content:center;flex-shrink:0;' +
105
+ 'margin-left:16px;transition:all 0.15s;"' +
106
+ ' onmouseover="this.style.color=\\'#fff\\';this.style.borderColor=\\'rgba(255,255,255,0.5)\\'"' +
107
+ ' onmouseout="this.style.color=\\'#999\\';this.style.borderColor=\\'rgba(255,255,255,0.2)\\'"' +
108
+ '>&times;</button>';
109
+ html += '</div>';
110
+
111
+ // ----- file location -----
112
+ if (data.file) {
113
+ html += '<div style="padding:10px 24px;color:#8be9fd;font-size:13px;">';
114
+ html += '<span style="color:#888;">File: </span>' + esc(data.file);
115
+ if (data.line) html += '<span style="color:#888;">:</span>' + data.line;
116
+ if (data.column) html += '<span style="color:#888;">:</span>' + data.column;
117
+ html += '</div>';
118
+ }
119
+
120
+ // ----- ZQueryError context (key/value pairs) -----
121
+ if (data.context && typeof data.context === 'object' && Object.keys(data.context).length) {
122
+ html += '<div style="padding:8px 24px;display:flex;flex-wrap:wrap;gap:8px;">';
123
+ var ctxKeys = Object.keys(data.context);
124
+ for (var ci = 0; ci < ctxKeys.length; ci++) {
125
+ var k = ctxKeys[ci], v = data.context[k];
126
+ html += '<span style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);' +
127
+ 'padding:3px 10px;border-radius:4px;font-size:12px;">' +
128
+ '<span style="color:#888;">' + esc(k) + ': </span>' +
129
+ '<span style="color:#f1fa8c;">' + esc(typeof v === 'object' ? JSON.stringify(v) : String(v)) + '</span>' +
130
+ '</span>';
131
+ }
132
+ html += '</div>';
133
+ }
134
+
135
+ // ----- code frame -----
136
+ if (data.frame) {
137
+ html += '<pre style="' +
138
+ 'margin:0;padding:16px 24px;background:rgba(255,255,255,0.04);' +
139
+ 'border-top:1px solid rgba(255,255,255,0.06);' +
140
+ 'border-bottom:1px solid rgba(255,255,255,0.06);' +
141
+ 'overflow-x:auto;line-height:1.6;font-size:13px;">';
142
+ var lines = data.frame.split('\\n');
143
+ for (var fi = 0; fi < lines.length; fi++) {
144
+ var fl = lines[fi];
145
+ if (fl.charAt(0) === '>') {
146
+ html += '<span style="color:#ff6b6b;font-weight:600;">' + esc(fl) + '</span>\\n';
147
+ } else if (fl.indexOf('^') !== -1 && fl.trim().replace(/[\\s|^]/g, '') === '') {
148
+ html += '<span style="color:#e74c3c;font-weight:700;">' + esc(fl) + '</span>\\n';
149
+ } else {
150
+ html += '<span style="color:#999;">' + esc(fl) + '</span>\\n';
151
+ }
152
+ }
153
+ html += '</pre>';
154
+ }
155
+
156
+ // ----- stack trace -----
157
+ if (data.stack) {
158
+ html += '<div style="padding:16px 24px;">';
159
+ html += '<div style="color:#888;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:8px;">Stack Trace</div>';
160
+ html += '<pre style="margin:0;color:#bbb;font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-word;">';
161
+ html += esc(data.stack);
162
+ html += '</pre></div>';
163
+ }
164
+
165
+ // ----- tip -----
166
+ html += '<div style="padding:16px 24px;color:#555;font-size:11px;border-top:1px solid rgba(255,255,255,0.06);">';
167
+ html += 'Fix the error and save \\u2014 the overlay will clear automatically. Press <kbd style="' +
168
+ 'background:rgba(255,255,255,0.1);padding:1px 6px;border-radius:3px;font-size:11px;' +
169
+ '">Esc</kbd> to dismiss.';
170
+ html += '</div>';
171
+
172
+ wrap.innerHTML = html;
173
+ document.body.appendChild(wrap);
174
+ overlayEl = wrap;
175
+
176
+ var closeBtn = document.getElementById('__zq_close');
177
+ if (closeBtn) closeBtn.addEventListener('click', removeOverlay);
178
+ wrap.addEventListener('keydown', function(e) { if (e.key === 'Escape') removeOverlay(); });
179
+ wrap.focus();
180
+ }
181
+
182
+ function removeOverlay() {
183
+ if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl);
184
+ overlayEl = null;
185
+ }
186
+
187
+ // =====================================================================
188
+ // Console helper
189
+ // =====================================================================
190
+ function logToConsole(data) {
191
+ var label = data.code ? data.code + ' ' : '';
192
+ var msg = '\\n%c zQuery DevError %c ' + label + data.type + ': ' + data.message;
193
+ if (data.file) msg += '\\n at ' + data.file + (data.line ? ':' + data.line : '') + (data.column ? ':' + data.column : '');
194
+ console.error(msg, 'background:#e74c3c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;', 'color:inherit;');
195
+ if (data.frame) console.error(data.frame);
196
+ }
197
+
198
+ function cleanStack(stack) {
199
+ return stack.split('\\n')
200
+ .filter(function(l) { return l.indexOf('__zq_') === -1 && l.indexOf('EventSource') === -1; })
201
+ .map(function(l) { return l.replace(location.origin, ''); })
202
+ .join('\\n');
203
+ }
204
+
205
+ // =====================================================================
206
+ // Runtime error hooks
207
+ // =====================================================================
208
+ window.addEventListener('error', function(e) {
209
+ if (!e.filename) return;
210
+ var err = e.error || {};
211
+ var data = {
212
+ code: err.code || '',
213
+ type: (err.constructor && err.constructor.name) || 'Error',
214
+ message: e.message || String(err),
215
+ file: e.filename.replace(location.origin, ''),
216
+ line: e.lineno || 0,
217
+ column: e.colno || 0,
218
+ context: err.context || null,
219
+ stack: err.stack ? cleanStack(err.stack) : ''
220
+ };
221
+ createOverlay(data);
222
+ logToConsole(data);
223
+ });
224
+
225
+ window.addEventListener('unhandledrejection', function(e) {
226
+ var err = e.reason || {};
227
+ var data = {
228
+ code: err.code || '',
229
+ type: err.name === 'ZQueryError' ? 'ZQueryError' : 'Unhandled Promise Rejection',
230
+ message: err.message || String(err),
231
+ context: err.context || null,
232
+ stack: err.stack ? cleanStack(err.stack) : ''
233
+ };
234
+ createOverlay(data);
235
+ logToConsole(data);
236
+ });
237
+
238
+ // =====================================================================
239
+ // Hook into zQuery's $.onError() when the library is loaded
240
+ // =====================================================================
241
+ function hookZQueryErrors() {
242
+ // $.onError is set by the framework - wait for it
243
+ if (typeof $ !== 'undefined' && typeof $.onError === 'function') {
244
+ $.onError(function(zqErr) {
245
+ var data = {
246
+ code: zqErr.code || '',
247
+ type: 'ZQueryError',
248
+ message: zqErr.message,
249
+ context: zqErr.context || null,
250
+ stack: zqErr.stack ? cleanStack(zqErr.stack) : ''
251
+ };
252
+ createOverlay(data);
253
+ logToConsole(data);
254
+ });
255
+ return;
256
+ }
257
+ // Retry until the library has loaded (max ~5s)
258
+ if (hookZQueryErrors._tries < 50) {
259
+ hookZQueryErrors._tries++;
260
+ setTimeout(hookZQueryErrors, 100);
261
+ }
262
+ }
263
+ hookZQueryErrors._tries = 0;
264
+ // Defer so the page's own scripts load first
265
+ if (document.readyState === 'loading') {
266
+ document.addEventListener('DOMContentLoaded', hookZQueryErrors);
267
+ } else {
268
+ setTimeout(hookZQueryErrors, 0);
269
+ }
270
+
271
+ // =====================================================================
272
+ // SSE connection (live-reload + server-pushed errors)
273
+ // =====================================================================
274
+ var es, reconnectTimer;
275
+
276
+ function connect() {
277
+ es = new EventSource('/__zq_reload');
278
+
279
+ es.addEventListener('reload', function() {
280
+ removeOverlay();
281
+ location.reload();
282
+ });
283
+
284
+ es.addEventListener('css', function(e) {
285
+ var changedPath = (e.data || '').replace(/^\\/+/, '');
286
+ var matched = false;
287
+
288
+ // 1) Try cache-busting matching <link rel="stylesheet"> tags
289
+ var sheets = document.querySelectorAll('link[rel="stylesheet"]');
290
+ sheets.forEach(function(l) {
291
+ var href = l.getAttribute('href');
292
+ if (!href) return;
293
+ var clean = href.replace(/[?&]_zqr=\\d+/, '').replace(/^\\/+/, '');
294
+ if (changedPath && clean.indexOf(changedPath) === -1) return;
295
+ matched = true;
296
+ var sep = href.indexOf('?') >= 0 ? '&' : '?';
297
+ l.setAttribute('href', href.replace(/[?&]_zqr=\\d+/, '') + sep + '_zqr=' + Date.now());
298
+ });
299
+
300
+ // 2) Try hot-swapping scoped <style data-zq-style-urls> elements
301
+ // These come from component styleUrl - the CSS was fetched, scoped,
302
+ // and injected as an inline <style>. We re-fetch and re-scope it.
303
+ if (!matched) {
304
+ var scopedEls = document.querySelectorAll('style[data-zq-style-urls]');
305
+ scopedEls.forEach(function(el) {
306
+ var urls = el.getAttribute('data-zq-style-urls') || '';
307
+ var hit = urls.split(' ').some(function(u) {
308
+ return u && u.replace(/^\\/+/, '').indexOf(changedPath) !== -1;
309
+ });
310
+ if (!hit) return;
311
+ matched = true;
312
+
313
+ var scopeAttr = el.getAttribute('data-zq-scope') || '';
314
+ var inlineStyles = el.getAttribute('data-zq-inline') || '';
315
+
316
+ // Re-fetch all style URLs (cache-busted)
317
+ var urlList = urls.split(' ').filter(Boolean);
318
+ var prevCSS = el.textContent; // preserve current styles as rollback
319
+ Promise.all(urlList.map(function(u) {
320
+ return fetch(u + (u.indexOf('?') >= 0 ? '&' : '?') + '_zqr=' + Date.now())
321
+ .then(function(r) {
322
+ if (!r.ok) throw new Error('CSS fetch failed: ' + r.status);
323
+ return r.text();
324
+ });
325
+ })).then(function(results) {
326
+ var raw = (inlineStyles ? inlineStyles + '\\n' : '') + results.join('\\n');
327
+ // Re-scope CSS with the same scope attribute
328
+ if (scopeAttr) {
329
+ var inAt = 0;
330
+ raw = raw.replace(/([^{}]+)\\{|\\}/g, function(m, sel) {
331
+ if (m === '}') { if (inAt > 0) inAt--; return m; }
332
+ var t = sel.trim();
333
+ if (t.charAt(0) === '@') { inAt++; return m; }
334
+ if (inAt > 0 && /^[\\d%\\s,fromto]+$/.test(t.replace(/\\s/g, ''))) return m;
335
+ return sel.split(',').map(function(s) { return '[' + scopeAttr + '] ' + s.trim(); }).join(', ') + ' {';
336
+ });
337
+ }
338
+ el.textContent = raw;
339
+ }).catch(function() {
340
+ // Restore previous CSS on failure to prevent blank page
341
+ el.textContent = prevCSS;
342
+ });
343
+ });
344
+ }
345
+
346
+ // 3) Nothing matched - fall back to full reload
347
+ if (!matched) { location.reload(); }
348
+ });
349
+
350
+ es.addEventListener('error:syntax', function(e) {
351
+ try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
352
+ });
353
+
354
+ es.addEventListener('error:runtime', function(e) {
355
+ try { var data = JSON.parse(e.data); createOverlay(data); logToConsole(data); } catch(_){}
356
+ });
357
+
358
+ es.addEventListener('error:clear', function() {
359
+ removeOverlay();
360
+ });
361
+
362
+ es.onerror = function() {
363
+ es.close();
364
+ clearTimeout(reconnectTimer);
365
+ reconnectTimer = setTimeout(connect, 2000);
366
+ };
367
+ }
368
+
369
+ connect();
370
+
371
+ // =====================================================================
372
+ // Fetch / $.http Interceptor - pretty console logging
373
+ // =====================================================================
374
+ var __zqChannel;
375
+ try { __zqChannel = new BroadcastChannel('__zq_devtools'); } catch(e) {}
376
+
377
+ var __zqRequests = [];
378
+ var __zqMorphEvents = [];
379
+ var __zqMorphCount = 0;
380
+ var __zqRenderCount = 0;
381
+ var __zqReqId = 0;
382
+ var _origFetch = window.fetch;
383
+
384
+ window.fetch = function(input, init) {
385
+ var url = typeof input === 'string' ? input : (input && input.url ? input.url : String(input));
386
+ var method = ((init && init.method) || (input && input.method) || 'GET').toUpperCase();
387
+ var id = ++__zqReqId;
388
+ var start = performance.now();
389
+
390
+ // Skip internal dev-server requests
391
+ if (url.indexOf('__zq_') !== -1 || url.indexOf('/_devtools') !== -1) {
392
+ return _origFetch.apply(this, arguments);
393
+ }
394
+
395
+ return _origFetch.apply(this, arguments).then(function(response) {
396
+ var elapsed = Math.round(performance.now() - start);
397
+ var status = response.status;
398
+ var cloned = response.clone();
399
+
400
+ cloned.text().then(function(bodyText) {
401
+ var entry = {
402
+ id: id, method: method, url: url, status: status,
403
+ elapsed: elapsed, bodyPreview: bodyText.slice(0, 5000),
404
+ timestamp: Date.now()
405
+ };
406
+ __zqRequests.push(entry);
407
+ if (__zqRequests.length > 500) __zqRequests.shift();
408
+ updateDevBar();
409
+
410
+ // Pretty console log
411
+ var isOk = status >= 200 && status < 300;
412
+ var color = isOk ? '#2ecc71' : status < 400 ? '#f39c12' : '#e74c3c';
413
+
414
+ console.groupCollapsed(
415
+ '%c ' + method + ' %c' + status + '%c ' + url + ' %c' + elapsed + 'ms',
416
+ 'background:' + color + ';color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
417
+ 'color:' + color + ';font-weight:700;margin-left:8px',
418
+ 'color:inherit;margin-left:4px',
419
+ 'color:#888;margin-left:8px;font-size:11px'
420
+ );
421
+
422
+ // Response body
423
+ try {
424
+ var parsed = JSON.parse(bodyText);
425
+ console.log('%c Response ', 'background:#1e1e2e;color:#8be9fd;padding:2px 6px;border-radius:2px;font-weight:600', parsed);
426
+ } catch(pe) {
427
+ if (bodyText.length > 0) {
428
+ console.log('%c Response ', 'background:#1e1e2e;color:#8be9fd;padding:2px 6px;border-radius:2px;font-weight:600',
429
+ bodyText.length > 500 ? bodyText.slice(0, 500) + '... (' + bodyText.length + ' chars)' : bodyText);
430
+ }
431
+ }
432
+
433
+ // Headers
434
+ try {
435
+ console.log('%c Headers ', 'background:#1e1e2e;color:#bd93f9;padding:2px 6px;border-radius:2px;font-weight:600',
436
+ Object.fromEntries(response.headers.entries()));
437
+ } catch(he) {}
438
+
439
+ // Request body (if sent)
440
+ if (init && init.body) {
441
+ try {
442
+ console.log('%c Request ', 'background:#1e1e2e;color:#f1fa8c;padding:2px 6px;border-radius:2px;font-weight:600',
443
+ JSON.parse(init.body));
444
+ } catch(re) {
445
+ console.log('%c Request ', 'background:#1e1e2e;color:#f1fa8c;padding:2px 6px;border-radius:2px;font-weight:600',
446
+ String(init.body).slice(0, 500));
447
+ }
448
+ }
449
+
450
+ console.groupEnd();
451
+
452
+ // Broadcast to devtools
453
+ if (__zqChannel) {
454
+ try { __zqChannel.postMessage({ type: 'http', data: entry }); } catch(ce) {}
455
+ }
456
+ }).catch(function() {});
457
+
458
+ return response;
459
+ }, function(err) {
460
+ var elapsed = Math.round(performance.now() - start);
461
+ console.groupCollapsed(
462
+ '%c ' + method + ' %cERR%c ' + url + ' %c' + elapsed + 'ms',
463
+ 'background:#e74c3c;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
464
+ 'color:#e74c3c;font-weight:700;margin-left:8px',
465
+ 'color:inherit;margin-left:4px',
466
+ 'color:#888;margin-left:8px;font-size:11px'
467
+ );
468
+ console.error(err);
469
+ console.groupEnd();
470
+
471
+ var entry = { id: id, method: method, url: url, status: 0, elapsed: elapsed, bodyPreview: err.message, timestamp: Date.now() };
472
+ __zqRequests.push(entry);
473
+ updateDevBar();
474
+ if (__zqChannel) {
475
+ try { __zqChannel.postMessage({ type: 'http', data: entry }); } catch(ce) {}
476
+ }
477
+
478
+ throw err;
479
+ });
480
+ };
481
+
482
+ // =====================================================================
483
+ // Morph instrumentation - hook via window.__zqMorphHook (set by diff.js)
484
+ // =====================================================================
485
+ window.__zqMorphHook = function(el, elapsed) {
486
+ __zqMorphCount++;
487
+ updateDevBar();
488
+
489
+ var evt = { target: el.id || el.tagName.toLowerCase(), elapsed: elapsed, kind: 'morph', timestamp: Date.now() };
490
+ __zqMorphEvents.push(evt);
491
+ if (__zqMorphEvents.length > 200) __zqMorphEvents.shift();
492
+
493
+ // Console timing for slow morphs (> 4ms)
494
+ if (elapsed > 4) {
495
+ console.log(
496
+ '%c morph %c' + elapsed.toFixed(2) + 'ms%c ' + (el.id || el.tagName.toLowerCase()),
497
+ 'background:#9b59b6;color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
498
+ 'color:' + (elapsed > 16 ? '#e74c3c' : '#f39c12') + ';font-weight:700;margin-left:8px',
499
+ 'color:#888;margin-left:4px'
500
+ );
501
+ }
502
+
503
+ // Broadcast to devtools
504
+ if (__zqChannel) {
505
+ try {
506
+ __zqChannel.postMessage({
507
+ type: 'morph-detail',
508
+ data: evt
509
+ });
510
+ } catch(ce) {}
511
+ }
512
+ };
513
+
514
+ // =====================================================================
515
+ // Render instrumentation - hook for first-renders & route swaps
516
+ // =====================================================================
517
+ window.__zqRenderHook = function(el, elapsed, kind, name) {
518
+ __zqRenderCount++;
519
+ __zqMorphCount++; // count renders in the morph total for the toolbar
520
+ updateDevBar();
521
+
522
+ var evt = { target: name || el.id || el.tagName.toLowerCase(), elapsed: elapsed, kind: kind, timestamp: Date.now() };
523
+ __zqMorphEvents.push(evt);
524
+ if (__zqMorphEvents.length > 200) __zqMorphEvents.shift();
525
+
526
+ // Console log for route/mount renders
527
+ var label = kind === 'route' ? ' route ' : ' mount ';
528
+ var bg = kind === 'route' ? '#d29922' : '#3fb950';
529
+ console.log(
530
+ '%c' + label + '%c' + elapsed.toFixed(2) + 'ms%c ' + (name || el.id || el.tagName.toLowerCase()),
531
+ 'background:' + bg + ';color:#fff;padding:2px 6px;border-radius:3px;font-weight:700;font-size:11px',
532
+ 'color:' + (elapsed > 16 ? '#e74c3c' : '#888') + ';font-weight:700;margin-left:8px',
533
+ 'color:#888;margin-left:4px'
534
+ );
535
+
536
+ // Broadcast to devtools
537
+ if (__zqChannel) {
538
+ try {
539
+ __zqChannel.postMessage({
540
+ type: 'render-detail',
541
+ data: evt
542
+ });
543
+ } catch(ce) {}
544
+ }
545
+ };
546
+
547
+ // =====================================================================
548
+ // Router instrumentation - history state tracking for devtools
549
+ // =====================================================================
550
+ var __zqRouterEvents = [];
551
+
552
+ var _origPushState = history.pushState;
553
+ history.pushState = function(state, title, url) {
554
+ _origPushState.apply(this, arguments);
555
+ var isSubstate = state && state.__zq === 'substate';
556
+ var evt = {
557
+ action: isSubstate ? 'substate' : 'navigate',
558
+ url: String(url || location.href).replace(location.origin, ''),
559
+ key: isSubstate ? state.key : null,
560
+ data: isSubstate ? state.data : null,
561
+ timestamp: Date.now()
562
+ };
563
+ __zqRouterEvents.push(evt);
564
+ if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
565
+ if (__zqChannel) {
566
+ try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
567
+ }
568
+ updateDevBar();
569
+ };
570
+
571
+ var _origReplaceState = history.replaceState;
572
+ history.replaceState = function(state, title, url) {
573
+ _origReplaceState.apply(this, arguments);
574
+ var isSubstate = state && state.__zq === 'substate';
575
+ var evt = {
576
+ action: 'replace',
577
+ url: String(url || location.href).replace(location.origin, ''),
578
+ key: isSubstate ? state.key : null,
579
+ data: isSubstate ? state.data : null,
580
+ timestamp: Date.now()
581
+ };
582
+ __zqRouterEvents.push(evt);
583
+ if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
584
+ if (__zqChannel) {
585
+ try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
586
+ }
587
+ updateDevBar();
588
+ };
589
+
590
+ window.addEventListener('popstate', function(e) {
591
+ var state = e.state;
592
+ var isSubstate = state && state.__zq === 'substate';
593
+ var evt = {
594
+ action: isSubstate ? 'pop-substate' : 'pop',
595
+ url: location.pathname + location.hash,
596
+ key: isSubstate ? state.key : null,
597
+ data: isSubstate ? state.data : null,
598
+ timestamp: Date.now()
599
+ };
600
+ __zqRouterEvents.push(evt);
601
+ if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
602
+ if (__zqChannel) {
603
+ try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
604
+ }
605
+ updateDevBar();
606
+ });
607
+
608
+ window.addEventListener('hashchange', function() {
609
+ var evt = {
610
+ action: 'hashchange',
611
+ url: location.hash,
612
+ timestamp: Date.now()
613
+ };
614
+ __zqRouterEvents.push(evt);
615
+ if (__zqRouterEvents.length > 200) __zqRouterEvents.shift();
616
+ if (__zqChannel) {
617
+ try { __zqChannel.postMessage({ type: 'router', data: evt }); } catch(e) {}
618
+ }
619
+ updateDevBar();
620
+ });
621
+
622
+ // =====================================================================
623
+ // Dev Toolbar - expandable floating bar with stats
624
+ // =====================================================================
625
+ var devBar;
626
+ var __zqBarExpanded = false;
627
+ try { __zqBarExpanded = localStorage.getItem('__zq_bar_expanded') === '1'; } catch(e) {}
628
+ var __zqRouteColors = {
629
+ navigate: { bg: 'rgba(63,185,80,0.12)', fg: '#3fb950' },
630
+ replace: { bg: 'rgba(210,153,34,0.12)', fg: '#d29922' },
631
+ pop: { bg: 'rgba(248,81,73,0.12)', fg: '#f85149' },
632
+ 'pop-substate':{ bg: 'rgba(248,81,73,0.12)', fg: '#f85149' },
633
+ substate: { bg: 'rgba(168,130,255,0.12)', fg: '#a882ff' },
634
+ hashchange: { bg: 'rgba(88,166,255,0.12)', fg: '#58a6ff' }
635
+ };
636
+ var __zqRouteDefault = { bg: 'rgba(227,155,55,0.12)', fg: '#e39b37' };
637
+ var __zqRouteFadeTimer = null;
638
+
639
+ function createDevBar() {
640
+ devBar = document.createElement('div');
641
+ devBar.id = '__zq_devbar';
642
+ devBar.setAttribute('style',
643
+ 'position:fixed;bottom:12px;right:12px;z-index:2147483646;' +
644
+ 'display:flex;align-items:center;gap:4px;' +
645
+ 'background:rgba(22,27,34,0.92);border:1px solid rgba(48,54,61,0.8);' +
646
+ 'border-radius:8px;padding:4px 6px;backdrop-filter:blur(8px);' +
647
+ 'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;' +
648
+ 'font-size:11px;color:#8b949e;user-select:none;cursor:default;' +
649
+ 'box-shadow:0 4px 12px rgba(0,0,0,0.4);transition:all .25s cubic-bezier(.22,1,.36,1);'
650
+ );
651
+
652
+ var statStyle = 'padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;cursor:pointer;white-space:nowrap;';
653
+ var expandedStyle = statStyle + 'display:none;transform:scale(0);opacity:0;transform-origin:left center;will-change:transform,opacity;transition:transform .25s cubic-bezier(.22,1,.36,1),opacity .2s ease;';
654
+
655
+ devBar.innerHTML =
656
+ '<span style="color:#58a6ff;font-weight:700;padding:0 4px;font-size:10px;letter-spacing:.5px">zQ</span>' +
657
+ // Expanded-only stats
658
+ '<span id="__zq_bar_route" class="__zq_ex" title="Current route" style="' + expandedStyle +
659
+ 'background:rgba(227,155,55,0.12);color:#e39b37;outline-offset:1px;transition:transform .25s cubic-bezier(.22,1,.36,1),opacity .2s ease,background .3s ease,color .3s ease,outline-color .6s ease;">/</span>' +
660
+ '<span id="__zq_bar_comps" class="__zq_ex" title="Registered components" style="' + expandedStyle +
661
+ 'background:rgba(168,130,255,0.1);color:#a882ff;">0 comps</span>' +
662
+ // Always-visible stats
663
+ '<span id="__zq_bar_morphs" title="Render operations" style="' + statStyle +
664
+ 'background:rgba(188,140,255,0.1);color:#bc8cff;">0 render</span>' +
665
+ '<span id="__zq_bar_reqs" title="Network requests" style="' + statStyle +
666
+ 'background:rgba(88,166,255,0.1);color:#58a6ff;">0 req</span>' +
667
+ // Expanded-only stats
668
+ '<span id="__zq_bar_els" class="__zq_ex" title="DOM elements" style="' + expandedStyle +
669
+ 'background:rgba(63,185,80,0.1);color:#3fb950;">0 els</span>' +
670
+ // Toggle expand/collapse
671
+ '<button id="__zq_bar_toggle" title="Expand toolbar" style="' +
672
+ 'padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;' +
673
+ 'background:rgba(88,166,255,0.08);color:#58a6ff;border:1px solid rgba(88,166,255,0.2);' +
674
+ 'cursor:pointer;font-family:inherit;transition:all .15s;line-height:1;' +
675
+ '"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 6 9 12 15 18"/></svg></button>' +
676
+ '<button id="__zq_bar_close" title="Close toolbar" style="' +
677
+ 'padding:0 4px;color:#484f58;cursor:pointer;font-size:14px;border:none;' +
678
+ 'background:none;font-family:inherit;line-height:1;' +
679
+ '">&times;</button>';
680
+
681
+ document.body.appendChild(devBar);
682
+
683
+ // If previously expanded, restore that state immediately
684
+ if (__zqBarExpanded) {
685
+ var items = devBar.querySelectorAll('.__zq_ex');
686
+ var btn = document.getElementById('__zq_bar_toggle');
687
+ if (btn) {
688
+ btn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>';
689
+ btn.title = 'Collapse toolbar';
690
+ }
691
+ for (var i = 0; i < items.length; i++) {
692
+ items[i].style.display = 'inline';
693
+ items[i].style.transform = 'scale(1)';
694
+ items[i].style.opacity = '1';
695
+ }
696
+ }
697
+ updateDevBar();
698
+
699
+ // Live-poll stats so numbers stay current without waiting for events
700
+ setInterval(updateDevBar, 1000);
701
+
702
+ // Check if we're inside a devtools split-view iframe
703
+ function isInSplitFrame() {
704
+ try { return window.parent !== window && window.parent.document.getElementById('app-frame'); }
705
+ catch(e) { return false; }
706
+ }
707
+
708
+ // Switch tab in devtools (works for both split iframe and popup)
709
+ function switchDevTab(tab) {
710
+ if (__zqChannel) {
711
+ __zqChannel.postMessage({ type: 'switch-tab', tab: tab });
712
+ }
713
+ }
714
+
715
+ // Open devtools popup
716
+ var __zqPopup = null;
717
+ function openDevToolsPopup(tab) {
718
+ if (__zqPopup && !__zqPopup.closed) {
719
+ switchDevTab(tab);
720
+ __zqPopup.focus();
721
+ return;
722
+ }
723
+ var w = 1080, h = 800;
724
+ var left = window.screenX + window.outerWidth - w - 20;
725
+ var top = window.screenY + 60;
726
+ var url = '/_devtools' + (tab ? '#' + tab : '');
727
+ __zqPopup = window.open(url, '__zq_devtools',
728
+ 'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top +
729
+ ',resizable=yes,scrollbars=yes');
730
+ }
731
+
732
+ function openTab(tab) {
733
+ if (isInSplitFrame()) { switchDevTab(tab); } else { openDevToolsPopup(tab); }
734
+ }
735
+
736
+ // Stat click handlers → open relevant devtools tab
737
+ document.getElementById('__zq_bar_route').addEventListener('click', function() { openTab('router'); });
738
+ document.getElementById('__zq_bar_comps').addEventListener('click', function() { openTab('components'); });
739
+ document.getElementById('__zq_bar_morphs').addEventListener('click', function() { openTab('perf'); });
740
+ document.getElementById('__zq_bar_reqs').addEventListener('click', function() { openTab('network'); });
741
+ document.getElementById('__zq_bar_els').addEventListener('click', function() { openTab('dom'); });
742
+
743
+ // Expand / collapse toggle
744
+ document.getElementById('__zq_bar_toggle').addEventListener('click', function() {
745
+ __zqBarExpanded = !__zqBarExpanded;
746
+ try { localStorage.setItem('__zq_bar_expanded', __zqBarExpanded ? '1' : '0'); } catch(e) {}
747
+ var items = devBar.querySelectorAll('.__zq_ex');
748
+ var btn = this;
749
+ if (__zqBarExpanded) {
750
+ btn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>';
751
+ btn.title = 'Collapse toolbar';
752
+ for (var i = 0; i < items.length; i++) {
753
+ items[i].style.display = 'inline';
754
+ items[i].offsetWidth; // reflow
755
+ items[i].style.transform = 'scale(1)';
756
+ items[i].style.opacity = '1';
757
+ }
758
+ updateDevBar();
759
+ } else {
760
+ btn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 6 9 12 15 18"/></svg>';
761
+ btn.title = 'Expand toolbar';
762
+ for (var i = 0; i < items.length; i++) {
763
+ items[i].style.transform = 'scale(0)';
764
+ items[i].style.opacity = '0';
765
+ }
766
+ setTimeout(function() {
767
+ if (!__zqBarExpanded) {
768
+ var items = devBar.querySelectorAll('.__zq_ex');
769
+ for (var i = 0; i < items.length; i++) items[i].style.display = 'none';
770
+ }
771
+ }, 250);
772
+ }
773
+ });
774
+
775
+ // Toggle hover
776
+ document.getElementById('__zq_bar_toggle').addEventListener('mouseover', function() {
777
+ this.style.background = 'rgba(88,166,255,0.18)';
778
+ });
779
+ document.getElementById('__zq_bar_toggle').addEventListener('mouseout', function() {
780
+ this.style.background = 'rgba(88,166,255,0.08)';
781
+ });
782
+
783
+ // Close button
784
+ document.getElementById('__zq_bar_close').addEventListener('click', function() {
785
+ devBar.style.display = 'none';
786
+ });
787
+ }
788
+
789
+ function updateDevBar() {
790
+ if (!devBar) return;
791
+ var reqEl = document.getElementById('__zq_bar_reqs');
792
+ var morphEl = document.getElementById('__zq_bar_morphs');
793
+ if (reqEl) reqEl.textContent = __zqRequests.length + ' req';
794
+ if (morphEl) morphEl.textContent = __zqMorphCount + ' render';
795
+
796
+ if (__zqBarExpanded) {
797
+ var routeEl = document.getElementById('__zq_bar_route');
798
+ var compsEl = document.getElementById('__zq_bar_comps');
799
+ var elsEl = document.getElementById('__zq_bar_els');
800
+ if (routeEl) {
801
+ var lastEvt = __zqRouterEvents.length ? __zqRouterEvents[__zqRouterEvents.length - 1] : null;
802
+ var action = lastEvt ? lastEvt.action : '';
803
+ var path = location.pathname + location.hash;
804
+ if (path.length > 16) path = path.substring(0, 14) + '…';
805
+ var colors = __zqRouteColors[action] || __zqRouteDefault;
806
+ routeEl.style.background = colors.bg;
807
+ routeEl.style.color = colors.fg;
808
+ if (action) {
809
+ var label = action === 'pop-substate' ? 'pop' : action;
810
+ routeEl.textContent = label + ' ' + path;
811
+ routeEl.title = action + ' → ' + location.pathname + location.hash;
812
+ } else {
813
+ routeEl.textContent = path;
814
+ }
815
+ // Flash brightness on fresh events
816
+ if (lastEvt && (Date.now() - lastEvt.timestamp) < 2000) {
817
+ routeEl.style.outline = '1px solid ' + colors.fg;
818
+ clearTimeout(__zqRouteFadeTimer);
819
+ __zqRouteFadeTimer = setTimeout(function() {
820
+ var el = document.getElementById('__zq_bar_route');
821
+ if (el) el.style.outline = 'none';
822
+ }, 1800);
823
+ } else {
824
+ routeEl.style.outline = 'none';
825
+ }
826
+ }
827
+ if (compsEl) {
828
+ var count = 0;
829
+ try {
830
+ if (window.$ && $.components) count = Object.keys($.components()).length;
831
+ else if (window.$ && $._components) count = Object.keys($._components).length;
832
+ } catch(e) {}
833
+ compsEl.textContent = count + ' comps';
834
+ }
835
+ if (elsEl) {
836
+ elsEl.textContent = document.querySelectorAll('*').length + ' els';
837
+ }
838
+ }
839
+ }
840
+
841
+ // Expose for devtools popup
842
+ window.__zqDevTools = {
843
+ get requests() { return __zqRequests; },
844
+ get morphEvents() { return __zqMorphEvents; },
845
+ get morphCount() { return __zqMorphCount; },
846
+ get renderCount() { return __zqRenderCount; },
847
+ get routerEvents() { return __zqRouterEvents; }
848
+ };
849
+
850
+ if (document.readyState === 'loading') {
851
+ document.addEventListener('DOMContentLoaded', createDevBar);
852
+ } else {
853
+ createDevBar();
854
+ }
855
+ })();
856
+ </script>`;
857
+
858
+ module.exports = OVERLAY_SCRIPT;