@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,227 @@
1
+ import { renderComponentPage, demo } from '../../lib/component-page.js'
2
+ import { prevNext } from '../../lib/nav.js'
3
+ import { table } from '../../lib/layout.js'
4
+ import { timeline, timelineItem, badge, card, stat, button } from '../../../../src/ui/index.js'
5
+
6
+ const { prev, next } = prevNext('/components/timeline')
7
+
8
+ // Shared check SVG for reuse
9
+ const checkIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`
10
+ const starIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`
11
+ const alertIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`
12
+
13
+ export default {
14
+ route: '/components/timeline',
15
+ meta: {
16
+ title: 'Timeline — Pulse Docs',
17
+ description: 'Timeline component for Pulse UI.',
18
+ styles: ['/pulse-ui.css', '/docs.css'],
19
+ },
20
+ state: {},
21
+ view: () => renderComponentPage({
22
+ currentHref: '/components/timeline',
23
+ prev,
24
+ next,
25
+ name: 'timeline',
26
+ description: 'Ordered sequence of events or steps connected by a line. Supports vertical (default) and horizontal orientations. Each item accepts a raw HTML content slot — pass any text, component, or markup.',
27
+ content: `
28
+
29
+ <h2 class="doc-h2" id="vertical">Vertical (default)</h2>
30
+ <p>Steps flow downward. The connector line links each dot to the next.</p>
31
+ ${demo(
32
+ timeline({
33
+ items: [
34
+ { label: 'Jan 2023', content: '<strong style="color:var(--ui-text)">Project kicked off</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Initial scope agreed with stakeholders. Repository created.</p>' },
35
+ { label: 'Mar 2023', content: '<strong style="color:var(--ui-text)">Alpha release</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Internal testing with 12 pilot users. Core features stable.</p>' },
36
+ { label: 'Jun 2023', content: '<strong style="color:var(--ui-text)">Public beta</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Open sign-up enabled. 400 users in first week.</p>' },
37
+ { label: 'Sep 2023', content: '<strong style="color:var(--ui-text)">v1.0 launched</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Billing live, docs published, ProductHunt launch.</p>' },
38
+ ],
39
+ }),
40
+ `timeline({
41
+ items: [
42
+ { label: 'Jan 2023', content: '<strong>Project kicked off</strong><p>Initial scope agreed.</p>' },
43
+ { label: 'Mar 2023', content: '<strong>Alpha release</strong><p>Internal testing with 12 pilot users.</p>' },
44
+ { label: 'Jun 2023', content: '<strong>Public beta</strong><p>Open sign-up enabled. 400 users in first week.</p>' },
45
+ { label: 'Sep 2023', content: '<strong>v1.0 launched</strong><p>Billing live, docs published.</p>' },
46
+ ],
47
+ })`
48
+ )}
49
+
50
+ <h2 class="doc-h2" id="horizontal">Horizontal</h2>
51
+ <p>Steps flow left to right — good for process flows or numbered stages.</p>
52
+ ${demo(
53
+ timeline({
54
+ direction: 'horizontal',
55
+ items: [
56
+ { label: 'Step 1', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Sign up</p>' },
57
+ { label: 'Step 2', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Connect data</p>' },
58
+ { label: 'Step 3', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Invite team</p>' },
59
+ { label: 'Step 4', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Go live</p>' },
60
+ ],
61
+ }),
62
+ `timeline({
63
+ direction: 'horizontal',
64
+ items: [
65
+ { label: 'Step 1', content: '<p>Sign up</p>' },
66
+ { label: 'Step 2', content: '<p>Connect data</p>' },
67
+ { label: 'Step 3', content: '<p>Invite team</p>' },
68
+ { label: 'Step 4', content: '<p>Go live</p>' },
69
+ ],
70
+ })`
71
+ )}
72
+
73
+ <h2 class="doc-h2" id="dot-colors">Dot colours</h2>
74
+ <p>Use <code>dotColor</code> to convey status: <code>'accent'</code> · <code>'success'</code> · <code>'warning'</code> · <code>'error'</code> · <code>'muted'</code>.</p>
75
+ ${demo(
76
+ timeline({
77
+ items: [
78
+ { dotColor: 'success', label: 'Deployed', content: '<p style="color:var(--ui-muted);margin:0">Production deploy completed successfully.</p>' },
79
+ { dotColor: 'success', label: 'Tested', content: '<p style="color:var(--ui-muted);margin:0">All 92 tests passed. Coverage 98%.</p>' },
80
+ { dotColor: 'warning', label: 'Review', content: '<p style="color:var(--ui-muted);margin:0">Awaiting sign-off from design lead.</p>' },
81
+ { dotColor: 'error', label: 'Blocked', content: '<p style="color:var(--ui-muted);margin:0">Dependency on payment API not yet ready.</p>' },
82
+ { dotColor: 'muted', label: 'Planned', content: '<p style="color:var(--ui-muted);margin:0">Mobile app release — Q1 2025.</p>' },
83
+ ],
84
+ }),
85
+ `timeline({
86
+ items: [
87
+ { dotColor: 'success', label: 'Deployed', content: '...' },
88
+ { dotColor: 'success', label: 'Tested', content: '...' },
89
+ { dotColor: 'warning', label: 'Review', content: '...' },
90
+ { dotColor: 'error', label: 'Blocked', content: '...' },
91
+ { dotColor: 'muted', label: 'Planned', content: '...' },
92
+ ],
93
+ })`
94
+ )}
95
+
96
+ <h2 class="doc-h2" id="icon-dots">Icon dots</h2>
97
+ <p>Pass any SVG or emoji as <code>dot</code>. The dot grows to accommodate the content and uses a tinted background matching its colour variant.</p>
98
+ ${demo(
99
+ timeline({
100
+ items: [
101
+ { dot: checkIcon, dotColor: 'success', label: 'Completed', content: '<strong style="color:var(--ui-text)">Onboarding</strong><p style="color:var(--ui-muted);margin:.2rem 0 0">Profile set up, preferences saved.</p>' },
102
+ { dot: starIcon, dotColor: 'accent', label: 'Milestone', content: '<strong style="color:var(--ui-text)">First 1,000 users</strong><p style="color:var(--ui-muted);margin:.2rem 0 0">Reached organically in 18 days.</p>' },
103
+ { dot: alertIcon, dotColor: 'warning', label: 'Incident', content: '<strong style="color:var(--ui-text)">Partial outage</strong><p style="color:var(--ui-muted);margin:.2rem 0 0">CDN edge node failed — resolved in 4 minutes.</p>' },
104
+ ],
105
+ }),
106
+ `timeline({
107
+ items: [
108
+ {
109
+ dot: checkSvg,
110
+ dotColor: 'success',
111
+ label: 'Completed',
112
+ content: '<strong>Onboarding</strong><p>Profile set up, preferences saved.</p>',
113
+ },
114
+ {
115
+ dot: starSvg,
116
+ dotColor: 'accent',
117
+ label: 'Milestone',
118
+ content: '<strong>First 1,000 users</strong>',
119
+ },
120
+ ],
121
+ })`
122
+ )}
123
+
124
+ <h2 class="doc-h2" id="rich-content">Rich content slot</h2>
125
+ <p>The <code>content</code> slot accepts any HTML — including other Pulse components like <code>card()</code>, <code>badge()</code>, or <code>stat()</code>.</p>
126
+ ${demo(
127
+ timeline({
128
+ items: [
129
+ {
130
+ dotColor: 'success',
131
+ label: 'Q1 2024',
132
+ content: card({
133
+ title: 'Series A closed',
134
+ content: `<div style="display:flex;gap:1.5rem;flex-wrap:wrap">` +
135
+ stat({ label: 'Raised', value: '$4.2M' }) +
136
+ stat({ label: 'Valuation', value: '$18M' }) +
137
+ stat({ label: 'Investors', value: '6' }) +
138
+ `</div>`,
139
+ }),
140
+ },
141
+ {
142
+ dotColor: 'accent',
143
+ label: 'Q3 2024',
144
+ content: card({
145
+ title: 'Product launch',
146
+ content: `<p style="color:var(--ui-muted);margin:0 0 .75rem">Shipped v1.0 to general availability. Three tiers, 14-day trial.</p>` +
147
+ `<div style="display:flex;gap:.5rem;flex-wrap:wrap">` +
148
+ badge({ label: 'Launch', variant: 'info' }) +
149
+ badge({ label: 'Billing live', variant: 'success' }) +
150
+ `</div>`,
151
+ }),
152
+ },
153
+ {
154
+ dotColor: 'muted',
155
+ label: 'Q1 2025 (planned)',
156
+ content: card({
157
+ title: 'Mobile apps',
158
+ content: `<p style="color:var(--ui-muted);margin:0 0 .75rem">iOS and Android apps in development. Public beta planned.</p>` +
159
+ button({ label: 'Join waitlist', size: 'sm', variant: 'secondary' }),
160
+ }),
161
+ },
162
+ ],
163
+ }),
164
+ `timeline({
165
+ items: [
166
+ {
167
+ dotColor: 'success',
168
+ label: 'Q1 2024',
169
+ content: card({
170
+ title: 'Series A closed',
171
+ content: stat({ label: 'Raised', value: '$4.2M' }) + ...,
172
+ }),
173
+ },
174
+ {
175
+ dotColor: 'accent',
176
+ label: 'Q3 2024',
177
+ content: card({
178
+ title: 'Product launch',
179
+ content: '<p>Shipped v1.0 to general availability.</p>' +
180
+ badge({ label: 'Billing live', variant: 'success' }),
181
+ }),
182
+ },
183
+ ],
184
+ })`
185
+ )}
186
+
187
+ <h2 class="doc-h2" id="item-fn">Using timelineItem()</h2>
188
+ <p>Build items individually with <code>timelineItem()</code> and pass the joined HTML as <code>content</code>. Useful for dynamic or conditional lists.</p>
189
+ ${demo(
190
+ timeline({
191
+ content:
192
+ timelineItem({ dotColor: 'success', label: 'Done', content: '<p style="color:var(--ui-muted);margin:0">Design system tokens agreed</p>' }) +
193
+ timelineItem({ dotColor: 'success', label: 'Done', content: '<p style="color:var(--ui-muted);margin:0">Component library built</p>' }) +
194
+ timelineItem({ dotColor: 'accent', label: 'Current', content: '<p style="color:var(--ui-muted);margin:0">Documentation in progress</p>' }) +
195
+ timelineItem({ dotColor: 'muted', label: 'Next', content: '<p style="color:var(--ui-muted);margin:0">Public launch</p>' }),
196
+ }),
197
+ `timeline({
198
+ content:
199
+ timelineItem({ dotColor: 'success', label: 'Done', content: '...' }) +
200
+ timelineItem({ dotColor: 'success', label: 'Done', content: '...' }) +
201
+ timelineItem({ dotColor: 'accent', label: 'Current', content: '...' }) +
202
+ timelineItem({ dotColor: 'muted', label: 'Next', content: '...' }),
203
+ })`
204
+ )}
205
+
206
+ ${table(
207
+ ['Prop', 'Type', 'Default', ''],
208
+ [
209
+ ['<code>direction</code>', 'string', "'vertical'", "'vertical' · 'horizontal'"],
210
+ ['<code>items</code>', 'array', '[]', 'Array of <code>timelineItem</code> option objects'],
211
+ ['<code>content</code>', 'string (HTML)', '—', 'Raw HTML alternative to <code>items</code> — use with <code>timelineItem()</code>'],
212
+ ]
213
+ )}
214
+
215
+ <h3 class="doc-h3" style="margin-top:2rem">timelineItem() props</h3>
216
+ ${table(
217
+ ['Prop', 'Type', 'Default', ''],
218
+ [
219
+ ['<code>content</code>', 'string (HTML)', '—', 'Raw HTML body — accepts any component output'],
220
+ ['<code>label</code>', 'string', '—', 'Timestamp or step label (escaped)'],
221
+ ['<code>dot</code>', 'string (HTML)', '—', 'Raw HTML inside the dot — SVG or emoji; grows the dot to 2rem'],
222
+ ['<code>dotColor</code>', 'string', "'accent'", "'accent' · 'success' · 'warning' · 'error' · 'muted'"],
223
+ ]
224
+ )}
225
+ `,
226
+ }),
227
+ }
@@ -0,0 +1,84 @@
1
+ import { renderComponentPage, demo } from '../../lib/component-page.js'
2
+ import { prevNext } from '../../lib/nav.js'
3
+ import { table, callout } from '../../lib/layout.js'
4
+ import { toggle, stack } from '../../../../src/ui/index.js'
5
+
6
+ const { prev, next } = prevNext('/components/toggle')
7
+
8
+ export default {
9
+ route: '/components/toggle',
10
+ meta: {
11
+ title: 'Toggle — Pulse Docs',
12
+ description: 'iOS-style switch toggle component for Pulse UI.',
13
+ styles: ['/pulse-ui.css', '/docs.css'],
14
+ },
15
+ state: {},
16
+ view: () => renderComponentPage({
17
+ currentHref: '/components/toggle',
18
+ prev,
19
+ next,
20
+ name: 'toggle',
21
+ description: 'iOS-style switch that renders a visually hidden <code>&lt;input type="checkbox"&gt;</code> with a custom track and thumb. No JavaScript required — state is read from FormData on submission.',
22
+ content: `
23
+
24
+ <h2 class="doc-h2" id="default">Default</h2>
25
+ ${demo(
26
+ stack({ gap: 'md', content:
27
+ toggle({ name: 'notifications', label: 'Email notifications' }) +
28
+ toggle({ name: 'updates', label: 'Product updates', checked: true }),
29
+ }),
30
+ `toggle({ name: 'notifications', label: 'Email notifications' })
31
+ toggle({ name: 'updates', label: 'Product updates', checked: true })`,
32
+ { col: true }
33
+ )}
34
+
35
+ <h2 class="doc-h2" id="hint">With hint</h2>
36
+ <p>Use <code>hint</code> to add supporting text below the switch.</p>
37
+ ${demo(
38
+ stack({ gap: 'lg', content:
39
+ toggle({ name: 'marketing', label: 'Marketing emails', hint: 'Receive tips, product news, and special offers.' }) +
40
+ toggle({ name: 'digest', label: 'Weekly digest', hint: 'A summary of activity sent every Monday morning.', checked: true }),
41
+ }),
42
+ `toggle({
43
+ name: 'marketing',
44
+ label: 'Marketing emails',
45
+ hint: 'Receive tips, product news, and special offers.',
46
+ })
47
+ toggle({
48
+ name: 'digest',
49
+ label: 'Weekly digest',
50
+ hint: 'A summary of activity sent every Monday morning.',
51
+ checked: true,
52
+ })`,
53
+ { col: true }
54
+ )}
55
+
56
+ <h2 class="doc-h2" id="disabled">Disabled</h2>
57
+ ${demo(
58
+ stack({ gap: 'md', content:
59
+ toggle({ name: 'a', label: 'Off and disabled', disabled: true }) +
60
+ toggle({ name: 'b', label: 'On and disabled', disabled: true, checked: true }),
61
+ }),
62
+ `toggle({ name: 'a', label: 'Off and disabled', disabled: true })
63
+ toggle({ name: 'b', label: 'On and disabled', disabled: true, checked: true })`,
64
+ { col: true }
65
+ )}
66
+
67
+ <h2 class="doc-h2" id="in-forms">In forms</h2>
68
+ ${callout('note', 'The switch submits as <code>\'on\'</code> under its <code>name</code> when checked. When unchecked, the field is absent from FormData entirely — the same behaviour as a native checkbox. Read it with <code>formData.get(\'name\') === \'on\'</code>.')}
69
+
70
+ ${table(
71
+ ['Prop', 'Type', 'Default', ''],
72
+ [
73
+ ['<code>name</code>', 'string', '—', 'Field name — submitted in FormData'],
74
+ ['<code>label</code>', 'string', '—', 'Visible label text'],
75
+ ['<code>checked</code>', 'boolean', 'false', 'Initial on/off state'],
76
+ ['<code>disabled</code>', 'boolean', 'false', ''],
77
+ ['<code>hint</code>', 'string', '—', 'Helper text below the switch'],
78
+ ['<code>id</code>', 'string', '—', 'Override the generated <code>id</code>'],
79
+ ['<code>class</code>', 'string', '—', ''],
80
+ ]
81
+ )}
82
+ `,
83
+ }),
84
+ }
@@ -0,0 +1,48 @@
1
+ import { renderComponentPage, demo } from '../../lib/component-page.js'
2
+ import { prevNext } from '../../lib/nav.js'
3
+ import { table } from '../../lib/layout.js'
4
+ import { tooltip, button, cluster } from '../../../../src/ui/index.js'
5
+
6
+ const { prev, next } = prevNext('/components/tooltip')
7
+
8
+ export default {
9
+ route: '/components/tooltip',
10
+ meta: {
11
+ title: 'Tooltip — Pulse Docs',
12
+ description: 'Tooltip component for Pulse UI.',
13
+ styles: ['/pulse-ui.css', '/docs.css'],
14
+ },
15
+ state: {},
16
+ view: () => renderComponentPage({
17
+ currentHref: '/components/tooltip',
18
+ prev,
19
+ next,
20
+ name: 'tooltip',
21
+ description: 'CSS-powered tooltip that wraps any element. No JavaScript required — the bubble appears on hover and <code>:focus-within</code>. Supports four placements.',
22
+ content: `
23
+ ${demo(
24
+ cluster({ gap: 'lg', justify: 'center', content:
25
+ tooltip({ content: 'This appears on top', position: 'top', trigger: button({ label: 'Top', variant: 'secondary' }) }) +
26
+ tooltip({ content: 'This appears below', position: 'bottom', trigger: button({ label: 'Bottom', variant: 'secondary' }) }) +
27
+ tooltip({ content: 'This appears to the left', position: 'left', trigger: button({ label: 'Left', variant: 'secondary' }) }) +
28
+ tooltip({ content: 'This appears to the right', position: 'right', trigger: button({ label: 'Right', variant: 'secondary' }) }),
29
+ }),
30
+ `tooltip({
31
+ content: 'This appears on top',
32
+ position: 'top', // top | bottom | left | right
33
+ trigger: button({ label: 'Hover me', variant: 'secondary' }),
34
+ })`
35
+ )}
36
+
37
+ ${table(
38
+ ['Prop', 'Type', 'Default', ''],
39
+ [
40
+ ['<code>content</code>', 'string', '—', 'Tooltip text (plain text only)'],
41
+ ['<code>trigger</code>', 'string (HTML)', '—', 'Raw HTML slot — the element the tooltip wraps'],
42
+ ['<code>position</code>', '<code>top | bottom | left | right</code>', '<code>top</code>', ''],
43
+ ['<code>class</code>', 'string', '—', ''],
44
+ ]
45
+ )}
46
+ `,
47
+ }),
48
+ }
@@ -0,0 +1,204 @@
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('/components')
6
+
7
+ export default {
8
+ route: '/components',
9
+ meta: {
10
+ title: 'Component Library — Pulse Docs',
11
+ description: 'Server-rendered UI components for Pulse — button, card, input, alert, stat, avatar, table and more. Fully accessible, mobile-ready, zero client JS.',
12
+ styles: ['/pulse-ui.css', '/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/components',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Component Library')}
21
+ ${lead('Server-rendered building blocks. Each component is a pure function that returns an HTML string — no client-side JS, no build step, accessible and mobile-ready by default. The output is the same every time the same props are passed.')}
22
+
23
+ ${section('setup', 'Setup')}
24
+ <p>Components are imported directly into the spec file alongside the stylesheet reference in <code>meta.styles</code>. No build step, no registration.</p>
25
+ ${codeBlock(highlight(`import { button, card, input } from '@invisibleloop/pulse/ui'
26
+ import { escHtml } from '@invisibleloop/pulse/html'
27
+
28
+ export default {
29
+ route: '/example',
30
+ meta: {
31
+ title: 'Example',
32
+ styles: ['/pulse-ui.css', '/app.css'],
33
+ },
34
+ view: () => \`
35
+ <main id="main-content">
36
+ \${card({
37
+ title: 'Welcome',
38
+ content: button({ label: 'Get started', href: '/start' }),
39
+ })}
40
+ </main>
41
+ \`,
42
+ }`, 'js'))}
43
+
44
+ ${section('theming', 'Theming')}
45
+ <p>All visual values flow through CSS custom properties. Override any <code>--ui-*</code> token in <code>:root</code> in your <code>app.css</code> to retheme the entire library at once.</p>
46
+ ${codeBlock(highlight(`/* app.css */
47
+ :root {
48
+ --ui-accent: #6366f1;
49
+ --ui-accent-hover: #818cf8;
50
+ --ui-accent-dim: rgba(99, 102, 241, .12);
51
+ --ui-radius: 6px;
52
+ --ui-bg: #ffffff;
53
+ --ui-surface: #f9fafb;
54
+ --ui-text: #111827;
55
+ --ui-muted: #6b7280;
56
+ --ui-border: #e5e7eb;
57
+ }`, 'css'))}
58
+ <p>New variants follow the <code>ui-btn--{name}</code> CSS modifier pattern. A brand-coloured button, for example, is just a new class in <code>app.css</code> — the component itself stays untouched.</p>
59
+ ${codeBlock(highlight(`.ui-btn--brand {
60
+ background: var(--brand);
61
+ color: #fff;
62
+ border: none;
63
+ }
64
+ .ui-btn--brand:hover:not(.ui-btn--disabled) {
65
+ background: var(--brand-hover);
66
+ }`, 'css'))}
67
+
68
+ ${section('components', 'Components')}
69
+ <p>Each component has its own page with full demos, code examples, and a props reference.</p>
70
+
71
+ <h3 class="doc-h3">UI</h3>
72
+ ${table(
73
+ ['Component', 'Description'],
74
+ [
75
+ ['<a href="/components/button">button</a>', 'Renders as <code>&lt;button&gt;</code> or <code>&lt;a&gt;</code>. Four variants, three sizes.'],
76
+ ['<a href="/components/badge">badge</a>', 'Inline status label. Five semantic colour variants.'],
77
+ ['<a href="/components/card">card</a>', 'Content surface with title, body, and optional footer.'],
78
+ ['<a href="/components/input">input</a>', 'Labelled text input with hint and error support.'],
79
+ ['<a href="/components/fieldset">fieldset</a>', 'Semantic grouping of related fields with an accessible legend.'],
80
+ ['<a href="/components/select">select</a>', 'Styled select with option groups and current-value support.'],
81
+ ['<a href="/components/textarea">textarea</a>', 'Multi-line input with label, hint, and error.'],
82
+ ['<a href="/components/alert">alert</a>', 'Inline feedback banner. ARIA roles wired by variant.'],
83
+ ['<a href="/components/stat">stat</a>', 'Numeric metric with optional trend arrow.'],
84
+ ['<a href="/components/avatar">avatar</a>', 'User avatar — image with fallback to initials.'],
85
+ ['<a href="/components/empty">empty</a>', 'Empty state with title, description, and optional CTA.'],
86
+ ['<a href="/components/table">table</a>', 'Accessible data table with scroll wrapper.'],
87
+ ]
88
+ )}
89
+
90
+ <h3 class="doc-h3">Landing page</h3>
91
+ ${table(
92
+ ['Component', 'Description'],
93
+ [
94
+ ['<a href="/components/nav">nav</a>', 'Site header with logo, links, and optional CTA.'],
95
+ ['<a href="/components/hero">hero</a>', 'Full-width hero section with eyebrow, title, and actions.'],
96
+ ['<a href="/components/app-badge">appBadge</a>', 'App Store / Google Play download badge.'],
97
+ ['<a href="/components/feature">feature</a>', 'Icon + title + description block for feature grids.'],
98
+ ['<a href="/components/testimonial">testimonial</a>', 'Customer quote with avatar and star rating.'],
99
+ ['<a href="/components/pricing">pricing</a>', 'Plan card with feature list and CTA. Supports highlighted state.'],
100
+ ['<a href="/components/accordion">accordion</a>', 'Collapsible FAQ items — no JS, native <code>&lt;details&gt;</code>.'],
101
+ ['<a href="/components/cta">cta</a>', 'Call-to-action block with eyebrow, heading, body, and actions slot.'],
102
+ ]
103
+ )}
104
+
105
+ <h3 class="doc-h3">Layout</h3>
106
+ ${table(
107
+ ['Component', 'Description'],
108
+ [
109
+ ['<a href="/components/container">container</a>', 'Max-width wrapper with horizontal padding.'],
110
+ ['<a href="/components/section">section</a>', 'Vertical padding block with background variant.'],
111
+ ['<a href="/components/grid">grid</a>', 'Responsive CSS grid. Collapses to one column on mobile.'],
112
+ ['<a href="/components/stack">stack</a>', 'Flex column with consistent vertical gap.'],
113
+ ['<a href="/components/cluster">cluster</a>', 'Flex row with wrapping — for badges, buttons, etc.'],
114
+ ['<a href="/components/divider">divider</a>', 'Horizontal rule, optionally with centred label.'],
115
+ ['<a href="/components/banner">banner</a>', 'Full-width announcement bar above the nav.'],
116
+ ['<a href="/components/media">media</a>', 'Two-column image + text layout, stacks on mobile.'],
117
+ ['<a href="/components/code-window">codeWindow</a>', 'macOS window chrome around a code block. Accepts pre-highlighted HTML.'],
118
+ ['<a href="/components/footer">footer</a>', 'Site footer with logo, nav links, and legal text. Stacks on mobile.'],
119
+ ]
120
+ )}
121
+
122
+ ${section('utilities', 'Utility classes')}
123
+ <p><code>pulse-ui.css</code> ships a utility layer (prefix <code>u-</code>) for common spacing, typography, and layout needs. Reach for these before writing custom CSS — they use the same <code>--ui-*</code> tokens as components so theme overrides apply everywhere.</p>
124
+
125
+ <h3 class="doc-h3">Spacing</h3>
126
+ <p>Scale: 1=4px 2=8px 3=12px 4=16px 5=20px 6=24px 8=32px 10=40px 12=48px 16=64px</p>
127
+ ${table(
128
+ ['Class', 'Property'],
129
+ [
130
+ ['<code>u-mt-{0–16}</code>', 'margin-top'],
131
+ ['<code>u-mb-{0–16}</code>', 'margin-bottom'],
132
+ ['<code>u-mx-auto</code>', 'margin-left + right: auto'],
133
+ ['<code>u-p-{0–8}</code>', 'padding (all sides)'],
134
+ ['<code>u-px-{0–8}</code>', 'padding-left + right'],
135
+ ['<code>u-py-{0–8}</code>', 'padding-top + bottom'],
136
+ ]
137
+ )}
138
+
139
+ <h3 class="doc-h3">Typography</h3>
140
+ ${table(
141
+ ['Class', 'Effect'],
142
+ [
143
+ ['<code>u-text-{xs,sm,base,lg,xl,2xl,3xl,4xl}</code>', 'Font size + matching line-height'],
144
+ ['<code>u-font-{normal,medium,semibold,bold}</code>', 'Font weight'],
145
+ ['<code>u-text-{left,center,right}</code>', 'Text alignment'],
146
+ ['<code>u-text-{default,muted,accent,green,red,yellow,blue}</code>', 'Token colour'],
147
+ ['<code>u-leading-{tight,snug,normal,relaxed,loose}</code>', 'Line height'],
148
+ ]
149
+ )}
150
+
151
+ <h3 class="doc-h3">Layout</h3>
152
+ ${table(
153
+ ['Class', 'Effect'],
154
+ [
155
+ ['<code>u-flex</code> / <code>u-flex-col</code>', 'Flex row or column'],
156
+ ['<code>u-items-{start,center,end,stretch}</code>', 'align-items'],
157
+ ['<code>u-justify-{start,center,end,between}</code>', 'justify-content'],
158
+ ['<code>u-gap-{1–8}</code>', 'gap'],
159
+ ['<code>u-w-full</code>', 'width: 100%'],
160
+ ['<code>u-max-w-{xs,sm,md,lg,xl,prose}</code>', 'max-width (320px–1024px, 65ch)'],
161
+ ['<code>u-hidden</code> / <code>u-block</code> / <code>u-inline-block</code>', 'display'],
162
+ ]
163
+ )}
164
+
165
+ <h3 class="doc-h3">Visual</h3>
166
+ ${table(
167
+ ['Class', 'Effect'],
168
+ [
169
+ ['<code>u-rounded</code> / <code>u-rounded-md</code> / <code>u-rounded-lg</code> / <code>u-rounded-full</code>', 'border-radius'],
170
+ ['<code>u-border</code> / <code>u-border-t</code> / <code>u-border-b</code>', '1px solid --ui-border'],
171
+ ['<code>u-bg-surface</code> / <code>u-bg-surface2</code> / <code>u-bg-accent</code>', 'background token'],
172
+ ['<code>u-overflow-hidden</code> / <code>u-overflow-auto</code>', 'overflow'],
173
+ ['<code>u-opacity-50</code> / <code>u-opacity-75</code>', 'opacity'],
174
+ ]
175
+ )}
176
+
177
+ <p>Utilities compose naturally with components and with each other:</p>
178
+ ${codeBlock(highlight(`<!-- centred hero block — no custom CSS needed -->
179
+ <div class="u-flex u-flex-col u-items-center u-text-center u-py-16 u-gap-4">
180
+ <h1 class="u-text-4xl u-font-bold">Hello</h1>
181
+ <p class="u-text-lg u-text-muted u-max-w-prose">Subtitle goes here.</p>
182
+ \${button({ label: 'Get started', href: '/start' })}
183
+ </div>`, 'html'))}
184
+
185
+ ${callout('note', 'Never use inline <code>style=""</code> attributes. Reach for utility classes first, and only add to <code>app.css</code> when you need something utilities cannot provide — a unique animation, a custom grid, or a one-off component variant.')}
186
+
187
+ ${section('composing', 'Composing components')}
188
+ <p>Components compose naturally — pass the output of one as the <code>content</code> or <code>footer</code> of another. Here's a stat dashboard card:</p>
189
+ ${codeBlock(highlight(`import { card, stat, button } from '@invisibleloop/pulse/ui'
190
+
191
+ card({
192
+ title: 'This week',
193
+ content: \`
194
+ \${stat({ label: 'Page views', value: '48,291', change: '+12%', trend: 'up' })}
195
+ \${stat({ label: 'New users', value: '1,042', change: '+4%', trend: 'up' })}
196
+ \${stat({ label: 'Bounced', value: '22%', change: '+1%', trend: 'down' })}
197
+ \`,
198
+ footer: button({ label: 'View full report', href: '/analytics', variant: 'ghost', size: 'sm' }),
199
+ })`, 'js'))}
200
+
201
+ ${callout('note', 'Text props like <code>label</code>, <code>title</code>, <code>value</code>, and <code>error</code> are escaped automatically. <code>content</code>, <code>footer</code>, <code>rows</code>, <code>actions</code>, <code>icon</code>, <code>action</code>, and <code>logo</code> are raw HTML slots — they pass through as-is, so any user-supplied data going into those slots should go through <code>escHtml()</code> first.')}
202
+ `,
203
+ }),
204
+ }