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

@@ -106,7 +106,7 @@ const Hero = () => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<div className="inline-flex items-center gap-2 px-4 py-1.5 mb-10 text-[10px] font-bold tracking-[0.3em] text-brand-dark/40 dark:text-white/40 uppercase bg-transparent border border-black/10 dark:border-white/10 rounded-full">
<div className="inline-flex items-center gap-2 px-4 py-1.5 mb-10 text-[10px] font-bold tracking-[0.3em] text-brand-dark/60 dark:text-white/60 uppercase bg-transparent border border-black/10 dark:border-white/10 rounded-full">
<Languages size={10} /> {t('landing.hero.tag')}
</div>
<h1 className="text-6xl md:text-8xl mb-10 text-brand-dark dark:text-white font-serif font-medium tracking-tight leading-[1.1]">
@@ -187,7 +187,7 @@ const Steps = () => {
<div className="mb-24">
<span className="accent-pill mb-6 block w-fit">{t('landing.steps.process')}</span>
<h2 className="text-4xl md:text-5xl text-brand-dark dark:text-white font-serif font-medium tracking-tight mb-6">{t('landing.steps.title')}</h2>
<p className="text-brand-dark/40 dark:text-white/40 text-lg max-w-xl font-light">{t('landing.steps.subtitle')}</p>
<p className="text-brand-dark/60 dark:text-white/60 text-lg max-w-xl font-light">{t('landing.steps.subtitle')}</p>
</div>
<div className="grid md:grid-cols-3 gap-16">
@@ -197,7 +197,7 @@ const Steps = () => {
{st.icon}
</div>
<h3 className="text-xl font-serif font-medium text-brand-dark dark:text-white mb-4">{st.title}</h3>
<p className="text-brand-dark/40 dark:text-white/40 leading-relaxed font-light">{st.desc}</p>
<p className="text-brand-dark/60 dark:text-white/60 leading-relaxed font-light">{st.desc}</p>
</div>
))}
</div>
@@ -222,9 +222,9 @@ const FeaturesContext = () => {
<div className="grid md:grid-cols-3 gap-8">
{[
{ icon: <Monitor className="text-brand-dark/40 dark:text-white/40" />, title: t('landing.features.context.title'), desc: t('landing.features.context.desc') },
{ icon: <Layers className="text-brand-dark/40 dark:text-white/40" />, title: t('landing.features.glossary.title'), desc: t('landing.features.glossary.desc') },
{ icon: <Eye className="text-brand-dark/40 dark:text-white/40" />, title: t('landing.features.vision.title'), desc: t('landing.features.vision.desc') },
{ icon: <Monitor className="text-brand-dark/60 dark:text-white/60" />, title: t('landing.features.context.title'), desc: t('landing.features.context.desc') },
{ icon: <Layers className="text-brand-dark/60 dark:text-white/60" />, title: t('landing.features.glossary.title'), desc: t('landing.features.glossary.desc') },
{ icon: <Eye className="text-brand-dark/60 dark:text-white/60" />, title: t('landing.features.vision.title'), desc: t('landing.features.vision.desc') },
].map((item, i) => (
<div key={i} className="editorial-card p-10 flex flex-col items-start gap-8">
<div className="w-10 h-10 border border-black/5 dark:border-white/10 rounded-full flex items-center justify-center">
@@ -232,7 +232,7 @@ const FeaturesContext = () => {
</div>
<div>
<h4 className="text-xl font-serif font-medium mb-4 text-brand-dark dark:text-white">{item.title}</h4>
<p className="text-brand-dark/40 dark:text-white/40 leading-relaxed font-light text-sm">{item.desc}</p>
<p className="text-brand-dark/60 dark:text-white/60 leading-relaxed font-light text-sm">{item.desc}</p>
</div>
</div>
))}
@@ -283,10 +283,10 @@ const LayoutFeatures = () => {
const FormatsSection = () => {
const { t } = useI18n();
const formats = [
{ id: ".DOCX", name: t('landing.formats.word.name'), icon: <FileText className="text-brand-dark/40 dark:text-white/40" />, items: [t('landing.formats.word.i1'), t('landing.formats.word.i2'), t('landing.formats.word.i3'), t('landing.formats.word.i4')] },
{ id: ".XLSX", name: t('landing.formats.excel.name'), icon: <FileSpreadsheet className="text-brand-dark/40 dark:text-white/40" />, items: [t('landing.formats.excel.i1'), t('landing.formats.excel.i2'), t('landing.formats.excel.i3'), t('landing.formats.excel.i4')] },
{ id: ".PPTX", name: t('landing.formats.pptx.name'), icon: <Presentation className="text-brand-dark/40 dark:text-white/40" />, items: [t('landing.formats.pptx.i1'), t('landing.formats.pptx.i2'), t('landing.formats.pptx.i3'), t('landing.formats.pptx.i4')] },
{ id: ".PDF", name: t('landing.formats.pdf.name'), icon: <FileText className="text-brand-dark/40 dark:text-white/40" />, items: [t('landing.formats.pdf.i1'), t('landing.formats.pdf.i2'), t('landing.formats.pdf.i3'), t('landing.formats.pdf.i4')] },
{ id: ".DOCX", name: t('landing.formats.word.name'), icon: <FileText className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.word.i1'), t('landing.formats.word.i2'), t('landing.formats.word.i3'), t('landing.formats.word.i4')] },
{ id: ".XLSX", name: t('landing.formats.excel.name'), icon: <FileSpreadsheet className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.excel.i1'), t('landing.formats.excel.i2'), t('landing.formats.excel.i3'), t('landing.formats.excel.i4')] },
{ id: ".PPTX", name: t('landing.formats.pptx.name'), icon: <Presentation className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.pptx.i1'), t('landing.formats.pptx.i2'), t('landing.formats.pptx.i3'), t('landing.formats.pptx.i4')] },
{ id: ".PDF", name: t('landing.formats.pdf.name'), icon: <FileText className="text-brand-dark/60 dark:text-white/60" />, items: [t('landing.formats.pdf.i1'), t('landing.formats.pdf.i2'), t('landing.formats.pdf.i3'), t('landing.formats.pdf.i4')] },
];
return (
@@ -295,7 +295,7 @@ const FormatsSection = () => {
<div className="mb-24">
<span className="accent-pill mb-6 block w-fit">{t('landing.formats.pill')}</span>
<h2 className="text-4xl md:text-5xl text-brand-dark dark:text-white mb-6 leading-tight font-serif font-medium tracking-tight">{t('landing.formats.title')} <span className="italic">{t('landing.formats.title2')}</span></h2>
<p className="text-brand-dark/40 dark:text-white/40 max-w-xl text-lg font-light">{t('landing.formats.subtitle')}</p>
<p className="text-brand-dark/60 dark:text-white/60 max-w-xl text-lg font-light">{t('landing.formats.subtitle')}</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{formats.map((f, i) => (
@@ -304,12 +304,12 @@ const FormatsSection = () => {
<div className="w-12 h-12 bg-brand-muted dark:bg-[#1f1f1f] rounded-xl flex items-center justify-center text-xl group-hover:bg-brand-dark dark:group-hover:bg-brand-accent group-hover:text-white dark:group-hover:text-brand-dark transition-all">
{f.icon}
</div>
<span className="text-[9px] font-bold text-brand-dark/20 dark:text-white/20 uppercase tracking-[0.2em]">{f.id}</span>
<span className="text-[9px] font-bold text-brand-dark/50 dark:text-white/50 uppercase tracking-[0.2em]">{f.id}</span>
</div>
<h3 className="text-xl font-serif font-medium mb-6 text-brand-dark dark:text-white tracking-tight">{f.name}</h3>
<ul className="space-y-4">
{f.items.map((item, j) => (
<li key={j} className="flex items-start gap-3 text-xs text-brand-dark/40 dark:text-white/40 font-medium">
<li key={j} className="flex items-start gap-3 text-xs text-brand-dark/60 dark:text-white/60 font-medium">
<div className="w-1 h-1 bg-brand-accent rounded-full mt-1.5 shrink-0 opacity-40"></div> {item}
</li>
))}
@@ -337,14 +337,14 @@ const PricingSection = () => {
<div className="max-w-7xl mx-auto">
<div className="text-center mb-24">
<h2 className="text-4xl md:text-5xl mb-8 leading-tight text-brand-dark dark:text-white font-serif font-medium tracking-tight">{t('landing.pricing.title')}</h2>
<p className="text-brand-dark/40 dark:text-white/40 text-xl font-light max-w-2xl mx-auto">{t('landing.pricing.subtitle')}</p>
<p className="text-brand-dark/60 dark:text-white/60 text-xl font-light max-w-2xl mx-auto">{t('landing.pricing.subtitle')}</p>
</div>
<div className="flex items-center justify-center gap-12 mb-32">
<button onClick={() => setIsAnnual(false)} className={`text-sm font-bold uppercase tracking-widest transition-all ${!isAnnual ? 'text-brand-dark dark:text-white underline underline-offset-8' : 'text-brand-dark/30 dark:text-white/30 hover:text-brand-dark dark:hover:text-white'}`}>
<button onClick={() => setIsAnnual(false)} className={`text-sm font-bold uppercase tracking-widest transition-all ${!isAnnual ? 'text-brand-dark dark:text-white underline underline-offset-8' : 'text-brand-dark/55 dark:text-white/55 hover:text-brand-dark dark:hover:text-white'}`}>
{t('landing.pricing.monthly')}
</button>
<button onClick={() => setIsAnnual(true)} className={`text-sm font-bold uppercase tracking-widest transition-all relative ${isAnnual ? 'text-brand-dark dark:text-white underline underline-offset-8' : 'text-brand-dark/30 dark:text-white/30 hover:text-brand-dark dark:hover:text-white'}`}>
<button onClick={() => setIsAnnual(true)} className={`text-sm font-bold uppercase tracking-widest transition-all relative ${isAnnual ? 'text-brand-dark dark:text-white underline underline-offset-8' : 'text-brand-dark/55 dark:text-white/55 hover:text-brand-dark dark:hover:text-white'}`}>
{t('landing.pricing.annual')}
<span className="absolute -top-6 left-1/2 -translate-x-1/2 text-[9px] font-bold text-brand-accent whitespace-nowrap italic">
(-20%)
@@ -356,11 +356,11 @@ const PricingSection = () => {
{plans.map((p, i) => (
<div key={i} className="p-16 bg-white dark:bg-[#141414] flex flex-col text-left transition-all duration-500 hover:z-10 hover:shadow-2xl hover:scale-[1.01]">
<h3 className="text-2xl font-serif font-medium mb-2 text-brand-dark dark:text-white tracking-tight">{p.name}</h3>
<p className="text-brand-dark/40 dark:text-white/40 text-xs font-light mb-14 tracking-widest uppercase">{p.desc}</p>
<p className="text-brand-dark/60 dark:text-white/60 text-xs font-light mb-14 tracking-widest uppercase">{p.desc}</p>
<div className="mb-14">
<span className="text-5xl font-serif font-medium leading-none text-brand-dark dark:text-white">{isAnnual ? p.annualPrice : p.monthlyPrice}</span>
<span className="text-brand-dark/30 dark:text-white/30 font-light ml-2 uppercase text-[10px] tracking-[0.2em]">/ {t('landing.pricing.month')}</span>
<span className="text-brand-dark/55 dark:text-white/55 font-light ml-2 uppercase text-[10px] tracking-[0.2em]">/ {t('landing.pricing.month')}</span>
</div>
<ul className="space-y-6 mb-16 flex-grow">
@@ -381,7 +381,7 @@ const PricingSection = () => {
))}
</div>
<p className="text-center mt-12 text-brand-dark/20 dark:text-white/20 text-xs font-bold uppercase tracking-widest">{t('landing.pricing.footer')}</p>
<p className="text-center mt-12 text-brand-dark/50 dark:text-white/50 text-xs font-bold uppercase tracking-widest">{t('landing.pricing.footer')}</p>
</div>
</section>
);
@@ -395,7 +395,7 @@ const CTASection = () => {
<h2 className="text-5xl md:text-7xl text-brand-dark dark:text-white mb-10 leading-tight tracking-tight font-serif font-medium">
{t('landing.cta.title')}
</h2>
<p className="text-lg text-brand-dark/40 dark:text-white/40 mb-16 font-light">
<p className="text-lg text-brand-dark/60 dark:text-white/60 mb-16 font-light">
{t('landing.cta.subtitle')}
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center items-center">
@@ -416,14 +416,14 @@ const Footer = () => {
<div className="grid md:grid-cols-12 gap-20 mb-40">
<div className="md:col-span-12 lg:col-span-6">
<Logo />
<p className="text-brand-dark/40 dark:text-white/40 text-lg font-medium mt-12 mb-12 max-w-sm leading-relaxed uppercase tracking-widest">
<p className="text-brand-dark/60 dark:text-white/60 text-lg font-medium mt-12 mb-12 max-w-sm leading-relaxed uppercase tracking-widest">
{t('landing.footer.desc')}
</p>
</div>
<div className="md:col-span-4 lg:col-span-2">
<h4 className="text-[10px] font-bold uppercase tracking-[0.3em] text-brand-dark dark:text-white mb-10">{t('landing.footer.product')}</h4>
<ul className="space-y-6 text-xs text-brand-dark/40 dark:text-white/40 font-bold uppercase tracking-widest leading-none">
<ul className="space-y-6 text-xs text-brand-dark/60 dark:text-white/60 font-bold uppercase tracking-widest leading-none">
<li><a href="#features" className="hover:text-brand-dark dark:hover:text-white transition-colors">{t('landing.nav.why')}</a></li>
<li><a href="#formats" className="hover:text-brand-dark dark:hover:text-white transition-colors">{t('landing.nav.formats')}</a></li>
<li><a href="#pricing" className="hover:text-brand-dark dark:hover:text-white transition-colors">{t('landing.nav.pricing')}</a></li>
@@ -433,11 +433,20 @@ const Footer = () => {
</div>
<div className="pt-10 border-t border-black/5 dark:border-white/5 flex flex-col md:flex-row justify-between items-center gap-10">
<div className="text-[10px] font-bold text-brand-dark/20 dark:text-white/20 uppercase tracking-[0.4em]">
<div className="text-[10px] font-bold text-brand-dark/50 dark:text-white/50 uppercase tracking-[0.4em]">
{t('landing.footer.rights')}
</div>
<div className="text-[10px] font-mono text-brand-dark/30 dark:text-white/30 uppercase tracking-widest">
wordly.art
<div className="flex items-center gap-6">
<button
type="button"
onClick={() => import('@/components/ui/cookie-consent').then(m => m.reopenCookieConsent())}
className="text-[10px] font-bold text-brand-dark/50 dark:text-white/50 uppercase tracking-[0.2em] hover:text-brand-dark dark:hover:text-white transition-colors"
>
{t('cookieConsent.reopen')}
</button>
<div className="text-[10px] font-mono text-brand-dark/55 dark:text-white/55 uppercase tracking-widest">
wordly.art
</div>
</div>
</div>
</div>