@eifi1/ui-kit 0.4.0 → 0.5.0

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 (410) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +498 -57
  3. package/dist/chart.d.ts +10 -0
  4. package/dist/chart.js +10 -0
  5. package/dist/chart.js.map +1 -0
  6. package/dist/components/account-settings.d.ts +11 -0
  7. package/dist/components/account-settings.js +19 -12
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/alert-banner.d.ts +10 -6
  10. package/dist/components/alert-banner.js +20 -16
  11. package/dist/components/alert-banner.js.map +1 -1
  12. package/dist/components/amount-input.d.ts +30 -4
  13. package/dist/components/amount-input.js +36 -14
  14. package/dist/components/amount-input.js.map +1 -1
  15. package/dist/components/calculator.d.ts +24 -7
  16. package/dist/components/calculator.js +37 -20
  17. package/dist/components/calculator.js.map +1 -1
  18. package/dist/components/chart-zoom.d.ts +156 -0
  19. package/dist/components/chart-zoom.js +262 -0
  20. package/dist/components/chart-zoom.js.map +1 -0
  21. package/dist/components/chart.d.ts +28 -3
  22. package/dist/components/chart.js +71 -32
  23. package/dist/components/chart.js.map +1 -1
  24. package/dist/components/checkbox.d.ts +51 -0
  25. package/dist/components/checkbox.js +123 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip.d.ts +128 -0
  28. package/dist/components/chip.js +357 -0
  29. package/dist/components/chip.js.map +1 -0
  30. package/dist/components/choice-parts.d.ts +24 -0
  31. package/dist/components/choice-parts.js +18 -0
  32. package/dist/components/choice-parts.js.map +1 -0
  33. package/dist/components/combobox-core.d.ts +52 -9
  34. package/dist/components/combobox-core.js +91 -20
  35. package/dist/components/combobox-core.js.map +1 -1
  36. package/dist/components/combobox.d.ts +43 -33
  37. package/dist/components/combobox.js +409 -340
  38. package/dist/components/combobox.js.map +1 -1
  39. package/dist/components/currency-select.d.ts +48 -8
  40. package/dist/components/currency-select.js +107 -65
  41. package/dist/components/currency-select.js.map +1 -1
  42. package/dist/components/data-table-filter-popover.d.ts +15 -3
  43. package/dist/components/data-table-filter-popover.js +23 -17
  44. package/dist/components/data-table-filter-popover.js.map +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +3 -2
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +53 -2
  49. package/dist/components/data-table-labels.js +31 -2
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table-pagination.d.ts +3 -1
  52. package/dist/components/data-table-pagination.js +16 -10
  53. package/dist/components/data-table-pagination.js.map +1 -1
  54. package/dist/components/data-table-sort.js +1 -0
  55. package/dist/components/data-table-sort.js.map +1 -1
  56. package/dist/components/data-table.d.ts +1 -1
  57. package/dist/components/data-table.js +201 -85
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/date-picker.d.ts +25 -9
  60. package/dist/components/date-picker.js +232 -94
  61. package/dist/components/date-picker.js.map +1 -1
  62. package/dist/components/dropdown.d.ts +96 -20
  63. package/dist/components/dropdown.js +77 -33
  64. package/dist/components/dropdown.js.map +1 -1
  65. package/dist/components/entity-combobox.d.ts +5 -4
  66. package/dist/components/entity-combobox.js +107 -76
  67. package/dist/components/entity-combobox.js.map +1 -1
  68. package/dist/components/facing-pair.d.ts +36 -0
  69. package/dist/components/facing-pair.js +40 -0
  70. package/dist/components/facing-pair.js.map +1 -0
  71. package/dist/components/field-sync.d.ts +164 -0
  72. package/dist/components/field-sync.js +264 -0
  73. package/dist/components/field-sync.js.map +1 -0
  74. package/dist/components/file-dropzone.d.ts +38 -6
  75. package/dist/components/file-dropzone.js +34 -9
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/full-bleed-dialog.d.ts +37 -26
  78. package/dist/components/full-bleed-dialog.js +20 -5
  79. package/dist/components/full-bleed-dialog.js.map +1 -1
  80. package/dist/components/grouped-picker.d.ts +21 -10
  81. package/dist/components/grouped-picker.js +66 -52
  82. package/dist/components/grouped-picker.js.map +1 -1
  83. package/dist/components/hover-menu.d.ts +20 -4
  84. package/dist/components/hover-menu.js +15 -4
  85. package/dist/components/hover-menu.js.map +1 -1
  86. package/dist/components/mini-calendar.d.ts +102 -14
  87. package/dist/components/mini-calendar.js +256 -70
  88. package/dist/components/mini-calendar.js.map +1 -1
  89. package/dist/components/modal.d.ts +9 -3
  90. package/dist/components/modal.js +34 -45
  91. package/dist/components/modal.js.map +1 -1
  92. package/dist/components/month-picker.d.ts +79 -0
  93. package/dist/components/month-picker.js +342 -0
  94. package/dist/components/month-picker.js.map +1 -0
  95. package/dist/components/multi-entity-combobox.d.ts +7 -6
  96. package/dist/components/multi-entity-combobox.js +112 -72
  97. package/dist/components/multi-entity-combobox.js.map +1 -1
  98. package/dist/components/multi-select.d.ts +13 -11
  99. package/dist/components/multi-select.js +161 -84
  100. package/dist/components/multi-select.js.map +1 -1
  101. package/dist/components/number-field.d.ts +92 -0
  102. package/dist/components/number-field.js +111 -0
  103. package/dist/components/number-field.js.map +1 -0
  104. package/dist/components/number-input.d.ts +28 -2
  105. package/dist/components/number-input.js +12 -2
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +32 -4
  108. package/dist/components/numpad-sheet.js +30 -13
  109. package/dist/components/numpad-sheet.js.map +1 -1
  110. package/dist/components/page-contents.d.ts +90 -0
  111. package/dist/components/page-contents.js +170 -0
  112. package/dist/components/page-contents.js.map +1 -0
  113. package/dist/components/password-strength.d.ts +106 -0
  114. package/dist/components/password-strength.js +132 -0
  115. package/dist/components/password-strength.js.map +1 -0
  116. package/dist/components/picker-sheet.d.ts +23 -15
  117. package/dist/components/picker-sheet.js +34 -14
  118. package/dist/components/picker-sheet.js.map +1 -1
  119. package/dist/components/popover.d.ts +62 -4
  120. package/dist/components/popover.js +52 -2
  121. package/dist/components/popover.js.map +1 -1
  122. package/dist/components/search-field.d.ts +25 -7
  123. package/dist/components/search-field.js +7 -5
  124. package/dist/components/search-field.js.map +1 -1
  125. package/dist/components/series-chart-labels.d.ts +14 -0
  126. package/dist/components/series-chart-labels.js +11 -0
  127. package/dist/components/series-chart-labels.js.map +1 -0
  128. package/dist/components/series-chart.d.ts +207 -0
  129. package/dist/components/series-chart.js +223 -0
  130. package/dist/components/series-chart.js.map +1 -0
  131. package/dist/components/settings-fields.d.ts +14 -4
  132. package/dist/components/settings-fields.js +19 -8
  133. package/dist/components/settings-fields.js.map +1 -1
  134. package/dist/components/signature-pad.d.ts +119 -0
  135. package/dist/components/signature-pad.js +405 -0
  136. package/dist/components/signature-pad.js.map +1 -0
  137. package/dist/components/slider.d.ts +80 -0
  138. package/dist/components/slider.js +155 -0
  139. package/dist/components/slider.js.map +1 -0
  140. package/dist/components/sparkline.d.ts +93 -0
  141. package/dist/components/sparkline.js +205 -0
  142. package/dist/components/sparkline.js.map +1 -0
  143. package/dist/components/stat-tile.d.ts +151 -0
  144. package/dist/components/stat-tile.js +236 -0
  145. package/dist/components/stat-tile.js.map +1 -0
  146. package/dist/components/swipeable-row.d.ts +25 -2
  147. package/dist/components/swipeable-row.js +31 -0
  148. package/dist/components/swipeable-row.js.map +1 -1
  149. package/dist/components/switch.d.ts +60 -0
  150. package/dist/components/switch.js +81 -0
  151. package/dist/components/switch.js.map +1 -0
  152. package/dist/components/time-input.d.ts +58 -0
  153. package/dist/components/time-input.js +50 -0
  154. package/dist/components/time-input.js.map +1 -0
  155. package/dist/components/toggle-group.d.ts +18 -3
  156. package/dist/components/toggle-group.js +10 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +115 -0
  159. package/dist/components/toggle-legend.js +114 -0
  160. package/dist/components/toggle-legend.js.map +1 -0
  161. package/dist/components/tooltip.d.ts +40 -11
  162. package/dist/components/tooltip.js +77 -20
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/treemap.d.ts +132 -0
  165. package/dist/components/treemap.js +179 -0
  166. package/dist/components/treemap.js.map +1 -0
  167. package/dist/components/ui.d.ts +159 -46
  168. package/dist/components/ui.js +220 -115
  169. package/dist/components/ui.js.map +1 -1
  170. package/dist/components/use-mobile-reveal.js +1 -0
  171. package/dist/components/use-mobile-reveal.js.map +1 -1
  172. package/dist/components/use-table-state.d.ts +1 -1
  173. package/dist/components/use-table-state.js +38 -17
  174. package/dist/components/use-table-state.js.map +1 -1
  175. package/dist/components/user-avatar.d.ts +15 -4
  176. package/dist/components/user-avatar.js +5 -4
  177. package/dist/components/user-avatar.js.map +1 -1
  178. package/dist/components/wizard-stepper.js +3 -2
  179. package/dist/components/wizard-stepper.js.map +1 -1
  180. package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
  181. package/dist/data-table.d.ts +7 -0
  182. package/dist/data-table.js +12 -0
  183. package/dist/data-table.js.map +1 -0
  184. package/dist/feedback/feedback-attachment.js +12 -11
  185. package/dist/feedback/feedback-attachment.js.map +1 -1
  186. package/dist/feedback/feedback-dialog.js +2 -1
  187. package/dist/feedback/feedback-dialog.js.map +1 -1
  188. package/dist/feedback/feedback-inbox.d.ts +33 -7
  189. package/dist/feedback/feedback-inbox.js +49 -37
  190. package/dist/feedback/feedback-inbox.js.map +1 -1
  191. package/dist/feedback.d.ts +5 -0
  192. package/dist/feedback.js +5 -0
  193. package/dist/feedback.js.map +1 -0
  194. package/dist/hooks/use-anchored-panel.js +1 -0
  195. package/dist/hooks/use-anchored-panel.js.map +1 -1
  196. package/dist/hooks/use-anchored-rect.js +1 -0
  197. package/dist/hooks/use-anchored-rect.js.map +1 -1
  198. package/dist/hooks/use-announce.d.ts +46 -0
  199. package/dist/hooks/use-announce.js +37 -0
  200. package/dist/hooks/use-announce.js.map +1 -0
  201. package/dist/hooks/use-body-scroll-lock.js +1 -0
  202. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  203. package/dist/hooks/use-close-transition.js +1 -0
  204. package/dist/hooks/use-close-transition.js.map +1 -1
  205. package/dist/hooks/use-dismiss.js +1 -0
  206. package/dist/hooks/use-dismiss.js.map +1 -1
  207. package/dist/hooks/use-focus-trap.d.ts +16 -0
  208. package/dist/hooks/use-focus-trap.js +67 -0
  209. package/dist/hooks/use-focus-trap.js.map +1 -0
  210. package/dist/hooks/use-media-query.js +1 -0
  211. package/dist/hooks/use-media-query.js.map +1 -1
  212. package/dist/hooks/use-overlay-history.js +27 -1
  213. package/dist/hooks/use-overlay-history.js.map +1 -1
  214. package/dist/hooks/use-row-swipe.js +1 -0
  215. package/dist/hooks/use-row-swipe.js.map +1 -1
  216. package/dist/i18n/defaults.d.ts +31 -0
  217. package/dist/i18n/defaults.js +63 -0
  218. package/dist/i18n/defaults.js.map +1 -0
  219. package/dist/i18n/kit-labels.d.ts +250 -0
  220. package/dist/i18n/kit-labels.js +184 -0
  221. package/dist/i18n/kit-labels.js.map +1 -0
  222. package/dist/index.d.ts +47 -22
  223. package/dist/index.js +28 -0
  224. package/dist/index.js.map +1 -1
  225. package/dist/lib/calc.js +1 -0
  226. package/dist/lib/calc.js.map +1 -1
  227. package/dist/lib/cn.js +1 -0
  228. package/dist/lib/cn.js.map +1 -1
  229. package/dist/lib/dates.js +1 -0
  230. package/dist/lib/dates.js.map +1 -1
  231. package/dist/lib/logger.js +1 -0
  232. package/dist/lib/logger.js.map +1 -1
  233. package/dist/lib/safe-storage.js +1 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/search/command-palette.d.ts +11 -1
  236. package/dist/search/command-palette.js +21 -15
  237. package/dist/search/command-palette.js.map +1 -1
  238. package/dist/search.d.ts +2 -0
  239. package/dist/search.js +3 -0
  240. package/dist/search.js.map +1 -0
  241. package/dist/shell/app-shell.d.ts +33 -4
  242. package/dist/shell/app-shell.js +275 -78
  243. package/dist/shell/app-shell.js.map +1 -1
  244. package/dist/shell/option-switcher-menu.js +3 -2
  245. package/dist/shell/option-switcher-menu.js.map +1 -1
  246. package/dist/shell/role-switcher.d.ts +6 -0
  247. package/dist/shell/role-switcher.js +7 -4
  248. package/dist/shell/role-switcher.js.map +1 -1
  249. package/dist/shell/top-bar.d.ts +15 -7
  250. package/dist/shell/top-bar.js +3 -5
  251. package/dist/shell/top-bar.js.map +1 -1
  252. package/dist/shell/topbar-action-menu.js +3 -2
  253. package/dist/shell/topbar-action-menu.js.map +1 -1
  254. package/dist/shell/topbar-controls.d.ts +5 -2
  255. package/dist/shell/topbar-controls.js +20 -15
  256. package/dist/shell/topbar-controls.js.map +1 -1
  257. package/dist/shell.d.ts +13 -0
  258. package/dist/shell.js +8 -0
  259. package/dist/shell.js.map +1 -0
  260. package/dist/theme/chart-palette.d.ts +19 -12
  261. package/dist/theme/chart-palette.js +24 -30
  262. package/dist/theme/chart-palette.js.map +1 -1
  263. package/dist/theme/color.d.ts +78 -0
  264. package/dist/theme/color.js +148 -0
  265. package/dist/theme/color.js.map +1 -0
  266. package/dist/theme/palette-derive.d.ts +140 -0
  267. package/dist/theme/palette-derive.js +376 -0
  268. package/dist/theme/palette-derive.js.map +1 -0
  269. package/dist/theme/palette-presets.d.ts +27 -5
  270. package/dist/theme/palette-presets.js +231 -33
  271. package/dist/theme/palette-presets.js.map +1 -1
  272. package/dist/theme/palette-store.js +1 -0
  273. package/dist/theme/palette-store.js.map +1 -1
  274. package/dist/theme/theme-store.js +1 -0
  275. package/dist/theme/theme-store.js.map +1 -1
  276. package/dist/tour/tour.d.ts +2 -1
  277. package/dist/tour/tour.js +15 -9
  278. package/dist/tour/tour.js.map +1 -1
  279. package/dist/tour.d.ts +2 -0
  280. package/dist/tour.js +3 -0
  281. package/dist/tour.js.map +1 -0
  282. package/dist/wizard/stepper-nav.js +10 -8
  283. package/dist/wizard/stepper-nav.js.map +1 -1
  284. package/dist/wizard/types.d.ts +18 -2
  285. package/dist/wizard/types.js +4 -1
  286. package/dist/wizard/types.js.map +1 -1
  287. package/dist/wizard/use-wizard.js +12 -4
  288. package/dist/wizard/use-wizard.js.map +1 -1
  289. package/dist/wizard/validation.js +1 -0
  290. package/dist/wizard/validation.js.map +1 -1
  291. package/dist/wizard/wizard-context.js +1 -0
  292. package/dist/wizard/wizard-context.js.map +1 -1
  293. package/dist/wizard/wizard-step.js +1 -0
  294. package/dist/wizard/wizard-step.js.map +1 -1
  295. package/dist/wizard/wizard-summary.js +5 -3
  296. package/dist/wizard/wizard-summary.js.map +1 -1
  297. package/dist/wizard.d.ts +8 -0
  298. package/dist/wizard.js +9 -0
  299. package/dist/wizard.js.map +1 -0
  300. package/package.json +67 -14
  301. package/src/chart.ts +22 -0
  302. package/src/components/account-settings.tsx +275 -0
  303. package/src/components/alert-banner.tsx +75 -0
  304. package/src/components/amount-input.tsx +471 -0
  305. package/src/components/calculator.tsx +252 -0
  306. package/src/components/chart-zoom.tsx +499 -0
  307. package/src/components/chart.tsx +363 -0
  308. package/src/components/checkbox.tsx +222 -0
  309. package/src/components/chip.tsx +570 -0
  310. package/src/components/choice-parts.ts +33 -0
  311. package/src/components/combobox-core.tsx +540 -0
  312. package/src/components/combobox.tsx +1026 -0
  313. package/src/components/currency-select.tsx +269 -0
  314. package/src/components/data-table-filter-popover.tsx +252 -0
  315. package/src/components/data-table-filters.ts +176 -0
  316. package/src/components/data-table-labels.ts +190 -0
  317. package/src/components/data-table-pagination.tsx +126 -0
  318. package/src/components/data-table-sort.ts +70 -0
  319. package/src/components/data-table.tsx +1740 -0
  320. package/src/components/date-picker.tsx +631 -0
  321. package/src/components/dropdown.tsx +341 -0
  322. package/src/components/entity-combobox.tsx +215 -0
  323. package/src/components/facing-pair.ts +86 -0
  324. package/src/components/field-sync.tsx +472 -0
  325. package/src/components/file-dropzone.tsx +181 -0
  326. package/src/components/full-bleed-dialog.tsx +179 -0
  327. package/src/components/grouped-picker.tsx +157 -0
  328. package/src/components/hover-menu.tsx +183 -0
  329. package/src/components/mini-calendar.tsx +554 -0
  330. package/src/components/modal.tsx +287 -0
  331. package/src/components/month-picker.tsx +549 -0
  332. package/src/components/multi-entity-combobox.tsx +226 -0
  333. package/src/components/multi-select.tsx +303 -0
  334. package/src/components/number-field.tsx +225 -0
  335. package/src/components/number-input.tsx +211 -0
  336. package/src/components/numpad-sheet.tsx +264 -0
  337. package/src/components/page-contents.tsx +289 -0
  338. package/src/components/password-strength.tsx +258 -0
  339. package/src/components/picker-sheet.tsx +251 -0
  340. package/src/components/popover.tsx +195 -0
  341. package/src/components/search-field.tsx +146 -0
  342. package/src/components/series-chart-labels.ts +26 -0
  343. package/src/components/series-chart.tsx +481 -0
  344. package/src/components/settings-fields.tsx +68 -0
  345. package/src/components/signature-pad.tsx +630 -0
  346. package/src/components/slider.tsx +313 -0
  347. package/src/components/sparkline.tsx +329 -0
  348. package/src/components/stat-tile.tsx +448 -0
  349. package/src/components/swipeable-row.tsx +271 -0
  350. package/src/components/switch.tsx +170 -0
  351. package/src/components/time-input.tsx +107 -0
  352. package/src/components/toggle-group.tsx +153 -0
  353. package/src/components/toggle-legend.tsx +229 -0
  354. package/src/components/tooltip.tsx +477 -0
  355. package/src/components/treemap.tsx +390 -0
  356. package/src/components/ui.tsx +1284 -0
  357. package/src/components/use-mobile-reveal.ts +54 -0
  358. package/src/components/use-table-state.ts +295 -0
  359. package/src/components/user-avatar.tsx +61 -0
  360. package/src/components/wizard-stepper.tsx +37 -0
  361. package/src/data-table.ts +19 -0
  362. package/src/feedback/feedback-attachment.tsx +246 -0
  363. package/src/feedback/feedback-dialog.tsx +209 -0
  364. package/src/feedback/feedback-inbox.tsx +615 -0
  365. package/src/feedback.ts +8 -0
  366. package/src/hooks/use-anchored-panel.ts +169 -0
  367. package/src/hooks/use-anchored-rect.ts +78 -0
  368. package/src/hooks/use-announce.ts +85 -0
  369. package/src/hooks/use-body-scroll-lock.ts +94 -0
  370. package/src/hooks/use-close-transition.ts +99 -0
  371. package/src/hooks/use-dismiss.ts +77 -0
  372. package/src/hooks/use-focus-trap.ts +160 -0
  373. package/src/hooks/use-media-query.ts +23 -0
  374. package/src/hooks/use-overlay-history.ts +345 -0
  375. package/src/hooks/use-row-swipe.ts +212 -0
  376. package/src/i18n/defaults.ts +68 -0
  377. package/src/i18n/kit-labels.tsx +447 -0
  378. package/src/index.ts +169 -0
  379. package/src/lib/calc.ts +309 -0
  380. package/src/lib/cn.ts +5 -0
  381. package/src/lib/dates.ts +187 -0
  382. package/src/lib/logger.ts +73 -0
  383. package/src/lib/safe-storage.ts +37 -0
  384. package/src/search/command-palette.tsx +342 -0
  385. package/src/search.ts +3 -0
  386. package/src/shell/app-shell.tsx +772 -0
  387. package/src/shell/option-switcher-menu.tsx +72 -0
  388. package/src/shell/role-switcher.tsx +46 -0
  389. package/src/shell/top-bar.tsx +39 -0
  390. package/src/shell/topbar-action-menu.tsx +91 -0
  391. package/src/shell/topbar-controls.tsx +229 -0
  392. package/src/shell.ts +10 -0
  393. package/src/theme/chart-palette.ts +145 -0
  394. package/src/theme/color.ts +266 -0
  395. package/src/theme/palette-derive.ts +681 -0
  396. package/src/theme/palette-presets.ts +760 -0
  397. package/src/theme/palette-store.ts +94 -0
  398. package/src/theme/theme-store.ts +145 -0
  399. package/src/tour/tour.tsx +586 -0
  400. package/src/tour.ts +3 -0
  401. package/src/vite-env.d.ts +26 -0
  402. package/src/wizard/stepper-nav.tsx +221 -0
  403. package/src/wizard/types.ts +178 -0
  404. package/src/wizard/use-wizard.ts +345 -0
  405. package/src/wizard/validation.ts +37 -0
  406. package/src/wizard/wizard-context.tsx +29 -0
  407. package/src/wizard/wizard-step.tsx +14 -0
  408. package/src/wizard/wizard-summary.tsx +69 -0
  409. package/src/wizard.ts +14 -0
  410. package/tokens.css +222 -20
