@etoile-dev/react 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/dist/Search.d.ts +36 -0
- package/dist/Search.js +31 -0
- package/dist/components/SearchIcon.d.ts +22 -0
- package/dist/components/SearchIcon.js +17 -0
- package/dist/components/SearchInput.d.ts +28 -0
- package/dist/components/SearchInput.js +35 -0
- package/dist/components/SearchKbd.d.ts +30 -0
- package/dist/components/SearchKbd.js +24 -0
- package/dist/components/SearchResult.d.ts +30 -0
- package/dist/components/SearchResult.js +39 -0
- package/dist/components/SearchResultThumbnail.d.ts +36 -0
- package/dist/components/SearchResultThumbnail.js +37 -0
- package/dist/components/SearchResults.d.ts +38 -0
- package/dist/components/SearchResults.js +52 -0
- package/dist/components/SearchRoot.d.ts +43 -0
- package/dist/components/SearchRoot.js +84 -0
- package/dist/context/SearchContext.d.ts +51 -0
- package/dist/context/SearchContext.js +36 -0
- package/dist/hooks/useSearch.d.ts +55 -0
- package/dist/hooks/useSearch.js +116 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +12 -0
- package/dist/styles.css +301 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/package.json +58 -0
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>
|
package/dist/Search.d.ts
ADDED
|
@@ -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
|
+
};
|