@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.
- package/README.md +20 -15
- package/dist/chart.d.ts +3 -4
- package/dist/components/alert-banner.js +1 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -4
- package/dist/components/amount-input.js +166 -157
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +14 -3
- package/dist/components/authed-image.js +32 -23
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/button-group.d.ts +3 -4
- package/dist/components/calculator.d.ts +3 -4
- package/dist/components/checkbox.js +1 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/choice-card.js +1 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +3 -4
- package/dist/components/error-boundary.js +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/facing-pair.d.ts +3 -4
- package/dist/components/file-button.d.ts +3 -4
- package/dist/components/file-button.js +64 -31
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -4
- package/dist/components/file-dropzone.js +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +4 -5
- package/dist/components/form-actions.js +18 -6
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +53 -2
- package/dist/components/line-items.js +73 -7
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +99 -5
- package/dist/components/list.js +103 -38
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +19 -3
- package/dist/components/loading-state.js +14 -3
- package/dist/components/loading-state.js.map +1 -1
- package/dist/components/money-field.d.ts +3 -4
- package/dist/components/number-field.d.ts +3 -4
- package/dist/components/number-input.d.ts +3 -4
- package/dist/components/number-input.js +4 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -4
- package/dist/components/passkeys-setting.d.ts +30 -10
- package/dist/components/passkeys-setting.js +19 -11
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +12 -3
- package/dist/components/progress-bar.js +21 -11
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +3 -4
- package/dist/components/settings-fields.d.ts +3 -4
- package/dist/components/signed-amount.d.ts +28 -4
- package/dist/components/signed-amount.js +17 -8
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.d.ts +31 -2
- package/dist/components/stat-tile.js +37 -6
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -1
- package/dist/components/status-dot.js +20 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/table.d.ts +18 -13
- package/dist/components/table.js +20 -5
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +3 -4
- package/dist/components/time-input.d.ts +3 -4
- package/dist/components/toggle-legend.d.ts +12 -1
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +3 -4
- package/dist/components/ui.js +156 -52
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +2 -82
- package/dist/feedback/feedback-attachment.js +285 -55
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +2 -105
- package/dist/feedback/feedback-dialog.js +57 -25
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +2 -2
- package/dist/feedback-attachment-WFiXJ8We.d.ts +302 -0
- package/dist/feedback.d.ts +1 -2
- package/dist/hooks/use-file-drop.d.ts +3 -4
- package/dist/i18n/defaults.d.ts +3 -4
- package/dist/i18n/kit-labels.d.ts +2 -3
- package/dist/i18n/locales/de-CH-informal.d.ts +3 -4
- package/dist/i18n/locales/de-CH.d.ts +3 -4
- package/dist/i18n/locales/de-informal.d.ts +3 -4
- package/dist/i18n/locales/de.d.ts +3 -4
- package/dist/i18n/locales/de.js +5 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -4
- package/dist/i18n/locales/es.js +5 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -4
- package/dist/i18n/locales/fr.js +5 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -4
- package/dist/i18n/locales/hu.js +5 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -4
- package/dist/i18n/locales/it.js +5 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -4
- package/dist/i18n/locales/zh.js +5 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +10 -11
- package/dist/{kit-labels-CnnBd-2N.d.ts → kit-labels-DE--LmCk.d.ts} +154 -14
- package/dist/lib/format.d.ts +18 -3
- package/dist/lib/format.js +2 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +3 -4
- package/dist/rhf/form.d.ts +3 -4
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +3 -4
- package/dist/shell/app-shell.d.ts +3 -4
- package/dist/shell/top-bar-brand.d.ts +3 -4
- package/dist/shell.d.ts +3 -4
- package/dist/theme/palette-derive.d.ts +3 -0
- package/dist/theme/palette-derive.js +7 -0
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +3 -4
- package/dist/wizard.d.ts +3 -4
- package/package.json +4 -2
- package/src/components/alert-banner.tsx +1 -1
- package/src/components/amount-input.tsx +47 -16
- package/src/components/authed-image.tsx +20 -2
- package/src/components/checkbox.tsx +1 -1
- package/src/components/choice-card.tsx +1 -1
- package/src/components/error-boundary.tsx +1 -1
- package/src/components/file-button.tsx +77 -27
- package/src/components/file-dropzone.tsx +1 -1
- package/src/components/form-actions.tsx +86 -8
- package/src/components/line-items.tsx +131 -14
- package/src/components/list.tsx +240 -50
- package/src/components/loading-state.tsx +35 -4
- package/src/components/number-input.tsx +18 -2
- package/src/components/passkeys-setting.tsx +45 -18
- package/src/components/progress-bar.tsx +46 -21
- package/src/components/signed-amount.tsx +47 -8
- package/src/components/stat-tile.tsx +76 -6
- package/src/components/status-dot.tsx +31 -0
- package/src/components/swatch-picker.tsx +3 -1
- package/src/components/table.tsx +42 -15
- package/src/components/toggle-legend.tsx +21 -4
- package/src/components/ui.tsx +280 -70
- package/src/feedback/feedback-attachment.tsx +472 -96
- package/src/feedback/feedback-dialog.tsx +162 -50
- package/src/i18n/locales/de.ts +4 -0
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/lib/format.ts +24 -3
- package/src/rhf/line-items.tsx +6 -0
- package/src/theme/palette-derive.ts +10 -0
- 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
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 =
|
|
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)
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -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(
|
|
376
|
-
|
|
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;
|
package/src/rhf/line-items.tsx
CHANGED
|
@@ -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
|
-
|
|
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 */
|