@etoile-dev/react 0.2.3 → 1.0.0

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 +341 -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 +122 -0
  7. package/dist/hooks/useEtoileSearch.js +138 -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
@@ -0,0 +1,122 @@
1
+ import type { SearchFilter, SearchResult } from "@etoile-dev/client";
2
+ export type UseEtoileSearchOptions = {
3
+ /** Your Etoile API key */
4
+ apiKey: string;
5
+ /** Collections to search */
6
+ collections: string[];
7
+ /** Current search query (controlled externally, e.g. from Searchbar.Root) */
8
+ query: string;
9
+ /** Maximum results to return (default: 10) */
10
+ limit?: number;
11
+ /** Number of results to skip for pagination (default: 0) */
12
+ offset?: number;
13
+ /** Debounce delay in ms before firing the request (default: 100) */
14
+ debounceMs?: number;
15
+ /**
16
+ * Explicit metadata filters applied to results.
17
+ * Mutually exclusive with `autoFilters`.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * filters={[
22
+ * { key: "artist", operator: "eq", value: "Vincent van Gogh" },
23
+ * { key: "year", operator: "lte", value: 1900 },
24
+ * ]}
25
+ * ```
26
+ */
27
+ filters?: SearchFilter[];
28
+ /**
29
+ * When true, the AI extracts filters from the query automatically.
30
+ * Returns `appliedFilters` and `refinedQuery` in the result.
31
+ * Mutually exclusive with `filters`.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * // Query: "Van Gogh paintings after 1888"
36
+ * // → refinedQuery: "paintings"
37
+ * // → appliedFilters: [{ key: "artist", operator: "eq", value: "Vincent van Gogh" }, ...]
38
+ * autoFilters={true}
39
+ * ```
40
+ */
41
+ autoFilters?: boolean;
42
+ baseUrl?: string;
43
+ };
44
+ export type UseEtoileSearchReturn = {
45
+ /** Results returned by the last successful search. Empty while loading or on error. */
46
+ results: SearchResult[];
47
+ /** True while a fetch is in flight. */
48
+ isLoading: boolean;
49
+ /** Set when the last request failed; null otherwise. */
50
+ error: unknown;
51
+ /** Filters that were applied. Populated when `filters` or `autoFilters` is used. */
52
+ appliedFilters?: SearchFilter[];
53
+ /**
54
+ * Query rewritten by the AI after extracting filters.
55
+ * Only populated when `autoFilters` is `true`.
56
+ */
57
+ refinedQuery?: string;
58
+ };
59
+ /**
60
+ * Fetch live Etoile results for a query.
61
+ *
62
+ * Handles debouncing, in-flight cancellation, loading, and error state.
63
+ * Use this hook when composing headless primitives with your own layout.
64
+ *
65
+ * @param options - Hook options.
66
+ * @param options.apiKey - Your Etoile API key.
67
+ * @param options.collections - Collections to search in (e.g. `["paintings", "artists"]`).
68
+ * @param options.query - Current query string (controlled externally).
69
+ * @param options.limit - Maximum results to return (default: `10`).
70
+ * @param options.offset - Number of results to skip for pagination (default: `0`).
71
+ * @param options.debounceMs - Debounce delay in ms before firing the request (default: `100`).
72
+ * @param options.filters - Explicit metadata filters. Mutually exclusive with `autoFilters`.
73
+ * @param options.autoFilters - When `true`, the AI extracts filters from the query automatically.
74
+ * @returns Current search state: `results`, `isLoading`, `error`, `appliedFilters`, `refinedQuery`.
75
+ *
76
+ * @example Basic usage
77
+ * ```tsx
78
+ * const [query, setQuery] = useState("");
79
+ * const { results, isLoading } = useEtoileSearch({
80
+ * apiKey: process.env.ETOILE_API_KEY!,
81
+ * collections: ["paintings"],
82
+ * query,
83
+ * });
84
+ * ```
85
+ *
86
+ * @example With explicit filters
87
+ * ```tsx
88
+ * const { results } = useEtoileSearch({
89
+ * apiKey: process.env.ETOILE_API_KEY!,
90
+ * collections: ["paintings"],
91
+ * query,
92
+ * filters: [{ key: "artist", operator: "eq", value: "Vincent van Gogh" }],
93
+ * });
94
+ * ```
95
+ *
96
+ * @example With AI-powered auto-filters
97
+ * ```tsx
98
+ * // Query: "Van Gogh paintings after 1888"
99
+ * // → refinedQuery: "paintings"
100
+ * // → appliedFilters: [{ key: "artist", operator: "eq", value: "Vincent van Gogh" }, ...]
101
+ * const { results, refinedQuery, appliedFilters } = useEtoileSearch({
102
+ * apiKey: process.env.ETOILE_API_KEY!,
103
+ * collections: ["paintings"],
104
+ * query,
105
+ * autoFilters: true,
106
+ * });
107
+ * ```
108
+ */
109
+ export declare function useEtoileSearch({ apiKey, collections, query, limit, offset, debounceMs, filters, autoFilters, baseUrl, }: UseEtoileSearchOptions): UseEtoileSearchReturn;
110
+ /**
111
+ * Alias of `useEtoileSearch` for ergonomic imports in examples.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * const { results } = useSearch({
116
+ * apiKey: process.env.ETOILE_API_KEY!,
117
+ * collections: ["paintings"],
118
+ * query,
119
+ * });
120
+ * ```
121
+ */
122
+ export declare const useSearch: typeof useEtoileSearch;
@@ -0,0 +1,138 @@
1
+ import * as React from "react";
2
+ import { Etoile } from "@etoile-dev/client";
3
+ /**
4
+ * Fetch live Etoile results for a query.
5
+ *
6
+ * Handles debouncing, in-flight cancellation, loading, and error state.
7
+ * Use this hook when composing headless primitives with your own layout.
8
+ *
9
+ * @param options - Hook options.
10
+ * @param options.apiKey - Your Etoile API key.
11
+ * @param options.collections - Collections to search in (e.g. `["paintings", "artists"]`).
12
+ * @param options.query - Current query string (controlled externally).
13
+ * @param options.limit - Maximum results to return (default: `10`).
14
+ * @param options.offset - Number of results to skip for pagination (default: `0`).
15
+ * @param options.debounceMs - Debounce delay in ms before firing the request (default: `100`).
16
+ * @param options.filters - Explicit metadata filters. Mutually exclusive with `autoFilters`.
17
+ * @param options.autoFilters - When `true`, the AI extracts filters from the query automatically.
18
+ * @returns Current search state: `results`, `isLoading`, `error`, `appliedFilters`, `refinedQuery`.
19
+ *
20
+ * @example Basic usage
21
+ * ```tsx
22
+ * const [query, setQuery] = useState("");
23
+ * const { results, isLoading } = useEtoileSearch({
24
+ * apiKey: process.env.ETOILE_API_KEY!,
25
+ * collections: ["paintings"],
26
+ * query,
27
+ * });
28
+ * ```
29
+ *
30
+ * @example With explicit filters
31
+ * ```tsx
32
+ * const { results } = useEtoileSearch({
33
+ * apiKey: process.env.ETOILE_API_KEY!,
34
+ * collections: ["paintings"],
35
+ * query,
36
+ * filters: [{ key: "artist", operator: "eq", value: "Vincent van Gogh" }],
37
+ * });
38
+ * ```
39
+ *
40
+ * @example With AI-powered auto-filters
41
+ * ```tsx
42
+ * // Query: "Van Gogh paintings after 1888"
43
+ * // → refinedQuery: "paintings"
44
+ * // → appliedFilters: [{ key: "artist", operator: "eq", value: "Vincent van Gogh" }, ...]
45
+ * const { results, refinedQuery, appliedFilters } = useEtoileSearch({
46
+ * apiKey: process.env.ETOILE_API_KEY!,
47
+ * collections: ["paintings"],
48
+ * query,
49
+ * autoFilters: true,
50
+ * });
51
+ * ```
52
+ */
53
+ export function useEtoileSearch({ apiKey, collections, query, limit = 10, offset, debounceMs = 100, filters, autoFilters, baseUrl, }) {
54
+ const [results, setResults] = React.useState([]);
55
+ const [isLoading, setIsLoading] = React.useState(false);
56
+ const [error, setError] = React.useState(null);
57
+ const [appliedFilters, setAppliedFilters] = React.useState(undefined);
58
+ const [refinedQuery, setRefinedQuery] = React.useState(undefined);
59
+ const client = React.useMemo(() => new Etoile({ apiKey, baseUrl }), [apiKey, baseUrl]);
60
+ // Stable refs so the effect dep array doesn't need filters/autoFilters arrays
61
+ const filtersRef = React.useRef(filters);
62
+ filtersRef.current = filters;
63
+ const autoFiltersRef = React.useRef(autoFilters);
64
+ autoFiltersRef.current = autoFilters;
65
+ // Debounce the raw query
66
+ const [debouncedQuery, setDebouncedQuery] = React.useState(query);
67
+ React.useEffect(() => {
68
+ const handle = setTimeout(() => setDebouncedQuery(query), debounceMs);
69
+ return () => clearTimeout(handle);
70
+ }, [query, debounceMs]);
71
+ // Show loading as soon as user types (avoids "no results" flash before debounced fetch)
72
+ React.useEffect(() => {
73
+ if (query.trim() !== "") {
74
+ setIsLoading(true);
75
+ }
76
+ else {
77
+ setIsLoading(false);
78
+ }
79
+ }, [query]);
80
+ // Fetch on debounced query / filter change
81
+ React.useEffect(() => {
82
+ if (debouncedQuery.trim() === "") {
83
+ setResults([]);
84
+ setAppliedFilters(undefined);
85
+ setRefinedQuery(undefined);
86
+ setIsLoading(false);
87
+ setError(null);
88
+ return;
89
+ }
90
+ let active = true;
91
+ setError(null);
92
+ client
93
+ .search({
94
+ collections,
95
+ query: debouncedQuery,
96
+ limit,
97
+ ...(offset !== undefined && { offset }),
98
+ ...(filtersRef.current !== undefined && { filters: filtersRef.current }),
99
+ ...(autoFiltersRef.current !== undefined && { autoFilters: autoFiltersRef.current }),
100
+ })
101
+ .then((res) => {
102
+ if (!active)
103
+ return;
104
+ setResults(Array.isArray(res.results) ? res.results : []);
105
+ setAppliedFilters(res.appliedFilters);
106
+ setRefinedQuery(res.refinedQuery);
107
+ })
108
+ .catch((err) => {
109
+ if (!active)
110
+ return;
111
+ setResults([]);
112
+ setAppliedFilters(undefined);
113
+ setRefinedQuery(undefined);
114
+ setError(err);
115
+ })
116
+ .finally(() => {
117
+ if (active)
118
+ setIsLoading(false);
119
+ });
120
+ return () => {
121
+ active = false;
122
+ };
123
+ }, [client, collections, debouncedQuery, limit, offset]);
124
+ return { results, isLoading, error, appliedFilters, refinedQuery };
125
+ }
126
+ /**
127
+ * Alias of `useEtoileSearch` for ergonomic imports in examples.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * const { results } = useSearch({
132
+ * apiKey: process.env.ETOILE_API_KEY!,
133
+ * collections: ["paintings"],
134
+ * query,
135
+ * });
136
+ * ```
137
+ */
138
+ export const useSearch = useEtoileSearch;
package/dist/index.d.ts CHANGED
@@ -1,20 +1,45 @@
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";
1
+ export { Searchbar, SearchModal } from "./Searchbar.js";
2
+ export { Root as SearchbarRoot } from "./primitives/Root.js";
3
+ export { Input as SearchbarInput } from "./primitives/Input.js";
4
+ export { List as SearchbarList } from "./primitives/List.js";
5
+ export { Item as SearchbarItem } from "./primitives/Item.js";
6
+ export { Group as SearchbarGroup } from "./primitives/Group.js";
7
+ export { Separator as SearchbarSeparator } from "./primitives/Separator.js";
8
+ export { Empty as SearchbarEmpty } from "./primitives/Empty.js";
9
+ export { Loading as SearchbarLoading } from "./primitives/Loading.js";
10
+ export { Error as SearchbarError } from "./primitives/Error.js";
11
+ export { Portal as SearchbarPortal } from "./primitives/Portal.js";
12
+ export { Overlay as SearchbarOverlay } from "./primitives/Overlay.js";
13
+ export { Content as SearchbarContent } from "./primitives/Content.js";
14
+ export { Modal as SearchbarModal } from "./primitives/Modal.js";
15
+ export { ModalInput as SearchbarModalInput } from "./primitives/ModalInput.js";
16
+ export { Trigger as SearchbarTrigger } from "./primitives/Trigger.js";
17
+ export { Icon as SearchbarIcon } from "./primitives/Icon.js";
18
+ export { Kbd as SearchbarKbd } from "./primitives/Kbd.js";
19
+ export { Thumbnail as SearchbarThumbnail } from "./primitives/Thumbnail.js";
20
+ export { useSearchbarContext, useSearchbarStore, useSearchbarState } from "./context.js";
21
+ export { useEtoileSearch, useSearch } from "./hooks/useEtoileSearch.js";
22
+ export type { SearchResult, SearchFilter, FilterOperator } from "@etoile-dev/client";
23
+ /** @deprecated Use `SearchResult` from `@etoile-dev/client` instead. */
11
24
  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";
