zero-query 0.5.2 → 0.7.5
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 +12 -10
- package/cli/commands/build.js +7 -5
- package/cli/commands/bundle.js +286 -8
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +366 -0
- package/cli/commands/dev/server.js +158 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +147 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/index.html +1 -0
- package/cli/scaffold/scripts/app.js +15 -22
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
- package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- package/cli/scaffold/styles/styles.css +1 -0
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2005 -216
- package/dist/zquery.min.js +3 -13
- package/index.d.ts +149 -1080
- package/index.js +18 -7
- package/package.json +9 -3
- package/src/component.js +186 -45
- package/src/core.js +327 -35
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +59 -6
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- package/tests/component.test.js +304 -0
- package/tests/core.test.js +726 -0
- package/tests/diff.test.js +194 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +334 -0
- package/tests/http.test.js +181 -0
- package/tests/reactive.test.js +191 -0
- package/tests/router.test.js +332 -0
- package/tests/store.test.js +253 -0
- package/tests/utils.test.js +353 -0
- package/types/collection.d.ts +368 -0
- package/types/component.d.ts +210 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +166 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +132 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
- /package/cli/commands/{dev.js → dev.old.js} +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/dev/watcher.js — File system watcher
|
|
3
|
+
*
|
|
4
|
+
* Recursively watches the project root for file changes, validates
|
|
5
|
+
* JS files for syntax errors, and broadcasts reload / CSS hot-swap /
|
|
6
|
+
* error events through the SSE pool.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const { validateJS } = require('./validator');
|
|
15
|
+
const { logCSS, logReload, logError } = require('./logger');
|
|
16
|
+
|
|
17
|
+
const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.cache']);
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function shouldWatch(filename) {
|
|
24
|
+
return !!filename && !filename.startsWith('.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isIgnored(filepath) {
|
|
28
|
+
return filepath.split(path.sep).some(p => IGNORE_DIRS.has(p));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Return the file's mtime as a millisecond timestamp, or 0 if unreadable.
|
|
33
|
+
* Used to ignore spurious fs.watch events (Windows fires on reads too).
|
|
34
|
+
*/
|
|
35
|
+
function mtime(filepath) {
|
|
36
|
+
try { return fs.statSync(filepath).mtimeMs; } catch { return 0; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Recursively collect every directory under `dir` (excluding ignored). */
|
|
40
|
+
function collectWatchDirs(dir) {
|
|
41
|
+
const dirs = [dir];
|
|
42
|
+
try {
|
|
43
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
if (!entry.isDirectory() || IGNORE_DIRS.has(entry.name)) continue;
|
|
45
|
+
dirs.push(...collectWatchDirs(path.join(dir, entry.name)));
|
|
46
|
+
}
|
|
47
|
+
} catch { /* unreadable dir — skip */ }
|
|
48
|
+
return dirs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Watcher factory
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Start watching `root` for file changes.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} opts
|
|
59
|
+
* @param {string} opts.root — absolute project root
|
|
60
|
+
* @param {SSEPool} opts.pool — SSE broadcast pool
|
|
61
|
+
* @returns {{ dirs: string[], destroy: Function }}
|
|
62
|
+
*/
|
|
63
|
+
function startWatcher({ root, pool }) {
|
|
64
|
+
const watchDirs = collectWatchDirs(root);
|
|
65
|
+
const watchers = [];
|
|
66
|
+
|
|
67
|
+
let debounceTimer;
|
|
68
|
+
let currentError = null; // track which file has an active error
|
|
69
|
+
|
|
70
|
+
// Track file mtimes so we only react to genuine writes.
|
|
71
|
+
// On Windows, fs.watch fires on reads/access too, which causes
|
|
72
|
+
// spurious reloads the first time the server serves a file.
|
|
73
|
+
// We seed the cache with current mtimes so the first real save
|
|
74
|
+
// (which changes the mtime) is always detected.
|
|
75
|
+
const mtimeCache = new Map();
|
|
76
|
+
for (const d of watchDirs) {
|
|
77
|
+
try {
|
|
78
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
79
|
+
if (entry.isFile()) {
|
|
80
|
+
const fp = path.join(d, entry.name);
|
|
81
|
+
const mt = mtime(fp);
|
|
82
|
+
if (mt) mtimeCache.set(fp, mt);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch { /* skip */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const dir of watchDirs) {
|
|
89
|
+
try {
|
|
90
|
+
const watcher = fs.watch(dir, (_, filename) => {
|
|
91
|
+
if (!shouldWatch(filename)) return;
|
|
92
|
+
const fullPath = path.join(dir, filename || '');
|
|
93
|
+
if (isIgnored(fullPath)) return;
|
|
94
|
+
|
|
95
|
+
clearTimeout(debounceTimer);
|
|
96
|
+
debounceTimer = setTimeout(() => {
|
|
97
|
+
// Skip if the file hasn't actually been modified
|
|
98
|
+
const mt = mtime(fullPath);
|
|
99
|
+
if (mt === 0) return; // deleted or unreadable
|
|
100
|
+
const prev = mtimeCache.get(fullPath);
|
|
101
|
+
mtimeCache.set(fullPath, mt);
|
|
102
|
+
if (prev !== undefined && mt === prev) return; // unchanged
|
|
103
|
+
|
|
104
|
+
const rel = path.relative(root, fullPath).replace(/\\/g, '/');
|
|
105
|
+
const ext = path.extname(filename).toLowerCase();
|
|
106
|
+
|
|
107
|
+
// ---- CSS hot-swap ----
|
|
108
|
+
if (ext === '.css') {
|
|
109
|
+
logCSS(rel);
|
|
110
|
+
pool.broadcast('css', rel);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- JS syntax check ----
|
|
115
|
+
if (ext === '.js') {
|
|
116
|
+
const err = validateJS(fullPath, rel);
|
|
117
|
+
if (err) {
|
|
118
|
+
currentError = rel;
|
|
119
|
+
logError(err);
|
|
120
|
+
pool.broadcast('error:syntax', JSON.stringify(err));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// File was fixed — clear previous overlay
|
|
124
|
+
if (currentError === rel) {
|
|
125
|
+
currentError = null;
|
|
126
|
+
pool.broadcast('error:clear', '');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---- Full reload ----
|
|
131
|
+
logReload(rel);
|
|
132
|
+
pool.broadcast('reload', rel);
|
|
133
|
+
}, 100);
|
|
134
|
+
});
|
|
135
|
+
watchers.push(watcher);
|
|
136
|
+
} catch { /* dir became inaccessible — skip */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function destroy() {
|
|
140
|
+
clearTimeout(debounceTimer);
|
|
141
|
+
watchers.forEach(w => w.close());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { dirs: watchDirs, destroy };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { startWatcher, collectWatchDirs };
|
|
Binary file
|
package/cli/scaffold/index.html
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<title>{{NAME}}</title>
|
|
7
7
|
<base href="/">
|
|
8
8
|
<link rel="stylesheet" href="styles/styles.css">
|
|
9
|
+
<link rel="icon" type="image/png" href="favicon.ico">
|
|
9
10
|
<script src="scripts/vendor/zquery.min.js"></script>
|
|
10
11
|
<script type="module" src="scripts/app.js"></script>
|
|
11
12
|
</head>
|
|
@@ -27,7 +27,7 @@ const router = $.router({
|
|
|
27
27
|
// Highlight the active nav link on every route change
|
|
28
28
|
router.onChange((to) => {
|
|
29
29
|
$.all('.nav-link').removeClass('active');
|
|
30
|
-
|
|
30
|
+
$(`.nav-link[z-link="${to.path}"]`).addClass('active');
|
|
31
31
|
|
|
32
32
|
// Close mobile menu on navigate
|
|
33
33
|
closeMobileMenu();
|
|
@@ -36,26 +36,20 @@ router.onChange((to) => {
|
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
37
|
// Responsive sidebar toggle
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
|
-
const sidebar =
|
|
40
|
-
const overlay =
|
|
41
|
-
const toggle =
|
|
39
|
+
const $sidebar = $('#sidebar');
|
|
40
|
+
const $overlay = $('#overlay');
|
|
41
|
+
const $toggle = $('#menu-toggle');
|
|
42
42
|
|
|
43
|
-
function
|
|
44
|
-
sidebar.
|
|
45
|
-
overlay.
|
|
46
|
-
toggle.
|
|
43
|
+
function toggleMobileMenu(open) {
|
|
44
|
+
$sidebar.toggleClass('open', open);
|
|
45
|
+
$overlay.toggleClass('visible', open);
|
|
46
|
+
$toggle.toggleClass('active', open);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function closeMobileMenu() {
|
|
50
|
-
sidebar.classList.remove('open');
|
|
51
|
-
overlay.classList.remove('visible');
|
|
52
|
-
toggle.classList.remove('active');
|
|
53
|
-
}
|
|
49
|
+
function closeMobileMenu() { toggleMobileMenu(false); }
|
|
54
50
|
|
|
55
51
|
// $.on — global delegated event listeners
|
|
56
|
-
$.on('click', '#menu-toggle', () =>
|
|
57
|
-
sidebar.classList.contains('open') ? closeMobileMenu() : openMobileMenu();
|
|
58
|
-
});
|
|
52
|
+
$.on('click', '#menu-toggle', () => toggleMobileMenu(!$sidebar.hasClass('open')));
|
|
59
53
|
$.on('click', '#overlay', closeMobileMenu);
|
|
60
54
|
|
|
61
55
|
// Close sidebar on Escape key — using $.on direct (no selector needed)
|
|
@@ -69,14 +63,13 @@ $.on('keydown', (e) => {
|
|
|
69
63
|
// Any component can emit: $.bus.emit('toast', { message, type })
|
|
70
64
|
// Types: 'success', 'error', 'info'
|
|
71
65
|
$.bus.on('toast', ({ message, type = 'info' }) => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
container.appendChild(toast);
|
|
66
|
+
const toast = $.create('div')
|
|
67
|
+
.addClass('toast', `toast-${type}`)
|
|
68
|
+
.text(message)
|
|
69
|
+
.appendTo('#toasts');
|
|
77
70
|
// Auto-remove after 3 seconds
|
|
78
71
|
setTimeout(() => {
|
|
79
|
-
toast.
|
|
72
|
+
toast.addClass('toast-exit');
|
|
80
73
|
setTimeout(() => toast.remove(), 300);
|
|
81
74
|
}, 3000);
|
|
82
75
|
});
|
|
@@ -46,6 +46,18 @@ $.component('about-page', {
|
|
|
46
46
|
<strong>$.component()</strong>
|
|
47
47
|
<span>Reactive components with state, lifecycle hooks, and template rendering</span>
|
|
48
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>
|
|
49
61
|
<div class="feature-item">
|
|
50
62
|
<strong>$.router()</strong>
|
|
51
63
|
<span>SPA routing with history mode, z-link navigation, and fallback pages</span>
|
|
@@ -79,8 +91,8 @@ $.component('about-page', {
|
|
|
79
91
|
<span>Safe rendering of user-generated and API content</span>
|
|
80
92
|
</div>
|
|
81
93
|
<div class="feature-item">
|
|
82
|
-
<strong
|
|
83
|
-
<span>
|
|
94
|
+
<strong>CSP-safe expressions</strong>
|
|
95
|
+
<span>Template expressions evaluated without <code>eval()</code> or <code>new Function()</code></span>
|
|
84
96
|
</div>
|
|
85
97
|
<div class="feature-item">
|
|
86
98
|
<strong>z-model / z-ref</strong>
|
|
@@ -216,13 +216,6 @@
|
|
|
216
216
|
font-weight: 500;
|
|
217
217
|
}
|
|
218
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
219
|
@keyframes slide-in {
|
|
227
220
|
from { opacity: 0; transform: translateY(-6px); }
|
|
228
221
|
to { opacity: 1; transform: translateY(0); }
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<h1>Contacts</h1>
|
|
14
14
|
<p class="subtitle">
|
|
15
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>,
|
|
16
|
+
Directives: <code>z-if</code>, <code>z-for</code> + <code>z-key</code>, <code>z-show</code>,
|
|
17
17
|
<code>z-bind</code>, <code>z-class</code>, <code>z-model</code>, and more.
|
|
18
18
|
</p>
|
|
19
19
|
</div>
|
|
@@ -68,11 +68,12 @@
|
|
|
68
68
|
</div>
|
|
69
69
|
|
|
70
70
|
<!-- Contact count -->
|
|
71
|
-
<div class="card" z-if="contacts.length > 0">
|
|
71
|
+
<div class="card" z-if="contacts.length > 0" z-key="contacts-list">
|
|
72
72
|
<!-- Contacts list — z-for renders each item -->
|
|
73
73
|
<ul class="contacts-list">
|
|
74
74
|
<li
|
|
75
75
|
z-for="contact in contacts"
|
|
76
|
+
z-key="{{contact.id}}"
|
|
76
77
|
class="contacts-item {{contact.id === selectedId ? 'selected' : ''}} {{contact.favorite ? 'is-favorite' : ''}}"
|
|
77
78
|
@click="selectContact({{contact.id}})"
|
|
78
79
|
>
|
|
@@ -101,25 +102,25 @@
|
|
|
101
102
|
</div>
|
|
102
103
|
|
|
103
104
|
<!-- Empty state -->
|
|
104
|
-
<div class="card" z-else>
|
|
105
|
+
<div class="card" z-else z-key="contacts-empty">
|
|
105
106
|
<div class="empty-state">
|
|
106
107
|
<p>No contacts yet — add one above!</p>
|
|
107
108
|
</div>
|
|
108
109
|
</div>
|
|
109
110
|
|
|
110
111
|
<!-- Selected contact detail panel — z-if conditional rendering -->
|
|
111
|
-
<div class="card contact-detail" z-if="selectedId !== null">
|
|
112
|
+
<div class="card contact-detail" z-if="selectedId !== null" z-key="contact-detail">
|
|
112
113
|
<div class="detail-header">
|
|
113
114
|
<div>
|
|
114
|
-
<h3 z-text="
|
|
115
|
-
<p class="muted" z-text="
|
|
115
|
+
<h3 z-text="selectedName"></h3>
|
|
116
|
+
<p class="muted" z-text="selectedEmail"></p>
|
|
116
117
|
</div>
|
|
117
118
|
<div class="detail-actions">
|
|
118
119
|
<button
|
|
119
120
|
class="btn btn-outline btn-sm"
|
|
120
121
|
@click="cycleStatus({{selectedId}})"
|
|
121
122
|
>
|
|
122
|
-
Status: <span z-text="
|
|
123
|
+
Status: <span z-text="selectedStatus"></span>
|
|
123
124
|
</button>
|
|
124
125
|
|
|
125
126
|
<!-- Confirm delete pattern using z-if / z-else -->
|
|
@@ -23,6 +23,9 @@ $.component('contacts-page', {
|
|
|
23
23
|
nameError: '',
|
|
24
24
|
emailError: '',
|
|
25
25
|
selectedId: null,
|
|
26
|
+
selectedName: '',
|
|
27
|
+
selectedEmail: '',
|
|
28
|
+
selectedStatus: '',
|
|
26
29
|
confirmDeleteId: null,
|
|
27
30
|
totalAdded: 0,
|
|
28
31
|
favoriteCount: 0,
|
|
@@ -42,6 +45,7 @@ $.component('contacts-page', {
|
|
|
42
45
|
this.state.contacts = store.state.contacts;
|
|
43
46
|
this.state.totalAdded = store.state.contactsAdded;
|
|
44
47
|
this.state.favoriteCount = store.getters.favoriteCount;
|
|
48
|
+
this._syncSelected();
|
|
45
49
|
},
|
|
46
50
|
|
|
47
51
|
// -- Actions --
|
|
@@ -101,8 +105,10 @@ $.component('contacts-page', {
|
|
|
101
105
|
},
|
|
102
106
|
|
|
103
107
|
selectContact(id) {
|
|
104
|
-
|
|
108
|
+
const numId = Number(id);
|
|
109
|
+
this.state.selectedId = this.state.selectedId === numId ? null : numId;
|
|
105
110
|
this.state.confirmDeleteId = null;
|
|
111
|
+
this._syncSelected();
|
|
106
112
|
},
|
|
107
113
|
|
|
108
114
|
confirmDelete(id) {
|
|
@@ -125,6 +131,16 @@ $.component('contacts-page', {
|
|
|
125
131
|
|
|
126
132
|
cycleStatus(id) {
|
|
127
133
|
$.getStore('main').dispatch('cycleContactStatus', Number(id));
|
|
134
|
+
this._syncSelected();
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
_syncSelected() {
|
|
138
|
+
const c = this.state.selectedId != null
|
|
139
|
+
? this.state.contacts.find(c => c.id === this.state.selectedId)
|
|
140
|
+
: null;
|
|
141
|
+
this.state.selectedName = c ? c.name : '';
|
|
142
|
+
this.state.selectedEmail = c ? c.email : '';
|
|
143
|
+
this.state.selectedStatus = c ? c.status : '';
|
|
128
144
|
},
|
|
129
145
|
|
|
130
146
|
_clearForm() {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// scripts/components/counter.js — interactive counter
|
|
2
2
|
//
|
|
3
|
-
// Demonstrates: component state,
|
|
4
|
-
// z-model two-way binding with
|
|
5
|
-
// z-if, z-for
|
|
3
|
+
// Demonstrates: component state, computed properties, watch callbacks,
|
|
4
|
+
// @click event binding, z-model two-way binding with
|
|
5
|
+
// z-number modifier, z-class, z-if, z-for with z-key,
|
|
6
|
+
// $.bus toast notifications
|
|
6
7
|
|
|
7
8
|
$.component('counter-page', {
|
|
8
9
|
state: () => ({
|
|
@@ -11,16 +12,35 @@ $.component('counter-page', {
|
|
|
11
12
|
history: [],
|
|
12
13
|
}),
|
|
13
14
|
|
|
15
|
+
// Computed properties — derived values that update automatically
|
|
16
|
+
computed: {
|
|
17
|
+
isNegative: (state) => state.count < 0,
|
|
18
|
+
historyCount: (state) => state.history.length,
|
|
19
|
+
lastAction: (state) => state.history.length ? state.history[state.history.length - 1] : null,
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Watch — react to specific state changes
|
|
23
|
+
watch: {
|
|
24
|
+
count(val) {
|
|
25
|
+
if (val === 100) $.bus.emit('toast', { message: 'Century! 🎉', type: 'success' });
|
|
26
|
+
if (val === -100) $.bus.emit('toast', { message: 'Negative century!', type: 'error' });
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
14
30
|
increment() {
|
|
15
31
|
this.state.count += this.state.step;
|
|
16
|
-
this.
|
|
17
|
-
if (this.state.history.length > 8) this.state.history.shift();
|
|
32
|
+
this._pushHistory('+', this.state.step, this.state.count);
|
|
18
33
|
},
|
|
19
34
|
|
|
20
35
|
decrement() {
|
|
21
36
|
this.state.count -= this.state.step;
|
|
22
|
-
this.
|
|
23
|
-
|
|
37
|
+
this._pushHistory('−', this.state.step, this.state.count);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
_pushHistory(action, value, result) {
|
|
41
|
+
const raw = this.state.history.__raw || this.state.history;
|
|
42
|
+
const next = [...raw, { id: Date.now(), action, value, result }];
|
|
43
|
+
this.state.history = next.length > 8 ? next.slice(-8) : next;
|
|
24
44
|
},
|
|
25
45
|
|
|
26
46
|
reset() {
|
|
@@ -33,7 +53,7 @@ $.component('counter-page', {
|
|
|
33
53
|
return `
|
|
34
54
|
<div class="page-header">
|
|
35
55
|
<h1>Counter</h1>
|
|
36
|
-
<p class="subtitle">
|
|
56
|
+
<p class="subtitle"><code>computed</code>, <code>watch</code>, <code>@click</code>, <code>z-model</code>, <code>z-class</code>, and <code>z-for</code> with <code>z-key</code>.</p>
|
|
37
57
|
</div>
|
|
38
58
|
|
|
39
59
|
<div class="card counter-card">
|
|
@@ -55,9 +75,9 @@ $.component('counter-page', {
|
|
|
55
75
|
</div>
|
|
56
76
|
|
|
57
77
|
<div class="card card-muted" z-if="history.length > 0">
|
|
58
|
-
<h3>History</h3>
|
|
78
|
+
<h3>History <small style="color:var(--text-muted);font-weight:400;">(${this.computed.historyCount} entries)</small></h3>
|
|
59
79
|
<div class="history-list">
|
|
60
|
-
<span z-for="e in history" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
|
|
80
|
+
<span z-for="e in history" z-key="{{e.id}}" class="history-item">{{e.action}}{{e.value}} → <strong>{{e.result}}</strong></span>
|
|
61
81
|
</div>
|
|
62
82
|
</div>
|
|
63
83
|
`;
|
|
@@ -62,19 +62,19 @@ $.component('home-page', {
|
|
|
62
62
|
|
|
63
63
|
<div class="card">
|
|
64
64
|
<h3>🔢 Counter</h3>
|
|
65
|
-
<p>
|
|
65
|
+
<p><code>computed</code> properties, <code>watch</code> callbacks, and <code>z-for</code> with <code>z-key</code> diffing.</p>
|
|
66
66
|
<a z-link="/counter" class="btn btn-outline">Try It →</a>
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
69
|
<div class="card">
|
|
70
70
|
<h3>✅ Todos</h3>
|
|
71
|
-
<p>Global store
|
|
71
|
+
<p>Global store, <code>z-key</code> keyed lists, DOM diffing. <strong>${store.getters.todoCount}</strong> items, <strong>${store.getters.doneCount}</strong> done.</p>
|
|
72
72
|
<a z-link="/todos" class="btn btn-outline">Try It →</a>
|
|
73
73
|
</div>
|
|
74
74
|
|
|
75
75
|
<div class="card">
|
|
76
76
|
<h3>📇 Contacts</h3>
|
|
77
|
-
<p>External templates
|
|
77
|
+
<p>External templates, scoped styles, and <code>z-key</code> keyed lists. <strong>${store.getters.contactCount}</strong> contacts, <strong>${store.getters.favoriteCount}</strong> ★ favorited.</p>
|
|
78
78
|
<a z-link="/contacts" class="btn btn-outline">Try It →</a>
|
|
79
79
|
</div>
|
|
80
80
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// scripts/components/todos.js — todo list with global store
|
|
2
2
|
//
|
|
3
3
|
// Demonstrates: $.getStore, store.dispatch, store.subscribe,
|
|
4
|
-
// store getters,
|
|
5
|
-
// z-if, z-show, @click
|
|
6
|
-
//
|
|
4
|
+
// store getters, computed properties, z-model, z-ref,
|
|
5
|
+
// z-class, z-for with z-key, z-if, z-show, @click
|
|
6
|
+
// with args, @submit.prevent, mounted/destroyed
|
|
7
|
+
// lifecycle, $.bus toast, $.debounce
|
|
7
8
|
|
|
8
9
|
$.component('todos-page', {
|
|
9
10
|
state: () => ({
|
|
@@ -83,7 +84,7 @@ $.component('todos-page', {
|
|
|
83
84
|
return `
|
|
84
85
|
<div class="page-header">
|
|
85
86
|
<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
|
+
<p class="subtitle">Global store with <code>$.store()</code>, <code>z-for</code> + <code>z-key</code>, <code>z-class</code>, <code>z-if</code>, and <code>z-show</code>.</p>
|
|
87
88
|
</div>
|
|
88
89
|
|
|
89
90
|
<div class="card">
|
|
@@ -114,7 +115,7 @@ $.component('todos-page', {
|
|
|
114
115
|
</div>
|
|
115
116
|
|
|
116
117
|
<ul z-else class="todo-list">
|
|
117
|
-
<li z-for="t in filtered" class="todo-item {{t.done ? 'done' : ''}}">
|
|
118
|
+
<li z-for="t in filtered" z-key="{{t.id}}" class="todo-item {{t.done ? 'done' : ''}}">
|
|
118
119
|
<button class="todo-check" @click="toggleTodo('{{t.id}}')"></button>
|
|
119
120
|
<span class="todo-text">{{$.escapeHtml(t.text)}}</span>
|
|
120
121
|
<button class="todo-remove" @click="removeTodo('{{t.id}}')">✕</button>
|
package/cli/utils.js
CHANGED
|
@@ -83,15 +83,120 @@ function stripComments(code) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
|
-
// minify —
|
|
86
|
+
// minify — single-pass minification
|
|
87
|
+
// Strips comments, collapses whitespace to the minimum required,
|
|
88
|
+
// and preserves string / template-literal / regex content verbatim.
|
|
87
89
|
// ---------------------------------------------------------------------------
|
|
88
90
|
|
|
89
91
|
function minify(code, banner) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
return banner + '\n' + _minifyBody(code.replace(banner, ''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Single-pass minifier: walks character-by-character, skips strings/regex,
|
|
97
|
+
* strips comments, and emits a space only when both neighbours are
|
|
98
|
+
* identifier-like characters (or when collapsing would create ++, --, // or /*).
|
|
99
|
+
*/
|
|
100
|
+
function _minifyBody(code) {
|
|
101
|
+
let out = '';
|
|
102
|
+
let i = 0;
|
|
103
|
+
|
|
104
|
+
while (i < code.length) {
|
|
105
|
+
const ch = code[i];
|
|
106
|
+
const nx = code[i + 1];
|
|
107
|
+
|
|
108
|
+
// ── String / template literal: copy verbatim ────────────────
|
|
109
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
110
|
+
const q = ch;
|
|
111
|
+
out += ch; i++;
|
|
112
|
+
while (i < code.length) {
|
|
113
|
+
if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
|
|
114
|
+
out += code[i];
|
|
115
|
+
if (code[i] === q) { i++; break; }
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Block comment: skip ─────────────────────────────────────
|
|
122
|
+
if (ch === '/' && nx === '*') {
|
|
123
|
+
i += 2;
|
|
124
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) i++;
|
|
125
|
+
i += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Line comment: skip ──────────────────────────────────────
|
|
130
|
+
if (ch === '/' && nx === '/') {
|
|
131
|
+
i += 2;
|
|
132
|
+
while (i < code.length && code[i] !== '\n') i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Regex literal: copy verbatim ────────────────────────────
|
|
137
|
+
if (ch === '/') {
|
|
138
|
+
if (_isRegexCtx(out)) {
|
|
139
|
+
out += ch; i++;
|
|
140
|
+
let inCC = false;
|
|
141
|
+
while (i < code.length) {
|
|
142
|
+
const rc = code[i];
|
|
143
|
+
if (rc === '\\') { out += rc + (code[i + 1] || ''); i += 2; continue; }
|
|
144
|
+
if (rc === '[') inCC = true;
|
|
145
|
+
if (rc === ']') inCC = false;
|
|
146
|
+
out += rc; i++;
|
|
147
|
+
if (rc === '/' && !inCC) {
|
|
148
|
+
while (i < code.length && /[gimsuy]/.test(code[i])) { out += code[i]; i++; }
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Whitespace: collapse ────────────────────────────────────
|
|
157
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
158
|
+
while (i < code.length && (code[i] === ' ' || code[i] === '\t' || code[i] === '\n' || code[i] === '\r')) i++;
|
|
159
|
+
const before = out[out.length - 1];
|
|
160
|
+
const after = code[i];
|
|
161
|
+
if (_needsSpace(before, after)) out += ' ';
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
out += ch;
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** True when removing the space between a and b would change semantics. */
|
|
173
|
+
function _needsSpace(a, b) {
|
|
174
|
+
if (!a || !b) return false;
|
|
175
|
+
const idA = (a >= 'a' && a <= 'z') || (a >= 'A' && a <= 'Z') || (a >= '0' && a <= '9') || a === '_' || a === '$';
|
|
176
|
+
const idB = (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b === '_' || b === '$';
|
|
177
|
+
if (idA && idB) return true; // e.g. const x, return value
|
|
178
|
+
if (a === '+' && b === '+') return true; // prevent ++
|
|
179
|
+
if (a === '-' && b === '-') return true; // prevent --
|
|
180
|
+
if (a === '/' && (b === '/' || b === '*')) return true; // prevent // or /*
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Heuristic: is the next '/' a regex start (vs division)? */
|
|
185
|
+
function _isRegexCtx(out) {
|
|
186
|
+
let end = out.length - 1;
|
|
187
|
+
while (end >= 0 && out[end] === ' ') end--;
|
|
188
|
+
if (end < 0) return true;
|
|
189
|
+
const last = out[end];
|
|
190
|
+
if ('=({[,;:!&|?~+-*/%^>'.includes(last)) return true;
|
|
191
|
+
const tail = out.substring(Math.max(0, end - 7), end + 1);
|
|
192
|
+
const kws = ['return', 'typeof', 'case', 'in', 'delete', 'void', 'throw', 'new'];
|
|
193
|
+
for (const kw of kws) {
|
|
194
|
+
if (tail.endsWith(kw)) {
|
|
195
|
+
const pos = end - kw.length;
|
|
196
|
+
if (pos < 0 || !((out[pos] >= 'a' && out[pos] <= 'z') || (out[pos] >= 'A' && out[pos] <= 'Z') || (out[pos] >= '0' && out[pos] <= '9') || out[pos] === '_' || out[pos] === '$')) return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
95
200
|
}
|
|
96
201
|
|
|
97
202
|
// ---------------------------------------------------------------------------
|
package/dist/zquery.dist.zip
CHANGED
|
Binary file
|