@stapel/search-react 0.36.0 → 0.37.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 +59 -0
- package/README.md +10 -1
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +1 -1
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +10 -3
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +17 -5
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/PartitionChips.d.ts +16 -0
- package/dist/default/PartitionChips.d.ts.map +1 -1
- package/dist/default/PartitionChips.js +14 -3
- package/dist/default/PartitionChips.js.map +1 -1
- package/dist/default/SearchPage.d.ts +24 -1
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +14 -7
- 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 +22 -3
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/SortSelect.d.ts +13 -2
- package/dist/default/SortSelect.d.ts.map +1 -1
- package/dist/default/SortSelect.js +63 -9
- package/dist/default/SortSelect.js.map +1 -1
- package/dist/default/index.d.ts +2 -2
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +2 -2
- package/dist/default/index.js.map +1 -1
- package/llms.txt +2 -2
- package/manifest.json +2 -1
- package/nav-manifest.json +1 -1
- package/package.json +4 -4
- package/src/analytics/generated/events.json +1 -1
- package/src/default/FacetPanelPane.tsx +12 -1
- package/src/default/FilterChips.tsx +17 -5
- package/src/default/PartitionChips.tsx +41 -2
- package/src/default/SearchPage.tsx +45 -5
- package/src/default/SearchResultsPane.tsx +55 -3
- package/src/default/SortSelect.tsx +91 -20
- package/src/default/index.ts +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/search-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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": "14.5 KB"
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
|
-
"name": "default — the antd skin (query box + a typeahead that offers CATEGORY destinations with their live counts, filters incl. ranges/geo/category slots + the phone chip row with its leading category chip and the location summary row + the rail's evidence-ranked disclosure groups, panel search and sticky count/clear footer, results incl. the view switch, the card photo GALLERY as a SkinCarousel strip, the empty state's derived exits, ranking, the dictionary control for a vocabulary facet, the popular-values block, the partition row in both its chip and segmented variants, the select-style dictionary FIELD the desktop rail opens, the from/to pickers a bounded integer axis draws and the rail's own scrollbar sheet) must stay out of the main bundle. Raised 24.5 → 25.5 KB for those four controls, then 25.5 → 26.25 KB for the chip row's APPLIED mode — one chip per applied value and per numeric bound, each removing that one constraint beside a rail that is already on screen (a storefront was carrying its own copy of it), then 26.25 → 27 KB for the \"search in other categories\" LINE — which is a net deletion on the page that mounts it, replacing a full-width block of one row per section that arrived after the results and pushed them, then 27 → 27.75 KB for the two frames the page now tells apart: the dictionary FIELD reaching `<SearchPage>` at all (per-layout default, field in the rail and inline in the sheet) and the footer bar being static in a column and sticky in a sheet, where it used to be pinned over the last two groups everywhere, then 27.75 → 28.5 KB for `categoryHref` on the other-categories line — a real `<a href>` per row with a modifier-click left to the browser, instead of a `<button>` with no address a person could open in a new tab, then 28.5 → 29.25 KB for the phone's dictionary SHEET — a trigger row over the shared `SkinPickerSheet` with a recommended band, an alphabetical rest paged fifty at a time and a draft committed in one write, which is the control the composer's vocabulary picker already was while the buyer got a wall of checkboxes, then 29.25 → 30.5 KB for the panel that draws all of it: groups and ranges as one ordered sequence with one «Apply» for the panel rather than one per row, the empty-group heading rule, `categoryFilter={false}` and the `resultsLead` slot for a page reached by walking the catalogue, `PopularValues columns=\\\"responsive\\\"` on a container query, `SortSelect` annotating a blocked option at every width, a loading arm that covers the whole pane (0.34 CLS on a live host), and the chip row reserving its own box (a further 0.045). Measured with dependencies held constant, this package's src at the commit before that work and then at it: 29.05 -> 30.16 KB — 1.11 KB, and every part of it is a shift or a mislabelled control a host was living with, then 30.5 -> 31 KB for `<SearchResultsPane columns>` and its pass-through on `<SearchPage resultsColumns>`: a fixed track count or a per-breakpoint map, emitted as a hoisted container-query sheet (the results block is the window minus a 280px rail, so a media query would answer about a width the cards never have). Measured with dependencies held constant, this package's src before and after: 30.40 -> 30.65 KB — 250 B, and what it replaces is a host's `!important` rule against a grid declaration it could not read back, then 31 -> 31.25 KB for the segmented partition being antd's `Segmented` rather than a row of buttons wearing a border: the walker read `data-variant=\"segmented\"` with `role=\"radiogroup\"` and found `.ant-segmented` zero times and `input[type=radio]` zero times (D304), so the cells are the design system's now — real radios under one name, the browser's own arrow keys and Tab stop. Measured with dependencies held constant, this package's src before and after: 30.95 -> 31.05 KB — 100 B, against ~30 lines of hand-rolled joined-row geometry deleted, then 31.25 -> 31.5 KB for the two PINNING seams: `<SearchPage railTop>` (the rail's sticky offset and its height cap moved together, so a rail under a 64px header still ends at the foot of the window) and `<SearchResultsPane stickyToolbar>` with the toolbar's own element in both header shapes. Measured with dependencies held constant, this package's src before and after: 31.05 -> 31.2 KB — 150 B, and what it replaces in one deployment is an `!important` over the pair's own inline geometry, a `:has()`, a `display: contents` and this pair's rail breakpoint restated in the host's media query, then 31.5 -> 31.75 KB for the rail saying when its answer is IN FLIGHT: `FacetPanelBag.refreshing` off `loadStateFromQuery(…, { keepPrevious })`, `data-facets-refreshing` on the rail, and each facet group standing on its own last measured height (a `min-block-size` floor held in a ref) until the new answer lands. Measured with dependencies held constant, this package's src before and after: 31.43 -> 31.51 KB — 80 B, against 0.0586 CLS on a partition press (p43), where the groups survive and RESIZE, then 31.75 -> 32 KB for the FIRST MOUNT, which had none of that: `refreshing` is never true on a cold load, so nothing above reached the one pass a plain load is made of. Three parts. `<SearchPage categoryFeaturesPending>` / `<FacetPanelPane categoryFeaturesPending>` is the third state `categoryFeatures` never had — undefined means both \"no schema\" and \"not yet\", and on a category leaf the schema is a SECOND read, so the panel drew the rail from the answer alone and then drew it again when the schema landed: on the live cars leaf make and model went from three-row checkbox lists to one-row dictionary fields, condition and colour from checkboxes to pills, and `orderFacetGroupsBySchema` reordered all of them (p41, 0.0586 CLS on a plain load of `/c/transport-avtomobili`, make -152px and model -76px). Told the schema is coming, the panel keeps the box it already reserves and draws the rail ONCE. Second, every group now stands on a DECLARED box from the frame it mounts in (`facetGroupReservedHeight` — heading, rows and fold per shape), with its measured height preferred only while an answer is in flight. Third, `<SearchPage filtersHeaderReserve>` puts the host's own band above the rail in flow from the first frame, for the partition row that is two chained catalogue reads behind the answer. Measured with dependencies held constant, this package's src before and after: 30.77 -> 31.14 KB — 366 B, then 32 -> 32.25 KB for the filter sheet's open state becoming the HOST's: `<SearchPage filtersOpen>` / `onFiltersOpenChange(open, reason)` as React's usual controlled pair, and `filtersHeader` accepting a function handed `{ closeFilters, open }`. What it costs is the four call sites naming WHY the sheet moved (`open`, `apply`, `dismiss`, `consumer`) and the slot's function arm; what it buys is a header whose own control NAVIGATES being able to take the sheet down on the same press — the page published only `defaultFiltersOpen`, so a partition chip inside the sheet left the drawer standing over the page it had just opened. Measured with dependencies held constant, this package's src before and after: 31.14 -> 31.26 KB — 120 B",
|
|
60
|
+
"name": "default — the antd skin (query box + a typeahead that offers CATEGORY destinations with their live counts, filters incl. ranges/geo/category slots + the phone chip row with its leading category chip and the location summary row + the rail's evidence-ranked disclosure groups, panel search and sticky count/clear footer, results incl. the view switch, the card photo GALLERY as a SkinCarousel strip, the empty state's derived exits, ranking, the dictionary control for a vocabulary facet, the popular-values block, the partition row in both its chip and segmented variants, the select-style dictionary FIELD the desktop rail opens, the from/to pickers a bounded integer axis draws and the rail's own scrollbar sheet) must stay out of the main bundle. Raised 24.5 → 25.5 KB for those four controls, then 25.5 → 26.25 KB for the chip row's APPLIED mode — one chip per applied value and per numeric bound, each removing that one constraint beside a rail that is already on screen (a storefront was carrying its own copy of it), then 26.25 → 27 KB for the \"search in other categories\" LINE — which is a net deletion on the page that mounts it, replacing a full-width block of one row per section that arrived after the results and pushed them, then 27 → 27.75 KB for the two frames the page now tells apart: the dictionary FIELD reaching `<SearchPage>` at all (per-layout default, field in the rail and inline in the sheet) and the footer bar being static in a column and sticky in a sheet, where it used to be pinned over the last two groups everywhere, then 27.75 → 28.5 KB for `categoryHref` on the other-categories line — a real `<a href>` per row with a modifier-click left to the browser, instead of a `<button>` with no address a person could open in a new tab, then 28.5 → 29.25 KB for the phone's dictionary SHEET — a trigger row over the shared `SkinPickerSheet` with a recommended band, an alphabetical rest paged fifty at a time and a draft committed in one write, which is the control the composer's vocabulary picker already was while the buyer got a wall of checkboxes, then 29.25 → 30.5 KB for the panel that draws all of it: groups and ranges as one ordered sequence with one «Apply» for the panel rather than one per row, the empty-group heading rule, `categoryFilter={false}` and the `resultsLead` slot for a page reached by walking the catalogue, `PopularValues columns=\\\"responsive\\\"` on a container query, `SortSelect` annotating a blocked option at every width, a loading arm that covers the whole pane (0.34 CLS on a live host), and the chip row reserving its own box (a further 0.045). Measured with dependencies held constant, this package's src at the commit before that work and then at it: 29.05 -> 30.16 KB — 1.11 KB, and every part of it is a shift or a mislabelled control a host was living with, then 30.5 -> 31 KB for `<SearchResultsPane columns>` and its pass-through on `<SearchPage resultsColumns>`: a fixed track count or a per-breakpoint map, emitted as a hoisted container-query sheet (the results block is the window minus a 280px rail, so a media query would answer about a width the cards never have). Measured with dependencies held constant, this package's src before and after: 30.40 -> 30.65 KB — 250 B, and what it replaces is a host's `!important` rule against a grid declaration it could not read back, then 31 -> 31.25 KB for the segmented partition being antd's `Segmented` rather than a row of buttons wearing a border: the walker read `data-variant=\"segmented\"` with `role=\"radiogroup\"` and found `.ant-segmented` zero times and `input[type=radio]` zero times (D304), so the cells are the design system's now — real radios under one name, the browser's own arrow keys and Tab stop. Measured with dependencies held constant, this package's src before and after: 30.95 -> 31.05 KB — 100 B, against ~30 lines of hand-rolled joined-row geometry deleted, then 31.25 -> 31.5 KB for the two PINNING seams: `<SearchPage railTop>` (the rail's sticky offset and its height cap moved together, so a rail under a 64px header still ends at the foot of the window) and `<SearchResultsPane stickyToolbar>` with the toolbar's own element in both header shapes. Measured with dependencies held constant, this package's src before and after: 31.05 -> 31.2 KB — 150 B, and what it replaces in one deployment is an `!important` over the pair's own inline geometry, a `:has()`, a `display: contents` and this pair's rail breakpoint restated in the host's media query, then 31.5 -> 31.75 KB for the rail saying when its answer is IN FLIGHT: `FacetPanelBag.refreshing` off `loadStateFromQuery(…, { keepPrevious })`, `data-facets-refreshing` on the rail, and each facet group standing on its own last measured height (a `min-block-size` floor held in a ref) until the new answer lands. Measured with dependencies held constant, this package's src before and after: 31.43 -> 31.51 KB — 80 B, against 0.0586 CLS on a partition press (p43), where the groups survive and RESIZE, then 31.75 -> 32 KB for the FIRST MOUNT, which had none of that: `refreshing` is never true on a cold load, so nothing above reached the one pass a plain load is made of. Three parts. `<SearchPage categoryFeaturesPending>` / `<FacetPanelPane categoryFeaturesPending>` is the third state `categoryFeatures` never had — undefined means both \"no schema\" and \"not yet\", and on a category leaf the schema is a SECOND read, so the panel drew the rail from the answer alone and then drew it again when the schema landed: on the live cars leaf make and model went from three-row checkbox lists to one-row dictionary fields, condition and colour from checkboxes to pills, and `orderFacetGroupsBySchema` reordered all of them (p41, 0.0586 CLS on a plain load of `/c/transport-avtomobili`, make -152px and model -76px). Told the schema is coming, the panel keeps the box it already reserves and draws the rail ONCE. Second, every group now stands on a DECLARED box from the frame it mounts in (`facetGroupReservedHeight` — heading, rows and fold per shape), with its measured height preferred only while an answer is in flight. Third, `<SearchPage filtersHeaderReserve>` puts the host's own band above the rail in flow from the first frame, for the partition row that is two chained catalogue reads behind the answer. Measured with dependencies held constant, this package's src before and after: 30.77 -> 31.14 KB — 366 B, then 32 -> 32.25 KB for the filter sheet's open state becoming the HOST's: `<SearchPage filtersOpen>` / `onFiltersOpenChange(open, reason)` as React's usual controlled pair, and `filtersHeader` accepting a function handed `{ closeFilters, open }`. What it costs is the four call sites naming WHY the sheet moved (`open`, `apply`, `dismiss`, `consumer`) and the slot's function arm; what it buys is a header whose own control NAVIGATES being able to take the sheet down on the same press — the page published only `defaultFiltersOpen`, so a partition chip inside the sheet left the drawer standing over the page it had just opened. Measured with dependencies held constant, this package's src before and after: 31.14 -> 31.26 KB — 120 B, then 32.25 -> 32.5 KB for the two levers a storefront could not reach: `<SearchPage footerBar>` (the rail's footer bar writes its own `display` inline, so a consumer stylesheet could only suppress it with an `!important` its own gate forbids, and this page hard-coded the column's value) and `PartitionChild.count`, which the chip draws itself in the muted weight the facet rows use instead of a host welding the number onto the name. Measured with dependencies held constant, this package's src before and after: 32.01 -> 32.10 KB — 90 B, against a string-joining hack and an unreachable bar, then 32.5 -> 32.75 KB for THE THREE BOXES A STOREFRONT WAS HOLDING FOR THIS PAIR. The phone chip row's reserve is `chipRowMinHeight(token.controlHeight)` instead of a `44 + …` constant: the touch floor raises the chips to 44 only BELOW the tablet breakpoint, so from 768px up the reserve stood 12px taller than the 40px row and the results pane ROSE when the row landed (0.0725 CLS on a category leaf at 768, against 0.00016 at 390). `<SortSelect compact>` reserves the width of the longest label it can display, measured by the browser through an `aria-hidden` sizer stacked with the select in one grid cell, instead of `minWidth: 0` inline and a control that GREW 115px when the answer named the sort. And `<SearchResultsPane>` names its own root (`data-testid=\"search-results-pane\"`) and takes `reserve` — `<SearchPage resultsReserve>` — for the box the feed arrives into, which a host was holding with `#search-page > :last-child`. Measured with dependencies held constant, this package's src before and after: 32.10 -> 32.25 KB — 150 B, against a hand-guessed height, a sibling-count selector and two stylesheet rules a consumer can now delete",
|
|
61
61
|
"path": "dist/default/index.js",
|
|
62
|
-
"limit": "32.
|
|
62
|
+
"limit": "32.75 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,10 +115,10 @@
|
|
|
115
115
|
"size-limit": "^11.2.0",
|
|
116
116
|
"typescript": "^5.8.3",
|
|
117
117
|
"vitest": "^3.2.4",
|
|
118
|
+
"@stapel/attributes-react": "^0.16.6",
|
|
118
119
|
"@stapel/core": "^0.26.0",
|
|
119
120
|
"@stapel/image": "^0.4.2",
|
|
120
121
|
"@stapel/showcase": "^0.3.0",
|
|
121
|
-
"@stapel/attributes-react": "^0.16.6",
|
|
122
122
|
"@stapel/tokens": "^0.8.0",
|
|
123
123
|
"@stapel/tokens-antd": "^0.18.0"
|
|
124
124
|
},
|
|
@@ -1139,7 +1139,18 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
|
|
|
1139
1139
|
}}
|
|
1140
1140
|
</LoadList>
|
|
1141
1141
|
|
|
1142
|
-
{
|
|
1142
|
+
{/* NOTHING MOUNTS IN THE HOLD THAT THE SETTLED RAIL WILL MOVE
|
|
1143
|
+
(D465). The count and the clear-all come from the ANSWER, which
|
|
1144
|
+
on 9 of 12 measured loads lands before the schema — so this bar
|
|
1145
|
+
used to mount under the loading box at `schema=pending`, in its
|
|
1146
|
+
own commit, and then travel down the rail when the groups
|
|
1147
|
+
replaced that box. A schema-first load drew the rail in one
|
|
1148
|
+
commit and shifted nothing, which is what made the shift
|
|
1149
|
+
intermittent (CLS 0.0056 at 1280, 0.0018 at 1920, 0.0076 at
|
|
1150
|
+
1100). The bar is part of the settled rail, so it waits for it:
|
|
1151
|
+
while the shape of the panel is undecided there is no floor to
|
|
1152
|
+
stick to and no place under the groups to sit. */}
|
|
1153
|
+
{footerBar !== "none" && !schemaPending && (
|
|
1143
1154
|
<RailFooterBar
|
|
1144
1155
|
activeFilters={bag.activeFilters}
|
|
1145
1156
|
clearAll={bag.clearAll}
|
|
@@ -325,10 +325,19 @@ export const CHIP_ROW_CAP = 8;
|
|
|
325
325
|
* answer really is "no row" and reserving would be the same shift in the
|
|
326
326
|
* opposite direction.
|
|
327
327
|
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
328
|
+
* A NUMBER OUT OF THE THEME, not a constant — the same discipline
|
|
329
|
+
* {@link appliedRowMinHeight} is written under, and this row did not have it.
|
|
330
|
+
* The chips are default-size `Button`s, so their height IS antd's
|
|
331
|
+
* `controlHeight`, and the shared `SkinTheme` raises that to the 44px touch
|
|
332
|
+
* floor only BELOW the tablet breakpoint. Written as `44 + …` the reserve was
|
|
333
|
+
* right on a phone and 12px too tall from 768px up, where the row renders at
|
|
334
|
+
* 32 + 8 = 40: the results pane ROSE by those 12px the moment the row landed,
|
|
335
|
+
* which is the shift the reservation exists to prevent, pointing the other
|
|
336
|
+
* way. Measured on a category leaf: 0.00016 CLS at 390 and 0.0725 at 768.
|
|
330
337
|
*/
|
|
331
|
-
export
|
|
338
|
+
export function chipRowMinHeight(chipHeight: number): number {
|
|
339
|
+
return chipHeight + spacing[1] * 2;
|
|
340
|
+
}
|
|
332
341
|
|
|
333
342
|
/**
|
|
334
343
|
* The visible row and what the door owes: the first `max` specs — and EVERY
|
|
@@ -480,6 +489,9 @@ export function FilterChips(props: FilterChipsProps): ReactElement | null {
|
|
|
480
489
|
|
|
481
490
|
function OpenerChipRow(props: FilterChipsOpenerProps): ReactElement | null {
|
|
482
491
|
const t = useT();
|
|
492
|
+
// The chips' own height, from the theme they are drawn in — see
|
|
493
|
+
// `chipRowMinHeight`.
|
|
494
|
+
const { token } = antdTheme.useToken();
|
|
483
495
|
const { state, setCategory } = useSearchState();
|
|
484
496
|
const bag = useFacetPanel({
|
|
485
497
|
...(props.categoryFeatures !== undefined
|
|
@@ -595,7 +607,7 @@ function OpenerChipRow(props: FilterChipsOpenerProps): ReactElement | null {
|
|
|
595
607
|
*/
|
|
596
608
|
const hasChips = showCategoryChip || ordered.length > 0;
|
|
597
609
|
if (!hasChips) {
|
|
598
|
-
// THE BOX THE ROW WILL ARRIVE INTO — see `
|
|
610
|
+
// THE BOX THE ROW WILL ARRIVE INTO — see `chipRowMinHeight`. Only
|
|
599
611
|
// while the answer is still in flight and this search is one a row is
|
|
600
612
|
// predictable for; a settled answer with no chips renders nothing, which
|
|
601
613
|
// is what it has always done and what it should do.
|
|
@@ -606,7 +618,7 @@ function OpenerChipRow(props: FilterChipsOpenerProps): ReactElement | null {
|
|
|
606
618
|
<div
|
|
607
619
|
aria-hidden="true"
|
|
608
620
|
data-testid="search-filter-chips-reserve"
|
|
609
|
-
style={{ minBlockSize:
|
|
621
|
+
style={{ minBlockSize: chipRowMinHeight(token.controlHeight) }}
|
|
610
622
|
/>
|
|
611
623
|
) : null;
|
|
612
624
|
}
|
|
@@ -55,7 +55,7 @@ import type {
|
|
|
55
55
|
ReactElement,
|
|
56
56
|
ReactNode,
|
|
57
57
|
} from "react";
|
|
58
|
-
import { Button, Segmented } from "antd";
|
|
58
|
+
import { Button, Segmented, Typography } from "antd";
|
|
59
59
|
import { useT } from "@stapel/core";
|
|
60
60
|
import { radii, spacing } from "@stapel/tokens";
|
|
61
61
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
@@ -67,6 +67,22 @@ export interface PartitionChild {
|
|
|
67
67
|
readonly id: number | string;
|
|
68
68
|
readonly path: string;
|
|
69
69
|
readonly name: string;
|
|
70
|
+
/**
|
|
71
|
+
* How many listings this section holds, drawn by the chip itself in the
|
|
72
|
+
* muted style every other counted control on the page uses.
|
|
73
|
+
*
|
|
74
|
+
* A host that has the number had to concatenate it into `name` — the only
|
|
75
|
+
* string this row accepted — which is how a count ends up in the same
|
|
76
|
+
* weight and colour as the word beside it, and how one storefront was
|
|
77
|
+
* joining the name and the total by hand. Passing the number instead lets
|
|
78
|
+
* the chip render it the way the facet rows render theirs, and keeps the
|
|
79
|
+
* label a label (a `Segmented` cell can lay the two out, and the name is not
|
|
80
|
+
* a string with an integer welded onto its end).
|
|
81
|
+
*
|
|
82
|
+
* Omit it and nothing is drawn — an absent count is not a zero, and a
|
|
83
|
+
* section whose total nobody asked for must not be captioned "0".
|
|
84
|
+
*/
|
|
85
|
+
readonly count?: number;
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
export interface PartitionChipsProps {
|
|
@@ -152,6 +168,27 @@ function takeArrowMove(): boolean {
|
|
|
152
168
|
return fresh;
|
|
153
169
|
}
|
|
154
170
|
|
|
171
|
+
/**
|
|
172
|
+
* One cell's label: the section's name, and its count beside it in the muted
|
|
173
|
+
* weight — the same `Typography.Text type="secondary"` a facet option's count
|
|
174
|
+
* is drawn in, so the two counted controls on one page read as one system.
|
|
175
|
+
*/
|
|
176
|
+
function ChildLabel(props: { readonly child: PartitionChild }): ReactElement {
|
|
177
|
+
const { child } = props;
|
|
178
|
+
if (child.count === undefined) return <>{child.name}</>;
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
{child.name}{" "}
|
|
182
|
+
<Typography.Text
|
|
183
|
+
type="secondary"
|
|
184
|
+
data-testid={`partition-count-${child.path}`}
|
|
185
|
+
>
|
|
186
|
+
{child.count}
|
|
187
|
+
</Typography.Text>
|
|
188
|
+
</>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
155
192
|
/** The row's cells, as `[value, label]` — the parent first, then the
|
|
156
193
|
* children in catalogue order. */
|
|
157
194
|
function cells(
|
|
@@ -160,7 +197,9 @@ function cells(
|
|
|
160
197
|
): readonly (readonly [string | null, ReactNode])[] {
|
|
161
198
|
return [
|
|
162
199
|
[null, allLabel] as const,
|
|
163
|
-
...items.map(
|
|
200
|
+
...items.map(
|
|
201
|
+
(item) => [item.path, <ChildLabel key={item.path} child={item} />] as const
|
|
202
|
+
),
|
|
164
203
|
];
|
|
165
204
|
}
|
|
166
205
|
|
|
@@ -80,6 +80,7 @@ import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
|
80
80
|
import { FacetPanelPane } from "./FacetPanelPane.js";
|
|
81
81
|
import type {
|
|
82
82
|
CategoryFilterSlotProps,
|
|
83
|
+
FacetPanelPaneProps,
|
|
83
84
|
GeoFilterSlotProps,
|
|
84
85
|
} from "./FacetPanelPane.js";
|
|
85
86
|
import { FilterChips } from "./FilterChips.js";
|
|
@@ -461,6 +462,22 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
461
462
|
* saves less than folding a column's does. Set it to override either.
|
|
462
463
|
*/
|
|
463
464
|
readonly visibleGroups?: number | null;
|
|
465
|
+
/**
|
|
466
|
+
* The filter panel's footer — what the filters DID (the live count) and the
|
|
467
|
+
* way out of them — see {@link FacetPanelPaneProps.footerBar}.
|
|
468
|
+
*
|
|
469
|
+
* Defaulted PER LAYOUT: `"static"` in the desktop COLUMN (the rail scrolls
|
|
470
|
+
* with the page, and a bar pinned to the port's floor sat on top of the last
|
|
471
|
+
* groups), and none in the phone SHEET, whose own "Show N results" footer is
|
|
472
|
+
* already the count AND the exit.
|
|
473
|
+
*
|
|
474
|
+
* It is a PROP because it could not be reached any other way: the bar writes
|
|
475
|
+
* its own `display` inline, so a host stylesheet cannot suppress it without
|
|
476
|
+
* `!important`, and this page hard-coded the column's value. A surface that
|
|
477
|
+
* draws its own count under the rail passes `false`; one that wants the bar
|
|
478
|
+
* inside the sheet as well passes `"sticky"`.
|
|
479
|
+
*/
|
|
480
|
+
readonly footerBar?: FacetPanelPaneProps["footerBar"];
|
|
464
481
|
/** Print the engine's list of uncounted facet slugs in the filter panel.
|
|
465
482
|
* Default `false` — see {@link FacetPanelPaneProps.skippedNotice}. */
|
|
466
483
|
readonly skippedNotice?: boolean;
|
|
@@ -776,6 +793,13 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
776
793
|
* grid it could not read back.
|
|
777
794
|
*/
|
|
778
795
|
readonly resultsColumns?: ResultsColumns;
|
|
796
|
+
/**
|
|
797
|
+
* The box the results arrive into — `<SearchResultsPane reserve>`,
|
|
798
|
+
* forwarded. Here for the same reason `resultsColumns` is: the page is what
|
|
799
|
+
* a storefront mounts, and this was the last thing on it a host could only
|
|
800
|
+
* reach with `#search-page > :last-child` in its own stylesheet.
|
|
801
|
+
*/
|
|
802
|
+
readonly resultsReserve?: number | string;
|
|
779
803
|
/**
|
|
780
804
|
* Is the results caption SEEN below the sheet breakpoint —
|
|
781
805
|
* `<SearchResultsPane headingVisible>`, forwarded. Default: the host's own
|
|
@@ -787,11 +811,13 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
787
811
|
interface SearchPageBodyProps {
|
|
788
812
|
readonly renderCard?: SearchCardRenderer;
|
|
789
813
|
readonly resultsColumns?: ResultsColumns;
|
|
814
|
+
readonly resultsReserve?: number | string;
|
|
790
815
|
readonly resultsHeadingVisible?: boolean;
|
|
791
816
|
readonly categoryFilter?: boolean;
|
|
792
817
|
readonly resultsLead?: ReactNode;
|
|
793
818
|
readonly dictionaryMode?: "field" | "inline" | "sheet";
|
|
794
819
|
readonly visibleGroups?: number | null;
|
|
820
|
+
readonly footerBar?: FacetPanelPaneProps["footerBar"];
|
|
795
821
|
readonly categoryFeatures?: readonly FeatureDef[];
|
|
796
822
|
readonly categoryFeaturesPending?: boolean;
|
|
797
823
|
readonly filtersHeaderReserve?: number | string;
|
|
@@ -862,6 +888,16 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
|
|
|
862
888
|
: surface === "sheet"
|
|
863
889
|
? "sheet"
|
|
864
890
|
: "column");
|
|
891
|
+
/**
|
|
892
|
+
* The rail's footer bar, defaulted PER LAYOUT and overridable — see
|
|
893
|
+
* {@link SearchPageProps.footerBar}.
|
|
894
|
+
*
|
|
895
|
+
* STATIC, not sticky, in the column: the rail scrolls with the page, and a
|
|
896
|
+
* bar pinned to the port's floor sat on top of the last groups. None in the
|
|
897
|
+
* sheet, whose own "Show N results" footer is already the count and the exit.
|
|
898
|
+
*/
|
|
899
|
+
const footerBar: FacetPanelPaneProps["footerBar"] =
|
|
900
|
+
props.footerBar ?? (layout === "sheet" ? undefined : "static");
|
|
865
901
|
// Controlled or not, decided by the PRESENCE of `filtersOpen` and read once
|
|
866
902
|
// per render — the state the page keeps is only ever the uncontrolled half,
|
|
867
903
|
// and a controlled host's value is never copied into it (copying it is how
|
|
@@ -994,11 +1030,8 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
|
|
|
994
1030
|
result count scrolled out of sight above the fold. */}
|
|
995
1031
|
{filtersEmpty ? null : (
|
|
996
1032
|
<FacetPanelPane
|
|
997
|
-
{...(layout === "sheet"
|
|
998
|
-
|
|
999
|
-
: // STATIC, not sticky: the rail scrolls with the page, and a bar
|
|
1000
|
-
// pinned to the port's floor sat on top of the last groups.
|
|
1001
|
-
{ footerBar: "static" as const })}
|
|
1033
|
+
{...(layout === "sheet" ? { heading: null } : {})}
|
|
1034
|
+
{...(footerBar !== undefined ? { footerBar } : {})}
|
|
1002
1035
|
dictionaryMode={props.dictionaryMode ?? (layout === "sheet" ? "sheet" : "field")}
|
|
1003
1036
|
// `??` would treat an explicit `null` ("never fold") the same as
|
|
1004
1037
|
// "not set": `visibleGroups` uses `null` as a real value, unlike
|
|
@@ -1087,6 +1120,9 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
|
|
|
1087
1120
|
{...(props.resultsColumns !== undefined
|
|
1088
1121
|
? { columns: props.resultsColumns }
|
|
1089
1122
|
: {})}
|
|
1123
|
+
{...(props.resultsReserve !== undefined
|
|
1124
|
+
? { reserve: props.resultsReserve }
|
|
1125
|
+
: {})}
|
|
1090
1126
|
{...(props.resultsHeadingVisible !== undefined
|
|
1091
1127
|
? { headingVisible: props.resultsHeadingVisible }
|
|
1092
1128
|
: {})}
|
|
@@ -1307,9 +1343,11 @@ export function SearchPage(props: SearchPageProps): ReactElement {
|
|
|
1307
1343
|
resultsAction,
|
|
1308
1344
|
resultsHeadingLevel,
|
|
1309
1345
|
resultsColumns,
|
|
1346
|
+
resultsReserve,
|
|
1310
1347
|
resultsHeadingVisible,
|
|
1311
1348
|
dictionaryMode,
|
|
1312
1349
|
visibleGroups,
|
|
1350
|
+
footerBar,
|
|
1313
1351
|
pinnedFacets,
|
|
1314
1352
|
mode,
|
|
1315
1353
|
...parseOptions
|
|
@@ -1321,9 +1359,11 @@ export function SearchPage(props: SearchPageProps): ReactElement {
|
|
|
1321
1359
|
<SearchPageBody
|
|
1322
1360
|
{...(renderCard !== undefined ? { renderCard } : {})}
|
|
1323
1361
|
{...(resultsColumns !== undefined ? { resultsColumns } : {})}
|
|
1362
|
+
{...(resultsReserve !== undefined ? { resultsReserve } : {})}
|
|
1324
1363
|
{...(resultsHeadingVisible !== undefined ? { resultsHeadingVisible } : {})}
|
|
1325
1364
|
{...(dictionaryMode !== undefined ? { dictionaryMode } : {})}
|
|
1326
1365
|
{...(visibleGroups !== undefined ? { visibleGroups } : {})}
|
|
1366
|
+
{...(footerBar !== undefined ? { footerBar } : {})}
|
|
1327
1367
|
{...(pinnedFacets !== undefined ? { pinnedFacets } : {})}
|
|
1328
1368
|
{...(categoryFeatures !== undefined ? { categoryFeatures } : {})}
|
|
1329
1369
|
{...(categoryFeaturesPending !== undefined
|
|
@@ -216,6 +216,24 @@ const WIDE_HEADING: CSSProperties = { margin: 0, minInlineSize: 0 };
|
|
|
216
216
|
*/
|
|
217
217
|
const TOOLBAR_END: CSSProperties = { flex: "0 0 auto" };
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The wide row's LEADING half, and it is always in the row (D466).
|
|
221
|
+
*
|
|
222
|
+
* The count arrives with the answer, one render after the toolbar is already
|
|
223
|
+
* on screen, and `<Count>` renders nothing at all until it does — so a row
|
|
224
|
+
* spaced by `justify: space-between` had ONE item in the first frame and TWO
|
|
225
|
+
* in the second, which moved the sort/view control from the leading edge to
|
|
226
|
+
* the trailing one as the number landed: x 328→459 at 1280, 564→863 at 1920,
|
|
227
|
+
* 312→796 at 1100, and the same jump on a seller's page.
|
|
228
|
+
*
|
|
229
|
+
* A leading box that is always there, and grows, is what fixes it: the
|
|
230
|
+
* trailing group's x is then `row right − its own width` in every frame,
|
|
231
|
+
* whatever the count says or whether it says anything. `min-inline-size: 0`
|
|
232
|
+
* lets a long count shrink into the space that is left rather than push the
|
|
233
|
+
* controls, which the row's own `overflow-x` then scrolls.
|
|
234
|
+
*/
|
|
235
|
+
const TOOLBAR_LEAD: CSSProperties = { flex: "1 1 auto", minInlineSize: 0 };
|
|
236
|
+
|
|
219
237
|
/** The compact shape's toolbar box — the row the pin acts on. */
|
|
220
238
|
const COMPACT_TOOLBAR: CSSProperties = {
|
|
221
239
|
display: "flex",
|
|
@@ -469,6 +487,25 @@ export interface SearchResultsPaneProps extends ThemeModeProp {
|
|
|
469
487
|
/** Widest the column of results may grow (default {@link RESULTS_MAX_WIDTH});
|
|
470
488
|
* `null` lets the container decide. */
|
|
471
489
|
readonly maxWidth?: number | null;
|
|
490
|
+
/**
|
|
491
|
+
* THE BOX THE RESULTS ARRIVE INTO — a block-size floor the pane holds while
|
|
492
|
+
* the first answer is in flight, and drops the moment the rows land.
|
|
493
|
+
*
|
|
494
|
+
* The pane cannot know how tall its own page will be (the row count is the
|
|
495
|
+
* host's `limit` and the card height is the host's card), so this is a
|
|
496
|
+
* number the host measures once for its own feed — `limit × card + gaps` —
|
|
497
|
+
* and it stops the fold below the results from rising into the space the
|
|
498
|
+
* feed is about to take.
|
|
499
|
+
*
|
|
500
|
+
* What it replaces is a stylesheet rule aimed at this pane with the only
|
|
501
|
+
* hold a consumer had on it: `#search-page > :last-child`, a selector that
|
|
502
|
+
* silently starts addressing something else the day this page grows a
|
|
503
|
+
* seventh child. The pane's root now also carries
|
|
504
|
+
* `data-testid="search-results-pane"`, so a host that wants its own rule
|
|
505
|
+
* has a handle that is part of this pair's surface instead of a count of
|
|
506
|
+
* siblings.
|
|
507
|
+
*/
|
|
508
|
+
readonly reserve?: number | string;
|
|
472
509
|
/**
|
|
473
510
|
* How the loaded rows are arranged: as many card columns as fit
|
|
474
511
|
* (`"grid"`, the default) or one wide row each (`"list"`). The view SWITCH
|
|
@@ -695,6 +732,7 @@ export function SearchResultsPane(props: SearchResultsPaneProps): ReactElement {
|
|
|
695
732
|
return (
|
|
696
733
|
<SkinTheme
|
|
697
734
|
surface="base"
|
|
735
|
+
data-testid="search-results-pane"
|
|
698
736
|
{...(props.mode !== undefined ? { mode: props.mode } : {})}
|
|
699
737
|
style={{
|
|
700
738
|
width: "100%",
|
|
@@ -703,7 +741,16 @@ export function SearchResultsPane(props: SearchResultsPaneProps): ReactElement {
|
|
|
703
741
|
>
|
|
704
742
|
<SearchResults {...(props.enabled !== undefined ? { enabled: props.enabled } : {})}>
|
|
705
743
|
{(bag) => (
|
|
706
|
-
<Flex
|
|
744
|
+
<Flex
|
|
745
|
+
vertical
|
|
746
|
+
gap={spacing[4]}
|
|
747
|
+
// The box the results arrive into — see `reserve`. Only until the
|
|
748
|
+
// FIRST answer lands: a floor under rows already on screen would
|
|
749
|
+
// hold a gap open under a short page for the rest of the session.
|
|
750
|
+
{...(props.reserve !== undefined && bag.state.status === "loading"
|
|
751
|
+
? { style: { minBlockSize: props.reserve } }
|
|
752
|
+
: {})}
|
|
753
|
+
>
|
|
707
754
|
{props.lead !== undefined && (
|
|
708
755
|
<div data-testid="search-results-lead">{props.lead}</div>
|
|
709
756
|
)}
|
|
@@ -751,14 +798,19 @@ export function SearchResultsPane(props: SearchResultsPaneProps): ReactElement {
|
|
|
751
798
|
</Typography.Title>
|
|
752
799
|
</div>
|
|
753
800
|
<Flex
|
|
754
|
-
justify="space-between"
|
|
755
801
|
align="center"
|
|
756
802
|
gap={spacing[3]}
|
|
757
803
|
className={RESULTS_TOOLBAR_CLASS}
|
|
758
804
|
data-testid="search-results-toolbar"
|
|
759
805
|
style={{ ...TOOLBAR_ROW, ...toolbarPin }}
|
|
760
806
|
>
|
|
761
|
-
|
|
807
|
+
{/* The elastic half, present whether or not there is a
|
|
808
|
+
count in it — see TOOLBAR_LEAD. It is what holds the
|
|
809
|
+
controls against the trailing edge in the frame before
|
|
810
|
+
the number arrives, so nothing travels when it does. */}
|
|
811
|
+
<div style={TOOLBAR_LEAD} data-testid="search-results-toolbar-lead">
|
|
812
|
+
<Count bag={bag} />
|
|
813
|
+
</div>
|
|
762
814
|
<Flex align="center" gap={spacing[3]} style={TOOLBAR_END}>
|
|
763
815
|
{props.toolbar}
|
|
764
816
|
</Flex>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* That is the whole defect this file used to carry: the one sort a person
|
|
16
16
|
* would most want on a phone was greyed out with its explanation in a hover.
|
|
17
17
|
*/
|
|
18
|
-
import type { ReactElement } from "react";
|
|
18
|
+
import type { CSSProperties, ReactElement } from "react";
|
|
19
19
|
import { Flex, Select, Typography } from "antd";
|
|
20
20
|
import { actionAvailable, actionBlocked, useT } from "@stapel/core";
|
|
21
21
|
import type { ActionAvailability } from "@stapel/core";
|
|
@@ -34,6 +34,61 @@ import { sortLabelKey } from "./sortLabels.js";
|
|
|
34
34
|
*/
|
|
35
35
|
export const SORT_SELECT_MIN_WIDTH = 200;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The select's own chrome around its label: antd's two inline paddings plus
|
|
39
|
+
* the caret and its margin. Added to the compact form's sizer (below), which
|
|
40
|
+
* measures a bare label.
|
|
41
|
+
*/
|
|
42
|
+
export const SORT_SELECT_CHROME = 40;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ── THE COMPACT CONTROL'S BOX, HELD FROM THE FIRST FRAME ────────────────────
|
|
46
|
+
*
|
|
47
|
+
* The compact arm draws `value={active ?? null}`, and `active` is only known
|
|
48
|
+
* once the page in cache reports the sort the SERVER applied (see
|
|
49
|
+
* {@link useAppliedSort}) — for an address that names no `sort`, that is a
|
|
50
|
+
* whole round trip after the first paint. So the control rendered as a bare
|
|
51
|
+
* caret and then GREW by the width of its label: measured on a category leaf,
|
|
52
|
+
* the caret's own box moved from x=22 to x=137, 115px, the largest single
|
|
53
|
+
* term in that page's layout shift. `minWidth: 0` was written inline, so no
|
|
54
|
+
* consumer stylesheet could hold the box either.
|
|
55
|
+
*
|
|
56
|
+
* The floor is not {@link SORT_SELECT_MIN_WIDTH}: 200px is the desktop arm's
|
|
57
|
+
* number and would wrap a two-control toolbar onto two rows at 390px. It is
|
|
58
|
+
* the width of the longest label THIS control can be asked to show, at the
|
|
59
|
+
* font it will show it in — so it is measured by the browser rather than
|
|
60
|
+
* guessed in pixels, and it is right in every locale (a Russian sort label is
|
|
61
|
+
* half again as long as its English original).
|
|
62
|
+
*
|
|
63
|
+
* One grid cell, two children stacked in it: an `aria-hidden` sizer carrying
|
|
64
|
+
* the longest label, and the select itself. The cell is as wide as the sizer,
|
|
65
|
+
* the select fills it, and neither depends on `active`.
|
|
66
|
+
*/
|
|
67
|
+
const COMPACT_WRAP: CSSProperties = {
|
|
68
|
+
display: "inline-grid",
|
|
69
|
+
flex: "0 1 auto",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const COMPACT_SIZER: CSSProperties = {
|
|
73
|
+
gridArea: "1 / 1",
|
|
74
|
+
visibility: "hidden",
|
|
75
|
+
blockSize: 0,
|
|
76
|
+
overflow: "hidden",
|
|
77
|
+
whiteSpace: "nowrap",
|
|
78
|
+
pointerEvents: "none",
|
|
79
|
+
paddingInline: SORT_SELECT_CHROME,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const COMPACT_SELECT: CSSProperties = { gridArea: "1 / 1", minWidth: 0 };
|
|
83
|
+
|
|
84
|
+
/** The longest of the labels the control can display, which is the one the
|
|
85
|
+
* sizer holds. Ties keep the first — they are the same width. */
|
|
86
|
+
export function longestSortLabel(labels: readonly string[]): string {
|
|
87
|
+
return labels.reduce((longest, label) =>
|
|
88
|
+
label.length > longest.length ? label : longest
|
|
89
|
+
, "");
|
|
90
|
+
}
|
|
91
|
+
|
|
37
92
|
/** Why `sort=distance` is refused without a centre — the server's own code, so
|
|
38
93
|
* the control and the 400 it would have earned say the same sentence. */
|
|
39
94
|
const SORT_DISTANCE_BLOCKED = "error.400.search_sort_needs_center";
|
|
@@ -49,9 +104,11 @@ export interface SortSelectProps {
|
|
|
49
104
|
*
|
|
50
105
|
* - the "Sort" caption goes (the select already shows a sort by name; the
|
|
51
106
|
* accessible name keeps the word);
|
|
52
|
-
* - the {@link SORT_SELECT_MIN_WIDTH} floor goes
|
|
107
|
+
* - the {@link SORT_SELECT_MIN_WIDTH} floor goes — the control shares one
|
|
53
108
|
* row with whatever the surface puts beside it instead of pushing it to
|
|
54
|
-
* the next line
|
|
109
|
+
* the next line — and is replaced by a floor the width of this control's
|
|
110
|
+
* OWN longest label, so the box does not grow when the answer names the
|
|
111
|
+
* sort (see `COMPACT_WRAP`);
|
|
55
112
|
* - the line under the control goes. The blocked option's REASON does not:
|
|
56
113
|
* it is on the option itself at every width now (see `optionsFor`), and
|
|
57
114
|
* what the compact form drops is the second, separate copy of it.
|
|
@@ -110,14 +167,25 @@ export function SortSelect(props: SortSelectProps): ReactElement {
|
|
|
110
167
|
* accessible description of the SELECT, the other is the label of the OPTION
|
|
111
168
|
* that is refused, and they are read in different moments.
|
|
112
169
|
*/
|
|
170
|
+
/**
|
|
171
|
+
* The names this control can DISPLAY, which is what the compact form's
|
|
172
|
+
* sizer is measured on. The blocked row's appended reason is deliberately
|
|
173
|
+
* not among them: it reaches the CLOSED control only for a sort the address
|
|
174
|
+
* itself names, and a sort in the address is known from the first frame —
|
|
175
|
+
* so it can widen the box but can never move it.
|
|
176
|
+
*/
|
|
177
|
+
const plainLabels = values.map((value) => {
|
|
178
|
+
const key = sortLabelKey(value);
|
|
179
|
+
return key !== undefined ? t(key) : value;
|
|
180
|
+
});
|
|
181
|
+
|
|
113
182
|
const optionsFor = (): {
|
|
114
183
|
readonly value: string;
|
|
115
184
|
readonly label: string;
|
|
116
185
|
readonly disabled: boolean;
|
|
117
186
|
}[] =>
|
|
118
|
-
values.map((value) => {
|
|
119
|
-
const
|
|
120
|
-
const label = key !== undefined ? t(key) : value;
|
|
187
|
+
values.map((value, index) => {
|
|
188
|
+
const label = plainLabels[index] ?? value;
|
|
121
189
|
const blocked = value === "distance" && !hasCentre;
|
|
122
190
|
return {
|
|
123
191
|
value,
|
|
@@ -128,20 +196,23 @@ export function SortSelect(props: SortSelectProps): ReactElement {
|
|
|
128
196
|
|
|
129
197
|
if (props.compact === true) {
|
|
130
198
|
return (
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
199
|
+
<div style={COMPACT_WRAP} data-testid="search-sort-compact">
|
|
200
|
+
{/* The box, not a caption — see `COMPACT_WRAP`. */}
|
|
201
|
+
<span aria-hidden="true" data-testid="search-sort-sizer" style={COMPACT_SIZER}>
|
|
202
|
+
{longestSortLabel(plainLabels)}
|
|
203
|
+
</span>
|
|
204
|
+
<Select<string>
|
|
205
|
+
data-testid="search-sort"
|
|
206
|
+
data-stapel-gated={hasCentre ? "available" : "blocked"}
|
|
207
|
+
aria-label={t(SEARCH_I18N_KEYS.sortLabel)}
|
|
208
|
+
style={COMPACT_SELECT}
|
|
209
|
+
value={active ?? null}
|
|
210
|
+
onChange={(next) => {
|
|
211
|
+
setSort(next);
|
|
212
|
+
}}
|
|
213
|
+
options={optionsFor()}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
145
216
|
);
|
|
146
217
|
}
|
|
147
218
|
|
package/src/default/index.ts
CHANGED
|
@@ -93,7 +93,6 @@ export {
|
|
|
93
93
|
CHIP_BAND_ORDER,
|
|
94
94
|
CHIP_ROW_CAP,
|
|
95
95
|
CHIP_ROW_CLASS,
|
|
96
|
-
CHIP_ROW_MIN_HEIGHT,
|
|
97
96
|
CHIP_ROW_STYLE_HREF,
|
|
98
97
|
appliedChipTestId,
|
|
99
98
|
appliedRowMinHeight,
|
|
@@ -101,6 +100,7 @@ export {
|
|
|
101
100
|
capChipRow,
|
|
102
101
|
categoryLeaf,
|
|
103
102
|
chipRowCss,
|
|
103
|
+
chipRowMinHeight,
|
|
104
104
|
orderChipFilters,
|
|
105
105
|
rangeChipText,
|
|
106
106
|
rangeLabelSource,
|
|
@@ -179,7 +179,12 @@ export type { RankingDisclosurePaneProps } from "./RankingDisclosurePane.js";
|
|
|
179
179
|
// ── controls, exported so a host can compose its own layout ─────────────────
|
|
180
180
|
export { SearchBox } from "./SearchBox.js";
|
|
181
181
|
export type { SearchBoxProps } from "./SearchBox.js";
|
|
182
|
-
export {
|
|
182
|
+
export {
|
|
183
|
+
SortSelect,
|
|
184
|
+
SORT_SELECT_CHROME,
|
|
185
|
+
SORT_SELECT_MIN_WIDTH,
|
|
186
|
+
longestSortLabel,
|
|
187
|
+
} from "./SortSelect.js";
|
|
183
188
|
export type { SortSelectProps } from "./SortSelect.js";
|
|
184
189
|
export { ViewSwitch, SEARCH_BUILTIN_VIEWS, resolveView } from "./ViewSwitch.js";
|
|
185
190
|
export type { ViewSwitchProps, SearchView, SearchResultsLayout } from "./ViewSwitch.js";
|