25
+ export type { SearchbarProps, SearchModalProps } from "./Searchbar.js";
26
+ export type { SearchbarRootProps } from "./primitives/Root.js";
27
+ export type { SearchbarInputProps } from "./primitives/Input.js";
28
+ export type { SearchbarListProps } from "./primitives/List.js";
29
+ export type { SearchbarItemProps } from "./primitives/Item.js";
30
+ export type { SearchbarGroupProps } from "./primitives/Group.js";
31
+ export type { SearchbarSeparatorProps } from "./primitives/Separator.js";
32
+ export type { SearchbarEmptyProps } from "./primitives/Empty.js";
33
+ export type { SearchbarLoadingProps } from "./primitives/Loading.js";
34
+ export type { SearchbarErrorProps } from "./primitives/Error.js";
35
+ export type { SearchbarPortalProps } from "./primitives/Portal.js";
36
+ export type { SearchbarOverlayProps } from "./primitives/Overlay.js";
37
+ export type { SearchbarContentProps } from "./primitives/Content.js";
38
+ export type { SearchbarModalProps } from "./primitives/Modal.js";
39
+ export type { SearchbarModalInputProps } from "./primitives/ModalInput.js";
40
+ export type { SearchbarTriggerProps } from "./primitives/Trigger.js";
41
+ export type { SearchbarIconProps } from "./primitives/Icon.js";
42
+ export type { SearchbarKbdProps } from "./primitives/Kbd.js";
43
+ export type { SearchbarThumbnailProps } from "./primitives/Thumbnail.js";
44
+ export type { SearchbarState, SearchbarStore } from "./store.js";
45
+ export type { UseEtoileSearchOptions, UseEtoileSearchReturn } from "./hooks/useEtoileSearch.js";
package/dist/index.js CHANGED
@@ -1,12 +1,37 @@
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";
1
+ // ─── Primary export ───────────────────────────────────────────────────────────
2
+ //
3
+ // `Searchbar` is both a ready-to-use Etoile search component AND
4
+ // the namespace for all headless primitives:
5
+ //
6
+ // <Searchbar apiKey="…" collections={["paintings"]} />
7
+ //
8
+ // <Searchbar.Root onSelect={}>
9
+ // <Searchbar.Input />
10
+ // <Searchbar.List>
11
+ // <Searchbar.Item value="…">…</Searchbar.Item>
12
+ // <Searchbar.Empty>No results</Searchbar.Empty>
13
+ // </Searchbar.List>
14
+ // </Searchbar.Root>
15
+ export { Searchbar, SearchModal } from "./Searchbar.js";
16
+ // ─── Individual primitive exports (tree-shakeable) ────────────────────────────
17
+ export { Root as SearchbarRoot } from "./primitives/Root.js";
18
+ export { Input as SearchbarInput } from "./primitives/Input.js";
19
+ export { List as SearchbarList } from "./primitives/List.js";
20
+ export { Item as SearchbarItem } from "./primitives/Item.js";
21
+ export { Group as SearchbarGroup } from "./primitives/Group.js";
22
+ export { Separator as SearchbarSeparator } from "./primitives/Separator.js";
23
+ export { Empty as SearchbarEmpty } from "./primitives/Empty.js";
24
+ export { Loading as SearchbarLoading } from "./primitives/Loading.js";
25
+ export { Error as SearchbarError } from "./primitives/Error.js";
26
+ export { Portal as SearchbarPortal } from "./primitives/Portal.js";
27
+ export { Overlay as SearchbarOverlay } from "./primitives/Overlay.js";
28
+ export { Content as SearchbarContent } from "./primitives/Content.js";
29
+ export { Modal as SearchbarModal } from "./primitives/Modal.js";
30
+ export { ModalInput as SearchbarModalInput } from "./primitives/ModalInput.js";
31
+ export { Trigger as SearchbarTrigger } from "./primitives/Trigger.js";
32
+ export { Icon as SearchbarIcon } from "./primitives/Icon.js";
33
+ export { Kbd as SearchbarKbd } from "./primitives/Kbd.js";
34
+ export { Thumbnail as SearchbarThumbnail } from "./primitives/Thumbnail.js";
35
+ // ─── Context hook (advanced usage) ───────────────────────────────────────────
36
+ export { useSearchbarContext, useSearchbarStore, useSearchbarState } from "./context.js";
37
+ export { useEtoileSearch, useSearch } from "./hooks/useEtoileSearch.js";
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ export type SearchbarContentProps = {
3
+ /** Accessible label for the dialog */
4
+ "aria-label"?: string;
5
+ className?: string;
6
+ asChild?: boolean;
7
+ children: React.ReactNode;
8
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role">;
9
+ /**
10
+ * Floating content panel for command palette / modal mode.
11
+ *
12
+ * Implements `role="dialog"` with `aria-modal`, focus trapping (focus stays
13
+ * inside when open), and restores focus to the Trigger on close.
14
+ *
15
+ * Height animates smoothly when results appear or disappear via ResizeObserver.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <Searchbar.Portal>
20
+ * <Searchbar.Overlay />
21
+ * <Searchbar.Content aria-label="Search">
22
+ * <Searchbar.ModalInput />
23
+ * <Searchbar.List>…</Searchbar.List>
24
+ * </Searchbar.Content>
25
+ * </Searchbar.Portal>
26
+ * ```
27
+ */
28
+ export declare const Content: React.ForwardRefExoticComponent<{
29
+ /** Accessible label for the dialog */
30
+ "aria-label"?: string;
31
+ className?: string;
32
+ asChild?: boolean;
33
+ children: React.ReactNode;
34
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchbarContext, useSearchbarStore, SearchbarHideListWhenQueryEmptyContext, } from "../context.js";
4
+ import { Slot } from "../utils/slot.js";
5
+ const PRESENCE_DURATION_MS = 300;
6
+ /**
7
+ * Floating content panel for command palette / modal mode.
8
+ *
9
+ * Implements `role="dialog"` with `aria-modal`, focus trapping (focus stays
10
+ * inside when open), and restores focus to the Trigger on close.
11
+ *
12
+ * Height animates smoothly when results appear or disappear via ResizeObserver.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Searchbar.Portal>
17
+ * <Searchbar.Overlay />
18
+ * <Searchbar.Content aria-label="Search">
19
+ * <Searchbar.ModalInput />
20
+ * <Searchbar.List>…</Searchbar.List>
21
+ * </Searchbar.Content>
22
+ * </Searchbar.Portal>
23
+ * ```
24
+ */
25
+ export const Content = React.forwardRef(({ "aria-label": ariaLabel = "Search", className, asChild = false, children, ...props }, forwardedRef) => {
26
+ const { store, rootId, rootClassName, handleKeyDown, triggerRef } = useSearchbarContext();
27
+ const isOpen = useSearchbarStore(store, (s) => s.open);
28
+ const [present, setPresent] = React.useState(isOpen);
29
+ const contentRef = React.useRef(null);
30
+ const innerRef = React.useRef(null);
31
+ const mergedClassName = [rootClassName, className].filter(Boolean).join(" ") || undefined;
32
+ // Focus first focusable on open; restore focus to Trigger on close; trap focus
33
+ React.useEffect(() => {
34
+ if (!isOpen)
35
+ return;
36
+ const node = contentRef.current;
37
+ const getFocusables = () => Array.from(node?.querySelectorAll('input, button, [tabindex]:not([tabindex="-1"])') ?? []);
38
+ const focusable = getFocusables()[0];
39
+ focusable?.focus();
40
+ const handleFocusIn = (e) => {
41
+ if (!node?.contains(e.target)) {
42
+ e.preventDefault();
43
+ getFocusables()[0]?.focus();
44
+ }
45
+ };
46
+ document.addEventListener("focusin", handleFocusIn);
47
+ return () => {
48
+ document.removeEventListener("focusin", handleFocusIn);
49
+ triggerRef?.current?.focus();
50
+ };
51
+ }, [isOpen, triggerRef]);
52
+ React.useEffect(() => {
53
+ if (isOpen) {
54
+ setPresent(true);
55
+ return;
56
+ }
57
+ const timeout = window.setTimeout(() => setPresent(false), PRESENCE_DURATION_MS);
58
+ return () => window.clearTimeout(timeout);
59
+ }, [isOpen]);
60
+ // Animate height via ResizeObserver watching the inner content wrapper.
61
+ // The first measurement is applied instantly (no transition) so the open
62
+ // animation isn't interrupted. Subsequent changes (results appearing /
63
+ // disappearing) are handled by the CSS transition on the outer panel.
64
+ React.useLayoutEffect(() => {
65
+ const inner = innerRef.current;
66
+ const outer = contentRef.current;
67
+ if (!inner || !outer)
68
+ return;
69
+ const outerStyle = getComputedStyle(outer);
70
+ const paddingY = parseFloat(outerStyle.paddingTop) + parseFloat(outerStyle.paddingBottom);
71
+ let initial = true;
72
+ const observer = new ResizeObserver(([entry]) => {
73
+ const targetHeight = entry.contentRect.height + paddingY;
74
+ if (initial) {
75
+ // Set instantly on first render — don't fight the open animation.
76
+ outer.style.transition = "none";
77
+ outer.style.height = `${targetHeight}px`;
78
+ requestAnimationFrame(() => {
79
+ outer.style.transition = "";
80
+ initial = false;
81
+ });
82
+ }
83
+ else {
84
+ outer.style.height = `${targetHeight}px`;
85
+ }
86
+ });
87
+ observer.observe(inner);
88
+ return () => {
89
+ observer.disconnect();
90
+ outer.style.height = "";
91
+ outer.style.transition = "";
92
+ };
93
+ }, [present]);
94
+ if (!present)
95
+ return null;
96
+ const Comp = asChild ? Slot : "div";
97
+ return (_jsx(SearchbarHideListWhenQueryEmptyContext.Provider, { value: true, children: _jsx(Comp, { ...props, ref: (node) => {
98
+ contentRef.current = node;
99
+ if (typeof forwardedRef === "function")
100
+ forwardedRef(node);
101
+ else if (forwardedRef)
102
+ forwardedRef.current = node;
103
+ }, role: "dialog", "aria-modal": "true", "aria-label": ariaLabel, className: mergedClassName, "data-state": isOpen ? "open" : "closed", "data-slot": "searchbar-content", "data-searchbar-root": rootId, onKeyDown: (event) => {
104
+ props.onKeyDown?.(event);
105
+ handleKeyDown(event);
106
+ }, children: _jsx("div", { ref: innerRef, "data-slot": "searchbar-content-inner", children: children }) }) }));
107
+ });
108
+ Content.displayName = "Searchbar.Content";
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ export type SearchbarEmptyProps = {
3
+ /** Defaults to "No results." */
4
+ children?: React.ReactNode;
5
+ className?: string;
6
+ asChild?: boolean;
7
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role">;
8
+ /**
9
+ * Renders only when the list is open, query is non-empty, no items match the
10
+ * current filter, and a search is not in progress.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <Searchbar.List>
15
+ * {items.map(…)}
16
+ * <Searchbar.Empty>No paintings found.</Searchbar.Empty>
17
+ * </Searchbar.List>
18
+ * ```
19
+ */
20
+ export declare const Empty: React.ForwardRefExoticComponent<{
21
+ /** Defaults to "No results." */
22
+ children?: React.ReactNode;
23
+ className?: string;
24
+ asChild?: boolean;
25
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchbarContext, useSearchbarStore } from "../context.js";
4
+ import { Slot } from "../utils/slot.js";
5
+ /**
6
+ * Renders only when the list is open, query is non-empty, no items match the
7
+ * current filter, and a search is not in progress.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <Searchbar.List>
12
+ * {items.map(…)}
13
+ * <Searchbar.Empty>No paintings found.</Searchbar.Empty>
14
+ * </Searchbar.List>
15
+ * ```
16
+ */
17
+ export const Empty = React.forwardRef(({ children = "No results.", className, asChild = false, ...props }, forwardedRef) => {
18
+ const { store } = useSearchbarContext();
19
+ const show = useSearchbarStore(store, (s) => s.open && s.query.trim() !== "" && s.filteredValues.length === 0 && !s.isLoading);
20
+ if (!show)
21
+ return null;
22
+ const Comp = asChild ? Slot : "div";
23
+ return (_jsx(Comp, { ...props, ref: forwardedRef, role: "status", className: className, "data-slot": "searchbar-empty", "data-state": "empty", children: children }));
24
+ });
25
+ Empty.displayName = "Searchbar.Empty";
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ export type SearchbarErrorProps = {
3
+ /**
4
+ * Content to render. Can be a node or a render function that receives the
5
+ * current error value.
6
+ */
7
+ children?: React.ReactNode | ((error: unknown) => React.ReactNode);
8
+ className?: string;
9
+ asChild?: boolean;
10
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role" | "children">;
11
+ /**
12
+ * Renders only when `error` is set on Searchbar.Root.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Searchbar.Error>
17
+ * {(err) => `Search failed: ${String(err)}`}
18
+ * </Searchbar.Error>
19
+ * ```
20
+ */
21
+ export declare const Error: React.ForwardRefExoticComponent<{
22
+ /**
23
+ * Content to render. Can be a node or a render function that receives the
24
+ * current error value.
25
+ */
26
+ children?: React.ReactNode | ((error: unknown) => React.ReactNode);
27
+ className?: string;
28
+ asChild?: boolean;
29
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "role"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchbarContext, useSearchbarStore } from "../context.js";
4
+ import { Slot } from "../utils/slot.js";
5
+ /**
6
+ * Renders only when `error` is set on Searchbar.Root.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * <Searchbar.Error>
11
+ * {(err) => `Search failed: ${String(err)}`}
12
+ * </Searchbar.Error>
13
+ * ```
14
+ */
15
+ export const Error = React.forwardRef(({ children = "Something went wrong.", className, asChild = false, ...props }, forwardedRef) => {
16
+ const { store } = useSearchbarContext();
17
+ const error = useSearchbarStore(store, (s) => s.error);
18
+ if (!error)
19
+ return null;
20
+ const Comp = asChild ? Slot : "div";
21
+ const content = typeof children === "function"
22
+ ? children(error)
23
+ : children;
24
+ return (_jsx(Comp, { ...props, ref: forwardedRef, role: "alert", className: className, "data-slot": "searchbar-error", "data-state": "error", children: content }));
25
+ });
26
+ Error.displayName = "Searchbar.Error";
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ export type SearchbarGroupProps = {
3
+ /** Accessible label for the group */
4
+ label?: string;
5
+ /** Additional CSS class name */
6
+ className?: string;
7
+ asChild?: boolean;
8
+ children: React.ReactNode;
9
+ } & React.HTMLAttributes<HTMLDivElement>;
10
+ /**
11
+ * Groups related search items under a labelled section.
12
+ *
13
+ * Renders a heading and wraps children in an ARIA group role.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <Searchbar.Group label="Paintings">
18
+ * <Searchbar.Item value="starry-night">The Starry Night</Searchbar.Item>
19
+ * <Searchbar.Item value="irises">Irises</Searchbar.Item>
20
+ * </Searchbar.Group>
21
+ * ```
22
+ */
23
+ export declare const Group: React.ForwardRefExoticComponent<{
24
+ /** Accessible label for the group */
25
+ label?: string;
26
+ /** Additional CSS class name */
27
+ className?: string;
28
+ asChild?: boolean;
29
+ children: React.ReactNode;
30
+ } & React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;