zero-query 0.2.5 → 0.2.7

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