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.
- package/README.md +36 -8
- package/cli/commands/build.js +50 -3
- package/cli/commands/create.js +22 -9
- package/cli/help.js +2 -0
- package/cli/scaffold/default/app/app.js +211 -0
- package/cli/scaffold/default/app/components/about.js +201 -0
- package/cli/scaffold/default/app/components/api-demo.js +143 -0
- package/cli/scaffold/default/app/components/contact-card.js +231 -0
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -0
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -0
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -0
- package/cli/scaffold/default/app/components/counter.js +127 -0
- package/cli/scaffold/default/app/components/home.js +249 -0
- package/cli/scaffold/{app → default/app}/components/not-found.js +2 -2
- package/cli/scaffold/default/app/components/playground/playground.css +116 -0
- package/cli/scaffold/default/app/components/playground/playground.html +162 -0
- package/cli/scaffold/default/app/components/playground/playground.js +117 -0
- package/cli/scaffold/default/app/components/todos.js +225 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -0
- package/cli/scaffold/default/app/routes.js +15 -0
- package/cli/scaffold/{app → default/app}/store.js +15 -10
- package/cli/scaffold/{global.css → default/global.css} +238 -252
- package/cli/scaffold/{index.html → default/index.html} +35 -0
- package/cli/scaffold/{app → minimal/app}/app.js +37 -39
- package/cli/scaffold/minimal/app/components/about.js +68 -0
- package/cli/scaffold/minimal/app/components/counter.js +122 -0
- package/cli/scaffold/minimal/app/components/home.js +68 -0
- package/cli/scaffold/minimal/app/components/not-found.js +16 -0
- package/cli/scaffold/minimal/app/routes.js +9 -0
- package/cli/scaffold/minimal/app/store.js +36 -0
- package/cli/scaffold/minimal/assets/.gitkeep +0 -0
- package/cli/scaffold/minimal/global.css +291 -0
- package/cli/scaffold/minimal/index.html +44 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1949 -1894
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +10 -1
- package/index.js +5 -3
- package/package.json +1 -1
- package/src/component.js +6 -3
- package/src/diff.js +15 -2
- package/src/http.js +37 -0
- package/tests/cli.test.js +304 -0
- package/tests/http.test.js +200 -0
- package/types/http.d.ts +15 -4
- package/cli/scaffold/app/components/about.js +0 -131
- package/cli/scaffold/app/components/api-demo.js +0 -103
- package/cli/scaffold/app/components/contacts/contacts.css +0 -246
- package/cli/scaffold/app/components/contacts/contacts.html +0 -140
- package/cli/scaffold/app/components/contacts/contacts.js +0 -153
- package/cli/scaffold/app/components/counter.js +0 -85
- package/cli/scaffold/app/components/home.js +0 -137
- package/cli/scaffold/app/components/todos.js +0 -131
- package/cli/scaffold/app/routes.js +0 -13
- /package/cli/scaffold/{LICENSE → default/LICENSE} +0 -0
- /package/cli/scaffold/{assets → default/assets}/.gitkeep +0 -0
- /package/cli/scaffold/{favicon.ico → default/favicon.ico} +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// about.js — About page with theme switcher
|
|
2
|
+
//
|
|
3
|
+
// Features used:
|
|
4
|
+
// $.storage — localStorage wrapper (get / set)
|
|
5
|
+
// $.version — library version string
|
|
6
|
+
// $.unitTests — build-time test results object
|
|
7
|
+
// $.bus.emit — toast notifications
|
|
8
|
+
// data-theme attr — dark / light theming
|
|
9
|
+
|
|
10
|
+
$.component('about-page', {
|
|
11
|
+
styles: `
|
|
12
|
+
/* -- Hero -- */
|
|
13
|
+
.about-hero { text-align: center; padding: 2.5rem 1rem 1.5rem; }
|
|
14
|
+
.about-hero h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.35rem; }
|
|
15
|
+
.about-hero .ver { display: inline-block; padding: 0.2rem 0.65rem; border-radius: 999px;
|
|
16
|
+
font-size: 0.78rem; font-weight: 600; color: var(--accent);
|
|
17
|
+
background: var(--accent-soft); border: 1px solid rgba(88,166,255,.15);
|
|
18
|
+
margin-bottom: 0.75rem; }
|
|
19
|
+
.about-hero p { color: var(--text-muted); font-size: 0.95rem; max-width: 520px; margin: 0 auto; }
|
|
20
|
+
|
|
21
|
+
/* -- Stats bar -- */
|
|
22
|
+
.about-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;
|
|
23
|
+
padding: 1.25rem 0; border-top: 1px solid var(--border);
|
|
24
|
+
border-bottom: 1px solid var(--border); margin: 1.25rem 0; }
|
|
25
|
+
.about-stat { text-align: center; }
|
|
26
|
+
.about-stat-val { font-size: 1.35rem; font-weight: 700; color: var(--accent);
|
|
27
|
+
font-variant-numeric: tabular-nums; }
|
|
28
|
+
.about-stat-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;
|
|
29
|
+
letter-spacing: 0.04em; }
|
|
30
|
+
|
|
31
|
+
/* -- Theme switcher -- */
|
|
32
|
+
.theme-switch { display: inline-flex; border-radius: var(--radius); overflow: hidden;
|
|
33
|
+
border: 1px solid var(--border); background: var(--bg); }
|
|
34
|
+
.theme-btn { padding: 0.45rem 1rem; font-size: 0.82rem; font-weight: 500;
|
|
35
|
+
background: transparent; border: none; color: var(--text-muted);
|
|
36
|
+
cursor: pointer; transition: all .15s ease; font-family: inherit;
|
|
37
|
+
display: inline-flex; align-items: center; gap: 0.35rem; }
|
|
38
|
+
.theme-btn:hover { color: var(--text); background: var(--bg-hover); }
|
|
39
|
+
.theme-btn.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
40
|
+
.theme-btn + .theme-btn { border-left: 1px solid var(--border); }
|
|
41
|
+
|
|
42
|
+
/* -- Feature categories -- */
|
|
43
|
+
.feat-section { margin-bottom: 0.5rem; }
|
|
44
|
+
.feat-label { font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
|
|
45
|
+
letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.4rem;
|
|
46
|
+
padding-left: 0.1rem; }
|
|
47
|
+
|
|
48
|
+
/* -- Test badge -- */
|
|
49
|
+
.test-badge { display: inline-flex; align-items: center; gap: 0.3rem;
|
|
50
|
+
padding: 0.2rem 0.55rem; border-radius: 999px;
|
|
51
|
+
font-size: 0.7rem; font-weight: 600; line-height: 1;
|
|
52
|
+
margin-top: 0.35rem; }
|
|
53
|
+
.test-badge.pass { background: rgba(63,185,80,.12); color: var(--success);
|
|
54
|
+
border: 1px solid rgba(63,185,80,.25); }
|
|
55
|
+
.test-badge.fail { background: rgba(248,81,73,.12); color: var(--danger);
|
|
56
|
+
border: 1px solid rgba(248,81,73,.25); }
|
|
57
|
+
.test-badge svg { width: 11px; height: 11px; }
|
|
58
|
+
|
|
59
|
+
@media (max-width: 768px) {
|
|
60
|
+
.about-hero { padding: 1.5rem 0.5rem 1rem; }
|
|
61
|
+
.about-hero h1 { font-size: 1.5rem; }
|
|
62
|
+
.about-stats { gap: 1rem; }
|
|
63
|
+
.about-stat-val { font-size: 1.1rem; }
|
|
64
|
+
.theme-btn { padding: 0.4rem 0.65rem; font-size: 0.78rem; }
|
|
65
|
+
}
|
|
66
|
+
@media (max-width: 480px) {
|
|
67
|
+
.about-hero h1 { font-size: 1.3rem; }
|
|
68
|
+
.about-stats { gap: 0.6rem 1rem; }
|
|
69
|
+
.about-stat-val { font-size: 1rem; }
|
|
70
|
+
}
|
|
71
|
+
`,
|
|
72
|
+
|
|
73
|
+
state: () => ({
|
|
74
|
+
theme: 'system',
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
mounted() {
|
|
78
|
+
this.state.theme = $.storage.get('theme') || 'system';
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
setTheme(mode) {
|
|
82
|
+
this.state.theme = mode;
|
|
83
|
+
$.storage.set('theme', mode);
|
|
84
|
+
window.__applyTheme(mode);
|
|
85
|
+
const label = mode === 'system' ? 'system (auto)' : mode;
|
|
86
|
+
$.bus.emit('toast', { message: `Theme: ${label}`, type: 'info' });
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
render() {
|
|
90
|
+
const t = this.state.theme;
|
|
91
|
+
const feats = {
|
|
92
|
+
'Core & Components': [
|
|
93
|
+
['$.component()', 'Reactive components with state, lifecycle, and template rendering'],
|
|
94
|
+
['computed / watch', 'Derived state and reactive watchers on the counter page'],
|
|
95
|
+
['DOM Diffing', 'Efficient morph() engine patches only changed DOM nodes'],
|
|
96
|
+
['z-key', 'Keyed list reconciliation in z-for loops'],
|
|
97
|
+
['z-model / z-ref', 'Two-way data binding and DOM element references'],
|
|
98
|
+
['CSP-safe expressions', 'Template expressions without eval() or new Function()'],
|
|
99
|
+
],
|
|
100
|
+
'Directives': [
|
|
101
|
+
['z-if / z-for / z-show', 'Structural directives for conditional & list rendering'],
|
|
102
|
+
['z-bind / z-class / z-style', 'Dynamic attributes, classes, and inline styles'],
|
|
103
|
+
['z-debounce', 'Debounced model updates — z-model z-debounce="300"'],
|
|
104
|
+
['z-lowercase / z-uppercase', 'Auto-transform text input on contacts email'],
|
|
105
|
+
['z-html', 'Trusted HTML injection in the playground'],
|
|
106
|
+
['templateUrl / styleUrl', 'External templates and CSS with auto-scoping'],
|
|
107
|
+
],
|
|
108
|
+
'Events': [
|
|
109
|
+
['@click.stop / .prevent', 'Event modifiers for fine-grained control'],
|
|
110
|
+
['@click.self / .once', 'Self-only and one-shot handlers in modals'],
|
|
111
|
+
['@click.outside', 'Outside-click detection for dropdowns'],
|
|
112
|
+
['@keydown.escape', 'Key modifiers — Escape to close forms'],
|
|
113
|
+
],
|
|
114
|
+
'Reactive Primitives': [
|
|
115
|
+
['$.signal() / $.computed()', 'Fine-grained reactive primitives for derived state'],
|
|
116
|
+
['$.effect()', 'Side-effects that auto-track signal dependencies'],
|
|
117
|
+
],
|
|
118
|
+
'State & Routing': [
|
|
119
|
+
['$.router()', 'SPA routing with history mode and fallback pages'],
|
|
120
|
+
['$.store()', 'Centralized state with actions, getters, subscriptions'],
|
|
121
|
+
['store.use / store.snapshot', 'Action middleware, deep-cloning, and history'],
|
|
122
|
+
['$.bus', 'Event bus for cross-component communication'],
|
|
123
|
+
['$.storage', 'localStorage wrapper for persisting preferences'],
|
|
124
|
+
],
|
|
125
|
+
'HTTP & Utilities': [
|
|
126
|
+
['$.get() / $.http', 'HTTP client, CRUD, interceptors, and abort signals'],
|
|
127
|
+
['$.pipe / $.memoize / $.retry', 'Function composition, LRU caching, resilient async'],
|
|
128
|
+
['$.escapeHtml()', 'Safe rendering of user-generated content'],
|
|
129
|
+
['$.fn', 'Custom chainable collection methods via plugins'],
|
|
130
|
+
['fadeIn / fadeOut / slideToggle', 'Promise-based animations with chaining'],
|
|
131
|
+
['$.on()', 'Global delegated event listeners'],
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return `
|
|
136
|
+
<div class="about-hero">
|
|
137
|
+
<span class="ver">v${$.version}</span>
|
|
138
|
+
<h1>zQuery</h1>
|
|
139
|
+
<p>A zero-dependency frontend micro-library — reactive components, routing, state management, and more in <strong>${$.libSize}</strong> minified.</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="about-stats">
|
|
143
|
+
<div class="about-stat"><div class="about-stat-val">${$.libSize}</div><div class="about-stat-lbl">Minified</div></div>
|
|
144
|
+
<div class="about-stat"><div class="about-stat-val">0</div><div class="about-stat-lbl">Dependencies</div></div>
|
|
145
|
+
<div class="about-stat">
|
|
146
|
+
<div class="about-stat-val">${$.unitTests.total}</div>
|
|
147
|
+
<div class="about-stat-lbl">Tests</div>
|
|
148
|
+
<span class="test-badge ${$.unitTests.ok ? 'pass' : 'fail'}">${$.unitTests.ok
|
|
149
|
+
? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg> passing'
|
|
150
|
+
: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg> ' + $.unitTests.failed + ' failing'}</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="card">
|
|
155
|
+
<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>
|
|
156
|
+
<p>Choose your preferred appearance. <strong>System</strong> follows your OS setting automatically.</p>
|
|
157
|
+
<div class="theme-switch">
|
|
158
|
+
<button class="theme-btn ${t === 'system' ? 'active' : ''}" @click="setTheme('system')">
|
|
159
|
+
<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;"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25Z"/></svg>
|
|
160
|
+
System
|
|
161
|
+
</button>
|
|
162
|
+
<button class="theme-btn ${t === 'dark' ? 'active' : ''}" @click="setTheme('dark')">
|
|
163
|
+
<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;"><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>
|
|
164
|
+
Dark
|
|
165
|
+
</button>
|
|
166
|
+
<button class="theme-btn ${t === 'light' ? 'active' : ''}" @click="setTheme('light')">
|
|
167
|
+
<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;"><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>
|
|
168
|
+
Light
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="card">
|
|
174
|
+
<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>
|
|
175
|
+
${Object.entries(feats).map(([cat, items]) => `
|
|
176
|
+
<div class="feat-section">
|
|
177
|
+
<div class="feat-label">${cat}</div>
|
|
178
|
+
<div class="feature-grid">
|
|
179
|
+
${items.map(([name, desc]) => `
|
|
180
|
+
<div class="feature-item">
|
|
181
|
+
<strong>${name}</strong>
|
|
182
|
+
<span>${desc}</span>
|
|
183
|
+
</div>
|
|
184
|
+
`).join('')}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
`).join('')}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="card card-muted">
|
|
191
|
+
<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>
|
|
192
|
+
<ul class="next-steps">
|
|
193
|
+
<li>Read the <a href="https://z-query.com/docs" target="_blank" rel="noopener">full documentation</a></li>
|
|
194
|
+
<li>Explore the <a href="https://github.com/tonywied17/zero-query" target="_blank" rel="noopener">source on GitHub</a></li>
|
|
195
|
+
<li>Run <code>npx zquery bundle</code> to build for production</li>
|
|
196
|
+
<li>Run <code>npx zquery dev</code> for live-reload development</li>
|
|
197
|
+
</ul>
|
|
198
|
+
</div>
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
@@ -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
|
+
});
|