"use client"; import { ArrowDownIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { createContext, type ReactNode, useContext, useEffect, useMemo, useState, } from "react"; import { useTranslation } from "react-i18next"; import { Command, CommandCollection, CommandDialog, CommandDialogPopup, CommandEmpty, CommandFooter, CommandGroup, CommandGroupLabel, CommandInput, CommandItem, CommandList, CommandPanel, } from "@/components/ui/command"; import { Kbd, KbdGroup } from "@/components/ui/kbd"; import { useAiAccess } from "@/lib/ai-policy"; import { useActiveRole, visibleNavItems } from "@/lib/roles"; type CommandPaletteContextValue = { open: () => void }; const CommandPaletteContext = createContext( null, ); export function useCommandPalette(): CommandPaletteContextValue { const ctx = useContext(CommandPaletteContext); if (!ctx) { throw new Error( "useCommandPalette must be used within a CommandPaletteProvider", ); } return ctx; } // Holds the ⌘K palette open state, wires the global shortcut, and renders the // dialog. Wrap the app shell so the sidebar (and any child) can open it. export function CommandPaletteProvider({ children }: { children: ReactNode }) { const router = useRouter(); const { t } = useTranslation(); const role = useActiveRole(); const { allowed: aiAllowed } = useAiAccess(); const [open, setOpen] = useState(false); useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setOpen((prev) => !prev); } }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, []); // One group ("Go to") matching the COSS command-palette particle shape. const groups = useMemo( () => [ { value: "pages", label: t("nav.commandGroup"), // Flatten sub-pages so e.g. "Appointments & Schedule" is reachable. // Filtered by role so reception can't jump to clinical pages, and by // the AI kill-switch so the disabled chat isn't listed. items: visibleNavItems(role) .filter( (item) => aiAllowed || (item.id !== "new-chat" && item.id !== "analysis"), ) .flatMap((item) => item.subs?.length ? item.subs.map((sub) => ({ id: sub.id, label: t(sub.labelKey), link: sub.link, Icon: sub.icon ?? item.icon, })) : [ { id: item.id, label: t(item.labelKey), link: item.link, Icon: item.icon, }, ], ), }, ], [t, role, aiAllowed], ); type Group = (typeof groups)[number]; type Item = Group["items"][number]; const value = useMemo( () => ({ open: () => setOpen(true) }), [], ); const go = (link: string) => { setOpen(false); router.push(link); }; return ( {children} {t("nav.commandEmpty")} {(group: Group) => ( {group.label} {(item: Item) => ( go(item.link)} value={item.label} > {item.label} )} )}
{t("nav.commandNavigate")} {t("nav.commandOpen")}
Esc {t("nav.commandClose")}
); }