@eifi1/ui-kit 0.20.1 → 0.22.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 (286) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/hover-menu.d.ts +3 -0
  72. package/dist/components/hover-menu.js +2 -2
  73. package/dist/components/hover-menu.js.map +1 -1
  74. package/dist/components/iban-input.d.ts +66 -0
  75. package/dist/components/iban-input.js +125 -0
  76. package/dist/components/iban-input.js.map +1 -0
  77. package/dist/components/icon-picker.d.ts +12 -2
  78. package/dist/components/icon-picker.js +9 -2
  79. package/dist/components/icon-picker.js.map +1 -1
  80. package/dist/components/inline-edit-field.d.ts +167 -0
  81. package/dist/components/inline-edit-field.js +293 -0
  82. package/dist/components/inline-edit-field.js.map +1 -0
  83. package/dist/components/language-select.d.ts +127 -0
  84. package/dist/components/language-select.js +18 -0
  85. package/dist/components/language-select.js.map +1 -0
  86. package/dist/components/line-items.d.ts +25 -3
  87. package/dist/components/line-items.js +55 -46
  88. package/dist/components/line-items.js.map +1 -1
  89. package/dist/components/list.d.ts +25 -0
  90. package/dist/components/list.js +16 -5
  91. package/dist/components/list.js.map +1 -1
  92. package/dist/components/money-field.d.ts +20 -8
  93. package/dist/components/money-field.js.map +1 -1
  94. package/dist/components/month-picker.d.ts +52 -4
  95. package/dist/components/month-picker.js +290 -86
  96. package/dist/components/month-picker.js.map +1 -1
  97. package/dist/components/multi-select.d.ts +8 -1
  98. package/dist/components/multi-select.js +9 -4
  99. package/dist/components/multi-select.js.map +1 -1
  100. package/dist/components/number-field.d.ts +25 -13
  101. package/dist/components/number-field.js +6 -20
  102. package/dist/components/number-field.js.map +1 -1
  103. package/dist/components/number-input.d.ts +70 -13
  104. package/dist/components/number-input.js +90 -38
  105. package/dist/components/number-input.js.map +1 -1
  106. package/dist/components/numpad-sheet.d.ts +10 -8
  107. package/dist/components/one-time-code-input.d.ts +100 -0
  108. package/dist/components/one-time-code-input.js +137 -0
  109. package/dist/components/one-time-code-input.js.map +1 -0
  110. package/dist/components/phone-input.d.ts +66 -0
  111. package/dist/components/phone-input.js +171 -0
  112. package/dist/components/phone-input.js.map +1 -0
  113. package/dist/components/series-chart.d.ts +10 -8
  114. package/dist/components/settings-fields.d.ts +10 -8
  115. package/dist/components/share-card.d.ts +8 -6
  116. package/dist/components/sign-chip.d.ts +66 -0
  117. package/dist/components/sign-chip.js +39 -0
  118. package/dist/components/sign-chip.js.map +1 -0
  119. package/dist/components/swatch-picker.d.ts +27 -2
  120. package/dist/components/swatch-picker.js +73 -31
  121. package/dist/components/swatch-picker.js.map +1 -1
  122. package/dist/components/switch.d.ts +19 -0
  123. package/dist/components/switch.js +24 -6
  124. package/dist/components/switch.js.map +1 -1
  125. package/dist/components/text-link.d.ts +10 -8
  126. package/dist/components/tile-radio.d.ts +68 -13
  127. package/dist/components/tile-radio.js +3 -3
  128. package/dist/components/tile-radio.js.map +1 -1
  129. package/dist/components/time-input.d.ts +15 -9
  130. package/dist/components/time-input.js.map +1 -1
  131. package/dist/components/toggle-group.d.ts +47 -1
  132. package/dist/components/toggle-group.js +110 -69
  133. package/dist/components/toggle-group.js.map +1 -1
  134. package/dist/components/ui.d.ts +10 -8
  135. package/dist/components/ui.js +280 -78
  136. package/dist/components/ui.js.map +1 -1
  137. package/dist/components/use-table-state.d.ts +1 -2
  138. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  139. package/dist/data-table.d.ts +1 -2
  140. package/dist/data-table.js +2 -1
  141. package/dist/data-table.js.map +1 -1
  142. package/dist/feedback/feedback-attachment.d.ts +1 -1
  143. package/dist/feedback/feedback-attachment.js +210 -3
  144. package/dist/feedback/feedback-attachment.js.map +1 -1
  145. package/dist/feedback/feedback-dialog.d.ts +1 -1
  146. package/dist/feedback/feedback-dialog.js.map +1 -1
  147. package/dist/feedback/feedback-inbox.d.ts +1 -1
  148. package/dist/feedback/feedback-thread.d.ts +37 -3
  149. package/dist/feedback/feedback-thread.js +38 -9
  150. package/dist/feedback/feedback-thread.js.map +1 -1
  151. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  152. package/dist/feedback.d.ts +2 -2
  153. package/dist/hooks/use-file-drop.d.ts +10 -8
  154. package/dist/i18n/defaults.d.ts +10 -8
  155. package/dist/i18n/defaults.js +17 -2
  156. package/dist/i18n/defaults.js.map +1 -1
  157. package/dist/i18n/german.d.ts +10 -8
  158. package/dist/i18n/german.js +50 -5
  159. package/dist/i18n/german.js.map +1 -1
  160. package/dist/i18n/kit-labels.d.ts +10 -8
  161. package/dist/i18n/kit-labels.js +8 -2
  162. package/dist/i18n/kit-labels.js.map +1 -1
  163. package/dist/i18n/languages.d.ts +10 -8
  164. package/dist/i18n/locales/de-CH.d.ts +10 -8
  165. package/dist/i18n/locales/en.d.ts +10 -8
  166. package/dist/i18n/locales/en.js +9 -0
  167. package/dist/i18n/locales/en.js.map +1 -1
  168. package/dist/i18n/locales/es.d.ts +10 -8
  169. package/dist/i18n/locales/es.js +48 -5
  170. package/dist/i18n/locales/es.js.map +1 -1
  171. package/dist/i18n/locales/fr.d.ts +10 -8
  172. package/dist/i18n/locales/fr.js +48 -5
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.d.ts +10 -8
  175. package/dist/i18n/locales/hu.js +49 -5
  176. package/dist/i18n/locales/hu.js.map +1 -1
  177. package/dist/i18n/locales/it.d.ts +10 -8
  178. package/dist/i18n/locales/it.js +49 -5
  179. package/dist/i18n/locales/it.js.map +1 -1
  180. package/dist/i18n/locales/zh.d.ts +10 -8
  181. package/dist/i18n/locales/zh.js +48 -5
  182. package/dist/i18n/locales/zh.js.map +1 -1
  183. package/dist/i18n/review.d.ts +10 -8
  184. package/dist/i18n/review.js +8 -1
  185. package/dist/i18n/review.js.map +1 -1
  186. package/dist/index.d.ts +17 -10
  187. package/dist/index.js +57 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  190. package/dist/lib/countries.d.ts +43 -0
  191. package/dist/lib/countries.js +39 -0
  192. package/dist/lib/countries.js.map +1 -0
  193. package/dist/lib/decimal-marks.d.ts +46 -0
  194. package/dist/lib/decimal-marks.js +40 -0
  195. package/dist/lib/decimal-marks.js.map +1 -0
  196. package/dist/lib/iban.d.ts +98 -0
  197. package/dist/lib/iban.js +154 -0
  198. package/dist/lib/iban.js.map +1 -0
  199. package/dist/lib/isin.d.ts +30 -0
  200. package/dist/lib/isin.js +36 -0
  201. package/dist/lib/isin.js.map +1 -0
  202. package/dist/lib/phone.d.ts +69 -0
  203. package/dist/lib/phone.js +121 -0
  204. package/dist/lib/phone.js.map +1 -0
  205. package/dist/rhf/fields.d.ts +148 -11
  206. package/dist/rhf/fields.js +332 -3
  207. package/dist/rhf/fields.js.map +1 -1
  208. package/dist/rhf/form.d.ts +10 -8
  209. package/dist/rhf.d.ts +13 -9
  210. package/dist/rhf.js.map +1 -1
  211. package/dist/shell/app-shell.d.ts +10 -8
  212. package/dist/shell/top-bar-brand.d.ts +10 -8
  213. package/dist/shell/topbar-action-menu.d.ts +13 -1
  214. package/dist/shell/topbar-action-menu.js +31 -28
  215. package/dist/shell/topbar-action-menu.js.map +1 -1
  216. package/dist/shell.d.ts +10 -8
  217. package/dist/tour/tour.js +20 -24
  218. package/dist/tour/tour.js.map +1 -1
  219. package/dist/wizard/stepper-nav.d.ts +10 -8
  220. package/dist/wizard.d.ts +10 -8
  221. package/package.json +1 -1
  222. package/src/components/account-chips.tsx +17 -3
  223. package/src/components/account-settings.tsx +92 -26
  224. package/src/components/amount-input.tsx +49 -56
  225. package/src/components/autocomplete.tsx +133 -110
  226. package/src/components/checkbox-group.tsx +229 -0
  227. package/src/components/checkbox.tsx +72 -8
  228. package/src/components/chip.tsx +80 -10
  229. package/src/components/combobox.tsx +292 -253
  230. package/src/components/country-select.tsx +443 -0
  231. package/src/components/currency-select.tsx +109 -33
  232. package/src/components/danger-confirm.tsx +345 -77
  233. package/src/components/data-table-labels.ts +16 -1
  234. package/src/components/data-table.tsx +175 -6
  235. package/src/components/date-picker.tsx +173 -62
  236. package/src/components/entity-combobox.tsx +107 -87
  237. package/src/components/field-anatomy.tsx +190 -0
  238. package/src/components/field-parts.tsx +184 -0
  239. package/src/components/file-button.tsx +36 -3
  240. package/src/components/file-dropzone.tsx +106 -38
  241. package/src/components/form-actions.tsx +115 -2
  242. package/src/components/hover-menu.tsx +7 -4
  243. package/src/components/iban-input.tsx +231 -0
  244. package/src/components/icon-picker.tsx +26 -4
  245. package/src/components/inline-edit-field.tsx +533 -0
  246. package/src/components/language-select.tsx +78 -0
  247. package/src/components/line-items.tsx +35 -2
  248. package/src/components/list.tsx +55 -6
  249. package/src/components/money-field.tsx +10 -0
  250. package/src/components/month-picker.tsx +399 -63
  251. package/src/components/multi-select.tsx +80 -58
  252. package/src/components/number-field.tsx +28 -46
  253. package/src/components/number-input.tsx +210 -43
  254. package/src/components/one-time-code-input.tsx +288 -0
  255. package/src/components/phone-input.tsx +299 -0
  256. package/src/components/sign-chip.tsx +103 -0
  257. package/src/components/swatch-picker.tsx +118 -30
  258. package/src/components/switch.tsx +58 -7
  259. package/src/components/tile-radio.tsx +71 -16
  260. package/src/components/time-input.tsx +8 -1
  261. package/src/components/toggle-group.tsx +206 -73
  262. package/src/components/ui.tsx +472 -105
  263. package/src/data-table.ts +2 -1
  264. package/src/feedback/feedback-attachment.tsx +313 -9
  265. package/src/feedback/feedback-dialog.tsx +2 -0
  266. package/src/feedback/feedback-thread.tsx +66 -5
  267. package/src/i18n/defaults.ts +16 -1
  268. package/src/i18n/german.ts +47 -0
  269. package/src/i18n/kit-labels.tsx +78 -2
  270. package/src/i18n/locales/en.ts +10 -0
  271. package/src/i18n/locales/es.ts +46 -0
  272. package/src/i18n/locales/fr.ts +45 -0
  273. package/src/i18n/locales/hu.ts +46 -0
  274. package/src/i18n/locales/it.ts +47 -0
  275. package/src/i18n/locales/zh.ts +44 -0
  276. package/src/i18n/review.ts +7 -0
  277. package/src/index.ts +53 -3
  278. package/src/lib/countries.ts +91 -0
  279. package/src/lib/decimal-marks.ts +99 -0
  280. package/src/lib/iban.ts +171 -0
  281. package/src/lib/isin.ts +62 -0
  282. package/src/lib/phone.ts +211 -0
  283. package/src/rhf/fields.tsx +606 -9
  284. package/src/rhf.ts +2 -1
  285. package/src/shell/topbar-action-menu.tsx +53 -29
  286. package/src/tour/tour.tsx +66 -28
