@etoile-dev/react 0.2.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +344 -206
- package/dist/Searchbar.d.ts +315 -0
- package/dist/Searchbar.js +207 -0
- package/dist/context.d.ts +57 -0
- package/dist/context.js +32 -0
- package/dist/hooks/useEtoileSearch.d.ts +136 -0
- package/dist/hooks/useEtoileSearch.js +187 -0
- package/dist/index.d.ts +44 -19
- package/dist/index.js +37 -12
- package/dist/primitives/Content.d.ts +34 -0
- package/dist/primitives/Content.js +108 -0
- package/dist/primitives/Empty.d.ts +25 -0
- package/dist/primitives/Empty.js +25 -0
- package/dist/primitives/Error.d.ts +29 -0
- package/dist/primitives/Error.js +26 -0
- package/dist/primitives/Group.d.ts +30 -0
- package/dist/primitives/Group.js +22 -0
- package/dist/primitives/Icon.d.ts +21 -0
- package/dist/primitives/Icon.js +14 -0
- package/dist/primitives/Input.d.ts +32 -0
- package/dist/primitives/Input.js +70 -0
- package/dist/primitives/Item.d.ts +61 -0
- package/dist/primitives/Item.js +76 -0
- package/dist/primitives/Kbd.d.ts +20 -0
- package/dist/primitives/Kbd.js +13 -0
- package/dist/primitives/List.d.ts +35 -0
- package/dist/primitives/List.js +37 -0
- package/dist/primitives/Loading.d.ts +25 -0
- package/dist/primitives/Loading.js +26 -0
- package/dist/primitives/Modal.d.ts +39 -0
- package/dist/primitives/Modal.js +37 -0
- package/dist/primitives/ModalInput.d.ts +61 -0
- package/dist/primitives/ModalInput.js +33 -0
- package/dist/primitives/Overlay.d.ts +21 -0
- package/dist/primitives/Overlay.js +41 -0
- package/dist/primitives/Portal.d.ts +28 -0
- package/dist/primitives/Portal.js +30 -0
- package/dist/primitives/Root.d.ts +116 -0
- package/dist/primitives/Root.js +413 -0
- package/dist/primitives/Separator.d.ts +19 -0
- package/dist/primitives/Separator.js +18 -0
- package/dist/primitives/Thumbnail.d.ts +31 -0
- package/dist/primitives/Thumbnail.js +59 -0
- package/dist/primitives/Trigger.d.ts +28 -0
- package/dist/primitives/Trigger.js +35 -0
- package/dist/store.d.ts +38 -0
- package/dist/store.js +63 -0
- package/dist/styles.css +480 -133
- package/dist/types.d.ts +3 -31
- package/dist/utils/composeRefs.d.ts +12 -0
- package/dist/utils/composeRefs.js +27 -0
- package/dist/utils/slot.d.ts +22 -0
- package/dist/utils/slot.js +58 -0
- package/package.json +9 -5
- package/dist/Search.d.ts +0 -39
- package/dist/Search.js +0 -31
- package/dist/components/SearchIcon.d.ts +0 -22
- package/dist/components/SearchIcon.js +0 -17
- package/dist/components/SearchInput.d.ts +0 -30
- package/dist/components/SearchInput.js +0 -59
- package/dist/components/SearchKbd.d.ts +0 -30
- package/dist/components/SearchKbd.js +0 -24
- package/dist/components/SearchResult.d.ts +0 -31
- package/dist/components/SearchResult.js +0 -40
- package/dist/components/SearchResultThumbnail.d.ts +0 -38
- package/dist/components/SearchResultThumbnail.js +0 -38
- package/dist/components/SearchResults.d.ts +0 -39
- package/dist/components/SearchResults.js +0 -53
- package/dist/components/SearchRoot.d.ts +0 -44
- package/dist/components/SearchRoot.js +0 -132
- package/dist/context/SearchContext.d.ts +0 -55
- package/dist/context/SearchContext.js +0 -36
- package/dist/hooks/useSearch.d.ts +0 -56
- package/dist/hooks/useSearch.js +0 -116
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import { useSearchContext } from "../context/SearchContext.js";
|
|
4
|
-
export const SearchResultIndexContext = React.createContext(null);
|
|
5
|
-
export const SearchResultDataContext = React.createContext(null);
|
|
6
|
-
/**
|
|
7
|
-
* Container component for rendering search results with keyboard navigation.
|
|
8
|
-
*
|
|
9
|
-
* Accepts a render function that receives each result. Automatically hides
|
|
10
|
-
* when query is empty or no results found. Includes ARIA listbox role.
|
|
11
|
-
* Accepts standard div props like onScroll and style.
|
|
12
|
-
*
|
|
13
|
-
* @param props - Component props
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```tsx
|
|
17
|
-
* <SearchResults>
|
|
18
|
-
* {(result) => <SearchResult>{result.title}</SearchResult>}
|
|
19
|
-
* </SearchResults>
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @example With styling and metadata
|
|
23
|
-
* ```tsx
|
|
24
|
-
* <SearchResults className="mt-2 border rounded-lg">
|
|
25
|
-
* {(result) => (
|
|
26
|
-
* <SearchResult className="p-4 hover:bg-gray-50">
|
|
27
|
-
* <h3>{result.title}</h3>
|
|
28
|
-
* <p>{result.metadata?.artist}</p>
|
|
29
|
-
* </SearchResult>
|
|
30
|
-
* )}
|
|
31
|
-
* </SearchResults>
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export const SearchResults = ({ className, children, ...props }) => {
|
|
35
|
-
const { query, results, isOpen, selectedIndex, listboxId, getResultNode } = useSearchContext();
|
|
36
|
-
const listboxRef = React.useRef(null);
|
|
37
|
-
React.useEffect(() => {
|
|
38
|
-
if (selectedIndex < 0) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const activeNode = getResultNode(selectedIndex);
|
|
42
|
-
activeNode?.scrollIntoView({ block: "nearest" });
|
|
43
|
-
if (activeNode &&
|
|
44
|
-
listboxRef.current &&
|
|
45
|
-
listboxRef.current.contains(document.activeElement)) {
|
|
46
|
-
activeNode.focus();
|
|
47
|
-
}
|
|
48
|
-
}, [getResultNode, selectedIndex]);
|
|
49
|
-
if (!isOpen || query.trim() === "" || results.length === 0) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return (_jsx("div", { ...props, role: "listbox", id: listboxId, className: className, ref: listboxRef, children: results.map((result, index) => (_jsx(SearchResultIndexContext.Provider, { value: index, children: _jsx(SearchResultDataContext.Provider, { value: result, children: children(result) }) }, result.external_id))) }));
|
|
53
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
export type SearchRootProps = {
|
|
3
|
-
/** Your Étoile API key. Get one at https://etoile.dev */
|
|
4
|
-
apiKey: string;
|
|
5
|
-
/** Collections to search in (e.g., ["paintings", "artists"]) */
|
|
6
|
-
collections: string[];
|
|
7
|
-
/** Maximum number of results to return (default: 10) */
|
|
8
|
-
limit?: number;
|
|
9
|
-
/** Debounce delay in milliseconds before triggering search (default: 100) */
|
|
10
|
-
debounceMs?: number;
|
|
11
|
-
/** Whether the search input should be focused on mount (default: false) */
|
|
12
|
-
autoFocus?: boolean;
|
|
13
|
-
/** Additional CSS class name (appended to "etoile-search") */
|
|
14
|
-
className?: string;
|
|
15
|
-
/** Child components (SearchInput, SearchResults, etc.) */
|
|
16
|
-
children: React.ReactNode;
|
|
17
|
-
baseUrl?: string;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Root component for Étoile search that provides context to all child components.
|
|
21
|
-
*
|
|
22
|
-
* Manages search state, keyboard navigation, result selection, and accessibility.
|
|
23
|
-
* Automatically applies `etoile-search` class for styling when using the theme.
|
|
24
|
-
*
|
|
25
|
-
* @param props - Component props
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```tsx
|
|
29
|
-
* <SearchRoot apiKey="your-api-key" collections={["paintings"]}>
|
|
30
|
-
* <SearchInput />
|
|
31
|
-
* <SearchResults>
|
|
32
|
-
* {(result) => <SearchResult>{result.title}</SearchResult>}
|
|
33
|
-
* </SearchResults>
|
|
34
|
-
* </SearchRoot>
|
|
35
|
-
* ```
|
|
36
|
-
*
|
|
37
|
-
* @example Dark mode
|
|
38
|
-
* ```tsx
|
|
39
|
-
* <SearchRoot apiKey="your-api-key" collections={["paintings"]} className="dark">
|
|
40
|
-
* ...
|
|
41
|
-
* </SearchRoot>
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export declare const SearchRoot: ({ apiKey, collections, limit, debounceMs, autoFocus, className, children, baseUrl, }: SearchRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import { SearchProvider } from "../context/SearchContext.js";
|
|
4
|
-
import { useSearch } from "../hooks/useSearch.js";
|
|
5
|
-
/**
|
|
6
|
-
* Root component for Étoile search that provides context to all child components.
|
|
7
|
-
*
|
|
8
|
-
* Manages search state, keyboard navigation, result selection, and accessibility.
|
|
9
|
-
* Automatically applies `etoile-search` class for styling when using the theme.
|
|
10
|
-
*
|
|
11
|
-
* @param props - Component props
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```tsx
|
|
15
|
-
* <SearchRoot apiKey="your-api-key" collections={["paintings"]}>
|
|
16
|
-
* <SearchInput />
|
|
17
|
-
* <SearchResults>
|
|
18
|
-
* {(result) => <SearchResult>{result.title}</SearchResult>}
|
|
19
|
-
* </SearchResults>
|
|
20
|
-
* </SearchRoot>
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* @example Dark mode
|
|
24
|
-
* ```tsx
|
|
25
|
-
* <SearchRoot apiKey="your-api-key" collections={["paintings"]} className="dark">
|
|
26
|
-
* ...
|
|
27
|
-
* </SearchRoot>
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export const SearchRoot = ({ apiKey, collections, limit, debounceMs, autoFocus = false, className, children, baseUrl, }) => {
|
|
31
|
-
const search = useSearch({ apiKey, collections, limit, debounceMs, baseUrl });
|
|
32
|
-
const listboxId = React.useId();
|
|
33
|
-
const resultRefs = React.useRef(new Map());
|
|
34
|
-
const rootRef = React.useRef(null);
|
|
35
|
-
const [isOpen, setOpen] = React.useState(false);
|
|
36
|
-
// Open the results list whenever results arrive and query is non-empty
|
|
37
|
-
React.useEffect(() => {
|
|
38
|
-
if (search.results.length > 0 && search.query.trim() !== "") {
|
|
39
|
-
setOpen(true);
|
|
40
|
-
}
|
|
41
|
-
}, [search.results, search.query]);
|
|
42
|
-
// Close results when query is cleared
|
|
43
|
-
React.useEffect(() => {
|
|
44
|
-
if (search.query.trim() === "") {
|
|
45
|
-
setOpen(false);
|
|
46
|
-
}
|
|
47
|
-
}, [search.query]);
|
|
48
|
-
// Click-outside: close results when clicking outside the root element
|
|
49
|
-
React.useEffect(() => {
|
|
50
|
-
const handlePointerDown = (event) => {
|
|
51
|
-
if (rootRef.current &&
|
|
52
|
-
event.target instanceof Node &&
|
|
53
|
-
!rootRef.current.contains(event.target)) {
|
|
54
|
-
setOpen(false);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
document.addEventListener("pointerdown", handlePointerDown);
|
|
58
|
-
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
59
|
-
}, []);
|
|
60
|
-
// Focus-out: close results when focus leaves the component entirely
|
|
61
|
-
const handleFocusOut = (event) => {
|
|
62
|
-
if (rootRef.current &&
|
|
63
|
-
event.relatedTarget instanceof Node &&
|
|
64
|
-
!rootRef.current.contains(event.relatedTarget)) {
|
|
65
|
-
setOpen(false);
|
|
66
|
-
}
|
|
67
|
-
// relatedTarget is null when focus moves outside the document (e.g. address bar)
|
|
68
|
-
if (!event.relatedTarget) {
|
|
69
|
-
setOpen(false);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
const registerResult = (index, node) => {
|
|
73
|
-
resultRefs.current.set(index, node);
|
|
74
|
-
};
|
|
75
|
-
const getResultNode = (index) => {
|
|
76
|
-
return resultRefs.current.get(index) ?? null;
|
|
77
|
-
};
|
|
78
|
-
const getResultId = (index) => `${listboxId}-option-${index}`;
|
|
79
|
-
const selectActiveResult = () => {
|
|
80
|
-
if (search.selectedIndex < 0) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const node = getResultNode(search.selectedIndex);
|
|
84
|
-
if (node && "click" in node) {
|
|
85
|
-
node.click();
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
const handleKeyDown = (event) => {
|
|
89
|
-
if (event.key === "ArrowDown") {
|
|
90
|
-
event.preventDefault();
|
|
91
|
-
setOpen(true);
|
|
92
|
-
search.setSelectedIndex(search.selectedIndex + 1);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (event.key === "ArrowUp") {
|
|
96
|
-
event.preventDefault();
|
|
97
|
-
setOpen(true);
|
|
98
|
-
search.setSelectedIndex(search.selectedIndex - 1);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (event.key === "Enter") {
|
|
102
|
-
if (search.selectedIndex >= 0) {
|
|
103
|
-
event.preventDefault();
|
|
104
|
-
selectActiveResult();
|
|
105
|
-
}
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (event.key === "Escape") {
|
|
109
|
-
event.preventDefault();
|
|
110
|
-
// First Escape closes results, second clears the query
|
|
111
|
-
if (isOpen) {
|
|
112
|
-
setOpen(false);
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
search.clear();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
const value = React.useMemo(() => ({
|
|
120
|
-
...search,
|
|
121
|
-
isOpen,
|
|
122
|
-
setOpen,
|
|
123
|
-
listboxId,
|
|
124
|
-
getResultId,
|
|
125
|
-
registerResult,
|
|
126
|
-
getResultNode,
|
|
127
|
-
selectActiveResult,
|
|
128
|
-
handleKeyDown,
|
|
129
|
-
autoFocus,
|
|
130
|
-
}), [search, isOpen, listboxId, autoFocus]);
|
|
131
|
-
return (_jsx(SearchProvider, { value: value, children: _jsx("div", { ref: rootRef, className: className ? `etoile-search ${className}` : "etoile-search", onBlur: handleFocusOut, children: children }) }));
|
|
132
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import type { SearchResultData } from "../types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Internal context value for search state and controls.
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
|
-
type SearchContextValue = {
|
|
8
|
-
query: string;
|
|
9
|
-
setQuery: (q: string) => void;
|
|
10
|
-
results: SearchResultData[];
|
|
11
|
-
isLoading: boolean;
|
|
12
|
-
selectedIndex: number;
|
|
13
|
-
setSelectedIndex: (i: number) => void;
|
|
14
|
-
clear: () => void;
|
|
15
|
-
/** Whether the results list is currently open/visible */
|
|
16
|
-
isOpen: boolean;
|
|
17
|
-
/** Open or close the results list */
|
|
18
|
-
setOpen: (open: boolean) => void;
|
|
19
|
-
listboxId: string;
|
|
20
|
-
getResultId: (index: number) => string;
|
|
21
|
-
registerResult: (index: number, node: HTMLElement | null) => void;
|
|
22
|
-
getResultNode: (index: number) => HTMLElement | null;
|
|
23
|
-
selectActiveResult: () => void;
|
|
24
|
-
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
25
|
-
autoFocus: boolean;
|
|
26
|
-
};
|
|
27
|
-
export declare const SearchProvider: React.Provider<SearchContextValue | null>;
|
|
28
|
-
/**
|
|
29
|
-
* Hook to access search context from child components.
|
|
30
|
-
*
|
|
31
|
-
* Must be used within SearchRoot component. Provides access to search state,
|
|
32
|
-
* results, loading status, and keyboard navigation controls.
|
|
33
|
-
*
|
|
34
|
-
* @throws Error if used outside of SearchRoot
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```tsx
|
|
38
|
-
* import { useSearchContext } from "@etoile-dev/react";
|
|
39
|
-
*
|
|
40
|
-
* function CustomSearchComponent() {
|
|
41
|
-
* const { query, results, isLoading, clear } = useSearchContext();
|
|
42
|
-
*
|
|
43
|
-
* return (
|
|
44
|
-
* <div>
|
|
45
|
-
* <p>Searching for: {query}</p>
|
|
46
|
-
* {isLoading && <span>Loading...</span>}
|
|
47
|
-
* <button onClick={clear}>Clear</button>
|
|
48
|
-
* <p>Found {results.length} results</p>
|
|
49
|
-
* </div>
|
|
50
|
-
* );
|
|
51
|
-
* }
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
export declare const useSearchContext: () => SearchContextValue;
|
|
55
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
const SearchContext = React.createContext(null);
|
|
3
|
-
export const SearchProvider = SearchContext.Provider;
|
|
4
|
-
/**
|
|
5
|
-
* Hook to access search context from child components.
|
|
6
|
-
*
|
|
7
|
-
* Must be used within SearchRoot component. Provides access to search state,
|
|
8
|
-
* results, loading status, and keyboard navigation controls.
|
|
9
|
-
*
|
|
10
|
-
* @throws Error if used outside of SearchRoot
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```tsx
|
|
14
|
-
* import { useSearchContext } from "@etoile-dev/react";
|
|
15
|
-
*
|
|
16
|
-
* function CustomSearchComponent() {
|
|
17
|
-
* const { query, results, isLoading, clear } = useSearchContext();
|
|
18
|
-
*
|
|
19
|
-
* return (
|
|
20
|
-
* <div>
|
|
21
|
-
* <p>Searching for: {query}</p>
|
|
22
|
-
* {isLoading && <span>Loading...</span>}
|
|
23
|
-
* <button onClick={clear}>Clear</button>
|
|
24
|
-
* <p>Found {results.length} results</p>
|
|
25
|
-
* </div>
|
|
26
|
-
* );
|
|
27
|
-
* }
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export const useSearchContext = () => {
|
|
31
|
-
const ctx = React.useContext(SearchContext);
|
|
32
|
-
if (!ctx) {
|
|
33
|
-
throw new Error("Search components must be used within SearchRoot.");
|
|
34
|
-
}
|
|
35
|
-
return ctx;
|
|
36
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { SearchResultData } from "../types.js";
|
|
2
|
-
export type UseSearchOptions = {
|
|
3
|
-
/** Your Étoile API key. Get one at https://etoile.dev */
|
|
4
|
-
apiKey: string;
|
|
5
|
-
/** Collections to search in (e.g., ["paintings", "artists"]) */
|
|
6
|
-
collections: string[];
|
|
7
|
-
/** Maximum number of results to return (default: 10) */
|
|
8
|
-
limit?: number;
|
|
9
|
-
/** Debounce delay in milliseconds before triggering search (default: 100) */
|
|
10
|
-
debounceMs?: number;
|
|
11
|
-
baseUrl?: string;
|
|
12
|
-
};
|
|
13
|
-
export type UseSearchReturn = {
|
|
14
|
-
/** Current search query string */
|
|
15
|
-
query: string;
|
|
16
|
-
/** Update the search query */
|
|
17
|
-
setQuery: (q: string) => void;
|
|
18
|
-
/** Array of search results */
|
|
19
|
-
results: SearchResultData[];
|
|
20
|
-
/** Whether a search is currently in progress */
|
|
21
|
-
isLoading: boolean;
|
|
22
|
-
/** Index of the currently selected result (-1 if none) */
|
|
23
|
-
selectedIndex: number;
|
|
24
|
-
/** Set the selected result index */
|
|
25
|
-
setSelectedIndex: (i: number) => void;
|
|
26
|
-
/** Clear the search query and results */
|
|
27
|
-
clear: () => void;
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* React hook for managing search state with automatic debouncing and API integration.
|
|
31
|
-
*
|
|
32
|
-
* Handles search queries, debouncing, API calls, loading states, and result management.
|
|
33
|
-
* Works seamlessly with Étoile's search API.
|
|
34
|
-
*
|
|
35
|
-
* @param options - Search configuration options
|
|
36
|
-
* @returns Search state and control functions
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```tsx
|
|
40
|
-
* const { query, setQuery, results } = useSearch({
|
|
41
|
-
* apiKey: "your-api-key",
|
|
42
|
-
* collections: ["paintings"],
|
|
43
|
-
* });
|
|
44
|
-
* ```
|
|
45
|
-
*
|
|
46
|
-
* @example With all options
|
|
47
|
-
* ```tsx
|
|
48
|
-
* const { query, setQuery, results, isLoading, clear } = useSearch({
|
|
49
|
-
* apiKey: "your-api-key",
|
|
50
|
-
* collections: ["paintings", "artists"],
|
|
51
|
-
* limit: 20,
|
|
52
|
-
* debounceMs: 150,
|
|
53
|
-
* });
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
export declare const useSearch: ({ apiKey, collections, limit, debounceMs, baseUrl, }: UseSearchOptions) => UseSearchReturn;
|
package/dist/hooks/useSearch.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { Etoile } from "@etoile-dev/client";
|
|
3
|
-
const clampIndex = (index, length) => {
|
|
4
|
-
if (length <= 0) {
|
|
5
|
-
return -1;
|
|
6
|
-
}
|
|
7
|
-
if (index < 0) {
|
|
8
|
-
return 0;
|
|
9
|
-
}
|
|
10
|
-
if (index > length - 1) {
|
|
11
|
-
return length - 1;
|
|
12
|
-
}
|
|
13
|
-
return index;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* React hook for managing search state with automatic debouncing and API integration.
|
|
17
|
-
*
|
|
18
|
-
* Handles search queries, debouncing, API calls, loading states, and result management.
|
|
19
|
-
* Works seamlessly with Étoile's search API.
|
|
20
|
-
*
|
|
21
|
-
* @param options - Search configuration options
|
|
22
|
-
* @returns Search state and control functions
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```tsx
|
|
26
|
-
* const { query, setQuery, results } = useSearch({
|
|
27
|
-
* apiKey: "your-api-key",
|
|
28
|
-
* collections: ["paintings"],
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* @example With all options
|
|
33
|
-
* ```tsx
|
|
34
|
-
* const { query, setQuery, results, isLoading, clear } = useSearch({
|
|
35
|
-
* apiKey: "your-api-key",
|
|
36
|
-
* collections: ["paintings", "artists"],
|
|
37
|
-
* limit: 20,
|
|
38
|
-
* debounceMs: 150,
|
|
39
|
-
* });
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export const useSearch = ({ apiKey, collections, limit = 10, debounceMs = 100, baseUrl, }) => {
|
|
43
|
-
const [query, setQuery] = React.useState("");
|
|
44
|
-
const [debouncedQuery, setDebouncedQuery] = React.useState("");
|
|
45
|
-
const [results, setResults] = React.useState([]);
|
|
46
|
-
const [isLoading, setIsLoading] = React.useState(false);
|
|
47
|
-
const [selectedIndex, setSelectedIndexState] = React.useState(-1);
|
|
48
|
-
const client = React.useMemo(() => new Etoile({ apiKey, baseUrl }), [apiKey, baseUrl]);
|
|
49
|
-
React.useEffect(() => {
|
|
50
|
-
const handle = setTimeout(() => {
|
|
51
|
-
setDebouncedQuery(query);
|
|
52
|
-
}, debounceMs);
|
|
53
|
-
return () => clearTimeout(handle);
|
|
54
|
-
}, [query, debounceMs]);
|
|
55
|
-
React.useEffect(() => {
|
|
56
|
-
let isActive = true;
|
|
57
|
-
if (debouncedQuery.trim() === "") {
|
|
58
|
-
setResults([]);
|
|
59
|
-
setIsLoading(false);
|
|
60
|
-
return () => {
|
|
61
|
-
isActive = false;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
const runSearch = async () => {
|
|
65
|
-
setIsLoading(true);
|
|
66
|
-
try {
|
|
67
|
-
const response = await client.search({
|
|
68
|
-
collections,
|
|
69
|
-
query: debouncedQuery,
|
|
70
|
-
limit,
|
|
71
|
-
});
|
|
72
|
-
if (!isActive) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
setResults(Array.isArray(response.results) ? response.results : []);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
if (!isActive) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
setResults([]);
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
if (isActive) {
|
|
85
|
-
setIsLoading(false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
runSearch();
|
|
90
|
-
return () => {
|
|
91
|
-
isActive = false;
|
|
92
|
-
};
|
|
93
|
-
}, [client, collections, debouncedQuery, limit]);
|
|
94
|
-
React.useEffect(() => {
|
|
95
|
-
setSelectedIndexState((current) => clampIndex(current, results.length));
|
|
96
|
-
}, [results.length]);
|
|
97
|
-
const setSelectedIndex = React.useCallback((index) => {
|
|
98
|
-
setSelectedIndexState(clampIndex(index, results.length));
|
|
99
|
-
}, [results.length]);
|
|
100
|
-
const clear = React.useCallback(() => {
|
|
101
|
-
setQuery("");
|
|
102
|
-
setDebouncedQuery("");
|
|
103
|
-
setResults([]);
|
|
104
|
-
setIsLoading(false);
|
|
105
|
-
setSelectedIndexState(-1);
|
|
106
|
-
}, []);
|
|
107
|
-
return {
|
|
108
|
-
query,
|
|
109
|
-
setQuery,
|
|
110
|
-
results,
|
|
111
|
-
isLoading,
|
|
112
|
-
selectedIndex,
|
|
113
|
-
setSelectedIndex,
|
|
114
|
-
clear,
|
|
115
|
-
};
|
|
116
|
-
};
|