@eifi1/ui-kit 0.21.0 → 0.23.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 (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -0,0 +1,104 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /** One box of a {@link CheckboxGroup}. */
5
+ interface CheckboxGroupOption<T extends string = string> {
6
+ value: T;
7
+ /** The words beside the box — its accessible name. */
8
+ label: ReactNode;
9
+ /** A line under the label ("Imprint, Privacy Policy and Terms only"), attached to the
10
+ * box with `aria-describedby` — {@link Checkbox}'s `description`. */
11
+ hint?: ReactNode;
12
+ /** This box cannot be changed; the others can. */
13
+ disabled?: boolean;
14
+ }
15
+ /** Whether the legend is drawn. `"sr-only"` keeps it as the group's name for a screen
16
+ * reader where the dialog's title already says it to the eye. */
17
+ type CheckboxGroupLegendVisibility = "visible" | "sr-only";
18
+ interface CheckboxGroupProps<T extends string = string> {
19
+ /** The question the boxes answer — the fieldset's `<legend>`, which names the group.
20
+ * Without one, pass `aria-label`. */
21
+ legend?: ReactNode;
22
+ /** Default `"visible"`. */
23
+ legendVisibility?: CheckboxGroupLegendVisibility;
24
+ "aria-label"?: string;
25
+ /** One box per option, in this order. */
26
+ options: CheckboxGroupOption<T>[];
27
+ /** The ticked values. Controlled. */
28
+ value: T[];
29
+ /**
30
+ * The new set on every tick. In the ORDER OF `options`, not the order the boxes were
31
+ * ticked, so a list saved from it does not change with the order someone clicked in
32
+ * (ChoiceCardGroup's rule). A value in `value` that no option carries — a grant for a
33
+ * locale the app no longer offers — is kept, after the known ones: a picker that
34
+ * dropped what it could not show would delete it on the next unrelated tick.
35
+ */
36
+ onChange: (value: T[]) => void;
37
+ /**
38
+ * `"vertical"` (default): one box per line. `"horizontal"`: a wrapping row, for a few
39
+ * short labels. With `columns`, vertical becomes a grid of that many columns from the
40
+ * `sm` breakpoint up and stays one column on a phone — the reviewer dialog's shape.
41
+ */
42
+ layout?: "vertical" | "horizontal";
43
+ /** Columns for a vertical group, from `sm` up (one on a phone). Default 1. */
44
+ columns?: 1 | 2 | 3 | 4;
45
+ /** Shared by every box — what a plain form submits the values under. */
46
+ name?: string;
47
+ /** Every box: the fieldset's own `disabled` (which disables them all natively), each
48
+ * box faded as a disabled Checkbox is, and the legend with them. */
49
+ disabled?: boolean;
50
+ /**
51
+ * At least one box must be ticked. Draws the required star after the legend, and puts
52
+ * the native `required` on every box while NONE is ticked — so a form refuses an
53
+ * empty set — and on none once one is (ChoiceCardGroup's rule: checkboxes cannot say
54
+ * "at least one" natively).
55
+ */
56
+ required?: boolean;
57
+ /**
58
+ * Standing advice for the group, under the boxes, attached to the fieldset with
59
+ * `aria-describedby` before the error — the field anatomy every kit field keeps.
60
+ */
61
+ hint?: ReactNode;
62
+ /** What is wrong with the set, in the caller's words. Under the boxes, on the
63
+ * fieldset's `aria-describedby`, and paints every box invalid. */
64
+ error?: ReactNode;
65
+ /** Paints every box invalid without a message (the message lives elsewhere). */
66
+ invalid?: boolean;
67
+ /**
68
+ * The group SAVES on change — each tick is written straight away (a reviewer's
69
+ * per-locale grants). Under a locked {@link WriteLockProvider} every box is locked
70
+ * the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.
71
+ */
72
+ commit?: boolean;
73
+ /** Why no box can be changed — every box gets it, and it wins over `disabled` (the
74
+ * boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */
75
+ disabledReason?: ReactNode;
76
+ /** The fieldset's id. */
77
+ id?: string;
78
+ /** Classes for the fieldset — the outermost element, where layout belongs. */
79
+ className?: string;
80
+ /** Classes for the box list (the grid or the row). */
81
+ listClassName?: string;
82
+ /** Ids of something else on the page that describes the group; merged before the
83
+ * group's own hint and error. */
84
+ "aria-describedby"?: string;
85
+ }
86
+ /**
87
+ * A set of kit {@link Checkbox}es answering one question, in a `<fieldset>`.
88
+ *
89
+ * Kurvenschmiede's reviewer dialog (features/admin/reviewer-dialog.tsx) builds this by
90
+ * hand — a fieldset, an `sr-only` legend, a grid of checkboxes, a `toggle(code, on)`
91
+ * that adds or filters a `string[]` — and keksdose's and kastlan's reviewer grants build
92
+ * the same picker for the same per-locale grant. The hand-built ones differ in the parts
93
+ * that are easy to get wrong: whether the legend names the group, whether a hint or an
94
+ * error is attached to anything, what order the saved list comes out in, and whether a
95
+ * value the picker cannot show survives a tick.
96
+ *
97
+ * Every box is a kit {@link Checkbox}, so the native checkbox semantics stay (each box
98
+ * submits under `name` in a plain form). The legend is the group's name; the hint and
99
+ * error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},
100
+ * {@link ToggleGroup}).
101
+ */
102
+ declare function CheckboxGroup<T extends string = string>({ legend, legendVisibility, "aria-label": ariaLabel, options, value, onChange, layout, columns, name, disabled, required, hint, error, invalid, commit, disabledReason, id, className, listClassName, "aria-describedby": ariaDescribedBy, }: CheckboxGroupProps<T>): react.JSX.Element;
103
+
104
+ export { CheckboxGroup, type CheckboxGroupLegendVisibility, type CheckboxGroupOption, type CheckboxGroupProps };
@@ -0,0 +1,111 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Checkbox } from "./checkbox.js";
6
+ import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
7
+ import { useCommitReason } from "./write-lock.js";
8
+ import { RequiredStarOnLegend } from "./field-parts.js";
9
+ const COLUMNS = {
10
+ 1: "flex flex-col gap-2",
11
+ 2: "grid grid-cols-1 gap-2 sm:grid-cols-2",
12
+ 3: "grid grid-cols-1 gap-2 sm:grid-cols-3",
13
+ 4: "grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4"
14
+ };
15
+ function CheckboxGroup({
16
+ legend,
17
+ legendVisibility = "visible",
18
+ "aria-label": ariaLabel,
19
+ options,
20
+ value,
21
+ onChange,
22
+ layout = "vertical",
23
+ columns = 1,
24
+ name,
25
+ disabled,
26
+ required,
27
+ hint,
28
+ error,
29
+ invalid,
30
+ commit,
31
+ disabledReason,
32
+ id,
33
+ className,
34
+ listClassName,
35
+ "aria-describedby": ariaDescribedBy
36
+ }) {
37
+ const generated = useId();
38
+ const hintId = `${generated}-hint`;
39
+ const errorId = `${generated}-error`;
40
+ const showHint = hasMessage(hint);
41
+ const showError = hasMessage(error);
42
+ const isInvalid = Boolean(invalid) || showError;
43
+ const ticked = new Set(value);
44
+ const reason = useCommitReason(commit, disabledReason);
45
+ const locked = hasMessage(reason);
46
+ const toggle = (v, on) => {
47
+ const next = new Set(value);
48
+ if (on) next.add(v);
49
+ else next.delete(v);
50
+ const known = new Set(options.map((o) => o.value));
51
+ onChange([
52
+ ...options.map((o) => o.value).filter((x) => next.has(x)),
53
+ // Values no option carries, kept in their own order — see `onChange`.
54
+ ...value.filter((x) => !known.has(x))
55
+ ]);
56
+ };
57
+ return /* @__PURE__ */ jsxs(
58
+ "fieldset",
59
+ {
60
+ id,
61
+ "aria-label": ariaLabel,
62
+ "aria-describedby": mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId),
63
+ disabled: disabled && !locked,
64
+ className: cn("relative m-0 min-w-0 border-0 p-0", className),
65
+ children: [
66
+ legend !== void 0 && /* @__PURE__ */ jsxs(
67
+ "legend",
68
+ {
69
+ className: legendVisibility === "sr-only" ? "sr-only" : cn("mb-2 p-0 text-sm font-medium text-[var(--text-primary)]", (disabled || locked) && "opacity-60"),
70
+ children: [
71
+ legend,
72
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-[var(--danger)]", children: "*" })
73
+ ]
74
+ }
75
+ ),
76
+ /* @__PURE__ */ jsx(RequiredStarOnLegend.Provider, { value: true, children: /* @__PURE__ */ jsx(
77
+ "div",
78
+ {
79
+ className: cn(
80
+ layout === "horizontal" ? "flex flex-wrap gap-x-4 gap-y-2" : COLUMNS[columns],
81
+ listClassName
82
+ ),
83
+ children: options.map((o) => /* @__PURE__ */ jsx(
84
+ Checkbox,
85
+ {
86
+ name,
87
+ value: o.value,
88
+ label: o.label,
89
+ description: o.hint,
90
+ checked: ticked.has(o.value),
91
+ onCheckedChange: (on) => toggle(o.value, on),
92
+ disabled: disabled || o.disabled,
93
+ invalid: isInvalid || void 0,
94
+ required: required && value.length === 0 ? true : void 0,
95
+ disabledReason: reason
96
+ },
97
+ o.value
98
+ ))
99
+ }
100
+ ) }),
101
+ showHint && // A `div`, not a `p`: a hint may be a node with a link in it.
102
+ /* @__PURE__ */ jsx("div", { id: hintId, className: "mt-1.5 text-[11px] leading-tight text-[var(--text-muted)]", children: hint }),
103
+ showError && /* @__PURE__ */ jsx("p", { id: errorId, className: "mt-1 text-[11px] leading-tight text-[var(--danger)]", children: error })
104
+ ]
105
+ }
106
+ );
107
+ }
108
+ export {
109
+ CheckboxGroup
110
+ };
111
+ //# sourceMappingURL=checkbox-group.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/checkbox-group.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Checkbox } from \"./checkbox\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { useCommitReason } from \"./write-lock\";\nimport { RequiredStarOnLegend } from \"./field-parts\";\n\n/** One box of a {@link CheckboxGroup}. */\nexport interface CheckboxGroupOption<T extends string = string> {\n value: T;\n /** The words beside the box — its accessible name. */\n label: ReactNode;\n /** A line under the label (\"Imprint, Privacy Policy and Terms only\"), attached to the\n * box with `aria-describedby` — {@link Checkbox}'s `description`. */\n hint?: ReactNode;\n /** This box cannot be changed; the others can. */\n disabled?: boolean;\n}\n\n/** Whether the legend is drawn. `\"sr-only\"` keeps it as the group's name for a screen\n * reader where the dialog's title already says it to the eye. */\nexport type CheckboxGroupLegendVisibility = \"visible\" | \"sr-only\";\n\nexport interface CheckboxGroupProps<T extends string = string> {\n /** The question the boxes answer — the fieldset's `<legend>`, which names the group.\n * Without one, pass `aria-label`. */\n legend?: ReactNode;\n /** Default `\"visible\"`. */\n legendVisibility?: CheckboxGroupLegendVisibility;\n \"aria-label\"?: string;\n /** One box per option, in this order. */\n options: CheckboxGroupOption<T>[];\n /** The ticked values. Controlled. */\n value: T[];\n /**\n * The new set on every tick. In the ORDER OF `options`, not the order the boxes were\n * ticked, so a list saved from it does not change with the order someone clicked in\n * (ChoiceCardGroup's rule). A value in `value` that no option carries — a grant for a\n * locale the app no longer offers — is kept, after the known ones: a picker that\n * dropped what it could not show would delete it on the next unrelated tick.\n */\n onChange: (value: T[]) => void;\n /**\n * `\"vertical\"` (default): one box per line. `\"horizontal\"`: a wrapping row, for a few\n * short labels. With `columns`, vertical becomes a grid of that many columns from the\n * `sm` breakpoint up and stays one column on a phone — the reviewer dialog's shape.\n */\n layout?: \"vertical\" | \"horizontal\";\n /** Columns for a vertical group, from `sm` up (one on a phone). Default 1. */\n columns?: 1 | 2 | 3 | 4;\n /** Shared by every box — what a plain form submits the values under. */\n name?: string;\n /** Every box: the fieldset's own `disabled` (which disables them all natively), each\n * box faded as a disabled Checkbox is, and the legend with them. */\n disabled?: boolean;\n /**\n * At least one box must be ticked. Draws the required star after the legend, and puts\n * the native `required` on every box while NONE is ticked — so a form refuses an\n * empty set — and on none once one is (ChoiceCardGroup's rule: checkboxes cannot say\n * \"at least one\" natively).\n */\n required?: boolean;\n /**\n * Standing advice for the group, under the boxes, attached to the fieldset with\n * `aria-describedby` before the error — the field anatomy every kit field keeps.\n */\n hint?: ReactNode;\n /** What is wrong with the set, in the caller's words. Under the boxes, on the\n * fieldset's `aria-describedby`, and paints every box invalid. */\n error?: ReactNode;\n /** Paints every box invalid without a message (the message lives elsewhere). */\n invalid?: boolean;\n /**\n * The group SAVES on change — each tick is written straight away (a reviewer's\n * per-locale grants). Under a locked {@link WriteLockProvider} every box is locked\n * the focusable way with the lock's reason. See {@link Checkbox}'s `commit`.\n */\n commit?: boolean;\n /** Why no box can be changed — every box gets it, and it wins over `disabled` (the\n * boxes stay focusable to say it). See {@link Checkbox}'s `disabledReason`. */\n disabledReason?: ReactNode;\n /** The fieldset's id. */\n id?: string;\n /** Classes for the fieldset — the outermost element, where layout belongs. */\n className?: string;\n /** Classes for the box list (the grid or the row). */\n listClassName?: string;\n /** Ids of something else on the page that describes the group; merged before the\n * group's own hint and error. */\n \"aria-describedby\"?: string;\n}\n\n// Literal class strings so Tailwind's scanner sees each one.\nconst COLUMNS: Record<NonNullable<CheckboxGroupProps[\"columns\"]>, string> = {\n 1: \"flex flex-col gap-2\",\n 2: \"grid grid-cols-1 gap-2 sm:grid-cols-2\",\n 3: \"grid grid-cols-1 gap-2 sm:grid-cols-3\",\n 4: \"grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4\",\n};\n\n/**\n * A set of kit {@link Checkbox}es answering one question, in a `<fieldset>`.\n *\n * Kurvenschmiede's reviewer dialog (features/admin/reviewer-dialog.tsx) builds this by\n * hand — a fieldset, an `sr-only` legend, a grid of checkboxes, a `toggle(code, on)`\n * that adds or filters a `string[]` — and keksdose's and kastlan's reviewer grants build\n * the same picker for the same per-locale grant. The hand-built ones differ in the parts\n * that are easy to get wrong: whether the legend names the group, whether a hint or an\n * error is attached to anything, what order the saved list comes out in, and whether a\n * value the picker cannot show survives a tick.\n *\n * Every box is a kit {@link Checkbox}, so the native checkbox semantics stay (each box\n * submits under `name` in a plain form). The legend is the group's name; the hint and\n * error describe the fieldset. For a choice of ONE, use radios ({@link ChoiceCardGroup},\n * {@link ToggleGroup}).\n */\nexport function CheckboxGroup<T extends string = string>({\n legend,\n legendVisibility = \"visible\",\n \"aria-label\": ariaLabel,\n options,\n value,\n onChange,\n layout = \"vertical\",\n columns = 1,\n name,\n disabled,\n required,\n hint,\n error,\n invalid,\n commit,\n disabledReason,\n id,\n className,\n listClassName,\n \"aria-describedby\": ariaDescribedBy,\n}: CheckboxGroupProps<T>) {\n const generated = useId();\n const hintId = `${generated}-hint`;\n const errorId = `${generated}-error`;\n const showHint = hasMessage(hint);\n const showError = hasMessage(error);\n const isInvalid = Boolean(invalid) || showError;\n const ticked = new Set(value);\n // Resolved here rather than per box: a reason has to keep the fieldset from being\n // natively `disabled` too, or it would take every box out of the tab order — the\n // thing a reason exists to prevent.\n const reason = useCommitReason(commit, disabledReason);\n const locked = hasMessage(reason);\n\n const toggle = (v: T, on: boolean) => {\n const next = new Set(value);\n if (on) next.add(v);\n else next.delete(v);\n const known = new Set(options.map((o) => o.value));\n onChange([\n ...options.map((o) => o.value).filter((x) => next.has(x)),\n // Values no option carries, kept in their own order — see `onChange`.\n ...value.filter((x) => !known.has(x)),\n ]);\n };\n\n return (\n <fieldset\n id={id}\n aria-label={ariaLabel}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, showHint && hintId, showError && errorId)}\n disabled={disabled && !locked}\n // `relative` holds an `sr-only` legend (sr-only-containment.test); `min-w-0`\n // undoes a fieldset's `min-width: min-content`, which would otherwise push a long\n // label past a phone's edge instead of wrapping it.\n className={cn(\"relative m-0 min-w-0 border-0 p-0\", className)}\n >\n {legend !== undefined && (\n <legend\n className={\n legendVisibility === \"sr-only\"\n ? \"sr-only\"\n : cn(\"mb-2 p-0 text-sm font-medium text-[var(--text-primary)]\", (disabled || locked) && \"opacity-60\")\n }\n >\n {legend}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\n </legend>\n )}\n <RequiredStarOnLegend.Provider value={true}>\n <div\n className={cn(\n layout === \"horizontal\" ? \"flex flex-wrap gap-x-4 gap-y-2\" : COLUMNS[columns],\n listClassName,\n )}\n >\n {options.map((o) => (\n <Checkbox\n key={o.value}\n name={name}\n value={o.value}\n label={o.label}\n description={o.hint}\n checked={ticked.has(o.value)}\n onCheckedChange={(on) => toggle(o.value, on)}\n disabled={disabled || o.disabled}\n invalid={isInvalid || undefined}\n required={required && value.length === 0 ? true : undefined}\n disabledReason={reason}\n />\n ))}\n </div>\n </RequiredStarOnLegend.Provider>\n {showHint && (\n // A `div`, not a `p`: a hint may be a node with a link in it.\n <div id={hintId} className=\"mt-1.5 text-[11px] leading-tight text-[var(--text-muted)]\">\n {hint}\n </div>\n )}\n {showError && (\n <p id={errorId} className=\"mt-1 text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n </fieldset>\n );\n}\n"],"mappings":"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","names":[]}
@@ -44,6 +44,25 @@ interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "typ
44
44
  * label WITHOUT a literal "*". No mark without a label.
