@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,38 @@
1
+ /**
2
+ * Pulse UI — Breadcrumbs
3
+ *
4
+ * Accessible breadcrumb navigation. The last item (current page) renders as a
5
+ * <span> with aria-current="page". All other items render as links.
6
+ *
7
+ * @param {object} opts
8
+ * @param {Array} opts.items - Array of { label, href }. Last item has no href.
9
+ * @param {string} opts.separator - Separator character (default: '/')
10
+ * @param {string} opts.class
11
+ */
12
+
13
+ import { escHtml as e } from '../html.js'
14
+
15
+ export function breadcrumbs({
16
+ items = [],
17
+ separator = '/',
18
+ class: cls = '',
19
+ } = {}) {
20
+ const navClasses = ['ui-breadcrumbs', cls].filter(Boolean).join(' ')
21
+
22
+ const listItems = items.map((item, i) => {
23
+ const isLast = i === items.length - 1
24
+ const sep = i > 0
25
+ ? `<span class="ui-breadcrumbs-sep" aria-hidden="true">${e(separator)}</span>`
26
+ : ''
27
+
28
+ const content = isLast
29
+ ? `<span class="ui-breadcrumbs-current" aria-current="page">${e(item.label)}</span>`
30
+ : `<a href="${e(item.href)}" class="ui-breadcrumbs-link">${e(item.label)}</a>`
31
+
32
+ return `<li class="ui-breadcrumbs-item">${sep}${content}</li>`
33
+ }).join('')
34
+
35
+ return `<nav aria-label="Breadcrumb" class="${e(navClasses)}">
36
+ <ol class="ui-breadcrumbs-list">${listItems}</ol>
37
+ </nav>`
38
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pulse UI — Button
3
+ *
4
+ * Renders as <a> when href is provided, <button> otherwise.
5
+ * All visual variation goes through CSS modifier classes — never inline styles.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.label - Visible text (required)
9
+ * @param {'primary'|'secondary'|'ghost'|'danger'} opts.variant
10
+ * @param {'sm'|'md'|'lg'} opts.size
11
+ * @param {string} opts.href - Renders as <a> when set
12
+ * @param {boolean} opts.disabled
13
+ * @param {'button'|'submit'|'reset'} opts.type
14
+ * @param {string} opts.icon - SVG HTML prepended inside the element
15
+ * @param {string} opts.iconAfter - SVG HTML appended inside the element
16
+ * @param {boolean} opts.fullWidth
17
+ * @param {string} opts.class - Extra CSS classes appended to the element
18
+ * @param {object} opts.attrs - Extra HTML attributes (key→value) for <button> only
19
+ */
20
+
21
+ import { escHtml as e } from '../html.js'
22
+
23
+ const VARIANTS = new Set(['primary', 'secondary', 'ghost', 'danger'])
24
+ const SIZES = new Set(['sm', 'md', 'lg'])
25
+
26
+ export function button({
27
+ label = '',
28
+ variant = 'primary',
29
+ size = 'md',
30
+ href,
31
+ disabled = false,
32
+ type = 'button',
33
+ icon = '',
34
+ iconAfter = '',
35
+ fullWidth = false,
36
+ class: cls = '',
37
+ attrs = {},
38
+ } = {}) {
39
+ if (!VARIANTS.has(variant)) variant = 'primary'
40
+ if (!SIZES.has(size)) size = 'md'
41
+
42
+ const classes = [
43
+ 'ui-btn',
44
+ `ui-btn--${variant}`,
45
+ `ui-btn--${size}`,
46
+ fullWidth ? 'ui-btn--full' : '',
47
+ disabled ? 'ui-btn--disabled' : '',
48
+ cls,
49
+ ].filter(Boolean).join(' ')
50
+
51
+ const inner = [
52
+ icon ? `<span class="ui-btn-icon" aria-hidden="true">${icon}</span>` : '',
53
+ `<span>${e(label)}</span>`,
54
+ iconAfter ? `<span class="ui-btn-icon ui-btn-icon--after" aria-hidden="true">${iconAfter}</span>` : '',
55
+ ].join('')
56
+
57
+ if (href) {
58
+ return `<a href="${e(href)}" class="${e(classes)}"${disabled ? ' aria-disabled="true" tabindex="-1"' : ''}>${inner}</a>`
59
+ }
60
+
61
+ const attrsStr = Object.entries(attrs)
62
+ .map(([k, v]) => ` ${e(k)}="${e(String(v))}"`)
63
+ .join('')
64
+
65
+ return `<button type="${e(type)}" class="${e(classes)}"${disabled ? ' disabled aria-disabled="true"' : ''}${attrsStr}>${inner}</button>`
66
+ }
package/src/ui/card.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pulse UI — Card
3
+ *
4
+ * Surface container with optional title and footer.
5
+ * Pass HTML strings for content and footer — they are not escaped.
6
+ * Escape user data before passing it in.
7
+ *
8
+ * @param {object} opts
9
+ * @param {string} opts.title - Escaped heading text (optional)
10
+ * @param {number} opts.level - Heading level 1–6 (default 3). Controls the tag; visual style is always ui-card-title.
11
+ * @param {string} opts.content - HTML string for the card body
12
+ * @param {string} opts.footer - HTML string for the card footer (optional)
13
+ * @param {boolean} opts.flush - Remove internal padding (for full-bleed content)
14
+ * @param {string} opts.class
15
+ */
16
+
17
+ import { escHtml as e } from '../html.js'
18
+
19
+ export function card({
20
+ title = '',
21
+ level = 3,
22
+ content = '',
23
+ footer = '',
24
+ flush = false,
25
+ class: cls = '',
26
+ } = {}) {
27
+ const classes = ['ui-card', flush ? 'ui-card--flush' : '', cls].filter(Boolean).join(' ')
28
+ const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
29
+
30
+ const titleHtml = title ? `<div class="ui-card-header"><${tag} class="ui-card-title">${e(title)}</${tag}></div>` : ''
31
+ const footerHtml = footer ? `<div class="ui-card-footer">${footer}</div>` : ''
32
+
33
+ return `<div class="${e(classes)}">${titleHtml}<div class="ui-card-body">${content}</div>${footerHtml}</div>`
34
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pulse UI — Carousel / Slider
3
+ *
4
+ * CSS scroll-snap carousel with optional prev/next arrows and dot navigation.
5
+ * Requires pulse-ui.js for button and dot interactivity.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string[]} opts.slides - Array of raw HTML strings — one per slide
9
+ * @param {boolean} opts.arrows - Show prev/next arrow buttons (default: true)
10
+ * @param {boolean} opts.dots - Show dot navigation (default: true)
11
+ * @param {string} opts.class
12
+ */
13
+
14
+ import { escHtml as e } from '../html.js'
15
+ import { iconChevronLeft, iconChevronRight } from './icons.js'
16
+
17
+ let _carouselId = 0
18
+
19
+ export function carousel({
20
+ slides = [],
21
+ arrows = true,
22
+ dots = true,
23
+ class: cls = '',
24
+ } = {}) {
25
+ const id = `carousel-${++_carouselId}`
26
+ const classes = ['ui-carousel', cls].filter(Boolean).join(' ')
27
+
28
+ const slidesHtml = slides
29
+ .map((s, i) => {
30
+ const panelId = `${id}-panel-${i + 1}`
31
+ const tabId = `${id}-tab-${i + 1}`
32
+ return `<div class="ui-carousel-slide" id="${panelId}" role="tabpanel" aria-labelledby="${tabId}" tabindex="0">${s}</div>`
33
+ })
34
+ .join('\n ')
35
+
36
+ const arrowsHtml = arrows ? `
37
+ <button class="ui-carousel-btn ui-carousel-prev" type="button" aria-label="Previous slide" hidden>
38
+ ${iconChevronLeft({ size: 16 })}
39
+ </button>
40
+ <button class="ui-carousel-btn ui-carousel-next" type="button" aria-label="Next slide"${slides.length <= 1 ? ' hidden' : ''}>
41
+ ${iconChevronRight({ size: 16 })}
42
+ </button>` : ''
43
+
44
+ const dotsHtml = dots && slides.length > 1 ? `
45
+ <div class="ui-carousel-dots" role="tablist" aria-label="Slides">
46
+ ${slides.map((_, i) => {
47
+ const tabId = `${id}-tab-${i + 1}`
48
+ const panelId = `${id}-panel-${i + 1}`
49
+ const active = i === 0
50
+ return `<button class="ui-carousel-dot${active ? ' active' : ''}" id="${tabId}" type="button" role="tab" aria-selected="${active}" aria-controls="${panelId}" tabindex="${active ? '0' : '-1'}" aria-label="Slide ${i + 1}"></button>`
51
+ }).join('\n ')}
52
+ </div>` : ''
53
+
54
+ return `<div class="${e(classes)}">
55
+ <div class="ui-carousel-track">
56
+ ${slidesHtml}
57
+ </div>${arrowsHtml}${dotsHtml}
58
+ </div>`
59
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Pulse UI — Charts
3
+ *
4
+ * Server-rendered SVG charts. Pure functions, no client JS, no dependencies.
5
+ * All charts use CSS custom properties for colours and scale to 100% width
6
+ * (except sparkline and donutChart which have explicit dimensions).
7
+ *
8
+ * @exports barChart - Vertical bar chart
9
+ * @exports lineChart - Line chart with optional area fill
10
+ * @exports donutChart - Donut / pie chart
11
+ * @exports sparkline - Minimal inline line for stat tiles
12
+ */
13
+
14
+ import { escHtml as e } from '../html.js'
15
+
16
+ // ─── Internal constants ───────────────────────────────────────────────────────
17
+
18
+ const IW = 500 // internal viewBox width for bar / line charts
19
+
20
+ const COLOR_MAP = {
21
+ accent: 'var(--ui-accent)',
22
+ success: 'var(--ui-green)',
23
+ warning: 'var(--ui-yellow)',
24
+ error: 'var(--ui-red)',
25
+ blue: 'var(--ui-blue)',
26
+ muted: 'var(--ui-muted)',
27
+ }
28
+
29
+ const PALETTE = ['accent', 'blue', 'success', 'warning', 'error', 'muted']
30
+
31
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
32
+
33
+ function col(c) { return COLOR_MAP[c] ?? COLOR_MAP.accent }
34
+ function r1(n) { return Math.round(n * 10) / 10 } // 1 decimal place
35
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
36
+
37
+ function fmt(n) {
38
+ const abs = Math.abs(n)
39
+ if (abs >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
40
+ if (abs >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k'
41
+ return String(Math.round(n))
42
+ }
43
+
44
+ // ─── Bar chart ────────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Vertical bar chart.
48
+ *
49
+ * @param {object} opts
50
+ * @param {Array<{label:string, value:number}>} opts.data
51
+ * @param {number} opts.height - SVG height in px (default: 220)
52
+ * @param {'accent'|'success'|'warning'|'error'|'blue'|'muted'} opts.color
53
+ * @param {boolean} opts.showValues - Show value labels above each bar
54
+ * @param {boolean} opts.showGrid - Show horizontal grid lines (default: true)
55
+ * @param {number} opts.gap - Gap between bars as fraction 0–0.9 (default: 0.25)
56
+ * @param {string} opts.class
57
+ */
58
+ export function barChart({
59
+ data = [],
60
+ height = 220,
61
+ color = 'accent',
62
+ showValues = false,
63
+ showGrid = true,
64
+ gap = 0.25,
65
+ class: cls = '',
66
+ } = {}) {
67
+ if (!data.length) return ''
68
+
69
+ const pad = { top: showValues ? 28 : 16, right: 16, bottom: 40, left: 44 }
70
+ const plotW = IW - pad.left - pad.right
71
+ const plotH = height - pad.top - pad.bottom
72
+
73
+ const values = data.map(d => Number(d.value) || 0)
74
+ const maxVal = Math.max(...values, 0)
75
+ const minVal = Math.min(...values, 0)
76
+ const range = maxVal - minVal || 1
77
+ const c = col(color)
78
+
79
+ const yFor = v => r1(pad.top + plotH - ((v - minVal) / range) * plotH)
80
+ const zeroY = yFor(0)
81
+
82
+ // Grid lines + Y-axis labels
83
+ let grid = ''
84
+ if (showGrid) {
85
+ for (let i = 0; i <= 4; i++) {
86
+ const v = minVal + (i / 4) * range
87
+ const y = yFor(v)
88
+ grid += `<line x1="${pad.left}" y1="${r1(y)}" x2="${IW - pad.right}" y2="${r1(y)}" stroke="var(--ui-border)" stroke-width="1"/>`
89
+ grid += `<text x="${pad.left - 6}" y="${r1(y + 4)}" text-anchor="end" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(fmt(v))}</text>`
90
+ }
91
+ }
92
+
93
+ // Zero baseline
94
+ const baseline = `<line x1="${pad.left}" y1="${zeroY}" x2="${IW - pad.right}" y2="${zeroY}" stroke="var(--ui-border)" stroke-width="1.5"/>`
95
+
96
+ // Bars + labels
97
+ const slotW = plotW / data.length
98
+ const barW = r1(slotW * (1 - clamp(gap, 0.05, 0.9)))
99
+
100
+ const bars = data.map((d, i) => {
101
+ const v = Number(d.value) || 0
102
+ const bx = r1(pad.left + i * slotW + (slotW - barW) / 2)
103
+ const by = r1(Math.min(yFor(v), zeroY))
104
+ const bh = r1(Math.max(Math.abs(yFor(v) - zeroY), 1))
105
+ const mx = r1(bx + barW / 2)
106
+
107
+ let out = `<rect x="${bx}" y="${by}" width="${barW}" height="${bh}" fill="${c}" rx="2"/>`
108
+ if (showValues) {
109
+ out += `<text x="${mx}" y="${r1(by - 5)}" text-anchor="middle" font-size="11" font-weight="600" fill="${c}" font-family="var(--ui-font)">${e(fmt(v))}</text>`
110
+ }
111
+ if (d.label != null) {
112
+ out += `<text x="${mx}" y="${height - 8}" text-anchor="middle" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(String(d.label))}</text>`
113
+ }
114
+ return out
115
+ }).join('')
116
+
117
+ const ariaLabel = `Bar chart: ${data.map(d => `${d.label ?? ''} ${d.value}`).join(', ')}`
118
+ const clsAttr = cls ? ` class="${e(cls)}"` : ''
119
+
120
+ return `<svg${clsAttr} viewBox="0 0 ${IW} ${height}" width="100%" height="${height}" role="img" aria-label="${e(ariaLabel)}">${grid}${baseline}${bars}</svg>`
121
+ }
122
+
123
+ // ─── Line chart ───────────────────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Line chart with optional area fill.
127
+ *
128
+ * @param {object} opts
129
+ * @param {Array<{label:string, value:number}>} opts.data
130
+ * @param {number} opts.height - SVG height in px (default: 220)
131
+ * @param {'accent'|'success'|'warning'|'error'|'blue'|'muted'} opts.color
132
+ * @param {boolean} opts.area - Fill the area under the line
133
+ * @param {boolean} opts.showDots - Show dots at each data point (default: true)
134
+ * @param {boolean} opts.showGrid - Show horizontal grid lines (default: true)
135
+ * @param {string} opts.class
136
+ */
137
+ export function lineChart({
138
+ data = [],
139
+ height = 220,
140
+ color = 'accent',
141
+ area = false,
142
+ showDots = true,
143
+ showGrid = true,
144
+ class: cls = '',
145
+ } = {}) {
146
+ if (data.length < 2) return ''
147
+
148
+ const pad = { top: 16, right: 16, bottom: 40, left: 44 }
149
+ const plotW = IW - pad.left - pad.right
150
+ const plotH = height - pad.top - pad.bottom
151
+
152
+ const values = data.map(d => Number(d.value) || 0)
153
+ const maxVal = Math.max(...values)
154
+ const minVal = Math.min(...values)
155
+ const range = maxVal - minVal || 1
156
+ const c = col(color)
157
+
158
+ const xFor = i => r1(pad.left + (i / (data.length - 1)) * plotW)
159
+ const yFor = v => r1(pad.top + plotH - ((v - minVal) / range) * plotH)
160
+
161
+ // Grid lines + Y-axis labels
162
+ let grid = ''
163
+ if (showGrid) {
164
+ for (let i = 0; i <= 4; i++) {
165
+ const v = minVal + (i / 4) * range
166
+ const y = yFor(v)
167
+ grid += `<line x1="${pad.left}" y1="${r1(y)}" x2="${IW - pad.right}" y2="${r1(y)}" stroke="var(--ui-border)" stroke-width="1"/>`
168
+ grid += `<text x="${pad.left - 6}" y="${r1(y + 4)}" text-anchor="end" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(fmt(v))}</text>`
169
+ }
170
+ }
171
+
172
+ const pts = data.map((d, i) => [xFor(i), yFor(Number(d.value) || 0)])
173
+ const points = pts.map(([x, y]) => `${x},${y}`).join(' ')
174
+
175
+ // Area fill
176
+ let areaPath = ''
177
+ if (area) {
178
+ const bottom = r1(pad.top + plotH)
179
+ const d = `M${pts[0][0]},${bottom} ` + pts.map(([x, y]) => `L${x},${y}`).join(' ') + ` L${pts.at(-1)[0]},${bottom} Z`
180
+ areaPath = `<path d="${d}" fill="${c}" fill-opacity="0.12"/>`
181
+ }
182
+
183
+ const line = `<polyline points="${points}" fill="none" stroke="${c}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`
184
+ const dots = showDots ? pts.map(([x, y]) => `<circle cx="${x}" cy="${y}" r="3.5" fill="${c}"/>`).join('') : ''
185
+
186
+ // X-axis labels
187
+ const xlabels = data.map((d, i) =>
188
+ d.label != null
189
+ ? `<text x="${xFor(i)}" y="${height - 8}" text-anchor="middle" font-size="11" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(String(d.label))}</text>`
190
+ : ''
191
+ ).join('')
192
+
193
+ const ariaLabel = `Line chart: ${data.map(d => `${d.label ?? ''} ${d.value}`).join(', ')}`
194
+ const clsAttr = cls ? ` class="${e(cls)}"` : ''
195
+
196
+ return `<svg${clsAttr} viewBox="0 0 ${IW} ${height}" width="100%" height="${height}" role="img" aria-label="${e(ariaLabel)}">${grid}${areaPath}${line}${dots}${xlabels}</svg>`
197
+ }
198
+
199
+ // ─── Donut chart ──────────────────────────────────────────────────────────────
200
+
201
+ /**
202
+ * Donut (ring) chart. Each segment can have its own colour.
203
+ *
204
+ * @param {object} opts
205
+ * @param {Array<{label:string, value:number, color?:string}>} opts.data
206
+ * @param {number} opts.size - Diameter in px (default: 200)
207
+ * @param {number} opts.thickness - Ring thickness in px (default: 40)
208
+ * @param {string} opts.label - Large text in the centre
209
+ * @param {string} opts.sublabel - Smaller text below the centre label
210
+ * @param {string} opts.class
211
+ */
212
+ export function donutChart({
213
+ data = [],
214
+ size = 200,
215
+ thickness = 40,
216
+ label = '',
217
+ sublabel = '',
218
+ class: cls = '',
219
+ } = {}) {
220
+ if (!data.length) return ''
221
+
222
+ const cx = size / 2
223
+ const cy = size / 2
224
+ const r = cx - 8
225
+ const innerR = r - clamp(thickness, 4, r - 4)
226
+ const total = data.reduce((s, d) => s + Math.max(0, Number(d.value) || 0), 0)
227
+ if (!total) return ''
228
+
229
+ let angle = -Math.PI / 2 // start at 12 o'clock
230
+
231
+ const segments = data.map((d, i) => {
232
+ const v = Math.max(0, Number(d.value) || 0)
233
+ const frac = v / total
234
+ const sweep = frac * 2 * Math.PI
235
+ const start = angle
236
+ const end = angle + sweep
237
+ angle = end
238
+
239
+ const c = col(d.color || PALETTE[i % PALETTE.length])
240
+
241
+ // Full circle edge case
242
+ if (frac >= 1 - 1e-10) {
243
+ const mid = r1((r + innerR) / 2)
244
+ return `<circle cx="${cx}" cy="${cy}" r="${mid}" fill="none" stroke="${c}" stroke-width="${thickness}"/>`
245
+ }
246
+
247
+ const x1 = r1(cx + r * Math.cos(start))
248
+ const y1 = r1(cy + r * Math.sin(start))
249
+ const x2 = r1(cx + r * Math.cos(end))
250
+ const y2 = r1(cy + r * Math.sin(end))
251
+ const x3 = r1(cx + innerR * Math.cos(end))
252
+ const y3 = r1(cy + innerR * Math.sin(end))
253
+ const x4 = r1(cx + innerR * Math.cos(start))
254
+ const y4 = r1(cy + innerR * Math.sin(start))
255
+ const lg = sweep > Math.PI ? 1 : 0
256
+
257
+ const path = `M${x1},${y1} A${r},${r} 0 ${lg} 1 ${x2},${y2} L${x3},${y3} A${innerR},${innerR} 0 ${lg} 0 ${x4},${y4} Z`
258
+ return `<path d="${path}" fill="${c}"/>`
259
+ }).join('')
260
+
261
+ // Centre text
262
+ const hasTwo = label && sublabel
263
+ const labelY = r1(cy + (hasTwo ? -6 : 6))
264
+ const subY = r1(cy + 16)
265
+ const labelEl = label ? `<text x="${cx}" y="${labelY}" text-anchor="middle" font-size="${thickness > 30 ? 22 : 16}" font-weight="700" fill="var(--ui-text)" font-family="var(--ui-font)">${e(label)}</text>` : ''
266
+ const subEl = sublabel ? `<text x="${cx}" y="${subY}" text-anchor="middle" font-size="12" fill="var(--ui-muted)" font-family="var(--ui-font)">${e(sublabel)}</text>` : ''
267
+
268
+ const ariaLabel = `Donut chart: ${data.map(d => `${d.label ?? ''} ${d.value}`).join(', ')}`
269
+ const clsAttr = cls ? ` class="${e(cls)}"` : ''
270
+
271
+ return `<svg${clsAttr} viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" role="img" aria-label="${e(ariaLabel)}">${segments}${labelEl}${subEl}</svg>`
272
+ }
273
+
274
+ // ─── Sparkline ────────────────────────────────────────────────────────────────
275
+
276
+ /**
277
+ * Minimal inline line chart for use inside stat tiles or table cells.
278
+ * Accepts a plain array of numbers.
279
+ *
280
+ * @param {object} opts
281
+ * @param {number[]} opts.data - Array of numeric values
282
+ * @param {number} opts.width - SVG width in px (default: 80)
283
+ * @param {number} opts.height - SVG height in px (default: 32)
284
+ * @param {'accent'|'success'|'warning'|'error'|'blue'|'muted'} opts.color
285
+ * @param {boolean} opts.area - Fill area under the line
286
+ * @param {string} opts.class
287
+ */
288
+ export function sparkline({
289
+ data = [],
290
+ width = 80,
291
+ height = 32,
292
+ color = 'accent',
293
+ area = false,
294
+ class: cls = '',
295
+ } = {}) {
296
+ if (data.length < 2) return ''
297
+
298
+ const values = data.map(v => Number(v) || 0)
299
+ const min = Math.min(...values)
300
+ const max = Math.max(...values)
301
+ const range = max - min || 1
302
+ const pad = 2
303
+ const c = col(color)
304
+
305
+ const xFor = i => r1(i * (width / (data.length - 1)))
306
+ const yFor = v => r1(height - pad - ((v - min) / range) * (height - pad * 2))
307
+
308
+ const pts = values.map((v, i) => [xFor(i), yFor(v)])
309
+ const points = pts.map(([x, y]) => `${x},${y}`).join(' ')
310
+
311
+ let areaPath = ''
312
+ if (area) {
313
+ const bottom = height - pad
314
+ const d = `M${pts[0][0]},${bottom} ` + pts.map(([x, y]) => `L${x},${y}`).join(' ') + ` L${pts.at(-1)[0]},${bottom} Z`
315
+ areaPath = `<path d="${d}" fill="${c}" fill-opacity="0.15"/>`
316
+ }
317
+
318
+ const clsAttr = cls ? ` class="${e(cls)}"` : ''
319
+
320
+ return `<svg${clsAttr} viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" aria-hidden="true">${areaPath}<polyline points="${points}" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`
321
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pulse UI — Checkbox
3
+ *
4
+ * Styled checkbox with optional label. Wraps a visually-hidden <input> and a
5
+ * custom box element driven by CSS :checked state.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.name - Field name
9
+ * @param {string} opts.value - Submitted value
10
+ * @param {string} opts.label - Visible label text (escaped)
11
+ * @param {string} opts.labelHtml - Raw HTML label slot (not escaped — use for styled spans)
12
+ * @param {boolean} opts.checked
13
+ * @param {boolean} opts.disabled
14
+ * @param {string} opts.id - Override generated id
15
+ * @param {string} opts.event - data-event binding (e.g. 'change:toggle')
16
+ * @param {string} opts.hint - Helper text below the label
17
+ * @param {string} opts.error - Validation error message
18
+ * @param {string} opts.class
19
+ */
20
+
21
+ import { escHtml as e } from '../html.js'
22
+
23
+ export function checkbox({
24
+ name = '',
25
+ value = '',
26
+ label = '',
27
+ labelHtml = '',
28
+ checked = false,
29
+ disabled = false,
30
+ id = '',
31
+ event = '',
32
+ hint = '',
33
+ error = '',
34
+ class: cls = '',
35
+ } = {}) {
36
+ const uid = e(id || ['checkbox', name, value].filter(Boolean).join('-'))
37
+
38
+ const classes = [
39
+ 'ui-checkbox',
40
+ disabled ? 'ui-checkbox--disabled' : '',
41
+ error ? 'ui-checkbox--error' : '',
42
+ cls,
43
+ ].filter(Boolean).join(' ')
44
+
45
+ const labelContent = labelHtml
46
+ ? labelHtml
47
+ : label ? `<span class="ui-checkbox-label">${e(label)}</span>` : ''
48
+
49
+ return `<label class="${e(classes)}">
50
+ <input
51
+ type="checkbox"
52
+ id="${uid}"
53
+ ${name ? `name="${e(name)}"` : ''}
54
+ ${value ? `value="${e(value)}"` : ''}
55
+ class="ui-checkbox-input"
56
+ ${checked ? 'checked' : ''}
57
+ ${disabled ? 'disabled' : ''}
58
+ ${event ? `data-event="${e(event)}"` : ''}
59
+ >
60
+ <span class="ui-checkbox-box" aria-hidden="true"></span>
61
+ ${labelContent}
62
+ ${hint ? `<p class="ui-hint">${e(hint)}</p>` : ''}
63
+ ${error ? `<p class="ui-error" role="alert">${e(error)}</p>` : ''}
64
+ </label>`
65
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pulse UI — Cluster
3
+ *
4
+ * Flex row with wrapping. Groups inline elements horizontally
5
+ * with consistent gap — buttons, badges, app store badges, stat rows, etc.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.content - Raw HTML slot
9
+ * @param {'xs'|'sm'|'md'|'lg'} opts.gap - Gap between children (default: 'md')
10
+ * @param {'start'|'center'|'end'|'between'} opts.justify - justify-content (default: 'start')
11
+ * @param {'start'|'center'|'end'} opts.align - align-items (default: 'center')
12
+ * @param {boolean} opts.wrap - Allow wrapping (default: true)
13
+ * @param {string} opts.class
14
+ */
15
+
16
+ import { escHtml as e } from '../html.js'
17
+
18
+ const GAPS = new Set(['xs', 'sm', 'md', 'lg'])
19
+ const JUSTIFYS = new Set(['start', 'center', 'end', 'between'])
20
+ const ALIGNS = new Set(['start', 'center', 'end'])
21
+
22
+ export function cluster({
23
+ content = '',
24
+ gap = 'md',
25
+ justify = 'start',
26
+ align = 'center',
27
+ wrap = true,
28
+ class: cls = '',
29
+ } = {}) {
30
+ if (!GAPS.has(gap)) gap = 'md'
31
+ if (!JUSTIFYS.has(justify)) justify = 'start'
32
+ if (!ALIGNS.has(align)) align = 'center'
33
+
34
+ const classes = [
35
+ 'ui-cluster',
36
+ gap !== 'md' && `ui-cluster--gap-${gap}`,
37
+ justify !== 'start' && `ui-cluster--justify-${justify}`,
38
+ align !== 'center' && `ui-cluster--align-${align}`,
39
+ !wrap && 'ui-cluster--nowrap',
40
+ cls,
41
+ ].filter(Boolean).join(' ')
42
+
43
+ return `<div class="${e(classes)}">${content}</div>`
44
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pulse UI — Code Window
3
+ *
4
+ * A code block with macOS-style window chrome — three coloured dots and an
5
+ * optional filename label. The content slot accepts pre-highlighted HTML
6
+ * (spans with syntax-token classes) or plain text.
7
+ *
8
+ * Interactive syntax highlighting is out of scope — pass pre-rendered HTML
9
+ * or plain text. The component handles all the chrome, layout, scroll, and
10
+ * monospace typography.
11
+ *
12
+ * @param {object} opts
13
+ * @param {string} opts.content - Raw HTML slot — highlighted code HTML or plain text
14
+ * @param {string} opts.filename - Filename shown in the chrome bar (e.g. 'home.js')
15
+ * @param {string} opts.lang - Language label shown on the right of the chrome (e.g. 'JavaScript')
16
+ * @param {string} opts.class
17
+ */
18
+
19
+ import { escHtml as e } from '../html.js'
20
+
21
+ export function codeWindow({
22
+ content = '',
23
+ filename = '',
24
+ lang = '',
25
+ class: cls = '',
26
+ } = {}) {
27
+ const classes = ['ui-code-window', cls].filter(Boolean).join(' ')
28
+
29
+ return `<div class="${e(classes)}" role="region"${filename ? ` aria-label="${e(filename)}"` : ''}>
30
+ <div class="ui-code-window-chrome" aria-hidden="true">
31
+ <span class="ui-code-window-dot"></span>
32
+ <span class="ui-code-window-dot"></span>
33
+ <span class="ui-code-window-dot"></span>
34
+ ${filename ? `<span class="ui-code-window-filename">${e(filename)}</span>` : ''}
35
+ ${lang ? `<span class="ui-code-window-lang">${e(lang)}</span>` : ''}
36
+ </div>
37
+ <pre class="ui-code-window-pre"><code class="ui-code-window-code">${content}</code></pre>
38
+ </div>`
39
+ }