zero-query 0.9.7 → 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 (56) hide show
  1. package/README.md +31 -3
  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 +1942 -1925
  38. package/dist/zquery.min.js +2 -2
  39. package/index.d.ts +10 -1
  40. package/index.js +4 -3
  41. package/package.json +1 -1
  42. package/src/component.js +6 -3
  43. package/src/diff.js +15 -2
  44. package/tests/cli.test.js +304 -0
  45. package/cli/scaffold/app/components/about.js +0 -131
  46. package/cli/scaffold/app/components/api-demo.js +0 -103
  47. package/cli/scaffold/app/components/contacts/contacts.css +0 -246
  48. package/cli/scaffold/app/components/contacts/contacts.html +0 -140
  49. package/cli/scaffold/app/components/contacts/contacts.js +0 -153
  50. package/cli/scaffold/app/components/counter.js +0 -85
  51. package/cli/scaffold/app/components/home.js +0 -137
  52. package/cli/scaffold/app/components/todos.js +0 -131
  53. package/cli/scaffold/app/routes.js +0 -13
  54. /package/cli/scaffold/{LICENSE → default/LICENSE} +0 -0
  55. /package/cli/scaffold/{assets → default/assets}/.gitkeep +0 -0
  56. /package/cli/scaffold/{favicon.ico → default/favicon.ico} +0 -0
