@invisibleloop/pulse 0.1.21

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 (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,157 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/accessibility')
6
+
7
+ export default {
8
+ route: '/accessibility',
9
+ meta: {
10
+ title: 'Accessibility — Pulse Docs',
11
+ description: 'Keyboard navigation, focus management, semantic HTML, and ARIA patterns in Pulse.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/accessibility',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Accessibility')}
21
+ ${lead('Pulse enforces a 100 Lighthouse Accessibility score as the baseline. The foundations — skip link, focus styles, and focus management on navigation — are provided by the framework on every page. There is nothing to configure and nothing to forget.')}
22
+
23
+ ${section('built-in', 'What the framework provides')}
24
+ ${table(
25
+ ['Feature', 'How'],
26
+ [
27
+ ['Skip link', 'Injected on every page as the first focusable element. Targets <code>#main-content</code>. Visible only on focus.'],
28
+ ['Focus styles', '<code>:focus-visible</code> outline applied globally — purple, 3px, offset 2px. Suppressed for mouse users via <code>:focus:not(:focus-visible)</code>.'],
29
+ ['Navigation focus', 'After client-side navigation, focus moves to <code>#main-content</code>, <code>&lt;main&gt;</code>, or <code>&lt;h1&gt;</code> — whichever is found first. Screen reader users hear the new page heading without a full reload.'],
30
+ ]
31
+ )}
32
+ ${callout('note', 'The skip link targets <code>#main-content</code>. Pages use <code>&lt;main id="main-content"&gt;</code> as the content wrapper so the link resolves correctly.')}
33
+
34
+ ${section('structure', 'Page structure')}
35
+ <p>Each page view is wrapped in <code>&lt;main id="main-content"&gt;</code> with a single <code>&lt;h1&gt;</code> that describes the current page. Landmark elements communicate structure to assistive technology:</p>
36
+ ${codeBlock(highlight(`view: (state) => \`
37
+ <main id="main-content">
38
+ <h1>Page title</h1>
39
+ <!-- page content -->
40
+ </main>
41
+ \``, 'js'))}
42
+ ${table(
43
+ ['Element', 'Purpose'],
44
+ [
45
+ ['<code>&lt;header&gt;</code>', 'Site header, logo, primary nav'],
46
+ ['<code>&lt;nav&gt;</code>', 'Navigation links — <code>aria-label</code> distinguishes multiple navs'],
47
+ ['<code>&lt;main id="main-content"&gt;</code>', 'Primary page content — one per page'],
48
+ ['<code>&lt;aside&gt;</code>', 'Supplementary content (sidebars, related links)'],
49
+ ['<code>&lt;footer&gt;</code>', 'Site footer'],
50
+ ]
51
+ )}
52
+
53
+ ${section('interactive', 'Interactive elements')}
54
+ <p>Actions are expressed as <code>&lt;button&gt;</code> elements, navigation as <code>&lt;a href&gt;</code> links. Both are keyboard-accessible by default. <code>&lt;div&gt;</code> and <code>&lt;span&gt;</code> elements with click handlers are not reachable by keyboard:</p>
55
+ ${codeBlock(highlight(`<!-- Keyboard accessible -->
56
+ <button data-event="toggle">Open menu</button>
57
+ <a href="/about">About</a>
58
+
59
+ <!-- Not keyboard accessible — avoid -->
60
+ <div data-event="toggle">Open menu</div>
61
+ <span onclick="...">About</span>`, 'html'))}
62
+ <p>Buttons that toggle state carry <code>aria-expanded</code> or <code>aria-pressed</code> to communicate the current state to screen readers:</p>
63
+ ${codeBlock(highlight(`view: (state) => \`
64
+ <button data-event="toggleMenu" aria-expanded="\${state.menuOpen}">
65
+ Menu
66
+ </button>
67
+ \${state.menuOpen ? \`<nav>...</nav>\` : ''}
68
+ \``, 'js'))}
69
+
70
+ ${section('focus', 'Focus management')}
71
+ <p>When a modal or dialog opens, focus moves inside it. When it closes, focus returns to the element that opened it. Since Pulse updates the DOM via morphing rather than a full replacement, the triggering element stays in the DOM and receives focus back naturally.</p>
72
+ <p>The <code>autofocus</code> attribute on the first interactive element inside newly revealed content moves focus there after the DOM update — no JavaScript required:</p>
73
+ ${codeBlock(highlight(`view: (state) => \`
74
+ <button data-event="openDialog">Delete item</button>
75
+
76
+ \${state.dialogOpen ? \`
77
+ <div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
78
+ <h2 id="dialog-title">Confirm deletion</h2>
79
+ <p>This cannot be undone.</p>
80
+ <button autofocus data-event="confirmDelete">Delete</button>
81
+ <button data-event="closeDialog">Cancel</button>
82
+ </div>
83
+ \` : ''}
84
+ \``, 'js'))}
85
+
86
+ ${section('live-regions', 'Dynamic content announcements')}
87
+ <p>State changes that produce status messages — loading indicators, confirmations, validation errors — are wrapped in live regions so screen readers announce them without a page reload:</p>
88
+ ${table(
89
+ ['Role', 'Politeness', 'Used for'],
90
+ [
91
+ ['<code>role="status"</code>', 'Polite — waits for the user to finish', 'Loading states, success messages, counts'],
92
+ ['<code>role="alert"</code>', 'Assertive — interrupts immediately', 'Validation errors, destructive confirmations'],
93
+ ]
94
+ )}
95
+ ${codeBlock(highlight(`view: (state) => \`
96
+ <form data-action="submit">
97
+ <!-- fields -->
98
+ <button type="submit" \${state.status === 'loading' ? 'disabled' : ''}>
99
+ \${state.status === 'loading' ? 'Saving…' : 'Save'}
100
+ </button>
101
+ </form>
102
+
103
+ \${state.status === 'loading' ? \`
104
+ <p role="status">Saving…</p>
105
+ \` : ''}
106
+
107
+ \${state.errors.length ? \`
108
+ <div role="alert">
109
+ \${state.errors.map(e => \`<p>\${e.message}</p>\`).join('')}
110
+ </div>
111
+ \` : ''}
112
+ \``, 'js'))}
113
+
114
+ ${section('forms', 'Forms')}
115
+ <p>Form controls are paired with <code>&lt;label&gt;</code> elements. Error messages are linked to their input via <code>aria-describedby</code> so screen readers announce them when the field receives focus:</p>
116
+ ${codeBlock(highlight(`<form data-action="submit">
117
+ <div class="field">
118
+ <label for="email">Email</label>
119
+ <input
120
+ id="email"
121
+ name="email"
122
+ type="email"
123
+ required
124
+ aria-describedby="\${state.emailError ? 'email-error' : ''}"
125
+ >
126
+ \${state.emailError
127
+ ? \`<p id="email-error" role="alert">\${state.emailError}</p>\`
128
+ : ''}
129
+ </div>
130
+
131
+ <fieldset>
132
+ <legend>Notification preferences</legend>
133
+ <label><input type="checkbox" name="email-notifs"> Email</label>
134
+ <label><input type="checkbox" name="sms-notifs"> SMS</label>
135
+ </fieldset>
136
+
137
+ <button type="submit">Submit</button>
138
+ </form>`, 'html'))}
139
+
140
+ ${section('images', 'Images')}
141
+ <p>Decorative images carry <code>alt=""</code> so screen readers skip them. Informative images have descriptive alt text. Icon-only buttons are labelled with <code>aria-label</code>, with the icon marked <code>aria-hidden="true"</code>:</p>
142
+ ${codeBlock(highlight(`<!-- Informative image -->
143
+ <img src="/team.jpg" alt="The Pulse team at the 2025 offsite" width="800" height="450">
144
+
145
+ <!-- Decorative image -->
146
+ <img src="/divider.svg" alt="" width="600" height="4">
147
+
148
+ <!-- Icon-only button -->
149
+ <button aria-label="Close">
150
+ <svg aria-hidden="true" focusable="false">...</svg>
151
+ </button>`, 'html'))}
152
+
153
+ ${section('lighthouse', 'Lighthouse score')}
154
+ <p>Every page is expected to score 100 on Lighthouse Accessibility. Run <code>/pulse-report</code> after every new page or significant change. Regressions — contrast failures, missing labels, unreachable controls — are caught before they reach users.</p>
155
+ `,
156
+ }),
157
+ }
@@ -0,0 +1,191 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/actions')
6
+
7
+ export default {
8
+ route: '/actions',
9
+ meta: {
10
+ title: 'Actions — Pulse Docs',
11
+ description: 'Async operations in Pulse — lifecycle, FormData, error handling.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/actions',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Actions')}
21
+ ${lead('Actions handle async operations with an enforced lifecycle. The order of steps — capture inputs, validate, run, succeed or fail — is fixed. Skipping validation or running async work before showing a loading state is not possible within the action structure.')}
22
+
23
+ ${section('lifecycle', 'The action lifecycle')}
24
+ <p>When a form with <code>data-action</code> is submitted, Pulse runs the action through a fixed sequence of steps:</p>
25
+ ${codeBlock(highlight(`onStart(state, formData)
26
+ ↓ (optional) validate — checks spec.validation rules
27
+ run(state, serverState, formData)
28
+ ↓ success ↓ error
29
+ onSuccess(state, payload) onError(state, err)`, 'bash'))}
30
+ <p>Each step triggers a view re-render, so the UI always reflects the current state — loading, validated, succeeded, or failed. The sequence cannot be reordered.</p>
31
+
32
+ ${section('defining', 'Defining an action')}
33
+ ${codeBlock(highlight(`export default {
34
+ route: '/contact',
35
+ state: {
36
+ status: 'idle', // 'idle' | 'loading' | 'success' | 'error'
37
+ errors: [],
38
+ },
39
+ validation: {
40
+ 'fields.name': { required: true, minLength: 2 },
41
+ 'fields.email': { required: true, format: 'email' },
42
+ 'fields.message': { required: true, minLength: 10 },
43
+ },
44
+ actions: {
45
+ submit: {
46
+ // 1. Immediately update state — show loading indicator
47
+ onStart: (state, formData) => ({
48
+ status: 'loading',
49
+ errors: [],
50
+ // Capture form values into state before validation runs
51
+ fields: {
52
+ name: formData.get('name'),
53
+ email: formData.get('email'),
54
+ message: formData.get('message'),
55
+ },
56
+ }),
57
+
58
+ // 2. Run spec.validation before proceeding to run()
59
+ validate: true,
60
+
61
+ // 3. Perform the async work
62
+ run: async (state, serverState, formData) => {
63
+ const res = await fetch('/api/contact', {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify(Object.fromEntries(formData)),
67
+ })
68
+ if (!res.ok) throw new Error('Request failed')
69
+ return res.json()
70
+ },
71
+
72
+ // 4a. Success
73
+ onSuccess: (state, payload) => ({
74
+ status: 'success',
75
+ errors: [],
76
+ }),
77
+
78
+ // 4b. Error — payload may have validation errors
79
+ onError: (state, err) => ({
80
+ status: 'error',
81
+ errors: err?.validation ?? [{ message: err.message }],
82
+ }),
83
+ },
84
+ },
85
+ }`, 'js'))}
86
+
87
+ ${section('binding', 'Binding actions to forms')}
88
+ <p>A <code>data-action</code> attribute on a <code>&lt;form&gt;</code> element binds it to an action. When the form is submitted, Pulse creates a <code>FormData</code> object from the form's inputs and passes it through the action lifecycle:</p>
89
+ ${codeBlock(highlight(`<form data-action="submit">
90
+ <input name="name" type="text" placeholder="Your name">
91
+ <input name="email" type="email" placeholder="Email">
92
+ <textarea name="message" placeholder="Message"></textarea>
93
+ <button type="submit">Send</button>
94
+ </form>`, 'html'))}
95
+ ${callout('note', 'Pulse intercepts and prevents the default form submission. The action lifecycle is fully in control of what happens with the data — no manual <code>event.preventDefault()</code> needed.')}
96
+
97
+ ${section('on-start', 'onStart')}
98
+ <p><code>onStart(state, formData)</code> runs synchronously as soon as the form is submitted, before any async work begins. It sets a loading state, captures form values into state so validation can check them, and clears previous errors.</p>
99
+ ${callout('warning', '<code>onStart</code> runs <strong>before</strong> validation. <code>FormData</code> values are captured into state first, because the HTML re-renders (destroying the form inputs) once validation runs. All form values are captured here so validation can read them from state via dot-paths.')}
100
+
101
+ ${section('validate', 'validate')}
102
+ <p>Set <code>validate: true</code> to run the spec's <a href="/validation">validation rules</a> after <code>onStart</code>. If validation fails, <code>onError</code> is called immediately — <code>run</code> is never reached. Async work cannot execute against invalid input.</p>
103
+ ${codeBlock(highlight(`// Validation error structure
104
+ {
105
+ message: 'Validation failed',
106
+ validation: [
107
+ { field: 'fields.email', rule: 'format', message: 'Must be a valid email' },
108
+ { field: 'fields.name', rule: 'required', message: 'Required' },
109
+ ]
110
+ }`, 'js'))}
111
+ <p>Access the errors in <code>onError</code>:</p>
112
+ ${codeBlock(highlight(`onError: (state, err) => ({
113
+ status: 'error',
114
+ errors: err?.validation ?? [{ message: err.message }],
115
+ })`, 'js'))}
116
+
117
+ ${section('run', 'run')}
118
+ <p><code>run(state, serverState, formData)</code> is where the async work happens. Throw or reject to trigger <code>onError</code>. The return value is passed to <code>onSuccess</code> as <code>payload</code>.</p>
119
+ ${codeBlock(highlight(`run: async (state, serverState, formData) => {
120
+ const res = await fetch('/api/submit', {
121
+ method: 'POST',
122
+ body: formData,
123
+ })
124
+ if (!res.ok) {
125
+ const err = await res.json()
126
+ throw Object.assign(new Error('Server error'), err)
127
+ }
128
+ return res.json() // → onSuccess payload
129
+ },`, 'js'))}
130
+
131
+ ${section('on-success', 'onSuccess')}
132
+ <p><code>onSuccess(state, payload)</code> receives the current state and whatever <code>run</code> returned. Return a partial state update:</p>
133
+ ${codeBlock(highlight(`onSuccess: (state, payload) => ({
134
+ status: 'success',
135
+ userId: payload.id,
136
+ })`, 'js'))}
137
+
138
+ ${section('on-error', 'onError')}
139
+ <p><code>onError(state, err)</code> receives the current state and the thrown error. Return a partial state update to surface the error in the view:</p>
140
+ ${codeBlock(highlight(`onError: (state, err) => ({
141
+ status: 'error',
142
+ errors: err?.validation ?? [{ message: err.message }],
143
+ })`, 'js'))}
144
+
145
+ ${section('toast', 'Toast notifications')}
146
+ <p>Return <code>_toast</code> from any action hook to show a notification. It is stripped from spec state automatically — it never appears in <code>getState()</code> or the view.</p>
147
+ ${codeBlock(highlight(`onSuccess: (state, payload) => ({
148
+ status: 'success',
149
+ _toast: { message: 'Saved successfully', variant: 'success' },
150
+ }),
151
+
152
+ onError: (state, err) => ({
153
+ status: 'error',
154
+ errors: err?.validation ?? [{ message: err.message }],
155
+ _toast: { message: 'Something went wrong', variant: 'error' },
156
+ }),`, 'js'))}
157
+ <p><code>_toast</code> works in <code>onStart</code>, <code>onSuccess</code>, and <code>onError</code>, and also in mutations. The toast container is injected into <code>document.body</code> once and survives client-side navigations.</p>
158
+ ${table(
159
+ ['Option', 'Type', 'Default', ''],
160
+ [
161
+ ['<code>message</code>', 'string', '—', 'Required. The notification text.'],
162
+ ['<code>variant</code>', '<code>success | error | warning | info</code>', '<code>info</code>', ''],
163
+ ['<code>duration</code>', 'number (ms)', '<code>4000</code>', 'Auto-dismiss delay. <code>0</code> = sticky until dismissed.'],
164
+ ]
165
+ )}
166
+
167
+ ${section('store-update', 'Pushing to the global store')}
168
+ <p>Return <code>_storeUpdate</code> from <code>onSuccess</code> to push a partial update into the <a href="/store">global store</a>. Every mounted page that subscribes to the updated keys re-renders immediately — no navigation, no polling.</p>
169
+ ${codeBlock(highlight(`onSuccess: (state, theme) => ({
170
+ saved: true,
171
+ _storeUpdate: { settings: { theme } }, // ← merged into global store state
172
+ }),`, 'js'))}
173
+ <p><code>_storeUpdate</code> is stripped from the page's own state — only the rest of the return object is merged into local state as usual. See <a href="/store">Global Store</a> for the full store API.</p>
174
+
175
+ ${section('rendering-errors', 'Rendering errors in the view')}
176
+ ${codeBlock(highlight(`view: (state) => \`
177
+ <form data-action="submit">
178
+ \${state.errors.map(e => \`
179
+ <p class="error">
180
+ \${e.field ? \`<strong>\${e.field}:</strong> \` : ''}\${e.message}
181
+ </p>
182
+ \`).join('')}
183
+ <!-- ... form fields ... -->
184
+ <button \${state.status === 'loading' ? 'disabled' : ''}>
185
+ \${state.status === 'loading' ? 'Sending…' : 'Send'}
186
+ </button>
187
+ </form>
188
+ \``, 'js'))}
189
+ `,
190
+ }),
191
+ }
@@ -0,0 +1,177 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/auth')
6
+
7
+ export default {
8
+ route: '/auth',
9
+ meta: {
10
+ title: 'Auth (Auth0) — Pulse Docs',
11
+ description: 'Integrating Auth0 OAuth authentication with Pulse using guard functions, ctx.setCookie, and raw response specs.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/auth',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Auth (Auth0)')}
21
+ ${lead('Pulse integrates with Auth0 — and any OAuth 2.0 provider — using plain HTTP redirects and a token exchange. No client-side auth SDK is required. Protected routes enforce access through <code>guard</code>, which runs before any data fetcher can execute.')}
22
+
23
+ ${callout('info', 'No Auth0 SDK required. The OAuth flow is plain HTTP redirects and a token exchange fetch — no client-side library needed.')}
24
+
25
+ ${section('setup', 'Setup')}
26
+ <p>Register your application in Auth0 and note your credentials. Store them in environment variables — never hardcode them in specs.</p>
27
+ ${codeBlock(highlight(`# .env
28
+ AUTH0_DOMAIN=your-tenant.auth0.com
29
+ AUTH0_CLIENT_ID=your_client_id
30
+ AUTH0_CLIENT_SECRET=your_client_secret
31
+ AUTH0_CALLBACK_URL=http://localhost:3000/auth/callback`, 'bash'))}
32
+
33
+ ${section('login', 'Login route')}
34
+ <p>The login route is a raw response spec that redirects the browser to Auth0's authorization endpoint.</p>
35
+ ${codeBlock(highlight(`// src/pages/auth/login.js
36
+ const {
37
+ AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CALLBACK_URL
38
+ } = process.env
39
+
40
+ export default {
41
+ route: '/auth/login',
42
+ contentType: 'text/html',
43
+
44
+ render: (ctx) => {
45
+ const params = new URLSearchParams({
46
+ response_type: 'code',
47
+ client_id: AUTH0_CLIENT_ID,
48
+ redirect_uri: AUTH0_CALLBACK_URL,
49
+ scope: 'openid profile email',
50
+ state: crypto.randomUUID(),
51
+ })
52
+ ctx.setHeader('Location', \`https://\${AUTH0_DOMAIN}/authorize?\${params}\`)
53
+ return { redirect: \`https://\${AUTH0_DOMAIN}/authorize?\${params}\` }
54
+ },
55
+ }`, 'js'))}
56
+
57
+ ${section('callback', 'Callback route')}
58
+ <p>Auth0 redirects back to <code>/auth/callback</code> with a <code>code</code> query parameter. The server exchanges it for tokens, sets a session cookie, and redirects to the app.</p>
59
+ ${codeBlock(highlight(`// src/pages/auth/callback.js
60
+ const {
61
+ AUTH0_DOMAIN, AUTH0_CLIENT_ID,
62
+ AUTH0_CLIENT_SECRET, AUTH0_CALLBACK_URL
63
+ } = process.env
64
+
65
+ export default {
66
+ route: '/auth/callback',
67
+ contentType: 'text/html',
68
+
69
+ server: {
70
+ session: async (ctx) => {
71
+ const { code } = ctx.query
72
+ if (!code) return null
73
+
74
+ // Exchange auth code for tokens
75
+ const res = await fetch(\`https://\${AUTH0_DOMAIN}/oauth/token\`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ grant_type: 'authorization_code',
80
+ client_id: AUTH0_CLIENT_ID,
81
+ client_secret: AUTH0_CLIENT_SECRET,
82
+ redirect_uri: AUTH0_CALLBACK_URL,
83
+ code,
84
+ }),
85
+ })
86
+
87
+ if (!res.ok) return null
88
+ const { access_token, id_token } = await res.json()
89
+
90
+ // Set a secure session cookie with the access token
91
+ ctx.setCookie('session', access_token, {
92
+ httpOnly: true,
93
+ secure: process.env.NODE_ENV === 'production',
94
+ sameSite: 'Lax',
95
+ maxAge: 86400, // 24 hours
96
+ })
97
+
98
+ return access_token
99
+ },
100
+ },
101
+
102
+ render: (ctx, server) => {
103
+ if (!server.session) return { redirect: '/auth/login' }
104
+ return { redirect: '/' }
105
+ },
106
+ }`, 'js'))}
107
+
108
+ ${section('logout', 'Logout route')}
109
+ <p>Clear the session cookie and redirect to Auth0's logout endpoint to invalidate the session there too.</p>
110
+ ${codeBlock(highlight(`// src/pages/auth/logout.js
111
+ const { AUTH0_DOMAIN, AUTH0_CLIENT_ID } = process.env
112
+
113
+ export default {
114
+ route: '/auth/logout',
115
+ contentType: 'text/html',
116
+
117
+ render: (ctx) => {
118
+ // Expire the session cookie
119
+ ctx.setCookie('session', '', { maxAge: 0 })
120
+
121
+ const returnTo = encodeURIComponent('http://localhost:3000')
122
+ return { redirect: \`https://\${AUTH0_DOMAIN}/v2/logout?client_id=\${AUTH0_CLIENT_ID}&returnTo=\${returnTo}\` }
123
+ },
124
+ }`, 'js'))}
125
+
126
+ ${section('guard', 'Protecting routes')}
127
+ <p>Use <code>guard</code> to verify the session token before any server data is fetched. For production, verify the JWT signature locally rather than calling Auth0 on every request.</p>
128
+ ${codeBlock(highlight(`// src/pages/dashboard.js
129
+ export default {
130
+ route: '/dashboard',
131
+
132
+ guard: async (ctx) => {
133
+ if (!ctx.cookies.session) return { redirect: '/auth/login' }
134
+
135
+ // Optional: verify JWT signature for production
136
+ // const user = await verifyJwt(ctx.cookies.session)
137
+ // if (!user) return { redirect: '/auth/login' }
138
+ },
139
+
140
+ server: {
141
+ profile: async (ctx) => fetchUserProfile(ctx.cookies.session),
142
+ },
143
+
144
+ state: {},
145
+ view: (state, server) => \`
146
+ <main id="main-content">
147
+ <h1>Welcome, \${server.profile.name}</h1>
148
+ </main>
149
+ \`,
150
+ }`, 'js'))}
151
+
152
+ ${callout('info', 'Guard runs before server data fetchers — if the session is invalid the profile fetch never happens.')}
153
+
154
+ ${section('ctx-reference', 'ctx reference')}
155
+ ${table(
156
+ ['Method', 'Description'],
157
+ [
158
+ ['<code>ctx.cookies.session</code>', 'Read the session cookie set during OAuth callback'],
159
+ ['<code>ctx.setCookie(name, value, opts)</code>', 'Set a response cookie — used in callback and logout routes'],
160
+ ['<code>ctx.setHeader(name, value)</code>', 'Set an arbitrary response header'],
161
+ ]
162
+ )}
163
+
164
+ ${table(
165
+ ['setCookie option', 'Type', 'Description'],
166
+ [
167
+ ['<code>httpOnly</code>', 'boolean', 'Prevents JS access — always use for session cookies'],
168
+ ['<code>secure</code>', 'boolean', 'HTTPS only — set <code>true</code> in production'],
169
+ ['<code>sameSite</code>', '<code>"Lax" | "Strict" | "None"</code>', '<code>Lax</code> works for most OAuth flows'],
170
+ ['<code>maxAge</code>', 'number', 'Lifetime in seconds — omit for session cookie'],
171
+ ['<code>path</code>', 'string', 'Defaults to <code>/</code>'],
172
+ ['<code>domain</code>', 'string', 'Scope to a domain — omit for current host'],
173
+ ]
174
+ )}
175
+ `,
176
+ }),
177
+ }
@@ -0,0 +1,95 @@
1
+ import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/caching')
6
+
7
+ export default {
8
+ route: '/caching',
9
+ meta: {
10
+ title: 'Caching — Pulse Docs',
11
+ description: 'HTTP cache headers, in-process server data caching, and asset caching in Pulse.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/caching',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Caching')}
21
+ ${lead('Pulse handles asset caching automatically — production bundles are content-hashed and served with immutable cache headers. Page caching is controlled declaratively in the spec: <code>serverTtl</code> for in-process data, <code>cache</code> for HTTP headers. Nothing is cached by default unless you declare it.')}
22
+
23
+ ${section('server-ttl', 'serverTtl — in-process data cache')}
24
+ <p><code>serverTtl</code> is a number of seconds to cache the result of <code>server.data()</code> in memory. Subsequent requests within the TTL window skip the async data fetch entirely and serve the cached result.</p>
25
+ ${codeBlock(highlight(`export default {
26
+ route: '/homepage',
27
+ serverTtl: 60, // cache server data for 60 seconds
28
+ server: {
29
+ data: async () => ({
30
+ featured: await db.products.getFeatured(),
31
+ stats: await analytics.getGlobalStats(),
32
+ }),
33
+ },
34
+ }`, 'js'))}
35
+ ${table(
36
+ ['Value', 'Behaviour'],
37
+ [
38
+ ['<code>undefined</code> (default)', 'No caching — <code>server.data()</code> runs on every request'],
39
+ ['<code>0</code>', 'No caching (same as undefined)'],
40
+ ['<code>60</code>', 'Cache for 60 seconds — at most one database hit per minute'],
41
+ ['<code>3600</code>', 'Cache for 1 hour'],
42
+ ]
43
+ )}
44
+ ${callout('note', 'The in-process cache is per-process and per-route. It is not shared across multiple server instances. Use a distributed cache (Redis, etc.) for cross-instance consistency.')}
45
+ ${callout('warning', 'In development, setting a long <code>serverTtl</code> can mask stale data. Set it to <code>0</code> or omit it during development, and add it when deploying to production.')}
46
+
47
+ ${section('http-cache', 'cache — HTTP response headers')}
48
+ <p>The <code>cache</code> field controls the <code>Cache-Control</code> header sent with the page HTML response. This tells browsers and CDNs how to cache the response.</p>
49
+ ${codeBlock(highlight(`export default {
50
+ route: '/blog/:slug',
51
+ cache: {
52
+ public: true, // allow CDN/proxy caching
53
+ maxAge: 300, // cache for 5 minutes
54
+ staleWhileRevalidate: 86400, // serve stale for up to 24 hours while revalidating
55
+ },
56
+ // ...
57
+ }`, 'js'))}
58
+
59
+ ${section('cache-fields', 'Cache field reference')}
60
+ ${table(
61
+ ['Field', 'Type', 'Description'],
62
+ [
63
+ ['<code>public</code>', '<code>boolean</code>', 'If true, emits <code>public</code> — allows CDN/proxy caching. Default: <code>private</code>.'],
64
+ ['<code>maxAge</code>', '<code>number</code>', 'Seconds before the response is considered stale. Emits <code>max-age=N</code>.'],
65
+ ['<code>staleWhileRevalidate</code>', '<code>number</code>', 'Seconds to serve stale content while revalidating in the background. Emits <code>stale-while-revalidate=N</code>.'],
66
+ ]
67
+ )}
68
+ ${codeBlock(highlight(`// private page — user-specific content
69
+ cache: { public: false, maxAge: 0 }
70
+ // → Cache-Control: private, no-store
71
+
72
+ // public marketing page — cached at CDN
73
+ cache: { public: true, maxAge: 3600, staleWhileRevalidate: 86400 }
74
+ // → Cache-Control: public, max-age=3600, stale-while-revalidate=86400`, 'js'))}
75
+
76
+ ${section('html-default', 'Default HTML caching')}
77
+ <p>By default, Pulse sends <code>Cache-Control: no-store</code> for all HTML responses. Users always see fresh content — stale HTML is never served from browser or proxy caches unless you explicitly declare a <code>cache</code> policy in the spec.</p>
78
+
79
+ ${section('asset-caching', 'Asset caching')}
80
+ <p>Static assets in <code>public/</code> receive <code>Cache-Control: max-age=3600</code> (one hour).</p>
81
+ <p>Production bundles in <code>public/dist/</code> receive <code>Cache-Control: public, max-age=31536000, immutable</code> (one year, immutable). This is guaranteed safe because bundle filenames include a content hash — code changes produce a new hash, and browsers fetch the updated file automatically. There is nothing to configure.</p>
82
+
83
+ ${section('dev-vs-prod', 'Development vs production')}
84
+ ${table(
85
+ ['Resource', 'Development', 'Production'],
86
+ [
87
+ ['HTML pages', '<code>no-store</code>', '<code>no-store</code> (or your <code>cache</code> config)'],
88
+ ['Static assets (<code>/public/*</code>)', '<code>max-age=3600</code>', '<code>max-age=3600</code>'],
89
+ ['JS bundles (<code>/dist/*</code>)', 'N/A (source files served directly)', '<code>immutable, max-age=31536000</code>'],
90
+ ]
91
+ )}
92
+ ${callout('tip', 'For maximum performance, combine <code>serverTtl</code> for expensive database queries with a short <code>cache.maxAge</code> and a generous <code>cache.staleWhileRevalidate</code>. Users get fast responses; data stays reasonably fresh.')}
93
+ `,
94
+ }),
95
+ }