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/src/component.js
CHANGED
|
@@ -502,11 +502,14 @@ class Component {
|
|
|
502
502
|
this._bindRefs();
|
|
503
503
|
this._bindModels();
|
|
504
504
|
|
|
505
|
-
// Restore focus if the morph replaced the focused element
|
|
505
|
+
// Restore focus if the morph replaced the focused element.
|
|
506
|
+
// Always restore selectionRange — even when the element is still
|
|
507
|
+
// the activeElement — because _bindModels or morph attribute syncing
|
|
508
|
+
// can alter the value and move the cursor.
|
|
506
509
|
if (_focusInfo) {
|
|
507
510
|
const el = this._el.querySelector(_focusInfo.selector);
|
|
508
|
-
if (el
|
|
509
|
-
el.focus();
|
|
511
|
+
if (el) {
|
|
512
|
+
if (el !== document.activeElement) el.focus();
|
|
510
513
|
try {
|
|
511
514
|
if (_focusInfo.start !== null && _focusInfo.start !== undefined) {
|
|
512
515
|
el.setSelectionRange(_focusInfo.start, _focusInfo.end, _focusInfo.dir);
|
package/src/diff.js
CHANGED
|
@@ -89,6 +89,11 @@ export function morphElement(oldEl, newHTML) {
|
|
|
89
89
|
return clone;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Aliases for the concat build — core.js imports these as _morph / _morphElement,
|
|
93
|
+
// but the build strips `import … as` lines, so the aliases must exist at runtime.
|
|
94
|
+
const _morph = morph;
|
|
95
|
+
const _morphElement = morphElement;
|
|
96
|
+
|
|
92
97
|
/**
|
|
93
98
|
* Reconcile children of `oldParent` to match `newParent`.
|
|
94
99
|
*
|
|
@@ -434,6 +439,13 @@ function _morphAttributes(oldEl, newEl) {
|
|
|
434
439
|
|
|
435
440
|
/**
|
|
436
441
|
* Sync input element value, checked, disabled states.
|
|
442
|
+
*
|
|
443
|
+
* Only updates the value when the new HTML explicitly carries a `value`
|
|
444
|
+
* attribute. Templates that use z-model manage values through reactive
|
|
445
|
+
* state + _bindModels — the morph engine should not interfere by wiping
|
|
446
|
+
* a live input's content to '' just because the template has no `value`
|
|
447
|
+
* attr. This prevents the wipe-then-restore cycle that resets cursor
|
|
448
|
+
* position on every keystroke.
|
|
437
449
|
*/
|
|
438
450
|
function _syncInputValue(oldEl, newEl) {
|
|
439
451
|
const type = (oldEl.type || '').toLowerCase();
|
|
@@ -441,8 +453,9 @@ function _syncInputValue(oldEl, newEl) {
|
|
|
441
453
|
if (type === 'checkbox' || type === 'radio') {
|
|
442
454
|
if (oldEl.checked !== newEl.checked) oldEl.checked = newEl.checked;
|
|
443
455
|
} else {
|
|
444
|
-
|
|
445
|
-
|
|
456
|
+
const newVal = newEl.getAttribute('value');
|
|
457
|
+
if (newVal !== null && oldEl.value !== newVal) {
|
|
458
|
+
oldEl.value = newVal;
|
|
446
459
|
}
|
|
447
460
|
}
|
|
448
461
|
|
package/tests/cli.test.js
CHANGED
|
@@ -454,3 +454,307 @@ describe('CLI — flag/option extra', () => {
|
|
|
454
454
|
expect(flag('watch', 'w')).toBe(true);
|
|
455
455
|
});
|
|
456
456
|
});
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
// ===========================================================================
|
|
460
|
+
// createProject — scaffold command
|
|
461
|
+
// ===========================================================================
|
|
462
|
+
|
|
463
|
+
describe('CLI — createProject', () => {
|
|
464
|
+
const fs = require('fs');
|
|
465
|
+
const path = require('path');
|
|
466
|
+
const os = require('os');
|
|
467
|
+
|
|
468
|
+
let createProject;
|
|
469
|
+
|
|
470
|
+
beforeEach(async () => {
|
|
471
|
+
vi.resetModules();
|
|
472
|
+
const mod = await import('../cli/commands/create.js');
|
|
473
|
+
createProject = mod.default || mod;
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
function tmpDir() {
|
|
477
|
+
const dir = path.join(os.tmpdir(), 'zq-create-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8));
|
|
478
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
479
|
+
return dir;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function cleanup(dir) {
|
|
483
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// -- scaffold variant directories exist --
|
|
487
|
+
|
|
488
|
+
it('default scaffold directory exists', () => {
|
|
489
|
+
const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
|
|
490
|
+
expect(fs.existsSync(dir)).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('minimal scaffold directory exists', () => {
|
|
494
|
+
const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
|
|
495
|
+
expect(fs.existsSync(dir)).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// -- default scaffold contains expected files --
|
|
499
|
+
|
|
500
|
+
it('default scaffold has index.html', () => {
|
|
501
|
+
const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'index.html');
|
|
502
|
+
expect(fs.existsSync(f)).toBe(true);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('default scaffold has app/app.js', () => {
|
|
506
|
+
const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'app.js');
|
|
507
|
+
expect(fs.existsSync(f)).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('default scaffold has app/routes.js', () => {
|
|
511
|
+
const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'routes.js');
|
|
512
|
+
expect(fs.existsSync(f)).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('default scaffold has app/store.js', () => {
|
|
516
|
+
const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'app', 'store.js');
|
|
517
|
+
expect(fs.existsSync(f)).toBe(true);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// -- minimal scaffold contains expected files --
|
|
521
|
+
|
|
522
|
+
it('minimal scaffold has index.html', () => {
|
|
523
|
+
const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'index.html');
|
|
524
|
+
expect(fs.existsSync(f)).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('minimal scaffold has app/app.js', () => {
|
|
528
|
+
const f = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'app', 'app.js');
|
|
529
|
+
expect(fs.existsSync(f)).toBe(true);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('minimal scaffold has 4 components', () => {
|
|
533
|
+
const dir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'app', 'components');
|
|
534
|
+
const files = fs.readdirSync(dir).sort();
|
|
535
|
+
expect(files).toEqual(['about.js', 'counter.js', 'home.js', 'not-found.js']);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// -- minimal scaffold is a subset of default --
|
|
539
|
+
|
|
540
|
+
it('minimal scaffold has fewer files than default', () => {
|
|
541
|
+
function countFiles(dir) {
|
|
542
|
+
let count = 0;
|
|
543
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
544
|
+
if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
|
|
545
|
+
else count++;
|
|
546
|
+
}
|
|
547
|
+
return count;
|
|
548
|
+
}
|
|
549
|
+
const defDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
|
|
550
|
+
const minDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
|
|
551
|
+
expect(countFiles(minDir)).toBeLessThan(countFiles(defDir));
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// -- {{NAME}} replacement --
|
|
555
|
+
|
|
556
|
+
it('scaffold templates contain {{NAME}} placeholder', () => {
|
|
557
|
+
const html = fs.readFileSync(
|
|
558
|
+
path.resolve(__dirname, '..', 'cli', 'scaffold', 'default', 'index.html'), 'utf-8'
|
|
559
|
+
);
|
|
560
|
+
expect(html).toContain('{{NAME}}');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('minimal scaffold templates contain {{NAME}} placeholder', () => {
|
|
564
|
+
const html = fs.readFileSync(
|
|
565
|
+
path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal', 'index.html'), 'utf-8'
|
|
566
|
+
);
|
|
567
|
+
expect(html).toContain('{{NAME}}');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// -- walkDir functionality (tested via module internals) --
|
|
571
|
+
|
|
572
|
+
it('scaffolds default project into a target directory', () => {
|
|
573
|
+
const target = tmpDir();
|
|
574
|
+
const projDir = path.join(target, 'test-app');
|
|
575
|
+
|
|
576
|
+
// Simulate: process.argv for default (no --minimal)
|
|
577
|
+
process.argv = ['node', 'zquery', 'create', 'test-app'];
|
|
578
|
+
vi.resetModules();
|
|
579
|
+
|
|
580
|
+
// We can't easily call createProject because it uses process.exit.
|
|
581
|
+
// Instead, test the walkDir + copy logic directly.
|
|
582
|
+
const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'default');
|
|
583
|
+
|
|
584
|
+
function walkDir(dir, prefix = '') {
|
|
585
|
+
const entries = [];
|
|
586
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
587
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
588
|
+
if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
|
|
589
|
+
else entries.push(rel);
|
|
590
|
+
}
|
|
591
|
+
return entries;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const files = walkDir(scaffoldDir);
|
|
595
|
+
expect(files.length).toBeGreaterThan(5);
|
|
596
|
+
expect(files).toContain('index.html');
|
|
597
|
+
expect(files).toContain('global.css');
|
|
598
|
+
expect(files).toContain('app/app.js');
|
|
599
|
+
expect(files).toContain('app/routes.js');
|
|
600
|
+
expect(files).toContain('app/store.js');
|
|
601
|
+
|
|
602
|
+
cleanup(target);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('walkDir lists minimal scaffold files correctly', () => {
|
|
606
|
+
const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
|
|
607
|
+
|
|
608
|
+
function walkDir(dir, prefix = '') {
|
|
609
|
+
const entries = [];
|
|
610
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
611
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
612
|
+
if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
|
|
613
|
+
else entries.push(rel);
|
|
614
|
+
}
|
|
615
|
+
return entries;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const files = walkDir(scaffoldDir);
|
|
619
|
+
expect(files).toContain('index.html');
|
|
620
|
+
expect(files).toContain('global.css');
|
|
621
|
+
expect(files).toContain('app/app.js');
|
|
622
|
+
expect(files).toContain('app/routes.js');
|
|
623
|
+
expect(files).toContain('app/store.js');
|
|
624
|
+
expect(files).toContain('app/components/home.js');
|
|
625
|
+
expect(files).toContain('app/components/counter.js');
|
|
626
|
+
expect(files).toContain('app/components/about.js');
|
|
627
|
+
expect(files).toContain('app/components/not-found.js');
|
|
628
|
+
expect(files).toContain('assets/.gitkeep');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// -- {{NAME}} replacement in copied files --
|
|
632
|
+
|
|
633
|
+
it('replaces {{NAME}} in copied scaffold files', () => {
|
|
634
|
+
const target = tmpDir();
|
|
635
|
+
const scaffoldDir = path.resolve(__dirname, '..', 'cli', 'scaffold', 'minimal');
|
|
636
|
+
const projectName = 'my-cool-app';
|
|
637
|
+
|
|
638
|
+
function walkDir(dir, prefix = '') {
|
|
639
|
+
const entries = [];
|
|
640
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
641
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
642
|
+
if (entry.isDirectory()) entries.push(...walkDir(path.join(dir, entry.name), rel));
|
|
643
|
+
else entries.push(rel);
|
|
644
|
+
}
|
|
645
|
+
return entries;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const files = walkDir(scaffoldDir);
|
|
649
|
+
|
|
650
|
+
for (const rel of files) {
|
|
651
|
+
let content = fs.readFileSync(path.join(scaffoldDir, rel), 'utf-8');
|
|
652
|
+
content = content.replace(/\{\{NAME\}\}/g, projectName);
|
|
653
|
+
const dest = path.join(target, rel);
|
|
654
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
655
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const html = fs.readFileSync(path.join(target, 'index.html'), 'utf-8');
|
|
659
|
+
expect(html).toContain(projectName);
|
|
660
|
+
expect(html).not.toContain('{{NAME}}');
|
|
661
|
+
|
|
662
|
+
const appJs = fs.readFileSync(path.join(target, 'app', 'app.js'), 'utf-8');
|
|
663
|
+
expect(appJs).toContain(projectName);
|
|
664
|
+
expect(appJs).not.toContain('{{NAME}}');
|
|
665
|
+
|
|
666
|
+
cleanup(target);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// -- conflict detection --
|
|
670
|
+
|
|
671
|
+
it('conflicts array detects existing files', () => {
|
|
672
|
+
const target = tmpDir();
|
|
673
|
+
fs.writeFileSync(path.join(target, 'index.html'), 'existing');
|
|
674
|
+
fs.mkdirSync(path.join(target, 'app'), { recursive: true });
|
|
675
|
+
|
|
676
|
+
const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
|
|
677
|
+
fs.existsSync(path.join(target, f))
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
expect(conflicts).toContain('index.html');
|
|
681
|
+
expect(conflicts).toContain('app');
|
|
682
|
+
expect(conflicts).not.toContain('global.css');
|
|
683
|
+
expect(conflicts).not.toContain('assets');
|
|
684
|
+
|
|
685
|
+
cleanup(target);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('no conflicts in empty directory', () => {
|
|
689
|
+
const target = tmpDir();
|
|
690
|
+
|
|
691
|
+
const conflicts = ['index.html', 'global.css', 'app', 'assets'].filter(f =>
|
|
692
|
+
fs.existsSync(path.join(target, f))
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
expect(conflicts).toHaveLength(0);
|
|
696
|
+
|
|
697
|
+
cleanup(target);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// -- flag parsing for --minimal --
|
|
701
|
+
|
|
702
|
+
it('--minimal flag resolves to minimal variant', async () => {
|
|
703
|
+
process.argv = ['node', 'zquery', 'create', '--minimal', 'my-app'];
|
|
704
|
+
vi.resetModules();
|
|
705
|
+
const { flag } = await import('../cli/args.js');
|
|
706
|
+
expect(flag('minimal', 'm')).toBe(true);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('-m short flag resolves to minimal variant', async () => {
|
|
710
|
+
process.argv = ['node', 'zquery', 'create', '-m', 'my-app'];
|
|
711
|
+
vi.resetModules();
|
|
712
|
+
const { flag } = await import('../cli/args.js');
|
|
713
|
+
expect(flag('minimal', 'm')).toBe(true);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('no flag defaults to default variant', async () => {
|
|
717
|
+
process.argv = ['node', 'zquery', 'create', 'my-app'];
|
|
718
|
+
vi.resetModules();
|
|
719
|
+
const { flag } = await import('../cli/args.js');
|
|
720
|
+
expect(flag('minimal', 'm')).toBe(false);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// -- dirArg parsing skips flags --
|
|
724
|
+
|
|
725
|
+
it('dirArg parsing skips flag args', () => {
|
|
726
|
+
const args = ['create', '--minimal', 'my-app'];
|
|
727
|
+
const dirArg = args.slice(1).find(a => !a.startsWith('-'));
|
|
728
|
+
expect(dirArg).toBe('my-app');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('dirArg parsing returns first positional', () => {
|
|
732
|
+
const args = ['create', 'my-app'];
|
|
733
|
+
const dirArg = args.slice(1).find(a => !a.startsWith('-'));
|
|
734
|
+
expect(dirArg).toBe('my-app');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('dirArg is undefined when no positional given', () => {
|
|
738
|
+
const args = ['create', '--minimal'];
|
|
739
|
+
const dirArg = args.slice(1).find(a => !a.startsWith('-'));
|
|
740
|
+
expect(dirArg).toBeUndefined();
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('dirArg with flag after dir name', () => {
|
|
744
|
+
const args = ['create', 'my-app', '--minimal'];
|
|
745
|
+
const dirArg = args.slice(1).find(a => !a.startsWith('-'));
|
|
746
|
+
expect(dirArg).toBe('my-app');
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// -- help text mentions --minimal --
|
|
750
|
+
|
|
751
|
+
it('help text includes --minimal flag', async () => {
|
|
752
|
+
vi.resetModules();
|
|
753
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
754
|
+
const showHelp = (await import('../cli/help.js')).default;
|
|
755
|
+
showHelp();
|
|
756
|
+
const text = spy.mock.calls.map(c => c.join(' ')).join('\n');
|
|
757
|
+
expect(text).toContain('--minimal');
|
|
758
|
+
spy.mockRestore();
|
|
759
|
+
});
|
|
760
|
+
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// scripts/components/about.js — about page with theme switcher
|
|
2
|
-
//
|
|
3
|
-
// Demonstrates: $.storage (localStorage wrapper), $.bus for notifications,
|
|
4
|
-
// $.version, component methods, data-theme attribute toggling
|
|
5
|
-
|
|
6
|
-
$.component('about-page', {
|
|
7
|
-
state: () => ({
|
|
8
|
-
theme: 'dark',
|
|
9
|
-
}),
|
|
10
|
-
|
|
11
|
-
mounted() {
|
|
12
|
-
// Read persisted theme via $.storage
|
|
13
|
-
this.state.theme = $.storage.get('theme') || 'dark';
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
toggleTheme() {
|
|
17
|
-
const next = this.state.theme === 'dark' ? 'light' : 'dark';
|
|
18
|
-
this.state.theme = next;
|
|
19
|
-
// Apply theme via data attribute
|
|
20
|
-
document.documentElement.setAttribute('data-theme', next);
|
|
21
|
-
// Persist via $.storage (wraps localStorage)
|
|
22
|
-
$.storage.set('theme', next);
|
|
23
|
-
$.bus.emit('toast', { message: `Switched to ${next} theme`, type: 'info' });
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
render() {
|
|
27
|
-
return `
|
|
28
|
-
<div class="page-header">
|
|
29
|
-
<h1>About</h1>
|
|
30
|
-
<p class="subtitle">Built with zQuery v${$.version} — a zero-dependency frontend library.</p>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
<div class="card">
|
|
34
|
-
<h3><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>
|
|
35
|
-
<p>Toggle between dark and light mode. Persisted to <code>localStorage</code> via <code>$.storage</code>.</p>
|
|
36
|
-
<div class="theme-toggle">
|
|
37
|
-
<span>Current: <strong>${this.state.theme}</strong></span>
|
|
38
|
-
<button class="btn btn-outline" @click="toggleTheme">${this.state.theme === 'dark' ? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:16px;height:16px;vertical-align:-3px;margin-right:0.15rem;"><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> Light Mode' : '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:16px;height:16px;vertical-align:-3px;margin-right:0.15rem;"><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> Dark Mode'}</button>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div class="card">
|
|
43
|
-
<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>
|
|
44
|
-
<div class="feature-grid">
|
|
45
|
-
<div class="feature-item">
|
|
46
|
-
<strong>$.component()</strong>
|
|
47
|
-
<span>Reactive components with state, lifecycle hooks, and template rendering</span>
|
|
48
|
-
</div>
|
|
49
|
-
<div class="feature-item">
|
|
50
|
-
<strong>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>
|
|
61
|
-
<div class="feature-item">
|
|
62
|
-
<strong>$.router()</strong>
|
|
63
|
-
<span>SPA routing with history mode, z-link navigation, and fallback pages</span>
|
|
64
|
-
</div>
|
|
65
|
-
<div class="feature-item">
|
|
66
|
-
<strong>$.store()</strong>
|
|
67
|
-
<span>Centralized state management with actions, getters, and subscriptions</span>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="feature-item">
|
|
70
|
-
<strong>$.get()</strong>
|
|
71
|
-
<span>HTTP client for fetching JSON APIs with async/await</span>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="feature-item">
|
|
74
|
-
<strong>$.signal() / $.computed()</strong>
|
|
75
|
-
<span>Fine-grained reactive primitives for derived state</span>
|
|
76
|
-
</div>
|
|
77
|
-
<div class="feature-item">
|
|
78
|
-
<strong>$.bus</strong>
|
|
79
|
-
<span>Event bus for cross-component communication (toast notifications)</span>
|
|
80
|
-
</div>
|
|
81
|
-
<div class="feature-item">
|
|
82
|
-
<strong>$.storage</strong>
|
|
83
|
-
<span>localStorage wrapper for persisting user preferences</span>
|
|
84
|
-
</div>
|
|
85
|
-
<div class="feature-item">
|
|
86
|
-
<strong>$.debounce()</strong>
|
|
87
|
-
<span>Debounced search input in the todos filter</span>
|
|
88
|
-
</div>
|
|
89
|
-
<div class="feature-item">
|
|
90
|
-
<strong>$.escapeHtml()</strong>
|
|
91
|
-
<span>Safe rendering of user-generated and API content</span>
|
|
92
|
-
</div>
|
|
93
|
-
<div class="feature-item">
|
|
94
|
-
<strong>CSP-safe expressions</strong>
|
|
95
|
-
<span>Template expressions evaluated without <code>eval()</code> or <code>new Function()</code></span>
|
|
96
|
-
</div>
|
|
97
|
-
<div class="feature-item">
|
|
98
|
-
<strong>z-model / z-ref</strong>
|
|
99
|
-
<span>Two-way data binding and DOM element references</span>
|
|
100
|
-
</div>
|
|
101
|
-
<div class="feature-item">
|
|
102
|
-
<strong>templateUrl / styleUrl</strong>
|
|
103
|
-
<span>External HTML templates and CSS with auto-scoping (contacts page)</span>
|
|
104
|
-
</div>
|
|
105
|
-
<div class="feature-item">
|
|
106
|
-
<strong>z-if / z-for / z-show</strong>
|
|
107
|
-
<span>Structural directives for conditional & list rendering</span>
|
|
108
|
-
</div>
|
|
109
|
-
<div class="feature-item">
|
|
110
|
-
<strong>z-bind / z-class / z-style</strong>
|
|
111
|
-
<span>Dynamic attributes, classes, and inline styles</span>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="feature-item">
|
|
114
|
-
<strong>$.on()</strong>
|
|
115
|
-
<span>Global delegated event listeners for the hamburger menu</span>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div class="card card-muted">
|
|
121
|
-
<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>
|
|
122
|
-
<ul class="next-steps">
|
|
123
|
-
<li>Read the <a href="https://z-query.com/docs" target="_blank" rel="noopener">full documentation</a></li>
|
|
124
|
-
<li>Explore the <a href="https://github.com/tonywied17/zero-query" target="_blank" rel="noopener">source on GitHub</a></li>
|
|
125
|
-
<li>Run <code>npx zquery bundle</code> to build for production</li>
|
|
126
|
-
<li>Run <code>npx zquery dev</code> for live-reload development</li>
|
|
127
|
-
</ul>
|
|
128
|
-
</div>
|
|
129
|
-
`;
|
|
130
|
-
}
|
|
131
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// scripts/components/api-demo.js — HTTP client demonstration
|
|
2
|
-
//
|
|
3
|
-
// Demonstrates: $.get() for fetching JSON, z-if/z-else conditional
|
|
4
|
-
// rendering, z-show visibility, z-for list rendering,
|
|
5
|
-
// z-text content binding, @click event handling,
|
|
6
|
-
// loading/error states, $.escapeHtml(), async patterns
|
|
7
|
-
|
|
8
|
-
$.component('api-demo', {
|
|
9
|
-
state: () => ({
|
|
10
|
-
users: [],
|
|
11
|
-
selectedUser: null,
|
|
12
|
-
posts: [],
|
|
13
|
-
loading: false,
|
|
14
|
-
error: '',
|
|
15
|
-
}),
|
|
16
|
-
|
|
17
|
-
mounted() {
|
|
18
|
-
this.fetchUsers();
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
async fetchUsers() {
|
|
22
|
-
this.state.loading = true;
|
|
23
|
-
this.state.error = '';
|
|
24
|
-
try {
|
|
25
|
-
// $.get() — zero-config JSON fetching
|
|
26
|
-
const res = await $.get('https://jsonplaceholder.typicode.com/users');
|
|
27
|
-
this.state.users = res.data.slice(0, 6);
|
|
28
|
-
} catch (err) {
|
|
29
|
-
this.state.error = 'Failed to load users. Check your connection.';
|
|
30
|
-
}
|
|
31
|
-
this.state.loading = false;
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
async selectUser(id) {
|
|
35
|
-
this.state.selectedUser = this.state.users.find(u => u.id === Number(id));
|
|
36
|
-
this.state.loading = true;
|
|
37
|
-
try {
|
|
38
|
-
const res = await $.get(`https://jsonplaceholder.typicode.com/posts?userId=${id}`);
|
|
39
|
-
this.state.posts = res.data.slice(0, 4);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
this.state.error = 'Failed to load posts.';
|
|
42
|
-
}
|
|
43
|
-
this.state.loading = false;
|
|
44
|
-
$.bus.emit('toast', { message: `Loaded posts for ${this.state.selectedUser.name}`, type: 'success' });
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
clearSelection() {
|
|
48
|
-
this.state.selectedUser = null;
|
|
49
|
-
this.state.posts = [];
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
render() {
|
|
53
|
-
const { selectedUser } = this.state;
|
|
54
|
-
|
|
55
|
-
return `
|
|
56
|
-
<div class="page-header">
|
|
57
|
-
<h1>API Demo</h1>
|
|
58
|
-
<p class="subtitle">Fetching data with <code>$.get()</code>. Directives: <code>z-if</code>, <code>z-show</code>, <code>z-for</code>, <code>z-text</code>.</p>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<div class="card card-error" z-show="error"><p><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:16px;height:16px;vertical-align:-3px;margin-right:0.15rem;"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/></svg> <span z-text="error"></span></p></div>
|
|
62
|
-
<div class="loading-bar" z-show="loading"></div>
|
|
63
|
-
|
|
64
|
-
<div z-if="!selectedUser">
|
|
65
|
-
<div class="card">
|
|
66
|
-
<h3>Users</h3>
|
|
67
|
-
<p class="muted">Click a user to fetch their posts.</p>
|
|
68
|
-
<div class="user-grid" z-if="users.length > 0">
|
|
69
|
-
<button z-for="u in users" class="user-card" @click="selectUser({{u.id}})">
|
|
70
|
-
<strong>{{u.name}}</strong>
|
|
71
|
-
<small>@{{u.username}}</small>
|
|
72
|
-
<small class="muted">{{u.company.name}}</small>
|
|
73
|
-
</button>
|
|
74
|
-
</div>
|
|
75
|
-
<p z-else z-show="!loading">No users loaded.</p>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
<div z-else>
|
|
80
|
-
<div class="card">
|
|
81
|
-
<div class="user-detail-header">
|
|
82
|
-
<div>
|
|
83
|
-
<h3>${selectedUser ? $.escapeHtml(selectedUser.name) : ''}</h3>
|
|
84
|
-
<p class="muted">${selectedUser ? `@${$.escapeHtml(selectedUser.username)} · ${$.escapeHtml(selectedUser.email)}` : ''}</p>
|
|
85
|
-
</div>
|
|
86
|
-
<button class="btn btn-ghost btn-sm" @click="clearSelection">← Back</button>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
<div class="card">
|
|
91
|
-
<h3>Recent Posts</h3>
|
|
92
|
-
<div class="posts-list" z-if="posts.length > 0">
|
|
93
|
-
<article z-for="p in posts" class="post-item">
|
|
94
|
-
<h4>{{p.title}}</h4>
|
|
95
|
-
<p>{{p.body.substring(0, 120)}}…</p>
|
|
96
|
-
</article>
|
|
97
|
-
</div>
|
|
98
|
-
<p z-else class="muted" z-show="!loading">No posts found.</p>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
`;
|
|
102
|
-
}
|
|
103
|
-
});
|