@headband/react 0.1.1 → 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 +280 -103
- package/dist/index.mjs +268 -109
- 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,96 +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);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
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]);
|
|
128
278
|
const executeSearch = useCallback(() => {
|
|
129
|
-
const
|
|
279
|
+
const requestId = ++requestIdRef.current;
|
|
130
280
|
setIsLoading(true);
|
|
131
281
|
setError(null);
|
|
132
|
-
const filter = buildFilterString(refinementsRef.current, rangesRef.current);
|
|
133
|
-
const facetList = facetsRef.current.size > 0 ? Array.from(facetsRef.current) : void 0;
|
|
134
|
-
const sortList = sortBy ? [sortBy] : void 0;
|
|
135
282
|
const params = {
|
|
136
283
|
limit: hitsPerPage,
|
|
137
284
|
offset: page * hitsPerPage,
|
|
138
285
|
...filter ? { filter } : {},
|
|
139
286
|
...facetList ? { facets: facetList } : {},
|
|
140
287
|
...sortList ? { sort: sortList } : {},
|
|
288
|
+
...experiment ? experiment : {},
|
|
289
|
+
...mode ? { mode } : {},
|
|
141
290
|
attributesToHighlight: ["*"]
|
|
142
291
|
};
|
|
143
292
|
client.search(index, query, params).then((res) => {
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
setIsLoading(false);
|
|
293
|
+
if (requestId !== requestIdRef.current) {
|
|
294
|
+
return;
|
|
147
295
|
}
|
|
296
|
+
setResults(res);
|
|
297
|
+
setIsLoading(false);
|
|
148
298
|
}).catch((err) => {
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
setIsLoading(false);
|
|
299
|
+
if (requestId !== requestIdRef.current) {
|
|
300
|
+
return;
|
|
152
301
|
}
|
|
302
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
303
|
+
setIsLoading(false);
|
|
153
304
|
});
|
|
154
|
-
}, [client, index, query,
|
|
305
|
+
}, [client, index, query, filter, facetList, sortList, experiment, mode, page, hitsPerPage]);
|
|
155
306
|
useEffect(() => {
|
|
156
|
-
|
|
157
|
-
clearTimeout(timerRef.current);
|
|
158
|
-
}
|
|
159
|
-
timerRef.current = setTimeout(executeSearch, 200);
|
|
307
|
+
const timer = setTimeout(executeSearch, 200);
|
|
160
308
|
return () => {
|
|
161
|
-
|
|
309
|
+
clearTimeout(timer);
|
|
162
310
|
};
|
|
163
|
-
}, [executeSearch
|
|
311
|
+
}, [executeSearch]);
|
|
164
312
|
const setQuery = useCallback((q) => {
|
|
165
313
|
setQueryRaw(q);
|
|
166
314
|
setPage(0);
|
|
167
315
|
}, []);
|
|
168
|
-
const toggleRefinement = useCallback(
|
|
169
|
-
(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return next;
|
|
191
|
-
});
|
|
192
|
-
setPage(0);
|
|
193
|
-
},
|
|
194
|
-
[]
|
|
195
|
-
);
|
|
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
|
+
}, []);
|
|
196
338
|
const clearRange = useCallback((attribute) => {
|
|
197
339
|
setRanges((prev) => {
|
|
198
340
|
const next = new Map(prev);
|
|
@@ -248,7 +390,18 @@ function HeadbandProvider({
|
|
|
248
390
|
unregisterFacet,
|
|
249
391
|
refresh
|
|
250
392
|
}),
|
|
251
|
-
[
|
|
393
|
+
[
|
|
394
|
+
setQuery,
|
|
395
|
+
setPage,
|
|
396
|
+
setSortBy,
|
|
397
|
+
setHitsPerPage,
|
|
398
|
+
toggleRefinement,
|
|
399
|
+
setRange,
|
|
400
|
+
clearRange,
|
|
401
|
+
registerFacet,
|
|
402
|
+
unregisterFacet,
|
|
403
|
+
refresh
|
|
404
|
+
]
|
|
252
405
|
);
|
|
253
406
|
const value = useMemo(
|
|
254
407
|
() => ({ client, index, state, actions }),
|
|
@@ -310,18 +463,19 @@ function useRefinementList({
|
|
|
310
463
|
actions.unregisterFacet(attribute);
|
|
311
464
|
};
|
|
312
465
|
}, [attribute, actions]);
|
|
313
|
-
const refined = state.refinements.get(attribute)
|
|
466
|
+
const refined = state.refinements.get(attribute);
|
|
314
467
|
const items = useMemo3(() => {
|
|
468
|
+
const activeRefinements = refined ?? /* @__PURE__ */ new Set();
|
|
315
469
|
const distribution = state.results?.facetDistribution?.[attribute] ?? {};
|
|
316
470
|
const allValues = /* @__PURE__ */ new Set([
|
|
317
471
|
...Object.keys(distribution),
|
|
318
|
-
...
|
|
472
|
+
...activeRefinements
|
|
319
473
|
]);
|
|
320
474
|
let result = Array.from(allValues).map(
|
|
321
475
|
(value) => ({
|
|
322
476
|
value,
|
|
323
477
|
count: distribution[value] ?? 0,
|
|
324
|
-
isRefined:
|
|
478
|
+
isRefined: activeRefinements.has(value)
|
|
325
479
|
})
|
|
326
480
|
);
|
|
327
481
|
if (sortOrder === "alpha") {
|
|
@@ -624,7 +778,11 @@ function SearchBox({
|
|
|
624
778
|
// src/components/Hits.tsx
|
|
625
779
|
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
626
780
|
function DefaultHit({ hit }) {
|
|
627
|
-
const
|
|
781
|
+
const rest = Object.fromEntries(
|
|
782
|
+
Object.entries(hit).filter(
|
|
783
|
+
([key]) => key !== "_formatted" && key !== "__data"
|
|
784
|
+
)
|
|
785
|
+
);
|
|
628
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) }) });
|
|
629
787
|
}
|
|
630
788
|
function DefaultEmpty() {
|
|
@@ -685,7 +843,7 @@ function RefinementList({
|
|
|
685
843
|
sortBy
|
|
686
844
|
});
|
|
687
845
|
const handleChange = useCallback8(
|
|
688
|
-
(value) => (
|
|
846
|
+
(value) => () => {
|
|
689
847
|
refine(value);
|
|
690
848
|
},
|
|
691
849
|
[refine]
|
|
@@ -914,11 +1072,7 @@ function Highlight({
|
|
|
914
1072
|
}
|
|
915
1073
|
|
|
916
1074
|
// src/components/RangeInput.tsx
|
|
917
|
-
import {
|
|
918
|
-
useState as useState2,
|
|
919
|
-
useCallback as useCallback11,
|
|
920
|
-
useEffect as useEffect3
|
|
921
|
-
} from "react";
|
|
1075
|
+
import { useCallback as useCallback11 } from "react";
|
|
922
1076
|
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
923
1077
|
function RangeInput({
|
|
924
1078
|
attribute,
|
|
@@ -929,17 +1083,15 @@ function RangeInput({
|
|
|
929
1083
|
maxPlaceholder = "Max"
|
|
930
1084
|
}) {
|
|
931
1085
|
const { min, max, stats, setRange, clear } = useRange({ attribute });
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
setLocalMin(min !== void 0 ? String(min) : "");
|
|
936
|
-
}, [min]);
|
|
937
|
-
useEffect3(() => {
|
|
938
|
-
setLocalMax(max !== void 0 ? String(max) : "");
|
|
939
|
-
}, [max]);
|
|
1086
|
+
const defaultMin = min !== void 0 ? String(min) : "";
|
|
1087
|
+
const defaultMax = max !== void 0 ? String(max) : "";
|
|
1088
|
+
const formKey = `${attribute}:${defaultMin}:${defaultMax}`;
|
|
940
1089
|
const handleSubmit = useCallback11(
|
|
941
1090
|
(e) => {
|
|
942
1091
|
e.preventDefault();
|
|
1092
|
+
const formData = new FormData(e.currentTarget);
|
|
1093
|
+
const localMin = String(formData.get("min") ?? "");
|
|
1094
|
+
const localMax = String(formData.get("max") ?? "");
|
|
943
1095
|
const parsedMin = localMin !== "" ? Number(localMin) : void 0;
|
|
944
1096
|
const parsedMax = localMax !== "" ? Number(localMax) : void 0;
|
|
945
1097
|
if (parsedMin === void 0 && parsedMax === void 0) {
|
|
@@ -951,15 +1103,7 @@ function RangeInput({
|
|
|
951
1103
|
});
|
|
952
1104
|
}
|
|
953
1105
|
},
|
|
954
|
-
[
|
|
955
|
-
);
|
|
956
|
-
const handleMinChange = useCallback11(
|
|
957
|
-
(e) => setLocalMin(e.target.value),
|
|
958
|
-
[]
|
|
959
|
-
);
|
|
960
|
-
const handleMaxChange = useCallback11(
|
|
961
|
-
(e) => setLocalMax(e.target.value),
|
|
962
|
-
[]
|
|
1106
|
+
[setRange, clear]
|
|
963
1107
|
);
|
|
964
1108
|
const containerClass = styled ? `space-y-2 ${className ?? ""}`.trim() : className ?? "";
|
|
965
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" : "";
|
|
@@ -994,9 +1138,9 @@ function RangeInput({
|
|
|
994
1138
|
/* @__PURE__ */ jsx9(
|
|
995
1139
|
"input",
|
|
996
1140
|
{
|
|
1141
|
+
name: "min",
|
|
997
1142
|
type: "number",
|
|
998
|
-
|
|
999
|
-
onChange: handleMinChange,
|
|
1143
|
+
defaultValue: defaultMin,
|
|
1000
1144
|
placeholder: minPlaceholder,
|
|
1001
1145
|
"aria-label": `Minimum ${attribute}`,
|
|
1002
1146
|
className: inputClass
|
|
@@ -1013,9 +1157,9 @@ function RangeInput({
|
|
|
1013
1157
|
/* @__PURE__ */ jsx9(
|
|
1014
1158
|
"input",
|
|
1015
1159
|
{
|
|
1160
|
+
name: "max",
|
|
1016
1161
|
type: "number",
|
|
1017
|
-
|
|
1018
|
-
onChange: handleMaxChange,
|
|
1162
|
+
defaultValue: defaultMax,
|
|
1019
1163
|
placeholder: maxPlaceholder,
|
|
1020
1164
|
"aria-label": `Maximum ${attribute}`,
|
|
1021
1165
|
className: inputClass
|
|
@@ -1024,7 +1168,8 @@ function RangeInput({
|
|
|
1024
1168
|
/* @__PURE__ */ jsx9("button", { type: "submit", className: buttonClass, children: "Go" })
|
|
1025
1169
|
] })
|
|
1026
1170
|
]
|
|
1027
|
-
}
|
|
1171
|
+
},
|
|
1172
|
+
formKey
|
|
1028
1173
|
);
|
|
1029
1174
|
}
|
|
1030
1175
|
|
|
@@ -1084,7 +1229,21 @@ export {
|
|
|
1084
1229
|
SearchBox,
|
|
1085
1230
|
SortBy,
|
|
1086
1231
|
Stats,
|
|
1232
|
+
andFilters,
|
|
1233
|
+
assignExperiment,
|
|
1234
|
+
buildExperimentConfigPath,
|
|
1235
|
+
chooseExperimentVariant,
|
|
1236
|
+
combineFilters,
|
|
1237
|
+
experimentSearchMetadata,
|
|
1238
|
+
filterEq,
|
|
1239
|
+
filterExists,
|
|
1240
|
+
filterIn,
|
|
1241
|
+
filterNotEq,
|
|
1242
|
+
filterRange,
|
|
1087
1243
|
headband,
|
|
1244
|
+
isRunnableExperiment,
|
|
1245
|
+
orFilters,
|
|
1246
|
+
quoteFilterValue,
|
|
1088
1247
|
useHeadbandContext,
|
|
1089
1248
|
useHighlight,
|
|
1090
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",
|