1
0
Fork 0
polaris-task-force/src/components/command-palette/CommandPaletteContext.tsx

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]);
}