@invisibleloop/pulse 0.12.3 → 0.14.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 (166) hide show
  1. package/.claude/commands/verify.md +9 -5
  2. package/.claude/pulse-checklist.md +21 -1
  3. package/.github/workflows/publish.yml +1 -0
  4. package/.pulse-verified +1 -0
  5. package/CLAUDE.md +56 -15
  6. package/docs/.claude/commands/verify.md +4 -0
  7. package/docs/.claude/pulse-checklist.md +20 -2
  8. package/docs/public/.pulse-ui-version +1 -1
  9. package/docs/public/andypixel.css +237 -0
  10. package/docs/public/bookworms.css +422 -0
  11. package/docs/public/coutumes.css +584 -0
  12. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  13. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  14. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  15. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  16. package/docs/public/images/journey/178.jpg +0 -0
  17. package/docs/public/images/journey/29.jpg +0 -0
  18. package/docs/public/images/retrowheels/hero.jpg +0 -0
  19. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  20. package/docs/public/images/suzukibento/1015.jpg +0 -0
  21. package/docs/public/images/suzukibento/1025.jpg +0 -0
  22. package/docs/public/images/suzukibento/1043.jpg +0 -0
  23. package/docs/public/images/suzukibento/1059.jpg +0 -0
  24. package/docs/public/images/suzukibento/1062.jpg +0 -0
  25. package/docs/public/images/suzukibento/1084.jpg +0 -0
  26. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  27. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  28. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  29. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  30. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  31. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  32. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  33. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  34. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  35. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  36. package/docs/public/journey.css +528 -0
  37. package/docs/public/pulse-ui.css +1036 -22
  38. package/docs/public/retrowheels.css +402 -0
  39. package/docs/public/shop.css +235 -0
  40. package/docs/public/sparkys.css +349 -0
  41. package/docs/public/suzukibento.css +517 -0
  42. package/docs/public/swish.css +394 -0
  43. package/docs/server.js +19 -4
  44. package/docs/src/lib/nav.js +14 -3
  45. package/docs/src/pages/components/decorate.js +74 -0
  46. package/docs/src/pages/components/gallery.js +102 -0
  47. package/docs/src/pages/components/hero.js +1 -0
  48. package/docs/src/pages/components/marquee.js +76 -0
  49. package/docs/src/pages/components/photo-card.js +74 -0
  50. package/docs/src/pages/components/section.js +40 -1
  51. package/docs/src/pages/meta.js +26 -0
  52. package/docs/src/pages/templates/andypixel.js +260 -0
  53. package/docs/src/pages/templates/bookworms.js +227 -0
  54. package/docs/src/pages/templates/coutumes.js +226 -0
  55. package/docs/src/pages/templates/journey.js +186 -0
  56. package/docs/src/pages/templates/retrowheels.js +342 -0
  57. package/docs/src/pages/templates/shop.js +257 -0
  58. package/docs/src/pages/templates/sparkys.js +249 -0
  59. package/docs/src/pages/templates/suzukibento.js +200 -0
  60. package/docs/src/pages/templates/swish.js +313 -0
  61. package/docs/src/pages/templates.js +313 -2
  62. package/docs/src/pages/testing.js +2 -1
  63. package/examples/andypixel.css +237 -0
  64. package/examples/andypixel.js +258 -0
  65. package/examples/bookworms.css +422 -0
  66. package/examples/bookworms.js +225 -0
  67. package/examples/contact.js +1 -0
  68. package/examples/counter.js +1 -0
  69. package/examples/dev.server.js +17 -0
  70. package/examples/journey.css +528 -0
  71. package/examples/journey.js +186 -0
  72. package/examples/pricing.js +1 -0
  73. package/examples/quiz.js +1 -0
  74. package/examples/retrowheels.css +402 -0
  75. package/examples/retrowheels.js +340 -0
  76. package/examples/shared.js +6 -0
  77. package/examples/suzukibento.css +513 -0
  78. package/examples/suzukibento.js +200 -0
  79. package/examples/swish.css +394 -0
  80. package/examples/swish.js +311 -0
  81. package/examples/todos.js +1 -0
  82. package/package.json +2 -3
  83. package/public/.pulse-ui-version +1 -1
  84. package/public/images/bookworms/hero-books.jpg +0 -0
  85. package/public/images/bookworms/how-we-read-now.jpg +0 -0
  86. package/public/images/bookworms/lantern-keepers.jpg +0 -0
  87. package/public/images/bookworms/provincial-bookseller.jpg +0 -0
  88. package/public/images/journey/178.jpg +0 -0
  89. package/public/images/journey/29.jpg +0 -0
  90. package/public/images/retrowheels/hero.jpg +0 -0
  91. package/public/images/shop/devbook-pro.jpg +0 -0
  92. package/public/images/suzukibento/1015.jpg +0 -0
  93. package/public/images/suzukibento/1025.jpg +0 -0
  94. package/public/images/suzukibento/1043.jpg +0 -0
  95. package/public/images/suzukibento/1059.jpg +0 -0
  96. package/public/images/suzukibento/1062.jpg +0 -0
  97. package/public/images/suzukibento/1084.jpg +0 -0
  98. package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  99. package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  100. package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  101. package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  102. package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  103. package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  104. package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  105. package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  106. package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  107. package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  108. package/public/pulse-ui.css +1036 -22
  109. package/scripts/build.js +72 -18
  110. package/src/agent/checklist.md +20 -2
  111. package/src/agent/commands/verify.md +4 -0
  112. package/src/agent/guide-components.md +145 -7
  113. package/src/agent/guide-design-gallery.md +288 -0
  114. package/src/agent/guide-design-references.md +172 -0
  115. package/src/agent/guide-explore.md +313 -0
  116. package/src/agent/guide-styles.md +197 -11
  117. package/src/agent/guide-templates.md +220 -10
  118. package/src/agent/guide.md +9 -4
  119. package/src/agent/workflow.md +208 -58
  120. package/src/cli/dev.js +39 -7
  121. package/src/cli/fmt.js +113 -0
  122. package/src/cli/index.js +63 -30
  123. package/src/cli/scaffold.js +2 -2
  124. package/src/html.js +6 -1
  125. package/src/mcp/server.js +2415 -101
  126. package/src/mcp/validate-worker.js +34 -5
  127. package/src/runtime/ssr.js +4 -1
  128. package/src/server/index.js +22 -2
  129. package/src/spec/schema.js +5 -0
  130. package/src/testing/html.js +130 -4
  131. package/src/testing/testing.test.js +58 -0
  132. package/src/ui/banner.js +24 -9
  133. package/src/ui/brand-statement.js +58 -0
  134. package/src/ui/button.js +12 -1
  135. package/src/ui/card.js +20 -1
  136. package/src/ui/colophon.js +29 -0
  137. package/src/ui/contact-row.js +41 -0
  138. package/src/ui/cta.js +18 -4
  139. package/src/ui/decorate.js +55 -0
  140. package/src/ui/display-heading.js +44 -0
  141. package/src/ui/drop-cap.js +29 -0
  142. package/src/ui/editorial-hero.js +63 -0
  143. package/src/ui/entry-list.js +36 -0
  144. package/src/ui/feature.js +20 -3
  145. package/src/ui/footer.js +78 -9
  146. package/src/ui/gallery.js +56 -0
  147. package/src/ui/hero.js +68 -18
  148. package/src/ui/image-pair.js +72 -0
  149. package/src/ui/index.js +20 -0
  150. package/src/ui/input.js +1 -0
  151. package/src/ui/marquee.js +48 -0
  152. package/src/ui/nav.js +19 -9
  153. package/src/ui/photo-card.js +44 -0
  154. package/src/ui/product-card.js +65 -0
  155. package/src/ui/pullquote.js +14 -3
  156. package/src/ui/search.js +1 -0
  157. package/src/ui/section-label.js +38 -0
  158. package/src/ui/section.js +1 -1
  159. package/src/ui/service-card.js +47 -0
  160. package/src/ui/stat-grid.js +40 -0
  161. package/src/ui/stat.js +16 -2
  162. package/src/ui/testimonial.js +17 -6
  163. package/src/ui/textarea.js +1 -0
  164. package/src/ui/top-strip.js +47 -0
  165. package/src/ui/trust-list.js +46 -0
  166. package/src/ui/ui.test.js +35 -0
