@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
@@ -1,8 +1,9 @@
1
- import { forwardRef, useCallback, useEffect, useId, useRef } from "react";
1
+ import { forwardRef, useCallback, useContext, useEffect, useId, useRef } from "react";
2
2
  import { Check, Minus } from "lucide-react";
3
3
  import type { ChangeEvent, InputHTMLAttributes, ReactNode } from "react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts";
6
+ import { LockedReason, RequiredStarOnLegend, useLockReason } from "./field-parts";
6
7
 
7
8
  /**
8
9
  * A checkbox that is still `<input type="checkbox">`.
@@ -87,6 +88,25 @@ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement
87
88
  * label WITHOUT a literal "*". No mark without a label.
88
89
  */
89
90
  required?: boolean;
91
+ /**
92
+ * Why the box cannot be changed — {@link Button}'s `disabledReason`, for a checkbox
93
+ * that SAVES on change (keksdose K3: a setting ticked straight into the server, kept
94
+ * behind a hand-rolled `SaveGuard` that forced a native `disabled` — out of the tab
95
+ * order, so the reason never reached a keyboard).
96
+ *
97
+ * With a reason the box is `aria-disabled` instead: still focusable, a click or Space
98
+ * changes nothing and fires no `onChange` / `onCheckedChange`, the reason is in the
99
+ * kit {@link Tooltip} on the box and on its `aria-describedby`, and the row fades as a
100
+ * disabled one does. It wins over `disabled`. Changing the lock mounts the box anew (it
101
+ * moves in or out of the Tooltip), as a locked Button is.
102
+ */
103
+ disabledReason?: ReactNode;
104
+ /**
105
+ * This box COMMITS — ticking it saves. Under a locked {@link WriteLockProvider} it is
106
+ * locked the `disabledReason` way with the lock's reason (which wins over its own).
107
+ * No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.
108
+ */
109
+ commit?: boolean;
90
110
  }
91
111
 
