import { useTranslation } from 'react-i18next' import { Link, useLocation, useNavigate } from '@tanstack/react-router' import { useDictionaryDetail } from '@/api/queries' import { LanguageSwitch } from '@/ui' import { AuthBadge } from '@/auth/AuthBadge' import { ThemeSwitch } from '@/components/layout/ThemeSwitch' import { VersionBadge } from '@/components/version/VersionBadge' import { SearchInput } from '@/ui/components/search-input' import { Menu } from 'lucide-react' import { useEffect, useRef, useState } from 'react' /** * TopBar — sticky header (h=56) per handoff design. * * Structure: * - Left: dynamic breadcrumb based on current route * - Center: global SearchInput → submits → navigates to /search?q= * + ⌘K / Ctrl+K shortcut focuses input from anywhere * - Right: VersionBadge · ThemeSwitch · LanguageSwitch · Docs · AuthBadge */ const LANG_OPTIONS = [ { id: 'ru-RU', label: 'RU' }, { id: 'en-US', label: 'EN' }, ] export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) { const { t, i18n } = useTranslation() const navigate = useNavigate() const [searchValue, setSearchValue] = useState('') const inputRef = useRef(null) const breadcrumb = useBreadcrumb() // ⌘K / Ctrl+K shortcut — focus search input from anywhere. Skip когда // активен другой input/textarea (юзер печатает в record edit form). useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { const target = e.target as HTMLElement if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) { // Уже в input — пусть default browser shortcut работает. if (target === inputRef.current) return } e.preventDefault() inputRef.current?.focus() inputRef.current?.select() } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, []) const handleSearchSubmit = (e: React.FormEvent) => { e.preventDefault() const q = searchValue.trim() if (q.length === 0) return // Navigate даже если q < 3 — search route сам покажет "min 3 chars" hint. // Раньше блокировал в TopBar → юзер думал «поиск не работает». void navigate({ to: '/search', search: { q } }) } return (
{/* Hamburger (lg:hidden) — toggle MobileSidebar */} {onMenuClick && ( )} {/* Breadcrumb — left. Editor route ('/dictionaries/$name') получает * augmented breadcrumb с displayName + version (per redesign prototype). * Остальные routes: regular path-based breadcrumb. */} {/* Search — center, max-width per handoff */}
setSearchValue(e.target.value)} onClear={() => setSearchValue('')} aria-label={t('topbar.search.label')} /> {/* Right cluster */}
{t('nav.docs')} {/* VersionBadge hide на narrow viewport — diagnostic info, не critical UX */} i18n.changeLanguage(id)} />
) } type Crumb = { label: string; to?: string; subtitle?: string } /** * Breadcrumb из current route. Для editor route ('/dictionaries/$name') * добавляет displayName + version (mono subtitle) per redesign prototype. */ function useBreadcrumb(): Crumb[] { const { t } = useTranslation() const location = useLocation() const path = location.pathname // Editor route: fetch dict detail для display name + schema version. const editorMatch = path.match(/^\/dictionaries\/([^/]+)/) const editorName = editorMatch ? decodeURIComponent(editorMatch[1]) : undefined const editorDict = useDictionaryDetail(editorName) if (path === '/' || path === '') { return [{ label: t('nav.home') }] } const segments = path.split('/').filter(Boolean) const first = segments[0] const ROOT_LABELS: Record = { dictionaries: t('nav.dictionaries'), search: t('nav.search'), graph: t('nav.graph'), 'my-drafts': t('nav.myDrafts'), reviews: t('nav.reviews'), audit: t('nav.audit'), outbox: t('nav.outbox'), webhooks: t('nav.webhooks'), } const rootLabel = ROOT_LABELS[first] ?? first const crumbs: Crumb[] = [ { label: rootLabel, to: segments.length === 1 ? undefined : `/${first}` }, ] if (segments.length >= 2) { // Editor route enrichment: displayName + version if (editorName && editorDict.data) { crumbs.push({ label: editorDict.data.displayName ?? editorDict.data.name, subtitle: `${editorDict.data.name} · v${editorDict.data.schemaVersion}`, }) } else { crumbs.push({ label: decodeURIComponent(segments[1]) }) } } return crumbs }