@headband/react 0.1.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 +520 -0
- package/dist/index.d.mts +416 -0
- package/dist/index.d.ts +416 -0
- package/dist/index.js +1124 -0
- package/dist/index.mjs +1092 -0
- package/package.json +61 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/client.ts
|
|
4
|
+
var HeadbandError = class extends Error {
|
|
5
|
+
constructor(message, status, code) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "HeadbandError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function headband(host, apiKey) {
|
|
13
|
+
const baseUrl = host.replace(/\/+$/, "");
|
|
14
|
+
async function request(path, body) {
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetch(`${baseUrl}${path}`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Authorization: `Bearer ${apiKey}`
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify(body)
|
|
24
|
+
});
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new HeadbandError(
|
|
27
|
+
err instanceof Error ? err.message : "Network request failed",
|
|
28
|
+
0
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
let errorBody = {};
|
|
33
|
+
try {
|
|
34
|
+
errorBody = await response.json();
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
throw new HeadbandError(
|
|
38
|
+
errorBody.error ?? `Request failed with status ${response.status}`,
|
|
39
|
+
response.status,
|
|
40
|
+
errorBody.code
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return response.json();
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
search(index, query, params) {
|
|
47
|
+
return request("/v1/search", {
|
|
48
|
+
index,
|
|
49
|
+
q: query,
|
|
50
|
+
...params
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
multiSearch(queries) {
|
|
54
|
+
return request("/v1/multi-search", { queries });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/provider.tsx
|
|
60
|
+
import {
|
|
61
|
+
createContext,
|
|
62
|
+
useContext,
|
|
63
|
+
useCallback,
|
|
64
|
+
useEffect,
|
|
65
|
+
useRef,
|
|
66
|
+
useMemo,
|
|
67
|
+
useState
|
|
68
|
+
} from "react";
|
|
69
|
+
import { jsx } from "react/jsx-runtime";
|
|
70
|
+
var HeadbandContext = createContext(null);
|
|
71
|
+
function useHeadbandContext() {
|
|
72
|
+
const ctx = useContext(HeadbandContext);
|
|
73
|
+
if (!ctx) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"useHeadbandContext must be used within a <HeadbandProvider>."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return ctx;
|
|
79
|
+
}
|
|
80
|
+
function buildFilterString(refinements, ranges) {
|
|
81
|
+
const parts = [];
|
|
82
|
+
refinements.forEach((values, attribute) => {
|
|
83
|
+
if (values.size === 0) return;
|
|
84
|
+
if (values.size === 1) {
|
|
85
|
+
const [v] = values;
|
|
86
|
+
parts.push(`${attribute} = "${v}"`);
|
|
87
|
+
} else {
|
|
88
|
+
const list = Array.from(values).map((v) => `"${v}"`).join(", ");
|
|
89
|
+
parts.push(`${attribute} IN [${list}]`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
ranges.forEach((range, attribute) => {
|
|
93
|
+
if (range.min !== void 0) {
|
|
94
|
+
parts.push(`${attribute} >= ${range.min}`);
|
|
95
|
+
}
|
|
96
|
+
if (range.max !== void 0) {
|
|
97
|
+
parts.push(`${attribute} <= ${range.max}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return parts.length > 0 ? parts.join(" AND ") : void 0;
|
|
101
|
+
}
|
|
102
|
+
function HeadbandProvider({
|
|
103
|
+
client,
|
|
104
|
+
index,
|
|
105
|
+
initialQuery = "",
|
|
106
|
+
children
|
|
107
|
+
}) {
|
|
108
|
+
const [query, setQueryRaw] = useState(initialQuery);
|
|
109
|
+
const [results, setResults] = useState(null);
|
|
110
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
111
|
+
const [error, setError] = useState(null);
|
|
112
|
+
const [refinements, setRefinements] = useState(
|
|
113
|
+
() => /* @__PURE__ */ new Map()
|
|
114
|
+
);
|
|
115
|
+
const [ranges, setRanges] = useState(() => /* @__PURE__ */ new Map());
|
|
116
|
+
const [sortBy, setSortBy] = useState(null);
|
|
117
|
+
const [page, setPage] = useState(0);
|
|
118
|
+
const [hitsPerPage, setHitsPerPage] = useState(20);
|
|
119
|
+
const [facets, setFacets] = useState(() => /* @__PURE__ */ new Set());
|
|
120
|
+
const timerRef = useRef(null);
|
|
121
|
+
const requestIdRef = useRef(0);
|
|
122
|
+
const executeSearch = useCallback(() => {
|
|
123
|
+
const id = ++requestIdRef.current;
|
|
124
|
+
setIsLoading(true);
|
|
125
|
+
setError(null);
|
|
126
|
+
const filter = buildFilterString(refinements, ranges);
|
|
127
|
+
const facetList = facets.size > 0 ? Array.from(facets) : void 0;
|
|
128
|
+
const sortList = sortBy ? [sortBy] : void 0;
|
|
129
|
+
const params = {
|
|
130
|
+
limit: hitsPerPage,
|
|
131
|
+
offset: page * hitsPerPage,
|
|
132
|
+
...filter ? { filter } : {},
|
|
133
|
+
...facetList ? { facets: facetList } : {},
|
|
134
|
+
...sortList ? { sort: sortList } : {},
|
|
135
|
+
attributesToHighlight: ["*"]
|
|
136
|
+
};
|
|
137
|
+
client.search(index, query, params).then((res) => {
|
|
138
|
+
if (id === requestIdRef.current) {
|
|
139
|
+
setResults(res);
|
|
140
|
+
setIsLoading(false);
|
|
141
|
+
}
|
|
142
|
+
}).catch((err) => {
|
|
143
|
+
if (id === requestIdRef.current) {
|
|
144
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
145
|
+
setIsLoading(false);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}, [client, index, query, refinements, ranges, sortBy, page, hitsPerPage, facets]);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (timerRef.current) {
|
|
151
|
+
clearTimeout(timerRef.current);
|
|
152
|
+
}
|
|
153
|
+
timerRef.current = setTimeout(executeSearch, 200);
|
|
154
|
+
return () => {
|
|
155
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
156
|
+
};
|
|
157
|
+
}, [executeSearch]);
|
|
158
|
+
const setQuery = useCallback((q) => {
|
|
159
|
+
setQueryRaw(q);
|
|
160
|
+
setPage(0);
|
|
161
|
+
}, []);
|
|
162
|
+
const toggleRefinement = useCallback(
|
|
163
|
+
(attribute, value2) => {
|
|
164
|
+
setRefinements((prev) => {
|
|
165
|
+
const next = new Map(prev);
|
|
166
|
+
const current = new Set(next.get(attribute) ?? []);
|
|
167
|
+
if (current.has(value2)) {
|
|
168
|
+
current.delete(value2);
|
|
169
|
+
} else {
|
|
170
|
+
current.add(value2);
|
|
171
|
+
}
|
|
172
|
+
next.set(attribute, current);
|
|
173
|
+
return next;
|
|
174
|
+
});
|
|
175
|
+
setPage(0);
|
|
176
|
+
},
|
|
177
|
+
[]
|
|
178
|
+
);
|
|
179
|
+
const setRange = useCallback(
|
|
180
|
+
(attribute, range) => {
|
|
181
|
+
setRanges((prev) => {
|
|
182
|
+
const next = new Map(prev);
|
|
183
|
+
next.set(attribute, range);
|
|
184
|
+
return next;
|
|
185
|
+
});
|
|
186
|
+
setPage(0);
|
|
187
|
+
},
|
|
188
|
+
[]
|
|
189
|
+
);
|
|
190
|
+
const clearRange = useCallback((attribute) => {
|
|
191
|
+
setRanges((prev) => {
|
|
192
|
+
const next = new Map(prev);
|
|
193
|
+
next.delete(attribute);
|
|
194
|
+
return next;
|
|
195
|
+
});
|
|
196
|
+
setPage(0);
|
|
197
|
+
}, []);
|
|
198
|
+
const registerFacet = useCallback((attribute) => {
|
|
199
|
+
setFacets((prev) => {
|
|
200
|
+
if (prev.has(attribute)) return prev;
|
|
201
|
+
const next = new Set(prev);
|
|
202
|
+
next.add(attribute);
|
|
203
|
+
return next;
|
|
204
|
+
});
|
|
205
|
+
}, []);
|
|
206
|
+
const unregisterFacet = useCallback((attribute) => {
|
|
207
|
+
setFacets((prev) => {
|
|
208
|
+
if (!prev.has(attribute)) return prev;
|
|
209
|
+
const next = new Set(prev);
|
|
210
|
+
next.delete(attribute);
|
|
211
|
+
return next;
|
|
212
|
+
});
|
|
213
|
+
}, []);
|
|
214
|
+
const refresh = useCallback(() => {
|
|
215
|
+
executeSearch();
|
|
216
|
+
}, [executeSearch]);
|
|
217
|
+
const state = useMemo(
|
|
218
|
+
() => ({
|
|
219
|
+
query,
|
|
220
|
+
results,
|
|
221
|
+
isLoading,
|
|
222
|
+
error,
|
|
223
|
+
refinements,
|
|
224
|
+
ranges,
|
|
225
|
+
sortBy,
|
|
226
|
+
page,
|
|
227
|
+
hitsPerPage,
|
|
228
|
+
facets
|
|
229
|
+
}),
|
|
230
|
+
[query, results, isLoading, error, refinements, ranges, sortBy, page, hitsPerPage, facets]
|
|
231
|
+
);
|
|
232
|
+
const actions = useMemo(
|
|
233
|
+
() => ({
|
|
234
|
+
setQuery,
|
|
235
|
+
setPage,
|
|
236
|
+
setSortBy,
|
|
237
|
+
setHitsPerPage,
|
|
238
|
+
toggleRefinement,
|
|
239
|
+
setRange,
|
|
240
|
+
clearRange,
|
|
241
|
+
registerFacet,
|
|
242
|
+
unregisterFacet,
|
|
243
|
+
refresh
|
|
244
|
+
}),
|
|
245
|
+
[setQuery, toggleRefinement, setRange, clearRange, registerFacet, unregisterFacet, refresh]
|
|
246
|
+
);
|
|
247
|
+
const value = useMemo(
|
|
248
|
+
() => ({ client, index, state, actions }),
|
|
249
|
+
[client, index, state, actions]
|
|
250
|
+
);
|
|
251
|
+
return /* @__PURE__ */ jsx(HeadbandContext.Provider, { value, children });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/hooks/useSearch.ts
|
|
255
|
+
function useSearch() {
|
|
256
|
+
const { state, actions } = useHeadbandContext();
|
|
257
|
+
return { ...state, ...actions };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/hooks/useSearchBox.ts
|
|
261
|
+
import { useCallback as useCallback2 } from "react";
|
|
262
|
+
function useSearchBox() {
|
|
263
|
+
const { state, actions } = useHeadbandContext();
|
|
264
|
+
const clear = useCallback2(() => {
|
|
265
|
+
actions.setQuery("");
|
|
266
|
+
}, [actions]);
|
|
267
|
+
return {
|
|
268
|
+
query: state.query,
|
|
269
|
+
setQuery: actions.setQuery,
|
|
270
|
+
clear
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/hooks/useHits.ts
|
|
275
|
+
import { useMemo as useMemo2 } from "react";
|
|
276
|
+
function useHits() {
|
|
277
|
+
const { state } = useHeadbandContext();
|
|
278
|
+
const hits = useMemo2(() => {
|
|
279
|
+
if (!state.results) return [];
|
|
280
|
+
return state.results.hits.map((raw) => {
|
|
281
|
+
const hit = { ...raw };
|
|
282
|
+
hit.__data = raw;
|
|
283
|
+
return hit;
|
|
284
|
+
});
|
|
285
|
+
}, [state.results]);
|
|
286
|
+
return {
|
|
287
|
+
hits,
|
|
288
|
+
isLoading: state.isLoading,
|
|
289
|
+
error: state.error
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/hooks/useRefinementList.ts
|
|
294
|
+
import { useEffect as useEffect2, useMemo as useMemo3, useCallback as useCallback3 } from "react";
|
|
295
|
+
function useRefinementList({
|
|
296
|
+
attribute,
|
|
297
|
+
limit = 20,
|
|
298
|
+
sortBy: sortOrder = "count"
|
|
299
|
+
}) {
|
|
300
|
+
const { state, actions } = useHeadbandContext();
|
|
301
|
+
useEffect2(() => {
|
|
302
|
+
actions.registerFacet(attribute);
|
|
303
|
+
return () => {
|
|
304
|
+
actions.unregisterFacet(attribute);
|
|
305
|
+
};
|
|
306
|
+
}, [attribute, actions]);
|
|
307
|
+
const refined = state.refinements.get(attribute) ?? /* @__PURE__ */ new Set();
|
|
308
|
+
const items = useMemo3(() => {
|
|
309
|
+
const distribution = state.results?.facetDistribution?.[attribute] ?? {};
|
|
310
|
+
const allValues = /* @__PURE__ */ new Set([
|
|
311
|
+
...Object.keys(distribution),
|
|
312
|
+
...refined
|
|
313
|
+
]);
|
|
314
|
+
let result = Array.from(allValues).map(
|
|
315
|
+
(value) => ({
|
|
316
|
+
value,
|
|
317
|
+
count: distribution[value] ?? 0,
|
|
318
|
+
isRefined: refined.has(value)
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
if (sortOrder === "alpha") {
|
|
322
|
+
result.sort((a, b) => a.value.localeCompare(b.value));
|
|
323
|
+
} else {
|
|
324
|
+
result.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
|
|
325
|
+
}
|
|
326
|
+
if (result.length > limit) {
|
|
327
|
+
result = result.slice(0, limit);
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}, [state.results, attribute, refined, sortOrder, limit]);
|
|
331
|
+
const refine = useCallback3(
|
|
332
|
+
(value) => {
|
|
333
|
+
actions.toggleRefinement(attribute, value);
|
|
334
|
+
},
|
|
335
|
+
[attribute, actions]
|
|
336
|
+
);
|
|
337
|
+
return {
|
|
338
|
+
items,
|
|
339
|
+
refine,
|
|
340
|
+
canToggle: items.length > 0,
|
|
341
|
+
isLoading: state.isLoading
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/hooks/usePagination.ts
|
|
346
|
+
import { useMemo as useMemo4, useCallback as useCallback4 } from "react";
|
|
347
|
+
function usePagination() {
|
|
348
|
+
const { state, actions } = useHeadbandContext();
|
|
349
|
+
const totalHits = useMemo4(() => {
|
|
350
|
+
if (!state.results) return 0;
|
|
351
|
+
return state.results.estimatedTotalHits ?? state.results.totalHits ?? 0;
|
|
352
|
+
}, [state.results]);
|
|
353
|
+
const totalPages = useMemo4(() => {
|
|
354
|
+
if (totalHits === 0) return 0;
|
|
355
|
+
return Math.ceil(totalHits / state.hitsPerPage);
|
|
356
|
+
}, [totalHits, state.hitsPerPage]);
|
|
357
|
+
const canPrevious = state.page > 0;
|
|
358
|
+
const canNext = state.page < totalPages - 1;
|
|
359
|
+
const setPage = useCallback4(
|
|
360
|
+
(page) => {
|
|
361
|
+
const clamped = Math.max(0, Math.min(page, totalPages - 1));
|
|
362
|
+
actions.setPage(clamped);
|
|
363
|
+
},
|
|
364
|
+
[actions, totalPages]
|
|
365
|
+
);
|
|
366
|
+
const pages = useMemo4(() => {
|
|
367
|
+
if (totalPages === 0) return [];
|
|
368
|
+
const windowSize = 5;
|
|
369
|
+
const half = Math.floor(windowSize / 2);
|
|
370
|
+
let start = Math.max(0, state.page - half);
|
|
371
|
+
const end = Math.min(totalPages, start + windowSize);
|
|
372
|
+
if (end - start < windowSize) {
|
|
373
|
+
start = Math.max(0, end - windowSize);
|
|
374
|
+
}
|
|
375
|
+
const result = [];
|
|
376
|
+
for (let i = start; i < end; i++) {
|
|
377
|
+
result.push(i);
|
|
378
|
+
}
|
|
379
|
+
return result;
|
|
380
|
+
}, [state.page, totalPages]);
|
|
381
|
+
return {
|
|
382
|
+
currentPage: state.page,
|
|
383
|
+
totalPages,
|
|
384
|
+
totalHits,
|
|
385
|
+
setPage,
|
|
386
|
+
canPrevious,
|
|
387
|
+
canNext,
|
|
388
|
+
pages
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/hooks/useStats.ts
|
|
393
|
+
import { useMemo as useMemo5 } from "react";
|
|
394
|
+
function useStats() {
|
|
395
|
+
const { state } = useHeadbandContext();
|
|
396
|
+
return useMemo5(
|
|
397
|
+
() => ({
|
|
398
|
+
totalHits: state.results ? state.results.estimatedTotalHits ?? state.results.totalHits ?? 0 : 0,
|
|
399
|
+
processingTimeMs: state.results?.processingTimeMs ?? 0,
|
|
400
|
+
query: state.query,
|
|
401
|
+
isLoading: state.isLoading
|
|
402
|
+
}),
|
|
403
|
+
[state.results, state.query, state.isLoading]
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/hooks/useSortBy.ts
|
|
408
|
+
import { useCallback as useCallback5, useMemo as useMemo6 } from "react";
|
|
409
|
+
function useSortBy({ items }) {
|
|
410
|
+
const { state, actions } = useHeadbandContext();
|
|
411
|
+
const setSort = useCallback5(
|
|
412
|
+
(value) => {
|
|
413
|
+
actions.setSortBy(value);
|
|
414
|
+
},
|
|
415
|
+
[actions]
|
|
416
|
+
);
|
|
417
|
+
const enrichedItems = useMemo6(
|
|
418
|
+
() => items.map((item) => ({
|
|
419
|
+
...item,
|
|
420
|
+
isSelected: state.sortBy === item.value
|
|
421
|
+
})),
|
|
422
|
+
[items, state.sortBy]
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
currentSort: state.sortBy,
|
|
426
|
+
setSort,
|
|
427
|
+
items: enrichedItems
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/hooks/useRange.ts
|
|
432
|
+
import { useCallback as useCallback6, useMemo as useMemo7 } from "react";
|
|
433
|
+
function useRange({ attribute }) {
|
|
434
|
+
const { state, actions } = useHeadbandContext();
|
|
435
|
+
const current = state.ranges.get(attribute);
|
|
436
|
+
const stats = useMemo7(() => {
|
|
437
|
+
return state.results?.facetStats?.[attribute];
|
|
438
|
+
}, [state.results, attribute]);
|
|
439
|
+
const setRange = useCallback6(
|
|
440
|
+
(range) => {
|
|
441
|
+
actions.setRange(attribute, range);
|
|
442
|
+
},
|
|
443
|
+
[attribute, actions]
|
|
444
|
+
);
|
|
445
|
+
const clear = useCallback6(() => {
|
|
446
|
+
actions.clearRange(attribute);
|
|
447
|
+
}, [attribute, actions]);
|
|
448
|
+
return {
|
|
449
|
+
min: current?.min,
|
|
450
|
+
max: current?.max,
|
|
451
|
+
stats,
|
|
452
|
+
setRange,
|
|
453
|
+
clear,
|
|
454
|
+
isLoading: state.isLoading
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/hooks/useHighlight.ts
|
|
459
|
+
import { useMemo as useMemo8 } from "react";
|
|
460
|
+
function sanitizeHighlight(raw) {
|
|
461
|
+
const OPEN = "\0OPEN\0";
|
|
462
|
+
const CLOSE = "\0CLOSE\0";
|
|
463
|
+
let sanitized = raw.replace(/<em>/gi, OPEN).replace(/<\/em>/gi, CLOSE);
|
|
464
|
+
sanitized = sanitized.replace(/<[^>]*>/g, "");
|
|
465
|
+
sanitized = sanitized.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
466
|
+
sanitized = sanitized.replace(new RegExp(escapeRegex(OPEN), "g"), "<em>").replace(new RegExp(escapeRegex(CLOSE), "g"), "</em>");
|
|
467
|
+
return sanitized;
|
|
468
|
+
}
|
|
469
|
+
function escapeRegex(s) {
|
|
470
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
471
|
+
}
|
|
472
|
+
function getNestedValue(obj, path) {
|
|
473
|
+
const parts = path.split(".");
|
|
474
|
+
let current = obj;
|
|
475
|
+
for (const part of parts) {
|
|
476
|
+
if (current === null || current === void 0) return void 0;
|
|
477
|
+
current = current[part];
|
|
478
|
+
}
|
|
479
|
+
return current;
|
|
480
|
+
}
|
|
481
|
+
function useHighlight({
|
|
482
|
+
hit,
|
|
483
|
+
attribute
|
|
484
|
+
}) {
|
|
485
|
+
return useMemo8(() => {
|
|
486
|
+
const formatted = hit._formatted;
|
|
487
|
+
const highlightedRaw = formatted ? getNestedValue(formatted, attribute) : void 0;
|
|
488
|
+
if (typeof highlightedRaw === "string") {
|
|
489
|
+
const sanitized = sanitizeHighlight(highlightedRaw);
|
|
490
|
+
const hasHighlight = sanitized.includes("<em>");
|
|
491
|
+
return { value: sanitized, highlighted: hasHighlight };
|
|
492
|
+
}
|
|
493
|
+
const rawValue = getNestedValue(hit, attribute);
|
|
494
|
+
const stringValue = rawValue !== null && rawValue !== void 0 ? String(rawValue) : "";
|
|
495
|
+
return { value: stringValue, highlighted: false };
|
|
496
|
+
}, [hit, attribute]);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/components/SearchBox.tsx
|
|
500
|
+
import {
|
|
501
|
+
useCallback as useCallback7,
|
|
502
|
+
useRef as useRef2
|
|
503
|
+
} from "react";
|
|
504
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
505
|
+
function SearchBox({
|
|
506
|
+
placeholder = "Search...",
|
|
507
|
+
className,
|
|
508
|
+
autoFocus = false,
|
|
509
|
+
onQueryChange,
|
|
510
|
+
styled = true
|
|
511
|
+
}) {
|
|
512
|
+
const { query, setQuery, clear } = useSearchBox();
|
|
513
|
+
const inputRef = useRef2(null);
|
|
514
|
+
const handleChange = useCallback7(
|
|
515
|
+
(e) => {
|
|
516
|
+
const value = e.target.value;
|
|
517
|
+
setQuery(value);
|
|
518
|
+
onQueryChange?.(value);
|
|
519
|
+
},
|
|
520
|
+
[setQuery, onQueryChange]
|
|
521
|
+
);
|
|
522
|
+
const handleClear = useCallback7(() => {
|
|
523
|
+
clear();
|
|
524
|
+
onQueryChange?.("");
|
|
525
|
+
inputRef.current?.focus();
|
|
526
|
+
}, [clear, onQueryChange]);
|
|
527
|
+
const handleSubmit = useCallback7((e) => {
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
}, []);
|
|
530
|
+
const handleKeyDown = useCallback7(
|
|
531
|
+
(e) => {
|
|
532
|
+
if (e.key === "Escape") {
|
|
533
|
+
handleClear();
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
[handleClear]
|
|
537
|
+
);
|
|
538
|
+
const baseClass = styled ? "relative flex items-center w-full" : "";
|
|
539
|
+
const inputClass = styled ? "w-full rounded-lg border border-gray-200 bg-white py-2.5 pl-10 pr-10 text-sm outline-none transition-colors placeholder:text-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white dark:placeholder:text-gray-500 dark:focus:border-indigo-400" : "";
|
|
540
|
+
return /* @__PURE__ */ jsxs(
|
|
541
|
+
"form",
|
|
542
|
+
{
|
|
543
|
+
onSubmit: handleSubmit,
|
|
544
|
+
role: "search",
|
|
545
|
+
className: `${baseClass} ${className ?? ""}`.trim(),
|
|
546
|
+
children: [
|
|
547
|
+
/* @__PURE__ */ jsx2(
|
|
548
|
+
"svg",
|
|
549
|
+
{
|
|
550
|
+
className: styled ? "pointer-events-none absolute left-3 h-4 w-4 text-gray-400 dark:text-gray-500" : "pointer-events-none absolute left-3 h-4 w-4",
|
|
551
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
552
|
+
fill: "none",
|
|
553
|
+
viewBox: "0 0 24 24",
|
|
554
|
+
strokeWidth: 2,
|
|
555
|
+
stroke: "currentColor",
|
|
556
|
+
"aria-hidden": "true",
|
|
557
|
+
children: /* @__PURE__ */ jsx2(
|
|
558
|
+
"path",
|
|
559
|
+
{
|
|
560
|
+
strokeLinecap: "round",
|
|
561
|
+
strokeLinejoin: "round",
|
|
562
|
+
d: "m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
),
|
|
567
|
+
/* @__PURE__ */ jsx2(
|
|
568
|
+
"input",
|
|
569
|
+
{
|
|
570
|
+
ref: inputRef,
|
|
571
|
+
type: "search",
|
|
572
|
+
value: query,
|
|
573
|
+
onChange: handleChange,
|
|
574
|
+
onKeyDown: handleKeyDown,
|
|
575
|
+
placeholder,
|
|
576
|
+
autoFocus,
|
|
577
|
+
autoComplete: "off",
|
|
578
|
+
autoCorrect: "off",
|
|
579
|
+
spellCheck: false,
|
|
580
|
+
"aria-label": "Search",
|
|
581
|
+
className: inputClass
|
|
582
|
+
}
|
|
583
|
+
),
|
|
584
|
+
query.length > 0 && /* @__PURE__ */ jsx2(
|
|
585
|
+
"button",
|
|
586
|
+
{
|
|
587
|
+
type: "button",
|
|
588
|
+
onClick: handleClear,
|
|
589
|
+
"aria-label": "Clear search",
|
|
590
|
+
className: styled ? "absolute right-3 flex h-5 w-5 items-center justify-center rounded text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300" : "absolute right-3 flex h-5 w-5 items-center justify-center",
|
|
591
|
+
children: /* @__PURE__ */ jsx2(
|
|
592
|
+
"svg",
|
|
593
|
+
{
|
|
594
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
595
|
+
fill: "none",
|
|
596
|
+
viewBox: "0 0 24 24",
|
|
597
|
+
strokeWidth: 2,
|
|
598
|
+
stroke: "currentColor",
|
|
599
|
+
className: "h-3.5 w-3.5",
|
|
600
|
+
"aria-hidden": "true",
|
|
601
|
+
children: /* @__PURE__ */ jsx2(
|
|
602
|
+
"path",
|
|
603
|
+
{
|
|
604
|
+
strokeLinecap: "round",
|
|
605
|
+
strokeLinejoin: "round",
|
|
606
|
+
d: "M6 18 18 6M6 6l12 12"
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
)
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/components/Hits.tsx
|
|
619
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
620
|
+
function DefaultHit({ hit }) {
|
|
621
|
+
const { _formatted, __data, ...rest } = hit;
|
|
622
|
+
return /* @__PURE__ */ jsx3("div", { className: "rounded-lg border border-gray-200 bg-white p-4 text-sm dark:border-gray-700 dark:bg-gray-900", children: /* @__PURE__ */ jsx3("pre", { className: "overflow-x-auto whitespace-pre-wrap text-xs text-gray-700 dark:text-gray-300", children: JSON.stringify(rest, null, 2) }) });
|
|
623
|
+
}
|
|
624
|
+
function DefaultEmpty() {
|
|
625
|
+
return /* @__PURE__ */ jsx3("div", { className: "py-12 text-center text-sm text-gray-500 dark:text-gray-400", children: "No results found." });
|
|
626
|
+
}
|
|
627
|
+
function Hits({
|
|
628
|
+
hitComponent,
|
|
629
|
+
className,
|
|
630
|
+
emptyComponent,
|
|
631
|
+
styled = true
|
|
632
|
+
}) {
|
|
633
|
+
const { hits, isLoading, error } = useHits();
|
|
634
|
+
const HitComponent = hitComponent ?? DefaultHit;
|
|
635
|
+
if (error) {
|
|
636
|
+
return /* @__PURE__ */ jsxs2(
|
|
637
|
+
"div",
|
|
638
|
+
{
|
|
639
|
+
role: "alert",
|
|
640
|
+
className: styled ? "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300" : void 0,
|
|
641
|
+
children: [
|
|
642
|
+
"Search error: ",
|
|
643
|
+
error.message
|
|
644
|
+
]
|
|
645
|
+
}
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (!isLoading && hits.length === 0) {
|
|
649
|
+
return /* @__PURE__ */ jsx3(Fragment, { children: emptyComponent ?? /* @__PURE__ */ jsx3(DefaultEmpty, {}) });
|
|
650
|
+
}
|
|
651
|
+
const listClass = styled ? `grid gap-3 ${className ?? ""}`.trim() : className ?? "";
|
|
652
|
+
return /* @__PURE__ */ jsxs2("div", { className: listClass, role: "list", "aria-label": "Search results", children: [
|
|
653
|
+
isLoading && hits.length === 0 && /* @__PURE__ */ jsx3(
|
|
654
|
+
"div",
|
|
655
|
+
{
|
|
656
|
+
className: styled ? "py-8 text-center text-sm text-gray-400 dark:text-gray-500" : void 0,
|
|
657
|
+
"aria-live": "polite",
|
|
658
|
+
children: "Searching..."
|
|
659
|
+
}
|
|
660
|
+
),
|
|
661
|
+
hits.map((hit, i) => /* @__PURE__ */ jsx3("div", { role: "listitem", children: /* @__PURE__ */ jsx3(HitComponent, { hit }) }, hit.id ?? i))
|
|
662
|
+
] });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/components/RefinementList.tsx
|
|
666
|
+
import { useCallback as useCallback8 } from "react";
|
|
667
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
668
|
+
function RefinementList({
|
|
669
|
+
attribute,
|
|
670
|
+
limit,
|
|
671
|
+
sortBy,
|
|
672
|
+
className,
|
|
673
|
+
styled = true,
|
|
674
|
+
header
|
|
675
|
+
}) {
|
|
676
|
+
const { items, refine, isLoading } = useRefinementList({
|
|
677
|
+
attribute,
|
|
678
|
+
limit,
|
|
679
|
+
sortBy
|
|
680
|
+
});
|
|
681
|
+
const handleChange = useCallback8(
|
|
682
|
+
(value) => (_e) => {
|
|
683
|
+
refine(value);
|
|
684
|
+
},
|
|
685
|
+
[refine]
|
|
686
|
+
);
|
|
687
|
+
const containerClass = styled ? `space-y-1 ${className ?? ""}`.trim() : className ?? "";
|
|
688
|
+
return /* @__PURE__ */ jsxs3("fieldset", { className: containerClass, "aria-label": `Filter by ${attribute}`, children: [
|
|
689
|
+
header && /* @__PURE__ */ jsx4(
|
|
690
|
+
"legend",
|
|
691
|
+
{
|
|
692
|
+
className: styled ? "mb-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400" : void 0,
|
|
693
|
+
children: header
|
|
694
|
+
}
|
|
695
|
+
),
|
|
696
|
+
isLoading && items.length === 0 && /* @__PURE__ */ jsx4(
|
|
697
|
+
"div",
|
|
698
|
+
{
|
|
699
|
+
className: styled ? "py-2 text-xs text-gray-400 dark:text-gray-500" : void 0,
|
|
700
|
+
"aria-live": "polite",
|
|
701
|
+
children: "Loading..."
|
|
702
|
+
}
|
|
703
|
+
),
|
|
704
|
+
items.map((item) => /* @__PURE__ */ jsxs3(
|
|
705
|
+
"label",
|
|
706
|
+
{
|
|
707
|
+
className: styled ? "flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800" : "flex items-center gap-2",
|
|
708
|
+
children: [
|
|
709
|
+
/* @__PURE__ */ jsx4(
|
|
710
|
+
"input",
|
|
711
|
+
{
|
|
712
|
+
type: "checkbox",
|
|
713
|
+
checked: item.isRefined,
|
|
714
|
+
onChange: handleChange(item.value),
|
|
715
|
+
className: styled ? "h-3.5 w-3.5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 dark:border-gray-600" : void 0,
|
|
716
|
+
"aria-label": `${item.value} (${item.count})`
|
|
717
|
+
}
|
|
718
|
+
),
|
|
719
|
+
/* @__PURE__ */ jsx4(
|
|
720
|
+
"span",
|
|
721
|
+
{
|
|
722
|
+
className: styled ? "flex-1 truncate text-gray-700 dark:text-gray-300" : "flex-1",
|
|
723
|
+
children: item.value
|
|
724
|
+
}
|
|
725
|
+
),
|
|
726
|
+
/* @__PURE__ */ jsx4(
|
|
727
|
+
"span",
|
|
728
|
+
{
|
|
729
|
+
className: styled ? "text-xs tabular-nums text-gray-400 dark:text-gray-500" : void 0,
|
|
730
|
+
children: item.count
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
]
|
|
734
|
+
},
|
|
735
|
+
item.value
|
|
736
|
+
))
|
|
737
|
+
] });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/components/Pagination.tsx
|
|
741
|
+
import { useCallback as useCallback9 } from "react";
|
|
742
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
743
|
+
function Pagination({ className, styled = true }) {
|
|
744
|
+
const { currentPage, totalPages, setPage, canPrevious, canNext, pages } = usePagination();
|
|
745
|
+
const handlePrevious = useCallback9(() => {
|
|
746
|
+
setPage(currentPage - 1);
|
|
747
|
+
}, [setPage, currentPage]);
|
|
748
|
+
const handleNext = useCallback9(() => {
|
|
749
|
+
setPage(currentPage + 1);
|
|
750
|
+
}, [setPage, currentPage]);
|
|
751
|
+
if (totalPages <= 1) return null;
|
|
752
|
+
const navClass = styled ? `flex items-center gap-1 ${className ?? ""}`.trim() : className ?? "";
|
|
753
|
+
const buttonBase = styled ? "inline-flex h-8 min-w-[2rem] items-center justify-center rounded text-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500" : "";
|
|
754
|
+
const activeClass = styled ? "bg-indigo-600 text-white" : "";
|
|
755
|
+
const inactiveClass = styled ? "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800" : "";
|
|
756
|
+
const disabledClass = styled ? "text-gray-300 cursor-not-allowed dark:text-gray-600" : "";
|
|
757
|
+
return /* @__PURE__ */ jsxs4("nav", { "aria-label": "Search results pagination", className: navClass, children: [
|
|
758
|
+
/* @__PURE__ */ jsx5(
|
|
759
|
+
"button",
|
|
760
|
+
{
|
|
761
|
+
type: "button",
|
|
762
|
+
onClick: handlePrevious,
|
|
763
|
+
disabled: !canPrevious,
|
|
764
|
+
"aria-label": "Previous page",
|
|
765
|
+
className: `${buttonBase} px-2 ${canPrevious ? inactiveClass : disabledClass}`.trim(),
|
|
766
|
+
children: /* @__PURE__ */ jsx5(
|
|
767
|
+
"svg",
|
|
768
|
+
{
|
|
769
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
770
|
+
fill: "none",
|
|
771
|
+
viewBox: "0 0 24 24",
|
|
772
|
+
strokeWidth: 2,
|
|
773
|
+
stroke: "currentColor",
|
|
774
|
+
className: "h-4 w-4",
|
|
775
|
+
"aria-hidden": "true",
|
|
776
|
+
children: /* @__PURE__ */ jsx5(
|
|
777
|
+
"path",
|
|
778
|
+
{
|
|
779
|
+
strokeLinecap: "round",
|
|
780
|
+
strokeLinejoin: "round",
|
|
781
|
+
d: "M15.75 19.5 8.25 12l7.5-7.5"
|
|
782
|
+
}
|
|
783
|
+
)
|
|
784
|
+
}
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
),
|
|
788
|
+
pages.map((page) => /* @__PURE__ */ jsx5(
|
|
789
|
+
"button",
|
|
790
|
+
{
|
|
791
|
+
type: "button",
|
|
792
|
+
onClick: () => setPage(page),
|
|
793
|
+
"aria-label": `Page ${page + 1}`,
|
|
794
|
+
"aria-current": page === currentPage ? "page" : void 0,
|
|
795
|
+
className: `${buttonBase} ${page === currentPage ? activeClass : inactiveClass}`.trim(),
|
|
796
|
+
children: page + 1
|
|
797
|
+
},
|
|
798
|
+
page
|
|
799
|
+
)),
|
|
800
|
+
/* @__PURE__ */ jsx5(
|
|
801
|
+
"button",
|
|
802
|
+
{
|
|
803
|
+
type: "button",
|
|
804
|
+
onClick: handleNext,
|
|
805
|
+
disabled: !canNext,
|
|
806
|
+
"aria-label": "Next page",
|
|
807
|
+
className: `${buttonBase} px-2 ${canNext ? inactiveClass : disabledClass}`.trim(),
|
|
808
|
+
children: /* @__PURE__ */ jsx5(
|
|
809
|
+
"svg",
|
|
810
|
+
{
|
|
811
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
812
|
+
fill: "none",
|
|
813
|
+
viewBox: "0 0 24 24",
|
|
814
|
+
strokeWidth: 2,
|
|
815
|
+
stroke: "currentColor",
|
|
816
|
+
className: "h-4 w-4",
|
|
817
|
+
"aria-hidden": "true",
|
|
818
|
+
children: /* @__PURE__ */ jsx5(
|
|
819
|
+
"path",
|
|
820
|
+
{
|
|
821
|
+
strokeLinecap: "round",
|
|
822
|
+
strokeLinejoin: "round",
|
|
823
|
+
d: "m8.25 4.5 7.5 7.5-7.5 7.5"
|
|
824
|
+
}
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
)
|
|
830
|
+
] });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/components/Stats.tsx
|
|
834
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
835
|
+
function Stats({ className, styled = true, render }) {
|
|
836
|
+
const { totalHits, processingTimeMs, query, isLoading } = useStats();
|
|
837
|
+
if (isLoading && totalHits === 0) return null;
|
|
838
|
+
const text = render ? render({ totalHits, processingTimeMs, query }) : `${totalHits.toLocaleString()} result${totalHits !== 1 ? "s" : ""} in ${processingTimeMs}ms`;
|
|
839
|
+
return /* @__PURE__ */ jsx6(
|
|
840
|
+
"p",
|
|
841
|
+
{
|
|
842
|
+
className: styled ? `text-sm text-gray-500 dark:text-gray-400 ${className ?? ""}`.trim() : className ?? "",
|
|
843
|
+
"aria-live": "polite",
|
|
844
|
+
role: "status",
|
|
845
|
+
children: text
|
|
846
|
+
}
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/components/SortBy.tsx
|
|
851
|
+
import { useCallback as useCallback10 } from "react";
|
|
852
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
853
|
+
function SortBy({
|
|
854
|
+
items: itemsProp,
|
|
855
|
+
className,
|
|
856
|
+
styled = true,
|
|
857
|
+
label = "Sort by"
|
|
858
|
+
}) {
|
|
859
|
+
const { currentSort, setSort, items } = useSortBy({ items: itemsProp });
|
|
860
|
+
const handleChange = useCallback10(
|
|
861
|
+
(e) => {
|
|
862
|
+
const value = e.target.value;
|
|
863
|
+
setSort(value === "" ? null : value);
|
|
864
|
+
},
|
|
865
|
+
[setSort]
|
|
866
|
+
);
|
|
867
|
+
const selectClass = styled ? "rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm outline-none transition-colors focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white" : "";
|
|
868
|
+
return /* @__PURE__ */ jsxs5("div", { className, children: [
|
|
869
|
+
/* @__PURE__ */ jsx7("label", { className: "sr-only", htmlFor: "headband-sort-by", children: label }),
|
|
870
|
+
/* @__PURE__ */ jsxs5(
|
|
871
|
+
"select",
|
|
872
|
+
{
|
|
873
|
+
id: "headband-sort-by",
|
|
874
|
+
value: currentSort ?? "",
|
|
875
|
+
onChange: handleChange,
|
|
876
|
+
className: selectClass,
|
|
877
|
+
"aria-label": label,
|
|
878
|
+
children: [
|
|
879
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "Relevance" }),
|
|
880
|
+
items.map((item) => /* @__PURE__ */ jsx7("option", { value: item.value, children: item.label }, item.value))
|
|
881
|
+
]
|
|
882
|
+
}
|
|
883
|
+
)
|
|
884
|
+
] });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/components/Highlight.tsx
|
|
888
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
889
|
+
function Highlight({
|
|
890
|
+
hit,
|
|
891
|
+
attribute,
|
|
892
|
+
className,
|
|
893
|
+
styled = true,
|
|
894
|
+
tagName: Tag = "span"
|
|
895
|
+
}) {
|
|
896
|
+
const { value, highlighted } = useHighlight({ hit, attribute });
|
|
897
|
+
const highlightClass = styled ? `[&_em]:bg-indigo-100 [&_em]:not-italic [&_em]:font-medium [&_em]:text-indigo-900 dark:[&_em]:bg-indigo-900/40 dark:[&_em]:text-indigo-200 ${className ?? ""}`.trim() : className ?? "";
|
|
898
|
+
if (highlighted) {
|
|
899
|
+
return /* @__PURE__ */ jsx8(
|
|
900
|
+
Tag,
|
|
901
|
+
{
|
|
902
|
+
className: highlightClass,
|
|
903
|
+
dangerouslySetInnerHTML: { __html: value }
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
return /* @__PURE__ */ jsx8(Tag, { className, children: value });
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/components/RangeInput.tsx
|
|
911
|
+
import {
|
|
912
|
+
useState as useState2,
|
|
913
|
+
useCallback as useCallback11,
|
|
914
|
+
useEffect as useEffect3
|
|
915
|
+
} from "react";
|
|
916
|
+
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
917
|
+
function RangeInput({
|
|
918
|
+
attribute,
|
|
919
|
+
className,
|
|
920
|
+
styled = true,
|
|
921
|
+
header,
|
|
922
|
+
minPlaceholder = "Min",
|
|
923
|
+
maxPlaceholder = "Max"
|
|
924
|
+
}) {
|
|
925
|
+
const { min, max, stats, setRange, clear } = useRange({ attribute });
|
|
926
|
+
const [localMin, setLocalMin] = useState2(min !== void 0 ? String(min) : "");
|
|
927
|
+
const [localMax, setLocalMax] = useState2(max !== void 0 ? String(max) : "");
|
|
928
|
+
useEffect3(() => {
|
|
929
|
+
setLocalMin(min !== void 0 ? String(min) : "");
|
|
930
|
+
}, [min]);
|
|
931
|
+
useEffect3(() => {
|
|
932
|
+
setLocalMax(max !== void 0 ? String(max) : "");
|
|
933
|
+
}, [max]);
|
|
934
|
+
const handleSubmit = useCallback11(
|
|
935
|
+
(e) => {
|
|
936
|
+
e.preventDefault();
|
|
937
|
+
const parsedMin = localMin !== "" ? Number(localMin) : void 0;
|
|
938
|
+
const parsedMax = localMax !== "" ? Number(localMax) : void 0;
|
|
939
|
+
if (parsedMin === void 0 && parsedMax === void 0) {
|
|
940
|
+
clear();
|
|
941
|
+
} else {
|
|
942
|
+
setRange({
|
|
943
|
+
min: parsedMin !== void 0 && !Number.isNaN(parsedMin) ? parsedMin : void 0,
|
|
944
|
+
max: parsedMax !== void 0 && !Number.isNaN(parsedMax) ? parsedMax : void 0
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
},
|
|
948
|
+
[localMin, localMax, setRange, clear]
|
|
949
|
+
);
|
|
950
|
+
const handleMinChange = useCallback11(
|
|
951
|
+
(e) => setLocalMin(e.target.value),
|
|
952
|
+
[]
|
|
953
|
+
);
|
|
954
|
+
const handleMaxChange = useCallback11(
|
|
955
|
+
(e) => setLocalMax(e.target.value),
|
|
956
|
+
[]
|
|
957
|
+
);
|
|
958
|
+
const containerClass = styled ? `space-y-2 ${className ?? ""}`.trim() : className ?? "";
|
|
959
|
+
const inputClass = styled ? "w-full rounded border border-gray-200 bg-white px-2.5 py-1.5 text-sm outline-none transition-colors placeholder:text-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white dark:placeholder:text-gray-500" : "";
|
|
960
|
+
const buttonClass = styled ? "rounded bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-indigo-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500" : "";
|
|
961
|
+
return /* @__PURE__ */ jsxs6(
|
|
962
|
+
"form",
|
|
963
|
+
{
|
|
964
|
+
onSubmit: handleSubmit,
|
|
965
|
+
className: containerClass,
|
|
966
|
+
"aria-label": `Filter by ${attribute} range`,
|
|
967
|
+
children: [
|
|
968
|
+
header && /* @__PURE__ */ jsx9(
|
|
969
|
+
"div",
|
|
970
|
+
{
|
|
971
|
+
className: styled ? "text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400" : void 0,
|
|
972
|
+
children: header
|
|
973
|
+
}
|
|
974
|
+
),
|
|
975
|
+
stats && /* @__PURE__ */ jsxs6(
|
|
976
|
+
"div",
|
|
977
|
+
{
|
|
978
|
+
className: styled ? "text-xs text-gray-400 dark:text-gray-500" : void 0,
|
|
979
|
+
children: [
|
|
980
|
+
"Range: ",
|
|
981
|
+
stats.min,
|
|
982
|
+
" \u2013 ",
|
|
983
|
+
stats.max
|
|
984
|
+
]
|
|
985
|
+
}
|
|
986
|
+
),
|
|
987
|
+
/* @__PURE__ */ jsxs6("div", { className: styled ? "flex items-center gap-2" : "flex gap-2", children: [
|
|
988
|
+
/* @__PURE__ */ jsx9(
|
|
989
|
+
"input",
|
|
990
|
+
{
|
|
991
|
+
type: "number",
|
|
992
|
+
value: localMin,
|
|
993
|
+
onChange: handleMinChange,
|
|
994
|
+
placeholder: minPlaceholder,
|
|
995
|
+
"aria-label": `Minimum ${attribute}`,
|
|
996
|
+
className: inputClass
|
|
997
|
+
}
|
|
998
|
+
),
|
|
999
|
+
/* @__PURE__ */ jsx9(
|
|
1000
|
+
"span",
|
|
1001
|
+
{
|
|
1002
|
+
className: styled ? "text-sm text-gray-400 dark:text-gray-500" : void 0,
|
|
1003
|
+
"aria-hidden": "true",
|
|
1004
|
+
children: "\u2013"
|
|
1005
|
+
}
|
|
1006
|
+
),
|
|
1007
|
+
/* @__PURE__ */ jsx9(
|
|
1008
|
+
"input",
|
|
1009
|
+
{
|
|
1010
|
+
type: "number",
|
|
1011
|
+
value: localMax,
|
|
1012
|
+
onChange: handleMaxChange,
|
|
1013
|
+
placeholder: maxPlaceholder,
|
|
1014
|
+
"aria-label": `Maximum ${attribute}`,
|
|
1015
|
+
className: inputClass
|
|
1016
|
+
}
|
|
1017
|
+
),
|
|
1018
|
+
/* @__PURE__ */ jsx9("button", { type: "submit", className: buttonClass, children: "Go" })
|
|
1019
|
+
] })
|
|
1020
|
+
]
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/components/PoweredBy.tsx
|
|
1026
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1027
|
+
function PoweredBy({
|
|
1028
|
+
className,
|
|
1029
|
+
styled = true,
|
|
1030
|
+
href = "https://headband.dev"
|
|
1031
|
+
}) {
|
|
1032
|
+
const linkClass = styled ? `inline-flex items-center gap-1.5 text-xs text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 ${className ?? ""}`.trim() : className ?? "";
|
|
1033
|
+
return /* @__PURE__ */ jsxs7(
|
|
1034
|
+
"a",
|
|
1035
|
+
{
|
|
1036
|
+
href,
|
|
1037
|
+
target: "_blank",
|
|
1038
|
+
rel: "noopener noreferrer",
|
|
1039
|
+
className: linkClass,
|
|
1040
|
+
"aria-label": "Powered by Headband - Search as a Service",
|
|
1041
|
+
children: [
|
|
1042
|
+
/* @__PURE__ */ jsx10(
|
|
1043
|
+
"svg",
|
|
1044
|
+
{
|
|
1045
|
+
width: "14",
|
|
1046
|
+
height: "14",
|
|
1047
|
+
viewBox: "0 0 24 24",
|
|
1048
|
+
fill: "none",
|
|
1049
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1050
|
+
"aria-hidden": "true",
|
|
1051
|
+
children: /* @__PURE__ */ jsx10(
|
|
1052
|
+
"path",
|
|
1053
|
+
{
|
|
1054
|
+
d: "m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z",
|
|
1055
|
+
stroke: "currentColor",
|
|
1056
|
+
strokeWidth: "2",
|
|
1057
|
+
strokeLinecap: "round",
|
|
1058
|
+
strokeLinejoin: "round"
|
|
1059
|
+
}
|
|
1060
|
+
)
|
|
1061
|
+
}
|
|
1062
|
+
),
|
|
1063
|
+
"Powered by ",
|
|
1064
|
+
/* @__PURE__ */ jsx10("strong", { className: "font-semibold", children: "Headband" })
|
|
1065
|
+
]
|
|
1066
|
+
}
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
export {
|
|
1070
|
+
HeadbandError,
|
|
1071
|
+
HeadbandProvider,
|
|
1072
|
+
Highlight,
|
|
1073
|
+
Hits,
|
|
1074
|
+
Pagination,
|
|
1075
|
+
PoweredBy,
|
|
1076
|
+
RangeInput,
|
|
1077
|
+
RefinementList,
|
|
1078
|
+
SearchBox,
|
|
1079
|
+
SortBy,
|
|
1080
|
+
Stats,
|
|
1081
|
+
headband,
|
|
1082
|
+
useHeadbandContext,
|
|
1083
|
+
useHighlight,
|
|
1084
|
+
useHits,
|
|
1085
|
+
usePagination,
|
|
1086
|
+
useRange,
|
|
1087
|
+
useRefinementList,
|
|
1088
|
+
useSearch,
|
|
1089
|
+
useSearchBox,
|
|
1090
|
+
useSortBy,
|
|
1091
|
+
useStats
|
|
1092
|
+
};
|