@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,355 +0,0 @@
1
- /**
2
- * Pulse — Retro Wheels Co. example
3
- *
4
- * Demonstrates:
5
- * - Retro/vintage light-theme branding (1970s enamel-sign aesthetic)
6
- * - stat(), cta(), card(), badge() layout components
7
- * - Icon imports: iconMapPin, iconPhone, iconClock, iconSend, iconStar
8
- * - Async action with onStart / run / onSuccess / onError lifecycle
9
- * - State-driven form (idle → sending → sent) with disabled submit
10
- * - input() fields inside a card() for the booking enquiry
11
- * - System web fonts (Rockwell + Courier New) — zero extra HTTP requests
12
- * - Full-bleed hero with background image + gradient overlay
13
- * - Stat strip with inverted navy background
14
- * - Reviews rendered with card() (quote + name/role + star rating)
15
- * - Opening hours list with Courier monospace styling
16
- *
17
- * Run: node examples/dev.server.js → http://localhost:3001/retrowheels
18
- */
19
-
20
- import {
21
- nav, footer, button, input, stat, cta, card, badge,
22
- } from '../../../../src/ui/index.js'
23
- import { iconMapPin, iconPhone, iconClock, iconSend, iconStar } from '../../../../src/ui/icons.js'
24
- import { asset } from '../../lib/layout.js'
25
-
26
- // ---------------------------------------------------------------------------
27
- // Static data
28
- // ---------------------------------------------------------------------------
29
-
30
- const SERVICES = [
31
- {
32
- num: 'No. 01',
33
- title: 'Restored Vintage Bicycles',
34
- body: 'Lovingly rebuilt Raleighs, Peugeots, and Bianchis from the workshop. Each one inspected, polished, and ready for the road.',
35
- price: 'From £325',
36
- },
37
- {
38
- num: 'No. 02',
39
- title: 'Hand-Built City Cruisers',
40
- body: 'Lugged steel frames, leather saddles, and brass bells. Built to your measurements over four to six weeks.',
41
- price: 'From £1,100',
42
- },
43
- {
44
- num: 'No. 03',
45
- title: 'Full-Service Workshop',
46
- body: 'Brakes, gears, wheels, headsets — whatever the trouble. We work on every era from penny-farthings to ten-speeds.',
47
- price: 'From £45',
48
- },
49
- {
50
- num: 'No. 04',
51
- title: 'Steel-Frame Tune-Ups',
52
- body: 'Bottom bracket overhaul, hub repack, true the wheels, polish the chrome. Your old steed will ride like 1976 again.',
53
- price: '£95',
54
- },
55
- {
56
- num: 'No. 05',
57
- title: 'Saddles & Leather Goods',
58
- body: 'Brooks saddles, twine-wrapped bar tape, canvas tool rolls, and waxed cotton panniers. Stocked and ready to fit.',
59
- price: 'From £18',
60
- },
61
- {
62
- num: 'No. 06',
63
- title: 'Weekend Group Rides',
64
- body: 'Every Saturday at 9 a.m. — twenty miles, a cafe stop, and home by lunch. All paces, all bikes, all welcome.',
65
- price: 'Free · BYO bicycle',
66
- },
67
- ]
68
-
69
- const HOURS = [
70
- { day: 'Tuesday', time: '9 a.m. — 6 p.m.' },
71
- { day: 'Wednesday', time: '9 a.m. — 6 p.m.' },
72
- { day: 'Thursday', time: '9 a.m. — 7 p.m.' },
73
- { day: 'Friday', time: '9 a.m. — 6 p.m.' },
74
- { day: 'Saturday', time: '9 a.m. — 5 p.m.' },
75
- { day: 'Sunday', time: '10 a.m. — 4 p.m.' },
76
- { day: 'Monday', time: 'Closed', closed: true },
77
- ]
78
-
79
- const REVIEWS = [
80
- {
81
- quote: 'They rebuilt my grandfather\u2019s 1972 Raleigh Twenty as if it had just left the factory. I cried a little. Worth every penny.',
82
- name: 'Eleanor Bishop',
83
- role: 'Brought back from the loft',
84
- rating: 5,
85
- },
86
- {
87
- quote: 'The custom cruiser I commissioned is the loveliest object I own. Hand-pinstriped name on the down tube. A proper bicycle.',
88
- name: 'Marcus O\u2019Halloran',
89
- role: 'Custom build, Spring 2026',
90
- rating: 5,
91
- },
92
- {
93
- quote: 'Saturday ride is the best thing in my week. Twenty miles, a cafe stop, and not a single person in lycra. Bliss.',
94
- name: 'Priya Ramachandran',
95
- role: 'Weekend rider since 2024',
96
- rating: 5,
97
- },
98
- ]
99
-
100
- // Logo uses system web fonts — no external request needed
101
- const LOGO = `<span class="rw-logo-text">Retro Wheels Co.</span>`
102
-
103
- // ---------------------------------------------------------------------------
104
- // Helpers
105
- // ---------------------------------------------------------------------------
106
-
107
- const renderService = (s) => `
108
- <article class="rw-service">
109
- <p class="rw-service-num">${s.num}</p>
110
- <h3>${s.title}</h3>
111
- <p>${s.body}</p>
112
- <p class="rw-service-price">${s.price}</p>
113
- </article>
114
- `
115
-
116
- const renderHourRow = (h) => `
117
- <li class="rw-hours-row">
118
- <span>${h.day}</span>
119
- <span class="${h.closed ? 'rw-closed' : ''}">${h.time}</span>
120
- </li>
121
- `
122
-
123
- // Review card — replaces the removed testimonial() component with card()
124
- const renderReview = (r) => card({
125
- content: `
126
- <div class="rw-review-stars" aria-hidden="true">${iconStar({ size: 14 }).repeat(r.rating || 0)}</div>
127
- <p class="rw-review-quote">&ldquo;${r.quote}&rdquo;</p>
128
- `,
129
- footer: `
130
- <p class="rw-review-name">${r.name}</p>
131
- <p class="rw-review-role">${r.role}</p>
132
- `,
133
- class: 'rw-review',
134
- })
135
-
136
- // ---------------------------------------------------------------------------
137
- // Spec
138
- // ---------------------------------------------------------------------------
139
-
140
- export default {
141
- route: '/templates/retrowheels',
142
-
143
- meta: {
144
- title: 'Retro Wheels Co. — Restored vintage bicycles, est. 1976',
145
- description: 'A neighbourhood bicycle shop specialising in restored vintage bikes, hand-built city cruisers, and old-school repairs. Established 1976.',
146
- theme: 'light',
147
- styles: ['/pulse-ui.css', '/retrowheels.css'],
148
- },
149
-
150
- state: {
151
- status: 'idle', // idle | sending | sent
152
- },
153
-
154
- view: (state) => `
155
- <a href="#main-content" class="rw-skip-link">Skip to main content</a>
156
-
157
- ${nav({
158
- logo: LOGO,
159
- logoHref: '/retrowheels',
160
- sticky: true,
161
- links: [
162
- { label: 'Workshop', href: '#workshop' },
163
- { label: 'Services', href: '#services' },
164
- { label: 'Reviews', href: '#reviews' },
165
- { label: 'Visit', href: '#visit' },
166
- ],
167
- action: button({ label: 'Book a service', href: '#book', size: 'sm' }),
168
- })}
169
-
170
- <main id="main-content">
171
- <section class="rw-hero" aria-labelledby="hero-title">
172
- <div class="rw-hero-inner">
173
- <p class="rw-hero-eyebrow">Established 1976</p>
174
- <h1 id="hero-title" class="rw-hero-title">
175
- Steel frames,<br>
176
- <em>old roads,</em><br>
177
- new adventures.
178
- </h1>
179
- <p class="rw-hero-sub">
180
- A neighbourhood bicycle shop on Saddler Lane &mdash; restoring vintage bikes,
181
- hand-building city cruisers, and keeping every steel-framed bicycle in town
182
- rolling sweetly since the Carter administration.
183
- </p>
184
- <div class="rw-hero-actions">
185
- ${button({ label: 'Browse the workshop', href: '#services', variant: 'primary', size: 'lg' })}
186
- ${button({ label: 'Saturday ride', href: '#ride', variant: 'secondary', size: 'lg' })}
187
- </div>
188
- </div>
189
- <p class="rw-hero-image-credit">Photo: Unsplash</p>
190
- </section>
191
-
192
- <section class="rw-stat-strip" aria-label="Shop facts">
193
- <div class="rw-stat-strip-inner">
194
- ${stat({ label: 'Established', value: '1976', center: true })}
195
- ${stat({ label: 'Bikes restored', value: '2,400+', center: true })}
196
- ${stat({ label: 'Saturday riders', value: '60/wk', center: true })}
197
- ${stat({ label: 'Carbon fibre', value: 'None', center: true })}
198
- </div>
199
- </section>
200
-
201
- <section id="workshop" class="rw-editorial">
202
- <p class="rw-eyebrow">The Workshop</p>
203
- <h2 class="rw-section-title u-mt-3">A bicycle is a promise that you&rsquo;ll go outside.</h2>
204
- <hr class="rw-rule">
205
- <p>
206
- We opened in the autumn of 1976 in a former ironmonger&rsquo;s on Saddler Lane.
207
- Fifty years on, the floorboards still creak, the tea is still strong, and the
208
- workbench is still covered in the same small jars of cotter pins and brass
209
- ferrules. What we build today is the same thing we built then: bicycles you
210
- could leave to your grandchildren.
211
- </p>
212
- <p>
213
- We don&rsquo;t do carbon. We don&rsquo;t do electronic shifting. We do lugged steel,
214
- hand-built wheels, three-speed hubs, and a fair few French derailleurs that
215
- require patience and the correct sort of grease. If your bicycle has a name,
216
- we&rsquo;d like to meet it.
217
- </p>
218
- ${badge({ label: 'Repairs by appointment · Workshop open Tues — Sun', variant: 'default' })}
219
- </section>
220
-
221
- <section id="services" class="rw-section rw-section--alt">
222
- <div class="rw-section-inner">
223
- <p class="rw-eyebrow">Services &amp; Goods</p>
224
- <h2 class="rw-section-title">Six things we&rsquo;re rather good at.</h2>
225
- <p class="rw-section-deck">
226
- Prices are honest, the work is guaranteed for a year, and every bicycle that
227
- leaves the workshop has been ridden round the block by someone who actually
228
- cares.
229
- </p>
230
- <div class="rw-service-grid">
231
- ${SERVICES.map(renderService).join('')}
232
- </div>
233
- </div>
234
- </section>
235
-
236
- <section id="reviews" class="rw-section">
237
- <div class="rw-section-inner">
238
- <p class="rw-eyebrow">From the Logbook</p>
239
- <h2 class="rw-section-title">What folks have been saying.</h2>
240
- <p class="rw-section-deck">Reviews collected from customers, riders, and the occasional postcard pinned to the workshop wall.</p>
241
- <div class="rw-service-grid">
242
- ${REVIEWS.map(renderReview).join('')}
243
- </div>
244
- </div>
245
- </section>
246
-
247
- <section id="visit" class="rw-section rw-section--alt">
248
- <div class="rw-section-inner">
249
- <p class="rw-eyebrow">Visit the Shop</p>
250
- <h2 class="rw-section-title">Find us, ring us, or write.</h2>
251
- <hr class="rw-rule">
252
- <div class="rw-visit-grid">
253
- <div>
254
- <h3 class="rw-hours-heading">
255
- ${iconClock({ size: 18 })} Opening hours
256
- </h3>
257
- <ul class="rw-hours" aria-label="Opening hours">
258
- ${HOURS.map(renderHourRow).join('')}
259
- </ul>
260
- <div class="rw-address">
261
- <p><strong>${iconMapPin({ size: 16 })} 14 Saddler Lane</strong><br>
262
- Trumpington, Cambridge<br>
263
- CB2 9PQ</p>
264
- <p class="u-mt-3">${iconPhone({ size: 16 })} 01223 555 0176<br>
265
- hello@retrowheels.co</p>
266
- </div>
267
- </div>
268
-
269
- <div id="book">
270
- ${card({
271
- title: 'Book a service',
272
- level: 3,
273
- content: state.status === 'sent'
274
- ? `<p>Thanks &mdash; we&rsquo;ll be in touch within a working day. Tea&rsquo;s on when you arrive.</p>`
275
- : `<form data-action="bookService" novalidate>
276
- <p class="u-mb-4 u-text-muted">Drop us a line about your bicycle. We&rsquo;ll reply with a slot and an estimate.</p>
277
- ${input({ name: 'name', label: 'Your name', required: true })}
278
- ${input({ name: 'email', label: 'Email', type: 'email', required: true, class: 'u-mt-3' })}
279
- ${input({ name: 'bike', label: 'Bicycle (make, year)', placeholder: 'e.g. 1972 Raleigh Twenty', class: 'u-mt-3' })}
280
- <div class="u-mt-4">
281
- ${button({
282
- label: state.status === 'sending' ? 'Sending\u2026' : 'Send enquiry',
283
- type: 'submit',
284
- icon: iconSend({ size: 16 }),
285
- attrs: state.status === 'sending' ? { 'aria-busy': 'true', disabled: '' } : {},
286
- })}
287
- </div>
288
- </form>`,
289
- })}
290
- </div>
291
- </div>
292
- </div>
293
- </section>
294
-
295
- <section id="ride" class="rw-ride-banner">
296
- <p class="rw-eyebrow rw-eyebrow--on-orange">Every Saturday, 9 a.m. sharp</p>
297
- <h2>${iconStar({ size: 28 })} Join the Saturday ride.</h2>
298
- <p>Twenty miles, one cafe stop, no lycra required. Meet at the shop. Helmets sensible, bicycle of any vintage welcome.</p>
299
- ${button({ label: 'Add me to the list', href: '#book', variant: 'secondary', size: 'lg' })}
300
- </section>
301
-
302
- <section class="rw-section">
303
- <div class="rw-section-inner">
304
- ${cta({
305
- eyebrow: 'Workshop & Showroom',
306
- title: 'Bring us a bicycle. Leave with a story.',
307
- subtitle: 'Walk-ins welcome during opening hours. The kettle is always on.',
308
- actions: button({ label: 'Find the shop', href: '#visit', variant: 'primary', size: 'lg' }),
309
- })}
310
- </div>
311
- </section>
312
- </main>
313
-
314
- ${footer({
315
- logo: LOGO,
316
- links: [
317
- { label: 'Workshop', href: '#workshop' },
318
- { label: 'Services', href: '#services' },
319
- { label: 'Visit', href: '#visit' },
320
- { label: 'Saturday ride', href: '#ride' },
321
- ],
322
- legal: '\u00a9 1976 \u2014 2026 Retro Wheels Co. \u00b7 14 Saddler Lane, Cambridge \u00b7 A bicycle is forever.',
323
- })}
324
- <script src="${asset('/pulse-ui.js')}" defer></script>
325
- `,
326
-
327
- actions: {
328
- bookService: {
329
- onStart: () => ({ status: 'sending' }),
330
- run: async (_state, _server, formData) => {
331
- const name = (formData.get('name') || '').trim()
332
- const email = (formData.get('email') || '').trim()
333
- if (!name) throw new Error('Please tell us your name.')
334
- if (!email) throw new Error('Please leave an email so we can reply.')
335
- await new Promise(r => setTimeout(r, 350))
336
- return { name }
337
- },
338
- onSuccess: () => ({
339
- status: 'sent',
340
- _toast: { message: 'Enquiry sent \u2014 we\u2019ll be in touch shortly.', variant: 'success' },
341
- }),
342
- onError: (_state, err) => ({
343
- status: 'idle',
344
- _toast: { message: err.message || 'Something went wrong. Try again?', variant: 'error' },
345
- }),
346
- },
347
- },
348
-
349
- onViewError: () => `
350
- <main id="main-content" class="rw-editorial">
351
- <h1>Spanner in the works</h1>
352
- <p>Something went sideways. <a href="/retrowheels">Reload the page</a> or give us a ring on 01223 555 0176.</p>
353
- </main>
354
- `,
355
- }
@@ -1,268 +0,0 @@
1
- /**
2
- * Template: Shop — Product Page
3
- *
4
- * A clean, interactive e-commerce product page.
5
- * Light theme, blue accent. Full add-to-cart flow with a basket modal,
6
- * quantity controls, spec grid, and key stats.
7
- *
8
- * Vibe: minimal Theme: light Palette: blue / white
9
- *
10
- * Demonstrates:
11
- * - nav() with a custom cart icon trigger and dialog-open
12
- * - modal() basket with a live cart table
13
- * - Quantity stepper with min/max constraints
14
- * - heading(), badge(), stat(), grid()
15
- * - Section eyebrow + rule rendered as raw HTML (replaces removed sectionLabel())
16
- * - Minimal end-of-page metadata footer rendered as raw HTML (replaces removed colophon())
17
- * - _toast on mutation success
18
- * - Light theme with custom accent override
19
- */
20
-
21
- import {
22
- nav, heading, badge, stat, grid,
23
- button, modal, empty,
24
- iconPlus, iconMinus, iconShoppingCart, iconCheck, iconPackage,
25
- } from '../../../../src/ui/index.js'
26
- import { asset } from '../../lib/layout.js'
27
-
28
- const SPECS = [
29
- { label: 'Display', value: '15.6" Retina, 2560×1664' },
30
- { label: 'Processor', value: 'M3 Pro, 12-core CPU' },
31
- { label: 'Memory', value: '32GB Unified Memory' },
32
- { label: 'Storage', value: '1TB SSD' },
33
- { label: 'Battery', value: 'Up to 18 hours' },
34
- { label: 'Chassis', value: 'Space Grey Aluminium' },
35
- { label: 'Ports', value: '3× USB-C, HDMI, SD card' },
36
- { label: 'Weight', value: '1.86 kg' },
37
- ]
38
-
39
- const UNIT_PRICE = 1899
40
-
41
- export const formatPrice = (p) => `£${p.toLocaleString('en-GB')}`
42
- export const cartTotal = (items) => items.reduce((sum, i) => sum + i.qty * i.unitPrice, 0)
43
- export const cartCount = (items) => items.reduce((sum, i) => sum + i.qty, 0)
44
-
45
- const cartRows = (items) => items.map(({ name, qty, unitPrice }) => [
46
- name,
47
- String(qty),
48
- formatPrice(unitPrice),
49
- formatPrice(qty * unitPrice),
50
- ])
51
-
52
- // Section eyebrow + heading + rule — replaces the removed sectionLabel() component
53
- const sectionLabel = ({ eyebrow, heading: headingText }) => `
54
- <div class="shop-section-label">
55
- <p class="shop-section-label-eyebrow">${eyebrow}</p>
56
- ${heading({ text: headingText, level: 2, class: 'shop-section-label-heading' })}
57
- <hr class="shop-section-label-rule" aria-hidden="true">
58
- </div>
59
- `
60
-
61
- export default {
62
- route: '/templates/shop',
63
- meta: {
64
- title: 'Shop — Product Page Template · Pulse',
65
- description: 'Interactive e-commerce product page with cart modal, quantity controls, and spec grid. Built with Pulse.',
66
- styles: [
67
- asset('/pulse-ui.css'),
68
- asset('/shop.css'),
69
- ],
70
- theme: 'light',
71
- },
72
- state: {
73
- qty: 1,
74
- cartItems: [],
75
- added: false,
76
- },
77
- mutations: {
78
- increment: (state) => ({
79
- qty: Math.min(state.qty + 1, 10),
80
- added: false,
81
- }),
82
- decrement: (state) => ({
83
- qty: Math.max(state.qty - 1, 1),
84
- added: false,
85
- }),
86
- addToCart: (state) => {
87
- const existing = state.cartItems.find(i => i.name === 'DevBook Pro 15')
88
- const cartItems = existing
89
- ? state.cartItems.map(i =>
90
- i.name === 'DevBook Pro 15'
91
- ? { ...i, qty: i.qty + state.qty }
92
- : i
93
- )
94
- : [...state.cartItems, { name: 'DevBook Pro 15', qty: state.qty, unitPrice: UNIT_PRICE }]
95
- return {
96
- cartItems,
97
- added: true,
98
- _toast: { message: `${state.qty} × DevBook Pro 15 added to cart`, variant: 'success' },
99
- }
100
- },
101
- },
102
- view: (state) => {
103
- const count = cartCount(state.cartItems)
104
- const total = cartTotal(state.cartItems)
105
-
106
- return `
107
- ${nav({
108
- logo: `${iconPackage({ size: 18 })} Tech Stuff Inc.`,
109
- logoHref: '/',
110
- sticky: true,
111
- links: [
112
- { label: 'Laptops', href: '/templates/shop' },
113
- { label: 'Accessories', href: '#' },
114
- { label: 'Support', href: '#' },
115
- ],
116
- action: `
117
- <button
118
- type="button"
119
- class="cart-trigger"
120
- data-dialog-open="cart"
121
- aria-label="${count > 0 ? `Open basket, ${count} item${count !== 1 ? 's' : ''}` : 'Open basket'}"
122
- >
123
- ${iconShoppingCart({ size: 18 })}
124
- ${count > 0 ? `<span class="cart-bubble" aria-hidden="true">${count}</span>` : ''}
125
- </button>
126
- `,
127
- })}
128
-
129
- <script src="${asset('/pulse-ui.js')}" defer></script>
130
-
131
- <main id="main-content">
132
-
133
- <div class="page-body product-display">
134
- ${heading({ text: 'DevBook Pro 15', level: 1, class: 'product-display-title' })}
135
- <p class="product-tagline">Premium developer laptop for people who build things.</p>
136
- <div class="product-meta-row">
137
- ${badge({ label: 'In Stock', variant: 'success' })}
138
- ${badge({ label: 'Ships in 24 hours', variant: 'info' })}
139
- </div>
140
- </div>
141
-
142
- ${modal({
143
- id: 'cart',
144
- title: 'Your basket',
145
- level: 2,
146
- size: 'lg',
147
- content: state.cartItems.length === 0
148
- ? empty({ title: 'Your basket is empty', description: 'Add a product to get started.' })
149
- : `
150
- <table class="ui-table cart-table">
151
- <thead>
152
- <tr>
153
- <th scope="col">Product</th>
154
- <th scope="col" class="cart-col-num">Qty</th>
155
- <th scope="col" class="cart-col-num">Unit price</th>
156
- <th scope="col" class="cart-col-num">Subtotal</th>
157
- </tr>
158
- </thead>
159
- <tbody>
160
- ${cartRows(state.cartItems).map(([name, qty, unit, sub]) => `
161
- <tr>
162
- <td>${name}</td>
163
- <td class="cart-col-num">${qty}</td>
164
- <td class="cart-col-num">${unit}</td>
165
- <td class="cart-col-num">${sub}</td>
166
- </tr>
167
- `).join('')}
168
- </tbody>
169
- <tfoot>
170
- <tr class="cart-total-row">
171
- <td colspan="3"><strong>Total</strong></td>
172
- <td class="cart-col-num"><strong>${formatPrice(total)}</strong></td>
173
- </tr>
174
- </tfoot>
175
- </table>
176
- `,
177
- footer: `
178
- ${button({ label: 'Continue shopping', variant: 'secondary', attrs: { 'data-dialog-close': '' } })}
179
- ${button({ label: 'Checkout', variant: 'primary', disabled: state.cartItems.length === 0 })}
180
- `,
181
- })}
182
-
183
- <img
184
- class="product-photo"
185
- src="/images/shop/devbook-pro.jpg"
186
- alt="DevBook Pro 15 open on a clean white desk showing a code editor, with headphones, a coffee mug, and a plant nearby"
187
- width="1366"
188
- height="912"
189
- />
190
-
191
- <div class="page-body product-purchase">
192
- <p class="product-price">£1,899</p>
193
-
194
- <div class="qty-control">
195
- ${button({
196
- label: 'Decrease quantity',
197
- variant: 'secondary',
198
- size: 'sm',
199
- disabled: state.qty <= 1,
200
- icon: iconMinus({ size: 14 }),
201
- class: 'qty-btn',
202
- attrs: { 'data-event': 'decrement', 'aria-label': 'Decrease quantity' },
203
- })}
204
- <span class="qty-value" aria-live="polite">${state.qty}</span>
205
- ${button({
206
- label: 'Increase quantity',
207
- variant: 'secondary',
208
- size: 'sm',
209
- disabled: state.qty >= 10,
210
- icon: iconPlus({ size: 14 }),
211
- class: 'qty-btn',
212
- attrs: { 'data-event': 'increment', 'aria-label': 'Increase quantity' },
213
- })}
214
- </div>
215
-
216
- <div class="cart-actions">
217
- ${button({
218
- label: state.added ? 'Added to cart' : 'Add to cart',
219
- variant: 'primary',
220
- size: 'lg',
221
- icon: state.added ? iconCheck({ size: 16 }) : iconShoppingCart({ size: 16 }),
222
- attrs: { 'data-event': 'addToCart' },
223
- })}
224
- </div>
225
- ${state.added ? `<p class="added-confirm">${state.cartItems.find(i => i.name === 'DevBook Pro 15')?.qty ?? 0} item${(state.cartItems.find(i => i.name === 'DevBook Pro 15')?.qty ?? 0) !== 1 ? 's' : ''} in your basket.</p>` : ''}
226
- </div>
227
-
228
- <div class="page-body product-specs">
229
- ${sectionLabel({ eyebrow: 'Under the hood', heading: 'Full specifications' })}
230
- <div class="spec-grid">
231
- ${SPECS.map(({ label, value }) => `
232
- <div class="spec-item">
233
- <span class="spec-label">${label}</span>
234
- <span class="spec-value">${value}</span>
235
- </div>
236
- `).join('')}
237
- </div>
238
- </div>
239
-
240
- <div class="page-body product-stats">
241
- ${sectionLabel({ eyebrow: 'At a glance', heading: 'Key numbers' })}
242
- <div class="product-stats-grid">
243
- ${grid({ cols: 3, content:
244
- stat({ label: 'Battery life', value: '18 hrs', trend: 'up' }) +
245
- stat({ label: 'Unified memory', value: '32 GB' }) +
246
- stat({ label: 'Delivery', value: '24 hrs', trend: 'up' })
247
- })}
248
- </div>
249
- </div>
250
-
251
- </main>
252
-
253
- <footer class="shop-colophon">
254
- <div class="shop-colophon-inner">
255
- <p>© 2026 Tech Stuff Inc. · <a href="#">Terms</a> · <a href="#">Privacy</a></p>
256
- </div>
257
- </footer>
258
- `
259
- },
260
-
261
- onViewError: () => `
262
- <main id="main-content">
263
- <div class="view-error">
264
- <p>Something went wrong loading this page. <a href="/templates/shop">Reload</a></p>
265
- </div>
266
- </main>
267
- `,
268
- }