zero-query 0.3.1 → 0.4.9

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.
@@ -0,0 +1,62 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{NAME}}</title>
7
+ <base href="/">
8
+ <link rel="stylesheet" href="styles/styles.css">
9
+ <script src="scripts/vendor/zquery.min.js"></script>
10
+ <script type="module" src="scripts/app.js"></script>
11
+ </head>
12
+ <body>
13
+
14
+ <!-- Sidebar Navigation -->
15
+ <aside class="sidebar" id="sidebar">
16
+ <div class="sidebar-header">
17
+ <span class="brand">⚡ {{NAME}}</span>
18
+ </div>
19
+ <nav class="sidebar-nav">
20
+ <a z-link="/" class="nav-link">
21
+ <span class="nav-icon">🏠</span> Home
22
+ </a>
23
+ <a z-link="/counter" class="nav-link">
24
+ <span class="nav-icon">🔢</span> Counter
25
+ </a>
26
+ <a z-link="/todos" class="nav-link">
27
+ <span class="nav-icon">✅</span> Todos
28
+ </a>
29
+ <a z-link="/contacts" class="nav-link">
30
+ <span class="nav-icon">📇</span> Contacts
31
+ </a>
32
+ <a z-link="/api" class="nav-link">
33
+ <span class="nav-icon">🌐</span> API Demo
34
+ </a>
35
+ <a z-link="/about" class="nav-link">
36
+ <span class="nav-icon">💡</span> About
37
+ </a>
38
+ </nav>
39
+ <div class="sidebar-footer">
40
+ <small>zQuery <span id="nav-version"></span></small>
41
+ </div>
42
+ </aside>
43
+
44
+ <!-- Mobile Top Bar -->
45
+ <header class="topbar" id="topbar">
46
+ <button class="hamburger" id="menu-toggle" aria-label="Toggle menu">
47
+ <span></span><span></span><span></span>
48
+ </button>
49
+ <span class="topbar-brand">⚡ {{NAME}}</span>
50
+ </header>
51
+
52
+ <!-- Overlay for mobile menu -->
53
+ <div class="overlay" id="overlay"></div>
54
+
55
+ <!-- Main Content -->
56
+ <main class="content" id="app"></main>
57
+
58
+ <!-- Toast container for notifications -->
59
+ <div class="toast-container" id="toasts"></div>
60
+
61
+ </body>
62
+ </html>
@@ -0,0 +1,101 @@
1
+ // scripts/app.js — application entry point
2
+ //
3
+ // This file bootstraps the zQuery app: imports all components,
4
+ // sets up routing, wires the responsive nav, and demonstrates
5
+ // several core APIs: $.router, $.ready, $.bus, $.on, and $.storage.
6
+
7
+ import './store.js';
8
+ import './components/home.js';
9
+ import './components/counter.js';
10
+ import './components/todos.js';
11
+ import './components/api-demo.js';
12
+ import './components/about.js';
13
+ import './components/contacts/contacts.js';
14
+ import './components/not-found.js';
15
+ import { routes } from './routes.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Router — SPA navigation with history mode
19
+ // ---------------------------------------------------------------------------
20
+ const router = $.router({
21
+ el: '#app',
22
+ routes,
23
+ fallback: 'not-found',
24
+ mode: 'history'
25
+ });
26
+
27
+ // Highlight the active nav link on every route change
28
+ router.onChange((to) => {
29
+ $.all('.nav-link').removeClass('active');
30
+ $.all(`.nav-link[z-link="${to.path}"]`).addClass('active');
31
+
32
+ // Close mobile menu on navigate
33
+ closeMobileMenu();
34
+ });
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Responsive sidebar toggle
38
+ // ---------------------------------------------------------------------------
39
+ const sidebar = $.id('sidebar');
40
+ const overlay = $.id('overlay');
41
+ const toggle = $.id('menu-toggle');
42
+
43
+ function openMobileMenu() {
44
+ sidebar.classList.add('open');
45
+ overlay.classList.add('visible');
46
+ toggle.classList.add('active');
47
+ }
48
+
49
+ function closeMobileMenu() {
50
+ sidebar.classList.remove('open');
51
+ overlay.classList.remove('visible');
52
+ toggle.classList.remove('active');
53
+ }
54
+
55
+ // $.on — global delegated event listeners
56
+ $.on('click', '#menu-toggle', () => {
57
+ sidebar.classList.contains('open') ? closeMobileMenu() : openMobileMenu();
58
+ });
59
+ $.on('click', '#overlay', closeMobileMenu);
60
+
61
+ // Close sidebar on Escape key — using $.on direct (no selector needed)
62
+ $.on('keydown', (e) => {
63
+ if (e.key === 'Escape') closeMobileMenu();
64
+ });
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Toast notification system via $.bus (event bus)
68
+ // ---------------------------------------------------------------------------
69
+ // Any component can emit: $.bus.emit('toast', { message, type })
70
+ // Types: 'success', 'error', 'info'
71
+ $.bus.on('toast', ({ message, type = 'info' }) => {
72
+ const container = $.id('toasts');
73
+ const toast = $.create('div');
74
+ toast.className = `toast toast-${type}`;
75
+ toast.textContent = message;
76
+ container.appendChild(toast);
77
+ // Auto-remove after 3 seconds
78
+ setTimeout(() => {
79
+ toast.classList.add('toast-exit');
80
+ setTimeout(() => toast.remove(), 300);
81
+ }, 3000);
82
+ });
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // On DOM ready — final setup
86
+ // ---------------------------------------------------------------------------
87
+ $.ready(() => {
88
+ // Display version in the sidebar footer
89
+ const versionEl = $.id('nav-version');
90
+ if (versionEl) versionEl.textContent = 'v' + $.version;
91
+
92
+ // Restore last theme from localStorage ($.storage)
93
+ const savedTheme = $.storage.get('theme');
94
+ if (savedTheme) document.documentElement.setAttribute('data-theme', savedTheme);
95
+
96
+ // Set active link on initial load
97
+ const current = window.location.pathname;
98
+ $.all(`.nav-link[z-link="${current}"]`).addClass('active');
99
+
100
+ console.log('⚡ {{NAME}} — powered by zQuery v' + $.version);
101
+ });
@@ -0,0 +1,119 @@
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>🎨 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' ? '☀️ Light Mode' : '🌙 Dark Mode'}</button>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="card">
43
+ <h3>🧰 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>$.router()</strong>
51
+ <span>SPA routing with history mode, z-link navigation, and fallback pages</span>
52
+ </div>
53
+ <div class="feature-item">
54
+ <strong>$.store()</strong>
55
+ <span>Centralized state management with actions, getters, and subscriptions</span>
56
+ </div>
57
+ <div class="feature-item">
58
+ <strong>$.get()</strong>
59
+ <span>HTTP client for fetching JSON APIs with async/await</span>
60
+ </div>
61
+ <div class="feature-item">
62
+ <strong>$.signal() / $.computed()</strong>
63
+ <span>Fine-grained reactive primitives for derived state</span>
64
+ </div>
65
+ <div class="feature-item">
66
+ <strong>$.bus</strong>
67
+ <span>Event bus for cross-component communication (toast notifications)</span>
68
+ </div>
69
+ <div class="feature-item">
70
+ <strong>$.storage</strong>
71
+ <span>localStorage wrapper for persisting user preferences</span>
72
+ </div>
73
+ <div class="feature-item">
74
+ <strong>$.debounce()</strong>
75
+ <span>Debounced search input in the todos filter</span>
76
+ </div>
77
+ <div class="feature-item">
78
+ <strong>$.escapeHtml()</strong>
79
+ <span>Safe rendering of user-generated and API content</span>
80
+ </div>
81
+ <div class="feature-item">
82
+ <strong>$.uuid()</strong>
83
+ <span>Unique ID generation for new todo items</span>
84
+ </div>
85
+ <div class="feature-item">
86
+ <strong>z-model / z-ref</strong>
87
+ <span>Two-way data binding and DOM element references</span>
88
+ </div>
89
+ <div class="feature-item">
90
+ <strong>templateUrl / styleUrl</strong>
91
+ <span>External HTML templates and CSS with auto-scoping (contacts page)</span>
92
+ </div>
93
+ <div class="feature-item">
94
+ <strong>z-if / z-for / z-show</strong>
95
+ <span>Structural directives for conditional &amp; list rendering</span>
96
+ </div>
97
+ <div class="feature-item">
98
+ <strong>z-bind / z-class / z-style</strong>
99
+ <span>Dynamic attributes, classes, and inline styles</span>
100
+ </div>
101
+ <div class="feature-item">
102
+ <strong>$.on()</strong>
103
+ <span>Global delegated event listeners for the hamburger menu</span>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="card card-muted">
109
+ <h3>📚 Next Steps</h3>
110
+ <ul class="next-steps">
111
+ <li>Read the <a href="https://z-query.com/docs" target="_blank" rel="noopener">full documentation</a></li>
112
+ <li>Explore the <a href="https://github.com/tonywied17/zero-query" target="_blank" rel="noopener">source on GitHub</a></li>
113
+ <li>Run <code>npx zquery bundle</code> to build for production</li>
114
+ <li>Run <code>npx zquery dev</code> for live-reload development</li>
115
+ </ul>
116
+ </div>
117
+ `;
118
+ }
119
+ });
@@ -0,0 +1,103 @@
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>⚠ <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
+ });
@@ -0,0 +1,253 @@
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
+
225
+ /* -- Animation -- */
226
+ @keyframes slide-in {
227
+ from { opacity: 0; transform: translateY(-6px); }
228
+ to { opacity: 1; transform: translateY(0); }
229
+ }
230
+
231
+ /* -- Responsive -- */
232
+ @media (max-width: 768px) {
233
+ .contacts-toolbar-row {
234
+ flex-direction: column;
235
+ }
236
+
237
+ .contacts-form {
238
+ grid-template-columns: 1fr;
239
+ }
240
+
241
+ .contacts-item {
242
+ flex-wrap: wrap;
243
+ }
244
+
245
+ .role-badge {
246
+ order: 10;
247
+ margin-left: calc(10px + 0.75rem);
248
+ }
249
+
250
+ .detail-header {
251
+ flex-direction: column;
252
+ }
253
+ }