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
@@ -0,0 +1,280 @@
1
+ // toolkit.js — HTTP, utilities, and advanced store features
2
+ //
3
+ // Features used:
4
+ // $.http / $.post / $.put / $.delete — HTTP client + interceptors
5
+ // $.pipe / $.memoize / $.retry — functional utilities
6
+ // $.groupBy / $.chunk / $.unique — collection helpers
7
+ // store.use / snapshot / history — middleware & time-travel
8
+ // templateUrl / styleUrl — external template & styles
9
+
10
+ $.component('toolkit-page', {
11
+ templateUrl: 'toolkit.html',
12
+ styleUrl: 'toolkit.css',
13
+
14
+ state: () => ({
15
+ activeTab: 'http',
16
+ httpLog: [],
17
+ httpMethod: '',
18
+ httpEndpoint: '',
19
+ httpStatusCode: null,
20
+ httpOutput: '',
21
+ searchQuery: '',
22
+ searchResults: [],
23
+ searchLoading: false,
24
+ activeUtil: '',
25
+ utilOutput: '',
26
+ storeLog: [],
27
+ storeSnap: null,
28
+ storeHistory: null,
29
+ // Derived state for template expressions
30
+ methodClass: 'get',
31
+ statusOk: false,
32
+ actionCount: 0,
33
+ }),
34
+
35
+ // init() lifecycle — runs before the first render
36
+ init() {
37
+ this._prevBaseURL = $.http.getConfig().baseURL || '';
38
+ $.http.configure({ baseURL: 'https://jsonplaceholder.typicode.com' });
39
+
40
+ this._unsubReq = $.http.onRequest((opts, url) => {
41
+ this._pushLog('→ ' + (opts.method || 'GET') + ' ' + url);
42
+ });
43
+ this._unsubRes = $.http.onResponse((result) => {
44
+ this._pushLog('← ' + result.status + ' ' + (result.statusText || 'OK'));
45
+ });
46
+
47
+ this._mwActive = true;
48
+ $.getStore('main').use((action, args) => {
49
+ if (!this._mwActive) return;
50
+ const raw = this.state.storeLog.__raw || this.state.storeLog;
51
+ this.state.storeLog = [...raw, {
52
+ id: Date.now(), action,
53
+ args: JSON.stringify(args).slice(0, 60),
54
+ time: new Date().toLocaleTimeString(),
55
+ }].slice(-8);
56
+ });
57
+
58
+ // Keep actionCount in sync with store history
59
+ this._syncActionCount();
60
+ this._storeSub = $.getStore('main').subscribe(() => this._syncActionCount());
61
+ },
62
+
63
+ destroyed() {
64
+ this._mwActive = false;
65
+ if (this._unsubReq) this._unsubReq();
66
+ if (this._unsubRes) this._unsubRes();
67
+ if (this._abortCtrl) this._abortCtrl.abort();
68
+ if (this._storeSub) this._storeSub();
69
+ $.http.configure({ baseURL: this._prevBaseURL });
70
+ },
71
+
72
+ _pushLog(msg) {
73
+ const raw = this.state.httpLog.__raw || this.state.httpLog;
74
+ this.state.httpLog = [...raw, { id: Date.now(), msg }].slice(-10);
75
+ },
76
+
77
+ _syncActionCount() {
78
+ const store = $.getStore('main');
79
+ this.state.actionCount = store.history ? store.history.length : 0;
80
+ },
81
+
82
+ _updateHttpMeta(method, endpoint) {
83
+ this.state.httpMethod = method;
84
+ this.state.httpEndpoint = endpoint;
85
+ this.state.httpOutput = '';
86
+ this.state.httpStatusCode = null;
87
+ this.state.methodClass = method === 'DELETE' ? 'del' : (method || 'get').toLowerCase();
88
+ this.state.statusOk = false;
89
+ },
90
+
91
+ _setHttpResult(status, output) {
92
+ this.state.httpStatusCode = status;
93
+ this.state.statusOk = status >= 200 && status < 400;
94
+ this.state.httpOutput = output;
95
+ },
96
+
97
+ setTab(tab) {
98
+ this.state.activeTab = tab;
99
+ this.state.utilOutput = '';
100
+ this.state.activeUtil = '';
101
+ this.state.httpOutput = '';
102
+ this.state.httpMethod = '';
103
+ this.state.httpStatusCode = null;
104
+ this.state.storeSnap = null;
105
+ this.state.storeHistory = null;
106
+ },
107
+
108
+ /* ── HTTP demos ───────────────────────────────────────────── */
109
+
110
+ async doGet() {
111
+ this._updateHttpMeta('GET', '/posts/1');
112
+ try {
113
+ const { data, status } = await $.get('/posts/1');
114
+ this._setHttpResult(status, JSON.stringify(data, null, 2));
115
+ } catch (e) { this._setHttpResult(0, 'Error: ' + e.message); }
116
+ },
117
+
118
+ async doPost() {
119
+ this._updateHttpMeta('POST', '/posts');
120
+ try {
121
+ const { data, status } = await $.post('/posts', {
122
+ title: 'Created with zQuery', body: 'Sent via $.post()', userId: 1,
123
+ });
124
+ this._setHttpResult(status, JSON.stringify(data, null, 2));
125
+ $.bus.emit('toast', { message: 'POST successful!', type: 'success' });
126
+ } catch (e) { this._setHttpResult(0, 'Error: ' + e.message); }
127
+ },
128
+
129
+ async doPut() {
130
+ this._updateHttpMeta('PUT', '/posts/1');
131
+ try {
132
+ const { data, status } = await $.put('/posts/1', {
133
+ id: 1, title: 'Updated via $.put()', body: 'Full resource replacement', userId: 1,
134
+ });
135
+ this._setHttpResult(status, JSON.stringify(data, null, 2));
136
+ $.bus.emit('toast', { message: 'PUT successful!', type: 'success' });
137
+ } catch (e) { this._setHttpResult(0, 'Error: ' + e.message); }
138
+ },
139
+
140
+ async doDelete() {
141
+ this._updateHttpMeta('DELETE', '/posts/1');
142
+ try {
143
+ const { status } = await $.delete('/posts/1');
144
+ this._setHttpResult(status, 'Resource deleted successfully.');
145
+ $.bus.emit('toast', { message: 'DELETE sent', type: 'info' });
146
+ } catch (e) { this._setHttpResult(0, 'Error: ' + e.message); }
147
+ },
148
+
149
+ async doSearch(e) {
150
+ const q = e.target.value.trim().toLowerCase();
151
+ this.state.searchQuery = e.target.value;
152
+ if (!q) { this.state.searchResults = []; return; }
153
+
154
+ if (this._abortCtrl) this._abortCtrl.abort();
155
+ this._abortCtrl = $.http.createAbort();
156
+ this.state.searchLoading = true;
157
+
158
+ try {
159
+ const { data } = await $.get('/users', null, { signal: this._abortCtrl.signal });
160
+ this.state.searchResults = data.filter(u =>
161
+ u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
162
+ ).slice(0, 5);
163
+ } catch (err) {
164
+ if (err.name !== 'AbortError') this.state.searchResults = [];
165
+ } finally { this.state.searchLoading = false; }
166
+ },
167
+
168
+ /* ── Utility demos ─────────────────────────────────────────── */
169
+
170
+ runUtil(name) {
171
+ this.state.activeUtil = name;
172
+ const demos = {
173
+ pipe: () => {
174
+ const slugify = $.pipe(
175
+ s => s.trim(), s => s.toLowerCase(),
176
+ s => s.replace(/[^a-z0-9]+/g, '-'), s => s.replace(/^-|-$/g, ''),
177
+ );
178
+ const input = ' Hello World! Creating Slugs ';
179
+ return `$.pipe(trim, lower, replace, strip)\n\nInput: "${input}"\nOutput: "${slugify(input)}"`;
180
+ },
181
+ memoize: () => {
182
+ const slowFib = (n) => n <= 1 ? n : slowFib(n - 1) + slowFib(n - 2);
183
+ const t0 = performance.now();
184
+ const result = slowFib(35);
185
+ const slowTime = performance.now() - t0;
186
+
187
+ const memoFib = $.memoize((n) => n <= 1 ? n : memoFib(n - 1) + memoFib(n - 2));
188
+ const t1 = performance.now();
189
+ memoFib(35);
190
+ const fastTime = performance.now() - t1;
191
+
192
+ const t2 = performance.now();
193
+ memoFib(35);
194
+ const cachedTime = performance.now() - t2;
195
+
196
+ return [
197
+ `$.memoize(fibonacci)`,
198
+ ``,
199
+ `fib(35) = ${result}`,
200
+ `────────────────────────────`,
201
+ `No memoize: ${slowTime.toFixed(1)} ms`,
202
+ `Memoized: ${fastTime.toFixed(3)} ms`,
203
+ `Cached: ${cachedTime.toFixed(4)} ms ⚡`,
204
+ ``,
205
+ `Speedup: ${(slowTime / Math.max(fastTime, 0.001)).toFixed(0)}× faster`,
206
+ ].join('\n');
207
+ },
208
+ retry: async () => {
209
+ this.state.utilOutput = '⏳ Running with exponential backoff…';
210
+ const log = [];
211
+ try {
212
+ await $.retry(async (attempt) => {
213
+ log.push(` #${attempt} ${attempt < 3 ? '✗ failed' : '✓ succeeded!'}`);
214
+ if (attempt < 3) throw new Error('Simulated failure');
215
+ return 'ok';
216
+ }, { attempts: 3, delay: 400, backoff: 2 });
217
+ } catch (e) { log.push(' All attempts failed.'); }
218
+ this.state.utilOutput = '$.retry({ attempts: 3, delay: 400, backoff: 2 })\n\n' + log.join('\n');
219
+ },
220
+ arrays: () => {
221
+ const data = [
222
+ { name: 'Alice', dept: 'eng' }, { name: 'Bob', dept: 'qa' },
223
+ { name: 'Carol', dept: 'eng' }, { name: 'Dave', dept: 'design' },
224
+ ];
225
+ return [
226
+ '$.groupBy(people, p => p.dept)',
227
+ JSON.stringify($.groupBy(data, p => p.dept), null, 2),
228
+ '',
229
+ '$.chunk($.range(1,9), 3) → ' + JSON.stringify($.chunk($.range(1, 9), 3)),
230
+ '$.unique([3,1,4,1,5,9]) → ' + JSON.stringify($.unique([3, 1, 4, 1, 5, 9, 2, 5])),
231
+ '$.range(0, 10, 2) → ' + JSON.stringify($.range(0, 10, 2)),
232
+ ].join('\n');
233
+ },
234
+ objects: () => {
235
+ const obj = { name: 'Tony', email: 'tony@dev.io', role: 'admin', secret: 'abc' };
236
+ const a = { x: 1, nested: { y: 2 } };
237
+ return [
238
+ `$.pick(obj, ["name","email"]) → ${JSON.stringify($.pick(obj, ['name', 'email']))}`,
239
+ `$.omit(obj, ["secret"]) → ${JSON.stringify($.omit(obj, ['secret']))}`,
240
+ `$.isEqual(a, b) → ${$.isEqual(a, { x: 1, nested: { y: 2 } })}`,
241
+ ``,
242
+ `$.deepMerge({}, a, { nested: { z: 3 } })`,
243
+ JSON.stringify($.deepMerge({}, a, { nested: { z: 3 } }), null, 2),
244
+ ].join('\n');
245
+ },
246
+ strings: () => [
247
+ `$.capitalize('hello') → "${$.capitalize('hello')}"`,
248
+ `$.truncate('Hello World!', 8) → "${$.truncate('Hello World!', 8)}"`,
249
+ `$.camelCase('my-component') → "${$.camelCase('my-component')}"`,
250
+ `$.kebabCase('myComponent') → "${$.kebabCase('myComponent')}"`,
251
+ ``,
252
+ `$.param({ q: 'test', page: 2 })`,
253
+ ` → "${$.param({ q: 'test', page: 2 })}"`,
254
+ ``,
255
+ `$.parseQuery('?q=test&page=2')`,
256
+ ` → ${JSON.stringify($.parseQuery('?q=test&page=2'))}`,
257
+ ].join('\n'),
258
+ };
259
+
260
+ const fn = demos[name];
261
+ if (!fn) return;
262
+ const result = fn();
263
+ if (result && typeof result.then === 'function') return;
264
+ this.state.utilOutput = result;
265
+ },
266
+
267
+ /* ── Store demos ──────────────────────────────────────────── */
268
+
269
+ takeSnapshot() {
270
+ this.state.storeSnap = JSON.stringify($.getStore('main').snapshot(), null, 2);
271
+ this.state.storeHistory = null;
272
+ $.bus.emit('toast', { message: 'Snapshot captured!', type: 'info' });
273
+ },
274
+
275
+ viewHistory() {
276
+ const h = $.getStore('main').history.slice(-6);
277
+ this.state.storeHistory = JSON.stringify(h, null, 2);
278
+ this.state.storeSnap = null;
279
+ },
280
+ });
@@ -0,0 +1,15 @@
1
+ // routes.js — Route definitions
2
+ //
3
+ // Maps URL paths to component tag names.
4
+ // Also supports :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: '/playground', component: 'playground-page' },
13
+ { path: '/toolkit', component: 'toolkit-page' },
14
+ { path: '/about', component: 'about-page' },
15
+ ];
@@ -1,8 +1,8 @@
1
- // app/store.js — global state management
1
+ // store.js — Global state management
2
2
  //
