@invisibleloop/pulse 0.12.3 → 0.14.2

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 (181) hide show
  1. package/.claude/commands/build-page.md +5 -1
  2. package/.claude/commands/verify.md +31 -17
  3. package/.claude/pulse-checklist.md +21 -1
  4. package/.github/workflows/publish.yml +1 -0
  5. package/.pulse-verified +1 -0
  6. package/CLAUDE.md +56 -15
  7. package/docs/.claude/commands/verify.md +4 -0
  8. package/docs/.claude/pulse-checklist.md +20 -2
  9. package/docs/public/.pulse-ui-version +1 -1
  10. package/docs/public/andypixel.css +237 -0
  11. package/docs/public/bookworms.css +422 -0
  12. package/docs/public/coutumes.css +584 -0
  13. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  14. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  15. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  16. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  17. package/docs/public/images/journey/178.jpg +0 -0
  18. package/docs/public/images/journey/29.jpg +0 -0
  19. package/docs/public/images/retrowheels/hero.jpg +0 -0
  20. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  21. package/docs/public/images/suzukibento/1015.jpg +0 -0
  22. package/docs/public/images/suzukibento/1025.jpg +0 -0
  23. package/docs/public/images/suzukibento/1043.jpg +0 -0
  24. package/docs/public/images/suzukibento/1059.jpg +0 -0
  25. package/docs/public/images/suzukibento/1062.jpg +0 -0
  26. package/docs/public/images/suzukibento/1084.jpg +0 -0
  27. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  28. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  29. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  30. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  31. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  32. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  33. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  34. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  35. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  36. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  37. package/docs/public/journey.css +528 -0
  38. package/docs/public/pulse-ui.css +1036 -22
  39. package/docs/public/retrowheels.css +402 -0
  40. package/docs/public/shop.css +235 -0
  41. package/docs/public/sparkys.css +349 -0
  42. package/docs/public/suzukibento.css +517 -0
  43. package/docs/public/swish.css +394 -0
  44. package/docs/server.js +19 -4
  45. package/docs/src/lib/nav.js +14 -3
  46. package/docs/src/pages/components/decorate.js +74 -0
  47. package/docs/src/pages/components/gallery.js +102 -0
  48. package/docs/src/pages/components/hero.js +1 -0
  49. package/docs/src/pages/components/marquee.js +76 -0
  50. package/docs/src/pages/components/photo-card.js +74 -0
  51. package/docs/src/pages/components/section.js +40 -1
  52. package/docs/src/pages/meta.js +26 -0
  53. package/docs/src/pages/templates/andypixel.js +260 -0
  54. package/docs/src/pages/templates/bookworms.js +227 -0
  55. package/docs/src/pages/templates/coutumes.js +226 -0
  56. package/docs/src/pages/templates/journey.js +186 -0
  57. package/docs/src/pages/templates/retrowheels.js +342 -0
  58. package/docs/src/pages/templates/shop.js +257 -0
  59. package/docs/src/pages/templates/sparkys.js +249 -0
  60. package/docs/src/pages/templates/suzukibento.js +200 -0
  61. package/docs/src/pages/templates/swish.js +313 -0
  62. package/docs/src/pages/templates.js +313 -2
  63. package/docs/src/pages/testing.js +2 -1
  64. package/examples/.claude/commands/pulse-build.md +7 -0
  65. package/examples/.claude/commands/pulse-dev.md +7 -0
  66. package/examples/.claude/commands/pulse-start.md +9 -0
  67. package/examples/.claude/commands/pulse-stop.md +5 -0
  68. package/examples/.claude/commands/verify.md +84 -0
  69. package/examples/.claude/coverage-check.js +127 -0
  70. package/examples/.claude/pulse-checklist.md +177 -0
  71. package/examples/andypixel.css +237 -0
  72. package/examples/andypixel.js +258 -0
  73. package/examples/bookworms.css +422 -0
  74. package/examples/bookworms.js +225 -0
  75. package/examples/contact.js +1 -0
  76. package/examples/counter.js +1 -0
  77. package/examples/dev.server.js +17 -0
  78. package/examples/journey.css +528 -0
  79. package/examples/journey.js +186 -0
  80. package/examples/pricing.js +1 -0
  81. package/examples/public/.pulse-ui-version +1 -0
  82. package/examples/public/pulse-ui.css +4031 -0
  83. package/examples/public/pulse-ui.js +257 -0
  84. package/examples/quiz.js +1 -0
  85. package/examples/retrowheels.css +402 -0
  86. package/examples/retrowheels.js +340 -0
  87. package/examples/shared.js +6 -0
  88. package/examples/suzukibento.css +513 -0
  89. package/examples/suzukibento.js +200 -0
  90. package/examples/swish.css +394 -0
  91. package/examples/swish.js +311 -0
  92. package/examples/todos.js +1 -0
  93. package/package.json +2 -3
  94. package/public/.pulse-ui-version +1 -1
  95. package/public/images/bookworms/hero-books.jpg +0 -0
  96. package/public/images/bookworms/how-we-read-now.jpg +0 -0
  97. package/public/images/bookworms/lantern-keepers.jpg +0 -0
  98. package/public/images/bookworms/provincial-bookseller.jpg +0 -0
  99. package/public/images/journey/178.jpg +0 -0
  100. package/public/images/journey/29.jpg +0 -0
  101. package/public/images/retrowheels/hero.jpg +0 -0
  102. package/public/images/shop/devbook-pro.jpg +0 -0
  103. package/public/images/suzukibento/1015.jpg +0 -0
  104. package/public/images/suzukibento/1025.jpg +0 -0
  105. package/public/images/suzukibento/1043.jpg +0 -0
  106. package/public/images/suzukibento/1059.jpg +0 -0
  107. package/public/images/suzukibento/1062.jpg +0 -0
  108. package/public/images/suzukibento/1084.jpg +0 -0
  109. package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  110. package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  111. package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  112. package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  113. package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  114. package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  115. package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  116. package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  117. package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  118. package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  119. package/public/pulse-ui.css +1080 -22
  120. package/scripts/build.js +72 -18
  121. package/src/agent/checklist.md +22 -2
  122. package/src/agent/commands/verify.md +4 -0
  123. package/src/agent/guide-components.md +146 -8
  124. package/src/agent/guide-design-gallery.md +288 -0
  125. package/src/agent/guide-design-references.md +172 -0
  126. package/src/agent/guide-explore.md +313 -0
  127. package/src/agent/guide-styles.md +212 -11
  128. package/src/agent/guide-templates.md +223 -10
  129. package/src/agent/guide.md +9 -4
  130. package/src/agent/identity.md +2 -1
  131. package/src/agent/skills/verify/SKILL.md +64 -28
  132. package/src/agent/workflow.md +247 -61
  133. package/src/cli/dev.js +39 -14
  134. package/src/cli/fmt.js +113 -0
  135. package/src/cli/index.js +60 -33
  136. package/src/cli/logger.js +145 -0
  137. package/src/cli/scaffold.js +57 -3
  138. package/src/html.js +6 -1
  139. package/src/mcp/server.js +2561 -99
  140. package/src/mcp/validate-worker.js +49 -5
  141. package/src/runtime/ssr.js +4 -1
  142. package/src/server/index.js +29 -5
  143. package/src/spec/schema.js +21 -0
  144. package/src/spec/schema.test.js +28 -0
  145. package/src/testing/html.js +130 -4
  146. package/src/testing/testing.test.js +58 -0
  147. package/src/ui/banner.js +24 -9
  148. package/src/ui/brand-statement.js +58 -0
  149. package/src/ui/button.js +13 -2
  150. package/src/ui/card.js +20 -1
  151. package/src/ui/colophon.js +29 -0
  152. package/src/ui/contact-row.js +41 -0
  153. package/src/ui/cta.js +18 -4
  154. package/src/ui/decorate.js +55 -0
  155. package/src/ui/display-heading.js +44 -0
  156. package/src/ui/drop-cap.js +29 -0
  157. package/src/ui/editorial-hero.js +63 -0
  158. package/src/ui/entry-list.js +36 -0
  159. package/src/ui/feature.js +20 -3
  160. package/src/ui/footer.js +78 -9
  161. package/src/ui/gallery.js +56 -0
  162. package/src/ui/hero.js +68 -18
  163. package/src/ui/image-pair.js +72 -0
  164. package/src/ui/index.js +20 -0
  165. package/src/ui/input.js +1 -0
  166. package/src/ui/marquee.js +48 -0
  167. package/src/ui/nav.js +19 -9
  168. package/src/ui/photo-card.js +44 -0
  169. package/src/ui/product-card.js +65 -0
  170. package/src/ui/pullquote.js +14 -3
  171. package/src/ui/search.js +1 -0
  172. package/src/ui/section-label.js +38 -0
  173. package/src/ui/section.js +1 -1
  174. package/src/ui/service-card.js +47 -0
  175. package/src/ui/stat-grid.js +40 -0
  176. package/src/ui/stat.js +16 -2
  177. package/src/ui/testimonial.js +17 -6
  178. package/src/ui/textarea.js +1 -0
  179. package/src/ui/top-strip.js +47 -0
  180. package/src/ui/trust-list.js +46 -0
  181. package/src/ui/ui.test.js +35 -0
