@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/dist/api/generated/schema.d.ts +20 -10
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/default/FacetGroupControl.d.ts +10 -0
  5. package/dist/default/FacetGroupControl.d.ts.map +1 -1
  6. package/dist/default/FacetGroupControl.js +122 -27
  7. package/dist/default/FacetGroupControl.js.map +1 -1
  8. package/dist/default/FacetPanelPane.d.ts +49 -35
  9. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  10. package/dist/default/FacetPanelPane.js +158 -7
  11. package/dist/default/FacetPanelPane.js.map +1 -1
  12. package/dist/default/FilterChips.d.ts +25 -0
  13. package/dist/default/FilterChips.d.ts.map +1 -1
  14. package/dist/default/FilterChips.js +39 -3
  15. package/dist/default/FilterChips.js.map +1 -1
  16. package/dist/default/SearchPage.d.ts +3 -0
  17. package/dist/default/SearchPage.d.ts.map +1 -1
  18. package/dist/default/SearchPage.js +15 -4
  19. package/dist/default/SearchPage.js.map +1 -1
  20. package/dist/default/SearchResultsPane.d.ts +9 -1
  21. package/dist/default/SearchResultsPane.d.ts.map +1 -1
  22. package/dist/default/SearchResultsPane.js +20 -7
  23. package/dist/default/SearchResultsPane.js.map +1 -1
  24. package/dist/default/index.d.ts +1 -1
  25. package/dist/default/index.d.ts.map +1 -1
  26. package/dist/default/index.js +1 -1
  27. package/dist/default/index.js.map +1 -1
  28. package/dist/headless/FacetPanel.d.ts.map +1 -1
  29. package/dist/headless/FacetPanel.js +1 -0
  30. package/dist/headless/FacetPanel.js.map +1 -1
  31. package/dist/i18n/es.d.ts.map +1 -1
  32. package/dist/i18n/es.js +5 -0
  33. package/dist/i18n/es.js.map +1 -1
  34. package/dist/i18n/keys.d.ts +19 -0
  35. package/dist/i18n/keys.d.ts.map +1 -1
  36. package/dist/i18n/keys.js +25 -0
  37. package/dist/i18n/keys.js.map +1 -1
  38. package/dist/i18n/ru.d.ts.map +1 -1
  39. package/dist/i18n/ru.js +7 -0
  40. package/dist/i18n/ru.js.map +1 -1
  41. package/dist/state/facets.d.ts +14 -0
  42. package/dist/state/facets.d.ts.map +1 -1
  43. package/dist/state/facets.js +59 -10
  44. package/dist/state/facets.js.map +1 -1
  45. package/llms.txt +2 -2
  46. package/manifest.json +8 -2
  47. package/nav-manifest.json +1 -1
  48. package/package.json +7 -7
  49. package/src/analytics/generated/events.json +1 -1
  50. package/src/api/generated/schema.ts +20 -10
  51. package/src/default/FacetGroupControl.tsx +217 -60
  52. package/src/default/FacetPanelPane.tsx +255 -27
  53. package/src/default/FilterChips.tsx +69 -2
  54. package/src/default/SearchPage.tsx +24 -1
  55. package/src/default/SearchResultsPane.tsx +20 -7
  56. package/src/default/index.ts +2 -0
  57. package/src/headless/FacetPanel.tsx +1 -0
  58. package/src/i18n/es.ts +5 -0
  59. package/src/i18n/keys.ts +25 -0
  60. package/src/i18n/ru.ts +7 -0
  61. 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": "Подняться на уровень выше",
@@ -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
- // Declared order first (closed sets), then whatever else the counter
356
- // returned, then anything selected that neither of them mentioned.
357
- for (const value of declared) if (value in counts) push(value);
358
- const remaining = Object.keys(counts)
359
- .filter((value) => !declared.includes(value))
360
- .sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0) || a.localeCompare(b));
361
- for (const value of remaining) push(value);
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 = {