zero-query 0.9.9 → 1.0.1

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.
Files changed (99) hide show
  1. package/README.md +34 -33
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +21 -18
  5. package/cli/commands/create.js +9 -2
  6. package/cli/commands/dev/devtools/index.js +1 -1
  7. package/cli/commands/dev/devtools/js/core.js +14 -14
  8. package/cli/commands/dev/devtools/js/elements.js +4 -4
  9. package/cli/commands/dev/devtools/js/stats.js +1 -1
  10. package/cli/commands/dev/devtools/styles.css +2 -2
  11. package/cli/commands/dev/index.js +2 -2
  12. package/cli/commands/dev/logger.js +1 -1
  13. package/cli/commands/dev/overlay.js +21 -14
  14. package/cli/commands/dev/server.js +5 -5
  15. package/cli/commands/dev/validator.js +7 -7
  16. package/cli/commands/dev/watcher.js +6 -6
  17. package/cli/help.js +3 -3
  18. package/cli/index.js +1 -1
  19. package/cli/scaffold/default/app/app.js +17 -18
  20. package/cli/scaffold/default/app/components/about.js +9 -9
  21. package/cli/scaffold/default/app/components/api-demo.js +6 -6
  22. package/cli/scaffold/default/app/components/contact-card.js +4 -4
  23. package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
  24. package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
  25. package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
  26. package/cli/scaffold/default/app/components/counter.js +8 -8
  27. package/cli/scaffold/default/app/components/home.js +13 -13
  28. package/cli/scaffold/default/app/components/not-found.js +1 -1
  29. package/cli/scaffold/default/app/components/playground/playground.css +1 -1
  30. package/cli/scaffold/default/app/components/playground/playground.html +11 -11
  31. package/cli/scaffold/default/app/components/playground/playground.js +11 -11
  32. package/cli/scaffold/default/app/components/todos.js +8 -8
  33. package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
  34. package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
  35. package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
  36. package/cli/scaffold/default/app/routes.js +1 -1
  37. package/cli/scaffold/default/app/store.js +1 -1
  38. package/cli/scaffold/default/global.css +2 -2
  39. package/cli/scaffold/default/index.html +2 -2
  40. package/cli/scaffold/minimal/app/app.js +6 -7
  41. package/cli/scaffold/minimal/app/components/about.js +5 -5
  42. package/cli/scaffold/minimal/app/components/counter.js +6 -6
  43. package/cli/scaffold/minimal/app/components/home.js +8 -8
  44. package/cli/scaffold/minimal/app/components/not-found.js +1 -1
  45. package/cli/scaffold/minimal/app/routes.js +1 -1
  46. package/cli/scaffold/minimal/app/store.js +1 -1
  47. package/cli/scaffold/minimal/global.css +2 -2
  48. package/cli/scaffold/minimal/index.html +1 -1
  49. package/cli/scaffold/ssr/app/app.js +1 -2
  50. package/cli/scaffold/ssr/app/components/about.js +5 -5
  51. package/cli/scaffold/ssr/app/components/home.js +2 -2
  52. package/cli/scaffold/ssr/app/components/not-found.js +2 -2
  53. package/cli/scaffold/ssr/app/routes.js +1 -1
  54. package/cli/scaffold/ssr/global.css +3 -4
  55. package/cli/scaffold/ssr/index.html +2 -2
  56. package/cli/scaffold/ssr/server/index.js +26 -25
  57. package/cli/utils.js +6 -6
  58. package/dist/zquery.dist.zip +0 -0
  59. package/dist/zquery.js +508 -227
  60. package/dist/zquery.min.js +2 -2
  61. package/index.d.ts +16 -13
  62. package/index.js +7 -5
  63. package/package.json +3 -3
  64. package/src/component.js +64 -63
  65. package/src/core.js +15 -15
  66. package/src/diff.js +38 -38
  67. package/src/errors.js +17 -17
  68. package/src/expression.js +15 -17
  69. package/src/http.js +4 -4
  70. package/src/reactive.js +75 -9
  71. package/src/router.js +104 -24
  72. package/src/ssr.js +28 -28
  73. package/src/store.js +103 -21
  74. package/src/utils.js +64 -12
  75. package/tests/audit.test.js +143 -15
  76. package/tests/cli.test.js +20 -20
  77. package/tests/component.test.js +121 -121
  78. package/tests/core.test.js +56 -56
  79. package/tests/diff.test.js +42 -42
  80. package/tests/errors.test.js +5 -5
  81. package/tests/expression.test.js +58 -53
  82. package/tests/http.test.js +20 -20
  83. package/tests/reactive.test.js +185 -24
  84. package/tests/router.test.js +501 -74
  85. package/tests/ssr.test.js +15 -13
  86. package/tests/store.test.js +264 -23
  87. package/tests/test-minifier.js +153 -0
  88. package/tests/test-ssr.js +27 -0
  89. package/tests/utils.test.js +163 -26
  90. package/types/collection.d.ts +2 -2
  91. package/types/component.d.ts +5 -5
  92. package/types/errors.d.ts +3 -3
  93. package/types/http.d.ts +3 -3
  94. package/types/misc.d.ts +9 -9
  95. package/types/reactive.d.ts +25 -3
  96. package/types/router.d.ts +10 -6
  97. package/types/ssr.d.ts +2 -2
  98. package/types/store.d.ts +40 -5
  99. package/types/utils.d.ts +1 -1