@@ -112,7 +112,7 @@ const result = await render(productSpec, {
112
112
  )}
113
113
 
114
114
  ${section('selectors', 'Supported selectors')}
115
- <p>The selector engine supports the most common patterns. Descendant combinators (<code>div p</code>) are not supported — use <code>element.findAll()</code> to search within a matched element instead.</p>
115
+ <p>The selector engine supports the most common CSS patterns including descendant combinators (<code>div p</code>, <code>.parent li</code>). Descendant selectors match the rightmost token within the ancestor context — if no ancestors match, the result is empty (count 0).</p>
116
116
  ${table(
117
117
  ['Selector', 'Example', 'Matches'],
118
118
  [
@@ -122,6 +122,7 @@ const result = await render(productSpec, {
122
122
  ['Attribute present','<code>[disabled]</code>', 'Elements with a <code>disabled</code> attribute'],
123
123
  ['Attribute value', '<code>[type="submit"]</code>', 'Elements where <code>type</code> equals <code>submit</code>'],
124
124
  ['Compound', '<code>button.primary[disabled]</code>', 'All conditions on the same element'],
125
+ ['Descendant', '<code>tbody tr</code>', '<code>&lt;tr&gt;</code> elements inside <code>&lt;tbody&gt;</code>'],
125
126
  ]
126
127
  )}
127
128
  ${codeBlock(highlight(`result.has('button') // any <button>
@@ -0,0 +1,237 @@
1
+ /*
2
+ * andypixel.css — Andy Pixel blog article example
3
+ *
4
+ * Token definitions (hex values) + layout styles (var() only).
5
+ * Orange accent palette — editorial light theme.
6
+ */
7
+
8
+ /* ─── Token definitions ──────────────────────────────────────── */
9
+
10
+ [data-theme="light"] {
11
+ --accent: #f97316;
12
+ --accent-hover: #ea6c0a;
13
+ --accent-dim: #fff3e8;
14
+ --surface-2: #f0f0f0;
15
+ --border: #e5e5e5;
16
+ --muted-bg: #f5f5f5;
17
+
18
+ /* Code block dark-shell tokens — defined here so app.css stays hex-free */
19
+ --ap-code-shell: #1e1e1e;
20
+ --ap-code-text: #d4d4d4;
21
+ --ap-btn-dark: #1a1a1a;
22
+ --ap-btn-dark-hover: #333333;
23
+ }
24
+
25
+ /* ─── Base ───────────────────────────────────────────────────── */
26
+
27
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
28
+
29
+ body {
30
+ font-family: var(--ui-font, system-ui, sans-serif);
31
+ line-height: 1.6;
32
+ background: var(--ui-bg);
33
+ color: var(--ui-text);
34
+ }
35
+
36
+ /* ─── Contrast fixes ─────────────────────────────────────────── */
37
+
38
+ /* Badge: muted on surface-2 fails WCAG AA at small size */
39
+ .ui-badge--default {
40
+ color: var(--ui-text);
41
+ }
42
+
43
+ /* Inline code: accent on surface-2 fails WCAG AA */
44
+ .ui-prose code {
45
+ color: var(--ui-text);
46
+ }
47
+
48
+ /* ─── Reading column ─────────────────────────────────────────── */
49
+
50
+ .ap-reading-col {
51
+ max-width: 72ch;
52
+ margin-left: auto;
53
+ margin-right: auto;
54
+ padding-left: var(--ui-space-6);
55
+ padding-right: var(--ui-space-6);
56
+ }
57
+
58
+ /* ─── Article header ─────────────────────────────────────────── */
59
+
60
+ .ap-article-header {
61
+ padding: var(--ui-space-12) 0 var(--ui-space-8);
62
+ border-bottom: 3px solid var(--ui-accent);
63
+ margin-bottom: var(--ui-space-10);
64
+ }
65
+
66
+ .ap-article-meta {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: var(--ui-space-2);
70
+ flex-wrap: wrap;
71
+ margin-bottom: var(--ui-space-5);
72
+ }
73
+
74
+ .ap-article-title {
75
+ font-family: var(--ui-font-display, system-ui, serif);
76
+ font-size: clamp(2rem, 5vw, 3.25rem);
77
+ font-weight: 800;
78
+ line-height: 1.1;
79
+ letter-spacing: -0.02em;
80
+ color: var(--ui-text);
81
+ margin-bottom: var(--ui-space-4);
82
+ }
83
+
84
+ .ap-article-subtitle {
85
+ font-size: var(--ui-text-xl);
86
+ color: var(--ui-muted);
87
+ line-height: 1.55;
88
+ margin-bottom: var(--ui-space-6);
89
+ max-width: 56ch;
90
+ }
91
+
92
+ .ap-article-author-strip {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: var(--ui-space-3);
96
+ }
97
+
98
+ /* ─── Article body ───────────────────────────────────────────── */
99
+
100
+ .ap-article-body {
101
+ padding-bottom: var(--ui-space-12);
102
+ }
103
+
104
+ .ap-article-lead {
105
+ font-size: var(--ui-text-xl) !important;
106
+ line-height: 1.65 !important;
107
+ color: var(--ui-text) !important;
108
+ }
109
+
110
+ /* ─── Code block ─────────────────────────────────────────────── */
111
+
112
+ .ap-code-block {
113
+ margin: var(--ui-space-6) 0;
114
+ border-radius: var(--ui-radius);
115
+ overflow: hidden;
116
+ border: 1px solid var(--ui-border);
117
+ }
118
+
119
+ .ap-code-block__header {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: var(--ui-space-2);
123
+ padding: var(--ui-space-2) var(--ui-space-4);
124
+ background: var(--ui-text);
125
+ color: var(--ui-accent);
126
+ font-family: var(--ui-mono, monospace);
127
+ font-size: var(--ui-text-sm);
128
+ }
129
+
130
+ .ap-code-block__pre {
131
+ background: var(--ap-code-shell);
132
+ padding: var(--ui-space-5) var(--ui-space-6);
133
+ overflow-x: auto;
134
+ margin: 0;
135
+ }
136
+
137
+ .ap-code-block__code {
138
+ font-family: var(--ui-mono, monospace);
139
+ font-size: var(--ui-text-sm);
140
+ line-height: 1.75;
141
+ color: var(--ap-code-text);
142
+ }
143
+
144
+ /* ─── Pullquote ──────────────────────────────────────────────── */
145
+
146
+ .ap-pullquote {
147
+ margin: var(--ui-space-8) 0;
148
+ padding: var(--ui-space-5) var(--ui-space-6);
149
+ border-left: 4px solid var(--ui-accent);
150
+ background: var(--accent-dim);
151
+ border-radius: 0 var(--ui-radius) var(--ui-radius) 0;
152
+ }
153
+
154
+ .ap-pullquote p {
155
+ font-family: var(--ui-font-display, system-ui, serif);
156
+ font-size: var(--ui-text-xl);
157
+ font-style: italic;
158
+ color: var(--ui-text);
159
+ margin: 0;
160
+ line-height: 1.55;
161
+ }
162
+
163
+ /* ─── Author bio ─────────────────────────────────────────────── */
164
+
165
+ .ap-author-bio-wrap {
166
+ border-top: 1px solid var(--ui-border);
167
+ border-bottom: 1px solid var(--ui-border);
168
+ padding: var(--ui-space-8) 0;
169
+ margin-bottom: var(--ui-space-10);
170
+ }
171
+
172
+ .ap-author-bio {
173
+ display: flex;
174
+ gap: var(--ui-space-4);
175
+ align-items: flex-start;
176
+ }
177
+
178
+ .ap-author-bio__name {
179
+ font-weight: 700;
180
+ font-size: var(--ui-text-base);
181
+ margin-bottom: var(--ui-space-1);
182
+ }
183
+
184
+ /* ─── Newsletter ─────────────────────────────────────────────── */
185
+
186
+ .ap-newsletter-section {
187
+ max-width: 540px;
188
+ margin: 0 auto;
189
+ text-align: center;
190
+ padding: var(--ui-space-12) var(--ui-space-6);
191
+ border-top: 3px solid var(--ui-accent);
192
+ }
193
+
194
+ .ap-newsletter-eyebrow {
195
+ font-family: var(--ui-mono, monospace);
196
+ font-size: var(--ui-text-sm);
197
+ text-transform: uppercase;
198
+ letter-spacing: 0.1em;
199
+ color: var(--ui-text);
200
+ margin-bottom: var(--ui-space-2);
201
+ }
202
+
203
+ .ap-newsletter-title {
204
+ font-family: var(--ui-font-display, system-ui, serif);
205
+ font-size: clamp(1.5rem, 4vw, 2.25rem);
206
+ font-weight: 800;
207
+ letter-spacing: -0.02em;
208
+ color: var(--ui-text);
209
+ margin-bottom: var(--ui-space-3);
210
+ line-height: 1.15;
211
+ }
212
+
213
+ .ap-newsletter-form {
214
+ display: flex;
215
+ gap: var(--ui-space-2);
216
+ margin-top: var(--ui-space-6);
217
+ flex-wrap: wrap;
218
+ justify-content: center;
219
+ }
220
+
221
+ /* Orange bg on small text fails WCAG AA — use near-black for contrast (15:1) */
222
+ .ap-newsletter-form .ui-btn--primary {
223
+ background: var(--ap-btn-dark);
224
+ }
225
+ .ap-newsletter-form .ui-btn--primary:hover {
226
+ background: var(--ap-btn-dark-hover);
227
+ }
228
+
229
+ .ap-newsletter-error {
230
+ color: var(--ui-error, #dc2626);
231
+ margin-top: var(--ui-space-3);
232
+ }
233
+
234
+ .ap-newsletter-success {
235
+ padding: var(--ui-space-12) var(--ui-space-6);
236
+ text-align: center;
237
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Pulse — Andy Pixel example
3
+ *
4
+ * Demonstrates:
5
+ * - Blog article layout — editorial light-theme, prose-width reading column
6
+ * - Composing nav, badge, avatar, prose, input, button, footer
7
+ * - Custom code block component (dark pre with labelled header)
8
+ * - Blockquote / pullquote styling
9
+ * - Async newsletter subscribe action with success/error state
10
+ * - Author bio strip below article content
11
+ * - iconCode, iconCheck from icons.js
12
+ * - Orange accent palette with WCAG contrast fixes for badge + inline code
13
+ *
14
+ * Run: node examples/dev.server.js → http://localhost:3001/andypixel
15
+ */
16
+
17
+ import {
18
+ nav,
19
+ footer,
20
+ badge,
21
+ button,
22
+ avatar,
23
+ prose,
24
+ input,
25
+ } from '../src/ui/index.js'
26
+
27
+ import { iconCode, iconCheck } from '../src/ui/icons.js'
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Code block helper
31
+ // ---------------------------------------------------------------------------
32
+
33
+ function codeBlock({ filename, code }) {
34
+ return `
35
+ <div class="ap-code-block" role="figure" aria-label="Code example: ${filename}">
36
+ <div class="ap-code-block__header">
37
+ ${iconCode({ size: 14 })}
38
+ <span>${filename}</span>
39
+ </div>
40
+ <pre class="ap-code-block__pre"><code class="ap-code-block__code">${code}</code></pre>
41
+ </div>
42
+ `
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Spec
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export default {
50
+ route: '/andypixel',
51
+
52
+ meta: {
53
+ title: 'JavaScript Proxies: The Hidden Superpower \u2014 Andy Pixel',
54
+ description: 'Discover how JavaScript Proxies let you intercept and redefine fundamental operations on objects.',
55
+ styles: ['/pulse-ui.css', '/examples/andypixel.css'],
56
+ theme: 'light',
57
+ },
58
+
59
+ state: {
60
+ status: 'idle',
61
+ },
62
+
63
+ view: (state) => `
64
+ <main id="main-content">
65
+
66
+ ${nav({
67
+ logo: 'Andy Pixel',
68
+ links: [
69
+ { label: 'Blog', href: '#blog' },
70
+ { label: 'Projects', href: '#projects' },
71
+ { label: 'About', href: '#about' },
72
+ ],
73
+ })}
74
+
75
+ <article>
76
+
77
+ <header class="ap-article-header">
78
+ <div class="ap-reading-col">
79
+
80
+ <div class="ap-article-meta">
81
+ ${badge({ label: 'JavaScript' })}
82
+ ${badge({ label: 'Advanced' })}
83
+ <span class="u-text-muted u-text-sm">\u00b7 May 28, 2026 \u00b7 8 min read</span>
84
+ </div>
85
+
86
+ <h1 class="ap-article-title">
87
+ JavaScript Proxies: The Hidden Superpower You\u2019re Not Using
88
+ </h1>
89
+
90
+ <p class="ap-article-subtitle">
91
+ Intercept, transform, and observe objects at a fundamental level \u2014
92
+ without touching the source.
93
+ </p>
94
+
95
+ <div class="ap-article-author-strip">
96
+ ${avatar({ name: 'Andy Pixel', size: 'sm' })}
97
+ <div>
98
+ <p class="u-text-sm u-font-bold">Andy Pixel</p>
99
+ <p class="u-text-sm u-text-muted">Frontend Engineer</p>
100
+ </div>
101
+ </div>
102
+
103
+ </div>
104
+ </header>
105
+
106
+ <div class="ap-article-body ap-reading-col">
107
+
108
+ ${prose({ content: `
109
+ <p class="ap-article-lead">If you\u2019ve ever wanted to validate object properties automatically,
110
+ track changes reactively, or build a mock API client that feels real \u2014 the
111
+ <code>Proxy</code> object is your tool. Yet somehow it flies under the radar
112
+ for most developers.</p>
113
+
114
+ <p>Introduced in ES2015, <code>Proxy</code> wraps an object and lets you intercept
115
+ fundamental operations \u2014 property reads, writes, deletions, and more \u2014 via
116
+ \u201ctraps\u201d defined on a handler object.</p>
117
+
118
+ <h2>The anatomy of a Proxy</h2>
119
+
120
+ <p>A Proxy takes two arguments: the <strong>target</strong> (the object being wrapped)
121
+ and the <strong>handler</strong> (an object defining which operations to intercept).
122
+ Every interceptable operation has a corresponding trap method on the handler.</p>
123
+ ` })}
124
+
125
+ ${codeBlock({
126
+ filename: 'proxy-basics.js',
127
+ code: `const handler = {
128
+ get(target, key) {
129
+ return key in target
130
+ ? target[key]
131
+ : \`Property "\${key}" doesn\u2019t exist\`
132
+ },
133
+ set(target, key, value) {
134
+ if (typeof value !== 'number') {
135
+ throw new TypeError('Only numbers allowed')
136
+ }
137
+ target[key] = value
138
+ return true
139
+ }
140
+ }
141
+
142
+ const proxy = new Proxy({}, handler)
143
+ proxy.score = 42 // \u2713 works
144
+ proxy.score = 'oops' // \u2717 TypeError`,
145
+ })}
146
+
147
+ ${prose({ content: `
148
+ <p>The <code>get</code> trap runs on every property access. The <code>set</code> trap
149
+ intercepts every assignment. Return <code>true</code> from <code>set</code> to signal
150
+ success \u2014 otherwise you\u2019ll get a <code>TypeError</code> in strict mode.</p>
151
+
152
+ <h2>Real-world use cases</h2>
153
+
154
+ <p>The real power isn\u2019t abstract. Proxy unlocks patterns that are otherwise
155
+ painful or impossible.</p>
156
+ ` })}
157
+
158
+ <blockquote class="ap-pullquote">
159
+ <p>\u201cA Proxy doesn\u2019t change what your object <em>is</em> \u2014 it changes what your
160
+ object <em>does</em> when the runtime touches it.\u201d</p>
161
+ </blockquote>
162
+
163
+ ${prose({ content: `
164
+ <h3>Reactive state</h3>
165
+ <p>Vue 3\u2019s reactivity system is built entirely on Proxy. When you access a reactive
166
+ property, Vue\u2019s <code>get</code> trap tracks the dependency. When you mutate it,
167
+ the <code>set</code> trap schedules re-renders. No syntax overhead. Just assignment.</p>
168
+
169
+ <h3>Input validation</h3>
170
+ <p>Instead of scattering guard clauses through your codebase, centralise validation
171
+ in a Proxy handler. The contract lives in one place. Every consumer benefits
172
+ automatically.</p>
173
+
174
+ <h3>API mocking</h3>
175
+ <p>Wrap a plain object in a Proxy that intercepts property access and returns async
176
+ functions. You get a fully-typed mock client that mirrors your real API surface \u2014
177
+ without any code generation.</p>
178
+
179
+ <h2>Performance considerations</h2>
180
+
181
+ <p>Proxies are not free. Every trap fires a function call. In tight loops over large
182
+ collections, this adds up. Benchmark before you commit. For most use cases \u2014 form
183
+ state, reactive stores, config validation \u2014 the overhead is completely negligible.</p>
184
+
185
+ <p>The V8 team has steadily optimised Proxy performance since ES2015. Don\u2019t avoid
186
+ Proxy out of dated fear \u2014 measure it first.</p>
187
+ ` })}
188
+
189
+ </div>
190
+
191
+ </article>
192
+
193
+ <div class="ap-author-bio-wrap">
194
+ <div class="ap-reading-col">
195
+ <div class="ap-author-bio">
196
+ ${avatar({ name: 'Andy Pixel', size: 'lg' })}
197
+ <div class="ap-author-bio__text">
198
+ <p class="ap-author-bio__name">Andy Pixel</p>
199
+ <p class="u-text-muted u-text-sm">
200
+ Frontend engineer obsessed with the web platform. Writing about JavaScript,
201
+ CSS, and creative code. Building things at the edge of what browsers can do.
202
+ </p>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
+ ${ state.status === 'success'
209
+ ? `<div class="ap-newsletter-success">
210
+ <p class="u-font-bold u-text-lg">${iconCheck({ size: 20, class: 'u-inline' })} You\u2019re in!</p>
211
+ <p class="u-text-muted u-mt-2">Next article lands in your inbox.</p>
212
+ </div>`
213
+ : `<div class="ap-newsletter-section">
214
+ <p class="ap-newsletter-eyebrow">Stay in the loop</p>
215
+ <h2 class="ap-newsletter-title">More like this, in your inbox.</h2>
216
+ <p class="u-text-muted">
217
+ Deep-dives on JavaScript, the web platform, and frontend engineering \u2014
218
+ whenever I publish. No noise.
219
+ </p>
220
+ ${ state.status === 'error'
221
+ ? `<p class="ap-newsletter-error u-mt-3">Something went wrong. Please try again.</p>`
222
+ : '' }
223
+ <form class="ap-newsletter-form" data-action="subscribe">
224
+ ${input({ type: 'email', name: 'email', placeholder: 'you@example.com', label: 'Email address', attrs: { autocomplete: 'email' } })}
225
+ ${button({ label: 'Subscribe', variant: 'primary', attrs: { type: 'submit' } })}
226
+ </form>
227
+ </div>`
228
+ }
229
+
230
+ ${footer({
231
+ logo: 'Andy Pixel',
232
+ links: [
233
+ { label: 'Blog', href: '#blog' },
234
+ { label: 'Projects', href: '#projects' },
235
+ { label: 'About', href: '#about' },
236
+ { label: 'RSS', href: '#feed' },
237
+ ],
238
+ })}
239
+
240
+ </main>
241
+ `,
242
+
243
+ actions: {
244
+ subscribe: {
245
+ onStart: (state, formData) => ({
246
+ status: 'loading',
247
+ }),
248
+ run: async (state, serverState, formData) => {
249
+ // Newsletter subscription would call an API here
250
+ },
251
+ onSuccess: (state) => ({ status: 'success' }),
252
+ onError: (state, err) => ({
253
+ status: 'error',
254
+ errors: err?.validation ?? [{ message: err.message }],
255
+ }),
256
+ },
257
+ },
258
+ }