Initial import

This commit is contained in:
how2ice
2026-04-21 03:33:23 +09:00
commit 9e4b70f1f1
495 changed files with 94680 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
import {
createContext,
useContext,
useMemo,
useRef,
useState,
type PropsWithChildren,
} from 'react';
import { SearchCommandModal, type SearchKeywordOption } from '../../../components/search';
import type { SearchLayerSnapshot, SearchOpenMode, SearchWindowSelection, SearchWindowSelectionDraft } from '../types';
type SearchLayerContextValue = SearchLayerSnapshot & {
setOptions: (options: SearchKeywordOption[]) => void;
openSearch: (mode?: SearchOpenMode) => void;
closeSearch: () => void;
clearWindowSelection: (instanceId: string) => void;
addWindowSelections: (selections: SearchWindowSelectionDraft[]) => void;
};
const SearchLayerContext = createContext<SearchLayerContextValue | null>(null);
const WINDOW_SELECTION_DEDUP_MS = 500;
export function SearchLayerProvider({ children }: PropsWithChildren) {
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<SearchKeywordOption[]>([]);
const [mode, setMode] = useState<SearchOpenMode>('navigate');
const [windowSelections, setWindowSelections] = useState<SearchWindowSelection[]>([]);
const lastWindowSelectionRef = useRef<{ id: string; at: number } | null>(null);
const value = useMemo<SearchLayerContextValue>(
() => ({
open,
options,
mode,
windowSelections,
setOptions,
openSearch: (nextMode = 'navigate') => {
lastWindowSelectionRef.current = null;
setMode(nextMode);
setOpen(true);
},
closeSearch: () => {
setOpen(false);
},
clearWindowSelection: (instanceId: string) => {
setWindowSelections((previous) => previous.filter((selection) => selection.instanceId !== instanceId));
},
addWindowSelections: (selections) => {
if (selections.length === 0) {
return;
}
setWindowSelections((previous) => [
...previous,
...selections.map((selection) => ({
...selection,
instanceId: `window-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
})),
]);
},
}),
[mode, open, options, windowSelections],
);
return (
<SearchLayerContext.Provider value={value}>
{children}
<SearchCommandModal
open={open}
options={options}
mode={mode}
onClose={value.closeSearch}
onSelectOption={(option) => {
if (mode === 'window') {
const now = Date.now();
const previousSelection = lastWindowSelectionRef.current;
if (
previousSelection &&
previousSelection.id === option.id &&
now - previousSelection.at <= WINDOW_SELECTION_DEDUP_MS
) {
return;
}
lastWindowSelectionRef.current = {
id: option.id,
at: now,
};
setOpen(false);
setWindowSelections((previous) => [
...previous,
{
instanceId: `window-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
id: option.id,
label: option.label,
group: option.group,
description: option.description,
keywords: option.keywords ?? [],
},
]);
(option.onSelectWindow ?? option.onSelect)();
return;
}
option.onSelect();
}}
/>
</SearchLayerContext.Provider>
);
}
export function useSearchLayerContext() {
const context = useContext(SearchLayerContext);
if (!context) {
throw new Error('useSearchLayerContext must be used within a SearchLayerProvider.');
}
return context;
}

View File

@@ -0,0 +1,5 @@
import { useSearchLayerContext } from '../context/SearchLayerContext';
export function useSearchLayer() {
return useSearchLayerContext();
}

3
src/layer/search/index.ts Executable file
View File

@@ -0,0 +1,3 @@
export * from './context/SearchLayerContext';
export * from './hooks/useSearchLayer';
export * from './types';

21
src/layer/search/types/index.ts Executable file
View File

@@ -0,0 +1,21 @@
import type { SearchKeywordOption } from '../../../components/search';
export type SearchOpenMode = 'navigate' | 'window';
export type SearchWindowSelection = {
instanceId: string;
id: string;
label: string;
group: string;
description?: string;
keywords: string[];
};
export type SearchWindowSelectionDraft = Omit<SearchWindowSelection, 'instanceId'>;
export type SearchLayerSnapshot = {
open: boolean;
options: SearchKeywordOption[];
mode: SearchOpenMode;
windowSelections: SearchWindowSelection[];
};