@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.
Files changed (74) hide show
  1. package/README.md +344 -206
  2. package/dist/Searchbar.d.ts +315 -0
  3. package/dist/Searchbar.js +207 -0
  4. package/dist/context.d.ts +57 -0
  5. package/dist/context.js +32 -0
  6. package/dist/hooks/useEtoileSearch.d.ts +136 -0
  7. package/dist/hooks/useEtoileSearch.js +187 -0
  8. package/dist/index.d.ts +44 -19
  9. package/dist/index.js +37 -12
  10. package/dist/primitives/Content.d.ts +34 -0
  11. package/dist/primitives/Content.js +108 -0
  12. package/dist/primitives/Empty.d.ts +25 -0
  13. package/dist/primitives/Empty.js +25 -0
  14. package/dist/primitives/Error.d.ts +29 -0
  15. package/dist/primitives/Error.js +26 -0
  16. package/dist/primitives/Group.d.ts +30 -0
  17. package/dist/primitives/Group.js +22 -0
  18. package/dist/primitives/Icon.d.ts +21 -0
  19. package/dist/primitives/Icon.js +14 -0
  20. package/dist/primitives/Input.d.ts +32 -0
  21. package/dist/primitives/Input.js +70 -0
  22. package/dist/primitives/Item.d.ts +61 -0
  23. package/dist/primitives/Item.js +76 -0
  24. package/dist/primitives/Kbd.d.ts +20 -0
  25. package/dist/primitives/Kbd.js +13 -0
  26. package/dist/primitives/List.d.ts +35 -0
  27. package/dist/primitives/List.js +37 -0
  28. package/dist/primitives/Loading.d.ts +25 -0
  29. package/dist/primitives/Loading.js +26 -0
  30. package/dist/primitives/Modal.d.ts +39 -0
  31. package/dist/primitives/Modal.js +37 -0
  32. package/dist/primitives/ModalInput.d.ts +61 -0
  33. package/dist/primitives/ModalInput.js +33 -0
  34. package/dist/primitives/Overlay.d.ts +21 -0
  35. package/dist/primitives/Overlay.js +41 -0
  36. package/dist/primitives/Portal.d.ts +28 -0
  37. package/dist/primitives/Portal.js +30 -0
  38. package/dist/primitives/Root.d.ts +116 -0
  39. package/dist/primitives/Root.js +413 -0
  40. package/dist/primitives/Separator.d.ts +19 -0
  41. package/dist/primitives/Separator.js +18 -0
  42. package/dist/primitives/Thumbnail.d.ts +31 -0
  43. package/dist/primitives/Thumbnail.js +59 -0
  44. package/dist/primitives/Trigger.d.ts +28 -0
  45. package/dist/primitives/Trigger.js +35 -0
  46. package/dist/store.d.ts +38 -0
  47. package/dist/store.js +63 -0
  48. package/dist/styles.css +480 -133
  49. package/dist/types.d.ts +3 -31
  50. package/dist/utils/composeRefs.d.ts +12 -0
  51. package/dist/utils/composeRefs.js +27 -0
  52. package/dist/utils/slot.d.ts +22 -0
  53. package/dist/utils/slot.js +58 -0
  54. package/package.json +9 -5
  55. package/dist/Search.d.ts +0 -39
  56. package/dist/Search.js +0 -31
  57. package/dist/components/SearchIcon.d.ts +0 -22
  58. package/dist/components/SearchIcon.js +0 -17
  59. package/dist/components/SearchInput.d.ts +0 -30
  60. package/dist/components/SearchInput.js +0 -59
  61. package/dist/components/SearchKbd.d.ts +0 -30
  62. package/dist/components/SearchKbd.js +0 -24
  63. package/dist/components/SearchResult.d.ts +0 -31
  64. package/dist/components/SearchResult.js +0 -40
  65. package/dist/components/SearchResultThumbnail.d.ts +0 -38
  66. package/dist/components/SearchResultThumbnail.js +0 -38
  67. package/dist/components/SearchResults.d.ts +0 -39
  68. package/dist/components/SearchResults.js +0 -53
  69. package/dist/components/SearchRoot.d.ts +0 -44
  70. package/dist/components/SearchRoot.js +0 -132
  71. package/dist/context/SearchContext.d.ts +0 -55
  72. package/dist/context/SearchContext.js +0 -36
  73. package/dist/hooks/useSearch.d.ts +0 -56
  74. 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;
@@ -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
- };