zero-query 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1271 @@
1
+ <p align="center">
2
+ <img src="docs/images/logo.svg" alt="zQuery logo" width="300" height="300">
3
+ </p>
4
+
5
+ <h1 align="center">zQuery</h1>
6
+
7
+ <p align="center">
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@tonywied17/zero-query.svg)](https://www.npmjs.com/package/@tonywied17/zero-query)
10
+ [![npm downloads](https://img.shields.io/npm/dm/@tonywied17/zero-query.svg)](https://www.npmjs.com/package/@tonywied17/zero-query)
11
+ [![GitHub](https://img.shields.io/badge/GitHub-zero--query-blue.svg)](https://github.com/tonywied17/zero-query)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
13
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](package.json)
14
+
15
+ </p>
16
+
17
+ > **Lightweight, zero-dependency frontend library that combines jQuery-style DOM manipulation with a modern reactive component system, SPA router, global state management, HTTP client, and utility toolkit — all in a single ~45 KB minified browser bundle. No build step required.**
18
+
19
+ ## Features
20
+
21
+ | Module | Highlights |
22
+ | --- | --- |
23
+ | **Core `$()`** | jQuery-like chainable selectors, traversal, DOM manipulation, events, animation |
24
+ | **Components** | Reactive state, template literals, `@event` delegation, `z-model` two-way binding, scoped styles, lifecycle hooks |
25
+ | **Router** | History & hash mode, route params (`:id`), guards, lazy loading, `z-link` navigation |
26
+ | **Store** | Reactive global state, named actions, computed getters, middleware, subscriptions |
27
+ | **HTTP** | Fetch wrapper with auto-JSON, interceptors, timeout/abort, base URL |
28
+ | **Reactive** | Deep proxy reactivity, Signals, computed values, effects |
29
+ | **Utils** | debounce, throttle, pipe, once, sleep, escapeHtml, uuid, deepClone, deepMerge, storage/session wrappers, event bus |
30
+
31
+ ---
32
+
33
+ ## Quick Start (Browser Bundle — Recommended)
34
+
35
+ The preferred way to use zQuery is with the **pre-built browser bundle** (`zQuery.min.js`). No npm install, no bundler, no transpiler.
36
+
37
+ ### 1. Build the bundle (one time)
38
+
39
+ ```bash
40
+ node build.js
41
+ ```
42
+
43
+ This creates `dist/zQuery.js` and `dist/zQuery.min.js`.
44
+
45
+ ### 2. Copy into your project
46
+
47
+ ```
48
+ my-app/
49
+ index.html
50
+ scripts/
51
+ vendor/
52
+ zQuery.min.js ← copy here
53
+ app.js
54
+ routes.js
55
+ store.js
56
+ components/
57
+ home.js
58
+ about.js
59
+ ```
60
+
61
+ ### 3. Include in HTML
62
+
63
+ ```html
64
+ <!DOCTYPE html>
65
+ <html lang="en">
66
+ <head>
67
+ <meta charset="UTF-8">
68
+ <title>My App</title>
69
+
70
+ <!-- Global styles — <link rel> in the HTML head is the recommended approach
71
+ for app-wide CSS (resets, layout, themes). Prevents FOUC reliably. -->
72
+ <link rel="stylesheet" href="styles/styles.css">
73
+
74
+ <!-- Load zQuery (global $ and zQuery are available immediately) -->
75
+ <script src="scripts/vendor/zQuery.min.js"></script>
76
+
77
+ <!-- Your app code as ES module (components use $ globally) -->
78
+ <script type="module" src="scripts/app.js"></script>
79
+ </head>
80
+ <body>
81
+ <nav>
82
+ <a z-link="/">Home</a>
83
+ <a z-link="/about">About</a>
84
+ </nav>
85
+ <div id="app"></div>
86
+ </body>
87
+ </html>
88
+ ```
89
+
90
+ ### 4. Register components and boot the router
91
+
92
+ ```js
93
+ // scripts/app.js
94
+ import './components/home.js';
95
+ import './components/about.js';
96
+ import { routes } from './routes.js';
97
+
98
+ // Global styles are loaded via <link rel="stylesheet"> in index.html (recommended).
99
+ // $.style() is available for dynamic stylesheets, runtime overrides, or theme switching.
100
+
101
+ const router = $.router({
102
+ el: '#app',
103
+ routes,
104
+ fallback: 'not-found',
105
+ });
106
+ ```
107
+
108
+ That's it — a fully working SPA with zero build tools.
109
+
110
+ ---
111
+
112
+ ## Recommended Project Structure
113
+
114
+ ```
115
+ my-app/
116
+ index.html ← entry point
117
+ scripts/
118
+ vendor/
119
+ zQuery.min.js ← the built library
120
+ app.js ← boot: imports components, creates router & store
121
+ app.css ← global styles (linked in index.html via <link rel>)
122
+ routes.js ← route definitions
123
+ store.js ← global store config
124
+ components/
125
+ home.js ← each page/component in its own file
126
+ counter.js
127
+ todos.js
128
+ about.js
129
+ not-found.js
130
+ ```
131
+
132
+ **Conventions:**
133
+ - Place `zQuery.min.js` in a `vendor/` folder.
134
+ - One component per file inside `components/`.
135
+ - Store and routes get their own files at the `scripts/` root.
136
+ - `app.js` is the single entry point — import components, create the store, and boot the router.
137
+ - Component names **must contain a hyphen** (Web Component convention): `home-page`, `app-counter`, etc.
138
+
139
+ ---
140
+
141
+ ## Selectors & DOM: `$(selector)` & `$.all(selector)`
142
+
143
+ zQuery provides two selector functions:
144
+
145
+ - **`$(selector)`** — returns a **single element** (`querySelector`) or `null`
146
+ - **`$.all(selector)`** — returns a **collection** (`querySelectorAll`) as a chainable `ZQueryCollection`
147
+
148
+ Both also accept DOM elements, NodeLists, HTML strings, and (for `$` only) a function for DOM-ready.
149
+
150
+ ```js
151
+ // Single element (querySelector)
152
+ const card = $('.card'); // first .card element (or null)
153
+ card.classList.add('active'); // plain DOM API
154
+
155
+ // Collection (querySelectorAll)
156
+ $.all('.card').addClass('active').css({ opacity: '1' });
157
+
158
+ // Create element from HTML
159
+ const el = $('<div class="alert">Hello!</div>');
160
+ document.body.appendChild(el);
161
+
162
+ // DOM ready shorthand
163
+ $(() => {
164
+ console.log('DOM is ready');
165
+ });
166
+
167
+ // Wrap an existing element
168
+ $(document.getElementById('app')); // returns the element as-is
169
+ ```
170
+
171
+ ### Quick-Ref Shortcuts
172
+
173
+ ```js
174
+ $.id('myId') // document.getElementById('myId')
175
+ $.class('myClass') // document.querySelector('.myClass')
176
+ $.classes('myClass') // Array.from(document.getElementsByClassName('myClass'))
177
+ $.tag('div') // Array.from(document.getElementsByTagName('div'))
178
+ $.children('parentId') // Array.from of children of #parentId
179
+ ```
180
+
181
+ ### Element Creation
182
+
183
+ ```js
184
+ const btn = $.create('button', {
185
+ class: 'primary',
186
+ style: { padding: '10px' },
187
+ onclick: () => alert('clicked'),
188
+ data: { action: 'submit' }
189
+ }, 'Click Me');
190
+
191
+ document.body.appendChild(btn);
192
+ ```
193
+
194
+ ### Collection Methods (on `ZQueryCollection` via `$.all()`)
195
+
196
+ **Traversal:** `find()`, `parent()`, `closest()`, `children()`, `siblings()`, `next()`, `prev()`, `filter()`, `not()`, `has()`
197
+
198
+ **Iteration:** `each()`, `map()`, `first()`, `last()`, `eq(i)`, `toArray()`
199
+
200
+ **Classes:** `addClass()`, `removeClass()`, `toggleClass()`, `hasClass()`
201
+
202
+ **Attributes:** `attr()`, `removeAttr()`, `prop()`, `data()`
203
+
204
+ **Content:** `html()`, `text()`, `val()`
205
+
206
+ **DOM Manipulation:** `append()`, `prepend()`, `after()`, `before()`, `wrap()`, `remove()`, `empty()`, `clone()`, `replaceWith()`
207
+
208
+ **CSS / Dimensions:** `css()`, `width()`, `height()`, `offset()`, `position()`
209
+
210
+ **Visibility:** `show()`, `hide()`, `toggle()`
211
+
212
+ **Events:** `on()`, `off()`, `one()`, `trigger()`, `click()`, `submit()`, `focus()`, `blur()`
213
+
214
+ **Animation:** `animate()`, `fadeIn()`, `fadeOut()`, `slideToggle()`
215
+
216
+ **Forms:** `serialize()`, `serializeObject()`
217
+
218
+ ### Delegated Events
219
+
220
+ ```js
221
+ // Direct event on a collection
222
+ $.all('.btn').on('click', (e) => console.log('clicked', e.target));
223
+
224
+ // Delegated event (like jQuery's .on with selector)
225
+ $.all('#list').on('click', '.item', function(e) {
226
+ console.log('Item clicked:', this.textContent);
227
+ });
228
+
229
+ // One-time event
230
+ $.all('.btn').one('click', () => console.log('fires once'));
231
+
232
+ // Custom event
233
+ $.all('.widget').trigger('custom:update', { value: 42 });
234
+ ```
235
+
236
+ ### Global Delegation
237
+
238
+ ```js
239
+ // Listen for clicks on any .delete-btn anywhere in the document
240
+ $.on('click', '.delete-btn', function(e) {
241
+ this.closest('.row').remove();
242
+ });
243
+ ```
244
+
245
+ ### Extend Collection Prototype
246
+
247
+ ```js
248
+ // Add custom methods to all collections (like $.fn in jQuery)
249
+ $.fn.highlight = function(color = 'yellow') {
250
+ return this.css({ background: color });
251
+ };
252
+
253
+ $.all('.important').highlight('#ff0');
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Components
259
+
260
+ Declarative components with reactive state, template literals, event delegation, two-way binding, scoped styles, and lifecycle hooks. No JSX, no virtual DOM, no build step.
261
+
262
+ ### Defining a Component
263
+
264
+ ```js
265
+ $.component('app-counter', {
266
+ // Initial state (object or function returning object)
267
+ state: () => ({ count: 0, step: 1 }),
268
+
269
+ // Lifecycle hooks
270
+ init() { /* runs before first render */ },
271
+ mounted() { /* runs after first render & DOM insert */ },
272
+ updated() { /* runs after every re-render */ },
273
+ destroyed() { /* runs on destroy — clean up subscriptions */ },
274
+
275
+ // Methods (available as this.methodName and in @event bindings)
276
+ increment() { this.state.count += this.state.step; },
277
+ decrement() { this.state.count -= this.state.step; },
278
+ reset() { this.state.count = 0; },
279
+
280
+ // Template (required) — return an HTML string
281
+ render() {
282
+ return `
283
+ <div class="counter">
284
+ <h2>Count: ${this.state.count}</h2>
285
+ <button @click="decrement">−</button>
286
+ <button @click="reset">Reset</button>
287
+ <button @click="increment">+</button>
288
+ <input z-model="step" type="number" min="1">
289
+ </div>
290
+ `;
291
+ },
292
+
293
+ // Scoped styles (optional — auto-prefixed to this component)
294
+ styles: `
295
+ .counter { text-align: center; }
296
+ button { margin: 4px; }
297
+ `
298
+ });
299
+ ```
300
+
301
+ ### Mounting
302
+
303
+ ```js
304
+ // Mount into a specific element
305
+ $.mount('#app', 'app-counter');
306
+
307
+ // Mount with props
308
+ $.mount('#app', 'app-counter', { initialCount: 10 });
309
+
310
+ // Auto-mount: scan DOM for registered custom tags
311
+ $.mountAll(); // finds all <app-counter> tags and mounts them
312
+ ```
313
+
314
+ ### Directives
315
+
316
+ | Directive | Purpose | Example |
317
+ | --- | --- | --- |
318
+ | `@event` | Delegated event binding | `@click="save"` or `@click="save(1, 'draft')"` |
319
+ | `@event.prevent` | `preventDefault()` modifier | `@submit.prevent="handleForm"` |
320
+ | `@event.stop` | `stopPropagation()` modifier | `@click.stop="toggle"` |
321
+ | `z-model` | Reactive two-way input binding | `<input z-model="name">` |
322
+ | `z-model` + `z-lazy` | Update on blur instead of every keystroke | `<input z-model="search" z-lazy>` |
323
+ | `z-model` + `z-trim` | Trim whitespace before writing to state | `<input z-model="name" z-trim>` |
324
+ | `z-model` + `z-number` | Force numeric conversion | `<input z-model="qty" z-number>` |
325
+ | `z-ref` | Element reference | `<input z-ref="emailInput">` → `this.refs.emailInput` |
326
+
327
+ ### Two-Way Binding (`z-model`)
328
+
329
+ `z-model` creates a **reactive two-way sync** between a form element and a state property. When the user types, state updates; when state changes, the element updates. Other parts of the template referencing the same state value re-render instantly.
330
+
331
+ Focus and cursor position are **automatically preserved** during re-renders.
332
+
333
+ ```js
334
+ $.component('profile-form', {
335
+ state: () => ({
336
+ user: { name: '', email: '' },
337
+ age: 25,
338
+ plan: 'free',
339
+ tags: [],
340
+ }),
341
+
342
+ render() {
343
+ const s = this.state;
344
+ return `
345
+ <!-- Text input -- live display updates as you type -->
346
+ <input z-model="user.name" z-trim placeholder="Name">
347
+ <p>Hello, ${s.user.name || 'stranger'}!</p>
348
+
349
+ <!-- Nested key, email type -->
350
+ <input z-model="user.email" type="email" placeholder="Email">
351
+
352
+ <!-- Number -- auto-converts to Number -->
353
+ <input z-model="age" type="number" min="0">
354
+ <p>Age: ${s.age} (type: ${typeof s.age})</p>
355
+
356
+ <!-- Radio group -->
357
+ <label><input z-model="plan" type="radio" value="free"> Free</label>
358
+ <label><input z-model="plan" type="radio" value="pro"> Pro</label>
359
+ <p>Plan: ${s.plan}</p>
360
+
361
+ <!-- Select multiple -- syncs as array -->
362
+ <select z-model="tags" multiple>
363
+ <option>javascript</option>
364
+ <option>html</option>
365
+ <option>css</option>
366
+ </select>
367
+ <p>Tags: ${s.tags.join(', ')}</p>
368
+ `;
369
+ }
370
+ });
371
+ ```
372
+
373
+ **Supported elements:** text inputs, textarea, number/range, checkbox, radio, select, select-multiple, contenteditable.
374
+
375
+ **Nested keys:** `z-model="user.name"` binds to `this.state.user.name`.
376
+
377
+ **Modifiers:** `z-lazy` (change event), `z-trim` (strip whitespace), `z-number` (force numeric). Combinable.
378
+
379
+ ### Event Arguments
380
+
381
+ ```js
382
+ // Pass arguments to methods from templates
383
+ // Supports: strings, numbers, booleans, null, state references
384
+ `<button @click="remove(${item.id})">Delete</button>`
385
+ `<button @click="setFilter('active')">Active</button>`
386
+ `<button @click="update(state.count)">Update</button>`
387
+ ```
388
+
389
+ ### Props
390
+
391
+ Props are passed as attributes on the custom element tag or via `$.mount()`:
392
+
393
+ ```js
394
+ $.component('user-card', {
395
+ render() {
396
+ return `<div class="card"><h3>${this.props.name}</h3></div>`;
397
+ }
398
+ });
399
+
400
+ // Via mount
401
+ $.mount('#target', 'user-card', { name: 'Tony' });
402
+
403
+ // Via HTML tag (auto-mounted)
404
+ // <user-card name="Tony"></user-card>
405
+ ```
406
+
407
+ ### Instance API
408
+
409
+ ```js
410
+ const instance = $.mount('#app', 'my-component');
411
+
412
+ instance.state.count = 5; // trigger re-render
413
+ instance.setState({ count: 5 }); // batch update
414
+ instance.emit('change', { v: 1 }); // dispatch custom event (bubbles)
415
+ instance.refs.myInput.focus(); // access z-ref elements
416
+ instance.destroy(); // teardown
417
+ ```
418
+
419
+ ### Getting & Destroying Instances
420
+
421
+ ```js
422
+ const inst = $.getInstance('#app'); // get instance for element
423
+ $.destroy('#app'); // destroy component at element
424
+ $.components(); // get registry of all definitions (debug)
425
+ ```
426
+
427
+ ### External Templates & Styles (`templateUrl` / `styleUrl`)
428
+
429
+ Components can load their HTML templates and CSS from external files instead of defining them inline. This is useful for large components, maintaining separation of concerns, or organizing components into folder structures.
430
+
431
+ **Relative Path Resolution:**
432
+
433
+ Relative `templateUrl`, `styleUrl`, and `pages.dir` paths are automatically resolved **relative to the component file** — no extra configuration needed:
434
+
435
+ ```js
436
+ // File: scripts/components/widget/index.js
437
+ $.component('my-widget', {
438
+ templateUrl: 'template.html', // → scripts/components/widget/template.html
439
+ styleUrl: 'styles.css', // → scripts/components/widget/styles.css
440
+ });
441
+ ```
442
+
443
+ > zQuery auto-detects the calling module's URL at registration time. If you need to override the resolved base, pass a `base` string (e.g. `base: 'scripts/shared/'`). Absolute paths and full URLs are never affected.
444
+
445
+ **`styleUrl`** — load styles from a CSS file:
446
+
447
+ ```js
448
+ $.component('my-widget', {
449
+ state: { title: 'Hello' },
450
+
451
+ render() {
452
+ return `<div class="widget"><h2>${this.state.title}</h2></div>`;
453
+ },
454
+
455
+ styleUrl: 'styles.css'
456
+ });
457
+ ```
458
+
459
+ **`templateUrl`** — load HTML template from a file:
460
+
461
+ ```js
462
+ $.component('my-widget', {
463
+ state: { title: 'Hello', items: ['A', 'B'] },
464
+
465
+ templateUrl: 'template.html',
466
+ styleUrl: 'styles.css'
467
+ });
468
+ ```
469
+
470
+ The template file uses `{{expression}}` interpolation to access component state:
471
+
472
+ ```html
473
+ <!-- components/my-widget/template.html -->
474
+ <div class="widget">
475
+ <h2>{{title}}</h2>
476
+ <p>Item count: {{items.length}}</p>
477
+ </div>
478
+ ```
479
+
480
+ > **Notes:**
481
+ > - If both `render()` and `templateUrl` are defined, `render()` takes priority.
482
+ > - If both `styles` and `styleUrl` are defined, they are merged.
483
+ > - Templates and styles are fetched once per component definition and cached — multiple instances share the same cache.
484
+ > - Relative paths resolve relative to the component file automatically. Absolute paths and full URLs are used as-is.
485
+ > - `{{expression}}` has access to all `state` properties via a `with(state)` context.
486
+
487
+ ### Multiple Templates — `templateUrl` as object or array
488
+
489
+ `templateUrl` also accepts an **object map** or **array** of URLs. When multiple templates are loaded, they are available inside the component via `this.templates` — a keyed map you can reference in your `render()` function.
490
+
491
+ ```js
492
+ // Object form — keyed by name
493
+ $.component('docs-page', {
494
+ templateUrl: {
495
+ 'router': 'pages/router.html',
496
+ 'store': 'pages/store.html',
497
+ 'components': 'pages/components.html',
498
+ },
499
+ render() {
500
+ const page = this.props.$params.section || 'router';
501
+ return `<div>${this.templates[page]}</div>`;
502
+ }
503
+ });
504
+
505
+ // Array form — keyed by index
506
+ $.component('multi-step', {
507
+ templateUrl: ['pages/step1.html', 'pages/step2.html'],
508
+ render() {
509
+ return `<div>${this.templates[this.state.step]}</div>`;
510
+ }
511
+ });
512
+ ```
513
+
514
+ ### Multiple Stylesheets — `styleUrl` as array
515
+
516
+ `styleUrl` can also accept an **array of URLs**. All stylesheets are fetched in parallel, concatenated, and scoped to the component.
517
+
518
+ ```js
519
+ $.component('my-widget', {
520
+ styleUrl: [
521
+ '../shared/base.css',
522
+ 'styles.css',
523
+ ],
524
+ render() { return '<div class="widget">Content</div>'; }
525
+ });
526
+ ```
527
+
528
+ ### Global Stylesheets
529
+
530
+ **Recommended:** Use a standard `<link rel="stylesheet">` tag in your `index.html` `<head>` for app-wide CSS (resets, layout, themes). This is the most reliable way to prevent FOUC (Flash of Unstyled Content) because the browser loads the stylesheet before first paint — no JavaScript execution needed.
531
+
532
+ ```html
533
+ <!-- index.html -->
534
+ <head>
535
+ <link rel="stylesheet" href="styles/styles.css">
536
+ <script src="scripts/vendor/zQuery.min.js"></script>
537
+ <script type="module" src="scripts/app.js"></script>
538
+ </head>
539
+ ```
540
+
541
+ ### Additional Stylesheets — `$.style()`
542
+
543
+ `$.style()` loads **global** (unscoped) stylesheet files programmatically — useful for **dynamic theme switching**, **loading additional CSS files at runtime**, **conditional styles**, or any case where a static `<link>` tag isn't flexible enough. Paths resolve **relative to the calling file**, just like component paths.
544
+
545
+ ```js
546
+ // Load a stylesheet file dynamically
547
+ $.style('themes/dark.css');
548
+
549
+ // Multiple files
550
+ $.style(['reset.css', 'theme.css']);
551
+
552
+ // Returns a handle to remove later (theme switching)
553
+ const dark = $.style('themes/dark.css');
554
+ // ... later
555
+ dark.remove(); // unloads the stylesheet
556
+
557
+ // Override global styles by loading an additional file
558
+ $.style('overrides.css');
559
+ ```
560
+
561
+ > **`<link rel>` vs `$.style()` vs `styleUrl`:**
562
+ > - Use a **`<link rel="stylesheet">`** in `index.html` for global/app-wide styles — best FOUC prevention.
563
+ > - Use **`$.style()`** to dynamically load additional stylesheet files (themes, overrides, conditional styles).
564
+ > - Use **`styleUrl`** on a component definition for styles scoped to that specific component.
565
+ > - Use component **`styles`** (inline string) for scoped inline CSS within a component definition.
566
+
567
+ ### Pages Config — Multi-Page Components
568
+
569
+ The `pages` option is a high-level shorthand for components that display content from multiple HTML files in a directory (e.g. documentation, wizards, tabbed content). It replaces the need to manually build a `templateUrl` object map and maintain a separate page list.
570
+
571
+ ```js
572
+ // File: scripts/components/docs/index.js
573
+ $.component('docs-page', {
574
+ pages: {
575
+ dir: 'pages', // → scripts/components/docs/pages/
576
+ param: 'section', // reads :section from the route
577
+ default: 'getting-started', // fallback when param absent
578
+ items: [
579
+ 'getting-started', // label auto-derived: 'Getting Started'
580
+ 'project-structure', // label auto-derived: 'Project Structure'
581
+ { id: 'http', label: 'HTTP Client' },
582
+ { id: 'utils', label: 'Utilities' },
583
+ ],
584
+ },
585
+
586
+ styleUrl: 'docs.css', // → scripts/components/docs/docs.css
587
+
588
+ render() {
589
+ return `
590
+ <nav>
591
+ ${this.pages.map(p => `
592
+ <a class="${this.activePage === p.id ? 'active' : ''}"
593
+ z-link="/docs/${p.id}">${p.label}</a>
594
+ `).join('')}
595
+ </nav>
596
+ <main>${this.templates[this.activePage] || ''}</main>
597
+ `;
598
+ }
599
+ });
600
+ ```
601
+
602
+ The `param` property tells the component **which route parameter to read**. It must match a `:param` segment in your router config. Use `fallback` so one route handles both the bare path and the parameterized path:
603
+
604
+ ```js
605
+ // routes.js
606
+ $.router({
607
+ routes: [
608
+ { path: '/docs/:section', component: 'docs-page', fallback: '/docs' },
609
+ ]
610
+ });
611
+ // /docs → activePage = default ('getting-started')
612
+ // /docs/router → activePage = 'router'
613
+ ```
614
+
615
+ | Property | Type | Description |
616
+ | --- | --- | --- |
617
+ | `dir` | `string` | Directory path containing the page HTML files (resolved via `base`) |
618
+ | `param` | `string` | Route param name — must match a `:param` segment in your route |
619
+ | `default` | `string` | Page id shown when the route param is absent |
620
+ | `ext` | `string` | File extension (default `'.html'`) |
621
+ | `items` | `Array` | Page ids (strings) and/or `{id, label}` objects |
622
+
623
+ > **How it works:** Under the hood, `pages` auto-generates a `templateUrl` object map (`{ id: 'dir/id.html' }`) and normalizes the items into a `{id, label}` array. String ids auto-derive labels by converting kebab-case to Title Case (e.g. `'getting-started'` → `'Getting Started'`). The component then exposes `this.pages`, `this.activePage`, and `this.templates` inside `render()`.
624
+
625
+ ---
626
+
627
+ ## Router
628
+
629
+ Client-side SPA router supporting both history mode and hash mode, with route params, query strings, navigation guards, and lazy loading.
630
+
631
+ ### Setup
632
+
633
+ ```js
634
+ const router = $.router({
635
+ el: '#app', // outlet element
636
+ mode: 'history', // 'history' (default) or 'hash'
637
+ base: '/my-app', // base path for sub-directory deployments
638
+ routes: [
639
+ { path: '/', component: 'home-page' },
640
+ { path: '/user/:id', component: 'user-page' },
641
+ { path: '/settings', component: 'settings-page' },
642
+ ],
643
+ fallback: 'not-found' // 404 component
644
+ });
645
+ ```
646
+
647
+ ### Route Definitions
648
+
649
+ ```js
650
+ {
651
+ path: '/user/:id', // :param for dynamic segments
652
+ component: 'user-page', // registered component name (string)
653
+ load: () => import('./pages/user.js'), // lazy load module before mount
654
+ }
655
+ ```
656
+
657
+ - **`path`** — URL pattern. Use `:param` for named params, `*` for wildcard.
658
+ - **`component`** — registered component name (string) or a render function `(route) => htmlString`.
659
+ - **`load`** — optional async function for lazy loading (called before component mount).
660
+ - **`fallback`** — an additional path that also matches this route. When matched via fallback, missing params are `undefined`. Useful for pages-config components: `{ path: '/docs/:section', fallback: '/docs' }`.
661
+
662
+ ### Navigation
663
+
664
+ ```js
665
+ router.navigate('/user/42'); // push + resolve
666
+ router.replace('/login'); // replace current history entry
667
+ router.back(); // history.back()
668
+ router.forward(); // history.forward()
669
+ router.go(-2); // history.go(n)
670
+ ```
671
+
672
+ ### Navigation Links (HTML)
673
+
674
+ ```html
675
+ <!-- z-link attribute for SPA navigation (no page reload) -->
676
+ <a z-link="/">Home</a>
677
+ <a z-link="/user/42">Profile</a>
678
+ ```
679
+
680
+ ### Route Params & Query
681
+
682
+ Inside a routed component, params and query are available as props:
683
+
684
+ ```js
685
+ $.component('user-page', {
686
+ render() {
687
+ const userId = this.props.$params.id;
688
+ const tab = this.props.$query.tab || 'overview';
689
+ return `<h1>User ${userId}</h1><p>Tab: ${tab}</p>`;
690
+ }
691
+ });
692
+ ```
693
+
694
+ ### Navigation Guards
695
+
696
+ ```js
697
+ // Before guard — return false to cancel, string to redirect
698
+ router.beforeEach((to, from) => {
699
+ if (to.path === '/admin' && !isLoggedIn()) {
700
+ return '/login'; // redirect
701
+ }
702
+ });
703
+
704
+ // After guard — analytics, etc.
705
+ router.afterEach((to, from) => {
706
+ trackPageView(to.path);
707
+ });
708
+ ```
709
+
710
+ ### Route Change Listener
711
+
712
+ ```js
713
+ const unsub = router.onChange((to, from) => {
714
+ console.log(`Navigated: ${from?.path} → ${to.path}`);
715
+ });
716
+ // unsub() to stop listening
717
+ ```
718
+
719
+ ### Current Route
720
+
721
+ ```js
722
+ router.current // { route, params, query, path }
723
+ router.path // current path string
724
+ router.query // current query as object
725
+ ```
726
+
727
+ ### Dynamic Routes
728
+
729
+ ```js
730
+ router.add({ path: '/new-page', component: 'new-page' });
731
+ router.remove('/old-page');
732
+ ```
733
+
734
+ ### Sub-Path Deployment & `<base href>`
735
+
736
+ When deploying under a sub-directory (e.g. `https://example.com/my-app/`), the router **auto-detects** `<base href>` — no extra code needed.
737
+
738
+ **Option 1 — HTML `<base href>` tag (recommended):**
739
+
740
+ Add a `<base href>` tag to your `index.html`:
741
+
742
+ ```html
743
+ <head>
744
+ <base href="/my-app/">
745
+ <!-- ... -->
746
+ </head>
747
+ ```
748
+
749
+ The router reads this automatically. No changes to `app.js` required — just call `$.router()` as usual:
750
+
751
+ ```js
752
+ $.router({ el: '#app', routes, fallback: 'not-found' });
753
+ ```
754
+
755
+ **Option 2 — Explicit `base` option:**
756
+
757
+ ```js
758
+ $.router({ el: '#app', base: '/my-app', routes: [...] });
759
+ ```
760
+
761
+ > **Tip:** Using `<base href>` is preferred because it also controls how the browser resolves relative URLs for scripts, stylesheets, images, and fetch requests — keeping all path configuration in one place. The router checks `config.base` → `window.__ZQ_BASE` → `<base href>` tag, in that order.
762
+
763
+ For history mode, configure your server to rewrite all non-file requests to `index.html`. Example `.htaccess`:
764
+
765
+ ```
766
+ RewriteEngine On
767
+ RewriteBase /my-app/
768
+ RewriteCond %{REQUEST_FILENAME} -f
769
+ RewriteRule ^ - [L]
770
+ RewriteRule ^ index.html [L]
771
+ ```
772
+
773
+ ---
774
+
775
+ ## Store
776
+
777
+ Lightweight global state management with reactive proxies, named actions, computed getters, middleware, and subscriptions.
778
+
779
+ ### Setup
780
+
781
+ ```js
782
+ const store = $.store({
783
+ state: {
784
+ count: 0,
785
+ user: null,
786
+ todos: [],
787
+ },
788
+
789
+ actions: {
790
+ increment(state) { state.count++; },
791
+ setUser(state, user) { state.user = user; },
792
+ addTodo(state, text) {
793
+ const raw = state.todos.__raw || state.todos;
794
+ state.todos = [...raw, { id: Date.now(), text, done: false }];
795
+ },
796
+ },
797
+
798
+ getters: {
799
+ doubleCount: (state) => state.count * 2,
800
+ doneCount: (state) => state.todos.filter(t => t.done).length,
801
+ },
802
+
803
+ debug: true, // logs actions to console
804
+ });
805
+ ```
806
+
807
+ ### Dispatching Actions
808
+
809
+ ```js
810
+ store.dispatch('increment');
811
+ store.dispatch('setUser', { name: 'Tony', role: 'admin' });
812
+ store.dispatch('addTodo', 'Write documentation');
813
+ ```
814
+
815
+ ### Reading State
816
+
817
+ ```js
818
+ store.state.count // reactive — triggers subscribers on change
819
+ store.getters.doubleCount // computed from state
820
+ store.snapshot() // deep-cloned plain object
821
+ ```
822
+
823
+ ### Subscriptions
824
+
825
+ ```js
826
+ // Subscribe to a specific key
827
+ const unsub = store.subscribe('count', (newVal, oldVal, key) => {
828
+ console.log(`count changed: ${oldVal} → ${newVal}`);
829
+ });
830
+
831
+ // Wildcard — subscribe to all changes
832
+ store.subscribe((key, newVal, oldVal) => {
833
+ console.log(`${key} changed`);
834
+ });
835
+
836
+ unsub(); // unsubscribe
837
+ ```
838
+
839
+ ### Using Store in Components
840
+
841
+ ```js
842
+ $.component('my-widget', {
843
+ mounted() {
844
+ // Re-render when store key changes
845
+ this._unsub = store.subscribe('count', () => {
846
+ this._scheduleUpdate();
847
+ });
848
+ },
849
+
850
+ destroyed() {
851
+ this._unsub?.();
852
+ },
853
+
854
+ render() {
855
+ return `<p>Count: ${store.state.count}</p>`;
856
+ }
857
+ });
858
+ ```
859
+
860
+ ### Middleware
861
+
862
+ ```js
863
+ store.use((actionName, args, state) => {
864
+ console.log(`[middleware] ${actionName}`, args);
865
+ // return false to block the action
866
+ });
867
+ ```
868
+
869
+ ### Named Stores
870
+
871
+ ```js
872
+ const userStore = $.store('users', { state: { list: [] }, actions: { ... } });
873
+ const appStore = $.store('app', { state: { theme: 'dark' }, actions: { ... } });
874
+
875
+ // Retrieve later
876
+ $.getStore('users');
877
+ $.getStore('app');
878
+ ```
879
+
880
+ ### State Management
881
+
882
+ ```js
883
+ store.replaceState({ count: 0, user: null, todos: [] });
884
+ store.reset({ count: 0, user: null, todos: [] }); // also clears history
885
+ store.history; // array of { action, args, timestamp }
886
+ ```
887
+
888
+ ---
889
+
890
+ ## HTTP Client
891
+
892
+ A lightweight fetch wrapper providing auto-JSON serialization, interceptors, timeout/abort support, and a clean chainable API.
893
+
894
+ ### Basic Requests
895
+
896
+ ```js
897
+ // GET with query params
898
+ const res = await $.get('/api/users', { page: 1, limit: 10 });
899
+ console.log(res.data); // parsed JSON
900
+
901
+ // POST with JSON body
902
+ const created = await $.post('/api/users', { name: 'Tony', email: 'tony@example.com' });
903
+
904
+ // PUT, PATCH, DELETE
905
+ await $.put('/api/users/1', { name: 'Updated' });
906
+ await $.patch('/api/users/1', { email: 'new@example.com' });
907
+ await $.delete('/api/users/1');
908
+ ```
909
+
910
+ ### Configuration
911
+
912
+ ```js
913
+ $.http.configure({
914
+ baseURL: 'https://api.example.com',
915
+ headers: { Authorization: 'Bearer token123' },
916
+ timeout: 15000, // ms (default: 30000)
917
+ });
918
+ ```
919
+
920
+ ### Interceptors
921
+
922
+ ```js
923
+ // Request interceptor
924
+ $.http.onRequest((fetchOpts, url) => {
925
+ fetchOpts.headers['X-Request-ID'] = $.uuid();
926
+ // return false to block | return { url, options } to modify
927
+ });
928
+
929
+ // Response interceptor
930
+ $.http.onResponse((result) => {
931
+ if (result.status === 401) {
932
+ window.location.href = '/login';
933
+ }
934
+ });
935
+ ```
936
+
937
+ ### Abort / Cancel
938
+
939
+ ```js
940
+ const controller = $.http.createAbort();
941
+
942
+ $.get('/api/slow', null, { signal: controller.signal })
943
+ .catch(err => console.log('Aborted:', err.message));
944
+
945
+ // Cancel after 2 seconds
946
+ setTimeout(() => controller.abort(), 2000);
947
+ ```
948
+
949
+ ### Response Object
950
+
951
+ Every request resolves with:
952
+
953
+ ```js
954
+ {
955
+ ok: true, // response.ok
956
+ status: 200, // HTTP status code
957
+ statusText: 'OK',
958
+ headers: { ... }, // parsed headers object
959
+ data: { ... }, // auto-parsed body (JSON, text, or blob)
960
+ response: Response, // raw fetch Response object
961
+ }
962
+ ```
963
+
964
+ ### FormData Upload
965
+
966
+ ```js
967
+ const formData = new FormData();
968
+ formData.append('file', fileInput.files[0]);
969
+ await $.post('/api/upload', formData);
970
+ // Content-Type header is automatically removed so the browser sets multipart boundary
971
+ ```
972
+
973
+ ### Raw Fetch
974
+
975
+ ```js
976
+ const raw = await $.http.raw('/api/stream', { method: 'GET' });
977
+ const reader = raw.body.getReader();
978
+ ```
979
+
980
+ ---
981
+
982
+ ## Reactive Primitives
983
+
984
+ ### Deep Reactive Proxy
985
+
986
+ ```js
987
+ const data = $.reactive({ user: { name: 'Tony' } }, (key, value, old) => {
988
+ console.log(`${key} changed: ${old} → ${value}`);
989
+ });
990
+
991
+ data.user.name = 'Updated'; // triggers callback
992
+ data.__isReactive; // true
993
+ data.__raw; // original plain object
994
+ ```
995
+
996
+ ### Signals
997
+
998
+ Lightweight reactive primitives inspired by Solid/Preact signals:
999
+
1000
+ ```js
1001
+ const count = $.signal(0);
1002
+
1003
+ // Auto-tracking effect
1004
+ $.effect(() => {
1005
+ console.log('Count is:', count.value); // runs immediately, re-runs on change
1006
+ });
1007
+
1008
+ count.value = 5; // triggers the effect
1009
+
1010
+ // Computed signal (derived)
1011
+ const doubled = $.computed(() => count.value * 2);
1012
+ console.log(doubled.value); // 10
1013
+
1014
+ // Manual subscription
1015
+ const unsub = count.subscribe(() => console.log('changed'));
1016
+
1017
+ // Peek without tracking
1018
+ count.peek(); // returns value without subscribing
1019
+ ```
1020
+
1021
+ ---
1022
+
1023
+ ## Utilities
1024
+
1025
+ All utilities are available on the `$` namespace.
1026
+
1027
+ ### Function Utilities
1028
+
1029
+ ```js
1030
+ // Debounce — delays until ms of inactivity
1031
+ const search = $.debounce((query) => fetchResults(query), 300);
1032
+ search('hello');
1033
+ search.cancel(); // cancel pending call
1034
+
1035
+ // Throttle — max once per ms
1036
+ const scroll = $.throttle(() => updatePosition(), 100);
1037
+
1038
+ // Pipe — left-to-right function composition
1039
+ const transform = $.pipe(trim, lowercase, capitalize);
1040
+ transform(' HELLO '); // 'Hello'
1041
+
1042
+ // Once — only runs the first time
1043
+ const init = $.once(() => { /* heavy setup */ });
1044
+
1045
+ // Sleep — async delay
1046
+ await $.sleep(1000);
1047
+ ```
1048
+
1049
+ ### String Utilities
1050
+
1051
+ ```js
1052
+ $.escapeHtml('<script>alert("xss")</script>');
1053
+ // &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;
1054
+
1055
+ // Template tag with auto-escaping
1056
+ const safe = $.html`<div>${userInput}</div>`;
1057
+
1058
+ // Mark trusted HTML (skips escaping)
1059
+ const raw = $.trust('<strong>Bold</strong>');
1060
+ const output = $.html`<div>${raw}</div>`; // <div><strong>Bold</strong></div>
1061
+
1062
+ $.uuid(); // 'a1b2c3d4-...'
1063
+ $.camelCase('my-component'); // 'myComponent'
1064
+ $.kebabCase('myComponent'); // 'my-component'
1065
+ ```
1066
+
1067
+ ### Object Utilities
1068
+
1069
+ ```js
1070
+ const clone = $.deepClone({ nested: { value: 1 } });
1071
+ const merged = $.deepMerge({}, defaults, userConfig);
1072
+ $.isEqual({ a: 1 }, { a: 1 }); // true
1073
+ ```
1074
+
1075
+ ### URL Utilities
1076
+
1077
+ ```js
1078
+ $.param({ page: 1, sort: 'name' }); // 'page=1&sort=name'
1079
+ $.parseQuery('page=1&sort=name'); // { page: '1', sort: 'name' }
1080
+ ```
1081
+
1082
+ ### Storage Wrappers
1083
+
1084
+ ```js
1085
+ // localStorage with auto JSON serialization
1086
+ $.storage.set('user', { name: 'Tony', prefs: { theme: 'dark' } });
1087
+ $.storage.get('user'); // { name: 'Tony', prefs: { theme: 'dark' } }
1088
+ $.storage.get('missing', []); // [] (fallback)
1089
+ $.storage.remove('user');
1090
+ $.storage.clear();
1091
+
1092
+ // sessionStorage (same API)
1093
+ $.session.set('token', 'abc123');
1094
+ $.session.get('token');
1095
+ ```
1096
+
1097
+ ### Event Bus
1098
+
1099
+ Global pub/sub for cross-component communication without direct coupling:
1100
+
1101
+ ```js
1102
+ // Subscribe
1103
+ const unsub = $.bus.on('user:login', (user) => {
1104
+ console.log('Logged in:', user.name);
1105
+ });
1106
+
1107
+ // Emit
1108
+ $.bus.emit('user:login', { name: 'Tony' });
1109
+
1110
+ // One-time listener
1111
+ $.bus.once('app:ready', () => { /* runs once */ });
1112
+
1113
+ // Unsubscribe
1114
+ unsub();
1115
+ $.bus.off('user:login', handler);
1116
+ $.bus.clear(); // remove all listeners
1117
+ ```
1118
+
1119
+ ---
1120
+
1121
+ ## DOM Ready
1122
+
1123
+ ```js
1124
+ // Shorthand (pass function to $)
1125
+ $(() => {
1126
+ console.log('DOM ready');
1127
+ });
1128
+
1129
+ // Explicit
1130
+ $.ready(() => {
1131
+ console.log('DOM ready');
1132
+ });
1133
+ ```
1134
+
1135
+ ---
1136
+
1137
+ ## No-Conflict Mode
1138
+
1139
+ ```js
1140
+ const mq = $.noConflict(); // removes $ from window, returns the library
1141
+ mq('.card').addClass('active');
1142
+ ```
1143
+
1144
+ ---
1145
+
1146
+ ## Building from Source
1147
+
1148
+ ```bash
1149
+ # One-time build
1150
+ node build.js
1151
+ # → dist/zQuery.js (development)
1152
+ # → dist/zQuery.min.js (production)
1153
+
1154
+ # Watch mode (rebuilds on file changes)
1155
+ node build.js --watch
1156
+ ```
1157
+
1158
+ The build script is zero-dependency — just Node.js. It concatenates all ES modules into a single IIFE and strips import/export statements. The minified version strips comments and collapses whitespace. For production builds, pipe through Terser for optimal compression.
1159
+
1160
+ ---
1161
+
1162
+ ## Running the Starter App
1163
+
1164
+ ```bash
1165
+ # From the project root
1166
+ node build.js # build the bundle
1167
+ cp dist/zQuery.min.js examples/starter-app/scripts/vendor/ # copy to app
1168
+
1169
+ # Start the dev server (uses zero-http)
1170
+ npm run serve
1171
+ # → http://localhost:3000
1172
+
1173
+ # Or use any static server
1174
+ npx serve examples/starter-app
1175
+ ```
1176
+
1177
+ The starter app includes: Home, Counter (reactive state + z-model), Todos (global store + subscriptions), API Docs (full reference), and About pages.
1178
+
1179
+ ### Local Dev Server
1180
+
1181
+ The project ships with a lightweight dev server powered by [zero-http](https://github.com/tonywied17/zero-http). It handles history-mode SPA routing (all non-file requests serve `index.html`).
1182
+
1183
+ ```bash
1184
+ # Default: port 3000
1185
+ npm run serve
1186
+
1187
+ # Custom port
1188
+ node examples/starter-app/local-server.js 8080
1189
+
1190
+ # Or install zero-http yourself for any project
1191
+ npm install zero-http --save-dev
1192
+ ```
1193
+
1194
+ The server source is at `examples/starter-app/local-server.js` — about 30 lines of code.
1195
+
1196
+ ### Production Deployment
1197
+
1198
+ For history-mode routing in production, configure your web server to rewrite non-file requests to `index.html`.
1199
+
1200
+ **Apache (.htaccess):**
1201
+
1202
+ ```apache
1203
+ RewriteEngine On
1204
+ RewriteBase /
1205
+ RewriteCond %{REQUEST_FILENAME} !-f
1206
+ RewriteCond %{REQUEST_FILENAME} !-d
1207
+ RewriteRule ^ index.html [L]
1208
+ ```
1209
+
1210
+ **Nginx:**
1211
+
1212
+ ```nginx
1213
+ location / {
1214
+ try_files $uri $uri/ /index.html;
1215
+ }
1216
+ ```
1217
+
1218
+ **Sub-path deployment** (e.g. hosted at `/my-app/`):
1219
+
1220
+ Set `<base href="/my-app/">` in your HTML `<head>` — the router auto-detects it. Or pass `base` explicitly:
1221
+
1222
+ ```js
1223
+ $.router({ el: '#app', base: '/my-app', routes });
1224
+ ```
1225
+
1226
+ ```apache
1227
+ # Apache — adjust RewriteBase
1228
+ RewriteBase /my-app/
1229
+ ```
1230
+
1231
+ ```nginx
1232
+ # Nginx — adjust location block
1233
+ location /my-app/ {
1234
+ try_files $uri $uri/ /my-app/index.html;
1235
+ }
1236
+ ```
1237
+
1238
+ ---
1239
+
1240
+ ## Complete API at a Glance
1241
+
1242
+ | Namespace | Methods |
1243
+ | --- | --- |
1244
+ | `$()` | Single-element selector → `Element \| null` |
1245
+ | `$.all()` | Collection selector → `ZQueryCollection` |
1246
+ | `$.id` `$.class` `$.classes` `$.tag` `$.children` | Quick DOM refs |
1247
+ | `$.create` | Element factory |
1248
+ | `$.ready` `$.on` | DOM ready, global delegation |
1249
+ | `$.fn` | Collection prototype (extend it) |
1250
+ | `$.component` `$.mount` `$.mountAll` `$.getInstance` `$.destroy` `$.components` | Component system |
1251
+ | `$.style` | Dynamically load additional global (unscoped) stylesheet file(s) — paths resolve relative to the calling file |
1252
+ | `$.router` `$.getRouter` | SPA router |
1253
+ | `$.store` `$.getStore` | State management |
1254
+ | `$.http` `$.get` `$.post` `$.put` `$.patch` `$.delete` | HTTP client |
1255
+ | `$.reactive` `$.signal` `$.computed` `$.effect` | Reactive primitives |
1256
+ | `$.debounce` `$.throttle` `$.pipe` `$.once` `$.sleep` | Function utils |
1257
+ | `$.escapeHtml` `$.html` `$.trust` `$.uuid` `$.camelCase` `$.kebabCase` | String utils |
1258
+ | `$.deepClone` `$.deepMerge` `$.isEqual` | Object utils |
1259
+ | `$.param` `$.parseQuery` | URL utils |
1260
+ | `$.storage` `$.session` | Storage wrappers |
1261
+ | `$.bus` | Event bus |
1262
+ | `$.version` | Library version |
1263
+ | `$.noConflict` | Release `$` global |
1264
+
1265
+ For full method signatures and options, see [API.md](API.md).
1266
+
1267
+ ---
1268
+
1269
+ ## License
1270
+
1271
+ MIT — [Anthony Wiedman / Molex](https://github.com/tonywied17)