fix(ui): wave 2 from critique re-run — AA contrast, unified PageHeader, checkout confirm
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m46s

Nav: teams/settings/services back out of the nav until product-ready (user
decision); teams page was already half-finished (UUID member display is a
backend limitation).

Payment: explicit confirmation dialog (plan, amount, billing period, Stripe
note) before any redirect; ?plan= URL now pre-opens the dialog instead of
triggering a silent checkout.

Contrast AA sweep (71 replacements): functional micro-labels raised to
>=60-65% opacity and >=10px across sidebar, header, pricing, landing;
decorative all-caps 'interface' label removed.

Design unification: new PageHeader component (accent pill + serif
base/accent title) applied to settings, services, reviews, teams; pricing
header returned to the editorial voice (serif + accent pill); dead
GlossaryCard deleted.

i18n residuals: suggestion chips, 'Standard' provider label, notification
close, model-combobox strings extracted (+broken bg-surface/border-border-
subtle tokens fixed); ~45 new keys EN+FR; 13 dead keys removed; zero
missing keys verified.

Reviews: icon-only row actions now carry visible text; 'Approve all' is
two-step armed-confirm.

Accelerators: Ctrl/Cmd+Enter submits; arrow-key navigation in the language
combobox; recent-jobs history cap 8->20 with per-job download; source=target
config rejected; empty 'Master Quality' badge removed.

Cookie consent: reopenable via footer link, emoji replaced with drawn icon.

Verified: build exit 0, vitest 9/9, eslint 64 errors = previous level,
0 missing i18n keys.
This commit is contained in:
2026-08-30 22:02:35 +02:00
parent 50047ea8a2
commit 111f3cb69d
41 changed files with 470 additions and 280 deletions

View File

@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/toast';
import { useI18n } from '@/lib/i18n';
import { PageHeader } from '@/components/layout/page-header';
import { cn } from '@/lib/utils';
interface Segment {
@@ -77,6 +78,7 @@ export default function ReviewPage() {
const [draft, setDraft] = useState('');
const [savingId, setSavingId] = useState<string | null>(null);
const [isRebuilding, setIsRebuilding] = useState(false);
const [approveArmed, setApproveArmed] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -136,6 +138,13 @@ export default function ReviewPage() {
};
const approveAll = async () => {
// Two-step: the first click arms, the second confirms — no accidental bulk-approve.
if (!approveArmed) {
setApproveArmed(true);
setTimeout(() => setApproveArmed(false), 4000);
return;
}
setApproveArmed(false);
const pending = segments.filter((s) => s.status === 'pending');
for (const seg of pending) {
// sequential is fine — server-side each is a tiny PATCH
@@ -232,10 +241,13 @@ export default function ReviewPage() {
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="text-xl font-semibold text-foreground">{t('reviews.title')}</h1>
<p className="text-sm text-muted-foreground truncate max-w-xl">
<span className="accent-pill mb-3 block w-fit">{t('reviews.pill')}</span>
<h1 className="mb-2 text-3xl font-serif font-medium leading-tight tracking-tight text-brand-dark dark:text-white">
{t('reviews.titleBase')} <span className="italic">{t('reviews.titleAccent')}</span>
</h1>
<p className="max-w-xl truncate text-sm text-brand-dark/60 dark:text-white/60">
{t('reviews.subtitle', {
file: fileName ?? jobId,
total: counts.total,
@@ -246,8 +258,11 @@ export default function ReviewPage() {
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={approveAll} disabled={counts.pending === 0}>
<CheckCheck className="size-3.5" /> {t('reviews.approveAll')}
<Button variant={approveArmed ? 'default' : 'outline'} size="sm" onClick={approveAll} disabled={counts.pending === 0}>
<CheckCheck className="size-3.5" />
{approveArmed
? t('reviews.approveAllConfirm', { count: counts.pending })
: t('reviews.approveAll')}
</Button>
<Button variant="outline" size="sm" onClick={exportXliff} disabled={!segments.length}>
<FileDown className="size-3.5" /> XLIFF
@@ -360,38 +375,35 @@ export default function ReviewPage() {
<div className="flex flex-col items-start gap-2">
{statusBadge(seg.status)}
{!isEditing && (
<div className="flex gap-1">
<div className="flex flex-wrap gap-1">
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
title={t('reviews.action.edit')}
className="h-7 gap-1 px-2 text-[11px]"
onClick={() => {
setEditingId(seg.id);
setDraft(finalText);
}}
>
<Pencil className="size-3" />
<Pencil className="size-3" /> {t('reviews.action.edit')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
title={t('reviews.action.approve')}
className="h-7 gap-1 px-2 text-[11px]"
disabled={seg.status === 'approved' || savingId === seg.id}
onClick={() => patchSegment(seg.id, { status: 'approved' })}
>
<Check className="size-3" />
<Check className="size-3" /> {t('reviews.action.approveShort')}
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
title={t('reviews.action.reset')}
className="h-7 gap-1 px-2 text-[11px]"
disabled={seg.status === 'pending'}
onClick={() => patchSegment(seg.id, { status: 'pending' })}
>
<Undo2 className="size-3" />
<Undo2 className="size-3" /> {t('reviews.action.resetShort')}
</Button>
</div>
)}