@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,293 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useAnnounce } from "../hooks/use-announce.js";
6
+ import { useKitLabels } from "../i18n/kit-labels.js";
7
+ import { Input, Spinner } from "./ui.js";
8
+ import { Tooltip } from "./tooltip.js";
9
+ import { useCommitReason } from "./write-lock.js";
10
+ const DEFAULT_INLINE_EDIT_LABELS = {
11
+ edit: (label) => `Edit ${label}`,
12
+ failed: "The change could not be saved.",
13
+ empty: "Empty"
14
+ };
15
+ function hasContent(node) {
16
+ return node !== void 0 && node !== null && node !== false && node !== "";
17
+ }
18
+ function RenderEditor({ render, ...props }) {
19
+ return render(props);
20
+ }
21
+ function asPromise(result) {
22
+ return result && typeof result.then === "function" ? result : null;
23
+ }
24
+ function InlineEditField(props) {
25
+ const {
26
+ value,
27
+ onCommit,
28
+ label,
29
+ display,
30
+ isEqual = Object.is,
31
+ placeholder,
32
+ align = "start",
33
+ readOnly = false,
34
+ disabledReason: ownDisabledReason,
35
+ commit,
36
+ redact = false,
37
+ formatError,
38
+ editing: editingProp,
39
+ onEditingChange,
40
+ inputProps,
41
+ className,
42
+ labels: labelsProp
43
+ } = props;
44
+ const editor = props.editor;
45
+ const labels = useKitLabels("inlineEdit", DEFAULT_INLINE_EDIT_LABELS, labelsProp);
46
+ const disabledReason = useCommitReason(commit, ownDisabledReason);
47
+ const locked = hasContent(disabledReason);
48
+ const [openState, setOpenState] = useState(false);
49
+ const open = editingProp ?? openState;
50
+ const editing = open && !locked && !readOnly;
51
+ const [draft, setDraft] = useState(value);
52
+ const [pending, setPending] = useState(false);
53
+ const [failure, setFailure] = useState(null);
54
+ const failed = hasContent(failure);
55
+ const errorId = useId();
56
+ const reasonId = useId();
57
+ const displayRef = useRef(null);
58
+ const editorRef = useRef(null);
59
+ const alert = useAnnounce({ politeness: "assertive" });
60
+ const ended = useRef(false);
61
+ const blurTimer = useRef(null);
62
+ const mounted = useRef(true);
63
+ const draftRef = useRef(draft);
64
+ useEffect(() => {
65
+ draftRef.current = draft;
66
+ });
67
+ useEffect(() => {
68
+ mounted.current = true;
69
+ return () => {
70
+ mounted.current = false;
71
+ if (blurTimer.current) clearTimeout(blurTimer.current);
72
+ };
73
+ }, []);
74
+ const [wasEditing, setWasEditing] = useState(editing);
75
+ if (wasEditing !== editing) {
76
+ setWasEditing(editing);
77
+ if (editing) {
78
+ setDraft(value);
79
+ setFailure(null);
80
+ setPending(false);
81
+ }
82
+ }
83
+ if (openState && !editing && editingProp === void 0) setOpenState(false);
84
+ const prevEditing = useRef(editing);
85
+ useEffect(() => {
86
+ const changed = prevEditing.current !== editing;
87
+ prevEditing.current = editing;
88
+ if (!changed) return;
89
+ if (editing) {
90
+ ended.current = false;
91
+ const control = editorRef.current?.querySelector(
92
+ "input:not([type=hidden]), textarea, select, [contenteditable='true'], [tabindex]:not([tabindex='-1']), button"
93
+ );
94
+ control?.focus();
95
+ if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) control.select();
96
+ return;
97
+ }
98
+ ended.current = true;
99
+ if (blurTimer.current) clearTimeout(blurTimer.current);
100
+ blurTimer.current = null;
101
+ const lost = document.activeElement === null || document.activeElement === document.body;
102
+ if (lost) displayRef.current?.focus();
103
+ }, [editing]);
104
+ const setOpen = (next) => {
105
+ if (editingProp === void 0) setOpenState(next);
106
+ onEditingChange?.(next);
107
+ };
108
+ const fail = (caught) => {
109
+ ended.current = false;
110
+ const message = formatError ? formatError(caught) : labels.failed;
111
+ setFailure(hasContent(message) ? message : labels.failed);
112
+ alert.announce(typeof message === "string" && message !== "" ? message : labels.failed);
113
+ const active = document.activeElement;
114
+ if (active === null || active === document.body) {
115
+ editorRef.current?.querySelector("input, textarea, select, [tabindex]")?.focus();
116
+ }
117
+ };
118
+ const save = (next = draftRef.current) => {
119
+ if (!editing || ended.current || pending) return;
120
+ ended.current = true;
121
+ if (next !== draftRef.current) setDraft(next);
122
+ if (isEqual(next, value)) {
123
+ setOpen(false);
124
+ return;
125
+ }
126
+ let result;
127
+ try {
128
+ result = onCommit(next);
129
+ } catch (caught) {
130
+ fail(caught);
131
+ return;
132
+ }
133
+ const promise = asPromise(result);
134
+ if (!promise) {
135
+ setOpen(false);
136
+ return;
137
+ }
138
+ setPending(true);
139
+ setFailure(null);
140
+ promise.then(
141
+ () => {
142
+ if (!mounted.current) return;
143
+ setPending(false);
144
+ setOpen(false);
145
+ },
146
+ (caught) => {
147
+ if (!mounted.current) return;
148
+ setPending(false);
149
+ fail(caught);
150
+ }
151
+ );
152
+ };
153
+ const cancel = () => {
154
+ if (!editing || pending) return;
155
+ ended.current = true;
156
+ setOpen(false);
157
+ };
158
+ const change = (next) => {
159
+ if (pending) return;
160
+ setDraft(next);
161
+ draftRef.current = next;
162
+ setFailure(null);
163
+ };
164
+ const onKeyDown = (e) => {
165
+ if (e.defaultPrevented) return;
166
+ if (e.key === "Escape") {
167
+ e.preventDefault();
168
+ e.stopPropagation();
169
+ cancel();
170
+ return;
171
+ }
172
+ if (e.key === "Enter" && !e.nativeEvent.isComposing && !e.shiftKey) {
173
+ e.preventDefault();
174
+ save();
175
+ }
176
+ };
177
+ const onBlur = (e) => {
178
+ if (e.currentTarget.contains(e.relatedTarget)) return;
179
+ if (blurTimer.current) clearTimeout(blurTimer.current);
180
+ blurTimer.current = setTimeout(() => {
181
+ blurTimer.current = null;
182
+ if (!mounted.current || failed) return;
183
+ save();
184
+ }, 0);
185
+ };
186
+ const onFocus = () => {
187
+ if (blurTimer.current) clearTimeout(blurTimer.current);
188
+ blurTimer.current = null;
189
+ };
190
+ const private_ = redact ? { "data-private": true } : void 0;
191
+ const shown = display ? display(value) : value === null || value === void 0 ? "" : String(value);
192
+ const content = hasContent(shown) ? shown : /* @__PURE__ */ jsx("span", { className: "text-[var(--text-muted)]", children: placeholder ?? labels.empty });
193
+ const alignClass = align === "end" ? "text-end" : "text-start";
194
+ let body;
195
+ if (readOnly) {
196
+ body = /* @__PURE__ */ jsx("span", { ...private_, className: cn("block min-w-0 truncate", alignClass), children: content });
197
+ } else if (editing) {
198
+ const describedBy = failed ? errorId : void 0;
199
+ const editorProps = {
200
+ value: draft,
201
+ onChange: change,
202
+ commit: save,
203
+ cancel,
204
+ pending,
205
+ label,
206
+ invalid: failed,
207
+ describedBy,
208
+ inputProps: {
209
+ "aria-label": label,
210
+ "aria-invalid": failed || void 0,
211
+ "aria-describedby": describedBy,
212
+ readOnly: pending
213
+ }
214
+ };
215
+ body = // A wrapper for the keys and the focus, not a control: the keys belong to the
216
+ // field inside it, which is what has focus.
217
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- see above
218
+ /* @__PURE__ */ jsxs(
219
+ "div",
220
+ {
221
+ ref: editorRef,
222
+ ...private_,
223
+ onKeyDown,
224
+ onBlur,
225
+ onFocus,
226
+ "aria-busy": pending || void 0,
227
+ className: "min-w-0",
228
+ children: [
229
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
230
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: editor ? /* @__PURE__ */ jsx(RenderEditor, { render: editor, ...editorProps }) : /* @__PURE__ */ jsx(
231
+ Input,
232
+ {
233
+ ...inputProps,
234
+ ...editorProps.inputProps,
235
+ value: String(draft ?? ""),
236
+ onChange: (e) => change(e.target.value),
237
+ className: cn("px-2 py-1", alignClass, inputProps?.className)
238
+ }
239
+ ) }),
240
+ pending && /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-4 shrink-0" })
241
+ ] }),
242
+ failed && /* @__PURE__ */ jsx("p", { id: errorId, className: "mt-1 text-[11px] leading-tight text-[var(--danger)]", children: failure })
243
+ ]
244
+ }
245
+ );
246
+ } else if (locked) {
247
+ body = /* @__PURE__ */ jsx(Tooltip, { label: disabledReason, className: "flex w-full min-w-0", children: /* @__PURE__ */ jsxs(Fragment, { children: [
248
+ /* @__PURE__ */ jsx(
249
+ "button",
250
+ {
251
+ ref: displayRef,
252
+ type: "button",
253
+ ...private_,
254
+ "aria-disabled": true,
255
+ "aria-describedby": reasonId,
256
+ onClick: (e) => e.preventDefault(),
257
+ className: cn(
258
+ "block w-full min-w-0 cursor-not-allowed truncate rounded-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
259
+ alignClass
260
+ ),
261
+ children: content
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: disabledReason })
265
+ ] }) });
266
+ } else {
267
+ body = // Lazy: a table of these mounts no bubble until one is hovered or focused, and
268
+ // the closed bubble's text is in no ancestor's accessible name.
269
+ /* @__PURE__ */ jsx(Tooltip, { label: labels.edit(label), lazy: true, className: "flex w-full min-w-0", children: /* @__PURE__ */ jsx(
270
+ "button",
271
+ {
272
+ ref: displayRef,
273
+ type: "button",
274
+ ...private_,
275
+ onClick: () => setOpen(true),
276
+ className: cn(
277
+ "block w-full min-w-0 truncate rounded-sm underline-offset-2 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
278
+ alignClass
279
+ ),
280
+ children: content
281
+ }
282
+ ) });
283
+ }
284
+ return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), "data-editing": editing || void 0, children: [
285
+ body,
286
+ /* @__PURE__ */ jsx("span", { ...alert.regionProps })
287
+ ] });
288
+ }
289
+ export {
290
+ DEFAULT_INLINE_EDIT_LABELS,
291
+ InlineEditField
292
+ };
293
+ //# sourceMappingURL=inline-edit-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/inline-edit-field.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { FocusEvent, InputHTMLAttributes, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Input, Spinner } from \"./ui\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason } from \"./write-lock\";\n\n/**\n * Every string {@link InlineEditField} says on its own behalf — the `inlineEdit`\n * namespace of `<UiKitProvider labels>`, overridable per instance through `labels`.\n */\nexport interface InlineEditLabels {\n /** The display button's tooltip and description — what pressing it does. Receives\n * the field's `label` (\"Budget name\" → \"Edit Budget name\"). */\n edit: (label: string) => string;\n /** Shown (and spoken) when `onCommit` rejects and no `formatError` says better. */\n failed: string;\n /** What the display shows for an empty value, when no `placeholder` is given. */\n empty: string;\n}\n\nexport const DEFAULT_INLINE_EDIT_LABELS: InlineEditLabels = {\n edit: (label) => `Edit ${label}`,\n failed: \"The change could not be saved.\",\n empty: \"Empty\",\n};\n\n/** What an `editor` render prop is handed: the draft and the means to end the edit. */\nexport interface InlineEditorProps<T> {\n /** The draft — seeded from `value` each time the editor opens. */\n value: T;\n /** Replace the draft (every keystroke). */\n onChange: (next: T) => void;\n /**\n * End the edit and save. Without an argument it saves the draft; WITH one it saves\n * that — for an editor whose own commit hands over a resolved value (`NumberInput`'s\n * `onCommit` gives \"1050\" for a typed \"1100-50\"). Called twice (the editor's own\n * Enter, then the field's), the second call does nothing.\n */\n commit: (next?: T) => void;\n /** End the edit and drop the draft — what Escape does. */\n cancel: () => void;\n /** The save is in flight: make the control read-only (not disabled — it has focus). */\n pending: boolean;\n /** The field's `label`, for the control's accessible name. */\n label: string;\n /** The last save failed and the draft has not been touched since. */\n invalid: boolean;\n /** The id of the failure message while there is one, for `aria-describedby`. */\n describedBy: string | undefined;\n /** The four above as attributes — spread them on a kit field that takes the DOM\n * spelling: `<AmountInput {...p.inputProps} … />`. */\n inputProps: {\n \"aria-label\": string;\n \"aria-invalid\": true | undefined;\n \"aria-describedby\": string | undefined;\n readOnly: boolean;\n };\n}\n\ninterface InlineEditFieldBaseProps<T> {\n /** The saved value — what the display shows, and what the editor opens on. */\n value: T;\n /**\n * Save `next`. Return a promise and the field waits: the editor stays open and\n * read-only with a spinner until it settles, closes when it resolves, and STAYS OPEN\n * with the failure under it when it rejects (or throws) — the typed value is kept for\n * a retry. Not called when the draft equals `value` (see `isEqual`): opening the\n * editor and leaving it unchanged is not a write.\n */\n onCommit: (next: T) => void | Promise<unknown>;\n /** What the value is (\"Budget name\", \"Assigned\") — the editor's accessible name and\n * the subject of the display's \"Edit …\" description. Not drawn: inline, the row or\n * the column header is the visible label. */\n label: string;\n /** The value as the display shows it — a formatted amount, a date. Default: the\n * value as text. */\n display?: (value: T) => ReactNode;\n /** Whether the draft is unchanged, so leaving the editor is not a write. Default\n * `Object.is`. keksdose compares assigned amounts as NUMBERS — \"1100\" is the\n * server's \"1100.0000\". */\n isEqual?: (a: T, b: T) => boolean;\n /** What the display shows for an empty value (`\"\"`, `null`, an empty `display`).\n * Default `labels.empty`. */\n placeholder?: ReactNode;\n /** `\"end\"` for a figure in a right-aligned money column. Default `\"start\"`. */\n align?: \"start\" | \"end\";\n /** Look, do not touch — a guest's view: the value as plain text, no button. */\n readOnly?: boolean;\n /**\n * Why it cannot be edited now — the write lock's sentence, a closed period. The\n * value stays a BUTTON (focusable, `aria-disabled`) with the reason in the kit\n * Tooltip and its description; it opens no editor. An editor that is open when the\n * reason arrives closes, its draft dropped: a field you can type into must be one\n * whose value can be saved (keksdose dev#496).\n */\n disabledReason?: ReactNode;\n /** This field COMMITS — under a locked {@link WriteLockProvider} it takes the\n * `disabledReason` path with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. */\n commit?: boolean;\n /** The value is the user's own data: `data-private` on the display and the editor,\n * so the app's demo-mode blur covers both. */\n redact?: boolean;\n /** Turns a rejection into words. Default `labels.failed`. */\n formatError?: (error: unknown) => ReactNode;\n /** Controlled open state — a row's swipe action that opens the editor from outside\n * (keksdose's assigned cell). Left out, the field keeps its own. */\n editing?: boolean;\n /** Called with the next open state: on open, on a save that resolved, on cancel. */\n onEditingChange?: (editing: boolean) => void;\n /** Attributes for the DEFAULT text editor's `<input>` — `maxLength`, `inputMode`,\n * `autoComplete`. Ignored with `editor`. */\n inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, \"value\" | \"defaultValue\" | \"onChange\" | \"type\">;\n /** Classes for the root. */\n className?: string;\n labels?: Partial<InlineEditLabels>;\n}\n\ntype EditorRender<T> = (props: InlineEditorProps<T>) => ReactNode;\n\n/**\n * The editor. Optional for a string value — the kit's `Input`, compact — and\n * REQUIRED for anything else, since only the caller knows how to edit a number or an\n * amount. Render a kit field from the props it is handed:\n *\n * ```tsx\n * editor={(p) => (\n * <NumberInput calculator={false} ariaLabel={p.label} value={p.value} onChange={p.onChange}\n * onCommit={p.commit} aria-invalid={p.invalid || undefined} aria-describedby={p.describedBy} />\n * )}\n * ```\n *\n * The field itself handles Enter (save), Escape (cancel) and focus leaving the editor\n * (save) around whatever is rendered — a key the editor has already handled\n * (`preventDefault`, as a listbox does for its Enter) is left to it. Focus counts as\n * \"inside\" across a portal, so a picker or a phone keypad the editor opens does not\n * end the edit.\n */\nexport type InlineEditFieldProps<T = string> = InlineEditFieldBaseProps<T> &\n ([T] extends [string] ? { editor?: EditorRender<T> } : { editor: EditorRender<T> });\n\nfunction hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\n/** Calls the `editor` render prop as a component of its own: the props it is handed\n * close over the field's refs, and a render prop called in the field's own render\n * would read like a ref read during render. Module-level, so the type is stable and\n * the editor never remounts. */\nfunction RenderEditor<T>({ render, ...props }: InlineEditorProps<T> & { render: EditorRender<T> }) {\n return render(props);\n}\n\n/** `onCommit`'s result, if it is a promise. */\nfunction asPromise(result: unknown): Promise<unknown> | null {\n return result && typeof (result as Promise<unknown>).then === \"function\" ? (result as Promise<unknown>) : null;\n}\n\n/**\n * A value that edits in place: a quiet button showing the value; click it (or Enter\n * on it) and it becomes a field; Enter or leaving the field saves, Escape cancels\n * (keksdose K9).\n *\n * keksdose has three of them by hand — the budget's assigned cell (budget-cells.tsx),\n * the invoice line's value cells (invoice-lines-table.tsx `EditableCell`) and the\n * budget rename (budgets-page.tsx, an `Input` + Save + Cancel with no Escape) — and\n * each had to learn the same rules:\n *\n * - **The display's accessible name is the VALUE.** A cell named \"Edit\" reads a table\n * of \"edit, edit, edit\"; the action is the description instead (\"Edit Assigned\"),\n * in the kit Tooltip on hover and focus.\n * - **An unchanged draft is not a write.** Opening the editor and leaving it was a PUT\n * in keksdose until `isEqual` compared numbers, and on a sealed budget an outbox row\n * for a change nobody made.\n * - **A failed save keeps the editor.** The typed value is the user's work; the\n * failure is shown under it (attached with `aria-describedby`, and spoken), and the\n * next keystroke clears it. Escape still drops the draft.\n * - **Locked means a disabled BUTTON, not a disabled field** (keksdose dev#496): at\n * rest a real row shows this button, so a locked page shows the same, with the\n * reason — never a field that takes keys it cannot save.\n *\n * Inside a {@link DataTable} cell it needs nothing extra: the table already leaves a\n * click or a key on a control of the cell to that control. Focus returns to the value\n * after Enter or Escape; after a save that came from focus leaving, it stays where the\n * user put it.\n *\n * ```tsx\n * <InlineEditField label={t(\"budgets.name\")} value={budget.name}\n * onCommit={(name) => rename.mutateAsync({ id: budget.id, name })} inputProps={{ maxLength: 120 }} />\n * ```\n */\nexport function InlineEditField<T = string>(props: InlineEditFieldProps<T>) {\n const {\n value,\n onCommit,\n label,\n display,\n isEqual = Object.is,\n placeholder,\n align = \"start\",\n readOnly = false,\n disabledReason: ownDisabledReason,\n commit,\n redact = false,\n formatError,\n editing: editingProp,\n onEditingChange,\n inputProps,\n className,\n labels: labelsProp,\n } = props;\n const editor = props.editor as EditorRender<T> | undefined;\n const labels = useKitLabels(\"inlineEdit\", DEFAULT_INLINE_EDIT_LABELS, labelsProp);\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n\n const [openState, setOpenState] = useState(false);\n const open = editingProp ?? openState;\n // Locked (or read-only) wins over an open flag: a stale `editing` must never bring a\n // live commit-on-blur field back on a page that cannot save it.\n const editing = open && !locked && !readOnly;\n const [draft, setDraft] = useState<T>(value);\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const failed = hasContent(failure);\n\n const errorId = useId();\n const reasonId = useId();\n const displayRef = useRef<HTMLButtonElement>(null);\n const editorRef = useRef<HTMLDivElement>(null);\n const alert = useAnnounce({ politeness: \"assertive\" });\n\n // One edit ends once: Enter in a NumberInput commits through its own `onCommit` and\n // then reaches the field's Enter; a save that closes the editor unmounts the focused\n // control, which some browsers answer with a blur. The second ending is a no-op.\n const ended = useRef(false);\n const blurTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const mounted = useRef(true);\n // The latest draft for the deferred blur check, which runs after a render.\n const draftRef = useRef(draft);\n useEffect(() => {\n draftRef.current = draft;\n });\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n };\n }, []);\n\n // Seed the draft when the editor opens — from wherever the open came (the button, a\n // controlled `editing`). During render, like DangerConfirm's wipe, so no frame shows\n // the editor holding the previous edit's text.\n const [wasEditing, setWasEditing] = useState(editing);\n if (wasEditing !== editing) {\n setWasEditing(editing);\n if (editing) {\n setDraft(value);\n setFailure(null);\n setPending(false);\n }\n }\n // An open editor that a lock (or `readOnly`) took away is closed for good, not\n // merely hidden until the lock lifts.\n if (openState && !editing && editingProp === undefined) setOpenState(false);\n\n // Focus follows the edit: into the editor when it opens, back to the value when it\n // closes — but only if focus went down with the editor (Enter, Escape, a save that\n // resolved under it). A save that came from focus LEAVING keeps focus where it went.\n const prevEditing = useRef(editing);\n useEffect(() => {\n const changed = prevEditing.current !== editing;\n prevEditing.current = editing;\n if (!changed) return;\n if (editing) {\n // A fresh edit, which may end once. Here rather than in the seed above: a ref is\n // not written during render, and no event can end the edit before this runs.\n ended.current = false;\n const control = editorRef.current?.querySelector<HTMLElement>(\n \"input:not([type=hidden]), textarea, select, [contenteditable='true'], [tabindex]:not([tabindex='-1']), button\",\n );\n control?.focus();\n if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) control.select();\n return;\n }\n // However it closed — a save, Escape, a lock taking it away — a blur check still\n // queued from the editor has nothing left to save.\n ended.current = true;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = null;\n const lost = document.activeElement === null || document.activeElement === document.body;\n if (lost) displayRef.current?.focus();\n }, [editing]);\n\n const setOpen = (next: boolean) => {\n if (editingProp === undefined) setOpenState(next);\n onEditingChange?.(next);\n };\n\n const fail = (caught: unknown) => {\n ended.current = false;\n const message = formatError ? formatError(caught) : labels.failed;\n setFailure(hasContent(message) ? message : labels.failed);\n alert.announce(typeof message === \"string\" && message !== \"\" ? message : labels.failed);\n // Focus back into the editor if the failure left it nowhere (a pending field that\n // the browser let go of); never pulled back from somewhere the user went.\n const active = document.activeElement;\n if (active === null || active === document.body) {\n editorRef.current?.querySelector<HTMLElement>(\"input, textarea, select, [tabindex]\")?.focus();\n }\n };\n\n const save = (next: T = draftRef.current) => {\n if (!editing || ended.current || pending) return;\n ended.current = true;\n if (next !== draftRef.current) setDraft(next);\n if (isEqual(next, value)) {\n setOpen(false);\n return;\n }\n let result: unknown;\n try {\n result = onCommit(next);\n } catch (caught) {\n fail(caught);\n return;\n }\n const promise = asPromise(result);\n if (!promise) {\n setOpen(false);\n return;\n }\n setPending(true);\n setFailure(null);\n promise.then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n setOpen(false);\n },\n (caught: unknown) => {\n if (!mounted.current) return;\n setPending(false);\n fail(caught);\n },\n );\n };\n\n const cancel = () => {\n // A request that has left cannot be called back; Escape waits for it.\n if (!editing || pending) return;\n ended.current = true;\n setOpen(false);\n };\n\n const change = (next: T) => {\n if (pending) return;\n setDraft(next);\n // Now, not after the render: a blur check scheduled before that render reads it.\n draftRef.current = next;\n // The failure described the draft that was sent; once edited it describes nothing\n // on screen, and the next save (or blur) is a fresh attempt.\n setFailure(null);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.defaultPrevented) return;\n if (e.key === \"Escape\") {\n // Ours, not the dialog's or the popover's around the field.\n e.preventDefault();\n e.stopPropagation();\n cancel();\n return;\n }\n // Not mid-composition: Enter there picks the IME's candidate (Japanese, Chinese).\n if (e.key === \"Enter\" && !e.nativeEvent.isComposing && !e.shiftKey) {\n e.preventDefault();\n save();\n }\n };\n\n // Leaving the editor saves. Deferred one task, and cancelled by any focus that lands\n // back inside: React reports focus moves through PORTALS to the component tree, so a\n // calculator popover or a phone keypad the editor opened counts as inside, which a\n // DOM `contains` check would get wrong. A failure on screen holds the editor: the\n // user is looking at why, and moving away is not a retry.\n const onBlur = (e: FocusEvent<HTMLDivElement>) => {\n if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = setTimeout(() => {\n blurTimer.current = null;\n if (!mounted.current || failed) return;\n save();\n }, 0);\n };\n const onFocus = () => {\n if (blurTimer.current) clearTimeout(blurTimer.current);\n blurTimer.current = null;\n };\n\n const private_ = redact ? { \"data-private\": true } : undefined;\n const shown = display ? display(value) : value === null || value === undefined ? \"\" : String(value);\n const content = hasContent(shown) ? (\n shown\n ) : (\n <span className=\"text-[var(--text-muted)]\">{placeholder ?? labels.empty}</span>\n );\n const alignClass = align === \"end\" ? \"text-end\" : \"text-start\";\n\n let body: ReactNode;\n if (readOnly) {\n body = (\n <span {...private_} className={cn(\"block min-w-0 truncate\", alignClass)}>\n {content}\n </span>\n );\n } else if (editing) {\n const describedBy = failed ? errorId : undefined;\n const editorProps: InlineEditorProps<T> = {\n value: draft,\n onChange: change,\n commit: save,\n cancel,\n pending,\n label,\n invalid: failed,\n describedBy,\n inputProps: {\n \"aria-label\": label,\n \"aria-invalid\": failed || undefined,\n \"aria-describedby\": describedBy,\n readOnly: pending,\n },\n };\n body = (\n // A wrapper for the keys and the focus, not a control: the keys belong to the\n // field inside it, which is what has focus.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- see above\n <div\n ref={editorRef}\n {...private_}\n onKeyDown={onKeyDown}\n onBlur={onBlur}\n onFocus={onFocus}\n aria-busy={pending || undefined}\n className=\"min-w-0\"\n >\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <div className=\"min-w-0 flex-1\">\n {editor ? (\n <RenderEditor render={editor} {...editorProps} />\n ) : (\n <Input\n {...inputProps}\n {...editorProps.inputProps}\n value={String(draft ?? \"\")}\n onChange={(e) => change(e.target.value as unknown as T)}\n className={cn(\"px-2 py-1\", alignClass, inputProps?.className)}\n />\n )}\n </div>\n {/* Decorative: `aria-busy` on the wrapper says it. */}\n {pending && <Spinner label={null} className=\"size-4 shrink-0\" />}\n </div>\n {failed && (\n <p id={errorId} className=\"mt-1 text-[11px] leading-tight text-[var(--danger)]\">\n {failure}\n </p>\n )}\n </div>\n );\n } else if (locked) {\n // Button's `disabledReason` path, spelled out for a button that must look like\n // the value rather than like a Button: focusable, `aria-disabled`, every\n // activation swallowed, the reason in the kit Tooltip and in a `hidden` copy that\n // describes it. The button in a FRAGMENT so the Tooltip does not add the bubble as\n // a second description of the same sentence.\n body = (\n <Tooltip label={disabledReason} className=\"flex w-full min-w-0\">\n <>\n <button\n ref={displayRef}\n type=\"button\"\n {...private_}\n aria-disabled\n aria-describedby={reasonId}\n onClick={(e) => e.preventDefault()}\n className={cn(\n \"block w-full min-w-0 cursor-not-allowed truncate rounded-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n alignClass,\n )}\n >\n {content}\n </button>\n <span id={reasonId} hidden>\n {disabledReason}\n </span>\n </>\n </Tooltip>\n );\n } else {\n body = (\n // Lazy: a table of these mounts no bubble until one is hovered or focused, and\n // the closed bubble's text is in no ancestor's accessible name.\n <Tooltip label={labels.edit(label)} lazy className=\"flex w-full min-w-0\">\n <button\n ref={displayRef}\n type=\"button\"\n {...private_}\n onClick={() => setOpen(true)}\n className={cn(\n \"block w-full min-w-0 truncate rounded-sm underline-offset-2 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n alignClass,\n )}\n >\n {content}\n </button>\n </Tooltip>\n );\n }\n\n return (\n <div className={cn(\"min-w-0\", className)} data-editing={editing || undefined}>\n {body}\n {/* Always mounted: a live region that mounts with its message is usually missed. */}\n <span {...alert.regionProps} />\n </div>\n );\n}\n"],"mappings":";AAyZI,SA0EI,UA1EJ,KA0CI,YA1CJ;AAzZJ,SAAS,WAAW,OAAO,QAAQ,gBAAgB;AAEnD,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAC7B,SAAS,OAAO,eAAe;AAC/B,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAgBzB,MAAM,6BAA+C;AAAA,EAC1D,MAAM,CAAC,UAAU,QAAQ,KAAK;AAAA,EAC9B,QAAQ;AAAA,EACR,OAAO;AACT;AAqHA,SAAS,WAAW,MAA0B;AAC5C,SAAO,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAC3E;AAMA,SAAS,aAAgB,EAAE,QAAQ,GAAG,MAAM,GAAuD;AACjG,SAAO,OAAO,KAAK;AACrB;AAGA,SAAS,UAAU,QAA0C;AAC3D,SAAO,UAAU,OAAQ,OAA4B,SAAS,aAAc,SAA8B;AAC5G;AAmCO,SAAS,gBAA4B,OAAgC;AAC1E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB;AAAA,IACA,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV,IAAI;AACJ,QAAM,SAAS,MAAM;AACrB,QAAM,SAAS,aAAa,cAAc,4BAA4B,UAAU;AAChF,QAAM,iBAAiB,gBAAgB,QAAQ,iBAAiB;AAChE,QAAM,SAAS,WAAW,cAAc;AAExC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,OAAO,eAAe;AAG5B,QAAM,UAAU,QAAQ,CAAC,UAAU,CAAC;AACpC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,KAAK;AAC3C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAoB,IAAI;AACtD,QAAM,SAAS,WAAW,OAAO;AAEjC,QAAM,UAAU,MAAM;AACtB,QAAM,WAAW,MAAM;AACvB,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,YAAY,OAAuB,IAAI;AAC7C,QAAM,QAAQ,YAAY,EAAE,YAAY,YAAY,CAAC;AAKrD,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,YAAY,OAA6C,IAAI;AACnE,QAAM,UAAU,OAAO,IAAI;AAE3B,QAAM,WAAW,OAAO,KAAK;AAC7B,YAAU,MAAM;AACd,aAAS,UAAU;AAAA,EACrB,CAAC;AACD,YAAU,MAAM;AACd,YAAQ,UAAU;AAClB,WAAO,MAAM;AACX,cAAQ,UAAU;AAClB,UAAI,UAAU,QAAS,cAAa,UAAU,OAAO;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,OAAO;AACpD,MAAI,eAAe,SAAS;AAC1B,kBAAc,OAAO;AACrB,QAAI,SAAS;AACX,eAAS,KAAK;AACd,iBAAW,IAAI;AACf,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF;AAGA,MAAI,aAAa,CAAC,WAAW,gBAAgB,OAAW,cAAa,KAAK;AAK1E,QAAM,cAAc,OAAO,OAAO;AAClC,YAAU,MAAM;AACd,UAAM,UAAU,YAAY,YAAY;AACxC,gBAAY,UAAU;AACtB,QAAI,CAAC,QAAS;AACd,QAAI,SAAS;AAGX,YAAM,UAAU;AAChB,YAAM,UAAU,UAAU,SAAS;AAAA,QACjC;AAAA,MACF;AACA,eAAS,MAAM;AACf,UAAI,mBAAmB,oBAAoB,mBAAmB,oBAAqB,SAAQ,OAAO;AAClG;AAAA,IACF;AAGA,UAAM,UAAU;AAChB,QAAI,UAAU,QAAS,cAAa,UAAU,OAAO;AACrD,cAAU,UAAU;AACpB,UAAM,OAAO,SAAS,kBAAkB,QAAQ,SAAS,kBAAkB,SAAS;AACpF,QAAI,KAAM,YAAW,SAAS,MAAM;AAAA,EACtC,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,UAAU,CAAC,SAAkB;AACjC,QAAI,gBAAgB,OAAW,cAAa,IAAI;AAChD,sBAAkB,IAAI;AAAA,EACxB;AAEA,QAAM,OAAO,CAAC,WAAoB;AAChC,UAAM,UAAU;AAChB,UAAM,UAAU,cAAc,YAAY,MAAM,IAAI,OAAO;AAC3D,eAAW,WAAW,OAAO,IAAI,UAAU,OAAO,MAAM;AACxD,UAAM,SAAS,OAAO,YAAY,YAAY,YAAY,KAAK,UAAU,OAAO,MAAM;AAGtF,UAAM,SAAS,SAAS;AACxB,QAAI,WAAW,QAAQ,WAAW,SAAS,MAAM;AAC/C,gBAAU,SAAS,cAA2B,qCAAqC,GAAG,MAAM;AAAA,IAC9F;AAAA,EACF;AAEA,QAAM,OAAO,CAAC,OAAU,SAAS,YAAY;AAC3C,QAAI,CAAC,WAAW,MAAM,WAAW,QAAS;AAC1C,UAAM,UAAU;AAChB,QAAI,SAAS,SAAS,QAAS,UAAS,IAAI;AAC5C,QAAI,QAAQ,MAAM,KAAK,GAAG;AACxB,cAAQ,KAAK;AACb;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,eAAS,SAAS,IAAI;AAAA,IACxB,SAAS,QAAQ;AACf,WAAK,MAAM;AACX;AAAA,IACF;AACA,UAAM,UAAU,UAAU,MAAM;AAChC,QAAI,CAAC,SAAS;AACZ,cAAQ,KAAK;AACb;AAAA,IACF;AACA,eAAW,IAAI;AACf,eAAW,IAAI;AACf,YAAQ;AAAA,MACN,MAAM;AACJ,YAAI,CAAC,QAAQ,QAAS;AACtB,mBAAW,KAAK;AAChB,gBAAQ,KAAK;AAAA,MACf;AAAA,MACA,CAAC,WAAoB;AACnB,YAAI,CAAC,QAAQ,QAAS;AACtB,mBAAW,KAAK;AAChB,aAAK,MAAM;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM;AAEnB,QAAI,CAAC,WAAW,QAAS;AACzB,UAAM,UAAU;AAChB,YAAQ,KAAK;AAAA,EACf;AAEA,QAAM,SAAS,CAAC,SAAY;AAC1B,QAAI,QAAS;AACb,aAAS,IAAI;AAEb,aAAS,UAAU;AAGnB,eAAW,IAAI;AAAA,EACjB;AAEA,QAAM,YAAY,CAAC,MAAqC;AACtD,QAAI,EAAE,iBAAkB;AACxB,QAAI,EAAE,QAAQ,UAAU;AAEtB,QAAE,eAAe;AACjB,QAAE,gBAAgB;AAClB,aAAO;AACP;AAAA,IACF;AAEA,QAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,YAAY,eAAe,CAAC,EAAE,UAAU;AAClE,QAAE,eAAe;AACjB,WAAK;AAAA,IACP;AAAA,EACF;AAOA,QAAM,SAAS,CAAC,MAAkC;AAChD,QAAI,EAAE,cAAc,SAAS,EAAE,aAA4B,EAAG;AAC9D,QAAI,UAAU,QAAS,cAAa,UAAU,OAAO;AACrD,cAAU,UAAU,WAAW,MAAM;AACnC,gBAAU,UAAU;AACpB,UAAI,CAAC,QAAQ,WAAW,OAAQ;AAChC,WAAK;AAAA,IACP,GAAG,CAAC;AAAA,EACN;AACA,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU,QAAS,cAAa,UAAU,OAAO;AACrD,cAAU,UAAU;AAAA,EACtB;AAEA,QAAM,WAAW,SAAS,EAAE,gBAAgB,KAAK,IAAI;AACrD,QAAM,QAAQ,UAAU,QAAQ,KAAK,IAAI,UAAU,QAAQ,UAAU,SAAY,KAAK,OAAO,KAAK;AAClG,QAAM,UAAU,WAAW,KAAK,IAC9B,QAEA,oBAAC,UAAK,WAAU,4BAA4B,yBAAe,OAAO,OAAM;AAE1E,QAAM,aAAa,UAAU,QAAQ,aAAa;AAElD,MAAI;AACJ,MAAI,UAAU;AACZ,WACE,oBAAC,UAAM,GAAG,UAAU,WAAW,GAAG,0BAA0B,UAAU,GACnE,mBACH;AAAA,EAEJ,WAAW,SAAS;AAClB,UAAM,cAAc,SAAS,UAAU;AACvC,UAAM,cAAoC;AAAA,MACxC,OAAO;AAAA,MACP,UAAU;AAAA,MACV,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT;AAAA,MACA,YAAY;AAAA,QACV,cAAc;AAAA,QACd,gBAAgB,UAAU;AAAA,QAC1B,oBAAoB;AAAA,QACpB,UAAU;AAAA,MACZ;AAAA,IACF;AACA;AAAA;AAAA;AAAA,IAIE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACJ,GAAG;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,QACA,aAAW,WAAW;AAAA,QACtB,WAAU;AAAA,QAEV;AAAA,+BAAC,SAAI,WAAU,qCACb;AAAA,gCAAC,SAAI,WAAU,kBACZ,mBACC,oBAAC,gBAAa,QAAQ,QAAS,GAAG,aAAa,IAE/C;AAAA,cAAC;AAAA;AAAA,gBACE,GAAG;AAAA,gBACH,GAAG,YAAY;AAAA,gBAChB,OAAO,OAAO,SAAS,EAAE;AAAA,gBACzB,UAAU,CAAC,MAAM,OAAO,EAAE,OAAO,KAAqB;AAAA,gBACtD,WAAW,GAAG,aAAa,YAAY,YAAY,SAAS;AAAA;AAAA,YAC9D,GAEJ;AAAA,YAEC,WAAW,oBAAC,WAAQ,OAAO,MAAM,WAAU,mBAAkB;AAAA,aAChE;AAAA,UACC,UACC,oBAAC,OAAE,IAAI,SAAS,WAAU,uDACvB,mBACH;AAAA;AAAA;AAAA,IAEJ;AAAA,EAEJ,WAAW,QAAQ;AAMjB,WACE,oBAAC,WAAQ,OAAO,gBAAgB,WAAU,uBACxC,2CACE;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACJ,GAAG;AAAA,UACJ,iBAAa;AAAA,UACb,oBAAkB;AAAA,UAClB,SAAS,CAAC,MAAM,EAAE,eAAe;AAAA,UACjC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MACA,oBAAC,UAAK,IAAI,UAAU,QAAM,MACvB,0BACH;AAAA,OACF,GACF;AAAA,EAEJ,OAAO;AACL;AAAA;AAAA,IAGE,oBAAC,WAAQ,OAAO,OAAO,KAAK,KAAK,GAAG,MAAI,MAAC,WAAU,uBACjD;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACJ,GAAG;AAAA,QACJ,SAAS,MAAM,QAAQ,IAAI;AAAA,QAC3B,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA;AAAA,IACH,GACF;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAW,GAAG,WAAW,SAAS,GAAG,gBAAc,WAAW,QAChE;AAAA;AAAA,IAED,oBAAC,UAAM,GAAG,MAAM,aAAa;AAAA,KAC/B;AAEJ;","names":[]}
@@ -0,0 +1,130 @@
1
+ import * as react from 'react';
2
+ import { KitLanguageCode, KitLanguage } from '../i18n/languages.js';
3
+ import { bw as SelectProps } from '../feedback-BxeQVzwq.js';
4
+ import '../shell/topbar-controls.js';
5
+ import '../theme/palette-presets.js';
6
+ import '../theme/chart-palette.js';
7
+ import '../theme/theme-store.js';
8
+ import 'zustand/middleware';
9
+ import 'zustand';
10
+ import '../feedback-attachment-fGAzZPf0.js';
11
+ import '../feedback/feedback-inbox.js';
12
+ import 'lucide-react';
13
+ import './tooltip.js';
14
+ import '../hooks/use-anchored-rect.js';
15
+ import '../lib/clipping.js';
16
+ import '../data-table-labels-B7OdnM0S.js';
17
+ import './data-table-sort.js';
18
+ import './swipeable-row.js';
19
+ import './mini-calendar.js';
20
+ import './calendar-heatmap.js';
21
+ import './popover.js';
22
+ import './chip.js';
23
+ import './field-sync.js';
24
+ import '../wizard/types.js';
25
+ import '../tour/tour.js';
26
+ import '../search/command-palette.js';
27
+ import '../search/global-search.js';
28
+ import '../search/search-index.js';
29
+ import './month-picker.js';
30
+ import './page-contents.js';
31
+ import './series-chart-labels.js';
32
+ import './pie-chart-labels.js';
33
+ import './sparkline.js';
34
+ import './stat-tile.js';
35
+ import './signature-pad.js';
36
+ import './password-strength.js';
37
+ import './swatch-picker.js';
38
+ import './tile-radio.js';
39
+ import './icon-picker.js';
40
+ import './dialog-frame.js';
41
+ import './modal.js';
42
+ import '../hooks/use-search-param-state.js';
43
+ import './measured-grid.js';
44
+ import './account-settings-labels.js';
45
+ import './floating-panel.js';
46
+ import './bulk-action-bar.js';
47
+ import './list.js';
48
+ import './signed-amount.js';
49
+ import './breadcrumbs.js';
50
+ import './toast.js';
51
+ import './description-list.js';
52
+ import './line-items.js';
53
+ import './progress-bar.js';
54
+ import './error-boundary.js';
55
+ import './authed-image.js';
56
+ import '../hooks/use-authed-src.js';
57
+ import './image-grid.js';
58
+ import './lightbox.js';
59
+ import './write-lock.js';
60
+ import './legal.js';
61
+ import './account-chips.js';
62
+ import '../lib/format.js';
63
+ import './reauth-dialog.js';
64
+ import './server-wake.js';
65
+ import '../lib/server-wake.js';
66
+ import './translation-review-labels.js';
67
+ import '../lib/translation-review.js';
68
+ import './country-select.js';
69
+ import './inline-edit-field.js';
70
+ import '../lib/iban.js';
71
+ import '../lib/phone.js';
72
+ import './sign-chip.js';
73
+ import './column-mapper.js';
74
+ import '../lib/column-mapping.js';
75
+ import '../lib/table-text.js';
76
+ import './field-parts.js';
77
+
78
+ /**
79
+ * A language as a FORM FIELD: one of the codes an app offers, picked in a form beside
80
+ * other fields — label, hint and error like every kit field, controlled, submitted with
81
+ * the rest.
82
+ *
83
+ * Kurvenschmiede's admin invites people by mail, and the invitation goes out in a
84
+ * language the admin picks, because the invitee has no account yet to say theirs
85
+ * (allowlist-panel.tsx: a `Select` with an `<option>` per language, spelled out by
86
+ * hand). Neither language control the kit had fits there: `LanguageMenu` is the top
87
+ * bar's switcher for the reader's OWN language, a flag in a hover menu that changes the
88
+ * page the moment it is picked; `LanguageSetting` is the settings row for the same
89
+ * thing and takes ready-made `{ code, label }` rows. This one is for a language that
90
+ * belongs to something else — an invitation, a mail template, a customer's
91
+ * correspondence — and takes only the CODES: the names come from the kit's registry
92
+ * ({@link languageOptions}), the same "Deutsch", "Français", "简体中文" the switcher shows.
93
+ *
94
+ * Every {@link Select} prop passes through (`label`, `hint`, `error`, `required`,
95
+ * `disabled`, `size`, `name`, `id`, `data-*`); only `value`, `onChange` and the options
96
+ * are this component's.
97
+ *
98
+ * WHAT IT SHOWS FOR `value`. The offered code that answers it, by
99
+ * {@link resolveLanguage}'s rule: so i18next's `resolvedLanguage` can be handed in as it
100
+ * comes, and a stored `de` or `de-AT` shows as the one German, `de-CH`. A value that
101
+ * answers to none of the codes shows the fallback `resolveLanguage` would pick (`de-CH`
102
+ * when offered, else the first code) — where a bare native select would show its first
103
+ * option and say nothing. Either way the field does not write the resolved code back:
104
+ * `onChange` fires when the person picks, as on any field. Keep `value` one of `codes`
105
+ * (resolve it once, where the state starts) and what is shown is what is submitted.
106
+ *
107
+ * Each name is marked with its own language (`lang`), so a screen reader says
108
+ * "Français" in French rather than spelling it in the page's voice. A caller's
109
+ * `optionLabel` (English names for a team-facing list) is in the page's language, and
110
+ * goes unmarked.
111
+ */
112
+ interface LanguageSelectProps extends Omit<SelectProps, "value" | "onChange" | "children" | "multiple"> {
113
+ /** The chosen language: one of `codes` — or any tag, shown as the code it resolves to. */
114
+ value: string;
115
+ /** The picked code — always one of `codes`. */
116
+ onChange: (code: KitLanguageCode) => void;
117
+ /** The languages to offer, in the order to list them. Default: all seven
118
+ * ({@link KIT_LANGUAGES}' order, alphabetical by native name). */
119
+ codes?: readonly KitLanguageCode[];
120
+ /**
121
+ * The text of each option. Default: the language's name in itself — the right name
122
+ * for a reader looking for theirs, and for an admin choosing what an invitee reads.
123
+ * `(language) => language.englishName` for a list read by a team rather than by the
124
+ * language's readers (a reviewer grant), or the app's own words for a code.
125
+ */
126
+ optionLabel?: (language: KitLanguage) => string;
127
+ }
128
+ declare const LanguageSelect: react.ForwardRefExoticComponent<LanguageSelectProps & react.RefAttributes<HTMLSelectElement>>;
129
+
130
+ export { LanguageSelect, type LanguageSelectProps };
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { forwardRef } from "react";
4
+ import { KIT_LANGUAGES, languageOptions, resolveLanguage } from "../i18n/languages.js";
5
+ import { Select } from "./ui.js";
6
+ const ALL_CODES = KIT_LANGUAGES.map((language) => language.code);
7
+ const LanguageSelect = forwardRef(function LanguageSelect2({ value, onChange, codes = ALL_CODES, optionLabel, ...rest }, ref) {
8
+ const shown = resolveLanguage([value], codes);
9
+ return /* @__PURE__ */ jsx(Select, { ...rest, ref, value: shown, onChange: (e) => onChange(e.target.value), children: languageOptions(codes).map((option) => {
10
+ const language = KIT_LANGUAGES.find((entry) => entry.code === option.code);
11
+ return /* @__PURE__ */ jsx("option", { value: option.code, lang: optionLabel ? void 0 : language.formatLocale, children: optionLabel ? optionLabel(language) : option.label }, option.code);
12
+ }) });
13
+ });
14
+ LanguageSelect.displayName = "LanguageSelect";
15
+ export {
16
+ LanguageSelect
17
+ };
18
+ //# sourceMappingURL=language-select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/language-select.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport { KIT_LANGUAGES, languageOptions, resolveLanguage } from \"../i18n/languages\";\nimport type { KitLanguage, KitLanguageCode } from \"../i18n/languages\";\nimport { Select } from \"./ui\";\nimport type { SelectProps } from \"./ui\";\n\n/**\n * A language as a FORM FIELD: one of the codes an app offers, picked in a form beside\n * other fields — label, hint and error like every kit field, controlled, submitted with\n * the rest.\n *\n * Kurvenschmiede's admin invites people by mail, and the invitation goes out in a\n * language the admin picks, because the invitee has no account yet to say theirs\n * (allowlist-panel.tsx: a `Select` with an `<option>` per language, spelled out by\n * hand). Neither language control the kit had fits there: `LanguageMenu` is the top\n * bar's switcher for the reader's OWN language, a flag in a hover menu that changes the\n * page the moment it is picked; `LanguageSetting` is the settings row for the same\n * thing and takes ready-made `{ code, label }` rows. This one is for a language that\n * belongs to something else — an invitation, a mail template, a customer's\n * correspondence — and takes only the CODES: the names come from the kit's registry\n * ({@link languageOptions}), the same \"Deutsch\", \"Français\", \"简体中文\" the switcher shows.\n *\n * Every {@link Select} prop passes through (`label`, `hint`, `error`, `required`,\n * `disabled`, `size`, `name`, `id`, `data-*`); only `value`, `onChange` and the options\n * are this component's.\n *\n * WHAT IT SHOWS FOR `value`. The offered code that answers it, by\n * {@link resolveLanguage}'s rule: so i18next's `resolvedLanguage` can be handed in as it\n * comes, and a stored `de` or `de-AT` shows as the one German, `de-CH`. A value that\n * answers to none of the codes shows the fallback `resolveLanguage` would pick (`de-CH`\n * when offered, else the first code) — where a bare native select would show its first\n * option and say nothing. Either way the field does not write the resolved code back:\n * `onChange` fires when the person picks, as on any field. Keep `value` one of `codes`\n * (resolve it once, where the state starts) and what is shown is what is submitted.\n *\n * Each name is marked with its own language (`lang`), so a screen reader says\n * \"Français\" in French rather than spelling it in the page's voice. A caller's\n * `optionLabel` (English names for a team-facing list) is in the page's language, and\n * goes unmarked.\n */\nexport interface LanguageSelectProps extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\" | \"multiple\"> {\n /** The chosen language: one of `codes` — or any tag, shown as the code it resolves to. */\n value: string;\n /** The picked code — always one of `codes`. */\n onChange: (code: KitLanguageCode) => void;\n /** The languages to offer, in the order to list them. Default: all seven\n * ({@link KIT_LANGUAGES}' order, alphabetical by native name). */\n codes?: readonly KitLanguageCode[];\n /**\n * The text of each option. Default: the language's name in itself — the right name\n * for a reader looking for theirs, and for an admin choosing what an invitee reads.\n * `(language) => language.englishName` for a list read by a team rather than by the\n * language's readers (a reviewer grant), or the app's own words for a code.\n */\n optionLabel?: (language: KitLanguage) => string;\n}\n\nconst ALL_CODES: readonly KitLanguageCode[] = KIT_LANGUAGES.map((language) => language.code);\n\nexport const LanguageSelect = forwardRef<HTMLSelectElement, LanguageSelectProps>(function LanguageSelect(\n { value, onChange, codes = ALL_CODES, optionLabel, ...rest },\n ref,\n) {\n const shown = resolveLanguage([value], codes);\n return (\n <Select {...rest} ref={ref} value={shown} onChange={(e) => onChange(e.target.value as KitLanguageCode)}>\n {languageOptions(codes).map((option) => {\n const language = KIT_LANGUAGES.find((entry) => entry.code === option.code)!;\n return (\n <option key={option.code} value={option.code} lang={optionLabel ? undefined : language.formatLocale}>\n {optionLabel ? optionLabel(language) : option.label}\n </option>\n );\n })}\n </Select>\n );\n});\nLanguageSelect.displayName = \"LanguageSelect\";\n"],"mappings":";AAqEU;AArEV,SAAS,kBAAkB;AAC3B,SAAS,eAAe,iBAAiB,uBAAuB;AAEhE,SAAS,cAAc;AAsDvB,MAAM,YAAwC,cAAc,IAAI,CAAC,aAAa,SAAS,IAAI;AAEpF,MAAM,iBAAiB,WAAmD,SAASA,gBACxF,EAAE,OAAO,UAAU,QAAQ,WAAW,aAAa,GAAG,KAAK,GAC3D,KACA;AACA,QAAM,QAAQ,gBAAgB,CAAC,KAAK,GAAG,KAAK;AAC5C,SACE,oBAAC,UAAQ,GAAG,MAAM,KAAU,OAAO,OAAO,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAwB,GAClG,0BAAgB,KAAK,EAAE,IAAI,CAAC,WAAW;AACtC,UAAM,WAAW,cAAc,KAAK,CAAC,UAAU,MAAM,SAAS,OAAO,IAAI;AACzE,WACE,oBAAC,YAAyB,OAAO,OAAO,MAAM,MAAM,cAAc,SAAY,SAAS,cACpF,wBAAc,YAAY,QAAQ,IAAI,OAAO,SADnC,OAAO,IAEpB;AAAA,EAEJ,CAAC,GACH;AAEJ,CAAC;AACD,eAAe,cAAc;","names":["LanguageSelect"]}
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef, ReactNode, Key } from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode, Key, HTMLAttributes } from 'react';
3
3
 
