@eifi1/ui-kit 0.25.1 → 0.26.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 +3 -3
- package/dist/components/data-table.js +3 -2
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +5 -1
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/translation-review-labels.d.ts +4 -0
- package/dist/components/translation-review-labels.js +2 -1
- package/dist/components/translation-review-labels.js.map +1 -1
- package/dist/components/translation-review.d.ts +27 -3
- package/dist/components/translation-review.js +50 -21
- package/dist/components/translation-review.js.map +1 -1
- package/dist/i18n/german.js +1 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +1 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.js +1 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +1 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +1 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +1 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +1 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +1 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/translation-review.d.ts +76 -1
- package/dist/lib/translation-review.js +30 -0
- package/dist/lib/translation-review.js.map +1 -1
- package/package.json +1 -1
- package/src/components/data-table.tsx +9 -2
- package/src/components/swipeable-row.tsx +5 -1
- package/src/components/translation-review-labels.ts +5 -0
- package/src/components/translation-review.tsx +102 -35
- package/src/i18n/german.ts +1 -0
- package/src/i18n/locales/en.ts +1 -0
- package/src/i18n/locales/es.ts +1 -0
- package/src/i18n/locales/fr.ts +1 -0
- package/src/i18n/locales/hu.ts +1 -0
- package/src/i18n/locales/it.ts +1 -0
- package/src/i18n/locales/zh.ts +1 -0
- package/src/i18n/review.ts +1 -0
- package/src/index.ts +6 -0
- package/src/lib/translation-review.ts +109 -0
- package/tokens.css +4 -0
|
@@ -7,6 +7,7 @@ import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
|
7
7
|
import { cn } from "../lib/cn";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_TRANSLATION_REVIEW_FILTER,
|
|
10
|
+
DEFAULT_TRANSLATION_REVIEW_SWIPE,
|
|
10
11
|
REVIEW_STATUSES,
|
|
11
12
|
filterTranslationRows,
|
|
12
13
|
groupTranslationRows,
|
|
@@ -14,12 +15,15 @@ import {
|
|
|
14
15
|
reviewUndo,
|
|
15
16
|
reviewWrite,
|
|
16
17
|
summariseRows,
|
|
18
|
+
translationReviewSwipePlan,
|
|
17
19
|
unreviewedRows,
|
|
18
20
|
} from "../lib/translation-review";
|
|
19
21
|
import type {
|
|
20
22
|
ReviewStatus,
|
|
21
23
|
TranslationReviewFilter,
|
|
22
24
|
TranslationReviewKey,
|
|
25
|
+
TranslationReviewSwipeAction,
|
|
26
|
+
TranslationReviewSwipeBinding,
|
|
23
27
|
TranslationReviewWrite,
|
|
24
28
|
TranslationRow,
|
|
25
29
|
TranslationRowGroup,
|
|
@@ -33,6 +37,7 @@ import type { DataTableColumn, DataTableProps, MobileSwipeActions, SortState } f
|
|
|
33
37
|
import { Disclosure } from "./disclosure";
|
|
34
38
|
import { SearchField } from "./search-field";
|
|
35
39
|
import { toast } from "./toast";
|
|
40
|
+
import type { SwipeAction } from "./swipeable-row";
|
|
36
41
|
import { ToggleGroup } from "./toggle-group";
|
|
37
42
|
import { TranslationReviewEditor } from "./translation-review-editor";
|
|
38
43
|
import { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from "./translation-review-labels";
|
|
@@ -101,7 +106,8 @@ export interface TranslationReviewSaveInfo {
|
|
|
101
106
|
/** The open editor, a row's approve button, a swipe on a phone card, a group's
|
|
102
107
|
* "Approve unreviewed", the selection's bulk bar, or an Undo pressed on the toast. */
|
|
103
108
|
origin: TranslationReviewOrigin;
|
|
104
|
-
/** The panel shows its own toast for this write (an approval's Undo toast
|
|
109
|
+
/** The panel shows its own toast for this write (an approval's Undo toast, or a
|
|
110
|
+
* swiped `clear`'s). */
|
|
105
111
|
toasted: boolean;
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -181,8 +187,31 @@ export interface TranslationReviewPanelProps {
|
|
|
181
187
|
*
|
|
182
188
|
* Off by default, so no existing page gains a gesture on the bump. Phones only; the
|
|
183
189
|
* desktop table keeps its approve button.
|
|
190
|
+
*
|
|
191
|
+
* A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *"Add
|
|
192
|
+
* the swipe options to be shown also to the settings /settings#interaction area where
|
|
193
|
+
* the other swipe options are defined"*): keksdose lets the user bind every list's
|
|
194
|
+
* swipes in Settings → Interaction, per side a primary action at the first threshold and
|
|
195
|
+
* a secondary one at a longer drag, and a fixed mapping is one its settings cannot
|
|
196
|
+
* reach. So each logical side takes an ordered ladder of
|
|
197
|
+
* {@link TranslationReviewSwipeAction}s — index 0 at the first threshold, index 1 at the
|
|
198
|
+
* longer drag, as {@link SwipeableRow} stages them:
|
|
199
|
+
*
|
|
200
|
+
* swipe={{ end: ["approve", "clear"], start: ["edit"] }}
|
|
201
|
+
*
|
|
202
|
+
* `approve` (with Undo), `edit` (the editor, cursor in the wording — "Needs a change"),
|
|
203
|
+
* `clear` (the verdict taken back, with Undo, which stores it again; only with
|
|
204
|
+
* `onClear`). An action a row cannot offer drops out and the ladder closes up behind it
|
|
205
|
+
* ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,
|
|
206
|
+
* `clear` on a row with no verdict. Every rule above still holds for every binding —
|
|
207
|
+
* no side swipes in a `readOnly` locale, under a write lock, with the editor open or
|
|
208
|
+
* the row's write out.
|
|
209
|
+
*
|
|
210
|
+
* `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: ["approve"], start:
|
|
211
|
+
* ["edit"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding
|
|
212
|
+
* turns the Undo toast on by default, as `true` does.
|
|
184
213
|
*/
|
|
185
|
-
swipe?: boolean;
|
|
214
|
+
swipe?: boolean | TranslationReviewSwipeBinding;
|
|
186
215
|
/**
|
|
187
216
|
* Answer every approval with the kit's Undo toast (`toast.undo`): a row's button, a
|
|
188
217
|
* swipe, the editor's Approve, a group's and the selection's bulk approve. Undo stores
|
|
@@ -435,7 +464,9 @@ export function TranslationReviewPanel({
|
|
|
435
464
|
const baseId = useId();
|
|
436
465
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
437
466
|
const editable = !readOnly && onSave !== undefined;
|
|
438
|
-
const
|
|
467
|
+
const binding: TranslationReviewSwipeBinding | null =
|
|
468
|
+
swipe === true ? DEFAULT_TRANSLATION_REVIEW_SWIPE : swipe || null;
|
|
469
|
+
const undoOn = undoProp ?? (binding !== null || groupBy !== undefined);
|
|
439
470
|
|
|
440
471
|
const [ownFilter, setOwnFilter] = useState<TranslationReviewFilter>(DEFAULT_TRANSLATION_REVIEW_FILTER);
|
|
441
472
|
// The app's fields over the panel's own; a field the app leaves `undefined` is the
|
|
@@ -546,10 +577,12 @@ export function TranslationReviewPanel({
|
|
|
546
577
|
};
|
|
547
578
|
|
|
548
579
|
/** "String approved — Undo", once `targets` (as they were BEFORE the write) are
|
|
549
|
-
* approved
|
|
550
|
-
|
|
580
|
+
* approved — or, after a swiped `clear`, "String marked unreviewed — Undo". Either way
|
|
581
|
+
* Undo puts back what the rows said before ({@link reviewUndo}), through the same queue
|
|
582
|
+
* as every write. */
|
|
583
|
+
const sayDone = (targets: readonly TranslationRow[], description?: ReactNode, cleared = false) => {
|
|
551
584
|
const back = reviewUndo(targets);
|
|
552
|
-
const message = labels.approvedToast(targets.length);
|
|
585
|
+
const message = (cleared ? labels.clearedToast : labels.approvedToast)(targets.length);
|
|
553
586
|
const save = onSave;
|
|
554
587
|
const clear = onClear;
|
|
555
588
|
// Taking back the approval of a row that had no verdict is a clear: without
|
|
@@ -578,16 +611,27 @@ export function TranslationReviewPanel({
|
|
|
578
611
|
if (!onSave || targets.length === 0) return false;
|
|
579
612
|
const save = onSave;
|
|
580
613
|
const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, "APPROVED")), { origin, toasted: undoOn }));
|
|
581
|
-
if (ok && undoOn)
|
|
614
|
+
if (ok && undoOn) sayDone(targets, description);
|
|
582
615
|
return ok;
|
|
583
616
|
};
|
|
584
617
|
|
|
618
|
+
/** A swipe bound to `clear`: the row's verdict taken back, answered like an approval —
|
|
619
|
+
* the Undo toast stores the verdict again. Under the row's own key, so it queues behind
|
|
620
|
+
* (and never overlaps) an approval of the same row. */
|
|
621
|
+
const clearRow = async (row: TranslationRow) => {
|
|
622
|
+
if (!onClear || row.review === null) return;
|
|
623
|
+
const clear = onClear;
|
|
624
|
+
const keys = [{ locale: row.locale, key: row.key }];
|
|
625
|
+
const ok = await run(`row:${row.id}`, () => clear(keys, { origin: "swipe", toasted: undoOn }));
|
|
626
|
+
if (ok && undoOn) sayDone([row], row.key, true);
|
|
627
|
+
};
|
|
628
|
+
|
|
585
629
|
/** The editor's own save: it waits, closes and shows its errors itself, so it does
|
|
586
630
|
* not go through the queue — but an approval there is answered like any other. */
|
|
587
631
|
const saveFromEditor = async (row: TranslationRow, write: TranslationReviewWrite) => {
|
|
588
632
|
const toasted = undoOn && write.verdict === "APPROVED";
|
|
589
633
|
await onSave?.([write], { origin: "editor", toasted });
|
|
590
|
-
if (toasted)
|
|
634
|
+
if (toasted) sayDone([row], row.key);
|
|
591
635
|
};
|
|
592
636
|
|
|
593
637
|
/**
|
|
@@ -713,29 +757,22 @@ export function TranslationReviewPanel({
|
|
|
713
757
|
});
|
|
714
758
|
}
|
|
715
759
|
|
|
716
|
-
/**
|
|
717
|
-
|
|
718
|
-
* None where the row must not be acted on — the same conditions under which its
|
|
719
|
-
* buttons are locked or absent.
|
|
720
|
-
*/
|
|
721
|
-
const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {
|
|
722
|
-
if (!editable || lock.locked || isQueued(`row:${r.id}`)) return null;
|
|
760
|
+
/** One bound swipe action on one row, as {@link SwipeableRow} draws and commits it. */
|
|
761
|
+
const swipeAction = (r: TranslationRow, id: TranslationReviewSwipeAction, groupKey?: string): SwipeAction => {
|
|
723
762
|
const missing = r.text === "";
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
start: [
|
|
738
|
-
{
|
|
763
|
+
switch (id) {
|
|
764
|
+
case "approve":
|
|
765
|
+
return {
|
|
766
|
+
label: labels.approve,
|
|
767
|
+
icon: <Check className="size-4" aria-hidden />,
|
|
768
|
+
onCommit: () => void approve(`row:${r.id}`, [r], "swipe", r.key),
|
|
769
|
+
// Text on the fill in its own contrast colour: the fills are pastels in dark
|
|
770
|
+
// mode, where SwipeableRow's default white text was unreadable (0.26).
|
|
771
|
+
className: "bg-[var(--success)] text-[var(--success-contrast)]",
|
|
772
|
+
armedClassName: "bg-[var(--success)] text-[var(--success-contrast)]",
|
|
773
|
+
};
|
|
774
|
+
case "edit":
|
|
775
|
+
return {
|
|
739
776
|
label: missing ? labels.suggest : labels.flag,
|
|
740
777
|
icon: <PencilLine className="size-4" aria-hidden />,
|
|
741
778
|
// Opens the editor rather than sending the string back: "needs a change"
|
|
@@ -745,10 +782,40 @@ export function TranslationReviewPanel({
|
|
|
745
782
|
setOpenId(r.id);
|
|
746
783
|
setFocusId(r.id);
|
|
747
784
|
},
|
|
748
|
-
className: missing
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
785
|
+
className: missing
|
|
786
|
+
? "bg-[var(--brand)] text-[var(--brand-contrast)]"
|
|
787
|
+
: "bg-[var(--danger)] text-[var(--danger-contrast)]",
|
|
788
|
+
armedClassName: missing
|
|
789
|
+
? "bg-[var(--brand-hover)] text-[var(--brand-contrast)]"
|
|
790
|
+
: "bg-[var(--danger-hover)] text-[var(--danger-contrast)]",
|
|
791
|
+
};
|
|
792
|
+
case "clear":
|
|
793
|
+
return {
|
|
794
|
+
label: labels.reset,
|
|
795
|
+
icon: <RotateCcw className="size-4" aria-hidden />,
|
|
796
|
+
onCommit: () => void clearRow(r),
|
|
797
|
+
// A step back, not a verdict: neither the approval's green nor the send-back's red.
|
|
798
|
+
// The surface colour as text: white on the grey in light mode, the dark page on
|
|
799
|
+
// the light grey in dark mode.
|
|
800
|
+
className: "bg-[var(--text-muted)] text-[var(--bg-surface)]",
|
|
801
|
+
armedClassName: "bg-[var(--text-secondary)] text-[var(--bg-surface)]",
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* A phone card's swipes: the binding ({@link TranslationReviewPanelProps.swipe}) resolved
|
|
808
|
+
* against the row, each side's ladder closed up over what the row cannot offer. None
|
|
809
|
+
* where the row must not be acted on — the same conditions under which its buttons are
|
|
810
|
+
* locked or absent.
|
|
811
|
+
*/
|
|
812
|
+
const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {
|
|
813
|
+
if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`)) return null;
|
|
814
|
+
const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== undefined });
|
|
815
|
+
if (plan.start.length === 0 && plan.end.length === 0) return null;
|
|
816
|
+
return {
|
|
817
|
+
start: plan.start.map((id) => swipeAction(r, id, groupKey)),
|
|
818
|
+
end: plan.end.map((id) => swipeAction(r, id, groupKey)),
|
|
752
819
|
};
|
|
753
820
|
};
|
|
754
821
|
|
|
@@ -834,7 +901,7 @@ export function TranslationReviewPanel({
|
|
|
834
901
|
</div>
|
|
835
902
|
</div>
|
|
836
903
|
)}
|
|
837
|
-
mobileSwipeActions={
|
|
904
|
+
mobileSwipeActions={binding ? (r) => swipeActions(r, group?.key) : undefined}
|
|
838
905
|
selection={selectionFor(list)}
|
|
839
906
|
/>
|
|
840
907
|
);
|
package/src/i18n/german.ts
CHANGED
|
@@ -790,6 +790,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
790
790
|
exportCorrections: (count) => `Korrekturen exportieren (${n(count)})`,
|
|
791
791
|
failed: "Das hat nicht geklappt. Bitte erneut versuchen.",
|
|
792
792
|
approvedToast: (count) => (count === 1 ? "Text bestätigt" : `${n(count)} Texte bestätigt`),
|
|
793
|
+
clearedToast: (count) => (count === 1 ? "Text als ungeprüft markiert" : `${n(count)} Texte als ungeprüft markiert`),
|
|
793
794
|
groupCount: (unreviewed, total) => `${n(unreviewed)} ungeprüft / ${n(total)}`,
|
|
794
795
|
approveGroup: (count) => `Ungeprüfte bestätigen (${n(count)})`,
|
|
795
796
|
// The group's name quoted, as `columnMapper.roleOf`'s column: it is often a key
|
package/src/i18n/locales/en.ts
CHANGED
|
@@ -127,6 +127,7 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
|
|
|
127
127
|
localeProgress: (approved, total) => `${n(approved)}/${n(total)}`,
|
|
128
128
|
exportCorrections: (count) => `Export corrections (${n(count)})`,
|
|
129
129
|
approvedToast: (count) => (count === 1 ? "String approved" : `${n(count)} strings approved`),
|
|
130
|
+
clearedToast: (count) => (count === 1 ? "String marked unreviewed" : `${n(count)} strings marked unreviewed`),
|
|
130
131
|
groupCount: (unreviewed, total) => `${n(unreviewed)} unreviewed / ${n(total)}`,
|
|
131
132
|
approveGroup: (count) => `Approve unreviewed (${n(count)})`,
|
|
132
133
|
confirmGroup: (count, group) =>
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -770,6 +770,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
770
770
|
exportCorrections: (count) => `Exportar correcciones (${n(count)})`,
|
|
771
771
|
failed: "No ha funcionado. Inténtelo de nuevo.",
|
|
772
772
|
approvedToast: (count) => (count === 1 ? "Texto aprobado" : `${n(count)} textos aprobados`),
|
|
773
|
+
clearedToast: (count) => (count === 1 ? "Texto marcado como sin revisar" : `${n(count)} textos marcados como sin revisar`),
|
|
773
774
|
// "sin revisar" does not agree with the count, so one form serves both.
|
|
774
775
|
groupCount: (unreviewed, total) => `${n(unreviewed)} sin revisar / ${n(total)}`,
|
|
775
776
|
approveGroup: (count) => `Aprobar los sin revisar (${n(count)})`,
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -787,6 +787,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
787
787
|
exportCorrections: (count) => `Exporter les corrections (${n(count)})`,
|
|
788
788
|
failed: "Cela n’a pas fonctionné. Veuillez réessayer.",
|
|
789
789
|
approvedToast: (count) => (count < 2 ? "Texte approuvé" : `${n(count)} textes approuvés`),
|
|
790
|
+
clearedToast: (count) => (count < 2 ? "Texte marqué comme non relu" : `${n(count)} textes marqués comme non relus`),
|
|
790
791
|
groupCount: (unreviewed, total) =>
|
|
791
792
|
`${n(unreviewed)} non ${plural(unreviewed, "relu", "relus")} / ${n(total)}`,
|
|
792
793
|
approveGroup: (count) => `Approuver les non relus (${n(count)})`,
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -774,6 +774,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
774
774
|
failed: "Ez nem sikerült. Kérjük, próbálja újra.",
|
|
775
775
|
// No plural after a numeral (convention 2).
|
|
776
776
|
approvedToast: (count) => (count === 1 ? "Szöveg jóváhagyva" : `${n(count)} szöveg jóváhagyva`),
|
|
777
|
+
clearedToast: (count) => (count === 1 ? "Szöveg ellenőrizetlenként megjelölve" : `${n(count)} szöveg ellenőrizetlenként megjelölve`),
|
|
777
778
|
groupCount: (unreviewed, total) => `${n(unreviewed)} ellenőrizetlen / ${n(total)}`,
|
|
778
779
|
approveGroup: (count) => `Ellenőrizetlenek jóváhagyása (${n(count)})`,
|
|
779
780
|
// Opens with the quoted name, as `filePicker`'s refusals: no case ending ("-ban/-ben")
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -774,6 +774,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
774
774
|
exportCorrections: (count) => `Esporta le correzioni (${n(count)})`,
|
|
775
775
|
failed: "Non ha funzionato. Riprovi.",
|
|
776
776
|
approvedToast: (count) => (count === 1 ? "Testo approvato" : `${n(count)} testi approvati`),
|
|
777
|
+
clearedToast: (count) => (count === 1 ? "Testo segnato come non rivisto" : `${n(count)} testi segnati come non rivisti`),
|
|
777
778
|
groupCount: (unreviewed, total) =>
|
|
778
779
|
`${n(unreviewed)} ${plural(unreviewed, "non rivisto", "non rivisti")} / ${n(total)}`,
|
|
779
780
|
approveGroup: (count) => `Approva i non rivisti (${n(count)})`,
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -749,6 +749,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
749
749
|
exportCorrections: (count) => `导出更正(${n(count)})`,
|
|
750
750
|
failed: "操作未成功,请重试。",
|
|
751
751
|
approvedToast: (count) => (count === 1 ? "文本已通过" : `已通过 ${n(count)} 条文本`),
|
|
752
|
+
clearedToast: (count) => (count === 1 ? "文本已标记为未审校" : `已将 ${n(count)} 条文本标记为未审校`),
|
|
752
753
|
groupCount: (unreviewed, total) => `未审校 ${n(unreviewed)} / ${n(total)}`,
|
|
753
754
|
approveGroup: (count) => `通过未审校项(${n(count)})`,
|
|
754
755
|
confirmGroup: (count, group) =>
|
package/src/i18n/review.ts
CHANGED
|
@@ -194,6 +194,7 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
|
|
|
194
194
|
"translationReview.scope": [["{{areas}}"]],
|
|
195
195
|
"translationReview.exportCorrections": [[0], [1], [3]],
|
|
196
196
|
"translationReview.approvedToast": one,
|
|
197
|
+
"translationReview.clearedToast": one,
|
|
197
198
|
"translationReview.groupCount": [
|
|
198
199
|
[1, 12],
|
|
199
200
|
[3, 12],
|
package/src/index.ts
CHANGED
|
@@ -395,6 +395,10 @@ export {
|
|
|
395
395
|
groupTranslationRows,
|
|
396
396
|
unreviewedRows,
|
|
397
397
|
reviewUndo,
|
|
398
|
+
// 0.26: swipe bindings for an app's Settings → Interaction (keksdose #377 rework).
|
|
399
|
+
TRANSLATION_REVIEW_SWIPE_ACTIONS,
|
|
400
|
+
DEFAULT_TRANSLATION_REVIEW_SWIPE,
|
|
401
|
+
translationReviewSwipePlan,
|
|
398
402
|
} from "./lib/translation-review";
|
|
399
403
|
export type {
|
|
400
404
|
TranslationVerdict,
|
|
@@ -411,6 +415,8 @@ export type {
|
|
|
411
415
|
TranslationCorrection,
|
|
412
416
|
TranslationRowGroup,
|
|
413
417
|
TranslationReviewUndo,
|
|
418
|
+
TranslationReviewSwipeAction,
|
|
419
|
+
TranslationReviewSwipeBinding,
|
|
414
420
|
} from "./lib/translation-review";
|
|
415
421
|
export {
|
|
416
422
|
TranslationReviewPanel,
|
|
@@ -512,6 +512,115 @@ export function dropReviews<T extends TranslationReviewKey>(
|
|
|
512
512
|
return prev.filter((r) => !gone.has(`${r.locale}|${r.key}`));
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
// ── Swipe bindings ──────────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* What a phone card's swipe can be bound to in `TranslationReviewPanel`'s `swipe`
|
|
519
|
+
* (keksdose — Marcel's live #377 rework, 2026-10-03: *"Add the swipe options to be shown
|
|
520
|
+
* also to the settings /settings#interaction area where the other swipe options are
|
|
521
|
+
* defined"*). keksdose lets the user bind every list's swipes there — per side, a primary
|
|
522
|
+
* action at the first threshold and a secondary one at a longer drag — and lists each
|
|
523
|
+
* surface's ids in its `SWIPE_ACTIONS`; this is the translation list's, in the order a
|
|
524
|
+
* settings page offers them.
|
|
525
|
+
*
|
|
526
|
+
* - `approve` — the string is right as it stands. With the panel's Undo toast.
|
|
527
|
+
* - `edit` — open the editor with the cursor in the wording: "Needs a change" (or
|
|
528
|
+
* "Suggest translation" on a missing string). Writes nothing; the editor does.
|
|
529
|
+
* - `clear` — take the verdict back: unreviewed again. With the Undo toast, which stores
|
|
530
|
+
* the verdict again.
|
|
531
|
+
*
|
|
532
|
+
* THERE IS NO SEPARATE "needs a change". The editor has no verdict to preselect: Approve
|
|
533
|
+
* and "Needs a change" stand side by side in it, and "Needs a change" takes a note or a
|
|
534
|
+
* different wording — a verdict without either tells whoever fixes the string nothing, so
|
|
535
|
+
* a swipe never sends one blind (0.25). The one honest thing a "needs a change" swipe can
|
|
536
|
+
* do is therefore open the editor with the cursor in the wording — which is `edit`, and
|
|
537
|
+
* `edit` already says "Needs a change" on the card. A second id doing the same would arm
|
|
538
|
+
* a second threshold that does what the first already did: the duplicate keksdose's
|
|
539
|
+
* `resolveSwipePlan` drops for exactly that reason.
|
|
540
|
+
*/
|
|
541
|
+
export const TRANSLATION_REVIEW_SWIPE_ACTIONS = ["approve", "edit", "clear"] as const;
|
|
542
|
+
|
|
543
|
+
/** One of {@link TRANSLATION_REVIEW_SWIPE_ACTIONS}. */
|
|
544
|
+
export type TranslationReviewSwipeAction = (typeof TRANSLATION_REVIEW_SWIPE_ACTIONS)[number];
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Which swipe does what on a phone card — `TranslationReviewPanel`'s `swipe`. Each side is
|
|
548
|
+
* an ordered ladder, exactly as `SwipeableRow` and DataTable's `mobileSwipeActions` model
|
|
549
|
+
* one: index 0 commits at the first threshold, index 1 at the longer drag (and so on — the
|
|
550
|
+
* thresholds spread evenly over the card's width). A side left out, or empty, does not
|
|
551
|
+
* swipe.
|
|
552
|
+
*
|
|
553
|
+
* LOGICAL SIDES: `end` is a drag toward the reading end of the line — right in a
|
|
554
|
+
* left-to-right page, left in a right-to-left one — and `start` the other way, so one
|
|
555
|
+
* binding holds in both directions. An app whose settings store physical sides (keksdose's
|
|
556
|
+
* `right`/`left`) hands `right` as `end` for a left-to-right page.
|
|
557
|
+
*/
|
|
558
|
+
export interface TranslationReviewSwipeBinding {
|
|
559
|
+
/** A drag toward the reading START, nearest threshold first. */
|
|
560
|
+
start?: readonly TranslationReviewSwipeAction[];
|
|
561
|
+
/** A drag toward the reading END, nearest threshold first. */
|
|
562
|
+
end?: readonly TranslationReviewSwipeAction[];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** What `swipe={true}` means — 0.25's fixed mapping: approve toward the end, open the
|
|
566
|
+
* editor ("Needs a change") toward the start. A settings page's "reset" goes back here. */
|
|
567
|
+
export const DEFAULT_TRANSLATION_REVIEW_SWIPE: TranslationReviewSwipeBinding = Object.freeze({
|
|
568
|
+
start: Object.freeze(["edit"] as const),
|
|
569
|
+
end: Object.freeze(["approve"] as const),
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
/** The ladders one row actually offers, from a {@link TranslationReviewSwipeBinding}. */
|
|
573
|
+
export interface TranslationReviewSwipePlan {
|
|
574
|
+
start: TranslationReviewSwipeAction[];
|
|
575
|
+
end: TranslationReviewSwipeAction[];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* The binding resolved against one row — keksdose's `resolveSwipePlan`, for this list.
|
|
580
|
+
* An id the row cannot offer DROPS OUT and the ladder closes up behind it, so the second
|
|
581
|
+
* stage becomes the first rather than the side going dead behind an unreachable first
|
|
582
|
+
* stage (or arming a threshold that does nothing):
|
|
583
|
+
*
|
|
584
|
+
* - `approve`: not on a missing string (nothing to approve) or an approved one (it would
|
|
585
|
+
* say it again) — the rows that have no approve button either;
|
|
586
|
+
* - `clear`: not without `canClear` (the app passed no `onClear`), and not on a row with
|
|
587
|
+
* no verdict to take back (unreviewed; missing with no suggestion);
|
|
588
|
+
* - `edit`: on every row;
|
|
589
|
+
* - an id named twice on one side: the second time (a longer drag to the same effect);
|
|
590
|
+
* - an id this version does not know (a binding stored by another version, an app's own
|
|
591
|
+
* "none" slot): dropped like one that does not apply.
|
|
592
|
+
*
|
|
593
|
+
* Only what the ROW decides. Whether the panel may write at all — `readOnly`, no
|
|
594
|
+
* `onSave`, a write lock, the row's own write still out — is the panel's, and then no
|
|
595
|
+
* side swipes.
|
|
596
|
+
*/
|
|
597
|
+
export function translationReviewSwipePlan(
|
|
598
|
+
binding: TranslationReviewSwipeBinding,
|
|
599
|
+
row: TranslationRow,
|
|
600
|
+
{ canClear = false }: { canClear?: boolean } = {},
|
|
601
|
+
): TranslationReviewSwipePlan {
|
|
602
|
+
const offers = (id: TranslationReviewSwipeAction): boolean => {
|
|
603
|
+
switch (id) {
|
|
604
|
+
case "approve":
|
|
605
|
+
return row.text !== "" && row.status !== "approved";
|
|
606
|
+
case "edit":
|
|
607
|
+
return true;
|
|
608
|
+
case "clear":
|
|
609
|
+
return canClear && row.review !== null;
|
|
610
|
+
default:
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
const side = (ids: readonly TranslationReviewSwipeAction[] | undefined): TranslationReviewSwipeAction[] => {
|
|
615
|
+
const out: TranslationReviewSwipeAction[] = [];
|
|
616
|
+
for (const id of ids ?? []) {
|
|
617
|
+
if (!out.includes(id) && offers(id)) out.push(id);
|
|
618
|
+
}
|
|
619
|
+
return out;
|
|
620
|
+
};
|
|
621
|
+
return { start: side(binding.start), end: side(binding.end) };
|
|
622
|
+
}
|
|
623
|
+
|
|
515
624
|
// ── Export ──────────────────────────────────────────────────────────────────────
|
|
516
625
|
|
|
517
626
|
/**
|
package/tokens.css
CHANGED
|
@@ -213,6 +213,9 @@
|
|
|
213
213
|
journey to the database, this one an ACTION's outcome. Emerald rather than the
|
|
214
214
|
money teal, so a "resolved" chip cannot be misread as an inflow. */
|
|
215
215
|
--success: #047857; /* emerald-700 */
|
|
216
|
+
/* Text on a `--success` fill (0.26: a swipe's approve panel) — white here, the
|
|
217
|
+
darkest emerald in dark mode, where the fill is a pastel and white text failed. */
|
|
218
|
+
--success-contrast: #ffffff;
|
|
216
219
|
--success-border: #6ee7b7; /* emerald-300 */
|
|
217
220
|
--success-bg: #d1fae5; /* emerald-100 */
|
|
218
221
|
}
|
|
@@ -237,6 +240,7 @@
|
|
|
237
240
|
--info-bg: rgba(12, 74, 110, 0.25);
|
|
238
241
|
|
|
239
242
|
--success: #6ee7b7; /* emerald-300 */
|
|
243
|
+
--success-contrast: #022c22; /* emerald-950 */
|
|
240
244
|
--success-border: rgba(4, 120, 87, 0.6);
|
|
241
245
|
--success-bg: rgba(6, 78, 59, 0.25);
|
|
242
246
|
}
|