@eifi1/ui-kit 0.22.0 → 0.24.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 (208) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +8 -7
  3. package/dist/components/amount-input.d.ts +34 -7
  4. package/dist/components/amount-input.js +14 -10
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +26 -0
  7. package/dist/components/autocomplete.js +164 -137
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +8 -7
  10. package/dist/components/calculator.d.ts +8 -7
  11. package/dist/components/column-mapper.d.ts +235 -0
  12. package/dist/components/column-mapper.js +411 -0
  13. package/dist/components/column-mapper.js.map +1 -0
  14. package/dist/components/combobox-core.d.ts +39 -2
  15. package/dist/components/combobox-core.js +29 -6
  16. package/dist/components/combobox-core.js.map +1 -1
  17. package/dist/components/combobox.d.ts +113 -3
  18. package/dist/components/combobox.js +456 -313
  19. package/dist/components/combobox.js.map +1 -1
  20. package/dist/components/confirm-dialog.d.ts +8 -7
  21. package/dist/components/confirm-dialog.js +7 -8
  22. package/dist/components/confirm-dialog.js.map +1 -1
  23. package/dist/components/copy-button.d.ts +5 -4
  24. package/dist/components/country-select.d.ts +50 -7
  25. package/dist/components/country-select.js +65 -9
  26. package/dist/components/country-select.js.map +1 -1
  27. package/dist/components/danger-confirm.d.ts +8 -7
  28. package/dist/components/danger-confirm.js +64 -7
  29. package/dist/components/danger-confirm.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +41 -0
  31. package/dist/components/date-picker.js +269 -8
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/entity-combobox.d.ts +48 -2
  34. package/dist/components/entity-combobox.js +159 -114
  35. package/dist/components/entity-combobox.js.map +1 -1
  36. package/dist/components/facing-pair.d.ts +8 -7
  37. package/dist/components/field-parts.d.ts +116 -6
  38. package/dist/components/field-parts.js +84 -0
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +105 -0
  41. package/dist/components/field-strip.js +46 -0
  42. package/dist/components/field-strip.js.map +1 -0
  43. package/dist/components/file-button.d.ts +8 -7
  44. package/dist/components/file-dropzone.d.ts +8 -7
  45. package/dist/components/form-actions.d.ts +8 -7
  46. package/dist/components/form-actions.js +23 -3
  47. package/dist/components/form-actions.js.map +1 -1
  48. package/dist/components/iban-input.d.ts +8 -7
  49. package/dist/components/icon-picker.d.ts +10 -1
  50. package/dist/components/icon-picker.js +12 -4
  51. package/dist/components/icon-picker.js.map +1 -1
  52. package/dist/components/language-select.d.ts +8 -7
  53. package/dist/components/money-field.d.ts +11 -7
  54. package/dist/components/money-field.js.map +1 -1
  55. package/dist/components/month-picker.js +2 -1
  56. package/dist/components/month-picker.js.map +1 -1
  57. package/dist/components/multi-entity-combobox.d.ts +31 -2
  58. package/dist/components/multi-entity-combobox.js +152 -103
  59. package/dist/components/multi-entity-combobox.js.map +1 -1
  60. package/dist/components/number-field.d.ts +8 -7
  61. package/dist/components/number-input.d.ts +8 -7
  62. package/dist/components/numpad-sheet.d.ts +8 -7
  63. package/dist/components/phone-input.d.ts +8 -7
  64. package/dist/components/reauth-dialog.d.ts +2 -2
  65. package/dist/components/reauth-dialog.js +6 -8
  66. package/dist/components/reauth-dialog.js.map +1 -1
  67. package/dist/components/series-chart.d.ts +8 -7
  68. package/dist/components/settings-fields.d.ts +8 -7
  69. package/dist/components/share-card.d.ts +8 -7
  70. package/dist/components/swatch-picker.d.ts +17 -1
  71. package/dist/components/swatch-picker.js +10 -4
  72. package/dist/components/swatch-picker.js.map +1 -1
  73. package/dist/components/text-link.d.ts +8 -7
  74. package/dist/components/tile-radio.d.ts +20 -0
  75. package/dist/components/tile-radio.js +25 -8
  76. package/dist/components/tile-radio.js.map +1 -1
  77. package/dist/components/time-input.d.ts +8 -7
  78. package/dist/components/toggle-group.js +2 -2
  79. package/dist/components/toggle-group.js.map +1 -1
  80. package/dist/components/trigger-aria.d.ts +3 -0
  81. package/dist/components/trigger-aria.js +3 -1
  82. package/dist/components/trigger-aria.js.map +1 -1
  83. package/dist/components/ui.d.ts +5 -4
  84. package/dist/components/ui.js +60 -28
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/feedback/feedback-attachment.d.ts +66 -1
  87. package/dist/feedback/feedback-attachment.js +185 -65
  88. package/dist/feedback/feedback-attachment.js.map +1 -1
  89. package/dist/feedback/feedback-dialog.d.ts +66 -1
  90. package/dist/feedback/feedback-dialog.js.map +1 -1
  91. package/dist/feedback/feedback-inbox.d.ts +67 -252
  92. package/dist/feedback/feedback-inbox.js.map +1 -1
  93. package/dist/feedback/feedback-thread.d.ts +66 -287
  94. package/dist/feedback/feedback-thread.js +16 -3
  95. package/dist/feedback/feedback-thread.js.map +1 -1
  96. package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
  97. package/dist/feedback.d.ts +65 -3
  98. package/dist/hooks/use-file-drop.d.ts +8 -7
  99. package/dist/i18n/defaults.d.ts +8 -7
  100. package/dist/i18n/defaults.js +3 -1
  101. package/dist/i18n/defaults.js.map +1 -1
  102. package/dist/i18n/german.d.ts +8 -7
  103. package/dist/i18n/german.js +36 -2
  104. package/dist/i18n/german.js.map +1 -1
  105. package/dist/i18n/kit-labels.d.ts +5 -4
  106. package/dist/i18n/kit-labels.js.map +1 -1
  107. package/dist/i18n/languages.d.ts +8 -7
  108. package/dist/i18n/locales/de-CH.d.ts +8 -7
  109. package/dist/i18n/locales/en.d.ts +8 -7
  110. package/dist/i18n/locales/en.js +7 -0
  111. package/dist/i18n/locales/en.js.map +1 -1
  112. package/dist/i18n/locales/es.d.ts +8 -7
  113. package/dist/i18n/locales/es.js +37 -2
  114. package/dist/i18n/locales/es.js.map +1 -1
  115. package/dist/i18n/locales/fr.d.ts +8 -7
  116. package/dist/i18n/locales/fr.js +36 -2
  117. package/dist/i18n/locales/fr.js.map +1 -1
  118. package/dist/i18n/locales/hu.d.ts +8 -7
  119. package/dist/i18n/locales/hu.js +38 -2
  120. package/dist/i18n/locales/hu.js.map +1 -1
  121. package/dist/i18n/locales/it.d.ts +8 -7
  122. package/dist/i18n/locales/it.js +37 -2
  123. package/dist/i18n/locales/it.js.map +1 -1
  124. package/dist/i18n/locales/zh.d.ts +8 -7
  125. package/dist/i18n/locales/zh.js +34 -2
  126. package/dist/i18n/locales/zh.js.map +1 -1
  127. package/dist/i18n/review.d.ts +8 -7
  128. package/dist/i18n/review.js +20 -1
  129. package/dist/i18n/review.js.map +1 -1
  130. package/dist/index.d.ts +6 -4
  131. package/dist/index.js +16 -0
  132. package/dist/index.js.map +1 -1
  133. package/dist/lib/column-mapping.d.ts +81 -0
  134. package/dist/lib/column-mapping.js +108 -0
  135. package/dist/lib/column-mapping.js.map +1 -0
  136. package/dist/lib/strip-fade.d.ts +2 -2
  137. package/dist/lib/strip-fade.js.map +1 -1
  138. package/dist/lib/table-text.d.ts +109 -1
  139. package/dist/lib/table-text.js +122 -1
  140. package/dist/lib/table-text.js.map +1 -1
  141. package/dist/rhf/fields.d.ts +142 -13
  142. package/dist/rhf/fields.js +185 -3
  143. package/dist/rhf/fields.js.map +1 -1
  144. package/dist/rhf/form.d.ts +8 -7
  145. package/dist/rhf.d.ts +9 -8
  146. package/dist/rhf.js.map +1 -1
  147. package/dist/shell/app-shell.d.ts +8 -7
  148. package/dist/shell/top-bar-brand.d.ts +8 -7
  149. package/dist/shell/topbar-action-menu.d.ts +36 -3
  150. package/dist/shell/topbar-action-menu.js +74 -33
  151. package/dist/shell/topbar-action-menu.js.map +1 -1
  152. package/dist/shell.d.ts +8 -7
  153. package/dist/table-text.d.ts +1 -1
  154. package/dist/wizard/stepper-nav.d.ts +38 -9
  155. package/dist/wizard/stepper-nav.js +4 -0
  156. package/dist/wizard/stepper-nav.js.map +1 -1
  157. package/dist/wizard.d.ts +8 -7
  158. package/package.json +1 -1
  159. package/src/components/amount-input.tsx +50 -14
  160. package/src/components/autocomplete.tsx +188 -116
  161. package/src/components/column-mapper.tsx +768 -0
  162. package/src/components/combobox-core.tsx +82 -9
  163. package/src/components/combobox.tsx +655 -335
  164. package/src/components/confirm-dialog.tsx +12 -8
  165. package/src/components/country-select.tsx +135 -22
  166. package/src/components/danger-confirm.tsx +274 -24
  167. package/src/components/date-picker.tsx +423 -10
  168. package/src/components/entity-combobox.tsx +243 -129
  169. package/src/components/field-parts.tsx +224 -5
  170. package/src/components/field-strip.tsx +149 -0
  171. package/src/components/form-actions.tsx +52 -5
  172. package/src/components/icon-picker.tsx +23 -4
  173. package/src/components/money-field.tsx +3 -0
  174. package/src/components/month-picker.tsx +2 -1
  175. package/src/components/multi-entity-combobox.tsx +221 -121
  176. package/src/components/reauth-dialog.tsx +17 -18
  177. package/src/components/swatch-picker.tsx +29 -5
  178. package/src/components/tile-radio.tsx +74 -13
  179. package/src/components/toggle-group.tsx +2 -2
  180. package/src/components/trigger-aria.ts +5 -0
  181. package/src/components/ui.tsx +114 -33
  182. package/src/feedback/feedback-attachment.tsx +372 -72
  183. package/src/feedback/feedback-dialog.tsx +7 -3
  184. package/src/feedback/feedback-inbox.tsx +3 -2
  185. package/src/feedback/feedback-thread.tsx +112 -9
  186. package/src/i18n/defaults.ts +2 -0
  187. package/src/i18n/german.ts +42 -0
  188. package/src/i18n/kit-labels.tsx +4 -0
  189. package/src/i18n/locales/en.ts +27 -5
  190. package/src/i18n/locales/es.ts +41 -0
  191. package/src/i18n/locales/fr.ts +42 -0
  192. package/src/i18n/locales/hu.ts +37 -0
  193. package/src/i18n/locales/it.ts +41 -0
  194. package/src/i18n/locales/zh.ts +32 -0
  195. package/src/i18n/review.ts +19 -0
  196. package/src/index.ts +18 -0
  197. package/src/lib/column-mapping.ts +234 -0
  198. package/src/lib/strip-fade.ts +2 -2
  199. package/src/lib/table-text.ts +271 -0
  200. package/src/rhf/fields.tsx +421 -6
  201. package/src/rhf.ts +2 -1
  202. package/src/shell/topbar-action-menu.tsx +134 -38
  203. package/src/wizard/stepper-nav.tsx +35 -1
  204. package/dist/components/field-anatomy.d.ts +0 -95
  205. package/dist/components/field-anatomy.js +0 -84
  206. package/dist/components/field-anatomy.js.map +0 -1
  207. package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
  208. package/src/components/field-anatomy.tsx +0 -190
