@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,363 @@
1
+ // shadcn/ui chart kit — vendored (MIT) and adapted for this app:
2
+ // - Recharts 3 instead of 2
3
+ // - the original keys its styling off shadcn theme tokens (--muted-foreground,
4
+ // --border, bg-background …). This app has no shadcn token layer, so those
5
+ // are mapped onto this kit's own tokens (--text-muted, --border, --bg-surface
6
+ // …), which follow the active palette preset and flip with the theme by
7
+ // themselves — hence no `dark:` variants anywhere below.
8
+ // The value on show is the *structure*: config-driven colors injected as CSS
9
+ // vars, muted axis/grid styling, and a polished tooltip/legend — the things
10
+ // that make shadcn charts read as "designed" rather than "default Recharts".
11
+ import { createContext, useContext, useId, useRef } from "react";
12
+ import type { ComponentProps, CSSProperties, ReactNode, RefObject } from "react";
13
+ import {
14
+ Legend as RechartsLegend,
15
+ ResponsiveContainer,
16
+ Tooltip as RechartsTooltip,
17
+ } from "recharts";
18
+ import { cn } from "../lib/cn";
19
+
20
+ export interface ChartSeriesConfig {
21
+ label: ReactNode;
22
+ color: string;
23
+ }
24
+
25
+ export type ChartConfig = Record<string, ChartSeriesConfig>;
26
+
27
+ const ChartContext = createContext<ChartConfig | null>(null);
28
+
29
+ export function useChart(): ChartConfig {
30
+ const ctx = useContext(ChartContext);
31
+ if (!ctx) throw new Error("useChart must be used within a <ChartContainer />");
32
+ return ctx;
33
+ }
34
+
35
+ interface ChartContainerProps extends ComponentProps<"div"> {
36
+ config: ChartConfig;
37
+ children: ComponentProps<typeof ResponsiveContainer>["children"];
38
+ }
39
+
40
+ export function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {
41
+ const uid = useId();
42
+ // The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be
43
+ // a CSS identifier: one stray space or `#` and the browser discards the whole rule,
44
+ // taking every series colour with it — silently, because an invalid rule is not an
45
+ // error anywhere. `useId()` gives `:r0:`, and a consumer-supplied id gives whatever
46
+ // they typed, so both are filtered rather than just the colons.
47
+ const chartId = `chart-${(id ?? uid).replace(/[^A-Za-z0-9_-]/g, "")}`;
48
+ return (
49
+ <ChartContext.Provider value={config}>
50
+ <div
51
+ data-chart={chartId}
52
+ className={cn(
53
+ // touch-pan-y lets a vertical finger drag still scroll the page while
54
+ // a horizontal drag is handed to recharts for tooltip scrubbing,
55
+ // instead of the page scrolling underneath the gesture (feedback #292).
56
+ "flex w-full touch-pan-y justify-center text-xs",
57
+ // muted axis labels + gridlines; one class each, since the tokens
58
+ // already carry the light/dark values
59
+ "[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]",
60
+ "[&_.recharts-cartesian-grid_line]:stroke-[var(--border)]/70",
61
+ // hairline hover cursor instead of the heavy default
62
+ "[&_.recharts-curve.recharts-tooltip-cursor]:stroke-[var(--border-strong)]",
63
+ "[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-[var(--bg-hover)]",
64
+ // kill the focus outlines & white sector strokes Recharts adds
65
+ "[&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent",
66
+ "[&_.recharts-layer]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-dot[stroke='#fff']]:stroke-transparent",
67
+ className,
68
+ )}
69
+ {...props}
70
+ >
71
+ <ChartStyle id={chartId} config={config} />
72
+ <ResponsiveContainer width="100%" height="100%">
73
+ {children}
74
+ </ResponsiveContainer>
75
+ </div>
76
+ </ChartContext.Provider>
77
+ );
78
+ }
79
+
80
+ /**
81
+ * A config key, which becomes the tail of a custom-property NAME.
82
+ *
83
+ * `dangerouslySetInnerHTML` means what it says: nothing below is escaped by React, and
84
+ * a `ChartConfig` is not necessarily written by hand — kastlan builds one from category
85
+ * rows in its database, so these keys are user data. A key containing `}` closes this
86
+ * rule, and whatever follows it in that key becomes a NEW rule — one that applies to
87
+ * the whole document rather than to this one chart.
88
+ */
89
+ const CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
90
+
91
+ /**
92
+ * A config colour, which becomes a declaration VALUE.
93
+ *
94
+ * Every form a palette actually produces: both hex spellings, the functional notations
95
+ * — with the argument list restricted to the characters those functions can legally
96
+ * contain, so no `;`, no `}` and no nested `url(` — and `var(--token)`, which is what
97
+ * this kit's own `paletteFor`/`CHART_COLORS` return. `var()`'s optional FALLBACK is
98
+ * deliberately not accepted: its second argument is an arbitrary value, i.e. the same
99
+ * hole in a shape that looks safe.
100
+ */
101
+ const CSS_COLOR =
102
+ /^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\([0-9A-Za-z.,%/ -]*\)|var\(--[A-Za-z0-9_-]+\))$/;
103
+
104
+ // Inject each series colour as a `--color-<key>` custom property scoped to this
105
+ // chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip
106
+ // stay in sync from one source of truth.
107
+ function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
108
+ const entries = Object.entries(config).filter(([key, c]) => {
109
+ if (!c.color) return false;
110
+ // Skipping the entry costs that one series its `var(--color-…)` and leaves the rest
111
+ // of the chart painted; emitting it unchecked costs the page. The warning is so the
112
+ // consumer finds out from their console rather than from a colourless bar.
113
+ if (!CSS_IDENT.test(key)) {
114
+ console.warn(`[ui-kit] chart config key is not a CSS identifier, skipped: ${key}`);
115
+ return false;
116
+ }
117
+ if (!CSS_COLOR.test(c.color)) {
118
+ console.warn(`[ui-kit] chart config colour for "${key}" is not a colour, skipped: ${c.color}`);
119
+ return false;
120
+ }
121
+ return true;
122
+ });
123
+ if (!entries.length) return null;
124
+ const css = `[data-chart=${id}] {\n${entries
125
+ .map(([key, c]) => ` --color-${key}: ${c.color};`)
126
+ .join("\n")}\n}`;
127
+ return <style dangerouslySetInnerHTML={{ __html: css }} />;
128
+ }
129
+
130
+ export const ChartTooltip = RechartsTooltip;
131
+ export const ChartLegend = RechartsLegend;
132
+
133
+ interface TooltipPayloadItem {
134
+ dataKey?: string | number;
135
+ name?: string | number;
136
+ value?: number | string;
137
+ color?: string;
138
+ payload?: Record<string, unknown>;
139
+ }
140
+
141
+ /**
142
+ * What a series has no value for at this point: a gap in a history, a projection that
143
+ * has not happened yet, a field that did not exist at an earlier capture.
144
+ *
145
+ * An em dash rather than an empty cell, so the row still lines up with its siblings and
146
+ * the reader can see that the series WAS asked and had nothing, rather than wondering
147
+ * whether it was dropped. It is a mark, not a word, so there is nothing here to
148
+ * translate and no label prop to resolve; a caller who wants words passes
149
+ * `formatValue`, which is handed the `undefined` and decides.
150
+ */
151
+ const NO_VALUE = "—";
152
+
153
+ /**
154
+ * The number this payload item carries, or `undefined` when it carries none.
155
+ *
156
+ * This was `item.value ?? 0`, so every one of the absences above printed a confident
157
+ * "0" that appears nowhere in the data — a fabricated figure in a tooltip, which is the
158
+ * one place a reader goes to check an exact number. keksdose found it and wrapped the
159
+ * component rather than trust it. `""` and a non-numeric string are the same absence
160
+ * spelled differently (recharts hands a string value straight through from the data),
161
+ * and are answered the same way.
162
+ */
163
+ function tooltipValue(value: number | string | undefined): number | undefined {
164
+ if (value == null || value === "") return undefined;
165
+ const n = typeof value === "string" ? Number(value) : value;
166
+ return Number.isFinite(n) ? n : undefined;
167
+ }
168
+
169
+ interface ChartTooltipContentProps {
170
+ active?: boolean;
171
+ payload?: TooltipPayloadItem[];
172
+ label?: string | number;
173
+ indicator?: "dot" | "line";
174
+ hideLabel?: boolean;
175
+ labelFormatter?: (label: string | number) => ReactNode;
176
+ /** Formats one series' value for display. NOT called when the series has no value at
177
+ * this point — {@link ChartTooltipContentProps.formatValue} is the one that is. */
178
+ valueFormatter?: (value: number) => ReactNode;
179
+ /** Formats one series' value INCLUDING its absence: handed `undefined` for a gap, a
180
+ * projection that has not happened yet, or a field that did not exist at an earlier
181
+ * capture, and whatever it returns is rendered as-is. Only the caller knows whether
182
+ * that absence reads as a dash, a "not yet" or a genuine zero, so this is how it
183
+ * says. Supplying it wins over `valueFormatter` for every value.
184
+ *
185
+ * It sits NEXT to `valueFormatter` rather than widening it because that prop's
186
+ * parameter is contextually typed at every call site there is — `(v) => money(v)` —
187
+ * so widening it to `number | undefined` is a compile error in all three consuming
188
+ * apps, which is a breaking change this module does not get to make by itself. */
189
+ formatValue?: (value: number | undefined) => ReactNode;
190
+ /** Cursor position within the chart, injected by recharts. Used with
191
+ * {@link boundaryRef} to decide whether to flip the tooltip. */
192
+ coordinate?: { x?: number; y?: number };
193
+ /** When set, the tooltip flips to the LEFT of the cursor if it would otherwise
194
+ * spill past the right edge of this (usually horizontally-scrolling) container
195
+ * — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */
196
+ boundaryRef?: RefObject<HTMLElement | null>;
197
+ }
198
+
199
+ export function ChartTooltipContent({
200
+ active,
201
+ payload,
202
+ label,
203
+ indicator = "dot",
204
+ hideLabel = false,
205
+ labelFormatter,
206
+ valueFormatter,
207
+ formatValue,
208
+ coordinate,
209
+ boundaryRef,
210
+ }: ChartTooltipContentProps) {
211
+ const config = useChart();
212
+ const tipRef = useRef<HTMLDivElement>(null);
213
+ if (!active || !payload?.length) return null;
214
+ // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart
215
+ // width; subtract the container's scrollLeft to get its on-screen x, then flip
216
+ // left if the tooltip's own width would run past the visible right edge.
217
+ let flip = false;
218
+ const boundary = boundaryRef?.current;
219
+ if (boundary && coordinate?.x != null) {
220
+ const visibleX = coordinate.x - boundary.scrollLeft;
221
+ const tipWidth = tipRef.current?.offsetWidth ?? 160;
222
+ flip = visibleX + tipWidth + 12 > boundary.clientWidth;
223
+ }
224
+ return (
225
+ <div
226
+ ref={tipRef}
227
+ style={boundaryRef ? { transform: flip ? "translateX(calc(-100% - 12px))" : "translateX(12px)" } : undefined}
228
+ className="min-w-[9rem] rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] px-2.5 py-2 text-xs shadow-xl"
229
+ >
230
+ {!hideLabel && label != null && (
231
+ <div className="mb-1.5 font-medium text-[var(--text-primary)]">
232
+ {labelFormatter ? labelFormatter(label) : label}
233
+ </div>
234
+ )}
235
+ <div className="grid gap-1.5">
236
+ {payload.map((item, i) => {
237
+ // Bars/lines/areas identify a series by dataKey; pies/treemaps key off
238
+ // `name` (the nameKey value). Prefer whichever the config knows.
239
+ const dk = item.dataKey != null ? String(item.dataKey) : undefined;
240
+ const nm = item.name != null ? String(item.name) : undefined;
241
+ const cfgKey = dk && config[dk] ? dk : nm && config[nm] ? nm : (dk ?? nm ?? String(i));
242
+ const series = config[cfgKey];
243
+ const fill = typeof item.payload?.fill === "string" ? item.payload.fill : undefined;
244
+ // The data point's own `fill` before recharts' `item.color`: a treemap tile,
245
+ // or a bar with a per-cell colour, IS the colour the reader is pointing at,
246
+ // while `item.color` is the series-wide one — every tile's swatch came out
247
+ // `--chart-1` whatever colour the tile was.
248
+ const color = series?.color ?? fill ?? item.color ?? "#64748b";
249
+ const name = series?.label ?? nm ?? cfgKey;
250
+ const raw = tooltipValue(item.value);
251
+ let shown: ReactNode;
252
+ if (formatValue) shown = formatValue(raw);
253
+ else if (raw == null) shown = NO_VALUE;
254
+ else shown = valueFormatter ? valueFormatter(raw) : raw;
255
+ return (
256
+ <div key={`${cfgKey}-${i}`} className="flex items-center gap-2">
257
+ <span
258
+ className={cn(
259
+ "shrink-0 rounded-[2px]",
260
+ indicator === "dot" ? "h-2.5 w-2.5 rounded-full" : "h-2.5 w-1",
261
+ )}
262
+ style={{ backgroundColor: color }}
263
+ />
264
+ <span className="text-[var(--text-muted)]">{name}</span>
265
+ <span data-private className="ml-auto font-mono font-medium tabular-nums text-[var(--text-primary)]">
266
+ {shown}
267
+ </span>
268
+ </div>
269
+ );
270
+ })}
271
+ </div>
272
+ </div>
273
+ );
274
+ }
275
+
276
+ interface LegendPayloadItem {
277
+ value?: string;
278
+ dataKey?: string | number;
279
+ color?: string;
280
+ }
281
+
282
+ interface ChartLegendContentProps {
283
+ payload?: LegendPayloadItem[];
284
+ onItemClick?: (key: string) => void;
285
+ /**
286
+ * TOGGLE mode (recommended): the series currently switched off. Each entry is a
287
+ * toggle button — pressed while its series is shown — and a hidden one is drawn
288
+ * faded with its label struck through. Several can be off at once; pair with
289
+ * `toggleHidden(hidden, key)` in `onItemClick` and `hide` on the chart's series.
290
+ */
291
+ hiddenKeys?: ReadonlySet<string> | readonly string[];
292
+ /**
293
+ * ISOLATE mode: the one series shown in full, every other entry dimmed. Kept for
294
+ * the charts built on it; a legend where each entry switches its own series is
295
+ * what readers expect from a legend, so prefer `hiddenKeys`.
296
+ */
297
+ activeKey?: string | null;
298
+ }
299
+
300
+ export function ChartLegendContent({
301
+ payload,
302
+ onItemClick,
303
+ hiddenKeys,
304
+ activeKey,
305
+ }: ChartLegendContentProps) {
306
+ const hidden = hiddenKeys ? new Set(hiddenKeys) : null;
307
+ const config = useChart();
308
+ if (!payload?.length) return null;
309
+ return (
310
+ <div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3">
311
+ {payload.map((item, i) => {
312
+ const dk = item.dataKey != null ? String(item.dataKey) : undefined;
313
+ const vv = item.value != null ? String(item.value) : undefined;
314
+ const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));
315
+ const series = config[cfgKey];
316
+ const color = series?.color ?? item.color ?? "#64748b";
317
+ const label = series?.label ?? vv ?? cfgKey;
318
+ const off = hidden?.has(cfgKey) ?? false;
319
+ const dimmed = off || (activeKey != null && activeKey !== cfgKey);
320
+ const className = cn(
321
+ "flex items-center gap-1.5 text-[var(--text-secondary)] transition-opacity",
322
+ onItemClick ? "cursor-pointer hover:opacity-80" : "cursor-default",
323
+ dimmed && "opacity-40",
324
+ );
325
+ const swatch = (
326
+ <span
327
+ aria-hidden
328
+ className="h-2.5 w-2.5 shrink-0 rounded-[3px]"
329
+ style={{ backgroundColor: color } as CSSProperties}
330
+ />
331
+ );
332
+ // A legend with no `onItemClick` is a KEY, not a control — and that is every
333
+ // legend in kastlan, where this rendered N focusable buttons per chart that did
334
+ // nothing when activated. A keyboard user paid the tab stops for all of them,
335
+ // and a screen reader announced each one as a button, promising an action there
336
+ // was none of. The interactive branch is unchanged.
337
+ return onItemClick ? (
338
+ <button
339
+ key={`${cfgKey}-${i}`}
340
+ type="button"
341
+ onClick={() => onItemClick(cfgKey)}
342
+ // Toggles report their state: in toggle mode pressed = the series is shown;
343
+ // in isolate mode pressed = the isolated one. With neither supplied the
344
+ // legend knows nothing about state, and a pressed="false" it cannot vouch
345
+ // for would lie — so it says nothing.
346
+ aria-pressed={
347
+ hidden ? !off : activeKey === undefined ? undefined : activeKey === cfgKey
348
+ }
349
+ className={className}
350
+ >
351
+ {swatch}
352
+ <span className={cn(off && "line-through")}>{label}</span>
353
+ </button>
354
+ ) : (
355
+ <span key={`${cfgKey}-${i}`} className={className}>
356
+ {swatch}
357
+ {label}
358
+ </span>
359
+ );
360
+ })}
361
+ </div>
362
+ );
363
+ }
@@ -0,0 +1,222 @@
1
+ import { forwardRef, useCallback, useEffect, useId, useRef } from "react";
2
+ import { Check, Minus } from "lucide-react";
3
+ import type { ChangeEvent, InputHTMLAttributes, ReactNode } from "react";
4
+ import { cn } from "../lib/cn";
5
+ import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts";
6
+
7
+ /**
8
+ * A checkbox that is still `<input type="checkbox">`.
9
+ *
10
+ * The three consuming apps had three answers: ~30 raw inputs in Keksdose leaning on
11
+ * `tokens.css`'s `accent-color`, a label-plus-box `CheckboxField` in Lenkbank, and a
12
+ * Radix `<button role="checkbox">` in Kastlan. The native element is the one that
13
+ * needs no ARIA to be a checkbox — it submits with its form, answers to `required`,
14
+ * resets with the form and takes a `<label for>` click — so it stays underneath, and
15
+ * only its paint is replaced (`appearance-none`, a token-coloured box, and a glyph
16
+ * laid over it that ignores the pointer so the click still lands on the input).
17
+ *
18
+ * `accent-color` alone was not enough: it tints the native box but cannot change its
19
+ * border, so an INVALID checkbox had no way to say so, and the unchecked box kept the
20
+ * browser's own grey outline in both themes.
21
+ */
22
+
23
+ // The box. 16px, like every raw checkbox it replaces, so a migration moves nothing.
24
+ //
25
+ // `focus-visible`, with an offset in the surface colour: a --brand ring touching a
26
+ // --brand-filled (checked) box would be a ring painted in the colour it sits on —
27
+ // present in the DOM and invisible on screen. The offset is what separates the two.
28
+ const BOX =
29
+ "peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-[var(--border-strong)] bg-[var(--bg-surface)] shadow-sm transition-colors " +
30
+ "checked:border-[var(--brand)] checked:bg-[var(--brand)] " +
31
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)] " +
32
+ "disabled:cursor-not-allowed";
33
+
34
+ // A mixed box wears the checked fill whether or not the underlying input is checked:
35
+ // `indeterminate` is a property the browser keeps separately from `checked`, and the
36
+ // dash means "some", which is neither answer.
37
+ const BOX_MIXED = "border-[var(--brand)] bg-[var(--brand)]";
38
+
39
+ // The same geometry as FIELD_INVALID — a border plus a 1px ring, so the highlight
40
+ // survives fractional display scaling on every edge (see the note in ui.tsx). The
41
+ // focus ring turns danger too, so focusing a wrong box does not repaint it as fine.
42
+ const BOX_INVALID =
43
+ "border-[var(--danger-border)] ring-1 ring-[var(--danger-border)] checked:border-[var(--danger-border)] focus-visible:ring-[var(--danger)]";
44
+
45
+ const ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
46
+
47
+ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
48
+ /**
49
+ * The words beside the box. Rendered as a `<label for>` rather than a label that
50
+ * wraps the input, so `description` and `error` can sit under it without becoming
51
+ * part of the accessible NAME — a wrapping label would have the screen reader read
52
+ * the whole paragraph as the checkbox's name.
53
+ */
54
+ label?: ReactNode;
55
+ /** Secondary text under the label ("Used when no account is chosen"). Pointed at by
56
+ * `aria-describedby`, merged with any the caller passed. Kastlan's
57
+ * `CheckboxField` `description` variant. */
58
+ description?: ReactNode;
59
+ /**
60
+ * "Some but not all" — the header box of a list whose rows are partly selected.
61
+ * A DOM property with no HTML attribute, so it is set on the element after every
62
+ * render; it does not touch `checked`, and a click clears it (the browser's rule,
63
+ * and the caller's `onChange` decides what the click means).
64
+ */
65
+ indeterminate?: boolean;
66
+ /** See {@link Input}'s `invalid`: paints the box and sets `aria-invalid` together,
67
+ * so the two cannot be spelled separately. */
68
+ invalid?: boolean;
69
+ /** What is wrong, in the caller's words ("Accept the terms to continue"). Rendered
70
+ * under the label, attached with `aria-describedby`, and implies `invalid`. */
71
+ error?: ReactNode;
72
+ /** The checked state as a boolean — the `(next) => …` shape Lenkbank's
73
+ * `CheckboxField` and Radix's `onCheckedChange` both used, so a migration does not
74
+ * have to unwrap `event.target.checked` at every call site. Fires alongside
75
+ * `onChange`, never instead of it. */
76
+ onCheckedChange?: (checked: boolean) => void;
77
+ /** Classes for the `<input>` itself. `className` styles the OUTERMOST element —
78
+ * the row when there is a label, the box's own wrapper when there is not — which
79
+ * is where layout (`self-end pb-2` to sit on a field's baseline) belongs. */
80
+ inputClassName?: string;
81
+ }
82
+
83
+ /**
84
+ * A native checkbox in the kit's tokens, with an optional label, description and
85
+ * error.
86
+ *
87
+ * Without `label`, `description` or `error` it renders the box and nothing else, so
88
+ * it drops into a table cell or a caller's own `<label>` exactly as the raw input
89
+ * did — give it an `aria-label` there. Every other prop reaches the `<input>`
90
+ * (`checked`, `defaultChecked`, `name`, `required`, `data-*`, `aria-*`), so it works
91
+ * controlled, uncontrolled, and inside a plain `<form>`.
92
+ */
93
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(
94
+ {
95
+ label,
96
+ description,
97
+ indeterminate = false,
98
+ invalid,
99
+ error,
100
+ onCheckedChange,
101
+ onChange,
102
+ className,
103
+ inputClassName,
104
+ id,
105
+ disabled,
106
+ ...rest
107
+ },
108
+ ref,
109
+ ) {
110
+ const generated = useId();
111
+ const inputId = id ?? generated;
112
+ const descriptionId = `${inputId}-description`;
113
+ const errorId = `${inputId}-error`;
114
+ const showError = hasMessage(error);
115
+ const showDescription = hasMessage(description);
116
+ const isInvalid = Boolean(invalid) || showError;
117
+ // Nothing to lay out beside the box: render the box alone, as the raw input was.
118
+ const bare = label === undefined && !showDescription && !showError;
119
+
120
+ const local = useRef<HTMLInputElement | null>(null);
121
+ const setRef = useCallback(
122
+ (node: HTMLInputElement | null) => {
123
+ local.current = node;
124
+ assignRef(ref, node);
125
+ },
126
+ [ref],
127
+ );
128
+ // After EVERY render, not only when the prop changes: a click clears the property
129
+ // in the DOM, and a controlled caller that keeps `indeterminate` true expects the
130
+ // dash back on the next render even though the prop never changed.
131
+ useEffect(() => {
132
+ if (local.current) local.current.indeterminate = indeterminate;
133
+ });
134
+
135
+ const box = (
136
+ <span className={cn("relative inline-flex shrink-0", bare && className)}>
137
+ <input
138
+ ref={setRef}
139
+ id={bare ? id : inputId}
140
+ disabled={disabled}
141
+ {...rest}
142
+ // After the spread, like Switch: a props object spread at a checkbox must not
143
+ // be able to turn it into something else.
144
+ type="checkbox"
145
+ // OR-ed with the spread so a caller's own `aria-invalid` survives — Input's rule.
146
+ aria-invalid={isInvalid || rest["aria-invalid"] || undefined}
147
+ aria-describedby={mergeDescribedBy(
148
+ rest["aria-describedby"],
149
+ showDescription && descriptionId,
150
+ showError && errorId,
151
+ )}
152
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
153
+ onChange?.(e);
154
+ onCheckedChange?.(e.target.checked);
155
+ }}
156
+ className={cn(BOX, indeterminate && BOX_MIXED, isInvalid && BOX_INVALID, inputClassName)}
157
+ />
158
+ {/* The glyph is drawn over the box, not inside it (an input has no children),
159
+ and ignores the pointer so a click on the tick still toggles the input. The
160
+ tick is shown by `peer-checked`, so an UNCONTROLLED box shows it too; the
161
+ dash replaces it outright while `indeterminate` holds. */}
162
+ {indeterminate ? (
163
+ <Minus
164
+ aria-hidden
165
+ strokeWidth={3}
166
+ className="pointer-events-none absolute inset-0 m-auto size-3 text-[var(--brand-contrast)]"
167
+ />
168
+ ) : (
169
+ <Check
170
+ aria-hidden
171
+ strokeWidth={3}
172
+ className="pointer-events-none invisible absolute inset-0 m-auto size-3 text-[var(--brand-contrast)] peer-checked:visible"
173
+ />
174
+ )}
175
+ </span>
176
+ );
177
+
178
+ if (bare) return box;
179
+
180
+ return (
181
+ <div
182
+ className={cn(
183
+ // `items-start` + a 20px cell for the box: the box centres on the label's FIRST
184
+ // line (text-sm is 20px tall), so a label that wraps keeps the box at the top
185
+ // rather than floating to the middle of a paragraph.
186
+ "flex items-start gap-2",
187
+ // The whole row fades, the way ToggleGroup and the fields do, so the label does
188
+ // not stay at full strength beside a box you cannot change.
189
+ disabled && "opacity-60",
190
+ className,
191
+ )}
192
+ >
193
+ <span className="flex h-5 shrink-0 items-center">{box}</span>
194
+ <span className="min-w-0">
195
+ {label !== undefined && (
196
+ <label
197
+ htmlFor={inputId}
198
+ className={cn(
199
+ "block text-sm leading-5 text-[var(--text-primary)]",
200
+ // `select-none`: a double-click to toggle twice would otherwise select the
201
+ // words, and a drag that starts on them starts a native text drag.
202
+ disabled ? "cursor-not-allowed" : "cursor-pointer select-none",
203
+ )}
204
+ >
205
+ {label}
206
+ </label>
207
+ )}
208
+ {showDescription && (
209
+ <span id={descriptionId} className="mt-0.5 block text-xs text-[var(--text-muted)]">
210
+ {description}
211
+ </span>
212
+ )}
213
+ {showError && (
214
+ <span id={errorId} className={cn("block", ERROR_CLASS)}>
215
+ {error}
216
+ </span>
217
+ )}
218
+ </span>
219
+ </div>
220
+ );
221
+ });
222
+ Checkbox.displayName = "Checkbox";