@headband/react 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +249 -159
- package/dist/index.d.mts +168 -5
- package/dist/index.d.ts +168 -5
- package/dist/index.js +279 -96
- package/dist/index.mjs +267 -102
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
// src/experiments.ts
|
|
4
|
+
function stableHash(input) {
|
|
5
|
+
let hash = 2166136261;
|
|
6
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
7
|
+
hash ^= input.charCodeAt(i);
|
|
8
|
+
hash = Math.imul(hash, 16777619);
|
|
9
|
+
}
|
|
10
|
+
return hash >>> 0;
|
|
11
|
+
}
|
|
12
|
+
function bucket(input, max) {
|
|
13
|
+
if (max <= 0) return 0;
|
|
14
|
+
return stableHash(input) % max;
|
|
15
|
+
}
|
|
16
|
+
function assignmentSeed(config, subjectId, scope) {
|
|
17
|
+
return `${config.assignmentSalt || config.experimentKey}:${config.experimentKey}:${scope}:${subjectId}`;
|
|
18
|
+
}
|
|
19
|
+
function buildExperimentConfigPath(query = {}) {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
if (query.projectId) params.set("projectId", query.projectId);
|
|
22
|
+
if (query.indexUid) params.set("indexUid", query.indexUid);
|
|
23
|
+
if (query.experimentKey) params.set("experimentKey", query.experimentKey);
|
|
24
|
+
const search = params.toString();
|
|
25
|
+
return `/v1/experiments/config${search ? `?${search}` : ""}`;
|
|
26
|
+
}
|
|
27
|
+
function isRunnableExperiment(config) {
|
|
28
|
+
return config.status === "active" && !config.killSwitch && config.trafficPercent > 0 && config.variants.length > 0;
|
|
29
|
+
}
|
|
30
|
+
function chooseExperimentVariant(config, subjectId) {
|
|
31
|
+
if (!isRunnableExperiment(config)) return null;
|
|
32
|
+
const trafficBucket = bucket(assignmentSeed(config, subjectId, "traffic"), 1e4) / 100;
|
|
33
|
+
if (trafficBucket >= config.trafficPercent) return null;
|
|
34
|
+
const totalWeight = config.variants.reduce(
|
|
35
|
+
(sum, variant) => sum + Math.max(0, variant.weight),
|
|
36
|
+
0
|
|
37
|
+
);
|
|
38
|
+
if (totalWeight <= 0) return null;
|
|
39
|
+
const variantBucket = bucket(assignmentSeed(config, subjectId, "variant"), totalWeight);
|
|
40
|
+
let cursor = 0;
|
|
41
|
+
for (const variant of config.variants) {
|
|
42
|
+
cursor += Math.max(0, variant.weight);
|
|
43
|
+
if (variantBucket < cursor) return variant;
|
|
44
|
+
}
|
|
45
|
+
return config.variants[config.variants.length - 1] ?? null;
|
|
46
|
+
}
|
|
47
|
+
function assignExperiment(config, subjectId, options = {}) {
|
|
48
|
+
const variant = chooseExperimentVariant(config, subjectId);
|
|
49
|
+
if (!variant) return null;
|
|
50
|
+
return {
|
|
51
|
+
experimentKey: config.experimentKey,
|
|
52
|
+
variant: variant.key,
|
|
53
|
+
indexUid: variant.indexUid,
|
|
54
|
+
sessionId: options.sessionId ?? subjectId,
|
|
55
|
+
config
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function experimentSearchMetadata(assignment) {
|
|
59
|
+
if (!assignment) return void 0;
|
|
60
|
+
return {
|
|
61
|
+
experimentKey: assignment.experimentKey,
|
|
62
|
+
variant: assignment.variant,
|
|
63
|
+
...assignment.sessionId ? { sessionId: assignment.sessionId } : {}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
3
67
|
// src/client.ts
|
|
4
68
|
var HeadbandError = class extends Error {
|
|
5
69
|
constructor(message, status, code) {
|
|
@@ -11,22 +75,20 @@ var HeadbandError = class extends Error {
|
|
|
11
75
|
};
|
|
12
76
|
function headband(host, apiKey) {
|
|
13
77
|
const baseUrl = host.replace(/\/+$/, "");
|
|
14
|
-
async function request(path,
|
|
78
|
+
async function request(path, options = {}) {
|
|
79
|
+
const method = options.method ?? "POST";
|
|
15
80
|
let response;
|
|
16
81
|
try {
|
|
17
82
|
response = await fetch(`${baseUrl}${path}`, {
|
|
18
|
-
method
|
|
83
|
+
method,
|
|
19
84
|
headers: {
|
|
20
|
-
"Content-Type": "application/json",
|
|
85
|
+
...method === "POST" ? { "Content-Type": "application/json" } : {},
|
|
21
86
|
Authorization: `Bearer ${apiKey}`
|
|
22
87
|
},
|
|
23
|
-
body: JSON.stringify(body)
|
|
88
|
+
...method === "POST" ? { body: JSON.stringify(options.body ?? {}) } : {}
|
|
24
89
|
});
|
|
25
90
|
} catch (err) {
|
|
26
|
-
throw new HeadbandError(
|
|
27
|
-
err instanceof Error ? err.message : "Network request failed",
|
|
28
|
-
0
|
|
29
|
-
);
|
|
91
|
+
throw new HeadbandError(err instanceof Error ? err.message : "Network request failed", 0);
|
|
30
92
|
}
|
|
31
93
|
if (!response.ok) {
|
|
32
94
|
let errorBody = {};
|
|
@@ -45,25 +107,110 @@ function headband(host, apiKey) {
|
|
|
45
107
|
return {
|
|
46
108
|
search(index, query, params) {
|
|
47
109
|
return request("/v1/search", {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
body: {
|
|
111
|
+
index,
|
|
112
|
+
q: query,
|
|
113
|
+
...params
|
|
114
|
+
}
|
|
51
115
|
});
|
|
52
116
|
},
|
|
53
117
|
multiSearch(queries) {
|
|
54
|
-
return request("/v1/multi-search", { queries });
|
|
118
|
+
return request("/v1/multi-search", { body: { queries } });
|
|
119
|
+
},
|
|
120
|
+
async getExperimentConfigs(query) {
|
|
121
|
+
const response = await request(
|
|
122
|
+
buildExperimentConfigPath(query),
|
|
123
|
+
{ method: "GET" }
|
|
124
|
+
);
|
|
125
|
+
return { experiments: response.data?.experiments ?? [] };
|
|
126
|
+
},
|
|
127
|
+
async assignExperiment(query) {
|
|
128
|
+
const response = await request(
|
|
129
|
+
"/v1/experiments/assign",
|
|
130
|
+
{ body: query }
|
|
131
|
+
);
|
|
132
|
+
if (!response.data) {
|
|
133
|
+
throw new HeadbandError("Experiment assignment response missing data", 500);
|
|
134
|
+
}
|
|
135
|
+
return response.data;
|
|
136
|
+
},
|
|
137
|
+
async recordExperimentEvent(event) {
|
|
138
|
+
const response = await request("/v1/experiments/events", {
|
|
139
|
+
body: event
|
|
140
|
+
});
|
|
141
|
+
if (!response.data) {
|
|
142
|
+
throw new HeadbandError("Experiment event response missing data", 500);
|
|
143
|
+
}
|
|
144
|
+
return response.data;
|
|
145
|
+
},
|
|
146
|
+
warm(target, opts) {
|
|
147
|
+
return request("/v1/warm", {
|
|
148
|
+
body: { ...target, ...opts?.level ? { level: opts.level } : {} }
|
|
149
|
+
});
|
|
55
150
|
}
|
|
56
151
|
};
|
|
57
152
|
}
|
|
58
153
|
|
|
154
|
+
// src/filters.ts
|
|
155
|
+
function quoteFilterValue(value) {
|
|
156
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
157
|
+
return JSON.stringify(value);
|
|
158
|
+
}
|
|
159
|
+
function filterEq(attribute, value) {
|
|
160
|
+
return `${attribute} = ${quoteFilterValue(value)}`;
|
|
161
|
+
}
|
|
162
|
+
function filterNotEq(attribute, value) {
|
|
163
|
+
return `${attribute} != ${quoteFilterValue(value)}`;
|
|
164
|
+
}
|
|
165
|
+
function filterIn(attribute, values) {
|
|
166
|
+
return `${attribute} IN [${values.map(quoteFilterValue).join(", ")}]`;
|
|
167
|
+
}
|
|
168
|
+
function filterExists(attribute) {
|
|
169
|
+
return `${attribute} EXISTS`;
|
|
170
|
+
}
|
|
171
|
+
function filterRange(attribute, range) {
|
|
172
|
+
const parts = [];
|
|
173
|
+
const gte = range.gte ?? range.min;
|
|
174
|
+
const lte = range.lte ?? range.max;
|
|
175
|
+
if (range.gt !== void 0) parts.push(`${attribute} > ${range.gt}`);
|
|
176
|
+
if (gte !== void 0) parts.push(`${attribute} >= ${gte}`);
|
|
177
|
+
if (range.lt !== void 0) parts.push(`${attribute} < ${range.lt}`);
|
|
178
|
+
if (lte !== void 0) parts.push(`${attribute} <= ${lte}`);
|
|
179
|
+
return parts.length ? andFilters(...parts) : void 0;
|
|
180
|
+
}
|
|
181
|
+
function normalizeFilter(input) {
|
|
182
|
+
if (!input) return [];
|
|
183
|
+
return (Array.isArray(input) ? input : [input]).filter((part) => Boolean(part));
|
|
184
|
+
}
|
|
185
|
+
function andFilters(...filters) {
|
|
186
|
+
const parts = filters.flatMap(normalizeFilter);
|
|
187
|
+
if (parts.length === 0) return void 0;
|
|
188
|
+
if (parts.length === 1) return parts[0];
|
|
189
|
+
return parts.map((part) => `(${part})`).join(" AND ");
|
|
190
|
+
}
|
|
191
|
+
function orFilters(...filters) {
|
|
192
|
+
const parts = filters.flatMap(normalizeFilter);
|
|
193
|
+
if (parts.length === 0) return void 0;
|
|
194
|
+
if (parts.length === 1) return parts[0];
|
|
195
|
+
return parts.map((part) => `(${part})`).join(" OR ");
|
|
196
|
+
}
|
|
197
|
+
function combineFilters(baseFilter, generatedFilter) {
|
|
198
|
+
const base = normalizeFilter(baseFilter);
|
|
199
|
+
const generated = normalizeFilter(generatedFilter);
|
|
200
|
+
const combined = [...base, ...generated];
|
|
201
|
+
if (combined.length === 0) return void 0;
|
|
202
|
+
if (combined.length === 1) return combined[0];
|
|
203
|
+
return combined;
|
|
204
|
+
}
|
|
205
|
+
|
|
59
206
|
// src/provider.tsx
|
|
60
207
|
import {
|
|
61
208
|
createContext,
|
|
62
209
|
useContext,
|
|
63
210
|
useCallback,
|
|
64
211
|
useEffect,
|
|
65
|
-
useRef,
|
|
66
212
|
useMemo,
|
|
213
|
+
useRef,
|
|
67
214
|
useState
|
|
68
215
|
} from "react";
|
|
69
216
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -71,9 +218,7 @@ var HeadbandContext = createContext(null);
|
|
|
71
218
|
function useHeadbandContext() {
|
|
72
219
|
const ctx = useContext(HeadbandContext);
|
|
73
220
|
if (!ctx) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
"useHeadbandContext must be used within a <HeadbandProvider>."
|
|
76
|
-
);
|
|
221
|
+
throw new Error("useHeadbandContext must be used within a <HeadbandProvider>.");
|
|
77
222
|
}
|
|
78
223
|
return ctx;
|
|
79
224
|
}
|
|
@@ -83,18 +228,19 @@ function buildFilterString(refinements, ranges) {
|
|
|
83
228
|
if (values.size === 0) return;
|
|
84
229
|
if (values.size === 1) {
|
|
85
230
|
const [v] = values;
|
|
86
|
-
parts.push(
|
|
231
|
+
parts.push(filterEq(attribute, v));
|
|
87
232
|
} else {
|
|
88
|
-
|
|
89
|
-
parts.push(`${attribute} IN [${list}]`);
|
|
233
|
+
parts.push(filterIn(attribute, Array.from(values)));
|
|
90
234
|
}
|
|
91
235
|
});
|
|
92
236
|
ranges.forEach((range, attribute) => {
|
|
93
237
|
if (range.min !== void 0) {
|
|
94
|
-
|
|
238
|
+
const generated = filterRange(attribute, { min: range.min });
|
|
239
|
+
if (generated) parts.push(generated);
|
|
95
240
|
}
|
|
96
241
|
if (range.max !== void 0) {
|
|
97
|
-
|
|
242
|
+
const generated = filterRange(attribute, { max: range.max });
|
|
243
|
+
if (generated) parts.push(generated);
|
|
98
244
|
}
|
|
99
245
|
});
|
|
100
246
|
return parts.length > 0 ? parts.join(" AND ") : void 0;
|
|
@@ -103,90 +249,92 @@ function HeadbandProvider({
|
|
|
103
249
|
client,
|
|
104
250
|
index,
|
|
105
251
|
initialQuery = "",
|
|
252
|
+
experiment = null,
|
|
253
|
+
baseFilter,
|
|
254
|
+
mode,
|
|
106
255
|
children
|
|
107
256
|
}) {
|
|
108
257
|
const [query, setQueryRaw] = useState(initialQuery);
|
|
109
258
|
const [results, setResults] = useState(null);
|
|
110
259
|
const [isLoading, setIsLoading] = useState(false);
|
|
111
260
|
const [error, setError] = useState(null);
|
|
112
|
-
const [refinements, setRefinements] = useState(
|
|
113
|
-
() => /* @__PURE__ */ new Map()
|
|
114
|
-
);
|
|
261
|
+
const [refinements, setRefinements] = useState(() => /* @__PURE__ */ new Map());
|
|
115
262
|
const [ranges, setRanges] = useState(() => /* @__PURE__ */ new Map());
|
|
116
263
|
const [sortBy, setSortBy] = useState(null);
|
|
117
264
|
const [page, setPage] = useState(0);
|
|
118
265
|
const [hitsPerPage, setHitsPerPage] = useState(20);
|
|
119
266
|
const [facets, setFacets] = useState(() => /* @__PURE__ */ new Set());
|
|
120
|
-
const timerRef = useRef(null);
|
|
121
267
|
const requestIdRef = useRef(0);
|
|
268
|
+
const generatedFilter = useMemo(
|
|
269
|
+
() => buildFilterString(refinements, ranges),
|
|
270
|
+
[refinements, ranges]
|
|
271
|
+
);
|
|
272
|
+
const filter = useMemo(
|
|
273
|
+
() => combineFilters(baseFilter, generatedFilter),
|
|
274
|
+
[baseFilter, generatedFilter]
|
|
275
|
+
);
|
|
276
|
+
const facetList = useMemo(() => facets.size > 0 ? Array.from(facets) : void 0, [facets]);
|
|
277
|
+
const sortList = useMemo(() => sortBy ? [sortBy] : void 0, [sortBy]);
|
|
122
278
|
const executeSearch = useCallback(() => {
|
|
123
|
-
const
|
|
279
|
+
const requestId = ++requestIdRef.current;
|
|
124
280
|
setIsLoading(true);
|
|
125
281
|
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
282
|
const params = {
|
|
130
283
|
limit: hitsPerPage,
|
|
131
284
|
offset: page * hitsPerPage,
|
|
132
285
|
...filter ? { filter } : {},
|
|
133
286
|
...facetList ? { facets: facetList } : {},
|
|
134
287
|
...sortList ? { sort: sortList } : {},
|
|
288
|
+
...experiment ? experiment : {},
|
|
289
|
+
...mode ? { mode } : {},
|
|
135
290
|
attributesToHighlight: ["*"]
|
|
136
291
|
};
|
|
137
292
|
client.search(index, query, params).then((res) => {
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
setIsLoading(false);
|
|
293
|
+
if (requestId !== requestIdRef.current) {
|
|
294
|
+
return;
|
|
141
295
|
}
|
|
296
|
+
setResults(res);
|
|
297
|
+
setIsLoading(false);
|
|
142
298
|
}).catch((err) => {
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
setIsLoading(false);
|
|
299
|
+
if (requestId !== requestIdRef.current) {
|
|
300
|
+
return;
|
|
146
301
|
}
|
|
302
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
303
|
+
setIsLoading(false);
|
|
147
304
|
});
|
|
148
|
-
}, [client, index, query,
|
|
305
|
+
}, [client, index, query, filter, facetList, sortList, experiment, mode, page, hitsPerPage]);
|
|
149
306
|
useEffect(() => {
|
|
150
|
-
|
|
151
|
-
clearTimeout(timerRef.current);
|
|
152
|
-
}
|
|
153
|
-
timerRef.current = setTimeout(executeSearch, 200);
|
|
307
|
+
const timer = setTimeout(executeSearch, 200);
|
|
154
308
|
return () => {
|
|
155
|
-
|
|
309
|
+
clearTimeout(timer);
|
|
156
310
|
};
|
|
157
311
|
}, [executeSearch]);
|
|
158
312
|
const setQuery = useCallback((q) => {
|
|
159
313
|
setQueryRaw(q);
|
|
160
314
|
setPage(0);
|
|
161
315
|
}, []);
|
|
162
|
-
const toggleRefinement = useCallback(
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return next;
|
|
185
|
-
});
|
|
186
|
-
setPage(0);
|
|
187
|
-
},
|
|
188
|
-
[]
|
|
189
|
-
);
|
|
316
|
+
const toggleRefinement = useCallback((attribute, value2) => {
|
|
317
|
+
setRefinements((prev) => {
|
|
318
|
+
const next = new Map(prev);
|
|
319
|
+
const current = new Set(next.get(attribute) ?? []);
|
|
320
|
+
if (current.has(value2)) {
|
|
321
|
+
current.delete(value2);
|
|
322
|
+
} else {
|
|
323
|
+
current.add(value2);
|
|
324
|
+
}
|
|
325
|
+
next.set(attribute, current);
|
|
326
|
+
return next;
|
|
327
|
+
});
|
|
328
|
+
setPage(0);
|
|
329
|
+
}, []);
|
|
330
|
+
const setRange = useCallback((attribute, range) => {
|
|
331
|
+
setRanges((prev) => {
|
|
332
|
+
const next = new Map(prev);
|
|
333
|
+
next.set(attribute, range);
|
|
334
|
+
return next;
|
|
335
|
+
});
|
|
336
|
+
setPage(0);
|
|
337
|
+
}, []);
|
|
190
338
|
const clearRange = useCallback((attribute) => {
|
|
191
339
|
setRanges((prev) => {
|
|
192
340
|
const next = new Map(prev);
|
|
@@ -242,7 +390,18 @@ function HeadbandProvider({
|
|
|
242
390
|
unregisterFacet,
|
|
243
391
|
refresh
|
|
244
392
|
}),
|
|
245
|
-
[
|
|
393
|
+
[
|
|
394
|
+
setQuery,
|
|
395
|
+
setPage,
|
|
396
|
+
setSortBy,
|
|
397
|
+
setHitsPerPage,
|
|
398
|
+
toggleRefinement,
|
|
399
|
+
setRange,
|
|
400
|
+
clearRange,
|
|
401
|
+
registerFacet,
|
|
402
|
+
unregisterFacet,
|
|
403
|
+
refresh
|
|
404
|
+
]
|
|
246
405
|
);
|
|
247
406
|
const value = useMemo(
|
|
248
407
|
() => ({ client, index, state, actions }),
|
|
@@ -304,18 +463,19 @@ function useRefinementList({
|
|
|
304
463
|
actions.unregisterFacet(attribute);
|
|
305
464
|
};
|
|
306
465
|
}, [attribute, actions]);
|
|
307
|
-
const refined = state.refinements.get(attribute)
|
|
466
|
+
const refined = state.refinements.get(attribute);
|
|
308
467
|
const items = useMemo3(() => {
|
|
468
|
+
const activeRefinements = refined ?? /* @__PURE__ */ new Set();
|
|
309
469
|
const distribution = state.results?.facetDistribution?.[attribute] ?? {};
|
|
310
470
|
const allValues = /* @__PURE__ */ new Set([
|
|
311
471
|
...Object.keys(distribution),
|
|
312
|
-
...
|
|
472
|
+
...activeRefinements
|
|
313
473
|
]);
|
|
314
474
|
let result = Array.from(allValues).map(
|
|
315
475
|
(value) => ({
|
|
316
476
|
value,
|
|
317
477
|
count: distribution[value] ?? 0,
|
|
318
|
-
isRefined:
|
|
478
|
+
isRefined: activeRefinements.has(value)
|
|
319
479
|
})
|
|
320
480
|
);
|
|
321
481
|
if (sortOrder === "alpha") {
|
|
@@ -618,7 +778,11 @@ function SearchBox({
|
|
|
618
778
|
// src/components/Hits.tsx
|
|
619
779
|
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
620
780
|
function DefaultHit({ hit }) {
|
|
621
|
-
const
|
|
781
|
+
const rest = Object.fromEntries(
|
|
782
|
+
Object.entries(hit).filter(
|
|
783
|
+
([key]) => key !== "_formatted" && key !== "__data"
|
|
784
|
+
)
|
|
785
|
+
);
|
|
622
786
|
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
787
|
}
|
|
624
788
|
function DefaultEmpty() {
|
|
@@ -679,7 +843,7 @@ function RefinementList({
|
|
|
679
843
|
sortBy
|
|
680
844
|
});
|
|
681
845
|
const handleChange = useCallback8(
|
|
682
|
-
(value) => (
|
|
846
|
+
(value) => () => {
|
|
683
847
|
refine(value);
|
|
684
848
|
},
|
|
685
849
|
[refine]
|
|
@@ -908,11 +1072,7 @@ function Highlight({
|
|
|
908
1072
|
}
|
|
909
1073
|
|
|
910
1074
|
// src/components/RangeInput.tsx
|
|
911
|
-
import {
|
|
912
|
-
useState as useState2,
|
|
913
|
-
useCallback as useCallback11,
|
|
914
|
-
useEffect as useEffect3
|
|
915
|
-
} from "react";
|
|
1075
|
+
import { useCallback as useCallback11 } from "react";
|
|
916
1076
|
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
917
1077
|
function RangeInput({
|
|
918
1078
|
attribute,
|
|
@@ -923,17 +1083,15 @@ function RangeInput({
|
|
|
923
1083
|
maxPlaceholder = "Max"
|
|
924
1084
|
}) {
|
|
925
1085
|
const { min, max, stats, setRange, clear } = useRange({ attribute });
|
|
926
|
-
const
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
setLocalMin(min !== void 0 ? String(min) : "");
|
|
930
|
-
}, [min]);
|
|
931
|
-
useEffect3(() => {
|
|
932
|
-
setLocalMax(max !== void 0 ? String(max) : "");
|
|
933
|
-
}, [max]);
|
|
1086
|
+
const defaultMin = min !== void 0 ? String(min) : "";
|
|
1087
|
+
const defaultMax = max !== void 0 ? String(max) : "";
|
|
1088
|
+
const formKey = `${attribute}:${defaultMin}:${defaultMax}`;
|
|
934
1089
|
const handleSubmit = useCallback11(
|
|
935
1090
|
(e) => {
|
|
936
1091
|
e.preventDefault();
|
|
1092
|
+
const formData = new FormData(e.currentTarget);
|
|
1093
|
+
const localMin = String(formData.get("min") ?? "");
|
|
1094
|
+
const localMax = String(formData.get("max") ?? "");
|
|
937
1095
|
const parsedMin = localMin !== "" ? Number(localMin) : void 0;
|
|
938
1096
|
const parsedMax = localMax !== "" ? Number(localMax) : void 0;
|
|
939
1097
|
if (parsedMin === void 0 && parsedMax === void 0) {
|
|
@@ -945,15 +1103,7 @@ function RangeInput({
|
|
|
945
1103
|
});
|
|
946
1104
|
}
|
|
947
1105
|
},
|
|
948
|
-
[
|
|
949
|
-
);
|
|
950
|
-
const handleMinChange = useCallback11(
|
|
951
|
-
(e) => setLocalMin(e.target.value),
|
|
952
|
-
[]
|
|
953
|
-
);
|
|
954
|
-
const handleMaxChange = useCallback11(
|
|
955
|
-
(e) => setLocalMax(e.target.value),
|
|
956
|
-
[]
|
|
1106
|
+
[setRange, clear]
|
|
957
1107
|
);
|
|
958
1108
|
const containerClass = styled ? `space-y-2 ${className ?? ""}`.trim() : className ?? "";
|
|
959
1109
|
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" : "";
|
|
@@ -988,9 +1138,9 @@ function RangeInput({
|
|
|
988
1138
|
/* @__PURE__ */ jsx9(
|
|
989
1139
|
"input",
|
|
990
1140
|
{
|
|
1141
|
+
name: "min",
|
|
991
1142
|
type: "number",
|
|
992
|
-
|
|
993
|
-
onChange: handleMinChange,
|
|
1143
|
+
defaultValue: defaultMin,
|
|
994
1144
|
placeholder: minPlaceholder,
|
|
995
1145
|
"aria-label": `Minimum ${attribute}`,
|
|
996
1146
|
className: inputClass
|
|
@@ -1007,9 +1157,9 @@ function RangeInput({
|
|
|
1007
1157
|
/* @__PURE__ */ jsx9(
|
|
1008
1158
|
"input",
|
|
1009
1159
|
{
|
|
1160
|
+
name: "max",
|
|
1010
1161
|
type: "number",
|
|
1011
|
-
|
|
1012
|
-
onChange: handleMaxChange,
|
|
1162
|
+
defaultValue: defaultMax,
|
|
1013
1163
|
placeholder: maxPlaceholder,
|
|
1014
1164
|
"aria-label": `Maximum ${attribute}`,
|
|
1015
1165
|
className: inputClass
|
|
@@ -1018,7 +1168,8 @@ function RangeInput({
|
|
|
1018
1168
|
/* @__PURE__ */ jsx9("button", { type: "submit", className: buttonClass, children: "Go" })
|
|
1019
1169
|
] })
|
|
1020
1170
|
]
|
|
1021
|
-
}
|
|
1171
|
+
},
|
|
1172
|
+
formKey
|
|
1022
1173
|
);
|
|
1023
1174
|
}
|
|
1024
1175
|
|
|
@@ -1078,7 +1229,21 @@ export {
|
|
|
1078
1229
|
SearchBox,
|
|
1079
1230
|
SortBy,
|
|
1080
1231
|
Stats,
|
|
1232
|
+
andFilters,
|
|
1233
|
+
assignExperiment,
|
|
1234
|
+
buildExperimentConfigPath,
|
|
1235
|
+
chooseExperimentVariant,
|
|
1236
|
+
combineFilters,
|
|
1237
|
+
experimentSearchMetadata,
|
|
1238
|
+
filterEq,
|
|
1239
|
+
filterExists,
|
|
1240
|
+
filterIn,
|
|
1241
|
+
filterNotEq,
|
|
1242
|
+
filterRange,
|
|
1081
1243
|
headband,
|
|
1244
|
+
isRunnableExperiment,
|
|
1245
|
+
orFilters,
|
|
1246
|
+
quoteFilterValue,
|
|
1082
1247
|
useHeadbandContext,
|
|
1083
1248
|
useHighlight,
|
|
1084
1249
|
useHits,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@headband/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React hooks and components for Headband search — an InstantSearch-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React hooks and components for Headband search — an InstantSearch-inspired SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|