@stapel/search-react 0.14.2 → 0.16.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 (65) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/api/generated/schema.d.ts +2 -0
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/default/EmptyExits.d.ts +75 -0
  5. package/dist/default/EmptyExits.d.ts.map +1 -0
  6. package/dist/default/EmptyExits.js +100 -0
  7. package/dist/default/EmptyExits.js.map +1 -0
  8. package/dist/default/FacetPanelPane.d.ts +28 -1
  9. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  10. package/dist/default/FacetPanelPane.js +1 -1
  11. package/dist/default/FacetPanelPane.js.map +1 -1
  12. package/dist/default/FilterChips.d.ts +4 -2
  13. package/dist/default/FilterChips.d.ts.map +1 -1
  14. package/dist/default/FilterChips.js +45 -5
  15. package/dist/default/FilterChips.js.map +1 -1
  16. package/dist/default/SearchPage.d.ts +11 -0
  17. package/dist/default/SearchPage.d.ts.map +1 -1
  18. package/dist/default/SearchPage.js +10 -4
  19. package/dist/default/SearchPage.js.map +1 -1
  20. package/dist/default/SearchResultsPane.d.ts +15 -0
  21. package/dist/default/SearchResultsPane.d.ts.map +1 -1
  22. package/dist/default/SearchResultsPane.js +7 -2
  23. package/dist/default/SearchResultsPane.js.map +1 -1
  24. package/dist/default/index.d.ts +2 -0
  25. package/dist/default/index.d.ts.map +1 -1
  26. package/dist/default/index.js +1 -0
  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/headless/useSearchBox.d.ts +16 -9
  32. package/dist/headless/useSearchBox.d.ts.map +1 -1
  33. package/dist/headless/useSearchBox.js +17 -12
  34. package/dist/headless/useSearchBox.js.map +1 -1
  35. package/dist/i18n/es.d.ts.map +1 -1
  36. package/dist/i18n/es.js +5 -0
  37. package/dist/i18n/es.js.map +1 -1
  38. package/dist/i18n/keys.d.ts +15 -0
  39. package/dist/i18n/keys.d.ts.map +1 -1
  40. package/dist/i18n/keys.js +21 -0
  41. package/dist/i18n/keys.js.map +1 -1
  42. package/dist/i18n/ru.d.ts.map +1 -1
  43. package/dist/i18n/ru.js +5 -0
  44. package/dist/i18n/ru.js.map +1 -1
  45. package/dist/state/facets.d.ts.map +1 -1
  46. package/dist/state/facets.js +40 -10
  47. package/dist/state/facets.js.map +1 -1
  48. package/llms.txt +3 -2
  49. package/manifest.json +25 -2
  50. package/nav-manifest.json +1 -1
  51. package/package.json +7 -7
  52. package/src/analytics/generated/events.json +1 -1
  53. package/src/api/generated/schema.ts +2 -0
  54. package/src/default/EmptyExits.tsx +203 -0
  55. package/src/default/FacetPanelPane.tsx +34 -7
  56. package/src/default/FilterChips.tsx +50 -7
  57. package/src/default/SearchPage.tsx +25 -0
  58. package/src/default/SearchResultsPane.tsx +33 -4
  59. package/src/default/index.ts +2 -0
  60. package/src/headless/FacetPanel.tsx +1 -0
  61. package/src/headless/useSearchBox.ts +17 -11
  62. package/src/i18n/es.ts +5 -0
  63. package/src/i18n/keys.ts +22 -0
  64. package/src/i18n/ru.ts +5 -0
  65. package/src/state/facets.ts +36 -8
