75 lines
3.5 KiB
TypeScript
75 lines
3.5 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
import { usePathname, useRouter } from "next/navigation"
|
|
import { navigationGroups } from "@/lib/navigation"
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
|
|
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"
|
|
|
|
/** Keyboard search from the App Shell 12 composition, using installed Base UI primitives. */
|
|
export function CommandPalette() {
|
|
const [open, setOpen] = useState(false)
|
|
const [query, setQuery] = useState("")
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const returnFocusRef = useRef<HTMLElement | null>(null)
|
|
const pathname = usePathname()
|
|
const router = useRouter()
|
|
|
|
useEffect(() => {
|
|
function onKey(event: KeyboardEvent) {
|
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k" && !event.isComposing) {
|
|
event.preventDefault()
|
|
if (!open) {
|
|
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
setQuery("")
|
|
}
|
|
setOpen(!open)
|
|
}
|
|
}
|
|
window.addEventListener("keydown", onKey)
|
|
return () => window.removeEventListener("keydown", onKey)
|
|
}, [open])
|
|
|
|
useEffect(() => {
|
|
queueMicrotask(() => setOpen(false))
|
|
}, [pathname])
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogContent
|
|
showCloseButton={false}
|
|
initialFocus={inputRef}
|
|
finalFocus={returnFocusRef}
|
|
className="flex h-[min(32rem,calc(100dvh-2rem))] flex-col gap-0 overflow-hidden p-0 sm:max-w-xl"
|
|
>
|
|
<DialogTitle className="sr-only">Поиск страниц</DialogTitle>
|
|
<DialogDescription className="sr-only">Найдите раздел по названию или ключевым словам. Стрелки выбирают страницу, Enter открывает её, Escape закрывает поиск.</DialogDescription>
|
|
<Command className="min-h-0 flex-1" label="Поиск страниц">
|
|
<CommandInput ref={inputRef} aria-label="Поиск страниц" placeholder="Название страницы или ключевое слово…" value={query} onValueChange={setQuery} />
|
|
<CommandList className="min-h-0 max-h-none flex-1" label="Найденные страницы">
|
|
<CommandEmpty><p role="status">Ничего не найдено. Попробуйте другое название.</p></CommandEmpty>
|
|
{navigationGroups.map(group => (
|
|
<CommandGroup key={group.label} heading={group.label}>
|
|
{group.items.map(item => {
|
|
const Icon = item.icon
|
|
return (
|
|
<CommandItem key={item.id} value={item.title} keywords={item.keywords} onSelect={() => {
|
|
setOpen(false)
|
|
router.push(item.url)
|
|
}}>
|
|
<Icon aria-hidden="true" />
|
|
<span>{item.title}</span>
|
|
{pathname === item.url && <span className="ml-auto text-xs text-muted-foreground">Текущая</span>}
|
|
</CommandItem>
|
|
)
|
|
})}
|
|
</CommandGroup>
|
|
))}
|
|
</CommandList>
|
|
</Command>
|
|
<p className="shrink-0 border-t px-3 py-2 text-xs text-muted-foreground">↑↓ выбрать · Enter открыть · Esc закрыть</p>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|