@eifi1/ui-kit 0.17.0 → 0.19.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 +83 -4
- package/dist/chart.d.ts +13 -1
- package/dist/components/account-chips.d.ts +153 -0
- package/dist/components/account-chips.js +135 -0
- package/dist/components/account-chips.js.map +1 -0
- package/dist/components/account-settings-labels.d.ts +10 -2
- package/dist/components/account-settings-labels.js +3 -0
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -1
- package/dist/components/button-group.d.ts +13 -1
- package/dist/components/calculator.d.ts +13 -1
- package/dist/components/chip.d.ts +25 -1
- package/dist/components/chip.js +143 -6
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +42 -1
- package/dist/components/confirm-dialog.js +36 -5
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +13 -1
- package/dist/components/danger-confirm.d.ts +55 -4
- package/dist/components/danger-confirm.js +67 -30
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/facing-pair.d.ts +13 -1
- package/dist/components/file-button.d.ts +13 -1
- package/dist/components/file-dropzone.d.ts +13 -1
- package/dist/components/floating-panel.d.ts +4 -1
- package/dist/components/floating-panel.js +15 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +13 -1
- package/dist/components/form-actions.js +3 -0
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/legal.d.ts +84 -0
- package/dist/components/legal.js +55 -0
- package/dist/components/legal.js.map +1 -0
- package/dist/components/money-field.d.ts +13 -1
- package/dist/components/number-field.d.ts +14 -2
- package/dist/components/number-field.js +23 -22
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +13 -1
- package/dist/components/numpad-sheet.d.ts +13 -1
- package/dist/components/passkeys-setting.d.ts +27 -1
- package/dist/components/passkeys-setting.js +31 -5
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +15 -1
- package/dist/components/progress-bar.js +8 -1
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +90 -0
- package/dist/components/reauth-dialog.js +158 -0
- package/dist/components/reauth-dialog.js.map +1 -0
- package/dist/components/series-chart.d.ts +13 -1
- package/dist/components/server-wake.d.ts +60 -0
- package/dist/components/server-wake.js +79 -0
- package/dist/components/server-wake.js.map +1 -0
- package/dist/components/settings-fields.d.ts +13 -1
- package/dist/components/share-card.d.ts +64 -0
- package/dist/components/share-card.js +350 -0
- package/dist/components/share-card.js.map +1 -0
- package/dist/components/status-dot.d.ts +2 -0
- package/dist/components/text-link.d.ts +13 -1
- package/dist/components/time-input.d.ts +13 -1
- package/dist/components/toast.d.ts +10 -1
- package/dist/components/toast.js +42 -2
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +5 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +56 -0
- package/dist/components/translation-review-editor.js +159 -0
- package/dist/components/translation-review-editor.js.map +1 -0
- package/dist/components/translation-review-labels.d.ts +92 -0
- package/dist/components/translation-review-labels.js +67 -0
- package/dist/components/translation-review-labels.js.map +1 -0
- package/dist/components/translation-review-parts.d.ts +97 -0
- package/dist/components/translation-review-parts.js +163 -0
- package/dist/components/translation-review-parts.js.map +1 -0
- package/dist/components/translation-review.d.ts +94 -0
- package/dist/components/translation-review.js +344 -0
- package/dist/components/translation-review.js.map +1 -0
- package/dist/components/ui.d.ts +13 -1
- package/dist/components/ui.js +43 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/user-avatar.d.ts +2 -0
- package/dist/components/write-lock.d.ts +68 -0
- package/dist/components/write-lock.js +33 -0
- package/dist/components/write-lock.js.map +1 -0
- package/dist/{data-table-CpkWUJBY.d.ts → data-table-3mi2R3so.d.ts} +19 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +13 -1
- package/dist/hooks/use-overlay-history.js +16 -0
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +13 -1
- package/dist/i18n/defaults.js +15 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +81 -0
- package/dist/i18n/{locales/de.js → german.js} +110 -9
- package/dist/i18n/german.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +13 -1
- package/dist/i18n/kit-labels.js +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +243 -0
- package/dist/i18n/languages.js +70 -0
- package/dist/i18n/languages.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +26 -8
- package/dist/i18n/locales/de-CH.js +7 -3
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/en.d.ts +13 -1
- package/dist/i18n/locales/en.js +8 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +13 -1
- package/dist/i18n/locales/es.js +127 -27
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +13 -1
- package/dist/i18n/locales/fr.js +101 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +13 -1
- package/dist/i18n/locales/hu.js +105 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +13 -1
- package/dist/i18n/locales/it.js +134 -34
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +13 -1
- package/dist/i18n/locales/zh.js +101 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +102 -0
- package/dist/i18n/review.js +158 -0
- package/dist/i18n/review.js.map +1 -0
- package/dist/i18n/swiss.d.ts +3 -3
- package/dist/i18n/swiss.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +97 -1
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-C4uB_zmg.d.ts → kit-labels-D1J2_jyD.d.ts} +236 -9
- package/dist/lib/floating-stack.d.ts +31 -0
- package/dist/lib/floating-stack.js +42 -0
- package/dist/lib/floating-stack.js.map +1 -0
- package/dist/lib/server-wake.d.ts +146 -0
- package/dist/lib/server-wake.js +122 -0
- package/dist/lib/server-wake.js.map +1 -0
- package/dist/lib/translation-review.d.ts +232 -0
- package/dist/lib/translation-review.js +194 -0
- package/dist/lib/translation-review.js.map +1 -0
- package/dist/rhf/fields.d.ts +13 -1
- package/dist/rhf/form.d.ts +13 -1
- package/dist/rhf.d.ts +13 -1
- package/dist/search/global-search.d.ts +3 -1
- package/dist/search/global-search.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/top-bar-brand.d.ts +13 -1
- package/dist/shell/topbar-action-menu.d.ts +2 -0
- package/dist/shell.d.ts +13 -1
- package/dist/wizard/stepper-nav.d.ts +13 -1
- package/dist/wizard.d.ts +13 -1
- package/package.json +1 -1
- package/src/components/account-chips.tsx +302 -0
- package/src/components/account-settings-labels.ts +16 -2
- package/src/components/chip.tsx +239 -6
- package/src/components/confirm-dialog.tsx +82 -7
- package/src/components/danger-confirm.tsx +118 -33
- package/src/components/data-table.tsx +20 -0
- package/src/components/floating-panel.tsx +19 -1
- package/src/components/form-actions.tsx +22 -3
- package/src/components/legal.tsx +159 -0
- package/src/components/number-field.tsx +32 -26
- package/src/components/passkeys-setting.tsx +68 -7
- package/src/components/progress-bar.tsx +21 -1
- package/src/components/reauth-dialog.tsx +273 -0
- package/src/components/server-wake.tsx +156 -0
- package/src/components/share-card.tsx +615 -0
- package/src/components/toast.tsx +72 -2
- package/src/components/toggle-group.tsx +5 -2
- package/src/components/translation-review-editor.tsx +261 -0
- package/src/components/translation-review-labels.ts +152 -0
- package/src/components/translation-review-parts.tsx +269 -0
- package/src/components/translation-review.tsx +502 -0
- package/src/components/ui.tsx +89 -32
- package/src/components/write-lock.tsx +96 -0
- package/src/hooks/use-overlay-history.ts +51 -0
- package/src/i18n/defaults.ts +14 -0
- package/src/i18n/{locales/de.ts → german.ts} +120 -13
- package/src/i18n/kit-labels.tsx +29 -2
- package/src/i18n/languages.ts +285 -0
- package/src/i18n/locales/de-CH.ts +17 -8
- package/src/i18n/locales/en.ts +8 -0
- package/src/i18n/locales/es.ts +128 -26
- package/src/i18n/locales/fr.ts +103 -0
- package/src/i18n/locales/hu.ts +107 -0
- package/src/i18n/locales/it.ts +135 -33
- package/src/i18n/locales/zh.ts +102 -0
- package/src/i18n/review.ts +193 -0
- package/src/i18n/swiss.ts +3 -3
- package/src/index.ts +86 -1
- package/src/lib/floating-stack.ts +66 -0
- package/src/lib/server-wake.ts +291 -0
- package/src/lib/translation-review.ts +465 -0
- package/src/search/global-search.tsx +3 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
- package/dist/i18n/locales/de-CH-informal.js +0 -8
- package/dist/i18n/locales/de-CH-informal.js.map +0 -1
- package/dist/i18n/locales/de-informal.d.ts +0 -82
- package/dist/i18n/locales/de-informal.js +0 -37
- package/dist/i18n/locales/de-informal.js.map +0 -1
- package/dist/i18n/locales/de.d.ts +0 -66
- package/dist/i18n/locales/de.js.map +0 -1
- package/src/i18n/locales/de-CH-informal.ts +0 -10
- package/src/i18n/locales/de-informal.ts +0 -61
package/src/components/toast.tsx
CHANGED
|
@@ -135,9 +135,33 @@ type Kind = "default" | "success" | "error" | "warning" | "info" | "loading";
|
|
|
135
135
|
|
|
136
136
|
const privately = (node: ReactNode) => <span data-private="">{node}</span>;
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The toast's id, written onto its `<li>` as a class — the one per-toast attribute
|
|
140
|
+
* sonner passes through untouched besides `testId`, which is the app's (sonner keeps the
|
|
141
|
+
* id in React state only). `<Toaster dismissOnMiddleClick>` reads it back to dismiss
|
|
142
|
+
* the toast under the pointer, close button or not. The type is kept (`s`/`n`) because
|
|
143
|
+
* sonner matches ids with `===`, and the value is URI-encoded so any string id is one
|
|
144
|
+
* whitespace-free class token.
|
|
145
|
+
*/
|
|
146
|
+
const ID_CLASS = "kit-toast-id-";
|
|
147
|
+
const idClass = (id: ToastId) =>
|
|
148
|
+
`${ID_CLASS}${typeof id === "number" ? "n" : "s"}-${encodeURIComponent(String(id))}`;
|
|
149
|
+
|
|
150
|
+
/** The id {@link idClass} stamped on a toast element, or null for a toast the kit did
|
|
151
|
+
* not make (an app still calling sonner directly). */
|
|
152
|
+
function readToastId(el: Element): ToastId | null {
|
|
153
|
+
for (const token of el.classList) {
|
|
154
|
+
if (!token.startsWith(ID_CLASS)) continue;
|
|
155
|
+
const kind = token.charAt(ID_CLASS.length);
|
|
156
|
+
const raw = decodeURIComponent(token.slice(ID_CLASS.length + 2));
|
|
157
|
+
return kind === "n" ? Number(raw) : raw;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
138
162
|
function toSonnerData(opts: ToastOptions | undefined, id: ToastId): SonnerData {
|
|
139
|
-
const { redact, description, duration, action, ...rest } = opts ?? {};
|
|
140
|
-
const data: SonnerData = { ...rest, id };
|
|
163
|
+
const { redact, description, duration, action, className, ...rest } = opts ?? {};
|
|
164
|
+
const data: SonnerData = { ...rest, id, className: className ? `${idClass(id)} ${className}` : idClass(id) };
|
|
141
165
|
if (description !== undefined) data.description = redact ? privately(description) : description;
|
|
142
166
|
if (action) data.action = action;
|
|
143
167
|
const resolved = duration ?? (action ? TOAST_ACTION_DURATION : undefined);
|
|
@@ -312,6 +336,15 @@ export interface ToasterProps {
|
|
|
312
336
|
hotkey?: string[];
|
|
313
337
|
gap?: number;
|
|
314
338
|
dir?: "ltr" | "rtl" | "auto";
|
|
339
|
+
/**
|
|
340
|
+
* A middle click on a toast dismisses it (0.18.0) — the gesture that closes a tab,
|
|
341
|
+
* which is what keksdose's users reached for (dev #583). By id, so it works on a
|
|
342
|
+
* toast with `closeButton: false` too; it counts as the user closing it, so
|
|
343
|
+
* `onDismiss` runs. Not on a link or a button inside the toast — a middle click on a
|
|
344
|
+
* link still opens it in a new tab — and not on a `dismissible: false` toast.
|
|
345
|
+
* Default true.
|
|
346
|
+
*/
|
|
347
|
+
dismissOnMiddleClick?: boolean;
|
|
315
348
|
className?: string;
|
|
316
349
|
style?: CSSProperties;
|
|
317
350
|
}
|
|
@@ -385,6 +418,7 @@ export function Toaster({
|
|
|
385
418
|
swipeDirections = ALL_DIRECTIONS,
|
|
386
419
|
offset,
|
|
387
420
|
mobileOffset,
|
|
421
|
+
dismissOnMiddleClick = true,
|
|
388
422
|
className,
|
|
389
423
|
style,
|
|
390
424
|
...rest
|
|
@@ -409,6 +443,42 @@ export function Toaster({
|
|
|
409
443
|
};
|
|
410
444
|
}, [labels]);
|
|
411
445
|
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (!dismissOnMiddleClick || !mod) return;
|
|
448
|
+
// On the document rather than the toaster: sonner owns the `<ol>`, and a listener
|
|
449
|
+
// there would have to be re-attached whenever it remounts it.
|
|
450
|
+
const toastUnder = (e: globalThis.MouseEvent) => {
|
|
451
|
+
if (e.button !== 1 || !(e.target instanceof Element)) return null;
|
|
452
|
+
const li = e.target.closest<HTMLElement>("[data-sonner-toast]");
|
|
453
|
+
if (!li || li.dataset.dismissible === "false") return null;
|
|
454
|
+
// A link keeps its middle click (open in a new tab); a button or a field inside
|
|
455
|
+
// the toast is the app's, not a background to click.
|
|
456
|
+
const control = e.target.closest("a[href], button, input, select, textarea, [role='button'], [role='link']");
|
|
457
|
+
if (control && li.contains(control)) return null;
|
|
458
|
+
return li;
|
|
459
|
+
};
|
|
460
|
+
// `mousedown`: the middle button starts autoscroll (Windows) before any click.
|
|
461
|
+
const onDown = (e: globalThis.MouseEvent) => {
|
|
462
|
+
if (toastUnder(e)) e.preventDefault();
|
|
463
|
+
};
|
|
464
|
+
const onAux = (e: globalThis.MouseEvent) => {
|
|
465
|
+
const li = toastUnder(e);
|
|
466
|
+
if (!li) return;
|
|
467
|
+
// No paste of the primary selection on Linux, no other handler's idea of it.
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
const id = readToastId(li);
|
|
470
|
+
if (id !== null) mod.toast.dismiss(id);
|
|
471
|
+
// A toast made by sonner directly carries no id: its close button, if any.
|
|
472
|
+
else li.querySelector<HTMLButtonElement>("[data-close-button]")?.click();
|
|
473
|
+
};
|
|
474
|
+
document.addEventListener("mousedown", onDown);
|
|
475
|
+
document.addEventListener("auxclick", onAux);
|
|
476
|
+
return () => {
|
|
477
|
+
document.removeEventListener("mousedown", onDown);
|
|
478
|
+
document.removeEventListener("auxclick", onAux);
|
|
479
|
+
};
|
|
480
|
+
}, [dismissOnMiddleClick, mod]);
|
|
481
|
+
|
|
412
482
|
const documentTheme = useSyncExternalStore(subscribeDocumentTheme, readDocumentTheme, () => "light" as const);
|
|
413
483
|
const phone = useMediaQuery(PHONE_QUERY, false);
|
|
414
484
|
const resolved: ToastPosition = position ?? (phone ? "bottom-center" : "top-center");
|
|
@@ -340,8 +340,11 @@ export function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): React
|
|
|
340
340
|
? "bg-[var(--bg-inverse)] text-[var(--text-inverse)]"
|
|
341
341
|
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
|
|
342
342
|
// No hover fill on a group that cannot be changed — a segment that
|
|
343
|
-
// lights up under the pointer is an offer, and there is none here.
|
|
344
|
-
|
|
343
|
+
// lights up under the pointer is an offer, and there is none here. Not on
|
|
344
|
+
// the chosen segment: its fill IS its state, and its inverse text would
|
|
345
|
+
// vanish on a transparent hover.
|
|
346
|
+
disabled && "cursor-not-allowed",
|
|
347
|
+
disabled && !active && "hover:bg-transparent dark:hover:bg-transparent",
|
|
345
348
|
optionClassName,
|
|
346
349
|
opt.className,
|
|
347
350
|
)}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { useKitFormat } from "../lib/format";
|
|
7
|
+
import { placeholderTokens, reviewWrite } from "../lib/translation-review";
|
|
8
|
+
import type {
|
|
9
|
+
TranslationReviewKey,
|
|
10
|
+
TranslationReviewWrite,
|
|
11
|
+
TranslationRow,
|
|
12
|
+
TranslationVerdict,
|
|
13
|
+
} from "../lib/translation-review";
|
|
14
|
+
import { AlertBanner } from "./alert-banner";
|
|
15
|
+
import { Caption, SectionLabel } from "./text";
|
|
16
|
+
import { DEFAULT_TRANSLATION_REVIEW_LABELS } from "./translation-review-labels";
|
|
17
|
+
import type { TranslationReviewLabels } from "./translation-review-labels";
|
|
18
|
+
import { Button, Textarea } from "./ui";
|
|
19
|
+
|
|
20
|
+
/** A callback may return a promise: resolve and the editor closes, reject and it stays
|
|
21
|
+
* open with the error. */
|
|
22
|
+
type MaybePromise = void | Promise<unknown>;
|
|
23
|
+
|
|
24
|
+
export interface TranslationReviewEditorProps {
|
|
25
|
+
row: TranslationRow;
|
|
26
|
+
/** The reference language's own name ("English", "Deutsch") — the app's choice of
|
|
27
|
+
* reference, so the app's word for it. */
|
|
28
|
+
referenceLabel: ReactNode;
|
|
29
|
+
/** The reviewed language's own name ("Français"). */
|
|
30
|
+
localeLabel: ReactNode;
|
|
31
|
+
/** Store a verdict. Left out (or `readOnly`): the editor shows the row and its last
|
|
32
|
+
* verdict, with no fields and no actions. */
|
|
33
|
+
onSave?: (write: TranslationReviewWrite) => MaybePromise;
|
|
34
|
+
/** Clear the verdict — back to unreviewed. Left out: no reset button. */
|
|
35
|
+
onClear?: (key: TranslationReviewKey) => MaybePromise;
|
|
36
|
+
/** The Cancel button — and called after a save or a reset resolves, so the panel
|
|
37
|
+
* that opened the editor closes it. */
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
/** Look, do not touch: a locale the viewer may read but not review. */
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
/** How the verdict's date reads. Default: the kit's `formatDate`, medium, in the
|
|
42
|
+
* provider's locale. */
|
|
43
|
+
formatDate?: (iso: string) => string;
|
|
44
|
+
/** Turns a rejection into words. Default {@link TranslationReviewLabels.failed}. */
|
|
45
|
+
formatError?: (error: unknown) => ReactNode;
|
|
46
|
+
className?: string;
|
|
47
|
+
labels?: Partial<TranslationReviewLabels>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The wording a verdict was given on, under the line that says it changed. */
|
|
51
|
+
function Then({ label, text }: { label: string; text: string }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="mt-1.5 min-w-0">
|
|
54
|
+
<span className="text-xs font-medium">{label}</span>
|
|
55
|
+
<p className="whitespace-pre-line break-words text-[var(--text-secondary)]">{text}</p>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One string, opened: the reference beside the translation, what changed since the last
|
|
62
|
+
* verdict, what a machine can see is wrong (the placeholders), and the two things a
|
|
63
|
+
* reviewer can say about it — keksdose's and kastlan's `translation-review-editor.tsx`.
|
|
64
|
+
*
|
|
65
|
+
* "Better wording" opens on the CURRENT text rather than empty, because the usual
|
|
66
|
+
* correction is a word or an ending, and retyping a sentence to change one word is how
|
|
67
|
+
* new mistakes get in. It is sent only with "Needs a change" — approving says the text is
|
|
68
|
+
* fine as it stands. A "Needs a change" with neither a note nor a different wording tells
|
|
69
|
+
* whoever fixes it nothing, so it takes one of the two.
|
|
70
|
+
*
|
|
71
|
+
* A MISSING string (kastlan: the locale lacks the key) cannot be approved — there is
|
|
72
|
+
* nothing to approve. Its field starts empty and is the translation to add; the verdict
|
|
73
|
+
* is NEEDS_CHANGE on the empty text, which the export hands a developer as the string to
|
|
74
|
+
* add.
|
|
75
|
+
*
|
|
76
|
+
* Every action waits for its callback: the buttons hold while the promise is out, a
|
|
77
|
+
* resolution closes the editor through `onClose`, and a rejection keeps it open with what
|
|
78
|
+
* was typed and the error under the buttons.
|
|
79
|
+
*/
|
|
80
|
+
export function TranslationReviewEditor({
|
|
81
|
+
row,
|
|
82
|
+
referenceLabel,
|
|
83
|
+
localeLabel,
|
|
84
|
+
onSave,
|
|
85
|
+
onClear,
|
|
86
|
+
onClose,
|
|
87
|
+
readOnly = false,
|
|
88
|
+
formatDate: formatDateProp,
|
|
89
|
+
formatError,
|
|
90
|
+
className,
|
|
91
|
+
labels: labelsProp,
|
|
92
|
+
}: TranslationReviewEditorProps) {
|
|
93
|
+
const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
|
|
94
|
+
const { formatDate } = useKitFormat();
|
|
95
|
+
const review = row.review;
|
|
96
|
+
const missing = row.text === "";
|
|
97
|
+
const editable = !readOnly && onSave !== undefined;
|
|
98
|
+
const [suggestion, setSuggestion] = useState(review?.suggestion ?? row.text);
|
|
99
|
+
const [note, setNote] = useState(review?.note ?? "");
|
|
100
|
+
const [busy, setBusy] = useState<"approve" | "flag" | "clear" | null>(null);
|
|
101
|
+
const [failure, setFailure] = useState<ReactNode>(null);
|
|
102
|
+
|
|
103
|
+
const canFlag = note.trim() !== "" || suggestion.trim() !== row.text.trim();
|
|
104
|
+
const changed = row.status === "changed" ? review : null;
|
|
105
|
+
const date = (iso: string) => (formatDateProp ? formatDateProp(iso) : formatDate(iso, "medium"));
|
|
106
|
+
|
|
107
|
+
const run = async (kind: "approve" | "flag" | "clear", action: () => MaybePromise) => {
|
|
108
|
+
setBusy(kind);
|
|
109
|
+
setFailure(null);
|
|
110
|
+
try {
|
|
111
|
+
await action();
|
|
112
|
+
onClose?.();
|
|
113
|
+
} catch (caught) {
|
|
114
|
+
setFailure(formatError ? formatError(caught) : labels.failed);
|
|
115
|
+
} finally {
|
|
116
|
+
setBusy(null);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const save = (verdict: TranslationVerdict) => {
|
|
121
|
+
if (!onSave) return;
|
|
122
|
+
const trimmedNote = note.trim() || null;
|
|
123
|
+
// Approving says the text is right as it stands: there is no wording to suggest.
|
|
124
|
+
const sent = verdict === "APPROVED" ? null : suggestion.trim() || null;
|
|
125
|
+
void run(verdict === "APPROVED" ? "approve" : "flag", () =>
|
|
126
|
+
onSave(reviewWrite(row, verdict, trimmedNote, sent)),
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className={cn("min-w-0 space-y-3 text-sm", className)}>
|
|
132
|
+
<code className="block break-all text-xs text-[var(--text-muted)]">{row.key}</code>
|
|
133
|
+
|
|
134
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
135
|
+
<div className="min-w-0">
|
|
136
|
+
<SectionLabel as="span" size="xs">
|
|
137
|
+
{referenceLabel}
|
|
138
|
+
</SectionLabel>
|
|
139
|
+
<p className="mt-1 whitespace-pre-line break-words text-[var(--text-secondary)]">{row.reference}</p>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="min-w-0">
|
|
142
|
+
<SectionLabel as="span" size="xs">
|
|
143
|
+
{localeLabel}
|
|
144
|
+
</SectionLabel>
|
|
145
|
+
{missing ? (
|
|
146
|
+
<p className="mt-1 italic text-[var(--text-muted)]">{labels.missingText}</p>
|
|
147
|
+
) : (
|
|
148
|
+
<p lang={row.locale} className="mt-1 whitespace-pre-line break-words">
|
|
149
|
+
{row.text}
|
|
150
|
+
</p>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{changed && (
|
|
156
|
+
<AlertBanner tone="warning" variant="inline" size="sm" block live={false}>
|
|
157
|
+
<div className="min-w-0">
|
|
158
|
+
<p>{labels.changedSince}</p>
|
|
159
|
+
{changed.text !== row.text && <Then label={labels.reviewedWording} text={changed.text || "—"} />}
|
|
160
|
+
{changed.referenceText != null && changed.referenceText !== row.reference && (
|
|
161
|
+
<Then label={labels.reviewedReference} text={changed.referenceText} />
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
</AlertBanner>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{row.placeholderMismatch && (
|
|
168
|
+
<AlertBanner tone="danger" variant="inline" size="sm" block live={false}>
|
|
169
|
+
<span className="min-w-0 break-words">
|
|
170
|
+
{labels.placeholderMismatch(
|
|
171
|
+
placeholderTokens(row.reference).join(" ") || "—",
|
|
172
|
+
placeholderTokens(row.text).join(" ") || "—",
|
|
173
|
+
)}
|
|
174
|
+
</span>
|
|
175
|
+
</AlertBanner>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{review && (
|
|
179
|
+
<Caption>
|
|
180
|
+
{(review.verdict === "APPROVED" ? labels.lastApproved : labels.lastFlagged)(
|
|
181
|
+
review.reviewerName ?? labels.erasedReviewer,
|
|
182
|
+
date(review.reviewedAt),
|
|
183
|
+
)}
|
|
184
|
+
</Caption>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Read-only: what the reviewer said, as text — the fields are for saying it. */}
|
|
188
|
+
{!editable && review?.verdict === "NEEDS_CHANGE" && (review.suggestion || review.note) && (
|
|
189
|
+
<div className="space-y-2">
|
|
190
|
+
{review.suggestion && <Then label={missing ? labels.translation : labels.suggestion} text={review.suggestion} />}
|
|
191
|
+
{review.note && <Then label={labels.note} text={review.note} />}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{editable && (
|
|
196
|
+
<>
|
|
197
|
+
<Textarea
|
|
198
|
+
label={missing ? labels.translation : labels.suggestion}
|
|
199
|
+
lang={row.locale}
|
|
200
|
+
rows={Math.min(8, Math.max(2, Math.ceil(Math.max(row.text.length, row.reference.length) / 80)))}
|
|
201
|
+
value={suggestion}
|
|
202
|
+
disabled={busy !== null}
|
|
203
|
+
onChange={(e) => setSuggestion(e.target.value)}
|
|
204
|
+
/>
|
|
205
|
+
<Textarea
|
|
206
|
+
label={labels.note}
|
|
207
|
+
rows={2}
|
|
208
|
+
value={note}
|
|
209
|
+
placeholder={labels.notePlaceholder}
|
|
210
|
+
disabled={busy !== null}
|
|
211
|
+
onChange={(e) => setNote(e.target.value)}
|
|
212
|
+
/>
|
|
213
|
+
<div className="flex flex-wrap gap-2">
|
|
214
|
+
{!missing && (
|
|
215
|
+
<Button
|
|
216
|
+
variant="primary"
|
|
217
|
+
commit
|
|
218
|
+
pending={busy === "approve"}
|
|
219
|
+
disabled={busy !== null}
|
|
220
|
+
onClick={() => save("APPROVED")}
|
|
221
|
+
>
|
|
222
|
+
{labels.approve}
|
|
223
|
+
</Button>
|
|
224
|
+
)}
|
|
225
|
+
<Button
|
|
226
|
+
variant={missing ? "primary" : "danger"}
|
|
227
|
+
commit
|
|
228
|
+
pending={busy === "flag"}
|
|
229
|
+
disabled={busy !== null || !canFlag}
|
|
230
|
+
onClick={() => save("NEEDS_CHANGE")}
|
|
231
|
+
>
|
|
232
|
+
{missing ? labels.suggest : labels.flag}
|
|
233
|
+
</Button>
|
|
234
|
+
{review && onClear && (
|
|
235
|
+
<Button
|
|
236
|
+
variant="ghost"
|
|
237
|
+
commit
|
|
238
|
+
pending={busy === "clear"}
|
|
239
|
+
disabled={busy !== null}
|
|
240
|
+
onClick={() => void run("clear", () => onClear({ locale: row.locale, key: row.key }))}
|
|
241
|
+
>
|
|
242
|
+
{labels.reset}
|
|
243
|
+
</Button>
|
|
244
|
+
)}
|
|
245
|
+
{onClose && (
|
|
246
|
+
<Button variant="ghost" disabled={busy !== null} onClick={onClose}>
|
|
247
|
+
{labels.cancel}
|
|
248
|
+
</Button>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{failure && (
|
|
255
|
+
<AlertBanner tone="danger" size="sm" role="alert">
|
|
256
|
+
{failure}
|
|
257
|
+
</AlertBanner>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { ReviewStatus } from "../lib/translation-review";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `translationReview` namespace: every word of the translation-review parts
|
|
5
|
+
* (`ReviewStatusChip`, `TranslationProgress`, `TranslationLocaleTabs`,
|
|
6
|
+
* `TranslationExportButton`, `TranslationReviewEditor`, `TranslationReviewPanel`).
|
|
7
|
+
*
|
|
8
|
+
* Its own module, like `data-table-labels.ts`: `kit-labels.tsx` and `defaults.ts` import
|
|
9
|
+
* it, and neither has to load the components to get the words. The English is keksdose's
|
|
10
|
+
* and kastlan's own (`translation_review.*`, `translations:*`), British.
|
|
11
|
+
*/
|
|
12
|
+
export interface TranslationReviewLabels {
|
|
13
|
+
/** The five statuses — the chip, the filter, the progress legend. */
|
|
14
|
+
statusMissing: string;
|
|
15
|
+
statusUnreviewed: string;
|
|
16
|
+
statusChanged: string;
|
|
17
|
+
statusNeedsChange: string;
|
|
18
|
+
statusApproved: string;
|
|
19
|
+
/** Accessible name of the status filter. */
|
|
20
|
+
statusFilter: string;
|
|
21
|
+
/** The status filter's first option. */
|
|
22
|
+
all: string;
|
|
23
|
+
/** A status filter option with its count: "Unreviewed · 12". */
|
|
24
|
+
filterCount: (label: string, count: number) => string;
|
|
25
|
+
/** The source filter's label — shown only when the rows come from several sources
|
|
26
|
+
* (kastlan's screen texts and documents). */
|
|
27
|
+
source: string;
|
|
28
|
+
allSources: string;
|
|
29
|
+
/** The namespace filter's label and its "every one" option. */
|
|
30
|
+
namespace: string;
|
|
31
|
+
allNamespaces: string;
|
|
32
|
+
search: string;
|
|
33
|
+
/** The placeholder filter, with how many rows it would show. */
|
|
34
|
+
placeholdersOnly: (count: number) => string;
|
|
35
|
+
/** The chip on a row whose placeholders differ from the reference's. */
|
|
36
|
+
placeholderChip: string;
|
|
37
|
+
/** The key column, and the key's caption in the editor. */
|
|
38
|
+
key: string;
|
|
39
|
+
statusColumn: string;
|
|
40
|
+
/** No row matches the filters. */
|
|
41
|
+
empty: string;
|
|
42
|
+
/** In place of the text of a string the locale lacks. */
|
|
43
|
+
missingText: string;
|
|
44
|
+
/** The progress bar's label. */
|
|
45
|
+
progress: (approved: number, total: number) => string;
|
|
46
|
+
/** A locale tab's badge. */
|
|
47
|
+
localeProgress: (approved: number, total: number) => string;
|
|
48
|
+
/** Accessible name of the locale tabs. */
|
|
49
|
+
locales: string;
|
|
50
|
+
approve: string;
|
|
51
|
+
/** Send a string back: "Needs a change". */
|
|
52
|
+
flag: string;
|
|
53
|
+
/** "Needs a change" for a MISSING string: the suggestion is the translation to add. */
|
|
54
|
+
suggest: string;
|
|
55
|
+
/** Clear a verdict: back to unreviewed. */
|
|
56
|
+
reset: string;
|
|
57
|
+
/** The editor's way out without saving. */
|
|
58
|
+
cancel: string;
|
|
59
|
+
approveSelected: string;
|
|
60
|
+
resetSelected: string;
|
|
61
|
+
/** Selects every row the filters show — the phone's way into the bulk actions, where
|
|
62
|
+
* the table has no checkboxes. */
|
|
63
|
+
selectShown: string;
|
|
64
|
+
/** Over the old wording of a row that changed since its verdict. */
|
|
65
|
+
changedSince: string;
|
|
66
|
+
reviewedWording: string;
|
|
67
|
+
reviewedReference: string;
|
|
68
|
+
/** The placeholders differ: the reference's and the text's, each a list. */
|
|
69
|
+
placeholderMismatch: (reference: string, text: string) => string;
|
|
70
|
+
lastApproved: (name: string, date: string) => string;
|
|
71
|
+
lastFlagged: (name: string, date: string) => string;
|
|
72
|
+
/** The reviewer's name once the account is gone — what `name` above becomes. */
|
|
73
|
+
erasedReviewer: string;
|
|
74
|
+
/** The editor's wording field, opened on the current text. */
|
|
75
|
+
suggestion: string;
|
|
76
|
+
/** The same field for a missing string. */
|
|
77
|
+
translation: string;
|
|
78
|
+
note: string;
|
|
79
|
+
notePlaceholder: string;
|
|
80
|
+
/** The viewer may read this locale but not review it. */
|
|
81
|
+
readOnly: string;
|
|
82
|
+
/** The reviewer is limited to some areas — `areas` is their names, joined. */
|
|
83
|
+
scope: (areas: string) => string;
|
|
84
|
+
exportCorrections: (count: number) => string;
|
|
85
|
+
/** A callback rejected and no `formatError` says better. */
|
|
86
|
+
failed: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {
|
|
90
|
+
statusMissing: "Missing",
|
|
91
|
+
statusUnreviewed: "Unreviewed",
|
|
92
|
+
statusChanged: "Changed since review",
|
|
93
|
+
statusNeedsChange: "Needs a change",
|
|
94
|
+
statusApproved: "Approved",
|
|
95
|
+
statusFilter: "Status",
|
|
96
|
+
all: "All",
|
|
97
|
+
filterCount: (label, count) => `${label} · ${count}`,
|
|
98
|
+
source: "Texts",
|
|
99
|
+
allSources: "All texts",
|
|
100
|
+
namespace: "Area",
|
|
101
|
+
allNamespaces: "All areas",
|
|
102
|
+
search: "Search keys and text",
|
|
103
|
+
placeholdersOnly: (count) => `Only placeholder problems (${count})`,
|
|
104
|
+
placeholderChip: "Placeholders",
|
|
105
|
+
key: "Key",
|
|
106
|
+
statusColumn: "Status",
|
|
107
|
+
empty: "No strings match these filters.",
|
|
108
|
+
missingText: "Missing in this language",
|
|
109
|
+
progress: (approved, total) => `${approved} of ${total} approved`,
|
|
110
|
+
localeProgress: (approved, total) => `${approved}/${total}`,
|
|
111
|
+
locales: "Languages",
|
|
112
|
+
approve: "Approve",
|
|
113
|
+
flag: "Needs a change",
|
|
114
|
+
suggest: "Suggest translation",
|
|
115
|
+
reset: "Mark unreviewed",
|
|
116
|
+
cancel: "Cancel",
|
|
117
|
+
approveSelected: "Approve selected",
|
|
118
|
+
resetSelected: "Mark selected unreviewed",
|
|
119
|
+
selectShown: "Select all shown",
|
|
120
|
+
changedSince: "This string changed after it was reviewed. Please read it again.",
|
|
121
|
+
reviewedWording: "Wording when reviewed",
|
|
122
|
+
reviewedReference: "Reference when reviewed",
|
|
123
|
+
placeholderMismatch: (reference, text) =>
|
|
124
|
+
`The placeholders differ from the reference — reference: ${reference}; this text: ${text}. The app fills these in, so they must stay exactly as they are.`,
|
|
125
|
+
lastApproved: (name, date) => `Approved by ${name} on ${date}`,
|
|
126
|
+
lastFlagged: (name, date) => `Marked as needing a change by ${name} on ${date}`,
|
|
127
|
+
erasedReviewer: "a deleted account",
|
|
128
|
+
suggestion: "Better wording",
|
|
129
|
+
translation: "Translation",
|
|
130
|
+
note: "Note (optional)",
|
|
131
|
+
notePlaceholder: "What is wrong, or what to keep in mind",
|
|
132
|
+
readOnly: "You can read this language but not review it.",
|
|
133
|
+
scope: (areas) => `Your review is limited to: ${areas}.`,
|
|
134
|
+
exportCorrections: (count) => `Export corrections (${count})`,
|
|
135
|
+
failed: "That did not work. Please try again.",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** A status's word, from the namespace's five. */
|
|
139
|
+
export function reviewStatusLabel(labels: TranslationReviewLabels, status: ReviewStatus): string {
|
|
140
|
+
switch (status) {
|
|
141
|
+
case "missing":
|
|
142
|
+
return labels.statusMissing;
|
|
143
|
+
case "unreviewed":
|
|
144
|
+
return labels.statusUnreviewed;
|
|
145
|
+
case "changed":
|
|
146
|
+
return labels.statusChanged;
|
|
147
|
+
case "needs_change":
|
|
148
|
+
return labels.statusNeedsChange;
|
|
149
|
+
case "approved":
|
|
150
|
+
return labels.statusApproved;
|
|
151
|
+
}
|
|
152
|
+
}
|