@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,353 +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
-
25
- // ---------------------------------------------------------------------------
26
- // Static data
27
- // ---------------------------------------------------------------------------
28
-
29
- const SERVICES = [
30
- {
31
- num: 'No. 01',
32
- title: 'Restored Vintage Bicycles',
33
- body: 'Lovingly rebuilt Raleighs, Peugeots, and Bianchis from the workshop. Each one inspected, polished, and ready for the road.',
34
- price: 'From £325',
35
- },
36
- {
37
- num: 'No. 02',
38
- title: 'Hand-Built City Cruisers',
39
- body: 'Lugged steel frames, leather saddles, and brass bells. Built to your measurements over four to six weeks.',
40
- price: 'From £1,100',
41
- },
42
- {
43
- num: 'No. 03',
44
- title: 'Full-Service Workshop',
45
- body: 'Brakes, gears, wheels, headsets — whatever the trouble. We work on every era from penny-farthings to ten-speeds.',
46
- price: 'From £45',
47
- },
48
- {
49
- num: 'No. 04',
50
- title: 'Steel-Frame Tune-Ups',
51
- body: 'Bottom bracket overhaul, hub repack, true the wheels, polish the chrome. Your old steed will ride like 1976 again.',
52
- price: '£95',
53
- },
54
- {
55
- num: 'No. 05',
56
- title: 'Saddles & Leather Goods',
57
- body: 'Brooks saddles, twine-wrapped bar tape, canvas tool rolls, and waxed cotton panniers. Stocked and ready to fit.',
58
- price: 'From £18',
59
- },
60
- {
61
- num: 'No. 06',
62
- title: 'Weekend Group Rides',
63
- body: 'Every Saturday at 9 a.m. — twenty miles, a cafe stop, and home by lunch. All paces, all bikes, all welcome.',
64
- price: 'Free · BYO bicycle',
65
- },
66
- ]
67
-
68
- const HOURS = [
69
- { day: 'Tuesday', time: '9 a.m. — 6 p.m.' },
70
- { day: 'Wednesday', time: '9 a.m. — 6 p.m.' },
71
- { day: 'Thursday', time: '9 a.m. — 7 p.m.' },
72
- { day: 'Friday', time: '9 a.m. — 6 p.m.' },
73
- { day: 'Saturday', time: '9 a.m. — 5 p.m.' },
74
- { day: 'Sunday', time: '10 a.m. — 4 p.m.' },
75
- { day: 'Monday', time: 'Closed', closed: true },
76
- ]
77
-
78
- const REVIEWS = [
79
- {
80
- quote: 'They rebuilt my grandfather\u2019s 1972 Raleigh Twenty as if it had just left the factory. I cried a little. Worth every penny.',
81
- name: 'Eleanor Bishop',
82
- role: 'Brought back from the loft',
83
- rating: 5,
84
- },
85
- {
86
- quote: 'The custom cruiser I commissioned is the loveliest object I own. Hand-pinstriped name on the down tube. A proper bicycle.',
87
- name: 'Marcus O\u2019Halloran',
88
- role: 'Custom build, Spring 2026',
89
- rating: 5,
90
- },
91
- {
92
- quote: 'Saturday ride is the best thing in my week. Twenty miles, a cafe stop, and not a single person in lycra. Bliss.',
93
- name: 'Priya Ramachandran',
94
- role: 'Weekend rider since 2024',
95
- rating: 5,
96
- },
97
- ]
98
-
99
- // Logo uses system web fonts — no external request needed
100
- const LOGO = `<span class="rw-logo-text">Retro Wheels Co.</span>`
101
-
102
- // ---------------------------------------------------------------------------
103
- // Helpers
104
- // ---------------------------------------------------------------------------
105
-
106
- const renderService = (s) => `
107
- <article class="rw-service">
108
- <p class="rw-service-num">${s.num}</p>
109
- <h3>${s.title}</h3>
110
- <p>${s.body}</p>
111
- <p class="rw-service-price">${s.price}</p>
112
- </article>
113
- `
114
-
115
- const renderHourRow = (h) => `
116
- <li class="rw-hours-row">
117
- <span>${h.day}</span>
118
- <span class="${h.closed ? 'rw-closed' : ''}">${h.time}</span>
119
- </li>
120
- `
121
-
122
- // Review card — replaces the removed testimonial() component with card()
123
- const renderReview = (r) => card({
124
- content: `
125
- <div class="rw-review-stars" aria-hidden="true">${iconStar({ size: 14 }).repeat(r.rating || 0)}</div>
126
- <p class="rw-review-quote">&ldquo;${r.quote}&rdquo;</p>
127
- `,
128
- footer: `
129
- <p class="rw-review-name">${r.name}</p>
130
- <p class="rw-review-role">${r.role}</p>
131
- `,
132
- class: 'rw-review',
133
- })
134
-
135
- // ---------------------------------------------------------------------------
136
- // Spec
137
- // ---------------------------------------------------------------------------
138
-
139
- export default {
140
- route: '/retrowheels',
141
-
142
- meta: {
143
- title: 'Retro Wheels Co. — Restored vintage bicycles, est. 1976',
144
- description: 'A neighbourhood bicycle shop specialising in restored vintage bikes, hand-built city cruisers, and old-school repairs. Established 1976.',
145
- theme: 'light',
146
- styles: ['/pulse-ui.css', '/examples/retrowheels.css'],
147
- },
148
-
149
- state: {
150
- status: 'idle', // idle | sending | sent
151
- },
152
-
153
- view: (state) => `
154
- <a href="#main-content" class="rw-skip-link">Skip to main content</a>
155
-
156
- ${nav({
157
- logo: LOGO,
158
- logoHref: '/retrowheels',
159
- sticky: true,
160
- links: [
161
- { label: 'Workshop', href: '#workshop' },
162
- { label: 'Services', href: '#services' },
163
- { label: 'Reviews', href: '#reviews' },
164
- { label: 'Visit', href: '#visit' },
165
- ],
166
- action: button({ label: 'Book a service', href: '#book', size: 'sm' }),
167
- })}
168
-
169
- <main id="main-content">
170
- <section class="rw-hero" aria-labelledby="hero-title">
171
- <div class="rw-hero-inner">
172
- <p class="rw-hero-eyebrow">Established 1976</p>
173
- <h1 id="hero-title" class="rw-hero-title">
174
- Steel frames,<br>
175
- <em>old roads,</em><br>
176
- new adventures.
177
- </h1>
178
- <p class="rw-hero-sub">
179
- A neighbourhood bicycle shop on Saddler Lane &mdash; restoring vintage bikes,
180
- hand-building city cruisers, and keeping every steel-framed bicycle in town
181
- rolling sweetly since the Carter administration.
182
- </p>
183
- <div class="rw-hero-actions">
184
- ${button({ label: 'Browse the workshop', href: '#services', variant: 'primary', size: 'lg' })}
185
- ${button({ label: 'Saturday ride', href: '#ride', variant: 'secondary', size: 'lg' })}
186
- </div>
187
- </div>
188
- <p class="rw-hero-image-credit">Photo: Unsplash</p>
189
- </section>
190
-
191
- <section class="rw-stat-strip" aria-label="Shop facts">
192
- <div class="rw-stat-strip-inner">
193
- ${stat({ label: 'Established', value: '1976', center: true })}
194
- ${stat({ label: 'Bikes restored', value: '2,400+', center: true })}
195
- ${stat({ label: 'Saturday riders', value: '60/wk', center: true })}
196
- ${stat({ label: 'Carbon fibre', value: 'None', center: true })}
197
- </div>
198
- </section>
199
-
200
- <section id="workshop" class="rw-editorial">
201
- <p class="rw-eyebrow">The Workshop</p>
202
- <h2 class="rw-section-title u-mt-3">A bicycle is a promise that you&rsquo;ll go outside.</h2>
203
- <hr class="rw-rule">
204
- <p>
205
- We opened in the autumn of 1976 in a former ironmonger&rsquo;s on Saddler Lane.
206
- Fifty years on, the floorboards still creak, the tea is still strong, and the
207
- workbench is still covered in the same small jars of cotter pins and brass
208
- ferrules. What we build today is the same thing we built then: bicycles you
209
- could leave to your grandchildren.
210
- </p>
211
- <p>
212
- We don&rsquo;t do carbon. We don&rsquo;t do electronic shifting. We do lugged steel,
213
- hand-built wheels, three-speed hubs, and a fair few French derailleurs that
214
- require patience and the correct sort of grease. If your bicycle has a name,
215
- we&rsquo;d like to meet it.
216
- </p>
217
- ${badge({ label: 'Repairs by appointment · Workshop open Tues — Sun', variant: 'default' })}
218
- </section>
219
-
220
- <section id="services" class="rw-section rw-section--alt">
221
- <div class="rw-section-inner">
222
- <p class="rw-eyebrow">Services &amp; Goods</p>
223
- <h2 class="rw-section-title">Six things we&rsquo;re rather good at.</h2>
224
- <p class="rw-section-deck">
225
- Prices are honest, the work is guaranteed for a year, and every bicycle that
226
- leaves the workshop has been ridden round the block by someone who actually
227
- cares.
228
- </p>
229
- <div class="rw-service-grid">
230
- ${SERVICES.map(renderService).join('')}
231
- </div>
232
- </div>
233
- </section>
234
-
235
- <section id="reviews" class="rw-section">
236
- <div class="rw-section-inner">
237
- <p class="rw-eyebrow">From the Logbook</p>
238
- <h2 class="rw-section-title">What folks have been saying.</h2>
239
- <p class="rw-section-deck">Reviews collected from customers, riders, and the occasional postcard pinned to the workshop wall.</p>
240
- <div class="rw-service-grid">
241
- ${REVIEWS.map(renderReview).join('')}
242
- </div>
243
- </div>
244
- </section>
245
-
246
- <section id="visit" class="rw-section rw-section--alt">
247
- <div class="rw-section-inner">
248
- <p class="rw-eyebrow">Visit the Shop</p>
249
- <h2 class="rw-section-title">Find us, ring us, or write.</h2>
250
- <hr class="rw-rule">
251
- <div class="rw-visit-grid">
252
- <div>
253
- <h3 class="rw-hours-heading">
254
- ${iconClock({ size: 18 })} Opening hours
255
- </h3>
256
- <ul class="rw-hours" aria-label="Opening hours">
257
- ${HOURS.map(renderHourRow).join('')}
258
- </ul>
259
- <div class="rw-address">
260
- <p><strong>${iconMapPin({ size: 16 })} 14 Saddler Lane</strong><br>
261
- Trumpington, Cambridge<br>
262
- CB2 9PQ</p>
263
- <p class="u-mt-3">${iconPhone({ size: 16 })} 01223 555 0176<br>
264
- hello@retrowheels.co</p>
265
- </div>
266
- </div>
267
-
268
- <div id="book">
269
- ${card({
270
- title: 'Book a service',
271
- level: 3,
272
- content: state.status === 'sent'
273
- ? `<p>Thanks &mdash; we&rsquo;ll be in touch within a working day. Tea&rsquo;s on when you arrive.</p>`
274
- : `<form data-action="bookService" novalidate>
275
- <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>
276
- ${input({ name: 'name', label: 'Your name', required: true })}
277
- ${input({ name: 'email', label: 'Email', type: 'email', required: true, class: 'u-mt-3' })}
278
- ${input({ name: 'bike', label: 'Bicycle (make, year)', placeholder: 'e.g. 1972 Raleigh Twenty', class: 'u-mt-3' })}
279
- <div class="u-mt-4">
280
- ${button({
281
- label: state.status === 'sending' ? 'Sending\u2026' : 'Send enquiry',
282
- type: 'submit',
283
- icon: iconSend({ size: 16 }),
284
- attrs: state.status === 'sending' ? { 'aria-busy': 'true', disabled: '' } : {},
285
- })}
286
- </div>
287
- </form>`,
288
- })}
289
- </div>
290
- </div>
291
- </div>
292
- </section>
293
-
294
- <section id="ride" class="rw-ride-banner">
295
- <p class="rw-eyebrow rw-eyebrow--on-orange">Every Saturday, 9 a.m. sharp</p>
296
- <h2>${iconStar({ size: 28 })} Join the Saturday ride.</h2>
297
- <p>Twenty miles, one cafe stop, no lycra required. Meet at the shop. Helmets sensible, bicycle of any vintage welcome.</p>
298
- ${button({ label: 'Add me to the list', href: '#book', variant: 'secondary', size: 'lg' })}
299
- </section>
300
-
301
- <section class="rw-section">
302
- <div class="rw-section-inner">
303
- ${cta({
304
- eyebrow: 'Workshop & Showroom',
305
- title: 'Bring us a bicycle. Leave with a story.',
306
- subtitle: 'Walk-ins welcome during opening hours. The kettle is always on.',
307
- actions: button({ label: 'Find the shop', href: '#visit', variant: 'primary', size: 'lg' }),
308
- })}
309
- </div>
310
- </section>
311
- </main>
312
-
313
- ${footer({
314
- logo: LOGO,
315
- links: [
316
- { label: 'Workshop', href: '#workshop' },
317
- { label: 'Services', href: '#services' },
318
- { label: 'Visit', href: '#visit' },
319
- { label: 'Saturday ride', href: '#ride' },
320
- ],
321
- legal: '\u00a9 1976 \u2014 2026 Retro Wheels Co. \u00b7 14 Saddler Lane, Cambridge \u00b7 A bicycle is forever.',
322
- })}
323
- `,
324
-
325
- actions: {
326
- bookService: {
327
- onStart: () => ({ status: 'sending' }),
328
- run: async (_state, _server, formData) => {
329
- const name = (formData.get('name') || '').trim()
330
- const email = (formData.get('email') || '').trim()
331
- if (!name) throw new Error('Please tell us your name.')
332
- if (!email) throw new Error('Please leave an email so we can reply.')
333
- await new Promise(r => setTimeout(r, 350))
334
- return { name }
335
- },
336
- onSuccess: () => ({
337
- status: 'sent',
338
- _toast: { message: 'Enquiry sent \u2014 we\u2019ll be in touch shortly.', variant: 'success' },
339
- }),
340
- onError: (_state, err) => ({
341
- status: 'idle',
342
- _toast: { message: err.message || 'Something went wrong. Try again?', variant: 'error' },
343
- }),
344
- },
345
- },
346
-
347
- onViewError: () => `
348
- <main id="main-content" class="rw-editorial">
349
- <h1>Spanner in the works</h1>
350
- <p>Something went sideways. <a href="/retrowheels">Reload the page</a> or give us a ring on 01223 555 0176.</p>
351
- </main>
352
- `,
353
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * Shared helpers for Pulse examples.
3
- * All examples import from here to stay DRY.
4
- */
5
-
6
- import { nav } from '../src/ui/index.js'
7
- import { iconSun, iconMoon } from '../src/ui/icons.js'
8
-
9
- const ALL_LINKS = [
10
- { label: 'Counter', href: '/counter' },
11
- { label: 'Todos', href: '/todos' },
12
- { label: 'Contact', href: '/contact' },
13
- { label: 'Quiz', href: '/quiz' },
14
- { label: 'Products', href: '/products' },
15
- { label: 'Pricing', href: '/pricing' },
16
- { label: 'Swish', href: '/swish' },
17
- { label: 'BookWorms', href: '/bookworms' },
18
- { label: 'Retro Wheels', href: '/retrowheels' },
19
- { label: 'Susuki Bento', href: '/suzukibento' },
20
- { label: 'Andy Pixel', href: '/andypixel' },
21
- { label: 'Journey', href: '/journey' },
22
- ]
23
-
24
- // Both icons rendered in DOM; JS hides the inactive one via `hidden` attribute.
25
- // Dark mode (default): show sun (click → go light). Light mode: show moon (click → go dark).
26
- const themeToggleButton = `
27
- <button id="theme-toggle" class="ui-btn ui-btn--ghost ui-btn--sm" aria-label="Switch to light mode" style="display:flex;align-items:center;">
28
- <span id="theme-icon-sun">${iconSun({ size: 16 })}</span>
29
- <span id="theme-icon-moon" hidden>${iconMoon({ size: 16 })}</span>
30
- </button>`
31
-
32
- /**
33
- * Returns a nonce'd theme-switcher script for use as `extraBody` in createServer.
34
- * Must be injected server-side so the nonce matches the page's CSP header.
35
- *
36
- * Usage in dev.server.js:
37
- * createServer(specs, { extraBody: themeScript })
38
- *
39
- * @param {string} nonce
40
- */
41
- export function themeScript(nonce) {
42
- return `<script nonce="${nonce}">
43
- (function () {
44
- var KEY = 'pulse-theme', LIGHT = 'light', DARK = 'dark'
45
- var theme = localStorage.getItem(KEY) === LIGHT ? LIGHT : DARK
46
- document.documentElement.dataset.theme = theme
47
-
48
- function updateUI(t) {
49
- var sun = document.getElementById('theme-icon-sun')
50
- var moon = document.getElementById('theme-icon-moon')
51
- if (sun) sun.hidden = (t === LIGHT)
52
- if (moon) moon.hidden = (t !== LIGHT)
53
- var btn = document.getElementById('theme-toggle')
54
- if (btn) btn.setAttribute('aria-label', t === LIGHT ? 'Switch to dark mode' : 'Switch to light mode')
55
- }
56
-
57
- if (document.readyState === 'loading') {
58
- document.addEventListener('DOMContentLoaded', function () { updateUI(theme) }, { once: true })
59
- } else {
60
- updateUI(theme)
61
- }
62
-
63
- if (window.__themeInit) return
64
- window.__themeInit = true
65
- document.addEventListener('click', function (e) {
66
- var btn = e.target.closest('#theme-toggle')
67
- if (!btn) return
68
- var next = document.documentElement.dataset.theme === LIGHT ? DARK : LIGHT
69
- localStorage.setItem(KEY, next)
70
- document.documentElement.dataset.theme = next
71
- updateUI(next)
72
- })
73
- })()
74
- </script>`
75
- }
76
-
77
- /**
78
- * Renders the shared examples nav bar using the ui-nav component.
79
- * @param {string} logo - Raw HTML logo slot
80
- * @param {string} logoHref - Logo link destination
81
- */
82
- export function examplesNav(logo, logoHref) {
83
- return nav({ logo, logoHref, links: ALL_LINKS, sticky: true, action: themeToggleButton })
84
- }