zero-query 0.9.7 → 0.9.9

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.
Files changed (72) hide show
  1. package/README.md +55 -4
  2. package/cli/commands/build.js +50 -3
  3. package/cli/commands/create.js +58 -11
  4. package/cli/help.js +4 -0
  5. package/cli/scaffold/default/app/app.js +211 -0
  6. package/cli/scaffold/default/app/components/about.js +201 -0
  7. package/cli/scaffold/default/app/components/api-demo.js +143 -0
  8. package/cli/scaffold/default/app/components/contact-card.js +231 -0
  9. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -0
  10. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -0
  11. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -0
  12. package/cli/scaffold/default/app/components/counter.js +127 -0
  13. package/cli/scaffold/default/app/components/home.js +249 -0
  14. package/cli/scaffold/{app → default/app}/components/not-found.js +2 -2
  15. package/cli/scaffold/default/app/components/playground/playground.css +116 -0
  16. package/cli/scaffold/default/app/components/playground/playground.html +162 -0
  17. package/cli/scaffold/default/app/components/playground/playground.js +117 -0
  18. package/cli/scaffold/default/app/components/todos.js +225 -0
  19. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -0
  20. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -0
  21. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -0
  22. package/cli/scaffold/default/app/routes.js +15 -0
  23. package/cli/scaffold/{app → default/app}/store.js +15 -10
  24. package/cli/scaffold/{global.css → default/global.css} +238 -252
  25. package/cli/scaffold/{index.html → default/index.html} +35 -0
  26. package/cli/scaffold/{app → minimal/app}/app.js +37 -39
  27. package/cli/scaffold/minimal/app/components/about.js +68 -0
  28. package/cli/scaffold/minimal/app/components/counter.js +122 -0
  29. package/cli/scaffold/minimal/app/components/home.js +68 -0
  30. package/cli/scaffold/minimal/app/components/not-found.js +16 -0
  31. package/cli/scaffold/minimal/app/routes.js +9 -0
  32. package/cli/scaffold/minimal/app/store.js +36 -0
  33. package/cli/scaffold/minimal/assets/.gitkeep +0 -0
  34. package/cli/scaffold/minimal/global.css +291 -0
  35. package/cli/scaffold/minimal/index.html +44 -0
  36. package/cli/scaffold/ssr/app/app.js +30 -0
  37. package/cli/scaffold/ssr/app/components/about.js +28 -0
  38. package/cli/scaffold/ssr/app/components/home.js +37 -0
  39. package/cli/scaffold/ssr/app/components/not-found.js +15 -0
  40. package/cli/scaffold/ssr/app/routes.js +6 -0
  41. package/cli/scaffold/ssr/global.css +113 -0
  42. package/cli/scaffold/ssr/index.html +31 -0
  43. package/cli/scaffold/ssr/package.json +8 -0
  44. package/cli/scaffold/ssr/server/index.js +118 -0
  45. package/dist/zquery.dist.zip +0 -0
  46. package/dist/zquery.js +2006 -1933
  47. package/dist/zquery.min.js +2 -2
  48. package/index.d.ts +20 -1
  49. package/index.js +8 -5
  50. package/package.json +8 -2
  51. package/src/component.js +6 -3
  52. package/src/diff.js +15 -2
  53. package/src/errors.js +59 -5
  54. package/src/package.json +1 -0
  55. package/src/ssr.js +116 -22
  56. package/tests/cli.test.js +304 -0
  57. package/tests/errors.test.js +423 -145
  58. package/tests/ssr.test.js +435 -3
  59. package/types/errors.d.ts +34 -2
  60. package/types/ssr.d.ts +21 -1
  61. package/cli/scaffold/app/components/about.js +0 -131
  62. package/cli/scaffold/app/components/api-demo.js +0 -103
  63. package/cli/scaffold/app/components/contacts/contacts.css +0 -246
  64. package/cli/scaffold/app/components/contacts/contacts.html +0 -140
  65. package/cli/scaffold/app/components/contacts/contacts.js +0 -153
  66. package/cli/scaffold/app/components/counter.js +0 -85
  67. package/cli/scaffold/app/components/home.js +0 -137
  68. package/cli/scaffold/app/components/todos.js +0 -131
  69. package/cli/scaffold/app/routes.js +0 -13
  70. /package/cli/scaffold/{LICENSE → default/LICENSE} +0 -0
  71. /package/cli/scaffold/{assets → default/assets}/.gitkeep +0 -0
  72. /package/cli/scaffold/{favicon.ico → default/favicon.ico} +0 -0
