zero-query 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  | Module | Highlights |
23
23
  | --- | --- |
24
- | **Components** | Reactive state, template literals, `@event` delegation (22 modifiers - key filters, system keys, `.outside`, timing, and more), `z-model` two-way binding (with `z-trim`, `z-number`, `z-lazy`, `z-debounce`, `z-uppercase`, `z-lowercase`), computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip`), DOM morphing engine with LIS-based keyed reconciliation (no innerHTML rebuild), CSP-safe expression evaluation with AST caching, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
24
+ | **Components** | Reactive state, template literals, `@event` delegation (key filters for any key via `KeyboardEvent.key`, system keys, `.outside`, timing, behavior modifiers, and more), `z-model` two-way binding (with `z-trim`, `z-number`, `z-lazy`, `z-debounce`, `z-uppercase`, `z-lowercase`), computed properties, watch callbacks, slot-based content projection, directives (`z-if`/`z-else-if`/`z-else`, `z-for`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip`), DOM morphing engine with LIS-based keyed reconciliation (no innerHTML rebuild), CSP-safe expression evaluation with AST caching, scoped styles, external templates (`templateUrl` / `styleUrl`), lifecycle hooks, auto-injected base styles |
25
25
  | **Router** | History & hash mode, route params (`:id`), wildcards, guards (`beforeEach`/`afterEach`), lazy loading, `z-link` navigation with `z-link-params`, `z-to-top` scroll modifier (`instant`/`smooth`), `z-active-route` active-link class directive, `<z-outlet>` declarative mount point, sub-route history substates (`pushSubstate`/`onSubstate`) |
26
26
  | **Directives** | `z-if`, `z-else-if`, `z-else`, `z-for`, `z-model`, `z-show`, `z-bind`/`:attr`, `z-class`, `z-style`, `z-text`, `z-html`, `z-ref`, `z-cloak`, `z-pre`, `z-key`, `z-skip`, `@event`/`z-on` &mdash; 17 built-in template directives |
27
27
  | **Reactive** | Deep proxy reactivity, Signals (`.value`, `.peek()`), computed values, effects (auto-tracked with dispose), `batch()` for deferred notifications, `untracked()` for dependency-free reads |
@@ -97,7 +97,7 @@ npx zquery build
97
97
  <a z-link="/">Home</a>
98
98
  <a z-link="/about">About</a>
99
99
  </nav>
100
- <div id="app"></div>
100
+ <z-outlet></z-outlet>
101
101
  </body>
102
102
  </html>
103
103
  ```
@@ -108,7 +108,7 @@ npx zquery build
108
108
  // app/app.js
109
109
  import './components/home.js';
110
110
  import './components/about.js';
111
- import './components/contacts/contacts.js';
111
+ import './components/not-found.js';
112
112
  import { routes } from './routes.js';
113
113
 
114
114
  $.router({ routes, fallback: 'not-found' });
@@ -116,6 +116,8 @@ $.router({ routes, fallback: 'not-found' });
116
116
 
117
117
  ### Define a Component
118
118
 
119
+ One component per file — each self-registers via `$.component()` when imported:
120
+
119
121
  ```js
120
122
  // app/components/home.js
