zero-query 0.6.3 → 0.7.5

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 (41) hide show
  1. package/README.md +6 -6
  2. package/cli/commands/build.js +3 -3
  3. package/cli/commands/bundle.js +286 -8
  4. package/cli/commands/dev/index.js +2 -2
  5. package/cli/commands/dev/overlay.js +51 -2
  6. package/cli/commands/dev/server.js +34 -5
  7. package/cli/commands/dev/watcher.js +33 -0
  8. package/cli/scaffold/index.html +1 -0
  9. package/cli/scaffold/scripts/app.js +15 -22
  10. package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
  11. package/cli/scaffold/scripts/components/contacts/contacts.html +3 -3
  12. package/cli/scaffold/styles/styles.css +1 -0
  13. package/cli/utils.js +111 -6
  14. package/dist/zquery.dist.zip +0 -0
  15. package/dist/zquery.js +379 -27
  16. package/dist/zquery.min.js +3 -16
  17. package/index.d.ts +127 -1290
  18. package/package.json +5 -5
  19. package/src/component.js +11 -1
  20. package/src/core.js +305 -10
  21. package/src/router.js +49 -2
  22. package/tests/component.test.js +304 -0
  23. package/tests/core.test.js +726 -0
  24. package/tests/diff.test.js +194 -0
  25. package/tests/errors.test.js +162 -0
  26. package/tests/expression.test.js +334 -0
  27. package/tests/http.test.js +181 -0
  28. package/tests/reactive.test.js +191 -0
  29. package/tests/router.test.js +332 -0
  30. package/tests/store.test.js +253 -0
  31. package/tests/utils.test.js +353 -0
  32. package/types/collection.d.ts +368 -0
  33. package/types/component.d.ts +210 -0
  34. package/types/errors.d.ts +103 -0
  35. package/types/http.d.ts +81 -0
  36. package/types/misc.d.ts +166 -0
  37. package/types/reactive.d.ts +76 -0
  38. package/types/router.d.ts +132 -0
  39. package/types/ssr.d.ts +49 -0
  40. package/types/store.d.ts +107 -0
  41. package/types/utils.d.ts +142 -0
package/dist/zquery.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.6.3
2
+ * zQuery (zeroQuery) v0.7.5
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
- * (c) 2026 Anthony Wiedman MIT License
5
+ * (c) 2026 Anthony Wiedman - MIT License
6
6
  */
