"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { AnimatePresence, motion } from "motion/react"; import { HexMark } from "@/components/honeycomb"; export interface PaletteEntry { href: string; label: string; section: "pages" | "actions"; } /** * Schnellsuche fuer den Admin-Bereich (Strg/Cmd + K): jede Seite und die * haeufigsten Aktionen sind zwei Tastendruecke entfernt, statt durch die * Seitenleiste zu scrollen. Pfeiltasten waehlen, Enter springt, Esc schliesst. * * Sucht tolerant: jedes getippte Wort muss irgendwo im Eintrag vorkommen * ("neu gut" findet "Neuen Gutschein anlegen"). Ziele sind ausschliesslich * die fest hinterlegten Eintraege, nie die Eingabe selbst. */ export function AdminCommandPalette({ entries, labels, open, onOpenChange, }: { entries: PaletteEntry[]; labels: { placeholder: string; noResults: string; pages: string; actions: string; dialog: string }; open: boolean; onOpenChange: (open: boolean) => void; }) { const router = useRouter(); const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const inputRef = useRef(null); // Globale Tastenkombination — auch wenn der Fokus gerade woanders liegt. useEffect(() => { const onKey = (event: KeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") { event.preventDefault(); onOpenChange(!open); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onOpenChange]); const results = useMemo(() => { const words = query.toLowerCase().split(/\s+/).filter(Boolean); if (words.length === 0) return entries; return entries.filter((entry) => words.every((word) => entry.label.toLowerCase().includes(word))); }, [entries, query]); function close() { onOpenChange(false); setQuery(""); setActive(0); } function go(entry: PaletteEntry | undefined) { if (!entry) return; close(); router.push(entry.href); } function onKeyDown(event: React.KeyboardEvent) { if (event.key === "Escape") { close(); } else if (event.key === "ArrowDown") { event.preventDefault(); setActive((i) => Math.min(results.length - 1, i + 1)); } else if (event.key === "ArrowUp") { event.preventDefault(); setActive((i) => Math.max(0, i - 1)); } else if (event.key === "Enter") { event.preventDefault(); go(results[active]); } } return ( {open ? ( { if (event.target === event.currentTarget) close(); }} > inputRef.current?.focus()} className="w-full max-w-lg overflow-hidden border border-ink/10 bg-background shadow-[0_40px_80px_-30px_rgba(36,53,43,0.6)]" >
{ setQuery(event.target.value); setActive(0); }} onKeyDown={onKeyDown} placeholder={labels.placeholder} role="combobox" aria-expanded="true" aria-controls="admin-palette-results" aria-activedescendant={results[active] ? `palette-${active}` : undefined} className="h-14 w-full bg-transparent text-base focus:outline-none focus-visible:outline-none focus:shadow-none" /> Esc
    {results.length === 0 ? (
  • {labels.noResults}
  • ) : null} {results.map((entry, i) => { const showSection = i === 0 || results[i - 1].section !== entry.section; return (
  • {showSection ? (

    {entry.section === "pages" ? labels.pages : labels.actions}

    ) : null}
  • ); })}
) : null}
); }