"use client"; import { useRouter } from "next/navigation"; import * as React from "react"; import { EmptyState, Modal, cx } from "@agy/ui"; import { ADMIN_ITEMS, NAV_ITEMS, type NavItem } from "@/lib/nav"; /** * Command palette over the app's destinations. * * PLAN §12.3 asks for a command palette so power users can move between * sections without hunting through the sidebar. It is a client-side filter * over a fixed route list — the app has no dynamic search surface yet, so a * round trip would buy nothing and would break while offline. * * The platform-owner group is passed in rather than read from the session so * this component stays free of provider setup and is renderable in isolation. */ /** ``⌘K`` on macOS, ``Ctrl+K`` everywhere else. */ function isPaletteShortcut(event: KeyboardEvent): boolean { return ( (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "k" ); } interface PalletGroup { title: string; items: NavItem[]; } function buildGroups(isPlatformOwner: boolean): PalletGroup[] { const groups: PalletGroup[] = [{ title: "بخش‌ها", items: NAV_ITEMS }]; if (isPlatformOwner) { groups.push({ title: "کنسول مالک سامانه", items: ADMIN_ITEMS }); } return groups; } function matches(item: NavItem, needle: string): boolean { return ( item.label.toLowerCase().includes(needle) || item.hint.toLowerCase().includes(needle) || item.href.includes(needle) ); } export function CommandPalette({ isPlatformOwner = false }: { isPlatformOwner?: boolean }) { const router = useRouter(); const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); const [active, setActive] = React.useState(0); const inputRef = React.useRef(null); const groups = React.useMemo(() => buildGroups(isPlatformOwner), [isPlatformOwner]); const filteredGroups = React.useMemo(() => { const needle = query.trim().toLowerCase(); const keep = (item: NavItem) => !needle || matches(item, needle); return groups .map((group) => ({ title: group.title, items: group.items.filter(keep) })) .filter((group) => group.items.length > 0); }, [groups, query]); /** * Keyboard navigation walks the flattened, visible item list. `indexOf` is * safe here because both lists hold the same module-level object references. */ const visible = React.useMemo( () => filteredGroups.flatMap((group) => group.items), [filteredGroups], ); // Toggle the palette from anywhere on the page without stealing focus. React.useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (!isPaletteShortcut(event)) return; event.preventDefault(); setQuery(""); setActive(0); setOpen((currentlyOpen) => !currentlyOpen); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); // Focus the input once the dialog is mounted, so typing starts immediately. React.useEffect(() => { if (open) inputRef.current?.focus(); }, [open]); // A narrowed list must not leave the highlight off the end. React.useEffect(() => { setActive(0); }, [query]); const close = React.useCallback(() => { setOpen(false); setQuery(""); }, []); const go = React.useCallback( (href: string) => { close(); router.push(href); }, [close, router], ); const onInputKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); close(); return; } if (visible.length === 0) return; if (event.key === "ArrowDown") { event.preventDefault(); setActive((index) => (index + 1) % visible.length); } else if (event.key === "ArrowUp") { event.preventDefault(); setActive((index) => (index - 1 + visible.length) % visible.length); } else if (event.key === "Enter") { const target = visible[active]; if (target) { event.preventDefault(); go(target.href); } } }; return ( <> setQuery(event.target.value)} onKeyDown={onInputKeyDown} placeholder="نام بخش…" aria-label="جست‌وجوی بخش‌ها" className="h-10 w-full rounded-lg border border-surface-border bg-surface-overlay px-3 text-sm text-slate-100 placeholder:text-slate-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60" />
{filteredGroups.length === 0 ? ( ) : ( filteredGroups.map((group) => (

{group.title}

{group.items.map((item) => { const index = visible.indexOf(item); const isActive = index === active; return ( ); })}
)) )}
); }