@@ -0,0 +1,153 @@
1
+ function minifyCSS(css) {
2
+ css = css.replace(/\/\*[\s\S]*?\*\//g, '');
3
+ css = css.replace(/\s{2,}/g, ' ');
4
+ css = css.replace(/\s*([{};,])\s*/g, '$1');
5
+ css = css.replace(/:\s+/g, ':');
6
+ css = css.replace(/;}/g, '}');
7
+ return css.trim();
8
+ }
9
+
10
+ const tests = [
11
+ // Pseudo-classes with descendant combinator (the bug we fixed)
12
+ ['.docs-section :not(pre) > code { color: red }', 'space before :not()'],
13
+ ['.foo :has(.bar) { color: red }', 'space before :has()'],
14
+ ['.foo :is(.a, .b) { color: red }', 'space before :is()'],
15
+ ['.foo :where(.a) { color: red }', 'space before :where()'],
16
+
17
+ // Pseudo-elements
18
+ ['.foo::before { content: "x" }', '::before'],
19
+ ['.foo ::after { content: "x" }', 'space before ::after'],
20
+
21
+ // Pseudo-classes directly on element (no space — should stay compact)
22
+ ['a:hover { color: red }', ':hover no space'],
23
+ ['li:nth-child(2n + 1) { color: red }', ':nth-child()'],
24
+ ['input:focus-visible { outline: 1px }', ':focus-visible'],
25
+
26
+ // calc() — spaces around operators are significant!
27
+ ['.foo { width: calc(100% - 20px) }', 'calc() spaces'],
28
+ ['.foo { width: calc(50vw + 2rem) }', 'calc() addition'],
29
+ ['.foo { font-size: clamp(1rem, 2vw, 3rem) }', 'clamp()'],
30
+
31
+ // @media
32
+ ['@media (min-width: 768px) { .foo { color: red } }', '@media query'],
33
+ ['@media screen and (max-width: 1024px) { .a { } }', '@media screen and'],
34
+
35
+ // Selector combinators
36
+ ['.foo > .bar { color: red }', 'child combinator >'],
37
+ ['.foo + .bar { color: red }', 'adjacent sibling +'],
38
+ ['.foo ~ .bar { color: red }', 'general sibling ~'],
39
+ ['.foo .bar { color: red }', 'descendant combinator'],
40
+
41
+ // Custom properties
42
+ ['body { --main-color: #333; color: var(--main-color) }', 'CSS variables'],
43
+
44
+ // Attribute selectors
45
+ ['[data-theme="dark"] { color: white }', 'attribute selector'],
46
+ ['a[href^="https:"] { color: green }', 'attr with colon'],
47
+
48
+ // content with quotes
49
+ ['.foo::before { content: "Hello World" }', 'content string'],
50
+
51
+ // Multiple selectors with newlines
52
+ ['.a,\n.b,\n.c { color: red }', 'selector list'],
53
+
54
+ // Keyframes
55
+ ['@keyframes fade { 0% { opacity: 0 } 100% { opacity: 1 } }', '@keyframes'],
56
+
57
+ // Nested :not with pseudo-class inside
58
+ ['.foo :not(:last-child) { margin: 0 }', ':not(:last-child)'],
59
+
60
+ // Double :not
61
+ ['.bar :not(.a):not(.b) { color: red }', 'chained :not()'],
62
+
63
+ // :first-child as descendant
64
+ ['.list :first-child { font-weight: bold }', 'space before :first-child'],
65
+
66
+ // content with multiple spaces (potential issue)
67
+ ['.foo::before { content: "a b" }', 'content double space'],
68
+
69
+ // grid-template-areas
70
+ ['.grid { grid-template-areas: "a a" "b c" }', 'grid-template-areas'],
71
+ ];
72
+
73
+ console.log('=== CSS Minifier Edge Case Tests ===\n');
74
+ let issues = 0;
75
+ for (const [input, label] of tests) {
76
+ const result = minifyCSS(input);
77
+ console.log(`${label.padEnd(30)}| ${result}`);
78
+ }
79
+
80
+ // Specific assertions
81
+ console.log('\n=== Assertions ===\n');
82
+ function assert(input, expected, label) {
83
+ const result = minifyCSS(input);
84
+ const pass = result === expected;
85
+ console.log(`${pass ? 'PASS' : 'FAIL'} ${label}`);
86
+ if (!pass) {
87
+ console.log(` Expected: ${expected}`);
88
+ console.log(` Got: ${result}`);
89
+ issues++;
90
+ }
91
+ }
92
+
93
+ assert('.parent :not(pre) > code { color: red }',
94
+ '.parent :not(pre) > code{color:red}',
95
+ 'space before :not() preserved');
96
+
97
+ assert('a:hover { color: red }',
98
+ 'a:hover{color:red}',
99
+ ':hover stays compact');
100
+
101
+ assert('.foo { width: calc(100% - 20px) }',
102
+ '.foo{width:calc(100% - 20px)}',
103
+ 'calc() spaces preserved');
104
+
105
+ assert('@media (min-width: 768px) { .a { color: red } }',
106
+ '@media (min-width:768px){.a{color:red}}',
107
+ '@media colon stripped');
108
+
109
+ assert('.foo > .bar { color: red }',
110
+ '.foo > .bar{color:red}',
111
+ 'child combinator preserved');
112
+
113
+ assert('.foo + .bar { color: red }',
114
+ '.foo + .bar{color:red}',
115
+ 'adjacent sibling preserved');
116
+
117
+ assert('.foo .bar { color: red }',
118
+ '.foo .bar{color:red}',
119
+ 'descendant space preserved');
120
+
121
+ assert('.foo::before { content: "x" }',
122
+ '.foo::before{content:"x"}',
123
+ '::before works');
124
+
125
+ assert('.foo ::after { content: "x" }',
126
+ '.foo ::after{content:"x"}',
127
+ 'space before ::after preserved');
128
+
129
+ assert('li:nth-child(2n + 1) { color: red }',
130
+ 'li:nth-child(2n + 1){color:red}',
131
+ ':nth-child args preserved');
132
+
133
+ assert('.list :first-child { font-weight: bold }',
134
+ '.list :first-child{font-weight:bold}',
135
+ 'space before :first-child preserved');
136
+
137
+ assert('.list :last-child { font-weight: bold }',
138
+ '.list :last-child{font-weight:bold}',
139
+ 'space before :last-child preserved');
140
+
141
+ assert('.bar :not(.a):not(.b) { color: red }',
142
+ '.bar :not(.a):not(.b){color:red}',
143
+ 'chained :not() with leading space');
144
+
145
+ // Edge case: content with double spaces inside quotes
146
+ const dblSpace = minifyCSS('.foo::before { content: "a b" }');
147
+ if (dblSpace.includes('content:"a b"')) {
148
+ console.log('WARN content double space — "a b" collapsed to "a b" (cosmetic, not functional)');
149
+ } else {
150
+ console.log('PASS content double space preserved');
151
+ }
152
+
153
+ console.log(`\n${issues === 0 ? 'All assertions passed!' : issues + ' assertion(s) failed!'}`);
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ const { execSync, spawn } = require('child_process');
3
+ const { mkdtempSync, rmSync } = require('fs');
4
+ const { join } = require('path');
5
+ const os = require('os');
6
+
7
+ const root = join(__dirname, '..');
8
+ const tmp = mkdtempSync(join(os.tmpdir(), 'zq-ssr-'));
9
+
10
+ console.log(`Scaffolding SSR app to ${tmp}\n`);
11
+ execSync(`node cli/index.js create "${tmp}" --ssr`, { cwd: root, stdio: 'inherit' });
12
+ console.log(`\nInstalling local zero-query...\n`);
13
+ execSync(`npm install "${root}"`, { cwd: tmp, stdio: 'inherit' });
14
+
15
+ console.log(`\nStarting SSR server...\n`);
16
+ const child = spawn('node', ['server/index.js'], { cwd: tmp, stdio: 'inherit' });
17
+
18
+ setTimeout(() => {
19
+ const open = process.platform === 'win32' ? 'start'
20
+ : process.platform === 'darwin' ? 'open' : 'xdg-open';
21
+ try { execSync(`${open} http://localhost:3000`, { stdio: 'ignore' }); } catch {}
22
+ }, 500);
23
+
24
+ function cleanup() { try { rmSync(tmp, { recursive: true, force: true }); } catch {} }
25
+ process.on('SIGINT', () => { child.kill(); cleanup(); process.exit(); });
26
+ process.on('SIGTERM', () => { child.kill(); cleanup(); process.exit(); });
27
+ child.on('exit', () => { cleanup(); process.exit(); });
@@ -321,10 +321,10 @@ describe('isEqual', () => {
321
321
  });