package/src/data-table.ts CHANGED
@@ -10,8 +10,9 @@ export * from "./components/data-table-pagination";
10
10
  export { FilterPopover } from "./components/data-table-filter-popover";
11
11
  // `SortState` comes from data-table-sort above; re-exporting the module wholesale
12
12
  // would collide with it, which is why the main barrel names these explicitly too.
13
- export { DataTable } from "./components/data-table";
13
+ export { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from "./components/data-table";
14
14
  export type {
15
+ DataTableSortLabels,
15
16
  DataTableColumn,
16
17
  DataTableProps,
17
18
  ServerPagination,
@@ -1,7 +1,7 @@
1
1
  import { cn } from "../lib/cn";
2
2
  import { useEffect, useLayoutEffect, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type RefObject } from "react";
3
- import { Camera, FileText, Paperclip, X } from "lucide-react";
4
- import { Button } from "../components/ui";
3
+ import { Camera, FileText, Image as ImageIcon, Paperclip, X } from "lucide-react";
4
+ import { Button, Spinner } from "../components/ui";
5
5
  import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
6
6
  import type { FeedbackAttachmentLabels } from "./feedback-dialog";
7
7
 
@@ -43,6 +43,10 @@ export interface FeedbackAttachmentFieldLabels {
43
43
  attachmentRemoveFile?: (name: string) => string;
44
44
  /** `multiple` mode: the line shown in place of the add buttons once `max` is reached. */
45
45
  attachmentLimit?: (max: number) => string;
46
+ /** `refs` mode (0.22.0, keksdose K16): the second line of a chip whose upload is still
47
+ * running, where an uploaded one shows its size — "Uploading…". Optional, like the
48
+ * keys above, so a `UiKitLabels` typed before it still compiles. */
49
+ attachmentUploading?: string;
46
50
  }
47
51
 
48
52
  export const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {
@@ -55,16 +59,36 @@ export const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels =
55
59
  attachmentRemoveFile: (name) => `Remove ${name}`,
56
60
  attachmentLimit: (max) =>
57
61
  `Up to ${max} ${max === 1 ? "attachment" : "attachments"} — remove one to add another.`,
62
+ attachmentUploading: "Uploading…",
58
63
  };
59
64
 
60
- /** Why a file was turned away. `"count"` only ever comes from `multiple` mode: more
61
- * files arrived at once than `max` left room for, and the surplus was dropped. */
65
+ /** Why a file was turned away. `"count"` only ever comes from `multiple` (and `refs`)
66
+ * mode: more files arrived at once than `max` left room for, and the surplus was
67
+ * dropped. */
62
68
  export type FeedbackAttachmentError = "type" | "size" | "count";
63
69
 
70
+ /** `refs` mode's reasons: {@link FeedbackAttachmentError}, or `"upload"` — `onUpload`
71
+ * rejected, and what it rejected with is `onError`'s second argument. A type of its
72
+ * own so a host's exhaustive switch over the File modes' reasons stays exhaustive. */
73
+ export type FeedbackAttachmentRefsError = FeedbackAttachmentError | "upload";
74
+
75
+ /**
76
+ * One uploaded attachment in `refs` mode: what the host's upload answered with. `key`
77
+ * identifies it (a storage key, an upload id) and is what a remove goes by; `name` is
78
+ * what the chip says. `size` (bytes) and `type` (mime) are optional — given, the chip
79
+ * shows the size and an image icon or a file icon accordingly.
80
+ */
81
+ export interface FeedbackAttachmentRef {
82
+ key: string;
83
+ name: string;
84
+ size?: number;
85
+ type?: string;
86
+ }
87
+
64
88
  interface FeedbackAttachmentFieldBaseProps {
65
89
  /** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional
66
90
  * since 0.7.0; `attachment` (the heading) is only ever read from here. */
67
- labels?: Partial<FeedbackAttachmentLabels>;
91
+ labels?: Partial<FeedbackAttachmentLabels> & Pick<Partial<FeedbackAttachmentFieldLabels>, "attachmentUploading">;
68
92
  accept?: string[];
69
93
  maxBytes?: number;
70
94
  /** Snapshot the app view behind this and return it as a File. A "Capture
@@ -84,6 +108,7 @@ interface FeedbackAttachmentFieldBaseProps {
84
108
  /** One file: choosing a second means removing the first. The field's only mode until 0.15.5. */
85
109
  export interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFieldBaseProps {
86
110
  multiple?: false;
111
+ refs?: false;
87
112
  value: File | null;
88
113
  onChange: (file: File | null) => void;
89
114
  onError?: (kind: "type" | "size") => void;
@@ -100,6 +125,7 @@ export interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFi
100
125
  */
101
126
  export interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachmentFieldBaseProps {
102
127
  multiple: true;
128
+ refs?: false;
103
129
  value: File[];
104
130
  onChange: (files: File[]) => void;
105
131
  /** How many files `value` may hold. Default {@link DEFAULT_MAX_ATTACHMENTS} (5). The
@@ -119,9 +145,51 @@ export interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachment
119
145
  onScreenshotChange?: (file: File | null) => void;
120
146
  }
121
147
 
148
+ /**
149
+ * Uploaded on pick (0.22.0, keksdose K16). keksdose's support chat uploads each file
150
+ * the moment it is chosen and sends the message with the REFS the uploads answered —
151
+ * a 10 MB screenshot is refused while the reporter is still typing, not when they
152
+ * press Send — so its `AttachmentPicker` could not use this field, whose value is
153
+ * `File`s held until the form is submitted. Here the value is
154
+ * {@link FeedbackAttachmentRef}s: every way in (pick, paste, capture) validates the
155
+ * file as the other modes do, then hands it to `onUpload` and shows a chip with a
156
+ * spinner until it answers; the ref it resolves to joins `value` through `onChange`.
157
+ * A rejection drops the chip and reports `onError("upload", error)`. A chip is removed
158
+ * BY KEY: `onChange` gets the list without it, and `onRemove` the key, for a host that
159
+ * deletes the upload on the server too.
160
+ *
161
+ * Uploads still running count against `max`, so five picks in quick succession cannot
162
+ * overshoot it; they are not in `value` until they land, so a send that reads `value`
163
+ * sends only what has arrived — hold Send back while {@link onUploadingChange} says
164
+ * an upload is running. Several files upload side by side and join in the order they
165
+ * finish. The File modes are unchanged.
166
+ */
167
+ export interface FeedbackAttachmentFieldRefsProps extends FeedbackAttachmentFieldBaseProps {
168
+ refs: true;
169
+ multiple?: never;
170
+ value: FeedbackAttachmentRef[];
171
+ onChange: (refs: FeedbackAttachmentRef[]) => void;
172
+ /** Upload one file; resolve to its ref, or reject to refuse it. Called once per
173
+ * accepted file, the moment it is chosen. */
174
+ onUpload: (file: File) => Promise<FeedbackAttachmentRef>;
175
+ /** A chip was removed — after `onChange` — with its ref's key. */
176
+ onRemove?: (key: string) => void;
177
+ /** Whether any upload is running: `true` when the first starts, `false` when the
178
+ * last settles — for the host's Send, which should wait. */
179
+ onUploadingChange?: (uploading: boolean) => void;
180
+ /** How many refs `value` may hold, uploads in flight included. Default
181
+ * {@link DEFAULT_MAX_ATTACHMENTS} (5). */
182
+ max?: number;
183
+ /** Per file. `"upload"` carries what `onUpload` rejected with. */
184
+ onError?: (kind: FeedbackAttachmentRefsError, error?: unknown) => void;
185
+ screenshot?: never;
186
+ onScreenshotChange?: never;
187
+ }
188
+
122
189
  export type FeedbackAttachmentFieldProps =
123
190
  | FeedbackAttachmentFieldSingleProps
124
- | FeedbackAttachmentFieldMultipleProps;
191
+ | FeedbackAttachmentFieldMultipleProps
192
+ | FeedbackAttachmentFieldRefsProps;
125
193
 
126
194
  /**
127
195
  * Picking one picture: the file dialog, a capture of the app view, or a paste.
@@ -154,9 +222,10 @@ export type FeedbackAttachmentFieldProps =
154
222
  * handed in by whoever renders both the box and this field side by side.
155
223
  */
156
224
  export function FeedbackAttachmentField(props: FeedbackAttachmentFieldProps) {
157
- // Two components rather than one with branches: the modes hold different state
158
- // (one preview vs. one per chip), and a hook order that depends on a prop is a
159
- // crash the day a caller flips it.
225
+ // One component per mode rather than one with branches: the modes hold different
226
+ // state (one preview vs. one per chip vs. uploads in flight), and a hook order that
227
+ // depends on a prop is a crash the day a caller flips it.
228
+ if (props.refs) return <RefsField {...props} />;
160
229
  return props.multiple ? <MultipleField {...props} /> : <SingleField {...props} />;
161
230
  }
162
231
 
@@ -480,6 +549,241 @@ function MultipleField({
480
549
  );
481
550
  }
482
551
 
552
+ /** A file `refs` mode is uploading: its own id (a ref has no key yet), and the file. */
553
+ interface Upload {
554
+ id: number;
555
+ file: File;
556
+ }
557
+
558
+ let nextUploadId = 0;
559
+
560
+ function RefsField({
561
+ value,
562
+ onChange,
563
+ onUpload,
564
+ onRemove,
565
+ onUploadingChange,
566
+ max = DEFAULT_MAX_ATTACHMENTS,
567
+ labels: labelsProp,
568
+ accept = DEFAULT_ATTACHMENT_ACCEPT,
569
+ maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
570
+ onError,
571
+ onCaptureScreenshot,
572
+ documentPaste = false,
573
+ pasteFrom,
574
+ className,
575
+ }: FeedbackAttachmentFieldRefsProps) {
576
+ const rootRef = useRef<HTMLDivElement>(null);
577
+ const fileInputRef = useRef<HTMLInputElement>(null);
578
+ const fileText = useKitFileLabels();
579
+ const text = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
580
+ const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;
581
+ const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;
582
+ const uploadingLabel = text.attachmentUploading ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploading!;
583
+
584
+ // As in `multiple` mode: `value` plus whatever has been handed to `onChange` since
585
+ // the parent last rendered — two uploads landing in one tick must both survive.
586
+ const latest = useRef(value);
587
+ useLayoutEffect(() => {
588
+ latest.current = value;
589
+ });
590
+ const commit = (next: FeedbackAttachmentRef[]) => {
591
+ latest.current = next;
592
+ onChange(next);
593
+ };
594
+
595
+ const [uploads, setUploads] = useState<Upload[]>([]);
596
+ // The uploads in flight, kept in a ref as well: `add` runs several times before a
597
+ // render when files arrive together, and each must see the others — their count
598
+ // against `max`, their names against a pasted image's.
599
+ const inFlight = useRef<Upload[]>([]);
600
+ // Unmounted (the dialog closed mid-upload): a late answer is not this field's to
601
+ // commit, and setting state on it would be a no-op at best.
602
+ const mounted = useRef(true);
603
+ useEffect(() => {
604
+ mounted.current = true;
605
+ return () => {
606
+ mounted.current = false;
607
+ };
608
+ }, []);
609
+ const uploadingChange = useRef(onUploadingChange);
610
+ useEffect(() => {
611
+ uploadingChange.current = onUploadingChange;
612
+ });
613
+
614
+ const room = Math.max(0, max - value.length - uploads.length);
615
+
616
+ const start = (file: File) => {
617
+ const upload: Upload = { id: nextUploadId++, file };
618
+ inFlight.current = [...inFlight.current, upload];
619
+ if (inFlight.current.length === 1) uploadingChange.current?.(true);
620
+ setUploads(inFlight.current);
621
+ const settle = () => {
622
+ inFlight.current = inFlight.current.filter((u) => u.id !== upload.id);
623
+ if (!mounted.current) return;
624
+ setUploads(inFlight.current);
625
+ if (inFlight.current.length === 0) uploadingChange.current?.(false);
626
+ };
627
+ let answer: Promise<FeedbackAttachmentRef>;
628
+ try {
629
+ answer = Promise.resolve(onUpload(file));
630
+ } catch (error) {
631
+ answer = Promise.reject(error);
632
+ }
633
+ answer.then(
634
+ (ref) => {
635
+ settle();
636
+ if (mounted.current) commit([...latest.current, ref]);
637
+ },
638
+ (error: unknown) => {
639
+ settle();
640
+ if (mounted.current) onError?.("upload", error);
641
+ },
642
+ );
643
+ };
644
+
645
+ const add = (incoming: File[], pasted = false) => {
646
+ let space = Math.max(0, max - latest.current.length - inFlight.current.length);
647
+ const taken = new Set([...latest.current.map((r) => r.name), ...inFlight.current.map((u) => u.file.name)]);
648
+ let dropped = false;
649
+ for (const raw of incoming) {
650
+ const problem = rejection(raw, accept, maxBytes);
651
+ if (problem) {
652
+ onError?.(problem);
653
+ continue;
654
+ }
655
+ if (space === 0) {
656
+ dropped = true;
657
+ continue;
658
+ }
659
+ // Pasted images are all "image.png" to the clipboard; as in `multiple` mode,
660
+ // each gets a name of its own before it is uploaded under it.
661
+ const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;
662
+ taken.add(file.name);
663
+ space -= 1;
664
+ start(file);
665
+ }
666
+ if (dropped) onError?.("count");
667
+ };
668
+
669
+ const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom);
670
+
671
+ // Focus after a remove, as in `multiple` mode: the next chip, else the first action.
672
+ const pendingFocus = useRef<number | null>(null);
673
+ useEffect(() => {
674
+ const index = pendingFocus.current;
675
+ if (index === null) return;
676
+ pendingFocus.current = null;
677
+ const root = rootRef.current;
678
+ if (!root) return;
679
+ const removes = root.querySelectorAll<HTMLElement>("[data-attachment-remove]");
680
+ const target =
681
+ removes[index] ?? root.querySelector<HTMLElement>("[data-attachment-action]") ?? removes[index - 1];
682
+ target?.focus();
683
+ }, [value]);
684
+
685
+ const remove = (key: string, index: number) => {
686
+ pendingFocus.current = index;
687
+ commit(latest.current.filter((r) => r.key !== key));
688
+ onRemove?.(key);
689
+ };
690
+
691
+ const hasChips = value.length > 0 || uploads.length > 0;
692
+
693
+ return (
694
+ <div ref={rootRef} className={cn("relative space-y-2", className)} onPaste={onPaste}>
695
+ <Heading text={labelsProp?.attachment} className="mb-0" />
696
+ {hasChips && (
697
+ <ul aria-label={text.attachmentList} className="flex flex-col gap-2">
698
+ {value.map((ref, index) => (
699
+ <li
700
+ key={`ref:${ref.key}`}
701
+ className="flex min-w-0 items-center gap-2 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-1"
702
+ >
703
+ <RefGlyph type={ref.type} />
704
+ <div className="min-w-0 flex-1">
705
+ <div className="truncate text-sm text-[var(--text-secondary)]">{ref.name}</div>
706
+ {ref.size !== undefined && (
707
+ <div className="truncate text-xs text-[var(--text-muted)]">{fileText.size(ref.size)}</div>
708
+ )}
709
+ </div>
710
+ <button
711
+ type="button"
712
+ data-attachment-remove=""
713
+ onClick={() => remove(ref.key, index)}
714
+ aria-label={removeLabel(ref.name)}
715
+ className="shrink-0 rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
716
+ >
717
+ <X className="size-4" />
718
+ </button>
719
+ </li>
720
+ ))}
721
+ {uploads.map((upload) => (
722
+ <li
723
+ key={`upload:${upload.id}`}
724
+ aria-busy="true"
725
+ data-attachment-uploading=""
726
+ className="flex min-w-0 items-center gap-2 rounded-md border border-dashed border-[var(--border)] bg-[var(--bg-surface)] p-1"
727
+ >
728
+ <span className="flex size-12 shrink-0 items-center justify-center rounded border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-placeholder)]">
729
+ <Spinner label={null} className="size-5" />
730
+ </span>
731
+ <div className="min-w-0 flex-1">
732
+ <div className="truncate text-sm text-[var(--text-secondary)]">{upload.file.name}</div>
733
+ <div className="truncate text-xs text-[var(--text-muted)]">{uploadingLabel}</div>
734
+ </div>
735
+ </li>
736
+ ))}
737
+ </ul>
738
+ )}
739
+ {room > 0 && (
740
+ <div className="flex flex-wrap gap-2">
741
+ <Button
742
+ type="button"
743
+ variant="secondary"
744
+ data-attachment-action=""
745
+ onClick={() => fileInputRef.current?.click()}
746
+ >
747
+ <Paperclip className="size-4" /> {text.attachmentAdd}
748
+ </Button>
749
+ {onCaptureScreenshot && (
750
+ <CaptureButton
751
+ capture={onCaptureScreenshot}
752
+ onFile={(file) => add([file])}
753
+ label={text.attachmentCapture}
754
+ data-attachment-action=""
755
+ />
756
+ )}
757
+ </div>
758
+ )}
759
+ <p className="text-xs text-[var(--text-muted)]">{room > 0 ? text.attachmentPaste : limitLabel(max)}</p>
760
+ <input
761
+ ref={fileInputRef}
762
+ type="file"
763
+ multiple={room > 1}
764
+ accept={accept.join(",")}
765
+ className="sr-only"
766
+ tabIndex={-1}
767
+ aria-hidden
768
+ onChange={(e) => {
769
+ add(Array.from(e.target.files ?? []));
770
+ e.target.value = "";
771
+ }}
772
+ />
773
+ </div>
774
+ );
775
+ }
776
+
777
+ /** A ref chip's picture. The bytes are on the server, not here, so there is nothing to
778
+ * preview: an image glyph for an image `type`, a file glyph for anything else. */
779
+ function RefGlyph({ type }: { type?: string }) {
780
+ return (
781
+ <span className="flex size-12 shrink-0 items-center justify-center rounded border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-placeholder)]">
782
+ {type?.startsWith("image/") ? <ImageIcon aria-hidden className="size-6" /> : <FileText aria-hidden className="size-6" />}
783
+ </span>
784
+ );
785
+ }
786
+
483
787
  function Heading({ text, className }: { text?: string; className?: string }) {
484
788
  if (!text) return null;
485
789
  return (
@@ -40,6 +40,8 @@ export interface FeedbackAttachmentLabels {
40
40
  attachmentRemoveFile?: (name: string) => string;
41
41
  /** `multiple` mode: the line shown once `max` files are attached. */
42
42
  attachmentLimit?: (max: number) => string;
43
+ /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */
44
+ attachmentUploading?: string;
43
45
  }
44
46
 
45
47
  export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
@@ -1,4 +1,4 @@
1
- import { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
1
+ import { useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
2
2
  import type { ReactNode, Ref } from "react";
3
3
  import { ImageIcon, ImageOff, Paperclip, Send } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
@@ -535,6 +535,23 @@ export interface FeedbackComposerProps {
535
535
  ref?: Ref<FeedbackComposerHandle>;
536
536
  labels?: Partial<FeedbackComposerLabels>;
537
537
  className?: string;
538
+ /**
539
+ * `"sm"` for a narrow host — keksdose's assistant launcher, a small floating panel
540
+ * (0.22): a small Send button, and the send hint ("Enter to send, Shift + Enter for a
541
+ * new line") hidden from view but kept as the box's description, so a keyboard or
542
+ * screen-reader user still learns the shortcut. Default `"md"`.
543
+ */
544
+ size?: "md" | "sm";
545
+ /**
546
+ * On the composer's root — the box, or the `disabledReason` line in its place —
547
+ * with any `data-*` attribute beside it (keksdose K17). keksdose's assistant tour
548
+ * points at the chat box with `[data-tour="assistant-ask"]`, and a composer that
549
+ * dropped every attribute it was not told about left the anchor nowhere: the
550
+ * assistant kept its hand-built box rather than lose the tour step.
551
+ */
552
+ id?: string;
553
+ /** `data-*` attributes for the root, as `id` — a tour anchor, a test id. */
554
+ [dataAttribute: `data-${string}`]: string | number | boolean | undefined;
538
555
  }
539
556
 
540
557
  /**
@@ -583,8 +600,13 @@ export function FeedbackComposer({
583
600
  ref,
584
601
  labels: labelsProp,
585
602
  className,
603
+ id,
604
+ size = "md",
605
+ ...rest
586
606
  }: FeedbackComposerProps) {
587
607
  const labels = useKitLabels("feedbackComposer", DEFAULT_FEEDBACK_COMPOSER_LABELS, labelsProp);
608
+ const rootAttributes = { id, ...dataAttributes(rest) };
609
+ const hintId = useId();
588
610
  const [ownDraft, setOwnDraft] = useState("");
589
611
  const controlled = value !== undefined;
590
612
  const draft = controlled ? value : ownDraft;
@@ -639,7 +661,11 @@ export function FeedbackComposer({
639
661
  );
640
662
 
641
663
  if (disabledReason != null && disabledReason !== false) {
642
- return <p className={cn("text-xs text-[var(--text-muted)]", className)}>{disabledReason}</p>;
664
+ return (
665
+ <p {...rootAttributes} className={cn("text-xs text-[var(--text-muted)]", className)}>
666
+ {disabledReason}
667
+ </p>
668
+ );
643
669
  }
644
670
 
645
671
  const canSend = (canSendProp ?? draft.trim().length > 0) && !pending;
@@ -658,11 +684,12 @@ export function FeedbackComposer({
658
684
  const config = attachment === true ? {} : attachment || null;
659
685
 
660
686
  return (
661
- <div ref={root} className={cn("space-y-2", className)}>
687
+ <div {...rootAttributes} ref={root} className={cn("space-y-2", className)}>
662
688
  <Textarea
663
689
  rows={rows}
664
690
  dir="auto"
665
691
  aria-label={labels.field}
692
+ aria-describedby={size === "sm" ? hintId : undefined}
666
693
  ref={box}
667
694
  placeholder={placeholder ?? labels.placeholder}
668
695
  value={draft}
@@ -707,10 +734,18 @@ export function FeedbackComposer({
707
734
  <span />
708
735
  )}
709
736
  <div className="ms-auto flex items-center gap-2">
710
- <span className="text-xs text-[var(--text-placeholder)]">
737
+ <span
738
+ id={hintId}
739
+ className={size === "sm" ? "sr-only" : "text-xs text-[var(--text-placeholder)]"}
740
+ >
711
741
  {sendOn === "enter" ? labels.sendHintEnter : labels.sendHint(modifier)}
712
742
  </span>
713
- <Button onClick={send} disabled={!canSend} aria-busy={pending || undefined}>
743
+ <Button
744
+ onClick={send}
745
+ disabled={!canSend}
746
+ aria-busy={pending || undefined}
747
+ size={size === "sm" ? "sm" : undefined}
748
+ >
714
749
  {pending ? (
715
750
  <Spinner label={null} className="size-4" />
716
751
  ) : (
@@ -723,3 +758,29 @@ export function FeedbackComposer({
723
758
  </div>
724
759
  );
725
760
  }
761
+
762
+ /** The `data-*` entries of a rest-props object, and nothing else: whatever a caller
763
+ * spread in that the composer does not know is not the root's to receive. */
764
+ function dataAttributes(props: object): Record<string, unknown> {
765
+ const out: Record<string, unknown> = {};
766
+ for (const [key, value] of Object.entries(props)) if (key.startsWith("data-")) out[key] = value;
767
+ return out;
768
+ }
769
+
770
+ /**
771
+ * {@link FeedbackComposer} under a name that is not about feedback (keksdose K17).
772
+ *
773
+ * The component was never feedback-specific: a textarea, an optional picture or a
774
+ * host's upload slot, Send, `sendOn="enter"` for the chat way. keksdose's assistant
775
+ * and support chat are chats, not reports, and an import of `FeedbackComposer` there
776
+ * reads as a mistake to the next person. The same function, not a wrapper, so the
777
+ * two names cannot drift.
778
+ */
779
+ export const ChatComposer = FeedbackComposer;
780
+ /** {@link FeedbackComposerProps}, for {@link ChatComposer}. */
781
+ export type ChatComposerProps = FeedbackComposerProps;
782
+ /** {@link FeedbackComposerHandle}, for {@link ChatComposer}. */
783
+ export type ChatComposerHandle = FeedbackComposerHandle;
784
+ /** {@link FeedbackComposerLabels}, for {@link ChatComposer} — still the `feedbackComposer`
785
+ * namespace of the provider. */
786
+ export type ChatComposerLabels = FeedbackComposerLabels;
@@ -6,7 +6,11 @@ import { DEFAULT_PAGE_CONTENTS_LABELS } from "../components/page-contents";
6
6
  import { DEFAULT_POPOVER_LABELS } from "../components/popover";
7
7
  import { DEFAULT_CHIP_INPUT_LABELS } from "../components/chip";
8
8
  import { DEFAULT_FIELD_SYNC_LABELS } from "../components/field-sync";
9
- import { DEFAULT_PASSWORD_REVEAL_LABELS, DEFAULT_TABS_LABELS } from "../components/ui";
9
+ import {
10
+ DEFAULT_CHARACTER_COUNT_LABELS,
11
+ DEFAULT_PASSWORD_REVEAL_LABELS,
12
+ DEFAULT_TABS_LABELS,
13
+ } from "../components/ui";
10
14
  import { DEFAULT_WIZARD_LABELS } from "../wizard/types";
11
15
  import { DEFAULT_TOUR_LABELS } from "../tour/tour";
12
16
  import { DEFAULT_COMMAND_PALETTE_LABELS } from "../search/command-palette";
@@ -53,6 +57,11 @@ import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card";
53
57
  import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog";
54
58
  import { DEFAULT_SERVER_WAKE_LABELS } from "../components/server-wake";
55
59
  import { DEFAULT_TRANSLATION_REVIEW_LABELS } from "../components/translation-review-labels";
60
+ import { DEFAULT_COUNTRY_SELECT_LABELS } from "../components/country-select";
61
+ import { DEFAULT_INLINE_EDIT_LABELS } from "../components/inline-edit-field";
62
+ import { DEFAULT_IBAN_INPUT_LABELS } from "../components/iban-input";
63
+ import { DEFAULT_PHONE_INPUT_LABELS } from "../components/phone-input";
64
+ import { DEFAULT_SIGN_CHIP_LABELS } from "../components/sign-chip";
56
65
  import {
57
66
  DEFAULT_APP_SHELL_LABELS,
58
67
  DEFAULT_CALCULATOR_LABELS,
@@ -142,4 +151,10 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
142
151
  serverWake: DEFAULT_SERVER_WAKE_LABELS,
143
152
  translationReview: DEFAULT_TRANSLATION_REVIEW_LABELS,
144
153
  legal: DEFAULT_LEGAL_LABELS,
154
+ characterCount: DEFAULT_CHARACTER_COUNT_LABELS,
155
+ countrySelect: DEFAULT_COUNTRY_SELECT_LABELS,
156
+ inlineEdit: DEFAULT_INLINE_EDIT_LABELS,
157
+ ibanInput: DEFAULT_IBAN_INPUT_LABELS,
158
+ phoneInput: DEFAULT_PHONE_INPUT_LABELS,
159
+ signChip: DEFAULT_SIGN_CHIP_LABELS,
145
160
  };
@@ -28,6 +28,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
28
28
  attachmentScreenshot: "Screenshot",
29
29
  attachmentRemoveFile: (name) => `${name} entfernen`,
30
30
  attachmentLimit: (max) => `Maximal ${n(max)} ${max === 1 ? "Anhang" : "Anhänge"} – zum Hinzufügen erst einen entfernen.`,
31
+ attachmentUploading: "Wird hochgeladen…",
31
32
  },
32
33
  feedbackDialog: {
33
34
  title: "Feedback senden",
@@ -210,6 +211,10 @@ export function germanLabels(numberLocale: string): UiKitLabels {
210
211
  booleanTrue: "Ja",
211
212
  booleanFalse: "Nein",
212
213
  booleanUnset: "Nicht festgelegt",
214
+ sortBy: "Sortieren nach",
215
+ sortDefault: "Standardreihenfolge",
216
+ sortAscending: "Aufsteigend",
217
+ sortDescending: "Absteigend",
213
218
  },
214
219
  miniCalendar: {
215
220
  previousMonth: "Vorheriger Monat",
@@ -249,6 +254,10 @@ export function germanLabels(numberLocale: string): UiKitLabels {
249
254
  previousMonth: "Vorheriger Monat",
250
255
  nextMonth: "Nächster Monat",
251
256
  today: "Heute",
257
+ yearPanel: "Jahr wählen",
258
+ earlierYears: "Frühere Jahre",
259
+ laterYears: "Spätere Jahre",
260
+ thisYear: "Dieses Jahr",
252
261
  },
253
262
  popover: {
254
263
  panel: "Aufklappfenster",
@@ -330,6 +339,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
330
339
  prompt: "Dies kann nicht rückgängig gemacht werden.",
331
340
  password: "Passwort",
332
341
  phrase: (phrase) => `Geben Sie zur Bestätigung „${phrase}“ ein`,
342
+ acknowledge: "Ich habe gelesen, was dies bewirkt, und möchte fortfahren.",
333
343
  },
334
344
  tabs: {
335
345
  // "Tab" is what German UIs say; "Registerkarte" reads like a 1990s manual.
@@ -541,6 +551,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
541
551
  form: {
542
552
  save: "Speichern",
543
553
  cancel: "Abbrechen",
554
+ // German keyboards label the key "Strg"; Apple's ⌘ is the same everywhere.
555
+ submitShortcut: (apple) => (apple ? "⌘ Enter" : "Strg+Enter"),
544
556
  },
545
557
  descriptionList: {
546
558
  empty: "—",
@@ -651,6 +663,41 @@ export function germanLabels(numberLocale: string): UiKitLabels {
651
663
  legal: {
652
664
  navLabel: "Rechtliches",
653
665
  },
666
+ characterCount: {
667
+ // "Zeichen" is the same in singular and plural.
668
+ count: (used, max) => `${n(used)} von ${n(max)} Zeichen`,
669
+ remaining: (left) => `Noch ${n(left)} Zeichen`,
670
+ limitReached: "Zeichenlimit erreicht",
671
+ },
672
+ countrySelect: {
673
+ country: "Land",
674
+ search: "Land suchen",
675
+ others: "Weitere Länder",
676
+ },
677
+ inlineEdit: {
678
+ edit: (label) => `${label} bearbeiten`,
679
+ failed: "Die Änderung konnte nicht gespeichert werden.",
680
+ empty: "Leer",
681
+ },
682
+ ibanInput: {
683
+ format: "Eine IBAN beginnt mit einem Ländercode aus zwei Buchstaben und zwei Prüfziffern.",
684
+ country: (code) => `„${code}“ ist kein Ländercode einer IBAN.`,
685
+ length: (actual, expected) =>
686
+ `Eine IBAN aus diesem Land hat ${n(expected)} Zeichen – diese hat ${n(actual)}.`,
687
+ checksum: "Die Prüfziffern stimmen nicht – vermutlich ist ein Zeichen falsch eingegeben.",
688
+ qrRequired: "Dies ist eine reguläre IBAN. Eine QR-Rechnung braucht die QR-IBAN des Kontos.",
689
+ qrNotAllowed:
690
+ "Dies ist eine QR-IBAN, die nur Zahlungen mit QR-Rechnung empfängt. Geben Sie die reguläre IBAN des Kontos ein.",
691
+ },
692
+ phoneInput: {
693
+ countryCode: "Ländervorwahl",
694
+ other: "Andere",
695
+ },
696
+ signChip: {
697
+ outflow: "Ausgabe",
698
+ inflow: "Einnahme",
699
+ direction: (current, next) => `Richtung: ${current} – zu ${next} wechseln`,
700
+ },
654
701
  translationReview: {
655
702
  statusMissing: "Fehlt",
656
703
  statusUnreviewed: "Ungeprüft",