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
@@ -0,0 +1,143 @@
1
+ // api-demo.js — HTTP client demo
2
+ //
3
+ // Features used:
4
+ // $.get() — fetch JSON from an API
5
+ // z-if / z-else / z-show — conditional rendering
6
+ // z-for / z-text — list rendering & text binding
7
+ // $.escapeHtml() — sanitize user-provided content
8
+ // async methods — loading & error state patterns
9
+
10
+ $.component('api-demo', {
11
+ styles: `
12
+ .api-users { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
13
+ gap: .75rem; }
14
+ .api-user { display: flex; flex-direction: column; gap: .2rem; padding: 1rem;
15
+ border-radius: var(--radius); border: 1px solid var(--border);
16
+ background: var(--bg-hover); cursor: pointer;
17
+ transition: border-color .15s, box-shadow .15s, transform .12s; text-align: left; }
18
+ .api-user:hover { border-color: var(--accent); transform: translateY(-2px);
19
+ box-shadow: 0 4px 12px rgba(88,166,255,.12); }
20
+ .api-user strong { color: var(--text); font-size: .95rem; }
21
+ .api-user small { color: var(--text-muted); font-size: .82rem; }
22
+ .api-user .handle { color: var(--accent); font-weight: 500; }
23
+
24
+ .api-detail { display: flex; align-items: flex-start; justify-content: space-between;
25
+ gap: 1rem; flex-wrap: wrap; }
26
+ .api-meta { font-size: .88rem; color: var(--text-muted); margin-top: .15rem; }
27
+ .api-meta span { margin-right: .75rem; }
28
+ .api-meta .accent { color: var(--accent); }
29
+
30
+ .api-posts { display: flex; flex-direction: column; gap: .5rem; }
31
+ .api-post { padding: .85rem 1rem; border-radius: var(--radius);
32
+ border-left: 3px solid var(--accent); background: var(--bg-hover);
33
+ transition: border-color .15s; }
34
+ .api-post:hover { border-left-color: #7ee787; }
35
+ .api-post h4 { font-size: .92rem; margin: 0 0 .3rem; color: var(--text); }
36
+ .api-post p { font-size: .84rem; color: var(--text-muted); margin: 0; line-height: 1.5; }
37
+
38
+ @media (max-width: 768px) {
39
+ .api-users { grid-template-columns: 1fr 1fr; }
40
+ .api-detail { flex-direction: column; }
41
+ }
42
+ @media (max-width: 480px) {
43
+ .api-users { grid-template-columns: 1fr; }
44
+ }
45
+ `,
46
+
47
+ state: () => ({
48
+ users: [],
49
+ selectedUser: null,
50
+ posts: [],
51
+ loading: false,
52
+ error: '',
53
+ }),
54
+
55
+ mounted() {
56
+ this.fetchUsers();
57
+ },
58
+
59
+ async fetchUsers() {
60
+ this.state.loading = true;
61
+ this.state.error = '';
62
+ try {
63
+ const res = await $.get('https://jsonplaceholder.typicode.com/users');
64
+ this.state.users = res.data.slice(0, 6);
65
+ } catch (err) {
66
+ this.state.error = 'Failed to load users. Check your connection.';
67
+ }
68
+ this.state.loading = false;
69
+ },
70
+
71
+ async selectUser(id) {
72
+ this.state.selectedUser = this.state.users.find(u => u.id === Number(id));
73
+ this.state.loading = true;
74
+ try {
75
+ const res = await $.get(`https://jsonplaceholder.typicode.com/posts?userId=${id}`);
76
+ this.state.posts = res.data.slice(0, 4);
77
+ } catch (err) {
78
+ this.state.error = 'Failed to load posts.';
79
+ }
80
+ this.state.loading = false;
81
+ $.bus.emit('toast', { message: `Loaded posts for ${this.state.selectedUser.name}`, type: 'success' });
82
+ },
83
+
84
+ clearSelection() {
85
+ this.state.selectedUser = null;
86
+ this.state.posts = [];
87
+ },
88
+
89
+ render() {
90
+ const { selectedUser } = this.state;
91
+
92
+ return `
93
+ <div class="page-header">
94
+ <h1>API Demo</h1>
95
+ <p class="subtitle">Fetching data with <code>$.get()</code>. Directives: <code>z-if</code>, <code>z-show</code>, <code>z-for</code>, <code>z-text</code>.</p>
96
+ </div>
97
+
98
+ <div class="card card-error" z-show="error"><p><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:16px;height:16px;vertical-align:-3px;margin-right:0.15rem;"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/></svg> <span z-text="error"></span></p></div>
99
+ <div class="loading-bar" z-show="loading"></div>
100
+
101
+ <div z-if="!selectedUser">
102
+ <div class="card">
103
+ <h3>Users</h3>
104
+ <p class="muted" style="margin-bottom:.75rem;">Click a user card to fetch their recent posts via <code>$.get()</code>.</p>
105
+ <div class="api-users" z-if="users.length > 0">
106
+ <button z-for="u in users" class="api-user" @click="selectUser({{u.id}})">
107
+ <strong>{{u.name}}</strong>
108
+ <small class="handle">@{{u.username}}</small>
109
+ <small>{{u.company.name}}</small>
110
+ </button>
111
+ </div>
112
+ <p z-else z-show="!loading">No users loaded.</p>
113
+ </div>
114
+ </div>
115
+
116
+ <div z-else>
117
+ <div class="card">
118
+ <div class="api-detail">
119
+ <div>
120
+ <h3 style="margin:0;">${selectedUser ? $.escapeHtml(selectedUser.name) : ''}</h3>
121
+ <div class="api-meta">
122
+ <span class="accent">@${selectedUser ? $.escapeHtml(selectedUser.username) : ''}</span>
123
+ <span>${selectedUser ? $.escapeHtml(selectedUser.email) : ''}</span>
124
+ </div>
125
+ </div>
126
+ <button class="btn btn-outline btn-sm" @click="clearSelection">← Back to users</button>
127
+ </div>
128
+ </div>
129
+
130
+ <div class="card">
131
+ <h3>Recent Posts</h3>
132
+ <div class="api-posts" z-if="posts.length > 0">
133
+ <article z-for="p in posts" class="api-post">
134
+ <h4>{{p.title}}</h4>
135
+ <p>{{p.body.substring(0, 120)}}…</p>
136
+ </article>
137
+ </div>
138
+ <p z-else class="muted" z-show="!loading">No posts found.</p>
139
+ </div>
140
+ </div>
141
+ `;
142
+ }
143
+ });
@@ -0,0 +1,231 @@
1
+ // contact-card.js — Global contact detail popup
2
+ //
3
+ // Always-mounted overlay that listens for $.bus 'openContact' events.
4
+ // Works from any page without navigation.
5
+ //
6
+ // Features used:
7
+ // $.bus.on / $.bus.emit — event-driven communication
8
+ // $.getStore — read contact data from store
9
+ // @click.self — dismiss on backdrop click
10
+
11
+ $.component('contact-card', {
12
+ styles: `
13
+ /* Overlay */
14
+ .cc-overlay {
15
+ position: fixed; inset: 0; z-index: 300;
16
+ background: rgba(0,0,0,0.55);
17
+ backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
18
+ display: flex; align-items: center; justify-content: center;
19
+ animation: cc-fade 0.15s ease;
20
+ }
21
+ @keyframes cc-fade { from { opacity: 0 } to { opacity: 1 } }
22
+
23
+ /* Card */
24
+ .cc-card {
25
+ position: relative; width: 400px;
26
+ max-width: calc(100vw - 2rem); max-height: calc(100vh - 4rem);
27
+ overflow-y: auto;
28
+ background: var(--bg-surface); border: 1px solid var(--border);
29
+ border-radius: var(--radius-lg);
30
+ box-shadow: 0 24px 64px rgba(0,0,0,0.35);
31
+ animation: cc-pop 0.2s var(--ease-out);
32
+ }
33
+ @keyframes cc-pop {
34
+ from { opacity: 0; transform: scale(0.95) translateY(10px); }
35
+ to { opacity: 1; transform: scale(1) translateY(0); }
36
+ }
37
+
38
+ /* Strip */
39
+ .cc-strip { height: 4px; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
40
+ transition: background 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease; }
41
+ .cc-strip-online { background: var(--success); box-shadow: 0 0 12px rgba(63,185,80,0.3); }
42
+ .cc-strip-away { background: var(--warning); }
43
+ .cc-strip-offline { background: var(--text-muted); opacity: 0.3; }
44
+
45
+ /* Close */
46
+ .cc-close {
47
+ position: absolute; top: 0.75rem; right: 0.75rem;
48
+ width: 28px; height: 28px; border-radius: 50%; border: none;
49
+ background: var(--bg-hover); color: var(--text-muted);
50
+ font-size: 0.85rem; cursor: pointer;
51
+ display: flex; align-items: center; justify-content: center;
52
+ transition: all 0.12s ease; z-index: 1;
53
+ }
54
+ .cc-close:hover { background: var(--danger); color: #fff; }
55
+
56
+ /* Profile */
57
+ .cc-profile {
58
+ display: flex; flex-direction: column; align-items: center;
59
+ padding: 1.75rem 1.5rem 1rem; position: relative;
60
+ }
61
+ .cc-avatar {
62
+ width: 72px; height: 72px; border-radius: 50%;
63
+ display: flex; align-items: center; justify-content: center;
64
+ font-weight: 700; font-size: 1.6rem; color: #fff;
65
+ text-transform: uppercase;
66
+ box-shadow: 0 4px 16px rgba(0,0,0,0.25);
67
+ margin-bottom: 0.75rem;
68
+ }
69
+ .cc-dot {
70
+ width: 14px; height: 14px; border-radius: 50%;
71
+ border: 3px solid var(--bg-surface);
72
+ position: absolute; top: calc(1.75rem + 56px); left: calc(50% + 22px);
73
+ transition: background 0.3s ease, box-shadow 0.3s ease;
74
+ }
75
+ .cc-dot-online { background: var(--success); box-shadow: 0 0 8px rgba(63,185,80,0.5); }
76
+ .cc-dot-away { background: var(--warning); }
77
+ .cc-dot-offline { background: var(--text-muted); }
78
+
79
+ .cc-profile h2 { font-size: 1.2rem; font-weight: 700; margin: 0 0 0.25rem; }
80
+ .cc-role {
81
+ font-size: 0.72rem; font-weight: 600; padding: 0.2rem 0.6rem;
82
+ border-radius: 999px; text-transform: uppercase; letter-spacing: 0.04em;
83
+ }
84
+ .cc-role-developer { background: rgba(96,165,250,0.12); color: #60a5fa; }
85
+ .cc-role-designer { background: rgba(168,85,247,0.12); color: #a855f7; }
86
+ .cc-role-manager { background: rgba(52,211,153,0.12); color: #34d399; }
87
+ .cc-role-qa { background: rgba(251,191,36,0.12); color: #fbbf24; }
88
+
89
+ /* Details grid */
90
+ .cc-details {
91
+ display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
92
+ padding: 0 1.5rem; margin-top: 0.5rem;
93
+ }
94
+ .cc-field { display: flex; flex-direction: column; gap: 0.15rem; }
95
+ .cc-label { font-size: 0.68rem; font-weight: 600; text-transform: uppercase;
96
+ letter-spacing: 0.04em; color: var(--text-muted); }
97
+ .cc-value { font-size: 0.85rem; color: var(--text); word-break: break-word; }
98
+ .cc-status { text-transform: capitalize; transition: color 0.3s ease; }
99
+
100
+ /* Bio */
101
+ .cc-bio { padding: 0.75rem 1.5rem 0; }
102
+ .cc-bio p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; margin-top: 0.2rem; }
103
+
104
+ /* Actions */
105
+ .cc-actions { display: flex; gap: 0.4rem; padding: 1rem 1.5rem 1.25rem; flex-wrap: wrap; }
106
+
107
+ /* Shared button styles (self-contained) */
108
+ .cc-btn {
109
+ padding: 0.4rem 0.85rem; font-size: 0.78rem; font-weight: 600;
110
+ border-radius: var(--radius); border: 1px solid var(--border);
111
+ background: transparent; color: var(--text-muted); cursor: pointer;
112
+ font-family: inherit; transition: all 0.12s ease;
113
+ }
114
+ .cc-btn:hover { background: var(--bg-hover); color: var(--text); }
115
+ .cc-btn-accent { border-color: var(--accent); color: var(--accent); }
116
+ .cc-btn-accent:hover { background: var(--accent); color: #fff; }
117
+
118
+ /* "View in Contacts" link */
119
+ .cc-goto { font-size: 0.72rem; color: var(--text-muted); margin-left: auto;
120
+ text-decoration: none; transition: color 0.15s ease; cursor: pointer; }
121
+ .cc-goto:hover { color: var(--accent); }
122
+
123
+ @media (max-width: 480px) {
124
+ .cc-details { grid-template-columns: 1fr; }
125
+ .cc-actions { justify-content: center; }
126
+ }
127
+ `,
128
+
129
+ state: () => ({
130
+ contact: null,
131
+ }),
132
+
133
+ mounted() {
134
+ this._onOpen = (id) => {
135
+ const store = $.getStore('main');
136
+ if (!store) return;
137
+ const c = store.state.contacts.find(c => c.id === Number(id));
138
+ if (c) this.state.contact = { ...c };
139
+ };
140
+ $.bus.on('openContact', this._onOpen);
141
+
142
+ this._onEsc = (e) => {
143
+ if (e.key === 'Escape' && this.state.contact) {
144
+ this.state.contact = null;
145
+ e.stopPropagation();
146
+ }
147
+ };
148
+ document.addEventListener('keydown', this._onEsc);
149
+
150
+ // Keep card in sync when store changes (e.g. status cycle, favorite toggle)
151
+ const store = $.getStore('main');
152
+ if (store) {
153
+ this._unsub = store.subscribe(() => {
154
+ if (!this.state.contact) return;
155
+ const c = store.state.contacts.find(c => c.id === this.state.contact.id);
156
+ if (c) this.state.contact = { ...c };
157
+ else this.state.contact = null; // deleted
158
+ });
159
+ }
160
+ },
161
+
162
+ destroyed() {
163
+ if (this._onOpen) $.bus.off('openContact', this._onOpen);
164
+ if (this._onEsc) document.removeEventListener('keydown', this._onEsc);
165
+ if (this._unsub) this._unsub();
166
+ },
167
+
168
+ close() { this.state.contact = null; },
169
+
170
+ toggleFav() {
171
+ if (!this.state.contact) return;
172
+ $.getStore('main').dispatch('toggleFavorite', this.state.contact.id);
173
+ },
174
+
175
+ cycleStatus() {
176
+ if (!this.state.contact) return;
177
+ $.getStore('main').dispatch('cycleContactStatus', this.state.contact.id);
178
+ },
179
+
180
+ goToContacts() {
181
+ this.state.contact = null;
182
+ $.getRouter().navigate('/contacts');
183
+ },
184
+
185
+ render() {
186
+ const c = this.state.contact;
187
+ if (!c) return '';
188
+
189
+ const hue = (c.name.charCodeAt(0) * 7) % 360;
190
+
191
+ return `
192
+ <div class="cc-overlay" @click.self="close">
193
+ <div class="cc-card">
194
+ <div class="cc-strip cc-strip-${c.status}"></div>
195
+ <button class="cc-close" @click="close">✕</button>
196
+
197
+ <div class="cc-profile">
198
+ <div class="cc-avatar" style="background:hsl(${hue},55%,42%)">
199
+ ${c.name.charAt(0).toUpperCase()}
200
+ </div>
201
+ <div class="cc-dot cc-dot-${c.status}"></div>
202
+ <h2>${$.escapeHtml(c.name)}</h2>
203
+ <span class="cc-role cc-role-${c.role.toLowerCase()}">${$.escapeHtml(c.role)}</span>
204
+ </div>
205
+
206
+ <div class="cc-details">
207
+ <div class="cc-field">
208
+ <span class="cc-label">Email</span>
209
+ <span class="cc-value">${$.escapeHtml(c.email)}</span>
210
+ </div>
211
+ ${c.phone ? `<div class="cc-field"><span class="cc-label">Phone</span><span class="cc-value">${$.escapeHtml(c.phone)}</span></div>` : ''}
212
+ ${c.location ? `<div class="cc-field"><span class="cc-label">Location</span><span class="cc-value">${$.escapeHtml(c.location)}</span></div>` : ''}
213
+ ${c.joined ? `<div class="cc-field"><span class="cc-label">Joined</span><span class="cc-value">${$.escapeHtml(c.joined)}</span></div>` : ''}
214
+ <div class="cc-field">
215
+ <span class="cc-label">Status</span>
216
+ <span class="cc-value cc-status">${c.status}</span>
217
+ </div>
218
+ </div>
219
+
220
+ ${c.bio ? `<div class="cc-bio"><span class="cc-label">Bio</span><p>${$.escapeHtml(c.bio)}</p></div>` : ''}
221
+
222
+ <div class="cc-actions">
223
+ <button class="cc-btn" @click="toggleFav">${c.favorite ? '★ Favorited' : '☆ Favorite'}</button>
224
+ <button class="cc-btn" @click="cycleStatus">Cycle Status</button>
225
+ <a class="cc-goto" @click="goToContacts">View in Contacts →</a>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ `;
230
+ }
231
+ });