zero-query 1.0.1 → 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 +47 -6
- package/cli/help.js +2 -1
- package/cli/scaffold/default/app/app.js +3 -6
- package/cli/scaffold/default/global.css +12 -3
- package/cli/scaffold/default/index.html +8 -8
- package/cli/scaffold/minimal/app/app.js +3 -9
- package/cli/scaffold/minimal/global.css +12 -3
- package/cli/scaffold/minimal/index.html +3 -3
- package/cli/scaffold/ssr/app/app.js +18 -6
- package/cli/scaffold/ssr/app/components/about.js +42 -15
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -0
- package/cli/scaffold/ssr/app/components/blog/post.js +78 -0
- package/cli/scaffold/ssr/app/routes.js +4 -2
- package/cli/scaffold/ssr/global.css +117 -1
- package/cli/scaffold/ssr/index.html +8 -2
- package/cli/scaffold/ssr/server/data/posts.js +144 -0
- package/cli/scaffold/ssr/server/index.js +151 -11
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +21 -6
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/component.js +17 -2
- package/tests/component.test.js +480 -0
- package/tests/test-minifier.js +4 -4
- package/types/misc.d.ts +23 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
| Module | Highlights |
|
|
23
23
|
| --- | --- |
|
|
24
|
-
| **Components** | Reactive state, template literals, `@event` delegation (
|
|
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` — 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 |
|
|
@@ -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/
|
|
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/`):
|
|
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
|
|
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)
|
|
@@ -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
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
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(
|
|
362
|
+
max-width: calc(1200px + var(--sidebar-w));
|
|
354
363
|
min-height: 100vh;
|
|
355
364
|
}
|
|
356
365
|
|
|
@@ -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
|
-
//
|
|
29
|
-
router.onChange((
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
193
|
+
max-width: calc(1200px + var(--sidebar-w));
|
|
185
194
|
min-height: 100vh;
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -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
|
-
//
|
|
24
|
-
router.onChange((
|
|
25
|
-
|
|
26
|
-
|
|
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">
|
|
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>
|
|
24
|
-
<ul style="padding-left:1.2rem; line-height:2;"
|
|
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
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// routes.js - Route definitions (shared between client and server)
|
|
2
2
|
|
|
3
3
|
export const routes = [
|
|
4
|
-
{ path: '/',
|
|
5
|
-
{ path: '/
|
|
4
|
+
{ path: '/', component: 'home-page' },
|
|
5
|
+
{ path: '/blog', component: 'blog-list' },
|
|
6
|
+
{ path: '/blog/:slug', component: 'blog-post' },
|
|
7
|
+
{ path: '/about', component: 'about-page' },
|
|
6
8
|
];
|