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>

View File

@@ -0,0 +1,40 @@
'use client';
import type { ReactNode } from 'react';
/**
* Single editorial page header: accent pill + serif title (base + italic accent)
* + subtitle. Every dashboard surface uses this so the product speaks one voice.
*/
export function PageHeader({
pill,
titleBase,
titleAccent,
subtitle,
action,
className = '',
}: {
pill: string;
titleBase: string;
titleAccent: string;
subtitle?: string;
action?: ReactNode;
className?: string;
}) {
return (
<div className={`mb-10 flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between ${className}`}>
<div>
<span className="accent-pill mb-4 block w-fit">{pill}</span>
<h1 className="mb-3 text-4xl font-serif font-medium leading-tight tracking-tight text-brand-dark dark:text-white md:text-5xl">
{titleBase} <span className="italic">{titleAccent}</span>
</h1>
{subtitle && (
<p className="max-w-xl text-sm font-light leading-relaxed text-brand-dark/60 dark:text-white/60">
{subtitle}
</p>
)}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
);
}

View File

@@ -6,6 +6,13 @@ import { motion, AnimatePresence } from "framer-motion";
import { Button } from "@/components/ui/button";
const STORAGE_KEY = "cookie-consent";
const REOPEN_EVENT = "wordly:reopen-cookie-consent";
/** Reopen the banner after a choice was made (footer link uses this). */
export function reopenCookieConsent() {
localStorage.removeItem(STORAGE_KEY);
window.dispatchEvent(new Event(REOPEN_EVENT));
}
export function CookieConsent() {
const { t } = useI18n();
@@ -13,6 +20,9 @@ export function CookieConsent() {
useEffect(() => {
setVisible(!localStorage.getItem(STORAGE_KEY));
const onReopen = () => setVisible(true);
window.addEventListener(REOPEN_EVENT, onReopen);
return () => window.removeEventListener(REOPEN_EVENT, onReopen);
}, []);
function acceptAll() {
@@ -38,7 +48,7 @@ export function CookieConsent() {
<div className="mx-auto max-w-3xl rounded-t-2xl border border-border bg-background/95 backdrop-blur-md shadow-2xl p-6">
<div className="flex items-start gap-3">
<span className="text-2xl shrink-0" aria-hidden="true">
🍪
<CookieIcon />
</span>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground mb-1">
@@ -63,3 +73,16 @@ export function CookieConsent() {
</AnimatePresence>
);
}
/** Drawn icon instead of an emoji — keeps one consistent icon system. */
function CookieIcon() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-foreground/70" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<circle cx="9" cy="9" r="0.5" fill="currentColor" />
<circle cx="14" cy="13" r="0.5" fill="currentColor" />
<circle cx="10" cy="15" r="0.5" fill="currentColor" />
<circle cx="16" cy="8" r="0.5" fill="currentColor" />
</svg>
);
}

View File

@@ -3,6 +3,7 @@
import React, { useState, useRef, useEffect, useCallback } from "react";
import { Search, List, Loader2, Check } from "lucide-react";
import { Input } from "@/components/ui/input";
import { useI18n } from "@/lib/i18n";
export interface ModelOption {
id: string;
@@ -31,6 +32,7 @@ export function ModelCombobox({
providerLabel,
placeholder = "nom-du-modele",
}: ModelComboboxProps) {
const { t } = useI18n();
const [isOpen, setIsOpen] = useState(false);
const [filter, setFilter] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
@@ -108,17 +110,17 @@ export function ModelCombobox({
type="button"
onClick={handleBrowse}
disabled={isLoading}
className="flex items-center gap-1.5 px-3 h-10 rounded-lg border border-border-subtle bg-surface text-sm text-muted-foreground hover:bg-surface-elevated hover:text-foreground transition-colors disabled:opacity-50"
className="flex items-center gap-1.5 px-3 h-10 rounded-lg border border-border bg-card text-sm text-muted-foreground hover:bg-card hover:text-foreground transition-colors disabled:opacity-50"
>
{isLoading ? <Loader2 className="size-4 animate-spin" /> : <List className="size-4" />}
<span className="hidden sm:inline">Parcourir</span>
<span className="hidden sm:inline">{t('common.browse')}</span>
</button>
</div>
{isOpen && (
<div className="absolute z-50 mt-1 w-full rounded-lg border border-border-subtle bg-card shadow-xl max-h-72 overflow-hidden flex flex-col">
<div className="absolute z-50 mt-1 w-full rounded-lg border border-border bg-card shadow-xl max-h-72 overflow-hidden flex flex-col">
{/* Filter */}
<div className="p-2 border-b border-border-subtle">
<div className="p-2 border-b border-border">
<Input
placeholder={`Filtrer...`}
value={filter}
@@ -132,7 +134,7 @@ export function ModelCombobox({
<ul ref={listRef} className="overflow-y-auto flex-1">
{filtered.length === 0 ? (
<li className="px-3 py-4 text-xs text-muted-foreground text-center">
{models.length === 0 ? "Aucun modele recupere" : "Aucun modele ne correspond"}
{models.length === 0 ? t('common.noModels') : t('common.noModelMatch')}
</li>
) : (
filtered.map((model, i) => (
@@ -141,7 +143,7 @@ export function ModelCombobox({
onClick={() => handleSelect(model)}
onMouseEnter={() => setHighlightIndex(i)}
className={`flex items-center justify-between px-3 py-2 text-xs cursor-pointer transition-colors ${
i === highlightIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-surface-elevated"
i === highlightIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-card"
} ${value === model.id ? "bg-primary/5" : ""}`}
>
<div className="flex-1 min-w-0">
@@ -160,7 +162,7 @@ export function ModelCombobox({
</ul>
{models.length > 0 && (
<div className="px-3 py-1.5 border-t border-border-subtle text-xs text-muted-foreground text-center bg-card">
<div className="px-3 py-1.5 border-t border-border text-xs text-muted-foreground text-center bg-card">
{filtered.length} modele{filtered.length !== 1 ? "s" : ""}
{filter ? ` sur ${models.length}` : ""} nom libre possible
</div>

View File

@@ -4,6 +4,7 @@ import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { X, CheckCircle, AlertCircle, AlertTriangle, Info, Loader2 } from "lucide-react"
import { cn } from "@/lib/utils"
import { useI18n } from "@/lib/i18n"
const notificationVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-4 pe-8 shadow-lg transition-all duration-300 ease-out",
@@ -74,6 +75,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
children,
...props
}, ref) => {
const { t } = useI18n()
const [visible, setVisible] = React.useState(true)
const [progress, setProgress] = React.useState(100)
@@ -181,7 +183,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
type="button"
onClick={handleClose}
className="absolute right-2 top-2 flex-shrink-0 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label="Close notification"
aria-label={t("common.closeNotification")}
>
<X className="h-4 w-4" />
</button>