@eifi1/ui-kit 0.15.3 → 0.15.5

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 (122) hide show
  1. package/README.md +11 -10
  2. package/dist/chart.d.ts +50 -1
  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 +5 -3
  6. package/dist/components/amount-input.js +6 -11
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/button-group.d.ts +2 -3
  9. package/dist/components/calculator.d.ts +2 -3
  10. package/dist/components/chart.d.ts +16 -1
  11. package/dist/components/chart.js +65 -37
  12. package/dist/components/chart.js.map +1 -1
  13. package/dist/components/checkbox.js +1 -1
  14. package/dist/components/checkbox.js.map +1 -1
  15. package/dist/components/chip.d.ts +8 -2
  16. package/dist/components/chip.js +12 -3
  17. package/dist/components/chip.js.map +1 -1
  18. package/dist/components/choice-card.js +1 -1
  19. package/dist/components/choice-card.js.map +1 -1
  20. package/dist/components/copy-button.d.ts +2 -3
  21. package/dist/components/error-boundary.js +1 -1
  22. package/dist/components/error-boundary.js.map +1 -1
  23. package/dist/components/facing-pair.d.ts +50 -0
  24. package/dist/components/file-button.d.ts +2 -3
  25. package/dist/components/file-dropzone.d.ts +2 -3
  26. package/dist/components/file-dropzone.js +1 -1
  27. package/dist/components/file-dropzone.js.map +1 -1
  28. package/dist/components/form-actions.d.ts +2 -3
  29. package/dist/components/form-actions.js +9 -1
  30. package/dist/components/form-actions.js.map +1 -1
  31. package/dist/components/money-field.d.ts +2 -3
  32. package/dist/components/number-field.d.ts +2 -3
  33. package/dist/components/number-input.d.ts +2 -3
  34. package/dist/components/number-input.js +17 -3
  35. package/dist/components/number-input.js.map +1 -1
  36. package/dist/components/numpad-sheet.d.ts +2 -3
  37. package/dist/components/series-chart.d.ts +78 -1
  38. package/dist/components/series-chart.js +74 -13
  39. package/dist/components/series-chart.js.map +1 -1
  40. package/dist/components/settings-fields.d.ts +2 -3
  41. package/dist/components/text-link.d.ts +2 -3
  42. package/dist/components/time-input.d.ts +2 -3
  43. package/dist/components/ui.d.ts +2 -3
  44. package/dist/components/ui.js +7 -7
  45. package/dist/components/ui.js.map +1 -1
  46. package/dist/feedback/feedback-attachment.d.ts +2 -82
  47. package/dist/feedback/feedback-attachment.js +271 -54
  48. package/dist/feedback/feedback-attachment.js.map +1 -1
  49. package/dist/feedback/feedback-dialog.d.ts +2 -105
  50. package/dist/feedback/feedback-dialog.js +56 -25
  51. package/dist/feedback/feedback-dialog.js.map +1 -1
  52. package/dist/feedback/feedback-inbox.d.ts +1 -1
  53. package/dist/feedback/feedback-thread.d.ts +2 -2
  54. package/dist/feedback-attachment-BYAHJMOr.d.ts +289 -0
  55. package/dist/feedback.d.ts +1 -2
  56. package/dist/hooks/use-file-drop.d.ts +2 -3
  57. package/dist/i18n/defaults.d.ts +2 -3
  58. package/dist/i18n/kit-labels.d.ts +2 -3
  59. package/dist/i18n/kit-labels.js +8 -2
  60. package/dist/i18n/kit-labels.js.map +1 -1
  61. package/dist/i18n/locales/de-CH-informal.d.ts +2 -3
  62. package/dist/i18n/locales/de-CH.d.ts +2 -3
  63. package/dist/i18n/locales/de-informal.d.ts +2 -3
  64. package/dist/i18n/locales/de.d.ts +2 -3
  65. package/dist/i18n/locales/de.js +5 -1
  66. package/dist/i18n/locales/de.js.map +1 -1
  67. package/dist/i18n/locales/es.d.ts +2 -3
  68. package/dist/i18n/locales/es.js +5 -1
  69. package/dist/i18n/locales/es.js.map +1 -1
  70. package/dist/i18n/locales/fr.d.ts +2 -3
  71. package/dist/i18n/locales/fr.js +5 -1
  72. package/dist/i18n/locales/fr.js.map +1 -1
  73. package/dist/i18n/locales/hu.d.ts +2 -3
  74. package/dist/i18n/locales/hu.js +5 -1
  75. package/dist/i18n/locales/hu.js.map +1 -1
  76. package/dist/i18n/locales/it.d.ts +2 -3
  77. package/dist/i18n/locales/it.js +5 -1
  78. package/dist/i18n/locales/it.js.map +1 -1
  79. package/dist/i18n/locales/zh.d.ts +2 -3
  80. package/dist/i18n/locales/zh.js +5 -1
  81. package/dist/i18n/locales/zh.js.map +1 -1
  82. package/dist/index.d.ts +4 -5
  83. package/dist/{kit-labels-BHfRBbHs.d.ts → kit-labels-BD2F7sOz.d.ts} +15 -5
  84. package/dist/lib/format.d.ts +15 -1
  85. package/dist/lib/format.js +16 -0
  86. package/dist/lib/format.js.map +1 -1
  87. package/dist/rhf/fields.d.ts +2 -3
  88. package/dist/rhf/form.d.ts +2 -3
  89. package/dist/rhf.d.ts +2 -3
  90. package/dist/shell/app-shell.d.ts +2 -3
  91. package/dist/shell/top-bar-brand.d.ts +2 -3
  92. package/dist/shell.d.ts +2 -3
  93. package/dist/theme/palette-derive.d.ts +3 -0
  94. package/dist/theme/palette-derive.js +7 -0
  95. package/dist/theme/palette-derive.js.map +1 -1
  96. package/dist/wizard/stepper-nav.d.ts +2 -3
  97. package/dist/wizard.d.ts +2 -3
  98. package/package.json +4 -2
  99. package/src/components/alert-banner.tsx +1 -1
  100. package/src/components/amount-input.tsx +16 -14
  101. package/src/components/chart.tsx +102 -41
  102. package/src/components/checkbox.tsx +1 -1
  103. package/src/components/chip.tsx +29 -4
  104. package/src/components/choice-card.tsx +1 -1
  105. package/src/components/error-boundary.tsx +1 -1
  106. package/src/components/file-dropzone.tsx +1 -1
  107. package/src/components/form-actions.tsx +9 -1
  108. package/src/components/number-input.tsx +22 -2
  109. package/src/components/series-chart.tsx +122 -20
  110. package/src/components/ui.tsx +7 -7
  111. package/src/feedback/feedback-attachment.tsx +441 -96
  112. package/src/feedback/feedback-dialog.tsx +147 -50
  113. package/src/i18n/kit-labels.tsx +19 -1
  114. package/src/i18n/locales/de.ts +4 -0
  115. package/src/i18n/locales/es.ts +4 -0
  116. package/src/i18n/locales/fr.ts +4 -0
  117. package/src/i18n/locales/hu.ts +4 -0
  118. package/src/i18n/locales/it.ts +4 -0
  119. package/src/i18n/locales/zh.ts +4 -0
  120. package/src/lib/format.ts +30 -0
  121. package/src/theme/palette-derive.ts +10 -0
  122. package/tokens.css +9 -1