92
112
  /**
@@ -113,12 +133,22 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
113
133
  id,
114
134
  disabled,
115
135
  required,
136
+ disabledReason,
137
+ commit,
138
+ onClick,
116
139
  ...rest
117
140
  },
118
141
  ref,
119
142
  ) {
120
143
  const generated = useId();
121
144
  const inputId = id ?? generated;
145
+ const lock = useLockReason(commit, disabledReason);
146
+ const locked = lock.locked;
147
+ // Locked is disabled the focusable way: the row fades and the words stop inviting a
148
+ // click exactly as they do for `disabled`.
149
+ const looksDisabled = Boolean(disabled) || locked;
150
+ // In a CheckboxGroup the legend carries the star; the box keeps `required` itself.
151
+ const starOnLegend = useContext(RequiredStarOnLegend);
122
152
  const descriptionId = `${inputId}-description`;
123
153
  const errorId = `${inputId}-error`;
124
154
  const showError = hasMessage(error);
@@ -142,29 +172,53 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
142
172
  if (local.current) local.current.indeterminate = indeterminate;
143
173
  });
144
174
 
145
- const box = (
146
- <span className={cn("relative inline-flex shrink-0", bare && className)}>
175
+ const control = (
176
+ <span className={cn("relative inline-flex shrink-0", bare && !locked && className)}>
147
177
  <input
148
178
  ref={setRef}
149
179
  id={bare ? id : inputId}
150
- disabled={disabled}
180
+ // A reason wins over `disabled`, as on Button: the point of giving one is that
181
+ // the box stays reachable.
182
+ disabled={locked ? undefined : disabled}
151
183
  required={required}
152
184
  {...rest}
153
185
  // After the spread, like Switch: a props object spread at a checkbox must not
154
186
  // be able to turn it into something else.
155
187
  type="checkbox"
188
+ aria-disabled={locked || rest["aria-disabled"] || undefined}
156
189
  // OR-ed with the spread so a caller's own `aria-invalid` survives — Input's rule.
157
190
  aria-invalid={isInvalid || rest["aria-invalid"] || undefined}
158
191
  aria-describedby={mergeDescribedBy(
159
192
  rest["aria-describedby"],
160
193
  showDescription && descriptionId,
194
+ locked && lock.reasonId,
161
195
  showError && errorId,
162
196
  )}
197
+ // A locked box's click is swallowed, as a locked Button's is.
198
+ onClick={locked ? undefined : onClick}
163
199
  onChange={(e: ChangeEvent<HTMLInputElement>) => {
200
+ if (locked) {
201
+ // Put the tick back, and report nothing. Undone here rather than by
202
+ // cancelling the click: a cancelled click is reverted by the browser AFTER
203
+ // React has restored a controlled box, and jsdom reverts it by toggling, which
204
+ // turned a restored tick back over (the suites the apps run would have seen a
205
+ // locked box change). Setting the property also keeps React's own record of
206
+ // the value in step, so an uncontrolled box reports its next real change.
207
+ // Covers a click on the label and Space too — both arrive as a click.
208
+ e.currentTarget.checked = !e.currentTarget.checked;
209
+ e.currentTarget.indeterminate = indeterminate;
210
+ return;
211
+ }
164
212
  onChange?.(e);
165
213
  onCheckedChange?.(e.target.checked);
166
214
  }}
167
- className={cn(BOX, indeterminate && BOX_MIXED, isInvalid && BOX_INVALID, inputClassName)}
215
+ className={cn(
216
+ BOX,
217
+ indeterminate && BOX_MIXED,
218
+ isInvalid && BOX_INVALID,
219
+ locked && "cursor-not-allowed",
220
+ inputClassName,
221
+ )}
168
222
  />
169
223
  {/* The glyph is drawn over the box, not inside it (an input has no children),
170
224
  and ignores the pointer so a click on the tick still toggles the input. The
@@ -185,6 +239,16 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
185
239
  )}
186
240
  </span>
187
241
  );
242
+ // The reason's Tooltip goes round the BOX, not the row: the row is the caller's
243
+ // layout (`className`), and a wrapper round it would move that into a span. A bare
244
+ // box hands its `className` to the wrapper, which is then the outermost element.
245
+ const box = locked ? (
246
+ <LockedReason lock={lock} className={cn("shrink-0", bare && className)}>
247
+ {control}
248
+ </LockedReason>
249
+ ) : (
250
+ control
251
+ );
188
252
 
189
253
  if (bare) return box;
190
254
 
@@ -197,7 +261,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
197
261
  "flex items-start gap-2",
198
262
  // The whole row fades, the way ToggleGroup and the fields do, so the label does
199
263
  // not stay at full strength beside a box you cannot change.
200
- disabled && "opacity-60",
264
+ looksDisabled && "opacity-60",
201
265
  className,
202
266
  )}
203
267
  >
@@ -210,11 +274,11 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
210
274
  "block text-sm leading-5 text-[var(--text-primary)]",
211
275
  // `select-none`: a double-click to toggle twice would otherwise select the
212
276
  // words, and a drag that starts on them starts a native text drag.
213
- disabled ? "cursor-not-allowed" : "cursor-pointer select-none",
277
+ looksDisabled ? "cursor-not-allowed" : "cursor-pointer select-none",
214
278
  )}
215
279
  >
216
280
  {label}
217
- {required && (
281
+ {required && !starOnLegend && (
218
282
  <span aria-hidden className="ms-0.5 text-[var(--danger)]">
219
283
  *
220
284
  </span>
@@ -7,6 +7,8 @@ import { horizontalStep } from "../lib/direction";
7
7
  import { FIELD_INVALID } from "./ui";
8
8
  import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels";
9
9
  import { pickLinkRenderer } from "./text-link";
10
+ import { Tooltip } from "./tooltip";
11
+ import { useCommitReason } from "./write-lock";
10
12
 
11
13
  /**
12
14
  * A chip: a compact pill carrying one value.
@@ -623,6 +625,8 @@ export type ChipProps = ChipBaseProps &
623
625
  * href, whichever element draws it.
624
626
  */
625
627
  renderLink?: (props: ChipLinkProps) => ReactElement;
628
+ commit?: never;
629
+ disabledReason?: never;
626
630
  }