4
4
  /** The words {@link LineItems} renders on its own behalf. Row numbers are 1-based. */
5
5
  interface LineItemsLabels {
@@ -97,6 +97,14 @@ interface LineItemsColumn<T> {
97
97
  narrowSpan?: 1 | 2;
98
98
  render: (ctx: LineItemCellContext<T>) => ReactNode;
99
99
  }
100
+ /**
101
+ * What {@link LineItemsProps.rowProps} returns for one row: `data-*` hooks, an `id`, a
102
+ * class, handlers. A `data-*` attribute is typed so `{ "data-tour": "rule-condition" }`
103
+ * needs no cast.
104
+ */
105
+ type LineItemsRowProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "dangerouslySetInnerHTML" | "role"> & {
106
+ [key: `data-${string}`]: string | number | boolean | undefined;
107
+ };
100
108
  interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
101
109
  items: readonly T[];
102
110
  columns: readonly LineItemsColumn<T>[];
@@ -155,6 +163,20 @@ interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "child
155
163
  removeAlign?: LineItemsRemoveAlign;
156
164
  /** Disables add and remove (the fields are the caller's to disable). */
157
165
  disabled?: boolean;
166
+ /**
167
+ * Extra attributes for one row's element (the `role="group"` that holds its fields) —
168
+ * keksdose K20: its rule editor's tour points at the first condition row with
169
+ * `[data-tour="rule-condition"]` (dev#495), and with nowhere to put the anchor on the
170
+ * row it wrapped a cell's field in a `span` instead, which the spotlight then drew
171
+ * around one field rather than the row. Test ids the same way.
172
+ *
173
+ * MERGED under the list's own, which win: `role`, the group's `aria-label` (that is
174
+ * {@link rowLabel}'s) and `data-line-items-row` stay the list's — the row's name and
175
+ * the hook its focus handling finds rows by. `className` is added after the row's
176
+ * own classes, `style` is passed through, and an `onKeyDown` runs BEFORE the row's
177
+ * Ctrl/⌘+Enter "add a row" (call `preventDefault()` to keep that from happening).
178
+ */
179
+ rowProps?: (item: T, index: number) => LineItemsRowProps | undefined;
158
180
  labels?: Partial<LineItemsLabels>;
159
181
  }
