"use client"; import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import type { HousekeepingSearchResult, HousekeepingSurface, } from "../contracts"; import { toHousekeepingHref } from "../routing/href"; import type { HousekeepingCommandHit, HousekeepingNavigationHit, } from "../search/navigation-search"; import type { HousekeepingSearchResponse } from "../search/search-service"; export interface CommandDeckLabels { readonly trigger: string; readonly placeholder: string; readonly navigation: string; readonly commands: string; readonly entities: string; readonly loading: string; readonly empty: string; readonly partial: string; readonly close: string; } export type HousekeepingSearchExecutor = ( term: string, ) => Promise; type CommandDeckHit = | HousekeepingNavigationHit | HousekeepingCommandHit | HousekeepingSearchResult; type CommandDeckGroup = "navigation" | "commands" | "entities"; export type CommandDeckKeyboardAction = | "close" | "next" | "previous" | "activate" | null; export interface CommandDeckEntry { readonly group: CommandDeckGroup; readonly value: string; readonly hit: CommandDeckHit; } const EMPTY_RESPONSE: HousekeepingSearchResponse = { navigation: [], commands: [], entities: [], errors: [], correlationId: "initial-search", }; export function isCommandDeckShortcut(input: { readonly key: string; readonly ctrlKey: boolean; readonly metaKey: boolean; readonly altKey?: boolean; readonly shiftKey?: boolean; }): boolean { return ( input.key.toLocaleLowerCase() === "k" && (input.ctrlKey || input.metaKey) && input.altKey !== true && input.shiftKey !== true ); } export function commandDeckKeyAction( key: string, open: boolean, ): CommandDeckKeyboardAction { if (!open) return null; if (key === "Escape") return "close"; if (key === "ArrowDown") return "next"; if (key === "ArrowUp") return "previous"; if (key === "Enter") return "activate"; return null; } export function nextCommandDeckIndex( current: number, count: number, direction: "next" | "previous", ): number { if (count <= 0) return -1; if (direction === "next") return (current + 1 + count) % count; return (current - 1 + count) % count; } export function restoreCommandDeckFocus( target: { focus(): void; } | null, ): void { target?.focus(); } export function projectHousekeepingSearchHref( href: HousekeepingNavigationHit["href"], surface: HousekeepingSurface, ) { return toHousekeepingHref(href, surface); } export function flattenHousekeepingSearchResults( response: HousekeepingSearchResponse, ): readonly CommandDeckEntry[] { const group = ( name: CommandDeckGroup, hits: readonly CommandDeckHit[], ): readonly CommandDeckEntry[] => hits.map((hit) => ({ group: name, value: `${name}:${hit.domain}:${hit.id}`, hit, })); return [ ...group("navigation", response.navigation), ...group("commands", response.commands), ...group("entities", response.entities), ]; } export function CommandDeck({ labels, search, surface, }: { readonly labels: CommandDeckLabels; readonly search: HousekeepingSearchExecutor; readonly surface: HousekeepingSurface; }) { const [open, setOpen] = useState(false); const [term, setTerm] = useState(""); const [response, setResponse] = useState(EMPTY_RESPONSE); const [loading, setLoading] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const triggerRef = useRef(null); const inputRef = useRef(null); const requestSequence = useRef(0); const wasOpen = useRef(false); const entries = useMemo( () => flattenHousekeepingSearchResults(response), [response], ); const close = useCallback(() => setOpen(false), []); useEffect(() => { const onShortcut = (event: globalThis.KeyboardEvent) => { if (!isCommandDeckShortcut(event)) return; event.preventDefault(); setOpen((current) => !current); }; document.addEventListener("keydown", onShortcut); return () => document.removeEventListener("keydown", onShortcut); }, []); useEffect(() => { if (open) { wasOpen.current = true; inputRef.current?.focus(); return; } if (wasOpen.current) { wasOpen.current = false; restoreCommandDeckFocus(triggerRef.current); } }, [open]); useEffect(() => { if (!open) return; const sequence = ++requestSequence.current; let active = true; setLoading(true); const timer = setTimeout(() => { search(term) .then((nextResponse) => { if (active && sequence === requestSequence.current) { setResponse(nextResponse); } }) .catch(() => { if (active && sequence === requestSequence.current) { setResponse({ ...EMPTY_RESPONSE, errors: [{ providerId: "search", code: "INTERNAL" }], correlationId: "client-search", }); } }) .finally(() => { if (active && sequence === requestSequence.current) setLoading(false); }); }, 120); return () => { active = false; clearTimeout(timer); }; }, [open, search, term]); useEffect(() => { setSelectedIndex(entries.length === 0 ? -1 : 0); }, [entries.length]); const activate = useCallback( (entry: CommandDeckEntry) => { const href = projectHousekeepingSearchHref(entry.hit.href, surface); close(); window.location.assign(href); }, [close, surface], ); const onKeyDown = (event: KeyboardEvent) => { const action = commandDeckKeyAction(event.key, open); if (action === null) return; event.preventDefault(); event.stopPropagation(); if (action === "close") { close(); return; } if (action === "activate") { const selected = entries[selectedIndex]; if (selected) activate(selected); return; } setSelectedIndex((current) => nextCommandDeckIndex(current, entries.length, action), ); }; const selectedValue = entries[selectedIndex]?.value ?? ""; const renderGroup = (group: CommandDeckGroup, heading: string) => { const groupedEntries = entries.filter((entry) => entry.group === group); if (groupedEntries.length === 0) return null; return ( {groupedEntries.map((entry) => ( activate(entry)} > {entry.hit.title} {entry.hit.domain} ))} ); }; return ( <> {open ? (
{ const index = entries.findIndex( (entry) => entry.value === value, ); if (index >= 0) setSelectedIndex(index); }} onKeyDownCapture={onKeyDown} > {loading ?

{labels.loading}

: null} {response.errors.length > 0 ? (

{labels.partial}

) : null} {!loading ? {labels.empty} : null} {renderGroup("navigation", labels.navigation)} {renderGroup("commands", labels.commands)} {renderGroup("entities", labels.entities)}
) : null} ); }