@etoile-dev/react 0.1.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/LICENSE +21 -0
- package/README.md +361 -0
- package/dist/Search.d.ts +36 -0
- package/dist/Search.js +31 -0
- package/dist/components/SearchIcon.d.ts +22 -0
- package/dist/components/SearchIcon.js +17 -0
- package/dist/components/SearchInput.d.ts +28 -0
- package/dist/components/SearchInput.js +35 -0
- package/dist/components/SearchKbd.d.ts +30 -0
- package/dist/components/SearchKbd.js +24 -0
- package/dist/components/SearchResult.d.ts +30 -0
- package/dist/components/SearchResult.js +39 -0
- package/dist/components/SearchResultThumbnail.d.ts +36 -0
- package/dist/components/SearchResultThumbnail.js +37 -0
- package/dist/components/SearchResults.d.ts +38 -0
- package/dist/components/SearchResults.js +52 -0
- package/dist/components/SearchRoot.d.ts +43 -0
- package/dist/components/SearchRoot.js +84 -0
- package/dist/context/SearchContext.d.ts +51 -0
- package/dist/context/SearchContext.js +36 -0
- package/dist/hooks/useSearch.d.ts +55 -0
- package/dist/hooks/useSearch.js +116 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +12 -0
- package/dist/styles.css +301 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type SearchResultThumbnailProps = {
|
|
2
|
+
/** Image source URL (defaults to result.metadata.thumbnailUrl) */
|
|
3
|
+
src?: string;
|
|
4
|
+
/** Alt text for the image (defaults to result.title) */
|
|
5
|
+
alt?: string;
|
|
6
|
+
/** Width and height in pixels (default: 40) */
|
|
7
|
+
size?: number;
|
|
8
|
+
/** CSS class name for styling */
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Thumbnail image for search results with automatic source detection.
|
|
13
|
+
*
|
|
14
|
+
* Automatically uses `metadata.thumbnailUrl` if available. Returns null
|
|
15
|
+
* if no image source is found. Must be used inside SearchResults.
|
|
16
|
+
*
|
|
17
|
+
* @param props - Component props
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <SearchResults>
|
|
22
|
+
* {(result) => (
|
|
23
|
+
* <SearchResult>
|
|
24
|
+
* <SearchResultThumbnail />
|
|
25
|
+
* <span>{result.title}</span>
|
|
26
|
+
* </SearchResult>
|
|
27
|
+
* )}
|
|
28
|
+
* </SearchResults>
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example With custom size and styling
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <SearchResultThumbnail size={48} className="rounded-full" />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare const SearchResultThumbnail: ({ src, alt, size, className, }: SearchResultThumbnailProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { SearchResultDataContext } from "./SearchResults.js";
|
|
4
|
+
/**
|
|
5
|
+
* Thumbnail image for search results with automatic source detection.
|
|
6
|
+
*
|
|
7
|
+
* Automatically uses `metadata.thumbnailUrl` if available. Returns null
|
|
8
|
+
* if no image source is found. Must be used inside SearchResults.
|
|
9
|
+
*
|
|
10
|
+
* @param props - Component props
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <SearchResults>
|
|
15
|
+
* {(result) => (
|
|
16
|
+
* <SearchResult>
|
|
17
|
+
* <SearchResultThumbnail />
|
|
18
|
+
* <span>{result.title}</span>
|
|
19
|
+
* </SearchResult>
|
|
20
|
+
* )}
|
|
21
|
+
* </SearchResults>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example With custom size and styling
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <SearchResultThumbnail size={48} className="rounded-full" />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const SearchResultThumbnail = ({ src, alt, size = 40, className, }) => {
|
|
30
|
+
const result = React.useContext(SearchResultDataContext);
|
|
31
|
+
const imageSrc = src ?? result?.metadata?.thumbnailUrl;
|
|
32
|
+
const imageAlt = alt ?? result?.title ?? "";
|
|
33
|
+
if (!imageSrc) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return (_jsx("img", { src: imageSrc, alt: imageAlt, width: size, height: size, className: className, draggable: false }));
|
|
37
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { SearchResultData } from "../types.js";
|
|
3
|
+
export type SearchResultsProps = {
|
|
4
|
+
/** CSS class name for the results container */
|
|
5
|
+
className?: string;
|
|
6
|
+
/** Render function that receives each search result */
|
|
7
|
+
children: (result: SearchResultData) => React.ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export declare const SearchResultIndexContext: React.Context<number | null>;
|
|
10
|
+
export declare const SearchResultDataContext: React.Context<SearchResultData | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Container component for rendering search results with keyboard navigation.
|
|
13
|
+
*
|
|
14
|
+
* Accepts a render function that receives each result. Automatically hides
|
|
15
|
+
* when query is empty or no results found. Includes ARIA listbox role.
|
|
16
|
+
*
|
|
17
|
+
* @param props - Component props
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <SearchResults>
|
|
22
|
+
* {(result) => <SearchResult>{result.title}</SearchResult>}
|
|
23
|
+
* </SearchResults>
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example With styling and metadata
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <SearchResults className="mt-2 border rounded-lg">
|
|
29
|
+
* {(result) => (
|
|
30
|
+
* <SearchResult className="p-4 hover:bg-gray-50">
|
|
31
|
+
* <h3>{result.title}</h3>
|
|
32
|
+
* <p>{result.metadata?.artist}</p>
|
|
33
|
+
* </SearchResult>
|
|
34
|
+
* )}
|
|
35
|
+
* </SearchResults>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const SearchResults: ({ className, children }: SearchResultsProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
*
|
|
12
|
+
* @param props - Component props
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <SearchResults>
|
|
17
|
+
* {(result) => <SearchResult>{result.title}</SearchResult>}
|
|
18
|
+
* </SearchResults>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example With styling and metadata
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <SearchResults className="mt-2 border rounded-lg">
|
|
24
|
+
* {(result) => (
|
|
25
|
+
* <SearchResult className="p-4 hover:bg-gray-50">
|
|
26
|
+
* <h3>{result.title}</h3>
|
|
27
|
+
* <p>{result.metadata?.artist}</p>
|
|
28
|
+
* </SearchResult>
|
|
29
|
+
* )}
|
|
30
|
+
* </SearchResults>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const SearchResults = ({ className, children }) => {
|
|
34
|
+
const { query, results, selectedIndex, listboxId, getResultNode } = useSearchContext();
|
|
35
|
+
const listboxRef = React.useRef(null);
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (selectedIndex < 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const activeNode = getResultNode(selectedIndex);
|
|
41
|
+
activeNode?.scrollIntoView({ block: "nearest" });
|
|
42
|
+
if (activeNode &&
|
|
43
|
+
listboxRef.current &&
|
|
44
|
+
listboxRef.current.contains(document.activeElement)) {
|
|
45
|
+
activeNode.focus();
|
|
46
|
+
}
|
|
47
|
+
}, [getResultNode, selectedIndex]);
|
|
48
|
+
if (query.trim() === "" || results.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return (_jsx("div", { 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))) }));
|
|
52
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Root component for Étoile search that provides context to all child components.
|
|
20
|
+
*
|
|
21
|
+
* Manages search state, keyboard navigation, result selection, and accessibility.
|
|
22
|
+
* Automatically applies `etoile-search` class for styling when using the theme.
|
|
23
|
+
*
|
|
24
|
+
* @param props - Component props
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <SearchRoot apiKey="your-api-key" collections={["paintings"]}>
|
|
29
|
+
* <SearchInput />
|
|
30
|
+
* <SearchResults>
|
|
31
|
+
* {(result) => <SearchResult>{result.title}</SearchResult>}
|
|
32
|
+
* </SearchResults>
|
|
33
|
+
* </SearchRoot>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example Dark mode
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <SearchRoot apiKey="your-api-key" collections={["paintings"]} className="dark">
|
|
39
|
+
* ...
|
|
40
|
+
* </SearchRoot>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare const SearchRoot: ({ apiKey, collections, limit, debounceMs, autoFocus, className, children, }: SearchRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,84 @@
|
|
|
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, }) => {
|
|
31
|
+
const search = useSearch({ apiKey, collections, limit, debounceMs });
|
|
32
|
+
const listboxId = React.useId();
|
|
33
|
+
const resultRefs = React.useRef(new Map());
|
|
34
|
+
const registerResult = (index, node) => {
|
|
35
|
+
resultRefs.current.set(index, node);
|
|
36
|
+
};
|
|
37
|
+
const getResultNode = (index) => {
|
|
38
|
+
return resultRefs.current.get(index) ?? null;
|
|
39
|
+
};
|
|
40
|
+
const getResultId = (index) => `${listboxId}-option-${index}`;
|
|
41
|
+
const selectActiveResult = () => {
|
|
42
|
+
if (search.selectedIndex < 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const node = getResultNode(search.selectedIndex);
|
|
46
|
+
if (node && "click" in node) {
|
|
47
|
+
node.click();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const handleKeyDown = (event) => {
|
|
51
|
+
if (event.key === "ArrowDown") {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
search.setSelectedIndex(search.selectedIndex + 1);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (event.key === "ArrowUp") {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
search.setSelectedIndex(search.selectedIndex - 1);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (event.key === "Enter") {
|
|
62
|
+
if (search.selectedIndex >= 0) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
selectActiveResult();
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (event.key === "Escape") {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
search.clear();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const value = React.useMemo(() => ({
|
|
74
|
+
...search,
|
|
75
|
+
listboxId,
|
|
76
|
+
getResultId,
|
|
77
|
+
registerResult,
|
|
78
|
+
getResultNode,
|
|
79
|
+
selectActiveResult,
|
|
80
|
+
handleKeyDown,
|
|
81
|
+
autoFocus,
|
|
82
|
+
}), [search, listboxId, autoFocus]);
|
|
83
|
+
return (_jsx(SearchProvider, { value: value, children: _jsx("div", { className: className ? `etoile-search ${className}` : "etoile-search", children: children }) }));
|
|
84
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
listboxId: string;
|
|
16
|
+
getResultId: (index: number) => string;
|
|
17
|
+
registerResult: (index: number, node: HTMLElement | null) => void;
|
|
18
|
+
getResultNode: (index: number) => HTMLElement | null;
|
|
19
|
+
selectActiveResult: () => void;
|
|
20
|
+
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
21
|
+
autoFocus: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare const SearchProvider: React.Provider<SearchContextValue | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Hook to access search context from child components.
|
|
26
|
+
*
|
|
27
|
+
* Must be used within SearchRoot component. Provides access to search state,
|
|
28
|
+
* results, loading status, and keyboard navigation controls.
|
|
29
|
+
*
|
|
30
|
+
* @throws Error if used outside of SearchRoot
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* import { useSearchContext } from "@etoile-dev/react";
|
|
35
|
+
*
|
|
36
|
+
* function CustomSearchComponent() {
|
|
37
|
+
* const { query, results, isLoading, clear } = useSearchContext();
|
|
38
|
+
*
|
|
39
|
+
* return (
|
|
40
|
+
* <div>
|
|
41
|
+
* <p>Searching for: {query}</p>
|
|
42
|
+
* {isLoading && <span>Loading...</span>}
|
|
43
|
+
* <button onClick={clear}>Clear</button>
|
|
44
|
+
* <p>Found {results.length} results</p>
|
|
45
|
+
* </div>
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare const useSearchContext: () => SearchContextValue;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
};
|
|
12
|
+
export type UseSearchReturn = {
|
|
13
|
+
/** Current search query string */
|
|
14
|
+
query: string;
|
|
15
|
+
/** Update the search query */
|
|
16
|
+
setQuery: (q: string) => void;
|
|
17
|
+
/** Array of search results */
|
|
18
|
+
results: SearchResultData[];
|
|
19
|
+
/** Whether a search is currently in progress */
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
/** Index of the currently selected result (-1 if none) */
|
|
22
|
+
selectedIndex: number;
|
|
23
|
+
/** Set the selected result index */
|
|
24
|
+
setSelectedIndex: (i: number) => void;
|
|
25
|
+
/** Clear the search query and results */
|
|
26
|
+
clear: () => void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* React hook for managing search state with automatic debouncing and API integration.
|
|
30
|
+
*
|
|
31
|
+
* Handles search queries, debouncing, API calls, loading states, and result management.
|
|
32
|
+
* Works seamlessly with Étoile's search API.
|
|
33
|
+
*
|
|
34
|
+
* @param options - Search configuration options
|
|
35
|
+
* @returns Search state and control functions
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const { query, setQuery, results } = useSearch({
|
|
40
|
+
* apiKey: "your-api-key",
|
|
41
|
+
* collections: ["paintings"],
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example With all options
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const { query, setQuery, results, isLoading, clear } = useSearch({
|
|
48
|
+
* apiKey: "your-api-key",
|
|
49
|
+
* collections: ["paintings", "artists"],
|
|
50
|
+
* limit: 20,
|
|
51
|
+
* debounceMs: 150,
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare const useSearch: ({ apiKey, collections, limit, debounceMs, }: UseSearchOptions) => UseSearchReturn;
|
|
@@ -0,0 +1,116 @@
|
|
|
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, }) => {
|
|
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
|
+
React.useEffect(() => {
|
|
49
|
+
const handle = setTimeout(() => {
|
|
50
|
+
setDebouncedQuery(query);
|
|
51
|
+
}, debounceMs);
|
|
52
|
+
return () => clearTimeout(handle);
|
|
53
|
+
}, [query, debounceMs]);
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
let isActive = true;
|
|
56
|
+
if (debouncedQuery.trim() === "") {
|
|
57
|
+
setResults([]);
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
return () => {
|
|
60
|
+
isActive = false;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const runSearch = async () => {
|
|
64
|
+
setIsLoading(true);
|
|
65
|
+
try {
|
|
66
|
+
const client = new Etoile({ apiKey });
|
|
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
|
+
}, [apiKey, 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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { SearchRoot } from "./components/SearchRoot.js";
|
|
2
|
+
export { SearchInput } from "./components/SearchInput.js";
|
|
3
|
+
export { SearchResults } from "./components/SearchResults.js";
|
|
4
|
+
export { SearchResult } from "./components/SearchResult.js";
|
|
5
|
+
export { SearchResultThumbnail } from "./components/SearchResultThumbnail.js";
|
|
6
|
+
export { SearchIcon } from "./components/SearchIcon.js";
|
|
7
|
+
export { SearchKbd } from "./components/SearchKbd.js";
|
|
8
|
+
export { Search } from "./Search.js";
|
|
9
|
+
export { useSearch } from "./hooks/useSearch.js";
|
|
10
|
+
export { useSearchContext } from "./context/SearchContext.js";
|
|
11
|
+
export type { SearchResultData } from "./types.js";
|
|
12
|
+
export type { SearchRootProps } from "./components/SearchRoot.js";
|
|
13
|
+
export type { SearchInputProps } from "./components/SearchInput.js";
|
|
14
|
+
export type { SearchResultsProps } from "./components/SearchResults.js";
|
|
15
|
+
export type { SearchResultProps } from "./components/SearchResult.js";
|
|
16
|
+
export type { SearchResultThumbnailProps } from "./components/SearchResultThumbnail.js";
|
|
17
|
+
export type { SearchIconProps } from "./components/SearchIcon.js";
|
|
18
|
+
export type { SearchKbdProps } from "./components/SearchKbd.js";
|
|
19
|
+
export type { SearchProps } from "./Search.js";
|
|
20
|
+
export type { UseSearchOptions, UseSearchReturn } from "./hooks/useSearch.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { SearchRoot } from "./components/SearchRoot.js";
|
|
3
|
+
export { SearchInput } from "./components/SearchInput.js";
|
|
4
|
+
export { SearchResults } from "./components/SearchResults.js";
|
|
5
|
+
export { SearchResult } from "./components/SearchResult.js";
|
|
6
|
+
export { SearchResultThumbnail } from "./components/SearchResultThumbnail.js";
|
|
7
|
+
export { SearchIcon } from "./components/SearchIcon.js";
|
|
8
|
+
export { SearchKbd } from "./components/SearchKbd.js";
|
|
9
|
+
export { Search } from "./Search.js";
|
|
10
|
+
// Hooks
|
|
11
|
+
export { useSearch } from "./hooks/useSearch.js";
|
|
12
|
+
export { useSearchContext } from "./context/SearchContext.js";
|