@@ -1,246 +0,0 @@
1
- /* contacts.css — scoped styles for contacts-page component
2
- *
3
- * Loaded via styleUrl — these styles are automatically scoped
4
- * to the contacts-page component by zQuery.
5
- */
6
-
7
- /* -- Toolbar -- */
8
- .contacts-toolbar-row {
9
- display: flex;
10
- gap: 0.75rem;
11
- align-items: center;
12
- justify-content: space-between;
13
- }
14
-
15
- /* -- Add Form -- */
16
- .contacts-form {
17
- display: grid;
18
- grid-template-columns: 1fr 1fr;
19
- gap: 0.75rem;
20
- margin-top: 1rem;
21
- padding-top: 1rem;
22
- border-top: 1px solid var(--border);
23
- }
24
-
25
- .form-field {
26
- display: flex;
27
- flex-direction: column;
28
- gap: 0.25rem;
29
- }
30
-
31
- .form-field-full {
32
- grid-column: 1 / -1;
33
- }
34
-
35
- .form-field .input {
36
- width: 100%;
37
- }
38
-
39
- .field-error {
40
- color: var(--danger);
41
- font-size: 0.78rem;
42
- min-height: 0;
43
- }
44
-
45
- .contacts-form .btn {
46
- grid-column: 1 / -1;
47
- justify-self: start;
48
- }
49
-
50
- /* -- Count -- */
51
- .contacts-count {
52
- font-size: 0.85rem;
53
- color: var(--text-muted);
54
- }
55
-
56
- /* -- List -- */
57
- .contacts-list {
58
- list-style: none;
59
- display: flex;
60
- flex-direction: column;
61
- gap: 2px;
62
- }
63
-
64
- .contacts-item {
65
- display: flex;
66
- align-items: center;
67
- gap: 0.75rem;
68
- padding: 0.7rem 0.85rem;
69
- border-radius: var(--radius);
70
- cursor: pointer;
71
- transition: all 0.15s ease;
72
- border: 1px solid transparent;
73
- }
74
-
75
- .contacts-item:hover {
76
- background: var(--bg-hover);
77
- }
78
-
79
- .contacts-item.selected {
80
- background: var(--accent-soft);
81
- border-color: var(--accent);
82
- }
83
-
84
- .contacts-item.is-favorite {
85
- border-left: 3px solid var(--accent);
86
- }
87
-
88
- /* -- Status dot -- */
89
- .status-dot {
90
- width: 10px;
91
- height: 10px;
92
- border-radius: 50%;
93
- flex-shrink: 0;
94
- background: var(--text-muted);
95
- }
96
-
97
- .status-online { background: var(--success); }
98
- .status-away { background: var(--info); }
99
- .status-offline { background: var(--text-muted); }
100
-
101
- /* -- Contact info -- */
102
- .contacts-info {
103
- flex: 1;
104
- display: flex;
105
- flex-direction: column;
106
- min-width: 0;
107
- }
108
-
109
- .contacts-info strong {
110
- font-size: 0.9rem;
111
- white-space: nowrap;
112
- overflow: hidden;
113
- text-overflow: ellipsis;
114
- }
115
-
116
- .contacts-info small {
117
- font-size: 0.8rem;
118
- color: var(--text-muted);
119
- white-space: nowrap;
120
- overflow: hidden;
121
- text-overflow: ellipsis;
122
- }
123
-
124
- /* -- Role badge -- */
125
- .role-badge {
126
- font-size: 0.75rem;
127
- font-weight: 600;
128
- padding: 0.2rem 0.55rem;
129
- border-radius: 99px;
130
- text-transform: uppercase;
131
- letter-spacing: 0.03em;
132
- white-space: nowrap;
133
- }
134
-
135
- .role-developer {
136
- background: rgba(96, 165, 250, 0.15);
137
- color: #60a5fa;
138
- }
139
-
140
- .role-designer {
141
- background: rgba(168, 85, 247, 0.15);
142
- color: #a855f7;
143
- }
144
-
145
- .role-manager {
146
- background: rgba(52, 211, 153, 0.15);
147
- color: #34d399;
148
- }
149
-
150
- .role-qa {
151
- background: rgba(251, 191, 36, 0.15);
152
- color: #fbbf24;
153
- }
154
-
155
- /* -- Favorite button -- */
156
- .fav-btn {
157
- background: none;
158
- border: none;
159
- font-size: 1.2rem;
160
- cursor: pointer;
161
- color: var(--text-muted);
162
- padding: 0.2rem;
163
- transition: all 0.15s ease;
164
- line-height: 1;
165
- }
166
-
167
- .fav-btn:hover {
168
- transform: scale(1.2);
169
- }
170
-
171
- .fav-btn.is-fav {
172
- color: var(--accent);
173
- }
174
-
175
- /* -- Detail panel -- */
176
- .contact-detail {
177
- border-left: 3px solid var(--accent);
178
- animation: slide-in 0.2s ease;
179
- }
180
-
181
- .detail-header {
182
- display: flex;
183
- justify-content: space-between;
184
- align-items: flex-start;
185
- gap: 1rem;
186
- flex-wrap: wrap;
187
- }
188
-
189
- .detail-header h3 {
190
- font-size: 1.1rem;
191
- margin-bottom: 0.15rem;
192
- }
193
-
194
- .detail-header .muted {
195
- font-size: 0.85rem;
196
- color: var(--text-muted);
197
- }
198
-
199
- .detail-actions {
200
- display: flex;
201
- gap: 0.5rem;
202
- align-items: center;
203
- flex-wrap: wrap;
204
- }
205
-
206
- /* -- Confirm group -- */
207
- .confirm-group {
208
- display: inline-flex;
209
- align-items: center;
210
- gap: 0.35rem;
211
- }
212
-
213
- .confirm-text {
214
- font-size: 0.82rem;
215
- color: var(--danger);
216
- font-weight: 500;
217
- }
218
-
219
- @keyframes slide-in {
220
- from { opacity: 0; transform: translateY(-6px); }
221
- to { opacity: 1; transform: translateY(0); }
222
- }
223
-
224
- /* -- Responsive -- */
225
- @media (max-width: 768px) {
226
- .contacts-toolbar-row {
227
- flex-direction: column;
228
- }
229
-
230
- .contacts-form {
231
- grid-template-columns: 1fr;
232
- }
233
-
234
- .contacts-item {
235
- flex-wrap: wrap;
236
- }
237
-
238
- .role-badge {
239
- order: 10;
240
- margin-left: calc(10px + 0.75rem);
241
- }
242
-
243
- .detail-header {
244
- flex-direction: column;
245
- }
246
- }
@@ -1,140 +0,0 @@
1
- <!--
2
- contacts.html — external template for contacts-page component
3
-
4
- This template is fetched via templateUrl and uses {{expression}} syntax
5
- for data binding. All zQuery directives work here: z-if, z-else,
6
- z-for, z-show, z-bind, z-class, z-style, z-text, z-model, z-ref,
7
- z-cloak, @click, @submit.prevent, etc.
8
-
9
- Expressions have access to `state` and `props` automatically.
10
- Inside z-for loops, use {{item.prop}} for per-item values.
11
- -->
12
- <div class="page-header" z-cloak>
13
- <h1>Contacts</h1>
14
- <p class="subtitle">
15
- External template &amp; styles via <code>templateUrl</code> / <code>styleUrl</code>.
16
- Directives: <code>z-if</code>, <code>z-for</code> + <code>z-key</code>, <code>z-show</code>,
17
- <code>z-bind</code>, <code>z-class</code>, <code>z-model</code>, and more.
18
- </p>
19
- </div>
20
-
21
- <!-- Toolbar: add button -->
22
- <div class="card contacts-toolbar">
23
- <div class="contacts-toolbar-row">
24
- <span class="contacts-count" z-if="contacts.length > 0">
25
- <strong z-text="contacts.length"></strong> contacts
26
- · <strong z-text="favoriteCount"></strong> ★ favorited
27
- <span z-show="totalAdded > 0"> · <span z-text="totalAdded"></span> added this session</span>
28
- </span>
29
- <span class="contacts-count" z-else>No contacts yet</span>
30
- <button class="btn btn-primary" @click="toggleForm">
31
- <span z-if="showForm">✕ Cancel</span>
32
- <span z-else>+ Add Contact</span>
33
- </button>
34
- </div>
35
-
36
- <!-- Add contact form — toggled via z-show -->
37
- <form class="contacts-form" z-show="showForm" @submit.prevent="addContact">
38
- <div class="form-field form-field-full">
39
- <input
40
- type="text"
41
- z-model="newName"
42
- z-trim
43
- placeholder="Full name"
44
- class="input"
45
- @blur="validateName"
46
- />
47
- <small class="field-error" z-show="nameError" z-text="nameError"></small>
48
- </div>
49
- <div class="form-field">
50
- <input
51
- type="email"
52
- z-model="newEmail"
53
- z-trim
54
- placeholder="Email address"
55
- class="input"
56
- @blur="validateEmail"
57
- />
58
- <small class="field-error" z-show="emailError" z-text="emailError"></small>
59
- </div>
60
- <select z-model="newRole" class="input">
61
- <option value="Developer">Developer</option>
62
- <option value="Designer">Designer</option>
63
- <option value="Manager">Manager</option>
64
- <option value="QA">QA</option>
65
- </select>
66
- <button type="submit" class="btn btn-primary">Save Contact</button>
67
- </form>
68
- </div>
69
-
70
- <!-- Contact count -->
71
- <div class="card" z-if="contacts.length > 0" z-key="contacts-list">
72
- <!-- Contacts list — z-for renders each item -->
73
- <ul class="contacts-list">
74
- <li
75
- z-for="contact in contacts"
76
- z-key="{{contact.id}}"
77
- class="contacts-item {{contact.id === selectedId ? 'selected' : ''}} {{contact.favorite ? 'is-favorite' : ''}}"
78
- @click="selectContact({{contact.id}})"
79
- >
80
- <!-- Status indicator — class set per status -->
81
- <span
82
- class="status-dot status-{{contact.status}}"
83
- title="{{contact.status}}"
84
- ></span>
85
-
86
- <!-- Contact info -->
87
- <div class="contacts-info">
88
- <strong>{{contact.name}}</strong>
89
- <small>{{contact.email}}</small>
90
- </div>
91
-
92
- <!-- Role badge -->
93
- <span class="role-badge role-{{contact.role.toLowerCase()}}">{{contact.role}}</span>
94
-
95
- <!-- Favorite toggle — .stop modifier prevents row click -->
96
- <button
97
- class="fav-btn {{contact.favorite ? 'is-fav' : ''}}"
98
- @click.stop="toggleFavorite({{contact.id}})"
99
- >{{contact.favorite ? '★' : '☆'}}</button>
100
- </li>
101
- </ul>
102
- </div>
103
-
104
- <!-- Empty state -->
105
- <div class="card" z-else z-key="contacts-empty">
106
- <div class="empty-state">
107
- <p>No contacts yet — add one above!</p>
108
- </div>
109
- </div>
110
-
111
- <!-- Selected contact detail panel — z-if conditional rendering -->
112
- <div class="card contact-detail" z-if="selectedId !== null" z-key="contact-detail">
113
- <div class="detail-header">
114
- <div>
115
- <h3 z-text="selectedName"></h3>
116
- <p class="muted" z-text="selectedEmail"></p>
117
- </div>
118
- <div class="detail-actions">
119
- <button
120
- class="btn btn-outline btn-sm"
121
- @click="cycleStatus({{selectedId}})"
122
- >
123
- Status: <span z-text="selectedStatus"></span>
124
- </button>
125
-
126
- <!-- Confirm delete pattern using z-if / z-else -->
127
- <button
128
- class="btn btn-danger btn-sm"
129
- z-if="confirmDeleteId !== selectedId"
130
- @click.stop="confirmDelete({{selectedId}})"
131
- >Delete</button>
132
-
133
- <span z-else class="confirm-group">
134
- <span class="confirm-text">Sure?</span>
135
- <button class="btn btn-danger btn-sm" @click.stop="deleteContact({{selectedId}})">Yes</button>
136
- <button class="btn btn-ghost btn-sm" @click.stop="cancelDelete">No</button>
137
- </span>
138
- </div>
139
- </div>
140
- </div>
@@ -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
- });