lib/command-palette/store.ts
Rama main · réplica de lectura del repositorio original.
/**
* Zustand store for the command palette.
*
* Centralized state management:
* - View routing (search, form, custom views)
* - Search query and category filter
* - Navigation (active index, selected item)
* - Actions popover state
* - Inline form state
*
* Note: the store is a module-level singleton — mount ONE palette per app
* (the standard pattern for a global command palette).
*/
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import type { ContextualAction } from "./registry/types";
import type { PaletteItem } from "./types";
// =============================================================================
// TYPES
// =============================================================================
/**
* Active view. "search" and "form" are built in;
* any other string refers to a custom view passed to <CommandPalette views>.
*/
export type PaletteView = string;
export interface PaletteState {
// === View Management ===
view: PaletteView;
// === Search State ===
query: string;
/** Active category tab ("all" or a CategoryDefinition id). */
categoryFilter: string;
// === Navigation State ===
activeIndex: number;
selectedItem: PaletteItem | null;
// === Actions Popover State ===
actionTargetItem: PaletteItem | null;
actionActiveIndex: number;
isActionsPopoverOpen: boolean;
actionsPopoverQuery: string;
// === Form State ===
pendingAction: ContextualAction | null;
pendingActionItem: PaletteItem | null;
formValues: Record<string, unknown>;
formErrors: Record<string, string>;
previousView: PaletteView | null;
// === UI State ===
isExpanded: boolean;
isLoading: boolean;
}
export interface PaletteActions {
setView: (view: PaletteView) => void;
setQuery: (query: string) => void;
setCategoryFilter: (filter: string) => void;
setActiveIndex: (index: number) => void;
moveActiveIndex: (delta: number, maxIndex: number) => void;
selectItem: (item: PaletteItem | null) => void;
openActionsForItem: (item: PaletteItem) => void;
openActionsPopover: () => void;
closeActionsPopover: () => void;
setActionsPopoverQuery: (query: string) => void;
setActionActiveIndex: (index: number) => void;
moveActionActiveIndex: (delta: number, maxIndex: number) => void;
openFormForAction: (action: ContextualAction, item: PaletteItem) => void;
setFormValue: (key: string, value: unknown) => void;
setFormValues: (values: Record<string, unknown>) => void;
setFormError: (key: string, error: string) => void;
clearFormErrors: () => void;
resetForm: () => void;
/** Close form and return to previous view. */
closeForm: () => void;
setExpanded: (expanded: boolean) => void;
toggleExpanded: () => void;
setLoading: (loading: boolean) => void;
reset: () => void;
resetToSearch: () => void;
}
export type PaletteStore = PaletteState & PaletteActions;
// =============================================================================
// INITIAL STATE
// =============================================================================
const initialState: PaletteState = {
view: "search",
query: "",
categoryFilter: "all",
activeIndex: 0,
selectedItem: null,
actionTargetItem: null,
actionActiveIndex: 0,
isActionsPopoverOpen: false,
actionsPopoverQuery: "",
pendingAction: null,
pendingActionItem: null,
formValues: {},
formErrors: {},
previousView: null,
isExpanded: false,
isLoading: false,
};
// =============================================================================
// STORE
// =============================================================================
export const usePaletteStore = create<PaletteStore>()(
devtools(
(set) => ({
...initialState,
setView: (view) => set({ view }, false, "setView"),
setQuery: (query) =>
set({ query, activeIndex: 0 }, false, "setQuery"),
setCategoryFilter: (categoryFilter) =>
set({ categoryFilter, activeIndex: 0 }, false, "setCategoryFilter"),
setActiveIndex: (activeIndex) =>
set({ activeIndex }, false, "setActiveIndex"),
moveActiveIndex: (delta, maxIndex) =>
set(
(state) => {
let newIndex = state.activeIndex + delta;
// Wrap around
if (newIndex < 0) newIndex = maxIndex;
if (newIndex > maxIndex) newIndex = 0;
return { activeIndex: newIndex };
},
false,
"moveActiveIndex"
),
selectItem: (selectedItem) => set({ selectedItem }, false, "selectItem"),
openActionsForItem: (item) =>
set(
{
actionTargetItem: item,
actionActiveIndex: 0,
isActionsPopoverOpen: true,
actionsPopoverQuery: "",
},
false,
"openActionsForItem"
),
openActionsPopover: () =>
set(
(state) => ({
isActionsPopoverOpen: true,
actionsPopoverQuery: "",
actionActiveIndex: 0,
actionTargetItem: state.selectedItem,
}),
false,
"openActionsPopover"
),
closeActionsPopover: () =>
set(
{
isActionsPopoverOpen: false,
actionTargetItem: null,
actionActiveIndex: 0,
actionsPopoverQuery: "",
},
false,
"closeActionsPopover"
),
setActionsPopoverQuery: (actionsPopoverQuery) =>
set(
{ actionsPopoverQuery, actionActiveIndex: 0 },
false,
"setActionsPopoverQuery"
),
setActionActiveIndex: (actionActiveIndex) =>
set({ actionActiveIndex }, false, "setActionActiveIndex"),
moveActionActiveIndex: (delta, maxIndex) =>
set(
(state) => {
let newIndex = state.actionActiveIndex + delta;
if (newIndex < 0) newIndex = maxIndex;
if (newIndex > maxIndex) newIndex = 0;
return { actionActiveIndex: newIndex };
},
false,
"moveActionActiveIndex"
),
openFormForAction: (action, item) =>
set(
(state) => ({
pendingAction: action,
pendingActionItem: item,
view: "form",
previousView: state.view,
formValues: action.requiresForm?.prefilledValues ?? {},
formErrors: {},
}),
false,
"openFormForAction"
),
setFormValue: (key, value) =>
set(
(state) => ({
formValues: { ...state.formValues, [key]: value },
formErrors: { ...state.formErrors, [key]: "" },
}),
false,
"setFormValue"
),
setFormValues: (values) =>
set({ formValues: values }, false, "setFormValues"),
setFormError: (key, error) =>
set(
(state) => ({ formErrors: { ...state.formErrors, [key]: error } }),
false,
"setFormError"
),
clearFormErrors: () => set({ formErrors: {} }, false, "clearFormErrors"),
resetForm: () =>
set(
{
pendingAction: null,
pendingActionItem: null,
formValues: {},
formErrors: {},
previousView: null,
},
false,
"resetForm"
),
closeForm: () =>
set(
(state) => ({
pendingAction: null,
pendingActionItem: null,
formValues: {},
formErrors: {},
view: state.previousView ?? "search",
previousView: null,
}),
false,
"closeForm"
),
setExpanded: (isExpanded) => set({ isExpanded }, false, "setExpanded"),
toggleExpanded: () =>
set((state) => ({ isExpanded: !state.isExpanded }), false, "toggleExpanded"),
setLoading: (isLoading) => set({ isLoading }, false, "setLoading"),
reset: () => set(initialState, false, "reset"),
resetToSearch: () =>
set(
{
view: "search",
actionTargetItem: null,
actionActiveIndex: 0,
isActionsPopoverOpen: false,
actionsPopoverQuery: "",
pendingAction: null,
pendingActionItem: null,
formValues: {},
formErrors: {},
previousView: null,
},
false,
"resetToSearch"
),
}),
{ name: "CommandPaletteStore" }
)
);