zero-query 0.9.6 → 0.9.8

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 (59) hide show
  1. package/README.md +36 -8
  2. package/cli/commands/build.js +50 -3
  3. package/cli/commands/create.js +22 -9
  4. package/cli/help.js +2 -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/dist/zquery.dist.zip +0 -0
  37. package/dist/zquery.js +1949 -1894
  38. package/dist/zquery.min.js +2 -2
  39. package/index.d.ts +10 -1
  40. package/index.js +5 -3
  41. package/package.json +1 -1
  42. package/src/component.js +6 -3
  43. package/src/diff.js +15 -2
  44. package/src/http.js +37 -0
  45. package/tests/cli.test.js +304 -0
  46. package/tests/http.test.js +200 -0
  47. package/types/http.d.ts +15 -4
  48. package/cli/scaffold/app/components/about.js +0 -131
  49. package/cli/scaffold/app/components/api-demo.js +0 -103
  50. package/cli/scaffold/app/components/contacts/contacts.css +0 -246
  51. package/cli/scaffold/app/components/contacts/contacts.html +0 -140
  52. package/cli/scaffold/app/components/contacts/contacts.js +0 -153
  53. package/cli/scaffold/app/components/counter.js +0 -85
  54. package/cli/scaffold/app/components/home.js +0 -137
  55. package/cli/scaffold/app/components/todos.js +0 -131
  56. package/cli/scaffold/app/routes.js +0 -13
  57. /package/cli/scaffold/{LICENSE → default/LICENSE} +0 -0
  58. /package/cli/scaffold/{assets → default/assets}/.gitkeep +0 -0
  59. /package/cli/scaffold/{favicon.ico → default/favicon.ico} +0 -0