627
631
  | {
628
632
  /** The link shape again, with an `onClick`. `href` is a required string here,
@@ -636,6 +640,8 @@ export type ChipProps = ChipBaseProps &
636
640
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
637
641
  /** See the link shape above. */
638
642
  renderLink?: (props: ChipLinkProps) => ReactElement;
643
+ commit?: never;
644
+ disabledReason?: never;
639
645
  }
640
646
  | {
641
647
  href?: never;
@@ -643,6 +649,20 @@ export type ChipProps = ChipBaseProps &
643
649
  /** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives
644
650
  * the click, so a chip inside a clickable row can `stopPropagation()`. */
645
651
  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
652
+ /**
653
+ * Why the toggle cannot be pressed right now — {@link Button}'s
654
+ * `disabledReason`, on the chip's button shape (keksdose K3: a filter or flag
655
+ * chip that saves on press, under a read-only lock). The chip stays focusable
656
+ * and `aria-disabled`, a press (and its `onRemove` ×) does nothing, and the
657
+ * reason shows in the kit {@link Tooltip} and describes the chip. Wins over
658
+ * `disabled`. A link chip navigates rather than commits, so it takes neither
659
+ * this nor `commit`; an inert chip has nothing to refuse.
660
+ */
661
+ disabledReason?: ReactNode;
662
+ /** This chip COMMITS — pressing it saves. Under a locked
663
+ * {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
664
+ * reason, as {@link Button}'s `commit` does. */
665
+ commit?: boolean;
646
666
  }
647
667
  );
648
668
 
@@ -678,11 +698,17 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
678
698
  onKeyDown,
679
699
  onFocus,
680
700
  snapEdges = false,
701
+ disabledReason: ownDisabledReason,
702
+ commit,
681
703
  ...rest
682
704
  },
683
705
  ref,
684
706
  ) {
685
707
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
708
+ const reasonId = useId();
709
+ const disabledReason = useCommitReason(commit, ownDisabledReason);
710
+ // Only the BUTTON shape commits: a link navigates, an inert chip does nothing.
711
+ const locked = Boolean(onClick) && !href && hasReason(disabledReason);
686
712
  const kitLink = useKitLink();
687
713
  const s = SIZE[size];
688
714
  const interactive = !!href || !!onClick;
@@ -694,7 +720,10 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
694
720
  const type = cn(caps && CAPS[size], variant === "solid" && "justify-center tabular-nums");
695
721
  const surface = cn(
696
722
  surfaceOf(tone, variant, selected),
697
- disabled && "cursor-default opacity-50",
723
+ // A reason wins over `disabled`: the focusable kind of disabled, with the cursor
724
+ // that says "refused" rather than "nothing here".
725
+ disabled && !locked && "cursor-default opacity-50",
726
+ locked && "cursor-not-allowed opacity-50",
698
727
  );
699
728
  const snap = snapEdges && !dot;
700
729
  const snapRef = useSnapEdges(snap);
@@ -713,7 +742,7 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
713
742
  type,
714
743
  radius,
715
744
  surface,
716
- interactive && !disabled && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
745
+ interactive && !disabled && !locked && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
717
746
  // An inert dot chip is text in a column: no inset, so it aligns with the header.
718
747
  dot && !interactive && "px-0",
719
748
  // The under-4px a snap adds is split between the two sides, not stacked at the end.
@@ -781,15 +810,21 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
781
810
  // The chip may itself be a link; removing it must not also follow it.
782
811
  e.preventDefault();
783
812
  e.stopPropagation();
813
+ // Removing a value from a chip that commits is a commit too.
814
+ if (locked) return;
784
815
  onRemove();
785
816
  }}
