diff --git a/apps/web/src/components/app-switcher.tsx b/apps/web/src/components/app-switcher.tsx new file mode 100644 index 0000000..d11a94e --- /dev/null +++ b/apps/web/src/components/app-switcher.tsx @@ -0,0 +1,97 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from '@evobgp/ui/components/dropdown-menu' +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@evobgp/ui/components/sidebar' +import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react' + +import { + APP_SWITCHER_ICONS, + CURRENT_APP_ID, + getCurrentApp, +} from '@/lib/app-switcher-config' +import { useAppSwitcherConfig } from '@/hooks/use-app-switcher' + +/** Sidebar app switcher — same chrome as CFDM / vps-tracker. @see https://reui.io/preview/base/app-shell-12 */ +export function AppSwitcher() { + const { isMobile } = useSidebar() + const { config, isLoading } = useAppSwitcherConfig() + const current = getCurrentApp(config) + const CurrentIcon = APP_SWITCHER_ICONS[current.icon] + + return ( + + + + + } + > + + + + + {current.name} + {current.subtitle ? ( + + {current.subtitle} + + ) : null} + + + + + + {isLoading ? 'Загрузка…' : config.menuLabel} + + {config.apps.map((app) => { + const Icon = APP_SWITCHER_ICONS[app.icon] + const isCurrent = app.id === CURRENT_APP_ID + + if (isCurrent) { + return ( + + + {app.name} + + + ) + } + + return ( + } + > + + {app.name} + {app.shortcut ? ( + {app.shortcut} + ) : null} + + ) + })} + + + + + ) +} diff --git a/apps/web/src/components/dashboard/dashboard-quick-links.tsx b/apps/web/src/components/dashboard/dashboard-quick-links.tsx index 9dc5d64..b073733 100644 --- a/apps/web/src/components/dashboard/dashboard-quick-links.tsx +++ b/apps/web/src/components/dashboard/dashboard-quick-links.tsx @@ -2,6 +2,7 @@ import { Gauge, Network, Play, Plus, Search, Share2, Tags } from 'lucide-react' import { QuickActionGrid, type QuickActionItem } from '@/components/reui-kit' +/** iconClassName: semantic text only (shared chrome with CFDM). @see https://reui.io/preview/base/stats-12 */ const ACTIONS: QuickActionItem[] = [ { id: 'lookup', @@ -9,7 +10,7 @@ const ACTIONS: QuickActionItem[] = [ description: 'Membership в списках и community (entry + snapshot).', to: '/lookup', icon: , - iconClassName: 'bg-primary text-primary-foreground [&_svg]:text-primary-foreground', + iconClassName: 'text-primary', }, { id: 'new-module', @@ -17,7 +18,7 @@ const ACTIONS: QuickActionItem[] = [ description: 'Новый модуль маршрутизации и источники префиксов.', to: '/modules/new', icon: , - iconClassName: 'bg-primary text-primary-foreground [&_svg]:text-primary-foreground', + iconClassName: 'text-primary', }, { id: 'communities', @@ -25,7 +26,7 @@ const ACTIONS: QuickActionItem[] = [ description: 'Справочник communities для политик экспорта.', to: '/directories', icon: , - iconClassName: 'bg-info text-info-foreground [&_svg]:text-info-foreground', + iconClassName: 'text-info', }, { id: 'network', @@ -34,7 +35,7 @@ const ACTIONS: QuickActionItem[] = [ to: '/network', search: { tab: 'overview' }, icon: , - iconClassName: 'bg-success text-success-foreground [&_svg]:text-success-foreground', + iconClassName: 'text-success', }, { id: 'add-peer', @@ -43,7 +44,7 @@ const ACTIONS: QuickActionItem[] = [ to: '/network', search: { tab: 'peers' }, icon: , - iconClassName: 'bg-warning text-warning-foreground [&_svg]:text-warning-foreground', + iconClassName: 'text-warning', }, { id: 'deploy', @@ -52,7 +53,7 @@ const ACTIONS: QuickActionItem[] = [ to: '/operations', search: { tab: 'revisions' }, icon: , - iconClassName: 'bg-focus text-focus-foreground [&_svg]:text-focus-foreground', + iconClassName: 'text-muted-foreground', }, { id: 'monitoring', @@ -61,7 +62,7 @@ const ACTIONS: QuickActionItem[] = [ to: '/monitoring', search: { tab: 'system' }, icon: , - iconClassName: 'bg-destructive text-destructive-foreground [&_svg]:text-destructive-foreground', + iconClassName: 'text-destructive', }, ] diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index a2f350f..bb4a1d2 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -38,10 +38,13 @@ import { } from '@evobgp/ui/components/breadcrumb' import { Separator } from '@evobgp/ui/components/separator' import { TooltipProvider } from '@evobgp/ui/components/tooltip' +import { cn } from '@evobgp/ui/lib/utils' import { Link, useRouterState } from '@tanstack/react-router' import type { ComponentType, CSSProperties, ReactNode } from 'react' +import { AppSwitcher } from '@/components/app-switcher' +import { AppsMenu } from '@/components/layout/apps-menu' import { CommandPalette, type CommandPaletteItem } from '@/components/layout/command-palette' import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover' import { ModeToggle } from '@/components/mode-toggle' @@ -118,6 +121,11 @@ const COMMAND_ITEMS: CommandPaletteItem[] = ALL_NAV_ITEMS.map((item) => ({ keywords: [item.to.replace(/^\//, '')], })) +/** + * Shared ops chrome — tokens match CFDM / vps-tracker. + * @see https://reui.io/preview/base/app-shell-12 + * @see docs/ui-design-contract.md — Shared App Shell chrome + */ export function AppShell({ children }: { children: ReactNode }) { const pathname = useRouterState({ select: (s) => s.location.pathname }) const activeItem = @@ -134,28 +142,21 @@ export function AppShell({ children }: { children: ReactNode }) { return ( - - - - B - - - EvoBGP - Плоскость управления - - - - - + + {NAV_GROUPS.map((group) => ( @@ -187,8 +188,8 @@ export function AppShell({ children }: { children: ReactNode }) { - - + + @@ -206,12 +207,16 @@ export function AppShell({ children }: { children: ReactNode }) { + - {children} + + {children} + + ) diff --git a/apps/web/src/components/layout/apps-menu.tsx b/apps/web/src/components/layout/apps-menu.tsx new file mode 100644 index 0000000..fa36544 --- /dev/null +++ b/apps/web/src/components/layout/apps-menu.tsx @@ -0,0 +1,93 @@ +import { Link } from '@tanstack/react-router' +import { LayoutGridIcon } from 'lucide-react' +import { Button } from '@evobgp/ui/components/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@evobgp/ui/components/dropdown-menu' +import { + APP_SWITCHER_ICONS, + CURRENT_APP_ID, +} from '@/lib/app-switcher-config' +import { useAppSwitcherConfig } from '@/hooks/use-app-switcher' + +/** Header apps grid — app-shell-12 AppsMenu. @see https://reui.io/preview/base/app-shell-12 */ +export function AppsMenu() { + const { config, isLoading } = useAppSwitcherConfig() + + return ( + + + } + > + + + + + + + {isLoading ? 'Загрузка…' : config.menuLabel} + + + {config.apps.map((app) => { + const Icon = APP_SWITCHER_ICONS[app.icon] + const isCurrent = app.id === CURRENT_APP_ID + + if (isCurrent) { + return ( + + + + + {app.name} + + ) + } + + return ( + } + className="h-auto flex-col gap-1.5 py-3 text-center [&_svg]:size-5" + > + + + + {app.name} + + ) + })} + + + } + className="justify-center text-sm font-medium" + > + Настройки + + + + + ) +} diff --git a/apps/web/src/components/layout/command-palette.tsx b/apps/web/src/components/layout/command-palette.tsx index 5576961..9a94bc9 100644 --- a/apps/web/src/components/layout/command-palette.tsx +++ b/apps/web/src/components/layout/command-palette.tsx @@ -29,9 +29,11 @@ export type CommandPaletteItem = { interface CommandPaletteProps { items: CommandPaletteItem[] className?: string + /** Hotkey-only: no sidebar search trigger (chrome parity with CFDM). */ + hotkeyOnly?: boolean } -export function CommandPalette({ items }: CommandPaletteProps) { +export function CommandPalette({ items, hotkeyOnly = false }: CommandPaletteProps) { const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const searchInputId = useId() @@ -68,25 +70,27 @@ export function CommandPalette({ items }: CommandPaletteProps) { return ( <> - - - setOpen(true)} - > - Поиск… - - - - ⌘K - - - + {!hotkeyOnly ? ( + + + setOpen(true)} + > + Поиск… + + + + ⌘K + + + + ) : null} + +export const APP_SWITCHER_ICONS: Record = { + server: ServerIcon, + cloud: CloudIcon, + globe: GlobeIcon, + dashboard: LayoutDashboardIcon, + chart: ChartBarIcon, +} + +const appSwitcherEntrySchema = z.object({ + id: z.string(), + name: z.string(), + subtitle: z.string().optional(), + url: z.string(), + icon: appSwitcherIconSchema.default('server'), + shortcut: z.string().optional(), +}) + +const appSwitcherConfigSchema = z.object({ + menuLabel: z.string().default('Приложения'), + apps: z.array(appSwitcherEntrySchema).min(1), +}) + +export type AppSwitcherEntry = z.infer +export type AppSwitcherConfig = z.infer + +/** Shared defaults across ops apps — chrome app switcher. */ +export const DEFAULT_APP_SWITCHER_CONFIG: AppSwitcherConfig = { + menuLabel: 'Приложения', + apps: [ + { + id: 'vps-tracker', + name: 'VPS Tracker', + subtitle: 'Учёт виртуальных серверов', + url: 'http://192.168.100.67:3001', + icon: 'server', + shortcut: '⌘1', + }, + { + id: 'cfdm', + name: 'CF Domain Manager', + subtitle: 'Управление доменами', + url: 'http://192.168.100.67:6363', + icon: 'cloud', + shortcut: '⌘2', + }, + { + id: 'evobgp', + name: 'EvoBGP', + subtitle: 'BGP маршрутизация', + url: 'http://192.168.100.67:3000', + icon: 'globe', + shortcut: '⌘3', + }, + ], +} + +export function parseAppSwitcherConfig(raw?: string): AppSwitcherConfig { + if (!raw?.trim()) { + return DEFAULT_APP_SWITCHER_CONFIG + } + + try { + const parsed = JSON.parse(raw) as unknown + return appSwitcherConfigSchema.parse(parsed) + } catch (error) { + console.warn('Invalid VITE_APP_SWITCHER, using defaults:', error) + return DEFAULT_APP_SWITCHER_CONFIG + } +} + +export function getAppSwitcherConfig(): AppSwitcherConfig { + return parseAppSwitcherConfig(import.meta.env.VITE_APP_SWITCHER) +} + +export function getAppUrl( + appId: string, + config: AppSwitcherConfig = getAppSwitcherConfig(), +): string | undefined { + return config.apps.find((app) => app.id === appId)?.url +} + +export function getCurrentApp( + config: AppSwitcherConfig = getAppSwitcherConfig(), +): AppSwitcherEntry { + return config.apps.find((app) => app.id === CURRENT_APP_ID) ?? config.apps[0]! +} diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..1935306 --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_SWITCHER?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo index ea812b4..7955207 100644 --- a/apps/web/tsconfig.tsbuildinfo +++ b/apps/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/counted-line-tabs.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/drawer-layout.tsx","./src/components/empty-state.tsx","./src/components/form-drawer.tsx","./src/components/kpi-stat-grid.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/panel-card.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/card-dot-field.tsx","./src/components/dashboard/dashboard-activity-timeline.tsx","./src/components/dashboard/dashboard-frame-panel.tsx","./src/components/dashboard/dashboard-kpi-grid.tsx","./src/components/dashboard/dashboard-kpi-sparkline-row.tsx","./src/components/dashboard/dashboard-modules-grid.tsx","./src/components/dashboard/dashboard-network-health.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-operations-breakdown.tsx","./src/components/dashboard/dashboard-quick-links.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-2.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rule-create-dialog.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/layout/command-palette.tsx","./src/components/layout/system-monitor-popover.tsx","./src/components/lookup/lookup-matches-grid.tsx","./src/components/lookup/lookup-search-form.tsx","./src/components/lookup/lookup-summary-kpi.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-card.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/patterns/donut-breakdown-card.tsx","./src/components/patterns/illustrated-empty-state.tsx","./src/components/patterns/index.ts","./src/components/patterns/kpi-sparkline-card.tsx","./src/components/patterns/metric-tone-styles.ts","./src/components/patterns/panel-corners.tsx","./src/components/patterns/projects-empty-state.tsx","./src/components/patterns/segmented-progress-card.tsx","./src/components/reui/alert.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/icon-stack.tsx","./src/components/reui/number-field.tsx","./src/components/reui/rating.tsx","./src/components/reui/timeline.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/reui-kit/detail-panel.tsx","./src/components/reui-kit/filter-utils.ts","./src/components/reui-kit/index.ts","./src/components/reui-kit/kpi-stat-grid.tsx","./src/components/reui-kit/ops-dashboard.tsx","./src/components/reui-kit/quick-action-grid.tsx","./src/components/reui-kit/resource-page.tsx","./src/components/reui-kit/settings-shell.tsx","./src/components/schedule/schedule-agenda-panel.tsx","./src/components/schedule/schedule-calendar-view.tsx","./src/components/schedule/schedule-jobs-card.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/appearance-settings-tab.tsx","./src/components/settings/connection-settings-tab.tsx","./src/components/settings/sections-settings-tab.tsx","./src/components/settings/session-settings-tab.tsx","./src/components/settings/settings-kv-grid.tsx","./src/components/settings/settings-page-shell.tsx","./src/components/settings/settings-setting-field.tsx","./src/components/settings/settings-tabs-data.tsx","./src/components/ui/svgs/anthropicblack.tsx","./src/components/ui/svgs/anthropicwhite.tsx","./src/components/ui/svgs/convex.tsx","./src/components/ui/svgs/discord.tsx","./src/components/ui/svgs/gemini.tsx","./src/components/ui/svgs/googlecloud.tsx","./src/components/ui/svgs/hono.tsx","./src/components/ui/svgs/loom.tsx","./src/components/ui/svgs/mintlify.tsx","./src/components/ui/svgs/n8n.tsx","./src/components/ui/svgs/neon.tsx","./src/components/ui/svgs/openai.tsx","./src/components/ui/svgs/openaidark.tsx","./src/components/ui/svgs/paper.tsx","./src/components/ui/svgs/planetscale.tsx","./src/components/ui/svgs/planetscaledark.tsx","./src/components/ui/svgs/prisma.tsx","./src/components/ui/svgs/prismadark.tsx","./src/components/ui/svgs/remixdark.tsx","./src/components/ui/svgs/remixlight.tsx","./src/components/ui/svgs/resendiconblack.tsx","./src/components/ui/svgs/resendiconwhite.tsx","./src/components/ui/svgs/slack.tsx","./src/components/ui/svgs/stripe.tsx","./src/components/ui/svgs/supabase.tsx","./src/components/ui/svgs/zoom.tsx","./src/hooks/use-client-data-grid.ts","./src/hooks/use-copy-to-clipboard.ts","./src/hooks/use-file-upload.ts","./src/hooks/use-mobile.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/ui-surface.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/lookup.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/lookup.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/vite-env.d.ts","./src/components/app-switcher.tsx","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/counted-line-tabs.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/drawer-layout.tsx","./src/components/empty-state.tsx","./src/components/form-drawer.tsx","./src/components/kpi-stat-grid.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/panel-card.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/card-dot-field.tsx","./src/components/dashboard/dashboard-activity-timeline.tsx","./src/components/dashboard/dashboard-frame-panel.tsx","./src/components/dashboard/dashboard-kpi-grid.tsx","./src/components/dashboard/dashboard-kpi-sparkline-row.tsx","./src/components/dashboard/dashboard-modules-grid.tsx","./src/components/dashboard/dashboard-network-health.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-operations-breakdown.tsx","./src/components/dashboard/dashboard-quick-links.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-2.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rule-create-dialog.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/layout/apps-menu.tsx","./src/components/layout/command-palette.tsx","./src/components/layout/system-monitor-popover.tsx","./src/components/lookup/lookup-matches-grid.tsx","./src/components/lookup/lookup-search-form.tsx","./src/components/lookup/lookup-summary-kpi.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-card.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/patterns/donut-breakdown-card.tsx","./src/components/patterns/illustrated-empty-state.tsx","./src/components/patterns/index.ts","./src/components/patterns/kpi-sparkline-card.tsx","./src/components/patterns/metric-tone-styles.ts","./src/components/patterns/panel-corners.tsx","./src/components/patterns/projects-empty-state.tsx","./src/components/patterns/segmented-progress-card.tsx","./src/components/reui/alert.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/icon-stack.tsx","./src/components/reui/number-field.tsx","./src/components/reui/rating.tsx","./src/components/reui/timeline.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/reui-kit/detail-panel.tsx","./src/components/reui-kit/filter-utils.ts","./src/components/reui-kit/index.ts","./src/components/reui-kit/kpi-stat-grid.tsx","./src/components/reui-kit/ops-dashboard.tsx","./src/components/reui-kit/quick-action-grid.tsx","./src/components/reui-kit/resource-page.tsx","./src/components/reui-kit/settings-shell.tsx","./src/components/schedule/schedule-agenda-panel.tsx","./src/components/schedule/schedule-calendar-view.tsx","./src/components/schedule/schedule-jobs-card.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/appearance-settings-tab.tsx","./src/components/settings/connection-settings-tab.tsx","./src/components/settings/sections-settings-tab.tsx","./src/components/settings/session-settings-tab.tsx","./src/components/settings/settings-kv-grid.tsx","./src/components/settings/settings-page-shell.tsx","./src/components/settings/settings-setting-field.tsx","./src/components/settings/settings-tabs-data.tsx","./src/components/ui/svgs/anthropicblack.tsx","./src/components/ui/svgs/anthropicwhite.tsx","./src/components/ui/svgs/convex.tsx","./src/components/ui/svgs/discord.tsx","./src/components/ui/svgs/gemini.tsx","./src/components/ui/svgs/googlecloud.tsx","./src/components/ui/svgs/hono.tsx","./src/components/ui/svgs/loom.tsx","./src/components/ui/svgs/mintlify.tsx","./src/components/ui/svgs/n8n.tsx","./src/components/ui/svgs/neon.tsx","./src/components/ui/svgs/openai.tsx","./src/components/ui/svgs/openaidark.tsx","./src/components/ui/svgs/paper.tsx","./src/components/ui/svgs/planetscale.tsx","./src/components/ui/svgs/planetscaledark.tsx","./src/components/ui/svgs/prisma.tsx","./src/components/ui/svgs/prismadark.tsx","./src/components/ui/svgs/remixdark.tsx","./src/components/ui/svgs/remixlight.tsx","./src/components/ui/svgs/resendiconblack.tsx","./src/components/ui/svgs/resendiconwhite.tsx","./src/components/ui/svgs/slack.tsx","./src/components/ui/svgs/stripe.tsx","./src/components/ui/svgs/supabase.tsx","./src/components/ui/svgs/zoom.tsx","./src/hooks/use-app-switcher.ts","./src/hooks/use-client-data-grid.ts","./src/hooks/use-copy-to-clipboard.ts","./src/hooks/use-file-upload.ts","./src/hooks/use-mobile.ts","./src/lib/api-client.ts","./src/lib/app-switcher-config.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/ui-surface.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/lookup.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/lookup.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index ef3c9d3..c936f72 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -48,9 +48,28 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad Gating: KV `ui_show_quick_actions` in `global_settings` via `PATCH /v1/settings` (default `true`). +## 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). + +При переключении между vps-tracker / CFDM / EvoBGP меняются **только** sidebar nav и `main` content. ClassNames 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** | +| `main` | `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` | +| Sidebar header | `AppSwitcher` (не static brand) | + +App Switcher ids: `vps-tracker` · `cfdm` · `evobgp`. Override: `VITE_APP_SWITCHER` JSON. + +QuickActionGrid icons: только semantic **text** (`text-info` / `text-primary` / …) на kit `bg-muted` — без solid `bg-primary` fills. Preview: [stats-12](https://reui.io/preview/base/stats-12). + ## System monitor -`SystemMonitorPopover` in app-shell header next to `ModeToggle`. Preview: https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7 +`SystemMonitorPopover` in app-shell header next to `ModeToggle` (после AppsMenu). Preview: https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7 ## MCP workflow @@ -64,7 +83,7 @@ Primitives: MCP `plugin-shadcn-shadcn` + `@evobgp/ui`. ## Spacing -- Main: `gap-4 md:gap-6`, `px-4 md:px-6` +- AppShell main: `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` (shared chrome) - No `space-y-*` / `space-x-*` — use `flex` + `gap-*` - Max 1 primary CTA per screen - Semantic tokens only (`variant="success"|"info"|"warning"`) — no raw `bg-emerald-*`