@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,315 @@
1
+ /**
2
+ * Searchbar — primary export of `@etoile-dev/react`.
3
+ *
4
+ * This module exposes three APIs:
5
+ *
6
+ * 1. **Convenience wrapper**: `<Searchbar />` (Etoile-powered, live data).
7
+ * Pass `apiKey` + `collections` and it handles query state, fetch, loading,
8
+ * error, and default rendering.
9
+ *
10
+ * 2. **Convenience modal**: `<SearchModal />` (Etoile-powered command palette).
11
+ * Pass `apiKey` + `collections` and it handles trigger, portal, overlay,
12
+ * content, and live results.
13
+ *
14
+ * 3. **Headless primitives**: `Searchbar.Root`, `Searchbar.Input`,
15
+ * `Searchbar.List`, `Searchbar.Item`, etc. are UI-only primitives with no
16
+ * built-in data fetching. Bring your own data layer.
17
+ */
18
+ import * as React from "react";
19
+ import type { SearchFilter, SearchResult } from "@etoile-dev/client";
20
+ import type { SearchbarRootProps } from "./primitives/Root.js";
21
+ export type SearchbarProps = {
22
+ /** Your Etoile API key. Get one at https://etoile.dev */
23
+ apiKey: string;
24
+ /** Collections to search in (e.g., ["paintings", "artists"]) */
25
+ collections: string[];
26
+ /** Maximum results to return (default: 10) */
27
+ limit?: number;
28
+ /** Number of results to skip for pagination (default: 0) */
29
+ offset?: number;
30
+ /** Debounce delay in ms (default: 100) */
31
+ debounceMs?: number;
32
+ /** Placeholder for the search input (default: "Search…") */
33
+ placeholder?: string;
34
+ /**
35
+ * Explicit metadata filters applied to results.
36
+ * Mutually exclusive with `autoFilters`.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * filters={[{ key: "artist", operator: "eq", value: "Vincent van Gogh" }]}
41
+ * ```
42
+ */
43
+ filters?: SearchFilter[];
44
+ /**
45
+ * When `true`, the AI extracts filters from the query automatically.
46
+ * Mutually exclusive with `filters`.
47
+ */
48
+ autoFilters?: boolean;
49
+ /**
50
+ * Custom render function for each result item.
51
+ * Return a `Searchbar.Item` with a stable `value`.
52
+ */
53
+ renderItem?: (result: SearchResult) => React.ReactNode;
54
+ /** Called when an item is selected. Receives the result's `external_id`. */
55
+ onSelect?: (value: string) => void;
56
+ baseUrl?: string;
57
+ } & Omit<SearchbarRootProps, "isLoading" | "error" | "search" | "onSearchChange" | "children" | "onSelect">;
58
+ export type SearchModalProps = {
59
+ /** Your Etoile API key. Get one at https://etoile.dev */
60
+ apiKey: string;
61
+ /** Collections to search in (e.g., ["paintings", "artists"]) */
62
+ collections: string[];
63
+ /** Maximum results to return (default: 10) */
64
+ limit?: number;
65
+ /** Number of results to skip for pagination (default: 0) */
66
+ offset?: number;
67
+ /** Debounce delay in ms (default: 100) */
68
+ debounceMs?: number;
69
+ /** Placeholder for the modal input (default: "Search…") */
70
+ placeholder?: string;
71
+ /**
72
+ * Explicit metadata filters applied to results.
73
+ * Mutually exclusive with `autoFilters`.
74
+ */
75
+ filters?: SearchFilter[];
76
+ /**
77
+ * When `true`, the AI extracts filters from the query automatically.
78
+ * Mutually exclusive with `filters`.
79
+ */
80
+ autoFilters?: boolean;
81
+ /**
82
+ * Global keyboard shortcut that opens the modal.
83
+ * Defaults to `"mod+k"` (⌘K on Mac / Ctrl+K elsewhere).
84
+ */
85
+ hotkey?: string;
86
+ /** Accessible label for modal content (default: "Search") */
87
+ modalLabel?: string;
88
+ /**
89
+ * Custom render function for each result item.
90
+ * Return a `Searchbar.Item` with a stable `value`.
91
+ */
92
+ renderItem?: (result: SearchResult) => React.ReactNode;
93
+ /** Called when an item is selected. Receives the result's `external_id`. */
94
+ onSelect?: (value: string) => void;
95
+ baseUrl?: string;
96
+ } & Omit<SearchbarRootProps, "isLoading" | "error" | "search" | "onSearchChange" | "children" | "onSelect" | "hotkey">;
97
+ /**
98
+ * All-in-one command palette powered by Etoile.
99
+ *
100
+ * Handles portal, overlay, modal content, and live search results.
101
+ * Includes built-in open/close logic and defaults to `hotkey="mod+k"`.
102
+ * Import `@etoile-dev/react/styles.css` for the default theme.
103
+ *
104
+ * @example Basic usage
105
+ * ```tsx
106
+ * <SearchModal apiKey="your-api-key" collections={["paintings"]} />
107
+ * ```
108
+ *
109
+ * @example With filters
110
+ * ```tsx
111
+ * <SearchModal
112
+ * apiKey={process.env.ETOILE_API_KEY!}
113
+ * collections={["paintings"]}
114
+ * filters={[{ key: "artist", operator: "eq", value: "Vincent van Gogh" }]}
115
+ * />
116
+ * ```
117
+ *
118
+ * @example With custom rendering
119
+ * ```tsx
120
+ * <SearchModal
121
+ * apiKey={process.env.ETOILE_API_KEY!}
122
+ * collections={["paintings", "artists"]}
123
+ * hotkey="mod+/"
124
+ * renderItem={(result) => (
125
+ * <Searchbar.Item value={result.external_id} label={result.title}>
126
+ * <Searchbar.Thumbnail />
127
+ * <div>
128
+ * <strong>{result.title}</strong>
129
+ * <span>{String(result.metadata?.artist ?? "")}</span>
130
+ * </div>
131
+ * </Searchbar.Item>
132
+ * )}
133
+ * />
134
+ * ```
135
+ */
136
+ export declare const SearchModal: React.ForwardRefExoticComponent<{
137
+ /** Your Etoile API key. Get one at https://etoile.dev */
138
+ apiKey: string;
139
+ /** Collections to search in (e.g., ["paintings", "artists"]) */
140
+ collections: string[];
141
+ /** Maximum results to return (default: 10) */
142
+ limit?: number;
143
+ /** Number of results to skip for pagination (default: 0) */
144
+ offset?: number;
145
+ /** Debounce delay in ms (default: 100) */
146
+ debounceMs?: number;
147
+ /** Placeholder for the modal input (default: "Search…") */
148
+ placeholder?: string;
149
+ /**
150
+ * Explicit metadata filters applied to results.
151
+ * Mutually exclusive with `autoFilters`.
152
+ */
153
+ filters?: SearchFilter[];
154
+ /**
155
+ * When `true`, the AI extracts filters from the query automatically.
156
+ * Mutually exclusive with `filters`.
157
+ */
158
+ autoFilters?: boolean;
159
+ /**
160
+ * Global keyboard shortcut that opens the modal.
161
+ * Defaults to `"mod+k"` (⌘K on Mac / Ctrl+K elsewhere).
162
+ */
163
+ hotkey?: string;
164
+ /** Accessible label for modal content (default: "Search") */
165
+ modalLabel?: string;
166
+ /**
167
+ * Custom render function for each result item.
168
+ * Return a `Searchbar.Item` with a stable `value`.
169
+ */
170
+ renderItem?: (result: SearchResult) => React.ReactNode;
171
+ /** Called when an item is selected. Receives the result's `external_id`. */
172
+ onSelect?: (value: string) => void;
173
+ baseUrl?: string;
174
+ } & Omit<SearchbarRootProps, "isLoading" | "error" | "children" | "onSelect" | "search" | "onSearchChange" | "hotkey"> & React.RefAttributes<HTMLDivElement>>;
175
+ export declare const Searchbar: React.ForwardRefExoticComponent<{
176
+ /** Your Etoile API key. Get one at https://etoile.dev */
177
+ apiKey: string;
178
+ /** Collections to search in (e.g., ["paintings", "artists"]) */
179
+ collections: string[];
180
+ /** Maximum results to return (default: 10) */
181
+ limit?: number;
182
+ /** Number of results to skip for pagination (default: 0) */
183
+ offset?: number;
184
+ /** Debounce delay in ms (default: 100) */
185
+ debounceMs?: number;
186
+ /** Placeholder for the search input (default: "Search…") */
187
+ placeholder?: string;
188
+ /**
189
+ * Explicit metadata filters applied to results.
190
+ * Mutually exclusive with `autoFilters`.
191
+ *
192
+ * @example
193
+ * ```tsx
194
+ * filters={[{ key: "artist", operator: "eq", value: "Vincent van Gogh" }]}
195
+ * ```
196
+ */
197
+ filters?: SearchFilter[];
198
+ /**
199
+ * When `true`, the AI extracts filters from the query automatically.
200
+ * Mutually exclusive with `filters`.
201
+ */
202
+ autoFilters?: boolean;
203
+ /**
204
+ * Custom render function for each result item.
205
+ * Return a `Searchbar.Item` with a stable `value`.
206
+ */
207
+ renderItem?: (result: SearchResult) => React.ReactNode;
208
+ /** Called when an item is selected. Receives the result's `external_id`. */
209
+ onSelect?: (value: string) => void;
210
+ baseUrl?: string;
211
+ } & Omit<SearchbarRootProps, "isLoading" | "error" | "children" | "onSelect" | "search" | "onSearchChange"> & React.RefAttributes<HTMLDivElement>> & {
212
+ Root: React.ForwardRefExoticComponent<{
213
+ open?: boolean;
214
+ defaultOpen?: boolean;
215
+ onOpenChange?: (open: boolean) => void;
216
+ search?: string;
217
+ defaultSearch?: string;
218
+ onSearchChange?: (search: string) => void;
219
+ value?: string | null;
220
+ defaultValue?: string | null;
221
+ onValueChange?: (value: string | null) => void;
222
+ isLoading?: boolean;
223
+ error?: unknown;
224
+ hotkey?: string;
225
+ hotkeyBehavior?: "focus" | "toggle";
226
+ onSelect?: (value: string) => void;
227
+ children: React.ReactNode;
228
+ className?: string;
229
+ asChild?: boolean;
230
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> & React.RefAttributes<HTMLDivElement>>;
231
+ Input: React.ForwardRefExoticComponent<{
232
+ placeholder?: string;
233
+ className?: string;
234
+ asChild?: boolean;
235
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange"> & React.RefAttributes<HTMLInputElement>>;
236
+ List: React.ForwardRefExoticComponent<{
237
+ className?: string;
238
+ asChild?: boolean;
239
+ children: React.ReactNode;
240
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
241
+ Item: React.ForwardRefExoticComponent<{
242
+ value: string;
243
+ label?: string;
244
+ disabled?: boolean;
245
+ onSelect?: (value: string) => void;
246
+ children: React.ReactNode;
247
+ className?: string;
248
+ asChild?: boolean;
249
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> & React.RefAttributes<HTMLDivElement>>;
250
+ Group: React.ForwardRefExoticComponent<{
251
+ label?: string;
252
+ className?: string;
253
+ asChild?: boolean;
254
+ children: React.ReactNode;
255
+ } & React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
256
+ Separator: React.ForwardRefExoticComponent<{
257
+ className?: string;
258
+ asChild?: boolean;
259
+ } & React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
260
+ Empty: React.ForwardRefExoticComponent<{
261
+ children?: React.ReactNode;
262
+ className?: string;
263
+ asChild?: boolean;
264
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
265
+ Loading: React.ForwardRefExoticComponent<{
266
+ children?: React.ReactNode;
267
+ className?: string;
268
+ asChild?: boolean;
269
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
270
+ Error: React.ForwardRefExoticComponent<{
271
+ children?: React.ReactNode | ((error: unknown) => React.ReactNode);
272
+ className?: string;
273
+ asChild?: boolean;
274
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "role"> & React.RefAttributes<HTMLDivElement>>;
275
+ Portal: {
276
+ ({ container, children }: import("./primitives/Portal.js").SearchbarPortalProps): React.ReactPortal | null;
277
+ displayName: string;
278
+ };
279
+ Overlay: React.ForwardRefExoticComponent<{
280
+ className?: string;
281
+ asChild?: boolean;
282
+ } & React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
283
+ Content: React.ForwardRefExoticComponent<{
284
+ "aria-label"?: string;
285
+ className?: string;
286
+ asChild?: boolean;
287
+ children: React.ReactNode;
288
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "role"> & React.RefAttributes<HTMLDivElement>>;
289
+ Modal: React.ForwardRefExoticComponent<{
290
+ "aria-label"?: string;
291
+ } & Omit<SearchbarRootProps, "asChild"> & React.RefAttributes<HTMLDivElement>>;
292
+ ModalInput: React.ForwardRefExoticComponent<{
293
+ placeholder?: string;
294
+ icon?: React.ReactNode | null;
295
+ kbd?: React.ReactNode | null;
296
+ className?: string;
297
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & React.RefAttributes<HTMLDivElement>>;
298
+ Trigger: React.ForwardRefExoticComponent<{
299
+ children?: React.ReactNode;
300
+ className?: string;
301
+ asChild?: boolean;
302
+ } & React.ButtonHTMLAttributes<HTMLButtonElement> & React.RefAttributes<HTMLButtonElement>>;
303
+ Icon: {
304
+ ({ size, className, ...props }: import("./primitives/Icon.js").SearchbarIconProps): import("react/jsx-runtime").JSX.Element;
305
+ displayName: string;
306
+ };
307
+ Kbd: {
308
+ ({ children, className, ...props }: import("./primitives/Kbd.js").SearchbarKbdProps): import("react/jsx-runtime").JSX.Element;
309
+ displayName: string;
310
+ };
311
+ Thumbnail: {
312
+ ({ src, alt, size, className, ...props }: import("./primitives/Thumbnail.js").SearchbarThumbnailProps): import("react/jsx-runtime").JSX.Element | null;
313
+ displayName: string;
314
+ };
315
+ };
@@ -0,0 +1,207 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Searchbar — primary export of `@etoile-dev/react`.
4
+ *
5
+ * This module exposes three APIs:
6
+ *
7
+ * 1. **Convenience wrapper**: `<Searchbar />` (Etoile-powered, live data).
8
+ * Pass `apiKey` + `collections` and it handles query state, fetch, loading,
9
+ * error, and default rendering.
10
+ *
11
+ * 2. **Convenience modal**: `<SearchModal />` (Etoile-powered command palette).
12
+ * Pass `apiKey` + `collections` and it handles trigger, portal, overlay,
13
+ * content, and live results.
14
+ *
15
+ * 3. **Headless primitives**: `Searchbar.Root`, `Searchbar.Input`,
16
+ * `Searchbar.List`, `Searchbar.Item`, etc. are UI-only primitives with no
17
+ * built-in data fetching. Bring your own data layer.
18
+ */
19
+ import * as React from "react";
20
+ import { Root } from "./primitives/Root.js";
21
+ import { Input } from "./primitives/Input.js";
22
+ import { List } from "./primitives/List.js";
23
+ import { Item } from "./primitives/Item.js";
24
+ import { Group } from "./primitives/Group.js";
25
+ import { Separator } from "./primitives/Separator.js";
26
+ import { Empty } from "./primitives/Empty.js";
27
+ import { Loading } from "./primitives/Loading.js";
28
+ import { Error as ErrorPrimitive } from "./primitives/Error.js";
29
+ import { Portal } from "./primitives/Portal.js";
30
+ import { Overlay } from "./primitives/Overlay.js";
31
+ import { Content } from "./primitives/Content.js";
32
+ import { Modal } from "./primitives/Modal.js";
33
+ import { ModalInput } from "./primitives/ModalInput.js";
34
+ import { Trigger } from "./primitives/Trigger.js";
35
+ import { Icon } from "./primitives/Icon.js";
36
+ import { Kbd } from "./primitives/Kbd.js";
37
+ import { Thumbnail } from "./primitives/Thumbnail.js";
38
+ import { useEtoileSearch } from "./hooks/useEtoileSearch.js";
39
+ import { SearchbarItemDataContext } from "./context.js";
40
+ /** Format hotkey string for display (e.g. "mod+k" → "⌘K" on Mac, "Ctrl+K" elsewhere). */
41
+ function formatHotkeyLabel(hotkey) {
42
+ const isMac = typeof navigator !== "undefined" && /mac|darwin/i.test(navigator.platform);
43
+ const parts = hotkey.toLowerCase().trim().split("+");
44
+ const key = parts[parts.length - 1];
45
+ const keyChar = key === "slash" ? "/" : key;
46
+ const mods = parts.slice(0, -1);
47
+ const modLabels = [];
48
+ if (mods.includes("mod"))
49
+ modLabels.push(isMac ? "⌘" : "Ctrl");
50
+ if (mods.includes("ctrl"))
51
+ modLabels.push("Ctrl");
52
+ if (mods.includes("alt"))
53
+ modLabels.push(isMac ? "⌥" : "Alt");
54
+ if (mods.includes("shift"))
55
+ modLabels.push(isMac ? "⇧" : "Shift");
56
+ const keyDisplay = keyChar === "/" || keyChar.length > 1 ? keyChar : keyChar.toUpperCase();
57
+ return modLabels.length > 0 ? `${modLabels.join("")}${keyDisplay}` : keyDisplay;
58
+ }
59
+ /**
60
+ * All-in-one search component powered by Etoile.
61
+ *
62
+ * Handles data fetching, debounce, keyboard navigation, and ARIA wiring.
63
+ * No default hotkey; pass `hotkey="/"` (or `hotkey="mod+k"`) if you want a global shortcut.
64
+ * Import `@etoile-dev/react/styles.css` for the default theme.
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * import "@etoile-dev/react/styles.css";
69
+ * import { Searchbar } from "@etoile-dev/react";
70
+ *
71
+ * <Searchbar apiKey="your-api-key" collections={["paintings"]} />
72
+ * ```
73
+ *
74
+ * @example With filters and custom rendering
75
+ * ```tsx
76
+ * <Searchbar
77
+ * apiKey={process.env.ETOILE_API_KEY!}
78
+ * collections={["paintings"]}
79
+ * filters={[{ key: "artist", operator: "eq", value: "Vincent van Gogh" }]}
80
+ * onSelect={(id) => router.push(`/painting/${id}`)}
81
+ * renderItem={(result) => (
82
+ * <Searchbar.Item value={result.external_id} label={result.title}>
83
+ * <Searchbar.Thumbnail />
84
+ * <div>
85
+ * <strong>{result.title}</strong>
86
+ * <span>{String(result.metadata?.year ?? "")}</span>
87
+ * </div>
88
+ * </Searchbar.Item>
89
+ * )}
90
+ * />
91
+ * ```
92
+ *
93
+ * @example Headless primitives (no Etoile dependency)
94
+ * ```tsx
95
+ * <Searchbar.Root onSelect={handleSelect}>
96
+ * <Searchbar.Input placeholder="Search paintings…" />
97
+ * <Searchbar.List>
98
+ * {paintings.map((p) => (
99
+ * <Searchbar.Item key={p.id} value={p.id} label={p.title}>
100
+ * {p.title}
101
+ * </Searchbar.Item>
102
+ * ))}
103
+ * <Searchbar.Empty>No results.</Searchbar.Empty>
104
+ * </Searchbar.List>
105
+ * </Searchbar.Root>
106
+ * ```
107
+ */
108
+ const SearchbarWrapper = React.forwardRef(({ apiKey, collections, limit, offset, debounceMs, placeholder = "Search…", filters, autoFilters, renderItem, onSelect, baseUrl, hotkey, className, ...rootProps }, ref) => {
109
+ const [query, setQuery] = React.useState("");
110
+ const { results, isLoading, error } = useEtoileSearch({
111
+ apiKey,
112
+ collections,
113
+ query,
114
+ limit,
115
+ offset,
116
+ debounceMs,
117
+ filters,
118
+ autoFilters,
119
+ baseUrl,
120
+ });
121
+ return (_jsxs(Root, { ...rootProps, ref: ref, hotkey: hotkey, hotkeyBehavior: hotkey ? "focus" : undefined, search: query, onSearchChange: setQuery, isLoading: isLoading, error: error ?? undefined, onSelect: onSelect, className: className
122
+ ? `etoile-search ${className}`
123
+ : "etoile-search", children: [_jsxs("div", { "data-slot": "searchbar-input-row", children: [_jsx(Icon, {}), _jsx(Input, { placeholder: placeholder }), hotkey ? _jsx(Kbd, { children: formatHotkeyLabel(hotkey) }) : null] }), _jsxs(List, { children: [results.map((result) => renderItem ? (
124
+ // User's custom renderer — wrap in data context so Thumbnail works
125
+ _jsx(SearchbarItemDataContext.Provider, { value: result, children: renderItem(result) }, result.external_id)) : (_jsx(DefaultItem, { result: result }, result.external_id))), _jsxs(Empty, { children: ["No results found for ", _jsxs("span", { "data-slot": "searchbar-empty-query", children: ["\"", query, "\""] })] }), _jsx(Loading, {}), _jsx(ErrorPrimitive, {})] })] }));
126
+ });
127
+ SearchbarWrapper.displayName = "Searchbar";
128
+ /**
129
+ * All-in-one command palette powered by Etoile.
130
+ *
131
+ * Handles portal, overlay, modal content, and live search results.
132
+ * Includes built-in open/close logic and defaults to `hotkey="mod+k"`.
133
+ * Import `@etoile-dev/react/styles.css` for the default theme.
134
+ *
135
+ * @example Basic usage
136
+ * ```tsx
137
+ * <SearchModal apiKey="your-api-key" collections={["paintings"]} />
138
+ * ```
139
+ *
140
+ * @example With filters
141
+ * ```tsx
142
+ * <SearchModal
143
+ * apiKey={process.env.ETOILE_API_KEY!}
144
+ * collections={["paintings"]}
145
+ * filters={[{ key: "artist", operator: "eq", value: "Vincent van Gogh" }]}
146
+ * />
147
+ * ```
148
+ *
149
+ * @example With custom rendering
150
+ * ```tsx
151
+ * <SearchModal
152
+ * apiKey={process.env.ETOILE_API_KEY!}
153
+ * collections={["paintings", "artists"]}
154
+ * hotkey="mod+/"
155
+ * renderItem={(result) => (
156
+ * <Searchbar.Item value={result.external_id} label={result.title}>
157
+ * <Searchbar.Thumbnail />
158
+ * <div>
159
+ * <strong>{result.title}</strong>
160
+ * <span>{String(result.metadata?.artist ?? "")}</span>
161
+ * </div>
162
+ * </Searchbar.Item>
163
+ * )}
164
+ * />
165
+ * ```
166
+ */
167
+ export const SearchModal = React.forwardRef(({ apiKey, collections, limit, offset, debounceMs, placeholder = "Search…", filters, autoFilters, hotkey = "mod+k", modalLabel = "Search", renderItem, onSelect, baseUrl, className, ...rootProps }, ref) => {
168
+ const [query, setQuery] = React.useState("");
169
+ const { results, isLoading, error } = useEtoileSearch({
170
+ apiKey,
171
+ collections,
172
+ query,
173
+ limit,
174
+ offset,
175
+ debounceMs,
176
+ filters,
177
+ autoFilters,
178
+ baseUrl,
179
+ });
180
+ return (_jsxs(Modal, { ...rootProps, ref: ref, hotkey: hotkey, search: query, onSearchChange: setQuery, isLoading: isLoading, error: error ?? undefined, onSelect: onSelect, className: className
181
+ ? `etoile-search ${className}`
182
+ : "etoile-search", "aria-label": modalLabel, children: [_jsx(ModalInput, { placeholder: placeholder }), _jsxs(List, { children: [results.map((result) => renderItem ? (_jsx(SearchbarItemDataContext.Provider, { value: result, children: renderItem(result) }, result.external_id)) : (_jsx(DefaultItem, { result: result }, result.external_id))), _jsxs(Empty, { children: ["No results found for ", _jsxs("span", { "data-slot": "searchbar-empty-query", children: ["\"", query, "\""] })] }), _jsx(Loading, {}), _jsx(ErrorPrimitive, {})] })] }));
183
+ });
184
+ SearchModal.displayName = "SearchModal";
185
+ // ─── Default item renderer ────────────────────────────────────────────────────
186
+ const DefaultItem = ({ result }) => (_jsx(SearchbarItemDataContext.Provider, { value: result, children: _jsxs(Item, { value: result.external_id, label: result.title, children: [_jsx(Thumbnail, {}), _jsxs("div", { "data-slot": "searchbar-result-content", children: [_jsx("span", { "data-slot": "searchbar-result-title", children: result.title }), _jsx("span", { "data-slot": "searchbar-result-subtitle", children: result.collection })] })] }) }));
187
+ // ─── Namespace assembly ───────────────────────────────────────────────────────
188
+ export const Searchbar = Object.assign(SearchbarWrapper, {
189
+ Root,
190
+ Input,
191
+ List,
192
+ Item,
193
+ Group,
194
+ Separator,
195
+ Empty,
196
+ Loading,
197
+ Error: ErrorPrimitive,
198
+ Portal,
199
+ Overlay,
200
+ Content,
201
+ Modal,
202
+ ModalInput,
203
+ Trigger,
204
+ Icon,
205
+ Kbd,
206
+ Thumbnail,
207
+ });
@@ -0,0 +1,57 @@
1
+ import * as React from "react";
2
+ import type { SearchbarStore, SearchbarState } from "./store.js";
3
+ export type SearchbarContextValue = {
4
+ store: SearchbarStore;
5
+ /** Stable listbox DOM id — wired to aria-controls on Input */
6
+ listId: string;
7
+ /**
8
+ * Unique id for the Root instance. Used as `data-searchbar-root` on every
9
+ * DOM node that belongs to this searchbar (including portaled content) so
10
+ * the click-outside handler can correctly ignore clicks inside portals.
11
+ */
12
+ rootId: string;
13
+ /** Optional Root className forwarded to portaled Content/Overlay. */
14
+ rootClassName?: string;
15
+ /** Whether search is controlled (Input must not mutate store) */
16
+ isSearchControlled?: boolean;
17
+ /** Called when query changes (for controlled mode, Input calls this) */
18
+ onSearchChange?: (query: string) => void;
19
+ /** Ref for the Trigger — Content restores focus here on close */
20
+ triggerRef?: React.RefObject<HTMLElement | null>;
21
+ /** Derive a stable DOM id for a given item value */
22
+ getItemId: (value: string) => string;
23
+ /** Trigger selection of an item by value — handles callbacks + state update */
24
+ onSelect: (value: string) => void;
25
+ /** Set open state — respects controlled mode (Overlay/Escape use this) */
26
+ setOpen: (open: boolean) => void;
27
+ /** Keyboard handler — Input calls this so portal-rendered Inputs get navigation */
28
+ handleKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;
29
+ /** Register an item's DOM node — called by Item on mount/unmount */
30
+ registerItem: (meta: {
31
+ value: string;
32
+ label: string;
33
+ disabled: boolean;
34
+ node: HTMLElement | null;
35
+ onSelect?: (value: string) => void;
36
+ }) => void;
37
+ /** Unregister an item on unmount */
38
+ unregisterItem: (value: string) => void;
39
+ };
40
+ export declare const SearchbarProvider: React.Provider<SearchbarContextValue | null>;
41
+ export declare function useSearchbarContext(): SearchbarContextValue;
42
+ /**
43
+ * Subscribe to a slice of the searchbar store.
44
+ * Re-renders only when the selected slice changes (reference equality).
45
+ *
46
+ * @example
47
+ * const query = useSearchbarStore(ctx.store, (s) => s.query);
48
+ */
49
+ export declare function useSearchbarStore<T>(store: SearchbarStore, selector: (state: SearchbarState) => T): T;
50
+ /** Convenience: read a slice within a component that already has context */
51
+ export declare function useSearchbarState<T>(selector: (state: SearchbarState) => T): T;
52
+ /** When true, List hides when query is empty (command palette mode) */
53
+ export declare const SearchbarHideListWhenQueryEmptyContext: React.Context<boolean>;
54
+ /** Context for the item currently being rendered (set by Searchbar.Item) */
55
+ export declare const SearchbarItemContext: React.Context<string | null>;
56
+ /** Context providing the raw SearchResult when rendered via the Etoile wrapper */
57
+ export declare const SearchbarItemDataContext: React.Context<Record<string, any> | null>;
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ const SearchbarContext = React.createContext(null);
3
+ export const SearchbarProvider = SearchbarContext.Provider;
4
+ export function useSearchbarContext() {
5
+ const ctx = React.useContext(SearchbarContext);
6
+ if (!ctx) {
7
+ throw new Error("Searchbar primitives must be used within Searchbar.Root.");
8
+ }
9
+ return ctx;
10
+ }
11
+ /**
12
+ * Subscribe to a slice of the searchbar store.
13
+ * Re-renders only when the selected slice changes (reference equality).
14
+ *
15
+ * @example
16
+ * const query = useSearchbarStore(ctx.store, (s) => s.query);
17
+ */
18
+ export function useSearchbarStore(store, selector) {
19
+ return React.useSyncExternalStore(store.subscribe, () => selector(store.getState()), () => selector(store.getState()));
20
+ }
21
+ /** Convenience: read a slice within a component that already has context */
22
+ export function useSearchbarState(selector) {
23
+ const { store } = useSearchbarContext();
24
+ return useSearchbarStore(store, selector);
25
+ }
26
+ /** When true, List hides when query is empty (command palette mode) */
27
+ export const SearchbarHideListWhenQueryEmptyContext = React.createContext(false);
28
+ /** Context for the item currently being rendered (set by Searchbar.Item) */
29
+ export const SearchbarItemContext = React.createContext(null);
30
+ /** Context providing the raw SearchResult when rendered via the Etoile wrapper */
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ export const SearchbarItemDataContext = React.createContext(null);