@@ -0,0 +1,60 @@
1
+ import * as react from 'react';
2
+ import { InputHTMLAttributes, ReactNode } from 'react';
3
+
4
+ /**
5
+ * An on/off switch that is still `<input type="checkbox">`, with `role="switch"`.
6
+ *
7
+ * Kastlan's was a Radix `<button>`; this keeps the native element for the reasons
8
+ * `Checkbox` gives (form submission, `required`, reset, `<label for>`), and adds the
9
+ * one thing a checkbox is not: `role="switch"`, which screen readers announce as
10
+ * "on/off" rather than "checked/not checked". Use it for a setting that takes effect
11
+ * the moment it is flipped; a choice that waits for a Save button is a checkbox.
12
+ */
13
+ /** Geometry per size. The thumb sits `2px` in from each end of the track, so its
14
+ * checked offset is `track width − thumb − 2px`. `start-*`, never `left-*` or a
15
+ * `translate-x`: an inline-start offset follows the writing direction, so in a
16
+ * right-to-left form the thumb travels right-to-left with no `rtl:` override to
17
+ * keep in step. */
18
+ declare const SIZES: {
19
+ readonly sm: {
20
+ readonly track: "h-4 w-7";
21
+ readonly thumb: "size-3 peer-checked:start-[14px]";
22
+ };
23
+ readonly md: {
24
+ readonly track: "h-5 w-9";
25
+ readonly thumb: "size-4 peer-checked:start-[18px]";
26
+ };
27
+ };
28
+ type SwitchSize = keyof typeof SIZES;
29
+ interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "size" | "role"> {
30
+ /** `"md"` (36×20, default) or `"sm"` (28×16) — Kastlan's `default` and `sm`. */
31
+ size?: SwitchSize;
32
+ /** The words beside the switch, as a `<label for>` — see {@link Checkbox}'s
33
+ * `label` for why it does not wrap the input. Without it, give the switch an
34
+ * `aria-label`. */
35
+ label?: ReactNode;
36
+ /** Secondary text under the label, attached with `aria-describedby` (merged with
37
+ * any the caller passed). */
38
+ description?: ReactNode;
39
+ /** Which end of the row the switch sits at when there is a label. `"end"` (the
40
+ * default) is the settings-list shape Kastlan's profile page uses — the words at
41
+ * the start, the control at the far end; `"start"` puts it before the words, like
42
+ * a checkbox. Logical, so it follows the writing direction. */
43
+ switchPosition?: "start" | "end";
44
+ /** The new state as a boolean — Radix's `onCheckedChange`, so Kastlan's call sites
45
+ * move without unwrapping the event. Fires alongside `onChange`. */
46
+ onCheckedChange?: (checked: boolean) => void;
47
+ /** Classes for the `<input>` (the track). `className` styles the outermost element:
48
+ * the row when there is a label, the switch's own wrapper when there is not. */
49
+ inputClassName?: string;
50
+ }
51
+ /**
52
+ * A native checkbox drawn as a track and thumb, announced as a switch.
53
+ *
54
+ * Every other prop reaches the `<input>`, so it works controlled (`checked` +
55
+ * `onChange`/`onCheckedChange`), uncontrolled (`defaultChecked`) and in a plain
56
+ * `<form>` (`name`, `value`).
57
+ */
58
+ declare const Switch: react.ForwardRefExoticComponent<SwitchProps & react.RefAttributes<HTMLInputElement>>;
59
+
60
+ export { Switch, type SwitchProps, type SwitchSize };
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useId } from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
6
+ const SIZES = {
7
+ // 28×16 track, 12px thumb: the dense setting rows in Kastlan's tables.
8
+ sm: { track: "h-4 w-7", thumb: "size-3 peer-checked:start-[14px]" },
9
+ // 36×20 track, 16px thumb.
10
+ md: { track: "h-5 w-9", thumb: "size-4 peer-checked:start-[18px]" }
11
+ };
12
+ const TRACK = "peer block shrink-0 cursor-pointer appearance-none rounded-full bg-[var(--border-strong)] transition-colors checked:bg-[var(--brand)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] disabled:cursor-not-allowed";
13
+ const THUMB = "pointer-events-none absolute start-0.5 top-1/2 -translate-y-1/2 rounded-full bg-[var(--bg-surface)] shadow-sm transition-[inset-inline-start,background-color] motion-reduce:transition-none peer-checked:bg-[var(--brand-contrast)]";
14
+ const Switch = forwardRef(function Switch2({
15
+ size = "md",
16
+ label,
17
+ description,
18
+ switchPosition = "end",
19
+ onCheckedChange,
20
+ onChange,
21
+ className,
22
+ inputClassName,
23
+ id,
24
+ disabled,
25
+ ...rest
26
+ }, ref) {
27
+ const generated = useId();
28
+ const inputId = id ?? generated;
29
+ const descriptionId = `${inputId}-description`;
30
+ const showDescription = hasMessage(description);
31
+ const bare = label === void 0 && !showDescription;
32
+ const geometry = SIZES[size];
33
+ const control = /* @__PURE__ */ jsxs("span", { className: cn("relative inline-flex shrink-0", bare && className), children: [
34
+ /* @__PURE__ */ jsx(
35
+ "input",
36
+ {
37
+ ref,
38
+ id: bare ? id : inputId,
39
+ disabled,
40
+ ...rest,
41
+ type: "checkbox",
42
+ role: "switch",
43
+ "aria-describedby": mergeDescribedBy(rest["aria-describedby"], showDescription && descriptionId),
44
+ onChange: (e) => {
45
+ onChange?.(e);
46
+ onCheckedChange?.(e.target.checked);
47
+ },
48
+ className: cn(TRACK, geometry.track, inputClassName)
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn(THUMB, geometry.thumb) })
52
+ ] });
53
+ if (bare) return control;
54
+ const cell = /* @__PURE__ */ jsx("span", { className: "flex h-5 shrink-0 items-center", children: control });
55
+ const words = /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
56
+ label !== void 0 && /* @__PURE__ */ jsx(
57
+ "label",
58
+ {
59
+ htmlFor: inputId,
60
+ className: cn(
61
+ "block text-sm leading-5 text-[var(--text-primary)]",
62
+ disabled ? "cursor-not-allowed" : "cursor-pointer select-none"
63
+ ),
64
+ children: label
65
+ }
66
+ ),
67
+ showDescription && /* @__PURE__ */ jsx("span", { id: descriptionId, className: "mt-0.5 block text-xs text-[var(--text-muted)]", children: description })
68
+ ] });
69
+ return /* @__PURE__ */ jsx("div", { className: cn("flex items-start gap-3", disabled && "opacity-60", className), children: switchPosition === "end" ? /* @__PURE__ */ jsxs(Fragment, { children: [
70
+ words,
71
+ cell
72
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
73
+ cell,
74
+ words
75
+ ] }) });
76
+ });
77
+ Switch.displayName = "Switch";
78
+ export {
79
+ Switch
80
+ };
81
+ //# sourceMappingURL=switch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/switch.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { ChangeEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * An on/off switch that is still `<input type=\"checkbox\">`, with `role=\"switch\"`.\n *\n * Kastlan's was a Radix `<button>`; this keeps the native element for the reasons\n * `Checkbox` gives (form submission, `required`, reset, `<label for>`), and adds the\n * one thing a checkbox is not: `role=\"switch\"`, which screen readers announce as\n * \"on/off\" rather than \"checked/not checked\". Use it for a setting that takes effect\n * the moment it is flipped; a choice that waits for a Save button is a checkbox.\n */\n\n/** Geometry per size. The thumb sits `2px` in from each end of the track, so its\n * checked offset is `track width − thumb − 2px`. `start-*`, never `left-*` or a\n * `translate-x`: an inline-start offset follows the writing direction, so in a\n * right-to-left form the thumb travels right-to-left with no `rtl:` override to\n * keep in step. */\nconst SIZES = {\n // 28×16 track, 12px thumb: the dense setting rows in Kastlan's tables.\n sm: { track: \"h-4 w-7\", thumb: \"size-3 peer-checked:start-[14px]\" },\n // 36×20 track, 16px thumb.\n md: { track: \"h-5 w-9\", thumb: \"size-4 peer-checked:start-[18px]\" },\n} as const;\n\nexport type SwitchSize = keyof typeof SIZES;\n\n// The track IS the input — `appearance-none` and a pill — so its box is the pointer\n// target and the focus ring lands around the whole control, not the thumb alone.\n//\n// Unchecked it takes `--border-strong`, not `--border`: a hairline-coloured track on\n// a surface-coloured card is a switch you have to look for. Checked it takes the\n// brand fill, which is the only difference in COLOUR; the thumb's position is the\n// difference in SHAPE, so the state never rests on colour alone.\nconst TRACK =\n \"peer block shrink-0 cursor-pointer appearance-none rounded-full bg-[var(--border-strong)] transition-colors \" +\n \"checked:bg-[var(--brand)] \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed\";\n\n// The thumb, laid over the input and ignoring the pointer so every click still reaches\n// the input. It swaps to `--brand-contrast` on the brand fill: the surface colour would\n// match the fill in some presets, and the contrast token is the one guaranteed to read\n// against --brand.\nconst THUMB =\n \"pointer-events-none absolute start-0.5 top-1/2 -translate-y-1/2 rounded-full bg-[var(--bg-surface)] shadow-sm \" +\n \"transition-[inset-inline-start,background-color] motion-reduce:transition-none \" +\n \"peer-checked:bg-[var(--brand-contrast)]\";\n\nexport interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\" | \"size\" | \"role\"> {\n /** `\"md\"` (36×20, default) or `\"sm\"` (28×16) — Kastlan's `default` and `sm`. */\n size?: SwitchSize;\n /** The words beside the switch, as a `<label for>` — see {@link Checkbox}'s\n * `label` for why it does not wrap the input. Without it, give the switch an\n * `aria-label`. */\n label?: ReactNode;\n /** Secondary text under the label, attached with `aria-describedby` (merged with\n * any the caller passed). */\n description?: ReactNode;\n /** Which end of the row the switch sits at when there is a label. `\"end\"` (the\n * default) is the settings-list shape Kastlan's profile page uses — the words at\n * the start, the control at the far end; `\"start\"` puts it before the words, like\n * a checkbox. Logical, so it follows the writing direction. */\n switchPosition?: \"start\" | \"end\";\n /** The new state as a boolean — Radix's `onCheckedChange`, so Kastlan's call sites\n * move without unwrapping the event. Fires alongside `onChange`. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>` (the track). `className` styles the outermost element:\n * the row when there is a label, the switch's own wrapper when there is not. */\n inputClassName?: string;\n}\n\n/**\n * A native checkbox drawn as a track and thumb, announced as a switch.\n *\n * Every other prop reaches the `<input>`, so it works controlled (`checked` +\n * `onChange`/`onCheckedChange`), uncontrolled (`defaultChecked`) and in a plain\n * `<form>` (`name`, `value`).\n */\nexport const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(\n {\n size = \"md\",\n label,\n description,\n switchPosition = \"end\",\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const inputId = id ?? generated;\n const descriptionId = `${inputId}-description`;\n const showDescription = hasMessage(description);\n const bare = label === undefined && !showDescription;\n const geometry = SIZES[size];\n\n const control = (\n <span className={cn(\"relative inline-flex shrink-0\", bare && className)}>\n <input\n ref={ref}\n id={bare ? id : inputId}\n disabled={disabled}\n {...rest}\n // After the spread: a caller's props object must not be able to turn this\n // back into a plain checkbox, or into a text field.\n type=\"checkbox\"\n role=\"switch\"\n aria-describedby={mergeDescribedBy(rest[\"aria-describedby\"], showDescription && descriptionId)}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(TRACK, geometry.track, inputClassName)}\n />\n <span aria-hidden className={cn(THUMB, geometry.thumb)} />\n </span>\n );\n\n if (bare) return control;\n\n // The switch's cell is one label line tall (text-sm, 20px) so it centres on the\n // FIRST line of a label that wraps, and a description underneath does not drag it\n // down to the middle of the paragraph.\n const cell = <span className=\"flex h-5 shrink-0 items-center\">{control}</span>;\n const words = (\n <span className=\"min-w-0 flex-1\">\n {label !== undefined && (\n <label\n htmlFor={inputId}\n className={cn(\n \"block text-sm leading-5 text-[var(--text-primary)]\",\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer select-none\",\n )}\n >\n {label}\n </label>\n )}\n {showDescription && (\n <span id={descriptionId} className=\"mt-0.5 block text-xs text-[var(--text-muted)]\">\n {description}\n </span>\n )}\n </span>\n );\n\n return (\n <div className={cn(\"flex items-start gap-3\", disabled && \"opacity-60\", className)}>\n {switchPosition === \"end\" ? (\n <>\n {words}\n {cell}\n </>\n ) : (\n <>\n {cell}\n {words}\n </>\n )}\n </div>\n );\n});\nSwitch.displayName = \"Switch\";\n"],"mappings":";AAyGI,SAmDI,UAlDF,KADF;AAzGJ,SAAS,YAAY,aAAa;AAElC,SAAS,UAAU;AACnB,SAAS,YAAY,wBAAwB;AAiB7C,MAAM,QAAQ;AAAA;AAAA,EAEZ,IAAI,EAAE,OAAO,WAAW,OAAO,mCAAmC;AAAA;AAAA,EAElE,IAAI,EAAE,OAAO,WAAW,OAAO,mCAAmC;AACpE;AAWA,MAAM,QACJ;AASF,MAAM,QACJ;AAkCK,MAAM,SAAS,WAA0C,SAASA,QACvE;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,YAAY,MAAM;AACxB,QAAM,UAAU,MAAM;AACtB,QAAM,gBAAgB,GAAG,OAAO;AAChC,QAAM,kBAAkB,WAAW,WAAW;AAC9C,QAAM,OAAO,UAAU,UAAa,CAAC;AACrC,QAAM,WAAW,MAAM,IAAI;AAE3B,QAAM,UACJ,qBAAC,UAAK,WAAW,GAAG,iCAAiC,QAAQ,SAAS,GACpE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,IAAI,OAAO,KAAK;AAAA,QAChB;AAAA,QACC,GAAG;AAAA,QAGJ,MAAK;AAAA,QACL,MAAK;AAAA,QACL,oBAAkB,iBAAiB,KAAK,kBAAkB,GAAG,mBAAmB,aAAa;AAAA,QAC7F,UAAU,CAAC,MAAqC;AAC9C,qBAAW,CAAC;AACZ,4BAAkB,EAAE,OAAO,OAAO;AAAA,QACpC;AAAA,QACA,WAAW,GAAG,OAAO,SAAS,OAAO,cAAc;AAAA;AAAA,IACrD;AAAA,IACA,oBAAC,UAAK,eAAW,MAAC,WAAW,GAAG,OAAO,SAAS,KAAK,GAAG;AAAA,KAC1D;AAGF,MAAI,KAAM,QAAO;AAKjB,QAAM,OAAO,oBAAC,UAAK,WAAU,kCAAkC,mBAAQ;AACvE,QAAM,QACJ,qBAAC,UAAK,WAAU,kBACb;AAAA,cAAU,UACT;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT,WAAW;AAAA,UACT;AAAA,UACA,WAAW,uBAAuB;AAAA,QACpC;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,IAED,mBACC,oBAAC,UAAK,IAAI,eAAe,WAAU,iDAChC,uBACH;AAAA,KAEJ;AAGF,SACE,oBAAC,SAAI,WAAW,GAAG,0BAA0B,YAAY,cAAc,SAAS,GAC7E,6BAAmB,QAClB,iCACG;AAAA;AAAA,IACA;AAAA,KACH,IAEA,iCACG;AAAA;AAAA,IACA;AAAA,KACH,GAEJ;AAEJ,CAAC;AACD,OAAO,cAAc;","names":["Switch"]}
@@ -0,0 +1,58 @@
1
+ import * as react from 'react';
2
+ import { InputProps } from './ui.js';
3
+
4
+ /**
5
+ * A time of day, as a `"HH:mm"` string (`"HH:mm:ss"` once `step` asks for seconds).
6
+ *
7
+ * NATIVE `<input type="time">`, deliberately, rather than a pair of custom spinners.
8
+ * What the browser's control gives for free is the part a hand-rolled one gets wrong:
9
+ * 12- versus 24-hour display follows the user's OWN locale settings (not the page's —
10
+ * which is the right authority for "how do I read a clock"), a phone opens its
11
+ * platform wheel or dial, each segment is a spin button a screen reader already knows,
12
+ * and typing "1430" fills both segments. The price is that the display cannot be
13
+ * forced to 24h from here and the look of the segments is the browser's; neither is a
14
+ * reason for three apps to ship an unaudited widget. So this is {@link Input} with
15
+ * `type="time"` and a string contract, and everything else — the floating label, the
16
+ * `invalid`/`error` wiring, the click-anywhere `showPicker()` (feedback #224) — is
17
+ * Input's own, inherited rather than copied.
18
+ *
19
+ * Two things it adds over `<Input type="time">`:
20
+ *
21
+ * - **A normalised value.** A time input's value is any valid time string, so a
22
+ * browser may leave off a zero seconds segment with `step={1}` or report
23
+ * milliseconds, and a stored value may carry seconds the field does not show.
24
+ * `onValueChange` always hands back exactly the shape `step` implies, so the value
25
+ * a form stores compares and sorts as a plain string.
26
+ * - **Out-of-range painted, not just flagged.** A native time input outside
27
+ * `min`/`max` matches `:invalid` and nothing styles that selector (see Input's
28
+ * `invalid`), so an out-of-window time looked fine. Here it wears
29
+ * {@link FIELD_INVALID} and `aria-invalid`. A reversed window (`min="22:00"
30
+ * max="06:00"`, a night shift, quiet hours) is read the way the HTML spec reads it:
31
+ * as wrapping past midnight.
32
+ */
33
+ interface TimeInputProps extends Omit<InputProps, "type" | "value" | "defaultValue" | "min" | "max" | "step" | "variant"> {
34
+ /** `"HH:mm"` (or `"HH:mm:ss"` with a sub-minute `step`); `""` is "no time". */
35
+ value: string;
36
+ /** The normalised value on every change the browser reports — which for a time
37
+ * input is only once every segment holds a digit, or when the field is cleared
38
+ * (`""`). Plain `onChange` still fires alongside it with the raw event. */
39
+ onValueChange?: (value: string) => void;
40
+ /** Earliest time, `"HH:mm"`. May be later than `max` for a window that wraps
41
+ * midnight. */
42
+ min?: string;
43
+ /** Latest time, `"HH:mm"`. */
44
+ max?: string;
45
+ /** Granularity in SECONDS, as on the native input: 60 (the default) is minutes,
46
+ * 900 is quarter hours, anything not a whole minute (e.g. 1) shows seconds. */
47
+ step?: number;
48
+ }
49
+ /** Any `HH:mm[:ss[.sss]]` the browser reports → exactly the shape `step` implies.
50
+ * Anything else (including `""`) comes back as it went in. */
51
+ declare function normalizeTime(raw: string, seconds: boolean): string;
52
+ /** Whether `value` lies inside `[min, max]`, wrapping midnight when `min > max`.
53
+ * Zero-padded times compare as strings; both sides are padded to seconds first so
54
+ * "14:30" and "14:30:00" are the same instant. */
55
+ declare function isTimeInRange(value: string, min?: string, max?: string): boolean;
56
+ declare const TimeInput: react.ForwardRefExoticComponent<TimeInputProps & react.RefAttributes<HTMLInputElement>>;
57
+
58
+ export { TimeInput, type TimeInputProps, isTimeInRange, normalizeTime };
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { forwardRef } from "react";
4
+ import { Input } from "./ui.js";
5
+ import { cn } from "../lib/cn.js";
6
+ function withSeconds(step) {
7
+ return step !== void 0 && step % 60 !== 0;
8
+ }
9
+ function normalizeTime(raw, seconds) {
10
+ const m = /^(\d{2}):(\d{2})(?::(\d{2}))?/.exec(raw);
11
+ if (!m) return raw;
12
+ return seconds ? `${m[1]}:${m[2]}:${m[3] ?? "00"}` : `${m[1]}:${m[2]}`;
13
+ }
14
+ function isTimeInRange(value, min, max) {
15
+ if (value === "") return true;
16
+ const v = normalizeTime(value, true);
17
+ const lo = min ? normalizeTime(min, true) : void 0;
18
+ const hi = max ? normalizeTime(max, true) : void 0;
19
+ if (lo !== void 0 && hi !== void 0 && lo > hi) return v >= lo || v <= hi;
20
+ return (lo === void 0 || v >= lo) && (hi === void 0 || v <= hi);
21
+ }
22
+ const TimeInput = forwardRef(function TimeInput2({ value, onValueChange, onChange, min, max, step, invalid, inputClassName, ...rest }, ref) {
23
+ const seconds = withSeconds(step);
24
+ const outOfRange = !isTimeInRange(value, min, max);
25
+ return /* @__PURE__ */ jsx(
26
+ Input,
27
+ {
28
+ ref,
29
+ ...rest,
30
+ type: "time",
31
+ value: normalizeTime(value, seconds),
32
+ min,
33
+ max,
34
+ step,
35
+ invalid: invalid || outOfRange,
36
+ onChange: (e) => {
37
+ onChange?.(e);
38
+ onValueChange?.(normalizeTime(e.target.value, seconds));
39
+ },
40
+ inputClassName: cn("tabular-nums", inputClassName)
41
+ }
42
+ );
43
+ });
44
+ TimeInput.displayName = "TimeInput";
45
+ export {
46
+ TimeInput,
47
+ isTimeInRange,
48
+ normalizeTime
49
+ };
50
+ //# sourceMappingURL=time-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/time-input.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { ChangeEvent } from \"react\";\nimport { Input, type InputProps } from \"./ui\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * A time of day, as a `\"HH:mm\"` string (`\"HH:mm:ss\"` once `step` asks for seconds).\n *\n * NATIVE `<input type=\"time\">`, deliberately, rather than a pair of custom spinners.\n * What the browser's control gives for free is the part a hand-rolled one gets wrong:\n * 12- versus 24-hour display follows the user's OWN locale settings (not the page's —\n * which is the right authority for \"how do I read a clock\"), a phone opens its\n * platform wheel or dial, each segment is a spin button a screen reader already knows,\n * and typing \"1430\" fills both segments. The price is that the display cannot be\n * forced to 24h from here and the look of the segments is the browser's; neither is a\n * reason for three apps to ship an unaudited widget. So this is {@link Input} with\n * `type=\"time\"` and a string contract, and everything else — the floating label, the\n * `invalid`/`error` wiring, the click-anywhere `showPicker()` (feedback #224) — is\n * Input's own, inherited rather than copied.\n *\n * Two things it adds over `<Input type=\"time\">`:\n *\n * - **A normalised value.** A time input's value is any valid time string, so a\n * browser may leave off a zero seconds segment with `step={1}` or report\n * milliseconds, and a stored value may carry seconds the field does not show.\n * `onValueChange` always hands back exactly the shape `step` implies, so the value\n * a form stores compares and sorts as a plain string.\n * - **Out-of-range painted, not just flagged.** A native time input outside\n * `min`/`max` matches `:invalid` and nothing styles that selector (see Input's\n * `invalid`), so an out-of-window time looked fine. Here it wears\n * {@link FIELD_INVALID} and `aria-invalid`. A reversed window (`min=\"22:00\"\n * max=\"06:00\"`, a night shift, quiet hours) is read the way the HTML spec reads it:\n * as wrapping past midnight.\n */\nexport interface TimeInputProps\n extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\" | \"min\" | \"max\" | \"step\" | \"variant\"> {\n /** `\"HH:mm\"` (or `\"HH:mm:ss\"` with a sub-minute `step`); `\"\"` is \"no time\". */\n value: string;\n /** The normalised value on every change the browser reports — which for a time\n * input is only once every segment holds a digit, or when the field is cleared\n * (`\"\"`). Plain `onChange` still fires alongside it with the raw event. */\n onValueChange?: (value: string) => void;\n /** Earliest time, `\"HH:mm\"`. May be later than `max` for a window that wraps\n * midnight. */\n min?: string;\n /** Latest time, `\"HH:mm\"`. */\n max?: string;\n /** Granularity in SECONDS, as on the native input: 60 (the default) is minutes,\n * 900 is quarter hours, anything not a whole minute (e.g. 1) shows seconds. */\n step?: number;\n}\n\n/** Seconds shown when `step` is not a whole number of minutes — the browser's own rule\n * for whether the seconds segment appears. */\nfunction withSeconds(step: number | undefined): boolean {\n return step !== undefined && step % 60 !== 0;\n}\n\n/** Any `HH:mm[:ss[.sss]]` the browser reports → exactly the shape `step` implies.\n * Anything else (including `\"\"`) comes back as it went in. */\nexport function normalizeTime(raw: string, seconds: boolean): string {\n const m = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?/.exec(raw);\n if (!m) return raw;\n return seconds ? `${m[1]}:${m[2]}:${m[3] ?? \"00\"}` : `${m[1]}:${m[2]}`;\n}\n\n/** Whether `value` lies inside `[min, max]`, wrapping midnight when `min > max`.\n * Zero-padded times compare as strings; both sides are padded to seconds first so\n * \"14:30\" and \"14:30:00\" are the same instant. */\nexport function isTimeInRange(value: string, min?: string, max?: string): boolean {\n if (value === \"\") return true;\n const v = normalizeTime(value, true);\n const lo = min ? normalizeTime(min, true) : undefined;\n const hi = max ? normalizeTime(max, true) : undefined;\n if (lo !== undefined && hi !== undefined && lo > hi) return v >= lo || v <= hi;\n return (lo === undefined || v >= lo) && (hi === undefined || v <= hi);\n}\n\nexport const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>(function TimeInput(\n { value, onValueChange, onChange, min, max, step, invalid, inputClassName, ...rest },\n ref,\n) {\n const seconds = withSeconds(step);\n const outOfRange = !isTimeInRange(value, min, max);\n return (\n <Input\n ref={ref}\n {...rest}\n type=\"time\"\n // Normalised on the way IN as well: a stored \"14:30:00\" handed to a minutes-only\n // field would otherwise make the browser show a seconds segment nobody asked for.\n value={normalizeTime(value, seconds)}\n min={min}\n max={max}\n step={step}\n invalid={invalid || outOfRange}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n onValueChange?.(normalizeTime(e.target.value, seconds));\n }}\n // The segments are digits that change under the caret as you spin them;\n // proportional figures make the whole field jitter sideways on every step.\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n );\n});\nTimeInput.displayName = \"TimeInput\";\n"],"mappings":";AAqFI;AArFJ,SAAS,kBAAkB;AAE3B,SAAS,aAA8B;AACvC,SAAS,UAAU;AAmDnB,SAAS,YAAY,MAAmC;AACtD,SAAO,SAAS,UAAa,OAAO,OAAO;AAC7C;AAIO,SAAS,cAAc,KAAa,SAA0B;AACnE,QAAM,IAAI,gCAAgC,KAAK,GAAG;AAClD,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,UAAU,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,KAAK,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACtE;AAKO,SAAS,cAAc,OAAe,KAAc,KAAuB;AAChF,MAAI,UAAU,GAAI,QAAO;AACzB,QAAM,IAAI,cAAc,OAAO,IAAI;AACnC,QAAM,KAAK,MAAM,cAAc,KAAK,IAAI,IAAI;AAC5C,QAAM,KAAK,MAAM,cAAc,KAAK,IAAI,IAAI;AAC5C,MAAI,OAAO,UAAa,OAAO,UAAa,KAAK,GAAI,QAAO,KAAK,MAAM,KAAK;AAC5E,UAAQ,OAAO,UAAa,KAAK,QAAQ,OAAO,UAAa,KAAK;AACpE;AAEO,MAAM,YAAY,WAA6C,SAASA,WAC7E,EAAE,OAAO,eAAe,UAAU,KAAK,KAAK,MAAM,SAAS,gBAAgB,GAAG,KAAK,GACnF,KACA;AACA,QAAM,UAAU,YAAY,IAAI;AAChC,QAAM,aAAa,CAAC,cAAc,OAAO,KAAK,GAAG;AACjD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ,MAAK;AAAA,MAGL,OAAO,cAAc,OAAO,OAAO;AAAA,MACnC;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,WAAW;AAAA,MACpB,UAAU,CAAC,MAAqC;AAC9C,mBAAW,CAAC;AACZ,wBAAgB,cAAc,EAAE,OAAO,OAAO,OAAO,CAAC;AAAA,MACxD;AAAA,MAGA,gBAAgB,GAAG,gBAAgB,cAAc;AAAA;AAAA,EACnD;AAEJ,CAAC;AACD,UAAU,cAAc;","names":["TimeInput"]}
@@ -1,11 +1,21 @@
1
1
  import * as react from 'react';
