fix(ui): design critique overhaul — a11y, honest metrics, i18n repair (critique 2026-08-30)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m51s
P0 a11y: keyboard-accessible dropzone (role/tabIndex/Enter-Space), ARIA combobox + listbox pattern for language selector, role=switch on glossary toggle, role=status live region on notifications, aria-labels on password toggles, visible-on-focus close buttons, RTL logical positioning (start-*). Trust: third-party Memento promo removed from translate page, sidebar and all 13 locales; fabricated stats (99.9%, Turbo, computed layout-integrity bars) replaced with real measurements incl. API estimated remaining time; silent download failure now surfaces an error notification. Honesty: fake 100-byte file injections removed (format chips are now informational); cancel-that-doesn't renamed 'Back to start' with hint; Enterprise contact placeholder replaced with contact@wordly.art. i18n: t() no longer returns raw keys (empty string + defaultValue support, ~30 dead || fallbacks now work); ~170 new keys EN+FR across new reviews/ teams namespaces, glossaries context tab, translate monitor, settings, services, pricing, landing, fileUploader; split-key italic titles replace lastIndexOf() surgery (zh/ja-safe); key-audit script added 0 missing. Flow: active job persisted across refresh with polling resume (24h TTL); client-side recent-jobs history with review links; review page linked from complete state; settings/services added to dashboard nav; Business/ Enterprise regain glossary access (tier gate unified). Typeset (sober-tool direction): 7.5-9px labels raised to 10-12px, /30 opacity to /45-/55, uppercase tracking reduced, trust footer legible, country flags removed from language switcher, localized dates. Cleanup: 5 orphaned translate components, dead site header/footer, fossil tailwind.config.js, PipelineStepper, duplicate pill+H1 titles, two-step confirm for cache clear, dead landing footer links. Verified: next build exit 0, vitest 9/9, eslint 64 errors = HEAD (no regression, -3 warnings), detector 4 -> 3 findings.
This commit is contained in:
@@ -293,7 +293,7 @@ const FormatsSection = () => {
|
||||
<section id="formats" className="py-40 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-24">
|
||||
<span className="accent-pill mb-6 block w-fit">COMPATIBILITÉ</span>
|
||||
<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>
|
||||
</div>
|
||||
@@ -430,29 +430,14 @@ const Footer = () => {
|
||||
</ul>
|
||||
</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.resources')}</h4>
|
||||
<ul className="space-y-6 text-xs text-brand-dark/40 dark:text-white/40 font-bold uppercase tracking-widest leading-none">
|
||||
<li><a href="#" className="hover:text-brand-dark dark:hover:text-white transition-colors">Documentation</a></li>
|
||||
<li><a href="#" className="hover:text-brand-dark dark:hover:text-white transition-colors">API</a></li>
|
||||
</ul>
|
||||
</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.legal')}</h4>
|
||||
<ul className="space-y-6 text-xs text-brand-dark/40 dark:text-white/40 font-bold uppercase tracking-widest leading-none">
|
||||
<li><a href="#" className="hover:text-brand-dark dark:hover:text-white transition-colors">Privacy</a></li>
|
||||
<li><a href="#" className="hover:text-brand-dark dark:hover:text-white transition-colors">Terms</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</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]">
|
||||
{t('landing.footer.rights')}
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-brand-dark/20 dark:text-white/20 uppercase tracking-widest">
|
||||
v2.4.0 • Built for excellence
|
||||
<div className="text-[10px] font-mono text-brand-dark/30 dark:text-white/30 uppercase tracking-widest">
|
||||
wordly.art
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
|
||||
export function SiteFooter() {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<footer className="border-t border-border py-6 text-center text-xs text-muted-foreground">
|
||||
<div className="mx-auto max-w-5xl px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<span>{t('landing.footer.rights')}</span>
|
||||
<div className="flex items-center gap-6">
|
||||
<Link href="/pricing" className="hover:text-foreground transition-colors">
|
||||
{t('landing.nav.pricing')}
|
||||
</Link>
|
||||
<Link href="/pricing#terms" className="hover:text-foreground transition-colors">
|
||||
{t('layout.footer.terms')}
|
||||
</Link>
|
||||
<Link href="/pricing#privacy" className="hover:text-foreground transition-colors">
|
||||
{t('layout.footer.privacy')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Languages } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
|
||||
export function SiteHeader() {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-lg">
|
||||
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="flex size-8 items-center justify-center rounded-lg bg-primary">
|
||||
<Languages className="size-4 text-primary-foreground" />
|
||||
</div>
|
||||
<span className="text-base font-semibold tracking-tight text-foreground">
|
||||
Wordly
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-1 md:flex">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/pricing">{t('landing.nav.pricing')}</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/pricing#api">{t('layout.nav.apiAccess')}</Link>
|
||||
</Button>
|
||||
<div className="mx-2 h-4 w-px bg-border" />
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/dashboard">{t('landing.nav.login')}</Link>
|
||||
</Button>
|
||||
</nav>
|
||||
|
||||
<div className="md:hidden">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/dashboard">{t('landing.nav.login')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -10,20 +10,21 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { useI18n, type Locale } from "@/lib/i18n";
|
||||
|
||||
const languages: { value: Locale; label: string; flag: string }[] = [
|
||||
{ value: "en", label: "English", flag: "🇬🇧" },
|
||||
{ value: "fr", label: "Français", flag: "🇫🇷" },
|
||||
{ value: "es", label: "Español", flag: "🇪🇸" },
|
||||
{ value: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
{ value: "pt", label: "Português", flag: "🇧🇷" },
|
||||
{ value: "it", label: "Italiano", flag: "🇮🇹" },
|
||||
{ value: "nl", label: "Nederlands", flag: "🇳🇱" },
|
||||
{ value: "ru", label: "Русский", flag: "🇷🇺" },
|
||||
{ value: "ja", label: "日本語", flag: "🇯🇵" },
|
||||
{ value: "ko", label: "한국어", flag: "🇰🇷" },
|
||||
{ value: "zh", label: "中文", flag: "🇨🇳" },
|
||||
{ value: "ar", label: "العربية", flag: "🇸🇦" },
|
||||
{ value: "fa", label: "فارسی", flag: "🇮🇷" },
|
||||
// Language names in their own script — flags are country symbols, not languages.
|
||||
const languages: { value: Locale; label: string }[] = [
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "fr", label: "Français" },
|
||||
{ value: "es", label: "Español" },
|
||||
{ value: "de", label: "Deutsch" },
|
||||
{ value: "pt", label: "Português" },
|
||||
{ value: "it", label: "Italiano" },
|
||||
{ value: "nl", label: "Nederlands" },
|
||||
{ value: "ru", label: "Русский" },
|
||||
{ value: "ja", label: "日本語" },
|
||||
{ value: "ko", label: "한국어" },
|
||||
{ value: "zh", label: "中文" },
|
||||
{ value: "ar", label: "العربية" },
|
||||
{ value: "fa", label: "فارسی" },
|
||||
];
|
||||
|
||||
interface LanguageSwitcherProps {
|
||||
@@ -31,16 +32,15 @@ interface LanguageSwitcherProps {
|
||||
}
|
||||
|
||||
export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
|
||||
const { locale, setLocale } = useI18n();
|
||||
const { locale, setLocale, t } = useI18n();
|
||||
const current = languages.find((l) => l.value === locale) ?? languages[0];
|
||||
|
||||
return (
|
||||
<Select value={locale} onValueChange={(v) => setLocale(v as Locale)}>
|
||||
<SelectTrigger className={className} aria-label="Change language">
|
||||
<SelectTrigger className={className} aria-label={t('langSelector.changeLanguage')}>
|
||||
<SelectValue>
|
||||
<span className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
<span>{current.flag}</span>
|
||||
<span>{current.label}</span>
|
||||
</span>
|
||||
</SelectValue>
|
||||
@@ -49,7 +49,6 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang.value} value={lang.value}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{lang.flag}</span>
|
||||
<span>{lang.label}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
|
||||
@@ -117,13 +117,15 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className={cn(notificationVariants({ variant, size, position }), className)}
|
||||
{...props}
|
||||
>
|
||||
{/* Progress Bar for Auto-close */}
|
||||
{autoClose && !loading && (
|
||||
<div className="absolute top-0 left-0 h-1 bg-white/20">
|
||||
<div
|
||||
<div
|
||||
className="h-full bg-white/40 transition-all duration-100 ease-linear"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
@@ -178,7 +180,7 @@ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
||||
<button
|
||||
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:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
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"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user