'use client'; import * as React from 'react'; import { Search } from 'lucide-react'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import { useRouter } from '@/i18n/navigation'; import { cn } from '@/lib/utils'; import { isItemActive, type NavGroup, type ShellVariant } from './nav-config'; /** * Palette de commandes (⌘K / Ctrl+K). * * Implementee sur Radix Dialog pour heriter du focus trap et de la fermeture * par Escape, avec une navigation clavier explicite (fleches, Entree). */ interface CommandMenuProps { open: boolean; onOpenChange: (open: boolean) => void; groups: NavGroup[]; pathname: string; variant: ShellVariant; labels: { title: string; placeholder: string; noResults: string; }; } /** Normalise pour une recherche insensible a la casse et aux accents. */ function normalize(value: string): string { return value .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); } export function CommandMenu({ open, onOpenChange, groups, pathname, variant, labels, }: CommandMenuProps) { const router = useRouter(); const [query, setQuery] = React.useState(''); const [highlight, setHighlight] = React.useState(0); const listRef = React.useRef(null); const entries = React.useMemo( () => groups.flatMap(group => group.items.map(item => ({ ...item, groupLabel: group.label })) ), [groups] ); const results = React.useMemo(() => { const q = normalize(query.trim()); if (!q) return entries; return entries.filter( e => normalize(e.label).includes(q) || normalize(e.groupLabel).includes(q) ); }, [entries, query]); // Reinitialise a chaque ouverture, et borne l'index quand la liste retrecit. React.useEffect(() => { if (open) { setQuery(''); setHighlight(0); } }, [open]); React.useEffect(() => { setHighlight(h => Math.min(h, Math.max(0, results.length - 1))); }, [results.length]); const select = React.useCallback( (href: string) => { onOpenChange(false); router.push(href); }, [onOpenChange, router] ); const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'ArrowDown') { event.preventDefault(); setHighlight(h => (results.length ? (h + 1) % results.length : 0)); } else if (event.key === 'ArrowUp') { event.preventDefault(); setHighlight(h => (results.length ? (h - 1 + results.length) % results.length : 0)); } else if (event.key === 'Enter') { event.preventDefault(); const target = results[highlight]; if (target) select(target.href); } }; // Maintient l'element surligne visible lors de la navigation au clavier. React.useEffect(() => { listRef.current ?.querySelector(`[data-index="${highlight}"]`) ?.scrollIntoView({ block: 'nearest' }); }, [highlight]); return ( {labels.title}
{results.length === 0 ? (

{labels.noResults}

) : ( results.map((item, index) => { const Icon = item.icon; const active = isItemActive(item.href, pathname, variant); return ( ); }) )}
); } /** Ouvre la palette sur ⌘K / Ctrl+K. */ export function useCommandMenuShortcut(onOpen: () => void) { React.useEffect(() => { const handler = (event: KeyboardEvent) => { if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); onOpen(); } }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [onOpen]); }