Files
homarr/packages/spotlight/src/components/spotlight.tsx
2025-01-06 19:59:40 +01:00

160 lines
5.1 KiB
TypeScript

"use client";
import type { Dispatch, SetStateAction } from "react";
import { useMemo, useRef, useState } from "react";
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
import { IconQuestionMark, IconSearch, IconX } from "@tabler/icons-react";
import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { inferSearchInteractionOptions } from "../lib/interaction";
import type { SearchMode } from "../lib/mode";
import { searchModes } from "../modes";
import { selectAction, spotlightStore } from "../spotlight-store";
import { SpotlightChildrenActions } from "./actions/children-actions";
import { SpotlightActionGroups } from "./actions/groups/action-group";
type SearchModeKey = keyof TranslationObject["search"]["mode"];
const defaultMode = "home";
export const Spotlight = () => {
const searchModeState = useState<SearchModeKey>(defaultMode);
const mode = searchModeState[0];
const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]);
if (!activeMode) {
return null;
}
// We use the "key" below to prevent the 'Different amounts of hooks' error
return <SpotlightWithActiveMode key={mode} modeState={searchModeState} activeMode={activeMode} />;
};
interface SpotlightWithActiveModeProps {
modeState: [SearchModeKey, Dispatch<SetStateAction<SearchModeKey>>];
activeMode: SearchMode;
}
const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveModeProps) => {
const [query, setQuery] = useState("");
const [mode, setMode] = modeState;
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
const t = useI18n();
const inputRef = useRef<HTMLInputElement>(null);
// Works as always the same amount of hooks are executed
const useGroups = "groups" in activeMode ? () => activeMode.groups : activeMode.useGroups;
const groups = useGroups();
return (
<MantineSpotlight.Root
yOffset={8}
onSpotlightClose={() => {
setMode(defaultMode);
setChildrenOptions(null);
}}
query={query}
onQueryChange={(query) => {
if (mode !== "help" || query.length !== 1) {
setQuery(query);
}
const modeToActivate = searchModes.find((mode) => mode.character === query);
if (!modeToActivate) {
return;
}
setMode(modeToActivate.modeKey);
setQuery("");
setTimeout(() => selectAction(0, spotlightStore));
}}
store={spotlightStore}
>
<MantineSpotlight.Search
placeholder={`${t("search.placeholder")}...`}
ref={inputRef}
leftSectionWidth={activeMode.modeKey !== defaultMode ? 80 : 48}
leftSection={
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
<Center w={48} h="100%">
<IconSearch stroke={1.5} />
</Center>
{activeMode.modeKey !== defaultMode ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
</Group>
}
styles={{
section: {
pointerEvents: "all",
},
}}
rightSection={
mode === defaultMode ? (
<ActionIcon
onClick={() => {
setMode("help");
inputRef.current?.focus();
}}
variant="subtle"
>
<IconQuestionMark stroke={1.5} />
</ActionIcon>
) : (
<ActionIcon
onClick={() => {
setMode(defaultMode);
setChildrenOptions(null);
inputRef.current?.focus();
}}
variant="subtle"
>
<IconX stroke={1.5} />
</ActionIcon>
)
}
value={query}
onKeyDown={(event) => {
if (query.length === 0 && mode !== defaultMode && event.key === "Backspace") {
setMode(defaultMode);
setChildrenOptions(null);
}
}}
/>
{childrenOptions ? (
<Group>
<childrenOptions.DetailComponent options={childrenOptions.option as never} />
</Group>
) : null}
<MantineSpotlight.ActionsList>
{childrenOptions ? (
<SpotlightChildrenActions
childrenOptions={childrenOptions}
query={query}
setChildrenOptions={setChildrenOptions}
/>
) : (
<SpotlightActionGroups
setMode={(mode) => {
setMode(mode);
setChildrenOptions(null);
setTimeout(() => selectAction(0, spotlightStore));
}}
setChildrenOptions={(options) => {
setChildrenOptions(options);
setTimeout(() => {
setQuery("");
selectAction(0, spotlightStore);
});
}}
query={query}
groups={groups}
/>
)}
</MantineSpotlight.ActionsList>
</MantineSpotlight.Root>
);
};