@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,65 @@
1
+ /**
2
+ * Pulse UI — Image
3
+ *
4
+ * Responsive image with optional aspect-ratio crop, caption, and rounded corners.
5
+ * Always uses loading="lazy" and decoding="async".
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.src - Image source URL
9
+ * @param {string} opts.alt - Alt text (required for accessibility)
10
+ * @param {string} opts.caption - Optional figcaption text
11
+ * @param {string} opts.ratio - CSS aspect-ratio string e.g. '16/9', '4/3', '1/1'
12
+ * @param {boolean} opts.rounded - Larger corner radius (1rem) — for photos, cards, book covers
13
+ * @param {boolean} opts.pill - Full pill/stadium radius (999px) — for avatars or circular crops
14
+ * @param {number|string} opts.width - img width attribute (browser hint only)
15
+ * @param {number|string} opts.height - img height attribute
16
+ * @param {number|string} opts.maxWidth - CSS max-width on the figure (px value or CSS string). Use this to constrain portrait/narrow images inside wide columns.
17
+ * @param {string} opts.class
18
+ */
19
+
20
+ import { escHtml as e } from '../html.js'
21
+
22
+ export function uiImage({
23
+ src = '',
24
+ alt = '',
25
+ caption = '',
26
+ ratio = '',
27
+ rounded = false,
28
+ pill = false,
29
+ width = '',
30
+ height = '',
31
+ maxWidth = '',
32
+ class: cls = '',
33
+ } = {}) {
34
+ // The outer figure uses display:contents so the inner crop div
35
+ // becomes a direct flex/block child of whatever container holds it.
36
+ // This is required for aspect-ratio to resolve correctly in flex contexts.
37
+ const figClasses = ['ui-image', rounded ? 'ui-image--rounded' : '', pill ? 'ui-image--pill' : '', cls].filter(Boolean).join(' ')
38
+
39
+ const maxWidthStyle = maxWidth
40
+ ? ` style="max-width:${e(typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)};margin-left:auto;margin-right:auto"`
41
+ : ''
42
+
43
+ const widthAttr = width ? ` width="${e(String(width))}"` : ''
44
+ const heightAttr = height ? ` height="${e(String(height))}"` : ''
45
+
46
+ const captionHtml = caption
47
+ ? `<figcaption class="ui-image-caption">${e(caption)}</figcaption>`
48
+ : ''
49
+
50
+ if (ratio) {
51
+ return `<figure class="${e(figClasses)}"${maxWidthStyle}>
52
+ <div class="ui-image-crop">
53
+ <img src="${e(src)}" alt="${e(alt)}" class="ui-image-img--cover" style="aspect-ratio:${e(ratio)}"${widthAttr}${heightAttr} loading="lazy" decoding="async">
54
+ </div>
55
+ ${captionHtml}
56
+ </figure>`
57
+ }
58
+
59
+ return `<figure class="${e(figClasses)}"${maxWidthStyle}>
60
+ <div class="ui-image-wrap">
61
+ <img src="${e(src)}" alt="${e(alt)}" class="ui-image-img"${widthAttr}${heightAttr} loading="lazy" decoding="async">
62
+ </div>
63
+ ${captionHtml}
64
+ </figure>`
65
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noEmit": true,
4
+ "strict": true,
5
+ "moduleResolution": "node16",
6
+ "module": "node16",
7
+ "target": "esnext",
8
+ "lib": ["esnext", "dom"],
9
+ "types": ["node"],
10
+ "allowJs": false
11
+ },
12
+ "include": ["types/**/*.d.ts"]
13
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Pulse — HTML escaping types
3
+ * @invisibleloop/pulse/html
4
+ */
5
+
6
+ /**
7
+ * Escape HTML special characters for safe embedding in HTML attributes or text.
8
+ *
9
+ * Always use this around values from user input, URL params, or external APIs.
10
+ * Omitting it is an XSS vulnerability.
11
+ *
12
+ * @example
13
+ * import { escHtml } from '@invisibleloop/pulse/html'
14
+ *
15
+ * view: (state) => `<p>Hello, ${escHtml(state.username)}</p>`
16
+ */
17
+ export function escHtml(str: unknown): string
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Pulse — Image helper types
3
+ * @invisibleloop/pulse/image
4
+ */
5
+
6
+ export interface ImgOptions {
7
+ /** Image URL */
8
+ src: string
9
+ /** Alt text (required for accessibility) */
10
+ alt: string
11
+ /** Intrinsic width in px — include to prevent CLS */
12
+ width?: number
13
+ /** Intrinsic height in px — include to prevent CLS */
14
+ height?: number
15
+ /**
16
+ * Set true for the LCP image (above-the-fold hero).
17
+ * Adds loading="eager" and fetchpriority="high".
18
+ */
19
+ priority?: boolean
20
+ /** CSS class applied to <img> */
21
+ class?: string
22
+ }
23
+
24
+ export interface PictureSource {
25
+ /** URL for this format variant */
26
+ src: string
27
+ /** MIME type, e.g. 'image/avif', 'image/webp' */
28
+ type: string
29
+ }
30
+
31
+ export interface PictureOptions extends ImgOptions {
32
+ /**
33
+ * Modern format sources in preference order (AVIF first, then WebP).
34
+ * The src on ImgOptions is the universal fallback (JPEG/PNG).
35
+ *
36
+ * @example
37
+ * sources: [
38
+ * { src: '/hero.avif', type: 'image/avif' },
39
+ * { src: '/hero.webp', type: 'image/webp' },
40
+ * ]
41
+ */
42
+ sources?: PictureSource[]
43
+ }
44
+
45
+ /**
46
+ * Generate an optimised <img> tag.
47
+ * Always include width + height to prevent CLS.
48
+ * Use priority: true for the LCP image.
49
+ *
50
+ * @example
51
+ * import { img } from '@invisibleloop/pulse/image'
52
+ * img({ src: '/hero.jpg', alt: 'Hero', width: 1200, height: 600, priority: true })
53
+ */
54
+ export function img(options: ImgOptions): string
55
+
56
+ /**
57
+ * Generate a <picture> element with modern format sources and a fallback <img>.
58
+ * Provide sources in preference order (AVIF first, then WebP, then the fallback src).
59
+ *
60
+ * @example
61
+ * import { picture } from '@invisibleloop/pulse/image'
62
+ * picture({
63
+ * src: '/hero.jpg', alt: 'Hero', width: 1200, height: 600,
64
+ * sources: [
65
+ * { src: '/hero.avif', type: 'image/avif' },
66
+ * { src: '/hero.webp', type: 'image/webp' },
67
+ * ]
68
+ * })
69
+ */
70
+ export function picture(options: PictureOptions): string
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pulse — root export types
3
+ * @invisibleloop/pulse
4
+ *
5
+ * The root export is the server. Import sub-paths for runtime, SSR, etc.
6
+ */
7
+ export * from './server.js'
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pulse — Client-side navigation types
3
+ * @invisibleloop/pulse/navigate
4
+ */
5
+
6
+ import type { MountResult } from './runtime.js'
7
+ import type { PulseSpec } from './schema.js'
8
+
9
+ /**
10
+ * Initialise client-side navigation for a Pulse app.
11
+ *
12
+ * Intercepts same-origin <a> clicks and browser back/forward events.
13
+ * Fetches new pages as JSON fragments (via X-Pulse-Navigate header),
14
+ * swaps #pulse-root, and re-mounts the new spec without a full page reload.
15
+ * Falls back to location.href on any fetch or import error.
16
+ *
17
+ * Call once after the initial mount().
18
+ *
19
+ * @param root The #pulse-root element
20
+ * @param mountFn The mount() function from @invisibleloop/pulse/runtime
21
+ *
22
+ * @example
23
+ * import { mount } from '@invisibleloop/pulse/runtime'
24
+ * import { initNavigation } from '@invisibleloop/pulse/navigate'
25
+ *
26
+ * const root = document.getElementById('pulse-root')
27
+ * const m = mount(spec, root, window.__PULSE_SERVER__ ?? {}, { ssr: true })
28
+ * initNavigation(root, mount)
29
+ */
30
+ export function initNavigation(
31
+ root: HTMLElement,
32
+ mountFn: (
33
+ spec: PulseSpec,
34
+ el: HTMLElement,
35
+ serverState?: Record<string, unknown>,
36
+ options?: { ssr?: boolean; store?: unknown }
37
+ ) => MountResult
38
+ ): void
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Pulse — Client runtime types
3
+ * @invisibleloop/pulse/runtime
4
+ */
5
+
6
+ import type { PulseSpec, PulseStoreDefinition } from './server.js'
7
+
8
+ export type { PulseSpec } from './schema.js'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // mount
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export interface MountOptions {
15
+ /**
16
+ * Set to true when mounting after SSR — skips the initial render and only
17
+ * binds event listeners. Preserves the SSR-painted LCP element and avoids a
18
+ * re-render flash. Always true in production bundle boots.
19
+ */
20
+ ssr?: boolean
21
+
22
+ /**
23
+ * Store definition (default export from pulse.store.js).
24
+ * Pass to the first mount() call on a page that uses spec.store.
25
+ */
26
+ store?: PulseStoreDefinition
27
+ }
28
+
29
+ export interface MountResult {
30
+ /** Destroy this mount, remove event listeners, and unsubscribe from the store. */
31
+ destroy(): void
32
+ }
33
+
34
+ /**
35
+ * Mount a Pulse spec to a DOM element.
36
+ *
37
+ * Binds events, applies mutations, enforces constraints, and re-renders the
38
+ * view on every state change. Call once per page load, then call destroy()
39
+ * before mounting a new spec during client-side navigation.
40
+ *
41
+ * @example
42
+ * import spec from './src/pages/counter.js'
43
+ * import { mount } from '@invisibleloop/pulse/runtime'
44
+ *
45
+ * const root = document.getElementById('pulse-root')
46
+ * mount(spec, root, window.__PULSE_SERVER__ ?? {}, { ssr: true })
47
+ */
48
+ export function mount(
49
+ spec: PulseSpec,
50
+ el: HTMLElement,
51
+ serverState?: Record<string, unknown>,
52
+ options?: MountOptions
53
+ ): MountResult
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Utility exports
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /** Debounce a function — returns a new function that delays invocation. */
60
+ export function debounce<T extends (...args: unknown[]) => unknown>(fn: T, ms: number): T
61
+
62
+ /** Throttle a function — returns a new function that limits invocation rate. */
63
+ export function throttle<T extends (...args: unknown[]) => unknown>(fn: T, ms: number): T
@@ -0,0 +1,243 @@
1
+ /// <reference types="node" />
2
+
3
+ /**
4
+ * Pulse — Core types
5
+ * Shared across server, runtime, and SSR.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Request context
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface RequestContext {
13
+ /** Route params extracted from the URL pattern, e.g. { id: '42' } */
14
+ params: Record<string, string>
15
+ /** Parsed query string, e.g. { q: 'widget' } */
16
+ query: Record<string, string>
17
+ /** Raw request headers */
18
+ headers: Record<string, string | string[] | undefined>
19
+ /** URL pathname */
20
+ pathname: string
21
+ /** HTTP method */
22
+ method: string
23
+ /** CSP nonce generated per request — pass to inline <script nonce="…"> */
24
+ nonce: string
25
+ /** Parsed cookies from the request Cookie header */
26
+ cookies: Record<string, string>
27
+ /** Brand config resolved by resolveBrand (undefined if not configured) */
28
+ brand?: unknown
29
+ /** Global store state for keys declared in spec.store */
30
+ store?: Record<string, unknown>
31
+ /** Active fetcher timeout in ms (null = no limit) */
32
+ fetcherTimeout: number | null
33
+
34
+ /** Parse a JSON request body. Returns null for an empty body. */
35
+ json(): Promise<Record<string, unknown> | null>
36
+ /** Read the body as a plain string. */
37
+ text(): Promise<string>
38
+ /** Parse a URL-encoded body into a plain object. Returns null for empty. */
39
+ formData(): Promise<Record<string, string> | null>
40
+ /** Read the raw body as a Node.js Buffer. */
41
+ buffer(): Promise<Buffer>
42
+
43
+ /**
44
+ * Queue a Set-Cookie header on the response.
45
+ * Defaults: Path=/, SameSite=Lax.
46
+ */
47
+ setCookie(
48
+ name: string,
49
+ value: string,
50
+ opts?: {
51
+ httpOnly?: boolean
52
+ secure?: boolean
53
+ path?: string
54
+ maxAge?: number
55
+ sameSite?: 'Strict' | 'Lax' | 'None'
56
+ domain?: string
57
+ }
58
+ ): void
59
+
60
+ /** Queue an arbitrary response header. */
61
+ setHeader(name: string, value: string): void
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Guard return value
66
+ // ---------------------------------------------------------------------------
67
+
68
+ export type GuardResult =
69
+ | { redirect: string }
70
+ | { status: number; json?: unknown; body?: string; headers?: Record<string, string> }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Spec sub-types
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export interface ValidationRule {
77
+ required?: boolean
78
+ minLength?: number
79
+ maxLength?: number
80
+ min?: number
81
+ max?: number
82
+ format?: 'email' | 'url' | 'numeric'
83
+ pattern?: RegExp
84
+ }
85
+
86
+ export interface StreamConfig {
87
+ /** Segment keys rendered in the first flush */
88
+ shell: string[]
89
+ /** Segment keys rendered after their async data resolves */
90
+ deferred?: string[]
91
+ }
92
+
93
+ export interface ActionConfig<S extends object = Record<string, unknown>> {
94
+ /** Run validation (spec.validation) before executing. Default false. */
95
+ validate?: boolean
96
+ /** Called before run() — optimistic update */
97
+ onStart?: (state: S, payload?: unknown) => Partial<S>
98
+ /** Async operation. Return value is passed to onSuccess. */
99
+ run: (state: S, server?: unknown, payload?: unknown) => Promise<unknown>
100
+ /** Called on success. Return partial state to merge. */
101
+ onSuccess: (state: S, payload: unknown) => Partial<S> & { _toast?: ToastOptions; _storeUpdate?: Record<string, unknown> }
102
+ /** Called on error. Return partial state to merge. */
103
+ onError: (state: S, err: Error) => Partial<S> & { _toast?: ToastOptions }
104
+ }
105
+
106
+ export interface ToastOptions {
107
+ message: string
108
+ variant?: 'success' | 'error' | 'warning' | 'info'
109
+ duration?: number
110
+ }
111
+
112
+ export interface CacheConfig {
113
+ public?: boolean
114
+ maxAge?: number
115
+ staleWhileRevalidate?: number
116
+ }
117
+
118
+ export interface MetaConfig {
119
+ title?: string | ((ctx: RequestContext) => string)
120
+ description?: string | ((ctx: RequestContext) => string)
121
+ styles?: string[] | ((ctx: RequestContext) => string[])
122
+ ogTitle?: string | ((ctx: RequestContext) => string)
123
+ ogImage?: string | ((ctx: RequestContext) => string)
124
+ schema?: Record<string, unknown>
125
+ theme?: 'light' | 'dark'
126
+ [key: string]: unknown
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // PulseSpec
131
+ // ---------------------------------------------------------------------------
132
+
133
+ export interface PulseSpec<S extends object = Record<string, unknown>> {
134
+ /** URL pattern. Supports :param segments. */
135
+ route: string
136
+
137
+ /**
138
+ * Initial client-side state. Deep-cloned on mount.
139
+ * Required for page specs; omit only for raw content specs (contentType set).
140
+ */
141
+ state: S
142
+
143
+ /**
144
+ * Pure function returning an HTML string.
145
+ * Receives (clientState, serverState).
146
+ * Can also be a keyed object of segment functions for streaming SSR.
147
+ */
148
+ view:
149
+ | ((state: S, server?: Record<string, unknown>) => string)
150
+ | Record<string, (state: S, server?: Record<string, unknown>) => string>
151
+
152
+ /**
153
+ * Browser-importable path to this spec file.
154
+ * Required for any spec with mutations, actions, or persist.
155
+ */
156
+ hydrate?: string
157
+
158
+ /** Streaming SSR config — split view into shell and deferred segments. */
159
+ stream?: StreamConfig
160
+
161
+ /** Server-side data fetchers. Results passed as second arg to view. */
162
+ server?: Record<string, (ctx: RequestContext) => Promise<unknown> | unknown>
163
+
164
+ /**
165
+ * Raw content spec — set Content-Type and implement render() instead of view().
166
+ * Accepts any HTTP method. Use for RSS feeds, JSON APIs, sitemaps, webhooks.
167
+ */
168
+ contentType?: string
169
+
170
+ /**
171
+ * Required when contentType is set.
172
+ * Receives (ctx, server) and returns the raw response body string.
173
+ */
174
+ render?: (ctx: RequestContext, server?: Record<string, unknown>) => Promise<string> | string
175
+
176
+ /** Page metadata for the <head>. Fields may be functions for per-request resolution. */
177
+ meta?: MetaConfig
178
+
179
+ /** Synchronous state updaters. Each returns a partial state to merge. */
180
+ mutations?: Record<string, (state: S, event?: Event | unknown) => Partial<S>>
181
+
182
+ /** Async operations with full lifecycle hooks. */
183
+ actions?: Record<string, ActionConfig<S>>
184
+
185
+ /** Declarative validation rules keyed by dot-path state keys. */
186
+ validation?: Record<string, ValidationRule>
187
+
188
+ /** Min/max bounds clamped after every mutation. Cannot be bypassed. */
189
+ constraints?: Record<string, { min?: number; max?: number }>
190
+
191
+ /** State keys to persist in localStorage between visits. */
192
+ persist?: string[]
193
+
194
+ /** Global store keys this page subscribes to. Appear in view's server arg. */
195
+ store?: string[]
196
+
197
+ /** HTTP methods this page accepts. Default ['GET', 'HEAD']. */
198
+ methods?: Array<'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'>
199
+
200
+ /**
201
+ * Called before server data fetchers on every request.
202
+ * Return { redirect } to redirect, { status, json } for a custom response,
203
+ * or nothing to allow the request to proceed.
204
+ */
205
+ guard?: (ctx: RequestContext) => Promise<GuardResult | void> | GuardResult | void
206
+
207
+ /** HTTP Cache-Control header config for the page response. */
208
+ cache?: CacheConfig
209
+
210
+ /** Seconds to cache server.data() result in-process. */
211
+ serverTtl?: number
212
+
213
+ /** Timeout in ms for all server fetchers on this page. Overrides createServer fetcherTimeout. */
214
+ serverTimeout?: number
215
+
216
+ /**
217
+ * Fallback renderer called when view() throws at runtime.
218
+ * Return an HTML string to display instead of the default error message.
219
+ * On the server, if not defined, view errors propagate to the 500 handler.
220
+ */
221
+ onViewError?: (err: Error, state: S, server?: Record<string, unknown>) => string
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // Validation utilities
226
+ // ---------------------------------------------------------------------------
227
+
228
+ export interface ValidationResult {
229
+ valid: boolean
230
+ errors: string[]
231
+ }
232
+
233
+ /** Validate a spec. Returns { valid, errors }. Does not throw. */
234
+ export function validateSpec(spec: unknown): ValidationResult
235
+
236
+ /** Validate a spec. Throws with all errors if invalid. */
237
+ export function assertValidSpec(spec: unknown): void
238
+
239
+ /** Returns the segment keys defined in spec.view. ['default'] for a function view. */
240
+ export function getViewSegments(spec: PulseSpec): string[]
241
+
242
+ /** Returns { shell, deferred } stream order. All segments in shell if no stream config. */
243
+ export function getStreamOrder(spec: PulseSpec): { shell: string[]; deferred: string[] }
@@ -0,0 +1,145 @@
1
+ /// <reference types="node" />
2
+
3
+ /**
4
+ * Pulse — Server API types
5
+ * @invisibleloop/pulse or @invisibleloop/pulse/server
6
+ */
7
+
8
+ import type { IncomingMessage, ServerResponse, Server } from 'http'
9
+ import type { PulseSpec } from './schema.js'
10
+
11
+ export type { PulseSpec, RequestContext, ValidationRule, StreamConfig, ActionConfig,
12
+ GuardResult, CacheConfig, MetaConfig, ToastOptions, ValidationResult } from './schema.js'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // createServer options
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface ServerOptions {
19
+ /** Port to listen on. Default 3000. */
20
+ port?: number
21
+
22
+ /** Enable streaming SSR globally. Default true. */
23
+ stream?: boolean
24
+
25
+ /** Path to a directory of static files served at their relative path. */
26
+ staticDir?: string
27
+
28
+ /** Explicit manifest path or object. Overrides auto-detection from staticDir/dist/manifest.json. */
29
+ manifest?: string | Record<string, string> | null
30
+
31
+ /**
32
+ * Trailing slash behaviour. Default 'remove'.
33
+ * 'remove' — 301 /about/ → /about
34
+ * 'add' — 301 /about → /about/
35
+ * 'allow' — serve both, no redirect
36
+ */
37
+ trailingSlash?: 'remove' | 'add' | 'allow'
38
+
39
+ /** Maximum request body size in bytes. Default 1 MB. Requests exceeding this receive a 413. */
40
+ maxBody?: number
41
+
42
+ /**
43
+ * Default HTML cache TTL for all pages in production.
44
+ * true = 1 h + 24 h SWR, number = max-age in seconds,
45
+ * object = { public, maxAge, staleWhileRevalidate }.
46
+ * spec.cache overrides per page.
47
+ */
48
+ defaultCache?: boolean | number | { public?: boolean; maxAge?: number; staleWhileRevalidate?: number } | null
49
+
50
+ /** Global timeout in ms for all server fetchers. null = no limit. Override per page with spec.serverTimeout. */
51
+ fetcherTimeout?: number | null
52
+
53
+ /**
54
+ * Multi-brand support. Called once per host (cached 60 s).
55
+ * Result is attached to ctx.brand and available in guard, server, and meta functions.
56
+ */
57
+ resolveBrand?: (host: string) => Promise<unknown> | unknown
58
+
59
+ /**
60
+ * Milliseconds to wait for in-flight requests to complete during graceful
61
+ * shutdown before force-exiting. Default 30 000 ms.
62
+ */
63
+ shutdownTimeout?: number
64
+
65
+ /**
66
+ * Path for the built-in health check endpoint. Default '/healthz'.
67
+ * Returns { status: 'ok', uptime: number } — bypasses onRequest so load
68
+ * balancers always get a response. Set to false to disable.
69
+ */
70
+ healthCheck?: string | false
71
+
72
+ /** Called on every request before routing. Return false to short-circuit Pulse handling. */
73
+ onRequest?: (req: IncomingMessage, res: ServerResponse) => false | void | unknown
74
+
75
+ /**
76
+ * Extra CSP sources to merge into the framework's default Content-Security-Policy.
77
+ * Use this to allow external stylesheets, fonts, or API origins.
78
+ * Each key is a CSP directive name; each value is an array of sources to append.
79
+ * @example
80
+ * // Allow Google Fonts
81
+ * csp: { 'style-src': ['https://fonts.googleapis.com'], 'font-src': ['https://fonts.gstatic.com'] }
82
+ */
83
+ csp?: Record<string, string[]>
84
+
85
+ /** Called on unhandled errors. Receives (err, req, res). */
86
+ onError?: (err: Error, req: IncomingMessage, res: ServerResponse) => void
87
+
88
+ /** Global store definition (default export from pulse.store.js). */
89
+ store?: PulseStoreDefinition | null
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Store (used via server options)
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export interface PulseStoreDefinition {
97
+ hydrate?: string
98
+ state: Record<string, unknown>
99
+ server?: Record<string, (ctx: import('./schema.js').RequestContext) => Promise<unknown> | unknown>
100
+ mutations?: Record<string, (state: Record<string, unknown>, payload?: unknown) => Partial<Record<string, unknown>>>
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // createServer return value
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export interface ServerInstance {
108
+ /** The underlying Node.js http.Server */
109
+ server: Server
110
+
111
+ /**
112
+ * Gracefully shut down the server.
113
+ * Stops accepting new connections, destroys idle keep-alive sockets, lets
114
+ * in-flight requests finish, then force-exits after shutdownTimeout ms.
115
+ * SIGTERM and SIGINT are already wired up automatically.
116
+ * Idempotent — safe to call multiple times.
117
+ */
118
+ shutdown(): void
119
+
120
+ /** Swap the spec list at runtime (used by the dev server for hot reload). */
121
+ updateSpecs(specs: PulseSpec[]): void
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // createServer
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * Create and start a Pulse HTTP server.
130
+ *
131
+ * All specs are validated at startup — an invalid spec throws before the
132
+ * server accepts any connections.
133
+ *
134
+ * @example
135
+ * import { createServer } from '@invisibleloop/pulse'
136
+ * import home from './src/pages/home.js'
137
+ * import contact from './src/pages/contact.js'
138
+ *
139
+ * const { server, shutdown } = createServer([home, contact], {
140
+ * port: 3000,
141
+ * stream: true,
142
+ * staticDir: 'public',
143
+ * })
144
+ */
145
+ export function createServer(specs: PulseSpec[], options?: ServerOptions): ServerInstance