merge: stack widget modernization on dnd kit

This commit is contained in:
Thomas Camlong committed 2026-08-08 20:40:26 +02:00
commit 9edeba5cd2
940 files changed
+104475 -33649

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
"use client";
export { Spotlight } from "./components/spotlight";
+30 -11
View File
@@ -11,18 +11,18 @@ import { hotkeys } from "@homarr/definitions";
import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { OpenMediaRequestSearchOptions } from "../index";
import {
consumePendingMediaRequestSearch,
consumePendingSpotlightOpen,
mediaRequestSearchEvent,
spotlightOpenEvent,
} from "../open";
import type { OpenMediaRequestSearchOptions } from "../open";
import type { inferSearchInteractionOptions } from "../lib/interaction";
import type { SearchMode } from "../lib/mode";
import { searchModes } from "../modes";
import { useHomeEmptyGroupsWithPreferences } from "../modes/help/home-empty-groups";
import {
mediaRequestSearchEvent,
mediaRequestSearchScopeAtom,
selectAction,
spotlightActions,
spotlightStore,
} from "../spotlight-store";
import { mediaRequestSearchScopeAtom, selectAction, spotlightActions, spotlightStore } from "../spotlight-store";
import { SpotlightChildrenActions } from "./actions/children-actions";
import { SpotlightActionGroups } from "./actions/groups/action-group";
@@ -64,6 +64,17 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
const homeEmptyGroups = useHomeEmptyGroupsWithPreferences();
const hasModeCharacter = activeMode.modeKey !== defaultMode && activeMode.character !== undefined;
useEffect(() => {
const handleOpen = () => {
consumePendingSpotlightOpen();
spotlightActions.open();
};
window.addEventListener(spotlightOpenEvent, handleOpen);
if (consumePendingSpotlightOpen()) spotlightActions.open();
return () => window.removeEventListener(spotlightOpenEvent, handleOpen);
}, []);
const clearChildrenStack = useCallback(() => {
setChildrenStack([]);
}, []);
@@ -84,17 +95,24 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
}, [setQuery]);
useEffect(() => {
const handleMediaRequestSearch = (event: Event) => {
const { integrationIds, query } = (event as CustomEvent<OpenMediaRequestSearchOptions>).detail ?? {};
const applyMediaRequestSearch = ({ integrationIds, query: searchQuery }: OpenMediaRequestSearchOptions) => {
setMediaRequestSearchScope({ integrationIds });
setMode("media");
clearChildrenStack();
setQuery(query ?? "");
setQuery(searchQuery ?? "");
spotlightActions.open();
setTimeout(() => selectAction(0, spotlightStore));
};
const handleMediaRequestSearch = (event: Event) => {
consumePendingMediaRequestSearch();
const { integrationIds, query: searchQuery } = (event as CustomEvent<OpenMediaRequestSearchOptions>).detail ?? {};
applyMediaRequestSearch({ integrationIds, query: searchQuery });
};
window.addEventListener(mediaRequestSearchEvent, handleMediaRequestSearch);
const pendingSearch = consumePendingMediaRequestSearch();
if (pendingSearch) applyMediaRequestSearch(pendingSearch);
return () => {
window.removeEventListener(mediaRequestSearchEvent, handleMediaRequestSearch);
};
@@ -134,6 +152,7 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
store={spotlightStore}
>
<MantineSpotlight.Search
data-homarr-dev-benchmark-spotlight
placeholder={`${t("search.placeholder")}...`}
ref={inputRef}
leftSectionWidth={hasModeCharacter ? 80 : 48}
+1 -21
View File
@@ -1,28 +1,8 @@
"use client";
import { mediaRequestSearchEvent, spotlightActions } from "./spotlight-store";
export { Spotlight } from "./components/spotlight";
export { openSpotlight, openMediaRequestSearch };
export { openSpotlight, openMediaRequestSearch, mediaRequestSearchEvent } from "./open";
export {
SpotlightProvider,
useRegisterSpotlightContextResults,
useRegisterSpotlightContextActions,
} from "./modes/home/context";
const openSpotlight = spotlightActions.open;
export interface OpenMediaRequestSearchOptions {
integrationIds?: string[];
query?: string;
}
const openMediaRequestSearch = (options: OpenMediaRequestSearchOptions = {}) => {
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent<OpenMediaRequestSearchOptions>(mediaRequestSearchEvent, { detail: options }));
}
spotlightActions.open();
};
export { mediaRequestSearchEvent };
+54
View File
@@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
consumePendingMediaRequestSearch,
consumePendingSpotlightOpen,
hasPendingSpotlightOpen,
mediaRequestSearchEvent,
openMediaRequestSearch,
openSpotlight,
spotlightOpenEvent,
} from "./open";
describe("lazy Spotlight open bridge", () => {
beforeEach(() => {
consumePendingSpotlightOpen();
consumePendingMediaRequestSearch();
});
it("keeps a pre-mount open request until the lazy component consumes it", () => {
const listener = vi.fn();
window.addEventListener(spotlightOpenEvent, listener);
openSpotlight();
expect(listener).toHaveBeenCalledOnce();
expect(consumePendingSpotlightOpen()).toBe(true);
expect(consumePendingSpotlightOpen()).toBe(false);
window.removeEventListener(spotlightOpenEvent, listener);
});
it("lets a listener installed after the request discover the pending open", () => {
openSpotlight();
expect(hasPendingSpotlightOpen()).toBe(true);
expect(consumePendingSpotlightOpen()).toBe(true);
expect(hasPendingSpotlightOpen()).toBe(false);
});
it("retains media options while dispatching both mount and media events", () => {
const openListener = vi.fn();
const mediaListener = vi.fn();
window.addEventListener(spotlightOpenEvent, openListener);
window.addEventListener(mediaRequestSearchEvent, mediaListener);
openMediaRequestSearch({ integrationIds: ["integration-a"], query: "movie" });
expect(openListener).toHaveBeenCalledOnce();
expect(mediaListener).toHaveBeenCalledOnce();
expect(consumePendingSpotlightOpen()).toBe(true);
expect(consumePendingMediaRequestSearch()).toEqual({ integrationIds: ["integration-a"], query: "movie" });
window.removeEventListener(spotlightOpenEvent, openListener);
window.removeEventListener(mediaRequestSearchEvent, mediaListener);
});
});
+44
View File
@@ -0,0 +1,44 @@
"use client";
export const spotlightOpenEvent = "homarr:spotlight:open";
export const mediaRequestSearchEvent = "homarr:spotlight:media-request-search";
export interface OpenMediaRequestSearchOptions {
integrationIds?: string[];
query?: string;
}
let pendingMediaRequestSearch: OpenMediaRequestSearchOptions | null = null;
let pendingSpotlightOpen = false;
const requestSpotlightMount = () => {
if (typeof window !== "undefined") window.dispatchEvent(new Event(spotlightOpenEvent));
};
export const openSpotlight = () => {
pendingSpotlightOpen = true;
requestSpotlightMount();
};
export const consumePendingSpotlightOpen = () => {
const pending = pendingSpotlightOpen;
pendingSpotlightOpen = false;
return pending;
};
export const hasPendingSpotlightOpen = () => pendingSpotlightOpen;
export const consumePendingMediaRequestSearch = () => {
const pending = pendingMediaRequestSearch;
pendingMediaRequestSearch = null;
return pending;
};
export const openMediaRequestSearch = (options: OpenMediaRequestSearchOptions = {}) => {
pendingMediaRequestSearch = options;
openSpotlight();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent<OpenMediaRequestSearchOptions>(mediaRequestSearchEvent, { detail: options }));
}
};
@@ -12,7 +12,6 @@ export interface MediaRequestSearchScope {
}
export const mediaRequestSearchScopeAtom = atom<MediaRequestSearchScope>({});
export const mediaRequestSearchEvent = "homarr:spotlight:media-request-search";
export const setSelectedAction = (index: number, store: SpotlightStore) => {
store.updateState((state) => ({ ...state, selected: index }));