7
7
  (function(global) {
8
8
  'use strict';
9
9
 
10
- // --- src/errors.js ———————————————————————————————————————————————
10
+ // --- src/errors.js -----------------------------------------------
11
11
  /**
12
12
  * zQuery Errors — Structured error handling system
13
13
  *
@@ -164,7 +164,7 @@ function validate(value, name, expectedType) {
164
164
  }
165
165
  }
166
166
 
167
- // --- src/reactive.js —————————————————————————————————————————————
167
+ // --- src/reactive.js ---------------------------------------------
168
168
  /**
169
169
  * zQuery Reactive — Proxy-based deep reactivity system
170
170
  *
@@ -315,7 +315,7 @@ function effect(fn) {
315
315
  };
316
316
  }
317
317
 
318
- // --- src/core.js —————————————————————————————————————————————————
318
+ // --- src/core.js -------------------------------------------------
319
319
  /**
320
320
  * zQuery Core — Selector engine & chainable DOM collection
321
321
  *
@@ -393,8 +393,96 @@ class ZQueryCollection {
393
393
  return new ZQueryCollection(sibs);
394
394
  }
395
395
 
396
- next() { return new ZQueryCollection(this.elements.map(el => el.nextElementSibling).filter(Boolean)); }
397
- prev() { return new ZQueryCollection(this.elements.map(el => el.previousElementSibling).filter(Boolean)); }
396
+ next(selector) {
397
+ const els = this.elements.map(el => el.nextElementSibling).filter(Boolean);
398
+ return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
399
+ }
400
+
401
+ prev(selector) {
402
+ const els = this.elements.map(el => el.previousElementSibling).filter(Boolean);
403
+ return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
404
+ }
405
+
406
+ nextAll(selector) {
407
+ const result = [];
408
+ this.elements.forEach(el => {
409
+ let sib = el.nextElementSibling;
410
+ while (sib) {
411
+ if (!selector || sib.matches(selector)) result.push(sib);
412
+ sib = sib.nextElementSibling;
413
+ }
414
+ });
415
+ return new ZQueryCollection(result);
416
+ }
417
+
418
+ nextUntil(selector, filter) {
419
+ const result = [];
420
+ this.elements.forEach(el => {
421
+ let sib = el.nextElementSibling;
422
+ while (sib) {
423
+ if (selector && sib.matches(selector)) break;
424
+ if (!filter || sib.matches(filter)) result.push(sib);
425
+ sib = sib.nextElementSibling;
426
+ }
427
+ });
428
+ return new ZQueryCollection(result);
429
+ }
430
+
431
+ prevAll(selector) {
432
+ const result = [];
433
+ this.elements.forEach(el => {
434
+ let sib = el.previousElementSibling;
435
+ while (sib) {
436
+ if (!selector || sib.matches(selector)) result.push(sib);
437
+ sib = sib.previousElementSibling;
438
+ }
439
+ });
440
+ return new ZQueryCollection(result);
441
+ }
442
+
443
+ prevUntil(selector, filter) {
444
+ const result = [];
445
+ this.elements.forEach(el => {
446
+ let sib = el.previousElementSibling;
447
+ while (sib) {
448
+ if (selector && sib.matches(selector)) break;
449
+ if (!filter || sib.matches(filter)) result.push(sib);
450
+ sib = sib.previousElementSibling;
451
+ }
452
+ });
453
+ return new ZQueryCollection(result);
454
+ }
455
+
456
+ parents(selector) {
457
+ const result = [];
458
+ this.elements.forEach(el => {
459
+ let parent = el.parentElement;
460
+ while (parent) {
461
+ if (!selector || parent.matches(selector)) result.push(parent);
462
+ parent = parent.parentElement;
463
+ }
464
+ });
465
+ return new ZQueryCollection([...new Set(result)]);
466
+ }
467
+
468
+ parentsUntil(selector, filter) {
469
+ const result = [];
470
+ this.elements.forEach(el => {
471
+ let parent = el.parentElement;
472
+ while (parent) {
473
+ if (selector && parent.matches(selector)) break;
474
+ if (!filter || parent.matches(filter)) result.push(parent);
475
+ parent = parent.parentElement;
476
+ }
477
+ });
478
+ return new ZQueryCollection([...new Set(result)]);
479
+ }
480
+
481
+ contents() {
482
+ const result = [];
483
+ this.elements.forEach(el => result.push(...el.childNodes));
484
+ return new ZQueryCollection(result);
485
+ }
398
486
 
399
487
  filter(selector) {
400
488
  if (typeof selector === 'function') {
@@ -414,6 +502,42 @@ class ZQueryCollection {
414
502
  return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
415
503
  }
416
504
 
505
+ is(selector) {
506
+ if (typeof selector === 'function') {
507
+ return this.elements.some((el, i) => selector.call(el, i, el));
508
+ }
509
+ return this.elements.some(el => el.matches(selector));
510
+ }
511
+
512
+ slice(start, end) {
513
+ return new ZQueryCollection(this.elements.slice(start, end));
514
+ }
515
+
516
+ add(selector, context) {
517
+ const toAdd = (selector instanceof ZQueryCollection)
518
+ ? selector.elements
519
+ : (selector instanceof Node)
520
+ ? [selector]
521
+ : Array.from((context || document).querySelectorAll(selector));
522
+ return new ZQueryCollection([...this.elements, ...toAdd]);
523
+ }
524
+
525
+ get(index) {
526
+ if (index === undefined) return [...this.elements];
527
+ return index < 0 ? this.elements[this.length + index] : this.elements[index];
528
+ }
529
+
530
+ index(selector) {
531
+ if (selector === undefined) {
532
+ const el = this.first();
533
+ return el ? Array.from(el.parentElement.children).indexOf(el) : -1;
534
+ }
535
+ const target = (typeof selector === 'string')
536
+ ? document.querySelector(selector)
537
+ : selector;
538
+ return this.elements.indexOf(target);
539
+ }
540
+
417
541
  // --- Classes -------------------------------------------------------------
418
542
 
419
543
  addClass(...names) {
@@ -426,8 +550,12 @@ class ZQueryCollection {
426
550
  return this.each((_, el) => el.classList.remove(...classes));
427
551
  }
428
552
 
429
- toggleClass(name, force) {
430
- return this.each((_, el) => el.classList.toggle(name, force));
553
+ toggleClass(...args) {
554
+ const force = typeof args[args.length - 1] === 'boolean' ? args.pop() : undefined;
555
+ const classes = args.flatMap(n => n.split(/\s+/));
556
+ return this.each((_, el) => {
557
+ classes.forEach(c => force !== undefined ? el.classList.toggle(c, force) : el.classList.toggle(c));
558
+ });
431
559
  }
432
560
 
433
561
  hasClass(name) {
@@ -481,6 +609,60 @@ class ZQueryCollection {
481
609
  return el ? { top: el.offsetTop, left: el.offsetLeft } : null;
482
610
  }
483
611
 
612
+ scrollTop(value) {
613
+ if (value === undefined) {
614
+ const el = this.first();
615
+ return el === window ? window.scrollY : el?.scrollTop;
616
+ }
617
+ return this.each((_, el) => {
618
+ if (el === window) window.scrollTo(window.scrollX, value);
619
+ else el.scrollTop = value;
620
+ });
621
+ }
622
+
623
+ scrollLeft(value) {
624
+ if (value === undefined) {
625
+ const el = this.first();
626
+ return el === window ? window.scrollX : el?.scrollLeft;
627
+ }
628
+ return this.each((_, el) => {
629
+ if (el === window) window.scrollTo(value, window.scrollY);
630
+ else el.scrollLeft = value;
631
+ });
632
+ }
633
+
634
+ innerWidth() {
635
+ const el = this.first();
636
+ return el?.clientWidth;
637
+ }
638
+
639
+ innerHeight() {
640
+ const el = this.first();
641
+ return el?.clientHeight;
642
+ }
643
+
644
+ outerWidth(includeMargin = false) {
645
+ const el = this.first();
646
+ if (!el) return undefined;
647
+ let w = el.offsetWidth;
648
+ if (includeMargin) {
649
+ const style = getComputedStyle(el);
650
+ w += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
651
+ }
652
+ return w;
653
+ }
654
+
655
+ outerHeight(includeMargin = false) {
656
+ const el = this.first();
657
+ if (!el) return undefined;
658
+ let h = el.offsetHeight;
659
+ if (includeMargin) {
660
+ const style = getComputedStyle(el);
661
+ h += parseFloat(style.marginTop) + parseFloat(style.marginBottom);
662
+ }
663
+ return h;
664
+ }
665
+
484
666
  // --- Content -------------------------------------------------------------
485
667
 
486
668
  html(content) {
@@ -560,6 +742,73 @@ class ZQueryCollection {
560
742
  });
561
743
  }
562
744
 
745
+ appendTo(target) {
746
+ const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
747
+ if (dest) this.each((_, el) => dest.appendChild(el));
748
+ return this;
749
+ }
750
+
751
+ prependTo(target) {
752
+ const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
753
+ if (dest) this.each((_, el) => dest.insertBefore(el, dest.firstChild));
754
+ return this;
755
+ }
756
+
757
+ insertAfter(target) {
758
+ const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
759
+ if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref.nextSibling));
760
+ return this;
761
+ }
762
+
763
+ insertBefore(target) {
764
+ const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
765
+ if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref));
766
+ return this;
767
+ }
768
+
769
+ replaceAll(target) {
770
+ const targets = typeof target === 'string'
771
+ ? Array.from(document.querySelectorAll(target))
772
+ : target instanceof ZQueryCollection ? target.elements : [target];
773
+ targets.forEach((t, i) => {
774
+ const nodes = i === 0 ? this.elements : this.elements.map(el => el.cloneNode(true));
775
+ nodes.forEach(el => t.parentNode.insertBefore(el, t));
776
+ t.remove();
777
+ });
778
+ return this;
779
+ }
780
+
781
+ unwrap(selector) {
782
+ this.elements.forEach(el => {
783
+ const parent = el.parentElement;
784
+ if (!parent || parent === document.body) return;
785
+ if (selector && !parent.matches(selector)) return;
786
+ parent.replaceWith(...parent.childNodes);
787
+ });
788
+ return this;
789
+ }
790
+
791
+ wrapAll(wrapper) {
792
+ const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
793
+ const first = this.first();
794
+ if (!first) return this;
795
+ first.parentNode.insertBefore(w, first);
796
+ this.each((_, el) => w.appendChild(el));
797
+ return this;
798
+ }
799
+
800
+ wrapInner(wrapper) {
801
+ return this.each((_, el) => {
802
+ const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
803
+ while (el.firstChild) w.appendChild(el.firstChild);
804
+ el.appendChild(w);
805
+ });
806
+ }
807
+
808
+ detach() {
809
+ return this.each((_, el) => el.remove());
810
+ }
811
+
563
812
  // --- Visibility ----------------------------------------------------------
564
813
 
565
814
  show(display = '') {
@@ -585,9 +834,10 @@ class ZQueryCollection {
585
834
  events.forEach(evt => {
586
835
  if (typeof selectorOrHandler === 'function') {
587
836
  el.addEventListener(evt, selectorOrHandler);
588
- } else {
589
- // Delegated event
837
+ } else if (typeof selectorOrHandler === 'string') {
838
+ // Delegated event — only works on elements that support closest()
590
839
  el.addEventListener(evt, (e) => {
840
+ if (!e.target || typeof e.target.closest !== 'function') return;
591
841
  const target = e.target.closest(selectorOrHandler);
592
842
  if (target && el.contains(target)) handler.call(target, e);
593
843
  });
@@ -620,6 +870,10 @@ class ZQueryCollection {
620
870
  submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); }
621
871
  focus() { this.first()?.focus(); return this; }
622
872
  blur() { this.first()?.blur(); return this; }
873
+ hover(enterFn, leaveFn) {
874
+ this.on('mouseenter', enterFn);
875
+ return this.on('mouseleave', leaveFn || enterFn);
876
+ }
623
877
 
624
878
  // --- Animation -----------------------------------------------------------
625
879
 
@@ -651,6 +905,40 @@ class ZQueryCollection {
651
905
  return this.animate({ opacity: '0' }, duration).then(col => col.hide());
652
906
  }
653
907
 
908
+ fadeToggle(duration = 300) {
909
+ return Promise.all(this.elements.map(el => {
910
+ const visible = getComputedStyle(el).opacity !== '0' && getComputedStyle(el).display !== 'none';
911
+ const col = new ZQueryCollection([el]);
912
+ return visible ? col.fadeOut(duration) : col.fadeIn(duration);
913
+ })).then(() => this);
914
+ }
915
+
916
+ fadeTo(duration, opacity) {
917
+ return this.animate({ opacity: String(opacity) }, duration);
918
+ }
919
+
920
+ slideDown(duration = 300) {
921
+ return this.each((_, el) => {
922
+ el.style.display = '';
923
+ el.style.overflow = 'hidden';
924
+ const h = el.scrollHeight + 'px';
925
+ el.style.maxHeight = '0';
926
+ el.style.transition = `max-height ${duration}ms ease`;
927
+ requestAnimationFrame(() => { el.style.maxHeight = h; });
928
+ setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
929
+ });
930
+ }
931
+
932
+ slideUp(duration = 300) {
933
+ return this.each((_, el) => {
934
+ el.style.overflow = 'hidden';
935
+ el.style.maxHeight = el.scrollHeight + 'px';
936
+ el.style.transition = `max-height ${duration}ms ease`;
937
+ requestAnimationFrame(() => { el.style.maxHeight = '0'; });
938
+ setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
939
+ });
940
+ }
941
+
654
942
  slideToggle(duration = 300) {
655
943
  return this.each((_, el) => {
656
944
  if (el.style.display === 'none' || getComputedStyle(el).display === 'none') {
@@ -798,7 +1086,7 @@ query.children = (parentId) => {
798
1086
  return new ZQueryCollection(p ? Array.from(p.children) : []);
799
1087
  };
800
1088
 
801
- // Create element shorthand
1089
+ // Create element shorthand — returns ZQueryCollection for chaining
802
1090
  query.create = (tag, attrs = {}, ...children) => {
803
1091
  const el = document.createElement(tag);
804
1092
  for (const [k, v] of Object.entries(attrs)) {
@@ -812,7 +1100,7 @@ query.create = (tag, attrs = {}, ...children) => {
812
1100
  if (typeof child === 'string') el.appendChild(document.createTextNode(child));
813
1101
  else if (child instanceof Node) el.appendChild(child);
814
1102
  });
815
- return el;
1103
+ return new ZQueryCollection(el);
816
1104
  };
817
1105
 
818
1106
  // DOM ready
@@ -821,17 +1109,24 @@ query.ready = (fn) => {
821
1109
  else document.addEventListener('DOMContentLoaded', fn);
822
1110
  };
823
1111
 
824
- // Global event listeners — supports direct and delegated forms
1112
+ // Global event listeners — supports direct, delegated, and target-bound forms
825
1113
  // $.on('keydown', handler) → direct listener on document
826
1114
  // $.on('click', '.btn', handler) → delegated via closest()
1115
+ // $.on('scroll', window, handler) → direct listener on target
827
1116
  query.on = (event, selectorOrHandler, handler) => {
828
1117
  if (typeof selectorOrHandler === 'function') {
829
1118
  // 2-arg: direct document listener (keydown, resize, etc.)
830
1119
  document.addEventListener(event, selectorOrHandler);
831
1120
  return;
832
1121
  }
833
- // 3-arg: delegated
1122
+ // EventTarget (window, element, etc.) — direct listener on target
1123
+ if (typeof selectorOrHandler === 'object' && typeof selectorOrHandler.addEventListener === 'function') {
1124
+ selectorOrHandler.addEventListener(event, handler);
1125
+ return;
1126
+ }
1127
+ // 3-arg string: delegated
834
1128
  document.addEventListener(event, (e) => {
1129
+ if (!e.target || typeof e.target.closest !== 'function') return;
835
1130
  const target = e.target.closest(selectorOrHandler);
836
1131
  if (target) handler.call(target, e);
837
1132
  });
@@ -845,7 +1140,7 @@ query.off = (event, handler) => {
845
1140
  // Extend collection prototype (like $.fn in jQuery)
846
1141
  query.fn = ZQueryCollection.prototype;
847
1142
 
848
- // --- src/expression.js ———————————————————————————————————————————
1143
+ // --- src/expression.js -------------------------------------------
849
1144
  /**
850
1145
  * zQuery Expression Parser — CSP-safe expression evaluator
851
1146
  *
@@ -1653,7 +1948,7 @@ function safeEval(expr, scope) {
1653
1948
  }
1654
1949
  }
1655
1950
 
1656
- // --- src/diff.js —————————————————————————————————————————————————
1951
+ // --- src/diff.js -------------------------------------------------
1657
1952
  /**
1658
1953
  * zQuery Diff — Lightweight DOM morphing engine
1659
1954
  *
@@ -1935,7 +2230,7 @@ function _getKey(node) {
1935
2230
  return node.getAttribute('z-key') || null;
1936
2231
  }
1937
2232
 
1938
- // --- src/component.js ————————————————————————————————————————————
2233
+ // --- src/component.js --------------------------------------------
1939
2234
  /**
1940
2235
  * zQuery Component — Lightweight reactive component system
1941
2236
  *
@@ -2318,11 +2613,14 @@ class Component {
2318
2613
  if (def.styleUrl && !def._styleLoaded) {
2319
2614
  const su = def.styleUrl;
2320
2615
  if (typeof su === 'string') {
2321
- def._externalStyles = await _fetchResource(_resolveUrl(su, base));
2616
+ const resolved = _resolveUrl(su, base);
2617
+ def._externalStyles = await _fetchResource(resolved);
2618
+ def._resolvedStyleUrls = [resolved];
2322
2619
  } else if (Array.isArray(su)) {
2323
2620
  const urls = su.map(u => _resolveUrl(u, base));
2324
2621
  const results = await Promise.all(urls.map(u => _fetchResource(u)));
2325
2622
  def._externalStyles = results.join('\n');
2623
+ def._resolvedStyleUrls = urls;
2326
2624
  }
2327
2625
  def._styleLoaded = true;
2328
2626
  }
@@ -2449,6 +2747,13 @@ class Component {
2449
2747
  const styleEl = document.createElement('style');
2450
2748
  styleEl.textContent = scoped;
2451
2749
  styleEl.setAttribute('data-zq-component', this._def._name || '');
2750
+ styleEl.setAttribute('data-zq-scope', scopeAttr);
2751
+ if (this._def._resolvedStyleUrls) {
2752
+ styleEl.setAttribute('data-zq-style-urls', this._def._resolvedStyleUrls.join(' '));
2753
+ if (this._def.styles) {
2754
+ styleEl.setAttribute('data-zq-inline', this._def.styles);
2755
+ }
2756
+ }
2452
2757
  document.head.appendChild(styleEl);
2453
2758
  this._styleEl = styleEl;
2454
2759
  }
@@ -3235,7 +3540,7 @@ function style(urls, opts = {}) {
3235
3540
  };
3236
3541
  }
3237
3542
 
3238
- // --- src/router.js ———————————————————————————————————————————————
3543
+ // --- src/router.js -----------------------------------------------
3239
3544
  /**
3240
3545
  * zQuery Router — Client-side SPA router
3241
3546
  *
@@ -3315,7 +3620,21 @@ class Router {
3315
3620
  if (!link) return;
3316
3621
  if (link.getAttribute('target') === '_blank') return;
3317
3622
  e.preventDefault();
3318
- this.navigate(link.getAttribute('z-link'));
3623
+ let href = link.getAttribute('z-link');
3624
+ // Support z-link-params for dynamic :param interpolation
3625
+ const paramsAttr = link.getAttribute('z-link-params');
3626
+ if (paramsAttr) {
3627
+ try {
3628
+ const params = JSON.parse(paramsAttr);
3629
+ href = this._interpolateParams(href, params);
3630
+ } catch { /* ignore malformed JSON */ }
3631
+ }
3632
+ this.navigate(href);
3633
+ // z-to-top modifier: scroll to top after navigation
3634
+ if (link.hasAttribute('z-to-top')) {
3635
+ const scrollBehavior = link.getAttribute('z-to-top') || 'instant';
3636
+ window.scrollTo({ top: 0, behavior: scrollBehavior });
3637
+ }
3319
3638
  });