322
322
 
323
323
  // ---------------------------------------------------------------------------
324
- // deepMerge circular reference safety
324
+ // deepMerge - circular reference safety
325
325
  // ---------------------------------------------------------------------------
326
326
 
327
- describe('deepMerge circular reference safety', () => {
327
+ describe('deepMerge - circular reference safety', () => {
328
328
  it('does not infinite-loop on circular source', () => {
329
329
  const a = { x: 1 };
330
330
  const b = { y: 2 };
@@ -541,14 +541,14 @@ describe('bus (event bus)', () => {
541
541
  describe('bus (EventBus)', () => {
542
542
  beforeEach(() => { bus.clear(); });
543
543
 
544
- it('on/emit fires handler for matching events', () => {
544
+ it('on/emit - fires handler for matching events', () => {
545
545
  const fn = vi.fn();
546
546
  bus.on('test', fn);
547
547
  bus.emit('test', 42);
548
548
  expect(fn).toHaveBeenCalledWith(42);
549
549
  });
550
550
 
551
- it('off removes handler', () => {
551
+ it('off - removes handler', () => {
552
552
  const fn = vi.fn();
553
553
  bus.on('test', fn);
554
554
  bus.off('test', fn);
@@ -564,7 +564,7 @@ describe('bus (EventBus)', () => {
564
564
  expect(fn).not.toHaveBeenCalled();
565
565
  });
566
566
 
567
- it('once fires handler only once', () => {
567
+ it('once - fires handler only once', () => {
568
568
  const fn = vi.fn();
569
569
  bus.once('test', fn);
570
570
  bus.emit('test', 'a');
@@ -573,7 +573,7 @@ describe('bus (EventBus)', () => {
573
573
  expect(fn).toHaveBeenCalledWith('a');
574
574
  });
575
575
 
576
- it('clear removes all handlers', () => {
576
+ it('clear - removes all handlers', () => {
577
577
  const fn = vi.fn();
578
578
  bus.on('a', fn);
579
579
  bus.on('b', fn);
@@ -590,10 +590,10 @@ describe('bus (EventBus)', () => {
590
590
 
591
591
 
592
592
  // ===========================================================================
593
- // throttle window reset
593
+ // throttle - window reset
594
594
  // ===========================================================================
595
595
 
596
- describe('throttle edge cases', () => {
596
+ describe('throttle - edge cases', () => {
597
597
  it('fires trailing call after wait period', async () => {
598
598
  vi.useFakeTimers();
599
599
  const fn = vi.fn();
@@ -612,10 +612,10 @@ describe('throttle — edge cases', () => {
612
612
 
613
613
 
614
614
  // ===========================================================================
615
- // deepClone edge cases
615
+ // deepClone - edge cases
616
616
  // ===========================================================================
617
617
 
618
- describe('deepClone edge cases', () => {
618
+ describe('deepClone - edge cases', () => {
619
619
  it('clones nested arrays', () => {
620
620
  const arr = [[1, 2], [3, 4]];
621
621
  const clone = deepClone(arr);
@@ -631,10 +631,10 @@ describe('deepClone — edge cases', () => {
631
631
 
632
632
 
633
633
  // ===========================================================================
634
- // deepMerge multiple sources
634
+ // deepMerge - multiple sources
635
635
  // ===========================================================================
636
636
 
637
- describe('deepMerge edge cases', () => {
637
+ describe('deepMerge - edge cases', () => {
638
638
  it('merges from multiple sources', () => {
639
639
  const result = deepMerge({}, { a: 1 }, { b: 2 }, { c: 3 });
640
640
  expect(result).toEqual({ a: 1, b: 2, c: 3 });
@@ -653,10 +653,10 @@ describe('deepMerge — edge cases', () => {
653
653
 
654
654
 
655
655
  // ===========================================================================
656
- // isEqual deeply nested
656
+ // isEqual - deeply nested
657
657
  // ===========================================================================
658
658
 
659
- describe('isEqual edge cases', () => {
659
+ describe('isEqual - edge cases', () => {
660
660
  it('deeply nested equal objects', () => {
661
661
  expect(isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } })).toBe(true);
662
662
  });
@@ -686,10 +686,10 @@ describe('isEqual — edge cases', () => {
686
686
 
687
687
 
688
688
  // ===========================================================================
689
- // camelCase / kebabCase edge cases
689
+ // camelCase / kebabCase - edge cases
690
690
  // ===========================================================================
691
691
 
692
- describe('camelCase / kebabCase edge cases', () => {
692
+ describe('camelCase / kebabCase - edge cases', () => {
693
693
  it('camelCase single word', () => {
694
694
  expect(camelCase('hello')).toBe('hello');
695
695
  });
@@ -709,10 +709,10 @@ describe('camelCase / kebabCase — edge cases', () => {
709
709
 
710
710
 
711
711
  // ===========================================================================
712
- // html tag escaping
712
+ // html tag - escaping
713
713
  // ===========================================================================
714
714
 
715
- describe('html tag edge cases', () => {
715
+ describe('html tag - edge cases', () => {
716
716
  it('handles null interp value', () => {
717
717
  const result = html`<div>${null}</div>`;
718
718
  expect(result).toBe('<div></div>');
@@ -734,10 +734,10 @@ describe('html tag — edge cases', () => {
734
734
 
735
735
 
736
736
  // ===========================================================================
737
- // storage error handling
737
+ // storage - error handling
738
738
  // ===========================================================================
739
739
 
740
- describe('storage parse error fallback', () => {
740
+ describe('storage - parse error fallback', () => {
741
741
  it('returns fallback when JSON.parse fails', () => {
742
742
  localStorage.setItem('bad', '{invalid json');
743
743
  expect(storage.get('bad', 'default')).toBe('default');
@@ -747,7 +747,7 @@ describe('storage — parse error fallback', () => {
747
747
 
748
748
 
749
749
  // ===========================================================================
750
- // NEW UTILITIES Array
750
+ // NEW UTILITIES - Array
751
751
  // ===========================================================================
752
752
 
753
753
  describe('range', () => {
@@ -866,7 +866,7 @@ describe('groupBy', () => {
866
866
 
867
867
 
868
868
  // ===========================================================================
869
- // NEW UTILITIES Object
869
+ // NEW UTILITIES - Object
870
870
  // ===========================================================================
871
871
 
872
872
  describe('pick', () => {
@@ -1012,7 +1012,7 @@ describe('isEmpty', () => {
1012
1012
 
1013
1013
 
1014
1014
  // ===========================================================================
1015
- // NEW UTILITIES String
1015
+ // NEW UTILITIES - String
1016
1016
  // ===========================================================================
1017
1017
 
1018
1018
  describe('capitalize', () => {
@@ -1066,7 +1066,7 @@ describe('truncate', () => {
1066
1066
 
1067
1067
 
1068
1068
  // ===========================================================================
1069
- // NEW UTILITIES Number
1069
+ // NEW UTILITIES - Number
1070
1070
  // ===========================================================================
1071
1071
 
1072
1072
  describe('clamp', () => {
@@ -1099,7 +1099,7 @@ describe('clamp', () => {
1099
1099
 
1100
1100
 
1101
1101
  // ===========================================================================
1102
- // NEW UTILITIES Function
1102
+ // NEW UTILITIES - Function
1103
1103
  // ===========================================================================
1104
1104
 
1105
1105
  describe('memoize', () => {
@@ -1154,7 +1154,7 @@ describe('memoize', () => {
1154
1154
 
1155
1155
 
1156
1156
  // ===========================================================================
1157
- // NEW UTILITIES Async
1157
+ // NEW UTILITIES - Async
1158
1158
  // ===========================================================================
1159
1159
 
1160
1160
  describe('retry', () => {
@@ -1238,3 +1238,140 @@ describe('timeout', () => {
1238
1238
  clearSpy.mockRestore();
1239
1239
  });
1240
1240
  });
1241
+
1242
+
1243
+ // ===========================================================================
1244
+ // memoize - LRU behaviour
1245
+ // ===========================================================================
1246
+
1247
+ describe('memoize - LRU eviction', () => {
1248
+ it('promotes recently-read entries so they survive eviction', () => {
1249
+ const fn = vi.fn(x => x * 2);
1250
+ const mem = memoize(fn, { maxSize: 3 });
1251
+
1252
+ mem(1); // cache: [1]
1253
+ mem(2); // cache: [1, 2]
1254
+ mem(3); // cache: [1, 2, 3]
1255
+
1256
+ // Access 1 to promote it (LRU moves it to newest)
1257
+ mem(1); // cache: [2, 3, 1]
1258
+ expect(fn).toHaveBeenCalledTimes(3); // still cached, no recompute
1259
+
1260
+ // Insert 4 -> should evict 2 (least recently used), NOT 1
1261
+ mem(4); // cache: [3, 1, 4]
1262
+ expect(fn).toHaveBeenCalledTimes(4);
1263
+
1264
+ // 1 should still be cached (was promoted)
1265
+ mem(1);
1266
+ expect(fn).toHaveBeenCalledTimes(4); // no recompute
1267
+
1268
+ // 2 should be evicted
1269
+ mem(2);
1270
+ expect(fn).toHaveBeenCalledTimes(5); // recomputation
1271
+ });
1272
+
1273
+ it('evicts in LRU order, not insertion order', () => {
1274
+ const fn = vi.fn(x => x);
1275
+ const mem = memoize(fn, { maxSize: 2 });
1276
+
1277
+ mem('a'); // [a]
1278
+ mem('b'); // [a, b]
1279
+
1280
+ // Read 'a' - makes 'b' the LRU
1281
+ mem('a'); // [b, a]
1282
+
1283
+ // Insert 'c' - should evict 'b', not 'a'
1284
+ mem('c'); // [a, c]
1285
+
1286
+ // 'a' still cached
1287
+ mem('a');
1288
+ expect(fn).toHaveBeenCalledTimes(3); // a, b, c
1289
+
1290
+ // 'b' was evicted
1291
+ mem('b');
1292
+ expect(fn).toHaveBeenCalledTimes(4);
1293
+ });
1294
+ });
1295
+
1296
+
1297
+ // ===========================================================================
1298
+ // deepClone - enhanced types
1299
+ // ===========================================================================
1300
+
1301
+ describe('deepClone - enhanced types', () => {
1302
+ it('clones Date objects', () => {
1303
+ const date = new Date('2024-01-15T12:00:00Z');
1304
+ const clone = deepClone(date);
1305
+ expect(clone).toEqual(date);
1306
+ expect(clone).not.toBe(date);
1307
+ expect(clone instanceof Date).toBe(true);
1308
+ expect(clone.getTime()).toBe(date.getTime());
1309
+ });
1310
+
1311
+ it('clones nested Dates', () => {
1312
+ const obj = { created: new Date('2024-01-15T12:00:00Z'), meta: { updated: new Date('2024-06-15T12:00:00Z') } };
1313
+ const clone = deepClone(obj);
1314
+ clone.created.setFullYear(2000);
1315
+ expect(obj.created.getFullYear()).toBe(2024);
1316
+ });
1317
+
1318
+ it('clones RegExp', () => {
1319
+ const re = /hello/gi;
1320
+ const clone = deepClone(re);
1321
+ expect(clone).toEqual(re);
1322
+ expect(clone).not.toBe(re);
1323
+ expect(clone.source).toBe('hello');
1324
+ expect(clone.flags).toBe('gi');
1325
+ });
1326
+
1327
+ it('clones Map', () => {
1328
+ const map = new Map([['a', 1], ['b', { deep: true }]]);
1329
+ const clone = deepClone(map);
1330
+ expect(clone).not.toBe(map);
1331
+ expect(clone.get('a')).toBe(1);
1332
+ clone.get('b').deep = false;
1333
+ expect(map.get('b').deep).toBe(true);
1334
+ });
1335
+
1336
+ it('clones Set', () => {
1337
+ const set = new Set([1, 2, { x: 3 }]);
1338
+ const clone = deepClone(set);
1339
+ expect(clone).not.toBe(set);
1340
+ expect(clone.size).toBe(3);
1341
+ expect(clone.has(1)).toBe(true);
1342
+ expect(clone.has(2)).toBe(true);
1343
+ });
1344
+
1345
+ it('handles undefined values in objects', () => {
1346
+ const obj = { a: 1, b: undefined, c: 'hello' };
1347
+ const clone = deepClone(obj);
1348
+ expect(clone.b).toBeUndefined();
1349
+ expect('b' in clone).toBe(true);
1350
+ });
1351
+
1352
+ it('handles null values', () => {
1353
+ const obj = { a: null, b: { c: null } };
1354
+ const clone = deepClone(obj);
1355
+ expect(clone.a).toBeNull();
1356
+ expect(clone.b.c).toBeNull();
1357
+ });
1358
+
1359
+ it('handles circular references', () => {
1360
+ const obj = { a: 1 };
1361
+ obj.self = obj;
1362
+ const clone = deepClone(obj);
1363
+ expect(clone.a).toBe(1);
1364
+ expect(clone.self).toBe(clone);
1365
+ expect(clone.self).not.toBe(obj);
1366
+ });
1367
+
1368
+ it('handles nested circular references', () => {
1369
+ const a = { name: 'a' };
1370
+ const b = { name: 'b', ref: a };
1371
+ a.ref = b;
1372
+ const clone = deepClone(a);
1373
+ expect(clone.name).toBe('a');
1374
+ expect(clone.ref.name).toBe('b');
1375
+ expect(clone.ref.ref).toBe(clone);
1376
+ });
1377
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ZQueryCollection chainable DOM element wrapper.
2
+ * ZQueryCollection - chainable DOM element wrapper.
3
3
  *
4
4
  * Returned by `$()`, `$.all()`, `$.create()`, and many traversal methods.
5
5
  * Similar to a jQuery object: wraps an array of elements with fluent methods.
@@ -50,7 +50,7 @@ export class ZQueryCollection {
50
50
  /** Convert to a plain `Element[]`. */
51
51
  toArray(): Element[];
52
52
 
53
- /** Iterable protocol works with `for...of` and spread. */
53
+ /** Iterable protocol - works with `for...of` and spread. */
54
54
  [Symbol.iterator](): IterableIterator<Element>;
55
55
 
56
56
  // -- Traversal -----------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Component system define, mount, and manage reactive components.
2
+ * Component system - define, mount, and manage reactive components.
3
3
  *
4
4
  * @module component
5
5
  */
@@ -21,7 +21,7 @@ export interface ComponentDefinition {
21
21
  */
22
22
  render?(this: ComponentInstance): string;
23
23
 
24
- /** CSS string scoped to the component root on first render. */
24
+ /** CSS string - scoped to the component root on first render. */
25
25
  styles?: string;
26
26
 
27
27
  /**
@@ -55,7 +55,7 @@ export interface ComponentDefinition {
55
55
  destroyed?(this: ComponentInstance): void;
56
56
 
57
57
  /**
58
- * Computed properties lazy getters derived from state.
58
+ * Computed properties - lazy getters derived from state.
59
59
  * Each function receives the raw state as its argument.
60
60
  * Access via `this.computed.name` in methods and templates.
61
61
  */
@@ -100,7 +100,7 @@ export interface ComponentInstance {
100
100
  templates: Record<string, string>;
101
101
 
102
102
  /**
103
- * Computed properties lazy getters derived from state.
103
+ * Computed properties - lazy getters derived from state.
104
104
  * Defined via `computed` in the component definition.
105
105
  */
106
106
  readonly computed: Record<string, any>;
@@ -116,7 +116,7 @@ export interface ComponentInstance {
116
116
 
117
117
  /**
118
118
  * Manually queue a re-render (microtask-batched).
119
- * Safe to call from anywhere state mutations during render are coalesced.
119
+ * Safe to call from anywhere - state mutations during render are coalesced.
120
120
  */
121
121
  _scheduleUpdate(): void;
122
122
 
package/types/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Structured error handling error codes, error class, and global handler.
2
+ * Structured error handling - error codes, error class, and global handler.
3
3
  *
4
4
  * @module errors
5
5
  */
@@ -84,7 +84,7 @@ export function onError(handler: ZQueryErrorHandler | null): () => void;
84
84
 
85
85
  /**
86
86
  * Report an error through the global handler and console.
87
- * Non-throwing used for recoverable errors in callbacks, lifecycle hooks, etc.
87
+ * Non-throwing - used for recoverable errors in callbacks, lifecycle hooks, etc.
88
88
  */
89
89
  export function reportError(
90
90
  code: ErrorCodeValue,
@@ -125,7 +125,7 @@ export interface FormattedError {
125
125
  export function formatError(err: ZQueryError | Error): FormattedError;
126
126
 
127
127
  /**
128
- * Async version of guardCallback wraps an async function so that
128
+ * Async version of guardCallback - wraps an async function so that
129
129
  * rejections are caught, reported, and don't crash execution.
130
130
  */
131
131
  export function guardAsync<T extends (...args: any[]) => Promise<any>>(
package/types/http.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HTTP Client fetch-based wrapper with interceptors and auto-JSON.
2
+ * HTTP Client - fetch-based wrapper with interceptors and auto-JSON.
3
3
  *
4
4
  * @module http
5
5
  */
@@ -61,7 +61,7 @@ export interface HttpClient {
61
61
  patch<T = any>(url: string, data?: any, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
62
62
  /** DELETE request. */
63
63
  delete<T = any>(url: string, data?: any, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
64
- /** HEAD request no body, useful for checking resource existence or headers. */
64
+ /** HEAD request - no body, useful for checking resource existence or headers. */
65
65
  head<T = any>(url: string, opts?: HttpRequestOptions): Promise<HttpResponse<T>>;
66
66
 
67
67
  /** Update default configuration for all subsequent requests. */
@@ -85,7 +85,7 @@ export interface HttpClient {
85
85
  /** Create a new `AbortController` for manual request cancellation. */
86
86
  createAbort(): AbortController;
87
87
 
88
- /** Direct passthrough to native `fetch()` no JSON handling, no interceptors. */
88
+ /** Direct passthrough to native `fetch()` - no JSON handling, no interceptors. */
89
89
  raw(url: string, opts?: RequestInit): Promise<Response>;
90
90
  }
91
91
 
package/types/misc.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  /**
12
12
  * Morph an existing DOM element's children to match new HTML.
13
- * Only touches nodes that actually differ preserves focus, scroll
13
+ * Only touches nodes that actually differ - preserves focus, scroll
14
14
  * positions, video playback, and other live DOM state.
15
15
  *
16
16
  * Use `z-key="uniqueId"` attributes on list items for keyed reconciliation.
@@ -22,7 +22,7 @@
22
22
  export function morph(rootEl: Element, newHTML: string): void;
23
23
 
24
24
  /**
25
- * Morph a single element in place diffs attributes and children
25
+ * Morph a single element in place - diffs attributes and children
26
26
  * without replacing the node reference. If the tag name matches, the
27
27
  * element is patched in place (preserving identity). If the tag differs,
28
28
  * the element is replaced.
@@ -58,11 +58,11 @@ export function safeEval(expr: string, scope: object[]): any;
58
58
  //
59
59
  // ─── Structural Directives ──────────────────────────────────────────────
60
60
  //
61
- // z-if="expression" Conditional rendering element removed when falsy.
61
+ // z-if="expression" Conditional rendering - element removed when falsy.
62
62
  // z-else-if="expression" Else-if branch (must be immediate sibling of z-if).
63
63
  // z-else Default branch (must follow z-if or z-else-if).
64
64
  //
65
- // z-for="item in items" List rendering repeats the element per item.
65
+ // z-for="item in items" List rendering - repeats the element per item.
66
66
  // {{item.prop}} Use double-brace interpolation for item data.
67
67
  // (item, index) in items Destructured index support.
68
68
  // n in 5 Number range → [1, 2, 3, 4, 5].
@@ -100,9 +100,9 @@ export function safeEval(expr: string, scope: object[]): any;
100
100
  // Supports: input, textarea, select, select[multiple], contenteditable.
101
101
  // Nested keys: z-model="user.name" → this.state.user.name.
102
102
  // Modifiers (boolean attributes on same element):
103
- // z-lazy update on 'change' instead of 'input' (update on blur).
104
- // z-trim auto .trim() whitespace before writing to state.
105
- // z-number force Number() conversion regardless of input type.
103
+ // z-lazy - update on 'change' instead of 'input' (update on blur).
104
+ // z-trim - auto .trim() whitespace before writing to state.
105
+ // z-number - force Number() conversion regardless of input type.
106
106
  //
107
107
  // z-ref="name" Element reference → this.refs.name.
108
108
  //
@@ -134,9 +134,9 @@ export function safeEval(expr: string, scope: object[]): any;
134
134
  //
135
135
  // ─── Slot System ────────────────────────────────────────────────────────
136
136
  //
137
- // <slot> Default slot replaced with child content
137
+ // <slot> Default slot - replaced with child content
138
138
  // passed by the parent component.
139
- // <slot name="header"> Named slot replaced with child content that
139
+ // <slot name="header"> Named slot - replaced with child content that
140
140
  // has a matching slot="header" attribute.
141
141
  // <slot>fallback</slot> Fallback content shown when no slot content provided.
142
142
  //