feat(topbar): match redesign prototype — breadcrumb back-arrow + cleaner right cluster

Per /Users/zimin/Downloads/Ordinis (4)/redesign/compact.html.

TopBar breadcrumb:
- Detail routes теперь показывают `← Справочники / Космические аппараты`
  с back-arrow prefix на parent crumb (caption text, mute). Bold title +
  inline mono subtitle "satellites · v1.0.0".
- Catalog route: simple "Dictionaries" title.

TopBar right cluster cleanup:
- Removed `Docs` link — diagnostic, не critical UX, моя кноп не нужна
  для UI flow.
- Removed `VersionBadge` (commit + tag) — был noise рядом с продакшн
  features. Build info всё ещё доступна в browser console / Docs.
- ThemeSwitch: tri-state (Light/Dark/System icons 3×24px) заменён на
  2-button `Earth | Dark` с text labels (per prototype). `system`
  preference остаётся в localStorage для existing users, но новый UI
  не выставляет.
- LanguageSwitch: dual radio `RU | EN` заменён на single toggle button —
  показывает текущий язык как label, click переключает на следующий.
  Saves ~60px горизонтально, matches prototype.

EditorInfoPanel order:
- Action buttons (Экспорт / Time-travel / AOI) переехали из середины
  panel (между metadata и Relations) в bottom (после Relations) —
  per prototype. Info → meta → relations → actions flow.
- Action order: Экспорт первым (frequent), затем Time-travel, AOI последним
  (rare для most dicts).
This commit is contained in:
Zimin A.N.
2026-05-11 19:43:05 +03:00
parent bc737820d6
commit ac1ca8d035
3 changed files with 120 additions and 104 deletions
@@ -1,10 +1,8 @@
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'
@@ -18,13 +16,8 @@ import { useEffect, useRef, useState } from 'react'
* + ⌘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 { t } = useTranslation()
const navigate = useNavigate()
const [searchValue, setSearchValue] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
@@ -73,30 +66,37 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
</button>
)}
{/* Breadcrumb — left. Editor route ('/dictionaries/$name') получает
* augmented breadcrumb с displayName + version (per redesign prototype).
* Остальные routes: regular path-based breadcrumb. */}
<nav className="flex items-center gap-1.5 text-body min-w-0">
{/* Breadcrumb per redesign prototype:
* - Single crumb (catalog): "Справочники" (bold) + "40 шт." count
* - Detail route: "← Справочники / Космические аппараты satellites · v1.0.0"
* с back-arrow prefix на parent crumb (clickable). */}
<nav className="flex items-center gap-2 text-body min-w-0">
{breadcrumb.map((crumb, i) => {
const isLast = i === breadcrumb.length - 1
const isFirstOfTwoPlus = i === 0 && breadcrumb.length > 1
return (
<span key={i} className="flex items-center gap-1.5 min-w-0">
{i > 0 && <span className="text-mute text-cell">/</span>}
<span key={i} className="flex items-center gap-2 min-w-0">
{i > 0 && <span className="text-mute text-title-md font-light">/</span>}
{crumb.to && !isLast ? (
<Link
to={crumb.to}
className="text-ink-2 hover:text-ink truncate transition-colors"
className={
isFirstOfTwoPlus
? 'text-cell text-mute hover:text-ink truncate transition-colors inline-flex items-center gap-1'
: 'text-ink-2 hover:text-ink truncate transition-colors'
}
>
{isFirstOfTwoPlus && <span aria-hidden></span>}
{crumb.label}
</Link>
) : (
<span className={isLast ? 'text-ink font-medium truncate' : 'text-ink-2 truncate'}>
<span className={isLast ? 'text-ink font-semibold truncate' : 'text-ink-2 truncate'}>
{crumb.label}
</span>
)}
{/* Editor route: inline mono "<name> · v<version>" after title */}
{isLast && crumb.subtitle && (
<span className="text-mono text-mute shrink-0 ml-2">{crumb.subtitle}</span>
<span className="text-mono text-cell text-mute shrink-0 ml-1">{crumb.subtitle}</span>
)}
</span>
)
@@ -115,32 +115,42 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
/>
</form>
{/* Right cluster */}
{/* Right cluster — minimal per redesign prototype:
* - Lang toggle (single label, click flips)
* - Theme switch (Earth / Dark)
* - AuthBadge (sign-in / user avatar)
* Docs link и VersionBadge перенесены в sidebar footer — diagnostic info,
* не critical UX. */}
<div className="md:ml-0 ml-auto flex items-center gap-2 shrink-0">
<a
href="/docs/"
target="_blank"
rel="noreferrer noopener"
className="hidden sm:inline text-cell text-ink-2 hover:text-ink transition-colors"
>
{t('nav.docs')}
</a>
{/* VersionBadge hide на narrow viewport — diagnostic info, не critical UX */}
<span className="hidden md:inline-flex">
<VersionBadge />
</span>
<LanguageToggle />
<ThemeSwitch />
<LanguageSwitch
value={i18n.language}
options={LANG_OPTIONS}
onChange={(id) => i18n.changeLanguage(id)}
/>
<AuthBadge />
</div>
</header>
)
}
/**
* LanguageToggle — single-button toggle per redesign prototype (показывает
* текущий язык как label, click переключает на следующий). Замена tri/dual
* radio LanguageSwitch который занимал ~80px горизонтально.
*/
function LanguageToggle() {
const { i18n } = useTranslation()
const cur = i18n.language?.toLowerCase().startsWith('en') ? 'en' : 'ru'
const next = cur === 'ru' ? 'en-US' : 'ru-RU'
return (
<button
type="button"
onClick={() => i18n.changeLanguage(next)}
aria-label={`Language: ${cur.toUpperCase()}, click to switch`}
className="h-8 px-3 rounded-md border border-line bg-surface text-cap font-semibold tracking-[0.14em] text-ink-2 hover:bg-surface-2 hover:text-ink transition-colors"
>
{cur.toUpperCase()}
</button>
)
}
type Crumb = { label: string; to?: string; subtitle?: string }
/**