@@ -8,6 +8,7 @@ import {
8
8
  DEFAULT_ATTACHMENT_ACCEPT,
9
9
  DEFAULT_MAX_ATTACHMENT_BYTES,
10
10
  FeedbackAttachmentField,
11
+ type FeedbackAttachmentError,
11
12
  } from "./feedback-attachment";
12
13
 
13
14
  export interface FeedbackCategoryOption {
@@ -27,6 +28,13 @@ export interface FeedbackAttachmentLabels {
27
28
  * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */
28
29
  attachmentPaste?: string;
29
30
  attachmentRemove: string;
31
+ /** `multiple` mode (0.15.5): the chip list's accessible name. Optional, as are the
32
+ * two below — they fall back to the provider's `feedbackAttachment`, then English. */
33
+ attachmentList?: string;
34
+ /** `multiple` mode: one chip's remove button, given the file's name. */
35
+ attachmentRemoveFile?: (name: string) => string;
36
+ /** `multiple` mode: the line shown once `max` files are attached. */
37
+ attachmentLimit?: (max: number) => string;
30
38
  }
31
39
 
32
40
  export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
@@ -38,6 +46,9 @@ export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
38
46
  submitHint: string;
39
47
  cancel: string;
40
48
  save: string;
49
+ /** The heading over the attachments under `attachments="multiple"` (0.15.5), where
50
+ * they are a screenshot AND photos. Optional; falls back to the provider, then English. */
51
+ attachments?: string;
41
52
  }
