@eifi1/ui-kit 0.12.0 → 0.13.1
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 +10 -10
- package/dist/components/amount-input.d.ts +23 -1
- package/dist/components/amount-input.js +78 -13
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +1 -1
- package/dist/components/calculator.d.ts +1 -1
- package/dist/components/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.js +20 -15
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox-core.d.ts +3 -1
- package/dist/components/combobox-core.js +16 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table.js +28 -20
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/entity-combobox.js +22 -17
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +202 -13
- package/dist/components/error-boundary.js +225 -21
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-dropzone.d.ts +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/list.d.ts +14 -2
- package/dist/components/list.js +3 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +23 -17
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +1 -1
- package/dist/components/number-input.d.ts +1 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -2
- package/dist/components/numpad-sheet.js +6 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/picker-sheet.js +3 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -1
- package/dist/components/signed-amount.d.ts +2 -1
- package/dist/components/signed-amount.js +1 -1
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.js +1 -1
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/text-link.d.ts +73 -11
- package/dist/components/text-link.js +45 -10
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -1
- package/dist/components/toggle-group.d.ts +22 -0
- package/dist/components/toggle-group.js +8 -8
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +17 -0
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +1 -1
- package/dist/components/ui.js +166 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +5 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +43 -3
- package/dist/feedback/feedback-thread.js +53 -26
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -1
- package/dist/i18n/defaults.d.ts +1 -1
- package/dist/i18n/kit-labels.d.ts +1 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +1 -1
- package/dist/i18n/locales/de-CH.d.ts +1 -1
- package/dist/i18n/locales/de-informal.d.ts +1 -1
- package/dist/i18n/locales/de.d.ts +1 -1
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +1 -1
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +1 -1
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +1 -1
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +1 -1
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/{kit-labels-v3biUF1L.d.ts → kit-labels-Mz8zZHIF.d.ts} +102 -3
- package/dist/lib/format.d.ts +23 -5
- package/dist/lib/format.js +4 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +59 -15
- package/dist/rhf/fields.js +41 -11
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -1
- package/dist/rhf.d.ts +2 -2
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/app-shell.js +62 -54
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +1 -1
- package/dist/shell/topbar-action-menu.d.ts +9 -3
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +11 -1
- package/dist/wizard/wizard-step.js +24 -6
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +1 -1
- package/package.json +6 -6
- package/src/components/amount-input.tsx +165 -12
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +6 -0
- package/src/components/combobox-core.tsx +28 -9
- package/src/components/combobox.tsx +2 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table.tsx +6 -0
- package/src/components/entity-combobox.tsx +7 -4
- package/src/components/error-boundary.tsx +470 -30
- package/src/components/field-sync.tsx +48 -40
- package/src/components/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- package/src/components/list.tsx +17 -3
- package/src/components/modal.tsx +4 -0
- package/src/components/multi-entity-combobox.tsx +8 -4
- package/src/components/number-input.tsx +1 -0
- package/src/components/numpad-sheet.tsx +17 -3
- package/src/components/page-header.tsx +42 -5
- package/src/components/picker-sheet.tsx +3 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/signed-amount.tsx +12 -3
- package/src/components/stat-tile.tsx +3 -1
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/text-link.tsx +126 -18
- package/src/components/text.tsx +47 -2
- package/src/components/toggle-group.tsx +35 -10
- package/src/components/toggle-legend.tsx +22 -1
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +307 -39
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +7 -4
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +109 -33
- package/src/i18n/kit-labels.tsx +16 -0
- package/src/i18n/locales/de.ts +8 -0
- package/src/i18n/locales/es.ts +8 -0
- package/src/i18n/locales/fr.ts +8 -0
- package/src/i18n/locales/hu.ts +8 -0
- package/src/i18n/locales/it.ts +8 -0
- package/src/i18n/locales/zh.ts +8 -0
- package/src/lib/format.ts +32 -7
- package/src/rhf/fields.tsx +99 -16
- package/src/rhf.ts +1 -1
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +6 -1
- package/src/shell/topbar-action-menu.tsx +11 -4
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-step.tsx +38 -6
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
1
|
+
import { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import type { ReactNode, Ref } from "react";
|
|
3
3
|
import { Paperclip, Send } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { formatRelativeTime } from "../lib/format";
|
|
5
6
|
import { Button, EmptyState, Spinner, Textarea } from "../components/ui";
|
|
6
7
|
import { Skeleton } from "../components/skeleton";
|
|
8
|
+
import { Tooltip } from "../components/tooltip";
|
|
7
9
|
import { useKitFileLabels, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
8
10
|
import { FeedbackAttachmentField } from "./feedback-attachment";
|
|
9
11
|
import type { FeedbackNoteAttachment } from "./feedback-inbox";
|
|
@@ -190,13 +192,19 @@ export function FeedbackThread({
|
|
|
190
192
|
</span>
|
|
191
193
|
)}
|
|
192
194
|
{date && (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
className="tabular-nums"
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
// The kit's Tooltip, not a native title: one tooltip look app-wide
|
|
196
|
+
// (keksdose F0 / dev#523).
|
|
197
|
+
<Tooltip label={new Intl.DateTimeFormat(locale, { dateStyle: "full", timeStyle: "short" }).format(date)}>
|
|
198
|
+
<time dateTime={date.toISOString()} className="tabular-nums">
|
|
199
|
+
{formatRelativeTime(date, {
|
|
200
|
+
now: reference,
|
|
201
|
+
locale,
|
|
202
|
+
// Past the kit's day steps it is a date: "2 weeks ago" makes
|
|
203
|
+
// the reader do arithmetic the date already did.
|
|
204
|
+
absoluteAfterDays: 6.5,
|
|
205
|
+
})}
|
|
206
|
+
</time>
|
|
207
|
+
</Tooltip>
|
|
200
208
|
)}
|
|
201
209
|
</header>
|
|
202
210
|
{message.body && (
|
|
@@ -287,27 +295,6 @@ function toDate(value: Date | string | number): Date | null {
|
|
|
287
295
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
288
296
|
}
|
|
289
297
|
|
|
290
|
-
const MINUTE = 60_000;
|
|
291
|
-
const HOUR = 60 * MINUTE;
|
|
292
|
-
const DAY = 24 * HOUR;
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* "just now", "5 minutes ago", "yesterday", "3 days ago" — `Intl.RelativeTimeFormat`
|
|
296
|
-
* in `locale`, so the grammar is the language's. Past a week it is a date: "12 days
|
|
297
|
-
* ago" makes the reader do arithmetic that the date already did. The full date and
|
|
298
|
-
* time are on the `<time>`'s title.
|
|
299
|
-
*/
|
|
300
|
-
function formatRelativeTime(date: Date, now: number, locale: string | undefined): string {
|
|
301
|
-
const diff = date.getTime() - now;
|
|
302
|
-
const abs = Math.abs(diff);
|
|
303
|
-
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
304
|
-
if (abs < 45_000) return rtf.format(0, "second");
|
|
305
|
-
if (abs < 45 * MINUTE) return rtf.format(Math.round(diff / MINUTE), "minute");
|
|
306
|
-
if (abs < 22 * HOUR) return rtf.format(Math.round(diff / HOUR), "hour");
|
|
307
|
-
if (abs < 7 * DAY) return rtf.format(Math.round(diff / DAY), "day");
|
|
308
|
-
return new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(date);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
298
|
/* ── Composer ─────────────────────────────────────────────────────────────── */
|
|
312
299
|
|
|
313
300
|
/** What the composer's attach control takes — {@link FeedbackNoteAttachment} with the
|
|
@@ -332,10 +319,51 @@ export interface FeedbackComposerProps {
|
|
|
332
319
|
* reader. */
|
|
333
320
|
disabledReason?: ReactNode;
|
|
334
321
|
rows?: number;
|
|
322
|
+
/**
|
|
323
|
+
* The box's hint, over the `placeholder` label — keksdose F9: an admin answering a
|
|
324
|
+
* support ticket is asked something else ("Reply to Anna…") than a member adding to
|
|
325
|
+
* their own report, and one provider-wide label cannot say both.
|
|
326
|
+
*/
|
|
327
|
+
placeholder?: string;
|
|
328
|
+
/**
|
|
329
|
+
* The draft, controlled — for a host that keeps it itself (saves it per ticket,
|
|
330
|
+
* prefills a template). Left out, the composer keeps its own. Either way a send
|
|
331
|
+
* that resolves clears only what was sent: controlled, that is an
|
|
332
|
+
* `onValueChange("")` the host receives like any keystroke, and only if the draft
|
|
333
|
+
* is still the one that went out.
|
|
334
|
+
*/
|
|
335
|
+
value?: string;
|
|
336
|
+
/** Every change to the draft — typing, {@link FeedbackComposerHandle.insertText}, the
|
|
337
|
+
* clear after a send. Required to change a controlled `value`. */
|
|
338
|
+
onValueChange?: (value: string) => void;
|
|
339
|
+
/** {@link FeedbackComposerHandle}: put text into the draft from outside (canned-reply
|
|
340
|
+
* chips) or focus the box. */
|
|
341
|
+
ref?: Ref<FeedbackComposerHandle>;
|
|
335
342
|
labels?: Partial<FeedbackComposerLabels>;
|
|
336
343
|
className?: string;
|
|
337
344
|
}
|
|
338
345
|
|
|
346
|
+
/**
|
|
347
|
+
* What a {@link FeedbackComposer} `ref` holds.
|
|
348
|
+
*
|
|
349
|
+
* keksdose F9: the support admin answers the same five questions all day and wants
|
|
350
|
+
* chips above the box ("Thanks, fixed in the next release") that drop their text in
|
|
351
|
+
* where the caret is. A controlled `value` alone cannot do that well — the host sees
|
|
352
|
+
* the string, not the caret, so it could only append — so the composer, which owns
|
|
353
|
+
* the textarea, does the splice.
|
|
354
|
+
*/
|
|
355
|
+
export interface FeedbackComposerHandle {
|
|
356
|
+
/**
|
|
357
|
+
* Insert `text` at the caret, replacing any selection (the caret the box last had:
|
|
358
|
+
* a click on a chip blurs it, and the selection survives the blur), then focus the
|
|
359
|
+
* box with the caret after the inserted text so the reply can go on. Nothing is
|
|
360
|
+
* added around it — put a space or a newline in `text` if the reply needs one. A
|
|
361
|
+
* no-op while `disabledReason` replaces the box.
|
|
362
|
+
*/
|
|
363
|
+
insertText: (text: string) => void;
|
|
364
|
+
focus: () => void;
|
|
365
|
+
}
|
|
366
|
+
|
|
339
367
|
const noSubscribe = () => () => {};
|
|
340
368
|
const isApple = () =>
|
|
341
369
|
typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);
|
|
@@ -352,13 +380,60 @@ export function FeedbackComposer({
|
|
|
352
380
|
attachment,
|
|
353
381
|
disabledReason,
|
|
354
382
|
rows = 3,
|
|
383
|
+
placeholder,
|
|
384
|
+
value,
|
|
385
|
+
onValueChange,
|
|
386
|
+
ref,
|
|
355
387
|
labels: labelsProp,
|
|
356
388
|
className,
|
|
357
389
|
}: FeedbackComposerProps) {
|
|
358
390
|
const labels = useKitLabels("feedbackComposer", DEFAULT_FEEDBACK_COMPOSER_LABELS, labelsProp);
|
|
359
|
-
const [
|
|
391
|
+
const [ownDraft, setOwnDraft] = useState("");
|
|
392
|
+
const controlled = value !== undefined;
|
|
393
|
+
const draft = controlled ? value : ownDraft;
|
|
360
394
|
const [file, setFile] = useState<File | null>(null);
|
|
361
395
|
const root = useRef<HTMLDivElement>(null);
|
|
396
|
+
const box = useRef<HTMLTextAreaElement>(null);
|
|
397
|
+
// The draft as of the last commit. The send's `.then` and `insertText` run after
|
|
398
|
+
// renders their closure did not see — text typed while the send was in flight, a
|
|
399
|
+
// second chip clicked before the first one's render — and must not act on a stale
|
|
400
|
+
// string (for a controlled draft there is no updater function to lean on).
|
|
401
|
+
const latest = useRef(draft);
|
|
402
|
+
useLayoutEffect(() => {
|
|
403
|
+
latest.current = draft;
|
|
404
|
+
});
|
|
405
|
+
// Where the caret goes once an inserted text has rendered.
|
|
406
|
+
const caret = useRef<number | null>(null);
|
|
407
|
+
useLayoutEffect(() => {
|
|
408
|
+
const el = box.current;
|
|
409
|
+
if (caret.current === null || !el) return;
|
|
410
|
+
el.focus();
|
|
411
|
+
el.setSelectionRange(caret.current, caret.current);
|
|
412
|
+
caret.current = null;
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const setDraft = (next: string) => {
|
|
416
|
+
latest.current = next;
|
|
417
|
+
if (!controlled) setOwnDraft(next);
|
|
418
|
+
onValueChange?.(next);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
useImperativeHandle(ref, () => ({
|
|
422
|
+
insertText: (text) => {
|
|
423
|
+
const el = box.current;
|
|
424
|
+
if (!el) return;
|
|
425
|
+
const current = latest.current;
|
|
426
|
+
// A second insert before the first has rendered goes after the first, not at the
|
|
427
|
+
// textarea's not-yet-moved caret. Otherwise the textarea's selection, which may
|
|
428
|
+
// be past the end of a controlled value the host has just shortened: clamp it.
|
|
429
|
+
const pending = caret.current;
|
|
430
|
+
const start = Math.min(pending ?? el.selectionStart ?? current.length, current.length);
|
|
431
|
+
const end = pending ?? Math.max(start, Math.min(el.selectionEnd ?? start, current.length));
|
|
432
|
+
caret.current = start + text.length;
|
|
433
|
+
setDraft(current.slice(0, start) + text + current.slice(end));
|
|
434
|
+
},
|
|
435
|
+
focus: () => box.current?.focus(),
|
|
436
|
+
}));
|
|
362
437
|
// The server snapshot is "Ctrl"; the client corrects it after hydration.
|
|
363
438
|
const modifier = useSyncExternalStore(
|
|
364
439
|
noSubscribe,
|
|
@@ -377,7 +452,7 @@ export function FeedbackComposer({
|
|
|
377
452
|
const sentFile = file;
|
|
378
453
|
Promise.resolve(onSend(draft.trim(), file))
|
|
379
454
|
.then(() => {
|
|
380
|
-
|
|
455
|
+
if (latest.current === sentDraft) setDraft("");
|
|
381
456
|
setFile((current) => (current === sentFile ? null : current));
|
|
382
457
|
})
|
|
383
458
|
.catch(() => {});
|
|
@@ -391,7 +466,8 @@ export function FeedbackComposer({
|
|
|
391
466
|
rows={rows}
|
|
392
467
|
dir="auto"
|
|
393
468
|
aria-label={labels.field}
|
|
394
|
-
|
|
469
|
+
ref={box}
|
|
470
|
+
placeholder={placeholder ?? labels.placeholder}
|
|
395
471
|
value={draft}
|
|
396
472
|
onChange={(event) => setDraft(event.target.value)}
|
|
397
473
|
onKeyDown={(event) => {
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -423,10 +423,26 @@ interface KitI18n {
|
|
|
423
423
|
* What every kit link receives: the anchor attributes the kit decided (href, class,
|
|
424
424
|
* aria-current, handlers, children, ref). An app's router link maps them —
|
|
425
425
|
* `({ href, ...p }) => <Link to={href} {...p} />` — ONCE, on the provider.
|
|
426
|
+
*
|
|
427
|
+
* `replace` is the one prop that is not an anchor attribute: see there. Map it
|
|
428
|
+
* explicitly if your router link is not react-router's —
|
|
429
|
+
* `({ href, replace, ...p }) => <Link to={href} replace={replace} {...p} />` — and never
|
|
430
|
+
* spread it onto a DOM `<a>`, where React warns about an unknown attribute.
|
|
426
431
|
*/
|
|
427
432
|
export type KitLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
428
433
|
href: string;
|
|
429
434
|
ref?: Ref<HTMLAnchorElement>;
|
|
435
|
+
/**
|
|
436
|
+
* Navigate by REPLACING the current history entry, so Back skips it — keksdose F1:
|
|
437
|
+
* a one-shot URL (a magic-link landing, `?checkout=done`, a wizard's "finish" hop)
|
|
438
|
+
* that must not come back when the user presses Back. Present ONLY when the kit link
|
|
439
|
+
* was given `replace` (`true`), so a provider link that spreads its props onto a
|
|
440
|
+
* plain `<a>` keeps working for every link that never asks for it. react-router's
|
|
441
|
+
* `<Link>` takes the same prop, so `({ href, ...p }) => <Link to={href} {...p} />`
|
|
442
|
+
* honours it with no change; any other router maps it to its own replace option.
|
|
443
|
+
* The kit never puts it on a DOM element itself.
|
|
444
|
+
*/
|
|
445
|
+
replace?: boolean;
|
|
430
446
|
};
|
|
431
447
|
export type KitLinkComponent = (props: KitLinkProps) => ReactElement;
|
|
432
448
|
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -553,6 +553,14 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
|
|
|
553
553
|
message: "Dieser Teil der Seite konnte nicht angezeigt werden. Bitte erneut versuchen oder die Seite neu laden.",
|
|
554
554
|
retry: "Erneut versuchen",
|
|
555
555
|
details: "Fehlerdetails",
|
|
556
|
+
reload: "Neu laden",
|
|
557
|
+
updateTitle: "Eine neue Version ist verfügbar",
|
|
558
|
+
updateMessage: "Ein Teil der App hat sich seit dem Laden dieser Seite geändert. Zum Aktualisieren die Seite neu laden.",
|
|
559
|
+
offlineTitle: "Keine Internetverbindung",
|
|
560
|
+
offlineMessage: "Diese Seite konnte ohne Verbindung nicht geladen werden. Nach dem Wiederherstellen der Verbindung die Seite neu laden.",
|
|
561
|
+
copyReport: "Fehlerbericht kopieren",
|
|
562
|
+
reported: "Der Fehler wurde automatisch gemeldet.",
|
|
563
|
+
reportedAs: (reference) => `Gemeldet unter ${reference}`,
|
|
556
564
|
},
|
|
557
565
|
authedImage: {
|
|
558
566
|
loading: "Bild wird geladen…",
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -543,6 +543,14 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
543
543
|
message: "No se pudo mostrar esta parte de la página. Inténtelo de nuevo o recargue la página.",
|
|
544
544
|
retry: "Reintentar",
|
|
545
545
|
details: "Detalles del error",
|
|
546
|
+
reload: "Recargar",
|
|
547
|
+
updateTitle: "Hay una nueva versión disponible",
|
|
548
|
+
updateMessage: "Una parte de la aplicación ha cambiado desde que se cargó esta página. Recárguela para obtener la nueva versión.",
|
|
549
|
+
offlineTitle: "Sin conexión",
|
|
550
|
+
offlineMessage: "No se pudo cargar esta página sin conexión. Vuelva a conectarse y recargue la página.",
|
|
551
|
+
copyReport: "Copiar informe de error",
|
|
552
|
+
reported: "El error se ha notificado automáticamente.",
|
|
553
|
+
reportedAs: (reference) => `Notificado con la referencia ${reference}`,
|
|
546
554
|
},
|
|
547
555
|
authedImage: {
|
|
548
556
|
loading: "Cargando imagen…",
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -559,6 +559,14 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
559
559
|
message: "Cette partie de la page n’a pas pu s’afficher. Réessayez ou rechargez la page.",
|
|
560
560
|
retry: "Réessayer",
|
|
561
561
|
details: "Détails de l’erreur",
|
|
562
|
+
reload: "Recharger",
|
|
563
|
+
updateTitle: "Une nouvelle version est disponible",
|
|
564
|
+
updateMessage: "Une partie de l’application a changé depuis le chargement de cette page. Rechargez-la pour obtenir la nouvelle version.",
|
|
565
|
+
offlineTitle: "Vous êtes hors ligne",
|
|
566
|
+
offlineMessage: "Cette page n’a pas pu être chargée sans connexion. Reconnectez-vous, puis rechargez la page.",
|
|
567
|
+
copyReport: "Copier le rapport d’erreur",
|
|
568
|
+
reported: "L’erreur a été signalée automatiquement.",
|
|
569
|
+
reportedAs: (reference) => `Signalée sous la référence ${reference}`,
|
|
562
570
|
},
|
|
563
571
|
authedImage: {
|
|
564
572
|
loading: "Chargement de l’image…",
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -545,6 +545,14 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
545
545
|
message: "Az oldal ezen része nem jeleníthető meg. Próbálja újra, vagy töltse be újra az oldalt.",
|
|
546
546
|
retry: "Újrapróbálás",
|
|
547
547
|
details: "Hiba részletei",
|
|
548
|
+
reload: "Újratöltés",
|
|
549
|
+
updateTitle: "Új verzió érhető el",
|
|
550
|
+
updateMessage: "Az alkalmazás egy része megváltozott az oldal betöltése óta. Az új verzióhoz töltse be újra az oldalt.",
|
|
551
|
+
offlineTitle: "Nincs internetkapcsolat",
|
|
552
|
+
offlineMessage: "Az oldal kapcsolat nélkül nem tölthető be. A kapcsolat helyreállása után töltse be újra az oldalt.",
|
|
553
|
+
copyReport: "Hibajelentés másolása",
|
|
554
|
+
reported: "A hibát automatikusan jelentettük.",
|
|
555
|
+
reportedAs: (reference) => `Bejelentés azonosítója: ${reference}`,
|
|
548
556
|
},
|
|
549
557
|
authedImage: {
|
|
550
558
|
loading: "Kép betöltése…",
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -546,6 +546,14 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
546
546
|
message: "Impossibile mostrare questa parte della pagina. Riprova o ricarica la pagina.",
|
|
547
547
|
retry: "Riprova",
|
|
548
548
|
details: "Dettagli dell’errore",
|
|
549
|
+
reload: "Ricarica",
|
|
550
|
+
updateTitle: "È disponibile una nuova versione",
|
|
551
|
+
updateMessage: "Una parte dell’app è cambiata da quando questa pagina è stata caricata. Ricarica per ottenere la nuova versione.",
|
|
552
|
+
offlineTitle: "Sei offline",
|
|
553
|
+
offlineMessage: "Impossibile caricare questa pagina senza connessione. Riconnettiti, poi ricarica la pagina.",
|
|
554
|
+
copyReport: "Copia il rapporto di errore",
|
|
555
|
+
reported: "L’errore è stato segnalato automaticamente.",
|
|
556
|
+
reportedAs: (reference) => `Segnalato con riferimento ${reference}`,
|
|
549
557
|
},
|
|
550
558
|
authedImage: {
|
|
551
559
|
loading: "Caricamento immagine…",
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -533,6 +533,14 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
533
533
|
message: "无法显示页面的这一部分。请重试或重新加载页面。",
|
|
534
534
|
retry: "重试",
|
|
535
535
|
details: "错误详情",
|
|
536
|
+
reload: "重新加载",
|
|
537
|
+
updateTitle: "有新版本可用",
|
|
538
|
+
updateMessage: "自此页面加载以来,应用的部分内容已更新。请重新加载以获取新版本。",
|
|
539
|
+
offlineTitle: "当前处于离线状态",
|
|
540
|
+
offlineMessage: "没有网络连接,无法加载此页面。请恢复连接后重新加载页面。",
|
|
541
|
+
copyReport: "复制错误报告",
|
|
542
|
+
reported: "此错误已自动报告。",
|
|
543
|
+
reportedAs: (reference) => `报告编号:${reference}`,
|
|
536
544
|
},
|
|
537
545
|
authedImage: {
|
|
538
546
|
loading: "正在加载图片…",
|
package/src/lib/format.ts
CHANGED
|
@@ -37,7 +37,8 @@ interface BaseOptions {
|
|
|
37
37
|
empty?: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/** The options every number formatter shares (money and percent carry their own unit). */
|
|
41
|
+
export interface FormatNumberBaseOptions extends BaseOptions {
|
|
41
42
|
digits?: FormatDigits;
|
|
42
43
|
/** Compact notation: 12 400 → "12K". */
|
|
43
44
|
compact?: boolean;
|
|
@@ -47,12 +48,33 @@ export interface FormatNumberOptions extends BaseOptions {
|
|
|
47
48
|
options?: Intl.NumberFormatOptions;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */
|
|
52
|
+
export interface FormatNumberOptions extends FormatNumberBaseOptions {
|
|
53
|
+
/**
|
|
54
|
+
* A unit printed after the figure, joined by a narrow no-break space (U+202F):
|
|
55
|
+
* "3.400 N", "12 kg", "4,5 m²". lenkbank (P8) prints forces, masses and lengths in
|
|
56
|
+
* every table and summary, and each call site glued `${n} N` together with a plain
|
|
57
|
+
* space — which wraps the "N" onto the next line in a narrow column and reads loose
|
|
58
|
+
* beside the digit grouping. The narrow no-break space is the SI brochure's and the
|
|
59
|
+
* Swiss typesetting rule's separator between value and unit, and it never breaks.
|
|
60
|
+
*
|
|
61
|
+
* Free text, not `Intl`'s `style: "unit"`: that one only knows the ECMA-402
|
|
62
|
+
* sanctioned units (no "N", no "kN", no "m³/h"), so an app would still be gluing.
|
|
63
|
+
* A missing value prints `empty` ALONE — "— N" says there is a newton-shaped
|
|
64
|
+
* nothing, which is noise in a column that already heads its unit.
|
|
65
|
+
*/
|
|
66
|
+
unit?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Narrow no-break space (U+202F): between a figure and its {@link FormatNumberOptions.unit}. */
|
|
70
|
+
const UNIT_SEPARATOR = "\u202F";
|
|
71
|
+
|
|
72
|
+
export interface FormatMoneyOptions extends FormatNumberBaseOptions {
|
|
51
73
|
/** `"symbol"` (default), `"narrowSymbol"`, `"code"` ("CHF 12.00") or `"name"`. */
|
|
52
74
|
currencyDisplay?: Intl.NumberFormatOptions["currencyDisplay"];
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
export interface FormatPercentOptions extends
|
|
77
|
+
export interface FormatPercentOptions extends FormatNumberBaseOptions {
|
|
56
78
|
/**
|
|
57
79
|
* `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read
|
|
58
80
|
* it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %
|
|
@@ -83,7 +105,7 @@ function digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOption
|
|
|
83
105
|
};
|
|
84
106
|
}
|
|
85
107
|
|
|
86
|
-
function numberOptions(o:
|
|
108
|
+
function numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {
|
|
87
109
|
return {
|
|
88
110
|
...digitOptions(o.digits),
|
|
89
111
|
...(o.compact ? { notation: "compact" } : null),
|
|
@@ -93,13 +115,16 @@ function numberOptions(o: FormatNumberOptions): Intl.NumberFormatOptions {
|
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
function isMissing(value: number | null | undefined): value is null | undefined {
|
|
96
|
-
|
|
118
|
+
// ±Infinity too (lenkbank, adopting 0.13): a division by zero upstream is a missing
|
|
119
|
+
// figure, and "∞" printed in a money or measurement column reads like data.
|
|
120
|
+
return value === null || value === undefined || !Number.isFinite(value);
|
|
97
121
|
}
|
|
98
122
|
|
|
99
|
-
/** A number in `locale`: "1,234.5", "1.234,5", "1’234.5". */
|
|
123
|
+
/** A number in `locale`: "1,234.5", "1.234,5", "1’234.5" — with `unit`, "3.400 N". */
|
|
100
124
|
export function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {
|
|
101
125
|
if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
102
|
-
|
|
126
|
+
const figure = numberFormatter(options.locale, numberOptions(options)).format(value);
|
|
127
|
+
return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
/** An amount of `currency` (ISO 4217) in `locale`: "CHF 1’234.50", "1.234,50 €". */
|