@findlane/browser 0.2.0 → 0.3.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.d.ts +21 -0
- package/dist/index.js +13 -4
- package/dist/react/components.d.ts +10 -0
- package/dist/react/components.js +11 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -2
- package/dist/react/search-provider.d.ts +11 -0
- package/dist/react/search-provider.js +26 -0
- package/dist/react/store.d.ts +3 -1
- package/dist/react/store.js +4 -0
- package/dist/react/styles.css +15 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,20 @@ export type SearchResponse<T> = {
|
|
|
39
39
|
facets: Record<string, Record<string, number>>;
|
|
40
40
|
tookMs: number;
|
|
41
41
|
sort?: string;
|
|
42
|
+
/** With the index's `didYouMean` setting: the query with the words typo tolerance corrected, when the
|
|
43
|
+
* results matched some words only by typo ("Showing results for …"), else null. */
|
|
44
|
+
didYouMean?: DidYouMean | null;
|
|
45
|
+
};
|
|
46
|
+
/** A corrected query. `applied`: the results are already for `q` (a search's "Showing results for");
|
|
47
|
+
* otherwise searching `q` finds results that the query didn't ("Did you mean"). `changes` lists each
|
|
48
|
+
* changed part of the query as typed. */
|
|
49
|
+
export type DidYouMean = {
|
|
50
|
+
q: string;
|
|
51
|
+
applied: boolean;
|
|
52
|
+
changes: {
|
|
53
|
+
from: string;
|
|
54
|
+
to: string;
|
|
55
|
+
}[];
|
|
42
56
|
};
|
|
43
57
|
export declare class FindlaneError extends Error {
|
|
44
58
|
status: number;
|
|
@@ -56,7 +70,14 @@ export declare class SearchBrowserClient {
|
|
|
56
70
|
private readonly url;
|
|
57
71
|
private readonly fetcher;
|
|
58
72
|
constructor(options: BrowserClientOptions);
|
|
73
|
+
private post;
|
|
59
74
|
search<T = Record<string, unknown>>(request: SearchRequest, options?: {
|
|
60
75
|
signal?: AbortSignal;
|
|
61
76
|
}): Promise<SearchResponse<T>>;
|
|
77
|
+
/** Did you mean: for a search that found nothing, a query that finds something, or null. Takes the same
|
|
78
|
+
* request (its q and filters matter). Needs the index's `didYouMean` setting; not counted as a search.
|
|
79
|
+
* Ask once the shopper stops typing, not for every keystroke. */
|
|
80
|
+
suggest(request: SearchRequest, options?: {
|
|
81
|
+
signal?: AbortSignal;
|
|
82
|
+
}): Promise<DidYouMean | null>;
|
|
62
83
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,19 +14,28 @@ export class SearchBrowserClient {
|
|
|
14
14
|
constructor(options) {
|
|
15
15
|
if (!/^pub_[a-f0-9]{32}$/.test(options.publicId))
|
|
16
16
|
throw new Error("A valid public index ID (pub_…) is required");
|
|
17
|
-
this.url = `${(options.baseUrl ?? defaultBaseUrl).replace(/\/$/, "")}/api/public/indexes/${options.publicId}
|
|
17
|
+
this.url = `${(options.baseUrl ?? defaultBaseUrl).replace(/\/$/, "")}/api/public/indexes/${options.publicId}`;
|
|
18
18
|
this.fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
19
19
|
}
|
|
20
|
-
async
|
|
21
|
-
const response = await this.fetcher(this.url
|
|
20
|
+
async post(operation, request, signal) {
|
|
21
|
+
const response = await this.fetcher(`${this.url}/${operation}`, {
|
|
22
22
|
method: "POST",
|
|
23
23
|
headers: { "content-type": "application/json" },
|
|
24
24
|
body: JSON.stringify(request),
|
|
25
|
-
signal
|
|
25
|
+
signal,
|
|
26
26
|
});
|
|
27
27
|
const data = await response.json();
|
|
28
28
|
if (!response.ok)
|
|
29
29
|
throw new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
|
|
30
30
|
return data;
|
|
31
31
|
}
|
|
32
|
+
search(request, options = {}) {
|
|
33
|
+
return this.post("search", request, options.signal);
|
|
34
|
+
}
|
|
35
|
+
/** Did you mean: for a search that found nothing, a query that finds something, or null. Takes the same
|
|
36
|
+
* request (its q and filters matter). Needs the index's `didYouMean` setting; not counted as a search.
|
|
37
|
+
* Ask once the shopper stops typing, not for every keystroke. */
|
|
38
|
+
async suggest(request, options = {}) {
|
|
39
|
+
return (await this.post("suggest", request, options.signal)).didYouMean;
|
|
40
|
+
}
|
|
32
41
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import type { DidYouMean as Correction } from "../index.js";
|
|
2
3
|
import { type Refinement } from "./search-provider.js";
|
|
3
4
|
import type { Hit } from "./state.js";
|
|
4
5
|
import { useAutocomplete, type AutocompleteOptions } from "./use-autocomplete.js";
|
|
@@ -80,6 +81,15 @@ export declare function LoadMore({ label, className }: {
|
|
|
80
81
|
label?: string;
|
|
81
82
|
className?: string;
|
|
82
83
|
}): import("react").JSX.Element | null;
|
|
84
|
+
/** "Showing results for …" when typo tolerance corrected the query, and "Did you mean …?", a button that
|
|
85
|
+
* searches for it, when nothing matched. Needs the index's didYouMean setting; otherwise shows nothing. */
|
|
86
|
+
export declare function DidYouMean({ delay, format, className }: {
|
|
87
|
+
/** How long the visitor must stop typing before a suggestion is asked for (default 400 ms). */
|
|
88
|
+
delay?: number;
|
|
89
|
+
/** The line's content (default "Showing results for …" or "Did you mean …?"). */
|
|
90
|
+
format?: (didYouMean: Correction, apply: () => void) => ReactNode;
|
|
91
|
+
className?: string;
|
|
92
|
+
}): import("react").JSX.Element | null;
|
|
83
93
|
/** "1,234 results". */
|
|
84
94
|
export declare function Stats({ format, className }: {
|
|
85
95
|
format?: (total: number) => ReactNode;
|
package/dist/react/components.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Highlight } from "./highlight.js";
|
|
5
5
|
import { messageFor } from "./messages.js";
|
|
6
|
-
import { useCurrentRefinements, useHits, useLoadMore, useRefinementList, useSearchBox, useSortBy, } from "./search-provider.js";
|
|
6
|
+
import { useCurrentRefinements, useDidYouMean, useHits, useLoadMore, useRefinementList, useSearchBox, useSortBy, } from "./search-provider.js";
|
|
7
7
|
import { useAutocomplete } from "./use-autocomplete.js";
|
|
8
8
|
// The components render plain elements marked with data-findlane-* attributes and no styles of
|
|
9
9
|
// their own; styles.css gives them a default look.
|
|
@@ -74,6 +74,16 @@ export function LoadMore({ label = "Show more", className }) {
|
|
|
74
74
|
return null;
|
|
75
75
|
return (_jsxs("div", { className: className, "data-findlane-load-more": "", children: [_jsx("button", { type: "button", onClick: loadMore, disabled: isLoading, children: isLoading ? "Loading…" : label }), _jsxs("span", { children: [number(shown), " of ", number(total)] })] }));
|
|
76
76
|
}
|
|
77
|
+
/** "Showing results for …" when typo tolerance corrected the query, and "Did you mean …?", a button that
|
|
78
|
+
* searches for it, when nothing matched. Needs the index's didYouMean setting; otherwise shows nothing. */
|
|
79
|
+
export function DidYouMean({ delay, format, className }) {
|
|
80
|
+
const { didYouMean, apply } = useDidYouMean({ delay });
|
|
81
|
+
if (!didYouMean)
|
|
82
|
+
return null;
|
|
83
|
+
return (_jsx("p", { className: className, "data-findlane-did-you-mean": "", children: format ? format(didYouMean, apply) : didYouMean.applied
|
|
84
|
+
? _jsxs(_Fragment, { children: ["Showing results for ", _jsx("b", { children: didYouMean.q })] })
|
|
85
|
+
: _jsxs(_Fragment, { children: ["Did you mean ", _jsx("button", { type: "button", onClick: apply, children: didYouMean.q }), "?"] }) }));
|
|
86
|
+
}
|
|
77
87
|
/** "1,234 results". */
|
|
78
88
|
export function Stats({ format, className }) {
|
|
79
89
|
const { total, totalRelation, isLoaded } = useHits();
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { FindlaneProvider, useClient, type ClientOptions } from "./client.js";
|
|
2
2
|
export { useSearch, type UseSearchOptions, type UseSearchResult } from "./use-search.js";
|
|
3
3
|
export { useAutocomplete, type AutocompleteOptions } from "./use-autocomplete.js";
|
|
4
|
-
export { SearchProvider, useCurrentRefinements, useHits, useLoadMore, usePagination, useRange, useRefinementList, useSearchBox, useSearchState, useSortBy, useToggle, type Range, type Refinement, type SearchProviderProps, } from "./search-provider.js";
|
|
5
|
-
export { CurrentRefinements, DefaultHit, Hits, LoadMore, RefinementList, SearchBox, SearchInput, SortSelect, Stats, type HitsProps, type RefinementListProps, type SearchBoxProps, } from "./components.js";
|
|
4
|
+
export { SearchProvider, useCurrentRefinements, useDidYouMean, useHits, useLoadMore, usePagination, useRange, useRefinementList, useSearchBox, useSearchState, useSortBy, useToggle, type Range, type Refinement, type SearchProviderProps, } from "./search-provider.js";
|
|
5
|
+
export { CurrentRefinements, DefaultHit, DidYouMean, Hits, LoadMore, RefinementList, SearchBox, SearchInput, SortSelect, Stats, type HitsProps, type RefinementListProps, type SearchBoxProps, } from "./components.js";
|
|
6
6
|
export { SearchDialog, type SearchDialogProps } from "./dialog.js";
|
|
7
7
|
export { Highlight, Snippet, excerpt, highlightParts, type HighlightProps } from "./highlight.js";
|
|
8
8
|
export { messageFor } from "./messages.js";
|
package/dist/react/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
export { FindlaneProvider, useClient } from "./client.js";
|
|
4
4
|
export { useSearch } from "./use-search.js";
|
|
5
5
|
export { useAutocomplete } from "./use-autocomplete.js";
|
|
6
|
-
export { SearchProvider, useCurrentRefinements, useHits, useLoadMore, usePagination, useRange, useRefinementList, useSearchBox, useSearchState, useSortBy, useToggle, } from "./search-provider.js";
|
|
7
|
-
export { CurrentRefinements, DefaultHit, Hits, LoadMore, RefinementList, SearchBox, SearchInput, SortSelect, Stats, } from "./components.js";
|
|
6
|
+
export { SearchProvider, useCurrentRefinements, useDidYouMean, useHits, useLoadMore, usePagination, useRange, useRefinementList, useSearchBox, useSearchState, useSortBy, useToggle, } from "./search-provider.js";
|
|
7
|
+
export { CurrentRefinements, DefaultHit, DidYouMean, Hits, LoadMore, RefinementList, SearchBox, SearchInput, SortSelect, Stats, } from "./components.js";
|
|
8
8
|
export { SearchDialog } from "./dialog.js";
|
|
9
9
|
export { Highlight, Snippet, excerpt, highlightParts } from "./highlight.js";
|
|
10
10
|
export { messageFor } from "./messages.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import type { DidYouMean } from "../index.js";
|
|
2
3
|
import { type ClientOptions } from "./client.js";
|
|
3
4
|
import { type ServerResults, type StateChange } from "./store.js";
|
|
4
5
|
import { type SearchConfig, type SearchState } from "./state.js";
|
|
@@ -45,6 +46,16 @@ export declare function useHits<T = Record<string, unknown>>(): {
|
|
|
45
46
|
isPending: boolean;
|
|
46
47
|
error: unknown;
|
|
47
48
|
};
|
|
49
|
+
/** Did you mean, with the index's didYouMean setting: the query the results are for when typo tolerance
|
|
50
|
+
* corrected some words (`applied`, "Showing results for"), or, when nothing matched, a query that
|
|
51
|
+
* finds results, asked for once the visitor has stopped typing for `delay` ms. `apply` searches for it.
|
|
52
|
+
* Searches carry `didYouMean` only when the setting is on, so other indexes are never asked. */
|
|
53
|
+
export declare function useDidYouMean({ delay }?: {
|
|
54
|
+
delay?: number;
|
|
55
|
+
}): {
|
|
56
|
+
didYouMean: DidYouMean | null;
|
|
57
|
+
apply: () => void;
|
|
58
|
+
};
|
|
48
59
|
/** A facet's values with their counts, most common first. Selected values stay listed (at 0 when the
|
|
49
60
|
* query no longer matches them), so they can be removed. */
|
|
50
61
|
export declare function useRefinementList(field: string): {
|
|
@@ -94,6 +94,32 @@ export function useHits() {
|
|
|
94
94
|
error,
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
+
/** Did you mean, with the index's didYouMean setting: the query the results are for when typo tolerance
|
|
98
|
+
* corrected some words (`applied`, "Showing results for"), or, when nothing matched, a query that
|
|
99
|
+
* finds results, asked for once the visitor has stopped typing for `delay` ms. `apply` searches for it.
|
|
100
|
+
* Searches carry `didYouMean` only when the setting is on, so other indexes are never asked. */
|
|
101
|
+
export function useDidYouMean({ delay = 400 } = {}) {
|
|
102
|
+
const [{ results, pending }, store] = useSnapshot();
|
|
103
|
+
const [suggested, setSuggested] = useState(null);
|
|
104
|
+
const empty = Boolean(results && !pending && !results.response.total && results.request.q?.trim() &&
|
|
105
|
+
results.response.didYouMean !== undefined);
|
|
106
|
+
const key = results?.key;
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!empty || !results)
|
|
109
|
+
return;
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timer = setTimeout(() => store.suggest(results.request, controller.signal)
|
|
112
|
+
.then((didYouMean) => setSuggested({ key: results.key, didYouMean }), () => undefined), delay);
|
|
113
|
+
return () => { clearTimeout(timer); controller.abort(); };
|
|
114
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
+
}, [store, key, empty, delay]);
|
|
116
|
+
const didYouMean = results?.response.didYouMean ?? (empty && suggested?.key === key ? suggested.didYouMean : null);
|
|
117
|
+
return {
|
|
118
|
+
didYouMean,
|
|
119
|
+
apply: () => { if (didYouMean)
|
|
120
|
+
store.update((current) => ({ ...current, q: didYouMean.q, page: 0 })); },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
97
123
|
/** A facet's values with their counts, most common first. Selected values stay listed (at 0 when the
|
|
98
124
|
* query no longer matches them), so they can be removed. */
|
|
99
125
|
export function useRefinementList(field) {
|
package/dist/react/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SearchBrowserClient, SearchRequest, SearchResponse } from "../index.js";
|
|
1
|
+
import type { DidYouMean, SearchBrowserClient, SearchRequest, SearchResponse } from "../index.js";
|
|
2
2
|
import { type Hit, type SearchConfig, type SearchState } from "./state.js";
|
|
3
3
|
/** Results computed on the server (see ssr.ts), so the first render needn't search again. */
|
|
4
4
|
export type ServerResults<T = unknown> = {
|
|
@@ -60,6 +60,8 @@ export declare class SearchStore<T = unknown> {
|
|
|
60
60
|
stop(): void;
|
|
61
61
|
private resultsFor;
|
|
62
62
|
private search;
|
|
63
|
+
/** A did-you-mean suggestion for a request that found nothing. */
|
|
64
|
+
suggest(request: SearchRequest, signal: AbortSignal): Promise<DidYouMean | null>;
|
|
63
65
|
/** Adds the next page's hits to the current ones. */
|
|
64
66
|
loadMore(): void;
|
|
65
67
|
}
|
package/dist/react/store.js
CHANGED
|
@@ -107,6 +107,10 @@ export class SearchStore {
|
|
|
107
107
|
else
|
|
108
108
|
go();
|
|
109
109
|
}
|
|
110
|
+
/** A did-you-mean suggestion for a request that found nothing. */
|
|
111
|
+
suggest(request, signal) {
|
|
112
|
+
return this.client.suggest(request, { signal });
|
|
113
|
+
}
|
|
110
114
|
/** Adds the next page's hits to the current ones. */
|
|
111
115
|
loadMore() {
|
|
112
116
|
const { results, loadingMore, pending } = this.snapshot;
|
package/dist/react/styles.css
CHANGED
|
@@ -239,6 +239,21 @@
|
|
|
239
239
|
cursor: pointer;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
:where([data-findlane-did-you-mean]) {
|
|
243
|
+
margin: 0;
|
|
244
|
+
color: var(--findlane-muted, color-mix(in srgb, currentColor 60%, transparent));
|
|
245
|
+
}
|
|
246
|
+
:where([data-findlane-did-you-mean]) :where(button) {
|
|
247
|
+
padding: 0;
|
|
248
|
+
font: inherit;
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
color: var(--findlane-accent, #2f5bff);
|
|
251
|
+
background: none;
|
|
252
|
+
border: 0;
|
|
253
|
+
text-decoration: underline;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
}
|
|
256
|
+
|
|
242
257
|
:where([data-findlane-stats]) {
|
|
243
258
|
margin: 0;
|
|
244
259
|
color: var(--findlane-muted, color-mix(in srgb, currentColor 60%, transparent));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Findlane browser SDK: fast, typo-tolerant public search from the browser with an index's public ID, plus React hooks and components.",
|
|
5
5
|
"keywords": ["findlane", "search", "full-text search", "typo tolerance", "instant search", "ecommerce search", "browser", "react", "autocomplete"],
|
|
6
6
|
"license": "MIT",
|