42
53
 
43
54
  /**
@@ -62,6 +73,9 @@ export interface FeedbackDialogTextLabels {
62
73
  submitHint: string;
63
74
  cancel: string;
64
75
  save: string;
76
+ /** The heading over the attachments under `attachments="multiple"` (0.15.5).
77
+ * Optional, so a complete `UiKitLabels` typed before 0.15.5 still compiles. */
78
+ attachments?: string;
65
79
  }
66
80
 
67
81
  export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
@@ -71,6 +85,7 @@ export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
71
85
  body: "What happened?",
72
86
  bodyOptional: "What happened? (optional)",
73
87
  attachment: "Screenshot",
88
+ attachments: "Attachments",
74
89
  submitHint: "Ctrl/⌘ + Enter to send",
75
90
  cancel: "Cancel",
76
91
  save: "Send",
@@ -84,36 +99,22 @@ export interface FeedbackSubmission {
84
99
  }
85
100
 
86
101
  /**
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.
102
+ * What `onSubmit` gets under `attachments="multiple"` (0.15.5): the screenshot and
103
+ * the picked/pasted files apart, because an app files them differently — keksdose
104
+ * attaches the screenshot to the report and the photos as its evidence list. No
105
+ * `attachment`: there is no one file it could name.
100
106
  */
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
- }: {
107
+ export interface FeedbackMultipleSubmission {
108
+ title: string;
109
+ body: string;
110
+ category: string;
111
+ /** What `onCaptureScreenshot` returned, or `null` — at most one. */
112
+ screenshot: File | null;
113
+ /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */
114
+ attachments: File[];
115
+ }
116
+
117
+ interface FeedbackDialogBaseProps {
117
118
  open: boolean;
118
119
  onClose: () => void;
119
120
  categories: FeedbackCategoryOption[];
@@ -122,12 +123,10 @@ export function FeedbackDialog({
122
123
  /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >
123
124
  * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */
124
125
  labels?: Partial<FeedbackDialogLabels>;
125
- onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
126
126
  submitting?: boolean;
127
127
  contextSlot?: ReactNode;
128
128
  attachmentAccept?: string[];
129
129
  maxAttachmentBytes?: number;
130
- onAttachmentError?: (kind: "type" | "size") => void;
131
130
  /**
132
131
  * Optional: capture a screenshot of the underlying app view and return it as a
133
132
  * File. When provided, a "Capture screenshot" button is shown next to "Add
@@ -140,15 +139,88 @@ export function FeedbackDialog({
140
139
  * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).
141
140
  */
142
141
  requireBody?: boolean;
143
- }) {
142
+ }
143
+
144
+ /** One attachment — a screenshot, a picked or a pasted image; a second replaces
145
+ * nothing, it has to wait for the first to be removed. The default. */
146
+ export interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {
147
+ attachments?: "single";
148
+ onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
149
+ onAttachmentError?: (kind: "type" | "size") => void;
150
+ /** Multiple mode only — refused here rather than silently ignored. */
151
+ maxAttachments?: never;
152
+ }
153
+
154
+ /**
155
+ * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose
156
+ * dev#578: "pasted two photos, the second overwrote the first"). The capture button is
157
+ * offered while there is no screenshot yet; the add button while there is room.
158
+ */
159
+ export interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {
160
+ attachments: "multiple";
161
+ /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */
162
+ maxAttachments?: number;
163
+ onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;
164
+ /** `"count"`: more files arrived at once than there was room for; the surplus was dropped. */
165
+ onAttachmentError?: (kind: FeedbackAttachmentError) => void;
166
+ }
167
+
168
+ export type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;
169
+
170
+ /**
171
+ * The generic feedback form dialog: category + subject + body + an optional image
172
+ * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the
173
+ * category options, labels and an `onSubmit` that talks to its own backend, plus
174
+ * an optional `contextSlot` for app-specific context (user, current URL, …).
175
+ *
176
+ * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog
177
+ * takes an image off the clipboard and makes it the attachment, through the same
178
+ * validation and the same preview as the file picker. It is the gesture the two
179
+ * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,
180
+ * and the file picker needs a file — so somebody who wanted to show one panel,
181
+ * or one region of one, had to save a crop to disk first and then find it again
182
+ * (Steering Design feedback #39). The clipboard is where a region snip already
183
+ * is on every platform.
184
+ *
185
+ * **`attachments="multiple"`** (0.15.5) takes several: every paste and pick adds a
186
+ * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link
187
+ * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed
188
+ * against the one-file submission keeps compiling and behaving as before.
189
+ */
190
+ export function FeedbackDialog(props: FeedbackDialogProps) {
191
+ const {
192
+ open,
193
+ onClose,
194
+ categories,
195
+ category,
196
+ onCategoryChange,
197
+ labels: labelsProp,
198
+ submitting = false,
199
+ contextSlot,
200
+ attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,
201
+ maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
202
+ onCaptureScreenshot,
203
+ requireBody = true,
204
+ } = props;
144
205
  const [title, setTitle] = useState("");
145
206
  const [body, setBody] = useState("");
146
207
  const [attachment, setAttachment] = useState<File | null>(null);
208
+ // `attachments="multiple"` only: the capture, apart from the picked/pasted files.
209
+ const [screenshot, setScreenshot] = useState<File | null>(null);
210
+ const [files, setFiles] = useState<File[]>([]);
147
211
  const isMobile = useMediaQuery(PHONE_QUERY, false);
148
212
  const labels = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);
149
213
  // The attachment field resolves its own keys from `feedbackAttachment`; it gets the
150
214
  // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.
151
- const attachmentLabels = { ...labelsProp, attachment: labels.attachment };
215
+ // Under `multiple` the heading is `attachments` — a screenshot AND photos — unless the
216
+ // caller named the multiple heading themselves.
217
+ const attachmentLabels = {
218
+ ...labelsProp,
219
+ attachment:
220
+ props.attachments === "multiple"
221
+ ? (labels.attachments ?? DEFAULT_FEEDBACK_DIALOG_LABELS.attachments)
222
+ : labels.attachment,
223
+ };
152
224
 
153
225
  // Reset the form whenever the dialog is (re)opened — during render, on the
154
226
  // closed-to-open transition, so the old draft never paints for a frame.
@@ -159,6 +231,8 @@ export function FeedbackDialog({
159
231
  setTitle("");
160
232
  setBody("");
161
233
  setAttachment(null);
234
+ setScreenshot(null);
235
+ setFiles([]);
162
236
  }
163
237
  }
164
238
 
@@ -166,7 +240,12 @@ export function FeedbackDialog({
166
240
  // with a 422, and a blank report is no report).
167
241
  const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;
168
242
  const trySubmit = () => {
169
- if (canSubmit) void onSubmit({ title, body, category, attachment });
243
+ if (!canSubmit) return;
244
+ if (props.attachments === "multiple") {
245
+ void props.onSubmit({ title, body, category, screenshot, attachments: files });
246
+ } else {
247
+ void props.onSubmit({ title, body, category, attachment });
248
+ }
170
249
  };
171
250
 
172
251
  if (!open) return null;
@@ -194,22 +273,40 @@ export function FeedbackDialog({
194
273
  onChange={(e) => setBody(e.target.value)}
195
274
  />
196
275
  );
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
- );
276
+ const fieldProps = {
277
+ labels: attachmentLabels,
278
+ accept: attachmentAccept,
279
+ maxBytes: maxAttachmentBytes,
280
+ onCaptureScreenshot,
281
+ // On `document`, not on the panel: the Modal focuses its own panel on open
282
+ // and traps Tab inside it, so while this dialog is up every paste in the
283
+ // page is meant for it — including the one made with nothing in
284
+ // particular focused, which never reaches a React `onPaste` on a child.
285
+ documentPaste: open,
286
+ };
287
+ const attachmentField =
288
+ props.attachments === "multiple" ? (
289
+ <FeedbackAttachmentField
290
+ {...fieldProps}
291
+ multiple
292
+ value={files}
293
+ onChange={setFiles}
294
+ max={props.maxAttachments}
295
+ // The capture goes to its own slot, so it is offered once and never crowds
296
+ // out a photo; a paste is a photo (a region snip is chosen evidence, not "the
297
+ // screenshot").
298
+ screenshot={screenshot}
299
+ onScreenshotChange={setScreenshot}
300
+ onError={props.onAttachmentError}
301
+ />
302
+ ) : (
303
+ <FeedbackAttachmentField
304
+ {...fieldProps}
305
+ value={attachment}
306
+ onChange={setAttachment}
307
+ onError={props.onAttachmentError}
308
+ />
309
+ );
213
310
 
214
311
  return (
215
312
  <Modal
@@ -417,8 +417,12 @@ interface KitI18n {
417
417
  locale?: string;
418
418
  weekStartsOn?: WeekDay;
419
419
  linkComponent?: KitLinkComponent;
420
+ chartTooltipPlacement?: ChartTooltipPlacement;
420
421
  }
421
422
 
423
+ /** Where a chart shows the values under the pointer — see `SeriesChartTooltip.placement`. */
424
+ export type ChartTooltipPlacement = "cursor" | "above" | "below" | "auto";
425
+
422
426
  /**
423
427
  * What every kit link receives: the anchor attributes the kit decided (href, class,
424
428
  * aria-current, handlers, children, ref). An app's router link maps them —
@@ -475,6 +479,13 @@ export interface UiKitProviderProps {
475
479
  * hash-router `#/path` is handed to it like `/path` (see `pickLinkRenderer`).
476
480
  */
477
481
  linkComponent?: KitLinkComponent;
482
+ /**
483
+ * Where every `SeriesChart` below shows its tooltip when it does not say so itself
484
+ * (`tooltip.placement`) — set once so an app's phone charts stop putting the box under
485
+ * the reader's finger (keksdose #359). Default: `"cursor"`, the box that follows the
486
+ * pointer, as before.
487
+ */
488
+ chartTooltipPlacement?: ChartTooltipPlacement;
478
489
  children: ReactNode;
479
490
  }
480
491
 
@@ -491,6 +502,7 @@ export function UiKitProvider({
491
502
  locale,
492
503
  weekStartsOn,
493
504
  linkComponent,
505
+ chartTooltipPlacement,
494
506
  children,
495
507
  }: UiKitProviderProps) {
496
508
  const outer = useContext(KitI18nContext);
@@ -500,8 +512,9 @@ export function UiKitProvider({
500
512
  weekStartsOn: weekStartsOn ?? outer.weekStartsOn,
501
513
  labels: mergeOverrides(outer.labels, labels),
502
514
  linkComponent: linkComponent ?? outer.linkComponent,
515
+ chartTooltipPlacement: chartTooltipPlacement ?? outer.chartTooltipPlacement,
503
516
  }),
504
- [outer, labels, locale, weekStartsOn, linkComponent],
517
+ [outer, labels, locale, weekStartsOn, linkComponent, chartTooltipPlacement],
505
518
  );
506
519
  return <KitI18nContext.Provider value={value}>{children}</KitI18nContext.Provider>;
507
520
  }
@@ -589,6 +602,11 @@ export function useKitWeekStart(): WeekDay | undefined {
589
602
  return useContext(KitI18nContext).weekStartsOn;
590
603
  }
591
604
 
605
+ /** The provider's `chartTooltipPlacement`, or `undefined` — a chart's own prop wins. */
606
+ export function useKitChartTooltipPlacement(): ChartTooltipPlacement | undefined {
607
+ return useContext(KitI18nContext).chartTooltipPlacement;
608
+ }
609
+
592
610
  /** {@link DEFAULT_FILE_LABELS}, but formatting in the provider's locale. */
593
611
  export function useKitFileLabels(prop?: Partial<FileLabels>): FileLabels {
594
612
  const locale = useKitLocale();
@@ -20,6 +20,9 @@ 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
+ attachmentRemoveFile: (name) => `${name} entfernen`,
25
+ attachmentLimit: (max) => `Maximal ${n(max)} ${max === 1 ? "Anhang" : "Anhänge"} – zum Hinzufügen erst einen entfernen.`,
23
26
  },
24
27
  feedbackDialog: {
25
28
  title: "Feedback senden",
@@ -28,6 +31,7 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
28
31
  body: "Was ist passiert?",
29
32
  bodyOptional: "Was ist passiert? (optional)",
30
33
  attachment: "Screenshot",
34
+ attachments: "Anhänge",
31
35
  submitHint: "Strg/⌘ + Enter zum Senden",
32
36
  cancel: "Abbrechen",
33
37
  save: "Senden",
@@ -21,6 +21,9 @@ 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
+ attachmentRemoveFile: (name) => `Quitar ${name}`,
26
+ attachmentLimit: (max) => `Máximo ${n(max)} ${max === 1 ? "adjunto" : "adjuntos"}: quita uno para añadir otro.`,
24
27
  },
25
28
  feedbackDialog: {
26
29
  title: "Enviar comentarios",
@@ -29,6 +32,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
29
32
  body: "¿Qué ha pasado?",
30
33
  bodyOptional: "¿Qué ha pasado? (opcional)",
31
34
  attachment: "Captura de pantalla",
35
+ attachments: "Adjuntos",
32
36
  submitHint: "Ctrl/⌘ + Intro para enviar",
33
37
  cancel: "Cancelar",
34
38
  save: "Enviar",
@@ -21,6 +21,9 @@ 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
+ attachmentRemoveFile: (name) => `Retirer ${name}`,
26
+ attachmentLimit: (max) => `${n(max)} ${max === 1 ? "pièce jointe" : "pièces jointes"} au maximum — retirez-en une pour en ajouter une autre.`,
24
27
  },
25
28
  feedbackDialog: {
26
29
  title: "Envoyer un retour",
@@ -29,6 +32,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
29
32
  body: "Que s’est-il passé\u202f?",
30
33
  bodyOptional: "Que s’est-il passé\u202f? (facultatif)",
31
34
  attachment: "Capture d’écran",
35
+ attachments: "Pièces jointes",
32
36
  submitHint: "Ctrl/⌘ + Entrée pour envoyer",
33
37
  cancel: "Annuler",
34
38
  save: "Envoyer",
@@ -20,6 +20,9 @@ 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
+ attachmentRemoveFile: (name) => `${name} eltávolítása`,
25
+ attachmentLimit: (max) => `Legfeljebb ${n(max)} melléklet – egy újabbhoz távolítson el egyet.`,
23
26
  },
24
27
  feedbackDialog: {
25
28
  title: "Visszajelzés küldése",
@@ -28,6 +31,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
28
31
  body: "Mi történt?",
29
32
  bodyOptional: "Mi történt? (nem kötelező)",
30
33
  attachment: "Képernyőkép",
34
+ attachments: "Mellékletek",
31
35
  submitHint: "Ctrl/⌘ + Enter a küldéshez",
32
36
  cancel: "Mégse",
33
37
  save: "Küldés",
@@ -21,6 +21,9 @@ 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
+ attachmentRemoveFile: (name) => `Rimuovi ${name}`,
26
+ attachmentLimit: (max) => `Massimo ${n(max)} ${max === 1 ? "allegato" : "allegati"}: rimuovine uno per aggiungerne un altro.`,
24
27
  },
25
28
  feedbackDialog: {
26
29
  title: "Invia feedback",
@@ -29,6 +32,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
29
32
  body: "Cosa è successo?",
30
33
  bodyOptional: "Cosa è successo? (facoltativo)",
31
34
  attachment: "Screenshot",
35
+ attachments: "Allegati",
32
36
  submitHint: "Ctrl/⌘ + Invio per inviare",
33
37
  cancel: "Annulla",
34
38
  save: "Invia",
@@ -20,6 +20,9 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
20
20
  attachmentCapture: "截取屏幕",
21
21
  attachmentPaste: "……或从剪贴板粘贴截图。",
22
22
  attachmentRemove: "移除附件",
23
+ attachmentList: "附件",
24
+ attachmentRemoveFile: (name) => `移除 ${name}`,
25
+ attachmentLimit: (max) => `最多 ${n(max)} 个附件,请先移除一个再添加。`,
23
26
  },
24
27
  feedbackDialog: {
25
28
  title: "发送反馈",
@@ -28,6 +31,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
28
31
  body: "发生了什么?",
29
32
  bodyOptional: "发生了什么?(可选)",
30
33
  attachment: "截图",
34
+ attachments: "附件",
31
35
  submitHint: "Ctrl/⌘ + Enter 发送",
32
36
  cancel: "取消",
33
37
  save: "发送",
package/src/lib/format.ts CHANGED
@@ -348,3 +348,33 @@ export function useKitFormat(locale?: string): KitFormat {
348
348
  [kitLocale],
349
349
  );
350
350
  }
351
+
352
+ /* ── Currency minor units ──────────────────────────────────────────────────── */
353
+
354
+ /**
355
+ * The decimals a currency is written with — its minor unit: CHF and EUR 2, JPY 0,
356
+ * KWD 3 — from `Intl`, or `undefined` for a code `Intl` does not accept. The rule
357
+ * AmountInput settles a typed figure by, for a host that needs it outside the field.
358
+ */
359
+ export function currencyMinorDigits(currency: string | null | undefined): number | undefined {
360
+ if (!currency) return undefined;
361
+ try {
362
+ return new Intl.NumberFormat("en", { style: "currency", currency }).resolvedOptions().maximumFractionDigits;
363
+ } catch {
364
+ return undefined;
365
+ }
366
+ }
367
+
368
+ /**
369
+ * `value` rounded to the currency's minor unit (or to `digits`), half away from zero,
370
+ * in decimal arithmetic: 93.4213 CHF → 93.42, 1.005 EUR → 1.01 (`Math.round(1.005 *
371
+ * 100)` is 100, since 1.005 is 1.00499… in binary). A non-finite value, or a currency
372
+ * `Intl` does not know without `digits`, comes back unchanged (keksdose live #356: an
373
+ * FX estimate set into an AmountInput showed four decimals).
374
+ */
375
+ export function roundToCurrency(value: number, currency: string | null | undefined, digits?: number): number {
376
+ const places = digits ?? currencyMinorDigits(currency);
377
+ if (places === undefined || !Number.isFinite(value)) return value;
378
+ const magnitude = Number(`${Math.round(Number(`${Math.abs(value)}e${places}`))}e-${places}`);
379
+ return value < 0 ? -magnitude : magnitude;
380
+ }
@@ -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 */