zero-query 0.9.8 → 1.0.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 (99) hide show
  1. package/README.md +55 -31
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +15 -15
  5. package/cli/commands/create.js +41 -7
  6. package/cli/commands/dev/devtools/index.js +1 -1
  7. package/cli/commands/dev/devtools/js/core.js +14 -14
  8. package/cli/commands/dev/devtools/js/elements.js +4 -4
  9. package/cli/commands/dev/devtools/js/stats.js +1 -1
  10. package/cli/commands/dev/devtools/styles.css +2 -2
  11. package/cli/commands/dev/index.js +2 -2
  12. package/cli/commands/dev/logger.js +1 -1
  13. package/cli/commands/dev/overlay.js +21 -14
  14. package/cli/commands/dev/server.js +5 -5
  15. package/cli/commands/dev/validator.js +7 -7
  16. package/cli/commands/dev/watcher.js +6 -6
  17. package/cli/help.js +4 -2
  18. package/cli/index.js +2 -2
  19. package/cli/scaffold/default/app/app.js +17 -18
  20. package/cli/scaffold/default/app/components/about.js +9 -9
  21. package/cli/scaffold/default/app/components/api-demo.js +6 -6
  22. package/cli/scaffold/default/app/components/contact-card.js +4 -4
  23. package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
  24. package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
  25. package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
  26. package/cli/scaffold/default/app/components/counter.js +8 -8
  27. package/cli/scaffold/default/app/components/home.js +13 -13
  28. package/cli/scaffold/default/app/components/not-found.js +1 -1
  29. package/cli/scaffold/default/app/components/playground/playground.css +1 -1
  30. package/cli/scaffold/default/app/components/playground/playground.html +11 -11
  31. package/cli/scaffold/default/app/components/playground/playground.js +11 -11
  32. package/cli/scaffold/default/app/components/todos.js +8 -8
  33. package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
  34. package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
  35. package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
  36. package/cli/scaffold/default/app/routes.js +1 -1
  37. package/cli/scaffold/default/app/store.js +1 -1
  38. package/cli/scaffold/default/global.css +2 -2
  39. package/cli/scaffold/default/index.html +2 -2
  40. package/cli/scaffold/minimal/app/app.js +6 -7
  41. package/cli/scaffold/minimal/app/components/about.js +5 -5
  42. package/cli/scaffold/minimal/app/components/counter.js +6 -6
  43. package/cli/scaffold/minimal/app/components/home.js +8 -8
  44. package/cli/scaffold/minimal/app/components/not-found.js +1 -1
  45. package/cli/scaffold/minimal/app/routes.js +1 -1
  46. package/cli/scaffold/minimal/app/store.js +1 -1
  47. package/cli/scaffold/minimal/global.css +2 -2
  48. package/cli/scaffold/minimal/index.html +1 -1
  49. package/cli/scaffold/ssr/app/app.js +29 -0
  50. package/cli/scaffold/ssr/app/components/about.js +28 -0
  51. package/cli/scaffold/ssr/app/components/home.js +37 -0
  52. package/cli/scaffold/ssr/app/components/not-found.js +15 -0
  53. package/cli/scaffold/ssr/app/routes.js +6 -0
  54. package/cli/scaffold/ssr/global.css +113 -0
  55. package/cli/scaffold/ssr/index.html +31 -0
  56. package/cli/scaffold/ssr/package.json +8 -0
  57. package/cli/scaffold/ssr/server/index.js +118 -0
  58. package/cli/utils.js +6 -6
  59. package/dist/zquery.dist.zip +0 -0
  60. package/dist/zquery.js +565 -228
  61. package/dist/zquery.min.js +2 -2
  62. package/index.d.ts +25 -12
  63. package/index.js +11 -7
  64. package/package.json +9 -3
  65. package/src/component.js +64 -63
  66. package/src/core.js +15 -15
  67. package/src/diff.js +38 -38
  68. package/src/errors.js +72 -18
  69. package/src/expression.js +15 -17
  70. package/src/http.js +4 -4
  71. package/src/package.json +1 -0
  72. package/src/reactive.js +75 -9
  73. package/src/router.js +104 -24
  74. package/src/ssr.js +133 -39
  75. package/src/store.js +103 -21
  76. package/src/utils.js +64 -12
  77. package/tests/audit.test.js +143 -15
  78. package/tests/cli.test.js +20 -20
  79. package/tests/component.test.js +121 -121
  80. package/tests/core.test.js +56 -56
  81. package/tests/diff.test.js +42 -42
  82. package/tests/errors.test.js +425 -147
  83. package/tests/expression.test.js +58 -53
  84. package/tests/http.test.js +20 -20
  85. package/tests/reactive.test.js +185 -24
  86. package/tests/router.test.js +501 -74
  87. package/tests/ssr.test.js +444 -10
  88. package/tests/store.test.js +264 -23
  89. package/tests/utils.test.js +163 -26
  90. package/types/collection.d.ts +2 -2
  91. package/types/component.d.ts +5 -5
  92. package/types/errors.d.ts +36 -4
  93. package/types/http.d.ts +3 -3
  94. package/types/misc.d.ts +9 -9
  95. package/types/reactive.d.ts +25 -3
  96. package/types/router.d.ts +10 -6
  97. package/types/ssr.d.ts +22 -2
  98. package/types/store.d.ts +40 -5
  99. package/types/utils.d.ts +1 -1
package/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.9.8
2
+ * zQuery (zeroQuery) v1.0.0
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -9,7 +9,7 @@
9
9
 
10
10
  // --- src/errors.js -----------------------------------------------
11
11
  /**
12
- * zQuery Errors Structured error handling system
12
+ * zQuery Errors - Structured error handling system
13
13
  *
14
14
  * Provides typed error classes and a configurable error handler so that
15
15
  * errors surface consistently across all modules (reactive, component,
@@ -21,7 +21,7 @@
21
21
  */
22
22
 
23
23
  // ---------------------------------------------------------------------------
24
- // Error codes every zQuery error has a unique code for programmatic use
24
+ // Error codes - every zQuery error has a unique code for programmatic use
25
25
  // ---------------------------------------------------------------------------
