@stapel/search-react 0.3.1 → 0.7.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/CHANGELOG.md +180 -0
- package/MODULE.md +43 -4
- package/README.md +1 -1
- package/dist/api/generated/schema.d.ts +5 -1
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/DegradationNotice.d.ts +34 -0
- package/dist/default/DegradationNotice.d.ts.map +1 -1
- package/dist/default/DegradationNotice.js +28 -6
- package/dist/default/DegradationNotice.js.map +1 -1
- package/dist/default/FacetPanelPane.d.ts +54 -3
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +119 -19
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/LanguageSelect.d.ts +9 -0
- package/dist/default/LanguageSelect.d.ts.map +1 -0
- package/dist/default/LanguageSelect.js +72 -0
- package/dist/default/LanguageSelect.js.map +1 -0
- package/dist/default/PageSizeSelect.d.ts +29 -0
- package/dist/default/PageSizeSelect.d.ts.map +1 -0
- package/dist/default/PageSizeSelect.js +25 -0
- package/dist/default/PageSizeSelect.js.map +1 -0
- package/dist/default/RangeFilterRow.d.ts +12 -0
- package/dist/default/RangeFilterRow.d.ts.map +1 -0
- package/dist/default/RangeFilterRow.js +75 -0
- package/dist/default/RangeFilterRow.js.map +1 -0
- package/dist/default/RankingDisclosurePane.d.ts +5 -0
- package/dist/default/RankingDisclosurePane.d.ts.map +1 -1
- package/dist/default/RankingDisclosurePane.js +15 -15
- package/dist/default/RankingDisclosurePane.js.map +1 -1
- package/dist/default/SearchBox.d.ts +13 -0
- package/dist/default/SearchBox.d.ts.map +1 -0
- package/dist/default/SearchBox.js +67 -0
- package/dist/default/SearchBox.js.map +1 -0
- package/dist/default/SearchPage.d.ts +58 -16
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +155 -6
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultCard.d.ts +19 -18
- package/dist/default/SearchResultCard.d.ts.map +1 -1
- package/dist/default/SearchResultCard.js +178 -5
- package/dist/default/SearchResultCard.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +54 -3
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +45 -25
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/SortSelect.d.ts +10 -2
- package/dist/default/SortSelect.d.ts.map +1 -1
- package/dist/default/SortSelect.js +27 -24
- package/dist/default/SortSelect.js.map +1 -1
- package/dist/default/UrlIssueNotice.d.ts.map +1 -1
- package/dist/default/UrlIssueNotice.js +19 -1
- package/dist/default/UrlIssueNotice.js.map +1 -1
- package/dist/default/index.d.ts +33 -16
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +26 -12
- package/dist/default/index.js.map +1 -1
- package/dist/default/sortLabels.d.ts +5 -0
- package/dist/default/sortLabels.d.ts.map +1 -0
- package/dist/default/sortLabels.js +32 -0
- package/dist/default/sortLabels.js.map +1 -0
- package/dist/headless/FacetPanel.d.ts +17 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +23 -2
- package/dist/headless/FacetPanel.js.map +1 -1
- package/dist/headless/SearchResults.d.ts +20 -3
- package/dist/headless/SearchResults.d.ts.map +1 -1
- package/dist/headless/SearchResults.js +3 -1
- package/dist/headless/SearchResults.js.map +1 -1
- package/dist/headless/useAppliedCount.d.ts +8 -0
- package/dist/headless/useAppliedCount.d.ts.map +1 -0
- package/dist/headless/useAppliedCount.js +30 -0
- package/dist/headless/useAppliedCount.js.map +1 -0
- package/dist/headless/useScorerNames.d.ts +4 -0
- package/dist/headless/useScorerNames.d.ts.map +1 -0
- package/dist/headless/useScorerNames.js +34 -0
- package/dist/headless/useScorerNames.js.map +1 -0
- package/dist/headless/useSearchBox.d.ts +44 -0
- package/dist/headless/useSearchBox.d.ts.map +1 -0
- package/dist/headless/useSearchBox.js +116 -0
- package/dist/headless/useSearchBox.js.map +1 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +32 -4
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +46 -2
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +86 -6
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +38 -4
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/model/queries.d.ts +26 -2
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +38 -1
- package/dist/model/queries.js.map +1 -1
- package/dist/state/degradations.d.ts +41 -0
- package/dist/state/degradations.d.ts.map +1 -1
- package/dist/state/degradations.js +41 -0
- package/dist/state/degradations.js.map +1 -1
- package/dist/state/limits.d.ts +44 -0
- package/dist/state/limits.d.ts.map +1 -0
- package/dist/state/limits.js +44 -0
- package/dist/state/limits.js.map +1 -0
- package/dist/state/ranges.d.ts +52 -0
- package/dist/state/ranges.d.ts.map +1 -0
- package/dist/state/ranges.js +108 -0
- package/dist/state/ranges.js.map +1 -0
- package/llms.txt +16 -5
- package/manifest.json +245 -22
- package/nav-manifest.json +1 -1
- package/package.json +15 -10
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +5 -1
- package/src/default/DegradationNotice.tsx +84 -14
- package/src/default/FacetPanelPane.tsx +343 -64
- package/src/default/LanguageSelect.tsx +93 -0
- package/src/default/PageSizeSelect.tsx +80 -0
- package/src/default/RangeFilterRow.tsx +148 -0
- package/src/default/RankingDisclosurePane.tsx +67 -35
- package/src/default/SearchBox.tsx +130 -0
- package/src/default/SearchPage.tsx +356 -41
- package/src/default/SearchResultCard.tsx +231 -24
- package/src/default/SearchResultsPane.tsx +147 -76
- package/src/default/SortSelect.tsx +50 -37
- package/src/default/UrlIssueNotice.tsx +21 -3
- package/src/default/index.ts +42 -16
- package/src/default/sortLabels.ts +34 -0
- package/src/headless/FacetPanel.tsx +30 -2
- package/src/headless/SearchResults.tsx +32 -4
- package/src/headless/useAppliedCount.ts +41 -0
- package/src/headless/useScorerNames.ts +36 -0
- package/src/headless/useSearchBox.ts +189 -0
- package/src/i18n/es.ts +39 -4
- package/src/i18n/keys.ts +97 -6
- package/src/i18n/ru.ts +45 -4
- package/src/index.ts +30 -3
- package/src/model/queries.ts +51 -1
- package/src/state/degradations.ts +60 -0
- package/src/state/limits.ts +50 -0
- package/src/state/ranges.ts +144 -0
- package/dist/default/ErrorAlert.d.ts +0 -9
- package/dist/default/ErrorAlert.d.ts.map +0 -1
- package/dist/default/ErrorAlert.js +0 -27
- package/dist/default/ErrorAlert.js.map +0 -1
- package/dist/default/theme.d.ts +0 -24
- package/dist/default/theme.d.ts.map +0 -1
- package/dist/default/theme.js +0 -39
- package/dist/default/theme.js.map +0 -1
- package/src/default/ErrorAlert.tsx +0 -46
- package/src/default/theme.tsx +0 -66
package/src/i18n/es.ts
CHANGED
|
@@ -22,6 +22,8 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
22
22
|
"search.results.retry": "Reintentar",
|
|
23
23
|
"search.results.count_approximate.one": "Aproximadamente {count} resultado",
|
|
24
24
|
"search.results.count_approximate.other": "Unos {count} resultados",
|
|
25
|
+
"search.results.count_at_least.one": "{count}+ resultado",
|
|
26
|
+
"search.results.count_at_least.other": "{count}+ resultados",
|
|
25
27
|
"search.results.count_exact.one": "{count} resultado",
|
|
26
28
|
"search.results.count_exact.other": "{count} resultados",
|
|
27
29
|
"search.results.took_ms": "{ms} ms",
|
|
@@ -37,6 +39,13 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
37
39
|
"search.results.distance_km": "a {km} km",
|
|
38
40
|
"search.results.untitled": "Sin título",
|
|
39
41
|
"search.results.open": "Abrir",
|
|
42
|
+
"search.results.image_alt": "Foto de {title}",
|
|
43
|
+
|
|
44
|
+
"search.box.label": "Buscar",
|
|
45
|
+
"search.box.placeholder": "¿Qué estás buscando?",
|
|
46
|
+
"search.box.submit": "Buscar",
|
|
47
|
+
"search.box.clear": "Borrar la búsqueda",
|
|
48
|
+
"search.box.suggestions": "Sugerencias",
|
|
40
49
|
|
|
41
50
|
"search.sort.label": "Orden",
|
|
42
51
|
"search.sort.relevance": "Más relevantes",
|
|
@@ -62,21 +71,47 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
62
71
|
"search.facets.range_from": "Desde",
|
|
63
72
|
"search.facets.range_to": "Hasta",
|
|
64
73
|
"search.facets.range_apply": "Aplicar",
|
|
74
|
+
"search.facets.range_clear": "Quitar",
|
|
75
|
+
"search.facets.range_from_aria": "{feature}, desde",
|
|
76
|
+
"search.facets.range_to_aria": "{feature}, hasta",
|
|
77
|
+
"search.facets.range_invalid":
|
|
78
|
+
"«Desde» es mayor que «hasta», así que nada podría coincidir. Intercámbialos para aplicar el rango.",
|
|
79
|
+
|
|
80
|
+
"search.filters.open": "Filtros ({count})",
|
|
81
|
+
"search.filters.apply": "Ver resultados",
|
|
82
|
+
"search.filters.show_count.one": "Ver {count} resultado",
|
|
83
|
+
"search.filters.show_count.other": "Ver {count} resultados",
|
|
84
|
+
"search.filters.show_count_at_least.one": "Ver {count}+ resultado",
|
|
85
|
+
"search.filters.show_count_at_least.other": "Ver {count}+ resultados",
|
|
86
|
+
"search.filters.dismiss": "Cerrar los filtros",
|
|
87
|
+
|
|
88
|
+
"search.category.title": "Categoría",
|
|
89
|
+
"search.category.clear": "Buscar en todo el catálogo",
|
|
90
|
+
"search.category.current": "Buscando dentro de {path}",
|
|
91
|
+
|
|
92
|
+
"search.language.label": "Idioma de la consulta",
|
|
93
|
+
"search.language.any": "Cualquier idioma",
|
|
94
|
+
|
|
95
|
+
"search.limit.label": "Por página",
|
|
96
|
+
"search.limit.option": "{count} por página",
|
|
97
|
+
"search.limit.from_link": "Este enlace fija su propio tamaño de página.",
|
|
65
98
|
|
|
66
99
|
"search.geo.title": "Ubicación",
|
|
67
100
|
"search.geo.radius_km": "A menos de {km} km",
|
|
101
|
+
"search.geo.radius_label": "Radio, km",
|
|
68
102
|
"search.geo.clear": "En cualquier lugar",
|
|
69
103
|
"search.geo.box": "Dentro del área mostrada",
|
|
70
104
|
"search.geo.center": "Cerca de {lat}, {lon}",
|
|
71
105
|
|
|
72
106
|
"search.url.issues_title": "Parte de este enlace no se pudo leer",
|
|
73
|
-
"search.url.issue.not_a_number":
|
|
107
|
+
"search.url.issue.not_a_number":
|
|
108
|
+
"«{param}» en este enlace no es un número, así que se ignoró",
|
|
74
109
|
"search.url.issue.geo_incomplete":
|
|
75
|
-
"la ubicación
|
|
110
|
+
"la ubicación de este enlace está incompleta, así que se ignoró",
|
|
76
111
|
"search.url.issue.bbox_malformed":
|
|
77
|
-
"el área
|
|
112
|
+
"el área del mapa de este enlace está incompleta, así que se ignoró",
|
|
78
113
|
"search.url.issue.range_malformed":
|
|
79
|
-
"{param}
|
|
114
|
+
"al rango «{param}» de este enlace le faltan números, así que se ignoró",
|
|
80
115
|
|
|
81
116
|
"search.degraded.title": "Lo que esta búsqueda no pudo hacer",
|
|
82
117
|
"search.degraded.typo_tolerance":
|
package/src/i18n/keys.ts
CHANGED
|
@@ -28,9 +28,18 @@ export const SEARCH_I18N_KEYS = {
|
|
|
28
28
|
resultsLoadFailed: "search.results.load_failed",
|
|
29
29
|
resultsEmpty: "search.results.empty",
|
|
30
30
|
resultsRetry: "search.results.retry",
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
31
|
+
/**
|
|
32
|
+
* "About N results" — the sentence for an ESTIMATED count. A PLURAL
|
|
33
|
+
* FAMILY: render with `tPlural`, never `t` (see SEARCH_I18N_PLURAL_KEYS).
|
|
34
|
+
*
|
|
35
|
+
* @deprecated stapel-search 0.2.0 stopped producing estimates: a count is
|
|
36
|
+
* exact, a FLOOR (`resultsCountAtLeast`), or absent. Kept so a host that
|
|
37
|
+
* translated it keeps a working key, and no longer rendered by the skin.
|
|
38
|
+
*/
|
|
33
39
|
resultsCountApproximate: "search.results.count_approximate",
|
|
40
|
+
/** "N+ results" — a LOWER BOUND: at least this many match, possibly more.
|
|
41
|
+
* A PLURAL FAMILY. Never render a floor as a plain number. */
|
|
42
|
+
resultsCountAtLeast: "search.results.count_at_least",
|
|
34
43
|
/** "N results" — only when `exact_total` is true. A PLURAL FAMILY. */
|
|
35
44
|
resultsCountExact: "search.results.count_exact",
|
|
36
45
|
resultsTookMs: "search.results.took_ms",
|
|
@@ -45,6 +54,16 @@ export const SEARCH_I18N_KEYS = {
|
|
|
45
54
|
resultsDistanceKm: "search.results.distance_km",
|
|
46
55
|
resultsUntitled: "search.results.untitled",
|
|
47
56
|
resultsOpen: "search.results.open",
|
|
57
|
+
/** Alt text for the card photo — the card's own title, in a sentence. */
|
|
58
|
+
resultsImageAlt: "search.results.image_alt",
|
|
59
|
+
|
|
60
|
+
// ── the query box ────────────────────────────────────────────────────────
|
|
61
|
+
boxLabel: "search.box.label",
|
|
62
|
+
boxPlaceholder: "search.box.placeholder",
|
|
63
|
+
boxSubmit: "search.box.submit",
|
|
64
|
+
boxClear: "search.box.clear",
|
|
65
|
+
/** Accessible name of the suggestion list under the box. */
|
|
66
|
+
boxSuggestions: "search.box.suggestions",
|
|
48
67
|
|
|
49
68
|
// ── sort ─────────────────────────────────────────────────────────────────
|
|
50
69
|
sortLabel: "search.sort.label",
|
|
@@ -70,10 +89,45 @@ export const SEARCH_I18N_KEYS = {
|
|
|
70
89
|
facetsRangeFrom: "search.facets.range_from",
|
|
71
90
|
facetsRangeTo: "search.facets.range_to",
|
|
72
91
|
facetsRangeApply: "search.facets.range_apply",
|
|
92
|
+
facetsRangeClear: "search.facets.range_clear",
|
|
93
|
+
/** Accessible names: the visible "From"/"To" repeat on every row, so the
|
|
94
|
+
* field's own name has to carry the feature it belongs to. */
|
|
95
|
+
facetsRangeFromAria: "search.facets.range_from_aria",
|
|
96
|
+
facetsRangeToAria: "search.facets.range_to_aria",
|
|
97
|
+
/** The reason "Apply" is off: the range reads backwards. */
|
|
98
|
+
facetsRangeInvalid: "search.facets.range_invalid",
|
|
99
|
+
|
|
100
|
+
// ── the filter panel as a whole (phone sheet + host slots) ───────────────
|
|
101
|
+
filtersOpen: "search.filters.open",
|
|
102
|
+
/** The sheet's commit button when the count is unknown. */
|
|
103
|
+
filtersApply: "search.filters.apply",
|
|
104
|
+
/** "Show N results" — an EXACT count on the sheet's commit button.
|
|
105
|
+
* A PLURAL FAMILY. */
|
|
106
|
+
filtersShowCount: "search.filters.show_count",
|
|
107
|
+
/** "Show N+ results" — a FLOOR on the same button. A PLURAL FAMILY. */
|
|
108
|
+
filtersShowCountAtLeast: "search.filters.show_count_at_least",
|
|
109
|
+
filtersDismiss: "search.filters.dismiss",
|
|
110
|
+
|
|
111
|
+
// ── category (a host slot, plus the control that removes it) ─────────────
|
|
112
|
+
categoryTitle: "search.category.title",
|
|
113
|
+
categoryClear: "search.category.clear",
|
|
114
|
+
categoryCurrent: "search.category.current",
|
|
115
|
+
|
|
116
|
+
// ── language of the query ────────────────────────────────────────────────
|
|
117
|
+
languageLabel: "search.language.label",
|
|
118
|
+
languageAny: "search.language.any",
|
|
119
|
+
|
|
120
|
+
// ── page size ────────────────────────────────────────────────────────────
|
|
121
|
+
limitLabel: "search.limit.label",
|
|
122
|
+
limitOption: "search.limit.option",
|
|
123
|
+
/** Said beside the control when the URL carries a size the ladder does not
|
|
124
|
+
* offer — otherwise the story of "we kept your link's size" is invisible. */
|
|
125
|
+
limitFromLink: "search.limit.from_link",
|
|
73
126
|
|
|
74
127
|
// ── geo ──────────────────────────────────────────────────────────────────
|
|
75
128
|
geoTitle: "search.geo.title",
|
|
76
129
|
geoRadiusKm: "search.geo.radius_km",
|
|
130
|
+
geoRadiusLabel: "search.geo.radius_label",
|
|
77
131
|
geoClear: "search.geo.clear",
|
|
78
132
|
geoBox: "search.geo.box",
|
|
79
133
|
geoCenter: "search.geo.center",
|
|
@@ -129,7 +183,10 @@ export type SearchI18nKey =
|
|
|
129
183
|
*/
|
|
130
184
|
export const SEARCH_I18N_PLURAL_KEYS: readonly SearchI18nKey[] = [
|
|
131
185
|
SEARCH_I18N_KEYS.resultsCountApproximate,
|
|
186
|
+
SEARCH_I18N_KEYS.resultsCountAtLeast,
|
|
132
187
|
SEARCH_I18N_KEYS.resultsCountExact,
|
|
188
|
+
SEARCH_I18N_KEYS.filtersShowCount,
|
|
189
|
+
SEARCH_I18N_KEYS.filtersShowCountAtLeast,
|
|
133
190
|
];
|
|
134
191
|
|
|
135
192
|
/**
|
|
@@ -149,6 +206,8 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
149
206
|
"search.results.retry": "Try again",
|
|
150
207
|
"search.results.count_approximate.one": "About {count} result",
|
|
151
208
|
"search.results.count_approximate.other": "About {count} results",
|
|
209
|
+
"search.results.count_at_least.one": "{count}+ result",
|
|
210
|
+
"search.results.count_at_least.other": "{count}+ results",
|
|
152
211
|
"search.results.count_exact.one": "{count} result",
|
|
153
212
|
"search.results.count_exact.other": "{count} results",
|
|
154
213
|
"search.results.took_ms": "{ms} ms",
|
|
@@ -164,6 +223,13 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
164
223
|
"search.results.distance_km": "{km} km away",
|
|
165
224
|
"search.results.untitled": "Untitled",
|
|
166
225
|
"search.results.open": "Open",
|
|
226
|
+
"search.results.image_alt": "Photo of {title}",
|
|
227
|
+
|
|
228
|
+
"search.box.label": "Search",
|
|
229
|
+
"search.box.placeholder": "What are you looking for?",
|
|
230
|
+
"search.box.submit": "Search",
|
|
231
|
+
"search.box.clear": "Clear the search",
|
|
232
|
+
"search.box.suggestions": "Suggestions",
|
|
167
233
|
|
|
168
234
|
"search.sort.label": "Sort",
|
|
169
235
|
"search.sort.relevance": "Most relevant",
|
|
@@ -189,21 +255,46 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
189
255
|
"search.facets.range_from": "From",
|
|
190
256
|
"search.facets.range_to": "To",
|
|
191
257
|
"search.facets.range_apply": "Apply",
|
|
258
|
+
"search.facets.range_clear": "Clear",
|
|
259
|
+
"search.facets.range_from_aria": "{feature}, from",
|
|
260
|
+
"search.facets.range_to_aria": "{feature}, up to",
|
|
261
|
+
"search.facets.range_invalid":
|
|
262
|
+
"“From” is larger than “to”, so nothing could match. Swap them to apply this range.",
|
|
263
|
+
|
|
264
|
+
"search.filters.open": "Filters ({count})",
|
|
265
|
+
"search.filters.apply": "Show results",
|
|
266
|
+
"search.filters.show_count.one": "Show {count} result",
|
|
267
|
+
"search.filters.show_count.other": "Show {count} results",
|
|
268
|
+
"search.filters.show_count_at_least.one": "Show {count}+ result",
|
|
269
|
+
"search.filters.show_count_at_least.other": "Show {count}+ results",
|
|
270
|
+
"search.filters.dismiss": "Close the filters",
|
|
271
|
+
|
|
272
|
+
"search.category.title": "Category",
|
|
273
|
+
"search.category.clear": "Search the whole catalogue",
|
|
274
|
+
"search.category.current": "Searching inside {path}",
|
|
275
|
+
|
|
276
|
+
"search.language.label": "Query language",
|
|
277
|
+
"search.language.any": "Any language",
|
|
278
|
+
|
|
279
|
+
"search.limit.label": "Per page",
|
|
280
|
+
"search.limit.option": "{count} per page",
|
|
281
|
+
"search.limit.from_link": "This link sets its own page size.",
|
|
192
282
|
|
|
193
283
|
"search.geo.title": "Location",
|
|
194
284
|
"search.geo.radius_km": "Within {km} km",
|
|
285
|
+
"search.geo.radius_label": "Radius, km",
|
|
195
286
|
"search.geo.clear": "Anywhere",
|
|
196
287
|
"search.geo.box": "Inside the shown area",
|
|
197
288
|
"search.geo.center": "Around {lat}, {lon}",
|
|
198
289
|
|
|
199
290
|
"search.url.issues_title": "Part of this link could not be read",
|
|
200
|
-
"search.url.issue.not_a_number": "{param} is not a number
|
|
291
|
+
"search.url.issue.not_a_number": "“{param}” in this link is not a number, so it was ignored",
|
|
201
292
|
"search.url.issue.geo_incomplete":
|
|
202
|
-
"the location
|
|
293
|
+
"the location in this link is only half there, so it was ignored",
|
|
203
294
|
"search.url.issue.bbox_malformed":
|
|
204
|
-
"the area
|
|
295
|
+
"the map area in this link is incomplete, so it was ignored",
|
|
205
296
|
"search.url.issue.range_malformed":
|
|
206
|
-
"{param}
|
|
297
|
+
"the range “{param}” in this link needs two numbers, so it was ignored",
|
|
207
298
|
|
|
208
299
|
"search.degraded.title": "What this search could not do",
|
|
209
300
|
"search.degraded.typo_tolerance":
|
package/src/i18n/ru.ts
CHANGED
|
@@ -41,6 +41,10 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
41
41
|
"search.results.count_approximate.few": "Примерно {count} объявления",
|
|
42
42
|
"search.results.count_approximate.many": "Примерно {count} объявлений",
|
|
43
43
|
"search.results.count_approximate.other": "Примерно {count} объявления",
|
|
44
|
+
"search.results.count_at_least.one": "{count}+ объявление",
|
|
45
|
+
"search.results.count_at_least.few": "{count}+ объявления",
|
|
46
|
+
"search.results.count_at_least.many": "{count}+ объявлений",
|
|
47
|
+
"search.results.count_at_least.other": "{count}+ объявления",
|
|
44
48
|
"search.results.count_exact.one": "{count} объявление",
|
|
45
49
|
"search.results.count_exact.few": "{count} объявления",
|
|
46
50
|
"search.results.count_exact.many": "{count} объявлений",
|
|
@@ -58,6 +62,13 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
58
62
|
"search.results.distance_km": "{km} км от вас",
|
|
59
63
|
"search.results.untitled": "Без названия",
|
|
60
64
|
"search.results.open": "Открыть",
|
|
65
|
+
"search.results.image_alt": "Фото: {title}",
|
|
66
|
+
|
|
67
|
+
"search.box.label": "Поиск",
|
|
68
|
+
"search.box.placeholder": "Что ищете?",
|
|
69
|
+
"search.box.submit": "Найти",
|
|
70
|
+
"search.box.clear": "Очистить запрос",
|
|
71
|
+
"search.box.suggestions": "Подсказки",
|
|
61
72
|
|
|
62
73
|
"search.sort.label": "Сортировка",
|
|
63
74
|
"search.sort.relevance": "По релевантности",
|
|
@@ -83,21 +94,51 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
83
94
|
"search.facets.range_from": "От",
|
|
84
95
|
"search.facets.range_to": "До",
|
|
85
96
|
"search.facets.range_apply": "Применить",
|
|
97
|
+
"search.facets.range_clear": "Сбросить",
|
|
98
|
+
"search.facets.range_from_aria": "{feature}, от",
|
|
99
|
+
"search.facets.range_to_aria": "{feature}, до",
|
|
100
|
+
"search.facets.range_invalid":
|
|
101
|
+
"«От» больше, чем «до», — под такой диапазон ничего не подойдёт. Поменяйте значения местами.",
|
|
102
|
+
|
|
103
|
+
"search.filters.open": "Фильтры ({count})",
|
|
104
|
+
"search.filters.apply": "Показать результаты",
|
|
105
|
+
"search.filters.show_count.one": "Показать {count} объявление",
|
|
106
|
+
"search.filters.show_count.few": "Показать {count} объявления",
|
|
107
|
+
"search.filters.show_count.many": "Показать {count} объявлений",
|
|
108
|
+
"search.filters.show_count.other": "Показать {count} объявления",
|
|
109
|
+
"search.filters.show_count_at_least.one": "Показать {count}+ объявление",
|
|
110
|
+
"search.filters.show_count_at_least.few": "Показать {count}+ объявления",
|
|
111
|
+
"search.filters.show_count_at_least.many": "Показать {count}+ объявлений",
|
|
112
|
+
"search.filters.show_count_at_least.other": "Показать {count}+ объявления",
|
|
113
|
+
"search.filters.dismiss": "Закрыть фильтры",
|
|
114
|
+
|
|
115
|
+
"search.category.title": "Категория",
|
|
116
|
+
"search.category.clear": "Искать по всему каталогу",
|
|
117
|
+
"search.category.current": "Ищем внутри {path}",
|
|
118
|
+
|
|
119
|
+
"search.language.label": "Язык запроса",
|
|
120
|
+
"search.language.any": "Любой язык",
|
|
121
|
+
|
|
122
|
+
"search.limit.label": "На странице",
|
|
123
|
+
"search.limit.option": "{count} на странице",
|
|
124
|
+
"search.limit.from_link": "Размер страницы задан этой ссылкой.",
|
|
86
125
|
|
|
87
126
|
"search.geo.title": "Где искать",
|
|
88
127
|
"search.geo.radius_km": "В радиусе {km} км",
|
|
128
|
+
"search.geo.radius_label": "Радиус, км",
|
|
89
129
|
"search.geo.clear": "Везде",
|
|
90
130
|
"search.geo.box": "В показанной области",
|
|
91
131
|
"search.geo.center": "Рядом с {lat}, {lon}",
|
|
92
132
|
|
|
93
133
|
"search.url.issues_title": "Часть этой ссылки прочитать не удалось",
|
|
94
|
-
"search.url.issue.not_a_number":
|
|
134
|
+
"search.url.issue.not_a_number":
|
|
135
|
+
"«{param}» в этой ссылке — не число, поэтому параметр пропущен",
|
|
95
136
|
"search.url.issue.geo_incomplete":
|
|
96
|
-
"
|
|
137
|
+
"точка в этой ссылке указана наполовину, поэтому она пропущена",
|
|
97
138
|
"search.url.issue.bbox_malformed":
|
|
98
|
-
"
|
|
139
|
+
"область на карте в этой ссылке указана не полностью, поэтому она пропущена",
|
|
99
140
|
"search.url.issue.range_malformed":
|
|
100
|
-
"{param}
|
|
141
|
+
"диапазону «{param}» в этой ссылке не хватает чисел, поэтому он пропущен",
|
|
101
142
|
|
|
102
143
|
"search.degraded.title": "Что этот поиск не смог сделать",
|
|
103
144
|
"search.degraded.typo_tolerance":
|
package/src/index.ts
CHANGED
|
@@ -86,7 +86,14 @@ export type {
|
|
|
86
86
|
SearchStatePatch,
|
|
87
87
|
} from "./state/urlState.js";
|
|
88
88
|
|
|
89
|
-
export {
|
|
89
|
+
export {
|
|
90
|
+
countIsEstimate,
|
|
91
|
+
countKind,
|
|
92
|
+
degradationMessageKey,
|
|
93
|
+
isCountNuanceOnly,
|
|
94
|
+
parseDegradations,
|
|
95
|
+
} from "./state/degradations.js";
|
|
96
|
+
export type { SearchCountKind } from "./state/degradations.js";
|
|
90
97
|
|
|
91
98
|
export { buildFacetGroups, facetOptionLabel } from "./state/facets.js";
|
|
92
99
|
export type {
|
|
@@ -95,6 +102,24 @@ export type {
|
|
|
95
102
|
FacetOption,
|
|
96
103
|
} from "./state/facets.js";
|
|
97
104
|
|
|
105
|
+
export {
|
|
106
|
+
RANGE_FEATURE_TYPES,
|
|
107
|
+
buildRangeGroups,
|
|
108
|
+
isRangeFeature,
|
|
109
|
+
isRangeUsable,
|
|
110
|
+
} from "./state/ranges.js";
|
|
111
|
+
export type { BuildRangeGroupsInput, RangeGroup } from "./state/ranges.js";
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
SEARCH_BOX_DEBOUNCE_MS,
|
|
115
|
+
SEARCH_BOX_SUGGEST_DEBOUNCE_MS,
|
|
116
|
+
SEARCH_DEFAULT_PAGE_SIZE,
|
|
117
|
+
SEARCH_MAX_PAGE_SIZE,
|
|
118
|
+
SEARCH_QUERY_MAX_CHARS,
|
|
119
|
+
SUGGEST_MAX_LIMIT,
|
|
120
|
+
SUGGEST_MIN_CHARS,
|
|
121
|
+
} from "./state/limits.js";
|
|
122
|
+
|
|
98
123
|
// ── model (runtime wiring, query keys, context, hooks) ───────────────────────
|
|
99
124
|
export { createSearchRuntime } from "./model/runtime.js";
|
|
100
125
|
export type {
|
|
@@ -110,7 +135,7 @@ export {
|
|
|
110
135
|
export { searchQueryKeys } from "./model/queryKeys.js";
|
|
111
136
|
export type { SearchQueryKeyParams } from "./model/queryKeys.js";
|
|
112
137
|
|
|
113
|
-
export { useRankingDisclosure, useSearchQuery } from "./model/queries.js";
|
|
138
|
+
export { useRankingDisclosure, useSearchQuery, useSuggest } from "./model/queries.js";
|
|
114
139
|
|
|
115
140
|
// ── headless (renderless components) ─────────────────────────────────────────
|
|
116
141
|
export { SearchProvider } from "./headless/SearchProvider.js";
|
|
@@ -122,9 +147,11 @@ export type {
|
|
|
122
147
|
} from "./headless/SearchStateProvider.js";
|
|
123
148
|
export { SearchResults } from "./headless/SearchResults.js";
|
|
124
149
|
export type { SearchPageInfo, SearchResultsBag } from "./headless/SearchResults.js";
|
|
125
|
-
export { FacetPanel } from "./headless/FacetPanel.js";
|
|
150
|
+
export { FacetPanel, useFacetPanel } from "./headless/FacetPanel.js";
|
|
126
151
|
export type { FacetPanelBag } from "./headless/FacetPanel.js";
|
|
127
152
|
export { useAppliedSort } from "./headless/useAppliedSort.js";
|
|
153
|
+
export { useSearchBox } from "./headless/useSearchBox.js";
|
|
154
|
+
export type { SearchBoxBag, UseSearchBoxOptions } from "./headless/useSearchBox.js";
|
|
128
155
|
export { RankingDisclosure } from "./headless/RankingDisclosure.js";
|
|
129
156
|
export type { RankingDisclosureBag } from "./headless/RankingDisclosure.js";
|
|
130
157
|
|
package/src/model/queries.ts
CHANGED
|
@@ -6,7 +6,9 @@ import type {
|
|
|
6
6
|
RankingResponse,
|
|
7
7
|
SearchQueryState,
|
|
8
8
|
SearchResponse,
|
|
9
|
+
SuggestResponse,
|
|
9
10
|
} from "../api/types.js";
|
|
11
|
+
import { SUGGEST_MAX_LIMIT, SUGGEST_MIN_CHARS } from "../state/limits.js";
|
|
10
12
|
import { useSearchApi } from "./context.js";
|
|
11
13
|
import { searchQueryKeys } from "./queryKeys.js";
|
|
12
14
|
|
|
@@ -60,6 +62,52 @@ export function useSearchQuery(
|
|
|
60
62
|
});
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Title prefixes from the index, for the search box's typeahead.
|
|
67
|
+
*
|
|
68
|
+
* Not from a query log: stapel-search keeps none, which is a privacy decision
|
|
69
|
+
* before it is a product one (`services.suggest`). So the list is what exists
|
|
70
|
+
* in the catalogue, and choosing one is always a search that has results.
|
|
71
|
+
*
|
|
72
|
+
* `enabled` is the debounce's partner, not its replacement: the CALLER holds a
|
|
73
|
+
* debounced prefix (see `useSearchBox`) and this hook refuses to ask about a
|
|
74
|
+
* prefix too short to mean anything. `placeholderData: keepPreviousData` keeps
|
|
75
|
+
* the previous list under the cursor while the next one is in flight — a menu
|
|
76
|
+
* that empties and refills on every keystroke is a menu nobody can click.
|
|
77
|
+
*
|
|
78
|
+
* `staleTime` is a minute: the index changes when something is published, not
|
|
79
|
+
* between two letters of one word.
|
|
80
|
+
*/
|
|
81
|
+
export function useSuggest(params: {
|
|
82
|
+
readonly type: string;
|
|
83
|
+
readonly q: string;
|
|
84
|
+
readonly limit?: number;
|
|
85
|
+
readonly enabled?: boolean;
|
|
86
|
+
}): UseQueryResult<SuggestResponse, StapelApiError> {
|
|
87
|
+
const api = useSearchApi();
|
|
88
|
+
const q = params.q.trim();
|
|
89
|
+
const limit = params.limit;
|
|
90
|
+
return useQuery({
|
|
91
|
+
queryKey: searchQueryKeys.suggest(params.type, q, limit),
|
|
92
|
+
queryFn: ({ signal }) =>
|
|
93
|
+
api.suggest(
|
|
94
|
+
{
|
|
95
|
+
type: params.type,
|
|
96
|
+
q,
|
|
97
|
+
...(limit !== undefined ? { limit: Math.min(limit, SUGGEST_MAX_LIMIT) } : {}),
|
|
98
|
+
},
|
|
99
|
+
{ signal }
|
|
100
|
+
),
|
|
101
|
+
enabled:
|
|
102
|
+
(params.enabled ?? true) &&
|
|
103
|
+
params.type.length > 0 &&
|
|
104
|
+
q.length >= SUGGEST_MIN_CHARS,
|
|
105
|
+
placeholderData: keepPreviousData,
|
|
106
|
+
staleTime: 60_000,
|
|
107
|
+
retry: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
63
111
|
/**
|
|
64
112
|
* The P2B Art. 5 ranking disclosure.
|
|
65
113
|
*
|
|
@@ -67,12 +115,14 @@ export function useSearchQuery(
|
|
|
67
115
|
* not while a person reads the page it explains.
|
|
68
116
|
*/
|
|
69
117
|
export function useRankingDisclosure(
|
|
70
|
-
type?: string
|
|
118
|
+
type?: string,
|
|
119
|
+
options?: { readonly enabled?: boolean }
|
|
71
120
|
): UseQueryResult<RankingResponse, StapelApiError> {
|
|
72
121
|
const api = useSearchApi();
|
|
73
122
|
return useQuery({
|
|
74
123
|
queryKey: searchQueryKeys.ranking(type),
|
|
75
124
|
queryFn: ({ signal }) => api.ranking(type, { signal }),
|
|
125
|
+
enabled: options?.enabled ?? true,
|
|
76
126
|
staleTime: Number.POSITIVE_INFINITY,
|
|
77
127
|
retry: false,
|
|
78
128
|
});
|
|
@@ -66,6 +66,11 @@ export function parseDegradations(
|
|
|
66
66
|
* the envelope's own `exact_total: false`, and the `"exact_total"`
|
|
67
67
|
* degradation. They agree in practice; treating either as decisive is what
|
|
68
68
|
* keeps "≈1 200" from ever being rendered as "1 200".
|
|
69
|
+
*
|
|
70
|
+
* @deprecated Since stapel-search 0.2.0 the envelope answers this directly
|
|
71
|
+
* with `count_is_lower_bound`, and "not exact" now means a FLOOR rather than
|
|
72
|
+
* a fuzzy number. Use {@link countKind}, which reads the wire field and also
|
|
73
|
+
* handles `count: null`.
|
|
69
74
|
*/
|
|
70
75
|
export function countIsEstimate(
|
|
71
76
|
exactTotal: boolean,
|
|
@@ -73,3 +78,58 @@ export function countIsEstimate(
|
|
|
73
78
|
): boolean {
|
|
74
79
|
return !exactTotal || degradations.some((d) => d.kind === "exact_total");
|
|
75
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* How a count may be spoken: as a number, as a floor, or not at all.
|
|
84
|
+
*
|
|
85
|
+
* The three states stapel-search 0.2.0 made explicit, because the version
|
|
86
|
+
* before it had only two and used `0` for both "none match" and "we do not
|
|
87
|
+
* know" — which is how a storefront came to print "About 0 listings" over
|
|
88
|
+
* four visible cards.
|
|
89
|
+
*/
|
|
90
|
+
export type SearchCountKind = "exact" | "at_least" | "unknown";
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read the envelope's three count fields as one decision.
|
|
94
|
+
*
|
|
95
|
+
* `null` is UNKNOWN and renders as no count line — never as `0`, and never
|
|
96
|
+
* as "about 0". Anything the server declines to call exact is rendered as a
|
|
97
|
+
* floor ("N+"), including the defensive case of an `exact_total: false` from
|
|
98
|
+
* a server that predates `count_is_lower_bound`: claiming "at least N" over
|
|
99
|
+
* a page that shows N rows is the one reading that cannot be contradicted by
|
|
100
|
+
* what the reader can see.
|
|
101
|
+
*/
|
|
102
|
+
export function countKind(
|
|
103
|
+
count: number | null | undefined,
|
|
104
|
+
isLowerBound: boolean | undefined,
|
|
105
|
+
exactTotal: boolean,
|
|
106
|
+
degradations: readonly SearchDegradation[]
|
|
107
|
+
): SearchCountKind {
|
|
108
|
+
if (count === null || count === undefined) return "unknown";
|
|
109
|
+
if (isLowerBound === true) return "at_least";
|
|
110
|
+
return countIsEstimate(exactTotal, degradations) ? "at_least" : "exact";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Is `exact_total` the ONLY thing the engine could not do?
|
|
115
|
+
*
|
|
116
|
+
* It is a count NUANCE, not a failed search: the answer is complete, the
|
|
117
|
+
* rows are the right rows, and the single consequence — that the total is a
|
|
118
|
+
* floor — is already spoken by the count itself as "N+". Raising a warning
|
|
119
|
+
* banner over it teaches a reader that a perfectly good result page is
|
|
120
|
+
* broken, and a banner that cries wolf on every landing page is a banner
|
|
121
|
+
* nobody reads on the day `category_rollup` shows up in it.
|
|
122
|
+
*
|
|
123
|
+
* Every other degradation changes what the page MEANS ("typos were not
|
|
124
|
+
* corrected", "subcategories may be missing", "counts are approximate") and
|
|
125
|
+
* still renders. So does `exact_total` when it arrives BESIDE one of them —
|
|
126
|
+
* the list is then a description of a genuinely degraded answer.
|
|
127
|
+
*/
|
|
128
|
+
export function isCountNuanceOnly(
|
|
129
|
+
degradations: readonly SearchDegradation[]
|
|
130
|
+
): boolean {
|
|
131
|
+
return (
|
|
132
|
+
degradations.length > 0 &&
|
|
133
|
+
degradations.every((degradation) => degradation.kind === "exact_total")
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backend's own limits, in one place — and the two timings the client
|
|
3
|
+
* chooses so that it never provokes them.
|
|
4
|
+
*
|
|
5
|
+
* Every number here is a fact about stapel-search's `conf.py` (or, for the
|
|
6
|
+
* debounces, a decision taken because of one). They live in the state layer,
|
|
7
|
+
* beside the codec, because they are properties of the CONTRACT rather than of
|
|
8
|
+
* any component: a control that caps its input at `SEARCH_QUERY_MAX_CHARS`
|
|
9
|
+
* cannot produce `error.400.search_query_too_long`, and a typeahead that waits
|
|
10
|
+
* for `SEARCH_BOX_SUGGEST_DEBOUNCE_MS` does not earn a 429 from a throttled
|
|
11
|
+
* endpoint for typing quickly.
|
|
12
|
+
*
|
|
13
|
+
* They are DEFAULTS, not assertions: a deployment may raise `MAX_PAGE_SIZE` or
|
|
14
|
+
* lower `MAX_QUERY_CHARS`, and nothing here is sent to the server as a claim.
|
|
15
|
+
* The server remains the one entitled to refuse.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** `MAX_QUERY_CHARS` — the longest `q` (and suggest prefix) the server reads. */
|
|
19
|
+
export const SEARCH_QUERY_MAX_CHARS = 200;
|
|
20
|
+
|
|
21
|
+
/** `DEFAULT_PAGE_SIZE` — what a request with no `limit` gets. */
|
|
22
|
+
export const SEARCH_DEFAULT_PAGE_SIZE = 24;
|
|
23
|
+
|
|
24
|
+
/** `MAX_PAGE_SIZE` — the server clamps anything above this. */
|
|
25
|
+
export const SEARCH_MAX_PAGE_SIZE = 100;
|
|
26
|
+
|
|
27
|
+
/** `services.suggest` clamps `limit` into `1..25`. */
|
|
28
|
+
export const SUGGEST_MAX_LIMIT = 25;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The shortest prefix worth asking the index about.
|
|
32
|
+
*
|
|
33
|
+
* One and two letters match nearly everything, so the answer is noise and the
|
|
34
|
+
* request is a keystroke tax on a throttled endpoint (`SuggestThrottle`,
|
|
35
|
+
* scope `search-suggest`). Three is where a prefix starts to mean something.
|
|
36
|
+
*/
|
|
37
|
+
export const SUGGEST_MIN_CHARS = 3;
|
|
38
|
+
|
|
39
|
+
/** How long the typing has to stop before the SEARCH runs. */
|
|
40
|
+
export const SEARCH_BOX_DEBOUNCE_MS = 350;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How long it has to stop before the INDEX is asked for prefixes.
|
|
44
|
+
*
|
|
45
|
+
* Shorter than the commit: a suggestion is only worth having while you are
|
|
46
|
+
* still typing, and it costs a cheap read of one column. Longer than zero: the
|
|
47
|
+
* endpoint is throttled per client, and a request per keystroke is how a person
|
|
48
|
+
* gets 429s for typing quickly.
|
|
49
|
+
*/
|
|
50
|
+
export const SEARCH_BOX_SUGGEST_DEBOUNCE_MS = 150;
|