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.
- package/README.md +42 -17
- package/cli/args.js +33 -0
- package/cli/commands/build.js +58 -0
- package/cli/commands/bundle.js +584 -0
- package/cli/commands/create.js +67 -0
- package/cli/commands/dev.js +516 -0
- package/cli/help.js +92 -0
- package/cli/index.js +53 -0
- package/cli/scaffold/LICENSE +21 -0
- package/cli/scaffold/index.html +62 -0
- package/cli/scaffold/scripts/app.js +101 -0
- package/cli/scaffold/scripts/components/about.js +119 -0
- package/cli/scaffold/scripts/components/api-demo.js +103 -0
- package/cli/scaffold/scripts/components/contacts/contacts.css +253 -0
- package/cli/scaffold/scripts/components/contacts/contacts.html +139 -0
- package/cli/scaffold/scripts/components/contacts/contacts.js +137 -0
- package/cli/scaffold/scripts/components/counter.js +65 -0
- package/cli/scaffold/scripts/components/home.js +137 -0
- package/cli/scaffold/scripts/components/not-found.js +16 -0
- package/cli/scaffold/scripts/components/todos.js +130 -0
- package/cli/scaffold/scripts/routes.js +13 -0
- package/cli/scaffold/scripts/store.js +96 -0
- package/cli/scaffold/styles/styles.css +556 -0
- package/cli/utils.js +122 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +431 -61
- package/dist/zquery.min.js +5 -5
- package/index.d.ts +206 -66
- package/index.js +5 -4
- package/package.json +8 -8
- package/src/component.js +408 -52
- package/src/core.js +16 -3
- package/src/router.js +2 -2
- package/cli.js +0 -1208
|
@@ -0,0 +1,139 @@
|
|
|
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 & styles via <code>templateUrl</code> / <code>styleUrl</code>.
|
|
16
|
+
Directives: <code>z-if</code>, <code>z-for</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">
|
|
72
|
+
<!-- Contacts list — z-for renders each item -->
|
|
73
|
+
<ul class="contacts-list">
|
|
74
|
+
<li
|
|
75
|
+
z-for="contact in contacts"
|
|
76
|
+
class="contacts-item {{contact.id === selectedId ? 'selected' : ''}} {{contact.favorite ? 'is-favorite' : ''}}"
|
|
77
|
+
@click="selectContact({{contact.id}})"
|
|
78
|
+
>
|
|
79
|
+
<!-- Status indicator — class set per status -->
|
|
80
|
+
<span
|
|
81
|
+
class="status-dot status-{{contact.status}}"
|
|
82
|
+
title="{{contact.status}}"
|
|
83
|
+
></span>
|
|
84
|
+
|
|
85
|
+
<!-- Contact info -->
|
|
86
|
+
<div class="contacts-info">
|
|
87
|
+
<strong>{{contact.name}}</strong>
|
|
88
|
+
<small>{{contact.email}}</small>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Role badge -->
|
|
92
|
+
<span class="role-badge role-{{contact.role.toLowerCase()}}">{{contact.role}}</span>
|
|
93
|
+
|
|
94
|
+
<!-- Favorite toggle — .stop modifier prevents row click -->
|
|
95
|
+
<button
|
|
96
|
+
class="fav-btn {{contact.favorite ? 'is-fav' : ''}}"
|
|
97
|
+
@click.stop="toggleFavorite({{contact.id}})"
|
|
98
|
+
>{{contact.favorite ? '★' : '☆'}}</button>
|
|
99
|
+
</li>
|
|
100
|
+
</ul>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Empty state -->
|
|
104
|
+
<div class="card" z-else>
|
|
105
|
+
<div class="empty-state">
|
|
106
|
+
<p>No contacts yet — add one above!</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Selected contact detail panel — z-if conditional rendering -->
|
|
111
|
+
<div class="card contact-detail" z-if="selectedId !== null">
|
|
112
|
+
<div class="detail-header">
|
|
113
|
+
<div>
|
|
114
|
+
<h3 z-text="contacts.find(c => c.id === selectedId)?.name || ''"></h3>
|
|
115
|
+
<p class="muted" z-text="contacts.find(c => c.id === selectedId)?.email || ''"></p>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="detail-actions">
|
|
118
|
+
<button
|
|
119
|
+
class="btn btn-outline btn-sm"
|
|
120
|
+
@click="cycleStatus({{selectedId}})"
|
|
121
|
+
>
|
|
122
|
+
Status: <span z-text="contacts.find(c => c.id === selectedId)?.status || ''"></span>
|
|
123
|
+
</button>
|
|
124
|
+
|
|
125
|
+
<!-- Confirm delete pattern using z-if / z-else -->
|
|
126
|
+
<button
|
|
127
|
+
class="btn btn-danger btn-sm"
|
|
128
|
+
z-if="confirmDeleteId !== selectedId"
|
|
129
|
+
@click.stop="confirmDelete({{selectedId}})"
|
|
130
|
+
>Delete</button>
|
|
131
|
+
|
|
132
|
+
<span z-else class="confirm-group">
|
|
133
|
+
<span class="confirm-text">Sure?</span>
|
|
134
|
+
<button class="btn btn-danger btn-sm" @click.stop="deleteContact({{selectedId}})">Yes</button>
|
|
135
|
+
<button class="btn btn-ghost btn-sm" @click.stop="cancelDelete">No</button>
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// scripts/components/contacts/contacts.js — contact book
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: external templateUrl + styleUrl, z-if/z-else, z-for,
|
|
4
|
+
// z-show, z-bind/:attr, z-class, z-style, z-text, z-html,
|
|
5
|
+
// z-model, z-ref, z-cloak, @click, @submit.prevent,
|
|
6
|
+
// @input.debounce, event modifiers, and template {{expressions}}
|
|
7
|
+
//
|
|
8
|
+
// This component uses external files for its template and styles,
|
|
9
|
+
// resolved automatically relative to this JS file's location.
|
|
10
|
+
// Contacts are persisted in the global $.store('main') so they
|
|
11
|
+
// survive navigation between routes.
|
|
12
|
+
|
|
13
|
+
$.component('contacts-page', {
|
|
14
|
+
templateUrl: 'contacts.html',
|
|
15
|
+
styleUrl: 'contacts.css',
|
|
16
|
+
|
|
17
|
+
state: () => ({
|
|
18
|
+
contacts: [],
|
|
19
|
+
showForm: false,
|
|
20
|
+
newName: '',
|
|
21
|
+
newEmail: '',
|
|
22
|
+
newRole: 'Developer',
|
|
23
|
+
nameError: '',
|
|
24
|
+
emailError: '',
|
|
25
|
+
selectedId: null,
|
|
26
|
+
confirmDeleteId: null,
|
|
27
|
+
totalAdded: 0,
|
|
28
|
+
favoriteCount: 0,
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
mounted() {
|
|
32
|
+
const store = $.getStore('main');
|
|
33
|
+
this._syncFromStore(store);
|
|
34
|
+
this._unsub = store.subscribe(() => this._syncFromStore(store));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
destroyed() {
|
|
38
|
+
if (this._unsub) this._unsub();
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
_syncFromStore(store) {
|
|
42
|
+
this.state.contacts = store.state.contacts;
|
|
43
|
+
this.state.totalAdded = store.state.contactsAdded;
|
|
44
|
+
this.state.favoriteCount = store.getters.favoriteCount;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// -- Actions --
|
|
48
|
+
|
|
49
|
+
toggleForm() {
|
|
50
|
+
this.state.showForm = !this.state.showForm;
|
|
51
|
+
if (!this.state.showForm) this._clearForm();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
_validateName(name) {
|
|
55
|
+
if (!name) return 'Name is required.';
|
|
56
|
+
if (name.length < 2) return 'Name must be at least 2 characters.';
|
|
57
|
+
return '';
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
_validateEmail(email) {
|
|
61
|
+
if (!email) return 'Email is required.';
|
|
62
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Enter a valid email address.';
|
|
63
|
+
const store = $.getStore('main');
|
|
64
|
+
if (store.state.contacts.some(c => c.email.toLowerCase() === email.toLowerCase())) {
|
|
65
|
+
return 'A contact with this email already exists.';
|
|
66
|
+
}
|
|
67
|
+
return '';
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
validateName() {
|
|
71
|
+
this.state.nameError = this._validateName(this.state.newName.trim());
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
validateEmail() {
|
|
75
|
+
this.state.emailError = this._validateEmail(this.state.newEmail.trim());
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
addContact() {
|
|
79
|
+
const name = this.state.newName.trim();
|
|
80
|
+
const email = this.state.newEmail.trim();
|
|
81
|
+
|
|
82
|
+
const nameError = this._validateName(name);
|
|
83
|
+
const emailError = this._validateEmail(email);
|
|
84
|
+
this.state.nameError = nameError;
|
|
85
|
+
this.state.emailError = emailError;
|
|
86
|
+
if (nameError || emailError) return;
|
|
87
|
+
|
|
88
|
+
$.getStore('main').dispatch('addContact', {
|
|
89
|
+
name,
|
|
90
|
+
email,
|
|
91
|
+
role: this.state.newRole,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this._clearForm();
|
|
95
|
+
this.state.showForm = false;
|
|
96
|
+
$.bus.emit('toast', { message: `${name} added!`, type: 'success' });
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
toggleFavorite(id) {
|
|
100
|
+
$.getStore('main').dispatch('toggleFavorite', Number(id));
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
selectContact(id) {
|
|
104
|
+
this.state.selectedId = this.state.selectedId === Number(id) ? null : Number(id);
|
|
105
|
+
this.state.confirmDeleteId = null;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
confirmDelete(id) {
|
|
109
|
+
this.state.confirmDeleteId = Number(id);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
cancelDelete() {
|
|
113
|
+
this.state.confirmDeleteId = null;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
deleteContact(id) {
|
|
117
|
+
const numId = Number(id);
|
|
118
|
+
const store = $.getStore('main');
|
|
119
|
+
const c = store.state.contacts.find(c => c.id === numId);
|
|
120
|
+
store.dispatch('deleteContact', numId);
|
|
121
|
+
this.state.selectedId = null;
|
|
122
|
+
this.state.confirmDeleteId = null;
|
|
123
|
+
$.bus.emit('toast', { message: `${c ? c.name : 'Contact'} removed`, type: 'error' });
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
cycleStatus(id) {
|
|
127
|
+
$.getStore('main').dispatch('cycleContactStatus', Number(id));
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
_clearForm() {
|
|
131
|
+
this.state.newName = '';
|
|
132
|
+
this.state.newEmail = '';
|
|
133
|
+
this.state.newRole = 'Developer';
|
|
134
|
+
this.state.nameError = '';
|
|
135
|
+
this.state.emailError = '';
|
|
136
|
+
},
|
|
137
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// scripts/components/counter.js — interactive counter
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: component state, instance methods, @click event binding,
|
|
4
|
+
// z-model two-way binding with z-number modifier, z-class,
|
|
5
|
+
// z-if, z-for, $.bus toast notifications
|
|
6
|
+
|
|
7
|
+
$.component('counter-page', {
|
|
8
|
+
state: () => ({
|
|
9
|
+
count: 0,
|
|
10
|
+
step: 1,
|
|
11
|
+
history: [],
|
|
12
|
+
}),
|
|
13
|
+
|
|
14
|
+
increment() {
|
|
15
|
+
this.state.count += this.state.step;
|
|
16
|
+
this.state.history.push({ action: '+', value: this.state.step, result: this.state.count });
|
|
17
|
+
if (this.state.history.length > 8) this.state.history.shift();
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
decrement() {
|
|
21
|
+
this.state.count -= this.state.step;
|
|
22
|
+
this.state.history.push({ action: '−', value: this.state.step, result: this.state.count });
|
|
23
|
+
if (this.state.history.length > 8) this.state.history.shift();
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
reset() {
|
|
27
|
+
this.state.count = 0;
|
|
28
|
+
this.state.history = [];
|
|
29
|
+
$.bus.emit('toast', { message: 'Counter reset!', type: 'info' });
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
return `
|
|
34
|
+
<div class="page-header">
|
|
35
|
+
<h1>Counter</h1>
|
|
36
|
+
<p class="subtitle">Component state, <code>@click</code> handlers, <code>z-model</code>, <code>z-class</code>, and <code>z-for</code>.</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="card counter-card">
|
|
40
|
+
<div class="counter-display">
|
|
41
|
+
<span class="counter-value" z-class="{'negative': count < 0}">${this.state.count}</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="counter-controls">
|
|
45
|
+
<button class="btn btn-danger" @click="decrement">− Subtract</button>
|
|
46
|
+
<button class="btn btn-primary" @click="increment">+ Add</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="counter-step">
|
|
50
|
+
<label>Step size:
|
|
51
|
+
<input type="number" z-model="step" z-number min="1" max="100" class="input input-sm" />
|
|
52
|
+
</label>
|
|
53
|
+
<button class="btn btn-ghost btn-sm" @click="reset">Reset</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="card card-muted" z-if="history.length > 0">
|
|
58
|
+
<h3>History</h3>
|
|
59
|
+
<div class="history-list">
|
|
60
|
+
<span z-for="e in history" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// scripts/components/home.js — dashboard / landing page
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: $.component, state, render, mounted lifecycle,
|
|
4
|
+
// signal + computed + effect (reactive primitives),
|
|
5
|
+
// $.store integration, $.bus, template rendering
|
|
6
|
+
|
|
7
|
+
$.component('home-page', {
|
|
8
|
+
state: () => ({
|
|
9
|
+
greeting: '',
|
|
10
|
+
signalDemo: 0,
|
|
11
|
+
}),
|
|
12
|
+
|
|
13
|
+
mounted() {
|
|
14
|
+
// $.signal() — fine-grained reactive primitive
|
|
15
|
+
const count = $.signal(0);
|
|
16
|
+
|
|
17
|
+
// $.computed() — derived reactive value that auto-updates
|
|
18
|
+
const doubled = $.computed(() => count.value * 2);
|
|
19
|
+
|
|
20
|
+
// $.effect() — runs whenever its dependencies change
|
|
21
|
+
$.effect(() => {
|
|
22
|
+
this.state.signalDemo = doubled.value;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Store the signal setter so the button can use it
|
|
26
|
+
this._signalCount = count;
|
|
27
|
+
|
|
28
|
+
// Greet based on time of day
|
|
29
|
+
const hour = new Date().getHours();
|
|
30
|
+
this.state.greeting = hour < 12 ? 'Good morning'
|
|
31
|
+
: hour < 18 ? 'Good afternoon'
|
|
32
|
+
: 'Good evening';
|
|
33
|
+
|
|
34
|
+
// Track page visit via the global store
|
|
35
|
+
const store = $.getStore('main');
|
|
36
|
+
store.dispatch('incrementVisits');
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
incrementSignal() {
|
|
40
|
+
if (this._signalCount) {
|
|
41
|
+
this._signalCount.value++;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
render() {
|
|
46
|
+
const store = $.getStore('main');
|
|
47
|
+
return `
|
|
48
|
+
<div class="page-header">
|
|
49
|
+
<h1>${this.state.greeting} 👋</h1>
|
|
50
|
+
<p class="subtitle">Welcome to your new <strong>zQuery</strong> app. Explore the pages to see different features in action.</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="card-grid">
|
|
54
|
+
<div class="card card-accent">
|
|
55
|
+
<h3>⚡ Reactive Signals</h3>
|
|
56
|
+
<p>Fine-grained reactivity with <code>signal()</code>, <code>computed()</code>, and <code>effect()</code>.</p>
|
|
57
|
+
<div class="signal-demo">
|
|
58
|
+
<span class="signal-value">Doubled: ${this.state.signalDemo}</span>
|
|
59
|
+
<button class="btn btn-sm" @click="incrementSignal">Increment Signal</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="card">
|
|
64
|
+
<h3>🔢 Counter</h3>
|
|
65
|
+
<p>Component state, two-way binding with <code>z-model</code>, and event handling.</p>
|
|
66
|
+
<a z-link="/counter" class="btn btn-outline">Try It →</a>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="card">
|
|
70
|
+
<h3>✅ Todos</h3>
|
|
71
|
+
<p>Global store with actions & getters. <strong>${store.getters.todoCount}</strong> items, <strong>${store.getters.doneCount}</strong> done.</p>
|
|
72
|
+
<a z-link="/todos" class="btn btn-outline">Try It →</a>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="card">
|
|
76
|
+
<h3>📇 Contacts</h3>
|
|
77
|
+
<p>External templates & styles via <code>templateUrl</code> / <code>styleUrl</code>. <strong>${store.getters.contactCount}</strong> contacts, <strong>${store.getters.favoriteCount}</strong> ★ favorited.</p>
|
|
78
|
+
<a z-link="/contacts" class="btn btn-outline">Try It →</a>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="card">
|
|
82
|
+
<h3>🌐 API Demo</h3>
|
|
83
|
+
<p>Fetch data with <code>$.get()</code>, loading states, and <code>$.escapeHtml()</code>.</p>
|
|
84
|
+
<a z-link="/api" class="btn btn-outline">Try It →</a>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="card card-muted">
|
|
89
|
+
<h3>📊 App Stats</h3>
|
|
90
|
+
<div class="stats-grid">
|
|
91
|
+
<div class="stat-group">
|
|
92
|
+
<span class="stat-group-title">🏠 General</span>
|
|
93
|
+
<div class="stat-group-values">
|
|
94
|
+
<div class="stat">
|
|
95
|
+
<span class="stat-value">${store.state.visits}</span>
|
|
96
|
+
<span class="stat-label">Page Views</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="stat-group">
|
|
102
|
+
<span class="stat-group-title">✅ Todos</span>
|
|
103
|
+
<div class="stat-group-values">
|
|
104
|
+
<div class="stat">
|
|
105
|
+
<span class="stat-value">${store.getters.todoCount}</span>
|
|
106
|
+
<span class="stat-label">Total</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="stat">
|
|
109
|
+
<span class="stat-value">${store.getters.pendingCount}</span>
|
|
110
|
+
<span class="stat-label">Pending</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="stat">
|
|
113
|
+
<span class="stat-value">${store.getters.doneCount}</span>
|
|
114
|
+
<span class="stat-label">Done</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="stat-group">
|
|
120
|
+
<span class="stat-group-title">📇 Contacts</span>
|
|
121
|
+
<div class="stat-group-values">
|
|
122
|
+
<div class="stat">
|
|
123
|
+
<span class="stat-value">${store.getters.contactCount}</span>
|
|
124
|
+
<span class="stat-label">Total</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="stat">
|
|
127
|
+
<span class="stat-value">${store.getters.favoriteCount}</span>
|
|
128
|
+
<span class="stat-label">★ Favorited</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<small class="muted">Stats powered by <code>$.store()</code> getters — visit count tracked globally.</small>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// scripts/components/not-found.js — 404 fallback page
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: $.getRouter() to read the current path
|
|
4
|
+
|
|
5
|
+
$.component('not-found', {
|
|
6
|
+
render() {
|
|
7
|
+
const router = $.getRouter();
|
|
8
|
+
return `
|
|
9
|
+
<div class="page-header center">
|
|
10
|
+
<h1>404</h1>
|
|
11
|
+
<p class="subtitle">The page <code>${$.escapeHtml(router.current?.path || '')}</code> was not found.</p>
|
|
12
|
+
<a z-link="/" class="btn btn-primary">← Go Home</a>
|
|
13
|
+
</div>
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// scripts/components/todos.js — todo list with global store
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: $.getStore, store.dispatch, store.subscribe,
|
|
4
|
+
// store getters, z-model, z-ref, z-class, z-for,
|
|
5
|
+
// z-if, z-show, @click with args, @submit.prevent,
|
|
6
|
+
// mounted/destroyed lifecycle, $.bus toast, $.debounce
|
|
7
|
+
|
|
8
|
+
$.component('todos-page', {
|
|
9
|
+
state: () => ({
|
|
10
|
+
newTodo: '',
|
|
11
|
+
filter: 'all', // 'all' | 'active' | 'done'
|
|
12
|
+
search: '',
|
|
13
|
+
filtered: [], // computed in render() for z-for access
|
|
14
|
+
total: 0,
|
|
15
|
+
done: 0,
|
|
16
|
+
pending: 0,
|
|
17
|
+
}),
|
|
18
|
+
|
|
19
|
+
mounted() {
|
|
20
|
+
const store = $.getStore('main');
|
|
21
|
+
this._unsub = store.subscribe(() => this.setState({}));
|
|
22
|
+
|
|
23
|
+
// $.debounce — debounced search filter (300ms)
|
|
24
|
+
this._debouncedSearch = $.debounce((val) => {
|
|
25
|
+
this.state.search = val;
|
|
26
|
+
}, 300);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
destroyed() {
|
|
30
|
+
if (this._unsub) this._unsub();
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
addTodo() {
|
|
34
|
+
const text = this.state.newTodo.trim();
|
|
35
|
+
if (!text) return;
|
|
36
|
+
$.getStore('main').dispatch('addTodo', text);
|
|
37
|
+
this.state.newTodo = '';
|
|
38
|
+
this.state.search = '';
|
|
39
|
+
this.state.filter = 'all';
|
|
40
|
+
$.bus.emit('toast', { message: 'Todo added!', type: 'success' });
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
toggleTodo(id) {
|
|
44
|
+
$.getStore('main').dispatch('toggleTodo', id);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
removeTodo(id) {
|
|
48
|
+
$.getStore('main').dispatch('removeTodo', id);
|
|
49
|
+
$.bus.emit('toast', { message: 'Todo removed', type: 'error' });
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
clearCompleted() {
|
|
53
|
+
$.getStore('main').dispatch('clearCompleted');
|
|
54
|
+
$.bus.emit('toast', { message: 'Completed todos cleared', type: 'info' });
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
setFilter(f) {
|
|
58
|
+
this.state.filter = f;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
onSearch(e) {
|
|
62
|
+
this._debouncedSearch(e.target.value);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
render() {
|
|
66
|
+
const store = $.getStore('main');
|
|
67
|
+
const todos = store.state.todos;
|
|
68
|
+
const { filter, search } = this.state;
|
|
69
|
+
|
|
70
|
+
// Compute filtered list and store stats into state for directive access
|
|
71
|
+
let list = todos;
|
|
72
|
+
if (filter === 'active') list = todos.filter(t => !t.done);
|
|
73
|
+
if (filter === 'done') list = todos.filter(t => t.done);
|
|
74
|
+
if (search) {
|
|
75
|
+
const q = search.toLowerCase();
|
|
76
|
+
list = list.filter(t => t.text.toLowerCase().includes(q));
|
|
77
|
+
}
|
|
78
|
+
this.state.filtered = list;
|
|
79
|
+
this.state.total = store.getters.todoCount;
|
|
80
|
+
this.state.done = store.getters.doneCount;
|
|
81
|
+
this.state.pending = store.getters.pendingCount;
|
|
82
|
+
|
|
83
|
+
return `
|
|
84
|
+
<div class="page-header">
|
|
85
|
+
<h1>Todos</h1>
|
|
86
|
+
<p class="subtitle">Global store with <code>$.store()</code>, <code>z-for</code>, <code>z-class</code>, <code>z-if</code>, and <code>z-show</code>.</p>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="card">
|
|
90
|
+
<form class="todo-form" @submit.prevent="addTodo">
|
|
91
|
+
<input
|
|
92
|
+
type="text"
|
|
93
|
+
z-model="newTodo" z-trim
|
|
94
|
+
placeholder="What needs to be done?"
|
|
95
|
+
class="input"
|
|
96
|
+
z-ref="todoInput"
|
|
97
|
+
/>
|
|
98
|
+
<button type="submit" class="btn btn-primary">Add</button>
|
|
99
|
+
</form>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="card">
|
|
103
|
+
<div class="todo-toolbar">
|
|
104
|
+
<div class="todo-filters">
|
|
105
|
+
<button class="btn btn-sm" z-class="{'btn-primary': filter === 'all', 'btn-ghost': filter !== 'all'}" @click="setFilter('all')">All (${this.state.total})</button>
|
|
106
|
+
<button class="btn btn-sm" z-class="{'btn-primary': filter === 'active', 'btn-ghost': filter !== 'active'}" @click="setFilter('active')">Active (${this.state.pending})</button>
|
|
107
|
+
<button class="btn btn-sm" z-class="{'btn-primary': filter === 'done', 'btn-ghost': filter !== 'done'}" @click="setFilter('done')">Done (${this.state.done})</button>
|
|
108
|
+
</div>
|
|
109
|
+
<input type="text" placeholder="Search…" class="input input-sm" @input="onSearch" value="${$.escapeHtml(this.state.search)}" />
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div z-if="filtered.length === 0" class="empty-state">
|
|
113
|
+
<p>${this.state.total === 0 ? 'No todos yet — add one above!' : 'No matching todos.'}</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<ul z-else class="todo-list">
|
|
117
|
+
<li z-for="t in filtered" class="todo-item {{t.done ? 'done' : ''}}">
|
|
118
|
+
<button class="todo-check" @click="toggleTodo('{{t.id}}')"></button>
|
|
119
|
+
<span class="todo-text">{{$.escapeHtml(t.text)}}</span>
|
|
120
|
+
<button class="todo-remove" @click="removeTodo('{{t.id}}')">✕</button>
|
|
121
|
+
</li>
|
|
122
|
+
</ul>
|
|
123
|
+
|
|
124
|
+
<div class="todo-footer" z-show="done > 0">
|
|
125
|
+
<button class="btn btn-ghost btn-sm" @click="clearCompleted">Clear completed (${this.state.done})</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// scripts/routes.js — route definitions
|
|
2
|
+
//
|
|
3
|
+
// Each route maps a URL path to a component tag name.
|
|
4
|
+
// Supports: static paths, :params, wildcards, and lazy loading via `load`.
|
|
5
|
+
|
|
6
|
+
export const routes = [
|
|
7
|
+
{ path: '/', component: 'home-page' },
|
|
8
|
+
{ path: '/counter', component: 'counter-page' },
|
|
9
|
+
{ path: '/todos', component: 'todos-page' },
|
|
10
|
+
{ path: '/contacts', component: 'contacts-page' },
|
|
11
|
+
{ path: '/api', component: 'api-demo' },
|
|
12
|
+
{ path: '/about', component: 'about-page' },
|
|
13
|
+
];
|