2
+ import { ComponentPropsWithoutRef } from 'react';
2
3
 
3
4
  interface ToggleOption<T extends string> {
4
5
  value: T;
5
6
  label: string;
6
7
  className?: string;
7
8
  }
8
- interface ToggleGroupProps<T extends string> {
9
+ /**
10
+ * `onChange` is the group's own — the chosen VALUE, not a DOM event — so the div's
11
+ * `onChange` is omitted rather than shadowed: leaving both in scope would give the
12
+ * prop two incompatible meanings depending on which overload TypeScript picked.
13
+ *
14
+ * `children` is omitted too: this renders its `options` and nothing else, so a `children` the type
15
+ * accepted and the component ignored would be a prop that silently does nothing —
16
+ * worse than one that does not compile.
17
+ */
18
+ interface ToggleGroupProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
9
19
  value: T;
10
20
  onChange: (value: T) => void;
11
21
  options: ToggleOption<T>[];
@@ -13,6 +23,11 @@ interface ToggleGroupProps<T extends string> {
13
23
  /** Applied to every option button (e.g. to tune height/rounding to match
14
24
  * adjacent fields). Per-option `className` still wins over this. */
15
25
  optionClassName?: string;
26
+ /**
27
+ * @deprecated Pass `aria-label` instead — the DOM spelling, which every other
28
+ * control in this kit now answers to. Kept working because three applications ship
29
+ * this one today; it names the group only when `aria-label` is absent.
30
+ */
16
31
  ariaLabel?: string;
17
32
  /**
18
33
  * Show, refuse the change (Keksdose live #288: a payment dated in the future has no
@@ -29,6 +44,6 @@ interface ToggleGroupProps<T extends string> {
29
44
  */
30
45
  disabled?: boolean;
31
46
  }
32
- declare function ToggleGroup<T extends string>({ value, onChange, options, className, optionClassName, ariaLabel, disabled, }: ToggleGroupProps<T>): react.JSX.Element;
47
+ declare function ToggleGroup<T extends string>({ value, onChange, options, className, optionClassName, ariaLabel, disabled, "aria-label": ariaLabelAttr, ...rest }: ToggleGroupProps<T>): react.JSX.Element;
33
48
 
34
- export { ToggleGroup };
49
+ export { ToggleGroup, type ToggleGroupProps, type ToggleOption };
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx } from "react/jsx-runtime";
2
3
  import { cn } from "../lib/cn.js";