package/README.md CHANGED
@@ -30,6 +30,7 @@
30
30
  | **HTTP** | Fetch wrapper with auto-JSON, interceptors (with unsubscribe & clear), HEAD requests, parallel requests (`http.all`), config inspection (`getConfig`), timeout/abort, base URL |
31
31
  | **Utils** | debounce, throttle, pipe, once, sleep, memoize, escapeHtml, stripHtml, uuid, capitalize, truncate, range, chunk, groupBy, unique, pick, omit, getPath/setPath, isEmpty, clamp, retry, timeout, deepClone, deepMerge, storage/session wrappers, event bus |
32
32
  | **Dev Tools** | CLI dev server with live-reload, CSS hot-swap, full-screen error overlay, floating toolbar, dark-themed inspector panel (Router view, DOM tree, network log, component viewer, performance dashboard), fetch interceptor, render instrumentation, CLI bundler for single-file production builds |
33
+ | **SSR** | Server-side rendering to HTML strings in Node.js — `createSSRApp()`, `renderToString()`, `renderPage()` with SEO/Open Graph support, `renderBatch()` for parallel rendering, fragment mode, hydration markers, graceful error handling, `escapeHtml()` utility |
33
34
 
34
35
  ---
35
36
 
@@ -53,7 +54,7 @@ npx zquery dev my-app
53
54
 
54
55
  > **Tip:** Stay in the project root (where `node_modules` lives) instead of `cd`-ing into `my-app`. This keeps `index.d.ts` accessible to your IDE for full type/intellisense support.
55
56
 
56
- The `create` command generates a ready-to-run project with a sidebar layout, router, multiple components (including a folder component with external template and styles), and responsive styles. The dev server watches for file changes, hot-swaps CSS in-place, full-reloads on other changes, and handles SPA fallback routing.
57
+ The `create` command generates a ready-to-run project with a sidebar layout, router, multiple components (including folder components with external templates and styles), and responsive styles. Use `--minimal` (or `-m`) to scaffold a lightweight 3-page starter instead. Use `--ssr` (or `-s`) to scaffold a project with a Node.js server-side rendering example. The dev server watches for file changes, hot-swaps CSS in-place, full-reloads on other changes, and handles SPA fallback routing.
57
58
 
58
59
  #### Error Overlay
59
60
 
@@ -136,7 +137,7 @@ That's it — a fully working SPA with the dev server's live-reload.
136
137
  ## Recommended Project Structure
137
138
 
138
139
  ```
139
- my-app/
140
+ my-app/ ← default scaffold (npx zquery create my-app)
140
141
  index.html
141
142
  global.css
142
143
  app/
@@ -150,15 +151,62 @@ my-app/
150
151
  api-demo.js
151
152
  about.js
152
153
  not-found.js
154
+ contact-card.js
153
155
  contacts/ ← folder component (templateUrl + styleUrl)
154
156
  contacts.js
155
157
  contacts.html
156
158
  contacts.css
159
+ playground/ ← folder component
160
+ playground.js
161
+ playground.html
162
+ playground.css
163
+ toolkit/ ← folder component
164
+ toolkit.js
165
+ toolkit.html
166
+ toolkit.css
157
167
  assets/
158
168
  scripts/ ← third-party JS (e.g. zquery.min.js for manual setup)
159
169
  styles/ ← additional stylesheets, fonts, etc.
160
170
  ```
161
171
 
172
+ Use `--minimal` for a lighter starting point (3 pages + 404 fallback):
173
+
174
+ ```
175
+ my-app/ ← minimal scaffold (npx zquery create my-app --minimal)
176
+ index.html
177
+ global.css
178
+ app/
179
+ app.js
180
+ routes.js
181
+ store.js
182
+ components/
183
+ home.js
184
+ counter.js
185
+ about.js
186
+ not-found.js ← 404 fallback
187
+ assets/
188
+ ```
189
+
190
+ Use `--ssr` for a project with server-side rendering:
191
+
192
+ ```
193
+ my-app/ ← SSR scaffold (npx zquery create my-app --ssr)
194
+ index.html ← client HTML shell
195
+ global.css
196
+ app/
197
+ app.js ← client entry — registers shared components
198
+ routes.js ← shared route definitions
199
+ components/
200
+ home.js ← shared component (SSR + client)
201
+ about.js
202
+ not-found.js
203
+ server/
204
+ index.js ← SSR HTTP server
205
+ assets/
206
+ ```
207
+
208
+ 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.
209
+
162
210
  - One component per file inside `components/`.
