@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,236 @@
1
+ /**
2
+ * pulse-ui.js — Vanilla JS for interactive Pulse UI components
3
+ *
4
+ * Handles:
5
+ * - Slider (.ui-slider — live fill + value output)
6
+ * - File Upload (.ui-upload — drag-and-drop, click-to-open)
7
+ * - Modal (data-modal-open, <dialog>, backdrop close)
8
+ * - Nav (.ui-nav — mobile burger menu)
9
+ * - Carousel (.ui-carousel — prev/next/dots)
10
+ *
11
+ * No dependencies. No build step required.
12
+ * CSP-safe: no inline handlers — all behaviour via event delegation.
13
+ * Include once per page: <script src="/pulse-ui.js"></script>
14
+ */
15
+
16
+ // ─── Slider ──────────────────────────────────────────────────────────────────
17
+ // Update --slider-fill on .ui-field wrapper + optional live value output.
18
+
19
+ document.addEventListener('input', (e) => {
20
+ const el = e.target
21
+ if (!el.classList.contains('ui-slider')) return
22
+ const field = el.closest('.ui-field')
23
+ if (!field) return
24
+ const pct = ((el.value - el.min) / (el.max - el.min) * 100).toFixed(2) + '%'
25
+ field.style.setProperty('--slider-fill', pct)
26
+ const out = field.querySelector('.ui-slider-output')
27
+ if (out) out.textContent = el.value
28
+ })
29
+
30
+ // ─── File Upload ─────────────────────────────────────────────────────────────
31
+ // Prevent browser navigating to dropped file anywhere on the page.
32
+ // Handle drag visual feedback + file assignment on drop.
33
+ // Handle click-to-open and keyboard activation.
34
+
35
+ document.addEventListener('dragover', (e) => {
36
+ e.preventDefault()
37
+ if (!(e.target instanceof Element)) return
38
+ const zone = e.target.closest('.ui-upload:not(.ui-upload--disabled)')
39
+ if (zone) zone.classList.add('ui-upload--active')
40
+ })
41
+
42
+ document.addEventListener('dragleave', (e) => {
43
+ if (!(e.target instanceof Element)) return
44
+ const zone = e.target.closest('.ui-upload')
45
+ if (zone && !zone.contains(e.relatedTarget)) {
46
+ zone.classList.remove('ui-upload--active')
47
+ }
48
+ })
49
+
50
+ document.addEventListener('drop', (e) => {
51
+ e.preventDefault()
52
+ if (!(e.target instanceof Element)) return
53
+ const zone = e.target.closest('.ui-upload:not(.ui-upload--disabled)')
54
+ if (!zone) return
55
+ zone.classList.remove('ui-upload--active')
56
+ const input = zone.querySelector('.ui-upload-input')
57
+ if (!input || !e.dataTransfer.files.length) return
58
+ const dt = new DataTransfer()
59
+ Array.from(e.dataTransfer.files).forEach(f => dt.items.add(f))
60
+ input.files = dt.files
61
+ input.dispatchEvent(new Event('change', { bubbles: true }))
62
+ })
63
+
64
+ // File Upload: show selected filename in zone after file chosen
65
+ document.addEventListener('change', (e) => {
66
+ const input = e.target
67
+ if (!input.classList.contains('ui-upload-input')) return
68
+ const zone = input.closest('.ui-upload')
69
+ if (!zone) return
70
+ const textEl = zone.querySelector('.ui-upload-text')
71
+ if (!textEl) return
72
+ if (input.files && input.files.length > 0) {
73
+ const names = Array.from(input.files).map(f => f.name).join(', ')
74
+ textEl.textContent = names
75
+ zone.classList.add('ui-upload--selected')
76
+ } else {
77
+ textEl.innerHTML = 'Drag &amp; drop or <span class="ui-upload-browse">browse</span>'
78
+ zone.classList.remove('ui-upload--selected')
79
+ }
80
+ })
81
+
82
+ // ─── Modal ──────────────────────────────────────────────────────────────────
83
+
84
+ document.addEventListener('click', (e) => {
85
+ // File upload: click zone → open file picker
86
+ // Skip synthetic clicks emitted by input.click() itself to avoid loops
87
+ if (!e.target.classList.contains('ui-upload-input')) {
88
+ const zone = e.target.closest('.ui-upload')
89
+ if (zone) {
90
+ const input = zone.querySelector('.ui-upload-input')
91
+ if (input && !input.disabled) { input.click(); return }
92
+ }
93
+ }
94
+
95
+ // Dialog open: data-dialog-open="dialogId" (first-class) or data-modal-open="dialogId" (compat)
96
+ const trigger = e.target.closest('[data-dialog-open],[data-modal-open]')
97
+ if (trigger) {
98
+ const id = trigger.dataset.dialogOpen ?? trigger.dataset.modalOpen
99
+ const dialog = document.getElementById(id)
100
+ if (dialog && typeof dialog.showModal === 'function') {
101
+ dialog.showModal()
102
+ }
103
+ return
104
+ }
105
+
106
+ // Dialog close: data-dialog-close (closes nearest ancestor <dialog>)
107
+ const closeTarget = e.target.closest('[data-dialog-close]')
108
+ if (closeTarget) {
109
+ const dialog = closeTarget.closest('dialog')
110
+ if (dialog) dialog.close()
111
+ return
112
+ }
113
+
114
+ // Backdrop close — click lands on <dialog> element itself
115
+ if (e.target.tagName === 'DIALOG') {
116
+ e.target.close()
117
+ }
118
+ })
119
+
120
+ // File upload: Enter / Space on focused zone → open picker
121
+ document.addEventListener('keydown', (e) => {
122
+ if (e.key !== 'Enter' && e.key !== ' ') return
123
+ const zone = e.target.closest('.ui-upload')
124
+ if (!zone) return
125
+ const input = zone.querySelector('.ui-upload-input')
126
+ if (input && !input.disabled) { e.preventDefault(); input.click() }
127
+ })
128
+
129
+ // ─── Nav (mobile burger) ────────────────────────────────────────────────────
130
+
131
+ function initNav(el) {
132
+ const burger = el.querySelector('.ui-nav-burger')
133
+ const mobile = el.querySelector('.ui-nav-mobile')
134
+ if (!burger || !mobile) return
135
+
136
+ const open = () => { el.classList.add('ui-nav--open'); burger.setAttribute('aria-expanded', 'true') }
137
+ const close = () => { el.classList.remove('ui-nav--open'); burger.setAttribute('aria-expanded', 'false') }
138
+ const toggle = () => el.classList.contains('ui-nav--open') ? close() : open()
139
+
140
+ burger.addEventListener('click', toggle)
141
+
142
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
143
+ document.addEventListener('click', (e) => { if (!el.contains(e.target)) close() })
144
+
145
+ // Close when a mobile link is clicked (navigating away)
146
+ mobile.querySelectorAll('.ui-nav-link').forEach(a => a.addEventListener('click', close))
147
+ }
148
+
149
+ function runNavs() { document.querySelectorAll('.ui-nav').forEach(initNav) }
150
+ document.readyState === 'loading'
151
+ ? document.addEventListener('DOMContentLoaded', runNavs)
152
+ : runNavs()
153
+ document.addEventListener('pulse:navigate', runNavs)
154
+
155
+ // ─── Carousel ───────────────────────────────────────────────────────────────
156
+
157
+ function initCarousel(el) {
158
+ const track = el.querySelector('.ui-carousel-track')
159
+ const prev = el.querySelector('.ui-carousel-prev')
160
+ const next = el.querySelector('.ui-carousel-next')
161
+ const dots = Array.from(el.querySelectorAll('.ui-carousel-dot'))
162
+ const slides = Array.from(el.querySelectorAll('.ui-carousel-slide'))
163
+
164
+ if (!track || slides.length === 0) return
165
+
166
+ let current = 0
167
+
168
+ const updateArrows = () => {
169
+ if (prev) prev.hidden = current === 0
170
+ if (next) next.hidden = current === slides.length - 1
171
+ }
172
+
173
+ const goTo = (i) => {
174
+ current = Math.max(0, Math.min(i, slides.length - 1))
175
+ track.scrollTo({ left: slides[current].offsetLeft, behavior: 'smooth' })
176
+ dots.forEach((d, j) => {
177
+ const active = j === current
178
+ d.classList.toggle('active', active)
179
+ d.setAttribute('aria-selected', String(active))
180
+ d.setAttribute('tabindex', active ? '0' : '-1')
181
+ })
182
+ updateArrows()
183
+ }
184
+
185
+ prev?.addEventListener('click', () => goTo(current - 1))
186
+ next?.addEventListener('click', () => goTo(current + 1))
187
+ dots.forEach((d, i) => d.addEventListener('click', () => goTo(i)))
188
+
189
+ // Keyboard navigation — roving tabindex for tablist
190
+ el.addEventListener('keydown', (e) => {
191
+ if (!e.target.classList.contains('ui-carousel-dot')) return
192
+ let next = null
193
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
194
+ e.preventDefault()
195
+ next = current > 0 ? current - 1 : slides.length - 1
196
+ } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
197
+ e.preventDefault()
198
+ next = current < slides.length - 1 ? current + 1 : 0
199
+ } else if (e.key === 'Home') {
200
+ e.preventDefault()
201
+ next = 0
202
+ } else if (e.key === 'End') {
203
+ e.preventDefault()
204
+ next = slides.length - 1
205
+ }
206
+ if (next !== null) {
207
+ goTo(next)
208
+ dots[next]?.focus()
209
+ }
210
+ })
211
+
212
+ // Sync dot state and arrows when user swipes / scrolls
213
+ track.addEventListener('scroll', () => {
214
+ const trackLeft = track.getBoundingClientRect().left
215
+ const idx = slides.findIndex((s) => Math.abs(s.getBoundingClientRect().left - trackLeft) < 10)
216
+ if (idx !== -1 && idx !== current) {
217
+ current = idx
218
+ dots.forEach((d, j) => {
219
+ const active = j === current
220
+ d.classList.toggle('active', active)
221
+ d.setAttribute('aria-selected', String(active))
222
+ d.setAttribute('tabindex', active ? '0' : '-1')
223
+ })
224
+ updateArrows()
225
+ }
226
+ }, { passive: true })
227
+ }
228
+
229
+ // Initialise all carousels on the page
230
+ const run = () => document.querySelectorAll('.ui-carousel').forEach(initCarousel)
231
+ document.readyState === 'loading'
232
+ ? document.addEventListener('DOMContentLoaded', run)
233
+ : run()
234
+
235
+ // Re-initialise after Pulse client-side navigation swaps the DOM
236
+ document.addEventListener('pulse:navigate', run)
@@ -0,0 +1,149 @@
1
+ /* Pulse — base stylesheet */
2
+
3
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
4
+
5
+ :root {
6
+ --bg: #0f0f0f;
7
+ --surface: #1a1a1a;
8
+ --border: #2a2a2a;
9
+ --text: #e8e8e8;
10
+ --muted: #888;
11
+ --accent: #9b8dff; /* links — passes 7:1 on dark bg */
12
+ --accent-h: #b8aeff; /* link hover */
13
+ --accent-btn: #5c4de3; /* button bg — white text passes 5.4:1 */
14
+ --success: #3ecf8e;
15
+ --error: #f03e3e;
16
+ --radius: 8px;
17
+ --font: system-ui, -apple-system, sans-serif;
18
+ }
19
+
20
+ html { font-size: 16px; }
21
+
22
+ body {
23
+ font-family: var(--font);
24
+ background-color: var(--bg);
25
+ color: var(--text);
26
+ line-height: 1.6;
27
+ min-height: 100vh;
28
+ }
29
+
30
+ a { color: var(--accent); text-decoration: none; }
31
+ a:hover { color: var(--accent-h); text-decoration: underline; }
32
+
33
+ /* Layout */
34
+ .page {
35
+ max-width: 640px;
36
+ margin: 0 auto;
37
+ padding: 3rem 1.5rem;
38
+ }
39
+
40
+ /* Typography */
41
+ h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 0.5rem; }
42
+ h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
43
+ p { color: var(--muted); margin-bottom: 1rem; }
44
+
45
+ /* Buttons */
46
+ button {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ gap: 0.4rem;
51
+ font-family: var(--font);
52
+ font-size: 0.9rem;
53
+ font-weight: 500;
54
+ padding: 0.5rem 1.25rem;
55
+ border-radius: var(--radius);
56
+ border: 1px solid var(--border);
57
+ background: var(--surface);
58
+ color: var(--text);
59
+ cursor: pointer;
60
+ transition: background 0.15s, border-color 0.15s;
61
+ }
62
+ button:hover:not([disabled]) {
63
+ background: var(--accent);
64
+ border-color: var(--accent);
65
+ color: #fff;
66
+ }
67
+ button[disabled] { opacity: 0.35; cursor: default; }
68
+ button.primary {
69
+ background: var(--accent-btn);
70
+ border-color: var(--accent-btn);
71
+ color: #fff;
72
+ }
73
+ button.primary:hover:not([disabled]) {
74
+ background: color-mix(in srgb, var(--accent-btn) 85%, #000);
75
+ border-color: color-mix(in srgb, var(--accent-btn) 85%, #000);
76
+ }
77
+
78
+ /* Forms */
79
+ label {
80
+ display: block;
81
+ font-size: 0.85rem;
82
+ font-weight: 500;
83
+ color: var(--muted);
84
+ margin-bottom: 1.25rem;
85
+ }
86
+ label span { display: block; margin-bottom: 0.35rem; }
87
+
88
+ input, textarea, select {
89
+ display: block;
90
+ width: 100%;
91
+ font-family: var(--font);
92
+ font-size: 0.95rem;
93
+ padding: 0.6rem 0.85rem;
94
+ background: var(--surface);
95
+ border: 1px solid var(--border);
96
+ border-radius: var(--radius);
97
+ color: var(--text);
98
+ outline: none;
99
+ transition: border-color 0.15s;
100
+ }
101
+ input:focus, textarea:focus { border-color: var(--accent-btn); }
102
+ textarea { min-height: 120px; resize: vertical; }
103
+
104
+ /* Feedback */
105
+ .errors {
106
+ list-style: none;
107
+ background: color-mix(in srgb, var(--error) 12%, transparent);
108
+ border: 1px solid color-mix(in srgb, var(--error) 30%, transparent);
109
+ border-radius: var(--radius);
110
+ padding: 0.75rem 1rem;
111
+ margin-bottom: 1.25rem;
112
+ font-size: 0.875rem;
113
+ color: var(--error);
114
+ }
115
+ .errors li + li { margin-top: 0.35rem; }
116
+
117
+ .success {
118
+ background: color-mix(in srgb, var(--success) 12%, transparent);
119
+ border: 1px solid color-mix(in srgb, var(--success) 30%, transparent);
120
+ border-radius: var(--radius);
121
+ padding: 1rem 1.25rem;
122
+ color: var(--success);
123
+ font-weight: 500;
124
+ }
125
+
126
+ /* Counter */
127
+ .counter { text-align: center; }
128
+ .counter .value {
129
+ font-size: 5rem;
130
+ font-weight: 800;
131
+ line-height: 1;
132
+ margin: 1.5rem 0;
133
+ letter-spacing: -0.04em;
134
+ color: var(--text);
135
+ }
136
+ .counter .controls { display: flex; gap: 0.75rem; justify-content: center; }
137
+ .counter .controls button { font-size: 1.25rem; min-width: 48px; }
138
+ .counter .hint { font-size: 0.8rem; color: var(--muted); margin-top: 1rem; }
139
+
140
+ /* Form loading state */
141
+ .form.loading { opacity: 0.6; pointer-events: none; }
142
+
143
+ /* Skip link — visually hidden until focused */
144
+ .pulse-skip-link{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}
145
+ .pulse-skip-link:focus{position:fixed;left:1rem;top:1rem;width:auto;height:auto;padding:.5rem 1rem;background:#000;color:#fff;font-size:1rem;font-weight:600;border-radius:4px;text-decoration:none;z-index:9999;outline:3px solid #fff;outline-offset:2px}
146
+
147
+ /* Focus ring */
148
+ :focus-visible{outline:3px solid #9b8dff;outline-offset:2px}
149
+ :focus:not(:focus-visible){outline:none}