@@ -0,0 +1,76 @@
1
+ import { renderComponentPage, demo } from '../../lib/component-page.js'
2
+ import { prevNext } from '../../lib/nav.js'
3
+ import { table } from '../../lib/layout.js'
4
+ import { marquee } from '../../../../src/ui/index.js'
5
+
6
+ const { prev, next } = prevNext('/components/marquee')
7
+
8
+ const LOGOS = [
9
+ `<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">ACME&nbsp;CORP</span>`,
10
+ `<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">NOVA&nbsp;INC</span>`,
11
+ `<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">STELLAR</span>`,
12
+ `<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">ORBIT&nbsp;CO</span>`,
13
+ `<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">HELIX</span>`,
14
+ `<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">PRISM&nbsp;AI</span>`,
15
+ ]
16
+
17
+ export default {
18
+ route: '/components/marquee',
19
+ meta: {
20
+ title: 'Marquee — Pulse Docs',
21
+ description: 'CSS-only infinite scrolling strip for logo clouds, trust badges, and tag lists in Pulse UI.',
22
+ styles: ['/pulse-ui.css', '/docs.css'],
23
+ },
24
+ view: () => renderComponentPage({
25
+ currentHref: '/components/marquee',
26
+ prev,
27
+ next,
28
+ name: 'marquee',
29
+ description: 'CSS-only infinite scrolling strip. No JavaScript — pure CSS animation. Use for logo clouds, press mentions, trust badges, or feature tags. <strong>The strip is hidden from screen readers</strong> (<code>aria-hidden="true"</code>) — it is decorative, not content.',
30
+ content: `
31
+ <h2 class="doc-h2" id="logos">Logo cloud</h2>
32
+ <p>A common use-case: a strip of client or partner logos scrolling slowly from right to left.</p>
33
+ ${demo(
34
+ marquee({ items: LOGOS }),
35
+ `marquee({
36
+ items: ['<img src="/logos/acme.svg" alt="">',
37
+ '<img src="/logos/nova.svg" alt="">',
38
+ '<img src="/logos/helix.svg" alt="">'],
39
+ })`
40
+ )}
41
+
42
+ <h2 class="doc-h2" id="slow">Custom speed</h2>
43
+ <p>The default <code>speed</code> is 30 seconds per loop. Slow it down for fewer items, or speed it up for a more energetic feel.</p>
44
+ ${demo(
45
+ marquee({ items: LOGOS, speed: 60 }),
46
+ `marquee({ items, speed: 60 }) // slow`
47
+ )}
48
+
49
+ <h2 class="doc-h2" id="direction">Direction</h2>
50
+ <p>Set <code>direction: 'right'</code> to scroll right-to-left instead.</p>
51
+ ${demo(
52
+ marquee({ items: LOGOS, direction: 'right' }),
53
+ `marquee({ items, direction: 'right' })`
54
+ )}
55
+
56
+ <h2 class="doc-h2" id="no-fade">Without fade</h2>
57
+ <p>Edge fading is on by default. Disable it with <code>fade: false</code>.</p>
58
+ ${demo(
59
+ marquee({ items: LOGOS, fade: false }),
60
+ `marquee({ items, fade: false })`
61
+ )}
62
+
63
+ ${table(
64
+ ['Prop', 'Type', 'Default', 'Description'],
65
+ [
66
+ ['<code>items</code>', 'string[]', '[]', 'Array of raw HTML strings — images, badges, or text'],
67
+ ['<code>speed</code>', 'number', '30', 'Duration (seconds) for one full loop. Lower = faster.'],
68
+ ['<code>gap</code>', 'string', "'lg'", "'sm' · 'md' · 'lg'"],
69
+ ['<code>direction</code>', 'string', "'left'", "'left' (right-to-left scroll) or 'right'"],
70
+ ['<code>pause</code>', 'boolean', 'true', 'Pause animation on hover'],
71
+ ['<code>fade</code>', 'boolean', 'true', 'Apply gradient mask to fade the left and right edges'],
72
+ ]
73
+ )}
74
+ `,
75
+ }),
76
+ }
@@ -0,0 +1,74 @@
1
+ import { renderComponentPage, demo } from '../../lib/component-page.js'
2
+ import { prevNext } from '../../lib/nav.js'
3
+ import { table } from '../../lib/layout.js'
4
+ import { photoCard } from '../../../../src/ui/index.js'
5
+
6
+ const { prev, next } = prevNext('/components/photo-card')
7
+
8
+ export default {
9
+ route: '/components/photo-card',
10
+ meta: {
11
+ title: 'Photo Card — Pulse Docs',
12
+ description: 'Polaroid-style photo card with optional tilt and caption for Pulse UI.',
13
+ styles: ['/pulse-ui.css', '/docs.css'],
14
+ },
15
+ view: () => renderComponentPage({
16
+ currentHref: '/components/photo-card',
17
+ prev,
18
+ next,
19
+ name: 'photoCard',
20
+ description: 'Polaroid-style photo card with a border, white padding, optional caption, and CSS tilt. Ideal for "meet the team" sections, testimonial portraits, and lifestyle photography.',
21
+ content: `
22
+ <h2 class="doc-h2" id="basic">Basic photo card</h2>
23
+ <p>A framed image with a white polaroid border.</p>
24
+ ${demo(
25
+ `<div style="display:flex;gap:2rem;align-items:center;justify-content:center;flex-wrap:wrap;padding:2rem">` +
26
+ photoCard({ src: '/img/placeholder-square.svg', alt: 'Team member portrait', caption: 'Sarah, Designer' }) +
27
+ `</div>`,
28
+ `photoCard({
29
+ src: '/img/team/sarah.jpg',
30
+ alt: 'Sarah, Designer',
31
+ caption: 'Sarah, Designer',
32
+ })`
33
+ )}
34
+
35
+ <h2 class="doc-h2" id="tilt">With tilt</h2>
36
+ <p>Pass a <code>tilt</code> value (degrees) to rotate the card. Negative values lean left, positive values lean right. Useful for giving sections a hand-placed, organic feeling.</p>
37
+ ${demo(
38
+ `<div style="display:flex;gap:2rem;align-items:center;justify-content:center;flex-wrap:wrap;padding:2rem">` +
39
+ photoCard({ src: '/img/placeholder-square.svg', alt: 'Team photo', caption: 'Andy, founder', tilt: -2 }) +
40
+ photoCard({ src: '/img/placeholder-tall.svg', alt: 'Office dog', caption: 'Biscuit, mascot', tilt: 1.5 }) +
41
+ photoCard({ src: '/img/placeholder-square.svg', alt: 'Team photo', caption: 'Jen, engineering', tilt: -1 }) +
42
+ `</div>`,
43
+ `photoCard({ src, alt: 'Andy, founder', caption: 'Andy, founder', tilt: -2 })
44
+ photoCard({ src, alt: 'Office dog', caption: 'Biscuit, mascot', tilt: 1.5 })
45
+ photoCard({ src, alt: 'Jen, engineering', caption: 'Jen, engineering', tilt: -1 })`
46
+ )}
47
+
48
+ <h2 class="doc-h2" id="ratios">Aspect ratios</h2>
49
+ <p>Set <code>ratio</code> to control the crop. <code>'1/1'</code> for square portraits, <code>'4/3'</code> for landscape shots.</p>
50
+ ${demo(
51
+ `<div style="display:flex;gap:2rem;align-items:flex-start;justify-content:center;flex-wrap:wrap;padding:2rem">` +
52
+ photoCard({ src: '/img/placeholder-square.svg', alt: 'Square crop', caption: "ratio: '1/1'", ratio: '1/1', tilt: 1 }) +
53
+ photoCard({ src: '/img/placeholder-landscape.svg', alt: 'Landscape crop', caption: "ratio: '4/3'", ratio: '4/3', tilt: -1.5 }) +
54
+ photoCard({ src: '/img/placeholder-tall.svg', alt: 'Tall crop', caption: "ratio: '3/4'", ratio: '3/4', tilt: 2 }) +
55
+ `</div>`,
56
+ `photoCard({ src, alt: 'Square portrait', ratio: '1/1', tilt: 1 })
57
+ photoCard({ src, alt: 'Landscape photo', ratio: '4/3', tilt: -1.5 })
58
+ photoCard({ src, alt: 'Tall portrait', ratio: '3/4', tilt: 2 })`
59
+ )}
60
+
61
+ ${table(
62
+ ['Prop', 'Type', 'Default', 'Description'],
63
+ [
64
+ ['<code>src</code>', 'string', '—', 'Image source URL'],
65
+ ['<code>alt</code>', 'string', '—', 'Alt text — required for accessibility'],
66
+ ['<code>caption</code>', 'string', '—', 'Caption text below the photo'],
67
+ ['<code>tilt</code>', 'number', '0', 'CSS rotate degrees — negative leans left, positive leans right. 0 = no tilt'],
68
+ ['<code>ratio</code>', 'string', "'4/3'", 'CSS aspect-ratio for the image crop'],
69
+ ['<code>rounded</code>', 'boolean', 'false', 'Rounds the image corners inside the card frame'],
70
+ ]
71
+ )}
72
+ `,
73
+ }),
74
+ }
@@ -39,11 +39,50 @@ export default {
39
39
  })`
40
40
  )}
