@eifi1/ui-kit 0.15.4 → 0.16.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 (160) hide show
  1. package/README.md +20 -15
  2. package/dist/chart.d.ts +3 -4
  3. package/dist/components/alert-banner.js +1 -1
  4. package/dist/components/alert-banner.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +13 -4
  6. package/dist/components/amount-input.js +166 -157
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/authed-image.d.ts +14 -3
  9. package/dist/components/authed-image.js +32 -23
  10. package/dist/components/authed-image.js.map +1 -1
  11. package/dist/components/button-group.d.ts +3 -4
  12. package/dist/components/calculator.d.ts +3 -4
  13. package/dist/components/checkbox.js +1 -1
  14. package/dist/components/checkbox.js.map +1 -1
  15. package/dist/components/choice-card.js +1 -1
  16. package/dist/components/choice-card.js.map +1 -1
  17. package/dist/components/copy-button.d.ts +3 -4
  18. package/dist/components/error-boundary.js +1 -1
  19. package/dist/components/error-boundary.js.map +1 -1
  20. package/dist/components/facing-pair.d.ts +3 -4
  21. package/dist/components/file-button.d.ts +3 -4
  22. package/dist/components/file-button.js +64 -31
  23. package/dist/components/file-button.js.map +1 -1
  24. package/dist/components/file-dropzone.d.ts +3 -4
  25. package/dist/components/file-dropzone.js +1 -1
  26. package/dist/components/file-dropzone.js.map +1 -1
  27. package/dist/components/form-actions.d.ts +4 -5
  28. package/dist/components/form-actions.js +18 -6
  29. package/dist/components/form-actions.js.map +1 -1
  30. package/dist/components/line-items.d.ts +53 -2
  31. package/dist/components/line-items.js +73 -7
  32. package/dist/components/line-items.js.map +1 -1
  33. package/dist/components/list.d.ts +99 -5
  34. package/dist/components/list.js +103 -38
  35. package/dist/components/list.js.map +1 -1
  36. package/dist/components/loading-state.d.ts +19 -3
  37. package/dist/components/loading-state.js +14 -3
  38. package/dist/components/loading-state.js.map +1 -1
  39. package/dist/components/money-field.d.ts +3 -4
  40. package/dist/components/number-field.d.ts +3 -4
  41. package/dist/components/number-input.d.ts +3 -4
  42. package/dist/components/number-input.js +4 -2
  43. package/dist/components/number-input.js.map +1 -1
  44. package/dist/components/numpad-sheet.d.ts +3 -4
  45. package/dist/components/passkeys-setting.d.ts +30 -10
  46. package/dist/components/passkeys-setting.js +19 -11
  47. package/dist/components/passkeys-setting.js.map +1 -1
  48. package/dist/components/progress-bar.d.ts +12 -3
  49. package/dist/components/progress-bar.js +21 -11
  50. package/dist/components/progress-bar.js.map +1 -1
  51. package/dist/components/series-chart.d.ts +3 -4
  52. package/dist/components/settings-fields.d.ts +3 -4
  53. package/dist/components/signed-amount.d.ts +28 -4
  54. package/dist/components/signed-amount.js +17 -8
  55. package/dist/components/signed-amount.js.map +1 -1
  56. package/dist/components/stat-tile.d.ts +31 -2
  57. package/dist/components/stat-tile.js +37 -6
  58. package/dist/components/stat-tile.js.map +1 -1
  59. package/dist/components/status-dot.d.ts +4 -1
  60. package/dist/components/status-dot.js +20 -1
  61. package/dist/components/status-dot.js.map +1 -1
  62. package/dist/components/swatch-picker.d.ts +3 -1
  63. package/dist/components/swatch-picker.js.map +1 -1
  64. package/dist/components/table.d.ts +18 -13
  65. package/dist/components/table.js +20 -5
  66. package/dist/components/table.js.map +1 -1
  67. package/dist/components/text-link.d.ts +3 -4
  68. package/dist/components/time-input.d.ts +3 -4
  69. package/dist/components/toggle-legend.d.ts +12 -1
  70. package/dist/components/toggle-legend.js +6 -2
  71. package/dist/components/toggle-legend.js.map +1 -1
  72. package/dist/components/ui.d.ts +3 -4
  73. package/dist/components/ui.js +156 -52
  74. package/dist/components/ui.js.map +1 -1
  75. package/dist/feedback/feedback-attachment.d.ts +2 -82
  76. package/dist/feedback/feedback-attachment.js +285 -55
  77. package/dist/feedback/feedback-attachment.js.map +1 -1
  78. package/dist/feedback/feedback-dialog.d.ts +2 -105
  79. package/dist/feedback/feedback-dialog.js +57 -25
  80. package/dist/feedback/feedback-dialog.js.map +1 -1
  81. package/dist/feedback/feedback-inbox.d.ts +1 -1
  82. package/dist/feedback/feedback-thread.d.ts +2 -2
  83. package/dist/feedback-attachment-WFiXJ8We.d.ts +302 -0
  84. package/dist/feedback.d.ts +1 -2
  85. package/dist/hooks/use-file-drop.d.ts +3 -4
  86. package/dist/i18n/defaults.d.ts +3 -4
  87. package/dist/i18n/kit-labels.d.ts +2 -3
  88. package/dist/i18n/locales/de-CH-informal.d.ts +3 -4
  89. package/dist/i18n/locales/de-CH.d.ts +3 -4
  90. package/dist/i18n/locales/de-informal.d.ts +3 -4
  91. package/dist/i18n/locales/de.d.ts +3 -4
  92. package/dist/i18n/locales/de.js +5 -1
  93. package/dist/i18n/locales/de.js.map +1 -1
  94. package/dist/i18n/locales/es.d.ts +3 -4
  95. package/dist/i18n/locales/es.js +5 -1
  96. package/dist/i18n/locales/es.js.map +1 -1
  97. package/dist/i18n/locales/fr.d.ts +3 -4
  98. package/dist/i18n/locales/fr.js +5 -1
  99. package/dist/i18n/locales/fr.js.map +1 -1
  100. package/dist/i18n/locales/hu.d.ts +3 -4
  101. package/dist/i18n/locales/hu.js +5 -1
  102. package/dist/i18n/locales/hu.js.map +1 -1
  103. package/dist/i18n/locales/it.d.ts +3 -4
  104. package/dist/i18n/locales/it.js +5 -1
  105. package/dist/i18n/locales/it.js.map +1 -1
  106. package/dist/i18n/locales/zh.d.ts +3 -4
  107. package/dist/i18n/locales/zh.js +5 -1
  108. package/dist/i18n/locales/zh.js.map +1 -1
  109. package/dist/index.d.ts +10 -11
  110. package/dist/{kit-labels-CnnBd-2N.d.ts → kit-labels-DE--LmCk.d.ts} +154 -14
  111. package/dist/lib/format.d.ts +18 -3
  112. package/dist/lib/format.js +2 -1
  113. package/dist/lib/format.js.map +1 -1
  114. package/dist/rhf/fields.d.ts +3 -4
  115. package/dist/rhf/form.d.ts +3 -4
  116. package/dist/rhf/line-items.js.map +1 -1
  117. package/dist/rhf.d.ts +3 -4
  118. package/dist/shell/app-shell.d.ts +3 -4
  119. package/dist/shell/top-bar-brand.d.ts +3 -4
  120. package/dist/shell.d.ts +3 -4
  121. package/dist/theme/palette-derive.d.ts +3 -0
  122. package/dist/theme/palette-derive.js +7 -0
  123. package/dist/theme/palette-derive.js.map +1 -1
  124. package/dist/wizard/stepper-nav.d.ts +3 -4
  125. package/dist/wizard.d.ts +3 -4
  126. package/package.json +4 -2
  127. package/src/components/alert-banner.tsx +1 -1
  128. package/src/components/amount-input.tsx +47 -16
  129. package/src/components/authed-image.tsx +20 -2
  130. package/src/components/checkbox.tsx +1 -1
  131. package/src/components/choice-card.tsx +1 -1
  132. package/src/components/error-boundary.tsx +1 -1
  133. package/src/components/file-button.tsx +77 -27
  134. package/src/components/file-dropzone.tsx +1 -1
  135. package/src/components/form-actions.tsx +86 -8
  136. package/src/components/line-items.tsx +131 -14
  137. package/src/components/list.tsx +240 -50
  138. package/src/components/loading-state.tsx +35 -4
  139. package/src/components/number-input.tsx +18 -2
  140. package/src/components/passkeys-setting.tsx +45 -18
  141. package/src/components/progress-bar.tsx +46 -21
  142. package/src/components/signed-amount.tsx +47 -8
  143. package/src/components/stat-tile.tsx +76 -6
  144. package/src/components/status-dot.tsx +31 -0
  145. package/src/components/swatch-picker.tsx +3 -1
  146. package/src/components/table.tsx +42 -15
  147. package/src/components/toggle-legend.tsx +21 -4
  148. package/src/components/ui.tsx +280 -70
  149. package/src/feedback/feedback-attachment.tsx +472 -96
  150. package/src/feedback/feedback-dialog.tsx +162 -50
  151. package/src/i18n/locales/de.ts +4 -0
  152. package/src/i18n/locales/es.ts +4 -0
  153. package/src/i18n/locales/fr.ts +4 -0
  154. package/src/i18n/locales/hu.ts +4 -0
  155. package/src/i18n/locales/it.ts +4 -0
  156. package/src/i18n/locales/zh.ts +4 -0
  157. package/src/lib/format.ts +24 -3
  158. package/src/rhf/line-items.tsx +6 -0
  159. package/src/theme/palette-derive.ts +10 -0
  160. package/tokens.css +9 -1
