@invisibleloop/pulse 0.20.10 → 0.20.19

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 (266) hide show
  1. package/package.json +6 -5
  2. package/public/.pulse-ui-version +1 -1
  3. package/scripts/build.js +26 -0
  4. package/scripts/release-version.js +17 -8
  5. package/scripts/strip-server.test.js +60 -0
  6. package/src/agent/checklist.md +8 -12
  7. package/src/agent/commands/pulse-start.md +2 -0
  8. package/src/agent/commands/verify.md +30 -8
  9. package/src/agent/guide-components.md +7 -25
  10. package/src/agent/guide-routing.md +0 -12
  11. package/src/agent/guide-server.md +102 -1
  12. package/src/agent/guide-spec.md +8 -1
  13. package/src/agent/guide-styles.md +23 -0
  14. package/src/agent/identity.md +9 -0
  15. package/src/agent/workflow.md +4 -23
  16. package/src/cli/check-bundles.js +113 -0
  17. package/src/cli/check-bundles.test.js +137 -0
  18. package/src/cli/cli.test.js +105 -0
  19. package/src/cli/dev.js +48 -3
  20. package/src/cli/diagnose.js +105 -0
  21. package/src/cli/diagnose.test.js +124 -0
  22. package/src/cli/discover.js +9 -2
  23. package/src/cli/index.js +171 -9
  24. package/src/cli/process-utils.js +57 -0
  25. package/src/cli/process-utils.test.js +114 -0
  26. package/src/cli/review.js +391 -0
  27. package/src/cli/review.test.js +143 -0
  28. package/src/cli/scaffold.js +9 -4
  29. package/src/cli/stamp.js +102 -0
  30. package/src/cli/stamp.test.js +149 -0
  31. package/src/cli/start.js +15 -2
  32. package/src/cli/validate.js +184 -0
  33. package/src/cli/validate.test.js +153 -0
  34. package/src/mcp/mcp.test.js +261 -30
  35. package/src/mcp/server.js +207 -515
  36. package/src/mcp/validate-worker.js +18 -1
  37. package/src/runtime/index.js +144 -11
  38. package/src/runtime/runtime.test.js +199 -0
  39. package/src/runtime/ssr.js +14 -7
  40. package/src/runtime/ssr.test.js +31 -0
  41. package/src/server/index.js +163 -10
  42. package/src/server/server.test.js +225 -2
  43. package/src/spec/schema.js +1 -1
  44. package/src/ui/index.js +0 -5
  45. package/src/ui/ui.test.js +0 -75
  46. package/types/ui.d.ts +0 -21
  47. package/.claude/commands/build-page.md +0 -63
  48. package/.claude/commands/new-doc-page.md +0 -45
  49. package/.claude/commands/pulse-start.md +0 -11
  50. package/.claude/commands/pulse-stop.md +0 -5
  51. package/.claude/commands/verify.md +0 -118
  52. package/.claude/pulse-checklist.md +0 -189
  53. package/.github/workflows/ci.yml +0 -25
  54. package/.github/workflows/publish.yml +0 -45
  55. package/.pulse/load-reports/home/1773432711417.json +0 -22
  56. package/.pulse-verified +0 -1
  57. package/AGENTS.md +0 -697
  58. package/CLAUDE.md +0 -697
  59. package/README.md +0 -95
  60. package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
  61. package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
  62. package/benchmark/public/dist/manifest.json +0 -5
  63. package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
  64. package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
  65. package/benchmark/results/2026-03-27-09-20.json +0 -188
  66. package/benchmark/results/2026-03-27-09-21.json +0 -188
  67. package/benchmark/results/2026-03-27-09-29.json +0 -188
  68. package/benchmark/results/2026-03-27-10-14.json +0 -188
  69. package/benchmark/results/2026-03-27-10-18.json +0 -232
  70. package/benchmark/src/pages/counter.js +0 -37
  71. package/benchmark/src/pages/home.js +0 -21
  72. package/docs/.claude/commands/pulse-build.md +0 -7
  73. package/docs/.claude/commands/pulse-dev.md +0 -7
  74. package/docs/.claude/commands/pulse-start.md +0 -9
  75. package/docs/.claude/commands/pulse-stop.md +0 -5
  76. package/docs/.claude/commands/verify.md +0 -84
  77. package/docs/.claude/coverage-check.js +0 -196
  78. package/docs/.claude/pulse-checklist.md +0 -189
  79. package/docs/public/.pulse-ui-version +0 -1
  80. package/docs/public/andypixel.css +0 -237
  81. package/docs/public/bookworms.css +0 -422
  82. package/docs/public/coutumes.css +0 -561
  83. package/docs/public/docs.css +0 -2388
  84. package/docs/public/favicon.svg +0 -4
  85. package/docs/public/home-brut.css +0 -982
  86. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  87. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  88. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  89. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  90. package/docs/public/images/journey/178.jpg +0 -0
  91. package/docs/public/images/journey/29.jpg +0 -0
  92. package/docs/public/images/retrowheels/hero.jpg +0 -0
  93. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  94. package/docs/public/images/suzukibento/1015.jpg +0 -0
  95. package/docs/public/images/suzukibento/1025.jpg +0 -0
  96. package/docs/public/images/suzukibento/1043.jpg +0 -0
  97. package/docs/public/images/suzukibento/1059.jpg +0 -0
  98. package/docs/public/images/suzukibento/1062.jpg +0 -0
  99. package/docs/public/images/suzukibento/1084.jpg +0 -0
  100. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  101. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  102. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  103. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  104. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  105. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  106. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  107. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  108. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  109. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  110. package/docs/public/img/heic1501a.jpg +0 -0
  111. package/docs/public/img/placeholder-landscape.svg +0 -18
  112. package/docs/public/img/placeholder-square.svg +0 -14
  113. package/docs/public/img/placeholder-tall.svg +0 -22
  114. package/docs/public/journey.css +0 -528
  115. package/docs/public/menu.js +0 -327
  116. package/docs/public/pulse-ui.css +0 -3028
  117. package/docs/public/pulse-ui.js +0 -257
  118. package/docs/public/retrowheels.css +0 -425
  119. package/docs/public/shop.css +0 -269
  120. package/docs/public/sparkys.css +0 -382
  121. package/docs/public/suzukibento.css +0 -517
  122. package/docs/public/swish.css +0 -394
  123. package/docs/public/theme.css +0 -52
  124. package/docs/public/themes/lumio.css +0 -50
  125. package/docs/public/themes/observatory.css +0 -104
  126. package/docs/server.js +0 -161
  127. package/docs/src/lib/component-page.js +0 -49
  128. package/docs/src/lib/highlight.js +0 -2
  129. package/docs/src/lib/layout.js +0 -190
  130. package/docs/src/lib/metrics-store.js +0 -6
  131. package/docs/src/lib/nav.js +0 -171
  132. package/docs/src/lib/stats.js +0 -81
  133. package/docs/src/pages/accessibility.js +0 -157
  134. package/docs/src/pages/actions.js +0 -194
  135. package/docs/src/pages/auth.js +0 -176
  136. package/docs/src/pages/caching.js +0 -95
  137. package/docs/src/pages/components/accordion.js +0 -48
  138. package/docs/src/pages/components/alert.js +0 -35
  139. package/docs/src/pages/components/app-badge.js +0 -41
  140. package/docs/src/pages/components/avatar.js +0 -35
  141. package/docs/src/pages/components/badge.js +0 -36
  142. package/docs/src/pages/components/breadcrumbs.js +0 -94
  143. package/docs/src/pages/components/button.js +0 -84
  144. package/docs/src/pages/components/card.js +0 -225
  145. package/docs/src/pages/components/charts.js +0 -283
  146. package/docs/src/pages/components/checkbox.js +0 -129
  147. package/docs/src/pages/components/cluster.js +0 -47
  148. package/docs/src/pages/components/container.js +0 -40
  149. package/docs/src/pages/components/cta.js +0 -54
  150. package/docs/src/pages/components/divider.js +0 -37
  151. package/docs/src/pages/components/empty.js +0 -36
  152. package/docs/src/pages/components/feature.js +0 -60
  153. package/docs/src/pages/components/fieldset.js +0 -65
  154. package/docs/src/pages/components/fileupload.js +0 -128
  155. package/docs/src/pages/components/footer.js +0 -58
  156. package/docs/src/pages/components/grid.js +0 -169
  157. package/docs/src/pages/components/heading.js +0 -107
  158. package/docs/src/pages/components/hero.js +0 -123
  159. package/docs/src/pages/components/icons.js +0 -410
  160. package/docs/src/pages/components/image.js +0 -71
  161. package/docs/src/pages/components/input.js +0 -51
  162. package/docs/src/pages/components/media.js +0 -51
  163. package/docs/src/pages/components/modal.js +0 -111
  164. package/docs/src/pages/components/nav.js +0 -136
  165. package/docs/src/pages/components/phone-frame.js +0 -74
  166. package/docs/src/pages/components/progress.js +0 -102
  167. package/docs/src/pages/components/prose.js +0 -111
  168. package/docs/src/pages/components/pullquote.js +0 -71
  169. package/docs/src/pages/components/radio.js +0 -194
  170. package/docs/src/pages/components/rating.js +0 -106
  171. package/docs/src/pages/components/search.js +0 -61
  172. package/docs/src/pages/components/section.js +0 -98
  173. package/docs/src/pages/components/segmented.js +0 -121
  174. package/docs/src/pages/components/select.js +0 -45
  175. package/docs/src/pages/components/slider.js +0 -115
  176. package/docs/src/pages/components/spinner.js +0 -73
  177. package/docs/src/pages/components/stack.js +0 -48
  178. package/docs/src/pages/components/stat.js +0 -55
  179. package/docs/src/pages/components/stepper.js +0 -66
  180. package/docs/src/pages/components/table.js +0 -45
  181. package/docs/src/pages/components/textarea.js +0 -31
  182. package/docs/src/pages/components/toggle.js +0 -84
  183. package/docs/src/pages/components/tooltip.js +0 -48
  184. package/docs/src/pages/components.js +0 -200
  185. package/docs/src/pages/config.js +0 -193
  186. package/docs/src/pages/constraints.js +0 -100
  187. package/docs/src/pages/deploy.js +0 -233
  188. package/docs/src/pages/error-pages.js +0 -99
  189. package/docs/src/pages/extending.js +0 -198
  190. package/docs/src/pages/faq.js +0 -99
  191. package/docs/src/pages/forms.js +0 -129
  192. package/docs/src/pages/getting-started.js +0 -147
  193. package/docs/src/pages/guard.js +0 -120
  194. package/docs/src/pages/home.js +0 -345
  195. package/docs/src/pages/how-it-works.js +0 -244
  196. package/docs/src/pages/hydration.js +0 -108
  197. package/docs/src/pages/images.js +0 -120
  198. package/docs/src/pages/markdown.js +0 -156
  199. package/docs/src/pages/meta.js +0 -193
  200. package/docs/src/pages/mutations.js +0 -107
  201. package/docs/src/pages/navigation.js +0 -85
  202. package/docs/src/pages/performance.js +0 -157
  203. package/docs/src/pages/persist.js +0 -89
  204. package/docs/src/pages/project-structure.js +0 -91
  205. package/docs/src/pages/prompt-examples.js +0 -186
  206. package/docs/src/pages/raw-responses.js +0 -122
  207. package/docs/src/pages/routing.js +0 -120
  208. package/docs/src/pages/server-api.js +0 -291
  209. package/docs/src/pages/server-data.js +0 -203
  210. package/docs/src/pages/slash-commands.js +0 -55
  211. package/docs/src/pages/spec.js +0 -207
  212. package/docs/src/pages/state.js +0 -102
  213. package/docs/src/pages/store.js +0 -203
  214. package/docs/src/pages/streaming.js +0 -107
  215. package/docs/src/pages/stripe.js +0 -194
  216. package/docs/src/pages/supabase.js +0 -324
  217. package/docs/src/pages/templates/andypixel.js +0 -260
  218. package/docs/src/pages/templates/blog-post.js +0 -226
  219. package/docs/src/pages/templates/bookworms.js +0 -227
  220. package/docs/src/pages/templates/coutumes.js +0 -237
  221. package/docs/src/pages/templates/journey.js +0 -186
  222. package/docs/src/pages/templates/mobile-app.js +0 -371
  223. package/docs/src/pages/templates/retrowheels.js +0 -355
  224. package/docs/src/pages/templates/shop.js +0 -268
  225. package/docs/src/pages/templates/sparkys.js +0 -264
  226. package/docs/src/pages/templates/suzukibento.js +0 -200
  227. package/docs/src/pages/templates/swish.js +0 -313
  228. package/docs/src/pages/templates.js +0 -534
  229. package/docs/src/pages/testing.js +0 -199
  230. package/docs/src/pages/validation.js +0 -138
  231. package/examples/.claude/commands/pulse-build.md +0 -7
  232. package/examples/.claude/commands/pulse-dev.md +0 -7
  233. package/examples/.claude/commands/pulse-start.md +0 -9
  234. package/examples/.claude/commands/pulse-stop.md +0 -5
  235. package/examples/.claude/commands/verify.md +0 -84
  236. package/examples/.claude/coverage-check.js +0 -196
  237. package/examples/.claude/pulse-checklist.md +0 -189
  238. package/examples/andypixel.css +0 -237
  239. package/examples/andypixel.js +0 -258
  240. package/examples/bookworms.css +0 -422
  241. package/examples/bookworms.js +0 -225
  242. package/examples/contact.js +0 -166
  243. package/examples/counter.js +0 -94
  244. package/examples/dev.server.js +0 -110
  245. package/examples/examples.test.js +0 -394
  246. package/examples/journey.css +0 -528
  247. package/examples/journey.js +0 -186
  248. package/examples/pricing.js +0 -262
  249. package/examples/products.js +0 -190
  250. package/examples/public/.pulse-ui-version +0 -1
  251. package/examples/public/pulse-ui.css +0 -3028
  252. package/examples/public/pulse-ui.js +0 -257
  253. package/examples/quiz.js +0 -208
  254. package/examples/retrowheels.css +0 -425
  255. package/examples/retrowheels.js +0 -353
  256. package/examples/shared.js +0 -84
  257. package/examples/suzukibento.css +0 -513
  258. package/examples/suzukibento.js +0 -200
  259. package/examples/swish.css +0 -394
  260. package/examples/swish.js +0 -311
  261. package/examples/todos.js +0 -162
  262. package/src/ui/fieldset.js +0 -37
  263. package/src/ui/phone-frame.js +0 -40
  264. package/src/ui/search.js +0 -78
  265. package/src/ui/segmented.js +0 -50
  266. package/src/ui/tooltip.js +0 -28
