pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/components/command-palette.tsx

51 lines1,683 bytesCodeBlame
1import { type ReactNode, Suspense, lazy, useEffect, useRef, useState } from "react";
2
3/*
4 * The ⌘K palette. The dialog itself, with its search and its dependencies,
5 * loads the first time it is opened, so pages that never open it do not
6 * carry it.
7 */
8const Dialog = lazy(() => import("./command-palette-dialog"));
9
10/** A page or action the palette can jump to. */
11export type PaletteCommand = { label: string; hint?: string; to: string; icon: ReactNode };
12
13/** Opens and closes the palette on ⌘K or Ctrl-K, from anywhere on the page. */
14export function usePaletteShortcut(toggle: () => void) {
15 const latest = useRef(toggle);
16 latest.current = toggle;
17 useEffect(() => {
18 const onKey = (event: KeyboardEvent) => {
19 if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
20 event.preventDefault();
21 latest.current();
22 }
23 };
24 window.addEventListener("keydown", onKey);
25 return () => window.removeEventListener("keydown", onKey);
26 }, []);
27}
28
29/**
30 * ⌘K: pages and actions from what the page already knows, and, as someone
31 * types, repositories, issues, pull requests and people from search
32 * (command-palette-dialog.tsx).
33 */
34export function CommandPalette(props: {
35 open: boolean;
36 onOpenChange: (open: boolean) => void;
37 commands: PaletteCommand[];
38 /** The repository being looked at, `owner/name`, to offer searching its code. */
39 repo?: string | null;
40}) {
41 const [wanted, setWanted] = useState(props.open);
42 useEffect(() => {
43 if (props.open) setWanted(true);
44 }, [props.open]);
45 if (!wanted) return null;
46 return (
47 <Suspense fallback={null}>
48 <Dialog {...props} />
49 </Suspense>
50 );
51}