zeroclaw/web/src/components/layout/Header.tsx
argenis de la rosa 3f5c57634b feat(web): replace native language selects with custom dropdown and fix RTL text alignment
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>
2026-03-09 13:50:21 -04:00

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>
);
}