@invisibleloop/pulse 0.20.11 → 0.20.20

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 +13 -6
  40. package/src/runtime/ssr.test.js +31 -0
  41. package/src/server/index.js +162 -9
  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 -350
  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,262 +0,0 @@
1
- /**
2
- * Pulse — Pricing page example
3
- *
4
- * Demonstrates:
5
- * - Landing page components (nav, accordion, cta, footer)
6
- * - Pricing tiers rendered with card() — the former pricing() component
7
- * was removed from the library as a narrow single-purpose wrapper
8
- * - Billing-period toggle (monthly / annual) via mutation
9
- * - Layout components (section, container, stack, cluster, grid)
10
- * - No server data — fully static, still gets streaming SSR
11
- *
12
- * Run: node examples/dev.server.js → http://localhost:3001/pricing
13
- */
14
-
15
- import {
16
- nav as uiNav, card, badge, accordion, cta, footer as uiFooter,
17
- section as uiSection, container, stack, cluster,
18
- button, heading,
19
- } from '../src/ui/index.js'
20
-
21
- // Pricing tile — replaces the removed pricing() component with card()
22
- const pricingCard = ({ name, price, period, description, features, badge: badgeLabel, highlighted, action, level = 2 }) => card({
23
- variant: highlighted ? 'elevated' : 'bordered',
24
- class: 'pr-plan',
25
- content: `
26
- ${badgeLabel ? badge({ label: badgeLabel }) : ''}
27
- <h${level} class="u-text-xl u-font-bold u-mt-2">${name}</h${level}>
28
- <p class="u-text-3xl u-font-bold u-mt-2"><span class="pr-plan-amount">${price}</span><span class="u-text-sm u-text-muted">${period}</span></p>
29
- <p class="u-text-sm u-text-muted u-mt-2">${description}</p>
30
- <ul class="u-mt-4 u-flex u-flex-col u-gap-2">
31
- ${features.map(f => `<li>${f}</li>`).join('')}
32
- </ul>
33
- `,
34
- footer: action,
35
- })
36
-
37
- const PLANS = {
38
- monthly: [
39
- {
40
- name: 'Starter',
41
- price: '£0',
42
- period: 'forever',
43
- description: 'For personal projects and tinkering.',
44
- features: [
45
- '1 project',
46
- '5,000 page views / month',
47
- 'Pulse framework',
48
- 'Community support',
49
- ],
50
- action: button({ label: 'Get started free', variant: 'secondary', href: '/getting-started', fullWidth: true }),
51
- highlighted: false,
52
- },
53
- {
54
- name: 'Pro',
55
- price: '£12',
56
- period: '/ month',
57
- description: 'For indie developers and small teams.',
58
- badge: 'Most popular',
59
- features: [
60
- '10 projects',
61
- '500,000 page views / month',
62
- 'Everything in Starter',
63
- 'Streaming SSR',
64
- 'Global CDN',
65
- 'Email support',
66
- ],
67
- action: button({ label: 'Start free trial', variant: 'primary', href: '/getting-started', fullWidth: true }),
68
- highlighted: true,
69
- },
70
- {
71
- name: 'Team',
72
- price: '£49',
73
- period: '/ month',
74
- description: 'For growing teams shipping fast.',
75
- features: [
76
- 'Unlimited projects',
77
- '5M page views / month',
78
- 'Everything in Pro',
79
- 'Team collaboration',
80
- 'Custom domains',
81
- 'Priority support',
82
- 'SLA guarantee',
83
- ],
84
- action: button({ label: 'Contact sales', variant: 'secondary', href: '/contact', fullWidth: true }),
85
- highlighted: false,
86
- },
87
- ],
88
- annual: [
89
- {
90
- name: 'Starter',
91
- price: '£0',
92
- period: 'forever',
93
- description: 'For personal projects and tinkering.',
94
- features: [
95
- '1 project',
96
- '5,000 page views / month',
97
- 'Pulse framework',
98
- 'Community support',
99
- ],
100
- action: button({ label: 'Get started free', variant: 'secondary', href: '/getting-started', fullWidth: true }),
101
- highlighted: false,
102
- },
103
- {
104
- name: 'Pro',
105
- price: '£9',
106
- period: '/ month',
107
- description: 'For indie developers and small teams.',
108
- badge: 'Most popular',
109
- features: [
110
- '10 projects',
111
- '500,000 page views / month',
112
- 'Everything in Starter',
113
- 'Streaming SSR',
114
- 'Global CDN',
115
- 'Email support',
116
- ],
117
- action: button({ label: 'Start free trial', variant: 'primary', href: '/getting-started', fullWidth: true }),
118
- highlighted: true,
119
- },
120
- {
121
- name: 'Team',
122
- price: '£39',
123
- period: '/ month',
124
- description: 'For growing teams shipping fast.',
125
- features: [
126
- 'Unlimited projects',
127
- '5M page views / month',
128
- 'Everything in Pro',
129
- 'Team collaboration',
130
- 'Custom domains',
131
- 'Priority support',
132
- 'SLA guarantee',
133
- ],
134
- action: button({ label: 'Contact sales', variant: 'secondary', href: '/contact', fullWidth: true }),
135
- highlighted: false,
136
- },
137
- ],
138
- }
139
-
140
- export const FAQ = [
141
- { question: 'Can I change plans later?', answer: 'Yes — upgrade or downgrade at any time. Changes take effect immediately; you\'ll be charged or credited on a pro-rated basis.' },
142
- { question: 'What counts as a page view?', answer: 'A page view is a single server-rendered HTML response. Static assets, API calls, and navigations that reuse cached HTML are not counted.' },
143
- { question: 'Do you offer a free trial?', answer: 'Pro and Team plans include a 14-day free trial — no credit card required. You\'ll get a reminder before the trial ends.' },
144
- { question: 'What happens if I exceed my page views?', answer: 'We\'ll notify you when you reach 80% of your limit. Your site stays live — we won\'t cut you off mid-month. Additional usage is billed at £1 per 100k views.' },
145
- { question: 'Is there a discount for non-profits?', answer: 'Yes — registered non-profits and open source projects qualify for 50% off any paid plan. Email us with your details.' },
146
- ]
147
-
148
- export default {
149
- route: '/pricing',
150
-
151
- meta: {
152
- title: 'Pricing — Pulse',
153
- description: 'Simple, transparent pricing for every stage. Starter is free forever. Pro and Team plans include a 14-day trial.',
154
- styles: ['/pulse-ui.css', '/examples/pricing.css'],
155
- theme: 'dark',
156
- },
157
-
158
- state: {
159
- billing: 'monthly', // monthly | annual
160
- },
161
-
162
- mutations: {
163
- setBilling: (_state, e) => ({
164
- billing: e.target.closest('[data-billing]')?.dataset.billing || 'monthly',
165
- }),
166
- },
167
-
168
- view: (state) => {
169
- const plans = PLANS[state.billing]
170
-
171
- return `
172
- ${uiNav({
173
- logo: `<span class="pr-logo">⚡ Pulse</span>`,
174
- logoHref: '/pricing',
175
- links: [
176
- { label: 'Counter', href: '/counter' },
177
- { label: 'Todos', href: '/todos' },
178
- { label: 'Contact', href: '/contact' },
179
- { label: 'Quiz', href: '/quiz' },
180
- { label: 'Products', href: '/products' },
181
- { label: 'Pricing', href: '/pricing' },
182
- ],
183
- sticky: true,
184
- })}
185
-
186
- <main id="main-content">
187
-
188
- ${uiSection({ content: container({ size: 'lg', content: stack({ gap: 'lg', align: 'center', content: `
189
-
190
- <div class="pr-eyebrow">Pricing</div>
191
- <h1 class="pr-hero-title">Simple, honest pricing.</h1>
192
- <p class="pr-hero-sub">Starter is free forever. Paid plans include a 14-day trial — no credit card required.</p>
193
-
194
- <div class="pr-toggle" role="group" aria-label="Billing period">
195
- ${button({ label: 'Monthly', variant: state.billing === 'monthly' ? 'primary' : 'ghost', size: 'sm', attrs: { 'data-event': 'setBilling', 'data-billing': 'monthly', 'aria-pressed': String(state.billing === 'monthly') } })}
196
- ${button({ label: 'Annual', variant: state.billing === 'annual' ? 'primary' : 'ghost', size: 'sm', iconAfter: '<span class="pr-save-label">Save 25%</span>', attrs: { 'data-event': 'setBilling', 'data-billing': 'annual', 'aria-pressed': String(state.billing === 'annual') } })}
197
- </div>
198
-
199
- ${state.billing === 'annual' ? `<p class="pr-annual-note">Annual plans are billed yearly. Prices shown are monthly equivalents.</p>` : ''}
200
-
201
- <div class="pr-plans">
202
- ${plans.map(p => pricingCard({ ...p, level: 2 })).join('')}
203
- </div>
204
-
205
- ` }) }) })}
206
-
207
- ${uiSection({ variant: 'alt', content: container({ size: 'md', content: stack({ gap: 'xl', align: 'center', content: `
208
-
209
- ${heading({ level: 2, text: 'Everything you need to ship', size: '3xl', class: 'u-text-center' })}
210
-
211
- <div class="pr-feature-table" role="region" aria-label="Feature comparison">
212
- <div class="pr-feature-header">
213
- <span>Feature</span>
214
- <span>Starter</span>
215
- <span>Pro</span>
216
- <span>Team</span>
217
- </div>
218
- ${[
219
- ['Streaming SSR', '✓', '✓', '✓'],
220
- ['Security headers', '✓', '✓', '✓'],
221
- ['Immutable asset caching','✓', '✓', '✓'],
222
- ['Custom domains', '—', '—', '✓'],
223
- ['Global CDN', '—', '✓', '✓'],
224
- ['Team seats', '1', '3', 'Unlimited'],
225
- ['Support', 'Community', 'Email', 'Priority + SLA'],
226
- ].map(([feat, ...vals]) => `
227
- <div class="pr-feature-row">
228
- <span class="pr-feature-name">${feat}</span>
229
- ${vals.map(v => `<span class="pr-feature-val${v === '—' ? ' pr-feature-val--no' : v === '✓' ? ' pr-feature-val--yes' : ''}">${v}</span>`).join('')}
230
- </div>`).join('')}
231
- </div>
232
-
233
- ` }) }) })}
234
-
235
- ${uiSection({ content: container({ size: 'md', content: stack({ gap: 'lg', align: 'center', content: `
236
- ${heading({ level: 2, text: 'Frequently asked questions', size: '3xl', class: 'u-text-center' })}
237
- ${accordion({ items: FAQ })}
238
- ` }) }) })}
239
-
240
- ${cta({
241
- title: 'Ready to build?',
242
- content: 'Starter is free forever. No credit card. No catch.',
243
- actions: cluster({ justify: 'center', content:
244
- button({ label: 'Get started free', href: '/getting-started', size: 'lg' }) + ' ' +
245
- button({ label: 'Read the docs', href: '/getting-started', variant: 'secondary', size: 'lg' })
246
- }),
247
- })}
248
-
249
- </main>
250
-
251
- ${uiFooter({
252
- logo: '<span class="pr-logo">⚡ Pulse</span>',
253
- tagline: 'The spec-first web framework.',
254
- columns: [
255
- { heading: 'Product', links: [{ label: 'Counter', href: '/counter' }, { label: 'Todos', href: '/todos' }, { label: 'Contact', href: '/contact' }] },
256
- { heading: 'Examples', links: [{ label: 'Quiz', href: '/quiz' }, { label: 'Products', href: '/products' }] },
257
- { heading: 'Docs', links: [{ label: 'Getting Started', href: '/getting-started' }, { label: 'Spec Reference', href: '/spec' }] },
258
- ],
259
- copy: `© ${new Date().getFullYear()} Pulse. Built with Pulse.`,
260
- })}`
261
- },
262
- }
@@ -1,190 +0,0 @@
1
- /**
2
- * Pulse — Product catalog example
3
- *
4
- * Demonstrates:
5
- * - Server data (20 mock products fetched before render)
6
- * - Client-side search, category filter, and sort — all via mutations
7
- * - Streaming SSR (shell instant, product grid deferred)
8
- * - Empty state when no results match
9
- *
10
- * Run: node examples/dev.server.js → http://localhost:3001/products
11
- */
12
-
13
- import { badge, button, empty, card, heading, search, select, uiImage } from '../src/ui/index.js'
14
- import { examplesNav } from './shared.js'
15
-
16
- function esc(s) {
17
- return String(s)
18
- .replace(/&/g, '&amp;').replace(/</g, '&lt;')
19
- .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
20
- }
21
-
22
- // ─── Mock data ───────────────────────────────────────────────────────────────
23
-
24
- const PRODUCTS = [
25
- { id: 1, name: 'Arc Desk Lamp', category: 'Lighting', price: 89, rating: 4.8, reviews: 312, img: 'https://picsum.photos/seed/desklamp/400/280', featured: true },
26
- { id: 2, name: 'Walnut Desk Tray', category: 'Storage', price: 45, rating: 4.7, reviews: 189, img: 'https://picsum.photos/seed/desktray/400/280', featured: false },
27
- { id: 3, name: 'Linen Desk Pad', category: 'Accessories', price: 32, rating: 4.6, reviews: 445, img: 'https://picsum.photos/seed/deskpad/400/280', featured: false },
28
- { id: 4, name: 'Cable Management Kit', category: 'Accessories', price: 24, rating: 4.5, reviews: 678, img: 'https://picsum.photos/seed/cables/400/280', featured: false },
29
- { id: 5, name: 'Monitor Riser', category: 'Furniture', price: 65, rating: 4.9, reviews: 521, img: 'https://picsum.photos/seed/monitorriser/400/280', featured: true },
30
- { id: 6, name: 'Ergonomic Chair Cushion', category: 'Furniture', price: 79, rating: 4.4, reviews: 234, img: 'https://picsum.photos/seed/cushion/400/280', featured: false },
31
- { id: 7, name: 'Wireless Charger Pad', category: 'Accessories', price: 38, rating: 4.3, reviews: 390, img: 'https://picsum.photos/seed/charger/400/280', featured: false },
32
- { id: 8, name: 'Bamboo Shelf Organiser', category: 'Storage', price: 55, rating: 4.6, reviews: 167, img: 'https://picsum.photos/seed/bamboo/400/280', featured: false },
33
- { id: 9, name: 'Mechanical Keyboard', category: 'Peripherals', price: 129, rating: 4.9, reviews: 891, img: 'https://picsum.photos/seed/keyboard/400/280', featured: true },
34
- { id: 10, name: 'Wrist Rest Pad', category: 'Accessories', price: 29, rating: 4.2, reviews: 276, img: 'https://picsum.photos/seed/wristrest/400/280', featured: false },
35
- { id: 11, name: 'Desk Plant Pot', category: 'Decor', price: 22, rating: 4.7, reviews: 152, img: 'https://picsum.photos/seed/plantpot/400/280', featured: false },
36
- { id: 12, name: 'Anti-Fatigue Mat', category: 'Furniture', price: 94, rating: 4.5, reviews: 308, img: 'https://picsum.photos/seed/floormat/400/280', featured: false },
37
- { id: 13, name: 'USB-C Hub 7-in-1', category: 'Peripherals', price: 69, rating: 4.8, reviews: 622, img: 'https://picsum.photos/seed/usbhub/400/280', featured: false },
38
- { id: 14, name: 'Sticky Note Dispenser', category: 'Storage', price: 18, rating: 4.1, reviews: 99, img: 'https://picsum.photos/seed/stickynote/400/280', featured: false },
39
- { id: 15, name: 'LED Bias Light Strip', category: 'Lighting', price: 42, rating: 4.6, reviews: 287, img: 'https://picsum.photos/seed/ledstrip/400/280', featured: false },
40
- { id: 16, name: 'Laptop Stand', category: 'Furniture', price: 58, rating: 4.7, reviews: 734, img: 'https://picsum.photos/seed/laptopstand/400/280', featured: true },
41
- { id: 17, name: 'Noise Cancelling Muffs', category: 'Peripherals', price: 49, rating: 4.4, reviews: 213, img: 'https://picsum.photos/seed/headphones/400/280', featured: false },
42
- { id: 18, name: 'Desktop Calendar', category: 'Decor', price: 16, rating: 4.3, reviews: 88, img: 'https://picsum.photos/seed/calendar/400/280', featured: false },
43
- { id: 19, name: 'Document Scanner', category: 'Peripherals', price: 149, rating: 4.8, reviews: 445, img: 'https://picsum.photos/seed/scanner/400/280', featured: false },
44
- { id: 20, name: 'Velvet Pen Cup', category: 'Decor', price: 19, rating: 4.5, reviews: 131, img: 'https://picsum.photos/seed/pencup/400/280', featured: false },
45
- ]
46
-
47
- export const CATEGORIES = ['All', ...new Set(PRODUCTS.map(p => p.category))].sort((a, b) =>
48
- a === 'All' ? -1 : b === 'All' ? 1 : a.localeCompare(b)
49
- )
50
-
51
- export function applyFilters(products, search, category, sort) {
52
- let out = products
53
-
54
- if (category && category !== 'All') {
55
- out = out.filter(p => p.category === category)
56
- }
57
-
58
- if (search) {
59
- const q = search.toLowerCase()
60
- out = out.filter(p => p.name.toLowerCase().includes(q) || p.category.toLowerCase().includes(q))
61
- }
62
-
63
- if (sort === 'price-asc') out = [...out].sort((a, b) => a.price - b.price)
64
- if (sort === 'price-desc') out = [...out].sort((a, b) => b.price - a.price)
65
- if (sort === 'rating') out = [...out].sort((a, b) => b.rating - a.rating)
66
- if (sort === 'reviews') out = [...out].sort((a, b) => b.reviews - a.reviews)
67
-
68
- return out
69
- }
70
-
71
- // ─── Spec ────────────────────────────────────────────────────────────────────
72
-
73
- export default {
74
- route: '/products',
75
-
76
- meta: {
77
- title: 'Products — Pulse',
78
- description: 'A product catalog built with Pulse. Demonstrates server data, client-side filtering, search, and sort.',
79
- styles: ['/pulse-ui.css', '/examples/products.css'],
80
- theme: 'light',
81
- },
82
-
83
- server: {
84
- products: async () => PRODUCTS,
85
- },
86
-
87
- stream: {
88
- shell: ['header', 'filters'],
89
- deferred: ['grid'],
90
- },
91
-
92
- state: {
93
- search: '',
94
- category: 'All',
95
- sort: 'featured',
96
- },
97
-
98
- mutations: {
99
- setSearch: (_state, e) => ({ search: e.target.value }),
100
- setCategory: (_state, e) => ({ category: e.target.closest('[data-cat]')?.dataset.cat || 'All' }),
101
- setSort: (_state, e) => ({ sort: e.target.value }),
102
- clearSearch: () => ({ search: '' }),
103
- },
104
-
105
- view: {
106
- header: (state, server) => {
107
- const total = server.products?.length ?? 0
108
- const filtered = applyFilters(server.products ?? [], state.search, state.category, state.sort)
109
-
110
- return `<div class="pd-root">
111
- ${examplesNav('<span class="pd-logo-text">🛍 Products</span>', '/products')}
112
-
113
- <main id="main-content" class="pd-main">
114
-
115
- <div class="pd-page-header">
116
- <div>
117
- ${heading({ level: 1, text: 'Home Office', size: '2xl' })}
118
- <p class="pd-subtitle">Showing <strong>${filtered.length}</strong> of <strong>${total}</strong> products</p>
119
- </div>
120
- <div class="pd-sort-wrap">
121
- ${select({ name: 'sort', label: 'Sort by', event: 'change:setSort', value: state.sort, options: [
122
- { value: 'featured', label: 'Featured' },
123
- { value: 'price-asc', label: 'Price: low to high' },
124
- { value: 'price-desc', label: 'Price: high to low' },
125
- { value: 'rating', label: 'Top rated' },
126
- { value: 'reviews', label: 'Most reviewed' },
127
- ]})}
128
- </div>
129
- </div>`
130
- },
131
-
132
- filters: (state) => {
133
- return `
134
- <div class="pd-sidebar">
135
-
136
- ${search({ name: 'search', label: 'Search', placeholder: 'Search products…', value: state.search, event: 'input:setSearch', debounce: 200, clearEvent: 'clearSearch' })}
137
-
138
- <div class="pd-category-wrap">
139
- <p class="pd-sidebar-label">Category</p>
140
- <ul class="pd-categories" role="list">
141
- ${CATEGORIES.map(cat => `<li>
142
- ${button({ label: cat, variant: state.category === cat ? 'primary' : 'secondary', size: 'sm', class: 'pd-cat-pill', attrs: { 'data-event': 'setCategory', 'data-cat': cat, 'aria-pressed': String(state.category === cat) } })}
143
- </li>`).join('')}
144
- </ul>
145
- </div>
146
-
147
- </div>`
148
- },
149
-
150
- grid: (state, server) => {
151
- const filtered = applyFilters(server.products ?? [], state.search, state.category, state.sort)
152
-
153
- const gridContent = filtered.length === 0
154
- ? empty({
155
- title: 'No products found',
156
- description: state.search ? `No results for "${esc(state.search)}".` : 'Try a different category.',
157
- })
158
- : `<ul class="pd-grid" aria-label="Products">
159
- ${filtered.map(p => `<li>
160
- ${card({
161
- flush: true,
162
- content: `
163
- ${uiImage({ src: p.img, alt: p.name, ratio: '16/9' })}
164
- <div class="pd-product-body">
165
- <div class="pd-product-meta">
166
- ${badge({ label: p.category, variant: 'default' })}
167
- ${p.featured ? badge({ label: 'Featured', variant: 'info' }) : ''}
168
- </div>
169
- ${heading({ level: 2, text: p.name, size: 'sm' })}
170
- <div class="pd-product-stats">
171
- <span class="pd-rating" aria-label="${p.rating} out of 5 stars">★ ${p.rating}</span>
172
- <span class="pd-reviews">(${p.reviews.toLocaleString()})</span>
173
- </div>
174
- <p class="pd-price">£${p.price}</p>
175
- </div>
176
- `,
177
- })}
178
- </li>`).join('')}
179
- </ul>`
180
-
181
- return `
182
- <div class="pd-content">
183
- ${gridContent}
184
- </div>
185
-
186
- </main>
187
- </div>`
188
- },
189
- },
190
- }
@@ -1 +0,0 @@
1
- 0.14.1