@findlane/browser 0.16.0 → 0.17.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 -10
- package/dist/react/cache.d.ts +3 -1
- package/dist/react/cache.js +5 -2
- package/dist/react/components.d.ts +4 -1
- package/dist/react/components.js +9 -4
- package/dist/react/highlight.js +1 -1
- package/dist/react/search-provider.d.ts +2 -0
- package/dist/react/search-provider.js +2 -0
- package/dist/react/store.d.ts +2 -1
- package/dist/react/store.js +5 -2
- package/dist/react/use-autocomplete.d.ts +1 -7
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export type SearchRequest = {
|
|
|
46
46
|
/** Only records inside a box, in degrees, such as a map's visible area. West greater than east crosses the
|
|
47
47
|
* antimeridian. */
|
|
48
48
|
within?: GeoBox;
|
|
49
|
+
/** With the index's related results on: false asks for none (0.17.0). */
|
|
50
|
+
related?: boolean;
|
|
49
51
|
};
|
|
50
52
|
/** A point in degrees: lat from -90 to 90, lng from -180 to 180. */
|
|
51
53
|
export type GeoPoint = {
|
|
@@ -59,16 +61,18 @@ export type GeoBox = {
|
|
|
59
61
|
east: number;
|
|
60
62
|
west: number;
|
|
61
63
|
};
|
|
64
|
+
/** A result. */
|
|
65
|
+
export type SearchHit<T> = {
|
|
66
|
+
id: string;
|
|
67
|
+
score: number | null;
|
|
68
|
+
record: T;
|
|
69
|
+
/** With `near`: meters from the point. */
|
|
70
|
+
distance?: number;
|
|
71
|
+
/** With `highlight`: matched searchable fields by path; list fields give one string per value. */
|
|
72
|
+
highlights?: Record<string, string | string[]>;
|
|
73
|
+
};
|
|
62
74
|
export type SearchResponse<T> = {
|
|
63
|
-
hits:
|
|
64
|
-
id: string;
|
|
65
|
-
score: number | null;
|
|
66
|
-
record: T;
|
|
67
|
-
/** With `near`: meters from the point. */
|
|
68
|
-
distance?: number;
|
|
69
|
-
/** With `highlight`: matched searchable fields by path; list fields give one string per value. */
|
|
70
|
-
highlights?: Record<string, string | string[]>;
|
|
71
|
-
}[];
|
|
75
|
+
hits: SearchHit<T>[];
|
|
72
76
|
total: number;
|
|
73
77
|
/** "gte" when `total` is a lower bound (page count mode). */
|
|
74
78
|
totalRelation?: "eq" | "gte";
|
|
@@ -95,6 +99,13 @@ export type SearchResponse<T> = {
|
|
|
95
99
|
appliedRules?: AppliedRule[];
|
|
96
100
|
/** When a rule replaced words: the query that was searched. */
|
|
97
101
|
rewrittenQuery?: string;
|
|
102
|
+
/** With the index's related results on (0.17.0): when the query's words found nothing (`hits` empty, `total`
|
|
103
|
+
* 0), the products closest to it in meaning, best first, with their similarity as `score`. Show them under
|
|
104
|
+
* a heading of their own, such as "No exact matches. Related products". */
|
|
105
|
+
related?: {
|
|
106
|
+
hits: SearchHit<T>[];
|
|
107
|
+
total: number;
|
|
108
|
+
};
|
|
98
109
|
};
|
|
99
110
|
/** A query rule a search fired, with its `userData`, if it has any. */
|
|
100
111
|
export type AppliedRule = {
|
|
@@ -227,7 +238,7 @@ export type EventItem = {
|
|
|
227
238
|
export type ClickDetails = {
|
|
228
239
|
position?: number;
|
|
229
240
|
query?: string;
|
|
230
|
-
list?: "goesWith" | "similar" | "together";
|
|
241
|
+
list?: "goesWith" | "similar" | "together" | "related";
|
|
231
242
|
from?: string;
|
|
232
243
|
};
|
|
233
244
|
/** Events from the page, sent in the background: a batch a second, and what's left when the page is
|
package/dist/react/cache.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { QuerySuggestion, RecommendationsRequest, RecommendationsResponse, SearchBrowserClient, SearchRequest, SearchResponse } from "../index.js";
|
|
2
2
|
/** Requests the API answers the same way share a key, as at the edge (workers/api/src/edge.ts):
|
|
3
|
-
* the query's first eight lowercase words, defaults left out, and list order ignored.
|
|
3
|
+
* the query's first eight lowercase words, defaults left out, and list order ignored. Words keep an
|
|
4
|
+
* apostrophe or period inside them (don't, 3.5), which the API matches as one word; the edge's exact
|
|
5
|
+
* rule needs lookbehind, which older Safari can't parse, so this keeps a little more apart than it. */
|
|
4
6
|
export declare function searchKey(request: SearchRequest): string;
|
|
5
7
|
/** A product's recommendations share a key when only defaults differ: the limit of 8 and empty filters. */
|
|
6
8
|
export declare function recommendationsKey(objectID: string, request?: RecommendationsRequest): string;
|
package/dist/react/cache.js
CHANGED
|
@@ -6,11 +6,14 @@ const searches = store();
|
|
|
6
6
|
const recommendations = store();
|
|
7
7
|
const suggestions = store();
|
|
8
8
|
/** Requests the API answers the same way share a key, as at the edge (workers/api/src/edge.ts):
|
|
9
|
-
* the query's first eight lowercase words, defaults left out, and list order ignored.
|
|
9
|
+
* the query's first eight lowercase words, defaults left out, and list order ignored. Words keep an
|
|
10
|
+
* apostrophe or period inside them (don't, 3.5), which the API matches as one word; the edge's exact
|
|
11
|
+
* rule needs lookbehind, which older Safari can't parse, so this keeps a little more apart than it. */
|
|
10
12
|
export function searchKey(request) {
|
|
11
13
|
const normal = { ...request };
|
|
12
14
|
if (typeof normal.q === "string" && normal.q.length <= 200)
|
|
13
|
-
normal.q = normal.q.toLowerCase().match(/[\p{L}\p{N}]+/gu)
|
|
15
|
+
normal.q = (normal.q.toLowerCase().replace(/[’‘]/gu, "'").match(/[\p{L}\p{M}\p{N}_'.:·,;]+/gu) ?? [])
|
|
16
|
+
.map((word) => word.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "")).filter(Boolean).slice(0, 8).join(" ");
|
|
14
17
|
const defaults = { q: "", limit: 20, offset: 0, countMode: "page", facetMode: "page" };
|
|
15
18
|
for (const [key, value] of Object.entries(defaults))
|
|
16
19
|
if (normal[key] === value)
|
|
@@ -56,6 +56,9 @@ export type HitsProps<T> = {
|
|
|
56
56
|
render?: (hit: Hit<T>, index: number) => ReactNode;
|
|
57
57
|
/** Shown when nothing matches (default "No results for “…”"). */
|
|
58
58
|
empty?: (query: string) => ReactNode;
|
|
59
|
+
/** The heading over related results, shown when nothing matched but the index's related results found
|
|
60
|
+
* products close in meaning (default "No exact matches. Related products"). */
|
|
61
|
+
relatedTitle?: (query: string) => ReactNode;
|
|
59
62
|
/** Shown before the first results arrive. */
|
|
60
63
|
loading?: ReactNode;
|
|
61
64
|
className?: string;
|
|
@@ -63,7 +66,7 @@ export type HitsProps<T> = {
|
|
|
63
66
|
/** Whether a click was on a link inside the element that handled it, such as a hit's link. */
|
|
64
67
|
export declare function onLink(event: MouseEvent<HTMLElement>): boolean;
|
|
65
68
|
/** The results. Following a link in a hit sends a click event, with its place and the query. */
|
|
66
|
-
export declare function Hits<T = Record<string, unknown>>({ render, empty, loading, className }: HitsProps<T>): import("react").JSX.Element;
|
|
69
|
+
export declare function Hits<T = Record<string, unknown>>({ render, empty, relatedTitle, loading, className }: HitsProps<T>): import("react").JSX.Element;
|
|
67
70
|
export type RefinementListProps = {
|
|
68
71
|
field: string;
|
|
69
72
|
/** The heading (default the field). */
|
package/dist/react/components.js
CHANGED
|
@@ -74,22 +74,27 @@ export function onLink(event) {
|
|
|
74
74
|
return Boolean(link && event.currentTarget.contains(link));
|
|
75
75
|
}
|
|
76
76
|
/** The results. Following a link in a hit sends a click event, with its place and the query. */
|
|
77
|
-
export function Hits({ render, empty, loading = null, className }) {
|
|
78
|
-
const { hits, isLoaded, isPending, error, click } = useHits();
|
|
77
|
+
export function Hits({ render, empty, relatedTitle, loading = null, className }) {
|
|
78
|
+
const { hits, related, isLoaded, isPending, error, click } = useHits();
|
|
79
79
|
const { query } = useSearchBox();
|
|
80
80
|
if (error && !isLoaded)
|
|
81
81
|
return _jsx("p", { "data-findlane-note": "", children: messageFor(error) });
|
|
82
82
|
if (!isLoaded)
|
|
83
83
|
return _jsx(_Fragment, { children: loading });
|
|
84
|
-
if (!hits.length)
|
|
84
|
+
if (!hits.length && !related.length)
|
|
85
85
|
return _jsx("p", { "data-findlane-empty": "", children: empty ? empty(query) : query ? _jsxs(_Fragment, { children: ["No results for \u201C", query, "\u201D"] }) : "No results" });
|
|
86
|
-
|
|
86
|
+
// Nothing matched, but related results found products close in meaning: those, under their own heading.
|
|
87
|
+
const shown = hits.length ? hits : related;
|
|
88
|
+
const list = (_jsx("ol", { className: className, "data-findlane-hits": "", "data-findlane-related": !hits.length || undefined, "data-pending": isPending || undefined, children: shown.map((hit, index) => {
|
|
87
89
|
// A click, and a middle click that opens the link in a new tab.
|
|
88
90
|
const clicked = (event) => { if (!event.defaultPrevented && onLink(event))
|
|
89
91
|
click(hit); };
|
|
90
92
|
return (_jsx("li", { onClick: clicked, onAuxClick: (event) => { if (event.button === 1)
|
|
91
93
|
clicked(event); }, children: render ? render(hit, index) : _jsx(DefaultHit, { hit: hit }) }, hit.id));
|
|
92
94
|
}) }));
|
|
95
|
+
if (hits.length)
|
|
96
|
+
return list;
|
|
97
|
+
return (_jsxs(_Fragment, { children: [_jsx("p", { "data-findlane-related-title": "", children: relatedTitle ? relatedTitle(query) : "No exact matches. Related products" }), list] }));
|
|
93
98
|
}
|
|
94
99
|
/** Checkboxes for a facet's values, with counts. */
|
|
95
100
|
export function RefinementList({ field, label, limit = 8, format, className }) {
|
package/dist/react/highlight.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment } from "react";
|
|
3
|
-
// The API escapes highlighted values (
|
|
3
|
+
// The API escapes highlighted values (the engine's, engine/src/present.rs) and wraps matched words in
|
|
4
4
|
// <mark>. Splitting on the tags and decoding the five escapes gives text React can render, so
|
|
5
5
|
// nothing is inserted as HTML.
|
|
6
6
|
const escapes = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'" };
|
|
@@ -35,6 +35,8 @@ export declare function useSearchBox(): {
|
|
|
35
35
|
export declare function useHits<T = Record<string, unknown>>(): {
|
|
36
36
|
hits: import("./state.js").Hit<T>[];
|
|
37
37
|
total: number;
|
|
38
|
+
/** With the index's related results on, when nothing matched: the products closest in meaning. */
|
|
39
|
+
related: import("../index.js").SearchHit<T>[];
|
|
38
40
|
/** "gte" when `total` is a lower bound (with `countMode: "page"`). */
|
|
39
41
|
totalRelation: "eq" | "gte";
|
|
40
42
|
response: import("../index.js").SearchResponse<T> | null;
|
|
@@ -82,6 +82,8 @@ export function useHits() {
|
|
|
82
82
|
return {
|
|
83
83
|
hits: results?.hits ?? [],
|
|
84
84
|
total: results?.response.total ?? 0,
|
|
85
|
+
/** With the index's related results on, when nothing matched: the products closest in meaning. */
|
|
86
|
+
related: results?.response.related?.hits ?? [],
|
|
85
87
|
/** "gte" when `total` is a lower bound (with `countMode: "page"`). */
|
|
86
88
|
totalRelation: results?.response.totalRelation ?? "eq",
|
|
87
89
|
response: results?.response ?? null,
|
package/dist/react/store.d.ts
CHANGED
|
@@ -62,7 +62,8 @@ export declare class SearchStore<T = unknown> {
|
|
|
62
62
|
get settings(): SearchConfig;
|
|
63
63
|
/** Opens the connection to the API before the first search needs it (the client's warm). */
|
|
64
64
|
warm(): void;
|
|
65
|
-
/** Sends a click on a result: its place in the results, from 1, and the query they're for.
|
|
65
|
+
/** Sends a click on a result: its place in the results, from 1, and the query they're for. A related
|
|
66
|
+
* result's click says so (list "related"), with its place among them. */
|
|
66
67
|
click(objectID: string): void;
|
|
67
68
|
/** Searches unless the results already answer the current state; with a state, such as the
|
|
68
69
|
* URL's, adopts it first. */
|
package/dist/react/store.js
CHANGED
|
@@ -67,12 +67,15 @@ export class SearchStore {
|
|
|
67
67
|
get settings() { return this.config; }
|
|
68
68
|
/** Opens the connection to the API before the first search needs it (the client's warm). */
|
|
69
69
|
warm() { this.client.warm(); }
|
|
70
|
-
/** Sends a click on a result: its place in the results, from 1, and the query they're for.
|
|
70
|
+
/** Sends a click on a result: its place in the results, from 1, and the query they're for. A related
|
|
71
|
+
* result's click says so (list "related"), with its place among them. */
|
|
71
72
|
click(objectID) {
|
|
72
73
|
const results = this.snapshot.results;
|
|
73
74
|
const index = results ? results.hits.findIndex((hit) => hit.id === objectID) : -1;
|
|
75
|
+
const related = index < 0 && results ? (results.response.related?.hits ?? []).findIndex((hit) => hit.id === objectID) : -1;
|
|
74
76
|
this.client.events.click(objectID, { query: results?.request.q ?? "",
|
|
75
|
-
...(index >= 0 ? { position: (results.request.offset ?? 0) + index + 1 } : {})
|
|
77
|
+
...(index >= 0 ? { position: (results.request.offset ?? 0) + index + 1 } : {}),
|
|
78
|
+
...(related >= 0 ? { position: related + 1, list: "related" } : {}) });
|
|
76
79
|
}
|
|
77
80
|
/** Searches unless the results already answer the current state; with a state, such as the
|
|
78
81
|
* URL's, adopts it first. */
|
|
@@ -63,13 +63,7 @@ export declare function fillTemplate<T>(template: string, hit: Hit<T>): string |
|
|
|
63
63
|
export declare function useAutocomplete<T = Record<string, unknown>>(options?: AutocompleteOptions<T>): {
|
|
64
64
|
query: string;
|
|
65
65
|
setQuery: (value: string) => void;
|
|
66
|
-
hits:
|
|
67
|
-
id: string;
|
|
68
|
-
score: number | null;
|
|
69
|
-
record: T;
|
|
70
|
-
distance?: number;
|
|
71
|
-
highlights?: Record<string, string | string[]>;
|
|
72
|
-
}[];
|
|
66
|
+
hits: import("../index.js").SearchHit<T>[];
|
|
73
67
|
total: number;
|
|
74
68
|
response: import("../index.js").SearchResponse<T> | null;
|
|
75
69
|
error: unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|