This commit modernizes the language selector UI across the ZeroClaw web dashboard by replacing native <select> elements with a shared custom dropdown component featuring styled flag icons, proper RTL/LTR text direction support, and consistent left-aligned text for all languages. Changes: - Add LanguageSelector component with custom dropdown, flag badges, and check mark for selected option - Wire document direction updates on locale change for Arabic, Hebrew, Urdu, and other RTL languages - Fix RTL text alignment in dropdown options by applying dir attribute only to text spans - Update pairing dialog and authenticated header to use the shared LanguageSelector - Add locale metadata helpers: getLanguageOption, getLanguageOptionLabel, getLocaleDirection, applyLocaleToDocument - Add Vitest configuration and unit tests for i18n helpers - Add Playwright E2E tests verifying all 31 locales with flag visibility and lang/dir attributes Testing: - Unit tests: 5 passed (npm run test:unit) - Build: passed (npm run build) - E2E tests: 34 passed (npx playwright test) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
import { useLocation } from 'react-router-dom';
|
|
import { LogOut, Menu } from 'lucide-react';
|
|
import { t, tLocale } from '@/lib/i18n';
|
|
import LanguageSelector from '@/components/controls/LanguageSelector';
|
|
import { useLocaleContext } from '@/App';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
|
|
const routeTitles: Record<string, string> = {
|
|
'/': 'nav.dashboard',
|
|
'/agent': 'nav.agent',
|
|
'/tools': 'nav.tools',
|
|
'/cron': 'nav.cron',
|
|
'/integrations': 'nav.integrations',
|
|
'/memory': 'nav.memory',
|
|
'/config': 'nav.config',
|
|
'/cost': 'nav.cost',
|
|
'/logs': 'nav.logs',
|
|
'/doctor': 'nav.doctor',
|
|
};
|
|
|
|
interface HeaderProps {
|
|
onToggleSidebar: () => void;
|
|
}
|
|
|
|
export default function Header({ onToggleSidebar }: HeaderProps) {
|
|
const location = useLocation();
|
|
const { logout } = useAuth();
|
|
const { locale, setAppLocale } = useLocaleContext();
|
|
|
|
const titleKey = routeTitles[location.pathname] ?? 'nav.dashboard';
|
|
const pageTitle = tLocale(titleKey, locale);
|
|
|
|
return (
|
|
<header className="glass-header relative flex min-h-[4.5rem] flex-wrap items-center justify-between gap-2 rounded-2xl border border-[#1a3670] px-4 py-3 sm:px-5 sm:py-3.5 md:flex-nowrap md:px-8 md:py-4">
|
|
<div className="absolute inset-0 pointer-events-none opacity-70 bg-[radial-gradient(circle_at_15%_30%,rgba(41,148,255,0.22),transparent_45%),radial-gradient(circle_at_85%_75%,rgba(0,209,255,0.14),transparent_40%)]" />
|
|
|
|
<div className="relative flex min-w-0 items-center gap-2.5 sm:gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onToggleSidebar}
|
|
aria-label={t('navigation.open')}
|
|
className="rounded-lg border border-[#294a8f] bg-[#081637]/70 p-1.5 text-[#9ec2ff] transition hover:border-[#4f83ff] hover:text-white md:hidden"
|
|
>
|
|
<Menu className="h-5 w-5" />
|
|
</button>
|
|
|
|
<div className="min-w-0">
|
|
<h1 className="truncate text-base font-semibold tracking-wide text-white sm:text-lg">
|
|
{pageTitle}
|
|
</h1>
|
|
<p className="hidden text-[10px] uppercase tracking-[0.16em] text-[#7ea5eb] sm:block">
|
|
{tLocale('header.dashboard_tagline', locale)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex w-full items-center justify-end gap-1.5 sm:gap-2 md:w-auto md:gap-3">
|
|
<LanguageSelector
|
|
locale={locale}
|
|
onChange={setAppLocale}
|
|
ariaLabel={t('common.select_language')}
|
|
title={t('common.languages')}
|
|
align="right"
|
|
buttonClassName="flex min-w-[11rem] items-center gap-2 rounded-xl border border-[#2b4f97] bg-[#091937]/75 px-2.5 py-1 text-[#c4d8ff] shadow-[0_0_0_1px_rgba(79,131,255,0.08)] transition hover:border-[#4f83ff] hover:text-white"
|
|
/>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={logout}
|
|
className="flex items-center gap-1 rounded-lg border border-[#2b4f97] bg-[#091937]/75 px-2.5 py-1.5 text-xs text-[#c4d8ff] transition hover:border-[#4f83ff] hover:text-white sm:gap-1.5 sm:px-3 sm:text-sm"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
<span className="hidden sm:inline">{t('auth.logout')}</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|