@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,103 @@
1
+ import{a}from"./runtime-QFURDKA2.js";import{a as i,b as d,c as l,d as p,e as o,g as t,h as e,i as c}from"./runtime-L2HNXIHW.js";import{a as r,b as u}from"./runtime-B73WLANC.js";var{prev:h,next:m}=i("/components"),s={route:"/components",meta:{title:"Component Library \u2014 Pulse Docs",description:"Server-rendered UI components for Pulse \u2014 button, card, input, alert, stat, avatar, table and more. Fully accessible, mobile-ready, zero client JS.",styles:["/pulse-ui.css","/docs.css"]},state:{},view:()=>d({currentHref:"/components",prev:h,next:m,content:`
2
+ ${l("Component Library")}
3
+ ${p("Server-rendered building blocks. Each component is a pure function that returns an HTML string \u2014 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.")}
4
+
5
+ ${o("setup","Setup")}
6
+ <p>Components are imported directly into the spec file alongside the stylesheet reference in <code>meta.styles</code>. No build step, no registration.</p>
7
+ ${t(a(`import { button, card, input } from '@invisibleloop/pulse/ui'
8
+ import { escHtml } from '@invisibleloop/pulse/html'
9
+
10
+ export default {
11
+ route: '/example',
12
+ meta: {
13
+ title: 'Example',
14
+ styles: ['/pulse-ui.css', '/app.css'],
15
+ },
16
+ view: () => \`
17
+ <main id="main-content">
18
+ \${card({
19
+ title: 'Welcome',
20
+ content: button({ label: 'Get started', href: '/start' }),
21
+ })}
22
+ </main>
23
+ \`,
24
+ }`,"js"))}
25
+
26
+ ${o("theming","Theming")}
27
+ <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>
28
+ ${t(a(`/* app.css */
29
+ :root {
30
+ --ui-accent: #6366f1;
31
+ --ui-accent-hover: #818cf8;
32
+ --ui-accent-dim: rgba(99, 102, 241, .12);
33
+ --ui-radius: 6px;
34
+ --ui-bg: #ffffff;
35
+ --ui-surface: #f9fafb;
36
+ --ui-text: #111827;
37
+ --ui-muted: #6b7280;
38
+ --ui-border: #e5e7eb;
39
+ }`,"css"))}
40
+ <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> \u2014 the component itself stays untouched.</p>
41
+ ${t(a(`.ui-btn--brand {
42
+ background: var(--brand);
43
+ color: #fff;
44
+ border: none;
45
+ }
46
+ .ui-btn--brand:hover:not(.ui-btn--disabled) {
47
+ background: var(--brand-hover);
48
+ }`,"css"))}
49
+
50
+ ${o("components","Components")}
51
+ <p>Each component has its own page with full demos, code examples, and a props reference.</p>
52
+
53
+ <h3 class="doc-h3">UI</h3>
54
+ ${e(["Component","Description"],[['<a href="/components/button">button</a>',"Renders as <code>&lt;button&gt;</code> or <code>&lt;a&gt;</code>. Four variants, three sizes."],['<a href="/components/badge">badge</a>',"Inline status label. Five semantic colour variants."],['<a href="/components/card">card</a>',"Content surface with title, body, and optional footer."],['<a href="/components/input">input</a>',"Labelled text input with hint and error support."],['<a href="/components/fieldset">fieldset</a>',"Semantic grouping of related fields with an accessible legend."],['<a href="/components/select">select</a>',"Styled select with option groups and current-value support."],['<a href="/components/textarea">textarea</a>',"Multi-line input with label, hint, and error."],['<a href="/components/alert">alert</a>',"Inline feedback banner. ARIA roles wired by variant."],['<a href="/components/stat">stat</a>',"Numeric metric with optional trend arrow."],['<a href="/components/avatar">avatar</a>',"User avatar \u2014 image with fallback to initials."],['<a href="/components/empty">empty</a>',"Empty state with title, description, and optional CTA."],['<a href="/components/table">table</a>',"Accessible data table with scroll wrapper."]])}
55
+
56
+ <h3 class="doc-h3">Landing page</h3>
57
+ ${e(["Component","Description"],[['<a href="/components/nav">nav</a>',"Site header with logo, links, and optional CTA."],['<a href="/components/hero">hero</a>',"Full-width hero section with eyebrow, title, and actions."],['<a href="/components/app-badge">appBadge</a>',"App Store / Google Play download badge."],['<a href="/components/feature">feature</a>',"Icon + title + description block for feature grids."],['<a href="/components/testimonial">testimonial</a>',"Customer quote with avatar and star rating."],['<a href="/components/pricing">pricing</a>',"Plan card with feature list and CTA. Supports highlighted state."],['<a href="/components/accordion">accordion</a>',"Collapsible FAQ items \u2014 no JS, native <code>&lt;details&gt;</code>."],['<a href="/components/cta">cta</a>',"Call-to-action block with eyebrow, heading, body, and actions slot."]])}
58
+
59
+ <h3 class="doc-h3">Layout</h3>
60
+ ${e(["Component","Description"],[['<a href="/components/container">container</a>',"Max-width wrapper with horizontal padding."],['<a href="/components/section">section</a>',"Vertical padding block with background variant."],['<a href="/components/grid">grid</a>',"Responsive CSS grid. Collapses to one column on mobile."],['<a href="/components/stack">stack</a>',"Flex column with consistent vertical gap."],['<a href="/components/cluster">cluster</a>',"Flex row with wrapping \u2014 for badges, buttons, etc."],['<a href="/components/divider">divider</a>',"Horizontal rule, optionally with centred label."],['<a href="/components/banner">banner</a>',"Full-width announcement bar above the nav."],['<a href="/components/media">media</a>',"Two-column image + text layout, stacks on mobile."],['<a href="/components/code-window">codeWindow</a>',"macOS window chrome around a code block. Accepts pre-highlighted HTML."],['<a href="/components/footer">footer</a>',"Site footer with logo, nav links, and legal text. Stacks on mobile."]])}
61
+
62
+ ${o("utilities","Utility classes")}
63
+ <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 \u2014 they use the same <code>--ui-*</code> tokens as components so theme overrides apply everywhere.</p>
64
+
65
+ <h3 class="doc-h3">Spacing</h3>
66
+ <p>Scale: 1=4px 2=8px 3=12px 4=16px 5=20px 6=24px 8=32px 10=40px 12=48px 16=64px</p>
67
+ ${e(["Class","Property"],[["<code>u-mt-{0\u201316}</code>","margin-top"],["<code>u-mb-{0\u201316}</code>","margin-bottom"],["<code>u-mx-auto</code>","margin-left + right: auto"],["<code>u-p-{0\u20138}</code>","padding (all sides)"],["<code>u-px-{0\u20138}</code>","padding-left + right"],["<code>u-py-{0\u20138}</code>","padding-top + bottom"]])}
68
+
69
+ <h3 class="doc-h3">Typography</h3>
70
+ ${e(["Class","Effect"],[["<code>u-text-{xs,sm,base,lg,xl,2xl,3xl,4xl}</code>","Font size + matching line-height"],["<code>u-font-{normal,medium,semibold,bold}</code>","Font weight"],["<code>u-text-{left,center,right}</code>","Text alignment"],["<code>u-text-{default,muted,accent,green,red,yellow,blue}</code>","Token colour"],["<code>u-leading-{tight,snug,normal,relaxed,loose}</code>","Line height"]])}
71
+
72
+ <h3 class="doc-h3">Layout</h3>
73
+ ${e(["Class","Effect"],[["<code>u-flex</code> / <code>u-flex-col</code>","Flex row or column"],["<code>u-items-{start,center,end,stretch}</code>","align-items"],["<code>u-justify-{start,center,end,between}</code>","justify-content"],["<code>u-gap-{1\u20138}</code>","gap"],["<code>u-w-full</code>","width: 100%"],["<code>u-max-w-{xs,sm,md,lg,xl,prose}</code>","max-width (320px\u20131024px, 65ch)"],["<code>u-hidden</code> / <code>u-block</code> / <code>u-inline-block</code>","display"]])}
74
+
75
+ <h3 class="doc-h3">Visual</h3>
76
+ ${e(["Class","Effect"],[["<code>u-rounded</code> / <code>u-rounded-md</code> / <code>u-rounded-lg</code> / <code>u-rounded-full</code>","border-radius"],["<code>u-border</code> / <code>u-border-t</code> / <code>u-border-b</code>","1px solid --ui-border"],["<code>u-bg-surface</code> / <code>u-bg-surface2</code> / <code>u-bg-accent</code>","background token"],["<code>u-overflow-hidden</code> / <code>u-overflow-auto</code>","overflow"],["<code>u-opacity-50</code> / <code>u-opacity-75</code>","opacity"]])}
77
+
78
+ <p>Utilities compose naturally with components and with each other:</p>
79
+ ${t(a(`<!-- centred hero block \u2014 no custom CSS needed -->
80
+ <div class="u-flex u-flex-col u-items-center u-text-center u-py-16 u-gap-4">
81
+ <h1 class="u-text-4xl u-font-bold">Hello</h1>
82
+ <p class="u-text-lg u-text-muted u-max-w-prose">Subtitle goes here.</p>
83
+ \${button({ label: 'Get started', href: '/start' })}
84
+ </div>`,"html"))}
85
+
86
+ ${c("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 \u2014 a unique animation, a custom grid, or a one-off component variant.')}
87
+
88
+ ${o("composing","Composing components")}
89
+ <p>Components compose naturally \u2014 pass the output of one as the <code>content</code> or <code>footer</code> of another. Here's a stat dashboard card:</p>
90
+ ${t(a(`import { card, stat, button } from '@invisibleloop/pulse/ui'
91
+
92
+ card({
93
+ title: 'This week',
94
+ content: \`
95
+ \${stat({ label: 'Page views', value: '48,291', change: '+12%', trend: 'up' })}
96
+ \${stat({ label: 'New users', value: '1,042', change: '+4%', trend: 'up' })}
97
+ \${stat({ label: 'Bounced', value: '22%', change: '+1%', trend: 'down' })}
98
+ \`,
99
+ footer: button({ label: 'View full report', href: '/analytics', variant: 'ghost', size: 'sm' }),
100
+ })`,"js"))}
101
+
102
+ ${c("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 \u2014 they pass through as-is, so any user-supplied data going into those slots should go through <code>escHtml()</code> first.")}
103
+ `})};var n=document.getElementById("pulse-root");n&&!n.dataset.pulseMounted&&(n.dataset.pulseMounted="1",r(s,n,window.__PULSE_SERVER__||{},{ssr:!0}),u(n,r));var S=s;export{S as default};
@@ -0,0 +1,126 @@
1
+ import{a as r}from"./runtime-QFURDKA2.js";import{a,b as i,c as l,d as u,e,g as o,h as t,i as d}from"./runtime-L2HNXIHW.js";import{a as s,b as h}from"./runtime-B73WLANC.js";var{prev:p,next:m}=a("/config"),n={route:"/config",meta:{title:"Configuration \u2014 Pulse Docs",description:"Full reference for pulse.config.js \u2014 port, Lighthouse thresholds, load test config, environments, and per-route overrides.",styles:["/docs.css"]},state:{},view:()=>i({currentHref:"/config",prev:p,next:m,content:`
2
+ ${l("Configuration")}
3
+ ${u(`<code>pulse.config.js</code> sets the performance and load thresholds that Pulse enforces across your application. All fields are optional \u2014 the defaults match Google's Core Web Vitals "good" band. Configuration here is not about enabling features; it is about deciding where, if anywhere, you need to lower a guaranteed baseline.`)}
4
+
5
+ ${e("schema","Full schema")}
6
+ ${o(r(`// pulse.config.js
7
+ export default {
8
+ port: 3000,
9
+
10
+ lighthouse: {
11
+ // Category scores \u2014 0 to 100. Default: 100 for all four.
12
+ performance: 100,
13
+ accessibility: 100,
14
+ bestPractices: 100,
15
+ seo: 100,
16
+
17
+ // Core Web Vitals and timing metrics.
18
+ // Defaults are the Google "good" thresholds.
19
+ lcp: 2500, // Largest Contentful Paint (ms)
20
+ cls: 0.1, // Cumulative Layout Shift
21
+ tbt: 200, // Total Blocking Time (ms)
22
+ fcp: 1800, // First Contentful Paint (ms)
23
+ si: 3400, // Speed Index (ms)
24
+ inp: 200, // Interaction to Next Paint (ms)
25
+ },
26
+
27
+ load: {
28
+ duration: 10,
29
+ connections: 10,
30
+ thresholds: {
31
+ rps: undefined, // minimum req/s (optional)
32
+ p99: undefined, // maximum p99 latency ms (optional)
33
+ errors: 0,
34
+ },
35
+ },
36
+
37
+ environments: {
38
+ // Environment names are bespoke \u2014 choose whatever suits your project.
39
+ local: { url: 'http://localhost:3000', default: true },
40
+ staging: {
41
+ url: 'https://staging.myapp.com',
42
+ headers: { Authorization: \`Bearer \${process.env.STAGING_TOKEN}\` },
43
+ load: { duration: 30, connections: 50 },
44
+ lighthouse: { performance: 90 },
45
+ },
46
+ production: { url: 'https://myapp.com' },
47
+ },
48
+
49
+ routes: {
50
+ // Per-route overrides \u2014 merged on top of global lighthouse/load config.
51
+ // Only specify what differs from the global defaults.
52
+ '/dashboard': {
53
+ lighthouse: {
54
+ performance: 85,
55
+ lcp: 4000,
56
+ },
57
+ },
58
+ '/embed': {
59
+ lighthouse: {
60
+ bestPractices: 85,
61
+ },
62
+ },
63
+ },
64
+ }`,"js"))}
65
+
66
+ ${e("port","port")}
67
+ ${t(["Field","Type","Default","Description"],[["<code>port</code>","<code>number</code>","<code>3000</code>","Port the dev and production servers listen on."]])}
68
+
69
+ ${e("lighthouse","lighthouse")}
70
+ <p>Global Lighthouse thresholds that every page must meet. <code>/pulse-report</code> enforces these after every audit \u2014 any page that falls below a threshold is reported as a failure, not a warning.</p>
71
+ ${t(["Field","Type","Default","Description"],[["<code>performance</code>","<code>number</code>","<code>100</code>","Lighthouse Performance category score (0\u2013100)."],["<code>accessibility</code>","<code>number</code>","<code>100</code>","Lighthouse Accessibility category score (0\u2013100)."],["<code>bestPractices</code>","<code>number</code>","<code>100</code>","Lighthouse Best Practices category score (0\u2013100)."],["<code>seo</code>","<code>number</code>","<code>100</code>","Lighthouse SEO category score (0\u2013100)."],["<code>lcp</code>","<code>number</code>","<code>2500</code>","Largest Contentful Paint budget (ms)."],["<code>cls</code>","<code>number</code>","<code>0.1</code>","Cumulative Layout Shift budget."],["<code>tbt</code>","<code>number</code>","<code>200</code>","Total Blocking Time budget (ms)."],["<code>fcp</code>","<code>number</code>","<code>1800</code>","First Contentful Paint budget (ms)."],["<code>si</code>","<code>number</code>","<code>3400</code>","Speed Index budget (ms)."],["<code>inp</code>","<code>number</code>","<code>200</code>","Interaction to Next Paint budget (ms)."]])}
72
+ ${d("note","Unset fields default to the values above. Setting a field to <code>null</code> removes that check for the project \u2014 use sparingly. Raising a threshold is always preferable to disabling it.")}
73
+
74
+ ${e("routes","routes")}
75
+ <p>Route-specific overrides let you lower a threshold for a specific page without relaxing the global guarantee. Only specify the fields that differ \u2014 everything else inherits from the global config.</p>
76
+ ${o(r(`routes: {
77
+ // This route uses a third-party chart library \u2014 relax performance only.
78
+ '/dashboard': {
79
+ lighthouse: {
80
+ performance: 85,
81
+ lcp: 4000,
82
+ },
83
+ },
84
+ }`,"js"))}
85
+ ${d("note","Route keys must exactly match the spec <code>route</code> field, including any leading slash. Dynamic segments are not supported \u2014 create a specific override for each route pattern.")}
86
+
87
+ ${e("load","load")}
88
+ <p>Load test thresholds enforced by <code>/pulse-load</code>. All fields are optional \u2014 omitting a threshold means that check is not enforced.</p>
89
+ ${t(["Field","Type","Default","Description"],[["<code>duration</code>","<code>number</code>","<code>10</code>","Test duration in seconds."],["<code>connections</code>","<code>number</code>","<code>10</code>","Number of concurrent request chains."],["<code>thresholds.rps</code>","<code>number</code>","<code>undefined</code>","Minimum acceptable requests per second. Unset = no check."],["<code>thresholds.p99</code>","<code>number</code>","<code>undefined</code>","Maximum acceptable p99 latency (ms). Unset = no check."],["<code>thresholds.errors</code>","<code>number</code>","<code>0</code>","Maximum acceptable error count."]])}
90
+ ${o(r(`load: {
91
+ duration: 30,
92
+ connections: 20,
93
+ thresholds: {
94
+ rps: 100, // fail if below 100 req/s
95
+ p99: 500, // fail if p99 exceeds 500ms
96
+ errors: 0,
97
+ },
98
+ },`,"js"))}
99
+ <p>Per-route overrides follow the same pattern as <code>lighthouse</code>:</p>
100
+ ${o(r(`routes: {
101
+ '/feed': {
102
+ load: { connections: 5, thresholds: { rps: 20 } },
103
+ },
104
+ }`,"js"))}
105
+ <p>Results are saved to <code>.pulse/load-reports/</code> and displayed in the Load Tests tab of the report dashboard, alongside the Lighthouse Performance tab for the same route.</p>
106
+
107
+ ${e("environments","environments")}
108
+ <p>Named environments let you enforce thresholds against different targets \u2014 local, staging, production \u2014 from the same config. Thresholds are applied per-environment, so staging and production can have different performance floors.</p>
109
+ ${t(["Field","Type","Description"],[["<code>url</code>","<code>string</code>","Base URL to test against. If it contains <code>localhost</code> or <code>127.0.0.1</code>, a local production build is spun up automatically. Remote URLs are tested directly."],["<code>default</code>","<code>boolean</code>","The environment used when none is explicitly specified by <code>pulse report</code> and <code>pulse load-test</code>."],["<code>headers</code>","<code>object</code>","HTTP headers sent with every request to this environment. Useful for authorization tokens on protected staging deployments \u2014 read values from <code>process.env</code> rather than hardcoding them."],["<code>load</code>","<code>object</code>","Load test config overrides for this environment. Same fields as the global <code>load</code> block. Merged on top of global config."],["<code>lighthouse</code>","<code>object</code>","Lighthouse threshold overrides for this environment. Same fields as the global <code>lighthouse</code> block. Merged on top of global config."]])}
110
+ ${d("note","Environment names are fully bespoke \u2014 <code>local</code>, <code>staging</code>, <code>production</code>, <code>preview</code>, <code>eu</code> \u2014 whatever maps to your project's infrastructure. There are no reserved names.")}
111
+ <p>Threshold merge order: <strong>global config \u2192 environment override \u2192 per-route override</strong>.</p>
112
+ ${o(r(`environments: {
113
+ local: { url: 'http://localhost:3000', default: true },
114
+ staging: {
115
+ url: 'https://staging.myapp.com',
116
+ headers: { Authorization: \`Bearer \${process.env.STAGING_TOKEN}\` },
117
+ load: { duration: 30, connections: 50, thresholds: { rps: 500 } },
118
+ lighthouse: { performance: 90 },
119
+ },
120
+ production: { url: 'https://myapp.com' },
121
+ }`,"js"))}
122
+
123
+ ${e("defaults","CWV default thresholds")}
124
+ <p>The default metric thresholds match the Google "good" band from the Core Web Vitals specification:</p>
125
+ ${t(["Metric","Default",'Google "good" threshold'],[["LCP","<code>2500 ms</code>","\u2264 2500 ms"],["CLS","<code>0.1</code>","\u2264 0.1"],["TBT","<code>200 ms</code>","\u2264 200 ms"],["FCP","<code>1800 ms</code>","\u2264 1800 ms"],["SI","<code>3400 ms</code>","\u2264 3400 ms"],["INP","<code>200 ms</code>","\u2264 200 ms"]])}
126
+ `})};var c=document.getElementById("pulse-root");c&&!c.dataset.pulseMounted&&(c.dataset.pulseMounted="1",s(n,c,window.__PULSE_SERVER__||{},{ssr:!0}),h(c,s));var P=n;export{P as default};
@@ -0,0 +1,71 @@
1
+ import{a}from"./runtime-QFURDKA2.js";import{a as i,b as r,c,d as l,e as t,g as e,h as d,i as m}from"./runtime-L2HNXIHW.js";import{a as n,b as u}from"./runtime-B73WLANC.js";var{prev:p,next:h}=i("/constraints"),s={route:"/constraints",meta:{title:"Constraints \u2014 Pulse Docs",description:"Automatic min/max bounds on state values, enforced after every mutation.",styles:["/docs.css"]},state:{},view:()=>r({currentHref:"/constraints",prev:p,next:h,content:`
2
+ ${c("Constraints")}
3
+ ${l("Constraints are always-on bounds for numeric state values. After every mutation, Pulse clamps constrained values to their declared range before the view re-renders. The value can never go out of range \u2014 there is no code path where it can.")}
4
+
5
+ ${t("declaring","Declaring constraints")}
6
+ <p>The <code>constraints</code> field maps top-level state keys to bounds objects with optional <code>min</code> and <code>max</code> properties:</p>
7
+ ${e(a(`export default {
8
+ route: '/cart',
9
+ state: {
10
+ quantity: 1,
11
+ zoom: 1.0,
12
+ rating: 0,
13
+ },
14
+ constraints: {
15
+ quantity: { min: 1, max: 99 },
16
+ zoom: { min: 0.5, max: 3.0 },
17
+ rating: { min: 0, max: 5 },
18
+ },
19
+ mutations: {
20
+ increaseQty: (state) => ({ quantity: state.quantity + 1 }),
21
+ decreaseQty: (state) => ({ quantity: state.quantity - 1 }),
22
+ zoomIn: (state) => ({ zoom: state.zoom + 0.1 }),
23
+ zoomOut: (state) => ({ zoom: state.zoom - 0.1 }),
24
+ },
25
+ }`,"js"))}
26
+ <p>When <code>decreaseQty</code> runs and <code>quantity</code> is already 1, the constraint clamps it back to 1 before the view renders. The mutation does not need to check bounds \u2014 the spec declares them once and Pulse enforces them everywhere.</p>
27
+
28
+ ${t("vs-validation","Constraints vs Validation")}
29
+ ${d(["","Constraints","Validation"],[["When it runs","After <strong>every</strong> mutation, automatically","Only when an action has <code>validate: true</code>"],["What it does","Clamps numeric values silently","Rejects the action and surfaces errors"],["User feedback","None \u2014 state is silently corrected","Explicit error messages shown in the view"],["Best for","Numeric ranges that must never be exceeded","Form field correctness before submission"]])}
30
+
31
+ ${m("note","Constraints and validation serve different purposes. Constraints silently enforce numeric bounds at every mutation \u2014 they cannot be bypassed. Validation rejects invalid form data before an action's async work begins \u2014 it only runs when explicitly declared.")}
32
+
33
+ ${t("one-sided","One-sided bounds")}
34
+ <p>Either <code>min</code> or <code>max</code> can be declared alone \u2014 both are optional:</p>
35
+ ${e(a(`constraints: {
36
+ count: { min: 0 }, // no upper limit
37
+ discount: { max: 100 }, // no lower limit
38
+ offset: { min: 0, max: 999 } // both bounds
39
+ }`,"js"))}
40
+
41
+ ${t("how-it-works","How clamping works")}
42
+ <p>After Pulse applies a mutation's partial state update, it iterates over all declared constraints and applies <code>Math.max(min, Math.min(max, value))</code> to each constrained key. The view is then called with the clamped state.</p>
43
+ ${e(a(`// state.count = 10, constraints.count = { min: 0, max: 10 }
44
+ mutations: {
45
+ increment: (state) => ({ count: state.count + 1 }),
46
+ // mutation returns { count: 11 }
47
+ // constraint clamps to 10
48
+ // view receives { count: 10 }
49
+ }`,"js"))}
50
+
51
+ ${t("top-level","Top-level keys only")}
52
+ <p>Constraints apply to top-level state keys. To constrain nested values, consider flattening your state structure or applying bounds logic in the mutation itself:</p>
53
+ ${e(a(`// Cannot do:
54
+ constraints: {
55
+ 'player.health': { min: 0, max: 100 } // \u2717 nested paths not supported
56
+ }
57
+
58
+ // Do instead:
59
+ state: { playerHealth: 100 },
60
+ constraints: { playerHealth: { min: 0, max: 100 } },
61
+
62
+ // Or handle in the mutation:
63
+ mutations: {
64
+ takeDamage: (state, _, amount) => ({
65
+ player: {
66
+ ...state.player,
67
+ health: Math.max(0, state.player.health - amount),
68
+ }
69
+ })
70
+ }`,"js"))}
71
+ `})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",n(s,o,window.__PULSE_SERVER__||{},{ssr:!0}),u(o,n));var k=s;export{k as default};
@@ -0,0 +1,163 @@
1
+ import{a as t}from"./runtime-QFURDKA2.js";import{a as d,b as c,c as p,d as l,e,g as o,h as r,i as s}from"./runtime-L2HNXIHW.js";import{a as n,b as u}from"./runtime-B73WLANC.js";var{prev:m,next:h}=d("/deploy"),i={route:"/deploy",meta:{title:"Deployment \u2014 Pulse Docs",description:"Deploy a Pulse app to a VPS, Docker, Fly.io, Railway, or Render.",styles:["/docs.css"]},state:{},view:()=>c({currentHref:"/deploy",prev:m,next:h,content:`
2
+ ${p("Deployment")}
3
+ ${l("Pulse deploys as a single Node.js process. No adapters, no serverless wrapping, no separate static file server required. Build once, run anywhere Node 22+ runs. All guarantees \u2014 security headers, brotli compression, immutable asset caching \u2014 are active in production automatically.")}
4
+
5
+ ${e("build","Build")}
6
+ <p>Run the production build before deploying:</p>
7
+ ${o(t("npm run build","bash"))}
8
+ <p>This generates content-hashed bundles in <code>public/dist/</code> and writes <code>public/dist/manifest.json</code>. The server reads the manifest at startup to resolve hydration script paths.</p>
9
+ ${s("warning","Without a manifest, the server falls back to serving source files directly \u2014 no compression, no content-hashed filenames, and no <code>immutable</code> cache headers. Always run <code>npm run build</code> before deploying to production.")}
10
+
11
+ ${e("files","What to deploy")}
12
+ ${r(["Include","Reason"],[["<code>src/</code>","Page specs \u2014 imported by the server at runtime"],["<code>public/</code>","Static assets and built bundles (<code>public/dist/</code>)"],["<code>server.js</code>","Entry point"],["<code>pulse.config.js</code>","Server config"],["<code>package.json</code> + <code>node_modules/</code>","Runtime dependencies"]])}
13
+ ${r(["Exclude","Reason"],[["<code>.claude/</code>","AI agent config \u2014 not needed at runtime"],["<code>.pulse/</code>","Local report data \u2014 not needed at runtime"]])}
14
+
15
+ ${e("env-vars","Environment variables")}
16
+ ${r(["Variable","Default","Description"],[["<code>NODE_ENV</code>","<code>development</code>","Set to <code>production</code> to enable HSTS headers and production cache behaviour."],["<code>PORT</code>","Value in <code>pulse.config.js</code> (default <code>3000</code>)","Override the listening port. Most PaaS platforms set this automatically."]])}
17
+ ${o(t("NODE_ENV=production pulse start","bash"))}
18
+
19
+ ${e("pm2","VPS with PM2")}
20
+ <p>PM2 keeps the process alive, restarts it on crash, and manages logs.</p>
21
+ ${o(t(`# Install PM2 globally
22
+ npm install -g pm2
23
+
24
+ # Start the app
25
+ NODE_ENV=production pm2 start server.js --name myapp
26
+
27
+ # Persist across reboots
28
+ pm2 save
29
+ pm2 startup
30
+
31
+ # Zero-downtime reload after a deploy
32
+ pm2 reload myapp`,"bash"))}
33
+ <p>For repeatable deployments, check an <code>ecosystem.config.cjs</code> into version control:</p>
34
+ ${o(t(`// ecosystem.config.cjs
35
+ module.exports = {
36
+ apps: [{
37
+ name: 'myapp',
38
+ script: 'server.js',
39
+ env_production: {
40
+ NODE_ENV: 'production',
41
+ PORT: 3000,
42
+ },
43
+ }],
44
+ }`,"js"))}
45
+ ${o(t("pm2 start ecosystem.config.cjs --env production","bash"))}
46
+
47
+ ${e("docker","Docker")}
48
+ <p>A two-stage build keeps the image small \u2014 build tools stay in the first stage.</p>
49
+ ${o(t(`# ---- build stage ----
50
+ FROM node:22-alpine AS build
51
+ WORKDIR /app
52
+ COPY package*.json ./
53
+ RUN npm ci
54
+ COPY . .
55
+ RUN npx pulse build
56
+
57
+ # ---- runtime stage ----
58
+ FROM node:22-alpine
59
+ WORKDIR /app
60
+ ENV NODE_ENV=production
61
+ COPY package*.json ./
62
+ RUN npm ci --omit=dev
63
+ COPY --from=build /app/src ./src
64
+ COPY --from=build /app/public ./public
65
+ COPY --from=build /app/server.js ./server.js
66
+ COPY pulse.config.js ./
67
+ EXPOSE 3000
68
+ CMD ["node", "server.js"]`,"bash"))}
69
+ ${o(t(`docker build -t myapp .
70
+ docker run -p 3000:3000 --env NODE_ENV=production myapp`,"bash"))}
71
+
72
+ ${e("fly","Fly.io")}
73
+ ${o(t(`# fly.toml
74
+ app = 'myapp'
75
+ primary_region = 'lhr'
76
+
77
+ [env]
78
+ NODE_ENV = 'production'
79
+
80
+ [build]
81
+ [build.args]
82
+ NODE_VERSION = '22'
83
+
84
+ [deploy]
85
+ release_command = 'npx pulse build'
86
+
87
+ [http_service]
88
+ internal_port = 3000
89
+ force_https = true
90
+ auto_stop_machines = 'stop'
91
+ auto_start_machines = true
92
+
93
+ [[vm]]
94
+ memory = '256mb'
95
+ cpu_kind = 'shared'
96
+ cpus = 1`,"bash"))}
97
+ ${o(t(`# First deploy
98
+ fly launch
99
+
100
+ # Subsequent deploys
101
+ fly deploy`,"bash"))}
102
+
103
+ ${e("railway","Railway")}
104
+ <p>Railway auto-detects Node apps. Add a <code>railway.json</code> to set the build and start commands:</p>
105
+ ${o(t(`{
106
+ "$schema": "https://railway.app/railway.schema.json",
107
+ "build": {
108
+ "builder": "NIXPACKS",
109
+ "buildCommand": "npm run build"
110
+ },
111
+ "deploy": {
112
+ "startCommand": "NODE_ENV=production node server.js",
113
+ "healthcheckPath": "/",
114
+ "restartPolicyType": "ON_FAILURE"
115
+ }
116
+ }`,"js"))}
117
+
118
+ ${e("render","Render")}
119
+ ${r(["Setting","Value"],[["Environment","Node"],["Build command","<code>npm install &amp;&amp; npm run build</code>"],["Start command","<code>NODE_ENV=production node server.js</code>"],["Node version","<code>22.x</code>"]])}
120
+ <p>Set <code>NODE_ENV=production</code> in the Render environment variables dashboard.</p>
121
+
122
+ ${e("serverless","Vercel, Cloudflare, and edge platforms")}
123
+ <p>These platforms each have multiple products with very different runtimes \u2014 the compatibility story varies significantly between them.</p>
124
+
125
+ ${e("vercel","Vercel")}
126
+ <p>Vercel has two distinct runtimes:</p>
127
+ ${r(["Product","Runtime","Pulse compatible?"],[["<strong>Functions</strong> (Node.js)","Full Node.js \u2014 same built-ins as a VPS","Partially \u2014 see below"],["<strong>Edge Functions</strong>","V8 isolates (no Node built-ins)","No"]])}
128
+ <p><strong>Vercel Functions (Node.js)</strong> can run Pulse with some differences in behaviour:</p>
129
+ ${r(["Feature","Behaviour on Vercel Functions"],[["<code>serverTtl</code> cache","Works within a warm instance, but cold starts reset it. Not reliable for expensive queries."],["Streaming SSR","Vercel Functions support streaming responses, but require explicit configuration via <code>supportsResponseStreaming</code>."],["Static files","Vercel serves <code>public/</code> automatically via its CDN \u2014 Pulse's static file serving is bypassed."],["Security headers","Work as normal \u2014 Pulse adds them to every response."]])}
130
+ ${s("warning","Vercel Functions are not a tested or officially supported deployment target for Pulse. The adapter pattern (exporting a request handler rather than starting a server) is not yet documented. Railway, Render, or Fly.io are simpler choices with no adaptation required.")}
131
+
132
+ ${e("cloudflare","Cloudflare")}
133
+ ${r(["Product","Runtime","Pulse compatible?"],[["<strong>Workers</strong>","V8 isolates \u2014 no <code>node:http</code>, <code>node:fs</code>, <code>node:zlib</code>","No"],["<strong>Pages Functions</strong>","Same V8 isolate runtime as Workers","No"],["<strong>CDN / proxy</strong>","Sits in front of your origin server","Yes \u2014 works great with Fly.io or a VPS behind it"]])}
134
+ ${s("tip","The recommended pattern for edge performance: deploy Pulse to <strong>Fly.io</strong> (which runs real VMs in many regions) and put <strong>Cloudflare as a CDN/proxy</strong> in front of it. Static assets and cached HTML are served from Cloudflare's edge; dynamic requests are proxied to the nearest Fly VM.")}
135
+
136
+ ${e("https","HTTPS and reverse proxy")}
137
+ <p>Pulse detects TLS automatically. When a request arrives with an <code>x-forwarded-proto: https</code> header or over a direct TLS socket, <code>Strict-Transport-Security: max-age=31536000; includeSubDomains</code> is added to the response. All four platforms above forward this header \u2014 no Pulse config is needed.</p>
138
+ <p>If running behind nginx for TLS termination:</p>
139
+ ${o(t(`# nginx \u2014 TLS termination, proxy to Pulse
140
+ server {
141
+ listen 443 ssl;
142
+ server_name myapp.com;
143
+
144
+ ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
145
+ ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
146
+
147
+ location / {
148
+ proxy_pass http://localhost:3000;
149
+ proxy_http_version 1.1;
150
+ proxy_set_header Host $host;
151
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
152
+ proxy_set_header X-Forwarded-Proto $scheme;
153
+ }
154
+ }
155
+
156
+ # Redirect HTTP to HTTPS
157
+ server {
158
+ listen 80;
159
+ server_name myapp.com;
160
+ return 301 https://$host$request_uri;
161
+ }`,"bash"))}
162
+ ${s("tip",`Use <a href="https://certbot.eff.org">Certbot</a> to obtain and auto-renew a free Let's Encrypt certificate: <code>certbot --nginx -d myapp.com</code>.`)}
163
+ `})};var a=document.getElementById("pulse-root");a&&!a.dataset.pulseMounted&&(a.dataset.pulseMounted="1",n(i,a,window.__PULSE_SERVER__||{},{ssr:!0}),u(a,n));var P=i;export{P as default};
@@ -0,0 +1 @@
1
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}:root{--bg:#0d0d10;--surface:#111116;--surface-2:#18181f;--border:#38383f;--border-subtle:#1a1a20;--text:#e2e2ea;--muted:#9090a0;--muted-2:#7e7e92;--accent:#c9b800;--accent-text:#0a0a0a;--accent-hover:#e0ce00;--accent-dim:rgba(201,184,0,0.12);--green:#3dd68c;--red:#ff6b6b;--tok-kw:#c792ea;--tok-str:#c3e88d;--tok-cmt:#7a8a9a;--tok-num:#f78c6c;--tok-fn:#82aaff;--tok-prop:#ffcb6b;--tok-op:#89ddff;--tok-punct:#7e8899;--sidebar-w:260px;--header-h:52px;--content-w:740px;--font:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--mono:"Fira Code","Cascadia Code","JetBrains Mono","Menlo","Monaco",monospace;--radius:8px}html{font-size:16px}body{font-family:var(--font);background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh;-webkit-font-smoothing:antialiased}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-hover)}.home{min-height:100vh;background:#f0e642;background-image:radial-gradient( circle,rgba(0,0,0,0.12) 1px,transparent 1px );background-size:22px 22px;color:#0a0a0a;--accent:#0a0a0a;--accent-hover:#2a2a2a;--accent-dim:rgba(0,0,0,0.07);--text:#0a0a0a;--muted:#52524a;--muted-2:#888880;--surface:#ffffff;--surface-2:#f0ede0;--border:#c8c4aa;--border-subtle:rgba(0,0,0,0.1);--green:#1a7a4a;--red:#c0392b}.home-nav{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 2rem;border-bottom:none;position:sticky;top:0;background:#0a0a0a;backdrop-filter:none;z-index:10;--accent:#f0e642;--text:#ffffff}.home-nav-links{display:flex;align-items:center;gap:1.5rem}.home-nav-links a{color:rgba(255,255,255,0.85);font-size:0.9rem;transition:color 0.15s}.home-nav-links a:hover{color:#ffffff}.home-nav-links .btn-primary{background:#f0e642;color:#0a0a0a;font-weight:600;padding:0.4rem 1rem;border-radius:6px;font-size:0.875rem}.home-nav-links .btn-primary:hover{background:#f7f040;color:#0a0a0a}.hero{max-width:860px;margin:0 auto;padding:5rem 2rem;text-align:center;background:none}.hero-icon{margin-bottom:1.25rem}.hero-badge{display:inline-flex;align-items:center;gap:0.4rem;font-size:0.78rem;font-weight:600;color:#0a0a0a;background:rgba(0,0,0,0.08);border:1px solid rgba(0,0,0,0.18);border-radius:100px;padding:0.3rem 0.85rem;margin-bottom:2rem;letter-spacing:0.06em;text-transform:uppercase}.hero h1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:800;line-height:1.1;letter-spacing:-0.03em;text-wrap:balance;margin-bottom:1.5rem;color:#0a0a0a;background:none;-webkit-background-clip:unset;-webkit-text-fill-color:#0a0a0a;background-clip:unset}.hero-subtitle{font-size:1.1rem;color:var(--muted);max-width:580px;margin:0 auto 2.5rem;line-height:1.65}.hero-ctas{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}.btn-primary{display:inline-flex;align-items:center;padding:0.7rem 1.5rem;border-radius:var(--radius);font-size:0.95rem;font-weight:600;background:#0a0a0a;color:#f0e642;text-decoration:none;border:2px solid #0a0a0a;transition:background 0.15s,color 0.15s,transform 0.15s}.btn-primary:hover{background:#f0e642;color:#0a0a0a;border-color:#0a0a0a;transform:translateY(-1px)}.btn-secondary{display:inline-flex;align-items:center;padding:0.7rem 1.5rem;border-radius:var(--radius);font-size:0.95rem;font-weight:600;background:transparent;color:#0a0a0a;border:1px solid rgba(0,0,0,0.25);text-decoration:none;transition:background 0.15s,border-color 0.15s}.btn-secondary:hover{background:rgba(0,0,0,0.06);border-color:#0a0a0a;color:#0a0a0a}.home-cta .btn-primary{background:#f0e642;color:#0a0a0a;border-color:#f0e642}.home-cta .btn-primary:hover{background:#0a0a0a;color:#f0e642;border-color:#f0e642;transform:translateY(-1px)}.home-cta .btn-secondary{background:transparent;border-color:rgba(255,255,255,0.25);color:rgba(255,255,255,0.75)}.home-cta .btn-secondary:hover{background:rgba(255,255,255,0.08);border-color:rgba(255,255,255,0.5);color:#ffffff}.home-code{background:#111114;padding:5rem 2rem 6rem}.home-code-inner{max-width:1000px;margin:0 auto}.home-code-header{text-align:center;margin-bottom:2.5rem}.home-code-header h2{font-size:clamp(1.6rem,3vw,2.2rem);font-weight:700;letter-spacing:-0.02em;color:#ffffff;margin-bottom:0.6rem}.home-code-header p{color:rgba(255,255,255,0.5);font-size:1.1rem;max-width:560px;margin:0 auto;line-height:1.65}.faq-item{padding:2rem 0;border-bottom:1px solid var(--border-subtle)}.faq-item:last-child{border-bottom:none}.faq-q{font-size:1.1rem;font-weight:600;color:var(--text);margin-bottom:0.875rem;letter-spacing:-0.01em;line-height:1.4}.faq-a p{font-size:0.925rem;color:var(--muted);line-height:1.75;max-width:68ch;margin-bottom:0.75rem}.faq-a p:last-child{margin-bottom:0}.faq-a .code-block{margin:0.75rem 0}.home-footer{border-top:1px solid rgba(255,255,255,0.08);text-align:center;padding:2rem;background:#0a0a0a;color:rgba(255,255,255,0.6);font-size:0.82rem}.home-footer a{color:rgba(255,255,255,0.75)}.home-footer a:hover{color:#f0e642}.section-label{font-size:1rem;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:#0a0a0a;text-align:center;margin-bottom:1rem;opacity:0.5}.home-stats{display:flex;align-items:center;justify-content:center;gap:0;padding:2.5rem 2rem;border-top:none;border-bottom:1px solid rgba(255,255,255,0.08);background:#0a0a0a;flex-wrap:wrap}.home-stat{display:flex;flex-direction:column;align-items:center;gap:0.35rem;padding:0.75rem 3rem}.home-stat-value{font-size:2rem;font-weight:700;color:#f0e642;letter-spacing:-0.02em;line-height:1}.home-stat-label{font-size:1.1rem;color:rgba(255,255,255,0.7);text-align:center}.home-stat-divider{width:1px;height:2.5rem;background:#f0e642;flex-shrink:0}.how{background:#0a0a0a;background-image:radial-gradient( circle,rgba(255,255,255,0.08) 1px,transparent 1px );background-size:22px 22px;padding:5rem 2rem;margin:0;max-width:none;--accent:#f0e642;--text:#ffffff;--muted:47473e;--border:rgba(255,255,255,0.12)}.how-inner{max-width:960px;margin:0 auto;text-align:center}.how .section-label{color:#f0e642;opacity:1}.how-steps{display:flex;align-items:flex-start;justify-content:center;gap:0;margin-top:3rem}.how-step{flex:1;max-width:260px;display:flex;flex-direction:column;align-items:center;gap:0.75rem;padding:0 1rem}.how-step-num{width:2.5rem;height:2.5rem;border-radius:50%;background:#f0e642;border:1px solid #f0e642;display:flex;align-items:center;justify-content:center;font-size:0.9rem;font-weight:700;color:#0a0a0a;flex-shrink:0}.how-step h3{font-size:1.1rem;font-weight:600;color:var(--text)}.how-step p{font-size:1.1rem;color:rgba(255,255,255,0.5);line-height:1.6}.how-connector{width:3rem;height:1px;background:#f0e642;margin-top:1.25rem;flex-shrink:0}.ai-first{padding:5rem 2rem;max-width:900px;margin:0 auto}.ai-first-title{font-size:clamp(1.6rem,4vw,2.2rem);font-weight:700;letter-spacing:-0.02em;margin:0.75rem 0 1.25rem}.ai-first-lead{color:var(--muted);font-size:1.1rem;line-height:1.75;max-width:72ch;margin-bottom:3rem}.ai-cols{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}@media (max-width:640px){.ai-cols{grid-template-columns:1fr}.ai-col--pulse{order:-1}}.ai-col{padding:1.75rem;border-radius:10px;border:1px solid var(--border);background:var(--surface)}.ai-col--pulse{border-color:#0a0a0a;background:#0a0a0a}.ai-col-title{font-size:0.8rem;font-weight:600;letter-spacing:0.07em;text-transform:uppercase;margin-bottom:1.25rem}.ai-col-title--bad{color:#5f5f5a}.ai-col-title--good{color:#f0e642}.ai-col-list{list-style:none;display:flex;flex-direction:column;gap:0.9rem}.ai-col-list li{font-size:1.1rem;color:var(--muted);line-height:1.6;padding-left:1.1rem;position:relative}.ai-col-list li::before{content:"–";position:absolute;left:0;color:var(--muted-2)}.ai-col--pulse .ai-col-list li{color:rgba(255,255,255,0.75)}.ai-col--pulse .ai-col-list li::before{content:"✓";color:#f0e642}.versus{padding:5rem 2rem;background:#0a0a0a;border-top:none;border-bottom:none;text-align:center;--accent:#f0e642}.versus-title{font-size:1.75rem;font-weight:700;margin-bottom:0.75rem;color:#ffffff}.versus-sub{color:rgba(255,255,255,0.5);max-width:560px;margin:0 auto 2.5rem;font-size:1.1rem;line-height:1.7}.versus .section-label{color:#f0e642;opacity:1}.table-sticky-col{overflow-x:auto}.table-sticky-col table th:first-child,.table-sticky-col table td:first-child{position:sticky;left:0;z-index:1;border-right:1px solid rgba(255,255,255,0.15)}.versus-table-wrap{overflow-x:auto;max-width:900px;margin:0 auto}.versus-table{width:100%;min-width:640px;border-collapse:collapse;font-size:1rem;text-align:left}.versus-table thead{background:rgba(255,255,255,0.06)}.versus-table thead th{padding:0.75rem 1rem;color:rgba(255,255,255,0.45);font-size:0.78rem;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;border-bottom:1px solid rgba(255,255,255,0.1)}.versus-table thead th:first-child{width:22%;background:#111314}.versus-table th[scope="row"]{background:#0a0a0a}.versus-table tbody tr:nth-child(odd) th[scope="row"]{background:#111214}.versus-table tbody tr:nth-child(odd){background:rgba(255,255,255,0.04)}.versus-table tr:hover td{background:rgba(255,255,255,0.08)}.versus-table td{padding:0.85rem 1rem;border-bottom:1px solid rgba(255,255,255,0.06);color:rgba(255,255,255,0.45);vertical-align:top;line-height:1.5}.versus-table th[scope="row"]{padding:0.85rem 1rem;border-bottom:1px solid rgba(255,255,255,0.06);color:rgba(255,255,255,0.75);font-weight:500;font-size:0.87rem;text-align:left}.versus-table .v-yes{color:#f0e642}.versus-table .v-partial{color:rgba(255,255,255,0.6)}.versus-table .v-no{color:rgba(255,255,255,0.6)}.usp-blocks{padding:4rem 2rem;max-width:1000px;margin:0 auto;display:flex;flex-direction:column;gap:0}.usp-block{display:grid;grid-template-columns:280px 1fr;gap:3rem;padding:3.5rem 0;border-bottom:1px solid var(--border-subtle);align-items:start}.usp-block:last-child{border-bottom:none}.usp-block-alt{direction:rtl}.usp-block-alt>*{direction:ltr}.usp-block-aside{display:flex;flex-direction:column;gap:0.75rem}.usp-icon{width:3rem;height:3rem;background:#0a0a0a;border:none;border-radius:var(--radius);display:flex;align-items:center;justify-content:center;margin-bottom:0.25rem;--accent:#f0e642}.usp-block-aside h2{font-size:1.2rem;font-weight:700}.usp-block-aside p{font-size:1.1rem;color:var(--muted);line-height:1.7}.usp-points{list-style:none;display:flex;flex-direction:column;gap:1.25rem;padding-top:0.25rem}.usp-points li{padding-left:1.25rem;border-left:2px solid #0a0a0a;font-size:1.1rem;color:var(--muted);line-height:1.7}.usp-points li strong{display:block;color:var(--text);font-weight:600;margin-bottom:0.15rem}.metrics-report{padding:5rem 2rem;border-top:1px solid var(--border-subtle);max-width:1100px;margin:0 auto}.metrics-header{text-align:center;margin-bottom:3rem}.metrics-title{font-size:clamp(1.6rem,3.5vw,2.2rem);font-weight:700;letter-spacing:-0.02em;margin-bottom:0.6rem}.metrics-generated{font-size:0.8rem;color:var(--muted);letter-spacing:0.01em}.metrics-groups{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}.metrics-group{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius);padding:1.25rem 1.5rem 1.5rem}.metrics-group-label{font-size:0.7rem;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin-bottom:1rem;padding-bottom:0.6rem;border-bottom:1px solid var(--border-subtle)}.metrics-items{display:flex;flex-direction:column;gap:1rem}.metric-item{display:flex;flex-direction:column;gap:0.15rem}.metric-val{font-size:1.5rem;font-weight:700;letter-spacing:-0.02em;color:var(--text);line-height:1}.metric-val--green{color:var(--green)}.metric-val--accent{color:var(--accent)}.metric-label{font-size:0.78rem;color:var(--muted);line-height:1.3}.home-cta{text-align:center;padding:5rem 2rem;background:#0a0a0a;background-image:radial-gradient( circle,rgba(255,255,255,0.07) 1px,transparent 1px );background-size:22px 22px;border-top:none}.home-cta h2{font-size:2rem;font-weight:700;margin-bottom:0.75rem;color:#ffffff}.home-cta p{color:rgba(255,255,255,0.5);margin-bottom:2rem;font-size:1.1rem}.home-cta-checks{list-style:none;display:flex;flex-wrap:wrap;justify-content:center;gap:0.6rem 1.5rem;margin-bottom:2rem}.home-cta-checks li{font-size:1.1rem;color:rgba(255,255,255,0.8);display:flex;align-items:center;gap:0.4rem}.home-cta-checks li::before{content:"✓";color:#f0e642;font-weight:700;font-size:1rem}.home-cta-actions{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:99;opacity:0;transition:opacity 0.25s ease}@media (max-width:768px){.sidebar-overlay{display:block;pointer-events:none}.sidebar-overlay.visible{opacity:1;pointer-events:auto}}.component-demo{border:1px solid var(--border);border-radius:var(--radius);margin:1.5rem 0}.demo-preview{padding:3.5rem 1.5rem 1.5rem;background:#0d0d10;border-radius:var(--radius) var(--radius) 0 0;display:flex;flex-direction:column;gap:0.75rem;position:relative}.demo-preview-inner{display:flex;flex-wrap:wrap;gap:0.75rem;align-items:flex-start;min-width:0;width:100%}.demo-preview--col .demo-preview-inner{flex-direction:column;align-items:stretch}.demo-preview--scroll .demo-preview-inner{flex-wrap:nowrap;overflow-x:auto}.demo-mobile-nav .ui-nav-links{display:none}.demo-mobile-nav .ui-nav-burger{display:flex}.demo-phone{width:320px;border-radius:32px;overflow:hidden;background:var(--bg,#0d0d10);box-shadow:0 0 0 7px var(--surface-2,#18181f),0 0 0 8px var(--border,#222228),0 24px 48px rgba(0,0,0,0.5)}.demo-phone-statusbar{height:28px;background:var(--bg,#0d0d10);display:flex;align-items:center;justify-content:center}.demo-phone-pill{width:64px;height:10px;background:var(--surface-2,#18181f);border-radius:99px}.demo-phone-content{min-height:160px;display:flex;align-items:center;justify-content:center;padding:1.5rem}.demo-phone-content p{margin:0;color:var(--muted,#6b6b80);font-size:0.8rem;text-align:center}.demo-preview.is-light{background:#f0f0f8}.demo-theme-toggle{position:absolute;top:0.6rem;right:0.6rem;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid rgba(255,255,255,0.15);border-radius:6px;background:rgba(255,255,255,0.07);color:rgba(255,255,255,0.5);cursor:pointer;transition:background 0.15s,color 0.15s;z-index:1}.demo-theme-toggle:hover{background:rgba(255,255,255,0.15);color:rgba(255,255,255,0.9)}.demo-preview.is-light .demo-theme-toggle{border-color:rgba(0,0,0,0.15);background:rgba(0,0,0,0.05);color:rgba(0,0,0,0.4)}.demo-preview.is-light .demo-theme-toggle:hover{background:rgba(0,0,0,0.1);color:rgba(0,0,0,0.8)}.demo-theme-toggle__light{display:none}.demo-preview.is-light .demo-theme-toggle__dark{display:none}.demo-preview.is-light .demo-theme-toggle__light{display:block}.demo-code pre.code-block{margin:0;border-radius:0;border:none;border-top:1px solid var(--border)}.demo-code .code-filename{border-radius:0;border:none;border-top:1px solid var(--border)}.prompt-group{margin-bottom:3rem}.prompt-group-title{font-size:0.78rem;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted);margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:1px solid var(--border-subtle)}.prompt-grid{display:flex;flex-direction:column;gap:0.75rem}.prompt-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem 1.5rem}.prompt-tag{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:var(--accent);margin-bottom:0.65rem}.prompt-text{font-size:0.93rem;color:var(--text);line-height:1.65;border-left:3px solid var(--accent);padding-left:1rem;margin:0 0 0.85rem;font-style:italic}.prompt-produces{font-size:0.82rem;color:var(--muted);line-height:1.6}.prompt-produces code{font-family:var(--mono);font-size:0.8rem;background:var(--surface-2);border:1px solid var(--border);border-radius:3px;padding:0.1em 0.35em;color:var(--tok-str);font-style:normal}.docs-sidebar{position:fixed;top:0;left:0;width:var(--sidebar-w);height:100vh;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:100;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.docs-sidebar::-webkit-scrollbar{width:4px}.docs-sidebar::-webkit-scrollbar-track{background:transparent}.docs-sidebar::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}.sidebar-logo{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 1.25rem 1rem;border-bottom:1px solid var(--border);flex-shrink:0}.logo-link{display:flex;align-items:center;gap:0.5rem;color:var(--text);font-weight:700;font-size:1rem;text-decoration:none}.logo-link:hover{color:var(--accent)}.logo-name{color:var(--text)}.version-badge{font-size:0.7rem;font-weight:600;color:var(--accent);background:var(--accent-dim);border:1px solid rgba(155,141,255,0.2);border-radius:4px;padding:0.15rem 0.45rem;font-family:var(--mono)}.sidebar-nav{padding:0.75rem 0 2rem;flex:1}.nav-section{padding:0.5rem 0}.nav-section+.nav-section{border-top:1px solid var(--border-subtle);margin-top:0.25rem;padding-top:0.75rem}.nav-section-title{font-size:0.68rem;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted-2);padding:0.25rem 1.25rem 0.5rem}.nav-link{display:block;padding:0.35rem 1.25rem;font-size:0.875rem;color:var(--muted);border-radius:0;transition:color 0.1s,background 0.1s;position:relative;border-left:2px solid transparent}.nav-link:hover{color:var(--text);background:var(--surface-2)}.nav-link.active{color:var(--accent);border-left-color:var(--accent);background:var(--accent-dim)}.docs-main{margin-left:var(--sidebar-w);flex:1;min-width:0;display:flex;flex-direction:column}.docs-header{height:var(--header-h);border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 2rem;position:sticky;top:0;background:rgba(13,13,16,0.9);backdrop-filter:blur(8px);z-index:10}.mobile-menu-btn{display:none;background:none;border:none;color:var(--muted);cursor:pointer;padding:0.25rem}.mobile-menu-btn:hover{color:var(--text)}.header-logo-mobile{display:none;color:var(--text)}.header-github{display:flex;align-items:center;gap:0.4rem;font-size:0.82rem;color:var(--muted);transition:color 0.1s;margin-left:auto}.header-github:hover{color:var(--text)}.docs-content{flex:1;padding:3rem 3.5rem 5rem;max-width:calc(var(--content-w)+7rem)}.doc-h1{font-size:2.25rem;font-weight:800;letter-spacing:-0.03em;line-height:1.15;margin-bottom:1rem;color:var(--text)}.doc-lead{font-size:1.1rem;color:var(--muted);line-height:1.65;max-width:600px;margin-bottom:2.5rem}.doc-h2{font-size:1.35rem;font-weight:700;letter-spacing:-0.02em;margin:2.5rem 0 0.75rem;color:var(--text);padding-top:1rem;border-top:1px solid var(--border-subtle)}.doc-h2:first-of-type{border-top:none;margin-top:0}.doc-h3{font-size:1.05rem;font-weight:600;margin:1.75rem 0 0.6rem;color:var(--text)}.definition-list{display:flex;flex-direction:column;gap:0.75rem;margin:1rem 0}.definition-list dt{margin:0;font-weight:600}.definition-list dd{margin:0.2rem 0 0 0;color:var(--muted);font-size:0.9rem;line-height:1.55;padding-bottom:0.75rem;border-bottom:1px solid var(--border-subtle)}.definition-list dd:last-of-type{border-bottom:none;padding-bottom:0}.docs-content p:not([class*="ui-"]){margin-bottom:0.9rem;line-height:1.7;color:var(--text)}.docs-content p+p{margin-top:-0.1rem}.docs-content ul:not([class*="ui-"]),.docs-content ol{padding-left:1.5rem;margin-bottom:1rem}.docs-content li:not([class*="ui-"]){margin-bottom:0.35rem;line-height:1.65;color:var(--text)}.docs-content strong:not([class*="ui-"]){font-weight:600;color:#fff}.docs-content a:not(.ui-btn):not(.ui-nav-link):not(.ui-nav-logo):not(.ui-app-badge){color:var(--accent);text-decoration:underline;text-underline-offset:2px}.docs-content a:not(.ui-btn):not(.ui-nav-link):not(.ui-nav-logo):not(.ui-app-badge):hover{color:var(--accent-hover)}.docs-content .ui-theme-light p,.docs-content .ui-theme-light li,.docs-content .ui-theme-light strong{color:var(--ui-text)}.docs-content code:not(pre code){font-family:var(--mono);font-size:0.82em;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:0.1em 0.35em;color:var(--accent-hover)}.heading-anchor{color:inherit;text-decoration:none}.heading-anchor:hover{color:var(--accent)}.code-filename{background:var(--surface-2);border:1px solid var(--border);border-bottom:none;border-radius:var(--radius) var(--radius) 0 0;padding:0.4rem 1rem;font-family:var(--mono);font-size:0.75rem;color:var(--muted)}.code-filename+.code-block{border-radius:0 0 var(--radius) var(--radius);margin-top:0}.code-block{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem 1.5rem;overflow-x:auto;font-family:var(--mono);font-size:0.9rem;line-height:1.75;margin:1.25rem 0;tab-size:2;white-space:pre;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.code-block::-webkit-scrollbar{height:4px}.code-block::-webkit-scrollbar-track{background:transparent}.code-block::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}.tok-kw{color:var(--tok-kw)}.tok-str{color:var(--tok-str)}.tok-cmt{color:var(--tok-cmt);font-style:italic}.tok-num{color:var(--tok-num)}.tok-fn{color:var(--tok-fn)}.tok-op{color:var(--tok-op)}.tok-punct{color:var(--tok-punct)}.table-wrap{overflow-x:auto;margin:1.25rem 0;border:1px solid var(--border);border-radius:var(--radius)}table{width:100%;border-collapse:collapse;font-size:0.875rem}thead{background:var(--surface-2)}th{font-weight:600;font-size:0.78rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);padding:0.65rem 1rem;text-align:left;border-bottom:1px solid var(--border)}td{padding:0.65rem 1rem;border-bottom:1px solid var(--border-subtle);vertical-align:top;line-height:1.5}tr:last-child td{border-bottom:none}tr:hover td{background:var(--surface-2)}.callout{display:flex;gap:0.75rem;padding:1rem 1.25rem;border-radius:var(--radius);margin:1.25rem 0;border-left:3px solid;font-size:0.9rem;line-height:1.6}.callout-note{background:rgba(130,170,255,0.07);border-color:rgba(130,170,255,0.5)}.callout-warning{background:rgba(255,107,107,0.07);border-color:rgba(255,107,107,0.5)}.callout-tip{background:rgba(61,214,140,0.07);border-color:rgba(61,214,140,0.5)}.callout-note .callout-icon{color:#82aaff}.callout-warning .callout-icon{color:var(--red)}.callout-tip .callout-icon{color:var(--green)}.callout-icon{font-size:1rem;flex-shrink:0;margin-top:0.1rem}.callout-body p:last-child{margin-bottom:0}.doc-prev-next{margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border)}.prev-next-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.prev-next-link{display:flex;flex-direction:column;gap:0.2rem;padding:1rem 1.25rem;border:1px solid var(--border);border-radius:var(--radius);color:inherit;text-decoration:none;transition:border-color 0.15s,background 0.15s}.prev-next-link:hover{border-color:var(--accent);background:var(--accent-dim);color:inherit}.next-link{text-align:right}.prev-next-label{font-size:0.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em}.prev-next-title{font-size:0.95rem;font-weight:600;color:var(--accent)}@media (max-width:768px){.docs-sidebar{transform:translateX(-100%);transition:transform 0.25s ease}.docs-sidebar.open{transform:translateX(0);box-shadow:4px 0 24px rgba(0,0,0,0.4)}.docs-main{margin-left:0}.mobile-menu-btn,.header-logo-mobile{display:flex}.docs-content{padding:2rem 1.25rem 4rem}.doc-h1{font-size:1.75rem}.prev-next-grid{grid-template-columns:1fr}.hero{padding:4rem 1.25rem 3rem}.home-nav{padding:1rem 1.25rem}.home-nav-links{gap:1rem}.home-stats{padding:2rem 1rem;gap:0}.home-stat{padding:0.75rem 1.5rem}.home-stat-value{font-size:1.5rem}.home-stat-divider{display:none}.how{padding:3.5rem 1.25rem}.how-steps{flex-direction:column;align-items:center;gap:1.5rem}.how-connector{width:1px;height:2rem}.how-step{max-width:100%}.versus{padding:3.5rem 1.25rem}.versus-title{font-size:1.4rem}.usp-blocks{padding:2.5rem 1.25rem}.usp-block{grid-template-columns:1fr;gap:1.5rem}.usp-block-alt{direction:ltr}.home-cta{padding:3.5rem 1.25rem}.home-cta h2{font-size:1.5rem}}.icon-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.5rem;margin-bottom:1rem}.icon-grid-item{display:flex;flex-direction:column;align-items:center;gap:0.5rem;padding:1rem 0.5rem;border:1px solid var(--border-subtle);border-radius:8px;background:var(--surface);cursor:default;transition:border-color 0.15s,background 0.15s}.icon-grid-item:hover{border-color:var(--border);background:var(--surface-2)}.icon-grid-preview{color:var(--text);line-height:1}.icon-grid-name{font-family:var(--mono,monospace);font-size:0.6rem;color:var(--muted);text-align:center;word-break:break-all;line-height:1.4}