@stapel/search-react 0.15.0 → 0.18.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 +132 -0
- package/dist/api/generated/schema.d.ts +20 -10
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/FacetGroupControl.d.ts +10 -0
- package/dist/default/FacetGroupControl.d.ts.map +1 -1
- package/dist/default/FacetGroupControl.js +122 -27
- package/dist/default/FacetGroupControl.js.map +1 -1
- package/dist/default/FacetPanelPane.d.ts +49 -35
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +158 -7
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +25 -0
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +39 -3
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/SearchPage.d.ts +3 -0
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +15 -4
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +9 -1
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +20 -7
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/index.d.ts +1 -1
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -1
- package/dist/default/index.js.map +1 -1
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +1 -0
- package/dist/headless/FacetPanel.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +5 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +19 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +25 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +7 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/state/facets.d.ts +14 -0
- package/dist/state/facets.d.ts.map +1 -1
- package/dist/state/facets.js +59 -10
- package/dist/state/facets.js.map +1 -1
- package/llms.txt +2 -2
- package/manifest.json +8 -2
- package/nav-manifest.json +1 -1
- package/package.json +7 -7
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +20 -10
- package/src/default/FacetGroupControl.tsx +217 -60
- package/src/default/FacetPanelPane.tsx +255 -27
- package/src/default/FilterChips.tsx +69 -2
- package/src/default/SearchPage.tsx +24 -1
- package/src/default/SearchResultsPane.tsx +20 -7
- package/src/default/index.ts +2 -0
- package/src/headless/FacetPanel.tsx +1 -0
- package/src/i18n/es.ts +5 -0
- package/src/i18n/keys.ts +25 -0
- package/src/i18n/ru.ts +7 -0
- package/src/state/facets.ts +55 -8
package/src/i18n/keys.ts
CHANGED
|
@@ -138,6 +138,22 @@ export const SEARCH_I18N_KEYS = {
|
|
|
138
138
|
* tail is longer than one row. */
|
|
139
139
|
facetsShowAll: "search.facets.show_all",
|
|
140
140
|
facetsShowLess: "search.facets.show_less",
|
|
141
|
+
/** The box that searches the FILTER PANEL itself — drawn from six groups
|
|
142
|
+
* up, because a panel a person can no longer scan is a panel they have to
|
|
143
|
+
* query. It narrows presentation only and never touches the URL. */
|
|
144
|
+
facetsSearch: "search.facets.search",
|
|
145
|
+
/** Said where the groups were when the panel-search matches none of them.
|
|
146
|
+
* Distinct from `facetsEmpty`, which says the SEARCH offers no filters —
|
|
147
|
+
* here the filters exist and the query missed. */
|
|
148
|
+
facetsSearchEmpty: "search.facets.search_empty",
|
|
149
|
+
/**
|
|
150
|
+
* The rail's sticky footer: what the applied filters LEFT — "N listings
|
|
151
|
+
* match". A PLURAL FAMILY: it counts a noun in words. Desktop filters
|
|
152
|
+
* apply instantly, so this is feedback, not an apply button; the floor
|
|
153
|
+
* case (`at_least`) reuses `resultsCountAtLeast` rather than growing a
|
|
154
|
+
* second family for the same number.
|
|
155
|
+
*/
|
|
156
|
+
facetsMatchCount: "search.facets.match_count",
|
|
141
157
|
|
|
142
158
|
// ── the filter panel as a whole (phone sheet + host slots) ───────────────
|
|
143
159
|
filtersOpen: "search.filters.open",
|
|
@@ -161,6 +177,9 @@ export const SEARCH_I18N_KEYS = {
|
|
|
161
177
|
/** A chip filtering on more than one value: "Bosch, +2". Not a plural
|
|
162
178
|
* family — nothing is being counted in words. */
|
|
163
179
|
filtersChipMore: "search.filters.chip_more",
|
|
180
|
+
/** The chip standing in for the row's capped tail — opens the full panel.
|
|
181
|
+
* `{count}` is how many chips it hides (D16). */
|
|
182
|
+
filtersChipOverflow: "search.filters.chips_overflow",
|
|
164
183
|
|
|
165
184
|
// ── the way OUT of a search that found nothing ───────────────────────────
|
|
166
185
|
/** Caption above the exit buttons. Not a second "nothing found" — that
|
|
@@ -308,6 +327,7 @@ export const SEARCH_I18N_PLURAL_KEYS: readonly SearchI18nKey[] = [
|
|
|
308
327
|
SEARCH_I18N_KEYS.filtersShowCount,
|
|
309
328
|
SEARCH_I18N_KEYS.filtersShowCountAtLeast,
|
|
310
329
|
SEARCH_I18N_KEYS.boxCategoryCount,
|
|
330
|
+
SEARCH_I18N_KEYS.facetsMatchCount,
|
|
311
331
|
];
|
|
312
332
|
|
|
313
333
|
/**
|
|
@@ -392,6 +412,10 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
392
412
|
"search.facets.range_to_aria": "{feature}, up to",
|
|
393
413
|
"search.facets.show_all": "Show all ({count})",
|
|
394
414
|
"search.facets.show_less": "Show fewer",
|
|
415
|
+
"search.facets.search": "Find a filter",
|
|
416
|
+
"search.facets.search_empty": "No filter matches this",
|
|
417
|
+
"search.facets.match_count.one": "{count} listing matches",
|
|
418
|
+
"search.facets.match_count.other": "{count} listings match",
|
|
395
419
|
"search.facets.range_invalid":
|
|
396
420
|
"“From” is larger than “to”, so nothing could match. Swap them to apply this range.",
|
|
397
421
|
|
|
@@ -406,6 +430,7 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
406
430
|
"search.filters.all": "All filters",
|
|
407
431
|
"search.filters.short": "Filters",
|
|
408
432
|
"search.filters.chip_more": ", +{count}",
|
|
433
|
+
"search.filters.chips_overflow": "More · {count}",
|
|
409
434
|
|
|
410
435
|
"search.empty.exits_title": "Try widening the search",
|
|
411
436
|
"search.empty.up_a_level": "Go up a level",
|
package/src/i18n/ru.ts
CHANGED
|
@@ -112,6 +112,12 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
112
112
|
"search.facets.range_to_aria": "{feature}, до",
|
|
113
113
|
"search.facets.show_all": "Показать все ({count})",
|
|
114
114
|
"search.facets.show_less": "Свернуть",
|
|
115
|
+
"search.facets.search": "Найти фильтр",
|
|
116
|
+
"search.facets.search_empty": "Среди фильтров такого нет",
|
|
117
|
+
"search.facets.match_count.one": "Найдено {count} объявление",
|
|
118
|
+
"search.facets.match_count.few": "Найдено {count} объявления",
|
|
119
|
+
"search.facets.match_count.many": "Найдено {count} объявлений",
|
|
120
|
+
"search.facets.match_count.other": "Найдено {count} объявления",
|
|
115
121
|
"search.facets.range_invalid":
|
|
116
122
|
"«От» больше, чем «до», — под такой диапазон ничего не подойдёт. Поменяйте значения местами.",
|
|
117
123
|
|
|
@@ -130,6 +136,7 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
130
136
|
"search.filters.all": "Все фильтры",
|
|
131
137
|
"search.filters.short": "Фильтры",
|
|
132
138
|
"search.filters.chip_more": ", +{count}",
|
|
139
|
+
"search.filters.chips_overflow": "Ещё {count}",
|
|
133
140
|
|
|
134
141
|
"search.empty.exits_title": "Попробуйте расширить поиск",
|
|
135
142
|
"search.empty.up_a_level": "Подняться на уровень выше",
|
package/src/state/facets.ts
CHANGED
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
* A slug the server SKIPPED (`facet_meta.skipped`, dropped at
|
|
32
32
|
* `MAX_FACET_FIELDS`) is not counted at all. Its options carry `count: null`,
|
|
33
33
|
* never `0` — "we did not count this" and "there are none" are different
|
|
34
|
-
* sentences and the honest one has to survive to the screen.
|
|
34
|
+
* sentences and the honest one has to survive to the screen. Its options come
|
|
35
|
+
* from the CATEGORY SCHEMA, because a count is not a licence to filter: the
|
|
36
|
+
* server accepts `f.<slug>` whether or not it counted it, so an uncounted
|
|
37
|
+
* facet is a filter that exists and a number that is missing, not a filter
|
|
38
|
+
* that does not exist. See {@link buildFacetGroups}.
|
|
35
39
|
*
|
|
36
40
|
* 3. **Not every counted slug is a FILTER.** The plan is built from the leaf
|
|
37
41
|
* category's feature defs and the counter counts whatever is indexed, so a
|
|
@@ -124,6 +128,25 @@ export interface FacetGroup {
|
|
|
124
128
|
readonly selected: readonly string[];
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
/**
|
|
132
|
+
* How many candidate documents carry ANY value of this axis — the evidence
|
|
133
|
+
* that people actually fill this field in. Uncounted options are `null`
|
|
134
|
+
* (never `0` — different sentences) and contribute nothing, so a group the
|
|
135
|
+
* server did not count sums to zero.
|
|
136
|
+
*
|
|
137
|
+
* Two surfaces rank by it, for the same reason: the chip row orders its
|
|
138
|
+
* counted band by coverage (D16 reopen — an imported catalogue gave the
|
|
139
|
+
* phones leaf option tables for its wholesale plumbing, and schema order put
|
|
140
|
+
* them ahead of the brand), and the filter panel opens its top-coverage
|
|
141
|
+
* groups and collapses the rest (the 5717px rail). One definition, or the
|
|
142
|
+
* two surfaces drift into two opinions about which axes matter.
|
|
143
|
+
*/
|
|
144
|
+
export function facetCoverage(group: FacetGroup): number {
|
|
145
|
+
let total = 0;
|
|
146
|
+
for (const option of group.options) total += option.count ?? 0;
|
|
147
|
+
return total;
|
|
148
|
+
}
|
|
149
|
+
|
|
127
150
|
export interface BuildFacetGroupsInput {
|
|
128
151
|
/** The envelope's `facets`: `{slug: {value: count}}`. */
|
|
129
152
|
readonly facets: Readonly<Record<string, Readonly<Record<string, number>>>>;
|
|
@@ -352,13 +375,37 @@ export function buildFacetGroups(input: BuildFacetGroupsInput): readonly FacetGr
|
|
|
352
375
|
const push = (value: string): void => {
|
|
353
376
|
if (!values.includes(value)) values.push(value);
|
|
354
377
|
};
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
378
|
+
if (counted) {
|
|
379
|
+
// Declared order first (closed sets), then whatever else the counter
|
|
380
|
+
// returned, then anything selected that neither of them mentioned.
|
|
381
|
+
for (const value of declared) if (value in counts) push(value);
|
|
382
|
+
const remaining = Object.keys(counts)
|
|
383
|
+
.filter((value) => !declared.includes(value))
|
|
384
|
+
.sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0) || a.localeCompare(b));
|
|
385
|
+
for (const value of remaining) push(value);
|
|
386
|
+
} else {
|
|
387
|
+
// A COUNT IS NOT A LICENCE TO FILTER.
|
|
388
|
+
//
|
|
389
|
+
// `counts` is empty here by definition — the counter never looked at
|
|
390
|
+
// this slug (`facet_meta.skipped`, the plan overrunning
|
|
391
|
+
// `MAX_FACET_FIELDS`) or the deployment counts nothing at all. Gating
|
|
392
|
+
// the options on `value in counts` therefore left the facet with NONE,
|
|
393
|
+
// and a group with no options is dropped by every surface that draws
|
|
394
|
+
// one. Measured on a live cars leaf: 26 facetable features declared, 12
|
|
395
|
+
// counted, 14 rendered as a warning naming filters the person could not
|
|
396
|
+
// then use — while `/query` accepts `f.<slug>` for every one of them.
|
|
397
|
+
//
|
|
398
|
+
// So an uncounted facet is built from the SCHEMA: the option table the
|
|
399
|
+
// category config already carries (`config.options`), or the captions
|
|
400
|
+
// the answer sent for it. The counts stay `null` below — "nobody
|
|
401
|
+
// counted this" is still said, in the place it belongs (beside the
|
|
402
|
+
// option), and it no longer decides whether the filter exists.
|
|
403
|
+
//
|
|
404
|
+
// A `ref_select` whose config is a bare `optionsRef` pointer has no
|
|
405
|
+
// table here and no options to draw; naming that gap is
|
|
406
|
+
// `MODULE.md`'s job, inventing values would not be.
|
|
407
|
+
for (const value of declared) push(value);
|
|
408
|
+
}
|
|
362
409
|
for (const value of selected) push(value);
|
|
363
410
|
|
|
364
411
|
const labelOptions = {
|