diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index 0f093a1..2b7a92d 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -39,6 +39,7 @@ import { } from '@cfdm/ui/components/breadcrumb' import { Separator } from '@cfdm/ui/components/separator' import { Badge } from '@cfdm/ui/components/badge' +import { TooltipProvider } from '@cfdm/ui/components/tooltip' import { Link, useRouterState } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' @@ -48,11 +49,8 @@ import { ModeToggle } from '@/components/mode-toggle' import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover' import { AppsMenu } from '@/components/layout/apps-menu' import { AppSwitcher } from '@/components/app-switcher' -import { GlobalSearch, GlobalSearchTrigger, useGlobalSearchHotkey } from '@/components/global-search' +import { GlobalSearch, useGlobalSearchHotkey } from '@/components/global-search' import { dashboardStatsQueryOptions } from '@/queries/dashboard' -import { formatRelativeSyncTime } from '@/lib/sync-format' -import { TruncatedText } from '@/components/truncated-text' -import { cn } from '@cfdm/ui/lib/utils' interface NavItem { to: string @@ -127,11 +125,14 @@ const PARENT_ROUTE: Record = { '/audit': '/settings', } +/** Shared ops chrome — etalon EvoBGP. @see docs/ui-design-contract.md */ export function AppShell({ children }: { children: ReactNode }) { const pathname = useRouterState({ select: (s) => s.location.pathname }) const [searchOpen, setSearchOpen] = useState(false) useGlobalSearchHotkey(() => setSearchOpen(true)) - const activeItem = ALL_NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) ?? ALL_NAV_ITEMS[0] + const activeItem = + ALL_NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) ?? + ALL_NAV_ITEMS[0] const parentTo = PARENT_ROUTE[activeItem.to] const parentLabel = parentTo ? ROUTE_LABELS[parentTo] : null @@ -148,111 +149,94 @@ export function AppShell({ children }: { children: ReactNode }) { })) return ( - - - - - - - {navGroups.map((group) => ( - - {group.label} - - - {group.items.map((item) => { - const Icon = item.icon - const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`) - return ( - - } - isActive={isActive} - tooltip={item.label} - > - - {item.label} - {item.badge ? ( - - {item.badge} - - ) : null} - - - ) - })} - - - - ))} - - - - - } tooltip="Настройки"> - - - Синк: {formatRelativeSyncTime(stats?.lastGlobalSyncAt)} - - {stats?.staleSyncAccountCount ? ( - - - {stats.staleSyncAccountCount} - + + + + + + + + {navGroups.map((group) => ( + + {group.label} + + + {group.items.map((item) => { + const Icon = item.icon + const isActive = + pathname === item.to || pathname.startsWith(`${item.to}/`) + return ( + + } + isActive={isActive} + tooltip={item.label} + > + + {item.label} + {item.badge ? ( + + {item.badge} + + ) : null} + + + ) + })} + + + + ))} + + + + +
+ + + + + {parentLabel && parentTo ? ( + <> + + }> + {parentLabel} + + + + ) : null} - - - - - - -
- - - - - {parentLabel && parentTo ? ( - <> - - }>{parentLabel} - - - - ) : null} - - {ROUTE_LABELS[activeItem.to] ?? ''} - - - -
- setSearchOpen(true)} /> - {stats?.issuesCount ? ( - - {stats.issuesCount} проблем - - ) : null} - - - -
-
-
{children}
-
- - + + + {ROUTE_LABELS[activeItem.to] ?? ''} + + +
+
+
+ + + +
+
+
+ {children} +
+
+ +
+
) } diff --git a/apps/web/src/components/page-header.tsx b/apps/web/src/components/page-header.tsx index 77ea88d..6dcee33 100644 --- a/apps/web/src/components/page-header.tsx +++ b/apps/web/src/components/page-header.tsx @@ -6,12 +6,15 @@ interface PageHeaderProps { actions?: ReactNode } +/** Page-level section header — etalon EvoBGP. */ export function PageHeader({ title, description, actions }: PageHeaderProps) { return (

{title}

- {description ?

{description}

: null} + {description ? ( +

{description}

+ ) : null}
{actions ?
{actions}
: null}
diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index aa35fe0..1259213 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -49,18 +49,22 @@ Gating: DB preference `showQuickActions` / `show_quick_actions` / `ui_show_quick ## Shared App Shell chrome -Эталон: CFDM + ReUI [app-shell-12](https://reui.io/preview/base/app-shell-12) · monitor/switchers [app-shell-7](https://reui.io/preview/base/app-shell-7). +Эталон: **EvoBGP** production AppShell + ReUI [app-shell-12](https://reui.io/preview/base/app-shell-12). -При переключении между vps-tracker / CFDM / EvoBGP меняются **только** sidebar nav и `main` content. ClassNames chrome идентичны. +При переключении между vps-tracker / CFDM / EvoBGP меняются **только** sidebar nav labels/hrefs и `main` content. Разметка, ширина, фон и hover chrome идентичны. | Токен / зона | Значение | |--------------|----------| -| `--sidebar-width` | `240px` | -| Sidebar accent | primary **14%** mix + transparent border | -| Header | `h-12`, `sticky`, `border-b`, `px-4 md:px-6` (без blur / без `h-16`) | -| Header right | **AppsMenu** → **SystemMonitorPopover** → **ModeToggle** | +| `SIDEBAR_WIDTH` / `--sidebar-width` | `240px` (в `packages/ui` sidebar + Provider style) | +| Sidebar / hover colors | theme `--sidebar` / `--sidebar-accent` из `globals.css` — **без** AppShell `color-mix` override | +| Header | `h-12`, `sticky`, `border-b`, `px-4 md:px-6` | +| Header left | `SidebarTrigger` + `Separator` + Breadcrumb | +| Header right | **AppsMenu** → **SystemMonitorPopover** → **ModeToggle** (без Search в chrome) | +| Sidebar | AppSwitcher → groups (`SidebarGroupContent`) → icons `size-4` → **пустой** `SidebarFooter` | | `main` | `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` | -| Sidebar header | `AppSwitcher` (не static brand) | +| Search | hotkey ⌘K / Ctrl+K only (не кнопка в header) | + +Запрещено в chrome: `SidebarRail`, `NavUser` footer, sync-row footer, Search/Ctrl+K pill в header, issues Badge в header, muted/hover cascade на right-cluster, Provider `color-mix` для `--sidebar*`. App Switcher ids: `vps-tracker` · `cfdm` · `evobgp`. Override: `VITE_APP_SWITCHER` JSON. diff --git a/packages/ui/src/components/sidebar.tsx b/packages/ui/src/components/sidebar.tsx index 4e47c11..84c7aaf 100644 --- a/packages/ui/src/components/sidebar.tsx +++ b/packages/ui/src/components/sidebar.tsx @@ -27,7 +27,7 @@ import { PanelLeftIcon } from "lucide-react" const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH = "240px" const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_KEYBOARD_SHORTCUT = "b"