@@ -6,8 +6,10 @@ import { useMediaQuery } from "../hooks/use-media-query";
6
6
  import { useKitLabels } from "../i18n/kit-labels";
7
7
  import {
8
8
  DEFAULT_ATTACHMENT_ACCEPT,
9
+ DEFAULT_FEEDBACK_ATTACHMENT_LABELS,
9
10
  DEFAULT_MAX_ATTACHMENT_BYTES,
10
11
  FeedbackAttachmentField,
12
+ type FeedbackAttachmentError,
11
13
  } from "./feedback-attachment";
12
14
 
13
15
  export interface FeedbackCategoryOption {
@@ -27,6 +29,17 @@ export interface FeedbackAttachmentLabels {
27
29
  * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */
28
30
  attachmentPaste?: string;
29
31
  attachmentRemove: string;
32
+ /** `multiple` mode (0.15.5): the chip list's accessible name — and, in
33
+ * `<FeedbackDialog attachments="multiple">`, the heading over it (0.16.0). Optional,
34
+ * as are the keys below — they fall back to the provider's `feedbackAttachment`,
35
+ * then English. */
36
+ attachmentList?: string;
37
+ /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */
38
+ attachmentScreenshot?: string;
39
+ /** `multiple` mode: one chip's remove button, given the file's name. */
40
+ attachmentRemoveFile?: (name: string) => string;
41
+ /** `multiple` mode: the line shown once `max` files are attached. */
42
+ attachmentLimit?: (max: number) => string;
30
43
  }
31
44
 
32
45
  export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
@@ -38,6 +51,10 @@ export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
38
51
  submitHint: string;
39
52
  cancel: string;
40
53
  save: string;
54
+ /** @deprecated since 0.16.0 — use `attachmentList`, the one key for the heading and
55
+ * the chip list it names. Still honoured: set, it wins over the provider's
56
+ * `attachmentList` (but not over an `attachmentList` passed beside it). */
57
+ attachments?: string;
41
58
  }
