zero-query 0.9.7 → 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 +31 -3
- 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 +1942 -1925
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +10 -1
- package/index.js +4 -3
- package/package.json +1 -1
- package/src/component.js +6 -3
- package/src/diff.js +15 -2
- package/tests/cli.test.js +304 -0
- 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
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
// app
|
|
1
|
+
// app.js — Application entry point
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Bootstraps the app: imports components, sets up routing,
|
|
4
|
+
// and wires the responsive sidebar.
|
|
5
|
+
//
|
|
6
|
+
// Key APIs used:
|
|
7
|
+
// $.router — SPA navigation (history mode)
|
|
8
|
+
// $.ready — run after DOM is loaded
|
|
9
|
+
// $.on — global delegated event listeners
|
|
10
|
+
// $.storage — localStorage wrapper
|
|
6
11
|
|
|
7
12
|
import './store.js';
|
|
8
13
|
import './components/home.js';
|
|
9
14
|
import './components/counter.js';
|
|
10
|
-
import './components/todos.js';
|
|
11
|
-
import './components/api-demo.js';
|
|
12
15
|
import './components/about.js';
|
|
13
|
-
import './components/contacts/contacts.js';
|
|
14
16
|
import './components/not-found.js';
|
|
15
17
|
import { routes } from './routes.js';
|
|
16
18
|
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
|
-
// Router
|
|
20
|
+
// Router
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
const router = $.router({
|
|
21
|
-
el: '#app',
|
|
23
|
+
el: '#app',
|
|
22
24
|
routes,
|
|
23
25
|
fallback: 'not-found',
|
|
24
26
|
mode: 'history'
|
|
@@ -48,47 +50,43 @@ function toggleMobileMenu(open) {
|
|
|
48
50
|
|
|
49
51
|
function closeMobileMenu() { toggleMobileMenu(false); }
|
|
50
52
|
|
|
51
|
-
// $.on — global delegated event listeners
|
|
52
53
|
$.on('click', '#menu-toggle', () => toggleMobileMenu(!$sidebar.hasClass('open')));
|
|
53
54
|
$.on('click', '#overlay', closeMobileMenu);
|
|
54
|
-
|
|
55
|
-
// Close sidebar on Escape key — using $.on direct (no selector needed)
|
|
56
|
-
$.on('keydown', (e) => {
|
|
57
|
-
if (e.key === 'Escape') closeMobileMenu();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Toast notification system via $.bus (event bus)
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Any component can emit: $.bus.emit('toast', { message, type })
|
|
64
|
-
// Types: 'success', 'error', 'info'
|
|
65
|
-
$.bus.on('toast', ({ message, type = 'info' }) => {
|
|
66
|
-
const toast = $.create('div')
|
|
67
|
-
.addClass('toast', `toast-${type}`)
|
|
68
|
-
.text(message)
|
|
69
|
-
.appendTo('#toasts');
|
|
70
|
-
// Auto-remove after 3 seconds
|
|
71
|
-
setTimeout(() => {
|
|
72
|
-
toast.addClass('toast-exit');
|
|
73
|
-
setTimeout(() => toast.remove(), 300);
|
|
74
|
-
}, 3000);
|
|
75
|
-
});
|
|
55
|
+
$.on('keydown', (e) => { if (e.key === 'Escape') closeMobileMenu(); });
|
|
76
56
|
|
|
77
57
|
// ---------------------------------------------------------------------------
|
|
78
|
-
// On DOM ready
|
|
58
|
+
// On DOM ready
|
|
79
59
|
// ---------------------------------------------------------------------------
|
|
80
60
|
$.ready(() => {
|
|
81
61
|
// Display version in the sidebar footer
|
|
82
|
-
|
|
83
|
-
if (versionEl) versionEl.textContent = 'v' + $.version;
|
|
62
|
+
$('#nav-version').text('v' + $.version);
|
|
84
63
|
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
64
|
+
// Theme: restore saved preference or auto-detect from system
|
|
65
|
+
const saved = $.storage.get('theme');
|
|
66
|
+
const preference = saved || 'system';
|
|
67
|
+
applyTheme(preference);
|
|
88
68
|
|
|
89
|
-
|
|
69
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
70
|
+
const current = $.storage.get('theme') || 'system';
|
|
71
|
+
if (current === 'system') applyTheme('system');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Highlight the active link on initial load
|
|
90
75
|
const current = window.location.pathname;
|
|
91
76
|
$.all(`.nav-link[z-link="${current}"]`).addClass('active');
|
|
92
77
|
|
|
93
78
|
console.log('⚡ {{NAME}} — powered by zQuery v' + $.version);
|
|
94
79
|
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Theme helper
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
function applyTheme(preference) {
|
|
85
|
+
let resolved = preference;
|
|
86
|
+
if (preference === 'system') {
|
|
87
|
+
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
88
|
+
}
|
|
89
|
+
$('html').attr('data-theme', resolved);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
window.__applyTheme = applyTheme;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// about.js — About page with theme switcher
|
|
2
|
+
//
|
|
3
|
+
// Features used:
|
|
4
|
+
// $.storage — localStorage wrapper (get / set)
|
|
5
|
+
// $.version — library version string
|
|
6
|
+
// data-theme — dark / light theming
|
|
7
|
+
|
|
8
|
+
$.component('about-page', {
|
|
9
|
+
styles: `
|
|
10
|
+
.theme-switch { display: inline-flex; border-radius: var(--radius); overflow: hidden;
|
|
11
|
+
border: 1px solid var(--border); background: var(--bg); }
|
|
12
|
+
.theme-btn { padding: 0.45rem 1rem; font-size: 0.82rem; font-weight: 500;
|
|
13
|
+
background: transparent; border: none; color: var(--text-muted);
|
|
14
|
+
cursor: pointer; transition: all .15s ease; font-family: inherit; }
|
|
15
|
+
.theme-btn:hover { color: var(--text); background: var(--bg-hover); }
|
|
16
|
+
.theme-btn.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
17
|
+
.theme-btn + .theme-btn { border-left: 1px solid var(--border); }
|
|
18
|
+
|
|
19
|
+
.next-steps { padding-left: 1.25rem; }
|
|
20
|
+
.next-steps li { margin-bottom: 0.4rem; font-size: 0.9rem; color: var(--text-muted); }
|
|
21
|
+
.next-steps a { color: var(--accent); }
|
|
22
|
+
`,
|
|
23
|
+
|
|
24
|
+
state: () => ({
|
|
25
|
+
theme: 'system',
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
mounted() {
|
|
29
|
+
this.state.theme = $.storage.get('theme') || 'system';
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
setTheme(mode) {
|
|
33
|
+
this.state.theme = mode;
|
|
34
|
+
$.storage.set('theme', mode);
|
|
35
|
+
window.__applyTheme(mode);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
render() {
|
|
39
|
+
const t = this.state.theme;
|
|
40
|
+
return `
|
|
41
|
+
<div class="page-header">
|
|
42
|
+
<h1>About</h1>
|
|
43
|
+
<p class="subtitle">zQuery v${$.version} — zero-dependency frontend micro-library.</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="card">
|
|
47
|
+
<h3>Theme</h3>
|
|
48
|
+
<p>Choose your preferred appearance. <strong>System</strong> follows your OS setting.</p>
|
|
49
|
+
<div class="theme-switch">
|
|
50
|
+
<button class="theme-btn ${t === 'system' ? 'active' : ''}" @click="setTheme('system')">System</button>
|
|
51
|
+
<button class="theme-btn ${t === 'dark' ? 'active' : ''}" @click="setTheme('dark')">Dark</button>
|
|
52
|
+
<button class="theme-btn ${t === 'light' ? 'active' : ''}" @click="setTheme('light')">Light</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="card">
|
|
57
|
+
<h3>Next Steps</h3>
|
|
58
|
+
<ul class="next-steps">
|
|
59
|
+
<li>Read the <a href="https://z-query.com/docs" target="_blank" rel="noopener">full documentation</a></li>
|
|
60
|
+
<li>Explore the <a href="https://github.com/tonywied17/zero-query" target="_blank" rel="noopener">source on GitHub</a></li>
|
|
61
|
+
<li>Run <code>npx zquery bundle</code> to build for production</li>
|
|
62
|
+
<li>Run <code>npx zquery dev</code> for live-reload development</li>
|
|
63
|
+
<li>Try <code>npx zquery create my-app</code> for the full-featured scaffold</li>
|
|
64
|
+
</ul>
|
|
65
|
+
</div>
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// counter.js — Interactive counter
|
|
2
|
+
//
|
|
3
|
+
// Features used:
|
|
4
|
+
// $.getStore / store.subscribe — shared state + live re-render
|
|
5
|
+
// @click / z-model + z-number — events + two-way binding
|
|
6
|
+
// z-if / z-for / z-key — conditional & keyed list rendering
|
|
7
|
+
|
|
8
|
+
$.component('counter-page', {
|
|
9
|
+
styles: `
|
|
10
|
+
.ctr-display { padding: 2rem 0 1.25rem; text-align: center; }
|
|
11
|
+
.ctr-num { font-size: 4rem; font-weight: 800; font-variant-numeric: tabular-nums;
|
|
12
|
+
color: var(--accent); transition: color .2s; line-height: 1; }
|
|
13
|
+
.ctr-num.negative { color: var(--danger); }
|
|
14
|
+
.ctr-label { font-size: .82rem; color: var(--text-muted); margin-top: .35rem; }
|
|
15
|
+
.ctr-actions { display: flex; justify-content: center; gap: .65rem;
|
|
16
|
+
margin-bottom: 1.5rem; }
|
|
17
|
+
.ctr-actions .btn { min-width: 120px; justify-content: center; }
|
|
18
|
+
.ctr-config { display: flex; align-items: center; justify-content: center; gap: 1.25rem;
|
|
19
|
+
padding-top: 1.15rem; border-top: 1px solid var(--border); }
|
|
20
|
+
.ctr-config label { display: flex; align-items: center; gap: .5rem;
|
|
21
|
+
color: var(--text-muted); font-size: .88rem; }
|
|
22
|
+
.ctr-config .input-sm { width: 65px; text-align: center; }
|
|
23
|
+
.ctr-hist { display: flex; flex-wrap: wrap; gap: .4rem; }
|
|
24
|
+
.ctr-hist-item { display: inline-flex; align-items: center; gap: .3rem;
|
|
25
|
+
padding: .3rem .65rem; border-radius: var(--radius);
|
|
26
|
+
background: var(--bg-hover); border: 1px solid var(--border);
|
|
27
|
+
font-size: .82rem; font-variant-numeric: tabular-nums; }
|
|
28
|
+
.ctr-hist-item:last-child { border-color: var(--accent); background: rgba(88,166,255,.06); }
|
|
29
|
+
.ctr-hist-op { color: var(--text-muted); font-weight: 500; }
|
|
30
|
+
.ctr-hist-val { color: var(--accent); font-weight: 600; }
|
|
31
|
+
|
|
32
|
+
@media (max-width: 768px) {
|
|
33
|
+
.ctr-num { font-size: 2.75rem; }
|
|
34
|
+
.ctr-actions { gap: .5rem; }
|
|
35
|
+
.ctr-actions .btn { min-width: 100px; }
|
|
36
|
+
.ctr-config { flex-wrap: wrap; gap: .75rem; }
|
|
37
|
+
}
|
|
38
|
+
@media (max-width: 480px) {
|
|
39
|
+
.ctr-num { font-size: 2.25rem; }
|
|
40
|
+
.ctr-actions .btn { min-width: 0; flex: 1; }
|
|
41
|
+
}
|
|
42
|
+
`,
|
|
43
|
+
|
|
44
|
+
state: () => ({
|
|
45
|
+
history: [],
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
mounted() {
|
|
49
|
+
this._unsub = $.getStore('main').subscribe(() => this.setState({}));
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
destroyed() {
|
|
53
|
+
if (this._unsub) this._unsub();
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
increment() {
|
|
57
|
+
const store = $.getStore('main');
|
|
58
|
+
store.dispatch('increment');
|
|
59
|
+
this._pushHistory('+', store.state.step, store.state.count);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
decrement() {
|
|
63
|
+
const store = $.getStore('main');
|
|
64
|
+
store.dispatch('decrement');
|
|
65
|
+
this._pushHistory('−', store.state.step, store.state.count);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
setStep(e) {
|
|
69
|
+
$.getStore('main').dispatch('setStep', Number(e.target.value));
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
_pushHistory(action, value, result) {
|
|
73
|
+
const raw = this.state.history.__raw || this.state.history;
|
|
74
|
+
const next = [...raw, { id: Date.now(), action, value, result }];
|
|
75
|
+
this.state.history = next.length > 8 ? next.slice(-8) : next;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
reset() {
|
|
79
|
+
$.getStore('main').dispatch('reset');
|
|
80
|
+
this.state.history = [];
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
render() {
|
|
84
|
+
const { count, step } = $.getStore('main').state;
|
|
85
|
+
|
|
86
|
+
return `
|
|
87
|
+
<div class="page-header">
|
|
88
|
+
<h1>Counter</h1>
|
|
89
|
+
<p class="subtitle">Reactive <code>store</code>, <code>subscribe</code>, <code>dispatch</code>, and <code>z-for</code>.</p>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="card">
|
|
93
|
+
<div class="ctr-display">
|
|
94
|
+
<div class="ctr-num ${count < 0 ? 'negative' : ''}">${count}</div>
|
|
95
|
+
<div class="ctr-label">current value${step !== 1 ? ` · step ${step}` : ''}</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="ctr-actions">
|
|
99
|
+
<button class="btn btn-outline" @click="decrement">− Subtract</button>
|
|
100
|
+
<button class="btn btn-primary" @click="increment">+ Add</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="ctr-config">
|
|
104
|
+
<label>Step size
|
|
105
|
+
<input type="number" value="${step}" @input="setStep" min="1" max="100" class="input input-sm" />
|
|
106
|
+
</label>
|
|
107
|
+
<button class="btn btn-ghost btn-sm" @click="reset">Reset</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="card" z-if="history.length > 0">
|
|
112
|
+
<h3>History <small style="color:var(--text-muted);font-weight:400;font-size:.85rem;">${this.state.history.length} entries</small></h3>
|
|
113
|
+
<div class="ctr-hist">
|
|
114
|
+
<span z-for="e in history" z-key="{{e.id}}" class="ctr-hist-item">
|
|
115
|
+
<span class="ctr-hist-op">{{e.action}}{{e.value}}</span>
|
|
116
|
+
<span class="ctr-hist-val">→ {{e.result}}</span>
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// home.js — Landing page
|
|
2
|
+
//
|
|
3
|
+
// Features used:
|
|
4
|
+
// $.component — define a component
|
|
5
|
+
// $.getStore — read/dispatch global store
|
|
6
|
+
// store.subscribe — re-render on store changes
|
|
7
|
+
// @click / z-model — event binding + two-way input
|
|
8
|
+
|
|
9
|
+
$.component('home-page', {
|
|
10
|
+
state: () => ({
|
|
11
|
+
greeting: '',
|
|
12
|
+
}),
|
|
13
|
+
|
|
14
|
+
mounted() {
|
|
15
|
+
const hour = new Date().getHours();
|
|
16
|
+
this.state.greeting =
|
|
17
|
+
hour < 12 ? 'Good morning' :
|
|
18
|
+
hour < 18 ? 'Good afternoon' : 'Good evening';
|
|
19
|
+
|
|
20
|
+
this._unsub = $.getStore('main').subscribe(() => this.setState({}));
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
destroyed() {
|
|
24
|
+
if (this._unsub) this._unsub();
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
increment() { $.getStore('main').dispatch('increment'); },
|
|
28
|
+
decrement() { $.getStore('main').dispatch('decrement'); },
|
|
29
|
+
reset() { $.getStore('main').dispatch('reset'); },
|
|
30
|
+
setStep(e) { $.getStore('main').dispatch('setStep', Number(e.target.value)); },
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
const { count, step } = $.getStore('main').state;
|
|
34
|
+
|
|
35
|
+
return `
|
|
36
|
+
<div class="page-header">
|
|
37
|
+
<h1>{{greeting}} 👋</h1>
|
|
38
|
+
<p class="subtitle">Welcome to your new zQuery project.</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="card">
|
|
42
|
+
<h3>Getting Started</h3>
|
|
43
|
+
<p>
|
|
44
|
+
This is the <strong>minimal</strong> scaffold — three pages, a global store,
|
|
45
|
+
and the router. Edit the files in <code>app/</code> to start building.
|
|
46
|
+
</p>
|
|
47
|
+
<p>
|
|
48
|
+
Run <code>npx zquery dev</code> for live-reload, or
|
|
49
|
+
<code>npx zquery bundle</code> when you're ready to ship.
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="card">
|
|
54
|
+
<h3>Global Store</h3>
|
|
55
|
+
<p>The counter from <a z-link="/counter">Counter</a> is backed by <code>$.store</code> — its value persists across pages:</p>
|
|
56
|
+
<div style="display:flex;align-items:center;gap:.75rem;margin-top:.75rem;">
|
|
57
|
+
<button class="btn btn-outline btn-sm" @click="decrement">−</button>
|
|
58
|
+
<span style="font-size:1.25rem;font-weight:700;color:var(--accent);min-width:2rem;text-align:center;">${count}</span>
|
|
59
|
+
<button class="btn btn-primary btn-sm" @click="increment">+</button>
|
|
60
|
+
<button class="btn btn-outline btn-sm" @click="reset" style="margin-left:.5rem;">Reset</button>
|
|
61
|
+
<label style="display:flex;align-items:center;gap:.4rem;margin-left:.5rem;font-size:.85rem;color:var(--text-muted);">
|
|
62
|
+
Step <input type="number" value="${step}" @input="setStep" min="1" max="100" class="input input-sm" style="width:55px;text-align:center;" />
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
`;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// not-found.js — 404 fallback page
|
|
2
|
+
//
|
|
3
|
+
// Uses $.getRouter() to display the unmatched 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,36 @@
|
|
|
1
|
+
// store.js — Global state management
|
|
2
|
+
//
|
|
3
|
+
// A simple centralized store. Any component can access it
|
|
4
|
+
// via $.getStore('main') and dispatch actions to update state.
|
|
5
|
+
|
|
6
|
+
export const store = $.store('main', {
|
|
7
|
+
state: {
|
|
8
|
+
count: 0,
|
|
9
|
+
step: 1,
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
actions: {
|
|
13
|
+
increment(state) {
|
|
14
|
+
state.count += state.step;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
decrement(state) {
|
|
18
|
+
state.count -= state.step;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
reset(state) {
|
|
22
|
+
state.count = 0;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
setStep(state, value) {
|
|
26
|
+
state.step = Math.max(1, Math.min(100, value));
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
getters: {
|
|
31
|
+
isNegative: (state) => state.count < 0,
|
|
32
|
+
isPositive: (state) => state.count > 0,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
debug: true,
|
|
36
|
+
});
|
|
File without changes
|