From c6fa3428e63db38acd5853c87ddde74f6041a424 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 17 Jul 2026 13:55:21 +0700 Subject: [PATCH] =?UTF-8?q?feat(layout):=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B8=D1=82=D1=8C=20chrome=20app-shell-12=20=D0=B2?= =?UTF-8?q?=20AppShell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SearchMenu (Cmd+K) и AppsMenu из конфига App Switcher в SiteHeader; sidebar без изменений. Co-authored-by: Cursor --- apps/web/src/components/layout/apps-menu.tsx | 93 +++++++++ .../web/src/components/layout/search-menu.tsx | 180 ++++++++++++++++++ .../web/src/components/layout/site-header.tsx | 12 +- 3 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/layout/apps-menu.tsx create mode 100644 apps/web/src/components/layout/search-menu.tsx 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..3b1387e --- /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 '@cfdm/ui/components/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@cfdm/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, wired to CFDM App Switcher. */ +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/search-menu.tsx b/apps/web/src/components/layout/search-menu.tsx new file mode 100644 index 0000000..8b35f76 --- /dev/null +++ b/apps/web/src/components/layout/search-menu.tsx @@ -0,0 +1,180 @@ +import { useEffect, useId, useMemo, useState } from 'react' +import { Link, useNavigate } from '@tanstack/react-router' +import { + FolderTreeIcon, + GlobeIcon, + LayoutDashboardIcon, + SearchIcon, + ServerIcon, + SettingsIcon, + ShieldCheckIcon, +} from 'lucide-react' +import { Button } from '@cfdm/ui/components/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@cfdm/ui/components/dialog' +import { Input } from '@cfdm/ui/components/input' +import { + Item, + ItemContent, + ItemGroup, + ItemMedia, + ItemTitle, +} from '@cfdm/ui/components/item' + +const NAV_ITEMS = [ + { + to: '/', + label: 'Панель управления', + keywords: ['dashboard', 'панель', 'обзор'], + icon: LayoutDashboardIcon, + }, + { + to: '/domains', + label: 'Домены', + keywords: ['domains', 'зоны', 'cloudflare'], + icon: GlobeIcon, + }, + { + to: '/groups', + label: 'Группы доменов', + keywords: ['groups', 'группы', 'канбан'], + icon: FolderTreeIcon, + }, + { + to: '/services', + label: 'Сервисы', + keywords: ['services', 'сервисы', 'fqdn'], + icon: ServerIcon, + }, + { + to: '/certificates', + label: 'Сертификаты', + keywords: ['certificates', 'ssl', 'tls'], + icon: ShieldCheckIcon, + }, + { + to: '/settings/integrations', + label: 'Настройки', + keywords: ['settings', 'интеграции', 'app switcher'], + icon: SettingsIcon, + }, +] as const + +/** Command-K search — app-shell-12 SearchMenu, routes only (no demo data). */ +export function SearchMenu() { + const [open, setOpen] = useState(false) + const [query, setQuery] = useState('') + const searchInputId = useId() + const navigate = useNavigate() + + useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) { + event.preventDefault() + setOpen(true) + } + } + + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, []) + + useEffect(() => { + if (!open) setQuery('') + }, [open]) + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase() + if (!q) return NAV_ITEMS + return NAV_ITEMS.filter( + (item) => + item.label.toLowerCase().includes(q) || + item.keywords.some((k) => k.includes(q)), + ) + }, [query]) + + function goTo(to: string) { + setOpen(false) + void navigate({ to }) + } + + return ( + <> + + + + + Поиск + + Переход к разделам приложения + + + +
+
+ + {filtered.length === 0 ? ( +

+ Ничего не найдено +

+ ) : ( + filtered.map((item) => ( + setOpen(false)} />} + > + + + + {item.label} + + + )) + )} +
+
+
+ + ) +} diff --git a/apps/web/src/components/layout/site-header.tsx b/apps/web/src/components/layout/site-header.tsx index bc76e9f..2ea6386 100644 --- a/apps/web/src/components/layout/site-header.tsx +++ b/apps/web/src/components/layout/site-header.tsx @@ -9,6 +9,8 @@ import { BreadcrumbSeparator, } from '@cfdm/ui/components/breadcrumb' import { ModeToggle } from '@/components/mode-toggle' +import { AppsMenu } from '@/components/layout/apps-menu' +import { SearchMenu } from '@/components/layout/search-menu' import { SidebarTrigger } from '@cfdm/ui/components/sidebar' export interface RouteBreadcrumbLoaderData { @@ -94,8 +96,9 @@ export function SiteHeader() { return (
- - +
+ + {crumbs.map((crumb, index) => { const isLast = index === crumbs.length - 1 @@ -120,7 +123,10 @@ export function SiteHeader() { })} -
+
+
+ +