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,131 +0,0 @@
1
- // scripts/components/about.js — about page with theme switcher
2
- //
3
- // Demonstrates: $.storage (localStorage wrapper), $.bus for notifications,
4
- // $.version, component methods, data-theme attribute toggling
5
-
6
- $.component('about-page', {
7
- state: () => ({
8
- theme: 'dark',
9
- }),
10
-
11
- mounted() {
12
- // Read persisted theme via $.storage
13
- this.state.theme = $.storage.get('theme') || 'dark';
14
- },
15
-
16
- toggleTheme() {
17
- const next = this.state.theme === 'dark' ? 'light' : 'dark';
18
- this.state.theme = next;
19
- // Apply theme via data attribute
20
- document.documentElement.setAttribute('data-theme', next);
21
- // Persist via $.storage (wraps localStorage)
22
- $.storage.set('theme', next);
23
- $.bus.emit('toast', { message: `Switched to ${next} theme`, type: 'info' });
24
- },
25
-
26
- render() {
27
- return `
28
- <div class="page-header">
29
- <h1>About</h1>
30
- <p class="subtitle">Built with zQuery v${$.version} — a zero-dependency frontend library.</p>
31
- </div>
32
-
33
- <div class="card">
34
- <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.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42"/></svg> Theme</h3>
35
- <p>Toggle between dark and light mode. Persisted to <code>localStorage</code> via <code>$.storage</code>.</p>
36
- <div class="theme-toggle">
37
- <span>Current: <strong>${this.state.theme}</strong></span>
38
- <button class="btn btn-outline" @click="toggleTheme">${this.state.theme === 'dark' ? '<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 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/></svg> Light Mode' : '<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="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/></svg> Dark Mode'}</button>
39
- </div>
40
- </div>
41
-
42
- <div class="card">
43
- <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="M21.75 6.75a4.5 4.5 0 0 1-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 1 1-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 0 1 6.336-4.486l-3.276 3.276a3.004 3.004 0 0 0 2.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852Z"/></svg> Features Used in This App</h3>
44
- <div class="feature-grid">
45
- <div class="feature-item">
46
- <strong>$.component()</strong>
47
- <span>Reactive components with state, lifecycle hooks, and template rendering</span>
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>
61
- <div class="feature-item">
62
- <strong>$.router()</strong>
63
- <span>SPA routing with history mode, z-link navigation, and fallback pages</span>
64
- </div>
65
- <div class="feature-item">
66
- <strong>$.store()</strong>
67
- <span>Centralized state management with actions, getters, and subscriptions</span>
68
- </div>
69
- <div class="feature-item">
70
- <strong>$.get()</strong>
71
- <span>HTTP client for fetching JSON APIs with async/await</span>
72
- </div>
73
- <div class="feature-item">
74
- <strong>$.signal() / $.computed()</strong>
75
- <span>Fine-grained reactive primitives for derived state</span>
76
- </div>
77
- <div class="feature-item">
78
- <strong>$.bus</strong>
79
- <span>Event bus for cross-component communication (toast notifications)</span>
80
- </div>
81
- <div class="feature-item">
82
- <strong>$.storage</strong>
83
- <span>localStorage wrapper for persisting user preferences</span>
84
- </div>
85
- <div class="feature-item">
86
- <strong>$.debounce()</strong>
87
- <span>Debounced search input in the todos filter</span>
88
- </div>
89
- <div class="feature-item">
90
- <strong>$.escapeHtml()</strong>
91
- <span>Safe rendering of user-generated and API content</span>
92
- </div>
93
- <div class="feature-item">
94
- <strong>CSP-safe expressions</strong>
95
- <span>Template expressions evaluated without <code>eval()</code> or <code>new Function()</code></span>
96
- </div>
97
- <div class="feature-item">
98
- <strong>z-model / z-ref</strong>
99
- <span>Two-way data binding and DOM element references</span>
100
- </div>
101
- <div class="feature-item">
102
- <strong>templateUrl / styleUrl</strong>
103
- <span>External HTML templates and CSS with auto-scoping (contacts page)</span>
104
- </div>
105
- <div class="feature-item">
106
- <strong>z-if / z-for / z-show</strong>
107
- <span>Structural directives for conditional &amp; list rendering</span>
108
- </div>
109
- <div class="feature-item">
110
- <strong>z-bind / z-class / z-style</strong>
111
- <span>Dynamic attributes, classes, and inline styles</span>
112
- </div>
113
- <div class="feature-item">
114
- <strong>$.on()</strong>
115
- <span>Global delegated event listeners for the hamburger menu</span>
116
- </div>
117
- </div>
118
- </div>
119
-
120
- <div class="card card-muted">
121
- <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 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/></svg> Next Steps</h3>
122
- <ul class="next-steps">
123
- <li>Read the <a href="https://z-query.com/docs" target="_blank" rel="noopener">full documentation</a></li>
124
- <li>Explore the <a href="https://github.com/tonywied17/zero-query" target="_blank" rel="noopener">source on GitHub</a></li>
125
- <li>Run <code>npx zquery bundle</code> to build for production</li>
126
- <li>Run <code>npx zquery dev</code> for live-reload development</li>
127
- </ul>
128
- </div>
129
- `;
130
- }
131
- });
@@ -1,103 +0,0 @@
1
- // scripts/components/api-demo.js — HTTP client demonstration
2
- //
3
- // Demonstrates: $.get() for fetching JSON, z-if/z-else conditional
4
- // rendering, z-show visibility, z-for list rendering,
5
- // z-text content binding, @click event handling,
6
- // loading/error states, $.escapeHtml(), async patterns
7
-
8
- $.component('api-demo', {
9
- state: () => ({
10
- users: [],
11
- selectedUser: null,
12
- posts: [],
13
- loading: false,
14
- error: '',
15
- }),
16
-
17
- mounted() {
18
- this.fetchUsers();
19
- },
20
-
21
- async fetchUsers() {
22
- this.state.loading = true;
23
- this.state.error = '';
24
- try {
25
- // $.get() — zero-config JSON fetching
26
- const res = await $.get('https://jsonplaceholder.typicode.com/users');
27
- this.state.users = res.data.slice(0, 6);
28
- } catch (err) {
29
- this.state.error = 'Failed to load users. Check your connection.';
30
- }
31
- this.state.loading = false;
32
- },
33
-
34
- async selectUser(id) {
35
- this.state.selectedUser = this.state.users.find(u => u.id === Number(id));
36
- this.state.loading = true;
37
- try {
38
- const res = await $.get(`https://jsonplaceholder.typicode.com/posts?userId=${id}`);
39
- this.state.posts = res.data.slice(0, 4);
40
- } catch (err) {
41
- this.state.error = 'Failed to load posts.';
42
- }
43
- this.state.loading = false;
44
- $.bus.emit('toast', { message: `Loaded posts for ${this.state.selectedUser.name}`, type: 'success' });
45
- },
46
-
47
- clearSelection() {
48
- this.state.selectedUser = null;
49
- this.state.posts = [];
50
- },
51
-
52
- render() {
53
- const { selectedUser } = this.state;
54
-
55
- return `
56
- <div class="page-header">
57
- <h1>API Demo</h1>
58
- <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>
59
- </div>
60
-
61
- <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>
62
- <div class="loading-bar" z-show="loading"></div>
63
-
64
- <div z-if="!selectedUser">
65
- <div class="card">
66
- <h3>Users</h3>
67
- <p class="muted">Click a user to fetch their posts.</p>
68
- <div class="user-grid" z-if="users.length > 0">
69
- <button z-for="u in users" class="user-card" @click="selectUser({{u.id}})">
70
- <strong>{{u.name}}</strong>
71
- <small>@{{u.username}}</small>
72
- <small class="muted">{{u.company.name}}</small>
73
- </button>
74
- </div>
75
- <p z-else z-show="!loading">No users loaded.</p>
76
- </div>
77
- </div>
78
-
79
- <div z-else>
80
- <div class="card">
81
- <div class="user-detail-header">
82
- <div>
83
- <h3>${selectedUser ? $.escapeHtml(selectedUser.name) : ''}</h3>
84
- <p class="muted">${selectedUser ? `@${$.escapeHtml(selectedUser.username)} · ${$.escapeHtml(selectedUser.email)}` : ''}</p>
85
- </div>
86
- <button class="btn btn-ghost btn-sm" @click="clearSelection">← Back</button>
87
- </div>
88
- </div>
89
-
90
- <div class="card">
91
- <h3>Recent Posts</h3>
92
- <div class="posts-list" z-if="posts.length > 0">
93
- <article z-for="p in posts" class="post-item">
94
- <h4>{{p.title}}</h4>
95
- <p>{{p.body.substring(0, 120)}}…</p>
96
- </article>
97
- </div>
98
- <p z-else class="muted" z-show="!loading">No posts found.</p>
99
- </div>
100
- </div>
101
- `;
102
- }
103
- });
@@ -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>