"use client"; import { type AutocompleteApi, type AutocompleteCollection, type AutocompleteState, createAutocomplete, } from "@algolia/autocomplete-core"; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, } from "@headlessui/react"; import clsx from "clsx"; import { useRouter } from "next/navigation"; import { type ComponentPropsWithoutRef, type ComponentRef, Fragment, forwardRef, type MouseEvent, type KeyboardEvent as ReactKeyboardEvent, Suspense, type SyntheticEvent, useCallback, useEffect, useId, useRef, useState, } from "react"; import Highlighter from "react-highlight-words"; import type { Result } from "@/mdx/search.mjs"; import { navigation } from "./Navigation"; type EmptyObject = Record; type Autocomplete = AutocompleteApi< Result, SyntheticEvent, MouseEvent, ReactKeyboardEvent >; function useAutocomplete({ close }: { close: () => void }) { const id = useId(); const router = useRouter(); const [autocompleteState, setAutocompleteState] = useState< AutocompleteState | EmptyObject >({}); function navigate({ itemUrl }: { itemUrl?: string }) { if (!itemUrl) { return; } router.push(itemUrl); if ( itemUrl === window.location.pathname + window.location.search + window.location.hash ) { close(); } } const [autocomplete] = useState(() => createAutocomplete< Result, SyntheticEvent, MouseEvent, ReactKeyboardEvent >({ id, placeholder: "Find something...", defaultActiveItemId: 0, onStateChange({ state }) { setAutocompleteState(state); }, shouldPanelOpen({ state }) { return state.query !== ""; }, navigator: { navigate, }, getSources({ query }) { return import("@/mdx/search.mjs").then(({ search }) => { return [ { sourceId: "documentation", getItems() { return search(query, { limit: 5 }); }, getItemUrl({ item }) { return item.url; }, onSelect: navigate, }, ]; }); }, }), ); return { autocomplete, autocompleteState }; } function SearchIcon(props: ComponentPropsWithoutRef<"svg">) { return ( ); } function NoResultsIcon(props: ComponentPropsWithoutRef<"svg">) { return ( ); } function LoadingIcon(props: ComponentPropsWithoutRef<"svg">) { const id = useId(); return ( ); } function HighlightQuery({ text, query }: { text: string; query: string }) { return ( ); } function SearchResult({ result, resultIndex, autocomplete, collection, query, }: { result: Result; resultIndex: number; autocomplete: Autocomplete; collection: AutocompleteCollection; query: string; }) { const id = useId(); const sectionTitle = navigation.find((section) => section.links.find((link) => link.href === result.url.split("#")[0]), )?.title; const hierarchy = [sectionTitle, result.pageTitle].filter( (x): x is string => typeof x === "string", ); return (
  • 0 && "border-t border-zinc-100 dark:border-zinc-800", )} aria-labelledby={`${id}-hierarchy ${id}-title`} {...autocomplete.getItemProps({ item: result, source: collection.source, })} > {hierarchy.length > 0 && ( )}
  • ); } function SearchResults({ autocomplete, query, collection, }: { autocomplete: Autocomplete; query: string; collection: AutocompleteCollection; }) { if (collection.items.length === 0) { return (

    Nothing found for{" "} ‘{query}’ . Please try again.

    ); } return (
      {collection.items.map((result, resultIndex) => ( ))}
    ); } const SearchInput = forwardRef< ComponentRef<"input">, { autocomplete: Autocomplete; autocompleteState: AutocompleteState | EmptyObject; onClose: () => void; } >(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) { const inputProps = autocomplete.getInputProps({ inputElement: null }); return (
    { if ( event.currentTarget.value .toLowerCase() .includes("barrel roll") ) { event.currentTarget.value = ""; document.body.classList.add("animate-roll"); setTimeout( () => document.body.classList.remove("animate-roll"), 2000, ); } }} onKeyDown={(event) => { if ( event.key === "Escape" && !autocompleteState.isOpen && autocompleteState.query === "" ) { // In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the // bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI. if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } onClose(); } else { inputProps.onKeyDown(event); } }} /> {autocompleteState.status === "stalled" && (
    )}
    ); }); function SearchDialog({ open, setOpen, className, }: { open: boolean; setOpen: (open: boolean) => void; className?: string; }) { const formRef = useRef>(null); const panelRef = useRef>(null); const inputRef = useRef>(null); const { autocomplete, autocompleteState } = useAutocomplete({ close() { setOpen(false); }, }); useEffect(() => { setOpen(false); }, [setOpen]); useEffect(() => { if (open) { return; } function onKeyDown(event: KeyboardEvent) { if (event.key === "k" && (event.metaKey || event.ctrlKey)) { event.preventDefault(); setOpen(true); } } window.addEventListener("keydown", onKeyDown); return () => { window.removeEventListener("keydown", onKeyDown); }; }, [open, setOpen]); return ( autocomplete.setQuery("")}>
    Search
    setOpen(false)} />
    {autocompleteState.isOpen && ( )}
    ); } function useSearchProps() { const buttonRef = useRef>(null); const [open, setOpen] = useState(false); return { buttonProps: { ref: buttonRef, onClick() { setOpen(true); }, }, dialogProps: { open, setOpen: useCallback((open: boolean) => { const { width = 0, height = 0 } = buttonRef.current?.getBoundingClientRect() ?? {}; if (!open || (width !== 0 && height !== 0)) { setOpen(open); } }, []), }, }; } export function Search() { const [modifierKey, setModifierKey] = useState(); const { buttonProps, dialogProps } = useSearchProps(); useEffect(() => { setModifierKey( /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌘" : "Ctrl ", ); }, []); return (
    ); } export function MobileSearch() { const { buttonProps, dialogProps } = useSearchProps(); return (
    ); }