26
26
  const ErrorCode = Object.freeze({
27
27
  // Reactive
@@ -59,20 +59,26 @@ const ErrorCode = Object.freeze({
59
59
  HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR',
60
60
  HTTP_PARSE: 'ZQ_HTTP_PARSE',
61
61
 
62
+ // SSR
63
+ SSR_RENDER: 'ZQ_SSR_RENDER',
64
+ SSR_COMPONENT: 'ZQ_SSR_COMPONENT',
65
+ SSR_HYDRATION: 'ZQ_SSR_HYDRATION',
66
+ SSR_PAGE: 'ZQ_SSR_PAGE',
67
+
62
68
  // General
63
69
  INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
64
70
  });
65
71
 
66
72
 
67
73
  // ---------------------------------------------------------------------------
68
- // ZQueryError custom error class
74
+ // ZQueryError - custom error class
69
75
  // ---------------------------------------------------------------------------
70
76
  class ZQueryError extends Error {
71
77
  /**
72
- * @param {string} code one of ErrorCode values
73
- * @param {string} message human-readable description
74
- * @param {object} [context] extra data (component name, expression, etc.)
75
- * @param {Error} [cause] original error
78
+ * @param {string} code - one of ErrorCode values
79
+ * @param {string} message - human-readable description
80
+ * @param {object} [context] - extra data (component name, expression, etc.)
81
+ * @param {Error} [cause] - original error
76
82
  */
77
83
  constructor(code, message, context = {}, cause) {
78
84
  super(message);
@@ -87,23 +93,35 @@ class ZQueryError extends Error {
87
93
  // ---------------------------------------------------------------------------
88
94
  // Global error handler
89
95
  // ---------------------------------------------------------------------------
90
- let _errorHandler = null;
96
+ let _errorHandlers = [];
91
97
 
92
98
  /**
93
99
  * Register a global error handler.
94
100
  * Called whenever zQuery catches an error internally.
101
+ * Multiple handlers are supported - each receives the error.
102
+ * Pass `null` to clear all handlers.
95
103
  *
96
- * @param {Function|null} handler (error: ZQueryError) => void
104
+ * @param {Function|null} handler - (error: ZQueryError) => void
105
+ * @returns {Function} unsubscribe function to remove this handler
97
106
  */
98
107
  function onError(handler) {
99
- _errorHandler = typeof handler === 'function' ? handler : null;
108
+ if (handler === null) {
109
+ _errorHandlers = [];
110
+ return () => {};
111
+ }
112
+ if (typeof handler !== 'function') return () => {};
113
+ _errorHandlers.push(handler);
114
+ return () => {
115
+ const idx = _errorHandlers.indexOf(handler);
116
+ if (idx !== -1) _errorHandlers.splice(idx, 1);
117
+ };
100
118
  }
101
119
 
102
120
  /**
103
121
  * Report an error through the global handler and console.
104
- * Non-throwing used for recoverable errors in callbacks, lifecycle hooks, etc.
122
+ * Non-throwing - used for recoverable errors in callbacks, lifecycle hooks, etc.
105
123
  *
106
- * @param {string} code ErrorCode
124
+ * @param {string} code - ErrorCode
107
125
  * @param {string} message
108
126
  * @param {object} [context]
109
127
  * @param {Error} [cause]
@@ -113,9 +131,9 @@ function reportError(code, message, context = {}, cause) {
113
131
  ? cause
114
132
  : new ZQueryError(code, message, context, cause);
115
133
 
116
- // User handler gets first crack
117
- if (_errorHandler) {
118
- try { _errorHandler(err); } catch { /* prevent handler from crashing framework */ }
134
+ // Notify all registered handlers
135
+ for (const handler of _errorHandlers) {
136
+ try { handler(err); } catch { /* prevent handler from crashing framework */ }
119
137
  }
120
138
 
121
139
  // Always log for developer visibility
@@ -127,7 +145,7 @@ function reportError(code, message, context = {}, cause) {
127
145
  * the current execution context.
128
146
  *
129
147
  * @param {Function} fn
130
- * @param {string} code ErrorCode to use if the callback throws
148
+ * @param {string} code - ErrorCode to use if the callback throws
131
149
  * @param {object} [context]
132
150
  * @returns {Function}
133
151
  */
@@ -146,8 +164,8 @@ function guardCallback(fn, code, context = {}) {
146
164
  * Throws ZQueryError on failure (for fast-fail at API boundaries).
147
165
  *
148
166
  * @param {*} value
149
- * @param {string} name parameter name for error message
150
- * @param {string} expectedType 'string', 'function', 'object', etc.
167
+ * @param {string} name - parameter name for error message
168
+ * @param {string} expectedType - 'string', 'function', 'object', etc.
151
169
  */
152
170
  function validate(value, name, expectedType) {
153
171
  if (value === undefined || value === null) {
@@ -162,11 +180,47 @@ function validate(value, name, expectedType) {
162
180
  `"${name}" must be a ${expectedType}, got ${typeof value}`
163
181
  );
164
182
  }
183
+ }
184
+
185
+ /**
186
+ * Format a ZQueryError into a structured object suitable for overlays/logging.
187
+ * @param {ZQueryError|Error} err
188
+ * @returns {{ code: string, type: string, message: string, context: object, stack: string }}
189
+ */
190
+ function formatError(err) {
191
+ const isZQ = err instanceof ZQueryError;
192
+ return {
193
+ code: isZQ ? err.code : '',
194
+ type: isZQ ? 'ZQueryError' : (err.name || 'Error'),
195
+ message: err.message || 'Unknown error',
196
+ context: isZQ ? err.context : {},
197
+ stack: err.stack || '',
198
+ cause: err.cause ? formatError(err.cause) : null,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Async version of guardCallback - wraps an async function so that
204
+ * rejections are caught, reported, and don't crash execution.
205
+ *
206
+ * @param {Function} fn - async function
207
+ * @param {string} code - ErrorCode to use
208
+ * @param {object} [context]
209
+ * @returns {Function}
210
+ */
211
+ function guardAsync(fn, code, context = {}) {
212
+ return async (...args) => {
213
+ try {
214
+ return await fn(...args);
215
+ } catch (err) {
216
+ reportError(code, err.message || 'Async callback error', context, err);
217
+ }
218
+ };
165
219
  }
166
220
 
167
221
  // --- src/reactive.js ---------------------------------------------
168
222
  /**
169
- * zQuery Reactive Proxy-based deep reactivity system
223
+ * zQuery Reactive - Proxy-based deep reactivity system
170
224
  *
171
225
  * Creates observable objects that trigger callbacks on mutation.
172
226
  * Used internally by components and store for auto-updates.
@@ -233,7 +287,7 @@ function reactive(target, onChange, _path = '') {
233
287
 
234
288
 
235
289
  // ---------------------------------------------------------------------------
236
- // Signal lightweight reactive primitive (inspired by Solid/Preact signals)
290
+ // Signal - lightweight reactive primitive (inspired by Solid/Preact signals)
237
291
  // ---------------------------------------------------------------------------
238
292
  class Signal {
239
293
  constructor(value) {
@@ -262,7 +316,11 @@ class Signal {
262
316
  peek() { return this._value; }
263
317
 
264
318
  _notify() {
265
- // Snapshot subscribers before iterating — a subscriber might modify
319
+ if (Signal._batching) {
320
+ Signal._batchQueue.add(this);
321
+ return;
322
+ }
323
+ // Snapshot subscribers before iterating - a subscriber might modify
266
324
  // the set (e.g., an effect re-running, adding itself back)
267
325
  const subs = [...this._subscribers];
268
326
  for (let i = 0; i < subs.length; i++) {
@@ -283,10 +341,13 @@ class Signal {
283
341
 
284
342
  // Active effect tracking
285
343
  Signal._activeEffect = null;
344
+ // Batch state
345
+ Signal._batching = false;
346
+ Signal._batchQueue = new Set();
286
347
 
287
348
  /**
288
349
  * Create a signal
289
- * @param {*} initial initial value
350
+ * @param {*} initial - initial value
290
351
  * @returns {Signal}
291
352
  */
292
353
  function signal(initial) {
@@ -295,7 +356,7 @@ function signal(initial) {
295
356
 
296
357
  /**
297
358
  * Create a computed signal (derived from other signals)
298
- * @param {Function} fn computation function
359
+ * @param {Function} fn - computation function
299
360
  * @returns {Signal}
300
361
  */
301
362
  function computed(fn) {
@@ -313,10 +374,10 @@ function computed(fn) {
313
374
  /**
314
375
  * Create a side-effect that auto-tracks signal dependencies.
315
376
  * Returns a dispose function that removes the effect from all
316
- * signals it subscribed to prevents memory leaks.
377
+ * signals it subscribed to - prevents memory leaks.
317
378
  *
318
- * @param {Function} fn effect function
319
- * @returns {Function} dispose function
379
+ * @param {Function} fn - effect function
380
+ * @returns {Function} - dispose function
320
381
  */
321
382
  function effect(fn) {
322
383
  const execute = () => {
@@ -349,13 +410,72 @@ function effect(fn) {
349
410
  }
350
411
  execute._deps.clear();
351
412
  }
352
- // Don't clobber _activeEffect another effect may be running
413
+ // Don't clobber _activeEffect - another effect may be running
353
414
  };
415
+ }
416
+
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // batch() - defer signal notifications until the batch completes
420
+ // ---------------------------------------------------------------------------
421
+
422
+ /**
423
+ * Batch multiple signal writes - subscribers and effects fire once at the end.
424
+ * @param {Function} fn - function that performs signal writes
425
+ */
426
+ function batch(fn) {
427
+ if (Signal._batching) {
428
+ // Already inside a batch, just run
429
+ fn();
430
+ return;
431
+ }
432
+ Signal._batching = true;
433
+ Signal._batchQueue.clear();
434
+ try {
435
+ fn();
436
+ } finally {
437
+ Signal._batching = false;
438
+ // Collect all unique subscribers across all queued signals
439
+ // so each subscriber/effect runs exactly once
440
+ const subs = new Set();
441
+ for (const sig of Signal._batchQueue) {
442
+ for (const sub of sig._subscribers) {
443
+ subs.add(sub);
444
+ }
445
+ }
446
+ Signal._batchQueue.clear();
447
+ for (const sub of subs) {
448
+ try { sub(); }
449
+ catch (err) {
450
+ reportError(ErrorCode.SIGNAL_CALLBACK, 'Signal subscriber threw', {}, err);
451
+ }
452
+ }
453
+ }
454
+ }
455
+
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // untracked() - read signals without creating dependencies
459
+ // ---------------------------------------------------------------------------
460
+
461
+ /**
462
+ * Execute a function without tracking signal reads as dependencies.
463
+ * @param {Function} fn - function to run
464
+ * @returns {*} the return value of fn
465
+ */
466
+ function untracked(fn) {
467
+ const prev = Signal._activeEffect;
468
+ Signal._activeEffect = null;
469
+ try {
470
+ return fn();
471
+ } finally {
472
+ Signal._activeEffect = prev;
473
+ }
354
474
  }
355
475
 
356
476
  // --- src/diff.js -------------------------------------------------
357
477
  /**
358
- * zQuery Diff Lightweight DOM morphing engine
478
+ * zQuery Diff - Lightweight DOM morphing engine
359
479
  *
360
480
  * Patches an existing DOM tree to match new HTML without destroying nodes
361
481
  * that haven't changed. Preserves focus, scroll positions, third-party
@@ -365,17 +485,17 @@ function effect(fn) {
365
485
  * Keyed elements (via `z-key`) get matched across position changes.
366
486
  *
367
487
  * Performance advantages over virtual DOM (React/Angular):
368
- * - No virtual tree allocation or diffing works directly on real DOM
488
+ * - No virtual tree allocation or diffing - works directly on real DOM
369
489
  * - Skips unchanged subtrees via fast isEqualNode() check
370
490
  * - z-skip attribute to opt out of diffing entire subtrees
371
491
  * - Reuses a single template element for HTML parsing (zero GC pressure)
372
492
  * - Keyed reconciliation uses LIS (Longest Increasing Subsequence) to
373
- * minimize DOM moves same algorithm as Vue 3 / ivi
493
+ * minimize DOM moves - same algorithm as Vue 3 / ivi
374
494
  * - Minimal attribute diffing with early bail-out
375
495
  */
376
496
 
377
497
  // ---------------------------------------------------------------------------
378
- // Reusable template element avoids per-call allocation
498
+ // Reusable template element - avoids per-call allocation
379
499
  // ---------------------------------------------------------------------------
380
500
  let _tpl = null;
381
501
 
@@ -385,15 +505,15 @@ function _getTemplate() {
385
505
  }
386
506
 
387
507
  // ---------------------------------------------------------------------------
388
- // morph(existingRoot, newHTML) patch existing DOM to match newHTML
508
+ // morph(existingRoot, newHTML) - patch existing DOM to match newHTML
389
509
  // ---------------------------------------------------------------------------
390
510
 
391
511
  /**
392
512
  * Morph an existing DOM element's children to match new HTML.
393
513
  * Only touches nodes that actually differ.
394
514
  *
395
- * @param {Element} rootEl The live DOM container to patch
396
- * @param {string} newHTML The desired HTML string
515
+ * @param {Element} rootEl - The live DOM container to patch
516
+ * @param {string} newHTML - The desired HTML string
397
517
  */
398
518
  function morph(rootEl, newHTML) {
399
519
  const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
@@ -402,7 +522,7 @@ function morph(rootEl, newHTML) {
402
522
  const newRoot = tpl.content;
403
523
 
404
524
  // Move children into a wrapper for consistent handling.
405
- // We move (not clone) from the template cheaper than cloning.
525
+ // We move (not clone) from the template - cheaper than cloning.
406
526
  const tempDiv = document.createElement('div');
407
527
  while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild);
408
528
 
@@ -412,16 +532,16 @@ function morph(rootEl, newHTML) {
412
532
  }
413
533
 
414
534
  /**
415
- * Morph a single element in place diffs attributes and children
535
+ * Morph a single element in place - diffs attributes and children
416
536
  * without replacing the node reference. Useful for replaceWith-style
417
537
  * updates where you want to keep the element identity when the tag
418
538
  * name matches.
419
539
  *
420
540
  * If the new HTML produces a different tag, falls back to native replace.
421
541
  *
422
- * @param {Element} oldEl The live DOM element to patch
423
- * @param {string} newHTML HTML string for the replacement element
424
- * @returns {Element} The resulting element (same ref if morphed, new if replaced)
542
+ * @param {Element} oldEl - The live DOM element to patch
543
+ * @param {string} newHTML - HTML string for the replacement element
544
+ * @returns {Element} - The resulting element (same ref if morphed, new if replaced)
425
545
  */
426
546
  function morphElement(oldEl, newHTML) {
427
547
  const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
@@ -430,7 +550,7 @@ function morphElement(oldEl, newHTML) {
430
550
  const newEl = tpl.content.firstElementChild;
431
551
  if (!newEl) return oldEl;
432
552
 
433
- // Same tag morph in place (preserves identity, event listeners, refs)
553
+ // Same tag - morph in place (preserves identity, event listeners, refs)
434
554
  if (oldEl.nodeName === newEl.nodeName) {
435
555
  _morphAttributes(oldEl, newEl);
436
556
  _morphChildren(oldEl, newEl);
@@ -438,14 +558,14 @@ function morphElement(oldEl, newHTML) {
438
558
  return oldEl;
439
559
  }
440
560
 
441
- // Different tag must replace (can't morph <div> into <span>)
561
+ // Different tag - must replace (can't morph <div> into <span>)
442
562
  const clone = newEl.cloneNode(true);
443
563
  oldEl.parentNode.replaceChild(clone, oldEl);
444
564
  if (start) window.__zqMorphHook(clone, performance.now() - start);
445
565
  return clone;
446
566
  }
447
567
 
448
- // Aliases for the concat build core.js imports these as _morph / _morphElement,
568
+ // Aliases for the concat build - core.js imports these as _morph / _morphElement,
449
569
  // but the build strips `import … as` lines, so the aliases must exist at runtime.
450
570
  const _morph = morph;
451
571
  const _morphElement = morphElement;
@@ -453,11 +573,11 @@ const _morphElement = morphElement;
453
573
  /**
454
574
  * Reconcile children of `oldParent` to match `newParent`.
455
575
  *
456
- * @param {Element} oldParent live DOM parent
457
- * @param {Element} newParent desired state parent
576
+ * @param {Element} oldParent - live DOM parent
577
+ * @param {Element} newParent - desired state parent
458
578
  */
459
579
  function _morphChildren(oldParent, newParent) {
460
- // Snapshot live NodeLists into arrays childNodes is live and
580
+ // Snapshot live NodeLists into arrays - childNodes is live and
461
581
  // mutates during insertBefore/removeChild. Using a for loop to push
462
582
  // avoids spread operator overhead for large child lists.
463
583
  const oldCN = oldParent.childNodes;
@@ -469,7 +589,7 @@ function _morphChildren(oldParent, newParent) {
469
589
  for (let i = 0; i < oldLen; i++) oldChildren[i] = oldCN[i];
470
590
  for (let i = 0; i < newLen; i++) newChildren[i] = newCN[i];
471
591
 
472
- // Scan for keyed elements only build maps if keys exist
592
+ // Scan for keyed elements - only build maps if keys exist
473
593
  let hasKeys = false;
474
594
  let oldKeyMap, newKeyMap;
475
595
 
@@ -500,7 +620,7 @@ function _morphChildren(oldParent, newParent) {
500
620
  }
501
621
 
502
622
  /**
503
- * Unkeyed reconciliation positional matching.
623
+ * Unkeyed reconciliation - positional matching.
504
624
  */
505
625
  function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
506
626
  const oldLen = oldChildren.length;
@@ -528,7 +648,7 @@ function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
528
648
  }
529
649
 
530
650
  /**
531
- * Keyed reconciliation match by z-key, reorder with minimal moves
651
+ * Keyed reconciliation - match by z-key, reorder with minimal moves
532
652
  * using Longest Increasing Subsequence (LIS) to find the maximum set
533
653
  * of nodes that are already in the correct relative order, then only
534
654
  * move the remaining nodes.
@@ -562,7 +682,7 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
562
682
 
563
683
  // Step 3: Build index array for LIS of matched old indices.
564
684
  // This finds the largest set of keyed nodes already in order,
565
- // so we only need to move the rest O(n log n) instead of O(n²).
685
+ // so we only need to move the rest - O(n log n) instead of O(n²).
566
686
  const oldIndices = []; // Maps new-position → old-position (or -1)
567
687
  for (let i = 0; i < newLen; i++) {
568
688
  if (matched[i]) {
@@ -575,7 +695,7 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
575
695
 
576
696
  const lisSet = _lis(oldIndices);
577
697
 
578
- // Step 4: Insert / reorder / morph walk new children forward,
698
+ // Step 4: Insert / reorder / morph - walk new children forward,
579
699
  // using LIS to decide which nodes stay in place.
580
700
  let cursor = oldParent.firstChild;
581
701
  const unkeyedOld = [];
@@ -600,7 +720,7 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
600
720
  if (!lisSet.has(i)) {
601
721
  oldParent.insertBefore(oldNode, cursor);
602
722
  }
603
- // Capture next sibling BEFORE _morphNode if _morphNode calls
723
+ // Capture next sibling BEFORE _morphNode - if _morphNode calls
604
724
  // replaceChild, oldNode is removed and nextSibling becomes stale.
605
725
  const nextSib = oldNode.nextSibling;
606
726
  _morphNode(oldParent, oldNode, newNode);
@@ -640,10 +760,10 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
640
760
  * Returns a Set of positions (in the input) that form the LIS.
641
761
  * Entries with value -1 (unmatched) are excluded.
642
762
  *
643
- * O(n log n) same algorithm used by Vue 3 and ivi.
763
+ * O(n log n) - same algorithm used by Vue 3 and ivi.
644
764
  *
645
- * @param {number[]} arr array of old-tree indices (-1 = unmatched)
646
- * @returns {Set<number>} positions in arr belonging to the LIS
765
+ * @param {number[]} arr - array of old-tree indices (-1 = unmatched)
766
+ * @returns {Set<number>} - positions in arr belonging to the LIS
647
767
  */
648
768
  function _lis(arr) {
649
769
  const len = arr.length;
@@ -687,7 +807,7 @@ function _lis(arr) {
687
807
  * Morph a single node in place.
688
808
  */
689
809
  function _morphNode(parent, oldNode, newNode) {
690
- // Text / comment nodes just update content
810
+ // Text / comment nodes - just update content
691
811
  if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
692
812
  if (newNode.nodeType === oldNode.nodeType) {
693
813
  if (oldNode.nodeValue !== newNode.nodeValue) {
@@ -695,26 +815,26 @@ function _morphNode(parent, oldNode, newNode) {
695
815
  }
696
816
  return;
697
817
  }
698
- // Different node types replace
818
+ // Different node types - replace
699
819
  parent.replaceChild(newNode.cloneNode(true), oldNode);
700
820
  return;
701
821
  }
702
822
 
703
- // Different node types or tag names replace entirely
823
+ // Different node types or tag names - replace entirely
704
824
  if (oldNode.nodeType !== newNode.nodeType ||
705
825
  oldNode.nodeName !== newNode.nodeName) {
706
826
  parent.replaceChild(newNode.cloneNode(true), oldNode);
707
827
  return;
708
828
  }
709
829
 
710
- // Both are elements diff attributes then recurse children
830
+ // Both are elements - diff attributes then recurse children
711
831
  if (oldNode.nodeType === 1) {
712
- // z-skip: developer opt-out skip diffing this subtree entirely.
832
+ // z-skip: developer opt-out - skip diffing this subtree entirely.
713
833
  // Useful for third-party widgets, canvas, video, or large static content.
714
834
  if (oldNode.hasAttribute('z-skip')) return;
715
835
 
716
836
  // Fast bail-out: if the elements are identical, skip everything.
717
- // isEqualNode() is a native C++ comparison much faster than walking
837
+ // isEqualNode() is a native C++ comparison - much faster than walking
718
838
  // attributes + children in JS when trees haven't changed.
719
839
  if (oldNode.isEqualNode(newNode)) return;
720
840
 
@@ -740,7 +860,7 @@ function _morphNode(parent, oldNode, newNode) {
740
860
  return;
741
861
  }
742
862
 
743
- // Generic element recurse children
863
+ // Generic element - recurse children
744
864
  _morphChildren(oldNode, newNode);
745
865
  }
746
866
  }
@@ -782,7 +902,7 @@ function _morphAttributes(oldEl, newEl) {
782
902
  }
783
903
  }
784
904
 
785
- // Remove stale attributes snapshot names first because oldAttrs
905
+ // Remove stale attributes - snapshot names first because oldAttrs
786
906
  // is a live NamedNodeMap that mutates on removeAttribute().
787
907
  const oldNames = new Array(oldLen);
788
908
  for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
@@ -798,7 +918,7 @@ function _morphAttributes(oldEl, newEl) {
798
918
  *
799
919
  * Only updates the value when the new HTML explicitly carries a `value`
800
920
  * attribute. Templates that use z-model manage values through reactive
801
- * state + _bindModels the morph engine should not interfere by wiping
921
+ * state + _bindModels - the morph engine should not interfere by wiping
802
922
  * a live input's content to '' just because the template has no `value`
803
923
  * attr. This prevents the wipe-then-restore cycle that resets cursor
804
924
  * position on every keystroke.
@@ -828,14 +948,14 @@ function _syncInputValue(oldEl, newEl) {
828
948
  *
829
949
  * This means the LIS-optimised keyed path activates automatically
830
950
  * whenever elements carry `id` or `data-id` / `data-key` attributes
831
- * no extra markup required.
951
+ * - no extra markup required.
832
952
  *
833
953
  * @returns {string|null}
834
954
  */
835
955
  function _getKey(node) {
836
956
  if (node.nodeType !== 1) return null;
837
957
 
838
- // Explicit z-key highest priority
958
+ // Explicit z-key - highest priority
839
959
  const zk = node.getAttribute('z-key');
840
960
  if (zk) return zk;
841
961
 
@@ -854,7 +974,7 @@ function _getKey(node) {
854
974
 
855
975
  // --- src/core.js -------------------------------------------------
856
976
  /**
857
- * zQuery Core Selector engine & chainable DOM collection
977
+ * zQuery Core - Selector engine & chainable DOM collection
858
978
  *
859
979
  * Extends the quick-ref pattern (Id, Class, Classes, Children)
860
980
  * into a full jQuery-like chainable wrapper with modern APIs.
@@ -862,7 +982,7 @@ function _getKey(node) {
862
982
 
863
983
 
864
984
  // ---------------------------------------------------------------------------
865
- // ZQueryCollection wraps an array of elements with chainable methods
985
+ // ZQueryCollection - wraps an array of elements with chainable methods
866
986
  // ---------------------------------------------------------------------------
867
987
  class ZQueryCollection {
868
988
  constructor(elements) {
@@ -1082,7 +1202,7 @@ class ZQueryCollection {
1082
1202
  // --- Classes -------------------------------------------------------------
1083
1203
 
1084
1204
  addClass(...names) {
1085
- // Fast path: single class, no spaces avoids flatMap + regex split allocation
1205
+ // Fast path: single class, no spaces - avoids flatMap + regex split allocation
1086
1206
  if (names.length === 1 && names[0].indexOf(' ') === -1) {
1087
1207
  const c = names[0];
1088
1208
  for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(c);
@@ -1244,7 +1364,7 @@ class ZQueryCollection {
1244
1364
  if (content === undefined) return this.first()?.innerHTML;
1245
1365
  // Auto-morph: if the element already has children, use the diff engine
1246
1366
  // to patch the DOM (preserves focus, scroll, state, keyed reorder via LIS).
1247
- // Empty elements get raw innerHTML for fast first-paint same strategy
1367
+ // Empty elements get raw innerHTML for fast first-paint - same strategy
1248
1368
  // the component system uses (first render = innerHTML, updates = morph).
1249
1369
  return this.each((_, el) => {
1250
1370
  if (el.childNodes.length > 0) {
@@ -1429,7 +1549,7 @@ class ZQueryCollection {
1429
1549
  if (typeof selectorOrHandler === 'function') {
1430
1550
  el.addEventListener(evt, selectorOrHandler);
1431
1551
  } else if (typeof selectorOrHandler === 'string') {
1432
- // Delegated event store wrapper so off() can remove it
1552
+ // Delegated event - store wrapper so off() can remove it
1433
1553
  const wrapper = (e) => {
1434
1554
  if (!e.target || typeof e.target.closest !== 'function') return;
1435
1555
  const target = e.target.closest(selectorOrHandler);
@@ -1491,7 +1611,7 @@ class ZQueryCollection {
1491
1611
  // --- Animation -----------------------------------------------------------
1492
1612
 
1493
1613
  animate(props, duration = 300, easing = 'ease') {
1494
- // Empty collection resolve immediately
1614
+ // Empty collection - resolve immediately
1495
1615
  if (this.length === 0) return Promise.resolve(this);
1496
1616
  return new Promise(resolve => {
1497
1617
  let resolved = false;
@@ -1617,7 +1737,7 @@ class ZQueryCollection {
1617
1737
 
1618
1738
 
1619
1739
  // ---------------------------------------------------------------------------
1620
- // Helper create document fragment from HTML string
1740
+ // Helper - create document fragment from HTML string
1621
1741
  // ---------------------------------------------------------------------------
1622
1742
  function createFragment(html) {
1623
1743
  const tpl = document.createElement('template');
@@ -1627,21 +1747,21 @@ function createFragment(html) {
1627
1747
 
1628
1748
 
1629
1749
  // ---------------------------------------------------------------------------
1630
- // $() main selector / creator (returns ZQueryCollection, like jQuery)
1750
+ // $() - main selector / creator (returns ZQueryCollection, like jQuery)
1631
1751
  // ---------------------------------------------------------------------------
1632
1752
  function query(selector, context) {
1633
1753
  // null / undefined
1634
1754
  if (!selector) return new ZQueryCollection([]);
1635
1755
 
1636
- // Already a collection return as-is
1756
+ // Already a collection - return as-is
1637
1757
  if (selector instanceof ZQueryCollection) return selector;
1638
1758
 
1639
- // DOM element or Window wrap in collection
1759
+ // DOM element or Window - wrap in collection
1640
1760
  if (selector instanceof Node || selector === window) {
1641
1761
  return new ZQueryCollection([selector]);
1642
1762
  }
1643
1763
 
1644
- // NodeList / HTMLCollection / Array wrap in collection
1764
+ // NodeList / HTMLCollection / Array - wrap in collection
1645
1765
  if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
1646
1766
  return new ZQueryCollection(Array.from(selector));
1647
1767
  }
@@ -1665,7 +1785,7 @@ function query(selector, context) {
1665
1785
 
1666
1786
 
1667
1787
  // ---------------------------------------------------------------------------
1668
- // $.all() collection selector (returns ZQueryCollection for CSS selectors)
1788
+ // $.all() - collection selector (returns ZQueryCollection for CSS selectors)
1669
1789
  // ---------------------------------------------------------------------------
1670
1790
  function queryAll(selector, context) {
1671
1791
  // null / undefined
@@ -1720,7 +1840,7 @@ query.children = (parentId) => {
1720
1840
  query.qs = (sel, ctx = document) => ctx.querySelector(sel);
1721
1841
  query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
1722
1842
 
1723
- // Create element shorthand returns ZQueryCollection for chaining
1843
+ // Create element shorthand - returns ZQueryCollection for chaining
1724
1844
  query.create = (tag, attrs = {}, ...children) => {
1725
1845
  const el = document.createElement(tag);
1726
1846
  for (const [k, v] of Object.entries(attrs)) {
@@ -1743,7 +1863,7 @@ query.ready = (fn) => {
1743
1863
  else document.addEventListener('DOMContentLoaded', fn);
1744
1864
  };
1745
1865
 
1746
- // Global event listeners supports direct, delegated, and target-bound forms
1866
+ // Global event listeners - supports direct, delegated, and target-bound forms
1747
1867
  // $.on('keydown', handler) → direct listener on document
1748
1868
  // $.on('click', '.btn', handler) → delegated via closest()
1749
1869
  // $.on('scroll', window, handler) → direct listener on target
@@ -1753,7 +1873,7 @@ query.on = (event, selectorOrHandler, handler) => {
1753
1873
  document.addEventListener(event, selectorOrHandler);
1754
1874
  return;
1755
1875
  }
1756
- // EventTarget (window, element, etc.) direct listener on target
1876
+ // EventTarget (window, element, etc.) - direct listener on target
1757
1877
  if (typeof selectorOrHandler === 'object' && typeof selectorOrHandler.addEventListener === 'function') {
1758
1878
  selectorOrHandler.addEventListener(event, handler);
1759
1879
  return;
@@ -1776,7 +1896,7 @@ query.fn = ZQueryCollection.prototype;
1776
1896
 
1777
1897
  // --- src/expression.js -------------------------------------------
1778
1898
  /**
1779
- * zQuery Expression Parser CSP-safe expression evaluator
1899
+ * zQuery Expression Parser - CSP-safe expression evaluator
1780
1900
  *
1781
1901
  * Replaces `new Function()` / `eval()` with a hand-written parser that
1782
1902
  * evaluates expressions safely without violating Content Security Policy.
@@ -1956,7 +2076,7 @@ function tokenize(expr) {
1956
2076
  i++; continue;
1957
2077
  }
1958
2078
 
1959
- // Unknown skip
2079
+ // Unknown - skip
1960
2080
  i++;
1961
2081
  }
1962
2082
 
@@ -1965,7 +2085,7 @@ function tokenize(expr) {
1965
2085
  }
1966
2086
 
1967
2087
  // ---------------------------------------------------------------------------
1968
- // Parser Pratt (precedence climbing)
2088
+ // Parser - Pratt (precedence climbing)
1969
2089
  // ---------------------------------------------------------------------------
1970
2090
  class Parser {
1971
2091
  constructor(tokens, scope) {
@@ -2197,7 +2317,7 @@ class Parser {
2197
2317
  let couldBeArrow = true;
2198
2318
 
2199
2319
  if (this.peek().t === T.PUNC && this.peek().v === ')') {
2200
- // () => ... no params
2320
+ // () => ... - no params
2201
2321
  } else {
2202
2322
  while (couldBeArrow) {
2203
2323
  const p = this.peek();
@@ -2223,7 +2343,7 @@ class Parser {
2223
2343
  }
2224
2344
  }
2225
2345
 
2226
- // Not an arrow restore and parse as grouping
2346
+ // Not an arrow - restore and parse as grouping
2227
2347
  this.pos = savedPos;
2228
2348
  this.next(); // consume (
2229
2349
  const expr = this.parseExpression(0);
@@ -2316,14 +2436,14 @@ class Parser {
2316
2436
  return { type: 'ident', name: tok.v };
2317
2437
  }
2318
2438
 
2319
- // Fallback return undefined for unparseable
2439
+ // Fallback - return undefined for unparseable
2320
2440
  this.next();
2321
2441
  return { type: 'literal', value: undefined };
2322
2442
  }
2323
2443
  }
2324
2444
 
2325
2445
  // ---------------------------------------------------------------------------
2326
- // Evaluator walks the AST, resolves against scope
2446
+ // Evaluator - walks the AST, resolves against scope
2327
2447
  // ---------------------------------------------------------------------------
2328
2448
 
2329
2449
  /** Safe property access whitelist for built-in prototypes */
@@ -2412,8 +2532,6 @@ function evaluate(node, scope) {
2412
2532
  if (name === 'console') return console;
2413
2533
  if (name === 'Map') return Map;
2414
2534
  if (name === 'Set') return Set;
2415
- if (name === 'RegExp') return RegExp;
2416
- if (name === 'Error') return Error;
2417
2535
  if (name === 'URL') return URL;
2418
2536
  if (name === 'URLSearchParams') return URLSearchParams;
2419
2537
  return undefined;
@@ -2456,7 +2574,7 @@ function evaluate(node, scope) {
2456
2574
  case 'optional_call': {
2457
2575
  const calleeNode = node.callee;
2458
2576
  const args = _evalArgs(node.args, scope);
2459
- // Method call: obj?.method() bind `this` to obj
2577
+ // Method call: obj?.method() - bind `this` to obj
2460
2578
  if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
2461
2579
  const obj = evaluate(calleeNode.obj, scope);
2462
2580
  if (obj == null) return undefined;
@@ -2475,9 +2593,9 @@ function evaluate(node, scope) {
2475
2593
  case 'new': {
2476
2594
  const Ctor = evaluate(node.callee, scope);
2477
2595
  if (typeof Ctor !== 'function') return undefined;
2478
- // Only allow safe constructors
2596
+ // Only allow safe constructors (no RegExp - ReDoS risk, no Error - info leak)
2479
2597
  if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
2480
- Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
2598
+ Ctor === URL || Ctor === URLSearchParams) {
2481
2599
  const args = _evalArgs(node.args, scope);
2482
2600
  return new Ctor(...args);
2483
2601
  }
@@ -2579,7 +2697,7 @@ function _resolveCall(node, scope) {
2579
2697
  const callee = node.callee;
2580
2698
  const args = _evalArgs(node.args, scope);
2581
2699
 
2582
- // Method call: obj.method() bind `this` to obj
2700
+ // Method call: obj.method() - bind `this` to obj
2583
2701
  if (callee.type === 'member' || callee.type === 'optional_member') {
2584
2702
  const obj = evaluate(callee.obj, scope);
2585
2703
  if (obj == null) return undefined;
@@ -2645,13 +2763,13 @@ function _evalBinary(node, scope) {
2645
2763
  /**
2646
2764
  * Safely evaluate a JS expression string against scope layers.
2647
2765
  *
2648
- * @param {string} expr expression string
2649
- * @param {object[]} scope array of scope objects, checked in order
2766
+ * @param {string} expr - expression string
2767
+ * @param {object[]} scope - array of scope objects, checked in order
2650
2768
  * Typical: [loopVars, state, { props, refs, $ }]
2651
- * @returns {*} evaluation result, or undefined on error
2769
+ * @returns {*} - evaluation result, or undefined on error
2652
2770
  */
2653
2771
 
2654
- // AST cache (LRU) avoids re-tokenizing and re-parsing the same expression.
2772
+ // AST cache (LRU) - avoids re-tokenizing and re-parsing the same expression.
2655
2773
  // Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
2656
2774
  // Eviction removes the least-recently-used (first) entry when at capacity.
2657
2775
  const _astCache = new Map();
@@ -2702,7 +2820,7 @@ function safeEval(expr, scope) {
2702
2820
 
2703
2821
  // --- src/component.js --------------------------------------------
2704
2822
  /**
2705
- * zQuery Component Lightweight reactive component system
2823
+ * zQuery Component - Lightweight reactive component system
2706
2824
  *
2707
2825
  * Declarative components using template literals with directive support.
2708
2826
  * Proxy-based state triggers targeted re-renders via event delegation.
@@ -2718,7 +2836,7 @@ function safeEval(expr, scope) {
2718
2836
  * - Scoped styles (inline or via styleUrl)
2719
2837
  * - External templates via templateUrl (with {{expression}} interpolation)
2720
2838
  * - External styles via styleUrl (fetched & scoped automatically)
2721
- * - Relative path resolution templateUrl and styleUrl
2839
+ * - Relative path resolution - templateUrl and styleUrl
2722
2840
  * resolve relative to the component file automatically
2723
2841
  */
2724
2842
 
@@ -2726,6 +2844,7 @@ function safeEval(expr, scope) {
2726
2844
 
2727
2845
 
2728
2846
 
2847
+
2729
2848
  // ---------------------------------------------------------------------------
2730
2849
  // Component registry & external resource cache
2731
2850
  // ---------------------------------------------------------------------------
@@ -2750,7 +2869,7 @@ const _throttleTimers = new WeakMap();
2750
2869
 
2751
2870
  /**
2752
2871
  * Fetch and cache a text resource (HTML template or CSS file).
2753
- * @param {string} url URL to fetch
2872
+ * @param {string} url - URL to fetch
2754
2873
  * @returns {Promise<string>}
2755
2874
  */
2756
2875
  function _fetchResource(url) {
@@ -2794,23 +2913,23 @@ function _fetchResource(url) {
2794
2913
  * - If `base` is an absolute URL (http/https/file), resolve directly.
2795
2914
  * - If `base` is a relative path string, resolve it against the page root
2796
2915
  * (or <base href>) first, then resolve `url` against that.
2797
- * - If `base` is falsy, return `url` unchanged _fetchResource's own
2916
+ * - If `base` is falsy, return `url` unchanged - _fetchResource's own
2798
2917
  * fallback (page root / <base href>) handles it.
2799
2918
  *
2800
- * @param {string} url URL or relative path to resolve
2801
- * @param {string} [base] auto-detected caller URL or explicit base path
2919
+ * @param {string} url - URL or relative path to resolve
2920
+ * @param {string} [base] - auto-detected caller URL or explicit base path
2802
2921
  * @returns {string}
2803
2922
  */
2804
2923
  function _resolveUrl(url, base) {
2805
2924
  if (!base || !url || typeof url !== 'string') return url;
2806
- // Already absolute nothing to do
2925
+ // Already absolute - nothing to do
2807
2926
  if (url.startsWith('/') || url.includes('://') || url.startsWith('//')) return url;
2808
2927
  try {
2809
2928
  if (base.includes('://')) {
2810
2929
  // Absolute base (auto-detected module URL)
2811
2930
  return new URL(url, base).href;
2812
2931
  }
2813
- // Relative base string resolve against page root first
2932
+ // Relative base string - resolve against page root first
2814
2933
  const baseEl = document.querySelector('base');
2815
2934
  const root = baseEl ? baseEl.href : (window.location.origin + '/');
2816
2935
  const absBase = new URL(base.endsWith('/') ? base : base + '/', root).href;
@@ -2842,13 +2961,13 @@ function _detectCallerBase() {
2842
2961
  for (const raw of urls) {
2843
2962
  // Strip line:col suffixes e.g. ":3:5" or ":12:1"
2844
2963
  const url = raw.replace(/:\d+:\d+$/, '').replace(/:\d+$/, '');
2845
- // Skip the zQuery library itself by filename pattern and captured URL
2964
+ // Skip the zQuery library itself - by filename pattern and captured URL
2846
2965
  if (/zquery(\.min)?\.js$/i.test(url)) continue;
2847
2966
  if (_ownScriptUrl && url.replace(/[?#].*$/, '') === _ownScriptUrl) continue;
2848
2967
  // Return directory (strip filename, keep trailing slash)
2849
2968
  return url.replace(/\/[^/]*$/, '/');
2850
2969
  }
2851
- } catch { /* stack parsing unsupported fall back silently */ }
2970
+ } catch { /* stack parsing unsupported - fall back silently */ }
2852
2971
  return undefined;
2853
2972
  }
2854
2973
 
@@ -2927,7 +3046,7 @@ class Component {
2927
3046
  }
2928
3047
  });
2929
3048
 
2930
- // Computed properties lazy getters derived from state
3049
+ // Computed properties - lazy getters derived from state
2931
3050
  this.computed = {};
2932
3051
  if (definition.computed) {
2933
3052
  for (const [name, fn] of Object.entries(definition.computed)) {
@@ -3056,7 +3175,7 @@ class Component {
3056
3175
  this._loadExternals().then(() => {
3057
3176
  if (!this._destroyed) this._render();
3058
3177
  });
3059
- return; // Skip this render will re-render after load
3178
+ return; // Skip this render - will re-render after load
3060
3179
  }
3061
3180
 
3062
3181
  // Expose multi-template map on instance (if available)
@@ -3081,7 +3200,7 @@ class Component {
3081
3200
  this.state.__raw || this.state,
3082
3201
  { props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
3083
3202
  ]);
3084
- return result != null ? result : '';
3203
+ return result != null ? escapeHtml(String(result)) : '';
3085
3204
  } catch { return ''; }
3086
3205
  });
3087
3206
  } else {
@@ -3127,13 +3246,13 @@ class Component {
3127
3246
  const trimmed = selector.trim();
3128
3247
  // Don't scope @-rules themselves
3129
3248
  if (trimmed.startsWith('@')) {
3130
- // @keyframes and @font-face contain non-selector content skip scoping inside them
3249
+ // @keyframes and @font-face contain non-selector content - skip scoping inside them
3131
3250
  if (/^@(keyframes|font-face)\b/.test(trimmed)) {
3132
3251
  noScopeDepth = braceDepth;
3133
3252
  }
3134
3253
  return match;
3135
3254
  }
3136
- // Inside @keyframes or @font-face don't scope inner rules
3255
+ // Inside @keyframes or @font-face - don't scope inner rules
3137
3256
  if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
3138
3257
  return match;
3139
3258
  }
@@ -3186,7 +3305,7 @@ class Component {
3186
3305
  }
3187
3306
  }
3188
3307
 
3189
- // Update DOM via morphing (diffing) preserves unchanged nodes
3308
+ // Update DOM via morphing (diffing) - preserves unchanged nodes
3190
3309
  // First render uses innerHTML for speed; subsequent renders morph.
3191
3310
  const _renderStart = typeof window !== 'undefined' && (window.__zqMorphHook || window.__zqRenderHook) ? performance.now() : 0;
3192
3311
  if (!this._mounted) {
@@ -3205,8 +3324,8 @@ class Component {
3205
3324
  this._bindModels();
3206
3325
 
3207
3326
  // Restore focus if the morph replaced the focused element.
3208
- // Always restore selectionRange even when the element is still
3209
- // the activeElement because _bindModels or morph attribute syncing
3327
+ // Always restore selectionRange - even when the element is still
3328
+ // the activeElement - because _bindModels or morph attribute syncing
3210
3329
  // can alter the value and move the cursor.
3211
3330
  if (_focusInfo) {
3212
3331
  const el = this._el.querySelector(_focusInfo.selector);
@@ -3242,7 +3361,7 @@ class Component {
3242
3361
  // Optimization: on the FIRST render, we scan for event attributes, build
3243
3362
  // a delegated handler map, and attach one listener per event type to the
3244
3363
  // component root. On subsequent renders (re-bind), we only rebuild the
3245
- // internal binding map existing DOM listeners are reused since they
3364
+ // internal binding map - existing DOM listeners are reused since they
3246
3365
  // delegate to event.target.closest(selector) at fire time.
3247
3366
  _bindEvents() {
3248
3367
  // Always rebuild the binding map from current DOM
@@ -3283,11 +3402,11 @@ class Component {
3283
3402
  // Store binding map for the delegated handlers to reference
3284
3403
  this._eventBindings = eventMap;
3285
3404
 
3286
- // Only attach DOM listeners once reuse on subsequent renders.
3405
+ // Only attach DOM listeners once - reuse on subsequent renders.
3287
3406
  // The handlers close over `this` and read `this._eventBindings`
3288
3407
  // at fire time, so they always use the latest binding map.
3289
3408
  if (this._delegatedEvents) {
3290
- // Already attached just make sure new event types are covered
3409
+ // Already attached - just make sure new event types are covered
3291
3410
  for (const event of eventMap.keys()) {
3292
3411
  if (!this._delegatedEvents.has(event)) {
3293
3412
  this._attachDelegatedEvent(event, eventMap.get(event));
@@ -3313,7 +3432,7 @@ class Component {
3313
3432
  this._attachDelegatedEvent(event, bindings);
3314
3433
  }
3315
3434
 
3316
- // .outside attach a document-level listener for bindings that need
3435
+ // .outside - attach a document-level listener for bindings that need
3317
3436
  // to detect clicks/events outside their element.
3318
3437
  this._outsideListeners = this._outsideListeners || [];
3319
3438
  for (const [event, bindings] of eventMap) {
@@ -3341,7 +3460,7 @@ class Component {
3341
3460
  : false;
3342
3461
 
3343
3462
  const handler = (e) => {
3344
- // Read bindings from live map always up to date after re-renders
3463
+ // Read bindings from live map - always up to date after re-renders
3345
3464
  const currentBindings = this._eventBindings?.get(event) || [];
3346
3465
 
3347
3466
  // Collect matching bindings with their matched elements, then sort
@@ -3362,7 +3481,7 @@ class Component {
3362
3481
  for (const { selector, methodExpr, modifiers, el, matched } of hits) {
3363
3482
 
3364
3483
  // In delegated events, .stop should prevent ancestor bindings from
3365
- // firing stopPropagation alone only stops real DOM bubbling.
3484
+ // firing - stopPropagation alone only stops real DOM bubbling.
3366
3485
  if (stoppedAt) {
3367
3486
  let blocked = false;
3368
3487
  for (const stopped of stoppedAt) {
@@ -3371,15 +3490,15 @@ class Component {
3371
3490
  if (blocked) continue;
3372
3491
  }
3373
3492
 
3374
- // .self only fire if target is the element itself
3493
+ // .self - only fire if target is the element itself
3375
3494
  if (modifiers.includes('self') && e.target !== el) continue;
3376
3495
 
3377
- // .outside only fire if event target is OUTSIDE the element
3496
+ // .outside - only fire if event target is OUTSIDE the element
3378
3497
  if (modifiers.includes('outside')) {
3379
3498
  if (el.contains(e.target)) continue;
3380
3499
  }
3381
3500
 
3382
- // Key modifiers filter keyboard events by key
3501
+ // Key modifiers - filter keyboard events by key
3383
3502
  const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
3384
3503
  let keyFiltered = false;
3385
3504
  for (const mod of modifiers) {
@@ -3390,7 +3509,7 @@ class Component {
3390
3509
  }
3391
3510
  if (keyFiltered) continue;
3392
3511
 
3393
- // System key modifiers require modifier keys to be held
3512
+ // System key modifiers - require modifier keys to be held
3394
3513
  if (modifiers.includes('ctrl') && !e.ctrlKey) continue;
3395
3514
  if (modifiers.includes('shift') && !e.shiftKey) continue;
3396
3515
  if (modifiers.includes('alt') && !e.altKey) continue;
@@ -3430,7 +3549,7 @@ class Component {
3430
3549
  }
3431
3550
  };
3432
3551
 
3433
- // .debounce.{ms} delay invocation until idle
3552
+ // .debounce.{ms} - delay invocation until idle
3434
3553
  const debounceIdx = modifiers.indexOf('debounce');
3435
3554
  if (debounceIdx !== -1) {
3436
3555
  const ms = parseInt(modifiers[debounceIdx + 1], 10) || 250;
@@ -3441,7 +3560,7 @@ class Component {
3441
3560
  continue;
3442
3561
  }
3443
3562
 
3444
- // .throttle.{ms} fire at most once per interval
3563
+ // .throttle.{ms} - fire at most once per interval
3445
3564
  const throttleIdx = modifiers.indexOf('throttle');
3446
3565
  if (throttleIdx !== -1) {
3447
3566
  const ms = parseInt(modifiers[throttleIdx + 1], 10) || 250;
@@ -3453,7 +3572,7 @@ class Component {
3453
3572
  continue;
3454
3573
  }
3455
3574
 
3456
- // .once fire once then ignore
3575
+ // .once - fire once then ignore
3457
3576
  if (modifiers.includes('once')) {
3458
3577
  if (el.dataset.zqOnce === event) continue;
3459
3578
  el.dataset.zqOnce = event;
@@ -3481,12 +3600,12 @@ class Component {
3481
3600
  // textarea, select (single & multiple), contenteditable
3482
3601
  // Nested state keys: z-model="user.name" → this.state.user.name
3483
3602
  // Modifiers (boolean attributes on the same element):
3484
- // z-lazy listen on 'change' instead of 'input' (update on blur / commit)
3485
- // z-trim trim whitespace before writing to state
3486
- // z-number force Number() conversion regardless of input type
3487
- // z-debounce debounce state writes (default 250ms, or z-debounce="300")
3488
- // z-uppercase convert string to uppercase before writing to state
3489
- // z-lowercase convert string to lowercase before writing to state
3603
+ // z-lazy - listen on 'change' instead of 'input' (update on blur / commit)
3604
+ // z-trim - trim whitespace before writing to state
3605
+ // z-number - force Number() conversion regardless of input type
3606
+ // z-debounce - debounce state writes (default 250ms, or z-debounce="300")
3607
+ // z-uppercase - convert string to uppercase before writing to state
3608
+ // z-lowercase - convert string to lowercase before writing to state
3490
3609
  //
3491
3610
  // Writes to reactive state so the rest of the UI stays in sync.
3492
3611
  // Focus and cursor position are preserved in _render() via focusInfo.
@@ -3568,7 +3687,7 @@ class Component {
3568
3687
  }
3569
3688
 
3570
3689
  // ---------------------------------------------------------------------------
3571
- // Expression evaluator CSP-safe parser (no eval / new Function)
3690
+ // Expression evaluator - CSP-safe parser (no eval / new Function)
3572
3691
  // ---------------------------------------------------------------------------
3573
3692
  _evalExpr(expr) {
3574
3693
  return safeEval(expr, [
@@ -3578,7 +3697,7 @@ class Component {
3578
3697
  }
3579
3698
 
3580
3699
  // ---------------------------------------------------------------------------
3581
- // z-for Expand list-rendering directives (pre-innerHTML, string level)
3700
+ // z-for - Expand list-rendering directives (pre-innerHTML, string level)
3582
3701
  //
3583
3702
  // <li z-for="item in items">{{item.name}}</li>
3584
3703
  // <li z-for="(item, i) in items">{{i}}: {{item.name}}</li>
@@ -3644,7 +3763,7 @@ class Component {
3644
3763
  this.state.__raw || this.state,
3645
3764
  { props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
3646
3765
  ]);
3647
- return result != null ? result : '';
3766
+ return result != null ? escapeHtml(String(result)) : '';
3648
3767
  } catch { return ''; }
3649
3768
  });
3650
3769
 
@@ -3667,7 +3786,7 @@ class Component {
3667
3786
  }
3668
3787
 
3669
3788
  // ---------------------------------------------------------------------------
3670
- // _expandContentDirectives Pre-morph z-html & z-text expansion
3789
+ // _expandContentDirectives - Pre-morph z-html & z-text expansion
3671
3790
  //
3672
3791
  // Evaluates z-html and z-text directives at the string level so the morph
3673
3792
  // engine receives HTML with the actual content inline. This lets the diff
@@ -3702,7 +3821,7 @@ class Component {
3702
3821
  }
3703
3822
 
3704
3823
  // ---------------------------------------------------------------------------
3705
- // _processDirectives Post-innerHTML DOM-level directive processing
3824
+ // _processDirectives - Post-innerHTML DOM-level directive processing
3706
3825
  // ---------------------------------------------------------------------------
3707
3826
  _processDirectives() {
3708
3827
  // z-pre: skip all directive processing on subtrees
@@ -3753,7 +3872,7 @@ class Component {
3753
3872
  });
3754
3873
 
3755
3874
  // -- z-bind:attr / :attr (dynamic attribute binding) -----------
3756
- // Use TreeWalker instead of querySelectorAll('*') avoids
3875
+ // Use TreeWalker instead of querySelectorAll('*') - avoids
3757
3876
  // creating a flat array of every single descendant element.
3758
3877
  // TreeWalker visits nodes lazily; FILTER_REJECT skips z-pre subtrees
3759
3878
  // at the walker level (faster than per-node closest('[z-pre]') checks).
@@ -3891,8 +4010,8 @@ const _reservedKeys = new Set([
3891
4010
 
3892
4011
  /**
3893
4012
  * Register a component
3894
- * @param {string} name tag name (must contain a hyphen, e.g. 'app-counter')
3895
- * @param {object} definition component definition
4013
+ * @param {string} name - tag name (must contain a hyphen, e.g. 'app-counter')
4014
+ * @param {object} definition - component definition
3896
4015
  */
3897
4016
  function component(name, definition) {
3898
4017
  if (!name || typeof name !== 'string') {
@@ -3917,9 +4036,9 @@ function component(name, definition) {
3917
4036
 
3918
4037
  /**
3919
4038
  * Mount a component into a target element
3920
- * @param {string|Element} target selector or element to mount into
3921
- * @param {string} componentName registered component name
3922
- * @param {object} props props to pass
4039
+ * @param {string|Element} target - selector or element to mount into
4040
+ * @param {string} componentName - registered component name
4041
+ * @param {object} props - props to pass
3923
4042
  * @returns {Component}
3924
4043
  */
3925
4044
  function mount(target, componentName, props = {}) {
@@ -3940,7 +4059,7 @@ function mount(target, componentName, props = {}) {
3940
4059
 
3941
4060
  /**
3942
4061
  * Scan a container for custom component tags and auto-mount them
3943
- * @param {Element} root root element to scan (default: document.body)
4062
+ * @param {Element} root - root element to scan (default: document.body)
3944
4063
  */
3945
4064
  function mountAll(root = document.body) {
3946
4065
  for (const [name, def] of _registry) {
@@ -3965,7 +4084,7 @@ function mountAll(root = document.body) {
3965
4084
  [...tag.attributes].forEach(attr => {
3966
4085
  if (attr.name.startsWith('@') || attr.name.startsWith('z-')) return;
3967
4086
 
3968
- // Dynamic prop: :propName="expression" evaluate in parent context
4087
+ // Dynamic prop: :propName="expression" - evaluate in parent context
3969
4088
  if (attr.name.startsWith(':')) {
3970
4089
  const propName = attr.name.slice(1);
3971
4090
  if (parentInstance) {
@@ -3974,7 +4093,7 @@ function mountAll(root = document.body) {
3974
4093
  { props: parentInstance.props, refs: parentInstance.refs, computed: parentInstance.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
3975
4094
  ]);
3976
4095
  } else {
3977
- // No parent try JSON parse
4096
+ // No parent - try JSON parse
3978
4097
  try { props[propName] = JSON.parse(attr.value); }
3979
4098
  catch { props[propName] = attr.value; }
3980
4099
  }
@@ -4023,8 +4142,8 @@ function getRegistry() {
4023
4142
  /**
4024
4143
  * Pre-load a component's external templates and styles so the next mount
4025
4144
  * renders synchronously (no blank flash while fetching).
4026
- * Safe to call multiple times skips if already loaded.
4027
- * @param {string} name registered component name
4145
+ * Safe to call multiple times - skips if already loaded.
4146
+ * @param {string} name - registered component name
4028
4147
  * @returns {Promise<void>}
4029
4148
  */
4030
4149
  async function prefetch(name) {
@@ -4052,27 +4171,27 @@ const _globalStyles = new Map(); // url → <link> element
4052
4171
  *
4053
4172
  * $.style('app.css') // critical by default
4054
4173
  * $.style(['app.css', 'theme.css']) // multiple files
4055
- * $.style('/assets/global.css') // absolute used as-is
4174
+ * $.style('/assets/global.css') // absolute - used as-is
4056
4175
  * $.style('app.css', { critical: false }) // opt out of FOUC prevention
4057
4176
  *
4058
4177
  * Options:
4059
- * critical (boolean, default true) When true, zQuery injects a tiny
4178
+ * critical - (boolean, default true) When true, zQuery injects a tiny
4060
4179
  * inline style that hides the page (`visibility: hidden`) and
4061
4180
  * removes it once the stylesheet has loaded. This prevents
4062
- * FOUC (Flash of Unstyled Content) entirely no special
4181
+ * FOUC (Flash of Unstyled Content) entirely - no special
4063
4182
  * markup needed in the HTML file. Set to false to load
4064
4183
  * the stylesheet without blocking paint.
4065
- * bg (string, default '#0d1117') Background color applied while
4184
+ * bg - (string, default '#0d1117') Background color applied while
4066
4185
  * the page is hidden during critical load. Prevents a white
4067
4186
  * flash on dark-themed apps. Only used when critical is true.
4068
4187
  *
4069
4188
  * Duplicate URLs are ignored (idempotent).
4070
4189
  *
4071
- * @param {string|string[]} urls stylesheet URL(s) to load
4072
- * @param {object} [opts] options
4073
- * @param {boolean} [opts.critical=true] hide page until loaded (prevents FOUC)
4074
- * @param {string} [opts.bg] background color while hidden (default '#0d1117')
4075
- * @returns {{ remove: Function, ready: Promise }} .remove() to unload, .ready resolves when loaded
4190
+ * @param {string|string[]} urls - stylesheet URL(s) to load
4191
+ * @param {object} [opts] - options
4192
+ * @param {boolean} [opts.critical=true] - hide page until loaded (prevents FOUC)
4193
+ * @param {string} [opts.bg] - background color while hidden (default '#0d1117')
4194
+ * @returns {{ remove: Function, ready: Promise }} - .remove() to unload, .ready resolves when loaded
4076
4195
  */
4077
4196
  function style(urls, opts = {}) {
4078
4197
  const callerBase = _detectCallerBase();
@@ -4081,7 +4200,7 @@ function style(urls, opts = {}) {
4081
4200
  const loadPromises = [];
4082
4201
 
4083
4202
  // Critical mode (default: true): inject a tiny inline <style> that hides the
4084
- // page and sets a background color. Fully self-contained no markup needed
4203
+ // page and sets a background color. Fully self-contained - no markup needed
4085
4204
  // in the HTML file. The style is removed once the sheet loads.
4086
4205
  let _criticalStyle = null;
4087
4206
  if (opts.critical !== false) {
@@ -4141,16 +4260,15 @@ function style(urls, opts = {}) {
4141
4260
 
4142
4261
  // --- src/router.js -----------------------------------------------
4143
4262
  /**
4144
- * zQuery Router Client-side SPA router
4263
+ * zQuery Router - Client-side SPA router
4145
4264
  *
4146
4265
  * Supports hash mode (#/path) and history mode (/path).
4147
4266
  * Route params, query strings, navigation guards, and lazy loading.
4148
4267
  * Sub-route history substates for in-page UI changes (modals, tabs, etc.).
4149
4268
  *
4150
4269
  * Usage:
4270
+ * // HTML: <z-outlet></z-outlet>
4151
4271
  * $.router({
4152
- * el: '#app',
4153
- * mode: 'hash',
4154
4272
  * routes: [
4155
4273
  * { path: '/', component: 'home-page' },
4156
4274
  * { path: '/user/:id', component: 'user-profile' },
@@ -4185,7 +4303,7 @@ function _shallowEqual(a, b) {
4185
4303
  class Router {
4186
4304
  constructor(config = {}) {
4187
4305
  this._el = null;
4188
- // file:// protocol can't use pushState always force hash mode
4306
+ // file:// protocol can't use pushState - always force hash mode
4189
4307
  const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
4190
4308
  this._mode = isFile ? 'hash' : (config.mode || 'history');
4191
4309
 
@@ -4220,8 +4338,30 @@ class Router {
4220
4338
  this._inSubstate = false; // true while substate entries are in the history stack
4221
4339
 
4222
4340
  // Set outlet element
4341
+ // Priority: explicit config.el → <z-outlet> tag in the DOM
4223
4342
  if (config.el) {
4224
4343
  this._el = typeof config.el === 'string' ? document.querySelector(config.el) : config.el;
4344
+ } else if (typeof document !== 'undefined') {
4345
+ const outlet = document.querySelector('z-outlet');
4346
+ if (outlet) {
4347
+ this._el = outlet;
4348
+ // Read inline attribute overrides from <z-outlet> (config takes priority)
4349
+ if (!config.fallback && outlet.getAttribute('fallback')) {
4350
+ this._fallback = outlet.getAttribute('fallback');
4351
+ }
4352
+ if (!config.mode && outlet.getAttribute('mode')) {
4353
+ const attrMode = outlet.getAttribute('mode');
4354
+ if (attrMode === 'hash' || attrMode === 'history') {
4355
+ this._mode = isFile ? 'hash' : attrMode;
4356
+ }
4357
+ }
4358
+ if (config.base == null && outlet.getAttribute('base')) {
4359
+ let ob = outlet.getAttribute('base');
4360
+ ob = String(ob).replace(/\/+$/, '');
4361
+ if (ob && !ob.startsWith('/')) ob = '/' + ob;
4362
+ this._base = ob;
4363
+ }
4364
+ }
4225
4365
  }
4226
4366
 
4227
4367
  // Register routes
@@ -4229,21 +4369,43 @@ class Router {
4229
4369
  config.routes.forEach(r => this.add(r));
4230
4370
  }
4231
4371
 
4232
- // Listen for navigation store handler references for cleanup in destroy()
4372
+ // Listen for navigation - store handler references for cleanup in destroy()
4233
4373
  if (this._mode === 'hash') {
4234
4374
  this._onNavEvent = () => this._resolve();
4235
4375
  window.addEventListener('hashchange', this._onNavEvent);
4376
+ // Hash mode also needs popstate for substates (pushSubstate uses pushState)
4377
+ this._onPopState = (e) => {
4378
+ const st = e.state;
4379
+ if (st && st[_ZQ_STATE_KEY] === 'substate') {
4380
+ const handled = this._fireSubstate(st.key, st.data, 'pop');
4381
+ if (handled) return;
4382
+ this._resolve().then(() => {
4383
+ this._fireSubstate(st.key, st.data, 'pop');
4384
+ });
4385
+ return;
4386
+ } else if (this._inSubstate) {
4387
+ this._inSubstate = false;
4388
+ this._fireSubstate(null, null, 'reset');
4389
+ }
4390
+ };
4391
+ window.addEventListener('popstate', this._onPopState);
4236
4392
  } else {
4237
4393
  this._onNavEvent = (e) => {
4238
- // Check for substate pop first if a listener handles it, don't route
4394
+ // Check for substate pop first - if a listener handles it, don't route
4239
4395
  const st = e.state;
4240
4396
  if (st && st[_ZQ_STATE_KEY] === 'substate') {
4241
4397
  const handled = this._fireSubstate(st.key, st.data, 'pop');
4242
4398
  if (handled) return;
4243
- // Unhandled substate — fall through to route resolve
4244
- // _inSubstate stays true so the next non-substate pop triggers reset
4399
+ // Unhandled substate — the owning component was likely destroyed
4400
+ // (e.g. user navigated away then pressed back). Resolve the route
4401
+ // first (which may mount a fresh component that registers a listener),
4402
+ // then retry the substate so the new listener can restore the UI.
4403
+ this._resolve().then(() => {
4404
+ this._fireSubstate(st.key, st.data, 'pop');
4405
+ });
4406
+ return;
4245
4407
  } else if (this._inSubstate) {
4246
- // Popped past all substates notify listeners to reset to defaults
4408
+ // Popped past all substates - notify listeners to reset to defaults
4247
4409
  this._inSubstate = false;
4248
4410
  this._fireSubstate(null, null, 'reset');
4249
4411
  }
@@ -4261,13 +4423,17 @@ class Router {
4261
4423
  if (link.getAttribute('target') === '_blank') return;
4262
4424
  e.preventDefault();
4263
4425
  let href = link.getAttribute('z-link');
4426
+ // Reject absolute URLs and dangerous protocols — z-link is for internal routes only
4427
+ if (href && /^[a-z][a-z0-9+.-]*:/i.test(href)) return;
4264
4428
  // Support z-link-params for dynamic :param interpolation
4265
4429
  const paramsAttr = link.getAttribute('z-link-params');
4266
4430
  if (paramsAttr) {
4267
4431
  try {
4268
4432
  const params = JSON.parse(paramsAttr);
4269
4433
  href = this._interpolateParams(href, params);
4270
- } catch { /* ignore malformed JSON */ }
4434
+ } catch (err) {
4435
+ reportError(ErrorCode.ROUTER_RESOLVE, 'Malformed JSON in z-link-params', { href, paramsAttr }, err);
4436
+ }
4271
4437
  }
4272
4438
  this.navigate(href);
4273
4439
  // z-to-top modifier: scroll to top after navigation
@@ -4321,8 +4487,8 @@ class Router {
4321
4487
 
4322
4488
  /**
4323
4489
  * Interpolate :param placeholders in a path with the given values.
4324
- * @param {string} path e.g. '/user/:id/posts/:pid'
4325
- * @param {Object} params e.g. { id: 42, pid: 7 }
4490
+ * @param {string} path - e.g. '/user/:id/posts/:pid'
4491
+ * @param {Object} params - e.g. { id: 42, pid: 7 }
4326
4492
  * @returns {string}
4327
4493
  */
4328
4494
  _interpolateParams(path, params) {
@@ -4366,7 +4532,7 @@ class Router {
4366
4532
  const currentURL = (window.location.pathname || '/') + (window.location.hash || '');
4367
4533
 
4368
4534
  if (targetURL === currentURL && !options.force) {
4369
- // Same full URL (path + hash) don't push duplicate entry.
4535
+ // Same full URL (path + hash) - don't push duplicate entry.
4370
4536
  // If only the hash changed to a fragment target, scroll to it.
4371
4537
  if (fragment) {
4372
4538
  const el = document.getElementById(fragment);
@@ -4375,7 +4541,7 @@ class Router {
4375
4541
  return this;
4376
4542
  }
4377
4543
 
4378
- // Same route path but different hash fragment use replaceState
4544
+ // Same route path but different hash fragment - use replaceState
4379
4545
  // so back goes to the previous *route*, not the previous scroll position.
4380
4546
  const targetPathOnly = this._base + normalized;
4381
4547
  const currentPathOnly = window.location.pathname || '/';
@@ -4390,7 +4556,7 @@ class Router {
4390
4556
  const el = document.getElementById(fragment);
4391
4557
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
4392
4558
  }
4393
- // Don't re-resolve same route, just a hash change
4559
+ // Don't re-resolve - same route, just a hash change
4394
4560
  return this;
4395
4561
  }
4396
4562
 
@@ -4426,8 +4592,8 @@ class Router {
4426
4592
 
4427
4593
  /**
4428
4594
  * Normalize an app-relative path and guard against double base-prefixing.
4429
- * @param {string} path e.g. '/docs', 'docs', or '/app/docs' when base is '/app'
4430
- * @returns {string} always starts with '/'
4595
+ * @param {string} path - e.g. '/docs', 'docs', or '/app/docs' when base is '/app'
4596
+ * @returns {string} - always starts with '/'
4431
4597
  */
4432
4598
  _normalizePath(path) {
4433
4599
  let p = path && path.startsWith('/') ? path : (path ? `/${path}` : '/');
@@ -4477,12 +4643,12 @@ class Router {
4477
4643
 
4478
4644
  /**
4479
4645
  * Push a lightweight history entry for in-component UI state.
4480
- * The URL path does NOT change only a history entry is added so the
4646
+ * The URL path does NOT change - only a history entry is added so the
4481
4647
  * back button can undo the UI change (close modal, revert tab, etc.)
4482
4648
  * before navigating away.
4483
4649
  *
4484
- * @param {string} key identifier (e.g. 'modal', 'tab', 'panel')
4485
- * @param {*} data arbitrary state (serializable)
4650
+ * @param {string} key - identifier (e.g. 'modal', 'tab', 'panel')
4651
+ * @param {*} data - arbitrary state (serializable)
4486
4652
  * @returns {Router}
4487
4653
  *
4488
4654
  * @example
@@ -4493,7 +4659,7 @@ class Router {
4493
4659
  pushSubstate(key, data) {
4494
4660
  this._inSubstate = true;
4495
4661
  if (this._mode === 'hash') {
4496
- // Hash mode: stash the substate in a global hashchange will check.
4662
+ // Hash mode: stash the substate in a global - hashchange will check.
4497
4663
  // We still push a history entry via a sentinel hash suffix.
4498
4664
  const current = window.location.hash || '#/';
4499
4665
  window.history.pushState(
@@ -4609,12 +4775,12 @@ class Router {
4609
4775
  async __resolve() {
4610
4776
  // Check if we're landing on a substate entry (e.g. page refresh on a
4611
4777
  // substate bookmark, or hash-mode popstate). Fire listeners and bail
4612
- // if handled the URL hasn't changed so there's no route to resolve.
4778
+ // if handled - the URL hasn't changed so there's no route to resolve.
4613
4779
  const histState = window.history.state;
4614
4780
  if (histState && histState[_ZQ_STATE_KEY] === 'substate') {
4615
4781
  const handled = this._fireSubstate(histState.key, histState.data, 'resolve');
4616
4782
  if (handled) return;
4617
- // No listener handled it fall through to normal routing
4783
+ // No listener handled it - fall through to normal routing
4618
4784
  }
4619
4785
 
4620
4786
  const fullPath = this.path;
@@ -4651,7 +4817,7 @@ class Router {
4651
4817
  const sameParams = _shallowEqual(params, from.params);
4652
4818
  const sameQuery = _shallowEqual(query, from.query);
4653
4819
  if (sameParams && sameQuery) {
4654
- // Identical navigation nothing to do
4820
+ // Identical navigation - nothing to do
4655
4821
  return;
4656
4822
  }
4657
4823
  }
@@ -4739,6 +4905,9 @@ class Router {
4739
4905
  }
4740
4906
  }
4741
4907
 
4908
+ // Update z-active-route elements
4909
+ this._updateActiveRoutes(path);
4910
+
4742
4911
  // Run after guards
4743
4912
  for (const guard of this._guards.after) {
4744
4913
  await guard(to, from);
@@ -4748,6 +4917,32 @@ class Router {
4748
4917
  this._listeners.forEach(fn => fn(to, from));
4749
4918
  }
4750
4919
 
4920
+ // --- Active route class management ----------------------------------------
4921
+
4922
+ /**
4923
+ * Update all elements with z-active-route to toggle their active class
4924
+ * based on the current path.
4925
+ *
4926
+ * Usage:
4927
+ * <a z-link="/docs" z-active-route="/docs">Docs</a>
4928
+ * <a z-link="/about" z-active-route="/about" z-active-class="selected">About</a>
4929
+ * <a z-link="/" z-active-route="/" z-active-exact>Home</a>
4930
+ */
4931
+ _updateActiveRoutes(currentPath) {
4932
+ if (typeof document === 'undefined') return;
4933
+ const els = document.querySelectorAll('[z-active-route]');
4934
+ for (let i = 0; i < els.length; i++) {
4935
+ const el = els[i];
4936
+ const route = el.getAttribute('z-active-route');
4937
+ const cls = el.getAttribute('z-active-class') || 'active';
4938
+ const exact = el.hasAttribute('z-active-exact');
4939
+ const isActive = exact
4940
+ ? currentPath === route
4941
+ : (route === '/' ? currentPath === '/' : currentPath.startsWith(route));
4942
+ el.classList.toggle(cls, isActive);
4943
+ }
4944
+ }
4945
+
4751
4946
  // --- Destroy -------------------------------------------------------------
4752
4947
 
4753
4948
  destroy() {
@@ -4756,6 +4951,10 @@ class Router {
4756
4951
  window.removeEventListener(this._mode === 'hash' ? 'hashchange' : 'popstate', this._onNavEvent);
4757
4952
  this._onNavEvent = null;
4758
4953
  }
4954
+ if (this._onPopState) {
4955
+ window.removeEventListener('popstate', this._onPopState);
4956
+ this._onPopState = null;
4957
+ }
4759
4958
  if (this._onLinkClick) {
4760
4959
  document.removeEventListener('click', this._onLinkClick);
4761
4960
  this._onLinkClick = null;
@@ -4786,7 +4985,7 @@ function getRouter() {
4786
4985
 
4787
4986
  // --- src/store.js ------------------------------------------------
4788
4987
  /**
4789
- * zQuery Store Global reactive state management
4988
+ * zQuery Store - Global reactive state management
4790
4989
  *
4791
4990
  * A lightweight Redux/Vuex-inspired store with:
4792
4991
  * - Reactive state via Proxy
@@ -4824,22 +5023,22 @@ class Store {
4824
5023
  this._history = []; // action log
4825
5024
  this._maxHistory = config.maxHistory || 1000;
4826
5025
  this._debug = config.debug || false;
5026
+ this._batching = false;
5027
+ this._batchQueue = []; // pending notifications during batch
5028
+ this._undoStack = [];
5029
+ this._redoStack = [];
5030
+ this._maxUndo = config.maxUndo || 50;
4827
5031
 
4828
- // Create reactive state
5032
+ // Store initial state for reset
4829
5033
  const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) };
5034
+ this._initialState = JSON.parse(JSON.stringify(initial));
4830
5035
 
4831
5036
  this.state = reactive(initial, (key, value, old) => {
4832
- // Notify key-specific subscribers
4833
- const subs = this._subscribers.get(key);
4834
- if (subs) subs.forEach(fn => {
4835
- try { fn(value, old, key); }
4836
- catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, `Subscriber for "${key}" threw`, { key }, err); }
4837
- });
4838
- // Notify wildcard subscribers
4839
- this._wildcards.forEach(fn => {
4840
- try { fn(key, value, old); }
4841
- catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, 'Wildcard subscriber threw', { key }, err); }
4842
- });
5037
+ if (this._batching) {
5038
+ this._batchQueue.push({ key, value, old });
5039
+ return;
5040
+ }
5041
+ this._notifySubscribers(key, value, old);
4843
5042
  });
4844
5043
 
4845
5044
  // Build getters as computed properties
@@ -4852,10 +5051,90 @@ class Store {
4852
5051
  }
4853
5052
  }
4854
5053
 
5054
+ /** @private Notify key-specific and wildcard subscribers */
5055
+ _notifySubscribers(key, value, old) {
5056
+ const subs = this._subscribers.get(key);
5057
+ if (subs) subs.forEach(fn => {
5058
+ try { fn(key, value, old); }
5059
+ catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, `Subscriber for "${key}" threw`, { key }, err); }
5060
+ });
5061
+ this._wildcards.forEach(fn => {
5062
+ try { fn(key, value, old); }
5063
+ catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, 'Wildcard subscriber threw', { key }, err); }
5064
+ });
5065
+ }
5066
+
5067
+ /**
5068
+ * Batch multiple state changes - subscribers fire once at the end
5069
+ * with only the latest value per key.
5070
+ */
5071
+ batch(fn) {
5072
+ this._batching = true;
5073
+ this._batchQueue = [];
5074
+ try {
5075
+ fn(this.state);
5076
+ } finally {
5077
+ this._batching = false;
5078
+ // Deduplicate: keep only the last change per key
5079
+ const last = new Map();
5080
+ for (const entry of this._batchQueue) {
5081
+ last.set(entry.key, entry);
5082
+ }
5083
+ this._batchQueue = [];
5084
+ for (const { key, value, old } of last.values()) {
5085
+ this._notifySubscribers(key, value, old);
5086
+ }
5087
+ }
5088
+ }
5089
+
5090
+ /**
5091
+ * Save a snapshot for undo. Call before making changes you want to be undoable.
5092
+ */
5093
+ checkpoint() {
5094
+ const snap = JSON.parse(JSON.stringify(this.state.__raw || this.state));
5095
+ this._undoStack.push(snap);
5096
+ if (this._undoStack.length > this._maxUndo) {
5097
+ this._undoStack.splice(0, this._undoStack.length - this._maxUndo);
5098
+ }
5099
+ this._redoStack = [];
5100
+ }
5101
+
5102
+ /**
5103
+ * Undo to the last checkpoint
5104
+ * @returns {boolean} true if undo was performed
5105
+ */
5106
+ undo() {
5107
+ if (this._undoStack.length === 0) return false;
5108
+ const current = JSON.parse(JSON.stringify(this.state.__raw || this.state));
5109
+ this._redoStack.push(current);
5110
+ const prev = this._undoStack.pop();
5111
+ this.replaceState(prev);
5112
+ return true;
5113
+ }
5114
+
5115
+ /**
5116
+ * Redo the last undone state change
5117
+ * @returns {boolean} true if redo was performed
5118
+ */
5119
+ redo() {
5120
+ if (this._redoStack.length === 0) return false;
5121
+ const current = JSON.parse(JSON.stringify(this.state.__raw || this.state));
5122
+ this._undoStack.push(current);
5123
+ const next = this._redoStack.pop();
5124
+ this.replaceState(next);
5125
+ return true;
5126
+ }
5127
+
5128
+ /** Check if undo is available */
5129
+ get canUndo() { return this._undoStack.length > 0; }
5130
+
5131
+ /** Check if redo is available */
5132
+ get canRedo() { return this._redoStack.length > 0; }
5133
+
4855
5134
  /**
4856
5135
  * Dispatch a named action
4857
- * @param {string} name action name
4858
- * @param {...any} args payload
5136
+ * @param {string} name - action name
5137
+ * @param {...any} args - payload
4859
5138
  */
4860
5139
  dispatch(name, ...args) {
4861
5140
  const action = this._actions[name];
@@ -4894,13 +5173,13 @@ class Store {
4894
5173
 
4895
5174
  /**
4896
5175
  * Subscribe to changes on a specific state key
4897
- * @param {string|Function} keyOrFn state key, or function for all changes
4898
- * @param {Function} [fn] callback (value, oldValue, key)
4899
- * @returns {Function} unsubscribe
5176
+ * @param {string|Function} keyOrFn - state key, or function for all changes
5177
+ * @param {Function} [fn] - callback (key, value, oldValue)
5178
+ * @returns {Function} - unsubscribe
4900
5179
  */
4901
5180
  subscribe(keyOrFn, fn) {
4902
5181
  if (typeof keyOrFn === 'function') {
4903
- // Wildcard listen to all changes
5182
+ // Wildcard - listen to all changes
4904
5183
  this._wildcards.add(keyOrFn);
4905
5184
  return () => this._wildcards.delete(keyOrFn);
4906
5185
  }
@@ -4946,11 +5225,13 @@ class Store {
4946
5225
  }
4947
5226
 
4948
5227
  /**
4949
- * Reset state to initial values
5228
+ * Reset state to initial values. If no argument, resets to the original state.
4950
5229
  */
4951
5230
  reset(initialState) {
4952
- this.replaceState(initialState);
5231
+ this.replaceState(initialState || JSON.parse(JSON.stringify(this._initialState)));
4953
5232
  this._history = [];
5233
+ this._undoStack = [];
5234
+ this._redoStack = [];
4954
5235
  }
4955
5236
  }
4956
5237
 
@@ -4977,7 +5258,7 @@ function getStore(name = 'default') {
4977
5258
 
4978
5259
  // --- src/http.js -------------------------------------------------
4979
5260
  /**
4980
- * zQuery HTTP Lightweight fetch wrapper
5261
+ * zQuery HTTP - Lightweight fetch wrapper
4981
5262
  *
4982
5263
  * Clean API for GET/POST/PUT/PATCH/DELETE with:
4983
5264
  * - Auto JSON serialization/deserialization
@@ -5167,7 +5448,7 @@ const http = {
5167
5448
 
5168
5449
  /**
5169
5450
  * Add request interceptor
5170
- * @param {Function} fn (fetchOpts, url) → void | false | { url, options }
5451
+ * @param {Function} fn - (fetchOpts, url) → void | false | { url, options }
5171
5452
  * @returns {Function} unsubscribe function
5172
5453
  */
5173
5454
  onRequest(fn) {
@@ -5180,7 +5461,7 @@ const http = {
5180
5461
 
5181
5462
  /**
5182
5463
  * Add response interceptor
5183
- * @param {Function} fn (result) → void
5464
+ * @param {Function} fn - (result) → void
5184
5465
  * @returns {Function} unsubscribe function
5185
5466
  */
5186
5467
  onResponse(fn) {
@@ -5192,7 +5473,7 @@ const http = {
5192
5473
  },
5193
5474
 
5194
5475
  /**
5195
- * Clear interceptors all, or just 'request' / 'response'
5476
+ * Clear interceptors - all, or just 'request' / 'response'
5196
5477
  */
5197
5478
  clearInterceptors(type) {
5198
5479
  if (!type || type === 'request') _interceptors.request.length = 0;
@@ -5221,7 +5502,7 @@ const http = {
5221
5502
 
5222
5503
  // --- src/utils.js ------------------------------------------------
5223
5504
  /**
5224
- * zQuery Utils Common utility functions
5505
+ * zQuery Utils - Common utility functions
5225
5506
  *
5226
5507
  * Quality-of-life helpers that every frontend project needs.
5227
5508
  * Attached to $ namespace for convenience.
@@ -5232,7 +5513,7 @@ const http = {
5232
5513
  // ---------------------------------------------------------------------------
5233
5514
 
5234
5515
  /**
5235
- * Debounce delays execution until after `ms` of inactivity
5516
+ * Debounce - delays execution until after `ms` of inactivity
5236
5517
  */
5237
5518
  function debounce(fn, ms = 250) {
5238
5519
  let timer;
@@ -5245,7 +5526,7 @@ function debounce(fn, ms = 250) {
5245
5526
  }
5246
5527
 
5247
5528
  /**
5248
- * Throttle limits execution to once per `ms`
5529
+ * Throttle - limits execution to once per `ms`
5249
5530
  */
5250
5531
  function throttle(fn, ms = 250) {
5251
5532
  let last = 0;
@@ -5264,14 +5545,14 @@ function throttle(fn, ms = 250) {
5264
5545
  }
5265
5546
 
5266
5547
  /**
5267
- * Pipe compose functions left-to-right
5548
+ * Pipe - compose functions left-to-right
5268
5549
  */
5269
5550
  function pipe(...fns) {
5270
5551
  return (input) => fns.reduce((val, fn) => fn(val), input);
5271
5552
  }
5272
5553
 
5273
5554
  /**
5274
- * Once function that only runs once
5555
+ * Once - function that only runs once
5275
5556
  */
5276
5557
  function once(fn) {
5277
5558
  let called = false, result;
@@ -5282,7 +5563,7 @@ function once(fn) {
5282
5563
  }
5283
5564
 
5284
5565
  /**
5285
- * Sleep promise-based delay
5566
+ * Sleep - promise-based delay
5286
5567
  */
5287
5568
  function sleep(ms) {
5288
5569
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -5333,8 +5614,12 @@ function trust(htmlStr) {
5333
5614
  * Generate UUID v4
5334
5615
  */
5335
5616
  function uuid() {
5336
- return crypto?.randomUUID?.() || 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
5337
- const r = Math.random() * 16 | 0;
5617
+ if (crypto?.randomUUID) return crypto.randomUUID();
5618
+ // Fallback using crypto.getRandomValues (wider support than randomUUID)
5619
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
5620
+ const buf = new Uint8Array(1);
5621
+ crypto.getRandomValues(buf);
5622
+ const r = buf[0] & 15;
5338
5623
  return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
5339
5624
  });
5340
5625
  }
@@ -5362,13 +5647,50 @@ function kebabCase(str) {
5362
5647
  // ---------------------------------------------------------------------------
5363
5648
 
5364
5649
  /**
5365
- * Deep clone
5650
+ * Deep clone via structuredClone (handles circular refs, Dates, etc.).
5651
+ * Falls back to a manual deep clone that preserves Date, RegExp, Map, Set,
5652
+ * ArrayBuffer, TypedArrays, undefined values, and circular references.
5366
5653
  */
5367
5654
  function deepClone(obj) {
5368
5655
  if (typeof structuredClone === 'function') return structuredClone(obj);
5369
- return JSON.parse(JSON.stringify(obj));
5656
+
5657
+ const seen = new Map();
5658
+ function clone(val) {
5659
+ if (val === null || typeof val !== 'object') return val;
5660
+ if (seen.has(val)) return seen.get(val);
5661
+ if (val instanceof Date) return new Date(val.getTime());
5662
+ if (val instanceof RegExp) return new RegExp(val.source, val.flags);
5663
+ if (val instanceof Map) {
5664
+ const m = new Map();
5665
+ seen.set(val, m);
5666
+ val.forEach((v, k) => m.set(clone(k), clone(v)));
5667
+ return m;
5668
+ }
5669
+ if (val instanceof Set) {
5670
+ const s = new Set();
5671
+ seen.set(val, s);
5672
+ val.forEach(v => s.add(clone(v)));
5673
+ return s;
5674
+ }
5675
+ if (ArrayBuffer.isView(val)) return new val.constructor(val.buffer.slice(0));
5676
+ if (val instanceof ArrayBuffer) return val.slice(0);
5677
+ if (Array.isArray(val)) {
5678
+ const arr = [];
5679
+ seen.set(val, arr);
5680
+ for (let i = 0; i < val.length; i++) arr[i] = clone(val[i]);
5681
+ return arr;
5682
+ }
5683
+ const result = Object.create(Object.getPrototypeOf(val));
5684
+ seen.set(val, result);
5685
+ for (const key of Object.keys(val)) result[key] = clone(val[key]);
5686
+ return result;
5687
+ }
5688
+ return clone(obj);
5370
5689
  }
5371
5690
 
5691
+ // Keys that must never be written through data-merge or path-set operations
5692
+ const _UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
5693
+
5372
5694
  /**
5373
5695
  * Deep merge objects
5374
5696
  */
@@ -5378,6 +5700,7 @@ function deepMerge(target, ...sources) {
5378
5700
  if (seen.has(src)) return tgt;
5379
5701
  seen.add(src);
5380
5702
  for (const key of Object.keys(src)) {
5703
+ if (_UNSAFE_KEYS.has(key)) continue;
5381
5704
  if (src[key] && typeof src[key] === 'object' && !Array.isArray(src[key])) {
5382
5705
  if (!tgt[key] || typeof tgt[key] !== 'object') tgt[key] = {};
5383
5706
  merge(tgt[key], src[key]);
@@ -5584,10 +5907,13 @@ function setPath(obj, path, value) {
5584
5907
  let cur = obj;
5585
5908
  for (let i = 0; i < keys.length - 1; i++) {
5586
5909
  const k = keys[i];
5910
+ if (_UNSAFE_KEYS.has(k)) return obj;
5587
5911
  if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
5588
5912
  cur = cur[k];
5589
5913
  }
5590
- cur[keys[keys.length - 1]] = value;
5914
+ const lastKey = keys[keys.length - 1];
5915
+ if (_UNSAFE_KEYS.has(lastKey)) return obj;
5916
+ cur[lastKey] = value;
5591
5917
  return obj;
5592
5918
  }
5593
5919
 
@@ -5638,9 +5964,16 @@ function memoize(fn, keyFnOrOpts) {
5638
5964
 
5639
5965
  const memoized = (...args) => {
5640
5966
  const key = keyFn ? keyFn(...args) : args[0];
5641
- if (cache.has(key)) return cache.get(key);
5967
+ if (cache.has(key)) {
5968
+ // LRU: promote to newest by re-inserting
5969
+ const value = cache.get(key);
5970
+ cache.delete(key);
5971
+ cache.set(key, value);
5972
+ return value;
5973
+ }
5642
5974
  const result = fn(...args);
5643
5975
  cache.set(key, result);
5976
+ // LRU eviction: drop the least-recently-used entry
5644
5977
  if (maxSize > 0 && cache.size > maxSize) {
5645
5978
  cache.delete(cache.keys().next().value);
5646
5979
  }
@@ -5687,7 +6020,7 @@ function timeout(promise, ms, message) {
5687
6020
  // --- index.js (assembly) ------------------------------------------
5688
6021
  /**
5689
6022
  * ┌---------------------------------------------------------┐
5690
- * │ zQuery (zeroQuery) Lightweight Frontend Library │
6023
+ * │ zQuery (zeroQuery) - Lightweight Frontend Library │
5691
6024
  * │ │
5692
6025
  * │ jQuery-like selectors · Reactive components │
5693
6026
  * │ SPA router · State management · Zero dependencies │
@@ -5707,11 +6040,11 @@ function timeout(promise, ms, message) {
5707
6040
 
5708
6041
 
5709
6042
  // ---------------------------------------------------------------------------
5710
- // $ The main function & namespace
6043
+ // $ - The main function & namespace
5711
6044
  // ---------------------------------------------------------------------------
5712
6045
 
5713
6046
  /**
5714
- * Main selector function always returns a ZQueryCollection (like jQuery).
6047
+ * Main selector function - always returns a ZQueryCollection (like jQuery).
5715
6048
  *
5716
6049
  * $('selector') → ZQueryCollection (querySelectorAll)
5717
6050
  * $('<div>hello</div>') → ZQueryCollection from created elements
@@ -5774,6 +6107,8 @@ $.Signal = Signal;
5774
6107
  $.signal = signal;
5775
6108
  $.computed = computed;
5776
6109
  $.effect = effect;
6110
+ $.batch = batch;
6111
+ $.untracked = untracked;
5777
6112
 
5778
6113
  // --- Components ------------------------------------------------------------
5779
6114
  $.component = component;
@@ -5849,12 +6184,14 @@ $.onError = onError;
5849
6184
  $.ZQueryError = ZQueryError;
5850
6185
  $.ErrorCode = ErrorCode;
5851
6186
  $.guardCallback = guardCallback;
6187
+ $.guardAsync = guardAsync;
5852
6188
  $.validate = validate;
6189
+ $.formatError = formatError;
5853
6190
 
5854
6191
  // --- Meta ------------------------------------------------------------------
5855
- $.version = '0.9.8';
5856
- $.libSize = '~101 KB';
5857
- $.unitTests = {"passed":1733,"failed":0,"total":1733,"suites":489,"duration":2847,"ok":true};
6192
+ $.version = '1.0.0';
6193
+ $.libSize = '~106 KB';
6194
+ $.unitTests = {"passed":1882,"failed":0,"total":1882,"suites":508,"duration":3548,"ok":true};
5858
6195
  $.meta = {}; // populated at build time by CLI bundler
5859
6196
 
5860
6197
  $.noConflict = () => {