package/dist/i18n/ru.js CHANGED
@@ -114,6 +114,11 @@ export const searchI18nBundleRu = {
114
114
  "search.filters.all": "Все фильтры",
115
115
  "search.filters.short": "Фильтры",
116
116
  "search.filters.chip_more": ", +{count}",
117
+ "search.empty.exits_title": "Попробуйте расширить поиск",
118
+ "search.empty.up_a_level": "Подняться на уровень выше",
119
+ "search.empty.widen_radius": "Искать в радиусе {km} км",
120
+ "search.empty.anywhere": "Искать везде",
121
+ "search.empty.drop_filter": "Без «{name}»",
117
122
  "search.category.title": "Категория",
118
123
  "search.category.clear": "Искать по всему каталогу",
119
124
  "search.category.current": "Ищем внутри {path}",
@@ -1 +1 @@
1
- {"version":3,"file":"ru.js","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ru.js","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"facets.d.ts","sourceRoot":"","sources":["../../src/state/facets.ts"],"names":[],"mappings":"AAoDA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAKpD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAK3E;AAED,8BAA8B;AAC9B,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;IACzC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAC7B,MAAM,CACJ,MAAM,EACN;QAAE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;KAAE,CACtF,CACF,CAAC;IACF,iCAAiC;IACjC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAgFD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,UAAU,GAAG,SAAS,EAC/B,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3E,MAAM,CAkCR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,UAAU,EAAE,CA4EpF"}
1
+ {"version":3,"file":"facets.d.ts","sourceRoot":"","sources":["../../src/state/facets.ts"],"names":[],"mappings":"AAwDA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAKpD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAK3E;AAED,8BAA8B;AAC9B,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;IACzC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAC7B,MAAM,CACJ,MAAM,EACN;QAAE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;KAAE,CACtF,CACF,CAAC;IACF,iCAAiC;IACjC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAgFD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,UAAU,GAAG,SAAS,EAC/B,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3E,MAAM,CAkCR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,SAAS,UAAU,EAAE,CAoGpF"}
@@ -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
@@ -255,16 +259,42 @@ export function buildFacetGroups(input) {
255
259
  if (!values.includes(value))
256
260
  values.push(value);
257
261
  };
258
- // Declared order first (closed sets), then whatever else the counter
259
- // returned, then anything selected that neither of them mentioned.
260
- for (const value of declared)
261
- if (value in counts)
262
+ if (counted) {
263
+ // Declared order first (closed sets), then whatever else the counter
264
+ // returned, then anything selected that neither of them mentioned.
265
+ for (const value of declared)
266
+ if (value in counts)
267
+ push(value);
268
+ const remaining = Object.keys(counts)
269
+ .filter((value) => !declared.includes(value))
270
+ .sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0) || a.localeCompare(b));
271
+ for (const value of remaining)
262
272
  push(value);
263
- const remaining = Object.keys(counts)
264
- .filter((value) => !declared.includes(value))
265
- .sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0) || a.localeCompare(b));
266
- for (const value of remaining)
267
- push(value);
273
+ }
274
+ else {
275
+ // A COUNT IS NOT A LICENCE TO FILTER.
276
+ //
277
+ // `counts` is empty here by definition — the counter never looked at
278
+ // this slug (`facet_meta.skipped`, the plan overrunning
279
+ // `MAX_FACET_FIELDS`) or the deployment counts nothing at all. Gating
280
+ // the options on `value in counts` therefore left the facet with NONE,
281
+ // and a group with no options is dropped by every surface that draws
282
+ // one. Measured on a live cars leaf: 26 facetable features declared, 12
283
+ // counted, 14 rendered as a warning naming filters the person could not
284
+ // then use — while `/query` accepts `f.<slug>` for every one of them.
285
+ //
286
+ // So an uncounted facet is built from the SCHEMA: the option table the
287
+ // category config already carries (`config.options`), or the captions
288
+ // the answer sent for it. The counts stay `null` below — "nobody
289
+ // counted this" is still said, in the place it belongs (beside the
290
+ // option), and it no longer decides whether the filter exists.
291
+ //
292
+ // A `ref_select` whose config is a bare `optionsRef` pointer has no
293
+ // table here and no options to draw; naming that gap is
294
+ // `MODULE.md`'s job, inventing values would not be.
295
+ for (const value of declared)
296
+ push(value);
297
+ }
268
298
  for (const value of selected)
269
299
  push(value);
