fix(ui): thème, textes et lisibilité restants de la revue
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m57s
CI / Deploy production (on server) (push) Successful in 24s

Les boutons suivent la couleur d’apparence, les libellés trop petits ou trop techniques sont clarifiés, et le catalogue des fournisseurs se met à jour tout seul.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-08-30 21:13:07 +00:00
parent ebf6f16fde
commit afbb0dfc2d
77 changed files with 2842 additions and 1546 deletions

View File

@@ -11,6 +11,7 @@ import { useLanguage } from '@/lib/i18n'
import type { SupportedLanguage } from '@/lib/i18n/load-translations'
import { SUBSCRIPTION_TRIAL_DAYS } from '@/lib/billing/trial-constants'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import { useQuery } from '@tanstack/react-query'
const ECHO_LINES = ['echo0', 'echo1', 'echo2'] as const
@@ -39,6 +40,16 @@ export function LandingPage() {
const [langOpen, setLangOpen] = useState(false)
const [echoIndex, setEchoIndex] = useState(0)
const langRef = useRef<HTMLDivElement>(null)
const { data: byokCatalog } = useQuery({
queryKey: ['public', 'byok-catalog'],
queryFn: async () => {
const res = await fetch('/api/public/byok-catalog')
if (!res.ok) throw new Error('catalog')
return res.json() as Promise<{ providers: { id: string; name: string }[] }>
},
staleTime: 60_000,
})
const byokProviders = byokCatalog?.providers ?? []
useEffect(() => {
if (!langOpen) return
@@ -102,6 +113,22 @@ export function LandingPage() {
{ href: '#pricing', label: t('landing.nav.pricing') },
]
const scrollPublicHash = (hash: string) => {
const id = hash.replace(/^#/, '')
const target = document.getElementById(id)
const root = document.querySelector<HTMLElement>('[data-public-scroll-root]')
if (!target || !root) return
const top = target.getBoundingClientRect().top - root.getBoundingClientRect().top + root.scrollTop - 88
root.scrollTo({ top, behavior: 'smooth' })
}
useEffect(() => {
const hash = window.location.hash
if (!hash) return
const id = window.requestAnimationFrame(() => scrollPublicHash(hash))
return () => window.cancelAnimationFrame(id)
}, [])
return (
<div className="min-h-screen bg-[#0B0A09] text-[#F4F1EA] font-[family-name:var(--font-manrope)] selection:bg-[#D4A373]/40 selection:text-white">
{/* Nav */}
@@ -114,7 +141,16 @@ export function LandingPage() {
</Link>
<div className="hidden lg:flex items-center gap-8">
{NAV.map((l) => (
<a key={l.href} href={l.href} className="text-[13px] text-white/55 hover:text-white transition-colors">
<a
key={l.href}
href={l.href}
onClick={(event) => {
event.preventDefault()
scrollPublicHash(l.href)
window.history.replaceState(null, '', l.href)
}}
className="text-[13px] text-white/75 hover:text-white transition-colors"
>
{l.label}
</a>
))}
@@ -167,7 +203,7 @@ export function LandingPage() {
</AnimatePresence>
</div>
<Link href="/login" className="hidden sm:inline text-[13px] text-white/55 hover:text-white transition-colors px-2">
<Link href="/login" className="hidden sm:inline text-[13px] text-white/75 hover:text-white transition-colors px-2">
{t('landing.nav.login')}
</Link>
<Link
@@ -200,13 +236,25 @@ export function LandingPage() {
<a
key={l.href}
href={l.href}
onClick={() => setMenuOpen(false)}
onClick={(event) => {
event.preventDefault()
setMenuOpen(false)
scrollPublicHash(l.href)
window.history.replaceState(null, '', l.href)
}}
className="py-4 text-3xl font-serif border-b border-white/10"
>
{l.label}
</a>
))}
<Link href="/register" onClick={() => setMenuOpen(false)} className="mt-10 py-4 rounded-2xl bg-[#F4F1EA] text-[#0B0A09] text-center font-semibold">
<Link
href="/login"
onClick={() => setMenuOpen(false)}
className="mt-10 py-4 text-2xl font-serif text-white/80 text-center border-b border-white/10"
>
{t('landing.nav.login')}
</Link>
<Link href="/register" onClick={() => setMenuOpen(false)} className="mt-4 py-4 rounded-2xl bg-[#F4F1EA] text-[#0B0A09] text-center font-semibold">
{t('landing.nav.cta')}
</Link>
</div>
@@ -237,7 +285,7 @@ export function LandingPage() {
</span>
</div>
<p className="text-[13px] tracking-[0.12em] uppercase text-white/40 mb-5 font-medium">
<p className="text-[13px] tracking-[0.12em] uppercase text-white/70 mb-5 font-medium">
{t('landing.hero.eyebrow')}
</p>
@@ -247,7 +295,7 @@ export function LandingPage() {
<span className="italic text-[#D4A373]">{t('landing.hero.headlineAccent')}</span>
</h1>
<p className="max-w-xl mx-auto text-[17px] sm:text-lg text-white/55 leading-relaxed mb-10">
<p className="max-w-xl mx-auto text-[17px] sm:text-lg text-white/75 leading-relaxed mb-10">
{t('landing.hero.subtitle')}
</p>
@@ -259,7 +307,7 @@ export function LandingPage() {
{t('landing.hero.cta')}
<ArrowRight size={18} className="transition-transform group-hover:translate-x-0.5" />
</Link>
<p className="text-[12px] text-white/35">{t('landing.hero.ctaHint')}</p>
<p className="text-[12px] text-white/60">{t('landing.hero.ctaHint')}</p>
</div>
</div>
@@ -307,7 +355,7 @@ export function LandingPage() {
{/* Trust strip */}
<section className="px-5 sm:px-8 py-10 border-y border-white/[0.06]">
<div className="max-w-5xl mx-auto flex flex-wrap items-center justify-center gap-x-10 gap-y-4 text-[12px] sm:text-[13px] text-white/40 tracking-wide">
<div className="max-w-5xl mx-auto flex flex-wrap items-center justify-center gap-x-10 gap-y-4 text-[12px] sm:text-[13px] text-white/70 tracking-wide">
{['trust0', 'trust1', 'trust2', 'trust3'].map((k) => (
<span key={k} className="flex items-center gap-2">
<Check size={14} className="text-[#D4A373]" />
@@ -324,7 +372,7 @@ export function LandingPage() {
<h2 className="font-serif text-3xl sm:text-5xl tracking-tight leading-[1.15] mb-6">
{t('landing.pain.title')}
</h2>
<p className="text-lg text-white/50 leading-relaxed mb-8">{t('landing.pain.desc')}</p>
<p className="text-lg text-white/70 leading-relaxed mb-8">{t('landing.pain.desc')}</p>
<p className="text-base sm:text-lg font-serif italic text-[#D4A373]/90 leading-relaxed">
{t('landing.pain.secondBrain')}
</p>
@@ -366,7 +414,7 @@ export function LandingPage() {
<div className="grid grid-cols-2 gap-2 p-2">
{['w0', 'w1', 'w2', 'w3'].map((w) => (
<div key={w} className="rounded-xl bg-[#0B0A09]/40 border border-black/10 p-4 min-h-[88px]">
<p className="text-[10px] uppercase tracking-wider text-[#A47148] mb-2">{t(`landing.moments.dashboard.${w}Label`)}</p>
<p className="text-[13px] font-medium text-[#A47148] mb-2">{t(`landing.moments.dashboard.${w}Label`)}</p>
<p className="text-sm font-medium text-[#0B0A09]/80">{t(`landing.moments.dashboard.${w}`)}</p>
</div>
))}
@@ -383,7 +431,7 @@ export function LandingPage() {
>
<div className="relative h-36 flex items-center justify-center">
<Network className="text-[#D4A373]/80" size={48} strokeWidth={1.25} />
<p className="absolute bottom-2 left-4 text-[11px] uppercase tracking-wider text-white/35">
<p className="absolute bottom-2 left-4 text-[13px] font-medium text-white/75">
{t('landing.moments.insights.chip')}
</p>
</div>
@@ -399,7 +447,7 @@ export function LandingPage() {
<p className="font-serif text-[#0B0A09] text-base mb-3">{t('landing.moments.revision.card')}</p>
<div className="flex gap-2">
<span className="flex-1 py-2 rounded-lg bg-[#0B0A09]/5 text-center text-[11px] font-semibold text-[#0B0A09]/50">?</span>
<span className="flex-1 py-2 rounded-lg bg-[#0B0A09] text-center text-[11px] font-semibold text-[#F4F1EA]">SM-2</span>
<span className="flex-1 py-2 rounded-lg bg-[#0B0A09] text-center text-[13px] font-semibold text-[#F4F1EA]">{t('landing.moments.revision.badge')}</span>
</div>
</div>
</ProductMoment>
@@ -418,7 +466,7 @@ export function LandingPage() {
<div key={s} className="relative text-center md:text-left">
<div className="text-[13px] font-semibold text-[#D4A373] mb-4 tracking-widest">0{i + 1}</div>
<h3 className="font-serif text-xl mb-3">{t(`landing.how.${s}.title`)}</h3>
<p className="text-sm text-white/45 leading-relaxed">{t(`landing.how.${s}.desc`)}</p>
<p className="text-sm text-white/70 leading-relaxed">{t(`landing.how.${s}.desc`)}</p>
</div>
))}
</div>
@@ -431,14 +479,14 @@ export function LandingPage() {
<div className="max-w-2xl mb-14">
<p className="text-[12px] uppercase tracking-[0.25em] text-[#D4A373] mb-4 font-medium">{t('landing.agents.label')}</p>
<h2 className="font-serif text-3xl sm:text-5xl tracking-tight mb-5">{t('landing.agents.title')}</h2>
<p className="text-white/50 text-lg leading-relaxed">{t('landing.agents.desc')}</p>
<p className="text-white/70 text-lg leading-relaxed">{t('landing.agents.desc')}</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{(['scraper', 'researcher', 'slideGen', 'monitor', 'diagramGen', 'custom'] as const).map((key) => (
<div key={key} className="p-6 rounded-2xl border border-white/[0.08] bg-white/[0.03] hover:bg-white/[0.06] transition-colors">
<Bot size={18} className="text-[#D4A373] mb-4" />
<h4 className="font-serif text-lg mb-2">{t(`landing.agents.${key}.title`)}</h4>
<p className="text-sm text-white/40 leading-relaxed">{t(`landing.agents.${key}.desc`)}</p>
<p className="text-sm text-white/70 leading-relaxed">{t(`landing.agents.${key}.desc`)}</p>
</div>
))}
</div>
@@ -454,15 +502,19 @@ export function LandingPage() {
<span className="text-[12px] uppercase tracking-[0.2em] font-semibold">{t('landing.byok.label')}</span>
</div>
<h3 className="font-serif text-3xl tracking-tight mb-4">{t('landing.byok.title')}</h3>
<p className="text-white/50 leading-relaxed">{t('landing.byok.desc')}</p>
<p className="text-white/70 leading-relaxed">{t('landing.byok.desc')}</p>
</div>
<div className="w-full md:w-[320px] font-mono text-[11px] text-white/35 space-y-1.5 rounded-2xl border border-white/10 bg-black/40 p-5">
<p className="text-[#D4A373]">{'{'}</p>
<p className="pl-3">&quot;provider&quot;: &quot;anthropic&quot;,</p>
<p className="pl-3">&quot;model&quot;: &quot;claude-sonnet&quot;,</p>
<p className="pl-3 text-[#F4F1EA]/70">&quot;apiKey&quot;: &quot;sk-ant-&quot;,</p>
<p className="pl-3">&quot;yours&quot;: true</p>
<p className="text-[#D4A373]">{'}'}</p>
<div className="w-full md:w-[320px] text-[13px] text-white/70 space-y-4 rounded-2xl border border-white/10 bg-black/40 p-5">
{byokProviders.length > 0 ? (
<ul className="grid grid-cols-1 gap-2 text-sm text-[#F4F1EA]">
{byokProviders.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
) : (
<p className="text-white/70">{t('landing.byok.pointProvider')}</p>
)}
<p className="text-[#D4A373]">{t('landing.byok.pointYours')}</p>
</div>
</div>
</section>
@@ -472,19 +524,19 @@ export function LandingPage() {
<div className="max-w-6xl mx-auto">
<div className="text-center mb-12">
<h2 className="font-serif text-3xl sm:text-5xl tracking-tight mb-4">{t('landing.pricing.title')}</h2>
<p className="text-white/45 mb-8">{t('landing.pricing.desc')}</p>
<p className="text-white/70 mb-8">{t('landing.pricing.desc')}</p>
<div className="inline-flex p-1 rounded-full border border-white/10 bg-white/[0.03]">
<button
type="button"
onClick={() => setBillingInterval('monthly')}
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all ${billingInterval === 'monthly' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/45'}`}
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all ${billingInterval === 'monthly' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
>
{t('landing.pricing.monthly')}
</button>
<button
type="button"
onClick={() => setBillingInterval('annual')}
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all relative ${billingInterval === 'annual' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/45'}`}
className={`px-5 py-2 rounded-full text-[12px] font-semibold transition-all relative ${billingInterval === 'annual' ? 'bg-[#F4F1EA] text-[#0B0A09]' : 'text-white/70'}`}
>
{t('landing.pricing.annual')}
<span className="absolute -top-3 -right-1 text-[10px] text-[#D4A373] whitespace-nowrap">
@@ -508,19 +560,19 @@ export function LandingPage() {
{t('landing.pricing.popular')}
</span>
)}
<h4 className="text-[12px] uppercase tracking-widest text-white/40 mb-2">
<h4 className="text-[13px] font-medium tracking-wide text-white/80 mb-2">
{t(`landing.pricing.${plan.key}.name`)}
</h4>
<div className="flex items-baseline gap-1 mb-2">
<span className="text-3xl font-serif">{plan.price}</span>
{plan.period && <span className="text-xs text-white/35">{plan.period}</span>}
{plan.period && <span className="text-sm text-white/70">{plan.period}</span>}
</div>
{plan.hasTrial && (
<p className="text-[11px] font-semibold text-[#D4A373] mb-3">
{t('landing.pricing.trialBadge', { days: trialDays })}
</p>
)}
<p className="text-sm text-white/45 mb-6">{t(`landing.pricing.${plan.key}.desc`)}</p>
<p className="text-sm text-white/70 mb-6">{t(`landing.pricing.${plan.key}.desc`)}</p>
<ul className="space-y-2.5 mb-8 flex-1">
{plan.hasTrial && (
<li className="flex gap-2 text-xs text-[#D4A373]/90">
@@ -529,10 +581,12 @@ export function LandingPage() {
</li>
)}
{[0, 1, 2, 3, 4, 5].map((j) => {
const feat = t(`landing.pricing.${plan.key}.feature${j}`)
const feat = t(`landing.pricing.${plan.key}.feature${j}`, {
count: byokProviders.length || '…',
})
if (!feat || feat.startsWith('landing.')) return null
return (
<li key={j} className="flex gap-2 text-xs text-white/60">
<li key={j} className="flex gap-2 text-sm text-white/80">
<Check size={12} className="text-[#D4A373] mt-0.5 shrink-0" />
{feat}
</li>
@@ -541,7 +595,7 @@ export function LandingPage() {
</ul>
<Link
href="/register"
className={`py-3 rounded-xl text-center text-[12px] font-semibold transition-colors ${
className={`py-3 rounded-xl text-center text-[13px] font-semibold transition-colors ${
plan.popular
? 'bg-[#F4F1EA] text-[#0B0A09] hover:bg-white'
: 'bg-white/10 text-white hover:bg-white/15'
@@ -567,7 +621,7 @@ export function LandingPage() {
<h2 className="font-serif text-4xl sm:text-6xl tracking-tight leading-tight mb-6">
{t('landing.cta.title')}
</h2>
<p className="text-white/50 text-lg mb-10">{t('landing.cta.desc')}</p>
<p className="text-white/70 text-lg mb-10">{t('landing.cta.desc')}</p>
<Link
href="/register"
className="inline-flex items-center gap-3 px-10 py-4 rounded-full bg-[#F4F1EA] text-[#0B0A09] text-[15px] font-semibold hover:bg-white transition-all hover:scale-[1.02]"
@@ -575,7 +629,7 @@ export function LandingPage() {
{t('landing.cta.button')}
<ArrowRight size={18} />
</Link>
<p className="mt-4 text-[12px] text-white/30">{t('landing.hero.ctaHint')}</p>
<p className="mt-4 text-[12px] text-white/75">{t('landing.hero.ctaHint')}</p>
</div>
</section>
@@ -588,15 +642,15 @@ export function LandingPage() {
</div>
<span className="font-serif text-lg">Memento</span>
</div>
<p className="text-sm text-white/35">{t('landing.footer.desc')}</p>
<p className="text-sm text-white/60">{t('landing.footer.desc')}</p>
</div>
<div className="grid grid-cols-3 gap-10 text-sm">
{(['product', 'community', 'legal'] as const).map((section) => (
<div key={section}>
<p className="text-[11px] uppercase tracking-widest text-white/40 mb-3">
<p className="text-[13px] font-medium tracking-wide text-white/70 mb-3">
{t(`landing.footer.${section}.title`)}
</p>
<ul className="space-y-2 text-white/50">
<ul className="space-y-2 text-white/70">
{[0, 1, 2].map((j) => {
const label = t(`landing.footer.${section}.link${j}`)
const href = t(`landing.footer.${section}.link${j}Href`)
@@ -616,7 +670,7 @@ export function LandingPage() {
))}
</div>
</div>
<p className="max-w-6xl mx-auto mt-12 pt-8 border-t border-white/[0.06] text-[11px] text-white/25 tracking-wide">
<p className="max-w-6xl mx-auto mt-12 pt-8 border-t border-white/[0.06] text-[13px] text-white/70 tracking-wide">
© 2026 Memento. {t('landing.footer.rights')}
</p>
</footer>
@@ -659,7 +713,7 @@ function ProductMoment({
{eyebrow}
</p>
<h3 className="font-serif text-2xl sm:text-3xl tracking-tight mb-3 leading-tight">{title}</h3>
<p className={`text-[15px] leading-relaxed ${dark ? 'text-white/50' : 'text-[#0B0A09]/55'}`}>{desc}</p>
<p className={`text-[15px] leading-relaxed ${dark ? 'text-white/70' : 'text-[#0B0A09]/75'}`}>{desc}</p>
</div>
<div className={`${compact ? 'px-4 pb-6' : 'p-6 sm:p-8'} flex items-center`}>
<div className="w-full">{children}</div>