160
182
  /**
@@ -184,6 +206,6 @@ interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "child
184
206
  * the row that took its place (else the add button). Ctrl/⌘+Enter in any row adds a
185
207
  * row.
186
208
  */
187
- declare function LineItems<T>({ items, columns, getKey, onAdd, onRemove, minItems, maxItems, confirmRemove, addLabel, rowLabel, totals, totalsLabel, empty, error, summary, fieldLabels, narrowColumns, removePlacement, removeAlign: removeAlignProp, disabled, labels: labelsProp, className, style, ...rest }: LineItemsProps<T>): react.JSX.Element;
209
+ declare function LineItems<T>({ items, columns, getKey, onAdd, onRemove, minItems, maxItems, confirmRemove, addLabel, rowLabel, totals, totalsLabel, empty, error, summary, fieldLabels, narrowColumns, removePlacement, removeAlign: removeAlignProp, disabled, rowProps, labels: labelsProp, className, style, ...rest }: LineItemsProps<T>): react.JSX.Element;
188
210
 
189
- export { DEFAULT_LINE_ITEMS_LABELS, type LineItemCellContext, LineItems, type LineItemsColumn, type LineItemsFieldLabels, type LineItemsLabels, type LineItemsProps, type LineItemsRemoveAlign, type LineItemsRemovePlacement, type LineItemsSummary, type LineItemsSummaryTone };
211
+ export { DEFAULT_LINE_ITEMS_LABELS, type LineItemCellContext, LineItems, type LineItemsColumn, type LineItemsFieldLabels, type LineItemsLabels, type LineItemsProps, type LineItemsRemoveAlign, type LineItemsRemovePlacement, type LineItemsRowProps, type LineItemsSummary, type LineItemsSummaryTone };