@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Etoile
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,361 @@
1
+ <p align="center">
2
+ <a href="https://etoile.dev">
3
+ <img src="https://etoile.dev/assets/logo-black.svg" alt="Étoile" height="32" />
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src="https://etoile.dev/assets/hands-of-god.jpg" alt="Add search to your app in seconds" width="100%" />
9
+ </p>
10
+
11
+ <h1 align="center">@etoile-dev/react</h1>
12
+
13
+ <p align="center">
14
+ <strong>Headless React primitives for search.</strong>
15
+ <br />
16
+ Composable. Accessible. Zero styling.
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://etoile.dev">Website</a> · <a href="https://etoile.dev/docs">Documentation</a>
21
+ </p>
22
+
23
+ ---
24
+
25
+ ## About
26
+
27
+ **@etoile-dev/react** provides headless, composable React components for search powered by [Étoile](https://etoile.dev).
28
+
29
+ Built on top of [@etoile-dev/client](https://www.npmjs.com/package/@etoile-dev/client), these primitives give you full control over styling while handling state, keyboard navigation, and accessibility.
30
+
31
+ ---
32
+
33
+ ## Philosophy
34
+
35
+ - **Headless-first** — You control the appearance
36
+ - **Composable** — Build your own search UX
37
+ - **Accessible** — Full ARIA support and keyboard navigation
38
+ - **No magic** — Behavior is predictable and documented
39
+ - **No opinions** — Bring your own styles (or use our optional theme)
40
+
41
+ ---
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ npm i @etoile-dev/react
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Quickstart
52
+
53
+ ```tsx
54
+ import { Search } from "@etoile-dev/react";
55
+
56
+ export default function App() {
57
+ return <Search apiKey="your-api-key" collections={["paintings"]} />;
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Composable Primitives
64
+
65
+ For full control, use the headless primitives:
66
+
67
+ ```tsx
68
+ import {
69
+ SearchRoot,
70
+ SearchInput,
71
+ SearchResults,
72
+ SearchResult,
73
+ } from "@etoile-dev/react";
74
+
75
+ export default function CustomSearch() {
76
+ return (
77
+ <SearchRoot
78
+ apiKey={process.env.ETOILE_API_KEY}
79
+ collections={["paintings"]}
80
+ limit={20}
81
+ >
82
+ <SearchInput placeholder="Search paintings..." className="search-input" />
83
+
84
+ <SearchResults className="results-list">
85
+ {(result) => (
86
+ <SearchResult className="result-item">
87
+ <h3>{result.title}</h3>
88
+ <p>{result.metadata.artist}</p>
89
+ <small>Score: {result.score.toFixed(2)}</small>
90
+ </SearchResult>
91
+ )}
92
+ </SearchResults>
93
+ </SearchRoot>
94
+ );
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Styling with data attributes
101
+
102
+ Each result automatically gets `data-selected` and `data-index` attributes:
103
+
104
+ ```css
105
+ .result-item {
106
+ padding: 1rem;
107
+ cursor: pointer;
108
+ }
109
+
110
+ .result-item[data-selected="true"] {
111
+ background: #f0f9ff;
112
+ border-left: 3px solid #0ea5e9;
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Default Theme
119
+
120
+ Import the optional theme for a polished, ready-to-use experience:
121
+
122
+ ```tsx
123
+ import "@etoile-dev/react/styles.css";
124
+ import { Search } from "@etoile-dev/react";
125
+
126
+ <Search apiKey="your-api-key" collections={["paintings"]} />
127
+ ```
128
+
129
+ That's it! The `etoile-search` class is applied automatically.
130
+
131
+ ### Dark Mode
132
+
133
+ Add `dark` to the className:
134
+
135
+ ```tsx
136
+ <Search apiKey="your-api-key" collections={["paintings"]} className="dark" />
137
+
138
+ // Or with SearchRoot
139
+ <SearchRoot apiKey="your-api-key" collections={["paintings"]} className="dark">
140
+ ...
141
+ </SearchRoot>
142
+ ```
143
+
144
+ ### CSS Variables
145
+
146
+ Customize the theme with CSS variables:
147
+
148
+ ```css
149
+ .etoile-search {
150
+ --etoile-bg: #ffffff;
151
+ --etoile-border: #e4e4e7;
152
+ --etoile-text: #09090b;
153
+ --etoile-text-muted: #71717a;
154
+ --etoile-ring: #18181b;
155
+ --etoile-selected: #f4f4f5;
156
+ --etoile-radius: 12px;
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Headless hook
163
+
164
+ For complete control, use the `useSearch` hook:
165
+
166
+ ```tsx
167
+ import { useSearch } from "@etoile-dev/react";
168
+
169
+ function MyCustomSearch() {
170
+ const { query, setQuery, results, isLoading } = useSearch({
171
+ apiKey: "your-api-key",
172
+ collections: ["paintings"],
173
+ });
174
+
175
+ return (
176
+ <div>
177
+ <input
178
+ value={query}
179
+ onChange={(e) => setQuery(e.target.value)}
180
+ placeholder="Search paintings..."
181
+ />
182
+ {isLoading && <p>Loading...</p>}
183
+ <ul>
184
+ {results.map((result) => (
185
+ <li key={result.external_id}>{result.title}</li>
186
+ ))}
187
+ </ul>
188
+ </div>
189
+ );
190
+ }
191
+ ```
192
+
193
+ ---
194
+
195
+ ## API
196
+
197
+ ### `<Search>`
198
+
199
+ Convenience component that composes all primitives.
200
+
201
+ | Prop | Type | Required | Default |
202
+ |----------------|-----------------------------------------------|----------|---------|
203
+ | `apiKey` | `string` | ✓ | |
204
+ | `collections` | `string[]` | ✓ | |
205
+ | `limit` | `number` | | `10` |
206
+ | `renderResult` | `(result: SearchResultData) => React.ReactNode` | | |
207
+
208
+ ---
209
+
210
+ ### `<SearchRoot>`
211
+
212
+ Context provider that manages search state and keyboard navigation.
213
+
214
+ | Prop | Type | Required | Default |
215
+ |---------------|-------------------|----------|---------|
216
+ | `apiKey` | `string` | ✓ | |
217
+ | `collections` | `string[]` | ✓ | |
218
+ | `limit` | `number` | | `10` |
219
+ | `debounceMs` | `number` | | `100` |
220
+ | `autoFocus` | `boolean` | | `false` |
221
+ | `children` | `React.ReactNode` | ✓ | |
222
+
223
+ ---
224
+
225
+ ### `<SearchInput>`
226
+
227
+ Controlled input with ARIA combobox role.
228
+
229
+ | Prop | Type |
230
+ |---------------|----------|
231
+ | `placeholder` | `string` |
232
+ | `className` | `string` |
233
+
234
+ **Keyboard shortcuts:**
235
+ - `ArrowUp` / `ArrowDown` — Navigate results
236
+ - `Enter` — Select active result
237
+ - `Escape` — Clear search
238
+
239
+ ---
240
+
241
+ ### `<SearchResults>`
242
+
243
+ Results container with ARIA listbox role.
244
+
245
+ | Prop | Type | Required |
246
+ |-------------|-----------------------------------------------|----------|
247
+ | `className` | `string` | |
248
+ | `children` | `(result: SearchResultData) => React.ReactNode` | ✓ |
249
+
250
+ ---
251
+
252
+ ### `<SearchResult>`
253
+
254
+ Individual result with ARIA option role.
255
+
256
+ | Prop | Type | Required |
257
+ |-------------|-------------------|----------|
258
+ | `className` | `string` | |
259
+ | `children` | `React.ReactNode` | ✓ |
260
+
261
+ **Data attributes:**
262
+ - `data-selected="true" | "false"` — Active state
263
+ - `data-index="number"` — Result position
264
+
265
+ ---
266
+
267
+ ### `<SearchResultThumbnail>`
268
+
269
+ Thumbnail image that auto-detects from `metadata.thumbnailUrl`.
270
+
271
+ | Prop | Type | Required | Default |
272
+ |-------------|----------|----------|-------------------------------|
273
+ | `src` | `string` | | `metadata.thumbnailUrl` |
274
+ | `alt` | `string` | | `result.title` |
275
+ | `size` | `number` | | `40` |
276
+ | `className` | `string` | | |
277
+
278
+ ---
279
+
280
+ ### `<SearchIcon>`
281
+
282
+ Built-in search magnifying glass SVG icon.
283
+
284
+ | Prop | Type | Required | Default |
285
+ |-------------|----------|----------|---------|
286
+ | `size` | `number` | | `18` |
287
+ | `className` | `string` | | |
288
+
289
+ ---
290
+
291
+ ### `<SearchKbd>`
292
+
293
+ Keyboard shortcut badge.
294
+
295
+ | Prop | Type | Required | Default |
296
+ |-------------|-------------------|----------|---------|
297
+ | `children` | `React.ReactNode` | | `⌘K` |
298
+ | `className` | `string` | | `etoile-kbd` |
299
+
300
+ ```tsx
301
+ <SearchKbd /> // Shows "⌘K"
302
+ <SearchKbd>/</SearchKbd> // Shows "/"
303
+ ```
304
+
305
+ ---
306
+
307
+ ### `useSearch(options)`
308
+
309
+ Headless hook for complete control.
310
+
311
+ **Options:**
312
+
313
+ | Field | Type | Required | Default |
314
+ |---------------|------------|----------|---------|
315
+ | `apiKey` | `string` | ✓ | |
316
+ | `collections` | `string[]` | ✓ | |
317
+ | `limit` | `number` | | `10` |
318
+ | `debounceMs` | `number` | | `100` |
319
+
320
+ **Returns:**
321
+
322
+ | Field | Type |
323
+ |--------------------|----------------------------|
324
+ | `query` | `string` |
325
+ | `setQuery` | `(q: string) => void` |
326
+ | `results` | `SearchResultData[]` |
327
+ | `isLoading` | `boolean` |
328
+ | `selectedIndex` | `number` |
329
+ | `setSelectedIndex` | `(i: number) => void` |
330
+ | `clear` | `() => void` |
331
+
332
+ ---
333
+
334
+ ## Types
335
+
336
+ ```ts
337
+ type SearchResultData = {
338
+ external_id: string;
339
+ title: string;
340
+ collection: string;
341
+ score: number;
342
+ content?: string;
343
+ metadata: Record<string, unknown>;
344
+ };
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Why @etoile-dev/react?
350
+
351
+ - **Radix / shadcn-style primitives** — Composable and unstyled
352
+ - **Accessibility built-in** — ARIA roles, keyboard navigation, focus management
353
+ - **Behavior, not appearance** — You own the design
354
+ - **TypeScript-first** — Full type safety
355
+ - **Zero dependencies** — Only React and @etoile-dev/client
356
+
357
+ ---
358
+
359
+ <p align="center">
360
+ <a href="https://etoile.dev/docs"><strong>Read the docs →</strong></a>
361
+ </p>
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ import type { SearchResultData } from "./types.js";
3
+ export type SearchProps = {
4
+ /** Your Étoile API key. Get one at https://etoile.dev */
5
+ apiKey: string;
6
+ /** Collections to search in (e.g., ["paintings", "artists"]) */
7
+ collections: string[];
8
+ /** Maximum number of results to return (default: 10) */
9
+ limit?: number;
10
+ /** Placeholder text for the search input */
11
+ placeholder?: string;
12
+ /** Additional CSS class name (e.g., "dark" for dark mode) */
13
+ className?: string;
14
+ /** Custom render function for each result (optional) */
15
+ renderResult?: (result: SearchResultData) => React.ReactNode;
16
+ };
17
+ /**
18
+ * All-in-one search component with sensible defaults.
19
+ *
20
+ * Provides a complete, polished search experience out of the box including
21
+ * search icon, keyboard shortcut badge, and result thumbnails. Just import
22
+ * `@etoile-dev/react/styles.css` for styling - no wrapper needed.
23
+ *
24
+ * @param props - Component props
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <Search apiKey="your-api-key" collections={["paintings"]} />
29
+ * ```
30
+ *
31
+ * @example Dark mode
32
+ * ```tsx
33
+ * <Search apiKey="your-api-key" collections={["paintings"]} className="dark" />
34
+ * ```
35
+ */
36
+ export declare const Search: ({ apiKey, collections, limit, placeholder, className, renderResult, }: SearchProps) => import("react/jsx-runtime").JSX.Element;
package/dist/Search.js ADDED
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SearchRoot } from "./components/SearchRoot.js";
3
+ import { SearchInput } from "./components/SearchInput.js";
4
+ import { SearchResults } from "./components/SearchResults.js";
5
+ import { SearchResult } from "./components/SearchResult.js";
6
+ import { SearchResultThumbnail } from "./components/SearchResultThumbnail.js";
7
+ import { SearchIcon } from "./components/SearchIcon.js";
8
+ import { SearchKbd } from "./components/SearchKbd.js";
9
+ const DefaultResult = (result) => (_jsxs(SearchResult, { children: [_jsx(SearchResultThumbnail, {}), _jsxs("div", { className: "etoile-result-content", children: [_jsx("span", { className: "etoile-result-title", children: result.title }), _jsx("span", { className: "etoile-result-subtitle", children: result.collection })] })] }));
10
+ /**
11
+ * All-in-one search component with sensible defaults.
12
+ *
13
+ * Provides a complete, polished search experience out of the box including
14
+ * search icon, keyboard shortcut badge, and result thumbnails. Just import
15
+ * `@etoile-dev/react/styles.css` for styling - no wrapper needed.
16
+ *
17
+ * @param props - Component props
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <Search apiKey="your-api-key" collections={["paintings"]} />
22
+ * ```
23
+ *
24
+ * @example Dark mode
25
+ * ```tsx
26
+ * <Search apiKey="your-api-key" collections={["paintings"]} className="dark" />
27
+ * ```
28
+ */
29
+ export const Search = ({ apiKey, collections, limit, placeholder = "Search...", className, renderResult, }) => {
30
+ return (_jsxs(SearchRoot, { apiKey: apiKey, collections: collections, limit: limit, className: className, children: [_jsxs("div", { className: "etoile-input-wrapper", children: [_jsx(SearchIcon, {}), _jsx(SearchInput, { placeholder: placeholder }), _jsx(SearchKbd, {})] }), _jsx(SearchResults, { children: renderResult ?? DefaultResult })] }));
31
+ };
@@ -0,0 +1,22 @@
1
+ export type SearchIconProps = {
2
+ /** Width and height in pixels (default: 18) */
3
+ size?: number;
4
+ /** CSS class name for styling */
5
+ className?: string;
6
+ };
7
+ /**
8
+ * Search magnifying glass icon.
9
+ *
10
+ * A minimal SVG icon that works perfectly with the default theme.
11
+ *
12
+ * @param props - Component props
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <div className="etoile-input-wrapper">
17
+ * <SearchIcon />
18
+ * <SearchInput placeholder="Search..." />
19
+ * </div>
20
+ * ```
21
+ */
22
+ export declare const SearchIcon: ({ size, className }: SearchIconProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Search magnifying glass icon.
4
+ *
5
+ * A minimal SVG icon that works perfectly with the default theme.
6
+ *
7
+ * @param props - Component props
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <div className="etoile-input-wrapper">
12
+ * <SearchIcon />
13
+ * <SearchInput placeholder="Search..." />
14
+ * </div>
15
+ * ```
16
+ */
17
+ export const SearchIcon = ({ size = 18, className }) => (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: className, "aria-hidden": "true", children: [_jsx("path", { d: "m21 21-4.34-4.34" }), _jsx("circle", { cx: "11", cy: "11", r: "8" })] }));
@@ -0,0 +1,28 @@
1
+ export type SearchInputProps = {
2
+ /** Placeholder text for the input field */
3
+ placeholder?: string;
4
+ /** CSS class name for styling the input */
5
+ className?: string;
6
+ };
7
+ /**
8
+ * Search input component with built-in keyboard navigation and accessibility.
9
+ *
10
+ * Integrates with SearchRoot context to provide debouncing and keyboard controls
11
+ * (ArrowUp, ArrowDown, Enter, Escape). Implements ARIA combobox pattern.
12
+ *
13
+ * @param props - Component props
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <SearchInput />
18
+ * ```
19
+ *
20
+ * @example With placeholder and styling
21
+ * ```tsx
22
+ * <SearchInput
23
+ * placeholder="Search paintings..."
24
+ * className="px-4 py-2 border rounded-lg"
25
+ * />
26
+ * ```
27
+ */
28
+ export declare const SearchInput: ({ placeholder, className }: SearchInputProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useSearchContext } from "../context/SearchContext.js";
3
+ /**
4
+ * Search input component with built-in keyboard navigation and accessibility.
5
+ *
6
+ * Integrates with SearchRoot context to provide debouncing and keyboard controls
7
+ * (ArrowUp, ArrowDown, Enter, Escape). Implements ARIA combobox pattern.
8
+ *
9
+ * @param props - Component props
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <SearchInput />
14
+ * ```
15
+ *
16
+ * @example With placeholder and styling
17
+ * ```tsx
18
+ * <SearchInput
19
+ * placeholder="Search paintings..."
20
+ * className="px-4 py-2 border rounded-lg"
21
+ * />
22
+ * ```
23
+ */
24
+ export const SearchInput = ({ placeholder, className }) => {
25
+ const { query, setQuery, results, selectedIndex, setSelectedIndex, listboxId, getResultId, handleKeyDown, autoFocus, } = useSearchContext();
26
+ const hasResults = results.length > 0;
27
+ const activeId = selectedIndex >= 0 && hasResults ? getResultId(selectedIndex) : undefined;
28
+ return (_jsx("input", { type: "text", placeholder: placeholder, className: className, value: query, autoFocus: autoFocus, role: "combobox", "aria-expanded": hasResults, "aria-controls": listboxId, "aria-activedescendant": activeId, onChange: (event) => {
29
+ const nextValue = event.target.value;
30
+ setQuery(nextValue);
31
+ if (nextValue.trim() !== "") {
32
+ setSelectedIndex(0);
33
+ }
34
+ }, onKeyDown: handleKeyDown }));
35
+ };
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ export type SearchKbdProps = {
3
+ /** Keyboard shortcut text (default: "⌘K") */
4
+ children?: React.ReactNode;
5
+ /** CSS class name for styling */
6
+ className?: string;
7
+ };
8
+ /**
9
+ * Keyboard shortcut badge for search.
10
+ *
11
+ * Displays a styled keyboard shortcut indicator. Works with the default theme.
12
+ *
13
+ * @param props - Component props
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <div className="etoile-input-wrapper">
18
+ * <SearchIcon />
19
+ * <SearchInput placeholder="Search..." />
20
+ * <SearchKbd />
21
+ * </div>
22
+ * ```
23
+ *
24
+ * @example Custom shortcut
25
+ * ```tsx
26
+ * <SearchKbd>/</SearchKbd>
27
+ * <SearchKbd>Ctrl K</SearchKbd>
28
+ * ```
29
+ */
30
+ export declare const SearchKbd: ({ children, className, }: SearchKbdProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Keyboard shortcut badge for search.
4
+ *
5
+ * Displays a styled keyboard shortcut indicator. Works with the default theme.
6
+ *
7
+ * @param props - Component props
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <div className="etoile-input-wrapper">
12
+ * <SearchIcon />
13
+ * <SearchInput placeholder="Search..." />
14
+ * <SearchKbd />
15
+ * </div>
16
+ * ```
17
+ *
18
+ * @example Custom shortcut
19
+ * ```tsx
20
+ * <SearchKbd>/</SearchKbd>
21
+ * <SearchKbd>Ctrl K</SearchKbd>
22
+ * ```
23
+ */
24
+ export const SearchKbd = ({ children = "⌘K", className, }) => (_jsx("kbd", { className: className ? `etoile-kbd ${className}` : "etoile-kbd", children: children }));
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ export type SearchResultProps = {
3
+ /** CSS class name for styling the result item */
4
+ className?: string;
5
+ /** Content to render inside the result */
6
+ children: React.ReactNode;
7
+ };
8
+ /**
9
+ * Individual search result item with selection state and keyboard navigation.
10
+ *
11
+ * Manages selection state and accessibility attributes. Provides `data-selected`
12
+ * attribute for styling the active result. Must be used inside SearchResults.
13
+ *
14
+ * @param props - Component props
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <SearchResult>{result.title}</SearchResult>
19
+ * ```
20
+ *
21
+ * @example With selection styling
22
+ * ```tsx
23
+ * <SearchResult className="result-item">
24
+ * <h3>{result.title}</h3>
25
+ * </SearchResult>
26
+ *
27
+ * // CSS: .result-item[data-selected="true"] { background: #f0f9ff; }
28
+ * ```
29
+ */
30
+ export declare const SearchResult: ({ className, children }: SearchResultProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { useSearchContext } from "../context/SearchContext.js";
4
+ import { SearchResultIndexContext } from "./SearchResults.js";
5
+ /**
6
+ * Individual search result item with selection state and keyboard navigation.
7
+ *
8
+ * Manages selection state and accessibility attributes. Provides `data-selected`
9
+ * attribute for styling the active result. Must be used inside SearchResults.
10
+ *
11
+ * @param props - Component props
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <SearchResult>{result.title}</SearchResult>
16
+ * ```
17
+ *
18
+ * @example With selection styling
19
+ * ```tsx
20
+ * <SearchResult className="result-item">
21
+ * <h3>{result.title}</h3>
22
+ * </SearchResult>
23
+ *
24
+ * // CSS: .result-item[data-selected="true"] { background: #f0f9ff; }
25
+ * ```
26
+ */
27
+ export const SearchResult = ({ className, children }) => {
28
+ const { selectedIndex, registerResult, getResultId } = useSearchContext();
29
+ const index = React.useContext(SearchResultIndexContext);
30
+ if (index === null) {
31
+ return null;
32
+ }
33
+ const isSelected = index === selectedIndex;
34
+ const id = React.useMemo(() => getResultId(index), [getResultId, index]);
35
+ const setRef = React.useCallback((node) => {
36
+ registerResult(index, node);
37
+ }, [index, registerResult]);
38
+ return (_jsx("div", { ref: setRef, id: id, role: "option", "aria-selected": isSelected, "data-selected": isSelected ? "true" : "false", "data-index": index, tabIndex: isSelected ? 0 : -1, className: className, children: children }));
39
+ };