163
211
  - Names **must contain a hyphen** (Web Component convention): `home-page`, `app-counter`, etc.
164
212
  - Components with external templates or styles can use a subfolder (e.g. `contacts/contacts.js` + `contacts.html` + `contacts.css`).
@@ -271,13 +319,16 @@ location / {
271
319
  | `$.retry` `$.timeout` | Async utils |
272
320
  | `$.param` `$.parseQuery` | URL utils |
273
321
  | `$.storage` `$.session` | Storage wrappers |
274
- | `$.EventBus` `$.bus` | Event bus || `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.validate` | Error handling || `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~100 KB\"`) |
322
+ | `$.EventBus` `$.bus` | Event bus |
323
+ | `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.guardAsync` `$.formatError` `$.validate` | Error handling |
324
+ | `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~100 KB\"`) |
325
+ | `$.unitTests` | Build-time test results `{ passed, failed, total, suites, duration, ok }` |
275
326
  | `$.meta` | Build metadata (populated by CLI bundler) |
276
327
  | `$.noConflict` | Release `$` global |
277
328
 
278
329
  | CLI Command | Description |
279
330
  | --- | --- |
280
- | `zquery create [dir]` | Scaffold a new project (index.html, components, store, styles) |
331
+ | `zquery create [dir]` | Scaffold a new project. Default: full-featured app. `--minimal` / `-m`: lightweight 3-page starter. `--ssr` / `-s`: SSR project with shared components and HTTP server. |
281
332
  | `zquery dev [root]` | Dev server with live-reload, CSS hot-swap, error overlay, expandable floating toolbar & five-tab inspector panel (port 3100). Visit `/_devtools` for the standalone panel. `--index` for custom HTML, `--bundle` for bundled mode, `--no-intercept` to skip CDN intercept. |
282
333
  | `zquery bundle [dir\|file]` | Bundle app into a single IIFE file. Accepts dir or direct entry file. |
283
334
  | `zquery build` | Build the zQuery library (`dist/zquery.min.js`) |
@@ -10,6 +10,7 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const zlib = require('zlib');
13
+ const { execSync } = require('child_process');
13
14
  const { minify, sizeKB } = require('../utils');
14
15
 