41
41
 
42
+ <h2 class="doc-h2" id="diagonal">Diagonal variant</h2>
43
+ <p>The <code>diagonal</code> variant clips the bottom edge to a slanted angle, creating a layered stacking effect when multiple sections follow each other. The section after it should start with no top gap.</p>
44
+ ${demo(
45
+ uiSection({
46
+ variant: 'diagonal',
47
+ eyebrow: 'Diagonal',
48
+ title: 'Slanted bottom edge.',
49
+ align: 'center',
50
+ content: container({ size: 'md', content: '<p style="text-align:center;color:var(--ui-muted)">Content here</p>' }),
51
+ }),
52
+ `section({ variant: 'diagonal', title: 'Slanted edge', align: 'center', content })`
53
+ )}
54
+
55
+ <h2 class="doc-h2" id="paper">Paper variant</h2>
56
+ <p>The <code>paper</code> variant applies a subtle off-white background and slight inset box shadow — useful for content sections that need to feel warmer and tactile.</p>
57
+ ${demo(
58
+ uiSection({
59
+ variant: 'paper',
60
+ eyebrow: 'Paper',
61
+ title: 'Warm and tactile.',
62
+ align: 'center',
63
+ content: container({ size: 'md', content: '<p style="text-align:center;color:var(--ui-muted)">Content here</p>' }),
64
+ }),
65
+ `section({ variant: 'paper', title: 'Warm and tactile', align: 'center', content })`
66
+ )}
67
+
68
+ <h2 class="doc-h2" id="spotlight">Spotlight variant</h2>
69
+ <p>The <code>spotlight</code> variant adds a radial gradient light source at the top centre — gives the section a dramatic, focused feeling.</p>
70
+ ${demo(
71
+ uiSection({
72
+ variant: 'spotlight',
73
+ eyebrow: 'Spotlight',
74
+ title: 'Dramatically focused.',
75
+ align: 'center',
76
+ content: container({ size: 'md', content: '<p style="text-align:center;color:var(--ui-muted)">Content here</p>' }),
77
+ }),
78
+ `section({ variant: 'spotlight', title: 'Dramatically focused', align: 'center', content })`
79
+ )}
80
+
42
81
  ${table(
43
82
  ['Prop', 'Type', 'Default', 'Description'],
44
83
  [
45
84
  ['<code>content</code>', 'string (HTML)', '—', 'Raw HTML slot'],
46
- ['<code>variant</code>', 'string', "'default'", "'default' · 'alt' · 'dark'"],
85
+ ['<code>variant</code>', 'string', "'default'", "'default' · 'alt' · 'dark' · 'diagonal' · 'paper' · 'spotlight'"],
47
86
  ['<code>padding</code>', 'string', "'md'", "'sm' · 'md' · 'lg'"],
48
87
  ['<code>id</code>', 'string', '—', 'Anchor id for nav links'],
49
88
  ['<code>eyebrow</code>', 'string', '—', 'Small label above the title'],
@@ -126,6 +126,32 @@ meta: {
126
126
  }`, 'js'))}
127
127
  ${callout('note', 'For maximum performance, host your own fonts rather than using Google Fonts. External stylesheet requests add render-blocking latency and a DNS lookup.')}
128
128
 
129
+ ${section('vibe', 'Personality presets (meta.vibe)')}
130
+ <p>Set <code>meta.vibe</code> to apply a personality preset that adjusts typography, border radius, and letter-spacing across all components on the page. The preset sets a <code>data-vibe</code> attribute on <code>&lt;body&gt;</code> — pair it with a <code>theme.css</code> for complete brand expression.</p>
131
+ ${codeBlock(highlight(`export default {
132
+ route: '/landing',
133
+ meta: {
134
+ vibe: 'playful',
135
+ styles: ['/pulse-ui.css', '/theme.css'],
136
+ },
137
+ }`, 'js'))}
138
+ ${table(
139
+ ['Vibe', 'Character', 'Best for'],
140
+ [
141
+ ['<code>warm</code>', 'Friendly, rounded (14px), humanist feel', 'Local businesses, wellness, food, services'],
142
+ ['<code>editorial</code>', 'Serif display, zero radius, tight tracking', 'Publications, blogs, journalism, literary'],
143
+ ['<code>playful</code>', 'Extra-large radius (22px), loose spacing', 'Consumer apps, kids, lifestyle, creators'],
144
+ ['<code>minimal</code>', 'Ultra-low radius (2px), refined whitespace', 'Portfolios, luxury, developer tools'],
145
+ ['<code>bold</code>', 'Tight tracking, strong presence', 'Agencies, startups, sports'],
146
+ ['<code>brutalist</code>', 'Zero radius, oversized Arial Black / Impact headings', 'Art, fashion, counter-culture, zines'],
147
+ ['<code>retro</code>', 'Slab serif, medium radius (8px), nostalgic geometry', 'Craft, breweries, vintage, heritage brands'],
148
+ ['<code>corporate</code>', 'Conservative radius (4px), tight tracking, professional', 'B2B, finance, legal, consultancies'],
149
+ ['<code>neon</code>', 'Monospace headline, futuristic, sharp edges', 'Gaming, crypto, dark-tech, cyberpunk'],
150
+ ['<code>paper</code>', 'Organic radius (10px), serif body, tactile feel', 'Journals, bookshops, newsletters, diaries'],
151
+ ]
152
+ )}
153
+ ${callout('tip', 'Vibe only adjusts shape and type tokens — it does not set colours. Add a <code>theme.css</code> that defines <code>--ui-accent</code>, <code>--color-bg</code>, and other colour tokens to complete the personality.')}
154
+
129
155
  ${section('seo-tips', 'SEO tips')}
130
156
  <ul>
131
157
  <li>Write a unique <code>title</code> and <code>description</code> for every page — duplicate metadata prevents pages from competing in search results.</li>
@@ -0,0 +1,260 @@
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
+ import { asset } from '../../lib/layout.js'
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Code block helper
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function codeBlock({ filename, code }) {
35
+ return `
36
+ <div class="ap-code-block" role="figure" aria-label="Code example: ${filename}">
37
+ <div class="ap-code-block__header">
38
+ ${iconCode({ size: 14 })}
39
+ <span>${filename}</span>
40
+ </div>
41
+ <pre class="ap-code-block__pre"><code class="ap-code-block__code">${code}</code></pre>
42
+ </div>
43
+ `
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Spec
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export default {
51
+ route: '/templates/andypixel',
52
+
53
+ meta: {
54
+ title: 'JavaScript Proxies: The Hidden Superpower \u2014 Andy Pixel',
55
+ description: 'Discover how JavaScript Proxies let you intercept and redefine fundamental operations on objects.',
56
+ styles: ['/pulse-ui.css', '/andypixel.css'],
57
+ theme: 'light',
58
+ },
59
+
60
+ state: {
61
+ status: 'idle',
62
+ },
63
+
64
+ view: (state) => `
65
+ <main id="main-content">
66
+
67
+ ${nav({
68
+ logo: 'Andy Pixel',
69
+ links: [
70
+ { label: 'Blog', href: '#blog' },
71
+ { label: 'Projects', href: '#projects' },
72
+ { label: 'About', href: '#about' },
73
+ ],
74
+ })}
75
+
76
+ <article>
77
+
78
+ <header class="ap-article-header">
79
+ <div class="ap-reading-col">
80
+
81
+ <div class="ap-article-meta">
82
+ ${badge({ label: 'JavaScript' })}
83
+ ${badge({ label: 'Advanced' })}
84
+ <span class="u-text-muted u-text-sm">\u00b7 May 28, 2026 \u00b7 8 min read</span>
85
+ </div>
86
+
87
+ <h1 class="ap-article-title">
88
+ JavaScript Proxies: The Hidden Superpower You\u2019re Not Using
89
+ </h1>
90
+
91
+ <p class="ap-article-subtitle">
92
+ Intercept, transform, and observe objects at a fundamental level \u2014
93
+ without touching the source.
94
+ </p>
95
+
96
+ <div class="ap-article-author-strip">
97
+ ${avatar({ name: 'Andy Pixel', size: 'sm' })}
98
+ <div>
99
+ <p class="u-text-sm u-font-bold">Andy Pixel</p>
100
+ <p class="u-text-sm u-text-muted">Frontend Engineer</p>
101
+ </div>
102
+ </div>
103
+
104
+ </div>
105
+ </header>
106
+
107
+ <div class="ap-article-body ap-reading-col">
108
+
109
+ ${prose({ content: `
110
+ <p class="ap-article-lead">If you\u2019ve ever wanted to validate object properties automatically,
111
+ track changes reactively, or build a mock API client that feels real \u2014 the
112
+ <code>Proxy</code> object is your tool. Yet somehow it flies under the radar
113
+ for most developers.</p>
114
+
115
+ <p>Introduced in ES2015, <code>Proxy</code> wraps an object and lets you intercept
116
+ fundamental operations \u2014 property reads, writes, deletions, and more \u2014 via
117
+ \u201ctraps\u201d defined on a handler object.</p>
118
+
119
+ <h2>The anatomy of a Proxy</h2>
120
+
121
+ <p>A Proxy takes two arguments: the <strong>target</strong> (the object being wrapped)
122
+ and the <strong>handler</strong> (an object defining which operations to intercept).
123
+ Every interceptable operation has a corresponding trap method on the handler.</p>
124
+ ` })}
125
+
126
+ ${codeBlock({
127
+ filename: 'proxy-basics.js',
128
+ code: `const handler = {
129
+ get(target, key) {
130
+ return key in target
131
+ ? target[key]
132
+ : \`Property "\${key}" doesn\u2019t exist\`
133
+ },
134
+ set(target, key, value) {
135
+ if (typeof value !== 'number') {
136
+ throw new TypeError('Only numbers allowed')
137
+ }
138
+ target[key] = value
139
+ return true
140
+ }
141
+ }
142
+
143
+ const proxy = new Proxy({}, handler)
144
+ proxy.score = 42 // \u2713 works
145
+ proxy.score = 'oops' // \u2717 TypeError`,
146
+ })}
147
+
148
+ ${prose({ content: `
149
+ <p>The <code>get</code> trap runs on every property access. The <code>set</code> trap
150
+ intercepts every assignment. Return <code>true</code> from <code>set</code> to signal
151
+ success \u2014 otherwise you\u2019ll get a <code>TypeError</code> in strict mode.</p>
152
+
153
+ <h2>Real-world use cases</h2>
154
+
155
+ <p>The real power isn\u2019t abstract. Proxy unlocks patterns that are otherwise
156
+ painful or impossible.</p>
157
+ ` })}
158
+
159
+ <blockquote class="ap-pullquote">
160
+ <p>\u201cA Proxy doesn\u2019t change what your object <em>is</em> \u2014 it changes what your
161
+ object <em>does</em> when the runtime touches it.\u201d</p>
162
+ </blockquote>
163
+
164
+ ${prose({ content: `
165
+ <h3>Reactive state</h3>
166
+ <p>Vue 3\u2019s reactivity system is built entirely on Proxy. When you access a reactive
167
+ property, Vue\u2019s <code>get</code> trap tracks the dependency. When you mutate it,
168
+ the <code>set</code> trap schedules re-renders. No syntax overhead. Just assignment.</p>
169
+
170
+ <h3>Input validation</h3>
171
+ <p>Instead of scattering guard clauses through your codebase, centralise validation
172
+ in a Proxy handler. The contract lives in one place. Every consumer benefits
173
+ automatically.</p>
174
+
175
+ <h3>API mocking</h3>
176
+ <p>Wrap a plain object in a Proxy that intercepts property access and returns async
177
+ functions. You get a fully-typed mock client that mirrors your real API surface \u2014
178
+ without any code generation.</p>
179
+
180
+ <h2>Performance considerations</h2>
181
+
182
+ <p>Proxies are not free. Every trap fires a function call. In tight loops over large
183
+ collections, this adds up. Benchmark before you commit. For most use cases \u2014 form
184
+ state, reactive stores, config validation \u2014 the overhead is completely negligible.</p>
185
+
186
+ <p>The V8 team has steadily optimised Proxy performance since ES2015. Don\u2019t avoid
187
+ Proxy out of dated fear \u2014 measure it first.</p>
188
+ ` })}
189
+
190
+ </div>
191
+
192
+ </article>
193
+
194
+ <div class="ap-author-bio-wrap">
195
+ <div class="ap-reading-col">
196
+ <div class="ap-author-bio">
197
+ ${avatar({ name: 'Andy Pixel', size: 'lg' })}
198
+ <div class="ap-author-bio__text">
199
+ <p class="ap-author-bio__name">Andy Pixel</p>
200
+ <p class="u-text-muted u-text-sm">
201
+ Frontend engineer obsessed with the web platform. Writing about JavaScript,
202
+ CSS, and creative code. Building things at the edge of what browsers can do.
203
+ </p>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ ${ state.status === 'success'
210
+ ? `<div class="ap-newsletter-success">
211
+ <p class="u-font-bold u-text-lg">${iconCheck({ size: 20, class: 'u-inline' })} You\u2019re in!</p>
212
+ <p class="u-text-muted u-mt-2">Next article lands in your inbox.</p>
213
+ </div>`
214
+ : `<div class="ap-newsletter-section">
215
+ <p class="ap-newsletter-eyebrow">Stay in the loop</p>
216
+ <h2 class="ap-newsletter-title">More like this, in your inbox.</h2>
217
+ <p class="u-text-muted">
218
+ Deep-dives on JavaScript, the web platform, and frontend engineering \u2014
219
+ whenever I publish. No noise.
220
+ </p>
221
+ ${ state.status === 'error'
222
+ ? `<p class="ap-newsletter-error u-mt-3">Something went wrong. Please try again.</p>`
223
+ : '' }
224
+ <form class="ap-newsletter-form" data-action="subscribe">
225
+ ${input({ type: 'email', name: 'email', placeholder: 'you@example.com', label: 'Email address', attrs: { autocomplete: 'email' } })}
226
+ ${button({ label: 'Subscribe', variant: 'primary', attrs: { type: 'submit' } })}
227
+ </form>
228
+ </div>`
229
+ }
230
+
231
+ ${footer({
232
+ logo: 'Andy Pixel',
233
+ links: [
234
+ { label: 'Blog', href: '#blog' },
235
+ { label: 'Projects', href: '#projects' },
236
+ { label: 'About', href: '#about' },
237
+ { label: 'RSS', href: '#feed' },
238
+ ],
239
+ })}
240
+
241
+ </main>
242
+ <script src="${asset('/pulse-ui.js')}" defer></script>
243
+ `,
244
+
245
+ actions: {
246
+ subscribe: {
247
+ onStart: (state, formData) => ({
248
+ status: 'loading',
249
+ }),
250
+ run: async (state, serverState, formData) => {
251
+ // Newsletter subscription would call an API here
252
+ },
253
+ onSuccess: (state) => ({ status: 'success' }),
254
+ onError: (state, err) => ({
255
+ status: 'error',
256
+ errors: err?.validation ?? [{ message: err.message }],
257
+ }),
258
+ },
259
+ },
260
+ }