zero-query 0.4.9 → 0.6.3

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.
@@ -321,10 +321,13 @@ function devServer() {
321
321
 
322
322
  const { createApp, static: serveStatic } = zeroHttp;
323
323
 
324
+ // Custom HTML entry file (default: index.html)
325
+ const htmlEntry = option('index', 'i', 'index.html');
326
+
324
327
  // Determine the project root to serve
325
328
  let root = null;
326
329
  for (let i = 1; i < args.length; i++) {
327
- if (!args[i].startsWith('-') && args[i - 1] !== '-p' && args[i - 1] !== '--port') {
330
+ if (!args[i].startsWith('-') && args[i - 1] !== '-p' && args[i - 1] !== '--port' && args[i - 1] !== '--index') {
328
331
  root = path.resolve(process.cwd(), args[i]);
329
332
  break;
330
333
  }
@@ -336,7 +339,7 @@ function devServer() {
336
339
  path.join(process.cwd(), 'src'),
337
340
  ];
338
341
  for (const c of candidates) {
339
- if (fs.existsSync(path.join(c, 'index.html'))) { root = c; break; }
342
+ if (fs.existsSync(path.join(c, htmlEntry))) { root = c; break; }
340
343
  }
341
344
  if (!root) root = process.cwd();
342
345
  }
@@ -389,9 +392,9 @@ function devServer() {
389
392
  res.status(404).send('Not Found');
390
393
  return;
391
394
  }
392
- const indexPath = path.join(root, 'index.html');
395
+ const indexPath = path.join(root, htmlEntry);
393
396
  if (!fs.existsSync(indexPath)) {
394
- res.status(404).send('index.html not found');
397
+ res.status(404).send(`${htmlEntry} not found`);
395
398
  return;
396
399
  }
397
400
  let html = fs.readFileSync(indexPath, 'utf-8');
@@ -495,6 +498,7 @@ function devServer() {
495
498
  console.log(` \x1b[2m${'-'.repeat(40)}\x1b[0m`);
496
499
  console.log(` Local: \x1b[36mhttp://localhost:${PORT}/\x1b[0m`);
497
500
  console.log(` Root: ${path.relative(process.cwd(), root) || '.'}`);
501
+ if (htmlEntry !== 'index.html') console.log(` HTML: \x1b[36m${htmlEntry}\x1b[0m`);
498
502
  console.log(` Live Reload: \x1b[32menabled\x1b[0m (SSE)`);
499
503
  console.log(` Overlay: \x1b[32menabled\x1b[0m (syntax + runtime errors)`);
500
504
  if (noIntercept) console.log(` Intercept: \x1b[33mdisabled\x1b[0m (--no-intercept)`);
package/cli/help.js CHANGED
@@ -12,6 +12,7 @@ function showHelp() {
12
12
 
13
13
  dev [root] Start a dev server with live-reload
14
14
  --port, -p <number> Port number (default: 3100)
15
+ --index, -i <file> Index HTML file (default: index.html)
15
16
  --no-intercept Disable auto-resolution of zquery.min.js
16
17
  (serve the on-disk vendor copy instead)
17
18
 
@@ -20,9 +21,10 @@ function showHelp() {
20
21
  overlay in the browser. Runtime errors and
21
22
  unhandled rejections are also captured.
22
23
 
23
- bundle [dir] Bundle app ES modules into a single file
24
- --out, -o <path> Output directory (default: dist/ next to index.html)
25
- --html <file> Use a specific HTML file (default: auto-detected)
24
+ bundle [dir|file] Bundle app ES modules into a single file
25
+ --out, -o <path> Output directory (default: dist/ next to HTML file)
26
+ --index, -i <file> Index HTML file (default: auto-detected)
27
+ --minimal, -m Only output HTML + bundled JS (skip static assets)
26
28
 
27
29
  build Build the zQuery library \u2192 dist/ --watch, -w Watch src/ and rebuild on changes (must be run from the project root where src/ lives)
28
30
 
@@ -34,9 +36,10 @@ function showHelp() {
34
36
  2. Within HTML: module script pointing to app.js, else first module script
35
37
  3. JS scan: $.router( first (entry point), then $.mount( / $.store(
36
38
  4. Convention fallbacks (scripts/app.js, app.js, etc.)
37
- \u2022 zquery.min.js is always embedded (auto-built from source if not found)
38
- \u2022 index.html is rewritten for both server and local (file://) use
39
- \u2022 Output goes to dist/server/ and dist/local/ next to the detected index.html
39
+ Passing a directory auto-detects the entry; passing a file uses it directly
40
+ zquery.min.js is always embedded (auto-built from source if not found)
41
+ HTML file is auto-detected (any .html, not just index.html)
42
+ • Output goes to dist/server/ and dist/local/ next to the detected HTML file
40
43
 
41
44
  OUTPUT
42
45
 
@@ -79,9 +82,18 @@ function showHelp() {
79
82
  # Bundle an app from the project root
80
83
  zquery bundle my-app/
81
84
 
85
+ # Pass a direct entry file (skip auto-detection)
86
+ zquery bundle my-app/scripts/main.js
87
+
82
88
  # Custom output directory
83
89
  zquery bundle my-app/ -o build/
84
90
 
91
+ # Minimal build (only HTML + bundled JS, no static assets)
92
+ zquery bundle my-app/ --minimal
93
+
94
+ # Dev server with a custom index page
95
+ zquery dev my-app/ --index home.html
96
+
85
97
  The bundler walks the ES module import graph starting from the entry
86
98
  file, topologically sorts dependencies, strips import/export syntax,
87
99
  and concatenates everything into a single IIFE with content-hashed
Binary file
@@ -46,6 +46,18 @@ $.component('about-page', {
46
46
  <strong>$.component()</strong>
47
47
  <span>Reactive components with state, lifecycle hooks, and template rendering</span>
48
48
  </div>
49
+ <div class="feature-item">
50
+ <strong>computed / watch</strong>
51
+ <span>Derived state properties and reactive watchers on the counter page</span>
52
+ </div>
53
+ <div class="feature-item">
54
+ <strong>DOM Diffing</strong>
55
+ <span>Efficient <code>morph()</code> engine patches only changed DOM nodes on re-render</span>
56
+ </div>
57
+ <div class="feature-item">
58
+ <strong>z-key</strong>
59
+ <span>Keyed list reconciliation in z-for loops (todos, counter history, contacts)</span>
60
+ </div>
49
61
  <div class="feature-item">
50
62
  <strong>$.router()</strong>
51
63
  <span>SPA routing with history mode, z-link navigation, and fallback pages</span>
@@ -79,8 +91,8 @@ $.component('about-page', {
79
91
  <span>Safe rendering of user-generated and API content</span>
80
92
  </div>
81
93
  <div class="feature-item">
82
- <strong>$.uuid()</strong>
83
- <span>Unique ID generation for new todo items</span>
94
+ <strong>CSP-safe expressions</strong>
95
+ <span>Template expressions evaluated without <code>eval()</code> or <code>new Function()</code></span>
84
96
  </div>
85
97
  <div class="feature-item">
86
98
  <strong>z-model / z-ref</strong>
@@ -13,7 +13,7 @@
13
13
  <h1>Contacts</h1>
14
14
  <p class="subtitle">
15
15
  External template &amp; styles via <code>templateUrl</code> / <code>styleUrl</code>.
16
- Directives: <code>z-if</code>, <code>z-for</code>, <code>z-show</code>,
16
+ Directives: <code>z-if</code>, <code>z-for</code> + <code>z-key</code>, <code>z-show</code>,
17
17
  <code>z-bind</code>, <code>z-class</code>, <code>z-model</code>, and more.
18
18
  </p>
19
19
  </div>
@@ -73,6 +73,7 @@
73
73
  <ul class="contacts-list">
74
74
  <li
75
75
  z-for="contact in contacts"
76
+ z-key="{{contact.id}}"
76
77
  class="contacts-item {{contact.id === selectedId ? 'selected' : ''}} {{contact.favorite ? 'is-favorite' : ''}}"
77
78
  @click="selectContact({{contact.id}})"
78
79
  >
@@ -111,15 +112,15 @@
111
112
  <div class="card contact-detail" z-if="selectedId !== null">
112
113
  <div class="detail-header">
113
114
  <div>
114
- <h3 z-text="contacts.find(c => c.id === selectedId)?.name || ''"></h3>
115
- <p class="muted" z-text="contacts.find(c => c.id === selectedId)?.email || ''"></p>
115
+ <h3 z-text="selectedName"></h3>
116
+ <p class="muted" z-text="selectedEmail"></p>
116
117
  </div>
117
118
  <div class="detail-actions">
118
119
  <button
119
120
  class="btn btn-outline btn-sm"
120
121
  @click="cycleStatus({{selectedId}})"
121
122
  >
122
- Status: <span z-text="contacts.find(c => c.id === selectedId)?.status || ''"></span>
123
+ Status: <span z-text="selectedStatus"></span>
123
124
  </button>
124
125
 
125
126
  <!-- Confirm delete pattern using z-if / z-else -->
@@ -23,6 +23,9 @@ $.component('contacts-page', {
23
23
  nameError: '',
24
24
  emailError: '',
25
25
  selectedId: null,
26
+ selectedName: '',
27
+ selectedEmail: '',
28
+ selectedStatus: '',
26
29
  confirmDeleteId: null,
27
30
  totalAdded: 0,
28
31
  favoriteCount: 0,
@@ -42,6 +45,7 @@ $.component('contacts-page', {
42
45
  this.state.contacts = store.state.contacts;
43
46
  this.state.totalAdded = store.state.contactsAdded;
44
47
  this.state.favoriteCount = store.getters.favoriteCount;
48
+ this._syncSelected();
45
49
  },
46
50
 
47
51
  // -- Actions --
@@ -101,8 +105,10 @@ $.component('contacts-page', {
101
105
  },
102
106
 
103
107
  selectContact(id) {
104
- this.state.selectedId = this.state.selectedId === Number(id) ? null : Number(id);
108
+ const numId = Number(id);
109
+ this.state.selectedId = this.state.selectedId === numId ? null : numId;
105
110
  this.state.confirmDeleteId = null;
111
+ this._syncSelected();
106
112
  },
107
113
 
108
114
  confirmDelete(id) {
@@ -125,6 +131,16 @@ $.component('contacts-page', {
125
131
 
126
132
  cycleStatus(id) {
127
133
  $.getStore('main').dispatch('cycleContactStatus', Number(id));
134
+ this._syncSelected();
135
+ },
136
+
137
+ _syncSelected() {
138
+ const c = this.state.selectedId != null
139
+ ? this.state.contacts.find(c => c.id === this.state.selectedId)
140
+ : null;
141
+ this.state.selectedName = c ? c.name : '';
142
+ this.state.selectedEmail = c ? c.email : '';
143
+ this.state.selectedStatus = c ? c.status : '';
128
144
  },
129
145
 
130
146
  _clearForm() {
@@ -1,8 +1,9 @@
1
1
  // scripts/components/counter.js — interactive counter
2
2
  //
3
- // Demonstrates: component state, instance methods, @click event binding,
4
- // z-model two-way binding with z-number modifier, z-class,
5
- // z-if, z-for, $.bus toast notifications
3
+ // Demonstrates: component state, computed properties, watch callbacks,
4
+ // @click event binding, z-model two-way binding with
5
+ // z-number modifier, z-class, z-if, z-for with z-key,
6
+ // $.bus toast notifications
6
7
 
7
8
  $.component('counter-page', {
8
9
  state: () => ({
@@ -11,16 +12,35 @@ $.component('counter-page', {
11
12
  history: [],
12
13
  }),
13
14
 
15
+ // Computed properties — derived values that update automatically
16
+ computed: {
17
+ isNegative: (state) => state.count < 0,
18
+ historyCount: (state) => state.history.length,
19
+ lastAction: (state) => state.history.length ? state.history[state.history.length - 1] : null,
20
+ },
21
+
22
+ // Watch — react to specific state changes
23
+ watch: {
24
+ count(val) {
25
+ if (val === 100) $.bus.emit('toast', { message: 'Century! 🎉', type: 'success' });
26
+ if (val === -100) $.bus.emit('toast', { message: 'Negative century!', type: 'error' });
27
+ },
28
+ },
29
+
14
30
  increment() {
15
31
  this.state.count += this.state.step;
16
- this.state.history.push({ action: '+', value: this.state.step, result: this.state.count });
17
- if (this.state.history.length > 8) this.state.history.shift();
32
+ this._pushHistory('+', this.state.step, this.state.count);
18
33
  },
19
34
 
20
35
  decrement() {
21
36
  this.state.count -= this.state.step;
22
- this.state.history.push({ action: '−', value: this.state.step, result: this.state.count });
23
- if (this.state.history.length > 8) this.state.history.shift();
37
+ this._pushHistory('−', this.state.step, this.state.count);
38
+ },
39
+
40
+ _pushHistory(action, value, result) {
41
+ const raw = this.state.history.__raw || this.state.history;
42
+ const next = [...raw, { id: Date.now(), action, value, result }];
43
+ this.state.history = next.length > 8 ? next.slice(-8) : next;
24
44
  },
25
45
 
26
46
  reset() {
@@ -33,7 +53,7 @@ $.component('counter-page', {
33
53
  return `
34
54
  <div class="page-header">
35
55
  <h1>Counter</h1>
36
- <p class="subtitle">Component state, <code>@click</code> handlers, <code>z-model</code>, <code>z-class</code>, and <code>z-for</code>.</p>
56
+ <p class="subtitle"><code>computed</code>, <code>watch</code>, <code>@click</code>, <code>z-model</code>, <code>z-class</code>, and <code>z-for</code> with <code>z-key</code>.</p>
37
57
  </div>
38
58
 
39
59
  <div class="card counter-card">
@@ -55,9 +75,9 @@ $.component('counter-page', {
55
75
  </div>
56
76
 
57
77
  <div class="card card-muted" z-if="history.length > 0">
58
- <h3>History</h3>
78
+ <h3>History <small style="color:var(--text-muted);font-weight:400;">(${this.computed.historyCount} entries)</small></h3>
59
79
  <div class="history-list">
60
- <span z-for="e in history" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
80
+ <span z-for="e in history" z-key="{{e.id}}" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
61
81
  </div>
62
82
  </div>
63
83
  `;
@@ -62,19 +62,19 @@ $.component('home-page', {
62
62
 
63
63
  <div class="card">
64
64
  <h3>🔢 Counter</h3>
65
- <p>Component state, two-way binding with <code>z-model</code>, and event handling.</p>
65
+ <p><code>computed</code> properties, <code>watch</code> callbacks, and <code>z-for</code> with <code>z-key</code> diffing.</p>
66
66
  <a z-link="/counter" class="btn btn-outline">Try It →</a>
67
67
  </div>
68
68
 
69
69
  <div class="card">
70
70
  <h3>✅ Todos</h3>
71
- <p>Global store with actions & getters. <strong>${store.getters.todoCount}</strong> items, <strong>${store.getters.doneCount}</strong> done.</p>
71
+ <p>Global store, <code>z-key</code> keyed lists, DOM diffing. <strong>${store.getters.todoCount}</strong> items, <strong>${store.getters.doneCount}</strong> done.</p>
72
72
  <a z-link="/todos" class="btn btn-outline">Try It →</a>
73
73
  </div>
74
74
 
75
75
  <div class="card">
76
76
  <h3>📇 Contacts</h3>
77
- <p>External templates &amp; styles via <code>templateUrl</code> / <code>styleUrl</code>. <strong>${store.getters.contactCount}</strong> contacts, <strong>${store.getters.favoriteCount}</strong> ★ favorited.</p>
77
+ <p>External templates, scoped styles, and <code>z-key</code> keyed lists. <strong>${store.getters.contactCount}</strong> contacts, <strong>${store.getters.favoriteCount}</strong> ★ favorited.</p>
78
78
  <a z-link="/contacts" class="btn btn-outline">Try It →</a>
79
79
  </div>
80
80
 
@@ -1,9 +1,10 @@
1
1
  // scripts/components/todos.js — todo list with global store
2
2
  //
3
3
  // Demonstrates: $.getStore, store.dispatch, store.subscribe,
4
- // store getters, z-model, z-ref, z-class, z-for,
5
- // z-if, z-show, @click with args, @submit.prevent,
6
- // mounted/destroyed lifecycle, $.bus toast, $.debounce
4
+ // store getters, computed properties, z-model, z-ref,
5
+ // z-class, z-for with z-key, z-if, z-show, @click
6
+ // with args, @submit.prevent, mounted/destroyed
7
+ // lifecycle, $.bus toast, $.debounce
7
8
 
8
9
  $.component('todos-page', {
9
10
  state: () => ({
@@ -83,7 +84,7 @@ $.component('todos-page', {
83
84
  return `
84
85
  <div class="page-header">
85
86
  <h1>Todos</h1>
86
- <p class="subtitle">Global store with <code>$.store()</code>, <code>z-for</code>, <code>z-class</code>, <code>z-if</code>, and <code>z-show</code>.</p>
87
+ <p class="subtitle">Global store with <code>$.store()</code>, <code>z-for</code> + <code>z-key</code>, <code>z-class</code>, <code>z-if</code>, and <code>z-show</code>.</p>
87
88
  </div>
88
89
 
89
90
  <div class="card">
@@ -114,7 +115,7 @@ $.component('todos-page', {
114
115
  </div>
115
116
 
116
117
  <ul z-else class="todo-list">
117
- <li z-for="t in filtered" class="todo-item {{t.done ? 'done' : ''}}">
118
+ <li z-for="t in filtered" z-key="{{t.id}}" class="todo-item {{t.done ? 'done' : ''}}">
118
119
  <button class="todo-check" @click="toggleTodo('{{t.id}}')"></button>
119
120
  <span class="todo-text">{{$.escapeHtml(t.text)}}</span>
120
121
  <button class="todo-remove" @click="removeTodo('{{t.id}}')">✕</button>
Binary file