1import { useEffect } from "react";
2import { create } from "zustand";
3
4import {
5 type CommandPaletteUiAction,
6 type CommandPaletteUiState,
7 reduceCommandPaletteUiState,
8} from "./components/CommandPalette.logic";
9import { getWorkspaceFocusRequestVersion } from "./lib/workspaceFocus";
10
11const INITIAL_STATE: CommandPaletteUiState = {
12 open: false,
13 mode: "command",
14 openIntent: null,
15 session: 0,
16};
17
18export const useCommandPaletteStore = create<CommandPaletteUiState>(() => INITIAL_STATE);
19
20let returnFocus: { element: HTMLElement | null; href: string; focusRequest: number } | null = null;
21
22export function dispatchCommandPalette(action: CommandPaletteUiAction): void {
23 const state = useCommandPaletteStore.getState();
24 const next = reduceCommandPaletteUiState(state, action);
25 if (!state.open && next.open) {
26 // The workspace turns inert before the popup mounts, which blurs the opener.
27 returnFocus = {
28 element: document.activeElement instanceof HTMLElement ? document.activeElement : null,
29 href: window.location.href,
30 focusRequest: getWorkspaceFocusRequestVersion(),
31 };
32 }
33 useCommandPaletteStore.setState(next, true);
34}
35
36/**
37 * Where the closing palette should return focus: `false` when a command navigated or asked
38 * for workspace focus (the destination owns it), else the opener, or `true` for the default.
39 */
40export function getCommandPaletteReturnFocus(): HTMLElement | boolean {
41 if (
42 !returnFocus ||
43 window.location.href !== returnFocus.href ||
44 getWorkspaceFocusRequestVersion() !== returnFocus.focusRequest
45 )
46 return false;
47 const { element } = returnFocus;
48 return element?.isConnected && !element.matches(":disabled") ? element : true;
49}
50
51// The palette is only mounted on app-shell routes (not /pair, sign-in or
52// landings), so its shortcuts must stay inert elsewhere.
53let mountedPalettes = 0;
54
55export function isCommandPaletteMounted(): boolean {
56 return mountedPalettes > 0;
57}
58
59/**
60 * Marks the palette as mounted while the calling component is. An unmounted
61 * palette cannot stay open, so the last unmount resets the store.
62 */
63export function useCommandPaletteMount(): void {
64 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- The palette's mount gates its global shortcuts, and leaving the route while it is open calls no close handler, so only unmount can reset the store.
65 useEffect(() => {
66 mountedPalettes += 1;
67 return () => {
68 mountedPalettes -= 1;
69 if (mountedPalettes === 0) {
70 useCommandPaletteStore.setState(
71 (state) => ({ ...INITIAL_STATE, session: state.session + 1 }),
72 true,
73 );
74 }
75 };
76 }, []);
77}