@@ -10,6 +10,7 @@ import {
10
10
  DEFAULT_MAX_ATTACHMENT_BYTES,
11
11
  FeedbackAttachmentField,
12
12
  type FeedbackAttachmentError,
13
+ type FeedbackAttachmentErrorInfo,
13
14
  } from "./feedback-attachment";
14
15
 
15
16
  export interface FeedbackCategoryOption {
@@ -157,7 +158,9 @@ interface FeedbackDialogBaseProps {
157
158
  export interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {
158
159
  attachments?: "single";
159
160
  onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
160
- onAttachmentError?: (kind: "type" | "size") => void;
161
+ /** `info` (0.23.0) names the refused file and the limit — see
162
+ * {@link FeedbackAttachmentErrorInfo}. */
163
+ onAttachmentError?: (kind: "type" | "size", info: FeedbackAttachmentErrorInfo) => void;
161
164
  /** Multiple mode only — refused here rather than silently ignored. */
162
165
  maxAttachments?: never;
163
166
  }
@@ -172,8 +175,9 @@ export interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {
172
175
  /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */
173
176
  maxAttachments?: number;
174
177
  onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;
175
- /** `"count"`: more files arrived at once than there was room for; the surplus was dropped. */
176
- onAttachmentError?: (kind: FeedbackAttachmentError) => void;
178
+ /** `"count"`: more files arrived at once than there was room for; the surplus was
179
+ * dropped. `info` (0.23.0) names the refused file(s) and the limit. */
180
+ onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;
177
181
  }
178
182
 
179
183
  export type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;
@@ -18,7 +18,7 @@ import type { LucideIcon } from "lucide-react";
18
18
  import { cn } from "../lib/cn";
19
19
  import { Button, Textarea } from "../components/ui";
20
20
  import { Tooltip } from "../components/tooltip";
21
- import { FeedbackAttachmentField } from "./feedback-attachment";
21
+ import { FeedbackAttachmentField, type FeedbackAttachmentErrorInfo } from "./feedback-attachment";
22
22
  import type { FeedbackAttachmentLabels } from "./feedback-dialog";
23
23
 
24
24
  /**
@@ -566,7 +566,8 @@ export interface FeedbackNoteAttachment {
566
566
  labels: FeedbackAttachmentLabels;
567
567
  accept?: string[];
568
568
  maxBytes?: number;
569
- onError?: (kind: "type" | "size") => void;
569
+ /** `info` (0.23.0) names the refused file and the limit. */
570
+ onError?: (kind: "type" | "size", info: FeedbackAttachmentErrorInfo) => void;
570
571
  onCaptureScreenshot?: () => Promise<File | null>;
571
572
  }
572
573
 
@@ -1,13 +1,15 @@
1
1
  import { useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
2
- import type { ReactNode, Ref } from "react";
2
+ import type { ReactNode, Ref, RefObject } from "react";
3
3
  import { ImageIcon, ImageOff, Paperclip, Send } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { formatDate, formatRelativeTime } from "../lib/format";
6
6
  import { Button, EmptyState, Spinner, Textarea } from "../components/ui";
7
+ import type { CharacterCountLabels } from "../components/ui";
7
8
  import { Skeleton } from "../components/skeleton";
8
9
  import { Tooltip } from "../components/tooltip";
9
10
  import { useKitFileLabels, useKitLabels, useKitLocale } from "../i18n/kit-labels";
10
11
  import { FeedbackAttachmentField } from "./feedback-attachment";
12
+ import type { FeedbackAttachmentFieldSingleProps } from "./feedback-attachment";
11
13
  import type { FeedbackNoteAttachment } from "./feedback-inbox";
12
14
  import type { FeedbackAttachmentLabels } from "./feedback-dialog";
13
15
 
@@ -463,10 +465,48 @@ function toDate(value: Date | string | number): Date | null {
463
465
  /* ── Composer ─────────────────────────────────────────────────────────────── */
464
466
 
465
467
  /** What the composer's attach control takes — {@link FeedbackNoteAttachment} with the
466
- * labels optional, since the field reads `feedbackAttachment` from the provider. */
467
- export type FeedbackComposerAttachment = Omit<FeedbackNoteAttachment, "labels"> & {
468
+ * labels optional, since the field reads `feedbackAttachment` from the provider, and
469
+ * `onError` the field's own (0.23.0): it is handed the refused file and the limit too
470
+ * (`FeedbackAttachmentErrorInfo`), and a `(kind) => …` handler still fits. Since
471
+ * 0.24.0 also the field's `buttonVariant` / `buttonSize`, for a composer whose row
472
+ * wants smaller or quieter attach buttons than the bordered `md` default. */
473
+ export type FeedbackComposerAttachment = Omit<FeedbackNoteAttachment, "labels" | "onError"> & {
468
474
  labels?: Partial<FeedbackAttachmentLabels>;
469
- };
475
+ onError?: FeedbackAttachmentFieldSingleProps["onError"];
476
+ } & Pick<FeedbackAttachmentFieldSingleProps, "buttonVariant" | "buttonSize">;
477
+
478
+ /**
479
+ * What a render-prop {@link FeedbackComposerProps.attachmentSlot} is handed (0.24.0,
480
+ * keksdose).
481
+ *
482
+ * keksdose's support chat put `<FeedbackAttachmentField refs>` in the slot and a
483
+ * screenshot pasted into the reply box never reached it: the box is the field's
484
+ * sibling, so the paste bubbles to their common parent — the composer's root — and the
485
+ * field hears it only through `pasteFrom`, which needs that root. The composer kept it
486
+ * to itself, so keksdose wrapped the whole composer in a ref'd `<div>` of its own just
487
+ * to have an element to hand over.
488
+ */
489
+ export interface FeedbackComposerSlotContext {
490
+ /**
491
+ * The composer's root, round the text box and this slot — the field's `pasteFrom`,
492
+ * as it is: `pasteFrom={slot.root}`.
493
+ *
494
+ * A ref, not the element, because `pasteFrom` is one: the field reads `.current` in
495
+ * an effect, and the slot renders INSIDE this root, so the two are committed together
496
+ * and React attaches the root before any effect of the slot's runs — the first paste
497
+ * after the first paint already lands. The element itself would be `null` on the
498
+ * first render (it does not exist yet) and could only arrive by a second render
499
+ * through state; the ref is the same object every render, so the field's listener is
500
+ * added once and never re-added.
501
+ *
502
+ * With the built-in `attachment` on as well, it listens on this root too: hand the
503
+ * root to one field only, or an image pasted once is attached twice.
504
+ */
505
+ root: RefObject<HTMLElement | null>;
506
+ /** The composer's `pending` — a send is in flight. For the field's `disabled`
507
+ * (keksdose G5b): the message carries the attachments as they were at the press. */
508
+ pending: boolean;
509
+ }
470
510
 
471
511
  export interface FeedbackComposerProps {
472
512
  /**
@@ -491,14 +531,27 @@ export interface FeedbackComposerProps {
491
531
  * The slot is the host's picker plus the chips of what is uploaded; the upload, the
492
532
  * refs and clearing them once `onSend` resolves stay with the host. Pair it with
493
533
  * `canSend`.
534
+ *
535
+ * A node, or (0.24.0) a function of {@link FeedbackComposerSlotContext} that returns
536
+ * one — for a field that wants the composer's root as its `pasteFrom`, so a
537
+ * screenshot pasted into the box reaches it, and `pending` for its `disabled`:
538
+ *
539
+ * ```tsx
540
+ * attachmentSlot={({ root, pending }) => (
541
+ * <FeedbackAttachmentField refs pasteFrom={root} disabled={pending} … />
542
+ * )}
543
+ * ```
544
+ *
545
+ * Called once per render of the composer, and not at all while `disabledReason`
546
+ * stands in for it.
494
547
  */
495
- attachmentSlot?: ReactNode;
548
+ attachmentSlot?: ReactNode | ((slot: FeedbackComposerSlotContext) => ReactNode);
496
549
  /**
497
550
  * Whether Send is enabled, over the composer's own rule (some text in the box) —
498
551
  * for state the composer cannot see, i.e. `attachmentSlot`'s (keksdose G5): `true`
499
552
  * with files ready makes an empty draft sendable (`onSend("", null)`), `false` while
500
553
  * an upload is still running holds even a written reply back. Left out, the text
501
- * decides. `pending` blocks a send either way.
554
+ * decides. `pending` blocks a send either way, as does a draft past `maxLength`.
502
555
  */
503
556
  canSend?: boolean;
504
557
  /**
@@ -513,6 +566,31 @@ export interface FeedbackComposerProps {
513
566
  * reader. */
514
567
  disabledReason?: ReactNode;
515
568
  rows?: number;
569
+ /**
570
+ * The longest draft the box takes, in UTF-16 code units as the browser counts — the
571
+ * server's limit, so a reply too long for it is cut off while it is typed rather than
572
+ * refused by a 422 after the press (keksdose G6: its support replies are capped at
573
+ * 4,000 on the server, and its other text fields already count against that with
574
+ * Textarea's `maxLength` + `showCount` since 0.22; the reply box, a composer, could
575
+ * not). The box's own `maxLength`, so typing and pasting stop at it.
576
+ *
577
+ * {@link FeedbackComposerHandle.insertText} is not stopped by it — the browser limits
578
+ * typing, not a script — and a canned reply is not cut mid-sentence to fit. A draft
579
+ * past the limit therefore holds Send back (whatever `canSend` says, as `pending`
580
+ * does) until it is trimmed; with `showCount` the counter, red at the limit, says by
581
+ * how much.
582
+ */
583
+ maxLength?: number;
584
+ /**
585
+ * A live "12/4000" under the box, with `maxLength` — Textarea's `showCount`, the same
586
+ * counter (keksdose G6): the count is part of the box's description, read with it on
587
+ * focus, and a polite live region speaks only on entering the last stretch and at the
588
+ * limit, never per keystroke. Under `size="sm"` the hidden send hint stays the box's
589
+ * description too, before the count. Ignored without `maxLength`.
590
+ */
591
+ showCount?: boolean;
592
+ /** The counter's words — the `characterCount` namespace, as on Textarea. */
593
+ countLabels?: Partial<CharacterCountLabels>;
516
594
  /**
517
595
  * The box's hint, over the `placeholder` label — keksdose F9: an admin answering a
518
596
  * support ticket is asked something else ("Reply to Anna…") than a member adding to
@@ -594,6 +672,9 @@ export function FeedbackComposer({
594
672
  sendOn = "mod-enter",
595
673
  disabledReason,
596
674
  rows = 3,
675
+ maxLength,
676
+ showCount,
677
+ countLabels,
597
678
  placeholder,
598
679
  value,
599
680
  onValueChange,
@@ -668,7 +749,10 @@ export function FeedbackComposer({
668
749
  );
669
750
  }
670
751
 
671
- const canSend = (canSendProp ?? draft.trim().length > 0) && !pending;
752
+ // Longer than `maxLength` only through `insertText` or a controlled value (typing
753
+ // stops at it): the server would refuse it, so it does not go.
754
+ const tooLong = maxLength !== undefined && draft.length > maxLength;
755
+ const canSend = (canSendProp ?? draft.trim().length > 0) && !pending && !tooLong;
672
756
  const send = () => {
673
757
  if (!canSend) return;
674
758
  const sentDraft = draft;
@@ -682,6 +766,14 @@ export function FeedbackComposer({
682
766
  };
683
767
 
684
768
  const config = attachment === true ? {} : attachment || null;
769
+ // The slot is handed the ref OBJECT, for a field's `pasteFrom`, which reads `.current`
770
+ // in an effect once the root is attached — nothing reads it during this render (see
771
+ // FeedbackComposerSlotContext.root).
772
+ const slot =
773
+ typeof attachmentSlot === "function"
774
+ ? // eslint-disable-next-line react-hooks/refs -- handed on for an effect, not read here
775
+ attachmentSlot({ root, pending })
776
+ : attachmentSlot;
685
777
 
686
778
  return (
687
779
  <div {...rootAttributes} ref={root} className={cn("space-y-2", className)}>
@@ -689,7 +781,14 @@ export function FeedbackComposer({
689
781
  rows={rows}
690
782
  dir="auto"
691
783
  aria-label={labels.field}
784
+ // Textarea puts the counter's description after this one, so under "sm" the box
785
+ // reads "Enter to send… 12 of 4000 characters".
692
786
  aria-describedby={size === "sm" ? hintId : undefined}
787
+ maxLength={maxLength}
788
+ // Passed only when given: Textarea keeps a wrapper for a `showCount` it was
789
+ // handed at all, and a composer without one keeps the DOM it had.
790
+ {...(showCount !== undefined ? { showCount } : null)}
791
+ countLabels={countLabels}
693
792
  ref={box}
694
793
  placeholder={placeholder ?? labels.placeholder}
695
794
  value={draft}
@@ -713,7 +812,7 @@ export function FeedbackComposer({
713
812
  }}
714
813
  />
715
814
  <div className="flex flex-wrap items-end justify-between gap-2">
716
- {config || attachmentSlot != null ? (
815
+ {config || slot != null ? (
717
816
  <div className="flex min-w-0 flex-wrap items-end gap-2">
718
817
  {config && (
719
818
  <FeedbackAttachmentField
@@ -724,11 +823,13 @@ export function FeedbackComposer({
724
823
  maxBytes={config.maxBytes}
725
824
  onError={config.onError}
726
825
  onCaptureScreenshot={config.onCaptureScreenshot}
826
+ buttonVariant={config.buttonVariant}
827
+ buttonSize={config.buttonSize}
727
828
  // The box is this field's sibling; a paste in it bubbles to `root`.
728
829
  pasteFrom={root}
729
830
  />
730
831
  )}
731
- {attachmentSlot}
832
+ {slot}
732
833
  </div>
733
834
  ) : (
734
835
  <span />
@@ -784,3 +885,5 @@ export type ChatComposerHandle = FeedbackComposerHandle;
784
885
  /** {@link FeedbackComposerLabels}, for {@link ChatComposer} — still the `feedbackComposer`
785
886
  * namespace of the provider. */
786
887
  export type ChatComposerLabels = FeedbackComposerLabels;
888
+ /** {@link FeedbackComposerSlotContext}, for {@link ChatComposer}'s `attachmentSlot`. */
889
+ export type ChatComposerSlotContext = FeedbackComposerSlotContext;
@@ -62,6 +62,7 @@ import { DEFAULT_INLINE_EDIT_LABELS } from "../components/inline-edit-field";
62
62
  import { DEFAULT_IBAN_INPUT_LABELS } from "../components/iban-input";
63
63
  import { DEFAULT_PHONE_INPUT_LABELS } from "../components/phone-input";
64
64
  import { DEFAULT_SIGN_CHIP_LABELS } from "../components/sign-chip";
65
+ import { DEFAULT_COLUMN_MAPPER_LABELS } from "../components/column-mapper";
65
66
  import {
66
67
  DEFAULT_APP_SHELL_LABELS,
67
68
  DEFAULT_CALCULATOR_LABELS,
@@ -157,4 +158,5 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
157
158
  ibanInput: DEFAULT_IBAN_INPUT_LABELS,
158
159
  phoneInput: DEFAULT_PHONE_INPUT_LABELS,
159
160
  signChip: DEFAULT_SIGN_CHIP_LABELS,
161
+ columnMapper: DEFAULT_COLUMN_MAPPER_LABELS,
160
162
  };
@@ -29,6 +29,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
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
31
  attachmentUploading: "Wird hochgeladen…",
32
+ attachmentUploadFailed: (name) => `„${name}“ konnte nicht hochgeladen werden`,
32
33
  },
33
34
  feedbackDialog: {
34
35
  title: "Feedback senden",
@@ -340,6 +341,9 @@ export function germanLabels(numberLocale: string): UiKitLabels {
340
341
  password: "Passwort",
341
342
  phrase: (phrase) => `Geben Sie zur Bestätigung „${phrase}“ ein`,
342
343
  acknowledge: "Ich habe gelesen, was dies bewirkt, und möchte fortfahren.",
344
+ needsPhrase: (phrase) => `Geben Sie zur Bestätigung „${phrase}“ ein`,
345
+ needsAcknowledge: "Setzen Sie zur Bestätigung das Häkchen",
346
+ needsPassword: "Geben Sie zur Bestätigung Ihr Passwort ein",
343
347
  },
344
348
  tabs: {
345
349
  // "Tab" is what German UIs say; "Registerkarte" reads like a 1990s manual.
@@ -698,6 +702,44 @@ export function germanLabels(numberLocale: string): UiKitLabels {
698
702
  inflow: "Einnahme",
699
703
  direction: (current, next) => `Richtung: ${current} – zu ${next} wechseln`,
700
704
  },
705
+ columnMapper: {
706
+ paste: "Tabelle einfügen",
707
+ pasteHint:
708
+ "Die Zeilen aus einer Tabellenkalkulation kopieren und hier einfügen – oder eine CSV- oder Textdatei hier ablegen.",
709
+ chooseFile: "Datei auswählen",
710
+ readError: (name) => `„${name}“ konnte nicht gelesen werden`,
711
+ headerRow: "Die erste Zeile enthält die Spaltennamen",
712
+ summary: (columns, rows) =>
713
+ `${n(columns)} ${columns === 1 ? "Spalte" : "Spalten"}, ${n(rows)} ${rows === 1 ? "Zeile" : "Zeilen"}`,
714
+ // Fragments of the summary line ("3 Spalten, 120 Zeilen · durch Semikolons getrennt").
715
+ separatorSemicolon: "durch Semikolons getrennt",
716
+ separatorComma: "durch Kommas getrennt",
717
+ separatorTab: "durch Tabulatoren getrennt",
718
+ separatorSpace: "durch Leerzeichen getrennt",
719
+ // The examples stay as written: they show the convention, not the reader's locale.
720
+ decimalComma: "Dezimalkomma (1,5)",
721
+ decimalPoint: "Dezimalpunkt (1.5)",
722
+ unreadCount: (count) =>
723
+ count === 1
724
+ ? "1 Zeile konnte nicht gelesen werden"
725
+ : `${n(count)} Zeilen konnten nicht gelesen werden`,
726
+ // A line number is a position, not a count: unformatted, as `measuredGrid.lineError`.
727
+ unreadLine: (line) => `Zeile ${line} konnte nicht gelesen werden`,
728
+ unreadMore: (count) => `…und ${n(count)} weitere`,
729
+ noRows: "Keine Zeile dieses Textes lässt sich als Tabellenzeile lesen.",
730
+ table: "Spaltenzuordnung",
731
+ columnN: (column) => `Spalte ${column}`,
732
+ roleOf: (column) => `Was enthält die Spalte „${column}“?`,
733
+ ignore: "Ignorieren",
734
+ requiredRole: (role) => `${role} (erforderlich)`,
735
+ previewOf: (shown, total) =>
736
+ shown === 1
737
+ ? `Die erste von ${n(total)} Zeilen`
738
+ : `Die ersten ${n(shown)} von ${n(total)} Zeilen`,
739
+ // `roles` arrives joined with "oder" ("Soll oder Haben").
740
+ oneOf: (roles) => `entweder ${roles}`,
741
+ missing: (roles) => `Noch erforderlich: ${roles}.`,
742
+ },
701
743
  translationReview: {
702
744
  statusMissing: "Fehlt",
703
745
  statusUnreviewed: "Ungeprüft",
@@ -58,6 +58,7 @@ import type { InlineEditLabels } from "../components/inline-edit-field";
58
58
  import type { IbanInputLabels } from "../components/iban-input";
59
59
  import type { PhoneInputLabels } from "../components/phone-input";
60
60
  import type { SignChipLabels } from "../components/sign-chip";
61
+ import type { ColumnMapperLabels } from "../components/column-mapper";
61
62
 
62
63
  /**
63
64
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -311,6 +312,9 @@ export interface UiKitLabels {
311
312
  phoneInput: PhoneInputLabels;
312
313
  /** 0.22.0: `SignChip` — the outflow / inflow toggle beside an amount. */
313
314
  signChip: SignChipLabels;
315
+ /** 0.23.0: `ColumnMapper` / `ColumnRoleTable` — a pasted or dropped table, a role per
316
+ * column. */
317
+ columnMapper: ColumnMapperLabels;
314
318
  }
315
319
 
316
320
  /**
@@ -33,7 +33,9 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
33
33
  ...d.measuredGrid,
34
34
  points: (count) => (count === 1 ? "1 point" : `${n(count)} points`),
35
35
  problems: (count) =>
36
- count === 1 ? "1 cell is not a number" : `${n(count)} cells are not numbers`,
36
+ count === 1
37
+ ? "1 cell is not a number"
38
+ : `${n(count)} cells are not numbers`,
37
39
  },
38
40
  dataTable: {
39
41
  ...d.dataTable,
@@ -51,9 +53,12 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
51
53
  combobox: {
52
54
  ...d.combobox,
53
55
  selectedCount: (count) => `${n(count)} selected`,
54
- resultCount: (count) => (count === 1 ? "1 result" : `${n(count)} results`),
56
+ resultCount: (count) =>
57
+ count === 1 ? "1 result" : `${n(count)} results`,
55
58
  minChars: (count) =>
56
- count === 1 ? "Type at least 1 character" : `Type at least ${n(count)} characters`,
59
+ count === 1
60
+ ? "Type at least 1 character"
61
+ : `Type at least ${n(count)} characters`,
57
62
  },
58
63
  multiSelect: {
59
64
  ...d.multiSelect,
@@ -81,7 +86,9 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
81
86
  `“${name}” was not added: at most ${n(maxFiles)} ${maxFiles === 1 ? "file" : "files"}`,
82
87
  rejectedMany: (count) => `${n(count)} files were not added`,
83
88
  rejectedPick: (count) =>
84
- count === 1 ? "The file was not added" : `None of the ${n(count)} files were added`,
89
+ count === 1
90
+ ? "The file was not added"
91
+ : `None of the ${n(count)} files were added`,
85
92
  selected: (count, firstName) =>
86
93
  count === 1 ? `“${firstName}” selected` : `${n(count)} files selected`,
87
94
  },
@@ -123,12 +130,27 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
123
130
  characterCount: {
124
131
  ...d.characterCount,
125
132
  count: (used, max) => `${n(used)} of ${n(max)} characters`,
126
- remaining: (left) => (left === 1 ? "1 character left" : `${n(left)} characters left`),
133
+ remaining: (left) =>
134
+ left === 1 ? "1 character left" : `${n(left)} characters left`,
127
135
  },
128
136
  ibanInput: {
129
137
  ...d.ibanInput,
130
138
  length: (actual, expected) =>
131
139
  `An IBAN from this country has ${n(expected)} characters — this one has ${n(actual)}.`,
132
140
  },
141
+ columnMapper: {
142
+ ...d.columnMapper,
143
+ summary: (columns, rows) =>
144
+ `${n(columns)} ${columns === 1 ? "column" : "columns"}, ${n(rows)} ${rows === 1 ? "row" : "rows"}`,
145
+ unreadCount: (count) =>
146
+ count === 1
147
+ ? "1 line could not be read"
148
+ : `${n(count)} lines could not be read`,
149
+ unreadMore: (count) => `…and ${n(count)} more`,
150
+ previewOf: (shown, total) =>
151
+ shown === 1
152
+ ? `The first of ${n(total)} rows`
153
+ : `The first ${n(shown)} of ${n(total)} rows`,
154
+ },
133
155
  };
134
156
  }
@@ -26,6 +26,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
26
26
  attachmentRemoveFile: (name) => `Quitar ${name}`,
27
27
  attachmentLimit: (max) => `Máximo ${n(max)} ${max === 1 ? "adjunto" : "adjuntos"}: quite uno para añadir otro.`,
28
28
  attachmentUploading: "Subiendo…",
29
+ attachmentUploadFailed: (name) => `No se ha podido subir «${name}»`,
29
30
  },
30
31
  feedbackDialog: {
31
32
  title: "Enviar comentarios",
@@ -330,6 +331,9 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
330
331
  password: "Contraseña",
331
332
  phrase: (phrase) => `Escriba «${phrase}» para confirmar`,
332
333
  acknowledge: "He leído lo que hace esta acción y deseo continuar.",
334
+ needsPhrase: (phrase) => `Escriba «${phrase}» para confirmar`,
335
+ needsAcknowledge: "Marque la casilla para confirmar",
336
+ needsPassword: "Introduzca su contraseña para confirmar",
333
337
  },
334
338
  tabs: {
335
339
  add: "Añadir pestaña",
@@ -680,6 +684,43 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
680
684
  inflow: "Entrada",
681
685
  direction: (current, next) => `Dirección: ${current} — cambiar a ${next}`,
682
686
  },
687
+ columnMapper: {
688
+ paste: "Pegar una tabla",
689
+ pasteHint:
690
+ "Copie las filas de una hoja de cálculo y péguelas aquí, o suelte un archivo CSV o de texto.",
691
+ chooseFile: "Elegir un archivo",
692
+ readError: (name) => `No se ha podido leer «${name}»`,
693
+ headerRow: "La primera línea contiene los nombres de las columnas",
694
+ summary: (columns, rows) =>
695
+ `${n(columns)} ${plural(columns, "columna", "columnas")}, ${n(rows)} ${plural(rows, "fila", "filas")}`,
696
+ // Fragments of the summary line. "Separado por…" would have to agree with a noun the
697
+ // line does not name, so the separator is stated as a value.
698
+ separatorSemicolon: "separador: punto y coma",
699
+ separatorComma: "separador: coma",
700
+ separatorTab: "separador: tabulación",
701
+ separatorSpace: "separador: espacio",
702
+ // The examples stay as written: they show the convention, not the reader's locale.
703
+ decimalComma: "coma decimal (1,5)",
704
+ decimalPoint: "punto decimal (1.5)",
705
+ unreadCount: (count) =>
706
+ count === 1 ? "No se ha podido leer 1 línea" : `No se han podido leer ${n(count)} líneas`,
707
+ unreadLine: (line) => `No se pudo leer la línea ${line}`,
708
+ unreadMore: (count) => `…y ${n(count)} más`,
709
+ noRows: "Ninguna línea de este texto se puede leer como una fila de la tabla.",
710
+ table: "Columnas y sus funciones",
711
+ columnN: (column) => `Columna ${column}`,
712
+ roleOf: (column) => `¿Qué contiene la columna «${column}»?`,
713
+ ignore: "Ignorar",
714
+ requiredRole: (role) => `${role} (obligatorio)`,
715
+ previewOf: (shown, total) =>
716
+ shown === 1
717
+ ? `La primera de ${n(total)} filas`
718
+ : `Las primeras ${n(shown)} de ${n(total)} filas`,
719
+ // `roles` arrives joined with "o" ("Debe o Haber").
720
+ oneOf: (roles) => `${roles} (a elegir)`,
721
+ // "Falta" + infinitive: no verb to agree with however many roles follow.
722
+ missing: (roles) => `Falta asignar: ${roles}.`,
723
+ },
683
724
  translationReview: {
684
725
  statusMissing: "Falta",
685
726
  statusUnreviewed: "Sin revisar",
@@ -26,6 +26,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
26
26
  attachmentRemoveFile: (name) => `Retirer ${name}`,
27
27
  attachmentLimit: (max) => `${n(max)} ${max === 1 ? "pièce jointe" : "pièces jointes"} au maximum — retirez-en une pour en ajouter une autre.`,
28
28
  attachmentUploading: "Envoi en cours…",
29
+ attachmentUploadFailed: (name) => `«\u202f${name}\u202f» n’a pas pu être envoyé`,
29
30
  },
30
31
  feedbackDialog: {
31
32
  title: "Envoyer un retour",
@@ -339,6 +340,9 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
339
340
  password: "Mot de passe",
340
341
  phrase: (phrase) => `Saisissez «\u202f${phrase}\u202f» pour confirmer`,
341
342
  acknowledge: "J’ai lu ce que fait cette action et je souhaite continuer.",
343
+ needsPhrase: (phrase) => `Saisissez «\u202f${phrase}\u202f» pour confirmer`,
344
+ needsAcknowledge: "Cochez la case pour confirmer",
345
+ needsPassword: "Saisissez votre mot de passe pour confirmer",
342
346
  },
343
347
  tabs: {
344
348
  add: "Ajouter un onglet",
@@ -696,6 +700,44 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
696
700
  inflow: "Entrée",
697
701
  direction: (current, next) => `Sens\u00a0: ${current} — passer à ${next}`,
698
702
  },
703
+ columnMapper: {
704
+ paste: "Coller un tableau",
705
+ pasteHint:
706
+ "Copiez les lignes d’un tableur et collez-les ici, ou déposez un fichier CSV ou texte.",
707
+ chooseFile: "Choisir un fichier",
708
+ readError: (name) => `«\u202f${name}\u202f» n’a pas pu être lu`,
709
+ headerRow: "La première ligne contient les noms des colonnes",
710
+ summary: (columns, rows) =>
711
+ `${n(columns)} ${plural(columns, "colonne", "colonnes")}, ${n(rows)} ${plural(rows, "ligne", "lignes")}`,
712
+ // Fragments of the summary line. "Séparé par…" would have to agree with a noun the
713
+ // line does not name, so the separator is stated as a value.
714
+ separatorSemicolon: "séparateur\u00a0: point-virgule",
715
+ separatorComma: "séparateur\u00a0: virgule",
716
+ separatorTab: "séparateur\u00a0: tabulation",
717
+ separatorSpace: "séparateur\u00a0: espace",
718
+ // The examples stay as written: they show the convention, not the reader's locale.
719
+ decimalComma: "virgule décimale (1,5)",
720
+ decimalPoint: "point décimal (1.5)",
721
+ unreadCount: (count) =>
722
+ count < 2
723
+ ? `${n(count)} ligne n’a pas pu être lue`
724
+ : `${n(count)} lignes n’ont pas pu être lues`,
725
+ unreadLine: (line) => `La ligne ${line} n\u2019a pas pu être lue`,
726
+ unreadMore: (count) => `…et ${n(count)} ${plural(count, "autre", "autres")}`,
727
+ noRows: "Aucune ligne de ce texte ne se lit comme une ligne du tableau.",
728
+ table: "Colonnes et leurs rôles",
729
+ columnN: (column) => `Colonne ${column}`,
730
+ roleOf: (column) => `Que contient la colonne «\u202f${column}\u202f»\u202f?`,
731
+ ignore: "Ignorer",
732
+ requiredRole: (role) => `${role} (obligatoire)`,
733
+ previewOf: (shown, total) =>
734
+ shown < 2
735
+ ? `La première ligne sur ${n(total)}`
736
+ : `Les ${n(shown)} premières lignes sur ${n(total)}`,
737
+ // `roles` arrives joined with "ou" ("Débit ou Crédit").
738
+ oneOf: (roles) => `${roles} (au choix)`,
739
+ missing: (roles) => `Il manque encore\u00a0: ${roles}.`,
740
+ },
699
741
  translationReview: {
700
742
  statusMissing: "Manquant",
701
743
  statusUnreviewed: "Non relu",
@@ -25,6 +25,8 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
25
25
  attachmentRemoveFile: (name) => `${name} eltávolítása`,
26
26
  attachmentLimit: (max) => `Legfeljebb ${n(max)} melléklet – egy újabbhoz távolítson el egyet.`,
27
27
  attachmentUploading: "Feltöltés…",
28
+ // Opens with the quoted name, as `filePicker`'s refusals: no a/az has to agree with it.
29
+ attachmentUploadFailed: (name) => `„${name}”: a feltöltés nem sikerült`,
28
30
  },
29
31
  feedbackDialog: {
30
32
  title: "Visszajelzés küldése",
@@ -330,6 +332,9 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
330
332
  // Phrase after a colon, so no article has to agree with it.
331
333
  phrase: (phrase) => `A megerősítéshez írja be: „${phrase}”`,
332
334
  acknowledge: "Elolvastam, mit eredményez ez a művelet, és folytatni szeretném.",
335
+ needsPhrase: (phrase) => `A megerősítéshez írja be: „${phrase}”`,
336
+ needsAcknowledge: "A megerősítéshez jelölje be a négyzetet",
337
+ needsPassword: "A megerősítéshez adja meg a jelszavát",
333
338
  },
334
339
  tabs: {
335
340
  add: "Lap hozzáadása",
@@ -686,6 +691,38 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
686
691
  inflow: "Bevétel",
687
692
  direction: (current, next) => `Irány: ${current} – váltás erre: ${next}`,
688
693
  },
694
+ columnMapper: {
695
+ paste: "Táblázat beillesztése",
696
+ pasteHint:
697
+ "Másolja ki a sorokat egy táblázatkezelőből, és illessze be ide, vagy húzzon ide egy CSV- vagy szövegfájlt.",
698
+ chooseFile: "Fájl kiválasztása",
699
+ readError: (name) => `„${name}”: a fájl nem olvasható`,
700
+ headerRow: "Az első sor az oszlopok nevét tartalmazza",
701
+ // No plural after a numeral (convention 2).
702
+ summary: (columns, rows) => `${n(columns)} oszlop, ${n(rows)} sor`,
703
+ separatorSemicolon: "pontosvesszővel elválasztva",
704
+ separatorComma: "vesszővel elválasztva",
705
+ separatorTab: "tabulátorral elválasztva",
706
+ separatorSpace: "szóközzel elválasztva",
707
+ // The examples stay as written: they show the convention, not the reader's locale.
708
+ decimalComma: "tizedesvessző (1,5)",
709
+ decimalPoint: "tizedespont (1.5)",
710
+ unreadCount: (count) => `${n(count)} sor nem olvasható`,
711
+ unreadLine: (line) => `A(z) ${line}. sor nem olvasható`,
712
+ unreadMore: (count) => `…és még ${n(count)}`,
713
+ noRows: "A szöveg egyetlen sora sem olvasható a táblázat soraként.",
714
+ table: "Oszlopok és szerepük",
715
+ columnN: (column) => `${column}. oszlop`,
716
+ // The name after a colon, so no article or case ending has to agree with it.
717
+ roleOf: (column) => `Mit tartalmaz ez az oszlop: „${column}”?`,
718
+ ignore: "Kihagyás",
719
+ requiredRole: (role) => `${role} (kötelező)`,
720
+ // "(összesen N)", as `dataTable.pageChanged`: both numerals stay bare (convention 3).
721
+ previewOf: (shown, total) => `Az első ${n(shown)} sor (összesen ${n(total)})`,
722
+ // `roles` arrives joined with "vagy" ("Tartozik vagy Követel").
723
+ oneOf: (roles) => `${roles} (valamelyik)`,
724
+ missing: (roles) => `Még hiányzik: ${roles}.`,
725
+ },
689
726
  translationReview: {
690
727
  statusMissing: "Hiányzik",
691
728
  statusUnreviewed: "Nincs ellenőrizve",