@etoile-dev/react 0.2.2 → 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.
- package/README.md +341 -205
- package/dist/Searchbar.d.ts +315 -0
- package/dist/Searchbar.js +207 -0
- package/dist/context.d.ts +57 -0
- package/dist/context.js +32 -0
- package/dist/hooks/useEtoileSearch.d.ts +122 -0
- package/dist/hooks/useEtoileSearch.js +138 -0
- package/dist/index.d.ts +44 -19
- package/dist/index.js +37 -12
- package/dist/primitives/Content.d.ts +34 -0
- package/dist/primitives/Content.js +108 -0
- package/dist/primitives/Empty.d.ts +25 -0
- package/dist/primitives/Empty.js +25 -0
- package/dist/primitives/Error.d.ts +29 -0
- package/dist/primitives/Error.js +26 -0
- package/dist/primitives/Group.d.ts +30 -0
- package/dist/primitives/Group.js +22 -0
- package/dist/primitives/Icon.d.ts +21 -0
- package/dist/primitives/Icon.js +14 -0
- package/dist/primitives/Input.d.ts +32 -0
- package/dist/primitives/Input.js +70 -0
- package/dist/primitives/Item.d.ts +61 -0
- package/dist/primitives/Item.js +76 -0
- package/dist/primitives/Kbd.d.ts +20 -0
- package/dist/primitives/Kbd.js +13 -0
- package/dist/primitives/List.d.ts +35 -0
- package/dist/primitives/List.js +37 -0
- package/dist/primitives/Loading.d.ts +25 -0
- package/dist/primitives/Loading.js +26 -0
- package/dist/primitives/Modal.d.ts +39 -0
- package/dist/primitives/Modal.js +37 -0
- package/dist/primitives/ModalInput.d.ts +61 -0
- package/dist/primitives/ModalInput.js +33 -0
- package/dist/primitives/Overlay.d.ts +21 -0
- package/dist/primitives/Overlay.js +41 -0
- package/dist/primitives/Portal.d.ts +28 -0
- package/dist/primitives/Portal.js +30 -0
- package/dist/primitives/Root.d.ts +116 -0
- package/dist/primitives/Root.js +413 -0
- package/dist/primitives/Separator.d.ts +19 -0
- package/dist/primitives/Separator.js +18 -0
- package/dist/primitives/Thumbnail.d.ts +31 -0
- package/dist/primitives/Thumbnail.js +59 -0
- package/dist/primitives/Trigger.d.ts +28 -0
- package/dist/primitives/Trigger.js +35 -0
- package/dist/store.d.ts +38 -0
- package/dist/store.js +63 -0
- package/dist/styles.css +480 -133
- package/dist/types.d.ts +3 -31
- package/dist/utils/composeRefs.d.ts +12 -0
- package/dist/utils/composeRefs.js +27 -0
- package/dist/utils/slot.d.ts +22 -0
- package/dist/utils/slot.js +58 -0
- package/package.json +8 -4
- package/dist/Search.d.ts +0 -37
- package/dist/Search.js +0 -31
- package/dist/components/SearchIcon.d.ts +0 -22
- package/dist/components/SearchIcon.js +0 -17
- package/dist/components/SearchInput.d.ts +0 -30
- package/dist/components/SearchInput.js +0 -59
- package/dist/components/SearchKbd.d.ts +0 -30
- package/dist/components/SearchKbd.js +0 -24
- package/dist/components/SearchResult.d.ts +0 -31
- package/dist/components/SearchResult.js +0 -40
- package/dist/components/SearchResultThumbnail.d.ts +0 -38
- package/dist/components/SearchResultThumbnail.js +0 -38
- package/dist/components/SearchResults.d.ts +0 -39
- package/dist/components/SearchResults.js +0 -53
- package/dist/components/SearchRoot.d.ts +0 -44
- package/dist/components/SearchRoot.js +0 -132
- package/dist/context/SearchContext.d.ts +0 -55
- package/dist/context/SearchContext.js +0 -36
- package/dist/hooks/useSearch.d.ts +0 -56
- package/dist/hooks/useSearch.js +0 -116
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<a href="https://etoile.dev">
|
|
3
|
-
<img src="https://etoile.dev/assets/logo-black.svg" alt="
|
|
3
|
+
<img src="https://etoile.dev/assets/logo-black.svg" alt="Etoile" height="32" />
|
|
4
4
|
</a>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<h1 align="center">@etoile-dev/react</h1>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
<strong>Headless React primitives for search.</strong>
|
|
14
|
+
<strong>Headless React primitives for search — with Etoile-powered components and hooks.</strong>
|
|
15
15
|
<br />
|
|
16
16
|
Composable. Accessible. Zero styling.
|
|
17
17
|
</p>
|
|
@@ -24,19 +24,23 @@
|
|
|
24
24
|
|
|
25
25
|
## About
|
|
26
26
|
|
|
27
|
-
**@etoile-dev/react**
|
|
27
|
+
**@etoile-dev/react** is the React SDK for **Etoile**, and also a set of unstyled primitives you can wire to any data source.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
You get three layers:
|
|
30
|
+
|
|
31
|
+
- **Ready-to-use Etoile components** — `<Searchbar />` and `<SearchModal />`
|
|
32
|
+
- **Etoile data hooks** — `useEtoileSearch` (and `useSearch` alias)
|
|
33
|
+
- **Headless primitives** — `Searchbar.Root`, `Searchbar.Input`, `Searchbar.List`, `Searchbar.Item`, …
|
|
30
34
|
|
|
31
35
|
---
|
|
32
36
|
|
|
33
37
|
## Philosophy
|
|
34
38
|
|
|
35
39
|
- **Headless-first** — You control the appearance
|
|
36
|
-
- **Composable** — Build
|
|
37
|
-
- **Accessible** — Full ARIA
|
|
38
|
-
- **No magic** —
|
|
39
|
-
- **
|
|
40
|
+
- **Composable** — Build any search UX from small primitives
|
|
41
|
+
- **Accessible** — Full ARIA combobox / listbox pattern, keyboard navigation
|
|
42
|
+
- **No magic** — Predictable behavior, clear contracts
|
|
43
|
+
- **Zero style opinions** — Bring your own CSS (or use our optional theme)
|
|
40
44
|
|
|
41
45
|
---
|
|
42
46
|
|
|
@@ -50,329 +54,461 @@ npm i @etoile-dev/react
|
|
|
50
54
|
|
|
51
55
|
## Quickstart
|
|
52
56
|
|
|
57
|
+
The simplest possible usage — just an API key and a collection:
|
|
58
|
+
|
|
53
59
|
```tsx
|
|
54
|
-
import
|
|
60
|
+
import "@etoile-dev/react/styles.css";
|
|
61
|
+
import { Searchbar } from "@etoile-dev/react";
|
|
55
62
|
|
|
56
63
|
export default function App() {
|
|
57
|
-
return <
|
|
64
|
+
return <Searchbar apiKey="your-api-key" collections={["paintings"]} />;
|
|
58
65
|
}
|
|
59
66
|
```
|
|
60
67
|
|
|
68
|
+
Search multiple collections and handle selection:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<Searchbar
|
|
72
|
+
apiKey={process.env.ETOILE_API_KEY!}
|
|
73
|
+
collections={["paintings", "artists"]}
|
|
74
|
+
limit={10}
|
|
75
|
+
onSelect={(id) => router.push(`/work/${id}`)}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
61
79
|
---
|
|
62
80
|
|
|
63
|
-
##
|
|
81
|
+
## Headless primitives
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
Use `Searchbar.Root` and friends for full control with no Etoile dependency:
|
|
66
84
|
|
|
67
85
|
```tsx
|
|
68
|
-
import {
|
|
69
|
-
SearchRoot,
|
|
70
|
-
SearchInput,
|
|
71
|
-
SearchResults,
|
|
72
|
-
SearchResult,
|
|
73
|
-
} from "@etoile-dev/react";
|
|
86
|
+
import { Searchbar } from "@etoile-dev/react";
|
|
74
87
|
|
|
75
|
-
export default function
|
|
88
|
+
export default function LocalSearch() {
|
|
76
89
|
return (
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<small>Score: {result.score.toFixed(2)}</small>
|
|
90
|
-
</SearchResult>
|
|
91
|
-
)}
|
|
92
|
-
</SearchResults>
|
|
93
|
-
</SearchRoot>
|
|
90
|
+
<Searchbar.Root onSelect={(id) => router.push(`/paintings/${id}`)}>
|
|
91
|
+
<Searchbar.Input placeholder="Search paintings…" />
|
|
92
|
+
<Searchbar.List>
|
|
93
|
+
{paintings.map((p) => (
|
|
94
|
+
<Searchbar.Item key={p.id} value={p.id} label={p.title}>
|
|
95
|
+
{p.title}
|
|
96
|
+
</Searchbar.Item>
|
|
97
|
+
))}
|
|
98
|
+
<Searchbar.Empty>No results.</Searchbar.Empty>
|
|
99
|
+
<Searchbar.Loading />
|
|
100
|
+
</Searchbar.List>
|
|
101
|
+
</Searchbar.Root>
|
|
94
102
|
);
|
|
95
103
|
}
|
|
96
104
|
```
|
|
97
105
|
|
|
98
|
-
|
|
106
|
+
Primitives are unstyled by default. To opt into the built-in theme while using
|
|
107
|
+
primitives, add `className="etoile-search"` on `Searchbar.Root` and import
|
|
108
|
+
`@etoile-dev/react/styles.css`.
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
---
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
## Custom rendering
|
|
103
113
|
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.result
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
+
```tsx
|
|
115
|
+
<Searchbar
|
|
116
|
+
apiKey={process.env.ETOILE_API_KEY!}
|
|
117
|
+
collections={["paintings", "artists"]}
|
|
118
|
+
onSelect={(id) => router.push(`/work/${id}`)}
|
|
119
|
+
renderItem={(result) => (
|
|
120
|
+
<Searchbar.Item value={result.external_id} label={result.title}>
|
|
121
|
+
<Searchbar.Thumbnail />
|
|
122
|
+
<div>
|
|
123
|
+
<strong>{result.title}</strong>
|
|
124
|
+
<span>{String(result.metadata?.artist ?? "")}</span>
|
|
125
|
+
</div>
|
|
126
|
+
</Searchbar.Item>
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
114
129
|
```
|
|
115
130
|
|
|
116
131
|
---
|
|
117
132
|
|
|
118
|
-
##
|
|
133
|
+
## Command palette / modal mode
|
|
119
134
|
|
|
120
|
-
|
|
135
|
+
Use the `<SearchModal />` convenience component for an Etoile-powered palette:
|
|
121
136
|
|
|
122
137
|
```tsx
|
|
123
138
|
import "@etoile-dev/react/styles.css";
|
|
124
|
-
import {
|
|
139
|
+
import { SearchModal } from "@etoile-dev/react";
|
|
125
140
|
|
|
126
|
-
<
|
|
141
|
+
<SearchModal apiKey="your-api-key" collections={["paintings"]} />;
|
|
127
142
|
```
|
|
128
143
|
|
|
129
|
-
|
|
144
|
+
Or compose the primitives yourself for full control:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<Searchbar.Root className="etoile-search">
|
|
148
|
+
<Searchbar.Trigger>
|
|
149
|
+
<Searchbar.Icon /> Search
|
|
150
|
+
</Searchbar.Trigger>
|
|
151
|
+
|
|
152
|
+
<Searchbar.Portal>
|
|
153
|
+
<Searchbar.Overlay className="overlay" />
|
|
154
|
+
<Searchbar.Content aria-label="Search paintings" className="palette">
|
|
155
|
+
<Searchbar.Input autoFocus placeholder="Search…" />
|
|
156
|
+
<Searchbar.List>
|
|
157
|
+
{results.map((r) => (
|
|
158
|
+
<Searchbar.Item key={r.id} value={r.id}>
|
|
159
|
+
{r.title}
|
|
160
|
+
</Searchbar.Item>
|
|
161
|
+
))}
|
|
162
|
+
<Searchbar.Empty>No results.</Searchbar.Empty>
|
|
163
|
+
</Searchbar.List>
|
|
164
|
+
</Searchbar.Content>
|
|
165
|
+
</Searchbar.Portal>
|
|
166
|
+
</Searchbar.Root>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Styling
|
|
172
|
+
|
|
173
|
+
### Data attributes
|
|
174
|
+
|
|
175
|
+
All primitives emit `data-*` attributes — no class coupling required:
|
|
130
176
|
|
|
131
|
-
|
|
177
|
+
```css
|
|
178
|
+
[role="option"][data-selected="true"] {
|
|
179
|
+
background: #f0f9ff;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
[role="option"][data-disabled="true"] {
|
|
183
|
+
opacity: 0.4;
|
|
184
|
+
cursor: not-allowed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
[data-state="open"] {
|
|
188
|
+
border-color: #3b82f6;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
132
191
|
|
|
133
|
-
|
|
192
|
+
### Dark mode
|
|
134
193
|
|
|
135
194
|
```tsx
|
|
136
|
-
<
|
|
195
|
+
<Searchbar className="dark" apiKey="your-api-key" collections={["paintings"]} />
|
|
196
|
+
```
|
|
137
197
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
198
|
+
Or wrap a parent element:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<div className="dark">
|
|
202
|
+
<Searchbar apiKey="your-api-key" collections={["paintings"]} />
|
|
203
|
+
</div>
|
|
142
204
|
```
|
|
143
205
|
|
|
144
|
-
### CSS
|
|
206
|
+
### CSS variables
|
|
145
207
|
|
|
146
|
-
Every value is customizable
|
|
208
|
+
Every value is customizable:
|
|
147
209
|
|
|
148
210
|
```css
|
|
149
211
|
.etoile-search {
|
|
150
|
-
/* Colors */
|
|
151
212
|
--etoile-bg: #ffffff;
|
|
152
213
|
--etoile-border: #e4e4e7;
|
|
153
214
|
--etoile-text: #09090b;
|
|
154
215
|
--etoile-text-muted: #71717a;
|
|
155
216
|
--etoile-selected: #f4f4f5;
|
|
156
|
-
--etoile-ring: #18181b;
|
|
157
|
-
|
|
158
|
-
/* Sizing */
|
|
159
217
|
--etoile-radius: 12px;
|
|
160
218
|
--etoile-input-height: 44px;
|
|
161
|
-
|
|
162
|
-
|
|
219
|
+
}
|
|
220
|
+
```
|
|
163
221
|
|
|
164
|
-
|
|
165
|
-
--etoile-input-padding-x: 16px;
|
|
166
|
-
--etoile-result-gap: 16px;
|
|
167
|
-
--etoile-results-offset: 8px;
|
|
222
|
+
See `styles.css` for all 40+ variables.
|
|
168
223
|
|
|
169
|
-
|
|
170
|
-
--etoile-font-size-input: 15px;
|
|
171
|
-
--etoile-font-size-title: 14px;
|
|
224
|
+
---
|
|
172
225
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
## API
|
|
227
|
+
|
|
228
|
+
### `<Searchbar />`
|
|
229
|
+
|
|
230
|
+
Ready-to-use search component powered by Etoile.
|
|
231
|
+
|
|
232
|
+
| Prop | Type | Default |
|
|
233
|
+
| ------------- | ------------------------------------- | ----------------- |
|
|
234
|
+
| `apiKey` | `string` | **required** |
|
|
235
|
+
| `collections` | `string[]` | **required** |
|
|
236
|
+
| `limit` | `number` | `10` |
|
|
237
|
+
| `offset` | `number` | `0` (API default) |
|
|
238
|
+
| `debounceMs` | `number` | `100` |
|
|
239
|
+
| `placeholder` | `string` | `"Search…"` |
|
|
240
|
+
| `filters` | `SearchFilter[]` | |
|
|
241
|
+
| `autoFilters` | `boolean` | |
|
|
242
|
+
| `renderItem` | `(result: SearchResult) => ReactNode` | |
|
|
243
|
+
| `onSelect` | `(value: string) => void` | |
|
|
244
|
+
| `hotkey` | `string` | |
|
|
245
|
+
| `className` | `string` | `"etoile-search"` |
|
|
177
246
|
|
|
178
|
-
|
|
247
|
+
Also supports non-state DOM/behavior props from `Searchbar.Root`.
|
|
179
248
|
|
|
180
249
|
---
|
|
181
250
|
|
|
182
|
-
|
|
251
|
+
### `<SearchModal />`
|
|
252
|
+
|
|
253
|
+
Ready-to-use command palette powered by Etoile.
|
|
254
|
+
|
|
255
|
+
| Prop | Type | Default |
|
|
256
|
+
| ------------- | ------------------------------------- | ----------------- |
|
|
257
|
+
| `apiKey` | `string` | **required** |
|
|
258
|
+
| `collections` | `string[]` | **required** |
|
|
259
|
+
| `limit` | `number` | `10` |
|
|
260
|
+
| `offset` | `number` | `0` (API default) |
|
|
261
|
+
| `debounceMs` | `number` | `100` |
|
|
262
|
+
| `placeholder` | `string` | `"Search…"` |
|
|
263
|
+
| `filters` | `SearchFilter[]` | |
|
|
264
|
+
| `autoFilters` | `boolean` | |
|
|
265
|
+
| `hotkey` | `string` | `"mod+k"` |
|
|
266
|
+
| `modalLabel` | `string` | `"Search"` |
|
|
267
|
+
| `renderItem` | `(result: SearchResult) => ReactNode` | |
|
|
268
|
+
| `onSelect` | `(value: string) => void` | |
|
|
269
|
+
| `className` | `string` | `"etoile-search"` |
|
|
183
270
|
|
|
184
|
-
|
|
271
|
+
---
|
|
185
272
|
|
|
186
|
-
|
|
187
|
-
|
|
273
|
+
### `<Searchbar.Root />`
|
|
274
|
+
|
|
275
|
+
Context provider and state machine for headless usage.
|
|
276
|
+
|
|
277
|
+
| Prop | Type | Default |
|
|
278
|
+
| ---------------- | --------------------------------- | ---------- |
|
|
279
|
+
| `open` | `boolean` | |
|
|
280
|
+
| `defaultOpen` | `boolean` | `false` |
|
|
281
|
+
| `onOpenChange` | `(open: boolean) => void` | |
|
|
282
|
+
| `search` | `string` | |
|
|
283
|
+
| `defaultSearch` | `string` | `""` |
|
|
284
|
+
| `onSearchChange` | `(search: string) => void` | |
|
|
285
|
+
| `value` | `string \| null` | |
|
|
286
|
+
| `defaultValue` | `string \| null` | `null` |
|
|
287
|
+
| `onValueChange` | `(value: string \| null) => void` | |
|
|
288
|
+
| `isLoading` | `boolean` | `false` |
|
|
289
|
+
| `error` | `unknown` | |
|
|
290
|
+
| `hotkey` | `string` | |
|
|
291
|
+
| `hotkeyBehavior` | `"focus" \| "toggle"` | `"toggle"` |
|
|
292
|
+
| `onSelect` | `(value: string) => void` | |
|
|
293
|
+
| `className` | `string` | |
|
|
294
|
+
| `asChild` | `boolean` | `false` |
|
|
188
295
|
|
|
189
|
-
|
|
190
|
-
const { query, setQuery, results, isLoading } = useSearch({
|
|
191
|
-
apiKey: "your-api-key",
|
|
192
|
-
collections: ["paintings"],
|
|
193
|
-
});
|
|
296
|
+
**Keyboard shortcuts:**
|
|
194
297
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
value={query}
|
|
199
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
200
|
-
placeholder="Search paintings..."
|
|
201
|
-
/>
|
|
202
|
-
{isLoading && <p>Loading...</p>}
|
|
203
|
-
<ul>
|
|
204
|
-
{results.map((result) => (
|
|
205
|
-
<li key={result.external_id}>{result.title}</li>
|
|
206
|
-
))}
|
|
207
|
-
</ul>
|
|
208
|
-
</div>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
```
|
|
298
|
+
- `↑` / `↓` — Navigate items
|
|
299
|
+
- `Enter` — Select active item
|
|
300
|
+
- `Escape` — Close list
|
|
212
301
|
|
|
213
302
|
---
|
|
214
303
|
|
|
215
|
-
|
|
304
|
+
### `<Searchbar.Modal />`
|
|
305
|
+
|
|
306
|
+
Headless modal primitive (`Root + Portal + Overlay + Content`).
|
|
216
307
|
|
|
217
|
-
|
|
308
|
+
| Prop | Type | Default |
|
|
309
|
+
| ------------ | -------- | ---------- |
|
|
310
|
+
| `aria-label` | `string` | `"Search"` |
|
|
311
|
+
|
|
312
|
+
Also accepts `Searchbar.Root` props.
|
|
313
|
+
|
|
314
|
+
---
|
|
218
315
|
|
|
219
|
-
|
|
316
|
+
### `<Searchbar.Input />`
|
|
220
317
|
|
|
221
|
-
| Prop
|
|
222
|
-
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `limit` | `number` | | `10` |
|
|
226
|
-
| `renderResult` | `(result: SearchResultData) => React.ReactNode` | | |
|
|
318
|
+
| Prop | Type | Default |
|
|
319
|
+
| ------------- | --------- | ------- |
|
|
320
|
+
| `placeholder` | `string` | |
|
|
321
|
+
| `asChild` | `boolean` | `false` |
|
|
227
322
|
|
|
228
323
|
---
|
|
229
324
|
|
|
230
|
-
### `<
|
|
325
|
+
### `<Searchbar.ModalInput />`
|
|
231
326
|
|
|
232
|
-
|
|
327
|
+
Pre-composed modal input row.
|
|
233
328
|
|
|
234
|
-
| Prop | Type
|
|
235
|
-
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `debounceMs` | `number` | | `100` |
|
|
240
|
-
| `autoFocus` | `boolean` | | `false` |
|
|
241
|
-
| `children` | `React.ReactNode` | ✓ | |
|
|
329
|
+
| Prop | Type | Default |
|
|
330
|
+
| ------------- | ------------------- | ----------- |
|
|
331
|
+
| `placeholder` | `string` | `"Search…"` |
|
|
332
|
+
| `icon` | `ReactNode \| null` | `<Icon />` |
|
|
333
|
+
| `kbd` | `ReactNode \| null` | `"Esc"` |
|
|
242
334
|
|
|
243
335
|
---
|
|
244
336
|
|
|
245
|
-
### `<
|
|
337
|
+
### `<Searchbar.List />`
|
|
246
338
|
|
|
247
|
-
|
|
339
|
+
Container with `role="listbox"`. Renders when open. In modal mode, it hides when
|
|
340
|
+
query is empty.
|
|
248
341
|
|
|
249
|
-
| Prop
|
|
250
|
-
|
|
251
|
-
| `
|
|
252
|
-
| `className` | `string` |
|
|
342
|
+
| Prop | Type | Default |
|
|
343
|
+
| --------- | --------- | ------- |
|
|
344
|
+
| `asChild` | `boolean` | `false` |
|
|
253
345
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
### `<Searchbar.Item />`
|
|
349
|
+
|
|
350
|
+
| Prop | Type | Default |
|
|
351
|
+
| ---------- | ------------------------- | -------- |
|
|
352
|
+
| `value` | `string` | required |
|
|
353
|
+
| `label` | `string` | `value` |
|
|
354
|
+
| `disabled` | `boolean` | `false` |
|
|
355
|
+
| `onSelect` | `(value: string) => void` | |
|
|
356
|
+
| `asChild` | `boolean` | `false` |
|
|
357
|
+
|
|
358
|
+
**Data attributes:** `data-selected`, `data-disabled`, `data-value`
|
|
258
359
|
|
|
259
360
|
---
|
|
260
361
|
|
|
261
|
-
### `<
|
|
362
|
+
### `<Searchbar.Group />`
|
|
363
|
+
|
|
364
|
+
| Prop | Type |
|
|
365
|
+
| ------- | -------- |
|
|
366
|
+
| `label` | `string` |
|
|
262
367
|
|
|
263
|
-
|
|
368
|
+
---
|
|
264
369
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
| `children` | `(result: SearchResultData) => React.ReactNode` | ✓ |
|
|
370
|
+
### `<Searchbar.Separator />`
|
|
371
|
+
|
|
372
|
+
Visual separator (`role="separator"`).
|
|
269
373
|
|
|
270
374
|
---
|
|
271
375
|
|
|
272
|
-
### `<
|
|
376
|
+
### `<Searchbar.Empty />`
|
|
377
|
+
|
|
378
|
+
Renders when: list is open, query is non-empty, no items match, and not loading.
|
|
273
379
|
|
|
274
|
-
|
|
380
|
+
---
|
|
275
381
|
|
|
276
|
-
|
|
277
|
-
|-------------|-------------------|----------|
|
|
278
|
-
| `className` | `string` | |
|
|
279
|
-
| `children` | `React.ReactNode` | ✓ |
|
|
382
|
+
### `<Searchbar.Loading />`
|
|
280
383
|
|
|
281
|
-
|
|
282
|
-
- `data-selected="true" | "false"` — Active state
|
|
283
|
-
- `data-index="number"` — Result position
|
|
384
|
+
Renders when `isLoading={true}` is passed to `Searchbar.Root`.
|
|
284
385
|
|
|
285
386
|
---
|
|
286
387
|
|
|
287
|
-
### `<
|
|
388
|
+
### `<Searchbar.Error />`
|
|
288
389
|
|
|
289
|
-
|
|
390
|
+
Renders when `error` is set on `Searchbar.Root`. Accepts a render function:
|
|
290
391
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
| `alt` | `string` | | `result.title` |
|
|
295
|
-
| `size` | `number` | | `40` |
|
|
296
|
-
| `className` | `string` | | |
|
|
392
|
+
```tsx
|
|
393
|
+
<Searchbar.Error>{(err) => `Search failed: ${String(err)}`}</Searchbar.Error>
|
|
394
|
+
```
|
|
297
395
|
|
|
298
396
|
---
|
|
299
397
|
|
|
300
|
-
### `<
|
|
398
|
+
### `<Searchbar.Portal />`
|
|
399
|
+
|
|
400
|
+
Portals children to `document.body` (or a custom `container`).
|
|
301
401
|
|
|
302
|
-
|
|
402
|
+
---
|
|
303
403
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
| `className` | `string` | | |
|
|
404
|
+
### `<Searchbar.Overlay />`
|
|
405
|
+
|
|
406
|
+
Backdrop for modal/palette mode. Renders when open.
|
|
308
407
|
|
|
309
408
|
---
|
|
310
409
|
|
|
311
|
-
### `<
|
|
410
|
+
### `<Searchbar.Content />`
|
|
312
411
|
|
|
313
|
-
|
|
412
|
+
Dialog panel for modal/palette mode. Focuses first focusable child on open.
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
### `<Searchbar.Trigger />`
|
|
314
417
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
418
|
+
Button that toggles open state.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
### `<Searchbar.Icon />`
|
|
423
|
+
|
|
424
|
+
Search magnifying glass SVG icon.
|
|
425
|
+
|
|
426
|
+
| Prop | Type | Default |
|
|
427
|
+
| ------ | -------- | ------- |
|
|
428
|
+
| `size` | `number` | `18` |
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
### `<Searchbar.Kbd />`
|
|
433
|
+
|
|
434
|
+
Keyboard shortcut badge.
|
|
319
435
|
|
|
320
436
|
```tsx
|
|
321
|
-
<
|
|
322
|
-
<
|
|
437
|
+
<Searchbar.Kbd /> // "⌘K"
|
|
438
|
+
<Searchbar.Kbd>/</Searchbar.Kbd>
|
|
323
439
|
```
|
|
324
440
|
|
|
325
441
|
---
|
|
326
442
|
|
|
327
|
-
###
|
|
443
|
+
### `<Searchbar.Thumbnail />`
|
|
444
|
+
|
|
445
|
+
Thumbnail image. Auto-reads `metadata.thumbnailUrl` from item context when
|
|
446
|
+
used inside the Etoile `<Searchbar />` wrapper.
|
|
447
|
+
|
|
448
|
+
| Prop | Type | Default |
|
|
449
|
+
| ------ | -------- | ------------ |
|
|
450
|
+
| `src` | `string` | from context |
|
|
451
|
+
| `alt` | `string` | item title |
|
|
452
|
+
| `size` | `number` | `40` |
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
### `useSearchbarContext()`
|
|
328
457
|
|
|
329
|
-
|
|
458
|
+
Access store helpers from any component inside `Searchbar.Root`.
|
|
330
459
|
|
|
331
|
-
|
|
460
|
+
```tsx
|
|
461
|
+
import { useSearchbarContext, useSearchbarStore } from "@etoile-dev/react";
|
|
332
462
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
463
|
+
function QueryDisplay() {
|
|
464
|
+
const { store } = useSearchbarContext();
|
|
465
|
+
const query = useSearchbarStore(store, (s) => s.query);
|
|
466
|
+
return <span>{query}</span>;
|
|
467
|
+
}
|
|
468
|
+
```
|
|
339
469
|
|
|
340
|
-
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
### `useEtoileSearch(options)`
|
|
341
473
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
474
|
+
Headless data hook for live Etoile search.
|
|
475
|
+
|
|
476
|
+
```tsx
|
|
477
|
+
import { useEtoileSearch } from "@etoile-dev/react";
|
|
478
|
+
|
|
479
|
+
const [query, setQuery] = useState("");
|
|
480
|
+
const { results, isLoading } = useEtoileSearch({
|
|
481
|
+
apiKey: process.env.ETOILE_API_KEY!,
|
|
482
|
+
collections: ["paintings"],
|
|
483
|
+
query,
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Returns `results`, `isLoading`, `error`, `appliedFilters`, and `refinedQuery`.
|
|
351
488
|
|
|
352
489
|
---
|
|
353
490
|
|
|
354
491
|
## Types
|
|
355
492
|
|
|
356
493
|
```ts
|
|
357
|
-
type
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
content?: string;
|
|
363
|
-
metadata: Record<string, unknown>;
|
|
364
|
-
};
|
|
494
|
+
import type {
|
|
495
|
+
SearchResult,
|
|
496
|
+
SearchFilter,
|
|
497
|
+
FilterOperator,
|
|
498
|
+
} from "@etoile-dev/react";
|
|
365
499
|
```
|
|
366
500
|
|
|
501
|
+
`SearchResultData` remains exported as a deprecated alias.
|
|
502
|
+
|
|
367
503
|
---
|
|
368
504
|
|
|
369
505
|
## Why @etoile-dev/react?
|
|
370
506
|
|
|
371
|
-
- **
|
|
372
|
-
- **
|
|
373
|
-
- **
|
|
374
|
-
- **
|
|
375
|
-
- **
|
|
507
|
+
- **Fast by default** — only the components that depend on the query re-render, not the whole tree
|
|
508
|
+
- **Stable selection** — items are identified by value, not by index
|
|
509
|
+
- **Controlled or uncontrolled** — every stateful prop supports both patterns
|
|
510
|
+
- **Composable** — render any element as any primitive with `asChild`
|
|
511
|
+
- **No opinions** — primitives emit `data-*` attributes and inject no theme classes
|
|
376
512
|
|
|
377
513
|
---
|
|
378
514
|
|