786
- disabled={disabled || removeDisabled}
817
+ disabled={(disabled && !locked) || removeDisabled}
818
+ aria-disabled={locked || undefined}
819
+ aria-describedby={locked ? reasonId : undefined}
787
820
  className={cn(
788
821
  // Logical margins: the × sits at the END of the pill, which is the left in RTL.
789
822
  "-me-0.5 ms-0.5 shrink-0 p-0.5 transition-colors",
790
823
  shape === "square" ? "rounded-sm" : "rounded-full",
791
- "hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
792
- (disabled || removeDisabled) && "pointer-events-none",
824
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
825
+ !locked && "hover:bg-[var(--bg-active)]",
826
+ locked && "cursor-not-allowed",
827
+ ((disabled && !locked) || removeDisabled) && "pointer-events-none",
793
828
  // A disabled chip is already dimmed as a whole; dimming the × again on top
794
829
  // would take it to a quarter. `removeDisabled` alone dims just the ×.
795
830
  removeDisabled && !disabled && "opacity-40",
@@ -814,8 +849,9 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
814
849
  CHIP_RING,
815
850
  s.split,
816
851
  type,
817
- !disabled && "cursor-pointer hover:bg-current/10",
818
- disabled && "cursor-default",
852
+ !disabled && !locked && "cursor-pointer hover:bg-current/10",
853
+ disabled && !locked && "cursor-default",
854
+ locked && "cursor-not-allowed",
819
855
  );
820
856
 
821
857
  // ── The three shapes.
@@ -855,12 +891,21 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
855
891
  // turn it into a button instead — it falls through to the inert span, as before.
856
892
  if (onClick && !href) {
857
893
  const onButtonClick = onClick as (event: MouseEvent<HTMLButtonElement>) => void;
894
+ const ownDescribedBy = rest["aria-describedby"];
858
895
  const button = (
859
896
  <button
860
897
  ref={(remove ? ref : ownRef) as React.Ref<HTMLButtonElement>}
861
898
  type="button"
862
- onClick={onButtonClick}
863
- disabled={disabled}
899
+ onClick={(e) => {
900
+ // Locked: the press is swallowed — the chip saves on press, and a lock that
901
+ // let it through would toggle a state that is never stored.
902
+ if (locked) {
903
+ e.preventDefault();
904
+ return;
905
+ }
906
+ onButtonClick(e);
907
+ }}
908
+ disabled={disabled && !locked}
864
909
  // Only when `selected` is PASSED: a chip with `onClick` and no `selected` is an
865
910
  // action button (it does something), not a toggle (it is on or off), and
866
911
  // announcing it "not pressed" would claim a state it does not have.
@@ -871,11 +916,30 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
871
916
  aria-checked={isCheckbox ? !!selected : undefined}
872
917
  className={remove ? inner : look}
873
918
  {...rest}
919
+ // After the spread, merged with it: the lock's state and reason are not the
920
+ // caller's to drop by passing an `aria-describedby` of their own.
921
+ aria-disabled={locked || (rest["aria-disabled"] as boolean | "true" | "false" | undefined)}
922
+ aria-describedby={
923
+ locked ? [ownDescribedBy, reasonId].filter(Boolean).join(" ") : (ownDescribedBy as string | undefined)
924
+ }
874
925
  >
875
926
  {body}
876
927
  </button>
877
928
  );
878
- return remove ? pill(button) : button;
929
+ const shaped = remove ? pill(button) : button;
930
+ if (!locked) return shaped;
931
+ return (
932
+ // The chip in a FRAGMENT, as a locked Button is: the bubble is visual, and the
933
+ // hidden copy beside it is what the chip (and its ×) is described by.
934
+ <Tooltip label={disabledReason}>
935
+ <>
936
+ {shaped}
937
+ <span id={reasonId} hidden>
938
+ {disabledReason}
939
+ </span>
940
+ </>
941
+ </Tooltip>
942
+ );
879
943
  }
880
944
 
881
945
  return (
@@ -897,6 +961,12 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
897
961
  );
898
962
  });
899
963
 
964
+ /** `null`, `false` and `""` are what a `cond && reason` gives on the happy path — no
965
+ * reason, so no lock. */
966
+ function hasReason(node: ReactNode): boolean {
967
+ return node !== undefined && node !== null && node !== false && node !== "";
968
+ }
969
+
900
970
  /** Calls a chip's `renderLink` with the props it was given — see its call site. */
901
971
  function RenderedLink({
902
972
  render,