@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,89 @@
1
+ /**
2
+ * Pulse UI — Component library
3
+ *
4
+ * Server-rendered HTML string components. Import what you need:
5
+ *
6
+ * import { button, card, input } from '@invisibleloop/pulse/ui'
7
+ *
8
+ * All components:
9
+ * - Are pure functions: (props) => HTML string
10
+ * - Escape user data automatically
11
+ * - Use CSS custom properties for all visual values
12
+ * - Require no client-side JS
13
+ * - Meet WCAG AA contrast and keyboard accessibility requirements
14
+ * - Work at all viewport sizes
15
+ *
16
+ * Theming: override CSS custom properties in :root in your stylesheet.
17
+ * Extending: add new CSS modifier classes (e.g. .ui-btn--brand).
18
+ * Do not modify component source files — extend via CSS only.
19
+ */
20
+
21
+ export { button } from './button.js'
22
+ export { badge } from './badge.js'
23
+ export { card } from './card.js'
24
+ export { input } from './input.js'
25
+ export { search } from './search.js'
26
+ export { fieldset } from './fieldset.js'
27
+ export { select } from './select.js'
28
+ export { textarea } from './textarea.js'
29
+ export { alert } from './alert.js'
30
+ export { stat } from './stat.js'
31
+ export { avatar } from './avatar.js'
32
+ export { empty } from './empty.js'
33
+ export { table } from './table.js'
34
+ export { hero } from './hero.js'
35
+ export { testimonial } from './testimonial.js'
36
+ export { feature } from './feature.js'
37
+ export { pricing } from './pricing.js'
38
+ export { accordion } from './accordion.js'
39
+ export { nav } from './nav.js'
40
+ export { appBadge } from './app-badge.js'
41
+ export { container } from './container.js'
42
+ export { section } from './section.js'
43
+ export { grid } from './grid.js'
44
+ export { stack } from './stack.js'
45
+ export { cluster } from './cluster.js'
46
+ export { divider } from './divider.js'
47
+ export { banner } from './banner.js'
48
+ export { media } from './media.js'
49
+ export { tooltip } from './tooltip.js'
50
+ export { modal, modalTrigger } from './modal.js'
51
+ export { carousel } from './carousel.js'
52
+ export { cta } from './cta.js'
53
+ export { codeWindow } from './code-window.js'
54
+ export { footer } from './footer.js'
55
+ export { timeline, timelineItem } from './timeline.js'
56
+ export { toggle } from './switch.js'
57
+ export { checkbox } from './checkbox.js'
58
+ export { radio, radioGroup } from './radio.js'
59
+ export { rating } from './rating.js'
60
+ export { spinner } from './spinner.js'
61
+ export { progress } from './progress.js'
62
+ export { barChart, lineChart, donutChart, sparkline } from './charts.js'
63
+ export { slider } from './slider.js'
64
+ export { segmented } from './segmented.js'
65
+ export { fileUpload } from './fileupload.js'
66
+ export { breadcrumbs } from './breadcrumbs.js'
67
+ export { stepper } from './stepper.js'
68
+ export { uiImage } from './uiimage.js'
69
+ export { pullquote } from './pullquote.js'
70
+ export { prose } from './prose.js'
71
+ export { heading } from './heading.js'
72
+ export { list } from './list.js'
73
+ export {
74
+ iconArrowLeft, iconArrowRight, iconArrowUp, iconArrowDown,
75
+ iconChevronLeft, iconChevronRight, iconChevronUp, iconChevronDown,
76
+ iconExternalLink, iconMenu, iconX, iconMoreHorizontal, iconMoreVertical,
77
+ iconCheck, iconCheckCircle, iconXCircle, iconAlertCircle, iconAlertTriangle, iconInfo,
78
+ iconPlus, iconMinus, iconEdit, iconTrash, iconCopy, iconSearch, iconFilter,
79
+ iconDownload, iconUpload, iconRefresh, iconSend,
80
+ iconEye, iconEyeOff, iconLock, iconUnlock, iconSettings, iconBell,
81
+ iconUser, iconUsers, iconMail, iconMessageSquare,
82
+ iconHome, iconLogOut, iconLogIn,
83
+ iconFile, iconImage, iconLink, iconCode, iconCalendar, iconClock, iconBookmark, iconTag,
84
+ iconPlay, iconPause, iconVolume, iconStar, iconHeart,
85
+ iconPhone, iconGamepad,
86
+ iconHandPointUp, iconHandPointDown, iconHandPointLeft, iconHandPointRight,
87
+ iconGlobe, iconShield, iconZap, iconTrendingUp, iconTrendingDown, iconLoader, iconGrid, iconBug, iconMapPin,
88
+ iconSun, iconMoon,
89
+ } from './icons.js'
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pulse UI — Input
3
+ *
4
+ * Text input with label, hint, and error message.
5
+ * aria-describedby is wired automatically from name when error/hint are present.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.name - Field name (also used as id base)
9
+ * @param {string} opts.label - Visible label text
10
+ * @param {string} opts.type - Input type (default: 'text')
11
+ * @param {string} opts.placeholder
12
+ * @param {string} opts.value - Pre-filled value
13
+ * @param {string} opts.error - Validation error message
14
+ * @param {string} opts.hint - Helper text below the input
15
+ * @param {boolean} opts.required
16
+ * @param {boolean} opts.disabled
17
+ * @param {string} opts.id - Override generated id
18
+ * @param {string} opts.class
19
+ * @param {object} opts.attrs - Extra HTML attributes for the <input>
20
+ */
21
+
22
+ import { escHtml as e } from '../html.js'
23
+
24
+ export function input({
25
+ name = '',
26
+ label = '',
27
+ type = 'text',
28
+ placeholder = '',
29
+ value = '',
30
+ error = '',
31
+ hint = '',
32
+ required = false,
33
+ disabled = false,
34
+ id = '',
35
+ class: cls = '',
36
+ attrs = {},
37
+ } = {}) {
38
+ const fieldId = e(id || `field-${name}`)
39
+ const errorId = `${fieldId}-error`
40
+ const hintId = `${fieldId}-hint`
41
+ const described = [error ? errorId : '', hint ? hintId : ''].filter(Boolean).join(' ')
42
+
43
+ const wrapClasses = ['ui-field', error ? 'ui-field--error' : '', cls].filter(Boolean).join(' ')
44
+
45
+ const attrsStr = Object.entries(attrs)
46
+ .map(([k, v]) => ` ${e(k)}="${e(String(v))}"`)
47
+ .join('')
48
+
49
+ const labelHtml = label
50
+ ? `<label for="${fieldId}" class="ui-label">${e(label)}${required ? ' <span class="ui-required" aria-hidden="true">*</span>' : ''}</label>`
51
+ : ''
52
+
53
+ const hintHtml = hint ? `<p id="${hintId}" class="ui-hint">${e(hint)}</p>` : ''
54
+ const errorHtml = error ? `<p id="${errorId}" class="ui-error" role="alert">${e(error)}</p>` : ''
55
+
56
+ return `<div class="${e(wrapClasses)}">
57
+ ${labelHtml}
58
+ <input
59
+ id="${fieldId}"
60
+ name="${e(name)}"
61
+ type="${e(type)}"
62
+ class="ui-input"
63
+ ${placeholder ? `placeholder="${e(placeholder)}"` : ''}
64
+ ${value ? `value="${e(value)}"` : ''}
65
+ ${required ? 'required aria-required="true"' : ''}
66
+ ${disabled ? 'disabled' : ''}
67
+ ${described ? `aria-describedby="${described}"` : ''}
68
+ ${error ? 'aria-invalid="true"' : ''}
69
+ ${attrsStr}
70
+ >
71
+ ${hintHtml}
72
+ ${errorHtml}
73
+ </div>`
74
+ }
package/src/ui/list.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pulse UI — List
3
+ *
4
+ * Styled ordered or unordered list. Use this instead of raw <ul>/<ol> to get
5
+ * consistent spacing, bullets, and colour tokens.
6
+ *
7
+ * Items are HTML strings — escape user data before passing them in.
8
+ *
9
+ * @param {object} opts
10
+ * @param {string[]} opts.items - Array of HTML strings for each list item
11
+ * @param {boolean} [opts.ordered] - true for <ol>, false for <ul> (default: false)
12
+ * @param {string} [opts.gap] - Item spacing: 'xs'|'sm'|'md' (default: 'sm')
13
+ * @param {string} [opts.class] - Extra classes on the list element
14
+ */
15
+
16
+ export function list({
17
+ items = [],
18
+ ordered = false,
19
+ gap = 'sm',
20
+ class: cls = '',
21
+ } = {}) {
22
+ if (!items.length) return ''
23
+
24
+ const tag = ordered ? 'ol' : 'ul'
25
+ const gapMap = { xs: 'u-gap-1', sm: 'u-gap-2', md: 'u-gap-3' }
26
+ const classes = [
27
+ 'ui-list',
28
+ ordered ? 'ui-list--ordered' : 'ui-list--unordered',
29
+ gapMap[gap] || 'u-gap-2',
30
+ cls,
31
+ ].filter(Boolean).join(' ')
32
+
33
+ const itemsHtml = items.map(item => `<li class="ui-list-item">${item}</li>`).join('')
34
+
35
+ return `<${tag} class="${classes}">${itemsHtml}</${tag}>`
36
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pulse UI — Media
3
+ *
4
+ * Image + text side-by-side layout. Stacks vertically on mobile.
5
+ * The image slot accepts any HTML — <img>, <figure>, an SVG, or a styled div.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.image - Raw HTML slot — the visual side
9
+ * @param {string} opts.content - Raw HTML slot — the text side
10
+ * @param {boolean} opts.reverse - Put text left, image right (default: false)
11
+ * @param {'start'|'center'} opts.align - Vertical alignment of columns (default: 'center')
12
+ * @param {'sm'|'md'|'lg'} opts.gap - Gap between columns (default: 'md')
13
+ * @param {string} opts.class
14
+ */
15
+
16
+ import { escHtml as e } from '../html.js'
17
+
18
+ const ALIGNS = new Set(['start', 'center'])
19
+ const GAPS = new Set(['sm', 'md', 'lg'])
20
+
21
+ export function media({
22
+ image = '',
23
+ content = '',
24
+ reverse = false,
25
+ align = 'center',
26
+ gap = 'md',
27
+ class: cls = '',
28
+ } = {}) {
29
+ if (!ALIGNS.has(align)) align = 'center'
30
+ if (!GAPS.has(gap)) gap = 'md'
31
+
32
+ const classes = [
33
+ 'ui-media',
34
+ reverse && 'ui-media--reverse',
35
+ align !== 'center' && `ui-media--align-${align}`,
36
+ gap !== 'md' && `ui-media--gap-${gap}`,
37
+ cls,
38
+ ].filter(Boolean).join(' ')
39
+
40
+ return `<div class="${e(classes)}">
41
+ <div class="ui-media-image">${image}</div>
42
+ <div class="ui-media-content">${content}</div>
43
+ </div>`
44
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Pulse UI — Modal
3
+ *
4
+ * A <dialog>-based modal. Use modal() to render the dialog element and
5
+ * modalTrigger() (or any element with data-modal-open="id") to open it.
6
+ *
7
+ * Requires pulse-ui.js for the open trigger.
8
+ * The close button inside the dialog uses native <form method="dialog"> — no JS needed.
9
+ * Clicking the backdrop also closes the modal (handled by pulse-ui.js).
10
+ *
11
+ * @param {object} opts
12
+ * @param {string} opts.id - Unique ID — required so triggers can target this dialog
13
+ * @param {string} opts.title - Dialog heading
14
+ * @param {number} opts.level - Heading level 1–6 (default 2). Visual style is always ui-modal-title.
15
+ * @param {string} opts.content - Body HTML
16
+ * @param {string} opts.footer - Footer HTML — typically button() calls
17
+ * @param {'sm'|'md'|'lg'|'xl'} opts.size
18
+ * @param {string} opts.class
19
+ */
20
+
21
+ import { escHtml as e } from '../html.js'
22
+ import { iconX } from './icons.js'
23
+
24
+ const SIZES = new Set(['sm', 'md', 'lg', 'xl'])
25
+
26
+ export function modal({
27
+ id = '',
28
+ title = '',
29
+ level = 2,
30
+ content = '',
31
+ footer = '',
32
+ size = 'md',
33
+ class: cls = '',
34
+ } = {}) {
35
+ if (!SIZES.has(size)) size = 'md'
36
+
37
+ const classes = ['ui-modal', size !== 'md' && `ui-modal--${size}`, cls].filter(Boolean).join(' ')
38
+ const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
39
+
40
+ return `<dialog class="${e(classes)}"${id ? ` id="${e(id)}"` : ''}${id ? ` aria-labelledby="${e(id)}-title"` : ''}>
41
+ <form method="dialog" class="ui-modal-inner">
42
+ <header class="ui-modal-header">
43
+ <${tag} class="ui-modal-title"${id ? ` id="${e(id)}-title"` : ''}>${e(title)}</${tag}>
44
+ <button type="submit" class="ui-modal-close" aria-label="Close dialog">
45
+ ${iconX({ size: 16 })}
46
+ </button>
47
+ </header>
48
+ <div class="ui-modal-body">${content}</div>
49
+ ${footer ? `<footer class="ui-modal-footer">${footer}</footer>` : ''}
50
+ </form>
51
+ </dialog>`
52
+ }
53
+
54
+ /**
55
+ * Button that opens a modal by ID.
56
+ * Any element with data-modal-open="<id>" also works with pulse-ui.js.
57
+ *
58
+ * @param {object} opts
59
+ * @param {string} opts.target - The modal's id attribute
60
+ * @param {string} opts.label - Button label
61
+ * @param {'primary'|'secondary'|'ghost'|'danger'} opts.variant
62
+ * @param {'sm'|'md'|'lg'} opts.size
63
+ * @param {string} opts.class
64
+ */
65
+ export function modalTrigger({
66
+ target = '',
67
+ label = 'Open',
68
+ variant = 'primary',
69
+ size = 'md',
70
+ class: cls = '',
71
+ } = {}) {
72
+ const VARIANTS = new Set(['primary', 'secondary', 'ghost', 'danger'])
73
+ const BTN_SIZES = new Set(['sm', 'md', 'lg'])
74
+ if (!VARIANTS.has(variant)) variant = 'primary'
75
+ if (!BTN_SIZES.has(size)) size = 'md'
76
+
77
+ const classes = ['ui-btn', `ui-btn--${variant}`, `ui-btn--${size}`, cls].filter(Boolean).join(' ')
78
+
79
+ return `<button type="button" class="${e(classes)}" data-dialog-open="${e(target)}"><span>${e(label)}</span></button>`
80
+ }
package/src/ui/nav.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pulse UI — Nav
3
+ *
4
+ * Site header with logo, navigation links, and an optional CTA slot.
5
+ * Set sticky: true for a position: sticky top bar with backdrop blur.
6
+ *
7
+ * On mobile (< 640px) links are hidden behind a burger button. Clicking
8
+ * the burger toggles the .ui-nav--open class on the <header>, which
9
+ * reveals an absolutely-positioned dropdown that overlays page content.
10
+ * Wired automatically by pulse-ui.js — no extra JS needed.
11
+ *
12
+ * @param {object} opts
13
+ * @param {string} opts.logo - Raw HTML slot — SVG, img, or text
14
+ * @param {string} opts.logoHref - Logo link destination (default: '/')
15
+ * @param {Array<{label: string, href: string}>} opts.links
16
+ * @param {string} opts.action - Raw HTML slot — typically a button()
17
+ * @param {boolean} opts.sticky - Position sticky with backdrop blur
18
+ * @param {'right'|'left'} opts.burgerAlign - Mobile burger position (default: 'right')
19
+ * @param {string} opts.class
20
+ */
21
+
22
+ import { escHtml as e } from '../html.js'
23
+ import { iconMenu, iconX } from './icons.js'
24
+
25
+ let _navId = 0
26
+
27
+ export function nav({
28
+ logo = '',
29
+ logoHref = '/',
30
+ links = [],
31
+ action = '',
32
+ sticky = false,
33
+ burgerAlign = 'right',
34
+ class: cls = '',
35
+ } = {}) {
36
+ const id = `ui-nav-${++_navId}`
37
+ const classes = ['ui-nav', sticky && 'ui-nav--sticky', burgerAlign === 'left' && 'ui-nav--burger-left', cls].filter(Boolean).join(' ')
38
+
39
+ const linksHtml = links.map(({ label = '', href = '' }) =>
40
+ `<a href="${e(href)}" class="ui-nav-link">${e(label)}</a>`
41
+ ).join('')
42
+
43
+ const burgerHtml = links.length ? `
44
+ <button class="ui-nav-burger" type="button" aria-label="Toggle menu" aria-expanded="false" aria-controls="${id}-mobile">
45
+ ${iconMenu({ size: 20, class: 'ui-nav-burger-open' })}
46
+ ${iconX({ size: 20, class: 'ui-nav-burger-close' })}
47
+ </button>` : ''
48
+
49
+ const mobileMenuHtml = links.length ? `
50
+ <div class="ui-nav-mobile" id="${id}-mobile" aria-label="Mobile navigation">
51
+ <nav>${linksHtml}</nav>
52
+ </div>` : ''
53
+
54
+ return `<header class="${e(classes)}">
55
+ <div class="ui-nav-inner">
56
+ <a href="${e(logoHref)}" class="ui-nav-logo">${logo}</a>
57
+ ${links.length ? `<nav class="ui-nav-links" aria-label="Site navigation">${linksHtml}</nav>` : ''}
58
+ ${action ? `<div class="ui-nav-action">${action}</div>` : ''}${burgerHtml}
59
+ </div>${mobileMenuHtml}
60
+ </header>`
61
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Pulse UI — Pricing
3
+ *
4
+ * A single pricing plan card with feature list and CTA.
5
+ * Compose multiple in a flex/grid container for a full pricing section.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.name - Plan name (e.g. "Pro")
9
+ * @param {number} opts.level - Heading level 1–6 for the plan name (default 3). Visual style is always ui-pricing-name.
10
+ * @param {string} opts.price - Price string (e.g. "$9", "Free")
11
+ * @param {string} opts.period - Billing period (e.g. "/month")
12
+ * @param {string} opts.description - One-line plan description
13
+ * @param {string[]} opts.features - Array of feature strings
14
+ * @param {string} opts.action - Raw HTML slot — typically a button()
15
+ * @param {boolean} opts.highlighted - Accent border + elevated appearance
16
+ * @param {string} opts.badge - Floating label above the card (e.g. "Most popular")
17
+ * @param {string} opts.class
18
+ */
19
+
20
+ import { escHtml as e } from '../html.js'
21
+
22
+ export function pricing({
23
+ name = '',
24
+ level = 3,
25
+ price = '',
26
+ period = '',
27
+ description = '',
28
+ features = [],
29
+ action = '',
30
+ highlighted = false,
31
+ badge = '',
32
+ class: cls = '',
33
+ } = {}) {
34
+ const classes = ['ui-pricing', highlighted && 'ui-pricing--highlighted', cls].filter(Boolean).join(' ')
35
+ const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
36
+
37
+ const featureList = features.length
38
+ ? `<ul class="ui-pricing-features">
39
+ ${features.map(f => `<li class="ui-pricing-feature"><span class="ui-pricing-check" aria-hidden="true">✓</span>${e(f)}</li>`).join('\n ')}
40
+ </ul>`
41
+ : ''
42
+
43
+ return `<div class="${e(classes)}">
44
+ ${badge ? `<p class="ui-pricing-badge">${e(badge)}</p>` : ''}
45
+ <div class="ui-pricing-header">
46
+ ${name ? `<${tag} class="ui-pricing-name">${e(name)}</${tag}>` : ''}
47
+ ${description ? `<p class="ui-pricing-desc">${e(description)}</p>` : ''}
48
+ </div>
49
+ <div class="ui-pricing-price">
50
+ <span class="ui-pricing-amount">${e(price)}</span>
51
+ ${period ? `<span class="ui-pricing-period">${e(period)}</span>` : ''}
52
+ </div>
53
+ ${featureList}
54
+ ${action ? `<div class="ui-pricing-action">${action}</div>` : ''}
55
+ </div>`
56
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Pulse UI — Progress
3
+ *
4
+ * Horizontal progress bar. Supports determinate and indeterminate states.
5
+ *
6
+ * @param {object} opts
7
+ * @param {number} opts.value - Current value (0–max). Omit for indeterminate.
8
+ * @param {number} opts.max - Maximum value (default: 100)
9
+ * @param {string} opts.label - Accessible label
10
+ * @param {boolean} opts.showLabel - Render label text above the bar
11
+ * @param {boolean} opts.showValue - Render percentage above the bar (right-aligned)
12
+ * @param {'accent'|'success'|'warning'|'error'} opts.variant
13
+ * @param {'sm'|'md'|'lg'} opts.size
14
+ * @param {string} opts.class
15
+ */
16
+
17
+ import { escHtml as e } from '../html.js'
18
+
19
+ const VARIANTS = { accent: 'accent', success: 'success', warning: 'warning', error: 'error' }
20
+ const SIZES = { sm: '.25rem', md: '.5rem', lg: '1rem' }
21
+
22
+ export function progress({
23
+ value = undefined,
24
+ max = 100,
25
+ label = '',
26
+ showLabel = false,
27
+ showValue = false,
28
+ variant = 'accent',
29
+ size = 'md',
30
+ class: cls = '',
31
+ } = {}) {
32
+ const indeterminate = value === undefined || value === null
33
+ const clamped = indeterminate ? 0 : Math.min(Math.max(Number(value), 0), max)
34
+ const pct = indeterminate ? 0 : Math.round((clamped / max) * 100)
35
+ const v = VARIANTS[variant] ?? 'accent'
36
+ const h = SIZES[size] ?? SIZES.md
37
+
38
+ const classes = [
39
+ 'ui-progress',
40
+ `ui-progress--${v}`,
41
+ indeterminate ? 'ui-progress--indeterminate' : '',
42
+ cls,
43
+ ].filter(Boolean).join(' ')
44
+
45
+ const header = (showLabel || showValue) ? `
46
+ <div class="ui-progress-header">
47
+ ${showLabel && label ? `<span class="ui-progress-label">${e(label)}</span>` : '<span></span>'}
48
+ ${showValue && !indeterminate ? `<span class="ui-progress-value">${pct}%</span>` : ''}
49
+ </div>` : ''
50
+
51
+ return `<div
52
+ class="${e(classes)}"
53
+ role="progressbar"
54
+ ${!indeterminate ? `aria-valuenow="${clamped}" aria-valuemin="0" aria-valuemax="${max}"` : ''}
55
+ ${label ? `aria-label="${e(label)}"` : ''}
56
+ style="--progress-height:${h}"
57
+ >${header}
58
+ <div class="ui-progress-track">
59
+ <div class="ui-progress-fill" style="${indeterminate ? '' : `width:${pct}%`}"></div>
60
+ </div>
61
+ </div>`
62
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pulse UI — Prose
3
+ *
4
+ * Typography wrapper for rich text content: CMS output, markdown-rendered HTML,
5
+ * blog posts, article bodies, legal text, etc.
6
+ *
7
+ * Styles all descendant elements (h1–h6, p, ul, ol, li, a, blockquote, code,
8
+ * pre, table, hr, img) without requiring classes on individual elements.
9
+ * Use this any time you're outputting HTML you don't control.
10
+ *
11
+ * @param {object} opts
12
+ * @param {string} opts.content - Raw HTML string (NOT escaped — trust only server-side content)
13
+ * @param {string} [opts.size] - 'sm' | 'base' | 'lg' (default: 'base')
14
+ * @param {string} [opts.class] - Extra classes on the wrapper
15
+ */
16
+
17
+ export function prose({
18
+ content = '',
19
+ size = 'base',
20
+ class: cls = '',
21
+ } = {}) {
22
+ const classes = [
23
+ 'ui-prose',
24
+ size !== 'base' ? `ui-prose--${size}` : '',
25
+ cls,
26
+ ].filter(Boolean).join(' ')
27
+
28
+ return `<div class="${classes}">${content}</div>`
29
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pulse UI — Pullquote
3
+ *
4
+ * Styled blockquote with accent left border and optional attribution.
5
+ *
6
+ * @param {object} opts
7
+ * @param {string} opts.quote - The quote text
8
+ * @param {string} opts.cite - Attribution (name, role, etc.)
9
+ * @param {'md'|'lg'} opts.size - Size variant (default: 'md')
10
+ * @param {string} opts.class
11
+ */
12
+
13
+ import { escHtml as e } from '../html.js'
14
+
15
+ export function pullquote({
16
+ quote = '',
17
+ cite = '',
18
+ size = 'md',
19
+ class: cls = '',
20
+ } = {}) {
21
+ const sizeClass = size === 'lg' ? 'ui-pullquote--lg' : ''
22
+ const figClasses = ['ui-pullquote', sizeClass, cls].filter(Boolean).join(' ')
23
+
24
+ const citeHtml = cite
25
+ ? `<figcaption class="ui-pullquote-cite">${e(cite)}</figcaption>`
26
+ : ''
27
+
28
+ return `<figure class="${e(figClasses)}">
29
+ <blockquote>
30
+ <p class="ui-pullquote-text">${e(quote)}</p>
31
+ </blockquote>
32
+ ${citeHtml}
33
+ </figure>`
34
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Pulse UI — Radio & RadioGroup
3
+ *
4
+ * radio() — single radio button with label
5
+ * radioGroup() — semantic fieldset of radio options
6
+ *
7
+ * @param {object} opts (radio)
8
+ * @param {string} opts.name - Field name
9
+ * @param {string} opts.value - Submitted value
10
+ * @param {string} opts.label - Visible label
11
+ * @param {boolean} opts.checked
12
+ * @param {boolean} opts.disabled
13
+ * @param {string} opts.id - Override generated id
14
+ * @param {string} opts.class
15
+ *
16
+ * @param {object} opts (radioGroup)
17
+ * @param {string} opts.name - Shared field name
18
+ * @param {string} opts.legend - Group label (renders as <legend>)
19
+ * @param {Array} opts.options - [{ value, label, hint?, disabled? }]
20
+ * @param {string} opts.value - Currently selected value
21
+ * @param {string} opts.hint - Helper text below the group
22
+ * @param {string} opts.error - Validation error message
23
+ * @param {'sm'|'md'|'lg'} opts.gap - Gap between options (default: 'md')
24
+ * @param {string} opts.class
25
+ */
26
+
27
+ import { escHtml as e } from '../html.js'
28
+
29
+ export function radio({
30
+ name = '',
31
+ value = '',
32
+ label = '',
33
+ checked = false,
34
+ disabled = false,
35
+ id = '',
36
+ event = '',
37
+ class: cls = '',
38
+ } = {}) {
39
+ const fieldId = e(id || `radio-${name}-${value}`)
40
+ const classes = ['ui-radio', cls].filter(Boolean).join(' ')
41
+
42
+ return `<label class="${e(classes)}${disabled ? ' ui-radio--disabled' : ''}">
43
+ <input
44
+ type="radio"
45
+ id="${fieldId}"
46
+ name="${e(name)}"
47
+ value="${e(value)}"
48
+ class="ui-radio-input"
49
+ ${checked ? 'checked' : ''}
50
+ ${disabled ? 'disabled' : ''}
51
+ ${event ? `data-event="${e(event)}"` : ''}
52
+ >
53
+ <span class="ui-radio-dot" aria-hidden="true"></span>
54
+ ${label ? `<span class="ui-radio-label">${e(label)}</span>` : ''}
55
+ </label>`
56
+ }
57
+
58
+ const GAPS = { sm: '.375rem', md: '.75rem', lg: '1.25rem' }
59
+
60
+ export function radioGroup({
61
+ name = '',
62
+ legend = '',
63
+ options = [],
64
+ value = '',
65
+ hint = '',
66
+ error = '',
67
+ gap = 'md',
68
+ event = '',
69
+ class: cls = '',
70
+ } = {}) {
71
+ const groupId = `radiogroup-${name}`
72
+ const errorId = `${groupId}-error`
73
+ const hintId = `${groupId}-hint`
74
+ const gapValue = GAPS[gap] ?? GAPS.md
75
+
76
+ const described = [error ? errorId : '', hint ? hintId : ''].filter(Boolean).join(' ')
77
+
78
+ const items = options.map(opt =>
79
+ radio({
80
+ name,
81
+ value: opt.value,
82
+ label: opt.label,
83
+ checked: String(opt.value) === String(value),
84
+ disabled: opt.disabled ?? false,
85
+ event,
86
+ }) + (opt.hint ? `<p class="ui-hint" style="margin:-.25rem 0 0 2rem">${e(opt.hint)}</p>` : '')
87
+ ).join('')
88
+
89
+ const hintHtml = hint ? `<p id="${hintId}" class="ui-hint">${e(hint)}</p>` : ''
90
+ const errorHtml = error ? `<p id="${errorId}" class="ui-error" role="alert">${e(error)}</p>` : ''
91
+
92
+ const classes = ['ui-radio-group', error ? 'ui-radio-group--error' : '', cls].filter(Boolean).join(' ')
93
+
94
+ return `<fieldset class="${e(classes)}"${described ? ` aria-describedby="${described}"` : ''}>
95
+ ${legend ? `<legend class="ui-fieldset-legend">${e(legend)}</legend>` : ''}
96
+ <div class="ui-radio-group-body" style="--radio-gap:${gapValue}">
97
+ ${items}
98
+ ${hintHtml}
99
+ ${errorHtml}
100
+ </div>
101
+ </fieldset>`
102
+ }