@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
|
@@ -15,7 +15,11 @@ interface SwipeAction {
|
|
|
15
15
|
/** Rendered inside the panel. Icons stay the consumer's choice — this package
|
|
16
16
|
* ships no opinion about which icon set an app uses. */
|
|
17
17
|
icon?: ReactNode;
|
|
18
|
-
/** Background utility class while the action is previewed but not yet armed.
|
|
18
|
+
/** Background utility class while the action is previewed but not yet armed. The
|
|
19
|
+
* panel's text is white unless this sets a text colour too — and a fill that turns
|
|
20
|
+
* light in dark mode needs one: pass the fill's contrast token
|
|
21
|
+
* (`bg-[var(--danger)] text-[var(--danger-contrast)]`; also `--brand-contrast`,
|
|
22
|
+
* `--success-contrast`), as the kit's own swipes do since 0.26. */
|
|
19
23
|
className: string;
|
|
20
24
|
/** Background utility class once the drag has passed this action's threshold. */
|
|
21
25
|
armedClassName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/swipeable-row.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useRowSwipe, type SwipeStage } from \"../hooks/use-row-swipe\";\nimport { DEFAULT_SWIPEABLE_ROW_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * One armed swipe action: what it does, and how the reveal panel presents it.\n *\n * The two background classes are the idle and armed states. Before the first\n * threshold is crossed the panel previews the nearest action in its IDLE colour, so\n * the user can see what a little more drag will do; crossing the threshold flips it to\n * `armedClassName`. Passing the same value for both simply removes that feedback.\n */\nexport interface SwipeAction {\n onCommit: () => void;\n label: string;\n /** Rendered inside the panel. Icons stay the consumer's choice — this package\n * ships no opinion about which icon set an app uses. */\n icon?: ReactNode;\n /** Background utility class while the action is previewed but not yet armed. */\n className: string;\n /** Background utility class once the drag has passed this action's threshold. */\n armedClassName: string;\n}\n\n/**\n * `right`/`left` are PHYSICAL drag directions in every writing direction, and stay so on\n * purpose: a swipe is a movement of the finger across the glass, the gesture hook\n * measures it in screen pixels, and \"which way is destructive\" is an app decision that\n * RTL guidelines do not agree on. An app that wants mirrored actions in RTL swaps the\n * two arrays itself. What the component does mirror is everything it lays out: the\n * reveal panel's contents hug the physical edge being uncovered, and the keyboard\n * buttons sit at the row's logical END.\n */\nexport interface SwipeableRowProps {\n /** Actions committed by dragging (physically) RIGHT, nearest threshold first. */\n right?: SwipeAction[];\n /** Actions committed by dragging (physically) LEFT, nearest threshold first. */\n left?: SwipeAction[];\n /** Turn the gesture off — an open editor, a pending mutation, a locked row. This\n * also withdraws the keyboard buttons below: a row that must not be acted on must\n * not be actionable by ANY input. */\n enabled?: boolean;\n /** Names the revealed action buttons as a set (\"Row actions\"), for a reader that\n * reaches the group before it reaches any one button. Default:\n * `swipeableRow.actions` from the {@link UiKitProvider}, else English. */\n actionsLabel?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** How much of the row stays on screen at full drag. A row that can be pulled\n * entirely off its own track reads as already deleted, and there is nothing left to\n * drag back; a sliver keeps the gesture reversible and the row identifiable. */\nconst PEEK_PX = 40;\n\n/**\n * A row whose horizontal drag reveals actions underneath it.\n *\n * The reveal panel spans the FULL row width (Keksdose feedback #174). It used to be\n * two half-width blocks side by side, which capped the drag at half the row so the\n * opposite panel could never be exposed — and on a row bound to three actions that\n * left ~55px between stages on a phone, close enough that the wrong one committed.\n * Only the side being dragged toward is painted now, so full width costs nothing:\n * at rest the content covers both anyway, and a drag only ever reveals one side.\n *\n * Thresholds are still not fixed pixel values — they are spread evenly across the\n * available drag, which is now the row width less a {@link PEEK_PX} sliver. A fixed\n * 72/150/228px ladder overshoots a narrow phone; a proportional one keeps every\n * action reachable and roughly doubles the room between them.\n *\n * **Whether the action will fire is stated three ways, not one** (feedback #174): the\n * panel goes from dimmed to solid, the icon grows and gains a filled disc, and the\n * label turns bold — plus the existing haptic tick per newly armed stage. A colour\n * shift alone is easy to miss mid-gesture, and it is invisible to anyone who cannot\n * distinguish the two tones.\n *\n * Clicks that follow a drag are swallowed in the capture phase: without that, a swipe\n * would also fire whatever click handler the row content carries (expanding it, opening\n * a dialog) on top of the action it just committed.\n *\n * ## The keyboard path is buttons, and deliberately not a typed gesture\n *\n * Every action here used to live exclusively inside a pointer drag — `onCommit` had no\n * other caller — so with a keyboard, with a screen reader, or on any input that cannot\n * express 120px of horizontal travel, the row's actions did not exist at all. That is\n * not a rough edge on a phone control: {@link DataTable} mounts this for every row of\n * its mobile layout, and the actions an app puts here are its destructive ones.\n *\n * The rejected fix was to map the gesture onto keys — arrows nudging `dx`, a chord that\n * \"swipes\". That invents a private idiom with nothing to discover it by, and it makes\n * the keyboard imitate a touchscreen instead of doing the job the touchscreen is doing.\n * So each action is ALSO a real button, `sr-only` until it takes focus and then shown at\n * the row's trailing edge, firing the identical `onCommit`: one definition of what an\n * action is, two ways to reach it, and nothing to drift apart. `sr-only` hides from\n * the eye and not from the accessibility tree, so the row also ANNOUNCES what can be\n * done to it rather than keeping it behind a gesture nobody can hear.\n */\nexport function SwipeableRow({\n right,\n left,\n enabled = true,\n actionsLabel,\n className,\n children,\n}: SwipeableRowProps) {\n const labels = useKitLabels(\"swipeableRow\", DEFAULT_SWIPEABLE_ROW_LABELS, {\n actions: actionsLabel,\n });\n const rowRef = useRef<HTMLDivElement>(null);\n const [rowWidth, setRowWidth] = useState(0);\n useEffect(() => {\n const el = rowRef.current;\n if (!el) return;\n setRowWidth(el.clientWidth);\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver((entries) => {\n for (const e of entries) setRowWidth(e.contentRect.width);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const rightActions = right ?? [];\n const leftActions = left ?? [];\n const active = enabled && (rightActions.length > 0 || leftActions.length > 0);\n // The whole row less a sliver, not half of it (feedback #174). `Math.max` keeps a\n // very narrow row (or one measured before layout) from producing a zero or\n // negative drag, which would arm every stage at once.\n const maxDrag = rowWidth > 0 ? Math.max(96, Math.round(rowWidth - PEEK_PX)) : 200;\n const spread = (actions: SwipeAction[]): SwipeStage[] =>\n actions.map((a, i) => ({\n threshold: Math.round((maxDrag * (i + 1)) / (actions.length + 1)),\n onCommit: a.onCommit,\n }));\n\n const swipe = useRowSwipe({\n enabled: active,\n rightStages: spread(rightActions),\n leftStages: spread(leftActions),\n maxDrag,\n });\n\n // A haptic tick per newly armed threshold, so each stage is felt and not only seen.\n const armedNow = swipe.dx >= 0 ? swipe.armedRightIndex : swipe.armedLeftIndex;\n const prevArmed = useRef(-1);\n useEffect(() => {\n if (armedNow > prevArmed.current && armedNow >= 0) navigator.vibrate?.(12);\n prevArmed.current = armedNow;\n }, [armedNow]);\n\n // Index -1 (nothing armed yet) still previews the nearest action, idle-coloured.\n const dx = swipe.dx;\n // Only the side being dragged toward is painted, which is what makes a full-width\n // panel safe.\n //\n // The side has to SURVIVE the release, though, and that is what this ref is for\n // (Keksdose feedback dev#467): *\"Swiping a category to the left with no option — if\n // letting it go it gets, for the way back, the background color of the opposite\n // swipe direction where there should be no color change at all.\"* On release dx\n // snaps to 0 while the row itself slides back over 150ms, so a plain `dx < 0` read\n // \"right\" for exactly the frames where the left side was still uncovered — and\n // painted the RIGHT action's colour into the gap. With no left action at all that\n // colour was the only thing the gesture ever showed.\n //\n // Adjusted during render rather than in an effect: an effect would repaint a frame\n // later (visibly, at 150ms). State set while rendering re-renders this component\n // before anything is committed, so the released frame already has the right side.\n const [lastSide, setLastSide] = useState<\"left\" | \"right\">(\"right\");\n const side = dx === 0 ? null : dx < 0 ? \"left\" : \"right\";\n if (side && side !== lastSide) setLastSide(side);\n const draggingLeft = side ? side === \"left\" : lastSide === \"left\";\n const shown = draggingLeft\n ? leftActions[Math.max(0, swipe.armedLeftIndex)]\n : rightActions[Math.max(0, swipe.armedRightIndex)];\n const armed = draggingLeft ? swipe.armedLeftIndex >= 0 : swipe.armedRightIndex >= 0;\n\n return (\n <div ref={rowRef} className={cn(\"relative overflow-hidden\", className)}>\n {active && shown && (\n <div\n className={cn(\n \"pointer-events-none absolute inset-0 flex items-center gap-2 px-4\",\n \"text-xs font-medium text-white transition-all\",\n // Dimmed until the drag has actually passed a threshold: \"nothing will\n // happen yet\" has to look different from \"let go and this fires\", and on\n // a moving row a one-step colour change was too quiet to notice.\n armed ? \"opacity-100\" : \"opacity-60\",\n armed ? shown.armedClassName : shown.className,\n // The panel is anchored to the edge the row is uncovering, so the label\n // sits where the eye already is rather than across the screen. That edge is\n // physical, while a flex row starts at the INLINE start — so in RTL the two\n // directions swap, or the label sat under the row on the covered side.\n draggingLeft ? \"flex-row-reverse rtl:flex-row\" : \"rtl:flex-row-reverse\",\n )}\n >\n {shown.icon && (\n <span\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-full transition-all\",\n // Armed, the icon gets a disc of its own and grows — a second,\n // non-colour signal, and the one that stays readable at a glance.\n armed ? \"size-8 scale-110 bg-white/25\" : \"size-7 bg-white/10\",\n )}\n >\n {shown.icon}\n </span>\n )}\n <span className={cn(\"truncate transition-all\", armed && \"text-sm font-semibold\")}>\n {shown.label}\n </span>\n </div>\n )}\n <div\n // Capture phase: the row's own click handler lives on a DESCENDANT, so a\n // bubble-phase listener here would run after it — too late to suppress.\n onClickCapture={(e) => {\n if (!swipe.consumeClick()) return;\n e.preventDefault();\n e.stopPropagation();\n }}\n className={cn(\n \"relative bg-[var(--bg-surface)]\",\n // A mouse drag is a supported way to swipe (use-row-swipe gates only on\n // the button, not the pointer type), and a horizontal mouse drag over\n // text also SELECTS that text — so committing an action left the row\n // highlighted blue. Suppressed only while dragging, so text in a row at\n // rest stays selectable.\n swipe.dragging ? \"select-none\" : \"transition-transform duration-150 motion-reduce:transition-none\",\n )}\n style={{\n transform: dx !== 0 ? `translate3d(${dx}px,0,0)` : undefined,\n touchAction: active ? \"pan-y\" : undefined,\n }}\n {...(active ? swipe.handlers : {})}\n >\n {children}\n </div>\n {active && (\n <div\n role=\"group\"\n aria-label={labels.actions}\n // OUTSIDE the sliding div on purpose: an action that travels with the row it\n // acts on is a moving target, and the drag transform would carry it off the\n // screen mid-gesture.\n //\n // `pointer-events-none` here, restored on a button only once it HAS focus: a\n // sr-only button is clipped to nothing but is still a hit target, and this\n // strip sits at the row's trailing edge — exactly where a drag toward the\n // start begins.\n //\n // `end-0`/`pe-2`, not `right-0`/`pr-2`: in RTL the right edge is where the\n // row's title starts, and a focused button there covered it.\n // A gesture that silently failed to start because it landed on an invisible\n // button is not a bug anyone would find by looking at the screen.\n className=\"pointer-events-none absolute inset-y-0 end-0 z-10 flex items-center gap-1 pe-2\"\n >\n {/* Both sides, each still nearest-threshold first, so the tab order reads in\n the order the gesture arms them. */}\n {[...rightActions, ...leftActions].map((action, i) => (\n <button\n key={`${action.label}-${i}`}\n type=\"button\"\n onClick={action.onCommit}\n className={cn(\n \"sr-only whitespace-nowrap rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-sm\",\n \"focus:not-sr-only focus:pointer-events-auto focus:inline-flex focus:items-center focus:gap-1.5\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n )}\n >\n {/* The label is the button's accessible name, so the caller's icon is\n decoration on top of it rather than a second reading of it. */}\n {action.icon && (\n <span aria-hidden=\"true\" className=\"[&_svg]:size-4\">\n {action.icon}\n </span>\n )}\n {action.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/swipeable-row.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useRowSwipe, type SwipeStage } from \"../hooks/use-row-swipe\";\nimport { DEFAULT_SWIPEABLE_ROW_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * One armed swipe action: what it does, and how the reveal panel presents it.\n *\n * The two background classes are the idle and armed states. Before the first\n * threshold is crossed the panel previews the nearest action in its IDLE colour, so\n * the user can see what a little more drag will do; crossing the threshold flips it to\n * `armedClassName`. Passing the same value for both simply removes that feedback.\n */\nexport interface SwipeAction {\n onCommit: () => void;\n label: string;\n /** Rendered inside the panel. Icons stay the consumer's choice — this package\n * ships no opinion about which icon set an app uses. */\n icon?: ReactNode;\n /** Background utility class while the action is previewed but not yet armed. The\n * panel's text is white unless this sets a text colour too — and a fill that turns\n * light in dark mode needs one: pass the fill's contrast token\n * (`bg-[var(--danger)] text-[var(--danger-contrast)]`; also `--brand-contrast`,\n * `--success-contrast`), as the kit's own swipes do since 0.26. */\n className: string;\n /** Background utility class once the drag has passed this action's threshold. */\n armedClassName: string;\n}\n\n/**\n * `right`/`left` are PHYSICAL drag directions in every writing direction, and stay so on\n * purpose: a swipe is a movement of the finger across the glass, the gesture hook\n * measures it in screen pixels, and \"which way is destructive\" is an app decision that\n * RTL guidelines do not agree on. An app that wants mirrored actions in RTL swaps the\n * two arrays itself. What the component does mirror is everything it lays out: the\n * reveal panel's contents hug the physical edge being uncovered, and the keyboard\n * buttons sit at the row's logical END.\n */\nexport interface SwipeableRowProps {\n /** Actions committed by dragging (physically) RIGHT, nearest threshold first. */\n right?: SwipeAction[];\n /** Actions committed by dragging (physically) LEFT, nearest threshold first. */\n left?: SwipeAction[];\n /** Turn the gesture off — an open editor, a pending mutation, a locked row. This\n * also withdraws the keyboard buttons below: a row that must not be acted on must\n * not be actionable by ANY input. */\n enabled?: boolean;\n /** Names the revealed action buttons as a set (\"Row actions\"), for a reader that\n * reaches the group before it reaches any one button. Default:\n * `swipeableRow.actions` from the {@link UiKitProvider}, else English. */\n actionsLabel?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** How much of the row stays on screen at full drag. A row that can be pulled\n * entirely off its own track reads as already deleted, and there is nothing left to\n * drag back; a sliver keeps the gesture reversible and the row identifiable. */\nconst PEEK_PX = 40;\n\n/**\n * A row whose horizontal drag reveals actions underneath it.\n *\n * The reveal panel spans the FULL row width (Keksdose feedback #174). It used to be\n * two half-width blocks side by side, which capped the drag at half the row so the\n * opposite panel could never be exposed — and on a row bound to three actions that\n * left ~55px between stages on a phone, close enough that the wrong one committed.\n * Only the side being dragged toward is painted now, so full width costs nothing:\n * at rest the content covers both anyway, and a drag only ever reveals one side.\n *\n * Thresholds are still not fixed pixel values — they are spread evenly across the\n * available drag, which is now the row width less a {@link PEEK_PX} sliver. A fixed\n * 72/150/228px ladder overshoots a narrow phone; a proportional one keeps every\n * action reachable and roughly doubles the room between them.\n *\n * **Whether the action will fire is stated three ways, not one** (feedback #174): the\n * panel goes from dimmed to solid, the icon grows and gains a filled disc, and the\n * label turns bold — plus the existing haptic tick per newly armed stage. A colour\n * shift alone is easy to miss mid-gesture, and it is invisible to anyone who cannot\n * distinguish the two tones.\n *\n * Clicks that follow a drag are swallowed in the capture phase: without that, a swipe\n * would also fire whatever click handler the row content carries (expanding it, opening\n * a dialog) on top of the action it just committed.\n *\n * ## The keyboard path is buttons, and deliberately not a typed gesture\n *\n * Every action here used to live exclusively inside a pointer drag — `onCommit` had no\n * other caller — so with a keyboard, with a screen reader, or on any input that cannot\n * express 120px of horizontal travel, the row's actions did not exist at all. That is\n * not a rough edge on a phone control: {@link DataTable} mounts this for every row of\n * its mobile layout, and the actions an app puts here are its destructive ones.\n *\n * The rejected fix was to map the gesture onto keys — arrows nudging `dx`, a chord that\n * \"swipes\". That invents a private idiom with nothing to discover it by, and it makes\n * the keyboard imitate a touchscreen instead of doing the job the touchscreen is doing.\n * So each action is ALSO a real button, `sr-only` until it takes focus and then shown at\n * the row's trailing edge, firing the identical `onCommit`: one definition of what an\n * action is, two ways to reach it, and nothing to drift apart. `sr-only` hides from\n * the eye and not from the accessibility tree, so the row also ANNOUNCES what can be\n * done to it rather than keeping it behind a gesture nobody can hear.\n */\nexport function SwipeableRow({\n right,\n left,\n enabled = true,\n actionsLabel,\n className,\n children,\n}: SwipeableRowProps) {\n const labels = useKitLabels(\"swipeableRow\", DEFAULT_SWIPEABLE_ROW_LABELS, {\n actions: actionsLabel,\n });\n const rowRef = useRef<HTMLDivElement>(null);\n const [rowWidth, setRowWidth] = useState(0);\n useEffect(() => {\n const el = rowRef.current;\n if (!el) return;\n setRowWidth(el.clientWidth);\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver((entries) => {\n for (const e of entries) setRowWidth(e.contentRect.width);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const rightActions = right ?? [];\n const leftActions = left ?? [];\n const active = enabled && (rightActions.length > 0 || leftActions.length > 0);\n // The whole row less a sliver, not half of it (feedback #174). `Math.max` keeps a\n // very narrow row (or one measured before layout) from producing a zero or\n // negative drag, which would arm every stage at once.\n const maxDrag = rowWidth > 0 ? Math.max(96, Math.round(rowWidth - PEEK_PX)) : 200;\n const spread = (actions: SwipeAction[]): SwipeStage[] =>\n actions.map((a, i) => ({\n threshold: Math.round((maxDrag * (i + 1)) / (actions.length + 1)),\n onCommit: a.onCommit,\n }));\n\n const swipe = useRowSwipe({\n enabled: active,\n rightStages: spread(rightActions),\n leftStages: spread(leftActions),\n maxDrag,\n });\n\n // A haptic tick per newly armed threshold, so each stage is felt and not only seen.\n const armedNow = swipe.dx >= 0 ? swipe.armedRightIndex : swipe.armedLeftIndex;\n const prevArmed = useRef(-1);\n useEffect(() => {\n if (armedNow > prevArmed.current && armedNow >= 0) navigator.vibrate?.(12);\n prevArmed.current = armedNow;\n }, [armedNow]);\n\n // Index -1 (nothing armed yet) still previews the nearest action, idle-coloured.\n const dx = swipe.dx;\n // Only the side being dragged toward is painted, which is what makes a full-width\n // panel safe.\n //\n // The side has to SURVIVE the release, though, and that is what this ref is for\n // (Keksdose feedback dev#467): *\"Swiping a category to the left with no option — if\n // letting it go it gets, for the way back, the background color of the opposite\n // swipe direction where there should be no color change at all.\"* On release dx\n // snaps to 0 while the row itself slides back over 150ms, so a plain `dx < 0` read\n // \"right\" for exactly the frames where the left side was still uncovered — and\n // painted the RIGHT action's colour into the gap. With no left action at all that\n // colour was the only thing the gesture ever showed.\n //\n // Adjusted during render rather than in an effect: an effect would repaint a frame\n // later (visibly, at 150ms). State set while rendering re-renders this component\n // before anything is committed, so the released frame already has the right side.\n const [lastSide, setLastSide] = useState<\"left\" | \"right\">(\"right\");\n const side = dx === 0 ? null : dx < 0 ? \"left\" : \"right\";\n if (side && side !== lastSide) setLastSide(side);\n const draggingLeft = side ? side === \"left\" : lastSide === \"left\";\n const shown = draggingLeft\n ? leftActions[Math.max(0, swipe.armedLeftIndex)]\n : rightActions[Math.max(0, swipe.armedRightIndex)];\n const armed = draggingLeft ? swipe.armedLeftIndex >= 0 : swipe.armedRightIndex >= 0;\n\n return (\n <div ref={rowRef} className={cn(\"relative overflow-hidden\", className)}>\n {active && shown && (\n <div\n className={cn(\n \"pointer-events-none absolute inset-0 flex items-center gap-2 px-4\",\n \"text-xs font-medium text-white transition-all\",\n // Dimmed until the drag has actually passed a threshold: \"nothing will\n // happen yet\" has to look different from \"let go and this fires\", and on\n // a moving row a one-step colour change was too quiet to notice.\n armed ? \"opacity-100\" : \"opacity-60\",\n armed ? shown.armedClassName : shown.className,\n // The panel is anchored to the edge the row is uncovering, so the label\n // sits where the eye already is rather than across the screen. That edge is\n // physical, while a flex row starts at the INLINE start — so in RTL the two\n // directions swap, or the label sat under the row on the covered side.\n draggingLeft ? \"flex-row-reverse rtl:flex-row\" : \"rtl:flex-row-reverse\",\n )}\n >\n {shown.icon && (\n <span\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-full transition-all\",\n // Armed, the icon gets a disc of its own and grows — a second,\n // non-colour signal, and the one that stays readable at a glance.\n armed ? \"size-8 scale-110 bg-white/25\" : \"size-7 bg-white/10\",\n )}\n >\n {shown.icon}\n </span>\n )}\n <span className={cn(\"truncate transition-all\", armed && \"text-sm font-semibold\")}>\n {shown.label}\n </span>\n </div>\n )}\n <div\n // Capture phase: the row's own click handler lives on a DESCENDANT, so a\n // bubble-phase listener here would run after it — too late to suppress.\n onClickCapture={(e) => {\n if (!swipe.consumeClick()) return;\n e.preventDefault();\n e.stopPropagation();\n }}\n className={cn(\n \"relative bg-[var(--bg-surface)]\",\n // A mouse drag is a supported way to swipe (use-row-swipe gates only on\n // the button, not the pointer type), and a horizontal mouse drag over\n // text also SELECTS that text — so committing an action left the row\n // highlighted blue. Suppressed only while dragging, so text in a row at\n // rest stays selectable.\n swipe.dragging ? \"select-none\" : \"transition-transform duration-150 motion-reduce:transition-none\",\n )}\n style={{\n transform: dx !== 0 ? `translate3d(${dx}px,0,0)` : undefined,\n touchAction: active ? \"pan-y\" : undefined,\n }}\n {...(active ? swipe.handlers : {})}\n >\n {children}\n </div>\n {active && (\n <div\n role=\"group\"\n aria-label={labels.actions}\n // OUTSIDE the sliding div on purpose: an action that travels with the row it\n // acts on is a moving target, and the drag transform would carry it off the\n // screen mid-gesture.\n //\n // `pointer-events-none` here, restored on a button only once it HAS focus: a\n // sr-only button is clipped to nothing but is still a hit target, and this\n // strip sits at the row's trailing edge — exactly where a drag toward the\n // start begins.\n //\n // `end-0`/`pe-2`, not `right-0`/`pr-2`: in RTL the right edge is where the\n // row's title starts, and a focused button there covered it.\n // A gesture that silently failed to start because it landed on an invisible\n // button is not a bug anyone would find by looking at the screen.\n className=\"pointer-events-none absolute inset-y-0 end-0 z-10 flex items-center gap-1 pe-2\"\n >\n {/* Both sides, each still nearest-threshold first, so the tab order reads in\n the order the gesture arms them. */}\n {[...rightActions, ...leftActions].map((action, i) => (\n <button\n key={`${action.label}-${i}`}\n type=\"button\"\n onClick={action.onCommit}\n className={cn(\n \"sr-only whitespace-nowrap rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-sm\",\n \"focus:not-sr-only focus:pointer-events-auto focus:inline-flex focus:items-center focus:gap-1.5\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n )}\n >\n {/* The label is the button's accessible name, so the caller's icon is\n decoration on top of it rather than a second reading of it. */}\n {action.icon && (\n <span aria-hidden=\"true\" className=\"[&_svg]:size-4\">\n {action.icon}\n </span>\n )}\n {action.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -94,6 +94,10 @@ interface TranslationReviewLabels {
|
|
|
94
94
|
/** 0.25: asked first when a group's bulk approve is larger than a page — more than the
|
|
95
95
|
* reviewer can have had on screen. `group` is the group's name. */
|
|
96
96
|
confirmGroup: (count: number, group: string) => string;
|
|
97
|
+
/** The Undo toast after a swipe bound to `clear` took a verdict back (keksdose, live
|
|
98
|
+
* #377 rework: the swipes bound in its settings) — one string, or a batch of them.
|
|
99
|
+
* The swipe itself is named by {@link reset}. */
|
|
100
|
+
clearedToast: (count: number) => string;
|
|
97
101
|
}
|
|
98
102
|
declare const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels;
|
|
99
103
|
/** A status's word, from the namespace's five. */
|
|
@@ -48,7 +48,8 @@ const DEFAULT_TRANSLATION_REVIEW_LABELS = {
|
|
|
48
48
|
approvedToast: (count) => count === 1 ? "String approved" : `${count} strings approved`,
|
|
49
49
|
groupCount: (unreviewed, total) => `${unreviewed} unreviewed / ${total}`,
|
|
50
50
|
approveGroup: (count) => `Approve unreviewed (${count})`,
|
|
51
|
-
confirmGroup: (count, group) => count === 1 ? `Approve the unreviewed string in ${group}?` : `Approve all ${count} unreviewed strings in ${group}, including those not on screen
|
|
51
|
+
confirmGroup: (count, group) => count === 1 ? `Approve the unreviewed string in ${group}?` : `Approve all ${count} unreviewed strings in ${group}, including those not on screen?`,
|
|
52
|
+
clearedToast: (count) => count === 1 ? "String marked unreviewed" : `${count} strings marked unreviewed`
|
|
52
53
|
};
|
|
53
54
|
function reviewStatusLabel(labels, status) {
|
|
54
55
|
switch (status) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/translation-review-labels.ts"],"sourcesContent":["import type { ReviewStatus } from \"../lib/translation-review\";\n\n/**\n * The `translationReview` namespace: every word of the translation-review parts\n * (`ReviewStatusChip`, `TranslationProgress`, `TranslationLocaleTabs`,\n * `TranslationExportButton`, `TranslationReviewEditor`, `TranslationReviewPanel`).\n *\n * Its own module, like `data-table-labels.ts`: `kit-labels.tsx` and `defaults.ts` import\n * it, and neither has to load the components to get the words. The English is keksdose's\n * and kastlan's own (`translation_review.*`, `translations:*`), British.\n */\nexport interface TranslationReviewLabels {\n /** The five statuses — the chip, the filter, the progress legend. */\n statusMissing: string;\n statusUnreviewed: string;\n statusChanged: string;\n statusNeedsChange: string;\n statusApproved: string;\n /** Accessible name of the status filter. */\n statusFilter: string;\n /** The status filter's first option. */\n all: string;\n /** A status filter option with its count: \"Unreviewed · 12\". */\n filterCount: (label: string, count: number) => string;\n /** The source filter's label — shown only when the rows come from several sources\n * (kastlan's screen texts and documents). */\n source: string;\n allSources: string;\n /** The namespace filter's label and its \"every one\" option. */\n namespace: string;\n allNamespaces: string;\n search: string;\n /** The placeholder filter, with how many rows it would show. */\n placeholdersOnly: (count: number) => string;\n /** The chip on a row whose placeholders differ from the reference's. */\n placeholderChip: string;\n /** The key column, and the key's caption in the editor. */\n key: string;\n statusColumn: string;\n /** No row matches the filters. */\n empty: string;\n /** In place of the text of a string the locale lacks. */\n missingText: string;\n /** The progress bar's label. */\n progress: (approved: number, total: number) => string;\n /** A locale tab's badge. */\n localeProgress: (approved: number, total: number) => string;\n /** Accessible name of the locale tabs. */\n locales: string;\n approve: string;\n /** Send a string back: \"Needs a change\". */\n flag: string;\n /** \"Needs a change\" for a MISSING string: the suggestion is the translation to add. */\n suggest: string;\n /** Clear a verdict: back to unreviewed. */\n reset: string;\n /** The editor's way out without saving. */\n cancel: string;\n approveSelected: string;\n resetSelected: string;\n /** Selects every row the filters show — the phone's way into the bulk actions, where\n * the table has no checkboxes. */\n selectShown: string;\n /** Over the old wording of a row that changed since its verdict. */\n changedSince: string;\n reviewedWording: string;\n reviewedReference: string;\n /** The placeholders differ: the reference's and the text's, each a list. */\n placeholderMismatch: (reference: string, text: string) => string;\n lastApproved: (name: string, date: string) => string;\n lastFlagged: (name: string, date: string) => string;\n /** The reviewer's name once the account is gone — what `name` above becomes. */\n erasedReviewer: string;\n /** The editor's wording field, opened on the current text. */\n suggestion: string;\n /** The same field for a missing string. */\n translation: string;\n note: string;\n notePlaceholder: string;\n /** The viewer may read this locale but not review it. */\n readOnly: string;\n /** The reviewer is limited to some areas — `areas` is their names, joined. */\n scope: (areas: string) => string;\n exportCorrections: (count: number) => string;\n /** A callback rejected and no `formatError` says better. */\n failed: string;\n /** 0.25: the Undo toast after an approval (`undo`) — one string, or a batch of them. */\n approvedToast: (count: number) => string;\n /** 0.25: a group's header under `groupBy` — how many of its rows nobody has read yet,\n * of how many the filters show in it. */\n groupCount: (unreviewed: number, total: number) => string;\n /** 0.25: a group's bulk action — approve its unreviewed rows. */\n approveGroup: (count: number) => string;\n /** 0.25: asked first when a group's bulk approve is larger than a page — more than the\n * reviewer can have had on screen. `group` is the group's name. */\n confirmGroup: (count: number, group: string) => string;\n}\n\nexport const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {\n statusMissing: \"Missing\",\n statusUnreviewed: \"Unreviewed\",\n statusChanged: \"Changed since review\",\n statusNeedsChange: \"Needs a change\",\n statusApproved: \"Approved\",\n statusFilter: \"Status\",\n all: \"All\",\n filterCount: (label, count) => `${label} · ${count}`,\n source: \"Texts\",\n allSources: \"All texts\",\n namespace: \"Area\",\n allNamespaces: \"All areas\",\n search: \"Search keys and text\",\n placeholdersOnly: (count) => `Only placeholder problems (${count})`,\n placeholderChip: \"Placeholders\",\n key: \"Key\",\n statusColumn: \"Status\",\n empty: \"No strings match these filters.\",\n missingText: \"Missing in this language\",\n progress: (approved, total) => `${approved} of ${total} approved`,\n localeProgress: (approved, total) => `${approved}/${total}`,\n locales: \"Languages\",\n approve: \"Approve\",\n flag: \"Needs a change\",\n suggest: \"Suggest translation\",\n reset: \"Mark unreviewed\",\n cancel: \"Cancel\",\n approveSelected: \"Approve selected\",\n resetSelected: \"Mark selected unreviewed\",\n selectShown: \"Select all shown\",\n changedSince: \"This string changed after it was reviewed. Please read it again.\",\n reviewedWording: \"Wording when reviewed\",\n reviewedReference: \"Reference when reviewed\",\n placeholderMismatch: (reference, text) =>\n `The placeholders differ from the reference — reference: ${reference}; this text: ${text}. The app fills these in, so they must stay exactly as they are.`,\n lastApproved: (name, date) => `Approved by ${name} on ${date}`,\n lastFlagged: (name, date) => `Marked as needing a change by ${name} on ${date}`,\n erasedReviewer: \"a deleted account\",\n suggestion: \"Better wording\",\n translation: \"Translation\",\n note: \"Note (optional)\",\n notePlaceholder: \"What is wrong, or what to keep in mind\",\n readOnly: \"You can read this language but not review it.\",\n scope: (areas) => `Your review is limited to: ${areas}.`,\n exportCorrections: (count) => `Export corrections (${count})`,\n failed: \"That did not work. Please try again.\",\n approvedToast: (count) => (count === 1 ? \"String approved\" : `${count} strings approved`),\n groupCount: (unreviewed, total) => `${unreviewed} unreviewed / ${total}`,\n approveGroup: (count) => `Approve unreviewed (${count})`,\n confirmGroup: (count, group) =>\n count === 1\n ? `Approve the unreviewed string in ${group}?`\n : `Approve all ${count} unreviewed strings in ${group}, including those not on screen?`,\n};\n\n/** A status's word, from the namespace's five. */\nexport function reviewStatusLabel(labels: TranslationReviewLabels, status: ReviewStatus): string {\n switch (status) {\n case \"missing\":\n return labels.statusMissing;\n case \"unreviewed\":\n return labels.statusUnreviewed;\n case \"changed\":\n return labels.statusChanged;\n case \"needs_change\":\n return labels.statusNeedsChange;\n case \"approved\":\n return labels.statusApproved;\n }\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/components/translation-review-labels.ts"],"sourcesContent":["import type { ReviewStatus } from \"../lib/translation-review\";\n\n/**\n * The `translationReview` namespace: every word of the translation-review parts\n * (`ReviewStatusChip`, `TranslationProgress`, `TranslationLocaleTabs`,\n * `TranslationExportButton`, `TranslationReviewEditor`, `TranslationReviewPanel`).\n *\n * Its own module, like `data-table-labels.ts`: `kit-labels.tsx` and `defaults.ts` import\n * it, and neither has to load the components to get the words. The English is keksdose's\n * and kastlan's own (`translation_review.*`, `translations:*`), British.\n */\nexport interface TranslationReviewLabels {\n /** The five statuses — the chip, the filter, the progress legend. */\n statusMissing: string;\n statusUnreviewed: string;\n statusChanged: string;\n statusNeedsChange: string;\n statusApproved: string;\n /** Accessible name of the status filter. */\n statusFilter: string;\n /** The status filter's first option. */\n all: string;\n /** A status filter option with its count: \"Unreviewed · 12\". */\n filterCount: (label: string, count: number) => string;\n /** The source filter's label — shown only when the rows come from several sources\n * (kastlan's screen texts and documents). */\n source: string;\n allSources: string;\n /** The namespace filter's label and its \"every one\" option. */\n namespace: string;\n allNamespaces: string;\n search: string;\n /** The placeholder filter, with how many rows it would show. */\n placeholdersOnly: (count: number) => string;\n /** The chip on a row whose placeholders differ from the reference's. */\n placeholderChip: string;\n /** The key column, and the key's caption in the editor. */\n key: string;\n statusColumn: string;\n /** No row matches the filters. */\n empty: string;\n /** In place of the text of a string the locale lacks. */\n missingText: string;\n /** The progress bar's label. */\n progress: (approved: number, total: number) => string;\n /** A locale tab's badge. */\n localeProgress: (approved: number, total: number) => string;\n /** Accessible name of the locale tabs. */\n locales: string;\n approve: string;\n /** Send a string back: \"Needs a change\". */\n flag: string;\n /** \"Needs a change\" for a MISSING string: the suggestion is the translation to add. */\n suggest: string;\n /** Clear a verdict: back to unreviewed. */\n reset: string;\n /** The editor's way out without saving. */\n cancel: string;\n approveSelected: string;\n resetSelected: string;\n /** Selects every row the filters show — the phone's way into the bulk actions, where\n * the table has no checkboxes. */\n selectShown: string;\n /** Over the old wording of a row that changed since its verdict. */\n changedSince: string;\n reviewedWording: string;\n reviewedReference: string;\n /** The placeholders differ: the reference's and the text's, each a list. */\n placeholderMismatch: (reference: string, text: string) => string;\n lastApproved: (name: string, date: string) => string;\n lastFlagged: (name: string, date: string) => string;\n /** The reviewer's name once the account is gone — what `name` above becomes. */\n erasedReviewer: string;\n /** The editor's wording field, opened on the current text. */\n suggestion: string;\n /** The same field for a missing string. */\n translation: string;\n note: string;\n notePlaceholder: string;\n /** The viewer may read this locale but not review it. */\n readOnly: string;\n /** The reviewer is limited to some areas — `areas` is their names, joined. */\n scope: (areas: string) => string;\n exportCorrections: (count: number) => string;\n /** A callback rejected and no `formatError` says better. */\n failed: string;\n /** 0.25: the Undo toast after an approval (`undo`) — one string, or a batch of them. */\n approvedToast: (count: number) => string;\n /** 0.25: a group's header under `groupBy` — how many of its rows nobody has read yet,\n * of how many the filters show in it. */\n groupCount: (unreviewed: number, total: number) => string;\n /** 0.25: a group's bulk action — approve its unreviewed rows. */\n approveGroup: (count: number) => string;\n /** 0.25: asked first when a group's bulk approve is larger than a page — more than the\n * reviewer can have had on screen. `group` is the group's name. */\n confirmGroup: (count: number, group: string) => string;\n /** The Undo toast after a swipe bound to `clear` took a verdict back (keksdose, live\n * #377 rework: the swipes bound in its settings) — one string, or a batch of them.\n * The swipe itself is named by {@link reset}. */\n clearedToast: (count: number) => string;\n}\n\nexport const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {\n statusMissing: \"Missing\",\n statusUnreviewed: \"Unreviewed\",\n statusChanged: \"Changed since review\",\n statusNeedsChange: \"Needs a change\",\n statusApproved: \"Approved\",\n statusFilter: \"Status\",\n all: \"All\",\n filterCount: (label, count) => `${label} · ${count}`,\n source: \"Texts\",\n allSources: \"All texts\",\n namespace: \"Area\",\n allNamespaces: \"All areas\",\n search: \"Search keys and text\",\n placeholdersOnly: (count) => `Only placeholder problems (${count})`,\n placeholderChip: \"Placeholders\",\n key: \"Key\",\n statusColumn: \"Status\",\n empty: \"No strings match these filters.\",\n missingText: \"Missing in this language\",\n progress: (approved, total) => `${approved} of ${total} approved`,\n localeProgress: (approved, total) => `${approved}/${total}`,\n locales: \"Languages\",\n approve: \"Approve\",\n flag: \"Needs a change\",\n suggest: \"Suggest translation\",\n reset: \"Mark unreviewed\",\n cancel: \"Cancel\",\n approveSelected: \"Approve selected\",\n resetSelected: \"Mark selected unreviewed\",\n selectShown: \"Select all shown\",\n changedSince: \"This string changed after it was reviewed. Please read it again.\",\n reviewedWording: \"Wording when reviewed\",\n reviewedReference: \"Reference when reviewed\",\n placeholderMismatch: (reference, text) =>\n `The placeholders differ from the reference — reference: ${reference}; this text: ${text}. The app fills these in, so they must stay exactly as they are.`,\n lastApproved: (name, date) => `Approved by ${name} on ${date}`,\n lastFlagged: (name, date) => `Marked as needing a change by ${name} on ${date}`,\n erasedReviewer: \"a deleted account\",\n suggestion: \"Better wording\",\n translation: \"Translation\",\n note: \"Note (optional)\",\n notePlaceholder: \"What is wrong, or what to keep in mind\",\n readOnly: \"You can read this language but not review it.\",\n scope: (areas) => `Your review is limited to: ${areas}.`,\n exportCorrections: (count) => `Export corrections (${count})`,\n failed: \"That did not work. Please try again.\",\n approvedToast: (count) => (count === 1 ? \"String approved\" : `${count} strings approved`),\n groupCount: (unreviewed, total) => `${unreviewed} unreviewed / ${total}`,\n approveGroup: (count) => `Approve unreviewed (${count})`,\n confirmGroup: (count, group) =>\n count === 1\n ? `Approve the unreviewed string in ${group}?`\n : `Approve all ${count} unreviewed strings in ${group}, including those not on screen?`,\n clearedToast: (count) => (count === 1 ? \"String marked unreviewed\" : `${count} strings marked unreviewed`),\n};\n\n/** A status's word, from the namespace's five. */\nexport function reviewStatusLabel(labels: TranslationReviewLabels, status: ReviewStatus): string {\n switch (status) {\n case \"missing\":\n return labels.statusMissing;\n case \"unreviewed\":\n return labels.statusUnreviewed;\n case \"changed\":\n return labels.statusChanged;\n case \"needs_change\":\n return labels.statusNeedsChange;\n case \"approved\":\n return labels.statusApproved;\n }\n}\n"],"mappings":";AAsGO,MAAM,oCAA6D;AAAA,EACxE,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,KAAK;AAAA,EACL,aAAa,CAAC,OAAO,UAAU,GAAG,KAAK,SAAM,KAAK;AAAA,EAClD,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,eAAe;AAAA,EACf,QAAQ;AAAA,EACR,kBAAkB,CAAC,UAAU,8BAA8B,KAAK;AAAA,EAChE,iBAAiB;AAAA,EACjB,KAAK;AAAA,EACL,cAAc;AAAA,EACd,OAAO;AAAA,EACP,aAAa;AAAA,EACb,UAAU,CAAC,UAAU,UAAU,GAAG,QAAQ,OAAO,KAAK;AAAA,EACtD,gBAAgB,CAAC,UAAU,UAAU,GAAG,QAAQ,IAAI,KAAK;AAAA,EACzD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,qBAAqB,CAAC,WAAW,SAC/B,gEAA2D,SAAS,gBAAgB,IAAI;AAAA,EAC1F,cAAc,CAAC,MAAM,SAAS,eAAe,IAAI,OAAO,IAAI;AAAA,EAC5D,aAAa,CAAC,MAAM,SAAS,iCAAiC,IAAI,OAAO,IAAI;AAAA,EAC7E,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,MAAM;AAAA,EACN,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,OAAO,CAAC,UAAU,8BAA8B,KAAK;AAAA,EACrD,mBAAmB,CAAC,UAAU,uBAAuB,KAAK;AAAA,EAC1D,QAAQ;AAAA,EACR,eAAe,CAAC,UAAW,UAAU,IAAI,oBAAoB,GAAG,KAAK;AAAA,EACrE,YAAY,CAAC,YAAY,UAAU,GAAG,UAAU,iBAAiB,KAAK;AAAA,EACtE,cAAc,CAAC,UAAU,uBAAuB,KAAK;AAAA,EACrD,cAAc,CAAC,OAAO,UACpB,UAAU,IACN,oCAAoC,KAAK,MACzC,eAAe,KAAK,0BAA0B,KAAK;AAAA,EACzD,cAAc,CAAC,UAAW,UAAU,IAAI,6BAA6B,GAAG,KAAK;AAC/E;AAGO,SAAS,kBAAkB,QAAiC,QAA8B;AAC/F,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,EAClB;AACF;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { TranslationRow, TranslationReviewWrite, TranslationReviewKey, TranslationReviewFilter } from '../lib/translation-review.js';
|
|
3
|
+
import { TranslationRow, TranslationReviewWrite, TranslationReviewKey, TranslationReviewFilter, TranslationReviewSwipeBinding } from '../lib/translation-review.js';
|
|
4
4
|
import { TranslationReviewLabels } from './translation-review-labels.js';
|
|
5
5
|
export { DEFAULT_TRANSLATION_REVIEW_LABELS } from './translation-review-labels.js';
|
|
6
6
|
export { REVIEW_STATUS_TONES, ReviewStatusChip, ReviewStatusChipProps, TranslationExportButton, TranslationExportButtonProps, TranslationLocaleTab, TranslationLocaleTabs, TranslationLocaleTabsProps, TranslationProgress, TranslationProgressProps } from './translation-review-parts.js';
|
|
@@ -46,7 +46,8 @@ interface TranslationReviewSaveInfo {
|
|
|
46
46
|
/** The open editor, a row's approve button, a swipe on a phone card, a group's
|
|
47
47
|
* "Approve unreviewed", the selection's bulk bar, or an Undo pressed on the toast. */
|
|
48
48
|
origin: TranslationReviewOrigin;
|
|
49
|
-
/** The panel shows its own toast for this write (an approval's Undo toast
|
|
49
|
+
/** The panel shows its own toast for this write (an approval's Undo toast, or a
|
|
50
|
+
* swiped `clear`'s). */
|
|
50
51
|
toasted: boolean;
|
|
51
52
|
}
|
|
52
53
|
/** How {@link TranslationReviewPanelProps.groupBy} names a row's group. */
|
|
@@ -124,8 +125,31 @@ interface TranslationReviewPanelProps {
|
|
|
124
125
|
*
|
|
125
126
|
* Off by default, so no existing page gains a gesture on the bump. Phones only; the
|
|
126
127
|
* desktop table keeps its approve button.
|
|
128
|
+
*
|
|
129
|
+
* A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *"Add
|
|
130
|
+
* the swipe options to be shown also to the settings /settings#interaction area where
|
|
131
|
+
* the other swipe options are defined"*): keksdose lets the user bind every list's
|
|
132
|
+
* swipes in Settings → Interaction, per side a primary action at the first threshold and
|
|
133
|
+
* a secondary one at a longer drag, and a fixed mapping is one its settings cannot
|
|
134
|
+
* reach. So each logical side takes an ordered ladder of
|
|
135
|
+
* {@link TranslationReviewSwipeAction}s — index 0 at the first threshold, index 1 at the
|
|
136
|
+
* longer drag, as {@link SwipeableRow} stages them:
|
|
137
|
+
*
|
|
138
|
+
* swipe={{ end: ["approve", "clear"], start: ["edit"] }}
|
|
139
|
+
*
|
|
140
|
+
* `approve` (with Undo), `edit` (the editor, cursor in the wording — "Needs a change"),
|
|
141
|
+
* `clear` (the verdict taken back, with Undo, which stores it again; only with
|
|
142
|
+
* `onClear`). An action a row cannot offer drops out and the ladder closes up behind it
|
|
143
|
+
* ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,
|
|
144
|
+
* `clear` on a row with no verdict. Every rule above still holds for every binding —
|
|
145
|
+
* no side swipes in a `readOnly` locale, under a write lock, with the editor open or
|
|
146
|
+
* the row's write out.
|
|
147
|
+
*
|
|
148
|
+
* `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: ["approve"], start:
|
|
149
|
+
* ["edit"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding
|
|
150
|
+
* turns the Undo toast on by default, as `true` does.
|
|
127
151
|
*/
|
|
128
|
-
swipe?: boolean;
|
|
152
|
+
swipe?: boolean | TranslationReviewSwipeBinding;
|
|
129
153
|
/**
|
|
130
154
|
* Answer every approval with the kit's Undo toast (`toast.undo`): a row's button, a
|
|
131
155
|
* swipe, the editor's Approve, a group's and the selection's bulk approve. Undo stores
|
|
@@ -7,6 +7,7 @@ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
|
7
7
|
import { cn } from "../lib/cn.js";
|
|
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,6 +15,7 @@ import {
|
|
|
14
15
|
reviewUndo,
|
|
15
16
|
reviewWrite,
|
|
16
17
|
summariseRows,
|
|
18
|
+
translationReviewSwipePlan,
|
|
17
19
|
unreviewedRows
|
|
18
20
|
} from "../lib/translation-review.js";
|
|
19
21
|
import { AlertBanner } from "./alert-banner.js";
|
|
@@ -124,7 +126,8 @@ function TranslationReviewPanel({
|
|
|
124
126
|
const baseId = useId();
|
|
125
127
|
const rootRef = useRef(null);
|
|
126
128
|
const editable = !readOnly && onSave !== void 0;
|
|
127
|
-
const
|
|
129
|
+
const binding = swipe === true ? DEFAULT_TRANSLATION_REVIEW_SWIPE : swipe || null;
|
|
130
|
+
const undoOn = undoProp ?? (binding !== null || groupBy !== void 0);
|
|
128
131
|
const [ownFilter, setOwnFilter] = useState(DEFAULT_TRANSLATION_REVIEW_FILTER);
|
|
129
132
|
const filter = { ...ownFilter };
|
|
130
133
|
for (const [name, value] of Object.entries(filterProp ?? {})) {
|
|
@@ -202,9 +205,9 @@ function TranslationReviewPanel({
|
|
|
202
205
|
tail.current = step;
|
|
203
206
|
return step;
|
|
204
207
|
};
|
|
205
|
-
const
|
|
208
|
+
const sayDone = (targets, description, cleared = false) => {
|
|
206
209
|
const back = reviewUndo(targets);
|
|
207
|
-
const message = labels.approvedToast(targets.length);
|
|
210
|
+
const message = (cleared ? labels.clearedToast : labels.approvedToast)(targets.length);
|
|
208
211
|
const save = onSave;
|
|
209
212
|
const clear = onClear;
|
|
210
213
|
if (!save || back.clears.length > 0 && !clear) {
|
|
@@ -224,13 +227,20 @@ function TranslationReviewPanel({
|
|
|
224
227
|
if (!onSave || targets.length === 0) return false;
|
|
225
228
|
const save = onSave;
|
|
226
229
|
const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, "APPROVED")), { origin, toasted: undoOn }));
|
|
227
|
-
if (ok && undoOn)
|
|
230
|
+
if (ok && undoOn) sayDone(targets, description);
|
|
228
231
|
return ok;
|
|
229
232
|
};
|
|
233
|
+
const clearRow = async (row) => {
|
|
234
|
+
if (!onClear || row.review === null) return;
|
|
235
|
+
const clear = onClear;
|
|
236
|
+
const keys = [{ locale: row.locale, key: row.key }];
|
|
237
|
+
const ok = await run(`row:${row.id}`, () => clear(keys, { origin: "swipe", toasted: undoOn }));
|
|
238
|
+
if (ok && undoOn) sayDone([row], row.key, true);
|
|
239
|
+
};
|
|
230
240
|
const saveFromEditor = async (row, write) => {
|
|
231
241
|
const toasted = undoOn && write.verdict === "APPROVED";
|
|
232
242
|
await onSave?.([write], { origin: "editor", toasted });
|
|
233
|
-
if (toasted)
|
|
243
|
+
if (toasted) sayDone([row], row.key);
|
|
234
244
|
};
|
|
235
245
|
const keepFocusAt = (groupKey) => {
|
|
236
246
|
const check = () => {
|
|
@@ -329,21 +339,21 @@ function TranslationReviewPanel({
|
|
|
329
339
|
)
|
|
330
340
|
});
|
|
331
341
|
}
|
|
332
|
-
const
|
|
333
|
-
if (!editable || lock.locked || isQueued(`row:${r.id}`)) return null;
|
|
342
|
+
const swipeAction = (r, id, groupKey) => {
|
|
334
343
|
const missing = r.text === "";
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
{
|
|
344
|
+
switch (id) {
|
|
345
|
+
case "approve":
|
|
346
|
+
return {
|
|
338
347
|
label: labels.approve,
|
|
339
348
|
icon: /* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
340
349
|
onCommit: () => void approve(`row:${r.id}`, [r], "swipe", r.key),
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
350
|
+
// Text on the fill in its own contrast colour: the fills are pastels in dark
|
|
351
|
+
// mode, where SwipeableRow's default white text was unreadable (0.26).
|
|
352
|
+
className: "bg-[var(--success)] text-[var(--success-contrast)]",
|
|
353
|
+
armedClassName: "bg-[var(--success)] text-[var(--success-contrast)]"
|
|
354
|
+
};
|
|
355
|
+
case "edit":
|
|
356
|
+
return {
|
|
347
357
|
label: missing ? labels.suggest : labels.flag,
|
|
348
358
|
icon: /* @__PURE__ */ jsx(PencilLine, { className: "size-4", "aria-hidden": true }),
|
|
349
359
|
// Opens the editor rather than sending the string back: "needs a change"
|
|
@@ -353,10 +363,29 @@ function TranslationReviewPanel({
|
|
|
353
363
|
setOpenId(r.id);
|
|
354
364
|
setFocusId(r.id);
|
|
355
365
|
},
|
|
356
|
-
className: missing ? "bg-[var(--brand)]" : "bg-[var(--danger)]",
|
|
357
|
-
armedClassName: missing ? "bg-[var(--brand-hover)]" : "bg-[var(--danger-hover)]"
|
|
358
|
-
}
|
|
359
|
-
|
|
366
|
+
className: missing ? "bg-[var(--brand)] text-[var(--brand-contrast)]" : "bg-[var(--danger)] text-[var(--danger-contrast)]",
|
|
367
|
+
armedClassName: missing ? "bg-[var(--brand-hover)] text-[var(--brand-contrast)]" : "bg-[var(--danger-hover)] text-[var(--danger-contrast)]"
|
|
368
|
+
};
|
|
369
|
+
case "clear":
|
|
370
|
+
return {
|
|
371
|
+
label: labels.reset,
|
|
372
|
+
icon: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4", "aria-hidden": true }),
|
|
373
|
+
onCommit: () => void clearRow(r),
|
|
374
|
+
// A step back, not a verdict: neither the approval's green nor the send-back's red.
|
|
375
|
+
// The surface colour as text: white on the grey in light mode, the dark page on
|
|
376
|
+
// the light grey in dark mode.
|
|
377
|
+
className: "bg-[var(--text-muted)] text-[var(--bg-surface)]",
|
|
378
|
+
armedClassName: "bg-[var(--text-secondary)] text-[var(--bg-surface)]"
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const swipeActions = (r, groupKey) => {
|
|
383
|
+
if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`)) return null;
|
|
384
|
+
const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== void 0 });
|
|
385
|
+
if (plan.start.length === 0 && plan.end.length === 0) return null;
|
|
386
|
+
return {
|
|
387
|
+
start: plan.start.map((id) => swipeAction(r, id, groupKey)),
|
|
388
|
+
end: plan.end.map((id) => swipeAction(r, id, groupKey))
|
|
360
389
|
};
|
|
361
390
|
};
|
|
362
391
|
const toggleIn = (prev, many, checked) => {
|
|
@@ -431,7 +460,7 @@ function TranslationReviewPanel({
|
|
|
431
460
|
r.placeholderMismatch && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "danger", variant: "outline", children: labels.placeholderChip })
|
|
432
461
|
] })
|
|
433
462
|
] }),
|
|
434
|
-
mobileSwipeActions:
|
|
463
|
+
mobileSwipeActions: binding ? (r) => swipeActions(r, group?.key) : void 0,
|
|
435
464
|
selection: selectionFor(list)
|
|
436
465
|
}
|
|
437
466
|
);
|