15
16
  function buildLibrary() {
@@ -18,7 +19,7 @@ function buildLibrary() {
18
19
 
19
20
  const modules = [
20
21
  'src/errors.js',
21
- 'src/reactive.js', 'src/core.js', 'src/expression.js', 'src/diff.js',
22
+ 'src/reactive.js', 'src/diff.js', 'src/core.js', 'src/expression.js',
22
23
  'src/component.js', 'src/router.js', 'src/store.js', 'src/http.js',
23
24
  'src/utils.js',
24
25
  ];
@@ -30,6 +31,49 @@ function buildLibrary() {
30
31
  const start = Date.now();
31
32
  if (!fs.existsSync(DIST)) fs.mkdirSync(DIST, { recursive: true });
32
33
 
34
+ // -----------------------------------------------------------------------
35
+ // Run unit tests and capture results for $.unitTests
36
+ // -----------------------------------------------------------------------
37
+ let testResults = { passed: 0, failed: 0, total: 0, suites: 0, duration: 0, ok: false };
38
+ try {
39
+ const json = execSync('npx vitest run --reporter=json', {
40
+ cwd: process.cwd(),
41
+ encoding: 'utf-8',
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ timeout: 120000,
44
+ });
45
+ // vitest --reporter=json outputs JSON to stdout; may include non-JSON lines
46
+ const jsonStart = json.indexOf('{');
47
+ if (jsonStart !== -1) {
48
+ const parsed = JSON.parse(json.slice(jsonStart));
49
+ const passed = parsed.numPassedTests || 0;
50
+ const failed = parsed.numFailedTests || 0;
51
+ const total = parsed.numTotalTests || 0;
52
+ const suites = parsed.numTotalTestSuites || 0;
53
+ const dur = Math.round((parsed.testResults || []).reduce((s, r) => s + (r.endTime - r.startTime), 0));
54
+ testResults = { passed, failed, total, suites, duration: dur, ok: parsed.success !== false };
55
+ }
56
+ console.log(` ✓ Tests: ${testResults.passed}/${testResults.total} passed (${testResults.suites} suites)\n`);
57
+ } catch (err) {
58
+ // Tests may fail but we still want to capture the numbers
59
+ const out = (err.stdout || '') + (err.stderr || '');
60
+ const jsonStart = out.indexOf('{');
61
+ if (jsonStart !== -1) {
62
+ try {
63
+ const parsed = JSON.parse(out.slice(jsonStart));
64
+ testResults = {
65
+ passed: parsed.numPassedTests || 0,
66
+ failed: parsed.numFailedTests || 0,
67
+ total: parsed.numTotalTests || 0,
68
+ suites: parsed.numTotalTestSuites || 0,
69
+ duration: Math.round((parsed.testResults || []).reduce((s, r) => s + (r.endTime - r.startTime), 0)),
70
+ ok: false,
71
+ };
72
+ } catch (_) { /* keep defaults */ }
73
+ }
74
+ console.log(` ⚠ Tests: ${testResults.passed}/${testResults.total} passed, ${testResults.failed} failed\n`);
75
+ }
76
+
33
77
  const parts = modules.map(file => {
34
78
  let code = fs.readFileSync(path.join(process.cwd(), file), 'utf-8');
35
79
  code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
@@ -54,8 +98,11 @@ function buildLibrary() {
54
98
  // Inject actual minified library size into both outputs
55
99
  const libSizeKB = Math.round(Buffer.from(minified).length / 1024);
56
100
  const libSizeStr = `~${libSizeKB} KB`;
57
- const outContent = fs.readFileSync(OUT_FILE, 'utf-8').replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
58
- const minContent = minified.replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
101
+ const testObj = JSON.stringify(testResults);
102
+ let outContent = fs.readFileSync(OUT_FILE, 'utf-8').replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
103
+ let minContent = minified.replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
104
+ outContent = outContent.replace("'__UNIT_TESTS__'", testObj);
105
+ minContent = minContent.replace("'__UNIT_TESTS__'", testObj);
59
106
  fs.writeFileSync(OUT_FILE, outContent, 'utf-8');
60
107
  fs.writeFileSync(MIN_FILE, minContent, 'utf-8');
61
108
 
@@ -1,9 +1,13 @@
1
1
  // cli/commands/create.js — scaffold a new zQuery project
2
- // Reads template files from cli/scaffold/, replaces {{NAME}} with the project
3
- // name, and writes them into the target directory.
2
+ //
3
+ // Templates live in cli/scaffold/<variant>/ (default or minimal).
4
+ // Reads template files, replaces {{NAME}} with the project name,
5
+ // and writes them into the target directory.
4
6
 
5
7
  const fs = require('fs');
6
8
  const path = require('path');
9
+ const { execSync, spawn } = require('child_process');
10
+ const { flag } = require('../args');
7
11
 
8
12
  /**
9
13
  * Recursively collect every file under `dir`, returning paths relative to `dir`.
@@ -22,24 +26,38 @@ function walkDir(dir, prefix = '') {
22
26
  }
23
27
 
24
28
  function createProject(args) {
25
- const target = args[1] ? path.resolve(args[1]) : process.cwd();
29
+ // First positional arg after "create" is the target dir (skip flags)
30
+ const dirArg = args.slice(1).find(a => !a.startsWith('-'));
31
+ const target = dirArg ? path.resolve(dirArg) : process.cwd();
26
32
  const name = path.basename(target);
27
33
 
34
+ // Determine scaffold variant: --minimal / -m or --ssr / -s or default
35
+ const variant = flag('minimal', 'm') ? 'minimal'
36
+ : flag('ssr', 's') ? 'ssr'
37
+ : 'default';
38
+
28
39
  // Guard: refuse to overwrite existing files
29
- const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
40
+ const checkFiles = ['index.html', 'global.css', 'app', 'assets'];
41
+ if (variant === 'ssr') checkFiles.push('server');
42
+ const conflicts = checkFiles.filter(f =>
30
43
  fs.existsSync(path.join(target, f))
31
44
  );
32
45
  if (conflicts.length) {
33
- console.error(`\n \u2717 Directory already contains: ${conflicts.join(', ')}`);
46
+ console.error(`\n Directory already contains: ${conflicts.join(', ')}`);
34
47
  console.error(` Aborting to avoid overwriting existing files.\n`);
35
48
  process.exit(1);
36
49
  }
37
50
 
38
- console.log(`\n zQuery \u2014 Create Project\n`);
51
+ console.log(`\n zQuery Create Project (${variant})\n`);
39
52
  console.log(` Scaffolding into ${target}\n`);
40
53
 
41
- // Resolve the scaffold template directory
42
- const scaffoldDir = path.resolve(__dirname, '..', 'scaffold');
54
+ // Resolve the scaffold template directory for the chosen variant
55
+ const scaffoldDir = path.resolve(__dirname, '..', 'scaffold', variant);
56
+
57
+ if (!fs.existsSync(scaffoldDir)) {
58
+ console.error(`\n ✗ Scaffold variant "${variant}" not found.\n`);
59
+ process.exit(1);
60
+ }
43
61
 
44
62
  // Walk the scaffold directory and copy each file
45
63
  const templateFiles = walkDir(scaffoldDir);
@@ -54,14 +72,43 @@ function createProject(args) {
54
72
  const dest = path.join(target, rel);
55
73
  fs.mkdirSync(path.dirname(dest), { recursive: true });
56
74
  fs.writeFileSync(dest, content, 'utf-8');
57
- console.log(` \u2713 ${rel}`);
75
+ console.log(` ${rel}`);
58
76
  }
59
77
 
60
- console.log(`
78
+ const devCmd = `npx zquery dev${target !== process.cwd() ? ` ${dirArg}` : ''}`;
79
+
80
+ if (variant === 'ssr') {
81
+ console.log(`\n Installing dependencies...\n`);
82
+ // Install zero-query from the same package that provides this CLI
83
+ // (works both in local dev and when installed from npm)
84
+ const zqRoot = path.resolve(__dirname, '..', '..');
85
+ try {
86
+ execSync(`npm install "${zqRoot}"`, { cwd: target, stdio: 'inherit' });
87
+ } catch {
88
+ console.error(`\n ✗ npm install failed. Run it manually:\n\n cd ${dirArg || '.'}\n npm install\n node server/index.js\n`);
89
+ process.exit(1);
90
+ }
91
+
92
+ console.log(`\n Starting SSR server...\n`);
93
+ const child = spawn('node', ['server/index.js'], { cwd: target, stdio: 'inherit' });
94
+
95
+ setTimeout(() => {
96
+ const open = process.platform === 'win32' ? 'start'
97
+ : process.platform === 'darwin' ? 'open' : 'xdg-open';
98
+ try { execSync(`${open} http://localhost:3000`, { stdio: 'ignore' }); } catch {}
99
+ }, 500);
100
+
101
+ process.on('SIGINT', () => { child.kill(); process.exit(); });
102
+ process.on('SIGTERM', () => { child.kill(); process.exit(); });
103
+ child.on('exit', (code) => process.exit(code || 0));
104
+ return; // keep process alive for the server
105
+ } else {
106
+ console.log(`
61
107
  Done! Next steps:
62
108
 
63
- npx zquery dev${target !== process.cwd() ? ` ${args[1]}` : ''}
109
+ ${devCmd}
64
110
  `);
111
+ }
65
112
  }
66
113
 
67
114
  module.exports = createProject;
package/cli/help.js CHANGED
@@ -9,6 +9,10 @@ function showHelp() {
9
9
  create [dir] Scaffold a new zQuery project
10
10
  Creates index.html, global.css, app/, assets/ in the target directory
11
11
  (defaults to the current directory)
12
+ --minimal, -m Use the minimal template (home, counter, about)
13
+ instead of the full-featured default scaffold
14
+ --ssr, -s Use the SSR template — includes server/index.js
15
+ SSR HTTP server with shared component definitions
12
16
 
13
17
  dev [root] Start a dev server with live-reload
14
18
  --port, -p <number> Port number (default: 3100)
@@ -0,0 +1,211 @@
1
+ // app.js — Application entry point
2
+ //
3
+ // Bootstraps the app: imports components, sets up routing,
4
+ // wires the responsive sidebar, and connects the store.
5
+ //
6
+ // Key APIs used:
7
+ // $.router — SPA navigation (history mode)
8
+ // $.ready — run after DOM is loaded
9
+ // $.bus — event bus (toast notifications, contact card)
10
+ // $.on — global delegated event listeners
11
+ // $.storage — localStorage wrapper
12
+ // $.create — create DOM elements
13
+
14
+ import './store.js';
15
+ import './components/home.js';
16
+ import './components/counter.js';
17
+ import './components/todos.js';
18
+ import './components/api-demo.js';
19
+ import './components/playground/playground.js';
20
+ import './components/toolkit/toolkit.js';
21
+ import './components/about.js';
22
+ import './components/contact-card.js';
23
+ import './components/contacts/contacts.js';
24
+ import './components/not-found.js';
25
+ import { routes } from './routes.js';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Router — SPA navigation with history mode
29
+ // ---------------------------------------------------------------------------
30
+ const router = $.router({
31
+ el: '#app', //@ Mount point (Set in index.html)
32
+ routes,
33
+ fallback: 'not-found',
34
+ mode: 'history'
35
+ });
36
+
37
+ // Post-navigation hook — track page views on every navigation
38
+ router.afterEach((to) => {
39
+ const store = $.getStore('main');
40
+ if (store) store.dispatch('incrementVisits');
41
+ console.log('📊 Navigated to:', to.path);
42
+ });
43
+
44
+ // Highlight the active nav link on every route change
45
+ router.onChange((to) => {
46
+ $.all('.nav-link').removeClass('active');
47
+ $(`.nav-link[z-link="${to.path}"]`).addClass('active');
48
+
49
+ // Close mobile menu on navigate
50
+ closeMobileMenu();
51
+ });
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Responsive sidebar toggle
55
+ // ---------------------------------------------------------------------------
56
+ const $sidebar = $('#sidebar');
57
+ const $overlay = $('#overlay');
58
+ const $toggle = $('#menu-toggle');
59
+
60
+ function toggleMobileMenu(open) {
61
+ $sidebar.toggleClass('open', open);
62
+ $overlay.toggleClass('visible', open);
63
+ $toggle.toggleClass('active', open);
64
+ }
65
+
66
+ function closeMobileMenu() { toggleMobileMenu(false); }
67
+
68
+ // $.on — global delegated event listeners
69
+ $.on('click', '#menu-toggle', () => toggleMobileMenu(!$sidebar.hasClass('open')));
70
+ $.on('click', '#overlay', closeMobileMenu);
71
+
72
+ // Close sidebar on Escape key — using $.on direct (no selector needed)
73
+ $.on('keydown', (e) => {
74
+ if (e.key === 'Escape') closeMobileMenu();
75
+ });
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Sidebar stats panel — collapsible, live-updating from $.store
79
+ // Starts expanded by default. Open/closed state saved via $.storage.
80
+ // ---------------------------------------------------------------------------
81
+ $.on('click', '#stats-toggle', () => {
82
+ const $body = $('#stats-body');
83
+ const $arrow = $('#stats-arrow');
84
+ if (!$body.length) return;
85
+ const open = $body.css('display') !== 'none';
86
+ open ? $body.hide() : $body.show();
87
+ $arrow.toggleClass('open', !open);
88
+ $.storage.set('statsOpen', !open);
89
+ });
90
+
91
+ function updateSidebarStats() {
92
+ const store = $.getStore('main');
93
+ if (!store) return;
94
+ $('#ss-visits').text(store.state.visits);
95
+ $('#ss-todos').text(store.getters.todoCount);
96
+ $('#ss-pending').text(store.getters.pendingCount);
97
+ $('#ss-done').text(store.getters.doneCount);
98
+ $('#ss-contacts').text(store.getters.contactCount);
99
+ $('#ss-favorites').text(store.getters.favoriteCount);
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Sidebar contacts — live status indicators from $.store
104
+ // ---------------------------------------------------------------------------
105
+ function updateSidebarContacts() {
106
+ const store = $.getStore('main');
107
+ if (!store) return;
108
+ const $list = $('#sc-list');
109
+ if (!$list.length) return;
110
+
111
+ const sorted = [...store.state.contacts].sort((a, b) => {
112
+ const order = { online: 0, away: 1, offline: 2 };
113
+ return (order[a.status] ?? 3) - (order[b.status] ?? 3);
114
+ });
115
+
116
+ const html = sorted.map(c => {
117
+ const hue = (c.name.charCodeAt(0) * 7) % 360;
118
+ const initial = $.escapeHtml(c.name.charAt(0).toUpperCase());
119
+ const name = $.escapeHtml(c.name);
120
+ return `<div class="sc-item" data-contact-id="${c.id}">
121
+ <span class="sc-avatar" style="background:hsl(${hue},55%,42%)">${initial}</span>
122
+ <span class="sc-dot sc-dot-${c.status}"></span>
123
+ <span class="sc-name">${name}</span>
124
+ </div>`;
125
+ }).join('');
126
+
127
+ $list.html(html);
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Toast notification system via $.bus (event bus)
132
+ // ---------------------------------------------------------------------------
133
+ // Any component can emit: $.bus.emit('toast', { message, type })
134
+ // Types: 'success', 'error', 'info'
135
+ $.bus.on('toast', ({ message, type = 'info' }) => {
136
+ const toast = $.create('div')
137
+ .addClass('toast', `toast-${type}`)
138
+ .text(message)
139
+ .appendTo('#toasts');
140
+ // Auto-remove after 3 seconds
141
+ setTimeout(() => {
142
+ toast.addClass('toast-exit');
143
+ setTimeout(() => toast.remove(), 300);
144
+ }, 3000);
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // On DOM ready — final setup
149
+ // ---------------------------------------------------------------------------
150
+ $.ready(() => {
151
+ // Display version in the sidebar footer
152
+ $('#nav-version').text('v' + $.version);
153
+
154
+ // Theme: restore saved preference or auto-detect from system
155
+ const saved = $.storage.get('theme'); // 'dark' | 'light' | 'system' | null
156
+ const preference = saved || 'system';
157
+ applyTheme(preference);
158
+
159
+ // Listen for OS color-scheme changes (only applies when preference is 'system')
160
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
161
+ const current = $.storage.get('theme') || 'system';
162
+ if (current === 'system') applyTheme('system');
163
+ });
164
+
165
+ // Set active link on initial load
166
+ const current = window.location.pathname;
167
+ $.all(`.nav-link[z-link="${current}"]`).addClass('active');
168
+
169
+ // Stats panel: restore open/closed from $.storage (defaults to open)
170
+ const statsOpen = $.storage.get('statsOpen');
171
+ if (statsOpen === false) {
172
+ $('#stats-body').hide();
173
+ $('#stats-arrow').removeClass('open');
174
+ }
175
+
176
+ // Sidebar contact click — open the global contact card overlay
177
+ $('#sc-list').on('click', '.sc-item', (e) => {
178
+ const item = e.target.closest('.sc-item');
179
+ if (!item) return;
180
+ const id = Number(item.dataset.contactId);
181
+ if (id) $.bus.emit('openContact', id);
182
+ });
183
+
184
+ // Initial sidebar stats + contacts + live subscription
185
+ updateSidebarStats();
186
+ updateSidebarContacts();
187
+ const store = $.getStore('main');
188
+ if (store) {
189
+ store.subscribe(updateSidebarStats);
190
+ store.subscribe(updateSidebarContacts);
191
+ }
192
+
193
+ // Mount any components outside the router outlet (e.g. <contact-card>)
194
+ $.mountAll();
195
+
196
+ console.log('⚡ {{NAME}} — powered by zQuery v' + $.version);
197
+ });
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Theme helper — resolves 'system' to actual dark/light and applies it
201
+ // ---------------------------------------------------------------------------
202
+ function applyTheme(preference) {
203
+ let resolved = preference;
204
+ if (preference === 'system') {
205
+ resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
206
+ }
207
+ $('html').attr('data-theme', resolved);
208
+ }
209
+
210
+ // Expose globally so components can call it
211
+ window.__applyTheme = applyTheme;