42
59
 
43
60
  /**
@@ -62,6 +79,11 @@ export interface FeedbackDialogTextLabels {
62
79
  submitHint: string;
63
80
  cancel: string;
64
81
  save: string;
82
+ /** @deprecated since 0.16.0 — use `feedbackAttachment.attachmentList`. It and this
83
+ * key were two names for one heading ("Attachments" over the chips that list names
84
+ * "Attachments"), and a catalogue that translated one left the other English. Still
85
+ * honoured when set: it wins over `feedbackAttachment.attachmentList`. */
86
+ attachments?: string;
65
87
  }
66
88
 
67
89
  export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
@@ -71,6 +93,8 @@ export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
71
93
  body: "What happened?",
72
94
  bodyOptional: "What happened? (optional)",
73
95
  attachment: "Screenshot",
96
+ // No `attachments`: the multiple-mode heading is `feedbackAttachment.attachmentList`,
97
+ // and a default here would shadow a provider that translated only that one.
74
98
  submitHint: "Ctrl/⌘ + Enter to send",
75
99
  cancel: "Cancel",
76
100
  save: "Send",
@@ -84,36 +108,22 @@ export interface FeedbackSubmission {
84
108
  }
85
109
 
86
110
  /**
87
- * The generic feedback form dialog: category + subject + body + an optional image
88
- * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the
89
- * category options, labels and an `onSubmit` that talks to its own backend, plus
90
- * an optional `contextSlot` for app-specific context (user, current URL, …).
91
- *
92
- * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog
93
- * takes an image off the clipboard and makes it the attachment, through the same
94
- * validation and the same preview as the file picker. It is the gesture the two
95
- * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,
96
- * and the file picker needs a file — so somebody who wanted to show one panel,
97
- * or one region of one, had to save a crop to disk first and then find it again
98
- * (Steering Design feedback #39). The clipboard is where a region snip already
99
- * is on every platform.
111
+ * What `onSubmit` gets under `attachments="multiple"` (0.15.5): the screenshot and
112
+ * the picked/pasted files apart, because an app files them differently — keksdose
113
+ * attaches the screenshot to the report and the photos as its evidence list. No
114
+ * `attachment`: there is no one file it could name.
100
115
  */