45
45
  */
46
46
  required?: boolean;
47
+ /**
48
+ * Why the box cannot be changed — {@link Button}'s `disabledReason`, for a checkbox
49
+ * that SAVES on change (keksdose K3: a setting ticked straight into the server, kept
50
+ * behind a hand-rolled `SaveGuard` that forced a native `disabled` — out of the tab
51
+ * order, so the reason never reached a keyboard).
52
+ *
53
+ * With a reason the box is `aria-disabled` instead: still focusable, a click or Space
54
+ * changes nothing and fires no `onChange` / `onCheckedChange`, the reason is in the
55
+ * kit {@link Tooltip} on the box and on its `aria-describedby`, and the row fades as a
56
+ * disabled one does. It wins over `disabled`. Changing the lock mounts the box anew (it
57
+ * moves in or out of the Tooltip), as a locked Button is.
58
+ */
59
+ disabledReason?: ReactNode;
60
+ /**
61
+ * This box COMMITS — ticking it saves. Under a locked {@link WriteLockProvider} it is
62
+ * locked the `disabledReason` way with the lock's reason (which wins over its own).
63
+ * No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.
64
+ */
65
+ commit?: boolean;
47
66
  }
48
67
  /**
49
68
  * A native checkbox in the kit's tokens, with an optional label, description and
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { forwardRef, useCallback, useEffect, useId, useRef } from "react";
3
+ import { forwardRef, useCallback, useContext, useEffect, useId, useRef } from "react";
4
4
  import { Check, Minus } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts.js";
7
+ import { LockedReason, RequiredStarOnLegend, useLockReason } from "./field-parts.js";
7
8
  const BOX = "peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-[var(--border-strong)] bg-[var(--bg-surface)] shadow-sm transition-colors checked:border-[var(--brand)] checked:bg-[var(--brand)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)] disabled:cursor-not-allowed";
8
9
  const BOX_MIXED = "border-[var(--brand)] bg-[var(--brand)]";
9
10
  const BOX_INVALID = "border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] checked:border-[var(--danger-border-strong)] focus-visible:ring-[var(--danger)]";
@@ -21,10 +22,17 @@ const Checkbox = forwardRef(function Checkbox2({
21
22
  id,
22
23
  disabled,
23
24
  required,
25
+ disabledReason,
26
+ commit,
27
+ onClick,
24
28
  ...rest
25
29
  }, ref) {
26
30
  const generated = useId();
27
31
  const inputId = id ?? generated;
32
+ const lock = useLockReason(commit, disabledReason);
33
+ const locked = lock.locked;
34
+ const looksDisabled = Boolean(disabled) || locked;
35
+ const starOnLegend = useContext(RequiredStarOnLegend);
28
36
  const descriptionId = `${inputId}-description`;
29
37
  const errorId = `${inputId}-error`;
30
38
  const showError = hasMessage(error);
@@ -42,27 +50,41 @@ const Checkbox = forwardRef(function Checkbox2({
42
50
  useEffect(() => {
43
51
  if (local.current) local.current.indeterminate = indeterminate;
44
52
  });
45
- const box = /* @__PURE__ */ jsxs("span", { className: cn("relative inline-flex shrink-0", bare && className), children: [
53
+ const control = /* @__PURE__ */ jsxs("span", { className: cn("relative inline-flex shrink-0", bare && !locked && className), children: [
46
54
  /* @__PURE__ */ jsx(
47
55
  "input",
48
56
  {
49
57
  ref: setRef,
50
58
  id: bare ? id : inputId,
51
- disabled,
59
+ disabled: locked ? void 0 : disabled,
52
60
  required,
53
61
  ...rest,
54
62
  type: "checkbox",
63
+ "aria-disabled": locked || rest["aria-disabled"] || void 0,
55
64
  "aria-invalid": isInvalid || rest["aria-invalid"] || void 0,
56
65
  "aria-describedby": mergeDescribedBy(
57
66
  rest["aria-describedby"],
58
67
  showDescription && descriptionId,
68
+ locked && lock.reasonId,
59
69
  showError && errorId
60
70
  ),
71
+ onClick: locked ? void 0 : onClick,
61
72
  onChange: (e) => {
73
+ if (locked) {
74
+ e.currentTarget.checked = !e.currentTarget.checked;
75
+ e.currentTarget.indeterminate = indeterminate;
76
+ return;
77
+ }
62
78
  onChange?.(e);
63
79
  onCheckedChange?.(e.target.checked);
64
80
  },
65
- className: cn(BOX, indeterminate && BOX_MIXED, isInvalid && BOX_INVALID, inputClassName)
81
+ className: cn(
82
+ BOX,
83
+ indeterminate && BOX_MIXED,
84
+ isInvalid && BOX_INVALID,
85
+ locked && "cursor-not-allowed",
86
+ inputClassName
87
+ )
66
88
  }
67
89
  ),
68
90
  indeterminate ? /* @__PURE__ */ jsx(
@@ -81,6 +103,7 @@ const Checkbox = forwardRef(function Checkbox2({
81
103
  }
82
104
  )
83
105
  ] });
106
+ const box = locked ? /* @__PURE__ */ jsx(LockedReason, { lock, className: cn("shrink-0", bare && className), children: control }) : control;
84
107
  if (bare) return box;
85
108
  return /* @__PURE__ */ jsxs(
86
109
  "div",
@@ -92,7 +115,7 @@ const Checkbox = forwardRef(function Checkbox2({
92
115
  "flex items-start gap-2",
93
116
  // The whole row fades, the way ToggleGroup and the fields do, so the label does
94
117
  // not stay at full strength beside a box you cannot change.
95
- disabled && "opacity-60",
118
+ looksDisabled && "opacity-60",
96
119
  className
97
120
  ),
98
121
  children: [
@@ -106,11 +129,11 @@ const Checkbox = forwardRef(function Checkbox2({
106
129
  "block text-sm leading-5 text-[var(--text-primary)]",
107
130
  // `select-none`: a double-click to toggle twice would otherwise select the
108
131
  // words, and a drag that starts on them starts a native text drag.
109
- disabled ? "cursor-not-allowed" : "cursor-pointer select-none"
132
+ looksDisabled ? "cursor-not-allowed" : "cursor-pointer select-none"
110
133
  ),
111
134
  children: [
112
135
  label,
113
- required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-[var(--danger)]", children: "*" })
136
+ required && !starOnLegend && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ms-0.5 text-[var(--danger)]", children: "*" })
114
137
  ]
115
138
  }
116
139
  ),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/checkbox.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useId, useRef } from \"react\";\nimport { Check, Minus } from \"lucide-react\";\nimport type { ChangeEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { assignRef, hasMessage, mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * A checkbox that is still `<input type=\"checkbox\">`.\n *\n * The three consuming apps had three answers: ~30 raw inputs in Keksdose leaning on\n * `tokens.css`'s `accent-color`, a label-plus-box `CheckboxField` in Lenkbank, and a\n * Radix `<button role=\"checkbox\">` in Kastlan. The native element is the one that\n * needs no ARIA to be a checkbox — it submits with its form, answers to `required`,\n * resets with the form and takes a `<label for>` click — so it stays underneath, and\n * only its paint is replaced (`appearance-none`, a token-coloured box, and a glyph\n * laid over it that ignores the pointer so the click still lands on the input).\n *\n * `accent-color` alone was not enough: it tints the native box but cannot change its\n * border, so an INVALID checkbox had no way to say so, and the unchecked box kept the\n * browser's own grey outline in both themes.\n */\n\n// The box. 16px, like every raw checkbox it replaces, so a migration moves nothing.\n//\n// `focus-visible`, with an offset in the surface colour: a --brand ring touching a\n// --brand-filled (checked) box would be a ring painted in the colour it sits on —\n// present in the DOM and invisible on screen. The offset is what separates the two.\nconst BOX =\n \"peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-[var(--border-strong)] bg-[var(--bg-surface)] shadow-sm transition-colors \" +\n \"checked:border-[var(--brand)] checked:bg-[var(--brand)] \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed\";\n\n// A mixed box wears the checked fill whether or not the underlying input is checked:\n// `indeterminate` is a property the browser keeps separately from `checked`, and the\n// dash means \"some\", which is neither answer.\nconst BOX_MIXED = \"border-[var(--brand)] bg-[var(--brand)]\";\n\n// The same geometry as FIELD_INVALID — a border plus a 1px ring, so the highlight\n// survives fractional display scaling on every edge (see the note in ui.tsx). The\n// focus ring turns danger too, so focusing a wrong box does not repaint it as fine.\nconst BOX_INVALID =\n \"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] checked:border-[var(--danger-border-strong)] focus-visible:ring-[var(--danger)]\";\n\nconst ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nexport interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * The words beside the box. Rendered as a `<label for>` rather than a label that\n * wraps the input, so `description` and `error` can sit under it without becoming\n * part of the accessible NAME — a wrapping label would have the screen reader read\n * the whole paragraph as the checkbox's name.\n */\n label?: ReactNode;\n /** Secondary text under the label (\"Used when no account is chosen\"). Pointed at by\n * `aria-describedby`, merged with any the caller passed. Kastlan's\n * `CheckboxField` `description` variant. */\n description?: ReactNode;\n /**\n * \"Some but not all\" — the header box of a list whose rows are partly selected.\n * A DOM property with no HTML attribute, so it is set on the element after every\n * render; it does not touch `checked`, and a click clears it (the browser's rule,\n * and the caller's `onChange` decides what the click means).\n */\n indeterminate?: boolean;\n /** See {@link Input}'s `invalid`: paints the box and sets `aria-invalid` together,\n * so the two cannot be spelled separately. */\n invalid?: boolean;\n /** What is wrong, in the caller's words (\"Accept the terms to continue\"). Rendered\n * under the label, attached with `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** The checked state as a boolean — the `(next) => …` shape Lenkbank's\n * `CheckboxField` and Radix's `onCheckedChange` both used, so a migration does not\n * have to unwrap `event.target.checked` at every call site. Fires alongside\n * `onChange`, never instead of it. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>` itself. `className` styles the OUTERMOST element —\n * the row when there is a label, the box's own wrapper when there is not — which\n * is where layout (`self-end pb-2` to sit on a field's baseline) belongs. */\n inputClassName?: string;\n /**\n * Must be ticked to submit — a consent, an acceptance before paying. Reaches the\n * `<input>` as the native `required` (so a `<form>` refuses to submit without it and a\n * screen reader announces \"required\"), and draws the kit's required mark after the\n * label, the same `aria-hidden` star as {@link Label}'s: the word is announced from\n * the control, so a star inside the name would only be read out as noise. Write the\n * label WITHOUT a literal \"*\". No mark without a label.\n */\n required?: boolean;\n}\n\n/**\n * A native checkbox in the kit's tokens, with an optional label, description and\n * error.\n *\n * Without `label`, `description` or `error` it renders the box and nothing else, so\n * it drops into a table cell or a caller's own `<label>` exactly as the raw input\n * did — give it an `aria-label` there. Every other prop reaches the `<input>`\n * (`checked`, `defaultChecked`, `name`, `required`, `data-*`, `aria-*`), so it works\n * controlled, uncontrolled, and inside a plain `<form>`.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n {\n label,\n description,\n indeterminate = false,\n invalid,\n error,\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const inputId = id ?? generated;\n const descriptionId = `${inputId}-description`;\n const errorId = `${inputId}-error`;\n const showError = hasMessage(error);\n const showDescription = hasMessage(description);\n const isInvalid = Boolean(invalid) || showError;\n // Nothing to lay out beside the box: render the box alone, as the raw input was.\n const bare = label === undefined && !showDescription && !showError;\n\n const local = useRef<HTMLInputElement | null>(null);\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n local.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n // After EVERY render, not only when the prop changes: a click clears the property\n // in the DOM, and a controlled caller that keeps `indeterminate` true expects the\n // dash back on the next render even though the prop never changed.\n useEffect(() => {\n if (local.current) local.current.indeterminate = indeterminate;\n });\n\n const box = (\n <span className={cn(\"relative inline-flex shrink-0\", bare && className)}>\n <input\n ref={setRef}\n id={bare ? id : inputId}\n disabled={disabled}\n required={required}\n {...rest}\n // After the spread, like Switch: a props object spread at a checkbox must not\n // be able to turn it into something else.\n type=\"checkbox\"\n // OR-ed with the spread so a caller's own `aria-invalid` survives — Input's rule.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n showError && errorId,\n )}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(BOX, indeterminate && BOX_MIXED, isInvalid && BOX_INVALID, inputClassName)}\n />\n {/* The glyph is drawn over the box, not inside it (an input has no children),\n and ignores the pointer so a click on the tick still toggles the input. The\n tick is shown by `peer-checked`, so an UNCONTROLLED box shows it too; the\n dash replaces it outright while `indeterminate` holds. */}\n {indeterminate ? (\n <Minus\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none absolute inset-0 m-auto size-3 text-[var(--brand-contrast)]\"\n />\n ) : (\n <Check\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none invisible absolute inset-0 m-auto size-3 text-[var(--brand-contrast)] peer-checked:visible\"\n />\n )}\n </span>\n );\n\n if (bare) return box;\n\n return (\n <div\n className={cn(\n // `items-start` + a 20px cell for the box: the box centres on the label's FIRST\n // line (text-sm is 20px tall), so a label that wraps keeps the box at the top\n // rather than floating to the middle of a paragraph.\n \"flex items-start gap-2\",\n // The whole row fades, the way ToggleGroup and the fields do, so the label does\n // not stay at full strength beside a box you cannot change.\n disabled && \"opacity-60\",\n className,\n )}\n >\n <span className=\"flex h-5 shrink-0 items-center\">{box}</span>\n <span className=\"min-w-0\">\n {label !== undefined && (\n <label\n htmlFor={inputId}\n className={cn(\n \"block text-sm leading-5 text-[var(--text-primary)]\",\n // `select-none`: a double-click to toggle twice would otherwise select the\n // words, and a drag that starts on them starts a native text drag.\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer select-none\",\n )}\n >\n {label}\n {required && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\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 {showError && (\n <span id={errorId} className={cn(\"block\", ERROR_CLASS)}>\n {error}\n </span>\n )}\n </span>\n </div>\n );\n});\nCheckbox.displayName = \"Checkbox\";\n"],"mappings":"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","names":["Checkbox"]}
1
+ {"version":3,"sources":["../../src/components/checkbox.tsx"],"sourcesContent":["import { forwardRef, useCallback, useContext, useEffect, useId, useRef } from \"react\";\nimport { Check, Minus } from \"lucide-react\";\nimport type { ChangeEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { assignRef, hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { LockedReason, RequiredStarOnLegend, useLockReason } from \"./field-parts\";\n\n/**\n * A checkbox that is still `<input type=\"checkbox\">`.\n *\n * The three consuming apps had three answers: ~30 raw inputs in Keksdose leaning on\n * `tokens.css`'s `accent-color`, a label-plus-box `CheckboxField` in Lenkbank, and a\n * Radix `<button role=\"checkbox\">` in Kastlan. The native element is the one that\n * needs no ARIA to be a checkbox — it submits with its form, answers to `required`,\n * resets with the form and takes a `<label for>` click — so it stays underneath, and\n * only its paint is replaced (`appearance-none`, a token-coloured box, and a glyph\n * laid over it that ignores the pointer so the click still lands on the input).\n *\n * `accent-color` alone was not enough: it tints the native box but cannot change its\n * border, so an INVALID checkbox had no way to say so, and the unchecked box kept the\n * browser's own grey outline in both themes.\n */\n\n// The box. 16px, like every raw checkbox it replaces, so a migration moves nothing.\n//\n// `focus-visible`, with an offset in the surface colour: a --brand ring touching a\n// --brand-filled (checked) box would be a ring painted in the colour it sits on —\n// present in the DOM and invisible on screen. The offset is what separates the two.\nconst BOX =\n \"peer size-4 shrink-0 cursor-pointer appearance-none rounded-[4px] border border-[var(--border-strong)] bg-[var(--bg-surface)] shadow-sm transition-colors \" +\n \"checked:border-[var(--brand)] checked:bg-[var(--brand)] \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed\";\n\n// A mixed box wears the checked fill whether or not the underlying input is checked:\n// `indeterminate` is a property the browser keeps separately from `checked`, and the\n// dash means \"some\", which is neither answer.\nconst BOX_MIXED = \"border-[var(--brand)] bg-[var(--brand)]\";\n\n// The same geometry as FIELD_INVALID — a border plus a 1px ring, so the highlight\n// survives fractional display scaling on every edge (see the note in ui.tsx). The\n// focus ring turns danger too, so focusing a wrong box does not repaint it as fine.\nconst BOX_INVALID =\n \"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] checked:border-[var(--danger-border-strong)] focus-visible:ring-[var(--danger)]\";\n\nconst ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nexport interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n /**\n * The words beside the box. Rendered as a `<label for>` rather than a label that\n * wraps the input, so `description` and `error` can sit under it without becoming\n * part of the accessible NAME — a wrapping label would have the screen reader read\n * the whole paragraph as the checkbox's name.\n */\n label?: ReactNode;\n /** Secondary text under the label (\"Used when no account is chosen\"). Pointed at by\n * `aria-describedby`, merged with any the caller passed. Kastlan's\n * `CheckboxField` `description` variant. */\n description?: ReactNode;\n /**\n * \"Some but not all\" — the header box of a list whose rows are partly selected.\n * A DOM property with no HTML attribute, so it is set on the element after every\n * render; it does not touch `checked`, and a click clears it (the browser's rule,\n * and the caller's `onChange` decides what the click means).\n */\n indeterminate?: boolean;\n /** See {@link Input}'s `invalid`: paints the box and sets `aria-invalid` together,\n * so the two cannot be spelled separately. */\n invalid?: boolean;\n /** What is wrong, in the caller's words (\"Accept the terms to continue\"). Rendered\n * under the label, attached with `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** The checked state as a boolean — the `(next) => …` shape Lenkbank's\n * `CheckboxField` and Radix's `onCheckedChange` both used, so a migration does not\n * have to unwrap `event.target.checked` at every call site. Fires alongside\n * `onChange`, never instead of it. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>` itself. `className` styles the OUTERMOST element —\n * the row when there is a label, the box's own wrapper when there is not — which\n * is where layout (`self-end pb-2` to sit on a field's baseline) belongs. */\n inputClassName?: string;\n /**\n * Must be ticked to submit — a consent, an acceptance before paying. Reaches the\n * `<input>` as the native `required` (so a `<form>` refuses to submit without it and a\n * screen reader announces \"required\"), and draws the kit's required mark after the\n * label, the same `aria-hidden` star as {@link Label}'s: the word is announced from\n * the control, so a star inside the name would only be read out as noise. Write the\n * label WITHOUT a literal \"*\". No mark without a label.\n */\n required?: boolean;\n /**\n * Why the box cannot be changed — {@link Button}'s `disabledReason`, for a checkbox\n * that SAVES on change (keksdose K3: a setting ticked straight into the server, kept\n * behind a hand-rolled `SaveGuard` that forced a native `disabled` — out of the tab\n * order, so the reason never reached a keyboard).\n *\n * With a reason the box is `aria-disabled` instead: still focusable, a click or Space\n * changes nothing and fires no `onChange` / `onCheckedChange`, the reason is in the\n * kit {@link Tooltip} on the box and on its `aria-describedby`, and the row fades as a\n * disabled one does. It wins over `disabled`. Changing the lock mounts the box anew (it\n * moves in or out of the Tooltip), as a locked Button is.\n */\n disabledReason?: ReactNode;\n /**\n * This box COMMITS — ticking it saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: boolean;\n}\n\n/**\n * A native checkbox in the kit's tokens, with an optional label, description and\n * error.\n *\n * Without `label`, `description` or `error` it renders the box and nothing else, so\n * it drops into a table cell or a caller's own `<label>` exactly as the raw input\n * did — give it an `aria-label` there. Every other prop reaches the `<input>`\n * (`checked`, `defaultChecked`, `name`, `required`, `data-*`, `aria-*`), so it works\n * controlled, uncontrolled, and inside a plain `<form>`.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n {\n label,\n description,\n indeterminate = false,\n invalid,\n error,\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n disabledReason,\n commit,\n onClick,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const inputId = id ?? generated;\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n // Locked is disabled the focusable way: the row fades and the words stop inviting a\n // click exactly as they do for `disabled`.\n const looksDisabled = Boolean(disabled) || locked;\n // In a CheckboxGroup the legend carries the star; the box keeps `required` itself.\n const starOnLegend = useContext(RequiredStarOnLegend);\n const descriptionId = `${inputId}-description`;\n const errorId = `${inputId}-error`;\n const showError = hasMessage(error);\n const showDescription = hasMessage(description);\n const isInvalid = Boolean(invalid) || showError;\n // Nothing to lay out beside the box: render the box alone, as the raw input was.\n const bare = label === undefined && !showDescription && !showError;\n\n const local = useRef<HTMLInputElement | null>(null);\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n local.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n // After EVERY render, not only when the prop changes: a click clears the property\n // in the DOM, and a controlled caller that keeps `indeterminate` true expects the\n // dash back on the next render even though the prop never changed.\n useEffect(() => {\n if (local.current) local.current.indeterminate = indeterminate;\n });\n\n const control = (\n <span className={cn(\"relative inline-flex shrink-0\", bare && !locked && className)}>\n <input\n ref={setRef}\n id={bare ? id : inputId}\n // A reason wins over `disabled`, as on Button: the point of giving one is that\n // the box stays reachable.\n disabled={locked ? undefined : disabled}\n required={required}\n {...rest}\n // After the spread, like Switch: a props object spread at a checkbox must not\n // be able to turn it into something else.\n type=\"checkbox\"\n aria-disabled={locked || rest[\"aria-disabled\"] || undefined}\n // OR-ed with the spread so a caller's own `aria-invalid` survives — Input's rule.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n locked && lock.reasonId,\n showError && errorId,\n )}\n // A locked box's click is swallowed, as a locked Button's is.\n onClick={locked ? undefined : onClick}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n if (locked) {\n // Put the tick back, and report nothing. Undone here rather than by\n // cancelling the click: a cancelled click is reverted by the browser AFTER\n // React has restored a controlled box, and jsdom reverts it by toggling, which\n // turned a restored tick back over (the suites the apps run would have seen a\n // locked box change). Setting the property also keeps React's own record of\n // the value in step, so an uncontrolled box reports its next real change.\n // Covers a click on the label and Space too — both arrive as a click.\n e.currentTarget.checked = !e.currentTarget.checked;\n e.currentTarget.indeterminate = indeterminate;\n return;\n }\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(\n BOX,\n indeterminate && BOX_MIXED,\n isInvalid && BOX_INVALID,\n locked && \"cursor-not-allowed\",\n inputClassName,\n )}\n />\n {/* The glyph is drawn over the box, not inside it (an input has no children),\n and ignores the pointer so a click on the tick still toggles the input. The\n tick is shown by `peer-checked`, so an UNCONTROLLED box shows it too; the\n dash replaces it outright while `indeterminate` holds. */}\n {indeterminate ? (\n <Minus\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none absolute inset-0 m-auto size-3 text-[var(--brand-contrast)]\"\n />\n ) : (\n <Check\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none invisible absolute inset-0 m-auto size-3 text-[var(--brand-contrast)] peer-checked:visible\"\n />\n )}\n </span>\n );\n // The reason's Tooltip goes round the BOX, not the row: the row is the caller's\n // layout (`className`), and a wrapper round it would move that into a span. A bare\n // box hands its `className` to the wrapper, which is then the outermost element.\n const box = locked ? (\n <LockedReason lock={lock} className={cn(\"shrink-0\", bare && className)}>\n {control}\n </LockedReason>\n ) : (\n control\n );\n\n if (bare) return box;\n\n return (\n <div\n className={cn(\n // `items-start` + a 20px cell for the box: the box centres on the label's FIRST\n // line (text-sm is 20px tall), so a label that wraps keeps the box at the top\n // rather than floating to the middle of a paragraph.\n \"flex items-start gap-2\",\n // The whole row fades, the way ToggleGroup and the fields do, so the label does\n // not stay at full strength beside a box you cannot change.\n looksDisabled && \"opacity-60\",\n className,\n )}\n >\n <span className=\"flex h-5 shrink-0 items-center\">{box}</span>\n <span className=\"min-w-0\">\n {label !== undefined && (\n <label\n htmlFor={inputId}\n className={cn(\n \"block text-sm leading-5 text-[var(--text-primary)]\",\n // `select-none`: a double-click to toggle twice would otherwise select the\n // words, and a drag that starts on them starts a native text drag.\n looksDisabled ? \"cursor-not-allowed\" : \"cursor-pointer select-none\",\n )}\n >\n {label}\n {required && !starOnLegend && (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n )}\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 {showError && (\n <span id={errorId} className={cn(\"block\", ERROR_CLASS)}>\n {error}\n </span>\n )}\n </span>\n </div>\n );\n});\nCheckbox.displayName = \"Checkbox\";\n"],"mappings":"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","names":["Checkbox"]}
@@ -209,6 +209,8 @@ type ChipProps = ChipBaseProps & ({
209
209
  * href, whichever element draws it.
210
210
  */
211
211
  renderLink?: (props: ChipLinkProps) => ReactElement;
212
+ commit?: never;
213
+ disabledReason?: never;
212
214
  } | {
213
215
  /** The link shape again, with an `onClick`. `href` is a required string here,
214
216
  * which is what lets TypeScript tell this shape from the button's: a bare
@@ -221,12 +223,28 @@ type ChipProps = ChipBaseProps & ({
221
223
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
222
224
  /** See the link shape above. */
223
225
  renderLink?: (props: ChipLinkProps) => ReactElement;
226
+ commit?: never;
227
+ disabledReason?: never;
224
228
  } | {
225
229
  href?: never;
226
230
  renderLink?: never;
227
231
  /** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives
228
232
  * the click, so a chip inside a clickable row can `stopPropagation()`. */
229
233
  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
234
+ /**
235
+ * Why the toggle cannot be pressed right now — {@link Button}'s
236
+ * `disabledReason`, on the chip's button shape (keksdose K3: a filter or flag
237
+ * chip that saves on press, under a read-only lock). The chip stays focusable
238
+ * and `aria-disabled`, a press (and its `onRemove` ×) does nothing, and the
239
+ * reason shows in the kit {@link Tooltip} and describes the chip. Wins over
240
+ * `disabled`. A link chip navigates rather than commits, so it takes neither
241
+ * this nor `commit`; an inert chip has nothing to refuse.
242
+ */
243
+ disabledReason?: ReactNode;
244
+ /** This chip COMMITS — pressing it saves. Under a locked
245
+ * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
246
+ * reason, as {@link Button}'s `commit` does. */
247
+ commit?: boolean;
230
248
  });
231
249
  /**
232
250
  * Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or
@@ -7,6 +7,8 @@ import { horizontalStep } from "../lib/direction.js";
7
7
  import { FIELD_INVALID } from "./ui.js";
8
8
  import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels.js";
9
9
  import { pickLinkRenderer } from "./text-link.js";
10
+ import { Tooltip } from "./tooltip.js";
11
+ import { useCommitReason } from "./write-lock.js";
10
12
  const TONE = {
11
13
  neutral: {
12
14
  idle: "border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]",
@@ -310,9 +312,14 @@ const Chip = forwardRef(function Chip2({
310
312
  onKeyDown,
311
313
  onFocus,
312
314
  snapEdges = false,
315
+ disabledReason: ownDisabledReason,
316
+ commit,
313
317
  ...rest
314
318
  }, ref) {
315
319
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
320
+ const reasonId = useId();
321
+ const disabledReason = useCommitReason(commit, ownDisabledReason);
322
+ const locked = Boolean(onClick) && !href && hasReason(disabledReason);
316
323
  const kitLink = useKitLink();
317
324
  const s = SIZE[size];
318
325
  const interactive = !!href || !!onClick;
@@ -321,7 +328,10 @@ const Chip = forwardRef(function Chip2({
321
328
  const type = cn(caps && CAPS[size], variant === "solid" && "justify-center tabular-nums");
322
329
  const surface = cn(
323
330
  surfaceOf(tone, variant, selected),
324
- disabled && "cursor-default opacity-50"
331
+ // A reason wins over `disabled`: the focusable kind of disabled, with the cursor
332
+ // that says "refused" rather than "nothing here".
333
+ disabled && !locked && "cursor-default opacity-50",
334
+ locked && "cursor-not-allowed opacity-50"
325
335
  );
326
336
  const snap = snapEdges && !dot;
327
337
  const snapRef = useSnapEdges(snap);
@@ -338,7 +348,7 @@ const Chip = forwardRef(function Chip2({
338
348
  type,
339
349
  radius,
340
350
  surface,
341
- interactive && !disabled && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
351
+ interactive && !disabled && !locked && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
342
352
  // An inert dot chip is text in a column: no inset, so it aligns with the header.
343
353
  dot && !interactive && "px-0",
344
354
  // The under-4px a snap adds is split between the two sides, not stacked at the end.
@@ -381,15 +391,20 @@ const Chip = forwardRef(function Chip2({
381
391
  onClick: (e) => {
382
392
  e.preventDefault();
383
393
  e.stopPropagation();
394
+ if (locked) return;
384
395
  onRemove();
385
396
  },
386
- disabled: disabled || removeDisabled,
397
+ disabled: disabled && !locked || removeDisabled,
398
+ "aria-disabled": locked || void 0,
399
+ "aria-describedby": locked ? reasonId : void 0,
387
400
  className: cn(
388
401
  // Logical margins: the × sits at the END of the pill, which is the left in RTL.
389
402
  "-me-0.5 ms-0.5 shrink-0 p-0.5 transition-colors",
390
403
  shape === "square" ? "rounded-sm" : "rounded-full",
391
- "hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
392
- (disabled || removeDisabled) && "pointer-events-none",
404
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
405
+ !locked && "hover:bg-[var(--bg-active)]",
406
+ locked && "cursor-not-allowed",
407
+ (disabled && !locked || removeDisabled) && "pointer-events-none",
393
408
  // A disabled chip is already dimmed as a whole; dimming the × again on top
394
409
  // would take it to a quarter. `removeDisabled` alone dims just the ×.
395
410
  removeDisabled && !disabled && "opacity-40"
@@ -407,8 +422,9 @@ const Chip = forwardRef(function Chip2({
407
422
  CHIP_RING,
408
423
  s.split,
409
424
  type,
410
- !disabled && "cursor-pointer hover:bg-current/10",
411
- disabled && "cursor-default"
425
+ !disabled && !locked && "cursor-pointer hover:bg-current/10",
426
+ disabled && !locked && "cursor-default",
427
+ locked && "cursor-not-allowed"
412
428
  );
413
429
  if (href && !disabled) {
414
430
  const onLinkClick = onClick;
@@ -442,22 +458,40 @@ const Chip = forwardRef(function Chip2({
442
458
  }
443
459
  if (onClick && !href) {
444
460
  const onButtonClick = onClick;
461
+ const ownDescribedBy = rest["aria-describedby"];
445
462
  const button = /* @__PURE__ */ jsx(
446
463
  "button",
447
464
  {
448
465
  ref: remove ? ref : ownRef,
449
466
  type: "button",
450
- onClick: onButtonClick,
451
- disabled,
467
+ onClick: (e) => {
468
+ if (locked) {
469
+ e.preventDefault();
470
+ return;
471
+ }
472
+ onButtonClick(e);
473
+ },
474
+ disabled: disabled && !locked,
452
475
  role: isCheckbox ? "checkbox" : void 0,
453
476
  "aria-pressed": isCheckbox ? void 0 : selected,
454
477
  "aria-checked": isCheckbox ? !!selected : void 0,
455
478
  className: remove ? inner : look,
456
479
  ...rest,
480
+ "aria-disabled": locked || rest["aria-disabled"],
481
+ "aria-describedby": locked ? [ownDescribedBy, reasonId].filter(Boolean).join(" ") : ownDescribedBy,
457
482
  children: body
458
483
  }
459
484
  );
460
- return remove ? pill(button) : button;
485
+ const shaped = remove ? pill(button) : button;
486
+ if (!locked) return shaped;
487
+ return (
488
+ // The chip in a FRAGMENT, as a locked Button is: the bubble is visual, and the
489
+ // hidden copy beside it is what the chip (and its ×) is described by.
490
+ /* @__PURE__ */ jsx(Tooltip, { label: disabledReason, children: /* @__PURE__ */ jsxs(Fragment, { children: [
491
+ shaped,
492
+ /* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: disabledReason })
493
+ ] }) })
494
+ );
461
495
  }
462
496
  return (
463
497
  /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
@@ -481,6 +515,9 @@ const Chip = forwardRef(function Chip2({
481
515
  )
482
516
  );
483
517
  });
518
+ function hasReason(node) {
519
+ return node !== void 0 && node !== null && node !== false && node !== "";
520
+ }
484
521
  function RenderedLink({
485
522
  render,
486
523
  ...props