121
123
  $.component('home-page', {
@@ -131,6 +133,24 @@ $.component('home-page', {
131
133
  });
132
134
  ```
133
135
 
136
+ The router's `fallback` component handles unmatched routes — same pattern. Use `$.getRouter().current?.path` to show the requested URL:
137
+
138
+ ```js
139
+ // app/components/not-found.js
140
+ $.component('not-found', {
141
+ render() {
142
+ const router = $.getRouter();
143
+ return `
144
+ <div class="card">
145
+ <h2>404</h2>
146
+ <p>The page <code>${$.escapeHtml(router.current?.path || '')}</code> was not found.</p>
147
+ <a z-link="/">Go Home</a>
148
+ </div>
149
+ `;
150
+ }
151
+ });
152
+ ```
153
+
134
154
  That's it - a fully working SPA with the dev server's live-reload.
135
155
 
136
156
  ---
@@ -192,8 +212,9 @@ Use `--ssr` for a project with server-side rendering:
192
212
 
193
213
  ```
194
214
  my-app/ ← SSR scaffold (npx zquery create my-app --ssr)
195
- index.html ← client HTML shell
215
+ index.html ← client HTML shell (meta tags, z-link nav)
196
216
  global.css
217
+ package.json
197
218
  app/
198
219
  app.js ← client entry - registers shared components
199
220
  routes.js ← shared route definitions
@@ -201,12 +222,17 @@ my-app/ ← SSR scaffold (npx zquery create my-app --ss
201
222
  home.js ← shared component (SSR + client)
202
223
  about.js
203
224
  not-found.js
225
+ blog/ ← folder component - param routing
226
+ index.js ← blog list (/blog)
227
+ post.js ← blog detail (/blog/:slug)
204
228
  server/
205
- index.js ← SSR HTTP server
229
+ index.js ← SSR HTTP server with JSON API
230
+ data/
231
+ posts.js ← sample blog data
206
232
  assets/
207
233
  ```
208
234
 
209
- Components in `app/components/` export plain definition objects - the client registers them with `$.component()`, the server with `app.component()`. The `--ssr` flag handles everything automatically - installs dependencies, starts the server at `http://localhost:3000`, and opens the browser.
235
+ Components in `app/components/` export plain definition objects - the client registers them with `$.component()`, the server with `app.component()`. The scaffold includes a blog with param-based routing (`/blog/:slug`), per-route SEO metadata, JSON API endpoints (`/api/posts`), and `window.__SSR_DATA__` hydration. The `--ssr` flag handles everything automatically - installs dependencies, starts the server at `http://localhost:3000`, and opens the browser.
210
236
 
211
237
  - One component per file inside `components/`.
212
238
  - Names **must contain a hyphen** (Web Component convention): `home-page`, `app-counter`, etc.
@@ -290,7 +316,22 @@ location / {
290
316
  }
291
317
  ```
292
318
 
293
- **Sub-path deployment** (e.g. `/my-app/`): set `<base href="/my-app/">` in your HTML - the router auto-detects it.
319
+ **Sub-path deployment** (e.g. `/my-app/`): add `<base href="/my-app/">` to your `<head>` the router auto-detects it:
320
+
321
+ ```html
322
+ <head>
323
+ <base href="/my-app/">
324
+ <meta charset="UTF-8">
325
+ <title>My App</title>
326
+ ...
327
+ </head>
328
+ ```
329
+
330
+ Or pass `base` directly in JavaScript:
331
+
332
+ ```js
333
+ $.router({ base: '/my-app', routes });
334
+ ```
294
335
 
295
336
  ---
296
337
 
@@ -172,8 +172,10 @@ function minifyCSS(css) {
172
172
  css = css.replace(/\/\*[\s\S]*?\*\//g, '');
173
173
  // Collapse whitespace
174
174
  css = css.replace(/\s{2,}/g, ' ');
175
- // Remove spaces around { } : ; ,
176
- css = css.replace(/\s*([{};:,])\s*/g, '$1');
175
+ // Remove spaces around { } ; , (but NOT : pseudo-selectors like :not() need the preceding space)
176
+ css = css.replace(/\s*([{};,])\s*/g, '$1');
177
+ // Remove space after : (safe in both selectors and declarations)
178
+ css = css.replace(/:\s+/g, ':');
177
179
  // Remove trailing semicolons before }
178
180
  css = css.replace(/;}/g, '}');
179
181
  return css.trim();
@@ -357,7 +359,8 @@ function _collapseTemplateCSS(tpl) {
357
359
  let t = segments[s].val;
358
360
  t = t.replace(/\/\*[\s\S]*?\*\//g, '');
359
361
  t = t.replace(/\s{2,}/g, ' ');
360
- t = t.replace(/\s*([{};:,])\s*/g, '$1');
362
+ t = t.replace(/\s*([{};,])\s*/g, '$1');
363
+ t = t.replace(/:\s+/g, ':');
361
364
  t = t.replace(/;}/g, '}');
362
365
  segments[s].val = t;
363
366
  }
@@ -89,6 +89,13 @@ function createProject(args) {
89
89
  process.exit(1);
90
90
  }
91
91
 
92
+ // Copy zquery.min.js into the project root so the SSR server can serve it
93
+ const zqMin = path.join(target, 'node_modules', 'zero-query', 'dist', 'zquery.min.js');
94
+ if (fs.existsSync(zqMin)) {
95
+ fs.copyFileSync(zqMin, path.join(target, 'zquery.min.js'));
96
+ console.log(` ✓ zquery.min.js`);
97
+ }
98
+
92
99
  console.log(`\n Starting SSR server...\n`);
93
100
  const child = spawn('node', ['server/index.js'], { cwd: target, stdio: 'inherit' });
94
101
 
package/cli/help.js CHANGED
@@ -12,7 +12,8 @@ function showHelp() {
12
12
  --minimal, -m Use the minimal template (home, counter, about)
13
13
  instead of the full-featured default scaffold
14
14
  --ssr, -s Use the SSR template - includes server/index.js
15
- SSR HTTP server with shared component definitions
15
+ SSR HTTP server with shared component definitions,
16
+ blog with param routing, JSON API, and SEO metadata
16
17
 
17
18
  dev [root] Start a dev server with live-reload
18
19
  --port, -p <number> Port number (default: 3100)
package/cli/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * zQuery CLI - entry point
@@ -42,9 +42,6 @@ router.afterEach((to) => {
42
42
 
43
43
  // Highlight the active nav link on every route change
44
44
  router.onChange((to) => {
45
- $.all('.nav-link').removeClass('active');
46
- $(`.nav-link[z-link="${to.path}"]`).addClass('active');
47
-
48
45
  // Close mobile menu on navigate
49
46
  closeMobileMenu();
50
47
  });
@@ -161,9 +158,9 @@ $.ready(() => {
161
158
  if (current === 'system') applyTheme('system');
162
159
  });
163
160
 
164
- // Set active link on initial load
165
- const current = window.location.pathname;
166
- $.all(`.nav-link[z-link="${current}"]`).addClass('active');
161
+ // Highlight the active link on initial load is handled by z-active-route
162
+
163
+ console.log('⚡ {{NAME}} - powered by zQuery v' + $.version);
167
164
 
168
165
  // Stats panel: restore open/closed from $.storage (defaults to open)
169
166
  const statsOpen = $.storage.get('statsOpen');
@@ -24,6 +24,8 @@
24
24
  --radius: 8px;
25
25
  --radius-lg: 12px;
26
26
  --sidebar-w: 240px;
27
+ --sidebar-bg: var(--bg-surface);
28
+ --sidebar-border: var(--border);
27
29
  --topbar-h: 56px;
28
30
  --font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
29
31
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
@@ -41,6 +43,8 @@
41
43
  --accent-hover:#0550ae;
42
44
  --accent-soft: rgba(9,105,218,0.08);
43
45
  --accent-glow: rgba(9,105,218,0.04);
46
+ --sidebar-bg: #eef2f9;
47
+ --sidebar-border: rgba(9, 105, 218, 0.12);
44
48
  --success: #1a7f37;
45
49
  --danger: #cf222e;
46
50
  --info: #0969da;
@@ -73,8 +77,8 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
73
77
  position: fixed;
74
78
  top: 0; left: 0; bottom: 0;
75
79
  width: var(--sidebar-w);
76
- background: var(--bg-surface);
77
- border-right: 1px solid var(--border);
80
+ background: var(--sidebar-bg);
81
+ border-right: 1px solid var(--sidebar-border);
78
82
  display: flex;
79
83
  flex-direction: column;
80
84
  z-index: 100;
@@ -134,6 +138,11 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
134
138
  font-size: 0.8rem;
135
139
  }
136
140
 
141
+ [data-theme="light"] .sidebar {
142
+ background: var(--sidebar-bg);
143
+ border-right-color: var(--sidebar-border);
144
+ }
145
+
137
146
  /* -- Sidebar Contacts (live status) -- */
138
147
  .sidebar-contacts {
139
148
  border-top: 1px solid var(--border);
@@ -350,7 +359,7 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
350
359
  margin-left: auto;
351
360
  margin-right: auto;
352
361
  padding: 2.5rem 3rem 2.5rem calc(var(--sidebar-w) + 3rem);
353
- max-width: calc(1010px + var(--sidebar-w));
362
+ max-width: calc(1200px + var(--sidebar-w));
354
363
  min-height: 100vh;
355
364
  }
356
365
 
@@ -504,7 +513,7 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
504
513
  @keyframes toast-out { from { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 0; transform: translateY(-10px) scale(0.95); } }
505
514
 
506
515
  /* -- Route Transition -- */
507
- z-outlet { animation: fade-in 0.25s var(--ease-out); }
516
+ z-outlet { display: block; animation: fade-in 0.25s var(--ease-out); }
508
517
  @keyframes fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
509
518
 
510
519
  /* -- Responsive: Mobile -- */
@@ -18,28 +18,28 @@
18
18
  <span class="brand"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--accent)" style="width:18px;height:18px;vertical-align:-3px;margin-right:0.2rem;"><path fill-rule="evenodd" d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" clip-rule="evenodd"/></svg> {{NAME}}</span>
19
19
  </div>
20
20
  <nav class="sidebar-nav">
21
- <a z-link="/" class="nav-link">
21
+ <a z-link="/" class="nav-link" z-active-route="/" z-active-exact>
22
22
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955a1.126 1.126 0 0 1 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg></span> Home
23
23
  </a>
24
- <a z-link="/counter" class="nav-link">
24
+ <a z-link="/counter" class="nav-link" z-active-route="/counter">
25
25
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5"/></svg></span> Counter
26
26
  </a>
27
- <a z-link="/todos" class="nav-link">
27
+ <a z-link="/todos" class="nav-link" z-active-route="/todos">
28
28
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg></span> Todos
29
29
  </a>
30
- <a z-link="/contacts" class="nav-link">
30
+ <a z-link="/contacts" class="nav-link" z-active-route="/contacts">
31
31
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z"/></svg></span> Contacts
32
32
  </a>
33
- <a z-link="/api" class="nav-link">
33
+ <a z-link="/api" class="nav-link" z-active-route="/api">
34
34
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"/></svg></span> API Demo
35
35
  </a>
36
- <a z-link="/playground" class="nav-link">
36
+ <a z-link="/playground" class="nav-link" z-active-route="/playground">
37
37
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"/></svg></span> Playground
38
38
  </a>
39
- <a z-link="/toolkit" class="nav-link">
39
+ <a z-link="/toolkit" class="nav-link" z-active-route="/toolkit">
40
40
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75a4.5 4.5 0 0 1-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 1 1-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 0 1 6.336-4.486l-3.276 3.276a3.004 3.004 0 0 0 2.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852Z"/></svg></span> Toolkit
41
41
  </a>
42
- <a z-link="/about" class="nav-link">
42
+ <a z-link="/about" class="nav-link" z-active-route="/about">
43
43
  <span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"/></svg></span> About
44
44
  </a>
45
45
  </nav>
@@ -25,12 +25,8 @@ const router = $.router({
25
25
  mode: 'history'
26
26
  });
27
27
 
28
- // Highlight the active nav link on every route change
29
- router.onChange((to) => {
30
- $.all('.nav-link').removeClass('active');
31
- $(`.nav-link[z-link="${to.path}"]`).addClass('active');
32
-
33
- // Close mobile menu on navigate
28
+ // Close mobile menu on route change
29
+ router.onChange(() => {
34
30
  closeMobileMenu();
35
31
  });
36
32
 
@@ -70,9 +66,7 @@ $.ready(() => {
70
66
  if (current === 'system') applyTheme('system');
71
67
  });
72
68
 
73
- // Highlight the active link on initial load
74
- const current = window.location.pathname;
75
- $.all(`.nav-link[z-link="${current}"]`).addClass('active');
69
+ // Active nav highlighting is handled by z-active-route in the HTML
76
70
 
77
71
  console.log('⚡ {{NAME}} - powered by zQuery v' + $.version);
78
72
  });
@@ -22,6 +22,8 @@
22
22
  --radius: 8px;
23
23
  --radius-lg: 12px;
24
24
  --sidebar-w: 220px;
25
+ --sidebar-bg: var(--bg-surface);
26
+ --sidebar-border: var(--border);
25
27
  --topbar-h: 56px;
26
28
  --font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
27
29
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
@@ -39,6 +41,8 @@
39
41
  --accent-hover:#0550ae;
40
42
  --accent-soft: rgba(9,105,218,0.08);
41
43
  --accent-glow: rgba(9,105,218,0.04);
44
+ --sidebar-bg: #eef2f9;
45
+ --sidebar-border: rgba(9, 105, 218, 0.12);
42
46
  --success: #1a7f37;
43
47
  --danger: #cf222e;
44
48
  }
@@ -64,8 +68,8 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
64
68
  position: fixed;
65
69
  top: 0; left: 0; bottom: 0;
66
70
  width: var(--sidebar-w);
67
- background: var(--bg-surface);
68
- border-right: 1px solid var(--border);
71
+ background: var(--sidebar-bg);
72
+ border-right: 1px solid var(--sidebar-border);
69
73
  display: flex;
70
74
  flex-direction: column;
71
75
  z-index: 100;
@@ -118,6 +122,11 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
118
122
  font-size: 0.8rem;
119
123
  }
120
124
 
125
+ [data-theme="light"] .sidebar {
126
+ background: var(--sidebar-bg);
127
+ border-right-color: var(--sidebar-border);
128
+ }
129
+
121
130
  /* -- Mobile Top Bar -- */
122
131
  .topbar {
123
132
  display: none;
@@ -181,7 +190,7 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
181
190
  margin-left: auto;
182
191
  margin-right: auto;
183
192
  padding: 2.5rem 3rem 2.5rem calc(var(--sidebar-w) + 3rem);
184
- max-width: calc(1010px + var(--sidebar-w));
193
+ max-width: calc(1200px + var(--sidebar-w));
185
194
  min-height: 100vh;
186
195
  }
187
196
 
@@ -259,7 +268,7 @@ code { background: var(--bg-hover); padding: 2px 6px; border-radius: 4px; font-s
259
268
  .muted { color: var(--text-muted); }
260
269
 
261
270
  /* -- Route Transition -- */
262
- z-outlet { animation: fade-in 0.25s var(--ease-out); }
271
+ z-outlet { display: block; animation: fade-in 0.25s var(--ease-out); }
263
272
  @keyframes fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
264
273
 
265
274
  /* -- Responsive: Mobile -- */
@@ -17,9 +17,9 @@
17
17
  <span class="brand">⚡ {{NAME}}</span>
18
18
  </div>
19
19
  <nav class="sidebar-nav">
20
- <a z-link="/" class="nav-link">Home</a>
21
- <a z-link="/counter" class="nav-link">Counter</a>
22
- <a z-link="/about" class="nav-link">About</a>
20
+ <a z-link="/" class="nav-link" z-active-route="/" z-active-exact>Home</a>
21
+ <a z-link="/counter" class="nav-link" z-active-route="/counter">Counter</a>
22
+ <a z-link="/about" class="nav-link" z-active-route="/about">About</a>
23
23
  </nav>
24
24
  <div class="sidebar-footer">
25
25
  <small>zQuery <span id="nav-version"></span></small>
@@ -2,17 +2,31 @@
2
2
  //
3
3
  // Imports shared component definitions and registers them with zQuery.
4
4
  // The SSR server imports the same definitions via createSSRApp().
5
+ // Server-fetched data is embedded in window.__SSR_DATA__ for hydration.
5
6
 
6
7
  import { homePage } from './components/home.js';
7
8
  import { aboutPage } from './components/about.js';
9
+ import { blogList } from './components/blog/index.js';
10
+ import { blogPost } from './components/blog/post.js';
8
11
  import { notFound } from './components/not-found.js';
9
12
  import { routes } from './routes.js';
10
13
 
11
14
  // Register shared component definitions on the client
12
15
  $.component('home-page', homePage);
13
16
  $.component('about-page', aboutPage);
17
+ $.component('blog-list', blogList);
18
+ $.component('blog-post', blogPost);
14
19
  $.component('not-found', notFound);
15
20
 
21
+ // Route → page title mapping (mirrors server getMetaForRoute)
22
+ const routeTitles = {
23
+ 'home-page': '{{NAME}} — Home',
24
+ 'blog-list': 'Blog — {{NAME}}',
25
+ 'blog-post': null, // dynamic — set by component
26
+ 'about-page': 'About — {{NAME}}',
27
+ 'not-found': 'Page Not Found — {{NAME}}',
28
+ };
29
+
16
30
  // Client-side router
17
31
  const router = $.router({
18
32
  routes,
@@ -20,10 +34,8 @@ const router = $.router({
20
34
  mode: 'history'
21
35
  });
22
36
 
23
- // Active nav highlighting
24
- router.onChange((to) => {
25
- $.qsa('.nav-link').forEach(link => {
26
- const href = link.getAttribute('z-link') || link.getAttribute('href');
27
- link.classList.toggle('active', href === to.path);
28
- });
37
+ // Update document.title on client-side navigations
38
+ router.onChange(({ component }) => {
39
+ const title = routeTitles[component];
40
+ if (title) document.title = title;
29
41
  });
@@ -1,27 +1,54 @@
1
1
  // about.js - About page component
2
2
 
3
3
  export const aboutPage = {
4
- state: () => ({
5
- features: [
6
- 'createSSRApp() - isolated component registry for Node.js',
7
- 'renderToString() - render a component to an HTML string',
8
- 'renderPage() - full HTML document with meta tags',
9
- 'renderBatch() - render multiple components in one call',
10
- 'Hydration markers (data-zq-ssr) for client takeover',
11
- 'SEO: description, canonical URL, Open Graph tags',
12
- ]
13
- }),
14
-
15
4
  render() {
16
- const list = this.state.features.map(f => `<li>${f}</li>`).join('');
17
5
  return `
18
6
  <div class="page-header">
19
7
  <h1>About</h1>
20
- <p class="subtitle">SSR capabilities in this scaffold.</p>
8
+ <p class="subtitle">This app is powered by zQuery — a zero-dependency frontend micro-library.</p>
9
+ </div>
10
+
11
+ <div class="card">
12
+ <h3>What is zQuery?</h3>
13
+ <p style="line-height:1.7;">
14
+ zQuery is a lightweight, zero-dependency JavaScript library for building
15
+ reactive web applications. It provides components, routing, state management,
16
+ server-side rendering, and more — all in a single, tiny package with no
17
+ build step required.
18
+ </p>
21
19
  </div>
20
+
21
+ <div class="card">
22
+ <h3>Core Features</h3>
23
+ <ul style="padding-left:1.2rem; line-height:2;">
24
+ <li><strong>Reactive state</strong> — fine-grained reactivity with <code>$.reactive()</code></li>
25
+ <li><strong>Components</strong> — <code>$.component()</code> with state, lifecycle hooks, and computed properties</li>
26
+ <li><strong>Routing</strong> — <code>$.router()</code> with history mode, params, guards, and <code>z-link</code> navigation</li>
27
+ <li><strong>Store</strong> — centralized state via <code>$.store()</code></li>
28
+ <li><strong>SSR</strong> — <code>createSSRApp()</code>, <code>renderToString()</code>, hydration markers</li>
29
+ <li><strong>HTTP</strong> — <code>$.http</code> with interceptors, timeout, and parallel requests</li>
30
+ <li><strong>Zero dependencies</strong> — nothing to install, audit, or keep up-to-date</li>
31
+ </ul>
32
+ </div>
33
+
34
+ <div class="card">
35
+ <h3>This Scaffold</h3>
36
+ <p style="line-height:1.7;">
37
+ You're running the <strong>SSR scaffold</strong> — a production-ready starter
38
+ with server-side rendering, client hydration, param-based routing, JSON API
39
+ endpoints, and per-route SEO metadata. Edit the components in <code>app/</code>
40
+ and the server in <code>server/</code> to make it your own.
41
+ </p>
42
+ </div>
43
+
22
44
  <div class="card">
23
- <h3>SSR API</h3>
24
- <ul style="padding-left:1.2rem; line-height:2;">${list}</ul>
45
+ <h3>Links</h3>
46
+ <ul style="padding-left:1.2rem; line-height:2.2; list-style:none;">
47
+ <li>📄 <a href="https://z-query.com" target="_blank" rel="noopener">zQuery Website</a></li>
48
+ <li>📦 <a href="https://www.npmjs.com/package/zero-query" target="_blank" rel="noopener">npm — zero-query</a></li>
49
+ <li>🛠️ <a href="https://github.com/tonywied17/zero-query" target="_blank" rel="noopener">GitHub Repository</a></li>
50
+ <li>📖 <a href="https://z-query.com/docs" target="_blank" rel="noopener">Documentation</a></li>
51
+ </ul>
25
52
  </div>
26
53
  `;
27
54
  }
@@ -0,0 +1,65 @@
1
+ // blog/index.js - Blog listing component
2
+ //
3
+ // Renders a grid of blog post cards. Data flow:
4
+ // SSR: Server passes { posts } as props → render() reads this.props.posts
5
+ // Client: init() checks window.__SSR_DATA__ first (hydration), then fetches
6
+ // from /api/posts for subsequent navigations.
7
+ //
8
+ // Demonstrates:
9
+ // - Shared component that works on both server and client
10
+ // - Server data injection via props (SSR) and fetch (client)
11
+ // - z-link with dynamic URLs for client-side navigation
12
+ // - Clean SSR-friendly templates (no DOM API in render)
13
+
14
+ export const blogList = {
15
+ state: () => ({
16
+ posts: [],
17
+ loaded: false,
18
+ }),
19
+
20
+ async init() {
21
+ // On the server, props.posts is injected by app.renderToString()
22
+ if (this.props.posts) {
23
+ this.state.posts = this.props.posts;
24
+ this.state.loaded = true;
25
+ return;
26
+ }
27
+
28
+ // On the client, check for SSR-embedded data first (initial page load)
29
+ const ssrData = typeof window !== 'undefined' && window.__SSR_DATA__;
30
+ if (ssrData && ssrData.component === 'blog-list') {
31
+ this.state.posts = ssrData.props.posts;
32
+ this.state.loaded = true;
33
+ window.__SSR_DATA__ = null;
34
+ return;
35
+ }
36
+
37
+ // Client navigation — fetch from server API
38
+ const res = await fetch('/api/posts');
39
+ this.state.posts = await res.json();
40
+ this.state.loaded = true;
41
+ },
42
+
43
+ render() {
44
+ const posts = this.state.posts;
45
+
46
+ const cards = posts.map(post => `
47
+ <a z-link="/blog/${post.slug}" class="blog-card">
48
+ <div class="blog-card-header">
49
+ <span class="badge badge-ssr">${post.tag}</span>
50
+ <time class="blog-date">${post.date}</time>
51
+ </div>
52
+ <h3 class="blog-title">${post.title}</h3>
53
+ <p class="blog-summary">${post.summary}</p>
54
+ </a>
55
+ `).join('');
56
+
57
+ return `
58
+ <div class="page-header">
59
+ <h1>Blog</h1>
60
+ <p class="subtitle">Server-rendered articles — fast first paint, fully crawlable.</p>
61
+ </div>
62
+ <div class="blog-grid">${cards}</div>
63
+ `;
64
+ }
65
+ };
@@ -0,0 +1,78 @@
1
+ // blog/post.js - Single blog post detail component
2
+ //
3
+ // Renders the full content of a blog post. Data flow:
4
+ // SSR: Server passes { post } as props → init() reads this.props.post
5
+ // Client: init() checks window.__SSR_DATA__ first, then fetches from
6
+ // /api/posts/:slug for client-side navigations.
7
+ //
8
+ // Route: /blog/:slug
9
+ //
10
+ // Demonstrates:
11
+ // - Param routing with this.props.$params.slug (or this.props.slug)
12
+ // - Server data injection via props (SSR) and fetch (client)
13
+ // - z-link for back-navigation without full page reload
14
+ // - Graceful handling of missing data (404-style fallback)
15
+
16
+ export const blogPost = {
17
+ state: () => ({
18
+ post: null,
19
+ loaded: false,
20
+ }),
21
+
22
+ async init() {
23
+ const slug = this.props.slug || (this.props.$params && this.props.$params.slug);
24
+
25
+ // On the server, props.post is injected by app.renderToString()
26
+ if (this.props.post) {
27
+ this.state.post = this.props.post;
28
+ this.state.loaded = true;
29
+ return;
30
+ }
31
+
32
+ // On the client, check for SSR-embedded data first (initial page load)
33
+ const ssrData = typeof window !== 'undefined' && window.__SSR_DATA__;
34
+ if (ssrData && ssrData.component === 'blog-post' && ssrData.params.slug === slug) {
35
+ this.state.post = ssrData.props.post;
36
+ this.state.loaded = true;
37
+ window.__SSR_DATA__ = null;
38
+ return;
39
+ }
40
+
41
+ // Client navigation — fetch from server API
42
+ const res = await fetch(`/api/posts/${slug}`);
43
+ if (res.ok) {
44
+ this.state.post = await res.json();
45
+ }
46
+ this.state.loaded = true;
47
+ },
48
+
49
+ render() {
50
+ const post = this.state.post;
51
+
52
+ if (!post) {
53
+ return `
54
+ <div class="page-header center">
55
+ <h1>Post Not Found</h1>
56
+ <p class="subtitle">The article you're looking for doesn't exist.</p>
57
+ <p style="margin-top:1rem;">
58
+ <a z-link="/blog" class="back-link">← Back to Blog</a>
59
+ </p>
60
+ </div>
61
+ `;
62
+ }
63
+
64
+ return `
65
+ <article class="blog-post">
66
+ <header class="page-header">
67
+ <a z-link="/blog" class="back-link">← Back to Blog</a>
68
+ <h1>${post.title}</h1>
69
+ <div class="blog-post-meta">
70
+ <span class="badge badge-ssr">${post.tag}</span>
71
+ <time class="blog-date">${post.date}</time>
72
+ </div>
73
+ </header>
74
+ <div class="blog-post-body">${post.body}</div>
75
+ </article>
76
+ `;
77
+ }
78
+ };