mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-21 15:33:30 +10:00
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { useHotkeys } from "@tanstack/react-hotkeys";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Command, CommandEmpty, CommandInput, CommandList } from "@reactive-resume/ui/components/command";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@reactive-resume/ui/components/dialog";
|
|
import { NavigationCommandGroup } from "./pages/navigation";
|
|
import { PreferencesCommandGroup } from "./pages/preferences";
|
|
import { ResumesCommandGroup } from "./pages/resumes";
|
|
import { useCommandPaletteStore } from "./store";
|
|
|
|
export function CommandPalette() {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const commandRef = useRef<HTMLDivElement>(null);
|
|
const [selectedValue, setSelectedValue] = useState<string>();
|
|
const { open, search, pages, setOpen, setSearch, goBack } = useCommandPaletteStore();
|
|
|
|
const isFirstPage = pages.length === 0;
|
|
const currentPage = pages[pages.length - 1];
|
|
const commandListPage = currentPage ?? "root";
|
|
|
|
// Toggle command palette with Cmd+K / Ctrl+K
|
|
useHotkeys([
|
|
{
|
|
hotkey: "Mod+K",
|
|
callback: () => {
|
|
setOpen(!open);
|
|
},
|
|
},
|
|
{
|
|
hotkey: "Escape",
|
|
callback: () => {
|
|
if (!open) return;
|
|
setOpen(false);
|
|
},
|
|
},
|
|
{
|
|
hotkey: "Backspace",
|
|
callback: (event) => {
|
|
// Only handle if the command palette is open
|
|
if (!open) return;
|
|
|
|
const input = inputRef.current;
|
|
if (!input) return;
|
|
|
|
// Only handle if input is focused
|
|
if (document.activeElement !== input) return;
|
|
|
|
// If input has text, let the default behavior handle it (delete character)
|
|
if (search.length > 0) return;
|
|
|
|
// If input is empty, prevent default and go back
|
|
event.preventDefault();
|
|
goBack();
|
|
},
|
|
},
|
|
]);
|
|
|
|
const handleOpenChange = (newOpen: boolean) => {
|
|
setOpen(newOpen);
|
|
};
|
|
|
|
const handleSearchChange = (value: string) => {
|
|
setSearch(value);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const firstItem = commandRef.current?.querySelector<HTMLElement>(
|
|
`[data-command-page="${commandListPage}"] [cmdk-item]:not([aria-disabled="true"])`,
|
|
);
|
|
const value = firstItem?.getAttribute("data-value");
|
|
|
|
if (value) setSelectedValue(value);
|
|
inputRef.current?.focus();
|
|
}, [open, commandListPage]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogHeader className="sr-only print:hidden">
|
|
<DialogTitle>
|
|
<Trans comment="Screen-reader dialog title for the command palette in the resume builder">
|
|
Builder Command Palette
|
|
</Trans>
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
<Trans comment="Screen-reader dialog description instructing users how to use the command palette">
|
|
Type a command or search…
|
|
</Trans>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<DialogContent
|
|
className="overflow-hidden p-0"
|
|
aria-label={
|
|
isFirstPage
|
|
? t({
|
|
comment: "Accessible label for the command palette dialog",
|
|
message: "Command Palette",
|
|
})
|
|
: t({
|
|
comment: "Accessible label for command palette dialog when browsing a nested command page",
|
|
message: `Command Palette - ${currentPage}`,
|
|
})
|
|
}
|
|
>
|
|
<Command
|
|
ref={commandRef}
|
|
loop
|
|
value={selectedValue}
|
|
onValueChange={setSelectedValue}
|
|
aria-label={t({
|
|
comment: "Accessible label for command list region inside command palette",
|
|
message: "Command Palette",
|
|
})}
|
|
className="[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground **:[[cmdk-group]]:px-2 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3"
|
|
>
|
|
<CommandInput
|
|
ref={inputRef}
|
|
value={search}
|
|
onValueChange={handleSearchChange}
|
|
placeholder={
|
|
isFirstPage
|
|
? t({
|
|
comment: "Placeholder in command palette input on root page",
|
|
message: "Type a command or search…",
|
|
})
|
|
: t({
|
|
comment: "Placeholder in command palette input on nested pages",
|
|
message: "Search…",
|
|
})
|
|
}
|
|
aria-label={t({
|
|
comment: "Accessible label for command palette search input",
|
|
message: "Search commands",
|
|
})}
|
|
/>
|
|
|
|
<CommandList key={commandListPage} data-command-page={commandListPage}>
|
|
<CommandEmpty>
|
|
<Trans comment="Empty-state message when no command palette results match the search query">
|
|
The command you're looking for doesn't exist.
|
|
</Trans>
|
|
</CommandEmpty>
|
|
|
|
<ResumesCommandGroup />
|
|
<PreferencesCommandGroup />
|
|
<NavigationCommandGroup />
|
|
</CommandList>
|
|
</Command>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|