@@ -1,225 +0,0 @@
1
- /**
2
- * Pulse — BookWorms example
3
- *
4
- * Demonstrates:
5
- * - Light-theme editorial branding for a physical/independent business
6
- * - nav() with logo text, anchor links, and a CTA action button
7
- * - Pure HTML sections — no Pulse section/container wrappers (shows the
8
- * framework is just strings; you can write vanilla HTML where it fits)
9
- * - Book card grid (staff picks) with cover images from picsum.photos
10
- * - Pull-quote / press quote breakout
11
- * - Two-column feature grid
12
- * - Bordered event list with date / title / host columns
13
- * - Centred CTA section
14
- * - footer() with flat link list
15
- * - Google Fonts (Fraunces) for display headings
16
- *
17
- * Run: node examples/dev.server.js → http://localhost:3001/bookworms
18
- */
19
-
20
- import { nav, button, footer } from '../src/ui/index.js'
21
-
22
- // ---------------------------------------------------------------------------
23
- // Static data
24
- // ---------------------------------------------------------------------------
25
-
26
- const picks = [
27
- {
28
- tag: 'Fiction · Staff pick',
29
- title: 'The Lantern Keepers',
30
- author: 'Iris Aldenmoor',
31
- note: 'A quiet novel about three sisters, an unreliable lighthouse, and the long winter that follows a single decision.',
32
- cover: '/images/bookworms/lantern-keepers.jpg',
33
- },
34
- {
35
- tag: 'Non-fiction · This week',
36
- title: 'How We Read Now',
37
- author: 'Dr. Samuel Okafor',
38
- note: 'A short, generous history of attention — from monastic libraries to the open browser tab.',
39
- cover: '/images/bookworms/how-we-read-now.jpg',
40
- },
41
- {
42
- tag: 'Rare find · Just arrived',
43
- title: 'Letters from a Provincial Bookseller',
44
- author: 'M. Halloran (1927)',
45
- note: 'First edition, lightly foxed. A wry account of running a shop through a quiet decade.',
46
- cover: '/images/bookworms/provincial-bookseller.jpg',
47
- },
48
- ]
49
-
50
- const features = [
51
- {
52
- title: 'Curated, not algorithmic',
53
- body: 'Every recommendation comes from one of our booksellers. No bestseller lists, no engagement metrics — just books we\'ve read and would lend you ourselves.',
54
- },
55
- {
56
- title: 'Browse by mood',
57
- body: 'Looking for something restorative? Something difficult? Our shelves are arranged the way readers actually think.',
58
- },
59
- {
60
- title: 'Author evenings & book clubs',
61
- body: 'Small, considered events with writers we admire. Wine, conversation, and signed copies — no panels of four.',
62
- },
63
- {
64
- title: 'Free delivery over £25',
65
- body: 'Wrapped by hand, delivered locally by bicycle. Posted further afield by the next morning.',
66
- },
67
- {
68
- title: 'Gift cards & reader subscriptions',
69
- body: 'A monthly book chosen for one specific reader, with a note from the bookseller who picked it.',
70
- },
71
- {
72
- title: 'Open seven days',
73
- body: 'Old Market Lane, opposite the bakery. Coffee from nine, books from ten.',
74
- },
75
- ]
76
-
77
- const events = [
78
- { date: '12 June', title: 'An evening with Iris Aldenmoor', host: 'In conversation with Priya Joshi' },
79
- { date: '21 June', title: 'Slow Reading Club — Middlemarch, part one', host: 'Hosted by the BookWorms team' },
80
- { date: '03 July', title: 'New voices in non-fiction', host: 'Three debut authors read and answer questions' },
81
- ]
82
-
83
- const navLinks = [
84
- { label: 'Browse', href: '#browse' },
85
- { label: 'Staff picks', href: '#picks' },
86
- { label: 'Events', href: '#events' },
87
- { label: 'Visit', href: '#visit' },
88
- ]
89
-
90
- // ---------------------------------------------------------------------------
91
- // Spec
92
- // ---------------------------------------------------------------------------
93
-
94
- export default {
95
- route: '/bookworms',
96
-
97
- meta: {
98
- title: 'BookWorms — an independent bookstore for curious readers',
99
- description: 'Handpicked fiction, non-fiction, and rare finds. Weekly staff picks, author evenings, and a slow reading club. Open seven days on Old Market Lane.',
100
- theme: 'light',
101
- styles: [
102
- 'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;1,9..144,400&display=swap',
103
- '/pulse-ui.css',
104
- '/examples/bookworms.css',
105
- ],
106
- },
107
-
108
- view: () => `
109
- ${nav({
110
- logo: 'BookWorms',
111
- links: navLinks,
112
- action: button({ label: 'Visit the shop', href: '#visit', variant: 'primary', size: 'sm' }),
113
- sticky: true,
114
- })}
115
-
116
- <main id="main-content">
117
- <section class="bw-hero">
118
- <div class="bw-shell bw-hero__inner">
119
- <div class="bw-hero__content">
120
- <p class="bw-eyebrow">Independent bookstore · Old Market Lane</p>
121
- <h1 class="bw-hero-title">Books, handpicked for the curious.</h1>
122
- <p class="bw-hero-lede">
123
- BookWorms is a small shop with a long memory. Every title on our shelves has been read by
124
- someone who works here — and is waiting for the right reader to find it.
125
- </p>
126
- <div class="bw-cta-actions bw-cta-actions--start">
127
- ${button({ label: 'See this week\u2019s picks', href: '#picks', variant: 'primary' })}
128
- ${button({ label: 'Upcoming events', href: '#events', variant: 'secondary' })}
129
- </div>
130
- </div>
131
- <div class="bw-hero__image" aria-hidden="true">
132
- <img src="/images/bookworms/hero-books.jpg" alt="" width="820" height="1080" />
133
- </div>
134
- </div>
135
- </section>
136
-
137
- <section class="bw-section" id="picks">
138
- <div class="bw-shell">
139
- <p class="bw-section-label">This week</p>
140
- <h2 class="bw-section-title">Three books we\u2019re pressing into people\u2019s hands.</h2>
141
- <div class="bw-picks">
142
- ${picks.map((p, i) => `
143
- <article>
144
- <div class="bw-pick-cover-wrap">
145
- <img class="bw-pick-cover" src="${p.cover}" alt="Cover of ${p.title} by ${p.author}" width="400" height="600" loading="lazy" />
146
- <div class="bw-pick-overlay">
147
- <h3 class="bw-pick-overlay-title">${p.title}</h3>
148
- <p class="bw-pick-overlay-author">${i === 2 ? p.author.replace(/\s*\(\d{4}\)/, '') : 'by ' + p.author}</p>
149
- </div>
150
- </div>
151
- <p class="bw-pick-meta">${p.tag}</p>
152
- <h3 class="bw-pick-title">${p.title}</h3>
153
- <p class="bw-pick-author">by ${p.author}</p>
154
- <p class="bw-pick-note">${p.note}</p>
155
- </article>
156
- `).join('')}
157
- </div>
158
- </div>
159
- </section>
160
-
161
- <section class="bw-pull">
162
- <div class="bw-shell">
163
- <p class="bw-pull-quote">\u201cThe kind of shop where you walk in for one book and leave with three you didn\u2019t know you needed.\u201d</p>
164
- <p class="bw-pull-cite">\u2014 The Quarterly Review</p>
165
- </div>
166
- </section>
167
-
168
- <section class="bw-section" id="browse">
169
- <div class="bw-shell">
170
- <p class="bw-section-label">What we do</p>
171
- <h2 class="bw-section-title">A bookstore that still reads.</h2>
172
- <div class="bw-features">
173
- ${features.map(f => `
174
- <div>
175
- <h3 class="bw-feature-title">${f.title}</h3>
176
- <p class="bw-feature-body">${f.body}</p>
177
- </div>
178
- `).join('')}
179
- </div>
180
- </div>
181
- </section>
182
-
183
- <section class="bw-section" id="events">
184
- <div class="bw-shell">
185
- <p class="bw-section-label">Upcoming</p>
186
- <h2 class="bw-section-title">Evenings worth turning up for.</h2>
187
- <div class="bw-events">
188
- ${events.map(e => `
189
- <article class="bw-event">
190
- <p class="bw-event-date">${e.date}</p>
191
- <h3 class="bw-event-title">${e.title}</h3>
192
- <p class="bw-event-host">${e.host}</p>
193
- </article>
194
- `).join('')}
195
- </div>
196
- </div>
197
- </section>
198
-
199
- <section class="bw-cta" id="visit">
200
- <div class="bw-shell">
201
- <h2 class="bw-cta-title">Come and browse.</h2>
202
- <p class="bw-cta-sub">
203
- 14 Old Market Lane, opposite the bakery. Open Monday to Sunday, 10am\u20137pm.
204
- Or sign up to receive one handpicked book each month.
205
- </p>
206
- <div class="bw-cta-actions">
207
- ${button({ label: 'Plan a visit', href: '#visit', variant: 'primary' })}
208
- ${button({ label: 'Reader subscription', href: '#visit', variant: 'secondary' })}
209
- </div>
210
- </div>
211
- </section>
212
- </main>
213
-
214
- ${footer({
215
- logo: 'BookWorms',
216
- links: [
217
- { label: 'Browse', href: '#browse' },
218
- { label: 'Events', href: '#events' },
219
- { label: 'Visit', href: '#visit' },
220
- { label: 'Contact', href: '#visit' },
221
- ],
222
- legal: '\u00a9 2026 BookWorms \u00b7 14 Old Market Lane',
223
- })}
224
- `,
225
- }
@@ -1,166 +0,0 @@
1
- /**
2
- * Pulse — Contact form example
3
- *
4
- * Demonstrates:
5
- * - Server data (fetched before render, passed to view)
6
- * - Validation (required, email format, length limits)
7
- * - Async action with full lifecycle: onStart → validate → run → onSuccess/onError
8
- * - Loading, success, and error states
9
- *
10
- * Run: node examples/dev.server.js → http://localhost:3001/contact
11
- */
12
-
13
- import { button, alert, heading, input, textarea, card, container, section, stack, grid, iconMail, iconPhone, iconMapPin, iconClock, iconCheckCircle } from '../src/ui/index.js'
14
- import { examplesNav } from './shared.js'
15
-
16
- function infoDetail(icon, label, value, href) {
17
- const valueHtml = href
18
- ? `<a href="${href}" class="u-text-default">${value}</a>`
19
- : `<span>${value}</span>`
20
- return `<div class="u-flex u-items-start u-gap-3">
21
- <span class="u-text-muted u-shrink-0" aria-hidden="true">${icon}</span>
22
- <div>
23
- <p class="u-text-xs u-text-muted u-font-semibold u-mb-1">${label}</p>
24
- <p class="u-text-sm">${valueHtml}</p>
25
- </div>
26
- </div>`
27
- }
28
-
29
- export default {
30
- route: '/contact',
31
-
32
- meta: {
33
- title: 'Contact — Pulse',
34
- description: 'A contact form built with Pulse. Demonstrates server data, validation, and async action lifecycle.',
35
- styles: ['/pulse-ui.css'],
36
- theme: 'dark',
37
- },
38
-
39
- server: {
40
- info: async () => ({
41
- email: 'hello@example.com',
42
- phone: '+44 20 7946 0958',
43
- address: '123 Shoreditch High St, London E1 6JE',
44
- hours: 'Monday – Friday, 9 am – 5 pm GMT',
45
- }),
46
- },
47
-
48
- state: {
49
- status: 'idle', // idle | loading | success | error
50
- errors: [],
51
- fields: { name: '', email: '', subject: '', message: '' },
52
- },
53
-
54
- validation: {
55
- 'fields.name': { required: true, minLength: 2, maxLength: 80 },
56
- 'fields.email': { required: true, format: 'email' },
57
- 'fields.subject': { required: true, minLength: 2, maxLength: 120 },
58
- 'fields.message': { required: true, minLength: 10, maxLength: 2000 },
59
- },
60
-
61
- mutations: {
62
- reset: () => ({ status: 'idle', errors: [], fields: { name: '', email: '', subject: '', message: '' } }),
63
- },
64
-
65
- actions: {
66
- submit: {
67
- onStart: (_state, formData) => ({
68
- status: 'loading',
69
- errors: [],
70
- fields: {
71
- name: formData.get('name') || '',
72
- email: formData.get('email') || '',
73
- subject: formData.get('subject') || '',
74
- message: formData.get('message') || '',
75
- },
76
- }),
77
-
78
- validate: true,
79
-
80
- run: async (_state, _server, formData) => {
81
- // Simulate sending — replace with a real fetch() to your API
82
- await new Promise(resolve => setTimeout(resolve, 900))
83
- // Demo: email addresses containing "fail" trigger the error path
84
- if ((formData.get('email') || '').includes('fail')) {
85
- throw new Error('Unable to deliver message. Please try another address.')
86
- }
87
- },
88
-
89
- onSuccess: () => ({ status: 'success', errors: [] }),
90
-
91
- onError: (_state, err) => ({
92
- status: 'error',
93
- errors: err?.validation
94
- ? err.validation.map(e => {
95
- const field = e.path?.split('.').pop() || ''
96
- const label = { name: 'Name', email: 'Email', subject: 'Subject', message: 'Message' }[field] || field
97
- const msg = e.rule === 'required' ? `${label} is required`
98
- : e.rule === 'minLength' ? `${label} is too short`
99
- : e.rule === 'maxLength' ? `${label} is too long`
100
- : e.rule === 'format' ? `${label} must be a valid email address`
101
- : e.message
102
- return { field, message: msg }
103
- })
104
- : [{ message: err?.message || 'Something went wrong. Please try again.' }],
105
- }),
106
- },
107
- },
108
-
109
- view: (state, server) => {
110
- const { info } = server
111
- const loading = state.status === 'loading'
112
- const isSuccess = state.status === 'success'
113
-
114
- const fieldError = (field) => state.errors.find(e => e.field === field)?.message || ''
115
-
116
- const infoCol = stack({ gap: 'lg', content: `
117
- ${heading({ level: 2, text: 'Get in touch', size: 'xl' })}
118
- <p class="u-text-sm u-text-muted">Fill in the form and we&rsquo;ll get back to you within one business day.</p>
119
- ${stack({ gap: 'md', content: `
120
- ${infoDetail(iconMail({ size: 18 }), 'Email', info.email, 'mailto:' + info.email)}
121
- ${infoDetail(iconPhone({ size: 18 }), 'Phone', info.phone, 'tel:' + info.phone.replace(/\s/g, ''))}
122
- ${infoDetail(iconMapPin({ size: 18 }), 'Address', info.address, null)}
123
- ${infoDetail(iconClock({ size: 18 }), 'Hours', info.hours, null)}
124
- ` })}
125
- ` })
126
-
127
- const cardContent = isSuccess
128
- ? stack({ gap: 'md', align: 'center', content:
129
- iconCheckCircle({ size: 48, bg: 'circle', bgColor: 'success' }) +
130
- heading({ level: 2, text: 'Message sent!', size: 'xl' }) +
131
- `<p class="u-text-sm u-text-muted u-text-center">Thanks for reaching out. We&rsquo;ll be in touch within one business day.</p>` +
132
- button({ label: 'Send another message', variant: 'secondary', attrs: { 'data-event': 'reset' } })
133
- })
134
- : `<form data-action="submit" novalidate>
135
- <fieldset style="border:none;padding:0;margin:0" ${loading ? 'disabled' : ''}>
136
- <legend class="u-text-lg u-font-semibold u-mb-5">Send a message</legend>
137
- ${stack({ gap: 'md', content: `
138
- ${state.status === 'error' && state.errors.length > 0
139
- ? alert({ variant: 'error', content: state.errors[0]?.message || 'Please check the fields below.' })
140
- : ''}
141
- ${grid({ cols: 2, gap: 'sm', content:
142
- input({ name: 'name', label: 'Name', type: 'text', placeholder: 'Jane Smith', value: state.fields.name, required: true, error: fieldError('name') }) +
143
- input({ name: 'email', label: 'Email address', type: 'email', placeholder: 'jane@example.com', value: state.fields.email, required: true, error: fieldError('email') })
144
- })}
145
- ${input({ name: 'subject', label: 'Subject', placeholder: 'How can we help?', value: state.fields.subject, required: true, error: fieldError('subject') })}
146
- ${textarea({ name: 'message', label: 'Message', placeholder: "Tell us what's on your mind…", value: state.fields.message, rows: 5, required: true, error: fieldError('message') })}
147
- ${button({ label: loading ? 'Sending…' : 'Send message', type: 'submit', fullWidth: true, disabled: loading })}
148
- ` })}
149
- </fieldset>
150
- </form>`
151
-
152
- return `
153
- ${examplesNav('<span>✉ Contact</span>', '/contact')}
154
-
155
- <main id="main-content">
156
- ${section({ eyebrow: 'Contact', title: 'We\u2019d love to hear from you', level: 1, content:
157
- container({ size: 'lg', content:
158
- grid({ cols: 2, gap: 'lg', content: `
159
- <div>${infoCol}</div>
160
- <div>${card({ content: cardContent })}</div>
161
- ` })
162
- })
163
- })}
164
- </main>`
165
- },
166
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Pulse — Counter example
3
- *
4
- * Demonstrates:
5
- * - State and mutations
6
- * - Constraints (min/max enforced after every mutation)
7
- * - data-event binding
8
- * - Hydration for client interactivity
9
- *
10
- * Run: node examples/dev.server.js → http://localhost:3001/counter
11
- */
12
-
13
- import { button, badge, card, container, section, stack, cluster, progress, segmented } from '../src/ui/index.js'
14
- import { examplesNav } from './shared.js'
15
-
16
- export default {
17
- route: '/counter',
18
-
19
- meta: {
20
- title: 'Counter — Pulse',
21
- description: 'An interactive counter built with Pulse. Demonstrates mutations, constraints, and data-event binding.',
22
- styles: ['/pulse-ui.css'],
23
- theme: 'dark',
24
- },
25
-
26
- state: {
27
- count: 0,
28
- step: 1,
29
- },
30
-
31
- constraints: {
32
- count: { min: 0, max: 20 },
33
- step: { min: 1, max: 5 },
34
- },
35
-
36
- mutations: {
37
- increment: (state) => ({ count: state.count + state.step }),
38
- decrement: (state) => ({ count: state.count - state.step }),
39
- reset: () => ({ count: 0 }),
40
- setStep: (_state, e) => ({ step: parseInt(e.target.value, 10) || 1 }),
41
- },
42
-
43
- view: (state) => {
44
- const atMin = state.count <= 0
45
- const atMax = state.count >= 20
46
-
47
-
48
- return `
49
- ${examplesNav('<span>⚡ Counter</span>', '/counter')}
50
-
51
- <main id="main-content">
52
- ${section({ eyebrow: 'Mutations & Constraints', title: 'Counter', level: 1, align: 'center',
53
- subtitle: 'Constraints are enforced after every mutation — the count can never go below 0 or above 20.',
54
- content:
55
- container({ size: 'sm', content: `
56
-
57
- ${card({ content: stack({ gap: 'lg', align: 'center', content: `
58
-
59
- <div class="u-flex u-items-end u-justify-center u-gap-3" aria-live="polite" aria-atomic="true">
60
- <span class="u-text-7xl u-font-bold u-text-accent">${state.count}</span>
61
- <span class="u-text-3xl u-text-muted u-mb-2">/ 20</span>
62
- </div>
63
-
64
- ${progress({ value: state.count, max: 20, label: 'Counter progress' })}
65
-
66
- ${cluster({ justify: 'center', gap: 'sm', content:
67
- button({ label: '−', variant: 'secondary', disabled: atMin, attrs: { 'data-event': 'decrement', 'aria-label': 'Decrement' } }) +
68
- button({ label: 'Reset', variant: 'ghost', attrs: { 'data-event': 'reset' } }) +
69
- button({ label: '+', variant: 'primary', disabled: atMax, attrs: { 'data-event': 'increment', 'aria-label': 'Increment' } })
70
- })}
71
-
72
- ${stack({ gap: 'xs', align: 'center', content: `
73
- <p class="u-text-xs u-text-muted" id="step-label">Step size</p>
74
- ${segmented({
75
- name: 'step',
76
- value: String(state.step),
77
- event: 'change:setStep',
78
- options: [1, 2, 5].map(n => ({ value: n, label: String(n) })),
79
- size: 'md',
80
- })}
81
- ` })}
82
-
83
- ${cluster({ justify: 'center', gap: 'xs', content:
84
- badge({ label: atMin ? 'At minimum' : 'Min: 0', variant: atMin ? 'warning' : 'default' }) +
85
- badge({ label: atMax ? 'At maximum' : 'Max: 20', variant: atMax ? 'warning' : 'default' })
86
- })}
87
-
88
- ` }) })}
89
-
90
- `})
91
- })}
92
- </main>`
93
- },
94
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * Pulse — Examples dev server
3
- *
4
- * Serves all six standalone example apps.
5
- * Handles /src/, /examples/, and /public/ as static JS so dev hydration works.
6
- *
7
- * Run: node examples/dev.server.js
8
- *
9
- * http://localhost:3001/counter — mutations + constraints
10
- * http://localhost:3001/todos — CRUD + persist + filter
11
- * http://localhost:3001/contact — server data + validation + action
12
- * http://localhost:3001/quiz — multi-step state machine
13
- * http://localhost:3001/products — server data + search/filter/sort
14
- * http://localhost:3001/pricing — landing page components + billing toggle
15
- * http://localhost:3001/swish — editorial fashion branding + light theme
16
- * http://localhost:3001/bookworms — independent bookshop, Fraunces font, picsum covers
17
- * http://localhost:3001/retrowheels — vintage bicycle shop, slab-serif, booking form
18
- * http://localhost:3001/suzukibento — artist portfolio, dark brutalist, asymmetric grid
19
- * http://localhost:3001/andypixel — blog article, editorial light, newsletter action
20
- * http://localhost:3001/journey — app landing page, dark split-screen, sticky timeline
21
- */
22
-
23
- import fs from 'fs'
24
- import path from 'path'
25
- import { createServer } from '../src/server/index.js'
26
- import { themeScript } from './shared.js'
27
-
28
- const ROOT = path.resolve(import.meta.dirname, '..')
29
-
30
- const MIME = {
31
- '.js': 'application/javascript',
32
- '.css': 'text/css',
33
- '.json': 'application/json',
34
- '.html': 'text/html; charset=utf-8',
35
- }
36
-
37
- // /@pulse/runtime/* → src/runtime/*
38
- // /@pulse/ui/* → src/ui/*
39
- // Maps bare-specifier aliases used by the dev-mode bootstrap.
40
- const PULSE_PREFIX_MAP = {
41
- '/@pulse/runtime/': path.join(ROOT, 'src/runtime/'),
42
- '/@pulse/ui/': path.join(ROOT, 'src/ui/'),
43
- }
44
-
45
- // Serve /src/, /examples/, and /@pulse/ alias paths so the browser can import
46
- // spec and runtime files during dev hydration.
47
- function staticHandler(req, res) {
48
- const url = new URL(req.url, 'http://localhost')
49
- const pathname = url.pathname
50
-
51
- // /@pulse/* prefix aliases → src/
52
- for (const [prefix, dir] of Object.entries(PULSE_PREFIX_MAP)) {
53
- if (pathname.startsWith(prefix)) {
54
- const filePath = path.join(dir, pathname.slice(prefix.length))
55
- if (!filePath.startsWith(dir)) return
56
- let stat
57
- try { stat = fs.statSync(filePath) } catch { return }
58
- if (!stat.isFile()) return
59
- res.writeHead(200, { 'Content-Type': 'application/javascript' })
60
- fs.createReadStream(filePath).pipe(res)
61
- return false
62
- }
63
- }
64
-
65
- if (!pathname.startsWith('/examples/') && !pathname.startsWith('/src/')) return
66
-
67
- const filePath = path.join(ROOT, pathname)
68
- if (!filePath.startsWith(ROOT + path.sep)) return
69
-
70
- let stat
71
- try { stat = fs.statSync(filePath) } catch { return }
72
- if (!stat.isFile()) return
73
-
74
- const ext = path.extname(filePath)
75
- const mime = MIME[ext] || 'text/plain'
76
-
77
- res.writeHead(200, { 'Content-Type': mime })
78
- fs.createReadStream(filePath).pipe(res)
79
- return false
80
- }
81
-
82
- await createServer(
83
- [
84
- new URL('./counter.js', import.meta.url),
85
- new URL('./todos.js', import.meta.url),
86
- new URL('./contact.js', import.meta.url),
87
- new URL('./quiz.js', import.meta.url),
88
- new URL('./products.js', import.meta.url),
89
- new URL('./pricing.js', import.meta.url),
90
- new URL('./swish.js', import.meta.url),
91
- new URL('./bookworms.js', import.meta.url),
92
- new URL('./retrowheels.js', import.meta.url),
93
- new URL('./suzukibento.js', import.meta.url),
94
- new URL('./andypixel.js', import.meta.url),
95
- new URL('./journey.js', import.meta.url),
96
- ],
97
- {
98
- port: 3001,
99
- stream: true,
100
- staticDir: path.join(ROOT, 'public'),
101
- root: new URL('..', import.meta.url), // pulse2/ — specs served at /examples/*
102
- onRequest: staticHandler,
103
- extraBody: themeScript,
104
- csp: {
105
- 'style-src': ['https://fonts.googleapis.com'],
106
- 'font-src': ['https://fonts.gstatic.com'],
107
- 'img-src': ['https://images.unsplash.com', 'https://picsum.photos', 'https://fastly.picsum.photos'],
108
- },
109
- }
110
- )