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
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ npx zquery dev my-app
|
|
|
53
53
|
|
|
54
54
|
> **Tip:** Stay in the project root (where `node_modules` lives) instead of `cd`-ing into `my-app`. This keeps `index.d.ts` accessible to your IDE for full type/intellisense support.
|
|
55
55
|
|
|
56
|
-
The `create` command generates a ready-to-run project with a sidebar layout, router, multiple components (including
|
|
56
|
+
The `create` command generates a ready-to-run project with a sidebar layout, router, multiple components (including folder components with external templates and styles), and responsive styles. Use `--minimal` (or `-m`) to scaffold a lightweight 3-page starter instead. The dev server watches for file changes, hot-swaps CSS in-place, full-reloads on other changes, and handles SPA fallback routing.
|
|
57
57
|
|
|
58
58
|
#### Error Overlay
|
|
59
59
|
|
|
@@ -136,7 +136,7 @@ That's it — a fully working SPA with the dev server's live-reload.
|
|
|
136
136
|
## Recommended Project Structure
|
|
137
137
|
|
|
138
138
|
```
|
|
139
|
-
my-app/
|
|
139
|
+
my-app/ ← default scaffold (npx zquery create my-app)
|
|
140
140
|
index.html
|
|
141
141
|
global.css
|
|
142
142
|
app/
|
|
@@ -150,15 +150,42 @@ my-app/
|
|
|
150
150
|
api-demo.js
|
|
151
151
|
about.js
|
|
152
152
|
not-found.js
|
|
153
|
+
contact-card.js
|
|
153
154
|
contacts/ ← folder component (templateUrl + styleUrl)
|
|
154
155
|
contacts.js
|
|
155
156
|
contacts.html
|
|
156
157
|
contacts.css
|
|
158
|
+
playground/ ← folder component
|
|
159
|
+
playground.js
|
|
160
|
+
playground.html
|
|
161
|
+
playground.css
|
|
162
|
+
toolkit/ ← folder component
|
|
163
|
+
toolkit.js
|
|
164
|
+
toolkit.html
|
|
165
|
+
toolkit.css
|
|
157
166
|
assets/
|
|
158
167
|
scripts/ ← third-party JS (e.g. zquery.min.js for manual setup)
|
|
159
168
|
styles/ ← additional stylesheets, fonts, etc.
|
|
160
169
|
```
|
|
161
170
|
|
|
171
|
+
Use `--minimal` for a lighter starting point (3 pages + 404 fallback):
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
my-app/ ← minimal scaffold (npx zquery create my-app --minimal)
|
|
175
|
+
index.html
|
|
176
|
+
global.css
|
|
177
|
+
app/
|
|
178
|
+
app.js
|
|
179
|
+
routes.js
|
|
180
|
+
store.js
|
|
181
|
+
components/
|
|
182
|
+
home.js
|
|
183
|
+
counter.js
|
|
184
|
+
about.js
|
|
185
|
+
not-found.js ← 404 fallback
|
|
186
|
+
assets/
|
|
187
|
+
```
|
|
188
|
+
|
|
162
189
|
- One component per file inside `components/`.
|
|
163
190
|
- Names **must contain a hyphen** (Web Component convention): `home-page`, `app-counter`, etc.
|
|
164
191
|
- Components with external templates or styles can use a subfolder (e.g. `contacts/contacts.js` + `contacts.html` + `contacts.css`).
|
|
@@ -272,12 +299,13 @@ location / {
|
|
|
272
299
|
| `$.param` `$.parseQuery` | URL utils |
|
|
273
300
|
| `$.storage` `$.session` | Storage wrappers |
|
|
274
301
|
| `$.EventBus` `$.bus` | Event bus || `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.validate` | Error handling || `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~100 KB\"`) |
|
|
302
|
+
| `$.unitTests` | Build-time test results `{ passed, failed, total, suites, duration, ok }` |
|
|
275
303
|
| `$.meta` | Build metadata (populated by CLI bundler) |
|
|
276
304
|
| `$.noConflict` | Release `$` global |
|
|
277
305
|
|
|
278
306
|
| CLI Command | Description |
|
|
279
307
|
| --- | --- |
|
|
280
|
-
| `zquery create [dir]` | Scaffold a new project
|
|
308
|
+
| `zquery create [dir]` | Scaffold a new project. Default: full-featured app. `--minimal` / `-m`: lightweight 3-page starter with 404 fallback. |
|
|
281
309
|
| `zquery dev [root]` | Dev server with live-reload, CSS hot-swap, error overlay, expandable floating toolbar & five-tab inspector panel (port 3100). Visit `/_devtools` for the standalone panel. `--index` for custom HTML, `--bundle` for bundled mode, `--no-intercept` to skip CDN intercept. |
|
|
282
310
|
| `zquery bundle [dir\|file]` | Bundle app into a single IIFE file. Accepts dir or direct entry file. |
|
|
283
311
|
| `zquery build` | Build the zQuery library (`dist/zquery.min.js`) |
|
package/cli/commands/build.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const zlib = require('zlib');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
13
14
|
const { minify, sizeKB } = require('../utils');
|
|
14
15
|
|
|
15
16
|
function buildLibrary() {
|
|
@@ -18,7 +19,7 @@ function buildLibrary() {
|
|
|
18
19
|
|
|
19
20
|
const modules = [
|
|
20
21
|
'src/errors.js',
|
|
21
|
-
'src/reactive.js', 'src/
|
|
22
|
+
'src/reactive.js', 'src/diff.js', 'src/core.js', 'src/expression.js',
|
|
22
23
|
'src/component.js', 'src/router.js', 'src/store.js', 'src/http.js',
|
|
23
24
|
'src/utils.js',
|
|
24
25
|
];
|
|
@@ -30,6 +31,49 @@ function buildLibrary() {
|
|
|
30
31
|
const start = Date.now();
|
|
31
32
|
if (!fs.existsSync(DIST)) fs.mkdirSync(DIST, { recursive: true });
|
|
32
33
|
|
|
34
|
+
// -----------------------------------------------------------------------
|
|
35
|
+
// Run unit tests and capture results for $.unitTests
|
|
36
|
+
// -----------------------------------------------------------------------
|
|
37
|
+
let testResults = { passed: 0, failed: 0, total: 0, suites: 0, duration: 0, ok: false };
|
|
38
|
+
try {
|
|
39
|
+
const json = execSync('npx vitest run --reporter=json', {
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
timeout: 120000,
|
|
44
|
+
});
|
|
45
|
+
// vitest --reporter=json outputs JSON to stdout; may include non-JSON lines
|
|
46
|
+
const jsonStart = json.indexOf('{');
|
|
47
|
+
if (jsonStart !== -1) {
|
|
48
|
+
const parsed = JSON.parse(json.slice(jsonStart));
|
|
49
|
+
const passed = parsed.numPassedTests || 0;
|
|
50
|
+
const failed = parsed.numFailedTests || 0;
|
|
51
|
+
const total = parsed.numTotalTests || 0;
|
|
52
|
+
const suites = parsed.numTotalTestSuites || 0;
|
|
53
|
+
const dur = Math.round((parsed.testResults || []).reduce((s, r) => s + (r.endTime - r.startTime), 0));
|
|
54
|
+
testResults = { passed, failed, total, suites, duration: dur, ok: parsed.success !== false };
|
|
55
|
+
}
|
|
56
|
+
console.log(` ✓ Tests: ${testResults.passed}/${testResults.total} passed (${testResults.suites} suites)\n`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Tests may fail but we still want to capture the numbers
|
|
59
|
+
const out = (err.stdout || '') + (err.stderr || '');
|
|
60
|
+
const jsonStart = out.indexOf('{');
|
|
61
|
+
if (jsonStart !== -1) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(out.slice(jsonStart));
|
|
64
|
+
testResults = {
|
|
65
|
+
passed: parsed.numPassedTests || 0,
|
|
66
|
+
failed: parsed.numFailedTests || 0,
|
|
67
|
+
total: parsed.numTotalTests || 0,
|
|
68
|
+
suites: parsed.numTotalTestSuites || 0,
|
|
69
|
+
duration: Math.round((parsed.testResults || []).reduce((s, r) => s + (r.endTime - r.startTime), 0)),
|
|
70
|
+
ok: false,
|
|
71
|
+
};
|
|
72
|
+
} catch (_) { /* keep defaults */ }
|
|
73
|
+
}
|
|
74
|
+
console.log(` ⚠ Tests: ${testResults.passed}/${testResults.total} passed, ${testResults.failed} failed\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
33
77
|
const parts = modules.map(file => {
|
|
34
78
|
let code = fs.readFileSync(path.join(process.cwd(), file), 'utf-8');
|
|
35
79
|
code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
@@ -54,8 +98,11 @@ function buildLibrary() {
|
|
|
54
98
|
// Inject actual minified library size into both outputs
|
|
55
99
|
const libSizeKB = Math.round(Buffer.from(minified).length / 1024);
|
|
56
100
|
const libSizeStr = `~${libSizeKB} KB`;
|
|
57
|
-
const
|
|
58
|
-
|
|
101
|
+
const testObj = JSON.stringify(testResults);
|
|
102
|
+
let outContent = fs.readFileSync(OUT_FILE, 'utf-8').replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
|
|
103
|
+
let minContent = minified.replace("'__LIB_SIZE__'", `'${libSizeStr}'`);
|
|
104
|
+
outContent = outContent.replace("'__UNIT_TESTS__'", testObj);
|
|
105
|
+
minContent = minContent.replace("'__UNIT_TESTS__'", testObj);
|
|
59
106
|
fs.writeFileSync(OUT_FILE, outContent, 'utf-8');
|
|
60
107
|
fs.writeFileSync(MIN_FILE, minContent, 'utf-8');
|
|
61
108
|
|
package/cli/commands/create.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// cli/commands/create.js — scaffold a new zQuery project
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
//
|
|
3
|
+
// Templates live in cli/scaffold/<variant>/ (default or minimal).
|
|
4
|
+
// Reads template files, replaces {{NAME}} with the project name,
|
|
5
|
+
// and writes them into the target directory.
|
|
4
6
|
|
|
5
7
|
const fs = require('fs');
|
|
6
8
|
const path = require('path');
|
|
9
|
+
const { flag } = require('../args');
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Recursively collect every file under `dir`, returning paths relative to `dir`.
|
|
@@ -22,24 +25,34 @@ function walkDir(dir, prefix = '') {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
function createProject(args) {
|
|
25
|
-
|
|
28
|
+
// First positional arg after "create" is the target dir (skip flags)
|
|
29
|
+
const dirArg = args.slice(1).find(a => !a.startsWith('-'));
|
|
30
|
+
const target = dirArg ? path.resolve(dirArg) : process.cwd();
|
|
26
31
|
const name = path.basename(target);
|
|
27
32
|
|
|
33
|
+
// Determine scaffold variant: --minimal / -m or default
|
|
34
|
+
const variant = flag('minimal', 'm') ? 'minimal' : 'default';
|
|
35
|
+
|
|
28
36
|
// Guard: refuse to overwrite existing files
|
|
29
37
|
const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
|
|
30
38
|
fs.existsSync(path.join(target, f))
|
|
31
39
|
);
|
|
32
40
|
if (conflicts.length) {
|
|
33
|
-
console.error(`\n
|
|
41
|
+
console.error(`\n ✗ Directory already contains: ${conflicts.join(', ')}`);
|
|
34
42
|
console.error(` Aborting to avoid overwriting existing files.\n`);
|
|
35
43
|
process.exit(1);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
console.log(`\n zQuery
|
|
46
|
+
console.log(`\n zQuery — Create Project (${variant})\n`);
|
|
39
47
|
console.log(` Scaffolding into ${target}\n`);
|
|
40
48
|
|
|
41
|
-
// Resolve the scaffold template directory
|
|
42
|
-
const scaffoldDir = path.resolve(__dirname, '..', 'scaffold');
|
|
49
|
+
// Resolve the scaffold template directory for the chosen variant
|
|
50
|
+
const scaffoldDir = path.resolve(__dirname, '..', 'scaffold', variant);
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(scaffoldDir)) {
|
|
53
|
+
console.error(`\n ✗ Scaffold variant "${variant}" not found.\n`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
43
56
|
|
|
44
57
|
// Walk the scaffold directory and copy each file
|
|
45
58
|
const templateFiles = walkDir(scaffoldDir);
|
|
@@ -54,13 +67,13 @@ function createProject(args) {
|
|
|
54
67
|
const dest = path.join(target, rel);
|
|
55
68
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
56
69
|
fs.writeFileSync(dest, content, 'utf-8');
|
|
57
|
-
console.log(`
|
|
70
|
+
console.log(` ✓ ${rel}`);
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
console.log(`
|
|
61
74
|
Done! Next steps:
|
|
62
75
|
|
|
63
|
-
npx zquery dev${target !== process.cwd() ? ` ${
|
|
76
|
+
npx zquery dev${target !== process.cwd() ? ` ${dirArg}` : ''}
|
|
64
77
|
`);
|
|
65
78
|
}
|
|
66
79
|
|
package/cli/help.js
CHANGED
|
@@ -9,6 +9,8 @@ function showHelp() {
|
|
|
9
9
|
create [dir] Scaffold a new zQuery project
|
|
10
10
|
Creates index.html, global.css, app/, assets/ in the target directory
|
|
11
11
|
(defaults to the current directory)
|
|
12
|
+
--minimal, -m Use the minimal template (home, counter, about)
|
|
13
|
+
instead of the full-featured default scaffold
|
|
12
14
|
|
|
13
15
|
dev [root] Start a dev server with live-reload
|
|
14
16
|
--port, -p <number> Port number (default: 3100)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// app.js — Application entry point
|
|
2
|
+
//
|
|
3
|
+
// Bootstraps the app: imports components, sets up routing,
|
|
4
|
+
// wires the responsive sidebar, and connects the store.
|
|
5
|
+
//
|
|
6
|
+
// Key APIs used:
|
|
7
|
+
// $.router — SPA navigation (history mode)
|
|
8
|
+
// $.ready — run after DOM is loaded
|
|
9
|
+
// $.bus — event bus (toast notifications, contact card)
|
|
10
|
+
// $.on — global delegated event listeners
|
|
11
|
+
// $.storage — localStorage wrapper
|
|
12
|
+
// $.create — create DOM elements
|
|
13
|
+
|
|
14
|
+
import './store.js';
|
|
15
|
+
import './components/home.js';
|
|
16
|
+
import './components/counter.js';
|
|
17
|
+
import './components/todos.js';
|
|
18
|
+
import './components/api-demo.js';
|
|
19
|
+
import './components/playground/playground.js';
|
|
20
|
+
import './components/toolkit/toolkit.js';
|
|
21
|
+
import './components/about.js';
|
|
22
|
+
import './components/contact-card.js';
|
|
23
|
+
import './components/contacts/contacts.js';
|
|
24
|
+
import './components/not-found.js';
|
|
25
|
+
import { routes } from './routes.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Router — SPA navigation with history mode
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const router = $.router({
|
|
31
|
+
el: '#app', //@ Mount point (Set in index.html)
|
|
32
|
+
routes,
|
|
33
|
+
fallback: 'not-found',
|
|
34
|
+
mode: 'history'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Post-navigation hook — track page views on every navigation
|
|
38
|
+
router.afterEach((to) => {
|
|
39
|
+
const store = $.getStore('main');
|
|
40
|
+
if (store) store.dispatch('incrementVisits');
|
|
41
|
+
console.log('📊 Navigated to:', to.path);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Highlight the active nav link on every route change
|
|
45
|
+
router.onChange((to) => {
|
|
46
|
+
$.all('.nav-link').removeClass('active');
|
|
47
|
+
$(`.nav-link[z-link="${to.path}"]`).addClass('active');
|
|
48
|
+
|
|
49
|
+
// Close mobile menu on navigate
|
|
50
|
+
closeMobileMenu();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Responsive sidebar toggle
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
const $sidebar = $('#sidebar');
|
|
57
|
+
const $overlay = $('#overlay');
|
|
58
|
+
const $toggle = $('#menu-toggle');
|
|
59
|
+
|
|
60
|
+
function toggleMobileMenu(open) {
|
|
61
|
+
$sidebar.toggleClass('open', open);
|
|
62
|
+
$overlay.toggleClass('visible', open);
|
|
63
|
+
$toggle.toggleClass('active', open);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function closeMobileMenu() { toggleMobileMenu(false); }
|
|
67
|
+
|
|
68
|
+
// $.on — global delegated event listeners
|
|
69
|
+
$.on('click', '#menu-toggle', () => toggleMobileMenu(!$sidebar.hasClass('open')));
|
|
70
|
+
$.on('click', '#overlay', closeMobileMenu);
|
|
71
|
+
|
|
72
|
+
// Close sidebar on Escape key — using $.on direct (no selector needed)
|
|
73
|
+
$.on('keydown', (e) => {
|
|
74
|
+
if (e.key === 'Escape') closeMobileMenu();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Sidebar stats panel — collapsible, live-updating from $.store
|
|
79
|
+
// Starts expanded by default. Open/closed state saved via $.storage.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
$.on('click', '#stats-toggle', () => {
|
|
82
|
+
const $body = $('#stats-body');
|
|
83
|
+
const $arrow = $('#stats-arrow');
|
|
84
|
+
if (!$body.length) return;
|
|
85
|
+
const open = $body.css('display') !== 'none';
|
|
86
|
+
open ? $body.hide() : $body.show();
|
|
87
|
+
$arrow.toggleClass('open', !open);
|
|
88
|
+
$.storage.set('statsOpen', !open);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function updateSidebarStats() {
|
|
92
|
+
const store = $.getStore('main');
|
|
93
|
+
if (!store) return;
|
|
94
|
+
$('#ss-visits').text(store.state.visits);
|
|
95
|
+
$('#ss-todos').text(store.getters.todoCount);
|
|
96
|
+
$('#ss-pending').text(store.getters.pendingCount);
|
|
97
|
+
$('#ss-done').text(store.getters.doneCount);
|
|
98
|
+
$('#ss-contacts').text(store.getters.contactCount);
|
|
99
|
+
$('#ss-favorites').text(store.getters.favoriteCount);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Sidebar contacts — live status indicators from $.store
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
function updateSidebarContacts() {
|
|
106
|
+
const store = $.getStore('main');
|
|
107
|
+
if (!store) return;
|
|
108
|
+
const $list = $('#sc-list');
|
|
109
|
+
if (!$list.length) return;
|
|
110
|
+
|
|
111
|
+
const sorted = [...store.state.contacts].sort((a, b) => {
|
|
112
|
+
const order = { online: 0, away: 1, offline: 2 };
|
|
113
|
+
return (order[a.status] ?? 3) - (order[b.status] ?? 3);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const html = sorted.map(c => {
|
|
117
|
+
const hue = (c.name.charCodeAt(0) * 7) % 360;
|
|
118
|
+
const initial = $.escapeHtml(c.name.charAt(0).toUpperCase());
|
|
119
|
+
const name = $.escapeHtml(c.name);
|
|
120
|
+
return `<div class="sc-item" data-contact-id="${c.id}">
|
|
121
|
+
<span class="sc-avatar" style="background:hsl(${hue},55%,42%)">${initial}</span>
|
|
122
|
+
<span class="sc-dot sc-dot-${c.status}"></span>
|
|
123
|
+
<span class="sc-name">${name}</span>
|
|
124
|
+
</div>`;
|
|
125
|
+
}).join('');
|
|
126
|
+
|
|
127
|
+
$list.html(html);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Toast notification system via $.bus (event bus)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Any component can emit: $.bus.emit('toast', { message, type })
|
|
134
|
+
// Types: 'success', 'error', 'info'
|
|
135
|
+
$.bus.on('toast', ({ message, type = 'info' }) => {
|
|
136
|
+
const toast = $.create('div')
|
|
137
|
+
.addClass('toast', `toast-${type}`)
|
|
138
|
+
.text(message)
|
|
139
|
+
.appendTo('#toasts');
|
|
140
|
+
// Auto-remove after 3 seconds
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
toast.addClass('toast-exit');
|
|
143
|
+
setTimeout(() => toast.remove(), 300);
|
|
144
|
+
}, 3000);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// On DOM ready — final setup
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
$.ready(() => {
|
|
151
|
+
// Display version in the sidebar footer
|
|
152
|
+
$('#nav-version').text('v' + $.version);
|
|
153
|
+
|
|
154
|
+
// Theme: restore saved preference or auto-detect from system
|
|
155
|
+
const saved = $.storage.get('theme'); // 'dark' | 'light' | 'system' | null
|
|
156
|
+
const preference = saved || 'system';
|
|
157
|
+
applyTheme(preference);
|
|
158
|
+
|
|
159
|
+
// Listen for OS color-scheme changes (only applies when preference is 'system')
|
|
160
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
161
|
+
const current = $.storage.get('theme') || 'system';
|
|
162
|
+
if (current === 'system') applyTheme('system');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Set active link on initial load
|
|
166
|
+
const current = window.location.pathname;
|
|
167
|
+
$.all(`.nav-link[z-link="${current}"]`).addClass('active');
|
|
168
|
+
|
|
169
|
+
// Stats panel: restore open/closed from $.storage (defaults to open)
|
|
170
|
+
const statsOpen = $.storage.get('statsOpen');
|
|
171
|
+
if (statsOpen === false) {
|
|
172
|
+
$('#stats-body').hide();
|
|
173
|
+
$('#stats-arrow').removeClass('open');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Sidebar contact click — open the global contact card overlay
|
|
177
|
+
$('#sc-list').on('click', '.sc-item', (e) => {
|
|
178
|
+
const item = e.target.closest('.sc-item');
|
|
179
|
+
if (!item) return;
|
|
180
|
+
const id = Number(item.dataset.contactId);
|
|
181
|
+
if (id) $.bus.emit('openContact', id);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Initial sidebar stats + contacts + live subscription
|
|
185
|
+
updateSidebarStats();
|
|
186
|
+
updateSidebarContacts();
|
|
187
|
+
const store = $.getStore('main');
|
|
188
|
+
if (store) {
|
|
189
|
+
store.subscribe(updateSidebarStats);
|
|
190
|
+
store.subscribe(updateSidebarContacts);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Mount any components outside the router outlet (e.g. <contact-card>)
|
|
194
|
+
$.mountAll();
|
|
195
|
+
|
|
196
|
+
console.log('⚡ {{NAME}} — powered by zQuery v' + $.version);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Theme helper — resolves 'system' to actual dark/light and applies it
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
function applyTheme(preference) {
|
|
203
|
+
let resolved = preference;
|
|
204
|
+
if (preference === 'system') {
|
|
205
|
+
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
206
|
+
}
|
|
207
|
+
$('html').attr('data-theme', resolved);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Expose globally so components can call it
|
|
211
|
+
window.__applyTheme = applyTheme;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// about.js — About page with theme switcher
|
|
2
|
+
//
|
|
3
|
+
// Features used:
|
|
4
|
+
// $.storage — localStorage wrapper (get / set)
|
|
5
|
+
// $.version — library version string
|
|
6
|
+
// $.unitTests — build-time test results object
|
|
7
|
+
// $.bus.emit — toast notifications
|
|
8
|
+
// data-theme attr — dark / light theming
|
|
9
|
+
|
|
10
|
+
$.component('about-page', {
|
|
11
|
+
styles: `
|
|
12
|
+
/* -- Hero -- */
|
|
13
|
+
.about-hero { text-align: center; padding: 2.5rem 1rem 1.5rem; }
|
|
14
|
+
.about-hero h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.35rem; }
|
|
15
|
+
.about-hero .ver { display: inline-block; padding: 0.2rem 0.65rem; border-radius: 999px;
|
|
16
|
+
font-size: 0.78rem; font-weight: 600; color: var(--accent);
|
|
17
|
+
background: var(--accent-soft); border: 1px solid rgba(88,166,255,.15);
|
|
18
|
+
margin-bottom: 0.75rem; }
|
|
19
|
+
.about-hero p { color: var(--text-muted); font-size: 0.95rem; max-width: 520px; margin: 0 auto; }
|
|
20
|
+
|
|
21
|
+
/* -- Stats bar -- */
|
|
22
|
+
.about-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;
|
|
23
|
+
padding: 1.25rem 0; border-top: 1px solid var(--border);
|
|
24
|
+
border-bottom: 1px solid var(--border); margin: 1.25rem 0; }
|
|
25
|
+
.about-stat { text-align: center; }
|
|
26
|
+
.about-stat-val { font-size: 1.35rem; font-weight: 700; color: var(--accent);
|
|
27
|
+
font-variant-numeric: tabular-nums; }
|
|
28
|
+
.about-stat-lbl { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;
|
|
29
|
+
letter-spacing: 0.04em; }
|
|
30
|
+
|
|
31
|
+
/* -- Theme switcher -- */
|
|
32
|
+
.theme-switch { display: inline-flex; border-radius: var(--radius); overflow: hidden;
|
|
33
|
+
border: 1px solid var(--border); background: var(--bg); }
|
|
34
|
+
.theme-btn { padding: 0.45rem 1rem; font-size: 0.82rem; font-weight: 500;
|
|
35
|
+
background: transparent; border: none; color: var(--text-muted);
|
|
36
|
+
cursor: pointer; transition: all .15s ease; font-family: inherit;
|
|
37
|
+
display: inline-flex; align-items: center; gap: 0.35rem; }
|
|
38
|
+
.theme-btn:hover { color: var(--text); background: var(--bg-hover); }
|
|
39
|
+
.theme-btn.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
40
|
+
.theme-btn + .theme-btn { border-left: 1px solid var(--border); }
|
|
41
|
+
|
|
42
|
+
/* -- Feature categories -- */
|
|
43
|
+
.feat-section { margin-bottom: 0.5rem; }
|
|
44
|
+
.feat-label { font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
|
|
45
|
+
letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.4rem;
|
|
46
|
+
padding-left: 0.1rem; }
|
|
47
|
+
|
|
48
|
+
/* -- Test badge -- */
|
|
49
|
+
.test-badge { display: inline-flex; align-items: center; gap: 0.3rem;
|
|
50
|
+
padding: 0.2rem 0.55rem; border-radius: 999px;
|
|
51
|
+
font-size: 0.7rem; font-weight: 600; line-height: 1;
|
|
52
|
+
margin-top: 0.35rem; }
|
|
53
|
+
.test-badge.pass { background: rgba(63,185,80,.12); color: var(--success);
|
|
54
|
+
border: 1px solid rgba(63,185,80,.25); }
|
|
55
|
+
.test-badge.fail { background: rgba(248,81,73,.12); color: var(--danger);
|
|
56
|
+
border: 1px solid rgba(248,81,73,.25); }
|
|
57
|
+
.test-badge svg { width: 11px; height: 11px; }
|
|
58
|
+
|
|
59
|
+
@media (max-width: 768px) {
|
|
60
|
+
.about-hero { padding: 1.5rem 0.5rem 1rem; }
|
|
61
|
+
.about-hero h1 { font-size: 1.5rem; }
|
|
62
|
+
.about-stats { gap: 1rem; }
|
|
63
|
+
.about-stat-val { font-size: 1.1rem; }
|
|
64
|
+
.theme-btn { padding: 0.4rem 0.65rem; font-size: 0.78rem; }
|
|
65
|
+
}
|
|
66
|
+
@media (max-width: 480px) {
|
|
67
|
+
.about-hero h1 { font-size: 1.3rem; }
|
|
68
|
+
.about-stats { gap: 0.6rem 1rem; }
|
|
69
|
+
.about-stat-val { font-size: 1rem; }
|
|
70
|
+
}
|
|
71
|
+
`,
|
|
72
|
+
|
|
73
|
+
state: () => ({
|
|
74
|
+
theme: 'system',
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
mounted() {
|
|
78
|
+
this.state.theme = $.storage.get('theme') || 'system';
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
setTheme(mode) {
|
|
82
|
+
this.state.theme = mode;
|
|
83
|
+
$.storage.set('theme', mode);
|
|
84
|
+
window.__applyTheme(mode);
|
|
85
|
+
const label = mode === 'system' ? 'system (auto)' : mode;
|
|
86
|
+
$.bus.emit('toast', { message: `Theme: ${label}`, type: 'info' });
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
render() {
|
|
90
|
+
const t = this.state.theme;
|
|
91
|
+
const feats = {
|
|
92
|
+
'Core & Components': [
|
|
93
|
+
['$.component()', 'Reactive components with state, lifecycle, and template rendering'],
|
|
94
|
+
['computed / watch', 'Derived state and reactive watchers on the counter page'],
|
|
95
|
+
['DOM Diffing', 'Efficient morph() engine patches only changed DOM nodes'],
|
|
96
|
+
['z-key', 'Keyed list reconciliation in z-for loops'],
|
|
97
|
+
['z-model / z-ref', 'Two-way data binding and DOM element references'],
|
|
98
|
+
['CSP-safe expressions', 'Template expressions without eval() or new Function()'],
|
|
99
|
+
],
|
|
100
|
+
'Directives': [
|
|
101
|
+
['z-if / z-for / z-show', 'Structural directives for conditional & list rendering'],
|
|
102
|
+
['z-bind / z-class / z-style', 'Dynamic attributes, classes, and inline styles'],
|
|
103
|
+
['z-debounce', 'Debounced model updates — z-model z-debounce="300"'],
|
|
104
|
+
['z-lowercase / z-uppercase', 'Auto-transform text input on contacts email'],
|
|
105
|
+
['z-html', 'Trusted HTML injection in the playground'],
|
|
106
|
+
['templateUrl / styleUrl', 'External templates and CSS with auto-scoping'],
|
|
107
|
+
],
|
|
108
|
+
'Events': [
|
|
109
|
+
['@click.stop / .prevent', 'Event modifiers for fine-grained control'],
|
|
110
|
+
['@click.self / .once', 'Self-only and one-shot handlers in modals'],
|
|
111
|
+
['@click.outside', 'Outside-click detection for dropdowns'],
|
|
112
|
+
['@keydown.escape', 'Key modifiers — Escape to close forms'],
|
|
113
|
+
],
|
|
114
|
+
'Reactive Primitives': [
|
|
115
|
+
['$.signal() / $.computed()', 'Fine-grained reactive primitives for derived state'],
|
|
116
|
+
['$.effect()', 'Side-effects that auto-track signal dependencies'],
|
|
117
|
+
],
|
|
118
|
+
'State & Routing': [
|
|
119
|
+
['$.router()', 'SPA routing with history mode and fallback pages'],
|
|
120
|
+
['$.store()', 'Centralized state with actions, getters, subscriptions'],
|
|
121
|
+
['store.use / store.snapshot', 'Action middleware, deep-cloning, and history'],
|
|
122
|
+
['$.bus', 'Event bus for cross-component communication'],
|
|
123
|
+
['$.storage', 'localStorage wrapper for persisting preferences'],
|
|
124
|
+
],
|
|
125
|
+
'HTTP & Utilities': [
|
|
126
|
+
['$.get() / $.http', 'HTTP client, CRUD, interceptors, and abort signals'],
|
|
127
|
+
['$.pipe / $.memoize / $.retry', 'Function composition, LRU caching, resilient async'],
|
|
128
|
+
['$.escapeHtml()', 'Safe rendering of user-generated content'],
|
|
129
|
+
['$.fn', 'Custom chainable collection methods via plugins'],
|
|
130
|
+
['fadeIn / fadeOut / slideToggle', 'Promise-based animations with chaining'],
|
|
131
|
+
['$.on()', 'Global delegated event listeners'],
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return `
|
|
136
|
+
<div class="about-hero">
|
|
137
|
+
<span class="ver">v${$.version}</span>
|
|
138
|
+
<h1>zQuery</h1>
|
|
139
|
+
<p>A zero-dependency frontend micro-library — reactive components, routing, state management, and more in <strong>${$.libSize}</strong> minified.</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="about-stats">
|
|
143
|
+
<div class="about-stat"><div class="about-stat-val">${$.libSize}</div><div class="about-stat-lbl">Minified</div></div>
|
|
144
|
+
<div class="about-stat"><div class="about-stat-val">0</div><div class="about-stat-lbl">Dependencies</div></div>
|
|
145
|
+
<div class="about-stat">
|
|
146
|
+
<div class="about-stat-val">${$.unitTests.total}</div>
|
|
147
|
+
<div class="about-stat-lbl">Tests</div>
|
|
148
|
+
<span class="test-badge ${$.unitTests.ok ? 'pass' : 'fail'}">${$.unitTests.ok
|
|
149
|
+
? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg> passing'
|
|
150
|
+
: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg> ' + $.unitTests.failed + ' failing'}</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="card">
|
|
155
|
+
<h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42"/></svg> Theme</h3>
|
|
156
|
+
<p>Choose your preferred appearance. <strong>System</strong> follows your OS setting automatically.</p>
|
|
157
|
+
<div class="theme-switch">
|
|
158
|
+
<button class="theme-btn ${t === 'system' ? 'active' : ''}" @click="setTheme('system')">
|
|
159
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:14px;height:14px;"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25Z"/></svg>
|
|
160
|
+
System
|
|
161
|
+
</button>
|
|
162
|
+
<button class="theme-btn ${t === 'dark' ? 'active' : ''}" @click="setTheme('dark')">
|
|
163
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:14px;height:14px;"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/></svg>
|
|
164
|
+
Dark
|
|
165
|
+
</button>
|
|
166
|
+
<button class="theme-btn ${t === 'light' ? 'active' : ''}" @click="setTheme('light')">
|
|
167
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:14px;height:14px;"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/></svg>
|
|
168
|
+
Light
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="card">
|
|
174
|
+
<h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75a4.5 4.5 0 0 1-4.884 4.484c-1.076-.091-2.264.071-2.95.904l-7.152 8.684a2.548 2.548 0 1 1-3.586-3.586l8.684-7.152c.833-.686.995-1.874.904-2.95a4.5 4.5 0 0 1 6.336-4.486l-3.276 3.276a3.004 3.004 0 0 0 2.25 2.25l3.276-3.276c.256.565.398 1.192.398 1.852Z"/></svg> Features Used in This App</h3>
|
|
175
|
+
${Object.entries(feats).map(([cat, items]) => `
|
|
176
|
+
<div class="feat-section">
|
|
177
|
+
<div class="feat-label">${cat}</div>
|
|
178
|
+
<div class="feature-grid">
|
|
179
|
+
${items.map(([name, desc]) => `
|
|
180
|
+
<div class="feature-item">
|
|
181
|
+
<strong>${name}</strong>
|
|
182
|
+
<span>${desc}</span>
|
|
183
|
+
</div>
|
|
184
|
+
`).join('')}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
`).join('')}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="card card-muted">
|
|
191
|
+
<h3><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--accent)" style="width:20px;height:20px;vertical-align:-4px;margin-right:0.25rem;"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/></svg> Next Steps</h3>
|
|
192
|
+
<ul class="next-steps">
|
|
193
|
+
<li>Read the <a href="https://z-query.com/docs" target="_blank" rel="noopener">full documentation</a></li>
|
|
194
|
+
<li>Explore the <a href="https://github.com/tonywied17/zero-query" target="_blank" rel="noopener">source on GitHub</a></li>
|
|
195
|
+
<li>Run <code>npx zquery bundle</code> to build for production</li>
|
|
196
|
+
<li>Run <code>npx zquery dev</code> for live-reload development</li>
|
|
197
|
+
</ul>
|
|
198
|
+
</div>
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
});
|