feat(admin-ui): Sidebar 220px + TopBar (Stage 3.1)
This commit is contained in:
@@ -1,118 +1,40 @@
|
||||
import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router'
|
||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LanguageSwitch } from '@/ui'
|
||||
import { AuthBadge } from '@/auth/AuthBadge'
|
||||
import { useCanMutate } from '@/auth/useCanMutate'
|
||||
import { ThemeSwitch } from '@/components/layout/ThemeSwitch'
|
||||
import { Sidebar } from '@/components/layout/Sidebar'
|
||||
import { TopBar } from '@/components/layout/TopBar'
|
||||
import { UpdateBanner } from '@/components/version/UpdateBanner'
|
||||
import { VersionBadge } from '@/components/version/VersionBadge'
|
||||
|
||||
/**
|
||||
* Root layout (Stage 3.1 design handoff):
|
||||
*
|
||||
* ┌─ Sidebar 220px (lg:flex) ──┬─ main column (flex-1) ────────────┐
|
||||
* │ logo │ UpdateBanner (when applicable) │
|
||||
* │ nav rail с counters │ TopBar h=56 sticky │
|
||||
* │ user profile │ Outlet (route content) │
|
||||
* └────────────────────────────┴───────────────────────────────────┘
|
||||
*
|
||||
* Responsive: на <1024px sidebar скрывается (hidden lg:flex). Main grid
|
||||
* растягивается на весь viewport. Mobile hamburger drawer — TODO Stage 3.x.
|
||||
*/
|
||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
||||
component: RootLayout,
|
||||
})
|
||||
|
||||
const LANG_OPTIONS = [
|
||||
{ id: 'ru-RU', label: 'RU' },
|
||||
{ id: 'en-US', label: 'EN' },
|
||||
]
|
||||
|
||||
function RootLayout() {
|
||||
const { t, i18n } = useTranslation()
|
||||
// Гость (anonymous) видит только ссылку на /dictionaries и /search.
|
||||
// Аудит, outbox, webhooks, drafts, reviews — admin-only страницы, требуют
|
||||
// JWT (backend SecurityConfig + admin role). Скрываем из top-nav чтобы
|
||||
// user не получал 401 toast при навигации.
|
||||
const canMutate = useCanMutate()
|
||||
return (
|
||||
<div className="min-h-full flex flex-col bg-white">
|
||||
<div className="h-screen flex flex-col bg-bg text-ink overflow-hidden">
|
||||
<UpdateBanner />
|
||||
<header className="border-b border-regolith bg-white">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center flex-wrap gap-3 sm:gap-6">
|
||||
<Link to="/" className="font-primary text-base sm:text-lg text-ultramarain">
|
||||
{t('app.title')}
|
||||
</Link>
|
||||
<nav className="flex gap-4 text-sm">
|
||||
<Link
|
||||
to="/dictionaries"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
activeProps={{ className: 'text-ultramarain font-medium' }}
|
||||
>
|
||||
{t('nav.dictionaries')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/search"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
activeProps={{ className: 'text-ultramarain font-medium' }}
|
||||
>
|
||||
{t('nav.search')}
|
||||
</Link>
|
||||
{/* Документация — внешняя страница (Docusaurus → ordinis-docs
|
||||
deployment монтируется на /docs ingress'ом). target=_blank
|
||||
чтобы не терять контекст приложения. */}
|
||||
<a
|
||||
href="/docs/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
>
|
||||
{t('nav.docs')}
|
||||
</a>
|
||||
{canMutate && (
|
||||
<>
|
||||
<Link
|
||||
to="/audit"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
activeProps={{ className: 'text-ultramarain font-medium' }}
|
||||
>
|
||||
{t('nav.audit')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/outbox"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
activeProps={{ className: 'text-ultramarain font-medium' }}
|
||||
>
|
||||
{t('nav.outbox')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/webhooks"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
activeProps={{ className: 'text-ultramarain font-medium' }}
|
||||
>
|
||||
{t('nav.webhooks')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/reviews"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
activeProps={{ className: 'text-ultramarain font-medium' }}
|
||||
>
|
||||
{t('nav.reviews')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/my-drafts"
|
||||
className="text-carbon hover:text-ultramarain"
|
||||
activeProps={{ className: 'text-ultramarain font-medium' }}
|
||||
>
|
||||
{t('nav.myDrafts')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<VersionBadge />
|
||||
<ThemeSwitch />
|
||||
<LanguageSwitch
|
||||
value={i18n.language}
|
||||
options={LANG_OPTIONS}
|
||||
onChange={(id) => i18n.changeLanguage(id)}
|
||||
/>
|
||||
<AuthBadge />
|
||||
</div>
|
||||
<div className="flex-1 flex min-h-0">
|
||||
<Sidebar />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<TopBar />
|
||||
<main className="flex-1 overflow-y-auto px-4 sm:px-6 py-4 sm:py-6">
|
||||
<div className="max-w-7xl mx-auto w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex-1 max-w-6xl mx-auto w-full px-4 sm:px-6 py-4 sm:py-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GitBranch } from 'lucide-react'
|
||||
import { PageHeader } from '@/ui'
|
||||
import { EmptyState } from '@/ui/components/empty-state'
|
||||
|
||||
/**
|
||||
* Граф связей справочников — d3-force network diagram.
|
||||
*
|
||||
* Stage 3.3 placeholder. Compute graph from {@code useDictionaries() +
|
||||
* useQueries(dependents)}: nodes = dicts, edges = FK refs. Layout —
|
||||
* d3-force с link/charge/center forces. Hover на node → highlight
|
||||
* incoming + outgoing. Click → navigate to dict.
|
||||
*
|
||||
* Dependency: `d3-force` (без full d3, ~30kb gz). Render через canvas
|
||||
* для 40+ nodes performance.
|
||||
*/
|
||||
export const Route = createFileRoute('/graph')({
|
||||
component: GraphPage,
|
||||
})
|
||||
|
||||
function GraphPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<PageHeader title={t('graph.title')} description={t('graph.description')} />
|
||||
<EmptyState
|
||||
icon={<GitBranch strokeWidth={1.5} />}
|
||||
title={t('graph.comingSoon.title')}
|
||||
description={t('graph.comingSoon.description')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user