@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,93 @@
1
+ /**
2
+ * Pulse UI — Rating
3
+ *
4
+ * Star rating display and interactive input.
5
+ *
6
+ * Without `name`: renders a read-only star display (role="img").
7
+ * With `name`: renders radio inputs — submits the numeric value in FormData.
8
+ *
9
+ * The interactive version uses a pure-CSS technique:
10
+ * - Stars are rendered in reverse DOM order inside a row-reverse flex container
11
+ * so they appear left-to-right (★1 … ★5) visually.
12
+ * - The hidden radio inputs sit between each label. Because they are
13
+ * position:absolute they are removed from the flex layout but remain in the
14
+ * DOM so CSS sibling selectors can reach from input → subsequent labels.
15
+ * - `input:checked ~ label` fills all labels (= lower-numbered stars) after
16
+ * the checked input in DOM, which appear to its left visually.
17
+ * - `label:hover ~ label` fills all subsequent labels (lower stars) on hover.
18
+ *
19
+ * @param {object} opts
20
+ * @param {number} opts.value - Current rating (0–max). Supports 0.5 steps for display.
21
+ * @param {number} opts.max - Total stars (default: 5)
22
+ * @param {string} opts.name - Field name — enables interactive radio mode
23
+ * @param {string} opts.label - Accessible group label (interactive mode)
24
+ * @param {'sm'|'md'|'lg'} opts.size
25
+ * @param {boolean} opts.disabled
26
+ * @param {string} opts.class
27
+ */
28
+
29
+ import { escHtml as e } from '../html.js'
30
+
31
+ const SIZES = { sm: '1rem', md: '1.5rem', lg: '2rem' }
32
+
33
+ const starFilled = `<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`
34
+ const starEmpty = `<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" aria-hidden="true"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`
35
+ const starHalf = `<svg width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true"><defs><linearGradient id="half"><stop offset="50%" stop-color="currentColor"/><stop offset="50%" stop-color="transparent"/></linearGradient></defs><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="url(#half)" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`
36
+
37
+ export function rating({
38
+ value = 0,
39
+ max = 5,
40
+ name = '',
41
+ label = '',
42
+ size = 'md',
43
+ disabled = false,
44
+ class: cls = '',
45
+ } = {}) {
46
+ const fontSize = SIZES[size] ?? SIZES.md
47
+ const classes = ['ui-rating', cls].filter(Boolean).join(' ')
48
+
49
+ // ── Read-only display ──────────────────────────────────────────────────────
50
+ if (!name) {
51
+ const stars = Array.from({ length: max }, (_, i) => {
52
+ const pos = i + 1
53
+ if (value >= pos) return `<span class="ui-rating-star ui-rating-star--filled">${starFilled}</span>`
54
+ if (value >= pos - 0.5) return `<span class="ui-rating-star ui-rating-star--half">${starHalf}</span>`
55
+ return `<span class="ui-rating-star">${starEmpty}</span>`
56
+ }).join('')
57
+
58
+ return `<div
59
+ class="${e(classes)}"
60
+ style="--rating-size:${fontSize}"
61
+ role="img"
62
+ aria-label="${e(value)} out of ${e(String(max))} stars"
63
+ >${stars}</div>`
64
+ }
65
+
66
+ // ── Interactive (radio) ────────────────────────────────────────────────────
67
+ // Each input lives INSIDE its label — labels are the only flex items so no
68
+ // hidden elements can intercept pointer events between stars.
69
+ // Stars rendered in reverse DOM order (max → 1) inside a row-reverse flex
70
+ // container so they appear visually as ★1 … ★max left-to-right.
71
+ // CSS :has() drives the checked + hover highlight via sibling combinators.
72
+ const items = Array.from({ length: max }, (_, i) => {
73
+ const v = max - i // counts down: max, max-1, …, 1
74
+ const checked = v === Math.round(value)
75
+ const title = `${v} out of ${max}`
76
+ return `<label class="ui-rating-star" title="${title}" aria-label="${title} stars"><input
77
+ type="radio"
78
+ name="${e(name)}"
79
+ value="${v}"
80
+ class="ui-rating-input"
81
+ ${checked ? 'checked' : ''}
82
+ ${disabled ? 'disabled' : ''}
83
+ >★</label>`
84
+ }).join('')
85
+
86
+ const srOnly = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0'
87
+ const groupLabel = label ? `<legend style="${srOnly}">${e(label)}</legend>` : ''
88
+
89
+ return `<fieldset class="${e(classes)}" style="--rating-size:${fontSize};border:0;padding:0;margin:0"${disabled ? ' disabled' : ''}>
90
+ ${groupLabel}
91
+ <div class="ui-rating-stars">${items}</div>
92
+ </fieldset>`
93
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Pulse UI — Search
3
+ *
4
+ * Search input with icon, optional clear button, and label.
5
+ * Handles the native browser cancel button, debounce binding,
6
+ * and the clear event all in one component.
7
+ *
8
+ * @param {object} opts
9
+ * @param {string} opts.name - Field name (also used as id base)
10
+ * @param {string} opts.label - Label text (visually shown or screen-reader only)
11
+ * @param {boolean} opts.labelHidden - Hide label visually but keep for screen readers
12
+ * @param {string} opts.placeholder
13
+ * @param {string} opts.value - Current search value (for re-renders)
14
+ * @param {string} opts.event - data-event binding, e.g. 'input:setSearch'
15
+ * @param {number} opts.debounce - Debounce delay in ms (default: 200)
16
+ * @param {string} opts.clearEvent - Click event fired by the clear button (shown when value is non-empty)
17
+ * @param {boolean} opts.disabled
18
+ * @param {string} opts.id - Override generated id
19
+ * @param {string} opts.class
20
+ * @param {object} opts.attrs - Extra HTML attributes for the <input>
21
+ */
22
+
23
+ import { escHtml as e } from '../html.js'
24
+ import { iconSearch, iconX } from './icons.js'
25
+
26
+ export function search({
27
+ name = '',
28
+ label = '',
29
+ labelHidden = false,
30
+ placeholder = '',
31
+ value = '',
32
+ event = '',
33
+ debounce = 200,
34
+ clearEvent = '',
35
+ disabled = false,
36
+ id = '',
37
+ class: cls = '',
38
+ attrs = {},
39
+ } = {}) {
40
+ const fieldId = e(id || `field-${name}`)
41
+
42
+ const wrapClasses = ['ui-field', 'ui-search', cls].filter(Boolean).join(' ')
43
+
44
+ const labelClasses = ['ui-label', labelHidden ? 'ui-sr-only' : ''].filter(Boolean).join(' ')
45
+
46
+ const labelHtml = label
47
+ ? `<label for="${fieldId}" class="${labelClasses}">${e(label)}</label>`
48
+ : ''
49
+
50
+ const attrsStr = Object.entries(attrs)
51
+ .map(([k, v]) => ` ${e(k)}="${e(String(v))}"`)
52
+ .join('')
53
+
54
+ const clearBtn = clearEvent && value
55
+ ? `<button class="ui-search-clear" data-event="${e(clearEvent)}" type="button" aria-label="Clear search">${iconX({ size: 14 })}</button>`
56
+ : ''
57
+
58
+ return `<div class="${e(wrapClasses)}">
59
+ ${labelHtml}
60
+ <div class="ui-search-wrap">
61
+ <span class="ui-search-icon" aria-hidden="true">${iconSearch({ size: 16 })}</span>
62
+ <input
63
+ id="${fieldId}"
64
+ name="${e(name)}"
65
+ type="search"
66
+ class="ui-search-input"
67
+ ${placeholder ? `placeholder="${e(placeholder)}"` : ''}
68
+ ${value ? `value="${e(value)}"` : ''}
69
+ ${disabled ? 'disabled' : ''}
70
+ ${event ? `data-event="${e(event)}"` : ''}
71
+ ${event && debounce > 0 ? `data-debounce="${debounce}"` : ''}
72
+ ${attrsStr}
73
+ >
74
+ ${clearBtn}
75
+ </div>
76
+ </div>`
77
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Pulse UI — Section
3
+ *
4
+ * Vertical padding wrapper with optional background variant and built-in
5
+ * section header (eyebrow + title + subtitle) above the content slot.
6
+ * Compose with container() to get a constrained-width section.
7
+ *
8
+ * @param {object} opts
9
+ * @param {string} opts.content - Raw HTML slot
10
+ * @param {'default'|'alt'|'dark'} opts.variant - Background variant (default: 'default')
11
+ * @param {'sm'|'md'|'lg'} opts.padding - Vertical padding size (default: 'md')
12
+ * @param {string} opts.id - Section id for anchor links
13
+ * @param {string} opts.eyebrow - Small label above the title
14
+ * @param {string} opts.title - Section heading
15
+ * @param {number} opts.level - Heading level 1–6 (default 2). Visual style is always ui-section-title.
16
+ * @param {string} opts.subtitle - Supporting text beneath the heading
17
+ * @param {'left'|'center'} opts.align - Header text alignment (default: 'left')
18
+ * @param {'sm'|'md'|'lg'|'none'} opts.gap - Gap between header and content (default: 'md' = 2.5rem)
19
+ * @param {string} opts.class
20
+ */
21
+
22
+ import { escHtml as e } from '../html.js'
23
+
24
+ const VARIANTS = new Set(['default', 'alt', 'dark'])
25
+ const PADDINGS = new Set(['sm', 'md', 'lg'])
26
+ const GAPS = new Set(['none', 'sm', 'md', 'lg'])
27
+
28
+ export function section({
29
+ content = '',
30
+ variant = 'default',
31
+ padding = 'md',
32
+ gap = 'md',
33
+ id = '',
34
+ eyebrow = '',
35
+ title = '',
36
+ level = 2,
37
+ subtitle = '',
38
+ align = 'left',
39
+ class: cls = '',
40
+ } = {}) {
41
+ if (!VARIANTS.has(variant)) variant = 'default'
42
+ if (!PADDINGS.has(padding)) padding = 'md'
43
+ if (!GAPS.has(gap)) gap = 'md'
44
+
45
+ const classes = [
46
+ 'ui-section',
47
+ variant !== 'default' && `ui-section--${variant}`,
48
+ padding !== 'md' && `ui-section--${padding}`,
49
+ cls,
50
+ ].filter(Boolean).join(' ')
51
+
52
+ const idAttr = id ? ` id="${e(id)}"` : ''
53
+ const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
54
+
55
+ const headerClasses = [
56
+ 'ui-section-header',
57
+ align === 'center' && 'ui-section-header--center',
58
+ gap !== 'md' && `ui-section-header--gap-${gap}`,
59
+ ].filter(Boolean).join(' ')
60
+
61
+ const header = (eyebrow || title || subtitle) ? `
62
+ <div class="${headerClasses}">
63
+ ${eyebrow ? `<p class="ui-section-eyebrow">${e(eyebrow)}</p>` : ''}
64
+ ${title ? `<${tag} class="ui-section-title">${e(title)}</${tag}>` : ''}
65
+ ${subtitle ? `<p class="ui-section-subtitle">${e(subtitle)}</p>` : ''}
66
+ </div>` : ''
67
+
68
+ return `<section class="${e(classes)}"${idAttr}>${header}${content}</section>`
69
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Pulse UI — Segmented Control
3
+ *
4
+ * iOS-style segmented control implemented with hidden radio inputs.
5
+ * The selected option's label is highlighted via input:checked + label CSS.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.name - Field name (submitted in FormData)
9
+ * @param {Array} opts.options - Array of { value, label }
10
+ * @param {string} opts.value - Currently selected value
11
+ * @param {boolean} opts.disabled
12
+ * @param {'sm'|'md'|'lg'} opts.size - Size variant (default: 'md')
13
+ * @param {string} opts.event - data-event binding, e.g. 'change:setTab'
14
+ * @param {string} opts.class
15
+ */
16
+
17
+ import { escHtml as e } from '../html.js'
18
+
19
+ export function segmented({
20
+ name = '',
21
+ options = [],
22
+ value = '',
23
+ disabled = false,
24
+ size = 'md',
25
+ event = '',
26
+ class: cls = '',
27
+ } = {}) {
28
+ const sizeClass = size === 'sm' ? 'ui-segmented--sm'
29
+ : size === 'lg' ? 'ui-segmented--lg'
30
+ : ''
31
+
32
+ const wrapClasses = ['ui-segmented', sizeClass, cls].filter(Boolean).join(' ')
33
+
34
+ const items = options.map((opt, i) => {
35
+ const optId = e(`seg-${name}-${i}`)
36
+ const checked = String(opt.value) === String(value)
37
+ return `<input
38
+ type="radio"
39
+ class="ui-segmented-input"
40
+ id="${optId}"
41
+ name="${e(name)}"
42
+ value="${e(String(opt.value))}"
43
+ ${checked ? 'checked' : ''}
44
+ ${disabled ? 'disabled' : ''}
45
+ ${event ? `data-event="${e(event)}"` : ''}
46
+ ><label class="ui-segmented-label" for="${optId}">${e(String(opt.label))}</label>`
47
+ }).join('')
48
+
49
+ return `<div class="${e(wrapClasses)}" role="group">${items}</div>`
50
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Pulse UI — Select
3
+ *
4
+ * Dropdown with label, hint, and error message.
5
+ * Options accept { value, label } objects or plain strings.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.name
9
+ * @param {string} opts.label
10
+ * @param {Array} opts.options - Array of strings or { value, label } objects
11
+ * @param {string} opts.value - Currently selected value
12
+ * @param {string} opts.error
13
+ * @param {string} opts.hint
14
+ * @param {boolean} opts.required
15
+ * @param {boolean} opts.disabled
16
+ * @param {string} opts.id
17
+ * @param {string} opts.event - data-event binding, e.g. 'change:setCategory'
18
+ * @param {string} opts.class
19
+ */
20
+
21
+ import { escHtml as e } from '../html.js'
22
+ import { iconChevronDown } from './icons.js'
23
+
24
+ export function select({
25
+ name = '',
26
+ label = '',
27
+ options = [],
28
+ value = '',
29
+ error = '',
30
+ hint = '',
31
+ required = false,
32
+ disabled = false,
33
+ id = '',
34
+ event = '',
35
+ class: cls = '',
36
+ } = {}) {
37
+ const fieldId = e(id || `field-${name}`)
38
+ const errorId = `${fieldId}-error`
39
+ const hintId = `${fieldId}-hint`
40
+ const described = [error ? errorId : '', hint ? hintId : ''].filter(Boolean).join(' ')
41
+
42
+ const wrapClasses = ['ui-field', error ? 'ui-field--error' : '', cls].filter(Boolean).join(' ')
43
+
44
+ const labelHtml = label
45
+ ? `<label for="${fieldId}" class="ui-label">${e(label)}${required ? ' <span class="ui-required" aria-hidden="true">*</span>' : ''}</label>`
46
+ : ''
47
+
48
+ const optionsHtml = options.map(opt => {
49
+ const v = typeof opt === 'string' ? opt : opt.value
50
+ const l = typeof opt === 'string' ? opt : opt.label
51
+ return `<option value="${e(v)}"${v === value ? ' selected' : ''}>${e(l)}</option>`
52
+ }).join('')
53
+
54
+ const chevron = iconChevronDown({ size: 12 })
55
+
56
+ const hintHtml = hint ? `<p id="${hintId}" class="ui-hint">${e(hint)}</p>` : ''
57
+ const errorHtml = error ? `<p id="${errorId}" class="ui-error" role="alert">${e(error)}</p>` : ''
58
+
59
+ return `<div class="${e(wrapClasses)}">
60
+ ${labelHtml}
61
+ <div class="ui-select-wrap">
62
+ <select
63
+ id="${fieldId}"
64
+ name="${e(name)}"
65
+ class="ui-select"
66
+ ${required ? 'required aria-required="true"' : ''}
67
+ ${disabled ? 'disabled' : ''}
68
+ ${event ? `data-event="${e(event)}"` : ''}
69
+ ${described ? `aria-describedby="${described}"` : ''}
70
+ ${error ? 'aria-invalid="true"' : ''}
71
+ >${optionsHtml}</select>
72
+ <span class="ui-select-chevron" aria-hidden="true">${chevron}</span>
73
+ </div>
74
+ ${hintHtml}
75
+ ${errorHtml}
76
+ </div>`
77
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Pulse UI — Slider
3
+ *
4
+ * Styled range input with label and hint.
5
+ * The fill gradient is driven by --slider-fill CSS custom property.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.name - Field name (submitted in FormData as a number string)
9
+ * @param {string} opts.label - Visible label text
10
+ * @param {number} opts.min - Minimum value (default: 0)
11
+ * @param {number} opts.max - Maximum value (default: 100)
12
+ * @param {number} opts.step - Step increment (default: 1)
13
+ * @param {number} opts.value - Current value (default: 50)
14
+ * @param {boolean} opts.disabled
15
+ * @param {string} opts.hint - Helper text below the slider
16
+ * @param {boolean} opts.showValue - Show the current value live beside the label
17
+ * @param {string} opts.id - Override generated id
18
+ * @param {string} opts.event - data-event binding, e.g. 'change:setBrightness'
19
+ * @param {string} opts.class
20
+ */
21
+
22
+ import { escHtml as e } from '../html.js'
23
+
24
+ export function slider({
25
+ name = '',
26
+ label = '',
27
+ min = 0,
28
+ max = 100,
29
+ step = 1,
30
+ value = 50,
31
+ disabled = false,
32
+ hint = '',
33
+ showValue = false,
34
+ id = '',
35
+ event = '',
36
+ class: cls = '',
37
+ } = {}) {
38
+ const fieldId = e(id || `slider-${name}`)
39
+ const hintId = `${fieldId}-hint`
40
+ const described = hint ? hintId : ''
41
+
42
+ const minN = Number(min)
43
+ const maxN = Number(max)
44
+ const valN = Math.min(Math.max(Number(value), minN), maxN)
45
+ const fillPct = maxN > minN
46
+ ? (((valN - minN) / (maxN - minN)) * 100).toFixed(2) + '%'
47
+ : '0%'
48
+
49
+ const wrapClasses = ['ui-field', cls].filter(Boolean).join(' ')
50
+
51
+ const outputId = `${fieldId}-output`
52
+
53
+ const labelHtml = label
54
+ ? `<label for="${fieldId}" class="ui-label${showValue ? ' ui-label--row' : ''}">
55
+ ${e(label)}
56
+ ${showValue ? `<output id="${outputId}" class="ui-slider-output" for="${fieldId}">${valN}</output>` : ''}
57
+ </label>`
58
+ : ''
59
+
60
+ const hintHtml = hint
61
+ ? `<p id="${hintId}" class="ui-hint">${e(hint)}</p>`
62
+ : ''
63
+
64
+ return `<div class="${e(wrapClasses)}" style="--slider-fill:${fillPct}">
65
+ ${labelHtml}
66
+ <input
67
+ type="range"
68
+ id="${fieldId}"
69
+ name="${e(name)}"
70
+ class="ui-slider"
71
+ min="${minN}"
72
+ max="${maxN}"
73
+ step="${e(String(step))}"
74
+ value="${valN}"
75
+ aria-valuemin="${minN}"
76
+ aria-valuemax="${maxN}"
77
+ aria-valuenow="${valN}"
78
+ ${disabled ? 'disabled' : ''}
79
+ ${event ? `data-event="${e(event)}"` : ''}
80
+ ${described ? `aria-describedby="${described}"` : ''}
81
+ >
82
+ ${hintHtml}
83
+ </div>`
84
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pulse UI — Spinner
3
+ *
4
+ * CSS-animated loading indicator. No JavaScript required.
5
+ *
6
+ * @param {object} opts
7
+ * @param {'sm'|'md'|'lg'} opts.size
8
+ * @param {'accent'|'muted'|'white'} opts.color
9
+ * @param {string} opts.label - Accessible label (default: 'Loading…')
10
+ * @param {string} opts.class
11
+ */
12
+
13
+ import { escHtml as e } from '../html.js'
14
+
15
+ const SIZES = { sm: '1rem', md: '1.5rem', lg: '2.5rem' }
16
+ const COLORS = { accent: 'var(--ui-accent)', muted: 'var(--ui-muted)', white: '#fff' }
17
+
18
+ export function spinner({
19
+ size = 'md',
20
+ color = 'accent',
21
+ label = 'Loading…',
22
+ class: cls = '',
23
+ } = {}) {
24
+ const sz = SIZES[size] ?? SIZES.md
25
+ const clr = COLORS[color] ?? COLORS.accent
26
+ const classes = ['ui-spinner', cls].filter(Boolean).join(' ')
27
+
28
+ return `<span
29
+ class="${e(classes)}"
30
+ role="status"
31
+ aria-label="${e(label)}"
32
+ style="--spinner-size:${sz};--spinner-color:${clr}"
33
+ ></span>`
34
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pulse UI — Stack
3
+ *
4
+ * Flex column with consistent vertical gap. The simplest way to space
5
+ * a sequence of elements vertically without writing custom CSS.
6
+ *
7
+ * @param {object} opts
8
+ * @param {string} opts.content - Raw HTML slot
9
+ * @param {'xs'|'sm'|'md'|'lg'|'xl'} opts.gap - Gap between children (default: 'md')
10
+ * @param {'stretch'|'start'|'center'|'end'} opts.align - align-items (default: 'stretch')
11
+ * @param {string} opts.class
12
+ */
13
+
14
+ import { escHtml as e } from '../html.js'
15
+
16
+ const GAPS = new Set(['xs', 'sm', 'md', 'lg', 'xl'])
17
+ const ALIGNS = new Set(['stretch', 'start', 'center', 'end'])
18
+
19
+ export function stack({
20
+ content = '',
21
+ gap = 'md',
22
+ align = 'stretch',
23
+ class: cls = '',
24
+ } = {}) {
25
+ if (!GAPS.has(gap)) gap = 'md'
26
+ if (!ALIGNS.has(align)) align = 'stretch'
27
+
28
+ const classes = [
29
+ 'ui-stack',
30
+ gap !== 'md' && `ui-stack--gap-${gap}`,
31
+ align !== 'stretch' && `ui-stack--align-${align}`,
32
+ cls,
33
+ ].filter(Boolean).join(' ')
34
+
35
+ return `<div class="${e(classes)}">${content}</div>`
36
+ }
package/src/ui/stat.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Pulse UI — Stat
3
+ *
4
+ * A single metric with label, value, and optional trend indicator.
5
+ *
6
+ * @param {object} opts
7
+ * @param {string} opts.label
8
+ * @param {string} opts.value - Formatted value string (e.g. "2.4k", "98%")
9
+ * @param {string} opts.change - Change label (e.g. "+12%", "−3")
10
+ * @param {'up'|'down'|'neutral'} opts.trend
11
+ * @param {boolean} opts.center - Centre-align all text
12
+ * @param {string} opts.class
13
+ */
14
+
15
+ import { escHtml as e } from '../html.js'
16
+ import { iconTrendingUp, iconTrendingDown, iconMinus } from './icons.js'
17
+
18
+ const TRENDS = new Set(['up', 'down', 'neutral'])
19
+
20
+ const TREND_ICONS = {
21
+ up: iconTrendingUp({ size: 13 }),
22
+ down: iconTrendingDown({ size: 13 }),
23
+ neutral: iconMinus({ size: 13 }),
24
+ }
25
+
26
+ const TREND_LABELS = { up: 'increase', down: 'decrease', neutral: 'no change' }
27
+
28
+ export function stat({
29
+ label = '',
30
+ value = '',
31
+ change = '',
32
+ trend = 'neutral',
33
+ center = false,
34
+ class: cls = '',
35
+ } = {}) {
36
+ if (!TRENDS.has(trend)) trend = 'neutral'
37
+
38
+ const classes = ['ui-stat', center && 'ui-stat--center', cls].filter(Boolean).join(' ')
39
+
40
+ const changeHtml = change
41
+ ? `<p class="ui-stat-change ui-stat-change--${e(trend)}">
42
+ <span aria-label="${e(TREND_LABELS[trend])}">${TREND_ICONS[trend]}</span>
43
+ ${e(change)}
44
+ </p>`
45
+ : ''
46
+
47
+ return `<div class="${e(classes)}">
48
+ <p class="ui-stat-label">${e(label)}</p>
49
+ <p class="ui-stat-value">${e(value)}</p>
50
+ ${changeHtml}
51
+ </div>`
52
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pulse UI — Stepper
3
+ *
4
+ * Horizontal step progress indicator.
5
+ * Steps before `current` are complete (filled accent dot with check icon).
6
+ * The step at `current` is active (accent border + accent number).
7
+ * Steps after `current` are upcoming (muted border + muted number).
8
+ *
9
+ * @param {object} opts
10
+ * @param {Array} opts.steps - Array of step label strings
11
+ * @param {number} opts.current - 0-based index of the active step
12
+ * @param {string} opts.class
13
+ */
14
+
15
+ import { escHtml as e } from '../html.js'
16
+ import { iconCheck } from './icons.js'
17
+
18
+ const CHECK_SVG = iconCheck({ size: 12 })
19
+
20
+ export function stepper({
21
+ steps = [],
22
+ current = 0,
23
+ class: cls = '',
24
+ } = {}) {
25
+ const wrapClasses = ['ui-stepper', cls].filter(Boolean).join(' ')
26
+
27
+ const items = steps.map((label, i) => {
28
+ const isComplete = i < current
29
+ const isActive = i === current
30
+
31
+ const modClass = isComplete ? 'ui-stepper-item--complete'
32
+ : isActive ? 'ui-stepper-item--active'
33
+ : ''
34
+
35
+ const dot = isComplete
36
+ ? `<div class="ui-stepper-dot">${CHECK_SVG}</div>`
37
+ : `<div class="ui-stepper-dot">${i + 1}</div>`
38
+
39
+ return `<div class="ui-stepper-item${modClass ? ` ${modClass}` : ''}">
40
+ ${dot}
41
+ <span class="ui-stepper-label">${e(label)}</span>
42
+ </div>`
43
+ }).join('')
44
+
45
+ return `<div class="${e(wrapClasses)}">${items}</div>`
46
+ }