85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
|
|
export interface ContextualCommand {
|
|
id: string;
|
|
group: string;
|
|
label: string;
|
|
icon?: React.ComponentType<{ className?: string }>;
|
|
shortcut?: string;
|
|
keywords?: string[];
|
|
onSelect: () => void;
|
|
}
|
|
|
|
interface CommandPaletteAPI {
|
|
registerCommand: (entry: ContextualCommand) => void;
|
|
unregisterCommand: (id: string) => void;
|
|
commands: ContextualCommand[];
|
|
}
|
|
|
|
const CommandPaletteContext = React.createContext<CommandPaletteAPI | null>(null);
|
|
|
|
export { CommandPaletteContext };
|
|
|
|
export function useCommandPalette() {
|
|
const ctx = React.useContext(CommandPaletteContext);
|
|
if (!ctx) {
|
|
throw new Error("useCommandPalette must be used within CommandPaletteProvider");
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
export function CommandPaletteProvider({ children }: { children: React.ReactNode }) {
|
|
const commandsRef = React.useRef<Map<string, ContextualCommand>>(new Map());
|
|
const [, forceUpdate] = React.useReducer((x: number) => x + 1, 0);
|
|
|
|
const registerCommand = React.useCallback((entry: ContextualCommand) => {
|
|
commandsRef.current.set(entry.id, entry);
|
|
forceUpdate();
|
|
}, []);
|
|
|
|
const unregisterCommand = React.useCallback((id: string) => {
|
|
commandsRef.current.delete(id);
|
|
forceUpdate();
|
|
}, []);
|
|
|
|
const commands = React.useMemo(
|
|
() => Array.from(commandsRef.current.values()),
|
|
// forceUpdate triggers re-render; recompute on each render via ref identity
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[commandsRef, forceUpdate],
|
|
);
|
|
|
|
const value = React.useMemo<CommandPaletteAPI>(
|
|
() => ({ registerCommand, unregisterCommand, commands }),
|
|
[registerCommand, unregisterCommand, commands],
|
|
);
|
|
|
|
return (
|
|
<CommandPaletteContext.Provider value={value}>{children}</CommandPaletteContext.Provider>
|
|
);
|
|
}
|
|
|
|
/** Register a set of commands for the current page. Unregisters on unmount. */
|
|
export function usePageCommands(commands: ContextualCommand[]) {
|
|
const { registerCommand, unregisterCommand } = useCommandPalette();
|
|
|
|
// Keep the latest commands in a ref so the effect doesn't re-run on every
|
|
// render when the parent recreates the array. The effect syncs the registry
|
|
// to the ref contents on each change.
|
|
const commandsRef = React.useRef(commands);
|
|
commandsRef.current = commands;
|
|
|
|
React.useEffect(() => {
|
|
const current = commandsRef.current;
|
|
for (const cmd of current) {
|
|
registerCommand(cmd);
|
|
}
|
|
return () => {
|
|
for (const cmd of current) {
|
|
unregisterCommand(cmd.id);
|
|
}
|
|
};
|
|
}, [registerCommand, unregisterCommand]);
|
|
}
|