@stapel/search-react 0.7.0 → 0.9.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 +61 -0
- package/dist/default/FacetGroupControl.d.ts +40 -0
- package/dist/default/FacetGroupControl.d.ts.map +1 -0
- package/dist/default/FacetGroupControl.js +176 -0
- package/dist/default/FacetGroupControl.js.map +1 -0
- package/dist/default/FacetPanelPane.d.ts +30 -0
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +28 -16
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +37 -0
- package/dist/default/FilterChips.d.ts.map +1 -0
- package/dist/default/FilterChips.js +203 -0
- package/dist/default/FilterChips.js.map +1 -0
- package/dist/default/SearchPage.d.ts +59 -0
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +60 -10
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +19 -0
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +15 -2
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/ViewSwitch.d.ts +66 -0
- package/dist/default/ViewSwitch.d.ts.map +1 -0
- package/dist/default/ViewSwitch.js +49 -0
- package/dist/default/ViewSwitch.js.map +1 -0
- package/dist/default/index.d.ts +12 -1
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +9 -1
- package/dist/default/index.js.map +1 -1
- package/dist/headless/SearchStateProvider.d.ts +35 -0
- package/dist/headless/SearchStateProvider.d.ts.map +1 -1
- package/dist/headless/SearchStateProvider.js +38 -3
- package/dist/headless/SearchStateProvider.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +9 -1
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +32 -1
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +42 -2
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +9 -1
- package/dist/i18n/ru.js.map +1 -1
- package/llms.txt +2 -1
- package/manifest.json +32 -3
- package/nav-manifest.json +1 -1
- package/package.json +8 -8
- package/src/analytics/generated/events.json +1 -1
- package/src/default/FacetGroupControl.tsx +312 -0
- package/src/default/FacetPanelPane.tsx +57 -63
- package/src/default/FilterChips.tsx +423 -0
- package/src/default/SearchPage.tsx +158 -16
- package/src/default/SearchResultsPane.tsx +39 -2
- package/src/default/ViewSwitch.tsx +147 -0
- package/src/default/index.ts +23 -1
- package/src/headless/SearchStateProvider.tsx +80 -3
- package/src/i18n/es.ts +10 -1
- package/src/i18n/keys.ts +44 -2
- package/src/i18n/ru.ts +10 -1
package/dist/i18n/ru.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ru.d.ts","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"ru.d.ts","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAsJhC,CAAC;AAEF;yDACyD;AACzD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAE7D"}
|
package/dist/i18n/ru.js
CHANGED
|
@@ -68,6 +68,9 @@ export const searchI18nBundleRu = {
|
|
|
68
68
|
"search.sort.price_desc": "Сначала дороже",
|
|
69
69
|
"search.sort.distance": "Сначала ближние",
|
|
70
70
|
"search.sort.server_chose": "Отсортировано: {sort}",
|
|
71
|
+
"search.view.label": "Вид",
|
|
72
|
+
"search.view.list": "Списком",
|
|
73
|
+
"search.view.grid": "Плиткой",
|
|
71
74
|
"search.facets.title": "Фильтры",
|
|
72
75
|
"search.facets.loading": "Загружаем фильтры…",
|
|
73
76
|
"search.facets.load_failed": "Не удалось загрузить фильтры.",
|
|
@@ -84,6 +87,8 @@ export const searchI18nBundleRu = {
|
|
|
84
87
|
"search.facets.range_clear": "Сбросить",
|
|
85
88
|
"search.facets.range_from_aria": "{feature}, от",
|
|
86
89
|
"search.facets.range_to_aria": "{feature}, до",
|
|
90
|
+
"search.facets.show_all": "Показать все ({count})",
|
|
91
|
+
"search.facets.show_less": "Свернуть",
|
|
87
92
|
"search.facets.range_invalid": "«От» больше, чем «до», — под такой диапазон ничего не подойдёт. Поменяйте значения местами.",
|
|
88
93
|
"search.filters.open": "Фильтры ({count})",
|
|
89
94
|
"search.filters.apply": "Показать результаты",
|
|
@@ -96,6 +101,9 @@ export const searchI18nBundleRu = {
|
|
|
96
101
|
"search.filters.show_count_at_least.many": "Показать {count}+ объявлений",
|
|
97
102
|
"search.filters.show_count_at_least.other": "Показать {count}+ объявления",
|
|
98
103
|
"search.filters.dismiss": "Закрыть фильтры",
|
|
104
|
+
"search.filters.chips_label": "Фильтры",
|
|
105
|
+
"search.filters.all": "Все фильтры",
|
|
106
|
+
"search.filters.chip_more": ", +{count}",
|
|
99
107
|
"search.category.title": "Категория",
|
|
100
108
|
"search.category.clear": "Искать по всему каталогу",
|
|
101
109
|
"search.category.current": "Ищем внутри {path}",
|
|
@@ -109,7 +117,7 @@ export const searchI18nBundleRu = {
|
|
|
109
117
|
"search.geo.radius_label": "Радиус, км",
|
|
110
118
|
"search.geo.clear": "Везде",
|
|
111
119
|
"search.geo.box": "В показанной области",
|
|
112
|
-
"search.geo.
|
|
120
|
+
"search.geo.chosen_place": "Выбранное место на карте",
|
|
113
121
|
"search.url.issues_title": "Часть этой ссылки прочитать не удалось",
|
|
114
122
|
"search.url.issue.not_a_number": "«{param}» в этой ссылке — не число, поэтому параметр пропущен",
|
|
115
123
|
"search.url.issue.geo_incomplete": "точка в этой ссылке указана наполовину, поэтому она пропущена",
|
package/dist/i18n/ru.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ru.js","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,GAAG,mBAAmB;IAEtB,sBAAsB,EAAE,iCAAiC;IAEzD,sBAAsB,EAAE,YAAY;IACpC,wBAAwB,EAAE,OAAO;IACjC,4BAA4B,EAAE,6BAA6B;IAC3D,sBAAsB,EAAE,qCAAqC;IAC7D,sBAAsB,EAAE,WAAW;IACnC,yEAAyE;IACzE,4DAA4D;IAC5D,sCAAsC,EAAE,6BAA6B;IACrE,sCAAsC,EAAE,6BAA6B;IACrE,uCAAuC,EAAE,6BAA6B;IACtE,wCAAwC,EAAE,6BAA6B;IACvE,mCAAmC,EAAE,qBAAqB;IAC1D,mCAAmC,EAAE,qBAAqB;IAC1D,oCAAoC,EAAE,qBAAqB;IAC3D,qCAAqC,EAAE,qBAAqB;IAC5D,gCAAgC,EAAE,oBAAoB;IACtD,gCAAgC,EAAE,oBAAoB;IACtD,iCAAiC,EAAE,oBAAoB;IACvD,kCAAkC,EAAE,oBAAoB;IACxD,wBAAwB,EAAE,SAAS;IACnC,qBAAqB,EAAE,oBAAoB;IAC3C,qBAAqB,EAAE,qBAAqB;IAC5C,+BAA+B,EAAE,wBAAwB;IACzD,iCAAiC,EAAE,qBAAqB;IACxD,gCAAgC,EAC9B,uFAAuF;IACzF,yBAAyB,EAAE,cAAc;IACzC,8BAA8B,EAC5B,+GAA+G;IACjH,4BAA4B,EAAE,gBAAgB;IAC9C,yBAAyB,EAAE,cAAc;IACzC,qBAAqB,EAAE,SAAS;IAChC,0BAA0B,EAAE,eAAe;IAE3C,kBAAkB,EAAE,OAAO;IAC3B,wBAAwB,EAAE,YAAY;IACtC,mBAAmB,EAAE,OAAO;IAC5B,kBAAkB,EAAE,iBAAiB;IACrC,wBAAwB,EAAE,WAAW;IAErC,mBAAmB,EAAE,YAAY;IACjC,uBAAuB,EAAE,kBAAkB;IAC3C,oBAAoB,EAAE,eAAe;IACrC,uBAAuB,EAAE,iBAAiB;IAC1C,wBAAwB,EAAE,gBAAgB;IAC1C,sBAAsB,EAAE,iBAAiB;IACzC,0BAA0B,EAAE,uBAAuB;IAEnD,qBAAqB,EAAE,SAAS;IAChC,uBAAuB,EAAE,oBAAoB;IAC7C,2BAA2B,EAAE,+BAA+B;IAC5D,qBAAqB,EAAE,gCAAgC;IACvD,qBAAqB,EAAE,UAAU;IACjC,yBAAyB,EAAE,gCAAgC;IAC3D,2BAA2B,EACzB,wFAAwF;IAC1F,uBAAuB,EACrB,uDAAuD;IACzD,2BAA2B,EAAE,cAAc;IAC3C,+BAA+B,EAC7B,6EAA6E;IAC/E,0BAA0B,EAAE,IAAI;IAChC,wBAAwB,EAAE,IAAI;IAC9B,2BAA2B,EAAE,WAAW;IACxC,2BAA2B,EAAE,UAAU;IACvC,+BAA+B,EAAE,eAAe;IAChD,6BAA6B,EAAE,eAAe;IAC9C,6BAA6B,EAC3B,6FAA6F;IAE/F,qBAAqB,EAAE,mBAAmB;IAC1C,sBAAsB,EAAE,qBAAqB;IAC7C,+BAA+B,EAAE,6BAA6B;IAC9D,+BAA+B,EAAE,6BAA6B;IAC9D,gCAAgC,EAAE,6BAA6B;IAC/D,iCAAiC,EAAE,6BAA6B;IAChE,wCAAwC,EAAE,8BAA8B;IACxE,wCAAwC,EAAE,8BAA8B;IACxE,yCAAyC,EAAE,8BAA8B;IACzE,0CAA0C,EAAE,8BAA8B;IAC1E,wBAAwB,EAAE,iBAAiB;
|
|
1
|
+
{"version":3,"file":"ru.js","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"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"}
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @stapel/search-react 0.
|
|
1
|
+
# @stapel/search-react 0.9.0
|
|
2
2
|
|
|
3
3
|
Headless React flow pair for stapel-search (contract >=0.2 <0.3) — business + state, zero visual opinion.
|
|
4
4
|
Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
|
|
@@ -76,6 +76,7 @@ const { tracked } = useTracked();
|
|
|
76
76
|
|
|
77
77
|
## Demos (defineDemo → manifest.demos; compiled, linted, rendered examples)
|
|
78
78
|
- search.box → <SearchBox> [empty|typed] demo/SearchBox.demo.tsx
|
|
79
|
+
- search.classified-filters → <FilterChips> [chips|applied|shapes|views] demo/ClassifiedFilters.demo.tsx
|
|
79
80
|
- search.degradation-notice → <DegradationNotice> [banner|inline] demo/DegradationNotice.demo.tsx
|
|
80
81
|
- search.facet-panel-pane → <FacetPanelPane> [open-search|narrowed] demo/FacetPanelPane.demo.tsx
|
|
81
82
|
- search.filter-sheet → <SearchPage> [open] demo/FilterSheet.demo.tsx
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"backend": {
|
|
6
6
|
"module": "stapel-search",
|
|
7
7
|
"contract": ">=0.2 <0.3"
|
|
@@ -417,6 +417,27 @@
|
|
|
417
417
|
],
|
|
418
418
|
"source": "demo/SearchBox.demo.tsx"
|
|
419
419
|
},
|
|
420
|
+
{
|
|
421
|
+
"id": "search.classified-filters",
|
|
422
|
+
"title": "Filter chips and group shapes",
|
|
423
|
+
"description": "The phone's filter row — one scrolling line of chips, each opening its own bottom sheet, with the whole panel behind the leading icon-only chip and a dot on it when something is applied. Beside it, the three shapes a facet group takes, all three DERIVED from the category schema rather than chosen per screen: pills for a single-choice facet, indented rows for a hierarchical one, and a fold for a long one that says how many are behind it.",
|
|
424
|
+
"component": "FilterChips",
|
|
425
|
+
"covers": [
|
|
426
|
+
"FacetGroupControl",
|
|
427
|
+
"ViewSwitch"
|
|
428
|
+
],
|
|
429
|
+
"tokens": [
|
|
430
|
+
"surface-raised",
|
|
431
|
+
"brand"
|
|
432
|
+
],
|
|
433
|
+
"variants": [
|
|
434
|
+
"chips",
|
|
435
|
+
"applied",
|
|
436
|
+
"shapes",
|
|
437
|
+
"views"
|
|
438
|
+
],
|
|
439
|
+
"source": "demo/ClassifiedFilters.demo.tsx"
|
|
440
|
+
},
|
|
420
441
|
{
|
|
421
442
|
"id": "search.degradation-notice",
|
|
422
443
|
"title": "Degradation notice",
|
|
@@ -694,9 +715,14 @@
|
|
|
694
715
|
"search.facets.range_invalid",
|
|
695
716
|
"search.facets.range_to",
|
|
696
717
|
"search.facets.range_to_aria",
|
|
718
|
+
"search.facets.show_all",
|
|
719
|
+
"search.facets.show_less",
|
|
697
720
|
"search.facets.skipped",
|
|
698
721
|
"search.facets.title",
|
|
722
|
+
"search.filters.all",
|
|
699
723
|
"search.filters.apply",
|
|
724
|
+
"search.filters.chip_more",
|
|
725
|
+
"search.filters.chips_label",
|
|
700
726
|
"search.filters.dismiss",
|
|
701
727
|
"search.filters.open",
|
|
702
728
|
"search.filters.show_count",
|
|
@@ -706,7 +732,7 @@
|
|
|
706
732
|
"search.filters.show_count_at_least.one",
|
|
707
733
|
"search.filters.show_count_at_least.other",
|
|
708
734
|
"search.geo.box",
|
|
709
|
-
"search.geo.
|
|
735
|
+
"search.geo.chosen_place",
|
|
710
736
|
"search.geo.clear",
|
|
711
737
|
"search.geo.radius_km",
|
|
712
738
|
"search.geo.radius_label",
|
|
@@ -764,7 +790,10 @@
|
|
|
764
790
|
"search.url.issue.geo_incomplete",
|
|
765
791
|
"search.url.issue.not_a_number",
|
|
766
792
|
"search.url.issue.range_malformed",
|
|
767
|
-
"search.url.issues_title"
|
|
793
|
+
"search.url.issues_title",
|
|
794
|
+
"search.view.grid",
|
|
795
|
+
"search.view.label",
|
|
796
|
+
"search.view.list"
|
|
768
797
|
],
|
|
769
798
|
"exports": {
|
|
770
799
|
"runtime": [
|
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/search-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": {
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"limit": "10 KB"
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
|
-
"name": "default — the antd skin (query box + typeahead, filters incl. ranges/geo/category slots, results, ranking) must stay out of the main bundle",
|
|
60
|
+
"name": "default — the antd skin (query box + typeahead, filters incl. ranges/geo/category slots + the phone chip row, results incl. the view switch, ranking) must stay out of the main bundle",
|
|
61
61
|
"path": "dist/default/index.js",
|
|
62
|
-
"limit": "
|
|
62
|
+
"limit": "18 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/image": "^0.4.1",
|
|
119
|
-
"@stapel/showcase": "^0.3.0",
|
|
120
|
-
"@stapel/core": "^0.18.1",
|
|
121
|
-
"@stapel/tokens": "^0.5.1",
|
|
122
118
|
"@stapel/attributes-react": "^0.3.1",
|
|
123
|
-
"@stapel/
|
|
119
|
+
"@stapel/core": "^0.20.0",
|
|
120
|
+
"@stapel/image": "^0.4.2",
|
|
121
|
+
"@stapel/showcase": "^0.3.0",
|
|
122
|
+
"@stapel/tokens": "^0.6.0",
|
|
123
|
+
"@stapel/tokens-antd": "^0.8.0"
|
|
124
124
|
},
|
|
125
125
|
"engines": {
|
|
126
126
|
"node": ">=22"
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<FacetGroupControl>` — ONE facet group, drawn the way its own schema says
|
|
3
|
+
* it should be drawn.
|
|
4
|
+
*
|
|
5
|
+
* Until this release every group was the same thing: a flat column of
|
|
6
|
+
* checkboxes, however many options it had and whatever the category schema
|
|
7
|
+
* said about it. That is wrong in three ways a person meets on the first
|
|
8
|
+
* screen of a real catalogue:
|
|
9
|
+
*
|
|
10
|
+
* 1. **A single-choice facet is not a checkbox list.** `maxSelected: 1` is a
|
|
11
|
+
* fact the schema already carries, and `@stapel/attributes-react`'s editor
|
|
12
|
+
* already reads it to draw a segmented control rather than a set of boxes.
|
|
13
|
+
* A filter that says "pick one" with controls that say "pick any" teaches
|
|
14
|
+
* the wrong thing before the click and surprises after it.
|
|
15
|
+
* 2. **A hierarchical facet is not flat.** `hierarchical_select` carries a
|
|
16
|
+
* TREE in `config.options`; rendering its values as one alphabetical
|
|
17
|
+
* column hides that "Sedan" is under "Cars" and puts a child next to its
|
|
18
|
+
* own parent as if they were siblings.
|
|
19
|
+
* 3. **A 60-option facet is not a list, it is a wall.** Every catalogue has
|
|
20
|
+
* one — brand, model, city — and printing all of it pushes every group
|
|
21
|
+
* under it off the screen.
|
|
22
|
+
*
|
|
23
|
+
* ── The presentation is DERIVED, never configured here ────────────────────
|
|
24
|
+
*
|
|
25
|
+
* {@link facetGroupShape} reads the same `config` keys the attributes editor
|
|
26
|
+
* reads (`maxSelected`, `type`, `options`), so a facet cannot look one way in
|
|
27
|
+
* the filter and another way in the composer that produced the value. There is
|
|
28
|
+
* no `presentation` prop on this component and there must not be one: a
|
|
29
|
+
* per-call-site override is how the two halves drift.
|
|
30
|
+
*
|
|
31
|
+
* A group with NO schema (the host passed no `categoryFeatures`, or the slug
|
|
32
|
+
* is not in it) is a flat checkbox list — the honest default, and the shape
|
|
33
|
+
* every group had before.
|
|
34
|
+
*/
|
|
35
|
+
import { useState } from "react";
|
|
36
|
+
import type { CSSProperties, ReactElement } from "react";
|
|
37
|
+
import { Button, Checkbox, Flex, Typography } from "antd";
|
|
38
|
+
import { useT } from "@stapel/core";
|
|
39
|
+
import { radii, spacing } from "@stapel/tokens";
|
|
40
|
+
import { featureConfig, featureType } from "@stapel/attributes-react";
|
|
41
|
+
import type { FeatureDef } from "@stapel/attributes-react";
|
|
42
|
+
import type { FacetGroup, FacetOption } from "../state/facets.js";
|
|
43
|
+
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* How many options a group shows before the rest go behind "Show all".
|
|
47
|
+
*
|
|
48
|
+
* Eight is the count at which a group stops being readable at a glance and
|
|
49
|
+
* starts being a scroll: it is two more than the six the visual pass measured
|
|
50
|
+
* as the fold on a 390px phone with the sheet's own chrome above it, which
|
|
51
|
+
* leaves a group of seven whole rather than truncated by one row.
|
|
52
|
+
*/
|
|
53
|
+
export const FACET_VISIBLE_OPTIONS = 8;
|
|
54
|
+
|
|
55
|
+
/** The three shapes a facet group takes. */
|
|
56
|
+
export type FacetGroupShape = "segmented" | "nested" | "checkbox";
|
|
57
|
+
|
|
58
|
+
/** The option rows of a group, already nested where the schema nests them. */
|
|
59
|
+
export interface FacetOptionNode {
|
|
60
|
+
readonly option: FacetOption;
|
|
61
|
+
/** 0 for a root option; 1+ for a child of a `hierarchical_select` tree. */
|
|
62
|
+
readonly depth: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function numberish(value: unknown): number | undefined {
|
|
66
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The DEPTH of every value a `hierarchical_select` config declares, walked
|
|
71
|
+
* once — `{value: depth}`.
|
|
72
|
+
*
|
|
73
|
+
* The tree lives in `config.options[].children[]` (the same shape
|
|
74
|
+
* `@stapel/attributes-react`'s `Cascader` reads). The facets themselves arrive
|
|
75
|
+
* FLAT from the server, because an index term has no parent; the schema is the
|
|
76
|
+
* only thing that knows one value sits under another.
|
|
77
|
+
*/
|
|
78
|
+
function optionDepths(
|
|
79
|
+
raw: unknown,
|
|
80
|
+
depth = 0,
|
|
81
|
+
into: Map<string, number> = new Map()
|
|
82
|
+
): Map<string, number> {
|
|
83
|
+
if (!Array.isArray(raw)) return into;
|
|
84
|
+
for (const option of raw) {
|
|
85
|
+
if (option === null || typeof option !== "object") continue;
|
|
86
|
+
const entry = option as { value?: unknown; children?: unknown };
|
|
87
|
+
if (entry.value !== undefined && entry.value !== null) {
|
|
88
|
+
into.set(String(entry.value), depth);
|
|
89
|
+
}
|
|
90
|
+
optionDepths(entry.children, depth + 1, into);
|
|
91
|
+
}
|
|
92
|
+
return into;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Is this feature single-choice? The schema's own answer, not a guess. */
|
|
96
|
+
function singleChoice(feature: FeatureDef | undefined): boolean {
|
|
97
|
+
if (feature === undefined) return false;
|
|
98
|
+
// `maxSelected` ABSENT means unlimited — the engine's own default. Reading
|
|
99
|
+
// an absent key as 1 would turn every unconfigured facet into a radio group.
|
|
100
|
+
return numberish(featureConfig(feature)["maxSelected"]) === 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Which of the three shapes a group takes.
|
|
105
|
+
*
|
|
106
|
+
* Order matters: a hierarchical facet is nested even when it is single-choice,
|
|
107
|
+
* because losing the tree costs more than losing the pills.
|
|
108
|
+
*/
|
|
109
|
+
export function facetGroupShape(group: FacetGroup): FacetGroupShape {
|
|
110
|
+
const feature = group.feature;
|
|
111
|
+
if (feature !== undefined && featureType(feature) === "hierarchical_select") {
|
|
112
|
+
return "nested";
|
|
113
|
+
}
|
|
114
|
+
return singleChoice(feature) ? "segmented" : "checkbox";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The group's options in render order, carrying the depth the schema gives. */
|
|
118
|
+
export function facetOptionNodes(group: FacetGroup): readonly FacetOptionNode[] {
|
|
119
|
+
if (facetGroupShape(group) !== "nested" || group.feature === undefined) {
|
|
120
|
+
return group.options.map((option) => ({ option, depth: 0 }));
|
|
121
|
+
}
|
|
122
|
+
const depths = optionDepths(featureConfig(group.feature)["options"]);
|
|
123
|
+
// Depth alone would leave children beside strangers: the options are
|
|
124
|
+
// re-ordered so each child follows its own parent, and a value the schema
|
|
125
|
+
// does not know keeps depth 0 rather than being hidden under someone.
|
|
126
|
+
const byValue = new Map(group.options.map((option) => [option.value, option]));
|
|
127
|
+
const out: FacetOptionNode[] = [];
|
|
128
|
+
const emitted = new Set<string>();
|
|
129
|
+
for (const [value, depth] of depths) {
|
|
130
|
+
const option = byValue.get(value);
|
|
131
|
+
if (option === undefined) continue;
|
|
132
|
+
out.push({ option, depth });
|
|
133
|
+
emitted.add(value);
|
|
134
|
+
}
|
|
135
|
+
for (const option of group.options) {
|
|
136
|
+
if (!emitted.has(option.value)) out.push({ option, depth: 0 });
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A count, or the honest "we did not count this". Never a zero standing in
|
|
142
|
+
* for an absent answer. */
|
|
143
|
+
function OptionCount(props: {
|
|
144
|
+
readonly group: FacetGroup;
|
|
145
|
+
readonly option: FacetOption;
|
|
146
|
+
}): ReactElement {
|
|
147
|
+
const t = useT();
|
|
148
|
+
return (
|
|
149
|
+
<Typography.Text
|
|
150
|
+
type="secondary"
|
|
151
|
+
data-testid={`facet-count-${props.group.slug}-${props.option.value}`}
|
|
152
|
+
>
|
|
153
|
+
{props.option.count === null
|
|
154
|
+
? t(SEARCH_I18N_KEYS.facetsNotCounted)
|
|
155
|
+
: props.option.count}
|
|
156
|
+
</Typography.Text>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The indent one level of a hierarchical facet is drawn with. */
|
|
161
|
+
const NEST_STEP = spacing[5];
|
|
162
|
+
|
|
163
|
+
function CheckboxRow(props: {
|
|
164
|
+
readonly group: FacetGroup;
|
|
165
|
+
readonly node: FacetOptionNode;
|
|
166
|
+
readonly onToggle: (slug: string, value: string) => void;
|
|
167
|
+
}): ReactElement {
|
|
168
|
+
const { group, node } = props;
|
|
169
|
+
return (
|
|
170
|
+
<Flex
|
|
171
|
+
justify="space-between"
|
|
172
|
+
align="center"
|
|
173
|
+
gap={spacing[2]}
|
|
174
|
+
style={
|
|
175
|
+
node.depth > 0
|
|
176
|
+
? { paddingInlineStart: node.depth * NEST_STEP }
|
|
177
|
+
: undefined
|
|
178
|
+
}
|
|
179
|
+
data-depth={node.depth}
|
|
180
|
+
>
|
|
181
|
+
<Checkbox
|
|
182
|
+
checked={node.option.selected}
|
|
183
|
+
data-testid={`facet-option-${group.slug}-${node.option.value}`}
|
|
184
|
+
data-analytics="none"
|
|
185
|
+
data-analytics-reason="a filter is a read, not a flow step"
|
|
186
|
+
onChange={() => {
|
|
187
|
+
props.onToggle(group.slug, node.option.value);
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{node.option.label}
|
|
191
|
+
</Checkbox>
|
|
192
|
+
<OptionCount group={group} option={node.option} />
|
|
193
|
+
</Flex>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One pill of a single-choice group.
|
|
199
|
+
*
|
|
200
|
+
* A real `<button aria-pressed>` rather than antd's `Segmented`: the count has
|
|
201
|
+
* to sit inside the pill (a filter without its remaining count is the
|
|
202
|
+
* drill-down facet turned naive), and `Segmented` takes labels, not rows.
|
|
203
|
+
* `aria-pressed` is what makes a styled button a TOGGLE to a screen reader
|
|
204
|
+
* instead of a link-shaped thing that mysteriously changes the page.
|
|
205
|
+
*/
|
|
206
|
+
function OptionPill(props: {
|
|
207
|
+
readonly group: FacetGroup;
|
|
208
|
+
readonly option: FacetOption;
|
|
209
|
+
readonly onToggle: (slug: string, value: string) => void;
|
|
210
|
+
}): ReactElement {
|
|
211
|
+
const { group, option } = props;
|
|
212
|
+
const style: CSSProperties = { borderRadius: radii.full };
|
|
213
|
+
return (
|
|
214
|
+
<Button
|
|
215
|
+
size="small"
|
|
216
|
+
shape="round"
|
|
217
|
+
type={option.selected ? "primary" : "default"}
|
|
218
|
+
aria-pressed={option.selected}
|
|
219
|
+
style={style}
|
|
220
|
+
data-testid={`facet-option-${group.slug}-${option.value}`}
|
|
221
|
+
data-analytics="none"
|
|
222
|
+
data-analytics-reason="a filter is a read, not a flow step"
|
|
223
|
+
onClick={() => {
|
|
224
|
+
props.onToggle(group.slug, option.value);
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{option.count === null ? option.label : `${option.label} ${option.count}`}
|
|
228
|
+
</Button>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface FacetGroupControlProps {
|
|
233
|
+
readonly group: FacetGroup;
|
|
234
|
+
readonly onToggle: (slug: string, value: string) => void;
|
|
235
|
+
/** Draw the group's own name above its options. `false` for a surface that
|
|
236
|
+
* has already said it — a per-group sheet whose title IS the group. */
|
|
237
|
+
readonly heading?: boolean;
|
|
238
|
+
/** How many options before "Show all". Default {@link FACET_VISIBLE_OPTIONS};
|
|
239
|
+
* `null` shows every option (a sheet devoted to one group has the room). */
|
|
240
|
+
readonly visibleOptions?: number | null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function FacetGroupControl(props: FacetGroupControlProps): ReactElement {
|
|
244
|
+
const t = useT();
|
|
245
|
+
const { group } = props;
|
|
246
|
+
const [expanded, setExpanded] = useState(false);
|
|
247
|
+
const shape = facetGroupShape(group);
|
|
248
|
+
const nodes = facetOptionNodes(group);
|
|
249
|
+
|
|
250
|
+
const limit =
|
|
251
|
+
props.visibleOptions === null
|
|
252
|
+
? null
|
|
253
|
+
: (props.visibleOptions ?? FACET_VISIBLE_OPTIONS);
|
|
254
|
+
// A group one row over the limit is not truncated: hiding a single option
|
|
255
|
+
// behind "Show all (9)" costs a tap to reveal exactly one thing.
|
|
256
|
+
const collapsible = limit !== null && nodes.length > limit + 1;
|
|
257
|
+
const shown = collapsible && !expanded ? nodes.slice(0, limit) : nodes;
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Flex
|
|
261
|
+
vertical
|
|
262
|
+
gap={shape === "segmented" ? spacing[2] : spacing[1]}
|
|
263
|
+
data-testid={`facet-group-${group.slug}`}
|
|
264
|
+
data-counted={group.counted ? "true" : "false"}
|
|
265
|
+
data-shape={shape}
|
|
266
|
+
>
|
|
267
|
+
{props.heading !== false && (
|
|
268
|
+
<Typography.Text strong>{group.label}</Typography.Text>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{shape === "segmented" ? (
|
|
272
|
+
<Flex wrap gap={spacing[2]}>
|
|
273
|
+
{shown.map((node) => (
|
|
274
|
+
<OptionPill
|
|
275
|
+
key={node.option.value}
|
|
276
|
+
group={group}
|
|
277
|
+
option={node.option}
|
|
278
|
+
onToggle={props.onToggle}
|
|
279
|
+
/>
|
|
280
|
+
))}
|
|
281
|
+
</Flex>
|
|
282
|
+
) : (
|
|
283
|
+
shown.map((node) => (
|
|
284
|
+
<CheckboxRow
|
|
285
|
+
key={node.option.value}
|
|
286
|
+
group={group}
|
|
287
|
+
node={node}
|
|
288
|
+
onToggle={props.onToggle}
|
|
289
|
+
/>
|
|
290
|
+
))
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{collapsible && (
|
|
294
|
+
<Button
|
|
295
|
+
type="link"
|
|
296
|
+
size="small"
|
|
297
|
+
style={{ alignSelf: "flex-start", paddingInline: 0 }}
|
|
298
|
+
data-testid={`facet-more-${group.slug}`}
|
|
299
|
+
data-analytics="none"
|
|
300
|
+
data-analytics-reason="expanding a filter group is a read, not a flow step"
|
|
301
|
+
onClick={() => {
|
|
302
|
+
setExpanded((was) => !was);
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
{expanded
|
|
306
|
+
? t(SEARCH_I18N_KEYS.facetsShowLess)
|
|
307
|
+
: t(SEARCH_I18N_KEYS.facetsShowAll, { count: nodes.length })}
|
|
308
|
+
</Button>
|
|
309
|
+
)}
|
|
310
|
+
</Flex>
|
|
311
|
+
);
|
|
312
|
+
}
|