@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/MODULE.md +12 -0
  3. package/README.md +122 -6
  4. package/dist/api/types.d.ts +13 -0
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/types.js.map +1 -1
  7. package/dist/default/FacetGroupControl.d.ts +22 -4
  8. package/dist/default/FacetGroupControl.d.ts.map +1 -1
  9. package/dist/default/FacetGroupControl.js +85 -9
  10. package/dist/default/FacetGroupControl.js.map +1 -1
  11. package/dist/default/FacetPanelPane.d.ts +41 -0
  12. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  13. package/dist/default/FacetPanelPane.js +72 -18
  14. package/dist/default/FacetPanelPane.js.map +1 -1
  15. package/dist/default/FilterChips.d.ts +86 -2
  16. package/dist/default/FilterChips.d.ts.map +1 -1
  17. package/dist/default/FilterChips.js +210 -4
  18. package/dist/default/FilterChips.js.map +1 -1
  19. package/dist/default/PartitionChips.d.ts +14 -0
  20. package/dist/default/PartitionChips.d.ts.map +1 -1
  21. package/dist/default/PartitionChips.js +30 -4
  22. package/dist/default/PartitionChips.js.map +1 -1
  23. package/dist/default/RangeFilterRow.d.ts.map +1 -1
  24. package/dist/default/RangeFilterRow.js +65 -11
  25. package/dist/default/RangeFilterRow.js.map +1 -1
  26. package/dist/default/SearchPage.d.ts +65 -1
  27. package/dist/default/SearchPage.d.ts.map +1 -1
  28. package/dist/default/SearchPage.js +47 -5
  29. package/dist/default/SearchPage.js.map +1 -1
  30. package/dist/default/index.d.ts +4 -4
  31. package/dist/default/index.d.ts.map +1 -1
  32. package/dist/default/index.js +3 -3
  33. package/dist/default/index.js.map +1 -1
  34. package/dist/i18n/es.d.ts.map +1 -1
  35. package/dist/i18n/es.js +9 -0
  36. package/dist/i18n/es.js.map +1 -1
  37. package/dist/i18n/keys.d.ts +39 -0
  38. package/dist/i18n/keys.d.ts.map +1 -1
  39. package/dist/i18n/keys.js +49 -0
  40. package/dist/i18n/keys.js.map +1 -1
  41. package/dist/i18n/ru.d.ts.map +1 -1
  42. package/dist/i18n/ru.js +9 -0
  43. package/dist/i18n/ru.js.map +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/state/facets.d.ts +64 -0
  49. package/dist/state/facets.d.ts.map +1 -1
  50. package/dist/state/facets.js +144 -0
  51. package/dist/state/facets.js.map +1 -1
  52. package/dist/state/ranges.d.ts +22 -0
  53. package/dist/state/ranges.d.ts.map +1 -1
  54. package/dist/state/ranges.js +30 -0
  55. package/dist/state/ranges.js.map +1 -1
  56. package/llms.txt +1 -1
  57. package/manifest.json +14 -1
  58. package/nav-manifest.json +1 -1
  59. package/package.json +7 -7
  60. package/src/analytics/generated/events.json +1 -1
  61. package/src/api/types.ts +13 -0
  62. package/src/default/FacetGroupControl.tsx +139 -14
  63. package/src/default/FacetPanelPane.tsx +131 -19
  64. package/src/default/FilterChips.tsx +313 -6
  65. package/src/default/PartitionChips.tsx +47 -4
  66. package/src/default/RangeFilterRow.tsx +116 -1
  67. package/src/default/SearchPage.tsx +94 -3
  68. package/src/default/index.ts +27 -3
  69. package/src/i18n/es.ts +9 -0
  70. package/src/i18n/keys.ts +50 -0
  71. package/src/i18n/ru.ts +9 -0
  72. package/src/index.ts +4 -0
  73. package/src/state/facets.ts +152 -0
  74. 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: `Все | child…` from `{id, path, name}`, controlled (the choice is
198
- a `category` in the URL), a real `radiogroup` with roving tabindex and arrow
199
- keys — because exactly one of them is true at a time, and `aria-pressed`
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` |
@@ -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>>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAoLA;;;;;;;;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"}
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: a hierarchical facet is nested even when it is single-choice,
52
- * because losing the tree costs more than losing the pills; and a dictionary
53
- * is a dictionary before it is a checkbox list, because the checkbox list is
54
- * the shape it was drawn as when nobody could pick a make.
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;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,eAAe,CAOlE;AAWD,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,eAAe,EAAE,CAkC9E;AAwRD,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;CAChC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,YAAY,CAmI7E"}
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: a hierarchical facet is nested even when it is single-choice,
157
- * because losing the tree costs more than losing the pills; and a dictionary
158
- * is a dictionary before it is a checkbox list, because the checkbox list is
159
- * the shape it was drawn as when nobody could pick a make.
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 (singleChoice(feature))
167
- return "segmented";
168
- return isDictionaryFacet(group) ? "dictionary" : "checkbox";
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 && shape === "dictionary" && (_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: () => {
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)