@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.
- package/README.md +11 -10
- package/dist/chart.d.ts +50 -1
- package/dist/components/alert-banner.js +1 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -3
- package/dist/components/amount-input.js +6 -11
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/button-group.d.ts +2 -3
- package/dist/components/calculator.d.ts +2 -3
- package/dist/components/chart.d.ts +16 -1
- package/dist/components/chart.js +65 -37
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.js +1 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +8 -2
- package/dist/components/chip.js +12 -3
- package/dist/components/chip.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 +2 -3
- package/dist/components/error-boundary.js +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/facing-pair.d.ts +50 -0
- package/dist/components/file-button.d.ts +2 -3
- package/dist/components/file-dropzone.d.ts +2 -3
- package/dist/components/file-dropzone.js +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +2 -3
- package/dist/components/form-actions.js +9 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/money-field.d.ts +2 -3
- package/dist/components/number-field.d.ts +2 -3
- package/dist/components/number-input.d.ts +2 -3
- package/dist/components/number-input.js +17 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +2 -3
- package/dist/components/series-chart.d.ts +78 -1
- package/dist/components/series-chart.js +74 -13
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -3
- package/dist/components/text-link.d.ts +2 -3
- package/dist/components/time-input.d.ts +2 -3
- package/dist/components/ui.d.ts +2 -3
- package/dist/components/ui.js +7 -7
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +2 -82
- package/dist/feedback/feedback-attachment.js +271 -54
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +2 -105
- package/dist/feedback/feedback-dialog.js +56 -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-BYAHJMOr.d.ts +289 -0
- package/dist/feedback.d.ts +1 -2
- package/dist/hooks/use-file-drop.d.ts +2 -3
- package/dist/i18n/defaults.d.ts +2 -3
- package/dist/i18n/kit-labels.d.ts +2 -3
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -3
- package/dist/i18n/locales/de-CH.d.ts +2 -3
- package/dist/i18n/locales/de-informal.d.ts +2 -3
- package/dist/i18n/locales/de.d.ts +2 -3
- package/dist/i18n/locales/de.js +5 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -3
- package/dist/i18n/locales/es.js +5 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -3
- package/dist/i18n/locales/fr.js +5 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -3
- package/dist/i18n/locales/hu.js +5 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -3
- package/dist/i18n/locales/it.js +5 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -3
- package/dist/i18n/locales/zh.js +5 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/{kit-labels-BHfRBbHs.d.ts → kit-labels-BD2F7sOz.d.ts} +15 -5
- package/dist/lib/format.d.ts +15 -1
- package/dist/lib/format.js +16 -0
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +2 -3
- package/dist/rhf/form.d.ts +2 -3
- package/dist/rhf.d.ts +2 -3
- package/dist/shell/app-shell.d.ts +2 -3
- package/dist/shell/top-bar-brand.d.ts +2 -3
- package/dist/shell.d.ts +2 -3
- 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 +2 -3
- package/dist/wizard.d.ts +2 -3
- package/package.json +4 -2
- package/src/components/alert-banner.tsx +1 -1
- package/src/components/amount-input.tsx +16 -14
- package/src/components/chart.tsx +102 -41
- package/src/components/checkbox.tsx +1 -1
- package/src/components/chip.tsx +29 -4
- package/src/components/choice-card.tsx +1 -1
- package/src/components/error-boundary.tsx +1 -1
- package/src/components/file-dropzone.tsx +1 -1
- package/src/components/form-actions.tsx +9 -1
- package/src/components/number-input.tsx +22 -2
- package/src/components/series-chart.tsx +122 -20
- package/src/components/ui.tsx +7 -7
- package/src/feedback/feedback-attachment.tsx +441 -96
- package/src/feedback/feedback-dialog.tsx +147 -50
- package/src/i18n/kit-labels.tsx +19 -1
- 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 +30 -0
- package/src/theme/palette-derive.ts +10 -0
- 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
|
-
*
|
|
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.
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -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();
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -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",
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -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
|
-
|
|
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 */
|