270
300
  const labelOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"facets.js","sourceRoot":"","sources":["../../src/state/facets.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"facets.js","sourceRoot":"","sources":["../../src/state/facets.ts"],"names":[],"mappings":"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"}
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
- # @stapel/search-react 0.14.2
1
+ # @stapel/search-react 0.16.0
2
2
 
3
- Headless React flow pair for stapel-search (contract >=0.7 <0.8) — business + state, zero visual opinion.
3
+ Headless React flow pair for stapel-search (contract >=0.9 <0.10) — business + state, zero visual opinion.
4
4
  Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
5
5
  verification-403 interception, i18n engine, analytics facade, TanStack Query layer.
6
6
 
@@ -78,6 +78,7 @@ const { tracked } = useTracked();
78
78
  - search.box → <SearchBox> [empty|typed] demo/SearchBox.demo.tsx
79
79
  - search.classified-filters → <FilterChips> [chips|applied|shapes|views] demo/ClassifiedFilters.demo.tsx
80
80
  - search.degradation-notice → <DegradationNotice> [banner|inline] demo/DegradationNotice.demo.tsx
81
+ - search.empty-exits → <EmptyExits> [every constraint|only the location|with the host's siblings] demo/EmptyExits.demo.tsx
81
82
  - search.facet-panel-pane → <FacetPanelPane> [open-search|narrowed|in the desktop rail] demo/FacetPanelPane.demo.tsx
82
83
  - search.filter-sheet → <SearchPage> [open] demo/FilterSheet.demo.tsx
83
84
  - search.language-select → <LanguageSelect> [offered|from-a-link] demo/LanguageSelect.demo.tsx
package/manifest.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
3
3
  "package": "@stapel/search-react",
4
- "version": "0.14.2",
4
+ "version": "0.16.0",
5
5
  "backend": {
6
6
  "module": "stapel-search",
7
- "contract": ">=0.7 <0.8"
7
+ "contract": ">=0.9 <0.10"
8
8
  },
9
9
  "layers": [
10
10
  "api",
@@ -452,6 +452,24 @@
452
452
  ],
453
453
  "source": "demo/DegradationNotice.demo.tsx"
454
454
  },
455
+ {
456
+ "id": "search.empty-exits",
457
+ "title": "Empty-result exits",
458
+ "description": "A search that found nothing, with a way out of every constraint that could have caused it — up a level of the category path, four times the radius, everywhere, drop one named filter, clear everything. Each button removes exactly one constraint and is offered only when that constraint is actually applied, so the row is never a wall of controls that change nothing. Sibling sections with counts are the exit a buyer most wants and the one this package must not build: walking the tree belongs to the categories pair, so it is a host slot rendered above the derived exits.",
459
+ "component": "EmptyExits",
460
+ "covers": [
461
+ "parentCategory"
462
+ ],
463
+ "tokens": [
464
+ "surface-raised"
465
+ ],
466
+ "variants": [
467
+ "every constraint",
468
+ "only the location",
469
+ "with the host's siblings"
470
+ ],
471
+ "source": "demo/EmptyExits.demo.tsx"
472
+ },
455
473
  {
456
474
  "id": "search.facet-panel-pane",
457
475
  "title": "Filter panel",
@@ -731,6 +749,11 @@
731
749
  "search.degraded.title",
732
750
  "search.degraded.typo_tolerance",
733
751
  "search.degraded.unknown",
752
+ "search.empty.anywhere",
753
+ "search.empty.drop_filter",
754
+ "search.empty.exits_title",
755
+ "search.empty.up_a_level",
756
+ "search.empty.widen_radius",
734
757
  "search.error.unknown",
735
758
  "search.facets.approximate",
736
759
  "search.facets.clear",
package/nav-manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@stapel/search-react",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "entries": [
5
5
  {
6
6
  "id": "search.results",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stapel/search-react",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "description": "Headless React pair for stapel-search: a typed query client, TanStack Query hooks, and a URL-first state codec that makes a search shareable by construction (filters, ranges, geo, sort and the keyset cursor all live in the query string). Drill-down facets rendered with their remaining counts and with the server's own honesty flags — approximate, skipped, degraded — never swallowed; keyset pagination with the window refusal named; DSA Art. 26 `promoted` marking carried into every card slot and the P2B Art. 5 ranking disclosure exposed as data. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin, and /router binds the codec to react-router's useSearchParams.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,12 +54,12 @@
54
54
  {
55
55
  "name": "index — the headless pair (client + hooks + URL codec + facet model incl. the facetability rule and the host label seam): no antd, no react-router",
56
56
  "path": "dist/index.js",
57
- "limit": "10.5 KB"
57
+ "limit": "11 KB"
58
58
  },
59
59
  {
60
- "name": "default — the antd skin (query box + a typeahead that offers CATEGORY destinations with their live counts, filters incl. ranges/geo/category slots + the phone chip row with its leading category chip and the location summary row, results incl. the view switch, the card photo GALLERY as a SkinCarousel strip, ranking) must stay out of the main bundle",
60
+ "name": "default — the antd skin (query box + a typeahead that offers CATEGORY destinations with their live counts, filters incl. ranges/geo/category slots + the phone chip row with its leading category chip and the location summary row, results incl. the view switch, the card photo GALLERY as a SkinCarousel strip, the empty state's derived exits, ranking) must stay out of the main bundle",
61
61
  "path": "dist/default/index.js",
62
- "limit": "20 KB"
62
+ "limit": "21 KB"
63
63
  },
64
64
  {
65
65
  "name": "router — the react-router binding is opt-in; the main entry must never pull a router",
@@ -115,12 +115,12 @@
115
115
  "size-limit": "^11.2.0",
116
116
  "typescript": "^5.8.3",
117
117
  "vitest": "^3.2.4",
118
- "@stapel/attributes-react": "^0.8.1",
119
- "@stapel/core": "^0.22.0",
118
+ "@stapel/attributes-react": "^0.10.0",
119
+ "@stapel/core": "^0.23.0",
120
120
  "@stapel/image": "^0.4.2",
121
121
  "@stapel/showcase": "^0.3.0",
122
122
  "@stapel/tokens": "^0.6.0",
123
- "@stapel/tokens-antd": "^0.11.0"
123
+ "@stapel/tokens-antd": "^0.12.0"
124
124
  },
125
125
  "engines": {
126
126
  "node": ">=22"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "by scripts/gen-events.mjs — do not edit; drift-gated (pnpm gen:events:check)",
3
3
  "package": "@stapel/search-react",
4
- "version": "0.14.2",
4
+ "version": "0.16.0",
5
5
  "defined": [],
6
6
  "flows": []
7
7
  }
@@ -175,6 +175,8 @@ export interface components {
175
175
  counted: string[];
176
176
  /** @description Plan slugs dropped at MAX_FACET_FIELDS — reported, not vanished. */
177
177
  skipped: string[];
178
+ /** @description `f.<slug>`/`r.<slug>` filters this answer did NOT apply, because the category marks the slug non-public (`FeatureDef.visibility` is `owner` or `staff`). A hidden value is not indexed and is not filterable: letting `?f.vin=<value>` through would make the index an exact-match oracle over an identifier. The answer is wider than what was asked for, and says so here rather than silently. */
179
+ dropped_filters: string[];
178
180
  /** @description Range slugs that address a core document column rather than an attribute (`r.price`). Offer them as filters unconditionally: they exist for every document in every category, which is why they are not in the category's own plan. */
179
181
  core_ranges: string[];
180
182
  };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * `<EmptyExits>` — the way out of a search that found nothing.
3
+ *
4
+ * ## What it replaces
5
+ *
6
+ * One sentence. "Nothing matches this search." — and then the page stopped:
7
+ * fifteen filter chips above it, a location the person never chose, and no
8
+ * control that widens anything. Measured on a live board where 2924
9
+ * leaves out of 2924 were empty, that sentence was the terminal state of the
10
+ * whole catalogue — and the two constraints most likely to have caused it (a
11
+ * 25 km radius the page applied on its own, and the narrowest segment of the
12
+ * category path) were exactly the two the person had never typed.
13
+ *
14
+ * ## The exits are derived, never invented
15
+ *
16
+ * Every button here is built out of state this pair already owns, and each
17
+ * one removes exactly one constraint:
18
+ *
19
+ * - **up a level** — drop the last segment of `category=`. The pair holds
20
+ * the path, so this needs no tree; what it cannot do is NAME the parent,
21
+ * which is why the label is "the level above" and not a guess.
22
+ * - **widen the radius** — multiply `geo.radiusKm`. Offered only when a
23
+ * radius is actually applied, because widening a search that was never
24
+ * narrowed is a button that changes nothing.
25
+ * - **search everywhere** — drop the geo constraint entirely. The single
26
+ * most likely culprit on this board: the SERP applied a default radius
27
+ * the category page did not, so the same category read 2 from the API and
28
+ * "0 listings, nothing found" on screen.
29
+ * - **drop one filter** — one button per applied facet slug and per applied
30
+ * range, labelled with the filter's own name. "The narrowest filter" is
31
+ * not a thing this pair can rank, so it does not pretend to: it offers
32
+ * every constraint separately and lets the person pick.
33
+ * - **clear everything** — the existing `search.facets.clear_all`, last,
34
+ * for the person who wants the catalogue back.
35
+ *
36
+ * ## Siblings come from the host
37
+ *
38
+ * "Show me the neighbouring sections, with their counts" is the exit a buyer
39
+ * most wants and the one this package must not build: walking the tree is
40
+ * `categories-react`'s job and the counts are the server's. So it is a SLOT
41
+ * (`renderExtra`), rendered above the derived exits — the host that already
42
+ * draws the category picker has the tree in hand, and a host that fills
43
+ * nothing still gets every exit above.
44
+ *
45
+ * ## Nothing to offer is a valid answer
46
+ *
47
+ * A bare, unfiltered, uncentred search that finds nothing has no exit to
48
+ * offer and this renders the sentence alone. A row of disabled buttons would
49
+ * be worse than the sentence.
50
+ */
51
+ import type { ReactElement, ReactNode } from "react";
52
+ import { Button, Flex, Typography } from "antd";
53
+ import { useT } from "@stapel/core";
54
+ import { spacing } from "@stapel/tokens";
55
+ import type { FeatureDef } from "@stapel/attributes-react";
56
+ import { featureName } from "@stapel/attributes-react";
57
+ import { useSearchState } from "../headless/SearchStateProvider.js";
58
+ import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
59
+
60
+ /**
61
+ * How much wider "widen the radius" makes it.
62
+ *
63
+ * Four, not two: doubling a 25 km radius covers four times the area and
64
+ * usually still nothing on a thin board, so the person taps the same button
65
+ * three times. Four times the radius is sixteen times the area — one tap
66
+ * that visibly changes the answer, which is what makes it an exit rather
67
+ * than a nudge.
68
+ */
69
+ export const RADIUS_WIDEN_FACTOR = 4;
70
+
71
+ export interface EmptyExitsProps {
72
+ /** The category schema, for naming an applied filter the way its chip does. */
73
+ readonly categoryFeatures?: readonly FeatureDef[];
74
+ /**
75
+ * The host's own exits — sibling sections with their counts, most usefully.
76
+ * Rendered first, above the derived ones.
77
+ */
78
+ readonly renderExtra?: () => ReactNode;
79
+ }
80
+
81
+ /** One exit, resolved to its label and the state change it performs. */
82
+ interface Exit {
83
+ readonly id: string;
84
+ readonly label: string;
85
+ readonly apply: () => void;
86
+ }
87
+
88
+ /** The parent of a `root/leaf` path, or `undefined` at the root. */
89
+ export function parentCategory(category: string | undefined): string | undefined {
90
+ if (category === undefined) return undefined;
91
+ const parts = category.split("/").filter((part) => part.length > 0);
92
+ if (parts.length <= 1) return undefined;
93
+ return parts.slice(0, -1).join("/");
94
+ }
95
+
96
+ export function EmptyExits(props: EmptyExitsProps): ReactElement | null {
97
+ const t = useT();
98
+ const { state, setCategory, setGeo, setFilter, setRange, clearAll } =
99
+ useSearchState();
100
+
101
+ const bySlug = new Map<string, FeatureDef>();
102
+ for (const feature of props.categoryFeatures ?? []) bySlug.set(feature.slug, feature);
103
+ const nameOf = (slug: string): string => {
104
+ const feature = bySlug.get(slug);
105
+ return feature === undefined ? slug : t(featureName(feature));
106
+ };
107
+
108
+ const exits: Exit[] = [];
109
+
110
+ const parent = parentCategory(state.category);
111
+ if (parent !== undefined) {
112
+ exits.push({
113
+ id: "up",
114
+ label: t(SEARCH_I18N_KEYS.emptyUpALevel),
115
+ apply: () => {
116
+ setCategory(parent);
117
+ },
118
+ });
119
+ }
120
+
121
+ const geo = state.geo;
122
+ if (geo !== undefined && geo.kind === "center" && geo.radiusKm !== undefined) {
123
+ const wider = Math.round(geo.radiusKm * RADIUS_WIDEN_FACTOR);
124
+ exits.push({
125
+ id: "widen",
126
+ label: t(SEARCH_I18N_KEYS.emptyWidenRadius, { km: wider }),
127
+ apply: () => {
128
+ setGeo({ ...geo, radiusKm: wider });
129
+ },
130
+ });
131
+ }
132
+ if (geo !== undefined) {
133
+ exits.push({
134
+ id: "anywhere",
135
+ label: t(SEARCH_I18N_KEYS.emptyAnywhere),
136
+ apply: () => {
137
+ setGeo(null);
138
+ },
139
+ });
140
+ }
141
+
142
+ for (const slug of Object.keys(state.filters)) {
143
+ if ((state.filters[slug] ?? []).length === 0) continue;
144
+ exits.push({
145
+ id: `filter:${slug}`,
146
+ label: t(SEARCH_I18N_KEYS.emptyDropFilter, { name: nameOf(slug) }),
147
+ apply: () => {
148
+ setFilter(slug, []);
149
+ },
150
+ });
151
+ }
152
+ for (const slug of Object.keys(state.ranges)) {
153
+ exits.push({
154
+ id: `range:${slug}`,
155
+ label: t(SEARCH_I18N_KEYS.emptyDropFilter, { name: nameOf(slug) }),
156
+ apply: () => {
157
+ setRange(slug, null);
158
+ },
159
+ });
160
+ }
161
+
162
+ if (exits.length > 1) {
163
+ exits.push({
164
+ id: "clear",
165
+ label: t(SEARCH_I18N_KEYS.facetsClearAll, { count: exits.length }),
166
+ apply: clearAll,
167
+ });
168
+ }
169
+
170
+ const extra = props.renderExtra?.();
171
+ if (exits.length === 0 && extra === undefined) return null;
172
+
173
+ return (
174
+ <Flex
175
+ vertical
176
+ gap={spacing[3]}
177
+ data-testid="search-empty-exits"
178
+ style={{ marginBlockStart: spacing[4] }}
179
+ >
180
+ {extra}
181
+ {exits.length > 0 && (
182
+ <Flex vertical gap={spacing[2]}>
183
+ <Typography.Text type="secondary">
184
+ {t(SEARCH_I18N_KEYS.emptyExitsTitle)}
185
+ </Typography.Text>
186
+ <Flex wrap gap={spacing[2]}>
187
+ {exits.map((exit) => (
188
+ <Button
189
+ key={exit.id}
190
+ data-testid={`search-empty-exit-${exit.id}`}
191
+ data-analytics="none"
192
+ data-analytics-reason="widening a search is a read, not a flow step"
193
+ onClick={exit.apply}
194
+ >
195
+ {exit.label}
196
+ </Button>
197
+ ))}
198
+ </Flex>
199
+ </Flex>
200
+ )}
201
+ </Flex>
202
+ );
203
+ }
@@ -17,7 +17,10 @@
17
17
  * genuinely are a sample above the backend's candidate cap.
18
18
  * - `skipped` — the slugs the server did not count at all. Their options show
19
19
  * "not counted", never `0`. A silent zero there is the same defect class as
20
- * `data ?? []`: a number that looks like an answer and is not one.
20
+ * `data ?? []`: a number that looks like an answer and is not one. The
21
+ * facet itself is still DRAWN, from the category schema, because the server
22
+ * filters on a slug it never counted; the engine's own list of skipped
23
+ * slugs is a developer's note and lives behind `skippedNotice`.
21
24
  *
22
25
  * ── Two slots, and why they are slots ─────────────────────────────────────
23
26
  *
@@ -183,6 +186,30 @@ export interface FacetPanelPaneProps extends ThemeModeProp {
183
186
  * "Clear all" control lives in it.
184
187
  */
185
188
  readonly heading?: ReactNode;
189
+ /**
190
+ * Print the engine's own note about which slugs it did not COUNT
191
+ * (`facet_meta.skipped`). Default `false` — off, and off for a shopper.
192
+ *
193
+ * ── Why this is opt-in and was not ────────────────────────────────────────
194
+ *
195
+ * The sentence is true and it is not a shopper's sentence. On a live cars
196
+ * leaf it rendered as a yellow warning naming forty-two of the category's
197
+ * own fields — climate-control sub-options, a video-file URL, nine dealer
198
+ * promotions — stacked above the filters. The same class of engine plumbing
199
+ * as the synonym-expansion notice this pair removed earlier, and the owner
200
+ * read it exactly that way: as the page saying something was broken.
201
+ *
202
+ * It was also, until this release, a WARNING ABOUT A CONSEQUENCE THE PANEL
203
+ * IMPOSED: an uncounted facet drew no options, so the note named filters
204
+ * the person could not then use. `buildFacetGroups` now builds those
205
+ * options from the category schema, so the filters are there, counted or
206
+ * not, and the only thing missing is the number beside each option — which
207
+ * every option already says for itself.
208
+ *
209
+ * `true` puts it back, for a developer looking at a deployment's facet plan
210
+ * on a staging surface. Nothing else changes with it.
211
+ */
212
+ readonly skippedNotice?: boolean;
186
213
  }
187
214
 
188
215
  /** The category constraint: the host's control, or the door out of it. */
@@ -487,7 +514,7 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
487
514
  title={t(SEARCH_I18N_KEYS.facetsApproximate)}
488
515
  />
489
516
  )}
490
- {bag.skipped.length > 0 && (
517
+ {props.skippedNotice === true && bag.skipped.length > 0 && (
491
518
  <Alert
492
519
  type="warning"
493
520
  showIcon
@@ -514,11 +541,11 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
514
541
  {(groups) => (
515
542
  <Flex vertical gap={spacing[4]}>
516
543
  {/* A group with no options is a heading with nothing under
517
- it. `power_w` arrives in `skipped` and in no facet map,
518
- so it produced exactly that "Power" printed twice on
519
- the desktop panel, once as the range row and once as a
520
- label over air. The skipped Alert above already names it;
521
- a heading with no control under it names nothing. */}
544
+ it. What is left in that state after `buildFacetGroups`
545
+ learned to read the schema is the genuinely unanswerable
546
+ case: a `ref_select` whose config is a bare pointer into
547
+ a vocabulary this pair cannot read. A heading with no
548
+ control under it names nothing, so it is not drawn. */}
522
549
  {/* Each group draws itself the way its own schema says:
523
550
  pills for a single-choice facet, indented children for a
524
551
  hierarchical one, a fold for a long one. The panel does
@@ -87,6 +87,22 @@
87
87
  * counter ran out of plan slots at `MAX_FACET_FIELDS`, not that a person
88
88
  * cannot filter by it. `r.<slug>` still answers for a skipped slug.
89
89
  *
90
+ * ## The one case where band 3 IS deleted: a barren result
91
+ *
92
+ * There is exactly one piece of evidence strong enough, and the server sends
93
+ * it: the plan was counted (`facet_meta.counted`) over a candidate set of
94
+ * ZERO (`facet_meta.candidates`). Then every counted group is empty and drops
95
+ * out on its own, and the only chips left standing are the ones that never
96
+ * needed a count — the category's numeric attributes, drawn from the schema
97
+ * alone. Measured live on a cars leaf inside a 25 km radius that held no
98
+ * cars, that row read "Price / Colour / Availability / Steering side / Year / VIN or body
99
+ * number / Dealer offer "…" x9": the make and the model gone
100
+ * because they had nothing to count, a body number and nine dealer
101
+ * promotions in their place. An unapplied numeric axis over an empty set
102
+ * narrows nothing that is not already nothing; it states nothing and takes
103
+ * the room the empty state's exits need. APPLIED ones stay, always — a
104
+ * constraint keeps the control that removes it.
105
+ *
90
106
  * ## Why the CATEGORY leads the row
91
107
  *
92
108
  * The owner's navigation model puts levels 1-2 of the catalogue on tiles and
@@ -207,7 +223,8 @@ function specApplied(spec: ChipSpec): boolean {
207
223
  */
208
224
  export function orderChipFilters(
209
225
  ranges: readonly RangeGroup[],
210
- facets: readonly FacetGroup[]
226
+ facets: readonly FacetGroup[],
227
+ options: { readonly barren?: boolean } = {}
211
228
  ): readonly ChipSpec[] {
212
229
  const specs: ChipSpec[] = [
213
230
  ...ranges.map(
@@ -217,7 +234,11 @@ export function orderChipFilters(
217
234
  : { band: "attribute_range", range }
218
235
  ),
219
236
  ...facets.map((facet): ChipSpec => ({ band: "facet", facet })),
220
- ];
237
+ ].filter(
238
+ (spec) =>
239
+ !(options.barren === true && spec.band === "attribute_range") ||
240
+ specApplied(spec)
241
+ );
221
242
  return [...specs].sort((a, b) => {
222
243
  const applied = Number(specApplied(b)) - Number(specApplied(a));
223
244
  if (applied !== 0) return applied;
@@ -311,9 +332,17 @@ function chipLabel(group: FacetGroup, t: (key: string, p?: Record<string, unknow
311
332
  * URL genuinely carries, which is the line the geo chip draws too: never print
312
333
  * a coordinate, always print the name you actually have.
313
334
  */
314
- export function categoryLeaf(path: string): string {
335
+ export function categoryLeaf(path: string): string | undefined {
315
336
  const parts = path.split("/").filter((part) => part.length > 0);
316
- return parts[parts.length - 1] ?? path;
337
+ const leaf = parts[parts.length - 1];
338
+ if (leaf === undefined) return undefined;
339
+ // A path of database IDS has no readable last segment, and printing one
340
+ // puts a green pill reading «165» on the SERP — which is what the live
341
+ // board did, permanently, on every category page. An id is not a
342
+ // half-answer the way a slug is: it names nothing a person could have
343
+ // typed. When the leaf is a bare number the chip falls back to the
344
+ // FILTER's own name, the same thing every other unset chip shows.
345
+ return /^\d+$/.test(leaf) ? undefined : leaf;
317
346
  }
318
347
 
319
348
  export function FilterChips(props: FilterChipsProps): ReactElement | null {
@@ -392,9 +421,23 @@ export function FilterChips(props: FilterChipsProps): ReactElement | null {
392
421
  const categoryChipLabel: ReactNode =
393
422
  category === undefined
394
423
  ? t(SEARCH_I18N_KEYS.categoryTitle)
395
- : (props.categoryLabel ?? categoryLeaf(category));
396
-
397
- const ordered = orderChipFilters(ranges, groups);
424
+ : (props.categoryLabel ??
425
+ categoryLeaf(category) ??
426
+ t(SEARCH_I18N_KEYS.categoryTitle));
427
+
428
+ // The server counted a plan over a candidate set of ZERO. Every counted
429
+ // facet is therefore empty and drops out of `groups` above — and what is
430
+ // left standing is the band that never needed a count: the numeric
431
+ // attributes drawn from the category schema alone. On the live cars leaf
432
+ // that row read "Price / Colour / Availability / Steering side / Year /
433
+ // VIN / Dealer offer x9" — the make and the model gone, a body-number field
434
+ // and nine dealer promotions in their place, on a page with no cars on it.
435
+ // An unapplied attribute range on an empty result narrows nothing that is
436
+ // not already nothing, so it states nothing and takes the room the exits
437
+ // need. Applied ones stay: a constraint always keeps the control that
438
+ // removes it.
439
+ const barren = bag.counted.length > 0 && bag.candidates === 0;
440
+ const ordered = orderChipFilters(ranges, groups, { barren });
398
441
 
399
442
  const geo = state.geo;
400
443
  const showGeoChip =