@@ -1,153 +0,0 @@
1
- // scripts/components/contacts/contacts.js — contact book
2
- //
3
- // Demonstrates: external templateUrl + styleUrl, z-if/z-else, z-for,
4
- // z-show, z-bind/:attr, z-class, z-style, z-text, z-html,
5
- // z-model, z-ref, z-cloak, @click, @submit.prevent,
6
- // @input.debounce, event modifiers, and template {{expressions}}
7
- //
8
- // This component uses external files for its template and styles,
9
- // resolved automatically relative to this JS file's location.
10
- // Contacts are persisted in the global $.store('main') so they
11
- // survive navigation between routes.
12
-
13
- $.component('contacts-page', {
14
- templateUrl: 'contacts.html',
15
- styleUrl: 'contacts.css',
16
-
17
- state: () => ({
18
- contacts: [],
19
- showForm: false,
20
- newName: '',
21
- newEmail: '',
22
- newRole: 'Developer',
23
- nameError: '',
24
- emailError: '',
25
- selectedId: null,
26
- selectedName: '',
27
- selectedEmail: '',
28
- selectedStatus: '',
29
- confirmDeleteId: null,
30
- totalAdded: 0,
31
- favoriteCount: 0,
32
- }),
33
-
34
- mounted() {
35
- const store = $.getStore('main');
36
- this._syncFromStore(store);
37
- this._unsub = store.subscribe(() => this._syncFromStore(store));
38
- },
39
-
40
- destroyed() {
41
- if (this._unsub) this._unsub();
42
- },
43
-
44
- _syncFromStore(store) {
45
- this.state.contacts = store.state.contacts;
46
- this.state.totalAdded = store.state.contactsAdded;
47
- this.state.favoriteCount = store.getters.favoriteCount;
48
- this._syncSelected();
49
- },
50
-
51
- // -- Actions --
52
-
53
- toggleForm() {
54
- this.state.showForm = !this.state.showForm;
55
- if (!this.state.showForm) this._clearForm();
56
- },
57
-
58
- _validateName(name) {
59
- if (!name) return 'Name is required.';
60
- if (name.length < 2) return 'Name must be at least 2 characters.';
61
- return '';
62
- },
63
-
64
- _validateEmail(email) {
65
- if (!email) return 'Email is required.';
66
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Enter a valid email address.';
67
- const store = $.getStore('main');
68
- if (store.state.contacts.some(c => c.email.toLowerCase() === email.toLowerCase())) {
69
- return 'A contact with this email already exists.';
70
- }
71
- return '';
72
- },
73
-
74
- validateName() {
75
- this.state.nameError = this._validateName(this.state.newName.trim());
76
- },
77
-
78
- validateEmail() {
79
- this.state.emailError = this._validateEmail(this.state.newEmail.trim());
80
- },
81
-
82
- addContact() {
83
- const name = this.state.newName.trim();
84
- const email = this.state.newEmail.trim();
85
-
86
- const nameError = this._validateName(name);
87
- const emailError = this._validateEmail(email);
88
- this.state.nameError = nameError;
89
- this.state.emailError = emailError;
90
- if (nameError || emailError) return;
91
-
92
- $.getStore('main').dispatch('addContact', {
93
- name,
94
- email,
95
- role: this.state.newRole,
96
- });
97
-
98
- this._clearForm();
99
- this.state.showForm = false;
100
- $.bus.emit('toast', { message: `${name} added!`, type: 'success' });
101
- },
102
-
103
- toggleFavorite(id) {
104
- $.getStore('main').dispatch('toggleFavorite', Number(id));
105
- },
106
-
107
- selectContact(id) {
108
- const numId = Number(id);
109
- this.state.selectedId = this.state.selectedId === numId ? null : numId;
110
- this.state.confirmDeleteId = null;
111
- this._syncSelected();
112
- },
113
-
114
- confirmDelete(id) {
115
- this.state.confirmDeleteId = Number(id);
116
- },
117
-
118
- cancelDelete() {
119
- this.state.confirmDeleteId = null;
120
- },
121
-
122
- deleteContact(id) {
123
- const numId = Number(id);
124
- const store = $.getStore('main');
125
- const c = store.state.contacts.find(c => c.id === numId);
126
- store.dispatch('deleteContact', numId);
127
- this.state.selectedId = null;
128
- this.state.confirmDeleteId = null;
129
- $.bus.emit('toast', { message: `${c ? c.name : 'Contact'} removed`, type: 'error' });
130
- },
131
-
132
- cycleStatus(id) {
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 : '';
144
- },
145
-
146
- _clearForm() {
147
- this.state.newName = '';
148
- this.state.newEmail = '';
149
- this.state.newRole = 'Developer';
150
- this.state.nameError = '';
151
- this.state.emailError = '';
152
- },
153
- });
@@ -1,85 +0,0 @@
1
- // scripts/components/counter.js — interactive counter
2
- //
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
7
-
8
- $.component('counter-page', {
9
- state: () => ({
10
- count: 0,
11
- step: 1,
12
- history: [],
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
-
30
- increment() {
31
- this.state.count += this.state.step;
32
- this._pushHistory('+', this.state.step, this.state.count);
33
- },
34
-
35
- decrement() {
36
- this.state.count -= this.state.step;
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;
44
- },
45
-
46
- reset() {
47
- this.state.count = 0;
48
- this.state.history = [];
49
- $.bus.emit('toast', { message: 'Counter reset!', type: 'info' });
50
- },
51
-
52
- render() {
53
- return `
54
- <div class="page-header">
55
- <h1>Counter</h1>
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>
57
- </div>
58
-
59
- <div class="card counter-card">
60
- <div class="counter-display">
61
- <span class="counter-value" z-class="{'negative': count < 0}">${this.state.count}</span>
62
- </div>
63
-
64
- <div class="counter-controls">
65
- <button class="btn btn-danger" @click="decrement">− Subtract</button>
66
- <button class="btn btn-primary" @click="increment">+ Add</button>
67
- </div>
68
-
69
- <div class="counter-step">
70
- <label>Step size:
71
- <input type="number" z-model="step" z-number min="1" max="100" class="input input-sm" />
72
- </label>
73
- <button class="btn btn-ghost btn-sm" @click="reset">Reset</button>
74
- </div>
75
- </div>
76
-
77
- <div class="card card-muted" z-if="history.length > 0">
78
- <h3>History <small style="color:var(--text-muted);font-weight:400;">(${this.computed.historyCount} entries)</small></h3>
79
- <div class="history-list">
80
- <span z-for="e in history" z-key="{{e.id}}" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
81
- </div>
82
- </div>
83
- `;
84
- }
85
- });
@@ -1,137 +0,0 @@
1
- // scripts/components/home.js — dashboard / landing page
2
- //
3
- // Demonstrates: $.component, state, render, mounted lifecycle,
4
- // signal + computed + effect (reactive primitives),
5
- // $.store integration, $.bus, template rendering
6
-
7
- $.component('home-page', {
8
- state: () => ({
9
- greeting: '',
10
- signalDemo: 0,
11
- }),
12
-
13
- mounted() {
14
- // $.signal() — fine-grained reactive primitive
15
- const count = $.signal(0);
16
-
17
- // $.computed() — derived reactive value that auto-updates
18
- const doubled = $.computed(() => count.value * 2);
19
-
20
- // $.effect() — runs whenever its dependencies change
21
- $.effect(() => {
22
- this.state.signalDemo = doubled.value;
23
- });
24
-
25
- // Store the signal setter so the button can use it
26
- this._signalCount = count;
27
-
28
- // Greet based on time of day
29
- const hour = new Date().getHours();
30
- this.state.greeting = hour < 12 ? 'Good morning'
31
- : hour < 18 ? 'Good afternoon'
32
- : 'Good evening';
33
-
34
- // Track page visit via the global store
35
- const store = $.getStore('main');
36
- store.dispatch('incrementVisits');
37
- },
38
-
39
- incrementSignal() {
40
- if (this._signalCount) {
41
- this._signalCount.value++;
42
- }
43
- },
44
-
45
- render() {
46
- const store = $.getStore('main');
47
- return `
48
- <div class="page-header">
49
- <h1>${this.state.greeting}</h1>
50
- <p class="subtitle">Welcome to your new <strong>zQuery</strong> app. Explore the pages to see different features in action.</p>
51
- </div>
52
-
53
- <div class="card-grid">
54
- <div class="card card-accent">
55
- <h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><path stroke-linecap="round" stroke-linejoin="round" d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"/></svg> Reactive Signals</h3>
56
- <p>Fine-grained reactivity with <code>signal()</code>, <code>computed()</code>, and <code>effect()</code>.</p>
57
- <div class="signal-demo">
58
- <span class="signal-value">Doubled: ${this.state.signalDemo}</span>
59
- <button class="btn btn-sm" @click="incrementSignal">Increment Signal</button>
60
- </div>
61
- </div>
62
-
63
- <div class="card">
64
- <h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><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> Counter</h3>
65
- <p><code>computed</code> properties, <code>watch</code> callbacks, and <code>z-for</code> with <code>z-key</code> diffing.</p>
66
- <a z-link="/counter" class="btn btn-outline">Try It →</a>
67
- </div>
68
-
69
- <div class="card">
70
- <h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><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> Todos</h3>
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
- <a z-link="/todos" class="btn btn-outline">Try It →</a>
73
- </div>
74
-
75
- <div class="card">
76
- <h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><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> Contacts</h3>
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
- <a z-link="/contacts" class="btn btn-outline">Try It →</a>
79
- </div>
80
-
81
- <div class="card">
82
- <h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><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> API Demo</h3>
83
- <p>Fetch data with <code>$.get()</code>, loading states, and <code>$.escapeHtml()</code>.</p>
84
- <a z-link="/api" class="btn btn-outline">Try It →</a>
85
- </div>
86
- </div>
87
-
88
- <div class="card card-muted">
89
- <h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/></svg> App Stats</h3>
90
- <div class="stats-grid">
91
- <div class="stat-group">
92
- <span class="stat-group-title"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:14px;height:14px;vertical-align:-2px;margin-right:0.2rem;"><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> General</span>
93
- <div class="stat-group-values">
94
- <div class="stat">
95
- <span class="stat-value">${store.state.visits}</span>
96
- <span class="stat-label">Page Views</span>
97
- </div>
98
- </div>
99
- </div>
100
-
101
- <div class="stat-group">
102
- <span class="stat-group-title"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:14px;height:14px;vertical-align:-2px;margin-right:0.2rem;"><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> Todos</span>
103
- <div class="stat-group-values">
104
- <div class="stat">
105
- <span class="stat-value">${store.getters.todoCount}</span>
106
- <span class="stat-label">Total</span>
107
- </div>
108
- <div class="stat">
109
- <span class="stat-value">${store.getters.pendingCount}</span>
110
- <span class="stat-label">Pending</span>
111
- </div>
112
- <div class="stat">
113
- <span class="stat-value">${store.getters.doneCount}</span>
114
- <span class="stat-label">Done</span>
115
- </div>
116
- </div>
117
- </div>
118
-
119
- <div class="stat-group">
120
- <span class="stat-group-title"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:14px;height:14px;vertical-align:-2px;margin-right:0.2rem;"><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> Contacts</span>
121
- <div class="stat-group-values">
122
- <div class="stat">
123
- <span class="stat-value">${store.getters.contactCount}</span>
124
- <span class="stat-label">Total</span>
125
- </div>
126
- <div class="stat">
127
- <span class="stat-value">${store.getters.favoriteCount}</span>
128
- <span class="stat-label">★ Favorited</span>
129
- </div>
130
- </div>
131
- </div>
132
- </div>
133
- <small class="muted">Stats powered by <code>$.store()</code> getters — visit count tracked globally.</small>
134
- </div>
135
- `;
136
- }
137
- });
@@ -1,131 +0,0 @@
1
- // scripts/components/todos.js — todo list with global store
2
- //
3
- // Demonstrates: $.getStore, store.dispatch, store.subscribe,
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
8
-
9
- $.component('todos-page', {
10
- state: () => ({
11
- newTodo: '',
12
- filter: 'all', // 'all' | 'active' | 'done'
13
- search: '',
14
- filtered: [], // computed in render() for z-for access
15
- total: 0,
16
- done: 0,
17
- pending: 0,
18
- }),
19
-
20
- mounted() {
21
- const store = $.getStore('main');
22
- this._unsub = store.subscribe(() => this.setState({}));
23
-
24
- // $.debounce — debounced search filter (300ms)
25
- this._debouncedSearch = $.debounce((val) => {
26
- this.state.search = val;
27
- }, 300);
28
- },
29
-
30
- destroyed() {
31
- if (this._unsub) this._unsub();
32
- },
33
-
34
- addTodo() {
35
- const text = this.state.newTodo.trim();
36
- if (!text) return;
37
- $.getStore('main').dispatch('addTodo', text);
38
- this.state.newTodo = '';
39
- this.state.search = '';
40
- this.state.filter = 'all';
41
- $.bus.emit('toast', { message: 'Todo added!', type: 'success' });
42
- },
43
-
44
- toggleTodo(id) {
45
- $.getStore('main').dispatch('toggleTodo', id);
46
- },
47
-
48
- removeTodo(id) {
49
- $.getStore('main').dispatch('removeTodo', id);
50
- $.bus.emit('toast', { message: 'Todo removed', type: 'error' });
51
- },
52
-
53
- clearCompleted() {
54
- $.getStore('main').dispatch('clearCompleted');
55
- $.bus.emit('toast', { message: 'Completed todos cleared', type: 'info' });
56
- },
57
-
58
- setFilter(f) {
59
- this.state.filter = f;
60
- },
61
-
62
- onSearch(e) {
63
- this._debouncedSearch(e.target.value);
64
- },
65
-
66
- render() {
67
- const store = $.getStore('main');
68
- const todos = store.state.todos;
69
- const { filter, search } = this.state;
70
-
71
- // Compute filtered list and store stats into state for directive access
72
- let list = todos;
73
- if (filter === 'active') list = todos.filter(t => !t.done);
74
- if (filter === 'done') list = todos.filter(t => t.done);
75
- if (search) {
76
- const q = search.toLowerCase();
77
- list = list.filter(t => t.text.toLowerCase().includes(q));
78
- }
79
- this.state.filtered = list;
80
- this.state.total = store.getters.todoCount;
81
- this.state.done = store.getters.doneCount;
82
- this.state.pending = store.getters.pendingCount;
83
-
84
- return `
85
- <div class="page-header">
86
- <h1>Todos</h1>
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>
88
- </div>
89
-
90
- <div class="card">
91
- <form class="todo-form" @submit.prevent="addTodo">
92
- <input
93
- type="text"
94
- z-model="newTodo" z-trim
95
- placeholder="What needs to be done?"
96
- class="input"
97
- z-ref="todoInput"
98
- />
99
- <button type="submit" class="btn btn-primary">Add</button>
100
- </form>
101
- </div>
102
-
103
- <div class="card">
104
- <div class="todo-toolbar">
105
- <div class="todo-filters">
106
- <button class="btn btn-sm" z-class="{'btn-primary': filter === 'all', 'btn-ghost': filter !== 'all'}" @click="setFilter('all')">All (${this.state.total})</button>
107
- <button class="btn btn-sm" z-class="{'btn-primary': filter === 'active', 'btn-ghost': filter !== 'active'}" @click="setFilter('active')">Active (${this.state.pending})</button>
108
- <button class="btn btn-sm" z-class="{'btn-primary': filter === 'done', 'btn-ghost': filter !== 'done'}" @click="setFilter('done')">Done (${this.state.done})</button>
109
- </div>
110
- <input type="text" placeholder="Search…" class="input input-sm" @input="onSearch" value="${$.escapeHtml(this.state.search)}" />
111
- </div>
112
-
113
- <div z-if="filtered.length === 0" class="empty-state">
114
- <p>${this.state.total === 0 ? 'No todos yet — add one above!' : 'No matching todos.'}</p>
115
- </div>
116
-
117
- <ul z-else class="todo-list">
118
- <li z-for="t in filtered" z-key="{{t.id}}" class="todo-item {{t.done ? 'done' : ''}}">
119
- <button class="todo-check" @click="toggleTodo('{{t.id}}')"></button>
120
- <span class="todo-text">{{$.escapeHtml(t.text)}}</span>
121
- <button class="todo-remove" @click="removeTodo('{{t.id}}')">✕</button>
122
- </li>
123
- </ul>
124
-
125
- <div class="todo-footer" z-show="done > 0">
126
- <button class="btn btn-ghost btn-sm" @click="clearCompleted">Clear completed (${this.state.done})</button>
127
- </div>
128
- </div>
129
- `;
130
- }
131
- });
@@ -1,13 +0,0 @@
1
- // app/routes.js — route definitions
2
- //
3
- // Each route maps a URL path to a component tag name.
4
- // Supports: static paths, :params, wildcards, and lazy loading via `load`.
5
-
6
- export const routes = [
7
- { path: '/', component: 'home-page' },
8
- { path: '/counter', component: 'counter-page' },
9
- { path: '/todos', component: 'todos-page' },
10
- { path: '/contacts', component: 'contacts-page' },
11
- { path: '/api', component: 'api-demo' },
12
- { path: '/about', component: 'about-page' },
13
- ];
File without changes