import { useMemo, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Check, ChevronDown, ChevronRight, Plus, Search, X, } from "lucide-react"; import { cn } from "@/lib/utils"; export type TileKind = "card" | "signal" | "action" | "session" | "resource" | "chart"; export interface TilePickerEntry { /** Tile id the grid gets when this entry is added. */ id: string; title: string; description?: string; /** Group heading, usually the plugin name. */ group: string; kind: TileKind; icon?: ReactNode; /** Already on the grid; shown checked and not added twice. */ placed?: boolean; } const KINDS: TileKind[] = [ "card", "chart", "signal", "action", "resource", "session", ]; const KIND_LABELS: Record = { all: "tileGrid.kind.all", card: "tileGrid.kind.card", chart: "tileGrid.kind.chart", signal: "tileGrid.kind.signal", action: "tileGrid.kind.action", resource: "tileGrid.kind.resource", session: "tileGrid.kind.session", }; /** * Everything that can go on a grid, in a narrow column beside it while * editing. Search, filter by kind and fold groups, so long lists from many * plugins stay usable. */ export function TilePicker({ entries, onAdd, onClose, className, }: { entries: TilePickerEntry[]; onAdd: (entry: TilePickerEntry) => void; onClose?: () => void; className?: string; }) { const { t } = useTranslation(); const [query, setQuery] = useState(""); const [kind, setKind] = useState(null); const [folded, setFolded] = useState>(() => new Set()); const kinds = useMemo( () => KINDS.filter((k) => entries.some((entry) => entry.kind === k)), [entries], ); const groups = useMemo(() => { const needle = query.trim().toLowerCase(); const shown = entries.filter( (entry) => (!kind || entry.kind === kind) && (!needle || entry.title.toLowerCase().includes(needle) || entry.group.toLowerCase().includes(needle) || entry.description?.toLowerCase().includes(needle)), ); const map = new Map(); for (const entry of shown) { const list = map.get(entry.group) ?? []; list.push(entry); map.set(entry.group, list); } return [...map.entries()].sort(([a], [b]) => a.localeCompare(b)); }, [entries, query, kind]); const searching = query.trim().length > 0; const toggle = (group: string) => setFolded((current) => { const next = new Set(current); if (next.has(group)) next.delete(group); else next.add(group); return next; }); return (
{t("tileGrid.addTiles")} {onClose && ( )}
{kinds.length > 1 && (
{[null, ...kinds].map((k) => ( ))}
)}
{groups.length === 0 && (

{t("tileGrid.noMatches")}

)} {groups.map(([group, list]) => { const open = searching || !folded.has(group); return (
{open && (
    {list.map((entry) => (
  • ))}
)}
); })}
); }