3
4
  function ToggleGroup({
@@ -7,13 +8,16 @@ function ToggleGroup({
7
8
  className,
8
9
  optionClassName,
9
10
  ariaLabel,
10
- disabled = false
11
+ disabled = false,
12
+ "aria-label": ariaLabelAttr,
13
+ ...rest
11
14
  }) {
12
15
  return /* @__PURE__ */ jsx(
13
16
  "div",
14
17
  {
18
+ ...rest,
15
19
  role: "radiogroup",
16
- "aria-label": ariaLabel,
20
+ "aria-label": ariaLabelAttr ?? ariaLabel,
17
21
  "aria-disabled": disabled || void 0,
18
22
  className: cn(
19
23
  // `gap-0.5` — the same 2px as the container's own padding, so EVERY segment
@@ -28,7 +32,7 @@ function ToggleGroup({
28
32
  // (The hover fill is the DESKTOP half of that report: Tailwind v4 wraps every
29
33
  // `hover:` in `@media (hover: hover)`, so a phone never paints it. The half a
30
34
  // phone does see is the focus ring — see the segment's own note below.)
31
- "inline-flex w-full gap-0.5 rounded-md border border-slate-300 bg-white p-0.5 shadow-sm dark:border-slate-700 dark:bg-slate-900",
35
+ "inline-flex w-full gap-0.5 rounded-md border border-[var(--border-strong)] bg-[var(--bg-surface)] p-0.5 shadow-sm",
32
36
  // The whole group fades, the way every other disabled control in this
33
37
  // package does; `cursor-not-allowed` is on the buttons, which is what a
34
38
  // pointer is actually over.
@@ -62,14 +66,14 @@ function ToggleGroup({
62
66
  //
63
67
  // `focus-visible` + `ring-inset`, not `focus` + an outset ring. A ring
64
68
  // is a box-shadow that spreads OUTWARD, so on a flush group it painted
65
- // 2px of slate over both neighbours and over the container's own border
69
+ // 2px of ring over both neighbours and over the container's own border
66
70
  // — and on a phone it appeared on every TAP, because a tap focuses the
67
71
  // button. That is the other half of what live #268's rework saw
68
72
  // overlaying the selected segment's boundary. Inset keeps the ring
69
73
  // inside the segment it belongs to; focus-visible keeps it for the
70
74
  // keyboard, which is the only input that needs it.
71
- "min-w-0 flex-1 basis-auto truncate rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-slate-400",
72
- active ? "bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900" : "text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800",
75
+ "min-w-0 flex-1 basis-auto truncate rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--border-strong)]",
76
+ active ? "bg-[var(--bg-inverse)] text-[var(--text-inverse)]" : "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
73
77
  // No hover fill on a group that cannot be changed — a segment that
74
78
  // lights up under the pointer is an offer, and there is none here.
75
79
  disabled && "cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/toggle-group.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\n\ninterface ToggleOption<T extends string> {\n value: T;\n label: string;\n className?: string;\n}\n\ninterface ToggleGroupProps<T extends string> {\n value: T;\n onChange: (value: T) => void;\n options: ToggleOption<T>[];\n className?: string;\n /** Applied to every option button (e.g. to tune height/rounding to match\n * adjacent fields). Per-option `className` still wins over this. */\n optionClassName?: string;\n ariaLabel?: string;\n /**\n * Show, refuse the change (Keksdose live #288: a payment dated in the future has no\n * state to set).\n *\n * Whatever `value` says stays pressed and keeps its own fill rather than going grey\n * with the rest — a reader who cannot see WHICH option is chosen has been told less\n * than before it was disabled. A caller with nothing to show passes no value, and\n * the group renders dimmed with nothing pressed, which is the shape Keksdose's\n * status picker uses for a row whose status does not exist yet.\n *\n * On the whole GROUP, not per option: a segmented control where some segments are\n * live and others are not is a menu with holes in it, and no caller here wants one.\n */\n disabled?: boolean;\n}\n\nexport function ToggleGroup<T extends string>({\n value,\n onChange,\n options,\n className,\n optionClassName,\n ariaLabel,\n disabled = false,\n}: ToggleGroupProps<T>) {\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n // `aria-disabled` on the group as well as `disabled` on each button: a radio\n // group is what the user is being refused, and a screen reader announcing\n // three separately-disabled radios does not say that.\n aria-disabled={disabled || undefined}\n className={cn(\n // `gap-0.5` — the same 2px as the container's own padding, so EVERY segment\n // sits in a uniform 2px moat and no two fills ever touch. Flush segments were\n // Keksdose live #268's rework: the pressed segment wears a saturated fill and\n // an unpressed neighbour wears a pale hover fill, and with a shared edge the\n // two rectangles read as one smeared shape — *\"the boundary of the selected\n // option and hovering next to it overlays the boundary of the selected\n // button\"*. A gap is what makes each segment its own chip; it cannot be\n // undone by a caller's per-option colour, which a hover-only fix could.\n //\n // (The hover fill is the DESKTOP half of that report: Tailwind v4 wraps every\n // `hover:` in `@media (hover: hover)`, so a phone never paints it. The half a\n // phone does see is the focus ring — see the segment's own note below.)\n \"inline-flex w-full gap-0.5 rounded-md border border-slate-300 bg-white p-0.5 shadow-sm dark:border-slate-700 dark:bg-slate-900\",\n // The whole group fades, the way every other disabled control in this\n // package does; `cursor-not-allowed` is on the buttons, which is what a\n // pointer is actually over.\n disabled && \"opacity-60\",\n className,\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n disabled={disabled}\n onClick={() => onChange(opt.value)}\n className={cn(\n // `truncate` (which carries whitespace-nowrap) rather than letting a\n // label wrap: a segmented control sizes its whole row to the tallest\n // option, so one two-word option — Keksdose feedback #147's \"Where I\n // am\" — silently doubles the height of every segment beside it.\n //\n // `basis-auto` is what keeps that ellipsis a LAST resort rather than\n // the normal state (Keksdose dev#475). With flex-1's `basis-0`, a\n // shrink-to-fit group (`w-auto`) still resolves to the sum of the\n // labels' widths — and then splits it EQUALLY, so the short option\n // got 66px it did not need and \"Where I am\" got 66 of the 81 it did:\n // truncated at 1778px of free screen. Basing each segment on its own\n // content and sharing only the LEFTOVER space keeps a full-width\n // group's segments near-equal and an auto-width group's exact.\n //\n // `focus-visible` + `ring-inset`, not `focus` + an outset ring. A ring\n // is a box-shadow that spreads OUTWARD, so on a flush group it painted\n // 2px of slate over both neighbours and over the container's own border\n // — and on a phone it appeared on every TAP, because a tap focuses the\n // button. That is the other half of what live #268's rework saw\n // overlaying the selected segment's boundary. Inset keeps the ring\n // inside the segment it belongs to; focus-visible keeps it for the\n // keyboard, which is the only input that needs it.\n \"min-w-0 flex-1 basis-auto truncate rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-slate-400\",\n active\n ? \"bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900\"\n : \"text-slate-700 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800\",\n // No hover fill on a group that cannot be changed — a segment that\n // lights up under the pointer is an offer, and there is none here.\n disabled && \"cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent\",\n optionClassName,\n opt.className,\n )}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":"AA0EU;AA1EV,SAAS,UAAU;AAiCZ,SAAS,YAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAAwB;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MAIZ,iBAAe,YAAY;AAAA,MAC3B,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAaT;AAAA;AAAA;AAAA;AAAA,QAIA,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,MAEC,kBAAQ,IAAI,CAAC,QAAQ;AACpB,cAAM,SAAS,IAAI,UAAU;AAC7B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd;AAAA,YACA,SAAS,MAAM,SAAS,IAAI,KAAK;AAAA,YACjC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAuBT;AAAA,cACA,SACI,kEACA;AAAA;AAAA;AAAA,cAGJ,YAAY;AAAA,cACZ;AAAA,cACA,IAAI;AAAA,YACN;AAAA,YAEC,cAAI;AAAA;AAAA,UAxCA,IAAI;AAAA,QAyCX;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/toggle-group.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"../lib/cn\";\n\nexport interface ToggleOption<T extends string> {\n value: T;\n label: string;\n className?: string;\n}\n\n/**\n * `onChange` is the group's own — the chosen VALUE, not a DOM event — so the div's\n * `onChange` is omitted rather than shadowed: leaving both in scope would give the\n * prop two incompatible meanings depending on which overload TypeScript picked.\n *\n * `children` is omitted too: this renders its `options` and nothing else, so a `children` the type\n * accepted and the component ignored would be a prop that silently does nothing —\n * worse than one that does not compile.\n */\nexport interface ToggleGroupProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\"> {\n value: T;\n onChange: (value: T) => void;\n options: ToggleOption<T>[];\n className?: string;\n /** Applied to every option button (e.g. to tune height/rounding to match\n * adjacent fields). Per-option `className` still wins over this. */\n optionClassName?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling, which every other\n * control in this kit now answers to. Kept working because three applications ship\n * this one today; it names the group only when `aria-label` is absent.\n */\n ariaLabel?: string;\n /**\n * Show, refuse the change (Keksdose live #288: a payment dated in the future has no\n * state to set).\n *\n * Whatever `value` says stays pressed and keeps its own fill rather than going grey\n * with the rest — a reader who cannot see WHICH option is chosen has been told less\n * than before it was disabled. A caller with nothing to show passes no value, and\n * the group renders dimmed with nothing pressed, which is the shape Keksdose's\n * status picker uses for a row whose status does not exist yet.\n *\n * On the whole GROUP, not per option: a segmented control where some segments are\n * live and others are not is a menu with holes in it, and no caller here wants one.\n */\n disabled?: boolean;\n}\n\nexport function ToggleGroup<T extends string>({\n value,\n onChange,\n options,\n className,\n optionClassName,\n ariaLabel,\n disabled = false,\n \"aria-label\": ariaLabelAttr,\n ...rest\n}: ToggleGroupProps<T>) {\n return (\n <div\n // The audit's named example of a closed prop list (§\"Public API design\"): the\n // tour locates a step by CSS SELECTOR, so a component that drops every attribute\n // it was not expecting cannot be spotlighted at all — and Keksdose's rule editor\n // carries a comment explaining that it wraps this group in a bare <div> for\n // exactly that reason.\n //\n // `...rest` first, then the attributes the group cannot do without: a caller\n // hanging an anchor or a test id on the group must not be able to overwrite the\n // radiogroup role or the disabled state by accident. `className` is destructured\n // out entirely and merged through `cn`, so it is never in here.\n {...rest}\n role=\"radiogroup\"\n // The DOM spelling wins; `ariaLabel` is the fallback for the call sites that\n // have not moved yet.\n aria-label={ariaLabelAttr ?? ariaLabel}\n // `aria-disabled` on the group as well as `disabled` on each button: a radio\n // group is what the user is being refused, and a screen reader announcing\n // three separately-disabled radios does not say that.\n aria-disabled={disabled || undefined}\n className={cn(\n // `gap-0.5` — the same 2px as the container's own padding, so EVERY segment\n // sits in a uniform 2px moat and no two fills ever touch. Flush segments were\n // Keksdose live #268's rework: the pressed segment wears a saturated fill and\n // an unpressed neighbour wears a pale hover fill, and with a shared edge the\n // two rectangles read as one smeared shape — *\"the boundary of the selected\n // option and hovering next to it overlays the boundary of the selected\n // button\"*. A gap is what makes each segment its own chip; it cannot be\n // undone by a caller's per-option colour, which a hover-only fix could.\n //\n // (The hover fill is the DESKTOP half of that report: Tailwind v4 wraps every\n // `hover:` in `@media (hover: hover)`, so a phone never paints it. The half a\n // phone does see is the focus ring — see the segment's own note below.)\n \"inline-flex w-full gap-0.5 rounded-md border border-[var(--border-strong)] bg-[var(--bg-surface)] p-0.5 shadow-sm\",\n // The whole group fades, the way every other disabled control in this\n // package does; `cursor-not-allowed` is on the buttons, which is what a\n // pointer is actually over.\n disabled && \"opacity-60\",\n className,\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n disabled={disabled}\n onClick={() => onChange(opt.value)}\n className={cn(\n // `truncate` (which carries whitespace-nowrap) rather than letting a\n // label wrap: a segmented control sizes its whole row to the tallest\n // option, so one two-word option — Keksdose feedback #147's \"Where I\n // am\" — silently doubles the height of every segment beside it.\n //\n // `basis-auto` is what keeps that ellipsis a LAST resort rather than\n // the normal state (Keksdose dev#475). With flex-1's `basis-0`, a\n // shrink-to-fit group (`w-auto`) still resolves to the sum of the\n // labels' widths — and then splits it EQUALLY, so the short option\n // got 66px it did not need and \"Where I am\" got 66 of the 81 it did:\n // truncated at 1778px of free screen. Basing each segment on its own\n // content and sharing only the LEFTOVER space keeps a full-width\n // group's segments near-equal and an auto-width group's exact.\n //\n // `focus-visible` + `ring-inset`, not `focus` + an outset ring. A ring\n // is a box-shadow that spreads OUTWARD, so on a flush group it painted\n // 2px of ring over both neighbours and over the container's own border\n // — and on a phone it appeared on every TAP, because a tap focuses the\n // button. That is the other half of what live #268's rework saw\n // overlaying the selected segment's boundary. Inset keeps the ring\n // inside the segment it belongs to; focus-visible keeps it for the\n // keyboard, which is the only input that needs it.\n \"min-w-0 flex-1 basis-auto truncate rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--border-strong)]\",\n active\n ? \"bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\",\n // No hover fill on a group that cannot be changed — a segment that\n // lights up under the pointer is an offer, and there is none here.\n disabled && \"cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent\",\n optionClassName,\n opt.className,\n )}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AAyGU;AAxGV,SAAS,UAAU;AAgDZ,SAAS,YAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,cAAc;AAAA,EACd,GAAG;AACL,GAAwB;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MAWE,GAAG;AAAA,MACJ,MAAK;AAAA,MAGL,cAAY,iBAAiB;AAAA,MAI7B,iBAAe,YAAY;AAAA,MAC3B,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAaT;AAAA;AAAA;AAAA;AAAA,QAIA,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,MAEC,kBAAQ,IAAI,CAAC,QAAQ;AACpB,cAAM,SAAS,IAAI,UAAU;AAC7B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd;AAAA,YACA,SAAS,MAAM,SAAS,IAAI,KAAK;AAAA,YACjC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAuBT;AAAA,cACA,SACI,sDACA;AAAA;AAAA;AAAA,cAGJ,YAAY;AAAA,cACZ;AAAA,cACA,IAAI;AAAA,YACN;AAAA,YAEC,cAAI;AAAA;AAAA,UAxCA,IAAI;AAAA,QAyCX;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -0,0 +1,115 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { SeriesChartLabels } from './series-chart-labels.js';
4
+
5
+ /**
6
+ * The strokes a reader can tell apart, in the order they are handed out.
7
+ *
8
+ * For the charts where the colour is already spoken for: a comparison draws each
9
+ * measurement in its own colour, so a chart carrying several CHANNELS of each has
10
+ * nothing left but the stroke — and "solid versus dashed" only gets you two.
11
+ *
12
+ * **Five, and the fifth is the last one.** They are the draughtsman's line types, in
13
+ * the draughtsman's order — continuous, dashed, dotted, dash-dot, dash-dot-dot — the
14
+ * one family of five a century of drawings has already proved legible at a hair's
15
+ * width. A sixth is not distinguishable from one of these at two pixels wide, so a
16
+ * chart needing one is a chart that should be split.
17
+ *
18
+ * Kept here, beside the legend's own mark, and read by `SeriesChart` from here — so
19
+ * the key under a chart and the lines on it cannot disagree.
20
+ */
21
+ declare const STROKE_PATTERNS: readonly (string | undefined)[];
22
+ /** The dash array for the nth pattern, wrapping past the fifth. */
23
+ declare function strokeDash(order: number): string | undefined;
24
+ /** Which of the patterns a `step` series is drawn in.
25
+ *
26
+ * A whole-number channel that jumps rather than travels is drawn dashed, and it has
27
+ * to be one of {@link STROKE_PATTERNS} rather than a dash array written out at the
28
+ * call site: a legend entry says its pattern by index, so a step line drawn in
29
+ * anything else would be a legend promising a solid line for a dashed one. */
30
+ declare const STEP_DASH = 1;
31
+ /**
32
+ * One key switched, as a new hidden set.
33
+ *
34
+ * Every caller writes `onToggle={(key) => setHidden(toggleHidden(hidden, key))}`. A
35
+ * new set rather than a mutated one, because the state is read during render and a
36
+ * mutation of something a component already holds is invisible to React.
37
+ */
38
+ declare function toggleHidden(hidden: ReadonlySet<string>, key: string): ReadonlySet<string>;
39
+ interface LegendEntry {
40
+ key: string;
41
+ label: ReactNode;
42
+ /** Any CSS colour — `paletteFor(i)` for the kit's own ramp. */
43
+ color: string;
44
+ /**
45
+ * What the entry's mark looks like. `swatch` — a filled square, the default — says
46
+ * WHICH MEASUREMENT. `stroke` says WHICH QUANTITY, for the charts that carry both at
47
+ * once and tell them apart by colour and by dash.
48
+ */
49
+ marker?: "swatch" | "stroke";
50
+ /** Which of {@link STROKE_PATTERNS}, when the marker is a stroke — the same index
51
+ * `SeriesChartSeries.dash` takes, so a legend cannot promise a dot-dash the plot
52
+ * draws dashed. */
53
+ dash?: number;
54
+ }
55
+ interface ToggleLegendProps {
56
+ entries: LegendEntry[];
57
+ /** The keys currently off the chart. */
58
+ hidden: ReadonlySet<string>;
59
+ onToggle: (key: string) => void;
60
+ /** `vertical` is for a legend standing BESIDE the charts rather than under them —
61
+ * what a row of charts sharing one legend wants, since under two charts there is
62
+ * no "under", and putting it under one says it belongs to that one. */
63
+ orientation?: "horizontal" | "vertical";
64
+ /**
65
+ * Draw a legend of ONE entry rather than nothing.
66
+ *
67
+ * A single line's legend is usually noise. It is not where the legend is the only
68
+ * SWITCH that line has — a series that starts hidden and has no entry to bring it
69
+ * back is gone for good.
70
+ */
71
+ showSingle?: boolean;
72
+ className?: string;
73
+ /** Per-instance strings over `<UiKitProvider labels={{ seriesChart }}>`. */
74
+ labels?: Partial<SeriesChartLabels>;
75
+ }
76
+ /**
77
+ * A legend whose entries are switches.
78
+ *
79
+ * `ChartLegendContent` with `onItemClick`/`activeKey` is the other kind: clicking an
80
+ * entry HIGHLIGHTS it and dims the rest, which is right when every series belongs on
81
+ * the chart and one of them is momentarily interesting. This is for the opposite —
82
+ * three coordinates on one axis where y runs to 800 and z barely moves, and the only
83
+ * way to see z is to take y off the chart entirely. Any number may be off at once.
84
+ *
85
+ * A hidden entry keeps its mark and its place. Removing it would reflow the legend on
86
+ * every click and lose the one thing it is for: saying what COULD be shown.
87
+ *
88
+ * Each entry is a real `<button>` with `aria-pressed` — pressed means "on the chart" —
89
+ * so Tab reaches every switch and Space/Enter flips it.
90
+ */
91
+ declare function ToggleLegend({ entries, hidden, onToggle, orientation, showSingle, className, labels: labelsProp, }: ToggleLegendProps): react.JSX.Element | null;
92
+ /**
93
+ * The column a vertical legend stands in: beside the charts, on their horizontal
94
+ * centre line. `h-full` + `justify-center`, so it centres in a grid cell or a flex row
95
+ * that stretches it — and stacks whatever it is given, which is what a panel showing
96
+ * two legends at once needs. The width is the caller's: how wide a legend column is
97
+ * is a fact about that screen's layout.
98
+ */
99
+ declare function LegendColumn({ children, className }: {
100
+ children: ReactNode;
101
+ className?: string;
102
+ }): react.JSX.Element;
103
+ /**
104
+ * A legend and what it is a legend OF, as a headed column.
105
+ *
106
+ * For legends that are several vocabularies at once — which measurement and which
107
+ * channel — so the entries are read down a group and the groups across. Wrapped rows
108
+ * would put the second half of one group on a line under the first half of the next.
109
+ */
110
+ declare function LegendGroup({ title, children }: {
111
+ title: ReactNode;
112
+ children: ReactNode;
113
+ }): react.JSX.Element;
114
+
115
+ export { LegendColumn, type LegendEntry, LegendGroup, STEP_DASH, STROKE_PATTERNS, ToggleLegend, type ToggleLegendProps, strokeDash, toggleHidden };