3320
3639
 
3321
3640
  // Initial resolve
@@ -3359,7 +3678,23 @@ class Router {
3359
3678
 
3360
3679
  // --- Navigation ----------------------------------------------------------
3361
3680
 
3681
+ /**
3682
+ * Interpolate :param placeholders in a path with the given values.
3683
+ * @param {string} path — e.g. '/user/:id/posts/:pid'
3684
+ * @param {Object} params — e.g. { id: 42, pid: 7 }
3685
+ * @returns {string}
3686
+ */
3687
+ _interpolateParams(path, params) {
3688
+ if (!params || typeof params !== 'object') return path;
3689
+ return path.replace(/:([\w]+)/g, (_, key) => {
3690
+ const val = params[key];
3691
+ return val != null ? encodeURIComponent(String(val)) : ':' + key;
3692
+ });
3693
+ }
3694
+
3362
3695
  navigate(path, options = {}) {
3696
+ // Interpolate :param placeholders if options.params is provided
3697
+ if (options.params) path = this._interpolateParams(path, options.params);
3363
3698
  // Separate hash fragment (e.g. /docs/getting-started#cli-bundler)
3364
3699
  const [cleanPath, fragment] = (path || '').split('#');
3365
3700
  let normalized = this._normalizePath(cleanPath);
@@ -3377,6 +3712,8 @@ class Router {
3377
3712
  }
3378
3713
 
3379
3714
  replace(path, options = {}) {
3715
+ // Interpolate :param placeholders if options.params is provided
3716
+ if (options.params) path = this._interpolateParams(path, options.params);
3380
3717
  const [cleanPath, fragment] = (path || '').split('#');
3381
3718
  let normalized = this._normalizePath(cleanPath);
3382
3719
  const hash = fragment ? '#' + fragment : '';
@@ -3490,6 +3827,7 @@ class Router {
3490
3827
  // Prevent re-entrant calls (e.g. listener triggering navigation)
3491
3828
  if (this._resolving) return;
3492
3829
  this._resolving = true;
3830
+ this._redirectCount = 0;
3493
3831
  try {
3494
3832
  await this.__resolve();
3495
3833
  } finally {
@@ -3531,7 +3869,21 @@ class Router {
3531
3869
  const result = await guard(to, from);
3532
3870
  if (result === false) return; // Cancel
3533
3871
  if (typeof result === 'string') { // Redirect
3534
- return this.navigate(result);
3872
+ if (++this._redirectCount > 10) {
3873
+ reportError(ErrorCode.ROUTER_GUARD, 'Too many guard redirects (possible loop)', { to }, null);
3874
+ return;
3875
+ }
3876
+ // Update URL directly and re-resolve (avoids re-entrancy block)
3877
+ const [rPath, rFrag] = result.split('#');
3878
+ const rNorm = this._normalizePath(rPath || '/');
3879
+ const rHash = rFrag ? '#' + rFrag : '';
3880
+ if (this._mode === 'hash') {
3881
+ if (rFrag) window.__zqScrollTarget = rFrag;
3882
+ window.location.replace('#' + rNorm);
3883
+ } else {
3884
+ window.history.replaceState({}, '', this._base + rNorm + rHash);
3885
+ }
3886
+ return this.__resolve();
3535
3887
  }
3536
3888
  } catch (err) {
3537
3889
  reportError(ErrorCode.ROUTER_GUARD, 'Before-guard threw', { to, from }, err);
@@ -3610,7 +3962,7 @@ function getRouter() {
3610
3962
  return _activeRouter;
3611
3963
  }
3612
3964
 
3613
- // --- src/store.js ————————————————————————————————————————————————
3965
+ // --- src/store.js ------------------------------------------------
3614
3966
  /**
3615
3967
  * zQuery Store — Global reactive state management
3616
3968
  *
@@ -3796,7 +4148,7 @@ function getStore(name = 'default') {
3796
4148
  return _stores.get(name) || null;
3797
4149
  }
3798
4150
 
3799
- // --- src/http.js —————————————————————————————————————————————————
4151
+ // --- src/http.js -------------------------------------------------
3800
4152
  /**
3801
4153
  * zQuery HTTP — Lightweight fetch wrapper
3802
4154
  *
@@ -3983,7 +4335,7 @@ const http = {
3983
4335
  raw: (url, opts) => fetch(url, opts),
3984
4336
  };
3985
4337
 
3986
- // --- src/utils.js ————————————————————————————————————————————————
4338
+ // --- src/utils.js ------------------------------------------------
3987
4339
  /**
3988
4340
  * zQuery Utils — Common utility functions
3989
4341
  *
@@ -4256,7 +4608,7 @@ class EventBus {
4256
4608
 
4257
4609
  const bus = new EventBus();
4258
4610
 
4259
- // --- index.js (assembly) ——————————————————————————————————————————
4611
+ // --- index.js (assembly) ------------------------------------------
4260
4612
  /**
4261
4613
  * ┌---------------------------------------------------------┐
4262
4614
  * │ zQuery (zeroQuery) — Lightweight Frontend Library │
@@ -4399,7 +4751,7 @@ $.ZQueryError = ZQueryError;
4399
4751
  $.ErrorCode = ErrorCode;
4400
4752
 
4401
4753
  // --- Meta ------------------------------------------------------------------
4402
- $.version = '0.6.3';
4754
+ $.version = '0.7.5';
4403
4755
  $.meta = {}; // populated at build time by CLI bundler
4404
4756
 
4405
4757
  $.noConflict = () => {