@invisibleloop/pulse 0.12.2 → 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.
- package/.claude/commands/verify.md +9 -5
- package/.claude/pulse-checklist.md +21 -1
- package/.github/workflows/publish.yml +1 -0
- package/.pulse-verified +1 -0
- package/CLAUDE.md +56 -15
- package/docs/.claude/commands/verify.md +4 -0
- package/docs/.claude/pulse-checklist.md +36 -2
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/andypixel.css +237 -0
- package/docs/public/bookworms.css +422 -0
- package/docs/public/coutumes.css +584 -0
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/journey.css +528 -0
- package/docs/public/pulse-ui.css +1051 -25
- package/docs/public/pulse-ui.js +6 -0
- package/docs/public/retrowheels.css +402 -0
- package/docs/public/shop.css +235 -0
- package/docs/public/sparkys.css +349 -0
- package/docs/public/suzukibento.css +517 -0
- package/docs/public/swish.css +394 -0
- package/docs/server.js +19 -4
- package/docs/src/lib/nav.js +14 -3
- package/docs/src/pages/components/decorate.js +74 -0
- package/docs/src/pages/components/gallery.js +102 -0
- package/docs/src/pages/components/hero.js +1 -0
- package/docs/src/pages/components/marquee.js +76 -0
- package/docs/src/pages/components/phone-frame.js +26 -7
- package/docs/src/pages/components/photo-card.js +74 -0
- package/docs/src/pages/components/section.js +40 -1
- package/docs/src/pages/meta.js +26 -0
- package/docs/src/pages/templates/andypixel.js +260 -0
- package/docs/src/pages/templates/bookworms.js +227 -0
- package/docs/src/pages/templates/coutumes.js +226 -0
- package/docs/src/pages/templates/journey.js +186 -0
- package/docs/src/pages/templates/retrowheels.js +342 -0
- package/docs/src/pages/templates/shop.js +257 -0
- package/docs/src/pages/templates/sparkys.js +249 -0
- package/docs/src/pages/templates/suzukibento.js +200 -0
- package/docs/src/pages/templates/swish.js +313 -0
- package/docs/src/pages/templates.js +313 -2
- package/docs/src/pages/testing.js +2 -1
- package/examples/andypixel.css +237 -0
- package/examples/andypixel.js +258 -0
- package/examples/bookworms.css +422 -0
- package/examples/bookworms.js +225 -0
- package/examples/contact.js +1 -0
- package/examples/counter.js +1 -0
- package/examples/dev.server.js +17 -0
- package/examples/journey.css +528 -0
- package/examples/journey.js +186 -0
- package/examples/pricing.js +1 -0
- package/examples/quiz.js +1 -0
- package/examples/retrowheels.css +402 -0
- package/examples/retrowheels.js +340 -0
- package/examples/shared.js +6 -0
- package/examples/suzukibento.css +513 -0
- package/examples/suzukibento.js +200 -0
- package/examples/swish.css +394 -0
- package/examples/swish.js +311 -0
- package/examples/todos.js +1 -0
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/public/images/bookworms/hero-books.jpg +0 -0
- package/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/public/images/journey/178.jpg +0 -0
- package/public/images/journey/29.jpg +0 -0
- package/public/images/retrowheels/hero.jpg +0 -0
- package/public/images/shop/devbook-pro.jpg +0 -0
- package/public/images/suzukibento/1015.jpg +0 -0
- package/public/images/suzukibento/1025.jpg +0 -0
- package/public/images/suzukibento/1043.jpg +0 -0
- package/public/images/suzukibento/1059.jpg +0 -0
- package/public/images/suzukibento/1062.jpg +0 -0
- package/public/images/suzukibento/1084.jpg +0 -0
- package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/public/pulse-ui.css +1051 -22
- package/public/pulse-ui.js +6 -0
- package/scripts/build.js +72 -18
- package/src/agent/checklist.md +20 -2
- package/src/agent/commands/verify.md +4 -0
- package/src/agent/guide-components.md +146 -8
- package/src/agent/guide-design-gallery.md +288 -0
- package/src/agent/guide-design-references.md +172 -0
- package/src/agent/guide-explore.md +313 -0
- package/src/agent/guide-styles.md +197 -11
- package/src/agent/guide-templates.md +220 -10
- package/src/agent/guide.md +9 -4
- package/src/agent/workflow.md +208 -58
- package/src/cli/dev.js +39 -7
- package/src/cli/fmt.js +113 -0
- package/src/cli/index.js +63 -30
- package/src/cli/scaffold.js +2 -2
- package/src/html.js +6 -1
- package/src/mcp/server.js +2415 -101
- package/src/mcp/validate-worker.js +34 -5
- package/src/runtime/ssr.js +4 -1
- package/src/server/index.js +22 -2
- package/src/spec/schema.js +5 -0
- package/src/testing/html.js +130 -4
- package/src/testing/testing.test.js +58 -0
- package/src/ui/banner.js +24 -9
- package/src/ui/brand-statement.js +58 -0
- package/src/ui/button.js +12 -1
- package/src/ui/card.js +20 -1
- package/src/ui/colophon.js +29 -0
- package/src/ui/contact-row.js +41 -0
- package/src/ui/cta.js +18 -4
- package/src/ui/decorate.js +55 -0
- package/src/ui/display-heading.js +44 -0
- package/src/ui/drop-cap.js +29 -0
- package/src/ui/editorial-hero.js +63 -0
- package/src/ui/entry-list.js +36 -0
- package/src/ui/feature.js +20 -3
- package/src/ui/footer.js +78 -9
- package/src/ui/gallery.js +56 -0
- package/src/ui/hero.js +68 -18
- package/src/ui/image-pair.js +72 -0
- package/src/ui/index.js +20 -0
- package/src/ui/input.js +1 -0
- package/src/ui/marquee.js +48 -0
- package/src/ui/nav.js +19 -9
- package/src/ui/phone-frame.js +6 -5
- package/src/ui/photo-card.js +44 -0
- package/src/ui/product-card.js +65 -0
- package/src/ui/pullquote.js +14 -3
- package/src/ui/search.js +1 -0
- package/src/ui/section-label.js +38 -0
- package/src/ui/section.js +1 -1
- package/src/ui/service-card.js +47 -0
- package/src/ui/stat-grid.js +40 -0
- package/src/ui/stat.js +16 -2
- package/src/ui/testimonial.js +17 -6
- package/src/ui/textarea.js +1 -0
- package/src/ui/top-strip.js +47 -0
- package/src/ui/trust-list.js +46 -0
- 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
|
|
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><tr></code> elements inside <code><tbody></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
|
+
}
|