@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,98 @@
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('/hydration')
6
+
7
+ export default {
8
+ route: '/hydration',
9
+ meta: {
10
+ title: 'Hydration — Pulse Docs',
11
+ description: 'How Pulse hydrates server-rendered HTML on the client — the hydrate field, mount(), and production bundles.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/hydration',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Hydration')}
21
+ ${lead('Hydration in Pulse is opt-in and minimal. Omit <code>hydrate</code> and zero JavaScript is sent to the browser. Add it and Pulse binds events to the server-rendered HTML without re-rendering it — the SSR-painted content is preserved exactly as the server sent it.')}
22
+
23
+ ${section('enabling', 'Enabling hydration')}
24
+ <p>Set the <code>hydrate</code> field to a browser-importable path to your spec file. Pulse uses this to generate the bootstrap script that mounts the client runtime:</p>
25
+ ${codeBlock(highlight(`export default {
26
+ route: '/counter',
27
+ hydrate: '/src/pages/counter.js', // browser path to this file
28
+ state: { count: 0 },
29
+ mutations: {
30
+ increment: (state) => ({ count: state.count + 1 }),
31
+ decrement: (state) => ({ count: state.count - 1 }),
32
+ },
33
+ view: (state) => \`
34
+ <div>
35
+ <button data-event="decrement">-</button>
36
+ <span>\${state.count}</span>
37
+ <button data-event="increment">+</button>
38
+ </div>
39
+ \`,
40
+ }`, 'js'))}
41
+
42
+ ${section('dev-bootstrap', 'Development bootstrap')}
43
+ <p>In development (when <code>hydrate</code> is a source file path, not a <code>/dist/</code> bundle), Pulse emits an inline bootstrap script:</p>
44
+ ${codeBlock(highlight(`<script type="module">
45
+ import spec from '/src/pages/counter.js'
46
+ import { mount } from '/src/runtime/index.js'
47
+ import { initNavigation } from '/src/runtime/navigate.js'
48
+ mount(spec, root, window.__PULSE_SERVER__ || {}, { ssr: true })
49
+ initNavigation(root, mount)
50
+ </script>`, 'html'))}
51
+ <p>This imports the spec and runtime source files directly — no build step required for development.</p>
52
+
53
+ ${section('production', 'Production bundles')}
54
+ <p>Run <code>npm run build</code> to generate production bundles. This creates content-hashed files in <code>public/dist/</code> and a <code>manifest.json</code> mapping spec hydrate paths to bundle paths.</p>
55
+ ${codeBlock(highlight(`# Generated by npm run build
56
+ public/dist/
57
+ runtime-abc123.js # shared runtime (~2.1 kB brotli)
58
+ counter.boot-def456.js # per-page spec bundle (~0.5 kB brotli)
59
+ manifest.json # { '/src/pages/counter.js': '/dist/counter.boot-def456.js' }`, 'bash'))}
60
+ <p>When Pulse detects a manifest (via <code>staticDir</code> auto-detection or explicit <code>manifest</code> option), it resolves the <code>hydrate</code> path to the bundle path and emits a single <code>&lt;script src&gt;</code> tag instead of the inline bootstrap:</p>
61
+ ${codeBlock(highlight(`<script type="module" src="/dist/counter.boot-def456.js"></script>`, 'html'))}
62
+
63
+ ${section('ssr-true', 'The { ssr: true } option')}
64
+ <p>The bootstrap script calls <code>mount(spec, root, serverState, { ssr: true })</code>. This tells the runtime to skip the initial re-render and bind event listeners to the existing DOM only.</p>
65
+ <p>This is what keeps LCP fast. The server-painted HTML is the LCP element. The JavaScript binds events without touching the DOM — no flash, no layout shift, no JS-rendered replacement.</p>
66
+ ${callout('warning', 'Never set <code>{ ssr: false }</code> on a server-rendered page. It re-renders the entire DOM on mount, replacing the server-painted HTML — causing a visible flash and pushing LCP to 400–600ms.')}
67
+
68
+ ${section('mount', 'mount()')}
69
+ <p>The <code>mount</code> function attaches the Pulse runtime to a DOM element:</p>
70
+ ${codeBlock(highlight(`import { mount } from '@invisibleloop/pulse/runtime'
71
+
72
+ mount(
73
+ spec, // the page spec
74
+ rootEl, // the DOM element to mount into
75
+ serverState, // window.__PULSE_SERVER__ (server data from SSR)
76
+ { ssr: true } // skip re-render on first mount
77
+ )`, 'js'))}
78
+ <p>After mount, all <code>data-event</code> and <code>data-action</code> attributes in the DOM are wired to the spec's mutations and actions. State updates trigger a full view re-render via <code>innerHTML</code> replacement.</p>
79
+
80
+ ${section('no-hydrate', 'Pages without hydration')}
81
+ <p>Omit <code>hydrate</code> and Pulse sends zero JavaScript to the browser — no runtime overhead, no hydration cost. This is the correct default for:</p>
82
+ <ul>
83
+ <li>Documentation pages</li>
84
+ <li>Marketing/landing pages</li>
85
+ <li>Blog posts and articles</li>
86
+ <li>Any page with no client-side interactivity</li>
87
+ </ul>
88
+ ${callout('tip', 'Start without <code>hydrate</code> and only add it when actual client interactivity is needed. Many pages that appear to need JavaScript can be handled server-side with <a href="/routing">routing</a> and <a href="/server-data">server data</a>.')}
89
+
90
+ ${section('server-state', 'Passing server state to the client')}
91
+ <p>Server data fetched via <code>server.data()</code> is serialised into the page HTML as <code>window.__PULSE_SERVER__</code>. The client runtime reads this on mount, making server data available to the view during client re-renders without an additional network request.</p>
92
+ ${codeBlock(highlight(`// Emitted in the page HTML
93
+ <script id="__PULSE_SERVER__" type="application/json">
94
+ {"product":{"id":1,"name":"Widget","price":9.99}}
95
+ </script>`, 'html'))}
96
+ `,
97
+ }),
98
+ }
@@ -0,0 +1,121 @@
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('/images')
6
+
7
+ export default {
8
+ route: '/images',
9
+ meta: {
10
+ title: 'Images — Pulse Docs',
11
+ description: 'The img() and picture() helpers for optimised, CLS-free image markup.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/images',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Images')}
21
+ ${lead('The <code>img()</code> and <code>picture()</code> helpers generate image markup that prevents CLS by requiring dimensions and handles loading priority correctly. Pulse targets 0.00 CLS — these helpers enforce the attributes that make that possible.')}
22
+
23
+ ${section('import', 'Importing')}
24
+ ${codeBlock(highlight(`// In your page spec or component
25
+ import { img, picture } from '@invisibleloop/pulse/image'`, 'js'))}
26
+
27
+ ${section('img', 'img(options)')}
28
+ <p>Generates an optimised <code>&lt;img&gt;</code> element:</p>
29
+ ${codeBlock(highlight(`img({
30
+ src: '/images/hero.jpg',
31
+ alt: 'A hero image showing our product',
32
+ width: 1200,
33
+ height: 630,
34
+ priority: true, // → eager loading + high fetchpriority
35
+ })`, 'js'))}
36
+ <p>Output:</p>
37
+ ${codeBlock(highlight(`<img src="/images/hero.jpg" alt="A hero image showing our product" width="1200" height="630" loading="eager" decoding="async" fetchpriority="high">`, 'html'))}
38
+
39
+ ${section('img-options', 'img() options')}
40
+ ${table(
41
+ ['Option', 'Type', 'Required', 'Description'],
42
+ [
43
+ ['<code>src</code>', '<code>string</code>', 'Yes', 'Image URL.'],
44
+ ['<code>alt</code>', '<code>string</code>', 'Yes', 'Alt text. Required for accessibility. Use an empty string for decorative images.'],
45
+ ['<code>width</code>', '<code>number</code>', 'Recommended', 'Intrinsic width in pixels. Prevents CLS by reserving layout space.'],
46
+ ['<code>height</code>', '<code>number</code>', 'Recommended', 'Intrinsic height in pixels. Prevents CLS.'],
47
+ ['<code>priority</code>', '<code>boolean</code>', 'No', 'If true: <code>loading="eager"</code> + <code>fetchpriority="high"</code>. Use for LCP images. Default: <code>false</code>.'],
48
+ ['<code>class</code>', '<code>string</code>', 'No', 'CSS class applied to the <code>&lt;img&gt;</code> element.'],
49
+ ]
50
+ )}
51
+
52
+ ${callout('warning', '<code>width</code> and <code>height</code> are required to prevent Cumulative Layout Shift. Without them the browser cannot reserve layout space before the image loads. Pulse targets 0.00 CLS — omitting these attributes breaks that guarantee.')}
53
+
54
+ ${section('picture', 'picture(options)')}
55
+ <p>Generates a <code>&lt;picture&gt;</code> element with modern format sources and a fallback <code>&lt;img&gt;</code>. Use this to serve AVIF or WebP to browsers that support them, with JPEG/PNG as the fallback:</p>
56
+ ${codeBlock(highlight(`picture({
57
+ src: '/images/hero.jpg', // fallback
58
+ alt: 'Hero image',
59
+ width: 1200,
60
+ height: 630,
61
+ priority: true,
62
+ sources: [
63
+ { src: '/images/hero.avif', type: 'image/avif' },
64
+ { src: '/images/hero.webp', type: 'image/webp' },
65
+ ],
66
+ })`, 'js'))}
67
+ <p>Output:</p>
68
+ ${codeBlock(highlight(`<picture>
69
+ <source srcset="/images/hero.avif" type="image/avif">
70
+ <source srcset="/images/hero.webp" type="image/webp">
71
+ <img src="/images/hero.jpg" alt="Hero image" width="1200" height="630" loading="eager" decoding="async" fetchpriority="high">
72
+ </picture>`, 'html'))}
73
+
74
+ ${section('picture-options', 'picture() options')}
75
+ ${table(
76
+ ['Option', 'Type', 'Description'],
77
+ [
78
+ ['<code>src</code>', '<code>string</code>', 'Fallback image URL (JPEG/PNG for universal compatibility).'],
79
+ ['<code>alt</code>', '<code>string</code>', 'Alt text — shared by the inner <code>&lt;img&gt;</code>.'],
80
+ ['<code>width</code>', '<code>number</code>', 'Intrinsic width — applied to inner <code>&lt;img&gt;</code>.'],
81
+ ['<code>height</code>', '<code>number</code>', 'Intrinsic height — applied to inner <code>&lt;img&gt;</code>.'],
82
+ ['<code>priority</code>', '<code>boolean</code>', 'If true: eager loading + high priority.'],
83
+ ['<code>class</code>', '<code>string</code>', 'CSS class on the inner <code>&lt;img&gt;</code>.'],
84
+ ['<code>sources</code>', '<code>{src, type}[]</code>', 'Modern format sources in preference order (AVIF first, WebP second).'],
85
+ ]
86
+ )}
87
+
88
+ ${section('priority', 'When to use priority')}
89
+ <p>Set <code>priority: true</code> on the <strong>Largest Contentful Paint (LCP) element</strong> — typically the hero image above the fold. This tells the browser to load it with high priority, improving LCP.</p>
90
+ <p>Every other image should omit <code>priority</code> (defaults to lazy loading with <code>loading="lazy"</code>). Lazy images are not fetched until they approach the viewport — reducing initial page weight and speeding up load time.</p>
91
+ ${callout('tip', 'Only one image per page should have <code>priority: true</code>. Using it on multiple images defeats the purpose — every image becomes "high priority" which is the same as no image being prioritised.')}
92
+
93
+ ${section('in-view', 'Using in a view')}
94
+ ${codeBlock(highlight(`import { img, picture } from '@invisibleloop/pulse/image'
95
+
96
+ export default {
97
+ route: '/blog/:slug',
98
+ state: {},
99
+ server: {
100
+ data: async (ctx) => ({ post: await db.posts.findBySlug(ctx.params.slug) }),
101
+ },
102
+ view: (state, server) => \`
103
+ <article>
104
+ \${picture({
105
+ src: server.post.heroImage,
106
+ alt: server.post.heroAlt,
107
+ width: 1200,
108
+ height: 630,
109
+ priority: true,
110
+ sources: [
111
+ { src: server.post.heroImage.replace('.jpg', '.avif'), type: 'image/avif' },
112
+ { src: server.post.heroImage.replace('.jpg', '.webp'), type: 'image/webp' },
113
+ ],
114
+ })}
115
+ <h1>\${server.post.title}</h1>
116
+ </article>
117
+ \`,
118
+ }`, 'js'))}
119
+ `,
120
+ }),
121
+ }
@@ -0,0 +1,120 @@
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('/meta')
6
+
7
+ export default {
8
+ route: '/meta',
9
+ meta: {
10
+ title: 'Metadata & SEO — Pulse Docs',
11
+ description: 'How to configure page metadata, Open Graph tags, and structured data in Pulse.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/meta',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Metadata & SEO')}
21
+ ${lead('The <code>meta</code> field declares everything that appears in the <code>&lt;head&gt;</code> — title, description, stylesheets, Open Graph tags, and structured data. All metadata is rendered server-side, so crawlers and social media scrapers see the final HTML without executing JavaScript.')}
22
+
23
+ ${section('basics', 'Basic metadata')}
24
+ ${codeBlock(highlight(`export default {
25
+ route: '/about',
26
+ meta: {
27
+ title: 'About Us — Acme Corp',
28
+ description: 'Learn about the team behind Acme Corp.',
29
+ styles: ['/app.css'],
30
+ },
31
+ state: {},
32
+ view: () => \`<h1>About Us</h1>\`,
33
+ }`, 'js'))}
34
+ <p>This generates:</p>
35
+ ${codeBlock(highlight(`<title>About Us — Acme Corp</title>
36
+ <meta name="description" content="Learn about the team behind Acme Corp.">
37
+ <link rel="stylesheet" href="/app.css">`, 'html'))}
38
+
39
+ ${section('all-fields', 'All meta fields')}
40
+ ${table(
41
+ ['Field', 'Type', 'Description'],
42
+ [
43
+ ['<code>title</code>', '<code>string</code>', 'Page title — appears in the browser tab and search results.'],
44
+ ['<code>description</code>', '<code>string</code>', 'Meta description — appears in search engine snippets. Keep under 160 characters.'],
45
+ ['<code>styles</code>', '<code>string[]</code>', 'Array of stylesheet URLs — each emits a <code>&lt;link rel="stylesheet"&gt;</code> tag.'],
46
+ ['<code>ogTitle</code>', '<code>string</code>', 'Open Graph title. If omitted, falls back to <code>title</code>.'],
47
+ ['<code>ogImage</code>', '<code>string</code>', 'Open Graph image URL — shown when the page is shared on social media.'],
48
+ ['<code>schema</code>', '<code>object</code>', 'JSON-LD structured data object — emitted as a <code>&lt;script type="application/ld+json"&gt;</code> tag.'],
49
+ ]
50
+ )}
51
+
52
+ ${section('open-graph', 'Open Graph')}
53
+ <p>Open Graph tags control how the page appears when shared on social media (Twitter/X, Facebook, LinkedIn, Slack, etc.):</p>
54
+ ${codeBlock(highlight(`meta: {
55
+ title: 'My Product — Acme Corp',
56
+ description: 'The best product ever made.',
57
+ ogTitle: 'My Product', // shorter for social
58
+ ogImage: 'https://acme.com/og/my-product.jpg', // 1200×630 recommended
59
+ }`, 'js'))}
60
+ <p>Generated tags:</p>
61
+ ${codeBlock(highlight(`<meta property="og:title" content="My Product">
62
+ <meta property="og:description" content="The best product ever made.">
63
+ <meta property="og:image" content="https://acme.com/og/my-product.jpg">
64
+ <meta name="twitter:card" content="summary_large_image">
65
+ <meta name="twitter:title" content="My Product">
66
+ <meta name="twitter:image" content="https://acme.com/og/my-product.jpg">`, 'html'))}
67
+ ${callout('tip', 'Use an absolute URL for <code>ogImage</code> — social media crawlers need the full URL to fetch the image. Recommended size: 1200×630 pixels.')}
68
+
69
+ ${section('structured-data', 'Structured data (ld+json)')}
70
+ <p>The <code>schema</code> field accepts a plain object conforming to <a href="https://schema.org" target="_blank" rel="noopener">schema.org</a> vocabulary. Pulse serialises it as a <code>&lt;script type="application/ld+json"&gt;</code> tag in the head:</p>
71
+ ${codeBlock(highlight(`meta: {
72
+ title: 'How to make sourdough — My Blog',
73
+ schema: {
74
+ '@context': 'https://schema.org',
75
+ '@type': 'Article',
76
+ headline: 'How to make sourdough',
77
+ author: {
78
+ '@type': 'Person',
79
+ name: 'Jane Smith',
80
+ },
81
+ datePublished: '2025-01-15',
82
+ image: 'https://myblog.com/sourdough.jpg',
83
+ },
84
+ }`, 'js'))}
85
+ <p>Common schema types:</p>
86
+ ${table(
87
+ ['@type', 'Use for'],
88
+ [
89
+ ['<code>WebSite</code>', 'The homepage or site root'],
90
+ ['<code>WebPage</code>', 'General pages'],
91
+ ['<code>Article</code>', 'Blog posts, news articles'],
92
+ ['<code>Product</code>', 'E-commerce product pages'],
93
+ ['<code>FAQPage</code>', 'FAQ pages (enables rich results in Google)'],
94
+ ['<code>BreadcrumbList</code>', 'Breadcrumb navigation'],
95
+ ['<code>Organization</code>', 'Company/brand information'],
96
+ ]
97
+ )}
98
+
99
+ ${section('styles', 'Stylesheets')}
100
+ <p>The <code>styles</code> array accepts any number of stylesheet URLs. They are emitted as <code>&lt;link rel="stylesheet"&gt;</code> tags in the <code>&lt;head&gt;</code> in the order declared:</p>
101
+ ${codeBlock(highlight(`meta: {
102
+ styles: [
103
+ '/app.css',
104
+ '/fonts.css',
105
+ 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap',
106
+ ],
107
+ }`, 'js'))}
108
+ ${callout('note', 'For maximum performance, host your own fonts rather than using Google Fonts. External stylesheet requests add render-blocking latency and a DNS lookup.')}
109
+
110
+ ${section('seo-tips', 'SEO tips')}
111
+ <ul>
112
+ <li>Write a unique <code>title</code> and <code>description</code> for every page — duplicate metadata prevents pages from competing in search results.</li>
113
+ <li>Keep descriptions under 160 characters — longer values are truncated.</li>
114
+ <li>Use structured data to qualify for Google rich results.</li>
115
+ <li>All metadata is in the server-rendered HTML — search engines and social scrapers do not need to execute JavaScript to read it.</li>
116
+ <li>Pulse targets 100/100 Lighthouse SEO out of the box. Run <code>/pulse-report</code> after new pages to confirm.</li>
117
+ </ul>
118
+ `,
119
+ }),
120
+ }
@@ -0,0 +1,106 @@
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('/mutations')
6
+
7
+ export default {
8
+ route: '/mutations',
9
+ meta: {
10
+ title: 'Mutations — Pulse Docs',
11
+ description: 'Synchronous state changes in Pulse — how to declare and trigger mutations.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/mutations',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Mutations')}
21
+ ${lead('Mutations are the only permitted way to change client state. They are synchronous pure functions — network requests, DOM manipulation, and timers are structurally excluded. After every mutation, Pulse automatically applies constraints and re-renders the view.')}
22
+
23
+ ${section('what', 'What is a mutation?')}
24
+ <p>A mutation is a function with the signature <code>(state, event) =&gt; partialState</code>. It receives the current state and the DOM event that triggered it, and returns a plain object to merge into state.</p>
25
+ ${codeBlock(highlight(`mutations: {
26
+ increment: (state) => ({ count: state.count + 1 }),
27
+ decrement: (state) => ({ count: state.count - 1 }),
28
+ reset: () => ({ count: 0 }),
29
+ }`, 'js'))}
30
+ <p>The returned partial is shallow-merged. Only the returned keys are changed — everything else in state is preserved.</p>
31
+
32
+ ${section('binding', 'Binding mutations to DOM events')}
33
+ <p>Mutations are bound to DOM events using the <code>data-event</code> attribute in the view HTML:</p>
34
+ ${codeBlock(highlight(`<button data-event="increment">+</button> <!-- click → increment -->
35
+ <button data-event="click:decrement">-</button> <!-- explicit click -->
36
+ <input data-event="change:setName"> <!-- change event -->
37
+ <input data-event="input:setQuery"> <!-- input event (every keystroke) -->`, 'html'))}
38
+
39
+ ${table(
40
+ ['Event type', 'Shorthand', 'Typical use'],
41
+ [
42
+ ['<code>click</code>', '<code>data-event="mutName"</code>', 'Buttons, links'],
43
+ ['<code>change</code>', '<code>data-event="change:mutName"</code>', 'Select dropdowns, checkboxes'],
44
+ ['<code>input</code>', '<code>data-event="input:mutName"</code>', 'Search/filter fields — add <code>data-debounce="300"</code> to rate-limit'],
45
+ ]
46
+ )}
47
+
48
+ ${section('debounce', 'Debounce and throttle')}
49
+ <p>Add <code>data-debounce="300"</code> alongside <code>data-event</code> to delay the mutation until typing stops. The mutation fires once, 300ms after the last keystroke — not on every character. Use this for live search and filter inputs.</p>
50
+ ${codeBlock(highlight(`<input data-event="input:search" data-debounce="300">
51
+ <input data-event="input:filter" data-throttle="100">`, 'html'))}
52
+ <p><code>data-throttle="100"</code> fires at most once per 100ms — useful when you want frequent updates but need to limit the rate. Both attributes accept a value in milliseconds and apply to <code>input</code> and <code>change</code> events. No per-spec timer code needed.</p>
53
+
54
+ ${section('event-arg', 'The event argument')}
55
+ <p>The second argument to a mutation is the native DOM <code>Event</code> object, giving access to the element and its value:</p>
56
+ ${codeBlock(highlight(`mutations: {
57
+ setName: (state, e) => ({ name: e.target.value }),
58
+ setCountry: (state, e) => ({ country: e.target.value }),
59
+ toggle: (state, e) => ({ checked: e.target.checked }),
60
+ }`, 'js'))}
61
+
62
+ ${section('partial-merge', 'Partial state merge')}
63
+ <p>Only the keys that need to change are returned. The runtime merges the returned object into the existing state at the top level:</p>
64
+ ${codeBlock(highlight(`// state = { step: 2, name: 'Alice', email: 'a@b.com' }
65
+
66
+ mutations: {
67
+ nextStep: (state) => ({ step: state.step + 1 }),
68
+ // After: { step: 3, name: 'Alice', email: 'a@b.com' }
69
+ // name and email are untouched
70
+ }`, 'js'))}
71
+ ${callout('warning', 'The merge is <strong>shallow</strong>. When state has nested objects, returning a new version of a nested key replaces the entire sub-object — not a deep merge. Use the spread operator to preserve nested fields:')}
72
+ ${codeBlock(highlight(`mutations: {
73
+ setEmail: (state, e) => ({
74
+ // Spread the nested object to preserve sibling keys
75
+ user: { ...state.user, email: e.target.value }
76
+ }),
77
+ }`, 'js'))}
78
+
79
+ ${section('no-side-effects', 'No side effects')}
80
+ <p>Mutations are pure functions. Network requests, DOM manipulation, and timers belong in <a href="/actions">actions</a> — not here. The structural separation is what lets Pulse re-render predictably, apply constraints safely, and make state changes auditable.</p>
81
+
82
+ ${section('constraints', 'Mutations and constraints')}
83
+ <p>After every mutation, Pulse automatically applies any <a href="/constraints">constraints</a> declared in the spec. State bounds are never checked inside mutations — they are declared once and enforced by the framework regardless of what a mutation returns:</p>
84
+ ${codeBlock(highlight(`{
85
+ state: { count: 0 },
86
+ constraints: { count: { min: 0, max: 10 } },
87
+ mutations: {
88
+ increment: (state) => ({ count: state.count + 1 }),
89
+ // count is automatically clamped to 10 — no need to check here
90
+ }
91
+ }`, 'js'))}
92
+
93
+ ${section('forms', 'Mutations and forms')}
94
+ <p>Mirroring every keystroke into state via <code>data-event="input:..."</code> causes <code>innerHTML</code> replacement on each keypress, which destroys input focus. Pulse prevents this by keeping form inputs uncontrolled — values are captured via <code>FormData</code> in an action's <code>onStart</code>, before the view re-renders:</p>
95
+ ${codeBlock(highlight(`<!-- mirroring every keystroke causes focus loss -->
96
+ <input data-event="input:setEmail">
97
+
98
+ <!-- uncontrolled: values captured at submit via FormData -->
99
+ <form data-action="submit">
100
+ <input name="email" type="email">
101
+ <button>Submit</button>
102
+ </form>`, 'html'))}
103
+ ${callout('tip', 'Mutations are the right tool for binary UI state (toggles, step counters, tab selections) and controls where a re-render on each change is acceptable — such as live character counters or filtered lists. For form submission, use actions.')}
104
+ `,
105
+ }),
106
+ }
@@ -0,0 +1,85 @@
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('/navigation')
6
+
7
+ export default {
8
+ route: '/navigation',
9
+ meta: {
10
+ title: 'Navigation — Pulse Docs',
11
+ description: 'Client-side navigation in Pulse — how link interception, JSON responses, and spec re-mounting work.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/navigation',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Navigation')}
21
+ ${lead('Client-side navigation in Pulse requires no configuration. When hydration is active, same-origin link clicks are intercepted automatically — the server renders the new page and returns JSON, and Pulse swaps the content without a full reload. If anything fails, it falls back to standard browser navigation.')}
22
+
23
+ ${section('how-it-works', 'How it works')}
24
+ <p>When <code>initNavigation</code> is called (part of the hydration bootstrap), Pulse attaches a click listener to the document. When a same-origin <code>&lt;a&gt;</code> is clicked:</p>
25
+ <ol>
26
+ <li>The default navigation is prevented.</li>
27
+ <li>A fetch request is sent to the new URL with the header <code>X-Pulse-Navigate: true</code>.</li>
28
+ <li>The server renders the page and returns a JSON response instead of full HTML.</li>
29
+ <li>Pulse replaces the current page's root <code>innerHTML</code> with the new content and updates <code>document.title</code>.</li>
30
+ <li>The new page's spec bundle is dynamically imported.</li>
31
+ <li><code>mount()</code> is called to bind the new spec's events.</li>
32
+ <li>The browser history is updated with <code>history.pushState</code>.</li>
33
+ </ol>
34
+ ${callout('note', 'If any step fails — network error, missing bundle, unexpected response — Pulse falls back to <code>location.href = url</code> for a standard full-page navigation.')}
35
+
36
+ ${section('init-navigation', 'initNavigation')}
37
+ <p><code>initNavigation</code> is called automatically by the hydration bootstrap script. You do not call it manually in application code. It is exported from the runtime for use in custom bootstrap scenarios:</p>
38
+ ${codeBlock(highlight(`import { mount } from '@invisibleloop/pulse/runtime'
39
+ import { initNavigation } from '@invisibleloop/pulse/navigate'
40
+
41
+ mount(spec, root, window.__PULSE_SERVER__ || {}, { ssr: true })
42
+ initNavigation(root, mount)`, 'js'))}
43
+
44
+ ${section('json-response', 'JSON response shape')}
45
+ <p>When Pulse receives a request with <code>X-Pulse-Navigate: true</code>, it renders the page and returns:</p>
46
+ ${codeBlock(highlight(`{
47
+ "html": "<main>...the page content...</main>",
48
+ "title": "New Page Title — Site",
49
+ "hydrate": "/dist/new-page.boot-abc123.js",
50
+ "serverState": { "key": "value" }
51
+ }`, 'js'))}
52
+ ${table(
53
+ ['Field', 'Description'],
54
+ [
55
+ ['<code>html</code>', 'The rendered page content (the output of the view function, without the full document wrapper).'],
56
+ ['<code>title</code>', 'The new page title, set via <code>document.title</code>.'],
57
+ ['<code>hydrate</code>', 'The bundle path for the new spec. <code>null</code> if the page has no hydration.'],
58
+ ['<code>serverState</code>', 'The server data for the new page, used when mounting the new spec.'],
59
+ ]
60
+ )}
61
+
62
+ ${section('link-interception', 'Which links are intercepted')}
63
+ <p>Only same-origin links are intercepted. Links with <code>target="_blank"</code>, <code>download</code>, <code>rel="external"</code>, or any cross-origin <code>href</code> are ignored and behave normally:</p>
64
+ ${codeBlock(highlight(`<!-- Intercepted by Pulse client navigation -->
65
+ <a href="/about">About</a>
66
+ <a href="/products/42">Product</a>
67
+
68
+ <!-- NOT intercepted — standard browser navigation -->
69
+ <a href="https://example.com">External</a>
70
+ <a href="/report.pdf" download>Download</a>
71
+ <a href="/admin" target="_blank">Open in new tab</a>`, 'html'))}
72
+
73
+ ${section('history', 'Browser history')}
74
+ <p>Pulse uses the History API (<code>history.pushState</code>) to update the URL after each navigation. The back and forward buttons work as expected — each history entry corresponds to a page navigation.</p>
75
+ <p>When the user navigates back, Pulse receives a <code>popstate</code> event and performs the same fetch-and-swap process for the previous URL.</p>
76
+
77
+ ${section('no-hydration', 'Navigation without hydration')}
78
+ <p>Client-side navigation only works on pages that have loaded the Pulse client runtime (i.e. pages with a <code>hydrate</code> path). If a user navigates from a hydrated page to a non-hydrated page, Pulse falls back to a full page load.</p>
79
+ ${callout('tip', 'For documentation sites or mostly-static apps, omitting <code>hydrate</code> entirely and relying on standard browser navigation is simpler and keeps the JS payload at zero.')}
80
+
81
+ ${section('scroll', 'Scroll behaviour')}
82
+ <p>After each client-side navigation, Pulse scrolls to the top of the page — matching the behaviour of a full page load. If the URL includes a hash (e.g. <code>/docs#section</code>), Pulse scrolls to the target element.</p>
83
+ `,
84
+ }),
85
+ }