zero-query 0.2.0 → 0.2.2

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.
package/dist/zquery.js ADDED
@@ -0,0 +1,2605 @@
1
+ /**
2
+ * zQuery (zeroQuery) v0.2.2
3
+ * Lightweight Frontend Library
4
+ * https://github.com/tonywied17/zero-query
5
+ * (c) 2026 Anthony Wiedman — MIT License
6
+ */
7
+ (function(global) {
8
+ 'use strict';
9
+
10
+ // --- src/reactive.js —————————————————————————————————————————————
11
+ /**
12
+ * zQuery Reactive — Proxy-based deep reactivity system
13
+ *
14
+ * Creates observable objects that trigger callbacks on mutation.
15
+ * Used internally by components and store for auto-updates.
16
+ */
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Deep reactive proxy
20
+ // ---------------------------------------------------------------------------
21
+ function reactive(target, onChange, _path = '') {
22
+ if (typeof target !== 'object' || target === null) return target;
23
+
24
+ const proxyCache = new WeakMap();
25
+
26
+ const handler = {
27
+ get(obj, key) {
28
+ if (key === '__isReactive') return true;
29
+ if (key === '__raw') return obj;
30
+
31
+ const value = obj[key];
32
+ if (typeof value === 'object' && value !== null) {
33
+ // Return cached proxy or create new one
34
+ if (proxyCache.has(value)) return proxyCache.get(value);
35
+ const childProxy = new Proxy(value, handler);
36
+ proxyCache.set(value, childProxy);
37
+ return childProxy;
38
+ }
39
+ return value;
40
+ },
41
+
42
+ set(obj, key, value) {
43
+ const old = obj[key];
44
+ if (old === value) return true;
45
+ obj[key] = value;
46
+ onChange(key, value, old);
47
+ return true;
48
+ },
49
+
50
+ deleteProperty(obj, key) {
51
+ const old = obj[key];
52
+ delete obj[key];
53
+ onChange(key, undefined, old);
54
+ return true;
55
+ }
56
+ };
57
+
58
+ return new Proxy(target, handler);
59
+ }
60
+
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Signal — lightweight reactive primitive (inspired by Solid/Preact signals)
64
+ // ---------------------------------------------------------------------------
65
+ class Signal {
66
+ constructor(value) {
67
+ this._value = value;
68
+ this._subscribers = new Set();
69
+ }
70
+
71
+ get value() {
72
+ // Track dependency if there's an active effect
73
+ if (Signal._activeEffect) {
74
+ this._subscribers.add(Signal._activeEffect);
75
+ }
76
+ return this._value;
77
+ }
78
+
79
+ set value(newVal) {
80
+ if (this._value === newVal) return;
81
+ this._value = newVal;
82
+ this._notify();
83
+ }
84
+
85
+ peek() { return this._value; }
86
+
87
+ _notify() {
88
+ this._subscribers.forEach(fn => fn());
89
+ }
90
+
91
+ subscribe(fn) {
92
+ this._subscribers.add(fn);
93
+ return () => this._subscribers.delete(fn);
94
+ }
95
+
96
+ toString() { return String(this._value); }
97
+ }
98
+
99
+ // Active effect tracking
100
+ Signal._activeEffect = null;
101
+
102
+ /**
103
+ * Create a signal
104
+ * @param {*} initial — initial value
105
+ * @returns {Signal}
106
+ */
107
+ function signal(initial) {
108
+ return new Signal(initial);
109
+ }
110
+
111
+ /**
112
+ * Create a computed signal (derived from other signals)
113
+ * @param {Function} fn — computation function
114
+ * @returns {Signal}
115
+ */
116
+ function computed(fn) {
117
+ const s = new Signal(undefined);
118
+ effect(() => { s._value = fn(); s._notify(); });
119
+ return s;
120
+ }
121
+
122
+ /**
123
+ * Create a side-effect that auto-tracks signal dependencies
124
+ * @param {Function} fn — effect function
125
+ * @returns {Function} — dispose function
126
+ */
127
+ function effect(fn) {
128
+ const execute = () => {
129
+ Signal._activeEffect = execute;
130
+ try { fn(); }
131
+ finally { Signal._activeEffect = null; }
132
+ };
133
+ execute();
134
+ return () => { /* Signals will hold weak refs if needed */ };
135
+ }
136
+
137
+ // --- src/core.js —————————————————————————————————————————————————
138
+ /**
139
+ * zQuery Core — Selector engine & chainable DOM collection
140
+ *
141
+ * Extends the quick-ref pattern (Id, Class, Classes, Children)
142
+ * into a full jQuery-like chainable wrapper with modern APIs.
143
+ */
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // ZQueryCollection — wraps an array of elements with chainable methods
147
+ // ---------------------------------------------------------------------------
148
+ class ZQueryCollection {
149
+ constructor(elements) {
150
+ this.elements = Array.isArray(elements) ? elements : [elements];
151
+ this.length = this.elements.length;
152
+ this.elements.forEach((el, i) => { this[i] = el; });
153
+ }
154
+
155
+ // --- Iteration -----------------------------------------------------------
156
+
157
+ each(fn) {
158
+ this.elements.forEach((el, i) => fn.call(el, i, el));
159
+ return this;
160
+ }
161
+
162
+ map(fn) {
163
+ return this.elements.map((el, i) => fn.call(el, i, el));
164
+ }
165
+
166
+ first() { return this.elements[0] || null; }
167
+ last() { return this.elements[this.length - 1] || null; }
168
+ eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); }
169
+ toArray(){ return [...this.elements]; }
170
+
171
+ [Symbol.iterator]() { return this.elements[Symbol.iterator](); }
172
+
173
+ // --- Traversal -----------------------------------------------------------
174
+
175
+ find(selector) {
176
+ const found = [];
177
+ this.elements.forEach(el => found.push(...el.querySelectorAll(selector)));
178
+ return new ZQueryCollection(found);
179
+ }
180
+
181
+ parent() {
182
+ const parents = [...new Set(this.elements.map(el => el.parentElement).filter(Boolean))];
183
+ return new ZQueryCollection(parents);
184
+ }
185
+
186
+ closest(selector) {
187
+ return new ZQueryCollection(
188
+ this.elements.map(el => el.closest(selector)).filter(Boolean)
189
+ );
190
+ }
191
+
192
+ children(selector) {
193
+ const kids = [];
194
+ this.elements.forEach(el => {
195
+ kids.push(...(selector
196
+ ? el.querySelectorAll(`:scope > ${selector}`)
197
+ : el.children));
198
+ });
199
+ return new ZQueryCollection([...kids]);
200
+ }
201
+
202
+ siblings() {
203
+ const sibs = [];
204
+ this.elements.forEach(el => {
205
+ sibs.push(...[...el.parentElement.children].filter(c => c !== el));
206
+ });
207
+ return new ZQueryCollection(sibs);
208
+ }
209
+
210
+ next() { return new ZQueryCollection(this.elements.map(el => el.nextElementSibling).filter(Boolean)); }
211
+ prev() { return new ZQueryCollection(this.elements.map(el => el.previousElementSibling).filter(Boolean)); }
212
+
213
+ filter(selector) {
214
+ if (typeof selector === 'function') {
215
+ return new ZQueryCollection(this.elements.filter(selector));
216
+ }
217
+ return new ZQueryCollection(this.elements.filter(el => el.matches(selector)));
218
+ }
219
+
220
+ not(selector) {
221
+ if (typeof selector === 'function') {
222
+ return new ZQueryCollection(this.elements.filter((el, i) => !selector.call(el, i, el)));
223
+ }
224
+ return new ZQueryCollection(this.elements.filter(el => !el.matches(selector)));
225
+ }
226
+
227
+ has(selector) {
228
+ return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
229
+ }
230
+
231
+ // --- Classes -------------------------------------------------------------
232
+
233
+ addClass(...names) {
234
+ const classes = names.flatMap(n => n.split(/\s+/));
235
+ return this.each((_, el) => el.classList.add(...classes));
236
+ }
237
+
238
+ removeClass(...names) {
239
+ const classes = names.flatMap(n => n.split(/\s+/));
240
+ return this.each((_, el) => el.classList.remove(...classes));
241
+ }
242
+
243
+ toggleClass(name, force) {
244
+ return this.each((_, el) => el.classList.toggle(name, force));
245
+ }
246
+
247
+ hasClass(name) {
248
+ return this.first()?.classList.contains(name) || false;
249
+ }
250
+
251
+ // --- Attributes ----------------------------------------------------------
252
+
253
+ attr(name, value) {
254
+ if (value === undefined) return this.first()?.getAttribute(name);
255
+ return this.each((_, el) => el.setAttribute(name, value));
256
+ }
257
+
258
+ removeAttr(name) {
259
+ return this.each((_, el) => el.removeAttribute(name));
260
+ }
261
+
262
+ prop(name, value) {
263
+ if (value === undefined) return this.first()?.[name];
264
+ return this.each((_, el) => { el[name] = value; });
265
+ }
266
+
267
+ data(key, value) {
268
+ if (value === undefined) {
269
+ if (key === undefined) return this.first()?.dataset;
270
+ const raw = this.first()?.dataset[key];
271
+ try { return JSON.parse(raw); } catch { return raw; }
272
+ }
273
+ return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; });
274
+ }
275
+
276
+ // --- CSS / Dimensions ----------------------------------------------------
277
+
278
+ css(props) {
279
+ if (typeof props === 'string') {
280
+ return getComputedStyle(this.first())[props];
281
+ }
282
+ return this.each((_, el) => Object.assign(el.style, props));
283
+ }
284
+
285
+ width() { return this.first()?.getBoundingClientRect().width; }
286
+ height() { return this.first()?.getBoundingClientRect().height; }
287
+
288
+ offset() {
289
+ const r = this.first()?.getBoundingClientRect();
290
+ return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null;
291
+ }
292
+
293
+ position() {
294
+ const el = this.first();
295
+ return el ? { top: el.offsetTop, left: el.offsetLeft } : null;
296
+ }
297
+
298
+ // --- Content -------------------------------------------------------------
299
+
300
+ html(content) {
301
+ if (content === undefined) return this.first()?.innerHTML;
302
+ return this.each((_, el) => { el.innerHTML = content; });
303
+ }
304
+
305
+ text(content) {
306
+ if (content === undefined) return this.first()?.textContent;
307
+ return this.each((_, el) => { el.textContent = content; });
308
+ }
309
+
310
+ val(value) {
311
+ if (value === undefined) return this.first()?.value;
312
+ return this.each((_, el) => { el.value = value; });
313
+ }
314
+
315
+ // --- DOM Manipulation ----------------------------------------------------
316
+
317
+ append(content) {
318
+ return this.each((_, el) => {
319
+ if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content);
320
+ else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c));
321
+ else if (content instanceof Node) el.appendChild(content);
322
+ });
323
+ }
324
+
325
+ prepend(content) {
326
+ return this.each((_, el) => {
327
+ if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content);
328
+ else if (content instanceof Node) el.insertBefore(content, el.firstChild);
329
+ });
330
+ }
331
+
332
+ after(content) {
333
+ return this.each((_, el) => {
334
+ if (typeof content === 'string') el.insertAdjacentHTML('afterend', content);
335
+ else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling);
336
+ });
337
+ }
338
+
339
+ before(content) {
340
+ return this.each((_, el) => {
341
+ if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content);
342
+ else if (content instanceof Node) el.parentNode.insertBefore(content, el);
343
+ });
344
+ }
345
+
346
+ wrap(wrapper) {
347
+ return this.each((_, el) => {
348
+ const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
349
+ el.parentNode.insertBefore(w, el);
350
+ w.appendChild(el);
351
+ });
352
+ }
353
+
354
+ remove() {
355
+ return this.each((_, el) => el.remove());
356
+ }
357
+
358
+ empty() {
359
+ return this.each((_, el) => { el.innerHTML = ''; });
360
+ }
361
+
362
+ clone(deep = true) {
363
+ return new ZQueryCollection(this.elements.map(el => el.cloneNode(deep)));
364
+ }
365
+
366
+ replaceWith(content) {
367
+ return this.each((_, el) => {
368
+ if (typeof content === 'string') {
369
+ el.insertAdjacentHTML('afterend', content);
370
+ el.remove();
371
+ } else if (content instanceof Node) {
372
+ el.parentNode.replaceChild(content, el);
373
+ }
374
+ });
375
+ }
376
+
377
+ // --- Visibility ----------------------------------------------------------
378
+
379
+ show(display = '') {
380
+ return this.each((_, el) => { el.style.display = display; });
381
+ }
382
+
383
+ hide() {
384
+ return this.each((_, el) => { el.style.display = 'none'; });
385
+ }
386
+
387
+ toggle(display = '') {
388
+ return this.each((_, el) => {
389
+ el.style.display = (el.style.display === 'none' || getComputedStyle(el).display === 'none') ? display : 'none';
390
+ });
391
+ }
392
+
393
+ // --- Events --------------------------------------------------------------
394
+
395
+ on(event, selectorOrHandler, handler) {
396
+ // Support multiple events: "click mouseenter"
397
+ const events = event.split(/\s+/);
398
+ return this.each((_, el) => {
399
+ events.forEach(evt => {
400
+ if (typeof selectorOrHandler === 'function') {
401
+ el.addEventListener(evt, selectorOrHandler);
402
+ } else {
403
+ // Delegated event
404
+ el.addEventListener(evt, (e) => {
405
+ const target = e.target.closest(selectorOrHandler);
406
+ if (target && el.contains(target)) handler.call(target, e);
407
+ });
408
+ }
409
+ });
410
+ });
411
+ }
412
+
413
+ off(event, handler) {
414
+ const events = event.split(/\s+/);
415
+ return this.each((_, el) => {
416
+ events.forEach(evt => el.removeEventListener(evt, handler));
417
+ });
418
+ }
419
+
420
+ one(event, handler) {
421
+ return this.each((_, el) => {
422
+ el.addEventListener(event, handler, { once: true });
423
+ });
424
+ }
425
+
426
+ trigger(event, detail) {
427
+ return this.each((_, el) => {
428
+ el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
429
+ });
430
+ }
431
+
432
+ // Convenience event shorthands
433
+ click(fn) { return fn ? this.on('click', fn) : this.trigger('click'); }
434
+ submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); }
435
+ focus() { this.first()?.focus(); return this; }
436
+ blur() { this.first()?.blur(); return this; }
437
+
438
+ // --- Animation -----------------------------------------------------------
439
+
440
+ animate(props, duration = 300, easing = 'ease') {
441
+ return new Promise(resolve => {
442
+ const count = { done: 0 };
443
+ this.each((_, el) => {
444
+ el.style.transition = `all ${duration}ms ${easing}`;
445
+ requestAnimationFrame(() => {
446
+ Object.assign(el.style, props);
447
+ const onEnd = () => {
448
+ el.removeEventListener('transitionend', onEnd);
449
+ el.style.transition = '';
450
+ if (++count.done >= this.length) resolve(this);
451
+ };
452
+ el.addEventListener('transitionend', onEnd);
453
+ });
454
+ });
455
+ // Fallback in case transitionend doesn't fire
456
+ setTimeout(() => resolve(this), duration + 50);
457
+ });
458
+ }
459
+
460
+ fadeIn(duration = 300) {
461
+ return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration);
462
+ }
463
+
464
+ fadeOut(duration = 300) {
465
+ return this.animate({ opacity: '0' }, duration).then(col => col.hide());
466
+ }
467
+
468
+ slideToggle(duration = 300) {
469
+ return this.each((_, el) => {
470
+ if (el.style.display === 'none' || getComputedStyle(el).display === 'none') {
471
+ el.style.display = '';
472
+ el.style.overflow = 'hidden';
473
+ const h = el.scrollHeight + 'px';
474
+ el.style.maxHeight = '0';
475
+ el.style.transition = `max-height ${duration}ms ease`;
476
+ requestAnimationFrame(() => { el.style.maxHeight = h; });
477
+ setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
478
+ } else {
479
+ el.style.overflow = 'hidden';
480
+ el.style.maxHeight = el.scrollHeight + 'px';
481
+ el.style.transition = `max-height ${duration}ms ease`;
482
+ requestAnimationFrame(() => { el.style.maxHeight = '0'; });
483
+ setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
484
+ }
485
+ });
486
+ }
487
+
488
+ // --- Form helpers --------------------------------------------------------
489
+
490
+ serialize() {
491
+ const form = this.first();
492
+ if (!form || form.tagName !== 'FORM') return '';
493
+ return new URLSearchParams(new FormData(form)).toString();
494
+ }
495
+
496
+ serializeObject() {
497
+ const form = this.first();
498
+ if (!form || form.tagName !== 'FORM') return {};
499
+ const obj = {};
500
+ new FormData(form).forEach((v, k) => {
501
+ if (obj[k] !== undefined) {
502
+ if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
503
+ obj[k].push(v);
504
+ } else {
505
+ obj[k] = v;
506
+ }
507
+ });
508
+ return obj;
509
+ }
510
+ }
511
+
512
+
513
+ // ---------------------------------------------------------------------------
514
+ // Helper — create document fragment from HTML string
515
+ // ---------------------------------------------------------------------------
516
+ function createFragment(html) {
517
+ const tpl = document.createElement('template');
518
+ tpl.innerHTML = html.trim();
519
+ return tpl.content;
520
+ }
521
+
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // $() — main selector / creator function (returns single element for CSS selectors)
525
+ // ---------------------------------------------------------------------------
526
+ function query(selector, context) {
527
+ // null / undefined
528
+ if (!selector) return null;
529
+
530
+ // Already a collection — return first element
531
+ if (selector instanceof ZQueryCollection) return selector.first();
532
+
533
+ // DOM element or Window — return as-is
534
+ if (selector instanceof Node || selector === window) {
535
+ return selector;
536
+ }
537
+
538
+ // NodeList / HTMLCollection / Array — return first element
539
+ if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
540
+ const arr = Array.from(selector);
541
+ return arr[0] || null;
542
+ }
543
+
544
+ // HTML string → create elements, return first
545
+ if (typeof selector === 'string' && selector.trim().startsWith('<')) {
546
+ const fragment = createFragment(selector);
547
+ const els = [...fragment.childNodes].filter(n => n.nodeType === 1);
548
+ return els[0] || null;
549
+ }
550
+
551
+ // CSS selector string → querySelector (single element)
552
+ if (typeof selector === 'string') {
553
+ const root = context
554
+ ? (typeof context === 'string' ? document.querySelector(context) : context)
555
+ : document;
556
+ return root.querySelector(selector);
557
+ }
558
+
559
+ return null;
560
+ }
561
+
562
+
563
+ // ---------------------------------------------------------------------------
564
+ // $.all() — collection selector (returns ZQueryCollection for CSS selectors)
565
+ // ---------------------------------------------------------------------------
566
+ function queryAll(selector, context) {
567
+ // null / undefined
568
+ if (!selector) return new ZQueryCollection([]);
569
+
570
+ // Already a collection
571
+ if (selector instanceof ZQueryCollection) return selector;
572
+
573
+ // DOM element or Window
574
+ if (selector instanceof Node || selector === window) {
575
+ return new ZQueryCollection([selector]);
576
+ }
577
+
578
+ // NodeList / HTMLCollection / Array
579
+ if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
580
+ return new ZQueryCollection(Array.from(selector));
581
+ }
582
+
583
+ // HTML string → create elements
584
+ if (typeof selector === 'string' && selector.trim().startsWith('<')) {
585
+ const fragment = createFragment(selector);
586
+ return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
587
+ }
588
+
589
+ // CSS selector string → querySelectorAll (collection)
590
+ if (typeof selector === 'string') {
591
+ const root = context
592
+ ? (typeof context === 'string' ? document.querySelector(context) : context)
593
+ : document;
594
+ return new ZQueryCollection([...root.querySelectorAll(selector)]);
595
+ }
596
+
597
+ return new ZQueryCollection([]);
598
+ }
599
+
600
+
601
+ // ---------------------------------------------------------------------------
602
+ // Quick-ref shortcuts, on $ namespace)
603
+ // ---------------------------------------------------------------------------
604
+ query.id = (id) => document.getElementById(id);
605
+ query.class = (name) => document.querySelector(`.${name}`);
606
+ query.classes = (name) => Array.from(document.getElementsByClassName(name));
607
+ query.tag = (name) => Array.from(document.getElementsByTagName(name));
608
+ query.children = (parentId) => {
609
+ const p = document.getElementById(parentId);
610
+ return p ? Array.from(p.children) : [];
611
+ };
612
+
613
+ // Create element shorthand
614
+ query.create = (tag, attrs = {}, ...children) => {
615
+ const el = document.createElement(tag);
616
+ for (const [k, v] of Object.entries(attrs)) {
617
+ if (k === 'class') el.className = v;
618
+ else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
619
+ else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
620
+ else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
621
+ else el.setAttribute(k, v);
622
+ }
623
+ children.flat().forEach(child => {
624
+ if (typeof child === 'string') el.appendChild(document.createTextNode(child));
625
+ else if (child instanceof Node) el.appendChild(child);
626
+ });
627
+ return el;
628
+ };
629
+
630
+ // DOM ready
631
+ query.ready = (fn) => {
632
+ if (document.readyState !== 'loading') fn();
633
+ else document.addEventListener('DOMContentLoaded', fn);
634
+ };
635
+
636
+ // Global event delegation
637
+ query.on = (event, selector, handler) => {
638
+ document.addEventListener(event, (e) => {
639
+ const target = e.target.closest(selector);
640
+ if (target) handler.call(target, e);
641
+ });
642
+ };
643
+
644
+ // Extend collection prototype (like $.fn in jQuery)
645
+ query.fn = ZQueryCollection.prototype;
646
+
647
+ // --- src/component.js ————————————————————————————————————————————
648
+ /**
649
+ * zQuery Component — Lightweight reactive component system
650
+ *
651
+ * Declarative components using template literals (no JSX, no build step).
652
+ * Proxy-based state triggers targeted re-renders via event delegation.
653
+ *
654
+ * Features:
655
+ * - Reactive state (auto re-render on mutation)
656
+ * - Template literals with full JS expression power
657
+ * - @event="method" syntax for event binding (delegated)
658
+ * - z-ref="name" for element references
659
+ * - z-model="stateKey" for two-way binding
660
+ * - Lifecycle hooks: init, mounted, updated, destroyed
661
+ * - Props passed via attributes
662
+ * - Scoped styles (inline or via styleUrl)
663
+ * - External templates via templateUrl (with {{expression}} interpolation)
664
+ * - External styles via styleUrl (fetched & scoped automatically)
665
+ * - Relative path resolution — templateUrl, styleUrl, and pages.dir
666
+ * resolve relative to the component file automatically
667
+ */
668
+
669
+
670
+ // ---------------------------------------------------------------------------
671
+ // Component registry & external resource cache
672
+ // ---------------------------------------------------------------------------
673
+ const _registry = new Map(); // name → definition
674
+ const _instances = new Map(); // element → instance
675
+ const _resourceCache = new Map(); // url → Promise<string>
676
+
677
+ // Unique ID counter
678
+ let _uid = 0;
679
+
680
+ /**
681
+ * Fetch and cache a text resource (HTML template or CSS file).
682
+ * @param {string} url — URL to fetch
683
+ * @returns {Promise<string>}
684
+ */
685
+ function _fetchResource(url) {
686
+ if (_resourceCache.has(url)) return _resourceCache.get(url);
687
+
688
+ // Check inline resource map (populated by CLI bundler for file:// support).
689
+ // Keys are relative paths; match against the URL suffix.
690
+ if (typeof window !== 'undefined' && window.__zqInline) {
691
+ for (const [path, content] of Object.entries(window.__zqInline)) {
692
+ if (url === path || url.endsWith('/' + path) || url.endsWith('\\' + path)) {
693
+ const resolved = Promise.resolve(content);
694
+ _resourceCache.set(url, resolved);
695
+ return resolved;
696
+ }
697
+ }
698
+ }
699
+
700
+ // Resolve relative URLs against <base href> or origin root.
701
+ // This prevents SPA route paths (e.g. /docs/advanced) from
702
+ // breaking relative resource URLs like 'scripts/components/foo.css'.
703
+ let resolvedUrl = url;
704
+ if (typeof url === 'string' && !url.startsWith('/') && !url.includes(':') && !url.startsWith('//')) {
705
+ try {
706
+ const baseEl = document.querySelector('base');
707
+ const root = baseEl ? baseEl.href : (window.location.origin + '/');
708
+ resolvedUrl = new URL(url, root).href;
709
+ } catch { /* keep original */ }
710
+ }
711
+
712
+ const promise = fetch(resolvedUrl).then(res => {
713
+ if (!res.ok) throw new Error(`zQuery: Failed to load resource "${url}" (${res.status})`);
714
+ return res.text();
715
+ });
716
+ _resourceCache.set(url, promise);
717
+ return promise;
718
+ }
719
+
720
+ /**
721
+ * Convert a kebab-case id to Title Case.
722
+ * 'getting-started' → 'Getting Started'
723
+ * @param {string} id
724
+ * @returns {string}
725
+ */
726
+ function _titleCase(id) {
727
+ return id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
728
+ }
729
+
730
+ /**
731
+ * Resolve a relative URL against a base.
732
+ *
733
+ * - If `base` is an absolute URL (http/https/file), resolve directly.
734
+ * - If `base` is a relative path string, resolve it against the page root
735
+ * (or <base href>) first, then resolve `url` against that.
736
+ * - If `base` is falsy, return `url` unchanged — _fetchResource's own
737
+ * fallback (page root / <base href>) handles it.
738
+ *
739
+ * @param {string} url — URL or relative path to resolve
740
+ * @param {string} [base] — auto-detected caller URL or explicit base path
741
+ * @returns {string}
742
+ */
743
+ function _resolveUrl(url, base) {
744
+ if (!base || !url || typeof url !== 'string') return url;
745
+ // Already absolute — nothing to do
746
+ if (url.startsWith('/') || url.includes('://') || url.startsWith('//')) return url;
747
+ try {
748
+ if (base.includes('://')) {
749
+ // Absolute base (auto-detected module URL)
750
+ return new URL(url, base).href;
751
+ }
752
+ // Relative base string — resolve against page root first
753
+ const baseEl = document.querySelector('base');
754
+ const root = baseEl ? baseEl.href : (window.location.origin + '/');
755
+ const absBase = new URL(base.endsWith('/') ? base : base + '/', root).href;
756
+ return new URL(url, absBase).href;
757
+ } catch {
758
+ return url;
759
+ }
760
+ }
761
+
762
+ // Capture the library's own script URL at load time for reliable filtering.
763
+ // This handles cases where the bundle is renamed (e.g., 'vendor.js').
764
+ let _ownScriptUrl;
765
+ try {
766
+ if (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) {
767
+ _ownScriptUrl = document.currentScript.src.replace(/[?#].*$/, '');
768
+ }
769
+ } catch { /* ignored */ }
770
+
771
+ /**
772
+ * Detect the URL of the module that called $.component().
773
+ * Parses Error().stack to find the first frame outside the zQuery bundle.
774
+ * Returns the directory URL (with trailing slash) or undefined.
775
+ * @returns {string|undefined}
776
+ */
777
+ function _detectCallerBase() {
778
+ try {
779
+ const stack = new Error().stack || '';
780
+ const urls = stack.match(/(?:https?|file):\/\/[^\s\)]+/g) || [];
781
+ for (const raw of urls) {
782
+ // Strip line:col suffixes e.g. ":3:5" or ":12:1"
783
+ const url = raw.replace(/:\d+:\d+$/, '').replace(/:\d+$/, '');
784
+ // Skip the zQuery library itself — by filename pattern and captured URL
785
+ if (/zquery(\.min)?\.js$/i.test(url)) continue;
786
+ if (_ownScriptUrl && url.replace(/[?#].*$/, '') === _ownScriptUrl) continue;
787
+ // Return directory (strip filename, keep trailing slash)
788
+ return url.replace(/\/[^/]*$/, '/');
789
+ }
790
+ } catch { /* stack parsing unsupported — fall back silently */ }
791
+ return undefined;
792
+ }
793
+
794
+ /**
795
+ * Get a value from a nested object by dot-path.
796
+ * _getPath(obj, 'user.name') → obj.user.name
797
+ * @param {object} obj
798
+ * @param {string} path
799
+ * @returns {*}
800
+ */
801
+ function _getPath(obj, path) {
802
+ return path.split('.').reduce((o, k) => o?.[k], obj);
803
+ }
804
+
805
+ /**
806
+ * Set a value on a nested object by dot-path, walking through proxy layers.
807
+ * _setPath(proxy, 'user.name', 'Tony') → proxy.user.name = 'Tony'
808
+ * @param {object} obj
809
+ * @param {string} path
810
+ * @param {*} value
811
+ */
812
+ function _setPath(obj, path, value) {
813
+ const keys = path.split('.');
814
+ const last = keys.pop();
815
+ const target = keys.reduce((o, k) => (o && typeof o === 'object') ? o[k] : undefined, obj);
816
+ if (target && typeof target === 'object') target[last] = value;
817
+ }
818
+
819
+
820
+ // ---------------------------------------------------------------------------
821
+ // Component class
822
+ // ---------------------------------------------------------------------------
823
+ class Component {
824
+ constructor(el, definition, props = {}) {
825
+ this._uid = ++_uid;
826
+ this._el = el;
827
+ this._def = definition;
828
+ this._mounted = false;
829
+ this._destroyed = false;
830
+ this._updateQueued = false;
831
+ this._listeners = [];
832
+
833
+ // Refs map
834
+ this.refs = {};
835
+
836
+ // Props (read-only from parent)
837
+ this.props = Object.freeze({ ...props });
838
+
839
+ // Reactive state
840
+ const initialState = typeof definition.state === 'function'
841
+ ? definition.state()
842
+ : { ...(definition.state || {}) };
843
+
844
+ this.state = reactive(initialState, () => {
845
+ if (!this._destroyed) this._scheduleUpdate();
846
+ });
847
+
848
+ // Bind all user methods to this instance
849
+ for (const [key, val] of Object.entries(definition)) {
850
+ if (typeof val === 'function' && !_reservedKeys.has(key)) {
851
+ this[key] = val.bind(this);
852
+ }
853
+ }
854
+
855
+ // Init lifecycle
856
+ if (definition.init) definition.init.call(this);
857
+ }
858
+
859
+ // Schedule a batched DOM update (microtask)
860
+ _scheduleUpdate() {
861
+ if (this._updateQueued) return;
862
+ this._updateQueued = true;
863
+ queueMicrotask(() => {
864
+ this._updateQueued = false;
865
+ if (!this._destroyed) this._render();
866
+ });
867
+ }
868
+
869
+ // Load external templateUrl / styleUrl if specified (once per definition)
870
+ //
871
+ // Relative paths are resolved automatically against the component file's
872
+ // own directory (auto-detected at registration time). You can override
873
+ // this with `base: 'some/path/'` on the definition.
874
+ //
875
+ // templateUrl accepts:
876
+ // - string → single template (used with {{expr}} interpolation)
877
+ // - string[] → array of URLs → indexed map via this.templates[0], …
878
+ // - { key: url, … } → named map → this.templates.key
879
+ //
880
+ // styleUrl accepts:
881
+ // - string → single stylesheet
882
+ // - string[] → array of URLs → all fetched & concatenated
883
+ //
884
+ // pages config (shorthand for multi-template + route-param page switching):
885
+ // pages: {
886
+ // dir: 'pages', // relative to component file (or base)
887
+ // param: 'section', // route param name → this.activePage
888
+ // default: 'getting-started', // fallback when param is absent
889
+ // ext: '.html', // file extension (default '.html')
890
+ // items: ['page-a', { id: 'page-b', label: 'Page B' }, ...]
891
+ // }
892
+ // Exposes this.pages (array of {id,label}), this.activePage (current id)
893
+ //
894
+ async _loadExternals() {
895
+ const def = this._def;
896
+ const base = def._base; // auto-detected or explicit
897
+
898
+ // ── Pages config ─────────────────────────────────────────────
899
+ if (def.pages && !def._pagesNormalized) {
900
+ const p = def.pages;
901
+ const ext = p.ext || '.html';
902
+ const dir = _resolveUrl((p.dir || '').replace(/\/+$/, ''), base);
903
+
904
+ // Normalize items → [{id, label}, …]
905
+ def._pages = (p.items || []).map(item => {
906
+ if (typeof item === 'string') return { id: item, label: _titleCase(item) };
907
+ return { id: item.id, label: item.label || _titleCase(item.id) };
908
+ });
909
+
910
+ // Auto-generate templateUrl object map
911
+ if (!def.templateUrl) {
912
+ def.templateUrl = {};
913
+ for (const { id } of def._pages) {
914
+ def.templateUrl[id] = `${dir}/${id}${ext}`;
915
+ }
916
+ }
917
+
918
+ def._pagesNormalized = true;
919
+ }
920
+
921
+ // ── External templates ──────────────────────────────────────
922
+ if (def.templateUrl && !def._templateLoaded) {
923
+ const tu = def.templateUrl;
924
+ if (typeof tu === 'string') {
925
+ def._externalTemplate = await _fetchResource(_resolveUrl(tu, base));
926
+ } else if (Array.isArray(tu)) {
927
+ const urls = tu.map(u => _resolveUrl(u, base));
928
+ const results = await Promise.all(urls.map(u => _fetchResource(u)));
929
+ def._externalTemplates = {};
930
+ results.forEach((html, i) => { def._externalTemplates[i] = html; });
931
+ } else if (typeof tu === 'object') {
932
+ const entries = Object.entries(tu);
933
+ // Pages config already resolved; plain objects still need resolving
934
+ const results = await Promise.all(
935
+ entries.map(([, url]) => _fetchResource(def._pagesNormalized ? url : _resolveUrl(url, base)))
936
+ );
937
+ def._externalTemplates = {};
938
+ entries.forEach(([key], i) => { def._externalTemplates[key] = results[i]; });
939
+ }
940
+ def._templateLoaded = true;
941
+ }
942
+
943
+ // ── External styles ─────────────────────────────────────────
944
+ if (def.styleUrl && !def._styleLoaded) {
945
+ const su = def.styleUrl;
946
+ if (typeof su === 'string') {
947
+ def._externalStyles = await _fetchResource(_resolveUrl(su, base));
948
+ } else if (Array.isArray(su)) {
949
+ const urls = su.map(u => _resolveUrl(u, base));
950
+ const results = await Promise.all(urls.map(u => _fetchResource(u)));
951
+ def._externalStyles = results.join('\n');
952
+ }
953
+ def._styleLoaded = true;
954
+ }
955
+ }
956
+
957
+ // Render the component
958
+ _render() {
959
+ // If externals haven't loaded yet, trigger async load then re-render
960
+ if ((this._def.templateUrl && !this._def._templateLoaded) ||
961
+ (this._def.styleUrl && !this._def._styleLoaded) ||
962
+ (this._def.pages && !this._def._pagesNormalized)) {
963
+ this._loadExternals().then(() => {
964
+ if (!this._destroyed) this._render();
965
+ });
966
+ return; // Skip this render — will re-render after load
967
+ }
968
+
969
+ // Expose multi-template map on instance (if available)
970
+ if (this._def._externalTemplates) {
971
+ this.templates = this._def._externalTemplates;
972
+ }
973
+
974
+ // Expose pages metadata and active page (derived from route param)
975
+ if (this._def._pages) {
976
+ this.pages = this._def._pages;
977
+ const pc = this._def.pages;
978
+ this.activePage = (pc.param && this.props.$params?.[pc.param]) || pc.default || this._def._pages[0]?.id || '';
979
+ }
980
+
981
+ // Determine HTML content
982
+ let html;
983
+ if (this._def.render) {
984
+ // Inline render function takes priority
985
+ html = this._def.render.call(this);
986
+ } else if (this._def._externalTemplate) {
987
+ // External template with {{expression}} interpolation
988
+ html = this._def._externalTemplate.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
989
+ try {
990
+ return new Function('state', 'props', '$', `with(state){return ${expr.trim()}}`)(
991
+ this.state.__raw || this.state,
992
+ this.props,
993
+ typeof window !== 'undefined' ? window.$ : undefined
994
+ );
995
+ } catch { return ''; }
996
+ });
997
+ } else {
998
+ html = '';
999
+ }
1000
+
1001
+ // Combine inline styles + external styles
1002
+ const combinedStyles = [
1003
+ this._def.styles || '',
1004
+ this._def._externalStyles || ''
1005
+ ].filter(Boolean).join('\n');
1006
+
1007
+ // Apply scoped styles on first render
1008
+ if (!this._mounted && combinedStyles) {
1009
+ const scopeAttr = `z-s${this._uid}`;
1010
+ this._el.setAttribute(scopeAttr, '');
1011
+ const scoped = combinedStyles.replace(/([^{}]+)\{/g, (match, selector) => {
1012
+ return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
1013
+ });
1014
+ const styleEl = document.createElement('style');
1015
+ styleEl.textContent = scoped;
1016
+ styleEl.setAttribute('data-zq-component', this._def._name || '');
1017
+ document.head.appendChild(styleEl);
1018
+ this._styleEl = styleEl;
1019
+ }
1020
+
1021
+ // ── Focus preservation for z-model ────────────────────────────
1022
+ // Before replacing innerHTML, save focus state so we can restore
1023
+ // cursor position after the DOM is rebuilt.
1024
+ let _focusInfo = null;
1025
+ const _active = document.activeElement;
1026
+ if (_active && this._el.contains(_active)) {
1027
+ const modelKey = _active.getAttribute?.('z-model');
1028
+ if (modelKey) {
1029
+ _focusInfo = {
1030
+ key: modelKey,
1031
+ start: _active.selectionStart,
1032
+ end: _active.selectionEnd,
1033
+ dir: _active.selectionDirection,
1034
+ };
1035
+ }
1036
+ }
1037
+
1038
+ // Update DOM
1039
+ this._el.innerHTML = html;
1040
+
1041
+ // Process directives
1042
+ this._bindEvents();
1043
+ this._bindRefs();
1044
+ this._bindModels();
1045
+
1046
+ // Restore focus to z-model element after re-render
1047
+ if (_focusInfo) {
1048
+ const el = this._el.querySelector(`[z-model="${_focusInfo.key}"]`);
1049
+ if (el) {
1050
+ el.focus();
1051
+ try {
1052
+ if (_focusInfo.start !== null && _focusInfo.start !== undefined) {
1053
+ el.setSelectionRange(_focusInfo.start, _focusInfo.end, _focusInfo.dir);
1054
+ }
1055
+ } catch (_) { /* some input types don't support setSelectionRange */ }
1056
+ }
1057
+ }
1058
+
1059
+ // Mount nested components
1060
+ mountAll(this._el);
1061
+
1062
+ if (!this._mounted) {
1063
+ this._mounted = true;
1064
+ if (this._def.mounted) this._def.mounted.call(this);
1065
+ } else {
1066
+ if (this._def.updated) this._def.updated.call(this);
1067
+ }
1068
+ }
1069
+
1070
+ // Bind @event="method" handlers via delegation
1071
+ _bindEvents() {
1072
+ // Clean up old delegated listeners
1073
+ this._listeners.forEach(({ event, handler }) => {
1074
+ this._el.removeEventListener(event, handler);
1075
+ });
1076
+ this._listeners = [];
1077
+
1078
+ // Find all elements with @event attributes
1079
+ const allEls = this._el.querySelectorAll('*');
1080
+ const eventMap = new Map(); // event → [{ selector, method, modifiers }]
1081
+
1082
+ allEls.forEach(child => {
1083
+ [...child.attributes].forEach(attr => {
1084
+ if (!attr.name.startsWith('@')) return;
1085
+
1086
+ const raw = attr.name.slice(1); // e.g. "click.prevent"
1087
+ const parts = raw.split('.');
1088
+ const event = parts[0];
1089
+ const modifiers = parts.slice(1);
1090
+ const methodExpr = attr.value;
1091
+
1092
+ // Give element a unique selector for delegation
1093
+ if (!child.dataset.zqEid) {
1094
+ child.dataset.zqEid = String(++_uid);
1095
+ }
1096
+ const selector = `[data-zq-eid="${child.dataset.zqEid}"]`;
1097
+
1098
+ if (!eventMap.has(event)) eventMap.set(event, []);
1099
+ eventMap.get(event).push({ selector, methodExpr, modifiers, el: child });
1100
+ });
1101
+ });
1102
+
1103
+ // Register delegated listeners on the component root
1104
+ for (const [event, bindings] of eventMap) {
1105
+ const handler = (e) => {
1106
+ for (const { selector, methodExpr, modifiers, el } of bindings) {
1107
+ if (!e.target.closest(selector)) continue;
1108
+
1109
+ // Handle modifiers
1110
+ if (modifiers.includes('prevent')) e.preventDefault();
1111
+ if (modifiers.includes('stop')) e.stopPropagation();
1112
+
1113
+ // Parse method expression: "method" or "method(arg1, arg2)"
1114
+ const match = methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
1115
+ if (match) {
1116
+ const methodName = match[1];
1117
+ const fn = this[methodName];
1118
+ if (typeof fn === 'function') {
1119
+ if (match[2] !== undefined) {
1120
+ // Parse arguments (supports strings, numbers, state refs)
1121
+ const args = match[2].split(',').map(a => {
1122
+ a = a.trim();
1123
+ if (a === '') return undefined;
1124
+ if (a === 'true') return true;
1125
+ if (a === 'false') return false;
1126
+ if (a === 'null') return null;
1127
+ if (/^-?\d+(\.\d+)?$/.test(a)) return Number(a);
1128
+ if ((a.startsWith("'") && a.endsWith("'")) || (a.startsWith('"') && a.endsWith('"'))) return a.slice(1, -1);
1129
+ // State reference
1130
+ if (a.startsWith('state.')) return this.state[a.slice(6)];
1131
+ return a;
1132
+ }).filter(a => a !== undefined);
1133
+ fn(e, ...args);
1134
+ } else {
1135
+ fn(e);
1136
+ }
1137
+ }
1138
+ }
1139
+ }
1140
+ };
1141
+ this._el.addEventListener(event, handler);
1142
+ this._listeners.push({ event, handler });
1143
+ }
1144
+ }
1145
+
1146
+ // Bind z-ref="name" → this.refs.name
1147
+ _bindRefs() {
1148
+ this.refs = {};
1149
+ this._el.querySelectorAll('[z-ref]').forEach(el => {
1150
+ this.refs[el.getAttribute('z-ref')] = el;
1151
+ });
1152
+ }
1153
+
1154
+ // Bind z-model="stateKey" for two-way binding
1155
+ //
1156
+ // Supported elements: input (text, number, range, checkbox, radio, date, color, …),
1157
+ // textarea, select (single & multiple), contenteditable
1158
+ // Nested state keys: z-model="user.name" → this.state.user.name
1159
+ // Modifiers (boolean attributes on the same element):
1160
+ // z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
1161
+ // z-trim — trim whitespace before writing to state
1162
+ // z-number — force Number() conversion regardless of input type
1163
+ //
1164
+ // Writes to reactive state so the rest of the UI stays in sync.
1165
+ // Focus and cursor position are preserved in _render() via focusInfo.
1166
+ //
1167
+ _bindModels() {
1168
+ this._el.querySelectorAll('[z-model]').forEach(el => {
1169
+ const key = el.getAttribute('z-model');
1170
+ const tag = el.tagName.toLowerCase();
1171
+ const type = (el.type || '').toLowerCase();
1172
+ const isEditable = el.hasAttribute('contenteditable');
1173
+
1174
+ // Modifiers
1175
+ const isLazy = el.hasAttribute('z-lazy');
1176
+ const isTrim = el.hasAttribute('z-trim');
1177
+ const isNum = el.hasAttribute('z-number');
1178
+
1179
+ // Read current state value (supports dot-path keys)
1180
+ const currentVal = _getPath(this.state, key);
1181
+
1182
+ // ── Set initial DOM value from state ────────────────────────
1183
+ if (tag === 'input' && type === 'checkbox') {
1184
+ el.checked = !!currentVal;
1185
+ } else if (tag === 'input' && type === 'radio') {
1186
+ el.checked = el.value === String(currentVal);
1187
+ } else if (tag === 'select' && el.multiple) {
1188
+ const vals = Array.isArray(currentVal) ? currentVal.map(String) : [];
1189
+ [...el.options].forEach(opt => { opt.selected = vals.includes(opt.value); });
1190
+ } else if (isEditable) {
1191
+ if (el.textContent !== String(currentVal ?? '')) {
1192
+ el.textContent = currentVal ?? '';
1193
+ }
1194
+ } else {
1195
+ el.value = currentVal ?? '';
1196
+ }
1197
+
1198
+ // ── Determine event type ────────────────────────────────────
1199
+ const event = isLazy || tag === 'select' || type === 'checkbox' || type === 'radio'
1200
+ ? 'change'
1201
+ : isEditable ? 'input' : 'input';
1202
+
1203
+ // ── Handler: read DOM → write to reactive state ─────────────
1204
+ const handler = () => {
1205
+ let val;
1206
+ if (type === 'checkbox') val = el.checked;
1207
+ else if (tag === 'select' && el.multiple) val = [...el.selectedOptions].map(o => o.value);
1208
+ else if (isEditable) val = el.textContent;
1209
+ else val = el.value;
1210
+
1211
+ // Apply modifiers
1212
+ if (isTrim && typeof val === 'string') val = val.trim();
1213
+ if (isNum || type === 'number' || type === 'range') val = Number(val);
1214
+
1215
+ // Write through the reactive proxy (triggers re-render).
1216
+ // Focus + cursor are preserved automatically by _render().
1217
+ _setPath(this.state, key, val);
1218
+ };
1219
+
1220
+ el.addEventListener(event, handler);
1221
+ });
1222
+ }
1223
+
1224
+ // Programmatic state update (batch-friendly)
1225
+ setState(partial) {
1226
+ Object.assign(this.state, partial);
1227
+ }
1228
+
1229
+ // Emit custom event up the DOM
1230
+ emit(name, detail) {
1231
+ this._el.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, cancelable: true }));
1232
+ }
1233
+
1234
+ // Destroy this component
1235
+ destroy() {
1236
+ if (this._destroyed) return;
1237
+ this._destroyed = true;
1238
+ if (this._def.destroyed) this._def.destroyed.call(this);
1239
+ this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
1240
+ this._listeners = [];
1241
+ if (this._styleEl) this._styleEl.remove();
1242
+ _instances.delete(this._el);
1243
+ this._el.innerHTML = '';
1244
+ }
1245
+ }
1246
+
1247
+
1248
+ // Reserved definition keys (not user methods)
1249
+ const _reservedKeys = new Set([
1250
+ 'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed', 'props',
1251
+ 'templateUrl', 'styleUrl', 'templates', 'pages', 'activePage', 'base'
1252
+ ]);
1253
+
1254
+
1255
+ // ---------------------------------------------------------------------------
1256
+ // Public API
1257
+ // ---------------------------------------------------------------------------
1258
+
1259
+ /**
1260
+ * Register a component
1261
+ * @param {string} name — tag name (must contain a hyphen, e.g. 'app-counter')
1262
+ * @param {object} definition — component definition
1263
+ */
1264
+ function component(name, definition) {
1265
+ if (!name.includes('-')) {
1266
+ throw new Error(`zQuery: Component name "${name}" must contain a hyphen (Web Component convention)`);
1267
+ }
1268
+ definition._name = name;
1269
+
1270
+ // Auto-detect the calling module's URL so that relative templateUrl,
1271
+ // styleUrl, and pages.dir paths resolve relative to the component file.
1272
+ // An explicit `base` string on the definition overrides auto-detection.
1273
+ if (definition.base !== undefined) {
1274
+ definition._base = definition.base; // explicit override
1275
+ } else {
1276
+ definition._base = _detectCallerBase();
1277
+ }
1278
+
1279
+ _registry.set(name, definition);
1280
+ }
1281
+
1282
+ /**
1283
+ * Mount a component into a target element
1284
+ * @param {string|Element} target — selector or element to mount into
1285
+ * @param {string} componentName — registered component name
1286
+ * @param {object} props — props to pass
1287
+ * @returns {Component}
1288
+ */
1289
+ function mount(target, componentName, props = {}) {
1290
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
1291
+ if (!el) throw new Error(`zQuery: Mount target "${target}" not found`);
1292
+
1293
+ const def = _registry.get(componentName);
1294
+ if (!def) throw new Error(`zQuery: Component "${componentName}" not registered`);
1295
+
1296
+ // Destroy existing instance
1297
+ if (_instances.has(el)) _instances.get(el).destroy();
1298
+
1299
+ const instance = new Component(el, def, props);
1300
+ _instances.set(el, instance);
1301
+ instance._render();
1302
+ return instance;
1303
+ }
1304
+
1305
+ /**
1306
+ * Scan a container for custom component tags and auto-mount them
1307
+ * @param {Element} root — root element to scan (default: document.body)
1308
+ */
1309
+ function mountAll(root = document.body) {
1310
+ for (const [name, def] of _registry) {
1311
+ const tags = root.querySelectorAll(name);
1312
+ tags.forEach(tag => {
1313
+ if (_instances.has(tag)) return; // Already mounted
1314
+
1315
+ // Extract props from attributes
1316
+ const props = {};
1317
+ [...tag.attributes].forEach(attr => {
1318
+ if (!attr.name.startsWith('@') && !attr.name.startsWith('z-')) {
1319
+ // Try JSON parse for objects/arrays
1320
+ try { props[attr.name] = JSON.parse(attr.value); }
1321
+ catch { props[attr.name] = attr.value; }
1322
+ }
1323
+ });
1324
+
1325
+ const instance = new Component(tag, def, props);
1326
+ _instances.set(tag, instance);
1327
+ instance._render();
1328
+ });
1329
+ }
1330
+ }
1331
+
1332
+ /**
1333
+ * Get the component instance for an element
1334
+ * @param {string|Element} target
1335
+ * @returns {Component|null}
1336
+ */
1337
+ function getInstance(target) {
1338
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
1339
+ return _instances.get(el) || null;
1340
+ }
1341
+
1342
+ /**
1343
+ * Destroy a component at the given target
1344
+ * @param {string|Element} target
1345
+ */
1346
+ function destroy(target) {
1347
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
1348
+ const inst = _instances.get(el);
1349
+ if (inst) inst.destroy();
1350
+ }
1351
+
1352
+ /**
1353
+ * Get the registry (for debugging)
1354
+ */
1355
+ function getRegistry() {
1356
+ return Object.fromEntries(_registry);
1357
+ }
1358
+
1359
+
1360
+ // ---------------------------------------------------------------------------
1361
+ // Global stylesheet loader
1362
+ // ---------------------------------------------------------------------------
1363
+ const _globalStyles = new Map(); // url → <link> element
1364
+
1365
+ /**
1366
+ * Load one or more global stylesheets into <head>.
1367
+ * Relative URLs resolve against the calling module's directory (auto-detected
1368
+ * from the stack trace), just like component styleUrl paths.
1369
+ * Returns a remove() handle so the caller can unload if needed.
1370
+ *
1371
+ * $.style('app.css') // critical by default
1372
+ * $.style(['app.css', 'theme.css']) // multiple files
1373
+ * $.style('/assets/global.css') // absolute — used as-is
1374
+ * $.style('app.css', { critical: false }) // opt out of FOUC prevention
1375
+ *
1376
+ * Options:
1377
+ * critical — (boolean, default true) When true, zQuery injects a tiny
1378
+ * inline style that hides the page (`visibility: hidden`) and
1379
+ * removes it once the stylesheet has loaded. This prevents
1380
+ * FOUC (Flash of Unstyled Content) entirely — no special
1381
+ * markup needed in the HTML file. Set to false to load
1382
+ * the stylesheet without blocking paint.
1383
+ * bg — (string, default '#0d1117') Background color applied while
1384
+ * the page is hidden during critical load. Prevents a white
1385
+ * flash on dark-themed apps. Only used when critical is true.
1386
+ *
1387
+ * Duplicate URLs are ignored (idempotent).
1388
+ *
1389
+ * @param {string|string[]} urls — stylesheet URL(s) to load
1390
+ * @param {object} [opts] — options
1391
+ * @param {boolean} [opts.critical=true] — hide page until loaded (prevents FOUC)
1392
+ * @param {string} [opts.bg] — background color while hidden (default '#0d1117')
1393
+ * @returns {{ remove: Function, ready: Promise }} — .remove() to unload, .ready resolves when loaded
1394
+ */
1395
+ function style(urls, opts = {}) {
1396
+ const callerBase = _detectCallerBase();
1397
+ const list = Array.isArray(urls) ? urls : [urls];
1398
+ const elements = [];
1399
+ const loadPromises = [];
1400
+
1401
+ // Critical mode (default: true): inject a tiny inline <style> that hides the
1402
+ // page and sets a background color. Fully self-contained — no markup needed
1403
+ // in the HTML file. The style is removed once the sheet loads.
1404
+ let _criticalStyle = null;
1405
+ if (opts.critical !== false) {
1406
+ _criticalStyle = document.createElement('style');
1407
+ _criticalStyle.setAttribute('data-zq-critical', '');
1408
+ _criticalStyle.textContent = `html{visibility:hidden!important;background:${opts.bg || '#0d1117'}}`;
1409
+ document.head.insertBefore(_criticalStyle, document.head.firstChild);
1410
+ }
1411
+
1412
+ for (let url of list) {
1413
+ // Resolve relative paths against the caller's directory first,
1414
+ // falling back to <base href> or origin root.
1415
+ if (typeof url === 'string' && !url.startsWith('/') && !url.includes(':') && !url.startsWith('//')) {
1416
+ url = _resolveUrl(url, callerBase);
1417
+ }
1418
+
1419
+ if (_globalStyles.has(url)) {
1420
+ elements.push(_globalStyles.get(url));
1421
+ continue;
1422
+ }
1423
+
1424
+ const link = document.createElement('link');
1425
+ link.rel = 'stylesheet';
1426
+ link.href = url;
1427
+ link.setAttribute('data-zq-style', '');
1428
+
1429
+ const p = new Promise(resolve => {
1430
+ link.onload = resolve;
1431
+ link.onerror = resolve; // don't block forever on error
1432
+ });
1433
+ loadPromises.push(p);
1434
+
1435
+ document.head.appendChild(link);
1436
+ _globalStyles.set(url, link);
1437
+ elements.push(link);
1438
+ }
1439
+
1440
+ // When all sheets are loaded, reveal the page if critical mode was used
1441
+ const ready = Promise.all(loadPromises).then(() => {
1442
+ if (_criticalStyle) {
1443
+ _criticalStyle.remove();
1444
+ }
1445
+ });
1446
+
1447
+ return {
1448
+ ready,
1449
+ remove() {
1450
+ for (const el of elements) {
1451
+ el.remove();
1452
+ for (const [k, v] of _globalStyles) {
1453
+ if (v === el) { _globalStyles.delete(k); break; }
1454
+ }
1455
+ }
1456
+ }
1457
+ };
1458
+ }
1459
+
1460
+ // --- src/router.js ———————————————————————————————————————————————
1461
+ /**
1462
+ * zQuery Router — Client-side SPA router
1463
+ *
1464
+ * Supports hash mode (#/path) and history mode (/path).
1465
+ * Route params, query strings, navigation guards, and lazy loading.
1466
+ *
1467
+ * Usage:
1468
+ * $.router({
1469
+ * el: '#app',
1470
+ * mode: 'hash',
1471
+ * routes: [
1472
+ * { path: '/', component: 'home-page' },
1473
+ * { path: '/user/:id', component: 'user-profile' },
1474
+ * { path: '/lazy', load: () => import('./pages/lazy.js'), component: 'lazy-page' },
1475
+ * ],
1476
+ * fallback: 'not-found'
1477
+ * });
1478
+ */
1479
+
1480
+
1481
+ class Router {
1482
+ constructor(config = {}) {
1483
+ this._el = null;
1484
+ // Auto-detect: file:// protocol can't use pushState, fall back to hash
1485
+ const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
1486
+ this._mode = config.mode || (isFile ? 'hash' : 'history');
1487
+
1488
+ // Base path for sub-path deployments
1489
+ // Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
1490
+ let rawBase = config.base;
1491
+ if (rawBase == null) {
1492
+ rawBase = (typeof window !== 'undefined' && window.__ZQ_BASE) || '';
1493
+ if (!rawBase && typeof document !== 'undefined') {
1494
+ const baseEl = document.querySelector('base');
1495
+ if (baseEl) {
1496
+ try { rawBase = new URL(baseEl.href).pathname; }
1497
+ catch { rawBase = baseEl.getAttribute('href') || ''; }
1498
+ if (rawBase === '/') rawBase = ''; // root = no sub-path
1499
+ }
1500
+ }
1501
+ }
1502
+ // Normalize: ensure leading /, strip trailing /
1503
+ this._base = String(rawBase).replace(/\/+$/, '');
1504
+ if (this._base && !this._base.startsWith('/')) this._base = '/' + this._base;
1505
+
1506
+ this._routes = [];
1507
+ this._fallback = config.fallback || null;
1508
+ this._current = null; // { route, params, query, path }
1509
+ this._guards = { before: [], after: [] };
1510
+ this._listeners = new Set();
1511
+ this._instance = null; // current mounted component
1512
+ this._resolving = false; // re-entrancy guard
1513
+
1514
+ // Set outlet element
1515
+ if (config.el) {
1516
+ this._el = typeof config.el === 'string' ? document.querySelector(config.el) : config.el;
1517
+ }
1518
+
1519
+ // Register routes
1520
+ if (config.routes) {
1521
+ config.routes.forEach(r => this.add(r));
1522
+ }
1523
+
1524
+ // Listen for navigation
1525
+ if (this._mode === 'hash') {
1526
+ window.addEventListener('hashchange', () => this._resolve());
1527
+ } else {
1528
+ window.addEventListener('popstate', () => this._resolve());
1529
+ }
1530
+
1531
+ // Intercept link clicks for SPA navigation
1532
+ document.addEventListener('click', (e) => {
1533
+ // Don't intercept modified clicks (Ctrl/Cmd+click = new tab)
1534
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
1535
+ const link = e.target.closest('[z-link]');
1536
+ if (!link) return;
1537
+ if (link.getAttribute('target') === '_blank') return;
1538
+ e.preventDefault();
1539
+ this.navigate(link.getAttribute('z-link'));
1540
+ });
1541
+
1542
+ // Initial resolve
1543
+ if (this._el) {
1544
+ // Defer to allow all components to register
1545
+ queueMicrotask(() => this._resolve());
1546
+ }
1547
+ }
1548
+
1549
+ // --- Route management ----------------------------------------------------
1550
+
1551
+ add(route) {
1552
+ // Compile path pattern into regex
1553
+ const keys = [];
1554
+ const pattern = route.path
1555
+ .replace(/:(\w+)/g, (_, key) => { keys.push(key); return '([^/]+)'; })
1556
+ .replace(/\*/g, '(.*)');
1557
+ const regex = new RegExp(`^${pattern}$`);
1558
+
1559
+ this._routes.push({ ...route, _regex: regex, _keys: keys });
1560
+
1561
+ // Per-route fallback: register an alias path for the same component.
1562
+ // e.g. { path: '/docs/:section', fallback: '/docs', component: 'docs-page' }
1563
+ // When matched via fallback, missing params are undefined → pages `default` kicks in.
1564
+ if (route.fallback) {
1565
+ const fbKeys = [];
1566
+ const fbPattern = route.fallback
1567
+ .replace(/:(\w+)/g, (_, key) => { fbKeys.push(key); return '([^/]+)'; })
1568
+ .replace(/\*/g, '(.*)');
1569
+ const fbRegex = new RegExp(`^${fbPattern}$`);
1570
+ this._routes.push({ ...route, path: route.fallback, _regex: fbRegex, _keys: fbKeys });
1571
+ }
1572
+
1573
+ return this;
1574
+ }
1575
+
1576
+ remove(path) {
1577
+ this._routes = this._routes.filter(r => r.path !== path);
1578
+ return this;
1579
+ }
1580
+
1581
+ // --- Navigation ----------------------------------------------------------
1582
+
1583
+ navigate(path, options = {}) {
1584
+ // Separate hash fragment (e.g. /docs/getting-started#cli-bundler)
1585
+ const [cleanPath, fragment] = (path || '').split('#');
1586
+ let normalized = this._normalizePath(cleanPath);
1587
+ const hash = fragment ? '#' + fragment : '';
1588
+ if (this._mode === 'hash') {
1589
+ // Hash mode uses the URL hash for routing, so a #fragment can't live
1590
+ // in the URL. Store it as a scroll target for the destination component.
1591
+ if (fragment) window.__zqScrollTarget = fragment;
1592
+ window.location.hash = '#' + normalized;
1593
+ } else {
1594
+ window.history.pushState(options.state || {}, '', this._base + normalized + hash);
1595
+ this._resolve();
1596
+ }
1597
+ return this;
1598
+ }
1599
+
1600
+ replace(path, options = {}) {
1601
+ const [cleanPath, fragment] = (path || '').split('#');
1602
+ let normalized = this._normalizePath(cleanPath);
1603
+ const hash = fragment ? '#' + fragment : '';
1604
+ if (this._mode === 'hash') {
1605
+ if (fragment) window.__zqScrollTarget = fragment;
1606
+ window.location.replace('#' + normalized);
1607
+ } else {
1608
+ window.history.replaceState(options.state || {}, '', this._base + normalized + hash);
1609
+ this._resolve();
1610
+ }
1611
+ return this;
1612
+ }
1613
+
1614
+ /**
1615
+ * Normalize an app-relative path and guard against double base-prefixing.
1616
+ * @param {string} path — e.g. '/docs', 'docs', or '/app/docs' when base is '/app'
1617
+ * @returns {string} — always starts with '/'
1618
+ */
1619
+ _normalizePath(path) {
1620
+ let p = path && path.startsWith('/') ? path : (path ? `/${path}` : '/');
1621
+ // Strip base prefix if caller accidentally included it
1622
+ if (this._base) {
1623
+ if (p === this._base) return '/';
1624
+ if (p.startsWith(this._base + '/')) p = p.slice(this._base.length) || '/';
1625
+ }
1626
+ return p;
1627
+ }
1628
+
1629
+ /**
1630
+ * Resolve an app-relative path to a full URL path (including base).
1631
+ * Useful for programmatic link generation.
1632
+ * @param {string} path
1633
+ * @returns {string}
1634
+ */
1635
+ resolve(path) {
1636
+ const normalized = path && path.startsWith('/') ? path : (path ? `/${path}` : '/');
1637
+ return this._base + normalized;
1638
+ }
1639
+
1640
+ back() { window.history.back(); return this; }
1641
+ forward() { window.history.forward(); return this; }
1642
+ go(n) { window.history.go(n); return this; }
1643
+
1644
+ // --- Guards --------------------------------------------------------------
1645
+
1646
+ beforeEach(fn) {
1647
+ this._guards.before.push(fn);
1648
+ return this;
1649
+ }
1650
+
1651
+ afterEach(fn) {
1652
+ this._guards.after.push(fn);
1653
+ return this;
1654
+ }
1655
+
1656
+ // --- Events --------------------------------------------------------------
1657
+
1658
+ onChange(fn) {
1659
+ this._listeners.add(fn);
1660
+ return () => this._listeners.delete(fn);
1661
+ }
1662
+
1663
+ // --- Current state -------------------------------------------------------
1664
+
1665
+ get current() { return this._current; }
1666
+
1667
+ /** The detected or configured base path (read-only) */
1668
+ get base() { return this._base; }
1669
+
1670
+ get path() {
1671
+ if (this._mode === 'hash') {
1672
+ const raw = window.location.hash.slice(1) || '/';
1673
+ // If the hash doesn't start with '/', it's an in-page anchor
1674
+ // (e.g. #some-heading), not a route. Treat it as a scroll target
1675
+ // and resolve to the last known route (or '/').
1676
+ if (raw && !raw.startsWith('/')) {
1677
+ window.__zqScrollTarget = raw;
1678
+ // Restore the route hash silently so the URL stays valid
1679
+ const fallbackPath = (this._current && this._current.path) || '/';
1680
+ window.location.replace('#' + fallbackPath);
1681
+ return fallbackPath;
1682
+ }
1683
+ return raw;
1684
+ }
1685
+ let pathname = window.location.pathname || '/';
1686
+ // Strip trailing slash for consistency (except root '/')
1687
+ if (pathname.length > 1 && pathname.endsWith('/')) {
1688
+ pathname = pathname.slice(0, -1);
1689
+ }
1690
+ if (this._base) {
1691
+ // Exact match: /app
1692
+ if (pathname === this._base) return '/';
1693
+ // Prefix match with boundary: /app/page (but NOT /application)
1694
+ if (pathname.startsWith(this._base + '/')) {
1695
+ return pathname.slice(this._base.length) || '/';
1696
+ }
1697
+ }
1698
+ return pathname;
1699
+ }
1700
+
1701
+ get query() {
1702
+ const search = this._mode === 'hash'
1703
+ ? (window.location.hash.split('?')[1] || '')
1704
+ : window.location.search.slice(1);
1705
+ return Object.fromEntries(new URLSearchParams(search));
1706
+ }
1707
+
1708
+ // --- Internal resolve ----------------------------------------------------
1709
+
1710
+ async _resolve() {
1711
+ // Prevent re-entrant calls (e.g. listener triggering navigation)
1712
+ if (this._resolving) return;
1713
+ this._resolving = true;
1714
+ try {
1715
+ await this.__resolve();
1716
+ } finally {
1717
+ this._resolving = false;
1718
+ }
1719
+ }
1720
+
1721
+ async __resolve() {
1722
+ const fullPath = this.path;
1723
+ const [pathPart, queryString] = fullPath.split('?');
1724
+ const path = pathPart || '/';
1725
+ const query = Object.fromEntries(new URLSearchParams(queryString || ''));
1726
+
1727
+ // Match route
1728
+ let matched = null;
1729
+ let params = {};
1730
+ for (const route of this._routes) {
1731
+ const m = path.match(route._regex);
1732
+ if (m) {
1733
+ matched = route;
1734
+ route._keys.forEach((key, i) => { params[key] = m[i + 1]; });
1735
+ break;
1736
+ }
1737
+ }
1738
+
1739
+ // Fallback
1740
+ if (!matched && this._fallback) {
1741
+ matched = { component: this._fallback, path: '*', _keys: [], _regex: /.*/ };
1742
+ }
1743
+
1744
+ if (!matched) return;
1745
+
1746
+ const to = { route: matched, params, query, path };
1747
+ const from = this._current;
1748
+
1749
+ // Run before guards
1750
+ for (const guard of this._guards.before) {
1751
+ const result = await guard(to, from);
1752
+ if (result === false) return; // Cancel
1753
+ if (typeof result === 'string') { // Redirect
1754
+ return this.navigate(result);
1755
+ }
1756
+ }
1757
+
1758
+ // Lazy load module if needed
1759
+ if (matched.load) {
1760
+ try { await matched.load(); }
1761
+ catch (err) {
1762
+ console.error(`zQuery Router: Failed to load module for "${matched.path}"`, err);
1763
+ return;
1764
+ }
1765
+ }
1766
+
1767
+ this._current = to;
1768
+
1769
+ // Mount component into outlet
1770
+ if (this._el && matched.component) {
1771
+ // Destroy previous
1772
+ if (this._instance) {
1773
+ this._instance.destroy();
1774
+ this._instance = null;
1775
+ }
1776
+
1777
+ // Create container
1778
+ this._el.innerHTML = '';
1779
+
1780
+ // Pass route params and query as props
1781
+ const props = { ...params, $route: to, $query: query, $params: params };
1782
+
1783
+ // If component is a string (registered name), mount it
1784
+ if (typeof matched.component === 'string') {
1785
+ const container = document.createElement(matched.component);
1786
+ this._el.appendChild(container);
1787
+ this._instance = mount(container, matched.component, props);
1788
+ }
1789
+ // If component is a render function
1790
+ else if (typeof matched.component === 'function') {
1791
+ this._el.innerHTML = matched.component(to);
1792
+ }
1793
+ }
1794
+
1795
+ // Run after guards
1796
+ for (const guard of this._guards.after) {
1797
+ await guard(to, from);
1798
+ }
1799
+
1800
+ // Notify listeners
1801
+ this._listeners.forEach(fn => fn(to, from));
1802
+ }
1803
+
1804
+ // --- Destroy -------------------------------------------------------------
1805
+
1806
+ destroy() {
1807
+ if (this._instance) this._instance.destroy();
1808
+ this._listeners.clear();
1809
+ this._routes = [];
1810
+ this._guards = { before: [], after: [] };
1811
+ }
1812
+ }
1813
+
1814
+
1815
+ // ---------------------------------------------------------------------------
1816
+ // Factory
1817
+ // ---------------------------------------------------------------------------
1818
+ let _activeRouter = null;
1819
+
1820
+ function createRouter(config) {
1821
+ _activeRouter = new Router(config);
1822
+ return _activeRouter;
1823
+ }
1824
+
1825
+ function getRouter() {
1826
+ return _activeRouter;
1827
+ }
1828
+
1829
+ // --- src/store.js ————————————————————————————————————————————————
1830
+ /**
1831
+ * zQuery Store — Global reactive state management
1832
+ *
1833
+ * A lightweight Redux/Vuex-inspired store with:
1834
+ * - Reactive state via Proxy
1835
+ * - Named actions for mutations
1836
+ * - Key-specific subscriptions
1837
+ * - Computed getters
1838
+ * - Middleware support
1839
+ * - DevTools-friendly (logs actions in dev mode)
1840
+ *
1841
+ * Usage:
1842
+ * const store = $.store({
1843
+ * state: { count: 0, user: null },
1844
+ * actions: {
1845
+ * increment(state) { state.count++; },
1846
+ * setUser(state, user) { state.user = user; }
1847
+ * },
1848
+ * getters: {
1849
+ * doubleCount: (state) => state.count * 2
1850
+ * }
1851
+ * });
1852
+ *
1853
+ * store.dispatch('increment');
1854
+ * store.subscribe('count', (val, old) => console.log(val));
1855
+ */
1856
+
1857
+
1858
+ class Store {
1859
+ constructor(config = {}) {
1860
+ this._subscribers = new Map(); // key → Set<fn>
1861
+ this._wildcards = new Set(); // subscribe to all changes
1862
+ this._actions = config.actions || {};
1863
+ this._getters = config.getters || {};
1864
+ this._middleware = [];
1865
+ this._history = []; // action log
1866
+ this._debug = config.debug || false;
1867
+
1868
+ // Create reactive state
1869
+ const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) };
1870
+
1871
+ this.state = reactive(initial, (key, value, old) => {
1872
+ // Notify key-specific subscribers
1873
+ const subs = this._subscribers.get(key);
1874
+ if (subs) subs.forEach(fn => fn(value, old, key));
1875
+ // Notify wildcard subscribers
1876
+ this._wildcards.forEach(fn => fn(key, value, old));
1877
+ });
1878
+
1879
+ // Build getters as computed properties
1880
+ this.getters = {};
1881
+ for (const [name, fn] of Object.entries(this._getters)) {
1882
+ Object.defineProperty(this.getters, name, {
1883
+ get: () => fn(this.state.__raw || this.state),
1884
+ enumerable: true
1885
+ });
1886
+ }
1887
+ }
1888
+
1889
+ /**
1890
+ * Dispatch a named action
1891
+ * @param {string} name — action name
1892
+ * @param {...any} args — payload
1893
+ */
1894
+ dispatch(name, ...args) {
1895
+ const action = this._actions[name];
1896
+ if (!action) {
1897
+ console.warn(`zQuery Store: Unknown action "${name}"`);
1898
+ return;
1899
+ }
1900
+
1901
+ // Run middleware
1902
+ for (const mw of this._middleware) {
1903
+ const result = mw(name, args, this.state);
1904
+ if (result === false) return; // blocked by middleware
1905
+ }
1906
+
1907
+ if (this._debug) {
1908
+ console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args);
1909
+ }
1910
+
1911
+ const result = action(this.state, ...args);
1912
+ this._history.push({ action: name, args, timestamp: Date.now() });
1913
+ return result;
1914
+ }
1915
+
1916
+ /**
1917
+ * Subscribe to changes on a specific state key
1918
+ * @param {string|Function} keyOrFn — state key, or function for all changes
1919
+ * @param {Function} [fn] — callback (value, oldValue, key)
1920
+ * @returns {Function} — unsubscribe
1921
+ */
1922
+ subscribe(keyOrFn, fn) {
1923
+ if (typeof keyOrFn === 'function') {
1924
+ // Wildcard — listen to all changes
1925
+ this._wildcards.add(keyOrFn);
1926
+ return () => this._wildcards.delete(keyOrFn);
1927
+ }
1928
+
1929
+ if (!this._subscribers.has(keyOrFn)) {
1930
+ this._subscribers.set(keyOrFn, new Set());
1931
+ }
1932
+ this._subscribers.get(keyOrFn).add(fn);
1933
+ return () => this._subscribers.get(keyOrFn)?.delete(fn);
1934
+ }
1935
+
1936
+ /**
1937
+ * Get current state snapshot (plain object)
1938
+ */
1939
+ snapshot() {
1940
+ return JSON.parse(JSON.stringify(this.state.__raw || this.state));
1941
+ }
1942
+
1943
+ /**
1944
+ * Replace entire state
1945
+ */
1946
+ replaceState(newState) {
1947
+ const raw = this.state.__raw || this.state;
1948
+ for (const key of Object.keys(raw)) {
1949
+ delete this.state[key];
1950
+ }
1951
+ Object.assign(this.state, newState);
1952
+ }
1953
+
1954
+ /**
1955
+ * Add middleware: fn(actionName, args, state) → false to block
1956
+ */
1957
+ use(fn) {
1958
+ this._middleware.push(fn);
1959
+ return this;
1960
+ }
1961
+
1962
+ /**
1963
+ * Get action history
1964
+ */
1965
+ get history() {
1966
+ return [...this._history];
1967
+ }
1968
+
1969
+ /**
1970
+ * Reset state to initial values
1971
+ */
1972
+ reset(initialState) {
1973
+ this.replaceState(initialState);
1974
+ this._history = [];
1975
+ }
1976
+ }
1977
+
1978
+
1979
+ // ---------------------------------------------------------------------------
1980
+ // Factory
1981
+ // ---------------------------------------------------------------------------
1982
+ let _stores = new Map();
1983
+
1984
+ function createStore(name, config) {
1985
+ // If called with just config (no name), use 'default'
1986
+ if (typeof name === 'object') {
1987
+ config = name;
1988
+ name = 'default';
1989
+ }
1990
+ const store = new Store(config);
1991
+ _stores.set(name, store);
1992
+ return store;
1993
+ }
1994
+
1995
+ function getStore(name = 'default') {
1996
+ return _stores.get(name) || null;
1997
+ }
1998
+
1999
+ // --- src/http.js —————————————————————————————————————————————————
2000
+ /**
2001
+ * zQuery HTTP — Lightweight fetch wrapper
2002
+ *
2003
+ * Clean API for GET/POST/PUT/PATCH/DELETE with:
2004
+ * - Auto JSON serialization/deserialization
2005
+ * - Request/response interceptors
2006
+ * - Timeout support
2007
+ * - Base URL configuration
2008
+ * - Abort controller integration
2009
+ *
2010
+ * Usage:
2011
+ * $.http.get('/api/users');
2012
+ * $.http.post('/api/users', { name: 'Tony' });
2013
+ * $.http.configure({ baseURL: 'https://api.example.com', headers: { Authorization: 'Bearer ...' } });
2014
+ */
2015
+
2016
+ const _config = {
2017
+ baseURL: '',
2018
+ headers: { 'Content-Type': 'application/json' },
2019
+ timeout: 30000,
2020
+ };
2021
+
2022
+ const _interceptors = {
2023
+ request: [],
2024
+ response: [],
2025
+ };
2026
+
2027
+
2028
+ /**
2029
+ * Core request function
2030
+ */
2031
+ async function request(method, url, data, options = {}) {
2032
+ let fullURL = url.startsWith('http') ? url : _config.baseURL + url;
2033
+ let headers = { ..._config.headers, ...options.headers };
2034
+ let body = undefined;
2035
+
2036
+ // Build fetch options
2037
+ const fetchOpts = {
2038
+ method: method.toUpperCase(),
2039
+ headers,
2040
+ ...options,
2041
+ };
2042
+
2043
+ // Handle body
2044
+ if (data !== undefined && method !== 'GET' && method !== 'HEAD') {
2045
+ if (data instanceof FormData) {
2046
+ body = data;
2047
+ delete fetchOpts.headers['Content-Type']; // Let browser set multipart boundary
2048
+ } else if (typeof data === 'object') {
2049
+ body = JSON.stringify(data);
2050
+ } else {
2051
+ body = data;
2052
+ }
2053
+ fetchOpts.body = body;
2054
+ }
2055
+
2056
+ // Query params for GET
2057
+ if (data && (method === 'GET' || method === 'HEAD') && typeof data === 'object') {
2058
+ const params = new URLSearchParams(data).toString();
2059
+ fullURL += (fullURL.includes('?') ? '&' : '?') + params;
2060
+ }
2061
+
2062
+ // Timeout via AbortController
2063
+ const controller = new AbortController();
2064
+ fetchOpts.signal = options.signal || controller.signal;
2065
+ const timeout = options.timeout ?? _config.timeout;
2066
+ let timer;
2067
+ if (timeout > 0) {
2068
+ timer = setTimeout(() => controller.abort(), timeout);
2069
+ }
2070
+
2071
+ // Run request interceptors
2072
+ for (const interceptor of _interceptors.request) {
2073
+ const result = await interceptor(fetchOpts, fullURL);
2074
+ if (result === false) throw new Error('Request blocked by interceptor');
2075
+ if (result?.url) fullURL = result.url;
2076
+ if (result?.options) Object.assign(fetchOpts, result.options);
2077
+ }
2078
+
2079
+ try {
2080
+ const response = await fetch(fullURL, fetchOpts);
2081
+ if (timer) clearTimeout(timer);
2082
+
2083
+ // Parse response
2084
+ const contentType = response.headers.get('Content-Type') || '';
2085
+ let responseData;
2086
+
2087
+ if (contentType.includes('application/json')) {
2088
+ responseData = await response.json();
2089
+ } else if (contentType.includes('text/')) {
2090
+ responseData = await response.text();
2091
+ } else if (contentType.includes('application/octet-stream') || contentType.includes('image/')) {
2092
+ responseData = await response.blob();
2093
+ } else {
2094
+ // Try JSON first, fall back to text
2095
+ const text = await response.text();
2096
+ try { responseData = JSON.parse(text); } catch { responseData = text; }
2097
+ }
2098
+
2099
+ const result = {
2100
+ ok: response.ok,
2101
+ status: response.status,
2102
+ statusText: response.statusText,
2103
+ headers: Object.fromEntries(response.headers.entries()),
2104
+ data: responseData,
2105
+ response,
2106
+ };
2107
+
2108
+ // Run response interceptors
2109
+ for (const interceptor of _interceptors.response) {
2110
+ await interceptor(result);
2111
+ }
2112
+
2113
+ if (!response.ok) {
2114
+ const err = new Error(`HTTP ${response.status}: ${response.statusText}`);
2115
+ err.response = result;
2116
+ throw err;
2117
+ }
2118
+
2119
+ return result;
2120
+ } catch (err) {
2121
+ if (timer) clearTimeout(timer);
2122
+ if (err.name === 'AbortError') {
2123
+ throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
2124
+ }
2125
+ throw err;
2126
+ }
2127
+ }
2128
+
2129
+
2130
+ // ---------------------------------------------------------------------------
2131
+ // Public API
2132
+ // ---------------------------------------------------------------------------
2133
+ const http = {
2134
+ get: (url, params, opts) => request('GET', url, params, opts),
2135
+ post: (url, data, opts) => request('POST', url, data, opts),
2136
+ put: (url, data, opts) => request('PUT', url, data, opts),
2137
+ patch: (url, data, opts) => request('PATCH', url, data, opts),
2138
+ delete: (url, data, opts) => request('DELETE', url, data, opts),
2139
+
2140
+ /**
2141
+ * Configure defaults
2142
+ */
2143
+ configure(opts) {
2144
+ if (opts.baseURL !== undefined) _config.baseURL = opts.baseURL;
2145
+ if (opts.headers) Object.assign(_config.headers, opts.headers);
2146
+ if (opts.timeout !== undefined) _config.timeout = opts.timeout;
2147
+ },
2148
+
2149
+ /**
2150
+ * Add request interceptor
2151
+ * @param {Function} fn — (fetchOpts, url) → void | false | { url, options }
2152
+ */
2153
+ onRequest(fn) {
2154
+ _interceptors.request.push(fn);
2155
+ },
2156
+
2157
+ /**
2158
+ * Add response interceptor
2159
+ * @param {Function} fn — (result) → void
2160
+ */
2161
+ onResponse(fn) {
2162
+ _interceptors.response.push(fn);
2163
+ },
2164
+
2165
+ /**
2166
+ * Create a standalone AbortController for manual cancellation
2167
+ */
2168
+ createAbort() {
2169
+ return new AbortController();
2170
+ },
2171
+
2172
+ /**
2173
+ * Raw fetch pass-through (for edge cases)
2174
+ */
2175
+ raw: (url, opts) => fetch(url, opts),
2176
+ };
2177
+
2178
+ // --- src/utils.js ————————————————————————————————————————————————
2179
+ /**
2180
+ * zQuery Utils — Common utility functions
2181
+ *
2182
+ * Quality-of-life helpers that every frontend project needs.
2183
+ * Attached to $ namespace for convenience.
2184
+ */
2185
+
2186
+ // ---------------------------------------------------------------------------
2187
+ // Function utilities
2188
+ // ---------------------------------------------------------------------------
2189
+
2190
+ /**
2191
+ * Debounce — delays execution until after `ms` of inactivity
2192
+ */
2193
+ function debounce(fn, ms = 250) {
2194
+ let timer;
2195
+ const debounced = (...args) => {
2196
+ clearTimeout(timer);
2197
+ timer = setTimeout(() => fn(...args), ms);
2198
+ };
2199
+ debounced.cancel = () => clearTimeout(timer);
2200
+ return debounced;
2201
+ }
2202
+
2203
+ /**
2204
+ * Throttle — limits execution to once per `ms`
2205
+ */
2206
+ function throttle(fn, ms = 250) {
2207
+ let last = 0;
2208
+ let timer;
2209
+ return (...args) => {
2210
+ const now = Date.now();
2211
+ const remaining = ms - (now - last);
2212
+ clearTimeout(timer);
2213
+ if (remaining <= 0) {
2214
+ last = now;
2215
+ fn(...args);
2216
+ } else {
2217
+ timer = setTimeout(() => { last = Date.now(); fn(...args); }, remaining);
2218
+ }
2219
+ };
2220
+ }
2221
+
2222
+ /**
2223
+ * Pipe — compose functions left-to-right
2224
+ */
2225
+ function pipe(...fns) {
2226
+ return (input) => fns.reduce((val, fn) => fn(val), input);
2227
+ }
2228
+
2229
+ /**
2230
+ * Once — function that only runs once
2231
+ */
2232
+ function once(fn) {
2233
+ let called = false, result;
2234
+ return (...args) => {
2235
+ if (!called) { called = true; result = fn(...args); }
2236
+ return result;
2237
+ };
2238
+ }
2239
+
2240
+ /**
2241
+ * Sleep — promise-based delay
2242
+ */
2243
+ function sleep(ms) {
2244
+ return new Promise(resolve => setTimeout(resolve, ms));
2245
+ }
2246
+
2247
+
2248
+ // ---------------------------------------------------------------------------
2249
+ // String utilities
2250
+ // ---------------------------------------------------------------------------
2251
+
2252
+ /**
2253
+ * Escape HTML entities
2254
+ */
2255
+ function escapeHtml(str) {
2256
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
2257
+ return String(str).replace(/[&<>"']/g, c => map[c]);
2258
+ }
2259
+
2260
+ /**
2261
+ * Template tag for auto-escaping interpolated values
2262
+ * Usage: $.html`<div>${userInput}</div>`
2263
+ */
2264
+ function html(strings, ...values) {
2265
+ return strings.reduce((result, str, i) => {
2266
+ const val = values[i - 1];
2267
+ const escaped = (val instanceof TrustedHTML) ? val.toString() : escapeHtml(val ?? '');
2268
+ return result + escaped + str;
2269
+ });
2270
+ }
2271
+
2272
+ /**
2273
+ * Mark HTML as trusted (skip escaping in $.html template)
2274
+ */
2275
+ class TrustedHTML {
2276
+ constructor(html) { this._html = html; }
2277
+ toString() { return this._html; }
2278
+ }
2279
+
2280
+ function trust(htmlStr) {
2281
+ return new TrustedHTML(htmlStr);
2282
+ }
2283
+
2284
+ /**
2285
+ * Generate UUID v4
2286
+ */
2287
+ function uuid() {
2288
+ return crypto?.randomUUID?.() || 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
2289
+ const r = Math.random() * 16 | 0;
2290
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
2291
+ });
2292
+ }
2293
+
2294
+ /**
2295
+ * Kebab-case to camelCase
2296
+ */
2297
+ function camelCase(str) {
2298
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2299
+ }
2300
+
2301
+ /**
2302
+ * CamelCase to kebab-case
2303
+ */
2304
+ function kebabCase(str) {
2305
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
2306
+ }
2307
+
2308
+
2309
+ // ---------------------------------------------------------------------------
2310
+ // Object utilities
2311
+ // ---------------------------------------------------------------------------
2312
+
2313
+ /**
2314
+ * Deep clone
2315
+ */
2316
+ function deepClone(obj) {
2317
+ if (typeof structuredClone === 'function') return structuredClone(obj);
2318
+ return JSON.parse(JSON.stringify(obj));
2319
+ }
2320
+
2321
+ /**
2322
+ * Deep merge objects
2323
+ */
2324
+ function deepMerge(target, ...sources) {
2325
+ for (const source of sources) {
2326
+ for (const key of Object.keys(source)) {
2327
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
2328
+ if (!target[key] || typeof target[key] !== 'object') target[key] = {};
2329
+ deepMerge(target[key], source[key]);
2330
+ } else {
2331
+ target[key] = source[key];
2332
+ }
2333
+ }
2334
+ }
2335
+ return target;
2336
+ }
2337
+
2338
+ /**
2339
+ * Simple object equality check
2340
+ */
2341
+ function isEqual(a, b) {
2342
+ if (a === b) return true;
2343
+ if (typeof a !== typeof b) return false;
2344
+ if (typeof a !== 'object' || a === null || b === null) return false;
2345
+ const keysA = Object.keys(a);
2346
+ const keysB = Object.keys(b);
2347
+ if (keysA.length !== keysB.length) return false;
2348
+ return keysA.every(k => isEqual(a[k], b[k]));
2349
+ }
2350
+
2351
+
2352
+ // ---------------------------------------------------------------------------
2353
+ // URL utilities
2354
+ // ---------------------------------------------------------------------------
2355
+
2356
+ /**
2357
+ * Serialize object to URL query string
2358
+ */
2359
+ function param(obj) {
2360
+ return new URLSearchParams(obj).toString();
2361
+ }
2362
+
2363
+ /**
2364
+ * Parse URL query string to object
2365
+ */
2366
+ function parseQuery(str) {
2367
+ return Object.fromEntries(new URLSearchParams(str));
2368
+ }
2369
+
2370
+
2371
+ // ---------------------------------------------------------------------------
2372
+ // Storage helpers (localStorage wrapper with JSON support)
2373
+ // ---------------------------------------------------------------------------
2374
+ const storage = {
2375
+ get(key, fallback = null) {
2376
+ try {
2377
+ const raw = localStorage.getItem(key);
2378
+ return raw !== null ? JSON.parse(raw) : fallback;
2379
+ } catch {
2380
+ return fallback;
2381
+ }
2382
+ },
2383
+
2384
+ set(key, value) {
2385
+ localStorage.setItem(key, JSON.stringify(value));
2386
+ },
2387
+
2388
+ remove(key) {
2389
+ localStorage.removeItem(key);
2390
+ },
2391
+
2392
+ clear() {
2393
+ localStorage.clear();
2394
+ },
2395
+ };
2396
+
2397
+ const session = {
2398
+ get(key, fallback = null) {
2399
+ try {
2400
+ const raw = sessionStorage.getItem(key);
2401
+ return raw !== null ? JSON.parse(raw) : fallback;
2402
+ } catch {
2403
+ return fallback;
2404
+ }
2405
+ },
2406
+
2407
+ set(key, value) {
2408
+ sessionStorage.setItem(key, JSON.stringify(value));
2409
+ },
2410
+
2411
+ remove(key) {
2412
+ sessionStorage.removeItem(key);
2413
+ },
2414
+
2415
+ clear() {
2416
+ sessionStorage.clear();
2417
+ },
2418
+ };
2419
+
2420
+
2421
+ // ---------------------------------------------------------------------------
2422
+ // Event bus (pub/sub)
2423
+ // ---------------------------------------------------------------------------
2424
+ class EventBus {
2425
+ constructor() { this._handlers = new Map(); }
2426
+
2427
+ on(event, fn) {
2428
+ if (!this._handlers.has(event)) this._handlers.set(event, new Set());
2429
+ this._handlers.get(event).add(fn);
2430
+ return () => this.off(event, fn);
2431
+ }
2432
+
2433
+ off(event, fn) {
2434
+ this._handlers.get(event)?.delete(fn);
2435
+ }
2436
+
2437
+ emit(event, ...args) {
2438
+ this._handlers.get(event)?.forEach(fn => fn(...args));
2439
+ }
2440
+
2441
+ once(event, fn) {
2442
+ const wrapper = (...args) => { fn(...args); this.off(event, wrapper); };
2443
+ return this.on(event, wrapper);
2444
+ }
2445
+
2446
+ clear() { this._handlers.clear(); }
2447
+ }
2448
+
2449
+ const bus = new EventBus();
2450
+
2451
+ // --- index.js (assembly) ——————————————————————————————————————————
2452
+ /**
2453
+ * ┌─────────────────────────────────────────────────────────┐
2454
+ * │ zQuery (zeroQuery) — Lightweight Frontend Library │
2455
+ * │ │
2456
+ * │ jQuery-like selectors · Reactive components │
2457
+ * │ SPA router · State management · Zero dependencies │
2458
+ * │ │
2459
+ * │ https://github.com/tonywied17/zero-query │
2460
+ * └─────────────────────────────────────────────────────────┘
2461
+ */
2462
+
2463
+
2464
+
2465
+
2466
+
2467
+
2468
+
2469
+
2470
+ // ---------------------------------------------------------------------------
2471
+ // $ — The main function & namespace
2472
+ // ---------------------------------------------------------------------------
2473
+
2474
+ /**
2475
+ * Main selector function
2476
+ *
2477
+ * $('selector') → single Element (querySelector)
2478
+ * $('<div>hello</div>') → create element (first created node)
2479
+ * $(element) → return element as-is
2480
+ * $(fn) → DOMContentLoaded shorthand
2481
+ *
2482
+ * @param {string|Element|NodeList|Function} selector
2483
+ * @param {string|Element} [context]
2484
+ * @returns {Element|null}
2485
+ */
2486
+ function $(selector, context) {
2487
+ // $(fn) → DOM ready shorthand
2488
+ if (typeof selector === 'function') {
2489
+ query.ready(selector);
2490
+ return;
2491
+ }
2492
+ return query(selector, context);
2493
+ }
2494
+
2495
+
2496
+ // --- Quick refs ------------------------------------------------------------
2497
+ $.id = query.id;
2498
+ $.class = query.class;
2499
+ $.classes = query.classes;
2500
+ $.tag = query.tag;
2501
+ $.children = query.children;
2502
+
2503
+ // --- Collection selector ---------------------------------------------------
2504
+ /**
2505
+ * Collection selector (like jQuery's $)
2506
+ *
2507
+ * $.all('selector') → ZQueryCollection (querySelectorAll)
2508
+ * $.all('<div>hello</div>') → create elements as collection
2509
+ * $.all(element) → wrap element in collection
2510
+ * $.all(nodeList) → wrap NodeList in collection
2511
+ *
2512
+ * @param {string|Element|NodeList|Array} selector
2513
+ * @param {string|Element} [context]
2514
+ * @returns {ZQueryCollection}
2515
+ */
2516
+ $.all = function(selector, context) {
2517
+ return queryAll(selector, context);
2518
+ };
2519
+
2520
+ // --- DOM helpers -----------------------------------------------------------
2521
+ $.create = query.create;
2522
+ $.ready = query.ready;
2523
+ $.on = query.on;
2524
+ $.fn = query.fn;
2525
+
2526
+ // --- Reactive primitives ---------------------------------------------------
2527
+ $.reactive = reactive;
2528
+ $.signal = signal;
2529
+ $.computed = computed;
2530
+ $.effect = effect;
2531
+
2532
+ // --- Components ------------------------------------------------------------
2533
+ $.component = component;
2534
+ $.mount = mount;
2535
+ $.mountAll = mountAll;
2536
+ $.getInstance = getInstance;
2537
+ $.destroy = destroy;
2538
+ $.components = getRegistry;
2539
+ $.style = style;
2540
+
2541
+ // --- Router ----------------------------------------------------------------
2542
+ $.router = createRouter;
2543
+ $.getRouter = getRouter;
2544
+
2545
+ // --- Store -----------------------------------------------------------------
2546
+ $.store = createStore;
2547
+ $.getStore = getStore;
2548
+
2549
+ // --- HTTP ------------------------------------------------------------------
2550
+ $.http = http;
2551
+ $.get = http.get;
2552
+ $.post = http.post;
2553
+ $.put = http.put;
2554
+ $.patch = http.patch;
2555
+ $.delete = http.delete;
2556
+
2557
+ // --- Utilities -------------------------------------------------------------
2558
+ $.debounce = debounce;
2559
+ $.throttle = throttle;
2560
+ $.pipe = pipe;
2561
+ $.once = once;
2562
+ $.sleep = sleep;
2563
+ $.escapeHtml = escapeHtml;
2564
+ $.html = html;
2565
+ $.trust = trust;
2566
+ $.uuid = uuid;
2567
+ $.camelCase = camelCase;
2568
+ $.kebabCase = kebabCase;
2569
+ $.deepClone = deepClone;
2570
+ $.deepMerge = deepMerge;
2571
+ $.isEqual = isEqual;
2572
+ $.param = param;
2573
+ $.parseQuery = parseQuery;
2574
+ $.storage = storage;
2575
+ $.session = session;
2576
+ $.bus = bus;
2577
+
2578
+ // --- Meta ------------------------------------------------------------------
2579
+ $.version = '0.2.2';
2580
+ $.meta = {}; // populated at build time by CLI bundler
2581
+
2582
+ $.noConflict = () => {
2583
+ if (typeof window !== 'undefined' && window.$ === $) {
2584
+ delete window.$;
2585
+ }
2586
+ return $;
2587
+ };
2588
+
2589
+
2590
+ // ---------------------------------------------------------------------------
2591
+ // Global exposure (browser)
2592
+ // ---------------------------------------------------------------------------
2593
+ if (typeof window !== 'undefined') {
2594
+ window.$ = $;
2595
+ window.zQuery = $;
2596
+ }
2597
+
2598
+
2599
+ // ---------------------------------------------------------------------------
2600
+ // Named exports (ES modules)
2601
+ // ---------------------------------------------------------------------------
2602
+
2603
+ $;
2604
+
2605
+ })(typeof window !== 'undefined' ? window : globalThis);