@stapel/search-react 0.22.0 → 0.24.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 +156 -0
- package/MODULE.md +12 -0
- package/README.md +122 -6
- package/dist/api/types.d.ts +13 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +1 -1
- package/dist/default/FacetGroupControl.d.ts +22 -4
- package/dist/default/FacetGroupControl.d.ts.map +1 -1
- package/dist/default/FacetGroupControl.js +85 -9
- package/dist/default/FacetGroupControl.js.map +1 -1
- package/dist/default/FacetPanelPane.d.ts +41 -0
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +72 -18
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +86 -2
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +210 -4
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/PartitionChips.d.ts +14 -0
- package/dist/default/PartitionChips.d.ts.map +1 -1
- package/dist/default/PartitionChips.js +30 -4
- package/dist/default/PartitionChips.js.map +1 -1
- package/dist/default/RangeFilterRow.d.ts.map +1 -1
- package/dist/default/RangeFilterRow.js +65 -11
- package/dist/default/RangeFilterRow.js.map +1 -1
- package/dist/default/SearchPage.d.ts +65 -1
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +47 -5
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/index.d.ts +4 -4
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +3 -3
- package/dist/default/index.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +9 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +39 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +49 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +9 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/state/facets.d.ts +64 -0
- package/dist/state/facets.d.ts.map +1 -1
- package/dist/state/facets.js +144 -0
- package/dist/state/facets.js.map +1 -1
- package/dist/state/ranges.d.ts +22 -0
- package/dist/state/ranges.d.ts.map +1 -1
- package/dist/state/ranges.js +30 -0
- package/dist/state/ranges.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +14 -1
- package/nav-manifest.json +1 -1
- package/package.json +7 -7
- package/src/analytics/generated/events.json +1 -1
- package/src/api/types.ts +13 -0
- package/src/default/FacetGroupControl.tsx +139 -14
- package/src/default/FacetPanelPane.tsx +131 -19
- package/src/default/FilterChips.tsx +313 -6
- package/src/default/PartitionChips.tsx +47 -4
- package/src/default/RangeFilterRow.tsx +116 -1
- package/src/default/SearchPage.tsx +94 -3
- package/src/default/index.ts +27 -3
- package/src/i18n/es.ts +9 -0
- package/src/i18n/keys.ts +50 -0
- package/src/i18n/ru.ts +9 -0
- package/src/index.ts +4 -0
- package/src/state/facets.ts +152 -0
- package/src/state/ranges.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,161 @@
|
|
|
1
1
|
# @stapel/search-react
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 01edcd1: search: the chip row gains an APPLIED mode — one chip per constraint, each beside the control that drops it
|
|
8
|
+
|
|
9
|
+
A storefront integrator built this row by hand and asked for it back. On a
|
|
10
|
+
desktop the filters are a rail two thousand pixels tall, and picking two values
|
|
11
|
+
left NOTHING between the page header and the first card: the only trace of a
|
|
12
|
+
choice was a pressed button somewhere inside the column and one "clear all (2)"
|
|
13
|
+
beside it. Dropping ONE of the two meant scrolling the rail until the same
|
|
14
|
+
button came back. A constraint on screen must keep the control that removes it,
|
|
15
|
+
and on that surface neither half was true — the constraint was not on screen
|
|
16
|
+
and its control was not beside it.
|
|
17
|
+
|
|
18
|
+
`<FilterChips>` could not be the answer as it stood. It is a row of OPENERS —
|
|
19
|
+
one chip per axis, applied or not, each opening its own `SkinDialog` — which is
|
|
20
|
+
the right shape where the panel is behind a tap and the wrong one where the
|
|
21
|
+
panel is already drawn: beside an open rail it prints the whole panel twice and
|
|
22
|
+
still removes nothing without a modal over the results.
|
|
23
|
+
|
|
24
|
+
**`mode?: "openers" | "applied"`**, default `openers`, which is byte-for-byte
|
|
25
|
+
the row that shipped. In `applied`:
|
|
26
|
+
|
|
27
|
+
- **one chip per applied VALUE and per applied numeric range**, never one per
|
|
28
|
+
axis — three chosen brands are three chips and three removals, where an
|
|
29
|
+
axis-shaped chip would drop all three with one press;
|
|
30
|
+
- **every caption names the axis AND the value** — "Brand: Bosch", "Price: from
|
|
31
|
+
100 to 500" — because beside a dozen axes a bare value names nothing. A core
|
|
32
|
+
money axis prints as money in the currency the answer's own cards carry; an
|
|
33
|
+
attribute prints its schema unit; the bounds themselves print exactly as the
|
|
34
|
+
URL carries them, since the wire never promised a number and reformatting one
|
|
35
|
+
would rewrite the link;
|
|
36
|
+
- **every chip is a real `<button>`** whose press removes exactly that
|
|
37
|
+
constraint and whose accessible name says so. Not an antd `Tag closable`,
|
|
38
|
+
whose close icon is a `<span>` with no tab stop — a constraint a keyboard can
|
|
39
|
+
read and cannot drop — and not a modal detour;
|
|
40
|
+
- **the rail's own clear-all**, beside the chips instead of a column-height
|
|
41
|
+
down the page;
|
|
42
|
+
- **the same label path as the rail**, stamped on the markup:
|
|
43
|
+
`data-label-source` for the axis and `data-value-label-source` for the value,
|
|
44
|
+
each `server | schema | host | none`. A raw index term reaching this row is
|
|
45
|
+
something a storefront's test can fail on rather than eyeball;
|
|
46
|
+
- **nothing at all when nothing is applied** — an empty band above the results
|
|
47
|
+
is furniture — and nothing before the answer lands: both halves of a caption
|
|
48
|
+
are named by the envelope (`facet_labels`, `facet_meta.core_ranges`), so a row
|
|
49
|
+
that drew early would caption a chip with a slug and rename it a moment
|
|
50
|
+
later.
|
|
51
|
+
|
|
52
|
+
Both modes read the SAME bag the rail reads (`useFacetPanel`, `buildRangeGroups`
|
|
53
|
+
over the page's own state), so no two surfaces can disagree about what is
|
|
54
|
+
applied or about what a value is called.
|
|
55
|
+
|
|
56
|
+
**`<SearchPage appliedChips="desktop">`** mounts the row in the results header
|
|
57
|
+
with one prop. `"desktop"` is the case this exists for — where the rail is on
|
|
58
|
+
screen; on the phone the opener row below already states every applied filter on
|
|
59
|
+
its own chip, and a second row would say it twice. `true` draws it in both
|
|
60
|
+
layouts; omitted, nothing changes.
|
|
61
|
+
|
|
62
|
+
`buildAppliedChips`, `rangeChipText`, `rangeLabelSource` and
|
|
63
|
+
`appliedChipTestId` are exported for a host composing its own row, along with
|
|
64
|
+
`FilterChipsMode` and the split prop types (`FilterChipsOpenerProps` /
|
|
65
|
+
`FilterChipsAppliedProps`, unioned as `FilterChipsProps`) — the applied mode
|
|
66
|
+
takes no `onOpenAll` because it opens nothing.
|
|
67
|
+
|
|
68
|
+
## 0.23.0
|
|
69
|
+
|
|
70
|
+
### Minor Changes
|
|
71
|
+
|
|
72
|
+
- e8d7744: search: the rail is the category's own form, and the axes a seller had to fill are the ones a buyer sees first
|
|
73
|
+
|
|
74
|
+
A walk of a live classified's desktop cars page reported three things in one
|
|
75
|
+
breath, and they turned out to be four faults with one shape: the panel was
|
|
76
|
+
answering questions the _answer_ had asked, not the ones the _category_ asks.
|
|
77
|
+
|
|
78
|
+
**The rail is in SCHEMA order, required first.** It ranked by evidence — the
|
|
79
|
+
sum of an axis's counts — which is the right question for a phone chip row
|
|
80
|
+
with room for four and the wrong one for the column a person narrows a
|
|
81
|
+
catalogue in. On three listings the busiest axis is whichever three values
|
|
82
|
+
happen to be counted, so the rail opened on condition and colour while the
|
|
83
|
+
make, the model and the year — the three fields the category marks `mandatory`,
|
|
84
|
+
i.e. the three every seller had to fill — sat below them. `orderFacetGroupsBySchema`
|
|
85
|
+
puts pinned slugs first, then the required features in the schema's own order,
|
|
86
|
+
then the rest in schema order, then what the schema does not name at all in
|
|
87
|
+
evidence order, because with no schema there is no other order to have. Stable
|
|
88
|
+
under a click, which a rail that reshuffles as you tick is not. Past
|
|
89
|
+
`visibleGroups` (default 8) the tail folds under one **All filters (K)**
|
|
90
|
+
control — never a group you have already chosen a value in, because the control
|
|
91
|
+
that removes a filter is the one you came back for. `<SearchPage>` gains
|
|
92
|
+
`partition` (drawn above the price: which half of one template a page is about
|
|
93
|
+
is not a filter among filters) and `pinnedFacets`.
|
|
94
|
+
|
|
95
|
+
**Why the make could vanish.** `facetGroupIsDrawable` is now the one predicate
|
|
96
|
+
the rail and the chip row share, and it says what the old duplicated
|
|
97
|
+
`options.length > 0` said without saying WHY: a group with nothing under it is
|
|
98
|
+
a heading over nothing. The catalogue's `make`, `model`, `generation` and
|
|
99
|
+
`body_type` are `ref_select` features whose config is a bare `optionsRef`
|
|
100
|
+
pointer — no option table in the schema and there never will be — so the moment
|
|
101
|
+
the server's plan does not count one, there is nothing on either side to
|
|
102
|
+
enumerate and the group leaves the page, while every `select`-typed comfort
|
|
103
|
+
option (steering side, power steering, heating) draws its own schema table and
|
|
104
|
+
stays. That is the exact set the walker saw and did not see. The drop is still
|
|
105
|
+
right — a dead heading helps nobody — and it is no longer SILENT: outside
|
|
106
|
+
production one `console.warn` names the axis, says which side is missing
|
|
107
|
+
(uncounted, or a schema that does not define it), and says when the schema
|
|
108
|
+
calls the axis **required**. Both owners of that wiring fault can now see it
|
|
109
|
+
from the page. The regression is pinned against the live answer itself, saved
|
|
110
|
+
as a fixture (`test/liveCars.ts`, captured 2026-09-04): an axis with evidence
|
|
111
|
+
buckets survives the parent node's EMPTY feature list and a def that names it
|
|
112
|
+
without typing it.
|
|
113
|
+
|
|
114
|
+
**A dictionary outranks the pills, and on desktop it is a FIELD.** The live
|
|
115
|
+
make axis is `maxSelected: 1` over a 418-value vocabulary, so "pick one" won
|
|
116
|
+
the shape contest and the control it produced was four hundred pills in a 280px
|
|
117
|
+
rail — a wall with a different border radius. `facetGroupShape` now asks
|
|
118
|
+
"dictionary?" before "single-choice?". And on the rail
|
|
119
|
+
(`dictionaryMode="field"`, which `<SearchPage>` sets for the column layout) a
|
|
120
|
+
dictionary closes into a select-style field reading its chosen values or _Any_,
|
|
121
|
+
which opens the searchable list you already had: a real `role="combobox"`
|
|
122
|
+
button, ArrowDown to open, Escape to close. The phone sheet keeps the list
|
|
123
|
+
inline — the sheet is already the disclosure.
|
|
124
|
+
|
|
125
|
+
**A bounded integer is a picker, not a bare number.** The year was two empty
|
|
126
|
+
number fields; it is `int` with `min: 1900, max: 2027`, which is 128 values and
|
|
127
|
+
therefore a list. `RangeGroup.picker` carries it (newest first — a year picker
|
|
128
|
+
that opens on 1900 is a picker nobody uses) for any `int` feature whose schema
|
|
129
|
+
declares both bounds and spans at most `RANGE_PICKER_MAX_VALUES` (300), and
|
|
130
|
+
`<RangeFilterRow>` draws two from/to selects. Typing still works and carries
|
|
131
|
+
the bounds: a valid in-range number narrows the list, anything else brings the
|
|
132
|
+
whole list back with the bounds said in words, because a year below the
|
|
133
|
+
catalogue's floor otherwise does nothing at all, silently. A mileage
|
|
134
|
+
(`1..1000000`) and the core price stay two typed fields.
|
|
135
|
+
|
|
136
|
+
**The rail's scrollbar is in the gutter, not on the filters.**
|
|
137
|
+
`scrollbar-width: thin` and `scrollbar-gutter: stable` are the standard half
|
|
138
|
+
and they are not enough: on every overlay-scrollbar platform — a Mac by
|
|
139
|
+
default, every iOS browser — the bar is painted OVER the content and the gutter
|
|
140
|
+
reserves nothing, which is why it lay across the checkbox labels. The rail now
|
|
141
|
+
also declares a classic bar through the WebKit pseudo-elements, with a real
|
|
142
|
+
width so it displaces rather than overlaps, and every colour a `--stapel-*`
|
|
143
|
+
custom property so it is the panel's own hairline in both themes rather than a
|
|
144
|
+
grey that glows in the dark one. `railScrollbarCss()` and `RAIL_CLASS` are
|
|
145
|
+
exported for a host that lays out its own column.
|
|
146
|
+
|
|
147
|
+
**`<PartitionChips variant="segmented">`** is the desktop rail's shape of the
|
|
148
|
+
same choice: one joined control under its own label instead of a wrapping pill
|
|
149
|
+
row, which in a 280px column is two ragged lines. The SEMANTICS do not vary
|
|
150
|
+
with the variant — the same `radiogroup`, the same roving tabindex, the same
|
|
151
|
+
arrow keys — because a segmented look is a border-radius decision, and swapping
|
|
152
|
+
in a component that draws joined cells by giving up "exactly one of these is
|
|
153
|
+
true" would trade the accessible half of the control for the visible half.
|
|
154
|
+
|
|
155
|
+
Also: `FacetLabels.label_translatable` is typed. The live answer sends it
|
|
156
|
+
beside every group label and the pinned `schema.json` does not describe it, so
|
|
157
|
+
a fixture captured from the wire was a type error.
|
|
158
|
+
|
|
3
159
|
## 0.22.0
|
|
4
160
|
|
|
5
161
|
### Minor Changes
|
package/MODULE.md
CHANGED
|
@@ -207,6 +207,18 @@ Set on `<SearchPage>`, it reaches both filter surfaces: the desktop panel and
|
|
|
207
207
|
the phone chip row read one `useFacetPanel`, so they cannot print two words for
|
|
208
208
|
one value.
|
|
209
209
|
|
|
210
|
+
**The gap the seam does not close: EXISTENCE.** `resolveFacetLabels` names
|
|
211
|
+
values; it does not enumerate them. A `ref_select` group the server did not
|
|
212
|
+
count therefore has no options from either side — the answer counted nothing,
|
|
213
|
+
the schema holds a pointer — and `facetGroupIsDrawable` refuses to draw a
|
|
214
|
+
heading over nothing. Measured on a live cars leaf, that is exactly the set a
|
|
215
|
+
buyer misses (make, model, generation, body type) while every `select`-typed
|
|
216
|
+
option keeps its schema table. The client cannot invent the values, so the fix
|
|
217
|
+
is on the other side — the facet plan must count a required vocabulary axis —
|
|
218
|
+
and until it does, the disappearance is reported: one `console.warn` per slug
|
|
219
|
+
outside production, naming the axis, the missing side, and whether the schema
|
|
220
|
+
marks it required.
|
|
221
|
+
|
|
210
222
|
## The chip row's order is the row's product
|
|
211
223
|
|
|
212
224
|
At 390px about four chips fit before the fold. On a live phone category the
|
package/README.md
CHANGED
|
@@ -157,6 +157,46 @@ Every `FacetGroup` and every `FacetOption` carries `labelSource:
|
|
|
157
157
|
"server" | "schema" | "host" | "none"`, so "did anybody actually name this?" is
|
|
158
158
|
answerable without looking at the string.
|
|
159
159
|
|
|
160
|
+
## The rail is in schema order, required first
|
|
161
|
+
|
|
162
|
+
The panel used to rank its groups by **evidence** — the sum of an axis's
|
|
163
|
+
counts. That is the right question for a phone chip row with room for four and
|
|
164
|
+
the wrong one for the column a person narrows a catalogue in: on a live cars
|
|
165
|
+
page it opened on *condition* and *colour*, because with three listings the
|
|
166
|
+
busiest axis is whichever three values happen to be counted, while *make*,
|
|
167
|
+
*model* and *year* — the three fields the category marks `mandatory`, i.e. the
|
|
168
|
+
three every seller had to fill — sat below them.
|
|
169
|
+
|
|
170
|
+
The rail now follows the **category's own order**, in four bands: pinned slugs,
|
|
171
|
+
then `mandatory` features in schema order, then the rest in schema order, then
|
|
172
|
+
whatever the schema does not name at all (evidence order among themselves —
|
|
173
|
+
with no schema there is no other order to have). Stable under a click, which a
|
|
174
|
+
rail that reshuffles as you tick is not.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<SearchPage
|
|
178
|
+
partition={<PartitionChips variant="segmented" items={children} … />}
|
|
179
|
+
pinnedFacets={["body_type_ref_select"]}
|
|
180
|
+
/>
|
|
181
|
+
orderFacetGroupsBySchema({ groups, categoryFeatures, pinned })
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Past `visibleGroups` (default `FACET_VISIBLE_GROUPS`, 8) the tail folds under
|
|
185
|
+
one **All filters (K)** control — except a group you have already chosen a
|
|
186
|
+
value in, which is never folded away: the control that removes a filter is the
|
|
187
|
+
one you came back for. The phone sheet passes `visibleGroups={null}`; a surface
|
|
188
|
+
devoted to filtering has the room.
|
|
189
|
+
|
|
190
|
+
`facetGroupIsDrawable(group)` is the one rule the rail and the chip row share
|
|
191
|
+
for "is there anything to draw here". A group with no options is a heading over
|
|
192
|
+
nothing — after `buildFacetGroups` learned to read the schema, what is left in
|
|
193
|
+
that state is a `ref_select` whose config is a bare `optionsRef` pointer and
|
|
194
|
+
which the server did not count, so there is nothing to enumerate from either
|
|
195
|
+
side. It is not drawn, and outside production it is **named**: a
|
|
196
|
+
`console.warn` says which axis went, why, and whether the schema calls it
|
|
197
|
+
required. That is the fault that took *make* off a live cars rail while every
|
|
198
|
+
`select`-typed comfort option drew its own table and stayed.
|
|
199
|
+
|
|
160
200
|
## A vocabulary is a dictionary, not a checkbox list
|
|
161
201
|
|
|
162
202
|
Past eight **evidence buckets** a `ref_select` group (or an untyped group that
|
|
@@ -173,11 +213,84 @@ answer — stapel-search caps a vocabulary-backed group at
|
|
|
173
213
|
this), so a 418-term make dictionary arrives whole and the box has everything
|
|
174
214
|
it filters.
|
|
175
215
|
|
|
216
|
+
A dictionary outranks the pills, too: the live `make` axis is `maxSelected: 1`
|
|
217
|
+
over a 418-value vocabulary, so "pick one" used to win and the control it drew
|
|
218
|
+
was four hundred pills in a 280px rail.
|
|
219
|
+
|
|
220
|
+
On the **desktop rail** a dictionary is a select-style FIELD
|
|
221
|
+
(`dictionaryMode="field"`, which `<SearchPage>` sets for the column layout):
|
|
222
|
+
closed it reads the chosen values or *Any*, and it opens the searchable list —
|
|
223
|
+
a real `role="combobox"` button, Enter/Space to open, ArrowDown to open,
|
|
224
|
+
Escape to close. The phone sheet keeps the list inline, because the sheet is
|
|
225
|
+
already the disclosure.
|
|
226
|
+
|
|
176
227
|
```tsx
|
|
177
228
|
facetGroupShape(group) // "segmented" | "nested" | "checkbox" | "dictionary"
|
|
178
229
|
isDictionaryFacet(group) // > FACET_DICTIONARY_THRESHOLD counted buckets
|
|
179
230
|
```
|
|
180
231
|
|
|
232
|
+
## A bounded integer is a picker, not a bare number
|
|
233
|
+
|
|
234
|
+
A year is not a quantity a person computes, it is one of a hundred-odd values,
|
|
235
|
+
and on a live cars page it was two empty number fields. An `int` feature whose
|
|
236
|
+
schema declares both bounds and spans at most `RANGE_PICKER_MAX_VALUES` (300)
|
|
237
|
+
gets `RangeGroup.picker` — the value list, newest first — and
|
|
238
|
+
`<RangeFilterRow>` draws two from/to selects over it. Typing still works and
|
|
239
|
+
carries the bounds: a valid in-range number narrows the list, anything else
|
|
240
|
+
brings the whole list back **with the bounds said in words** ("from 1900 to
|
|
241
|
+
2027"), because a year below the catalogue's floor otherwise does nothing at
|
|
242
|
+
all, silently. A mileage (`1..1000000`) and the core price stay two typed
|
|
243
|
+
fields.
|
|
244
|
+
|
|
245
|
+
## The applied filters have a row of their own
|
|
246
|
+
|
|
247
|
+
`<FilterChips>` has two modes. The default, `mode="openers"`, is the phone's
|
|
248
|
+
row: one chip per axis, applied or not, each opening its own picker. Beside a
|
|
249
|
+
desktop rail that shape is wrong twice — it prints the whole panel a second
|
|
250
|
+
time, and no chip in it removes anything without a modal over the results.
|
|
251
|
+
|
|
252
|
+
`mode="applied"` is the other row, and a page mounts it with one prop:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
<SearchPage adapter={adapter} appliedChips="desktop" categoryFeatures={features} />
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`"desktop"` draws it only where the rail is on screen (on the phone the opener
|
|
259
|
+
row below already states every applied filter on its own chip); `true` draws it
|
|
260
|
+
in both layouts. `<FilterChips mode="applied" />` mounts the same row anywhere
|
|
261
|
+
a host wants it.
|
|
262
|
+
|
|
263
|
+
What it draws:
|
|
264
|
+
|
|
265
|
+
- **one chip per applied VALUE and per applied numeric range**, never one per
|
|
266
|
+
axis — three chosen brands are three chips and three removals;
|
|
267
|
+
- **each caption names the axis and the value** — "Brand: Bosch", "Price: from
|
|
268
|
+
100 to 500" — because beside a dozen axes a bare value names nothing. A core
|
|
269
|
+
money axis prints as money, in the currency the answer's own cards carry;
|
|
270
|
+
- **each chip is a real `<button>`** whose press drops exactly that constraint
|
|
271
|
+
and whose accessible name says so (an antd `Tag closable` puts the removal in
|
|
272
|
+
a `<span>` with no tab stop: a constraint a keyboard can read and cannot
|
|
273
|
+
drop), plus the same clear-all the rail's footer runs;
|
|
274
|
+
- **the label path of the rail**, stamped: `data-label-source` for the axis and
|
|
275
|
+
`data-value-label-source` for the value, both `server | schema | host | none`;
|
|
276
|
+
- **nothing at all when nothing is applied**, and nothing before the answer
|
|
277
|
+
lands — a caption that renames itself under the reader is worse than one that
|
|
278
|
+
arrives a moment later.
|
|
279
|
+
|
|
280
|
+
`buildAppliedChips`, `rangeChipText`, `rangeLabelSource` and
|
|
281
|
+
`appliedChipTestId` are exported for a host composing its own row.
|
|
282
|
+
|
|
283
|
+
## The rail's scrollbar is in the gutter, not on the filters
|
|
284
|
+
|
|
285
|
+
The rail scrolls on its own when it outgrows the window. `scrollbar-width:
|
|
286
|
+
thin` plus `scrollbar-gutter: stable` is the standard half and it is not
|
|
287
|
+
enough: on every overlay-scrollbar platform (a Mac by default, every iOS
|
|
288
|
+
browser) the bar is drawn OVER the content and the gutter reserves nothing. So
|
|
289
|
+
the rail also declares a classic bar through the WebKit pseudo-elements, with a
|
|
290
|
+
real width and every colour a `--stapel-*` custom property, so it is the
|
|
291
|
+
panel's own hairline in both themes. `railScrollbarCss()` and `RAIL_CLASS` are
|
|
292
|
+
exported for a host that lays out its own column.
|
|
293
|
+
|
|
181
294
|
## Two browse surfaces the page places
|
|
182
295
|
|
|
183
296
|
Both are exported from `./default` and neither lays itself out — where they
|
|
@@ -194,13 +307,16 @@ not a media query: whether a 390px screen has room is a fact about the page.
|
|
|
194
307
|
```
|
|
195
308
|
|
|
196
309
|
`<PartitionChips>` is the single-select row a `chips` category draws instead of
|
|
197
|
-
a tile grid:
|
|
198
|
-
a `category` in the URL), a real `radiogroup` with
|
|
199
|
-
keys — because exactly one of them is true at a time,
|
|
200
|
-
toggles say the opposite.
|
|
310
|
+
a tile grid: an "all" cell plus one per child, from `{id, path, name}`,
|
|
311
|
+
controlled (the choice is a `category` in the URL), a real `radiogroup` with
|
|
312
|
+
roving tabindex and arrow keys — because exactly one of them is true at a time,
|
|
313
|
+
and `aria-pressed` toggles say the opposite. `variant="segmented"` is the
|
|
314
|
+
desktop rail's shape (one joined control under its own label); the semantics do
|
|
315
|
+
not vary with it.
|
|
201
316
|
|
|
202
317
|
```tsx
|
|
203
318
|
<PartitionChips items={children} value={state.category ?? null}
|
|
319
|
+
variant={isPhone ? "chips" : "segmented"}
|
|
204
320
|
onChange={(path) => patch({ category: path })} />
|
|
205
321
|
```
|
|
206
322
|
|
|
@@ -254,10 +370,10 @@ the configured engine cannot evaluate.
|
|
|
254
370
|
| Layer | Exports |
|
|
255
371
|
|---|---|
|
|
256
372
|
| api | `createSearchApi`, `searchQueryParams`, `SEARCH_SORTS`, wire types |
|
|
257
|
-
| state (pure) | `parseSearchState`, `writeSearchState`, `patchSearchState`, `toggleFilterValue`, `setFilterValues`, `setRangeValue`, `clearFilters`, `activeFilterCount`, `parseDegradations`, `countIsEstimate`, `buildFacetGroups`, `facetOptionLabel`, `translitPrefixMatch`, `translitKey`, `consonantKey` |
|
|
373
|
+
| state (pure) | `parseSearchState`, `writeSearchState`, `patchSearchState`, `toggleFilterValue`, `setFilterValues`, `setRangeValue`, `clearFilters`, `activeFilterCount`, `parseDegradations`, `countIsEstimate`, `buildFacetGroups`, `orderFacetGroupsBySchema`, `facetGroupIsDrawable`, `facetGroupHasEvidence`, `facetOptionLabel`, `translitPrefixMatch`, `translitKey`, `consonantKey` |
|
|
258
374
|
| model | `createSearchRuntime`, `searchQueryKeys`, `useSearchQuery`, `useRankingDisclosure` |
|
|
259
375
|
| headless | `SearchProvider`, `SearchStateProvider`/`useSearchState`, `SearchResults`, `FacetPanel`, `RankingDisclosure` |
|
|
260
|
-
| `./default` | `SearchPage`, `SearchResultsPane`, `FacetPanelPane`, `RankingDisclosurePane`, `SearchBox`, `SortSelect`, `PageSizeSelect`, `LanguageSelect`, `SearchResultCard`, `RangeFilterRow`, `DegradationNotice`, `UrlIssueNotice`, `PopularValues`, `PartitionChips`, `facetGroupShape`, `isDictionaryFacet` (the skin themes itself through `SkinTheme` from `@stapel/tokens-antd/skin`; the pair's own `SearchSkinTheme` is gone as of 0.6.0) |
|
|
376
|
+
| `./default` | `SearchPage`, `SearchResultsPane`, `FacetPanelPane`, `RankingDisclosurePane`, `SearchBox`, `SortSelect`, `PageSizeSelect`, `LanguageSelect`, `SearchResultCard`, `RangeFilterRow`, `DegradationNotice`, `UrlIssueNotice`, `PopularValues`, `PartitionChips`, `FilterChips` (`mode="openers" | "applied"`), `buildAppliedChips`, `facetGroupShape`, `isDictionaryFacet`, `railScrollbarCss`, `RAIL_CLASS`, `FACET_VISIBLE_GROUPS` (the skin themes itself through `SkinTheme` from `@stapel/tokens-antd/skin`; the pair's own `SearchSkinTheme` is gone as of 0.6.0) |
|
|
261
377
|
| `./router` | `useRouterSearchParams` |
|
|
262
378
|
| i18n | `registerSearchI18n` (+ `./i18n/ru`, `./i18n/es`) |
|
|
263
379
|
| errors | `SEARCH_ERRORS`, `explainSearchError`, `SEARCH_WINDOW_EXCEEDED`, `SEARCH_BACKEND_UNAVAILABLE` |
|
package/dist/api/types.d.ts
CHANGED
|
@@ -28,6 +28,19 @@ export type Schemas = components["schemas"];
|
|
|
28
28
|
*/
|
|
29
29
|
export type FacetLabels = Schemas["FacetLabels"] & {
|
|
30
30
|
readonly label?: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Whether {@link label} is a translation KEY rather than a caption, the way
|
|
33
|
+
* `translatable` says it for the values.
|
|
34
|
+
*
|
|
35
|
+
* Measured on a live answer (`category=141/151`, 2026-09-04): the server
|
|
36
|
+
* sends it beside every label — a make caption with
|
|
37
|
+
* `label_translatable: false`, a condition caption with `true` — and the
|
|
38
|
+
* describe it. Declared, not consumed: this pair passes the group heading
|
|
39
|
+
* through its translator either way, and a caption that is not a key comes
|
|
40
|
+
* back unchanged (`translate()` keeps a key it cannot resolve). Typed so a
|
|
41
|
+
* fixture captured from the wire is not a type error.
|
|
42
|
+
*/
|
|
43
|
+
readonly label_translatable?: boolean;
|
|
31
44
|
};
|
|
32
45
|
/** `facet_labels` as a whole: `{slug: {label, translatable, values}}`. */
|
|
33
46
|
export type FacetLabelsMap = Readonly<Record<string, FacetLabels>>;
|
package/dist/api/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,4EAA4E;AAC5E,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;AAE5C;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG;IACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"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"}
|
package/dist/api/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAiMA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,sBAAsB,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,iBAAiB,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiC;IAC5D,OAAO,MAAM,EAAE,KAAK,IAAI,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC;AAC9C,CAAC;AAQD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,YAAY,GAAsB;IAC7C,WAAW;IACX,QAAQ;IACR,WAAW;IACX,YAAY;IACZ,UAAU;CACX,CAAC"}
|
|
@@ -48,10 +48,20 @@ export declare function isDictionaryFacet(group: FacetGroup): boolean;
|
|
|
48
48
|
/**
|
|
49
49
|
* Which of the four shapes a group takes.
|
|
50
50
|
*
|
|
51
|
-
* Order matters
|
|
52
|
-
*
|
|
53
|
-
* is
|
|
54
|
-
* the
|
|
51
|
+
* Order matters, and it changed in one place after a live measurement:
|
|
52
|
+
*
|
|
53
|
+
* - a hierarchical facet is nested even when it is single-choice, because
|
|
54
|
+
* losing the tree costs more than losing the pills;
|
|
55
|
+
* - a DICTIONARY outranks the pills. The make axis on the live cars leaf
|
|
56
|
+
* `maxSelected: 1` over a 418-value vocabulary, so "pick one" won and the
|
|
57
|
+
* control it produced was four hundred pills in a 280px rail — a wall
|
|
58
|
+
* with a different border radius. Above the fold the shape a person needs
|
|
59
|
+
* is a search box, whether or not they may tick two; below it,
|
|
60
|
+
* single-choice still means pills, because `isDictionaryFacet` requires
|
|
61
|
+
* more than {@link FACET_DICTIONARY_THRESHOLD} counted buckets;
|
|
62
|
+
* - and a dictionary is a dictionary before it is a checkbox list, because
|
|
63
|
+
* the checkbox list is the shape it was drawn as when nobody could pick a
|
|
64
|
+
* make.
|
|
55
65
|
*/
|
|
56
66
|
export declare function facetGroupShape(group: FacetGroup): FacetGroupShape;
|
|
57
67
|
/** The group's options in render order, carrying the depth the schema gives. */
|
|
@@ -75,6 +85,14 @@ export interface FacetGroupControlProps {
|
|
|
75
85
|
/** Whether a `collapsible` group STARTS open. Default `true`. The initial
|
|
76
86
|
* value only — the person owns the state after the first click. */
|
|
77
87
|
readonly defaultOpen?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* How a `"dictionary"` group is drawn. `"field"` is the desktop shape — a
|
|
90
|
+
* select-style field reading its chosen values or "Any", which opens the
|
|
91
|
+
* searchable list; `"inline"` (the default) keeps the list open, the shape
|
|
92
|
+
* a phone sheet wants because the sheet is already the disclosure.
|
|
93
|
+
* Meaningless for the other three shapes.
|
|
94
|
+
*/
|
|
95
|
+
readonly dictionaryMode?: "field" | "inline";
|
|
78
96
|
}
|
|
79
97
|
export declare function FacetGroupControl(props: FacetGroupControlProps): ReactElement;
|
|
80
98
|
//# sourceMappingURL=FacetGroupControl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FacetGroupControl.d.ts","sourceRoot":"","sources":["../../src/default/FacetGroupControl.tsx"],"names":[],"mappings":"AAiEA,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,OAAO,CAAC;AAUzD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIlE;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAA8B,CAAC;AAExE,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GACvB,WAAW,GACX,QAAQ,GACR,UAAU,GACV,YAAY,CAAC;AAEjB,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAwCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAS5D;AAED
|
|
1
|
+
{"version":3,"file":"FacetGroupControl.d.ts","sourceRoot":"","sources":["../../src/default/FacetGroupControl.tsx"],"names":[],"mappings":"AAiEA,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,OAAO,CAAC;AAUzD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIlE;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAA8B,CAAC;AAExE,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GACvB,WAAW,GACX,QAAQ,GACR,UAAU,GACV,YAAY,CAAC;AAEjB,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAwCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAS5D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,eAAe,CAOlE;AAWD,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,eAAe,EAAE,CAkC9E;AA2XD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD;2EACuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;gFAC4E;IAC5E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;uEACmE;IACnE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,YAAY,CA2I7E"}
|
|
@@ -66,7 +66,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
66
66
|
import { useState } from "react";
|
|
67
67
|
import { Button, Checkbox, Flex, Input, Typography } from "antd";
|
|
68
68
|
import { useT } from "@stapel/core";
|
|
69
|
-
import { radii, spacing } from "@stapel/tokens";
|
|
69
|
+
import { controls, cssVar, radii, spacing } from "@stapel/tokens";
|
|
70
70
|
import { VOCABULARY_BACKED_TYPES, featureConfig, featureType, } from "@stapel/attributes-react";
|
|
71
71
|
import { translitPrefixMatch } from "../state/translit.js";
|
|
72
72
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
@@ -153,19 +153,29 @@ export function isDictionaryFacet(group) {
|
|
|
153
153
|
/**
|
|
154
154
|
* Which of the four shapes a group takes.
|
|
155
155
|
*
|
|
156
|
-
* Order matters
|
|
157
|
-
*
|
|
158
|
-
* is
|
|
159
|
-
* the
|
|
156
|
+
* Order matters, and it changed in one place after a live measurement:
|
|
157
|
+
*
|
|
158
|
+
* - a hierarchical facet is nested even when it is single-choice, because
|
|
159
|
+
* losing the tree costs more than losing the pills;
|
|
160
|
+
* - a DICTIONARY outranks the pills. The make axis on the live cars leaf
|
|
161
|
+
* `maxSelected: 1` over a 418-value vocabulary, so "pick one" won and the
|
|
162
|
+
* control it produced was four hundred pills in a 280px rail — a wall
|
|
163
|
+
* with a different border radius. Above the fold the shape a person needs
|
|
164
|
+
* is a search box, whether or not they may tick two; below it,
|
|
165
|
+
* single-choice still means pills, because `isDictionaryFacet` requires
|
|
166
|
+
* more than {@link FACET_DICTIONARY_THRESHOLD} counted buckets;
|
|
167
|
+
* - and a dictionary is a dictionary before it is a checkbox list, because
|
|
168
|
+
* the checkbox list is the shape it was drawn as when nobody could pick a
|
|
169
|
+
* make.
|
|
160
170
|
*/
|
|
161
171
|
export function facetGroupShape(group) {
|
|
162
172
|
const feature = group.feature;
|
|
163
173
|
if (feature !== undefined && featureType(feature) === "hierarchical_select") {
|
|
164
174
|
return "nested";
|
|
165
175
|
}
|
|
166
|
-
if (
|
|
167
|
-
return "
|
|
168
|
-
return
|
|
176
|
+
if (isDictionaryFacet(group))
|
|
177
|
+
return "dictionary";
|
|
178
|
+
return singleChoice(feature) ? "segmented" : "checkbox";
|
|
169
179
|
}
|
|
170
180
|
/**
|
|
171
181
|
* Is this node in the group's uncounted tail — an option with no evidence
|
|
@@ -339,6 +349,70 @@ function DictionaryBody(props) {
|
|
|
339
349
|
? t(SEARCH_I18N_KEYS.facetsShowLess)
|
|
340
350
|
: t(SEARCH_I18N_KEYS.facetsShowAll, { count: matched.length }) }))] }));
|
|
341
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* The closed face of a dictionary group on DESKTOP: a select-shaped field
|
|
354
|
+
* that reads what is chosen, or "Any".
|
|
355
|
+
*
|
|
356
|
+
* A 418-value vocabulary rendered as a permanently-open box plus a scrolling
|
|
357
|
+
* list is right in a phone sheet, where the sheet IS the disclosure and there
|
|
358
|
+
* is one group on screen. In a 280px rail it is the whole rail: the reference
|
|
359
|
+
* classified draws the make as a field reading "Any" that opens the
|
|
360
|
+
* searchable list, and every axis under it stays reachable at a glance.
|
|
361
|
+
*
|
|
362
|
+
* A native `<button role="combobox">` rather than antd's `Select`, for the
|
|
363
|
+
* same reason the option pills are native buttons: the list underneath is
|
|
364
|
+
* this component's — it carries per-option counts, a chosen block and a fold
|
|
365
|
+
* — and a `Select` that only lends its trigger is a dependency on a popup
|
|
366
|
+
* layer for a border. `aria-expanded` on a real button is the disclosure
|
|
367
|
+
* pattern; Escape closes, and the field keeps focus so the next Tab goes
|
|
368
|
+
* where the person expects.
|
|
369
|
+
*/
|
|
370
|
+
const DICTIONARY_FIELD = {
|
|
371
|
+
display: "flex",
|
|
372
|
+
alignItems: "center",
|
|
373
|
+
gap: spacing[1],
|
|
374
|
+
inlineSize: "100%",
|
|
375
|
+
minBlockSize: controls.height,
|
|
376
|
+
paddingInline: spacing[2],
|
|
377
|
+
paddingBlock: spacing[1],
|
|
378
|
+
border: `1px solid ${cssVar("border")}`,
|
|
379
|
+
borderRadius: cssVar("radius-md"),
|
|
380
|
+
background: cssVar("surface"),
|
|
381
|
+
color: "inherit",
|
|
382
|
+
font: "inherit",
|
|
383
|
+
textAlign: "start",
|
|
384
|
+
cursor: "pointer",
|
|
385
|
+
};
|
|
386
|
+
/** The chosen values, or the word for "no constraint on this axis". Never a
|
|
387
|
+
* count: "3 chosen" makes a person open the field to find out which three. */
|
|
388
|
+
const DICTIONARY_FIELD_TEXT = {
|
|
389
|
+
flex: "1 1 auto",
|
|
390
|
+
minInlineSize: 0,
|
|
391
|
+
overflow: "hidden",
|
|
392
|
+
textOverflow: "ellipsis",
|
|
393
|
+
whiteSpace: "nowrap",
|
|
394
|
+
};
|
|
395
|
+
function DictionaryField(props) {
|
|
396
|
+
const t = useT();
|
|
397
|
+
const { group } = props;
|
|
398
|
+
const [open, setOpen] = useState(false);
|
|
399
|
+
const chosen = group.options.filter((option) => option.selected);
|
|
400
|
+
const text = chosen.length > 0
|
|
401
|
+
? chosen.map((option) => option.label).join(", ")
|
|
402
|
+
: t(SEARCH_I18N_KEYS.facetsDictionaryAny);
|
|
403
|
+
return (_jsxs(Flex, { vertical: true, gap: spacing[1], children: [_jsxs("button", { type: "button", role: "combobox", "aria-expanded": open, "aria-haspopup": "listbox", "aria-label": group.label, style: DICTIONARY_FIELD, "data-testid": `facet-dictionary-field-${group.slug}`, "data-chosen": chosen.length, "data-analytics": "none", "data-analytics-reason": "opening a filter group is a read, not a flow step", onKeyDown: (event) => {
|
|
404
|
+
if (event.key === "Escape" && open) {
|
|
405
|
+
event.preventDefault();
|
|
406
|
+
setOpen(false);
|
|
407
|
+
}
|
|
408
|
+
if (event.key === "ArrowDown" && !open) {
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
setOpen(true);
|
|
411
|
+
}
|
|
412
|
+
}, onClick: () => {
|
|
413
|
+
setOpen((was) => !was);
|
|
414
|
+
}, children: [_jsx("span", { style: DICTIONARY_FIELD_TEXT, children: text }), _jsx(ChevronGlyph, { open: open })] }), open && (_jsx(DictionaryBody, { group: group, onToggle: props.onToggle, visible: props.visible }))] }));
|
|
415
|
+
}
|
|
342
416
|
export function FacetGroupControl(props) {
|
|
343
417
|
const t = useT();
|
|
344
418
|
const { group } = props;
|
|
@@ -367,7 +441,9 @@ export function FacetGroupControl(props) {
|
|
|
367
441
|
return (_jsxs(Flex, { vertical: true, gap: shape === "segmented" ? spacing[2] : spacing[1], "data-testid": `facet-group-${group.slug}`, "data-counted": group.counted ? "true" : "false", "data-shape": shape, "data-label-source": group.labelSource, children: [props.heading !== false &&
|
|
368
442
|
(disclosure ? (_jsxs("button", { type: "button", style: DISCLOSURE_HEADER, "aria-expanded": open, "data-testid": `facet-toggle-${group.slug}`, "data-analytics": "none", "data-analytics-reason": "opening a filter group is a read, not a flow step", onClick: () => {
|
|
369
443
|
setOpenState((was) => !was);
|
|
370
|
-
}, children: [_jsx(Typography.Text, { strong: true, children: group.label }), group.selected.length > 0 && (_jsx(Typography.Text, { type: "secondary", "data-testid": `facet-toggle-count-${group.slug}`, children: group.selected.length })), _jsx(ChevronGlyph, { open: open })] })) : (_jsx(Typography.Text, { strong: true, children: group.label }))), open &&
|
|
444
|
+
}, children: [_jsx(Typography.Text, { strong: true, children: group.label }), group.selected.length > 0 && (_jsx(Typography.Text, { type: "secondary", "data-testid": `facet-toggle-count-${group.slug}`, children: group.selected.length })), _jsx(ChevronGlyph, { open: open })] })) : (_jsx(Typography.Text, { strong: true, children: group.label }))), open &&
|
|
445
|
+
shape === "dictionary" &&
|
|
446
|
+
(props.dictionaryMode === "field" ? (_jsx(DictionaryField, { group: group, onToggle: props.onToggle, visible: limit ?? FACET_VISIBLE_OPTIONS })) : (_jsx(DictionaryBody, { group: group, onToggle: props.onToggle, visible: limit ?? FACET_VISIBLE_OPTIONS }))), open && shape !== "dictionary" && (_jsxs(_Fragment, { children: [shape === "segmented" ? (_jsx(Flex, { wrap: true, gap: spacing[2], children: shown.map((node) => (_jsx(OptionPill, { group: group, option: node.option, onToggle: props.onToggle }, node.option.value))) })) : (shown.map((node) => (_jsx(CheckboxRow, { group: group, node: node, onToggle: props.onToggle }, node.option.value)))), folded && (_jsx(Button, { type: "link", size: "small", style: { alignSelf: "flex-start", paddingInline: 0 }, "data-testid": `facet-more-${group.slug}`, "data-analytics": "none", "data-analytics-reason": "expanding a filter group is a read, not a flow step", onClick: () => {
|
|
371
447
|
setExpanded((was) => !was);
|
|
372
448
|
}, children: expanded
|
|
373
449
|
? t(SEARCH_I18N_KEYS.facetsShowLess)
|