3
- // $.store() creates a centralized store with state, actions, and getters.
4
- // Components can dispatch actions and subscribe to changes.
5
- // The store is accessible anywhere via $.getStore('main').
3
+ // Defines a centralized store with state, actions, and getters.
4
+ // Any component can access it via $.getStore('main').
5
+ // Dispatch actions to update state; subscribe to react to changes.
6
6
 
7
7
  export const store = $.store('main', {
8
8
  state: {
@@ -11,11 +11,11 @@ export const store = $.store('main', {
11
11
 
12
12
  // Contacts
13
13
  contacts: [
14
- { id: 1, name: 'Tony Wiedman', email: 'tony@z-query.com', role: 'Developer', status: 'online', favorite: true },
15
- { id: 2, name: 'Robert Baratheon', email: 'robert@stormlands.io', role: 'Manager', status: 'offline', favorite: false },
16
- { id: 3, name: 'Terry A. Davis', email: 'terry@templeos.net', role: 'Developer', status: 'online', favorite: true },
17
- { id: 4, name: 'Trevor Moore', email: 'trevor@wkuk.tv', role: 'Designer', status: 'away', favorite: false },
18
- { id: 5, name: 'Carlo Acutis', email: 'carlo@vatican.va', role: 'Developer', status: 'online', favorite: false },
14
+ { id: 1, name: 'Tony Wiedman', email: 'tony@z-query.com', role: 'Developer', status: 'online', favorite: true, phone: '+1 (555) 201-0017', location: 'Philadelphia, PA', joined: '2024-01-15', bio: 'Full-stack developer & creator of zQuery.' },
15
+ { id: 2, name: 'Robert Baratheon', email: 'robert@stormlands.io', role: 'Manager', status: 'offline', favorite: false, phone: '+1 (555) 783-1042', location: "Storm's End, Westeros", joined: '2024-03-22', bio: 'Former king turned project manager.' },
16
+ { id: 3, name: 'Terry A. Davis', email: 'terry@templeos.net', role: 'Developer', status: 'online', favorite: true, phone: '+1 (555) 640-8086', location: 'Las Vegas, NV', joined: '2024-02-10', bio: 'Built an entire OS from scratch. Legendary.' },
17
+ { id: 4, name: 'Trevor Moore', email: 'trevor@wkuk.tv', role: 'Designer', status: 'away', favorite: false, phone: '+1 (555) 994-2287', location: 'Los Angeles, CA', joined: '2024-05-08', bio: 'Comedian, writer, and creative visionary.' },
18
+ { id: 5, name: 'Carlo Acutis', email: 'carlo@vatican.va', role: 'Developer', status: 'online', favorite: false, phone: '+39 (02) 555-1430', location: 'Milan, Italy', joined: '2024-04-01', bio: 'Patron saint of the internet.' },
19
19
  ],
20
20
  contactsAdded: 0,
21
21
  },
@@ -54,7 +54,8 @@ export const store = $.store('main', {
54
54
 
55
55
  // -- Contact actions --
56
56
 
57
- addContact(state, { name, email, role }) {
57
+ addContact(state, { name, email, role, phone, location, bio }) {
58
+ const cities = ['New York, NY', 'San Francisco, CA', 'Austin, TX', 'Seattle, WA', 'Chicago, IL', 'Denver, CO', 'Portland, OR', 'Boston, MA'];
58
59
  state.contacts.push({
59
60
  id: Date.now(),
60
61
  name,
@@ -62,6 +63,10 @@ export const store = $.store('main', {
62
63
  role,
63
64
  status: 'offline',
64
65
  favorite: false,
66
+ phone: phone || '',
67
+ location: location || cities[Math.floor(Math.random() * cities.length)],
68
+ joined: new Date().toISOString().slice(0, 10),
69
+ bio: bio || '',
65
70
  });
66
71
  state.contactsAdded++;
67
72
  },