@headband/react 0.1.1 → 0.2.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 +307 -160
- package/dist/index.d.mts +271 -5
- package/dist/index.d.ts +271 -5
- package/dist/index.js +485 -132
- package/dist/index.mjs +469 -139
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @headband/react
|
|
2
2
|
|
|
3
|
-
React hooks and components for [Headband](https://headband.dev) search. Drop-in building blocks for building fast, faceted search UIs -- designed as an InstantSearch-
|
|
3
|
+
React hooks and components for [Headband](https://headband.dev) search. Drop-in building blocks for building fast, faceted search UIs -- designed as an InstantSearch-inspired alternative that works with Headband's unified search API.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,6 +10,8 @@ npm install @headband/react
|
|
|
10
10
|
|
|
11
11
|
**Peer dependencies:** React 18+ and React DOM 18+.
|
|
12
12
|
|
|
13
|
+
Browser SDK examples must use scoped `hb_src_*` search keys. Keep `hb_adm_*` admin keys server-side for indexing, settings, and connector jobs.
|
|
14
|
+
|
|
13
15
|
## Quick Start
|
|
14
16
|
|
|
15
17
|
```tsx
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
Stats,
|
|
24
26
|
} from "@headband/react";
|
|
25
27
|
|
|
26
|
-
const client = headband("https://your-headband-instance.com", "
|
|
28
|
+
const client = headband("https://your-headband-instance.com", "hb_src_YOUR_SEARCH_KEY");
|
|
27
29
|
|
|
28
30
|
function App() {
|
|
29
31
|
return (
|
|
@@ -68,6 +70,33 @@ HeadbandProvider -- owns all search state, executes searches
|
|
|
68
70
|
|-- useSearch() -- full state + actions (escape hatch)
|
|
69
71
|
```
|
|
70
72
|
|
|
73
|
+
### Base filters and full results pages
|
|
74
|
+
|
|
75
|
+
Use `baseFilter` on `HeadbandProvider` for app-owned constraints that should apply
|
|
76
|
+
to every query: tenant isolation, RBAC, publish status, availability, or custom
|
|
77
|
+
OR/AND groups. Refinement and range hooks keep adding UI filters, and Headband
|
|
78
|
+
ANDs them with `baseFilter` while preserving OR groups.
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { HeadbandProvider, filterEq, orFilters } from "@headband/react";
|
|
82
|
+
|
|
83
|
+
const availability = orFilters(filterEq("status", "published"), filterEq("status", "backorder"));
|
|
84
|
+
|
|
85
|
+
<HeadbandProvider
|
|
86
|
+
client={client}
|
|
87
|
+
index="products"
|
|
88
|
+
baseFilter={[filterEq("tenantId", "acme"), availability]}
|
|
89
|
+
>
|
|
90
|
+
<SearchBox />
|
|
91
|
+
<RefinementList attribute="brand" />
|
|
92
|
+
<RangeInput attribute="price" />
|
|
93
|
+
<Hits />
|
|
94
|
+
</HeadbandProvider>;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This supports text search plus filters, filter-only browse pages, and mixed
|
|
98
|
+
boolean filters on every Headband engine.
|
|
99
|
+
|
|
71
100
|
---
|
|
72
101
|
|
|
73
102
|
## Client
|
|
@@ -79,15 +108,98 @@ Creates a Headband API client.
|
|
|
79
108
|
```ts
|
|
80
109
|
import { headband } from "@headband/react";
|
|
81
110
|
|
|
82
|
-
const client = headband("https://search.example.com", "
|
|
111
|
+
const client = headband("https://search.example.com", "hb_src_YOUR_SEARCH_KEY");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Param | Type | Description |
|
|
115
|
+
| -------- | -------- | -------------------------- |
|
|
116
|
+
| `host` | `string` | Headband instance URL |
|
|
117
|
+
| `apiKey` | `string` | API key for authentication |
|
|
118
|
+
|
|
119
|
+
Returns a `HeadbandClient` with `search()`, `multiSearch()`, `getExperimentConfigs()`, `assignExperiment()`, and `recordExperimentEvent()` methods.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Retrieval experiments
|
|
124
|
+
|
|
125
|
+
Headband can store managed experiment rules, rollout percentages, variant weights, and kill-switch state. Apps can call hosted `client.assignExperiment()` when they want Headband to apply those rules, or use deterministic local helpers when offline/app-owned assignment is required.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { HeadbandProvider, headband } from "@headband/react";
|
|
129
|
+
|
|
130
|
+
const client = headband("https://headband.dev", "hb_src_your_search_key");
|
|
131
|
+
const assignment = await client.assignExperiment({
|
|
132
|
+
indexUid: "products",
|
|
133
|
+
experimentKey: "engine-ab-test",
|
|
134
|
+
subjectId: sessionId,
|
|
135
|
+
sessionId,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (assignment.assigned) {
|
|
139
|
+
await client.recordExperimentEvent({
|
|
140
|
+
eventType: "experiment_view",
|
|
141
|
+
experimentKey: assignment.experimentKey,
|
|
142
|
+
variant: assignment.variant!,
|
|
143
|
+
indexUid: assignment.indexUid!,
|
|
144
|
+
sessionId,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
<HeadbandProvider
|
|
149
|
+
client={client}
|
|
150
|
+
index={assignment.assigned ? assignment.indexUid! : "products"}
|
|
151
|
+
experiment={
|
|
152
|
+
assignment.assigned
|
|
153
|
+
? {
|
|
154
|
+
experimentKey: assignment.experimentKey,
|
|
155
|
+
variant: assignment.variant!,
|
|
156
|
+
sessionId: assignment.sessionId,
|
|
157
|
+
}
|
|
158
|
+
: undefined
|
|
159
|
+
}
|
|
160
|
+
>
|
|
161
|
+
<SearchBox />
|
|
162
|
+
<Hits />
|
|
163
|
+
</HeadbandProvider>;
|
|
83
164
|
```
|
|
84
165
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
166
|
+
Hosted assignment only returns a variant when the rule is `active`, the kill switch is off, and the subject falls inside `trafficPercent`. For local assignment, `assignExperiment(config, subjectId)` uses `assignmentSalt`, `experimentKey`, and your subject/session ID for stable bucketing. Headband receives `experimentKey`, `variant`, and `sessionId` on search calls for analytics attribution.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Click feedback
|
|
171
|
+
|
|
172
|
+
When click feedback is on for your organization, every search response carries a `queryId`. Send clicks and conversions with it, and Headband counts them per query. Turn on "Use click feedback in ranking" for an index (dashboard or `PATCH /v1/indexes/{uid}/click-feedback`) and results people pick for a query move up for that query. No experiment is needed.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { useClickTracking, useHits } from "@headband/react";
|
|
176
|
+
|
|
177
|
+
function Results() {
|
|
178
|
+
const { hits } = useHits<{ id: string; title: string; url: string }>();
|
|
179
|
+
const { trackClick } = useClickTracking();
|
|
180
|
+
return (
|
|
181
|
+
<ul>
|
|
182
|
+
{hits.map((hit, i) => (
|
|
183
|
+
<li key={hit.__data.id}>
|
|
184
|
+
<a href={hit.__data.url} onClick={() => trackClick(hit, i)}>
|
|
185
|
+
{hit.__data.title}
|
|
186
|
+
</a>
|
|
187
|
+
</li>
|
|
188
|
+
))}
|
|
189
|
+
</ul>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Later, on the order page:
|
|
194
|
+
const { trackConversion } = useClickTracking();
|
|
195
|
+
trackConversion(productId, { value: 1999 });
|
|
196
|
+
```
|
|
89
197
|
|
|
90
|
-
|
|
198
|
+
- `trackClick(hit, positionOnPage)` sends the hit's id, the search's `queryId`, its position in the whole result list and a session id. Events are sent with `keepalive`, so a click that opens a new page still arrives.
|
|
199
|
+
- `trackConversion(docId)` names the search the document was last clicked from in this tab.
|
|
200
|
+
- Ids are read from `id`. Set `idField` on `<HeadbandProvider>` if your documents use another field. Set `sessionId` to your own visitor session id; without it, the experiment's session id is used, then an id kept for the browser tab.
|
|
201
|
+
- Outside React: `client.trackClick({ index, docId, queryId, position, sessionId })`, `client.trackConversion({ index, docId, value })` and `client.sendEvents([...])` (up to 100 events).
|
|
202
|
+
- Tracking never throws from the hook. The client methods reject with a `HeadbandError`, as the other calls do.
|
|
91
203
|
|
|
92
204
|
---
|
|
93
205
|
|
|
@@ -98,21 +210,24 @@ Returns a `HeadbandClient` with `search()` and `multiSearch()` methods.
|
|
|
98
210
|
Wraps your search UI and manages all search state.
|
|
99
211
|
|
|
100
212
|
```tsx
|
|
101
|
-
<HeadbandProvider
|
|
102
|
-
client={client}
|
|
103
|
-
index="products"
|
|
104
|
-
initialQuery="shoes"
|
|
105
|
-
>
|
|
213
|
+
<HeadbandProvider client={client} index="products" initialQuery="shoes">
|
|
106
214
|
{/* search components */}
|
|
107
215
|
</HeadbandProvider>
|
|
108
216
|
```
|
|
109
217
|
|
|
110
|
-
| Prop
|
|
111
|
-
|
|
|
112
|
-
| `client`
|
|
113
|
-
| `index`
|
|
114
|
-
| `initialQuery`
|
|
115
|
-
| `
|
|
218
|
+
| Prop | Type | Default | Description |
|
|
219
|
+
| ------------------- | ---------------------------------- | ------- | ---------------------------------------------------------------- |
|
|
220
|
+
| `client` | `HeadbandClient` | -- | Client returned by `headband()` |
|
|
221
|
+
| `index` | `string` | -- | Index to search |
|
|
222
|
+
| `initialQuery` | `string` | `""` | Starting query value |
|
|
223
|
+
| `experiment` | `ExperimentSearchMetadata \| null` | `null` | Optional experiment telemetry attached to search calls |
|
|
224
|
+
| `mode` | `"instant" \| "retrieval"` | -- | Visor's ranking mode; other engines ignore it |
|
|
225
|
+
| `disjunctiveFacets` | `boolean` | `true` | Facet lists keep counting their other values after one is ticked |
|
|
226
|
+
| `children` | `ReactNode` | -- | Child components |
|
|
227
|
+
|
|
228
|
+
`mode` is also a field of `client.search()` params and of each `client.multiSearch()` query.
|
|
229
|
+
On a Visor index, `instant` (the default) is typo-tolerant, as-you-type search, and
|
|
230
|
+
`retrieval` ranks longer queries by BM25F.
|
|
116
231
|
|
|
117
232
|
---
|
|
118
233
|
|
|
@@ -126,11 +241,11 @@ Binds to the search input. Returns the current query plus setters.
|
|
|
126
241
|
const { query, setQuery, clear } = useSearchBox();
|
|
127
242
|
```
|
|
128
243
|
|
|
129
|
-
| Return | Type
|
|
130
|
-
| ---------- |
|
|
131
|
-
| `query` | `string`
|
|
132
|
-
| `setQuery` | `(query: string) => void`
|
|
133
|
-
| `clear` | `() => void`
|
|
244
|
+
| Return | Type | Description |
|
|
245
|
+
| ---------- | ------------------------- | ------------------------------------ |
|
|
246
|
+
| `query` | `string` | Current query string |
|
|
247
|
+
| `setQuery` | `(query: string) => void` | Update the query (resets pagination) |
|
|
248
|
+
| `clear` | `() => void` | Clear the query to an empty string |
|
|
134
249
|
|
|
135
250
|
---
|
|
136
251
|
|
|
@@ -142,11 +257,11 @@ Returns the current search result hits with loading and error state.
|
|
|
142
257
|
const { hits, isLoading, error } = useHits<Movie>();
|
|
143
258
|
```
|
|
144
259
|
|
|
145
|
-
| Return | Type | Description
|
|
146
|
-
| ----------- | ------------- |
|
|
147
|
-
| `hits` | `Hit<T>[]` | Array of hit objects
|
|
148
|
-
| `isLoading` | `boolean` | Whether a search is in progress
|
|
149
|
-
| `error` | `Error\|null` | Error from the last search, if any
|
|
260
|
+
| Return | Type | Description |
|
|
261
|
+
| ----------- | ------------- | ---------------------------------- |
|
|
262
|
+
| `hits` | `Hit<T>[]` | Array of hit objects |
|
|
263
|
+
| `isLoading` | `boolean` | Whether a search is in progress |
|
|
264
|
+
| `error` | `Error\|null` | Error from the last search, if any |
|
|
150
265
|
|
|
151
266
|
Each `Hit<T>` includes the original document fields, an optional `_formatted` object with highlighted values, and a typed `__data: T` accessor.
|
|
152
267
|
|
|
@@ -164,21 +279,27 @@ const { items, refine, canToggle, isLoading } = useRefinementList({
|
|
|
164
279
|
});
|
|
165
280
|
```
|
|
166
281
|
|
|
167
|
-
| Prop | Type | Default | Description
|
|
168
|
-
| ----------- | -------------------- | --------- |
|
|
169
|
-
| `attribute` | `string` | -- | Facet attribute (e.g. `"genres"`)
|
|
170
|
-
| `limit` | `number` | `20` | Max facet values to show
|
|
171
|
-
| `sortBy` | `"count" \| "alpha"` | `"count"` | Sort order for facet values
|
|
282
|
+
| Prop | Type | Default | Description |
|
|
283
|
+
| ----------- | -------------------- | --------- | --------------------------------- |
|
|
284
|
+
| `attribute` | `string` | -- | Facet attribute (e.g. `"genres"`) |
|
|
285
|
+
| `limit` | `number` | `20` | Max facet values to show |
|
|
286
|
+
| `sortBy` | `"count" \| "alpha"` | `"count"` | Sort order for facet values |
|
|
172
287
|
|
|
173
|
-
| Return | Type
|
|
174
|
-
| ----------- |
|
|
175
|
-
| `items` | `RefinementListItem[]`
|
|
176
|
-
| `refine` | `(value: string) => void
|
|
177
|
-
| `canToggle` | `boolean`
|
|
178
|
-
| `isLoading` | `boolean`
|
|
288
|
+
| Return | Type | Description |
|
|
289
|
+
| ----------- | ------------------------- | -------------------------------- |
|
|
290
|
+
| `items` | `RefinementListItem[]` | Facet values with count & state |
|
|
291
|
+
| `refine` | `(value: string) => void` | Toggle a facet value on/off |
|
|
292
|
+
| `canToggle` | `boolean` | Whether any values are available |
|
|
293
|
+
| `isLoading` | `boolean` | Loading state |
|
|
179
294
|
|
|
180
295
|
Each `RefinementListItem` has `{ value: string; count: number; isRefined: boolean }`.
|
|
181
296
|
|
|
297
|
+
Ticking a value does not hide the other values. The provider runs one more search for each facet
|
|
298
|
+
with a ticked value, filtered by everything except that facet's own values, and uses its counts
|
|
299
|
+
for the list. Each of those searches counts as a search on your plan. Pass
|
|
300
|
+
`disjunctiveFacets={false}` to `<HeadbandProvider>` for one search per request; the list then
|
|
301
|
+
shows only the values left in the results. New in 0.2.1: in 0.1.1 and 0.2.0 the list always narrows.
|
|
302
|
+
|
|
182
303
|
---
|
|
183
304
|
|
|
184
305
|
### `usePagination()`
|
|
@@ -190,15 +311,15 @@ const { currentPage, totalPages, totalHits, setPage, canPrevious, canNext, pages
|
|
|
190
311
|
usePagination();
|
|
191
312
|
```
|
|
192
313
|
|
|
193
|
-
| Return | Type
|
|
194
|
-
| ------------- |
|
|
195
|
-
| `currentPage` | `number`
|
|
196
|
-
| `totalPages` | `number`
|
|
197
|
-
| `totalHits` | `number`
|
|
198
|
-
| `setPage` | `(page: number) => void`
|
|
199
|
-
| `canPrevious` | `boolean`
|
|
200
|
-
| `canNext` | `boolean`
|
|
201
|
-
| `pages` | `number[]`
|
|
314
|
+
| Return | Type | Description |
|
|
315
|
+
| ------------- | ------------------------ | ------------------------------------ |
|
|
316
|
+
| `currentPage` | `number` | Current page index (0-based) |
|
|
317
|
+
| `totalPages` | `number` | Total number of pages |
|
|
318
|
+
| `totalHits` | `number` | Total result count |
|
|
319
|
+
| `setPage` | `(page: number) => void` | Navigate to a page (clamped) |
|
|
320
|
+
| `canPrevious` | `boolean` | Whether a previous page exists |
|
|
321
|
+
| `canNext` | `boolean` | Whether a next page exists |
|
|
322
|
+
| `pages` | `number[]` | Window of page numbers for rendering |
|
|
202
323
|
|
|
203
324
|
---
|
|
204
325
|
|
|
@@ -210,12 +331,12 @@ Returns summary statistics about the current search.
|
|
|
210
331
|
const { totalHits, processingTimeMs, query, isLoading } = useStats();
|
|
211
332
|
```
|
|
212
333
|
|
|
213
|
-
| Return
|
|
214
|
-
|
|
|
215
|
-
| `totalHits`
|
|
216
|
-
| `processingTimeMs
|
|
217
|
-
| `query`
|
|
218
|
-
| `isLoading`
|
|
334
|
+
| Return | Type | Description |
|
|
335
|
+
| ------------------ | --------- | -------------------------------- |
|
|
336
|
+
| `totalHits` | `number` | Total results for the query |
|
|
337
|
+
| `processingTimeMs` | `number` | Server-side processing time (ms) |
|
|
338
|
+
| `query` | `string` | The query that produced results |
|
|
339
|
+
| `isLoading` | `boolean` | Loading state |
|
|
219
340
|
|
|
220
341
|
---
|
|
221
342
|
|
|
@@ -233,17 +354,17 @@ const { currentSort, setSort, items } = useSortBy({
|
|
|
233
354
|
});
|
|
234
355
|
```
|
|
235
356
|
|
|
236
|
-
| Prop | Type | Description
|
|
237
|
-
| ------- | -------------- |
|
|
238
|
-
| `items` | `SortByItem[]` | Available sort options
|
|
357
|
+
| Prop | Type | Description |
|
|
358
|
+
| ------- | -------------- | ---------------------- |
|
|
359
|
+
| `items` | `SortByItem[]` | Available sort options |
|
|
239
360
|
|
|
240
361
|
Each `SortByItem` has `{ value: string; label: string }`.
|
|
241
362
|
|
|
242
|
-
| Return | Type | Description
|
|
243
|
-
| ------------- | --------------------------------- |
|
|
363
|
+
| Return | Type | Description |
|
|
364
|
+
| ------------- | --------------------------------- | ------------------------------------ |
|
|
244
365
|
| `currentSort` | `string \| null` | Active sort value (null = relevance) |
|
|
245
|
-
| `setSort` | `(value: string\|null) => void` | Set the sort order
|
|
246
|
-
| `items` | `(SortByItem & { isSelected })[]` | Items with `isSelected` flag
|
|
366
|
+
| `setSort` | `(value: string\|null) => void` | Set the sort order |
|
|
367
|
+
| `items` | `(SortByItem & { isSelected })[]` | Items with `isSelected` flag |
|
|
247
368
|
|
|
248
369
|
---
|
|
249
370
|
|
|
@@ -257,18 +378,18 @@ const { min, max, stats, setRange, clear, isLoading } = useRange({
|
|
|
257
378
|
});
|
|
258
379
|
```
|
|
259
380
|
|
|
260
|
-
| Prop | Type | Description
|
|
261
|
-
| ----------- | -------- |
|
|
262
|
-
| `attribute` | `string` | Numeric attribute (e.g. `"price"`)
|
|
381
|
+
| Prop | Type | Description |
|
|
382
|
+
| ----------- | -------- | ---------------------------------- |
|
|
383
|
+
| `attribute` | `string` | Numeric attribute (e.g. `"price"`) |
|
|
263
384
|
|
|
264
|
-
| Return | Type
|
|
265
|
-
| ----------- |
|
|
266
|
-
| `min` | `number \| undefined`
|
|
267
|
-
| `max` | `number \| undefined`
|
|
268
|
-
| `stats` | `{ min: number; max: number } \| undefined
|
|
269
|
-
| `setRange` | `(range: { min?: number; max?: number }) => void` | Set range filter
|
|
270
|
-
| `clear` | `() => void`
|
|
271
|
-
| `isLoading` | `boolean`
|
|
385
|
+
| Return | Type | Description |
|
|
386
|
+
| ----------- | ------------------------------------------------- | --------------------------- |
|
|
387
|
+
| `min` | `number \| undefined` | Current min bound |
|
|
388
|
+
| `max` | `number \| undefined` | Current max bound |
|
|
389
|
+
| `stats` | `{ min: number; max: number } \| undefined` | Overall min/max from engine |
|
|
390
|
+
| `setRange` | `(range: { min?: number; max?: number }) => void` | Set range filter |
|
|
391
|
+
| `clear` | `() => void` | Remove the range filter |
|
|
392
|
+
| `isLoading` | `boolean` | Loading state |
|
|
272
393
|
|
|
273
394
|
---
|
|
274
395
|
|
|
@@ -283,15 +404,15 @@ const { value, highlighted } = useHighlight({
|
|
|
283
404
|
});
|
|
284
405
|
```
|
|
285
406
|
|
|
286
|
-
| Prop | Type
|
|
287
|
-
| ----------- |
|
|
288
|
-
| `hit` | `Record<string, unknown
|
|
289
|
-
| `attribute` | `string`
|
|
407
|
+
| Prop | Type | Description |
|
|
408
|
+
| ----------- | ------------------------- | ------------------------------- |
|
|
409
|
+
| `hit` | `Record<string, unknown>` | The hit object from `useHits()` |
|
|
410
|
+
| `attribute` | `string` | Attribute to highlight |
|
|
290
411
|
|
|
291
|
-
| Return | Type | Description
|
|
292
|
-
| ------------- | --------- |
|
|
293
|
-
| `value` | `string` | Highlighted HTML string (safe for innerHTML)
|
|
294
|
-
| `highlighted` | `boolean` | Whether the value contains highlight marks
|
|
412
|
+
| Return | Type | Description |
|
|
413
|
+
| ------------- | --------- | -------------------------------------------- |
|
|
414
|
+
| `value` | `string` | Highlighted HTML string (safe for innerHTML) |
|
|
415
|
+
| `highlighted` | `boolean` | Whether the value contains highlight marks |
|
|
295
416
|
|
|
296
417
|
Supports nested attributes via dot notation (e.g. `"author.name"`).
|
|
297
418
|
|
|
@@ -303,9 +424,25 @@ Escape-hatch hook that exposes the full search state and all actions. Prefer the
|
|
|
303
424
|
|
|
304
425
|
```tsx
|
|
305
426
|
const {
|
|
306
|
-
query,
|
|
307
|
-
|
|
308
|
-
|
|
427
|
+
query,
|
|
428
|
+
results,
|
|
429
|
+
isLoading,
|
|
430
|
+
error,
|
|
431
|
+
refinements,
|
|
432
|
+
ranges,
|
|
433
|
+
sortBy,
|
|
434
|
+
page,
|
|
435
|
+
hitsPerPage,
|
|
436
|
+
facets,
|
|
437
|
+
setQuery,
|
|
438
|
+
setPage,
|
|
439
|
+
setSortBy,
|
|
440
|
+
setHitsPerPage,
|
|
441
|
+
toggleRefinement,
|
|
442
|
+
setRange,
|
|
443
|
+
clearRange,
|
|
444
|
+
registerFacet,
|
|
445
|
+
unregisterFacet,
|
|
309
446
|
refresh,
|
|
310
447
|
} = useSearch();
|
|
311
448
|
```
|
|
@@ -316,25 +453,35 @@ const {
|
|
|
316
453
|
|
|
317
454
|
All components accept a `styled` prop (default `true`) that toggles built-in Tailwind CSS classes. Set `styled={false}` to bring your own styles.
|
|
318
455
|
|
|
456
|
+
The built-in styles are Tailwind classes inside this package. Tailwind v4 does not scan
|
|
457
|
+
`node_modules`, so add an `@source` line to the CSS file that imports Tailwind. The path is
|
|
458
|
+
relative to that CSS file. For a Next.js `app/globals.css`:
|
|
459
|
+
|
|
460
|
+
```css
|
|
461
|
+
@import "tailwindcss";
|
|
462
|
+
@source "../node_modules/@headband/react";
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
For `src/app/globals.css`, use `"../../node_modules/@headband/react"`.
|
|
466
|
+
|
|
319
467
|
### `<SearchBox>`
|
|
320
468
|
|
|
321
|
-
Search input with built-in clear button and search icon.
|
|
469
|
+
Search input with built-in clear button and search icon. From 0.2.1 it is a text input with
|
|
470
|
+
`role="searchbox"`, so Chrome and Safari add no second clear button. On 0.1.1 and 0.2.0, hide the browser's
|
|
471
|
+
own button with
|
|
472
|
+
`input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }`.
|
|
322
473
|
|
|
323
474
|
```tsx
|
|
324
|
-
<SearchBox
|
|
325
|
-
placeholder="Search products..."
|
|
326
|
-
autoFocus
|
|
327
|
-
onQueryChange={(q) => console.log(q)}
|
|
328
|
-
/>
|
|
475
|
+
<SearchBox placeholder="Search products..." autoFocus onQueryChange={(q) => console.log(q)} />
|
|
329
476
|
```
|
|
330
477
|
|
|
331
|
-
| Prop | Type
|
|
332
|
-
| --------------- |
|
|
333
|
-
| `placeholder` | `string`
|
|
334
|
-
| `className` | `string`
|
|
335
|
-
| `autoFocus` | `boolean`
|
|
336
|
-
| `onQueryChange` | `(query: string) => void`
|
|
337
|
-
| `styled` | `boolean`
|
|
478
|
+
| Prop | Type | Default | Description |
|
|
479
|
+
| --------------- | ------------------------- | ------------- | ---------------------------- |
|
|
480
|
+
| `placeholder` | `string` | `"Search..."` | Input placeholder |
|
|
481
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
482
|
+
| `autoFocus` | `boolean` | `false` | Auto-focus on mount |
|
|
483
|
+
| `onQueryChange` | `(query: string) => void` | -- | Called on every query change |
|
|
484
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
338
485
|
|
|
339
486
|
---
|
|
340
487
|
|
|
@@ -346,12 +493,12 @@ Renders search result hits in a grid layout.
|
|
|
346
493
|
<Hits hitComponent={ProductCard} emptyComponent={<p>No results.</p>} />
|
|
347
494
|
```
|
|
348
495
|
|
|
349
|
-
| Prop | Type
|
|
350
|
-
| ---------------- |
|
|
351
|
-
| `hitComponent` | `ComponentType<{ hit: Hit }>`
|
|
352
|
-
| `className` | `string`
|
|
353
|
-
| `emptyComponent` | `ReactNode`
|
|
354
|
-
| `styled` | `boolean`
|
|
496
|
+
| Prop | Type | Default | Description |
|
|
497
|
+
| ---------------- | ----------------------------- | ------------ | ----------------------------- |
|
|
498
|
+
| `hitComponent` | `ComponentType<{ hit: Hit }>` | JSON card | Custom component per hit |
|
|
499
|
+
| `className` | `string` | -- | CSS classes for the container |
|
|
500
|
+
| `emptyComponent` | `ReactNode` | "No results" | Shown when there are no hits |
|
|
501
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
355
502
|
|
|
356
503
|
---
|
|
357
504
|
|
|
@@ -363,14 +510,14 @@ Checkbox list of facet values with counts.
|
|
|
363
510
|
<RefinementList attribute="brand" header="Brand" limit={10} sortBy="count" />
|
|
364
511
|
```
|
|
365
512
|
|
|
366
|
-
| Prop | Type | Default | Description
|
|
367
|
-
| ----------- | -------------------- | --------- |
|
|
368
|
-
| `attribute` | `string` | -- | Facet attribute
|
|
369
|
-
| `limit` | `number` | `20` | Max values to show
|
|
370
|
-
| `sortBy` | `"count" \| "alpha"` | `"count"` | Value sort order
|
|
371
|
-
| `header` | `string` | -- | Optional heading above the list
|
|
372
|
-
| `className` | `string` | -- | Additional CSS classes
|
|
373
|
-
| `styled` | `boolean` | `true` | Enable built-in styles
|
|
513
|
+
| Prop | Type | Default | Description |
|
|
514
|
+
| ----------- | -------------------- | --------- | ------------------------------- |
|
|
515
|
+
| `attribute` | `string` | -- | Facet attribute |
|
|
516
|
+
| `limit` | `number` | `20` | Max values to show |
|
|
517
|
+
| `sortBy` | `"count" \| "alpha"` | `"count"` | Value sort order |
|
|
518
|
+
| `header` | `string` | -- | Optional heading above the list |
|
|
519
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
520
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
374
521
|
|
|
375
522
|
---
|
|
376
523
|
|
|
@@ -382,10 +529,10 @@ Page navigation with Previous/Next buttons and numbered pages.
|
|
|
382
529
|
<Pagination />
|
|
383
530
|
```
|
|
384
531
|
|
|
385
|
-
| Prop | Type | Default | Description
|
|
386
|
-
| ----------- | --------- | ------- |
|
|
387
|
-
| `className` | `string` | -- | Additional CSS classes
|
|
388
|
-
| `styled` | `boolean` | `true` | Enable built-in styles
|
|
532
|
+
| Prop | Type | Default | Description |
|
|
533
|
+
| ----------- | --------- | ------- | ---------------------- |
|
|
534
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
535
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
389
536
|
|
|
390
537
|
---
|
|
391
538
|
|
|
@@ -400,11 +547,11 @@ Displays result count and processing time (e.g. "1,234 results in 3ms").
|
|
|
400
547
|
} />
|
|
401
548
|
```
|
|
402
549
|
|
|
403
|
-
| Prop | Type
|
|
404
|
-
| ----------- |
|
|
405
|
-
| `render` | `(stats: { totalHits; processingTimeMs; query }) => string` | --
|
|
406
|
-
| `className` | `string`
|
|
407
|
-
| `styled` | `boolean`
|
|
550
|
+
| Prop | Type | Default | Description |
|
|
551
|
+
| ----------- | ----------------------------------------------------------- | ------- | ---------------------- |
|
|
552
|
+
| `render` | `(stats: { totalHits; processingTimeMs; query }) => string` | -- | Custom render function |
|
|
553
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
554
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
408
555
|
|
|
409
556
|
---
|
|
410
557
|
|
|
@@ -421,12 +568,12 @@ Sort-by select dropdown.
|
|
|
421
568
|
/>
|
|
422
569
|
```
|
|
423
570
|
|
|
424
|
-
| Prop | Type | Default | Description
|
|
425
|
-
| ----------- | -------------- | ----------- |
|
|
426
|
-
| `items` | `SortByItem[]` | -- | Sort options
|
|
427
|
-
| `label` | `string` | `"Sort by"` | Accessibility label
|
|
428
|
-
| `className` | `string` | -- | Additional CSS classes
|
|
429
|
-
| `styled` | `boolean` | `true` | Enable built-in styles
|
|
571
|
+
| Prop | Type | Default | Description |
|
|
572
|
+
| ----------- | -------------- | ----------- | ---------------------- |
|
|
573
|
+
| `items` | `SortByItem[]` | -- | Sort options |
|
|
574
|
+
| `label` | `string` | `"Sort by"` | Accessibility label |
|
|
575
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
576
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
430
577
|
|
|
431
578
|
---
|
|
432
579
|
|
|
@@ -438,13 +585,13 @@ Renders highlighted text for a hit attribute. Safe for use with `dangerouslySetI
|
|
|
438
585
|
<Highlight hit={hit} attribute="title" />
|
|
439
586
|
```
|
|
440
587
|
|
|
441
|
-
| Prop | Type
|
|
442
|
-
| ----------- |
|
|
443
|
-
| `hit` | `Record<string, unknown>`
|
|
444
|
-
| `attribute` | `string`
|
|
445
|
-
| `tagName` | `keyof HTMLElementTagNameMap` | `"span"` | Wrapper element tag
|
|
446
|
-
| `className` | `string`
|
|
447
|
-
| `styled` | `boolean`
|
|
588
|
+
| Prop | Type | Default | Description |
|
|
589
|
+
| ----------- | ----------------------------- | -------- | --------------------------- |
|
|
590
|
+
| `hit` | `Record<string, unknown>` | -- | Hit object from `useHits()` |
|
|
591
|
+
| `attribute` | `string` | -- | Attribute to highlight |
|
|
592
|
+
| `tagName` | `keyof HTMLElementTagNameMap` | `"span"` | Wrapper element tag |
|
|
593
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
594
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
448
595
|
|
|
449
596
|
---
|
|
450
597
|
|
|
@@ -456,14 +603,14 @@ Min/max number inputs for numeric range filtering.
|
|
|
456
603
|
<RangeInput attribute="price" header="Price" />
|
|
457
604
|
```
|
|
458
605
|
|
|
459
|
-
| Prop | Type | Default | Description
|
|
460
|
-
| ---------------- | --------- | ------- |
|
|
461
|
-
| `attribute` | `string` | -- | Numeric attribute to filter
|
|
462
|
-
| `header` | `string` | -- | Optional heading
|
|
463
|
-
| `minPlaceholder` | `string` | `"Min"` | Min input placeholder
|
|
464
|
-
| `maxPlaceholder` | `string` | `"Max"` | Max input placeholder
|
|
465
|
-
| `className` | `string` | -- | Additional CSS classes
|
|
466
|
-
| `styled` | `boolean` | `true` | Enable built-in styles
|
|
606
|
+
| Prop | Type | Default | Description |
|
|
607
|
+
| ---------------- | --------- | ------- | --------------------------- |
|
|
608
|
+
| `attribute` | `string` | -- | Numeric attribute to filter |
|
|
609
|
+
| `header` | `string` | -- | Optional heading |
|
|
610
|
+
| `minPlaceholder` | `string` | `"Min"` | Min input placeholder |
|
|
611
|
+
| `maxPlaceholder` | `string` | `"Max"` | Max input placeholder |
|
|
612
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
613
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
467
614
|
|
|
468
615
|
---
|
|
469
616
|
|
|
@@ -475,30 +622,30 @@ Min/max number inputs for numeric range filtering.
|
|
|
475
622
|
<PoweredBy />
|
|
476
623
|
```
|
|
477
624
|
|
|
478
|
-
| Prop | Type | Default | Description
|
|
479
|
-
| ----------- | --------- | ------------------------ |
|
|
480
|
-
| `href` | `string` | `"https://headband.dev"` | Link URL
|
|
481
|
-
| `className` | `string` | -- | Additional CSS classes
|
|
482
|
-
| `styled` | `boolean` | `true` | Enable built-in styles
|
|
625
|
+
| Prop | Type | Default | Description |
|
|
626
|
+
| ----------- | --------- | ------------------------ | ---------------------- |
|
|
627
|
+
| `href` | `string` | `"https://headband.dev"` | Link URL |
|
|
628
|
+
| `className` | `string` | -- | Additional CSS classes |
|
|
629
|
+
| `styled` | `boolean` | `true` | Enable built-in styles |
|
|
483
630
|
|
|
484
631
|
---
|
|
485
632
|
|
|
486
633
|
## Comparison to Algolia InstantSearch
|
|
487
634
|
|
|
488
|
-
| Feature
|
|
489
|
-
|
|
|
490
|
-
| Provider
|
|
491
|
-
| Search input
|
|
492
|
-
| Results
|
|
493
|
-
| Facets
|
|
494
|
-
| Pagination
|
|
495
|
-
| Stats
|
|
496
|
-
| Sort
|
|
497
|
-
| Range
|
|
498
|
-
| Highlighting
|
|
499
|
-
| Bundle size
|
|
500
|
-
| Backend lock-in
|
|
501
|
-
| Built-in styles
|
|
635
|
+
| Feature | @headband/react | react-instantsearch |
|
|
636
|
+
| --------------- | ---------------------------------------- | ------------------------------ |
|
|
637
|
+
| Provider | `<HeadbandProvider>` | `<InstantSearch>` |
|
|
638
|
+
| Search input | `<SearchBox>` / `useSearchBox` | `<SearchBox>` / `useSearchBox` |
|
|
639
|
+
| Results | `<Hits>` / `useHits` | `<Hits>` / `useHits` |
|
|
640
|
+
| Facets | `<RefinementList>` | `<RefinementList>` |
|
|
641
|
+
| Pagination | `<Pagination>` | `<Pagination>` |
|
|
642
|
+
| Stats | `<Stats>` | `<Stats>` |
|
|
643
|
+
| Sort | `<SortBy>` | `<SortBy>` |
|
|
644
|
+
| Range | `<RangeInput>` / `useRange` | `<RangeInput>` / `useRange` |
|
|
645
|
+
| Highlighting | `<Highlight>` | `<Highlight>` |
|
|
646
|
+
| Bundle size | ~33 KB (ESM, unminified) | ~120 KB+ |
|
|
647
|
+
| Backend lock-in | None (Headband is engine-agnostic) | Algolia only |
|
|
648
|
+
| Built-in styles | Tailwind (opt-out with `styled={false}`) | CSS classes |
|
|
502
649
|
|
|
503
650
|
## TypeScript
|
|
504
651
|
|