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