@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,78 @@
1
+ /**
2
+ * Shared helpers for Pulse examples.
3
+ * All examples import from here to stay DRY.
4
+ */
5
+
6
+ import { nav } from '../src/ui/index.js'
7
+ import { iconSun, iconMoon } from '../src/ui/icons.js'
8
+
9
+ const ALL_LINKS = [
10
+ { label: 'Counter', href: '/counter' },
11
+ { label: 'Todos', href: '/todos' },
12
+ { label: 'Contact', href: '/contact' },
13
+ { label: 'Quiz', href: '/quiz' },
14
+ { label: 'Products', href: '/products' },
15
+ { label: 'Pricing', href: '/pricing' },
16
+ ]
17
+
18
+ // Both icons rendered in DOM; JS hides the inactive one via `hidden` attribute.
19
+ // Dark mode (default): show sun (click → go light). Light mode: show moon (click → go dark).
20
+ const themeToggleButton = `
21
+ <button id="theme-toggle" class="ui-btn ui-btn--ghost ui-btn--sm" aria-label="Switch to light mode" style="display:flex;align-items:center;">
22
+ <span id="theme-icon-sun">${iconSun({ size: 16 })}</span>
23
+ <span id="theme-icon-moon" hidden>${iconMoon({ size: 16 })}</span>
24
+ </button>`
25
+
26
+ /**
27
+ * Returns a nonce'd theme-switcher script for use as `extraBody` in createServer.
28
+ * Must be injected server-side so the nonce matches the page's CSP header.
29
+ *
30
+ * Usage in dev.server.js:
31
+ * createServer(specs, { extraBody: themeScript })
32
+ *
33
+ * @param {string} nonce
34
+ */
35
+ export function themeScript(nonce) {
36
+ return `<script nonce="${nonce}">
37
+ (function () {
38
+ var KEY = 'pulse-theme', LIGHT = 'light', DARK = 'dark'
39
+ var theme = localStorage.getItem(KEY) === LIGHT ? LIGHT : DARK
40
+ document.documentElement.dataset.theme = theme
41
+
42
+ function updateUI(t) {
43
+ var sun = document.getElementById('theme-icon-sun')
44
+ var moon = document.getElementById('theme-icon-moon')
45
+ if (sun) sun.hidden = (t === LIGHT)
46
+ if (moon) moon.hidden = (t !== LIGHT)
47
+ var btn = document.getElementById('theme-toggle')
48
+ if (btn) btn.setAttribute('aria-label', t === LIGHT ? 'Switch to dark mode' : 'Switch to light mode')
49
+ }
50
+
51
+ if (document.readyState === 'loading') {
52
+ document.addEventListener('DOMContentLoaded', function () { updateUI(theme) }, { once: true })
53
+ } else {
54
+ updateUI(theme)
55
+ }
56
+
57
+ if (window.__themeInit) return
58
+ window.__themeInit = true
59
+ document.addEventListener('click', function (e) {
60
+ var btn = e.target.closest('#theme-toggle')
61
+ if (!btn) return
62
+ var next = document.documentElement.dataset.theme === LIGHT ? DARK : LIGHT
63
+ localStorage.setItem(KEY, next)
64
+ document.documentElement.dataset.theme = next
65
+ updateUI(next)
66
+ })
67
+ })()
68
+ </script>`
69
+ }
70
+
71
+ /**
72
+ * Renders the shared examples nav bar using the ui-nav component.
73
+ * @param {string} logo - Raw HTML logo slot
74
+ * @param {string} logoHref - Logo link destination
75
+ */
76
+ export function examplesNav(logo, logoHref) {
77
+ return nav({ logo, logoHref, links: ALL_LINKS, sticky: true, action: themeToggleButton })
78
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Pulse — Todo list example
3
+ *
4
+ * Demonstrates:
5
+ * - Client-side CRUD mutations
6
+ * - persist (survives page reload)
7
+ * - Constraints (max 20 items)
8
+ * - Filter state (all / active / done)
9
+ * - Empty state handling
10
+ *
11
+ * Run: node examples/dev.server.js → http://localhost:3001/todos
12
+ */
13
+
14
+ import { button, card, checkbox, empty, input, section, container, stack, cluster } from '../src/ui/index.js'
15
+ import { examplesNav } from './shared.js'
16
+
17
+ function esc(s) {
18
+ return String(s)
19
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
21
+ }
22
+
23
+ export function filterTodos(todos, filter) {
24
+ if (filter === 'active') return todos.filter(t => !t.done)
25
+ if (filter === 'done') return todos.filter(t => t.done)
26
+ return todos
27
+ }
28
+
29
+ export function countByStatus(todos) {
30
+ const active = todos.filter(t => !t.done).length
31
+ return { active, done: todos.length - active, total: todos.length }
32
+ }
33
+
34
+ export default {
35
+ route: '/todos',
36
+ hydrate: '/examples/todos.js',
37
+
38
+ meta: {
39
+ title: 'Todos — Pulse',
40
+ description: 'A todo list built with Pulse. Demonstrates client mutations, persist, constraints, and filter state.',
41
+ styles: ['/pulse-ui.css'],
42
+ },
43
+
44
+ state: {
45
+ todos: [], // [{ id, text, done }]
46
+ filter: 'all', // all | active | done
47
+ nextId: 1,
48
+ },
49
+
50
+ persist: ['todos', 'nextId'],
51
+
52
+ constraints: {
53
+ nextId: { min: 1 },
54
+ },
55
+
56
+ mutations: {
57
+ add: (state, formData) => {
58
+ const text = formData.get('text')?.trim()
59
+ if (!text || state.todos.length >= 20) return state
60
+ return {
61
+ todos: [...state.todos, { id: state.nextId, text, done: false }],
62
+ nextId: state.nextId + 1,
63
+ }
64
+ },
65
+
66
+ toggle: (state, e) => {
67
+ const id = parseInt(e.target.closest('[data-id]')?.dataset.id, 10)
68
+ return {
69
+ todos: state.todos.map(t => t.id === id ? { ...t, done: !t.done } : t),
70
+ }
71
+ },
72
+
73
+ remove: (state, e) => {
74
+ const id = parseInt(e.target.closest('[data-id]')?.dataset.id, 10)
75
+ return { todos: state.todos.filter(t => t.id !== id) }
76
+ },
77
+
78
+ clearDone: (state) => ({
79
+ todos: state.todos.filter(t => !t.done),
80
+ }),
81
+
82
+ setFilter: (_state, e) => ({
83
+ filter: e.target.closest('[data-filter]')?.dataset.filter || 'all',
84
+ }),
85
+ },
86
+
87
+ view: (state) => {
88
+ const counts = countByStatus(state.todos)
89
+ const visible = filterTodos(state.todos, state.filter)
90
+ const atLimit = state.todos.length >= 20
91
+
92
+ const filterBtn = (value, label, count) => button({
93
+ label: `${label} (${count})`,
94
+ variant: state.filter === value ? 'primary' : 'ghost',
95
+ size: 'sm',
96
+ attrs: { 'data-event': 'setFilter', 'data-filter': value, 'aria-pressed': String(state.filter === value) },
97
+ })
98
+
99
+ const todoItem = (todo, last = false) => `
100
+ <li class="u-flex u-items-center u-gap-3 u-p-3${last ? '' : ' u-border-b'}" data-id="${todo.id}">
101
+ ${checkbox({
102
+ id: `todo-${todo.id}`,
103
+ event: 'change:toggle',
104
+ checked: todo.done,
105
+ class: 'u-flex-1',
106
+ labelHtml: `<span class="${todo.done ? 'u-text-muted' : ''}">${esc(todo.text)}</span>`,
107
+ })}
108
+ ${button({
109
+ label: '',
110
+ variant: 'ghost',
111
+ size: 'sm',
112
+ attrs: { 'data-event': 'remove', 'aria-label': `Remove "${esc(todo.text)}"` },
113
+ icon: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
114
+ })}
115
+ </li>`
116
+
117
+ const listContent = visible.length === 0
118
+ ? `<div class="u-p-6">${empty({
119
+ title: state.filter === 'done' ? 'No completed todos yet'
120
+ : state.filter === 'active' ? 'Nothing left to do!'
121
+ : 'No todos yet',
122
+ description: state.filter === 'all' ? 'Add something above to get started.' : '',
123
+ })}</div>`
124
+ : `<ul aria-label="Todo items">${visible.map((t, i) => todoItem(t, i === visible.length - 1)).join('')}</ul>`
125
+
126
+ return `
127
+ ${examplesNav('<span>✓ Todos</span>', '/todos')}
128
+
129
+ <main id="main-content">
130
+ ${section({ eyebrow: 'Client State & Persistence', title: 'My Todos', level: 1, align: 'center',
131
+ subtitle: 'Todos persist across reloads. Mutations are pure — add, toggle, remove, and filter without a server round-trip.',
132
+ content:
133
+ container({ size: 'md', content: stack({ gap: 'md', content: `
134
+
135
+ <form data-action="add" data-reset aria-label="Add a todo">
136
+ ${stack({ gap: 'xs', content: `
137
+ <div class="u-flex u-gap-2 u-items-end">
138
+ ${input({ name: 'text', placeholder: 'What needs doing?', disabled: atLimit, class: 'u-flex-1', attrs: { maxlength: '120', 'aria-label': 'New todo text' } })}
139
+ ${button({ label: 'Add', type: 'submit', disabled: atLimit })}
140
+ </div>
141
+ ${atLimit ? `<p class="u-text-sm u-text-yellow" role="alert">Maximum of 20 todos reached.</p>` : ''}
142
+ ` })}
143
+ </form>
144
+
145
+ ${cluster({ justify: 'between', content: `
146
+ <div role="group" aria-label="Filter todos" class="u-flex u-gap-1">
147
+ ${filterBtn('all', 'All', counts.total)}
148
+ ${filterBtn('active', 'Active', counts.active)}
149
+ ${filterBtn('done', 'Done', counts.done)}
150
+ </div>
151
+ ${counts.done > 0 && state.filter !== 'active'
152
+ ? button({ label: 'Clear done', variant: 'ghost', size: 'sm', attrs: { 'data-event': 'clearDone' } })
153
+ : ''}
154
+ ` })}
155
+
156
+ ${card({ flush: true, content: listContent })}
157
+
158
+ ` }) })
159
+ })}
160
+ </main>`
161
+ },
162
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@invisibleloop/pulse",
3
+ "version": "0.1.21",
4
+ "type": "module",
5
+ "description": "AI-first frontend framework. The spec is the source of truth.",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "pulse": "./src/cli/index.js"
9
+ },
10
+ "types": "./types/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./types/index.d.ts",
14
+ "default": "./src/server/index.js"
15
+ },
16
+ "./server": {
17
+ "types": "./types/server.d.ts",
18
+ "default": "./src/server/index.js"
19
+ },
20
+ "./runtime": {
21
+ "types": "./types/runtime.d.ts",
22
+ "default": "./src/runtime/index.js"
23
+ },
24
+ "./ssr": {
25
+ "types": "./types/ssr.d.ts",
26
+ "default": "./src/runtime/ssr.js"
27
+ },
28
+ "./navigate": {
29
+ "types": "./types/navigate.d.ts",
30
+ "default": "./src/runtime/navigate.js"
31
+ },
32
+ "./schema": {
33
+ "types": "./types/schema.d.ts",
34
+ "default": "./src/spec/schema.js"
35
+ },
36
+ "./image": {
37
+ "types": "./types/image.d.ts",
38
+ "default": "./src/runtime/image.js"
39
+ },
40
+ "./html": {
41
+ "types": "./types/html.d.ts",
42
+ "default": "./src/html.js"
43
+ },
44
+ "./ui": {
45
+ "types": "./types/ui.d.ts",
46
+ "default": "./src/ui/index.js"
47
+ },
48
+ "./testing": {
49
+ "types": "./types/testing.d.ts",
50
+ "default": "./src/testing/index.js"
51
+ }
52
+ },
53
+ "scripts": {
54
+ "dev": "node --watch src/cli/dev.js",
55
+ "build": "node scripts/build.js",
56
+ "test": "node src/spec/schema.test.js && node src/runtime/runtime.test.js && node src/runtime/ssr.test.js && node src/runtime/image.test.js && node src/server/server.test.js && node src/cli/cli.test.js && node src/ui/ui.test.js && node src/testing/testing.test.js && node src/store/store.test.js",
57
+ "typecheck": "tsc --noEmit",
58
+ "docs": "node --watch src/cli/dev.js --root docs --port 4000",
59
+ "docs:start": "node docs/server.js"
60
+ },
61
+ "engines": {
62
+ "node": ">=22"
63
+ },
64
+ "dependencies": {
65
+ "@modelcontextprotocol/sdk": "^1.27.1",
66
+ "esbuild": "^0.27.4"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^25.5.0",
73
+ "typescript": "^5.9.3"
74
+ }
75
+ }
@@ -0,0 +1 @@
1
+ 0.1.17
@@ -0,0 +1,246 @@
1
+ /* Chippy Bird — Landing page theme
2
+ * Overrides --ui-* tokens and adds page-specific styles only.
3
+ * Layout is handled by pulse-ui.css layout components.
4
+ */
5
+
6
+ :root {
7
+ --ui-accent: #f7b731;
8
+ --ui-accent-hover: #f9c74f;
9
+ --ui-accent-dim: rgba(247, 183, 49, .15);
10
+ --ui-bg: #08111f;
11
+ --ui-surface: #0d1c36;
12
+ --ui-surface-2: #12233f;
13
+ --ui-border: #1a3055;
14
+ --ui-text: #eef3ff;
15
+ --ui-muted: #6e88b0;
16
+ --ui-green: #3dd68c;
17
+ --ui-red: #ff6b6b;
18
+
19
+ /* Game palette — extracted from screenshots */
20
+ --cb-sky: #87ceeb;
21
+ --cb-sky-mid: #5aabe0;
22
+ --cb-wood: #9e6b2e;
23
+ }
24
+
25
+ /* ─── Base ───────────────────────────────────────────────────────────────── */
26
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
27
+ html { font-size: 16px; }
28
+ body {
29
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
30
+ background-color: var(--ui-bg);
31
+ color: var(--ui-text);
32
+ line-height: 1.6;
33
+ min-height: 100vh;
34
+ -webkit-font-smoothing: antialiased;
35
+ }
36
+ a { color: var(--ui-accent); text-decoration: none; }
37
+ a:hover { opacity: .85; }
38
+
39
+ /* ─── Nav logo ───────────────────────────────────────────────────────────── */
40
+ .cb-logo { font-weight: 800; font-size: 1rem; letter-spacing: -.01em; }
41
+
42
+ /* ─── Eyebrow + titles ───────────────────────────────────────────────────── */
43
+ .cb-eyebrow {
44
+ font-size: .78rem;
45
+ font-weight: 600;
46
+ letter-spacing: .08em;
47
+ text-transform: uppercase;
48
+ color: var(--ui-accent);
49
+ }
50
+
51
+ .cb-section-title {
52
+ font-size: clamp(1.75rem, 4vw, 2.5rem);
53
+ font-weight: 800;
54
+ line-height: 1.15;
55
+ letter-spacing: -.02em;
56
+ color: var(--ui-text);
57
+ text-align: center;
58
+ }
59
+
60
+ .cb-muted {
61
+ font-size: 1rem;
62
+ color: var(--ui-muted);
63
+ line-height: 1.6;
64
+ text-align: center;
65
+ max-width: 480px;
66
+ }
67
+
68
+ /* ─── Stats divider ──────────────────────────────────────────────────────── */
69
+ .cb-stat-divider {
70
+ width: 1px;
71
+ height: 3rem;
72
+ background: var(--ui-border);
73
+ flex-shrink: 0;
74
+ }
75
+
76
+ .ui-stat-value { color: var(--ui-accent); }
77
+
78
+ /* ─── Free badge ─────────────────────────────────────────────────────────── */
79
+ .cb-free-badge {
80
+ display: inline-block;
81
+ background: var(--ui-accent-dim);
82
+ color: var(--ui-accent);
83
+ font-size: .78rem;
84
+ font-weight: 700;
85
+ letter-spacing: .06em;
86
+ text-transform: uppercase;
87
+ padding: .3rem .9rem;
88
+ border-radius: 99px;
89
+ }
90
+
91
+ /* ─── CTA section ────────────────────────────────────────────────────────── */
92
+ .cb-cta { background: linear-gradient(135deg, #0a1628 0%, #0f2a5a 100%); }
93
+
94
+ .cb-cta-title {
95
+ font-size: clamp(2rem, 5vw, 3rem);
96
+ font-weight: 800;
97
+ letter-spacing: -.02em;
98
+ color: var(--ui-text);
99
+ }
100
+
101
+ /* ─── Notify form ────────────────────────────────────────────────────────── */
102
+ .cb-notify-form { width: 100%; max-width: 420px; }
103
+ .cb-notify-form--loading { opacity: .6; pointer-events: none; }
104
+
105
+ .cb-notify-input {
106
+ flex: 1;
107
+ min-width: 0;
108
+ height: 44px;
109
+ padding: 0 .875rem;
110
+ background: var(--ui-surface);
111
+ border: 1px solid var(--ui-border);
112
+ border-radius: var(--ui-radius, 8px);
113
+ color: var(--ui-text);
114
+ font-size: .9rem;
115
+ font-family: inherit;
116
+ outline: none;
117
+ }
118
+ .cb-notify-input:focus-visible {
119
+ border-color: var(--ui-accent);
120
+ box-shadow: 0 0 0 2px var(--ui-accent-dim);
121
+ }
122
+ .cb-notify-input::placeholder { color: var(--ui-muted); }
123
+
124
+ /* ─── Footer ─────────────────────────────────────────────────────────────── */
125
+ .cb-footer {
126
+ background: var(--ui-surface);
127
+ border-top: 1px solid var(--ui-border);
128
+ padding: 1.5rem;
129
+ text-align: center;
130
+ }
131
+ .cb-footer p { font-size: .85rem; color: var(--ui-muted); margin: 0; }
132
+ .cb-footer a { color: var(--ui-accent); text-decoration: underline; text-underline-offset: 2px; }
133
+
134
+ /* ─── Split Hero ─────────────────────────────────────────────────────────── */
135
+ .cb-hero-wrap {
136
+ background:
137
+ radial-gradient(ellipse 55% 80% at 78% 55%, rgba(87, 171, 224, .18) 0%, transparent 65%),
138
+ radial-gradient(ellipse 30% 40% at 78% 20%, rgba(135, 206, 235, .10) 0%, transparent 55%),
139
+ var(--ui-bg);
140
+ }
141
+
142
+ .cb-hero-split {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ gap: 4rem;
147
+ max-width: 1100px;
148
+ margin: 0 auto;
149
+ padding: 6rem 2rem 5rem;
150
+ min-height: 680px;
151
+ }
152
+
153
+ .cb-hero-content { flex: 1; max-width: 500px; }
154
+
155
+ .cb-hero-heading {
156
+ font-size: clamp(2.75rem, 5.5vw, 4.25rem);
157
+ font-weight: 900;
158
+ line-height: 1.0;
159
+ letter-spacing: -.03em;
160
+ color: var(--ui-text);
161
+ margin: .6rem 0 1.25rem;
162
+ }
163
+
164
+ .cb-hero-sub {
165
+ font-size: 1.05rem;
166
+ color: var(--ui-muted);
167
+ line-height: 1.65;
168
+ margin-bottom: 2rem;
169
+ max-width: 400px;
170
+ }
171
+
172
+ .cb-hero-actions { display: flex; gap: .75rem; flex-wrap: wrap; }
173
+
174
+ /* ─── iPhone Frame ───────────────────────────────────────────────────────── */
175
+ .cb-hero-visual {
176
+ flex-shrink: 0;
177
+ display: flex;
178
+ align-items: center;
179
+ }
180
+
181
+ .cb-iphone {
182
+ position: relative;
183
+ width: 230px;
184
+ background: #0a0b14;
185
+ border-radius: 42px;
186
+ padding: 13px;
187
+ box-shadow:
188
+ 0 0 0 2px rgba(255,255,255,.09),
189
+ 0 0 0 7px #0a0b14,
190
+ 0 0 0 9px rgba(255,255,255,.04),
191
+ 0 50px 100px rgba(0,0,0,.75),
192
+ 0 0 80px rgba(87, 171, 224, .22);
193
+ animation: cb-float 5s ease-in-out infinite;
194
+ transform: rotate(3deg);
195
+ }
196
+
197
+ /* Dynamic Island */
198
+ .cb-iphone::before {
199
+ content: '';
200
+ position: absolute;
201
+ top: 17px;
202
+ left: 50%;
203
+ transform: translateX(-50%);
204
+ width: 78px;
205
+ height: 24px;
206
+ background: #0a0b14;
207
+ border-radius: 12px;
208
+ z-index: 3;
209
+ }
210
+
211
+ .cb-iphone-screen {
212
+ border-radius: 31px;
213
+ overflow: hidden;
214
+ line-height: 0;
215
+ }
216
+
217
+ .cb-iphone-screen img {
218
+ display: block;
219
+ width: 100%;
220
+ height: auto;
221
+ }
222
+
223
+ @keyframes cb-float {
224
+ 0%, 100% { transform: rotate(3deg) translateY(0); }
225
+ 50% { transform: rotate(3deg) translateY(-14px); }
226
+ }
227
+
228
+ /* ─── Mobile ─────────────────────────────────────────────────────────────── */
229
+ @media (max-width: 640px) {
230
+ .cb-stat-divider { display: none; }
231
+ .cb-notify-form .ui-cluster { flex-direction: column; }
232
+ .cb-notify-form .ui-btn { width: 100%; }
233
+ }
234
+
235
+ @media (max-width: 768px) {
236
+ .cb-hero-split {
237
+ flex-direction: column-reverse;
238
+ gap: 2.5rem;
239
+ padding: 3rem 1.5rem;
240
+ text-align: center;
241
+ }
242
+ .cb-hero-content { max-width: 100%; }
243
+ .cb-hero-sub { max-width: 100%; margin-left: auto; margin-right: auto; }
244
+ .cb-hero-actions { justify-content: center; }
245
+ .cb-iphone { width: 200px; }
246
+ }
@@ -0,0 +1,119 @@
1
+ /* ─── Contact example ────────────────────────────────────────────────────── */
2
+
3
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
4
+
5
+ body {
6
+ font-family: var(--ui-font, system-ui, sans-serif);
7
+ background: var(--ui-bg);
8
+ color: var(--ui-text);
9
+ min-height: 100dvh;
10
+ }
11
+
12
+ .ct-root { display: flex; flex-direction: column; min-height: 100dvh; }
13
+
14
+ /* Main */
15
+ .ct-main {
16
+ flex: 1;
17
+ padding: 3rem 1rem;
18
+ }
19
+
20
+ /* Grid */
21
+ .ct-grid {
22
+ display: grid;
23
+ grid-template-columns: 1fr 1.4fr;
24
+ gap: 4rem;
25
+ align-items: start;
26
+ }
27
+
28
+ @media (max-width: 720px) {
29
+ .ct-grid { grid-template-columns: 1fr; gap: 2rem; }
30
+ }
31
+
32
+ /* Info column */
33
+ .ct-eyebrow {
34
+ font-size: .75rem;
35
+ text-transform: uppercase;
36
+ letter-spacing: .08em;
37
+ color: var(--ui-accent);
38
+ font-weight: 600;
39
+ margin-bottom: .75rem;
40
+ }
41
+
42
+ .ct-subtitle {
43
+ color: var(--ui-muted);
44
+ line-height: 1.6;
45
+ margin-bottom: 2.5rem;
46
+ }
47
+
48
+ .ct-details { display: flex; flex-direction: column; gap: 1.5rem; }
49
+
50
+ .ct-detail { display: flex; align-items: flex-start; gap: .875rem; }
51
+
52
+ .ct-detail-icon {
53
+ margin-top: .1rem;
54
+ flex-shrink: 0;
55
+ }
56
+
57
+ .ct-detail-label {
58
+ font-size: .75rem;
59
+ text-transform: uppercase;
60
+ letter-spacing: .05em;
61
+ color: var(--ui-muted);
62
+ margin-bottom: .2rem;
63
+ }
64
+
65
+ .ct-detail-value {
66
+ font-size: .95rem;
67
+ color: var(--ui-text);
68
+ text-decoration: none;
69
+ }
70
+
71
+ a.ct-detail-value:hover { color: var(--ui-accent); }
72
+
73
+ /* Form */
74
+ .ct-form-alerts:not(:empty) { margin-bottom: 1.25rem; }
75
+
76
+ .ct-fieldset {
77
+ border: none;
78
+ padding: 0;
79
+ margin: 0;
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 1.25rem;
83
+ }
84
+
85
+ .ct-legend {
86
+ font-size: 1.1rem;
87
+ font-weight: 600;
88
+ margin-bottom: 1.25rem;
89
+ }
90
+
91
+ .ct-row {
92
+ display: grid;
93
+ grid-template-columns: 1fr 1fr;
94
+ gap: 1rem;
95
+ }
96
+
97
+ @media (max-width: 480px) {
98
+ .ct-row { grid-template-columns: 1fr; }
99
+ }
100
+
101
+ /* Success */
102
+ .ct-success-icon {
103
+ width: 3.5rem;
104
+ height: 3.5rem;
105
+ border-radius: 50%;
106
+ background: #d1fae5;
107
+ color: #059669;
108
+ font-size: 1.5rem;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ font-weight: 700;
113
+ }
114
+
115
+ .ct-success-msg {
116
+ color: var(--ui-muted);
117
+ text-align: center;
118
+ max-width: 30ch;
119
+ }