101
- export function FeedbackDialog({
102
- open,
103
- onClose,
104
- categories,
105
- category,
106
- onCategoryChange,
107
- labels: labelsProp,
108
- onSubmit,
109
- submitting = false,
110
- contextSlot,
111
- attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,
112
- maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
113
- onAttachmentError,
114
- onCaptureScreenshot,
115
- requireBody = true,
116
- }: {
116
+ export interface FeedbackMultipleSubmission {
117
+ title: string;
118
+ body: string;
119
+ category: string;
120
+ /** What `onCaptureScreenshot` returned, or `null` — at most one. */
121
+ screenshot: File | null;
122
+ /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */
123
+ attachments: File[];
124
+ }
125
+
126
+ interface FeedbackDialogBaseProps {
117
127
  open: boolean;
118
128
  onClose: () => void;
119
129
  categories: FeedbackCategoryOption[];
@@ -122,12 +132,10 @@ export function FeedbackDialog({
122
132
  /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >
123
133
  * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */
124
134
  labels?: Partial<FeedbackDialogLabels>;
125
- onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
126
135
  submitting?: boolean;
127
136
  contextSlot?: ReactNode;
128
137
  attachmentAccept?: string[];
129
138
  maxAttachmentBytes?: number;
130
- onAttachmentError?: (kind: "type" | "size") => void;
131
139
  /**
132
140
  * Optional: capture a screenshot of the underlying app view and return it as a
133
141
  * File. When provided, a "Capture screenshot" button is shown next to "Add
@@ -140,15 +148,94 @@ export function FeedbackDialog({
140
148
  * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).
141
149
  */
142
150
  requireBody?: boolean;
143
- }) {
151
+ }
152
+
153
+ /** One attachment — a screenshot, a picked or a pasted image; a second replaces
154
+ * nothing, it has to wait for the first to be removed. The default. */
155
+ export interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {
156
+ attachments?: "single";
157
+ onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
158
+ onAttachmentError?: (kind: "type" | "size") => void;
159
+ /** Multiple mode only — refused here rather than silently ignored. */
160
+ maxAttachments?: never;
161
+ }
162
+
163
+ /**
164
+ * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose
165
+ * dev#578: "pasted two photos, the second overwrote the first"). The capture button is
166
+ * offered while there is no screenshot yet; the add button while there is room.
167
+ */
168
+ export interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {
169
+ attachments: "multiple";
170
+ /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */
171
+ maxAttachments?: number;
172
+ onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;
173
+ /** `"count"`: more files arrived at once than there was room for; the surplus was dropped. */
174
+ onAttachmentError?: (kind: FeedbackAttachmentError) => void;
175
+ }
176
+
177
+ export type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;
178
+
179
+ /**
180
+ * The generic feedback form dialog: category + subject + body + an optional image
181
+ * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the
182
+ * category options, labels and an `onSubmit` that talks to its own backend, plus
183
+ * an optional `contextSlot` for app-specific context (user, current URL, …).
184
+ *
185
+ * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog
186
+ * takes an image off the clipboard and makes it the attachment, through the same
187
+ * validation and the same preview as the file picker. It is the gesture the two
188
+ * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,
189
+ * and the file picker needs a file — so somebody who wanted to show one panel,
190
+ * or one region of one, had to save a crop to disk first and then find it again
191
+ * (Steering Design feedback #39). The clipboard is where a region snip already
192
+ * is on every platform.
193
+ *
194
+ * **`attachments="multiple"`** (0.15.5) takes several: every paste and pick adds a
195
+ * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link
196
+ * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed
197
+ * against the one-file submission keeps compiling and behaving as before.
198
+ */
199
+ export function FeedbackDialog(props: FeedbackDialogProps) {
200
+ const {
201
+ open,
202
+ onClose,
203
+ categories,
204
+ category,
205
+ onCategoryChange,
206
+ labels: labelsProp,
207
+ submitting = false,
208
+ contextSlot,
209
+ attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,
210
+ maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
211
+ onCaptureScreenshot,
212
+ requireBody = true,
213
+ } = props;
144
214
  const [title, setTitle] = useState("");
145
215
  const [body, setBody] = useState("");
146
216
  const [attachment, setAttachment] = useState<File | null>(null);
217
+ // `attachments="multiple"` only: the capture, apart from the picked/pasted files.
218
+ const [screenshot, setScreenshot] = useState<File | null>(null);
219
+ const [files, setFiles] = useState<File[]>([]);
147
220
  const isMobile = useMediaQuery(PHONE_QUERY, false);
148
221
  const labels = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);
222
+ const attachmentText = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
223
+ // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which
224
+ // also names the chip list beneath it: one string, so the two cannot disagree. The
225
+ // deprecated `feedbackDialog.attachments` (prop or provider) still wins over the
226
+ // provider's `attachmentList`, as it did before 0.16.0; an `attachmentList` prop wins
227
+ // over both.
228
+ const listHeading =
229
+ labelsProp?.attachmentList ??
230
+ labels.attachments ??
231
+ attachmentText.attachmentList ??
232
+ DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;
149
233
  // The attachment field resolves its own keys from `feedbackAttachment`; it gets the
150
234
  // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.
151
- const attachmentLabels = { ...labelsProp, attachment: labels.attachment };
235
+ const attachmentLabels =
236
+ props.attachments === "multiple"
237
+ ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }
238
+ : { ...labelsProp, attachment: labels.attachment };
152
239
 
153
240
  // Reset the form whenever the dialog is (re)opened — during render, on the
154
241
  // closed-to-open transition, so the old draft never paints for a frame.
@@ -159,6 +246,8 @@ export function FeedbackDialog({
159
246
  setTitle("");
160
247
  setBody("");
161
248
  setAttachment(null);
249
+ setScreenshot(null);
250
+ setFiles([]);
162
251
  }
163
252
  }
164
253
 
@@ -166,7 +255,12 @@ export function FeedbackDialog({
166
255
  // with a 422, and a blank report is no report).
167
256
  const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;
168
257
  const trySubmit = () => {
169
- if (canSubmit) void onSubmit({ title, body, category, attachment });
258
+ if (!canSubmit) return;
259
+ if (props.attachments === "multiple") {
260
+ void props.onSubmit({ title, body, category, screenshot, attachments: files });
261
+ } else {
262
+ void props.onSubmit({ title, body, category, attachment });
263
+ }
170
264
  };
171
265
 
172
266
  if (!open) return null;
@@ -194,22 +288,40 @@ export function FeedbackDialog({
194
288
  onChange={(e) => setBody(e.target.value)}
195
289
  />
196
290
  );
197
- const attachmentField = (
198
- <FeedbackAttachmentField
199
- value={attachment}
200
- onChange={setAttachment}
201
- labels={attachmentLabels}
202
- accept={attachmentAccept}
203
- maxBytes={maxAttachmentBytes}
204
- onError={onAttachmentError}
205
- onCaptureScreenshot={onCaptureScreenshot}
206
- // On `document`, not on the panel: the Modal focuses its own panel on open
207
- // and traps Tab inside it, so while this dialog is up every paste in the
208
- // page is meant for it — including the one made with nothing in
209
- // particular focused, which never reaches a React `onPaste` on a child.
210
- documentPaste={open}
211
- />
212
- );
291
+ const fieldProps = {
292
+ labels: attachmentLabels,
293
+ accept: attachmentAccept,
294
+ maxBytes: maxAttachmentBytes,
295
+ onCaptureScreenshot,
296
+ // On `document`, not on the panel: the Modal focuses its own panel on open
297
+ // and traps Tab inside it, so while this dialog is up every paste in the
298
+ // page is meant for it — including the one made with nothing in
299
+ // particular focused, which never reaches a React `onPaste` on a child.
300
+ documentPaste: open,
301
+ };
302
+ const attachmentField =
303
+ props.attachments === "multiple" ? (
304
+ <FeedbackAttachmentField
305
+ {...fieldProps}
306
+ multiple
307
+ value={files}
308
+ onChange={setFiles}
309
+ max={props.maxAttachments}
310
+ // The capture goes to its own slot, so it is offered once and never crowds
311
+ // out a photo; a paste is a photo (a region snip is chosen evidence, not "the
312
+ // screenshot").
313
+ screenshot={screenshot}
314
+ onScreenshotChange={setScreenshot}
315
+ onError={props.onAttachmentError}
316
+ />
317
+ ) : (
318
+ <FeedbackAttachmentField
319
+ {...fieldProps}
320
+ value={attachment}
321
+ onChange={setAttachment}
322
+ onError={props.onAttachmentError}
323
+ />
324
+ );
213
325
 
214
326
  return (
215
327
  <Modal
@@ -20,6 +20,10 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
20
20
  attachmentCapture: "Screenshot aufnehmen",
21
21
  attachmentPaste: "…oder einen Screenshot aus der Zwischenablage einfügen.",
22
22
  attachmentRemove: "Anhang entfernen",
23
+ attachmentList: "Anhänge",
24
+ attachmentScreenshot: "Screenshot",
25
+ attachmentRemoveFile: (name) => `${name} entfernen`,
26
+ attachmentLimit: (max) => `Maximal ${n(max)} ${max === 1 ? "Anhang" : "Anhänge"} – zum Hinzufügen erst einen entfernen.`,
23
27
  },
24
28
  feedbackDialog: {
25
29
  title: "Feedback senden",
@@ -21,6 +21,10 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
21
21
  attachmentCapture: "Capturar pantalla",
22
22
  attachmentPaste: "…o pega una captura de pantalla desde el portapapeles.",
23
23
  attachmentRemove: "Quitar adjunto",
24
+ attachmentList: "Adjuntos",
25
+ attachmentScreenshot: "Captura de pantalla",
26
+ attachmentRemoveFile: (name) => `Quitar ${name}`,
27
+ attachmentLimit: (max) => `Máximo ${n(max)} ${max === 1 ? "adjunto" : "adjuntos"}: quita uno para añadir otro.`,
24
28
  },
25
29
  feedbackDialog: {
26
30
  title: "Enviar comentarios",
@@ -21,6 +21,10 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
21
21
  attachmentCapture: "Capturer l’écran",
22
22
  attachmentPaste: "…ou collez une capture d’écran depuis le presse-papiers.",
23
23
  attachmentRemove: "Retirer la pièce jointe",
24
+ attachmentList: "Pièces jointes",
25
+ attachmentScreenshot: "Capture d’écran",
26
+ attachmentRemoveFile: (name) => `Retirer ${name}`,
27
+ attachmentLimit: (max) => `${n(max)} ${max === 1 ? "pièce jointe" : "pièces jointes"} au maximum — retirez-en une pour en ajouter une autre.`,
24
28
  },
25
29
  feedbackDialog: {
26
30
  title: "Envoyer un retour",
@@ -20,6 +20,10 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
20
20
  attachmentCapture: "Képernyőkép készítése",
21
21
  attachmentPaste: "…vagy illesszen be egy képernyőképet a vágólapról.",
22
22
  attachmentRemove: "Melléklet eltávolítása",
23
+ attachmentList: "Mellékletek",
24
+ attachmentScreenshot: "Képernyőkép",
25
+ attachmentRemoveFile: (name) => `${name} eltávolítása`,
26
+ attachmentLimit: (max) => `Legfeljebb ${n(max)} melléklet – egy újabbhoz távolítson el egyet.`,
23
27
  },
24
28
  feedbackDialog: {
25
29
  title: "Visszajelzés küldése",
@@ -21,6 +21,10 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
21
21
  attachmentCapture: "Cattura schermata",
22
22
  attachmentPaste: "…oppure incolla uno screenshot dagli appunti.",
23
23
  attachmentRemove: "Rimuovi allegato",
24
+ attachmentList: "Allegati",
25
+ attachmentScreenshot: "Screenshot",
26
+ attachmentRemoveFile: (name) => `Rimuovi ${name}`,
27
+ attachmentLimit: (max) => `Massimo ${n(max)} ${max === 1 ? "allegato" : "allegati"}: rimuovine uno per aggiungerne un altro.`,
24
28
  },
25
29
  feedbackDialog: {
26
30
  title: "Invia feedback",
@@ -20,6 +20,10 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
20
20
  attachmentCapture: "截取屏幕",
21
21
  attachmentPaste: "……或从剪贴板粘贴截图。",
22
22
  attachmentRemove: "移除附件",
23
+ attachmentList: "附件",
24
+ attachmentScreenshot: "截图",
25
+ attachmentRemoveFile: (name) => `移除 ${name}`,
26
+ attachmentLimit: (max) => `最多 ${n(max)} 个附件,请先移除一个再添加。`,
23
27
  },
24
28
  feedbackDialog: {
25
29
  title: "发送反馈",
package/src/lib/format.ts CHANGED
@@ -365,15 +365,36 @@ export function currencyMinorDigits(currency: string | null | undefined): number
365
365
  }
366
366
  }
367
367
 
368
+ /** The options form of {@link roundToCurrency}'s third argument. */
369
+ export interface RoundToCurrencyOptions {
370
+ /** Decimals to round to, whatever the currency — the plain `digits` argument. */
371
+ digits?: number;
372
+ /**
373
+ * Decimals for a currency `Intl` cannot resolve — no code yet (`null`, `""`), or a
374
+ * malformed one ("EURO", "€"). Left out, such a value comes back unchanged, as
375
+ * before. Note that `Intl` resolves ANY well-formed three-letter code, a made-up
376
+ * "XYZ" included (to 2), so this is the case of a missing or broken code, not of a
377
+ * rare currency.
378
+ */
379
+ fallbackDigits?: number;
380
+ }
381
+
368
382
  /**
369
383
  * `value` rounded to the currency's minor unit (or to `digits`), half away from zero,
370
384
  * in decimal arithmetic: 93.4213 CHF → 93.42, 1.005 EUR → 1.01 (`Math.round(1.005 *
371
385
  * 100)` is 100, since 1.005 is 1.00499… in binary). A non-finite value, or a currency
372
386
  * `Intl` does not know without `digits`, comes back unchanged (keksdose live #356: an
373
- * FX estimate set into an AmountInput showed four decimals).
387
+ * FX estimate set into an AmountInput showed four decimals) — unless the options form
388
+ * gives `fallbackDigits`: `roundToCurrency(v, code, { fallbackDigits: 2 })` rounds a
389
+ * figure whose currency is not chosen yet to cents rather than leaving 0.1 + 0.2 as is.
374
390
  */
375
- export function roundToCurrency(value: number, currency: string | null | undefined, digits?: number): number {
376
- const places = digits ?? currencyMinorDigits(currency);
391
+ export function roundToCurrency(
392
+ value: number,
393
+ currency: string | null | undefined,
394
+ digits?: number | RoundToCurrencyOptions,
395
+ ): number {
396
+ const options = typeof digits === "object" && digits !== null ? digits : { digits };
397
+ const places = options.digits ?? currencyMinorDigits(currency) ?? options.fallbackDigits;
377
398
  if (places === undefined || !Number.isFinite(value)) return value;
378
399
  const magnitude = Number(`${Math.round(Number(`${Math.abs(value)}e${places}`))}e-${places}`);
379
400
  return value < 0 ? -magnitude : magnitude;
@@ -23,6 +23,12 @@
23
23
  * itself — the `rules` here, or a resolver's `lines: "At least two lines"` — is the
24
24
  * list's `error`. Each cell's `render` gets `name`, the row's path
25
25
  * (`"components.2"`), to bind its field to.
26
+ *
27
+ * `summary`, `fieldLabels` and `narrowColumns` pass through to {@link LineItems}. With
28
+ * `fieldLabels="floating"` a cell's `fieldLabel` is the column's name; the bound
29
+ * fields' own `label` is a `FormLabel` ABOVE the control, so for the floating
30
+ * one render the kit field through `RhfField`:
31
+ * `render={({ field, invalid }) => <Input label={fieldLabel} aria-label={label} {...field} invalid={invalid} />}`.
26
32
  */
27
33
  import type { ReactNode } from "react";
28
34
  import {
@@ -64,6 +64,9 @@ export interface SemanticTokens {
64
64
  dangerHover: string;
65
65
  dangerContrast: string;
66
66
  dangerBorder: string;
67
+ /** The error line (invalid field, danger banner, destructive focus ring): as loud as
68
+ * `dangerBorder` was before 0.15.5, so it keeps 3:1 against the surface. */
69
+ dangerBorderStrong: string;
67
70
  dangerBg: string;
68
71
  warning: string;
69
72
  warningBorder: string;
@@ -374,7 +377,14 @@ export function derivePalette({
374
377
  l: mode === "light" ? dangerLch.l - 0.06 : dangerLch.l + 0.06,
375
378
  }),
376
379
  dangerContrast: contrastOn(danger),
380
+ // The tone border follows its siblings' lightness in dark mode (warning 0.44, info
381
+ // and success 0.45) — a soft frame; the strong line keeps the old, louder recipe.
377
382
  dangerBorder: oklchToHex({
383
+ l: mode === "light" ? dangerLch.l + 0.22 : 0.45,
384
+ c: semanticChroma.danger,
385
+ h: semanticHue.danger,
386
+ }),
387
+ dangerBorderStrong: oklchToHex({
378
388
  l: mode === "light" ? dangerLch.l + 0.22 : dangerLch.l - 0.22,
379
389
  c: semanticChroma.danger,
380
390
  h: semanticHue.danger,
package/tokens.css CHANGED
@@ -191,6 +191,10 @@
191
191
  --danger-hover: #9f1239; /* rose-800 */
192
192
  --danger-contrast: #ffffff;
193
193
  --danger-border: #fb7185; /* rose-400 */
194
+ /* The error LINE: an invalid field, a failed upload, a danger banner's frame, a focus
195
+ ring on a destructive control. Equal to the border in light mode; in dark the tone
196
+ border is soft like its siblings and this one stays loud (WCAG 1.4.11's 3:1). */
197
+ --danger-border-strong: #fb7185; /* rose-400 */
194
198
  --danger-bg: #fff1f2; /* rose-50 */
195
199
 
196
200
  --warning: #b45309; /* amber-700 */
@@ -217,7 +221,11 @@
217
221
  --danger: #fda4af; /* rose-300 */
218
222
  --danger-hover: #fecdd3; /* rose-200 */
219
223
  --danger-contrast: #4c0519; /* rose-950 */
220
- --danger-border: #e11d48; /* rose-600 */
224
+ /* The tone family's recipe — hue at 0.6 alpha, as warning/info/success below — so a
225
+ soft danger Chip sits in the same weight of frame as its siblings (keksdose dev
226
+ #576: FAILED was visibly louder than READY and NEEDS REVIEW in dark mode). */
227
+ --danger-border: rgba(190, 18, 60, 0.6); /* rose-700 at the family's alpha */
228
+ --danger-border-strong: #e11d48; /* rose-600 */
221
229
  --danger-bg: rgba(136, 19, 55, 0.2);
222
230
 
223
231
  --warning: #fcd34d; /* amber-300 */