@stapel/search-react 0.4.0 → 0.7.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 (139) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +1 -1
  3. package/dist/default/DegradationNotice.d.ts +7 -0
  4. package/dist/default/DegradationNotice.d.ts.map +1 -1
  5. package/dist/default/DegradationNotice.js +17 -6
  6. package/dist/default/DegradationNotice.js.map +1 -1
  7. package/dist/default/FacetPanelPane.d.ts +54 -3
  8. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  9. package/dist/default/FacetPanelPane.js +119 -19
  10. package/dist/default/FacetPanelPane.js.map +1 -1
  11. package/dist/default/LanguageSelect.d.ts +9 -0
  12. package/dist/default/LanguageSelect.d.ts.map +1 -0
  13. package/dist/default/LanguageSelect.js +72 -0
  14. package/dist/default/LanguageSelect.js.map +1 -0
  15. package/dist/default/PageSizeSelect.d.ts +29 -0
  16. package/dist/default/PageSizeSelect.d.ts.map +1 -0
  17. package/dist/default/PageSizeSelect.js +25 -0
  18. package/dist/default/PageSizeSelect.js.map +1 -0
  19. package/dist/default/RangeFilterRow.d.ts +12 -0
  20. package/dist/default/RangeFilterRow.d.ts.map +1 -0
  21. package/dist/default/RangeFilterRow.js +75 -0
  22. package/dist/default/RangeFilterRow.js.map +1 -0
  23. package/dist/default/RankingDisclosurePane.d.ts +5 -0
  24. package/dist/default/RankingDisclosurePane.d.ts.map +1 -1
  25. package/dist/default/RankingDisclosurePane.js +15 -15
  26. package/dist/default/RankingDisclosurePane.js.map +1 -1
  27. package/dist/default/SearchBox.d.ts +13 -0
  28. package/dist/default/SearchBox.d.ts.map +1 -0
  29. package/dist/default/SearchBox.js +67 -0
  30. package/dist/default/SearchBox.js.map +1 -0
  31. package/dist/default/SearchPage.d.ts +54 -16
  32. package/dist/default/SearchPage.d.ts.map +1 -1
  33. package/dist/default/SearchPage.js +155 -8
  34. package/dist/default/SearchPage.js.map +1 -1
  35. package/dist/default/SearchResultCard.d.ts +19 -18
  36. package/dist/default/SearchResultCard.d.ts.map +1 -1
  37. package/dist/default/SearchResultCard.js +178 -5
  38. package/dist/default/SearchResultCard.js.map +1 -1
  39. package/dist/default/SearchResultsPane.d.ts +42 -3
  40. package/dist/default/SearchResultsPane.d.ts.map +1 -1
  41. package/dist/default/SearchResultsPane.js +38 -21
  42. package/dist/default/SearchResultsPane.js.map +1 -1
  43. package/dist/default/SortSelect.d.ts +10 -2
  44. package/dist/default/SortSelect.d.ts.map +1 -1
  45. package/dist/default/SortSelect.js +27 -24
  46. package/dist/default/SortSelect.js.map +1 -1
  47. package/dist/default/UrlIssueNotice.d.ts.map +1 -1
  48. package/dist/default/UrlIssueNotice.js +19 -1
  49. package/dist/default/UrlIssueNotice.js.map +1 -1
  50. package/dist/default/index.d.ts +32 -15
  51. package/dist/default/index.d.ts.map +1 -1
  52. package/dist/default/index.js +26 -12
  53. package/dist/default/index.js.map +1 -1
  54. package/dist/default/sortLabels.d.ts +5 -0
  55. package/dist/default/sortLabels.d.ts.map +1 -0
  56. package/dist/default/sortLabels.js +32 -0
  57. package/dist/default/sortLabels.js.map +1 -0
  58. package/dist/headless/FacetPanel.d.ts +17 -0
  59. package/dist/headless/FacetPanel.d.ts.map +1 -1
  60. package/dist/headless/FacetPanel.js +23 -2
  61. package/dist/headless/FacetPanel.js.map +1 -1
  62. package/dist/headless/useAppliedCount.d.ts +8 -0
  63. package/dist/headless/useAppliedCount.d.ts.map +1 -0
  64. package/dist/headless/useAppliedCount.js +30 -0
  65. package/dist/headless/useAppliedCount.js.map +1 -0
  66. package/dist/headless/useScorerNames.d.ts +4 -0
  67. package/dist/headless/useScorerNames.d.ts.map +1 -0
  68. package/dist/headless/useScorerNames.js +34 -0
  69. package/dist/headless/useScorerNames.js.map +1 -0
  70. package/dist/headless/useSearchBox.d.ts +44 -0
  71. package/dist/headless/useSearchBox.d.ts.map +1 -0
  72. package/dist/headless/useSearchBox.js +116 -0
  73. package/dist/headless/useSearchBox.js.map +1 -0
  74. package/dist/i18n/es.d.ts.map +1 -1
  75. package/dist/i18n/es.js +30 -4
  76. package/dist/i18n/es.js.map +1 -1
  77. package/dist/i18n/keys.d.ts +35 -0
  78. package/dist/i18n/keys.d.ts.map +1 -1
  79. package/dist/i18n/keys.js +72 -4
  80. package/dist/i18n/keys.js.map +1 -1
  81. package/dist/i18n/ru.d.ts.map +1 -1
  82. package/dist/i18n/ru.js +34 -4
  83. package/dist/i18n/ru.js.map +1 -1
  84. package/dist/index.d.ts +7 -2
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +5 -2
  87. package/dist/index.js.map +1 -1
  88. package/dist/model/queries.d.ts +26 -2
  89. package/dist/model/queries.d.ts.map +1 -1
  90. package/dist/model/queries.js +38 -1
  91. package/dist/model/queries.js.map +1 -1
  92. package/dist/state/limits.d.ts +44 -0
  93. package/dist/state/limits.d.ts.map +1 -0
  94. package/dist/state/limits.js +44 -0
  95. package/dist/state/limits.js.map +1 -0
  96. package/dist/state/ranges.d.ts +52 -0
  97. package/dist/state/ranges.d.ts.map +1 -0
  98. package/dist/state/ranges.js +108 -0
  99. package/dist/state/ranges.js.map +1 -0
  100. package/llms.txt +15 -4
  101. package/manifest.json +238 -21
  102. package/nav-manifest.json +1 -1
  103. package/package.json +15 -10
  104. package/src/analytics/generated/events.json +1 -1
  105. package/src/default/DegradationNotice.tsx +34 -12
  106. package/src/default/FacetPanelPane.tsx +343 -64
  107. package/src/default/LanguageSelect.tsx +93 -0
  108. package/src/default/PageSizeSelect.tsx +80 -0
  109. package/src/default/RangeFilterRow.tsx +148 -0
  110. package/src/default/RankingDisclosurePane.tsx +67 -35
  111. package/src/default/SearchBox.tsx +130 -0
  112. package/src/default/SearchPage.tsx +351 -44
  113. package/src/default/SearchResultCard.tsx +231 -24
  114. package/src/default/SearchResultsPane.tsx +120 -70
  115. package/src/default/SortSelect.tsx +50 -37
  116. package/src/default/UrlIssueNotice.tsx +21 -3
  117. package/src/default/index.ts +38 -15
  118. package/src/default/sortLabels.ts +34 -0
  119. package/src/headless/FacetPanel.tsx +30 -2
  120. package/src/headless/useAppliedCount.ts +41 -0
  121. package/src/headless/useScorerNames.ts +36 -0
  122. package/src/headless/useSearchBox.ts +189 -0
  123. package/src/i18n/es.ts +37 -4
  124. package/src/i18n/keys.ts +83 -4
  125. package/src/i18n/ru.ts +41 -4
  126. package/src/index.ts +22 -2
  127. package/src/model/queries.ts +51 -1
  128. package/src/state/limits.ts +50 -0
  129. package/src/state/ranges.ts +144 -0
  130. package/dist/default/ErrorAlert.d.ts +0 -9
  131. package/dist/default/ErrorAlert.d.ts.map +0 -1
  132. package/dist/default/ErrorAlert.js +0 -27
  133. package/dist/default/ErrorAlert.js.map +0 -1
  134. package/dist/default/theme.d.ts +0 -24
  135. package/dist/default/theme.d.ts.map +0 -1
  136. package/dist/default/theme.js +0 -39
  137. package/dist/default/theme.js.map +0 -1
  138. package/src/default/ErrorAlert.tsx +0 -46
  139. package/src/default/theme.tsx +0 -66
package/CHANGELOG.md CHANGED
@@ -1,5 +1,156 @@
1
1
  # @stapel/search-react
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9545a2f: Search: the five blank stories, the phone filter path, money, and the dark scope
8
+
9
+ **Five stories rendered nothing.** `results-pane--*` and `filter-panel--*` crashed on
10
+ `data.items.filter` / `data.facet_meta.skipped` — 15 blank shots, including the designed
11
+ empty state. The seeded demos wrote the answer into the query cache and `useSearchQuery`
12
+ sets no `staleTime` (drill-down facets must never serve a stale page), so TanStack
13
+ refetched on mount and the demo `fetch`, which had no handler for a variant that seeded
14
+ instead of mocking, replaced the page with `{}`. A seed now mounts as a handler too, and
15
+ an UNMOCKED path answers 503 rather than an empty 200, so a forgotten handler renders the
16
+ pane's "we could not run this search" arm instead of a white screen.
17
+
18
+ **The phone filter sheet had no visual evidence at all** — the only filter path a phone
19
+ has, reachable only by a tap. `<SearchPage defaultFiltersOpen>` opens it on mount (for a
20
+ container that deep-links into the filters), and `search.filter-sheet` photographs it.
21
+ Its commit button now says **"Show 25+ results"** (`useAppliedCount`, cache-only — no
22
+ second request), the sheet no longer prints "Filters" twice (`FacetPanelPane heading`),
23
+ and the opener drops the "(0)" until something is applied.
24
+
25
+ **Money.** `3200 RUB` → `₽3,200`, through core's `useFormat().number` with
26
+ `style: "currency"` — the same `Intl` path `@stapel/currencies-react`'s `formatMoney`
27
+ takes. Non-numeric prices pass through; an unusable code falls back to a grouped number.
28
+
29
+ **Contrast, touch targets, orphans.** The DSA "Promoted" tag was `warning-on` (white)
30
+ over `warning-bg` (cream) — the one legally mandated string in the package, at ~1.2:1;
31
+ it is `warning` now, in both themes. Degradation banner lines drop `type="secondary"`
32
+ (grey on the warning tint failed AA). `size="small"` is gone from the range Apply/Clear,
33
+ the category/geo/clear-all buttons, so the shared skin's 44px phone control height
34
+ applies. The range Apply is primary when there is something to apply, not before. An
35
+ unfilled `renderGeoFilter` no longer leaves a "Location" heading over empty space in a
36
+ production build, and a facet group with no options draws no heading.
37
+
38
+ **Copy.** Scorer slugs are named from the ranking disclosure the pair already fetches
39
+ (`geo_decay` → "Distance"); skipped facet slugs from the category schema (`power_w` →
40
+ "Power"); `applies_to_sorts` through the sort control's own labels; the weight tag is
41
+ labelled; the unreadable-link notice says "price", not `r.price`, and no longer explains
42
+ `from..to` or `lat`/`lon`. New keys `search.filters.show_count{,_at_least}` (plural
43
+ families) and `search.limit.from_link`, in en/ru/es.
44
+
45
+ **Layout.** The desktop filter rail is a fixed 280px instead of `Col md={7}` (measured at
46
+ 45% of a 1280px page). Result cards carry the photo well filled (`fit` needs a box when
47
+ the snapshot has no aspect) and the whole row is one link when the doc type stores
48
+ `card.url`. The three superseded headless debug-dump demos are gone; their components are
49
+ covered by the skin demos that supersede them.
50
+
51
+ ## 0.6.0
52
+
53
+ ### Minor Changes
54
+
55
+ - 80617e9: The search page can start a search.
56
+
57
+ `setText` had zero callers in the entire repository: the codec carried `q`, the state
58
+ machine could set it, the request sent it, and no screen could type one. Six of the nine
59
+ state setters had no control at all. This release is the missing half of the pair.
60
+
61
+ - **`<SearchBox>`** — the query box, debounced (350ms, `replace: true`, so ten letters are
62
+ one history entry), capped at the server's own `MAX_QUERY_CHARS`, with a typeahead over
63
+ `GET /suggest` — an endpoint that had been typed and unreachable since 0.1. `useSearchBox`
64
+ is the headless half; `useSuggest` the hook. Exported, so a container's header can mount
65
+ the same box the page does (`searchBox={false}` then keeps exactly one on screen).
66
+ - **Range filters** — `r.<slug>=from..to` finally has a control. `state/ranges.ts` decides
67
+ which rows exist (numeric features of the category schema, plus any slug the URL already
68
+ constrains); `<RangeFilterRow>` commits on Apply and refuses a backwards range with the
69
+ reason beside the button instead of returning an empty page.
70
+ - **Category, location, language and page size** — `renderCategoryFilter` and
71
+ `renderGeoFilter` are named slots the categories/geo pairs fill, with `SlotPlaceholder`
72
+ where they are not; either way a constraint that arrived in a shared link now has a control
73
+ that widens it again (clear the category, adjust or clear the radius). `<LanguageSelect>`
74
+ and `<PageSizeSelect>` bind `setLanguage` and `setLimit`.
75
+ - **No reason lives in a hover any more.** The pager, the distance sort and the DSA Art. 26
76
+ `promoted` explanation were all `title=`/`<Tooltip>` — invisible on every phone and on
77
+ every disabled button. They are visible text now (`GatedButton`/`GatedControl`, and plain
78
+ copy under the marking). The pager is absent, not dead, when there is nothing to page.
79
+ - **The generic card draws `image_url`** through `@stapel/image` (new optional peer), in an
80
+ aspect box, with the promoted tag on a `--stapel-*` role instead of antd's `gold` preset.
81
+ - **On a phone the filters are a bottom sheet** behind a "Filters (N)" button, through the
82
+ shared `SkinDialog`, instead of a full-width panel stacked above the first result.
83
+ - **The pair's `theme.tsx` and `ErrorAlert.tsx` are deleted** in favour of
84
+ `@stapel/tokens-antd/skin`'s `SkinTheme` / `ErrorAlert` / `EmptyState` / `LoadList`.
85
+ `SearchSkinTheme` is no longer exported (pre-1.0 breaking = minor): import `SkinTheme`
86
+ from the substrate — same props, and a runtime `data-theme` flip repaints it.
87
+
88
+ Peers: `@stapel/core >=0.18.0`, `@stapel/tokens-antd >=0.6.0`, `@stapel/image >=0.3.0`
89
+ (optional — only the `/default` skin needs it). The `/default` size budget moves 13 KB → 16 KB.
90
+
91
+ ### Patch Changes
92
+
93
+ - 308e3d6: Every `/default` surface is drawn, seeded and photographed.
94
+
95
+ The skin shipped in the last release with one demo out of twelve names: the pair could
96
+ be read in source and not LOOKED at, which is the state §54's gate exists to end. All
97
+ twelve are covered now, and covered in the state each one is named for.
98
+
99
+ - **Eleven new skin demos** — the results pane (found / phone / a search that ran and
100
+ matched nothing), the filter panel (open, and narrowed by a shared link so every
101
+ constraint's door-out is visible), the ranking disclosure, the result card (promoted
102
+ with a photo, and plain), the query box, the sort gate (blocked and open), the page-size
103
+ ladder, the language filter, the numeric range row, the degradation notice (banner and
104
+ inline) and the unreadable-link notice. Each imports from `src/default`, each has a
105
+ `viewport: "phone"` variant, each variant declares the `step` it is seeded at.
106
+ - **Demos are SEEDED, not fetched** (`DemoSeed` in the demo harness): the answer is written
107
+ into the query cache under the key the pair's own codec derives, so a variant opens in its
108
+ state on the first frame instead of photographing the same skeleton under three names.
109
+ `assertVariantsRenderDistinctly` is wired into the demo suite and enforces it.
110
+ - **Dedicated suites** for `useSearchBox` (debounce, one history entry per word, the URL
111
+ winning whenever it moves on its own, the `MAX_QUERY_CHARS` cap), `useSuggest` (the
112
+ three-character floor, the clamped limit, a refusal that leaves the box typeable, a menu
113
+ that stays shut on an empty answer) and the range model (which slugs get a row, what the
114
+ row refuses, and that the reason stands beside the button).
115
+ - **A render matrix per surface** — each of the four surfaces in phone and desktop, light
116
+ and dark, asserting the mode is the DOCUMENT's rather than a default baked into the skin,
117
+ plus the filter surface following the viewport. `setViewport` / `setDocumentTheme` are the
118
+ test harness helpers the setup file has been pointing at.
119
+
120
+ No public API change: `src/` is untouched. The demo registry, manifest and `llms.txt` are
121
+ regenerated.
122
+
123
+ ## 0.5.0
124
+
125
+ ### Minor Changes
126
+
127
+ - d778c54: `<SearchPage>` stops laying out a column for filters that do not exist, and
128
+ stops printing a second heading over the list.
129
+
130
+ - The filter column was unconditional. On a deployment whose search plan
131
+ declares no facets that spent a quarter of `/s`, of every category page and
132
+ of every seller page on an illustration saying "no filters for this search" —
133
+ three screens with a hole in them. The page now asks the facet bag what it
134
+ has and gives the results the whole width when the answer is a LOADED zero.
135
+ `loading` and `failed` keep the column: a panel that has not answered yet is
136
+ not a panel with nothing in it, and a layout that reflowed mid-load would be
137
+ worse than the hole. An active filter keeps it too.
138
+ - `resultsHeading` / `<SearchResultsPane heading>`: what this surface calls its
139
+ list. "Results" is only right when the person performed a search — a
140
+ landing's newest-first strip, a seller's page and a category page each wrote
141
+ their own caption above the pane and got "Results" underneath it a moment
142
+ later. The name now goes INTO the heading row that already exists.
143
+ - `filtersHeader`: a slot at the top of the filter column, for a filter this
144
+ pair cannot ship. `SearchQueryState` has carried `geo` since 0.2 and
145
+ `<SortSelect>` already disables "by distance" with the reason "no centre
146
+ set", but nothing here could ever SET one — turning an address into a
147
+ coordinate needs a geocoder, and a geocoder is the deployment's. Whatever a
148
+ host renders here reads and writes the same URL state as the facets beside
149
+ it.
150
+ - `useFacetPanel()` is exported: the same bag as `<FacetPanel>`, for a caller
151
+ that must know what the panel will render before it renders it. A render prop
152
+ cannot answer a question asked one level up.
153
+
3
154
  ## 0.4.0
4
155
 
5
156
  ### Minor Changes
package/README.md CHANGED
@@ -152,7 +152,7 @@ the configured engine cannot evaluate.
152
152
  | state (pure) | `parseSearchState`, `writeSearchState`, `patchSearchState`, `toggleFilterValue`, `setFilterValues`, `setRangeValue`, `clearFilters`, `activeFilterCount`, `parseDegradations`, `countIsEstimate`, `buildFacetGroups`, `facetOptionLabel` |
153
153
  | model | `createSearchRuntime`, `searchQueryKeys`, `useSearchQuery`, `useRankingDisclosure` |
154
154
  | headless | `SearchProvider`, `SearchStateProvider`/`useSearchState`, `SearchResults`, `FacetPanel`, `RankingDisclosure` |
155
- | `./default` | `SearchPage`, `SearchResultsPane`, `FacetPanelPane`, `RankingDisclosurePane`, `SearchResultCard`, `DegradationNotice`, `UrlIssueNotice`, `SortSelect`, `SearchSkinTheme` |
155
+ | `./default` | `SearchPage`, `SearchResultsPane`, `FacetPanelPane`, `RankingDisclosurePane`, `SearchBox`, `SortSelect`, `PageSizeSelect`, `LanguageSelect`, `SearchResultCard`, `RangeFilterRow`, `DegradationNotice`, `UrlIssueNotice` (the skin themes itself through `SkinTheme` from `@stapel/tokens-antd/skin`; the pair's own `SearchSkinTheme` is gone as of 0.6.0) |
156
156
  | `./router` | `useRouterSearchParams` |
157
157
  | i18n | `registerSearchI18n` (+ `./i18n/ru`, `./i18n/es`) |
158
158
  | errors | `SEARCH_ERRORS`, `explainSearchError`, `SEARCH_WINDOW_EXCEEDED`, `SEARCH_BACKEND_UNAVAILABLE` |
@@ -41,6 +41,13 @@ export type DegradationNoticeVariant = "banner" | "inline" | "off";
41
41
  export interface DegradationNoticeProps {
42
42
  readonly degradations: readonly SearchDegradation[];
43
43
  readonly variant?: DegradationNoticeVariant;
44
+ /**
45
+ * Names a `scorer:` degradation's slug, when something on the page knows
46
+ * one. `<SearchResultsPane>` passes the ranking disclosure's own names
47
+ * (`useScorerNames`); with no answer the slug is printed, which is what a
48
+ * registry identifier is worth on its own.
49
+ */
50
+ readonly scorerName?: (slug: string) => string | undefined;
44
51
  }
45
52
  export declare function DegradationNotice(props: DegradationNoticeProps): ReactElement | null;
46
53
  //# sourceMappingURL=DegradationNotice.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DegradationNotice.d.ts","sourceRoot":"","sources":["../../src/default/DegradationNotice.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAG1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAIzD;;;;;;;GAOG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEnE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC;CAC7C;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,YAAY,GAAG,IAAI,CA4CrB"}
1
+ {"version":3,"file":"DegradationNotice.d.ts","sourceRoot":"","sources":["../../src/default/DegradationNotice.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAI1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAIzD;;;;;;;GAOG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEnE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,YAAY,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC5D;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,YAAY,GAAG,IAAI,CA0DrB"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Alert, Flex, Typography } from "antd";
3
+ import { fontSize, spacing } from "@stapel/tokens";
3
4
  import { useT } from "@stapel/core";
4
5
  import { isCountNuanceOnly } from "../state/degradations.js";
5
6
  import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
@@ -13,13 +14,23 @@ export function DegradationNotice(props) {
13
14
  // A count nuance is not a degraded search — the count already says "N+".
14
15
  if (isCountNuanceOnly(props.degradations))
15
16
  return null;
16
- const lines = props.degradations.map((degradation) => (_jsx("li", { "data-degradation": degradation.raw, children: _jsx(Typography.Text, { type: "secondary", children: t(degradation.messageKey, {
17
- scorer: degradation.scorer ?? "",
18
- raw: degradation.raw,
19
- }) }) }, degradation.raw)));
17
+ const say = (degradation) => {
18
+ const slug = degradation.scorer;
19
+ return t(degradation.messageKey, {
20
+ scorer: slug === undefined
21
+ ? ""
22
+ : (props.scorerName?.(slug) ?? slug),
23
+ raw: degradation.raw,
24
+ });
25
+ };
26
+ // NOT `type="secondary"` in the banner: antd paints a warning Alert in the
27
+ // theme's warning tint, and grey body text on it measured under 3:1 in the
28
+ // visual pass. Inside a coloured box the readable colour is the box's own
29
+ // text colour, which is what a plain `<Typography.Text>` inherits.
30
+ const lines = props.degradations.map((degradation) => (_jsx("li", { "data-degradation": degradation.raw, children: _jsx(Typography.Text, { children: say(degradation) }) }, degradation.raw)));
20
31
  if (variant === "inline") {
21
- return (_jsxs(Flex, { vertical: true, gap: 2, "data-testid": "search-degraded", "data-variant": "inline", children: [_jsx(Typography.Text, { type: "secondary", style: { fontSize: 12 }, children: t(SEARCH_I18N_KEYS.degradedTitle) }), _jsx("ul", { style: { margin: 0, paddingInlineStart: 20, fontSize: 12 }, children: lines })] }));
32
+ return (_jsxs(Flex, { vertical: true, gap: spacing[1], "data-testid": "search-degraded", "data-variant": "inline", children: [_jsx(Typography.Text, { type: "secondary", style: { fontSize: fontSize.xs.fontSize }, children: t(SEARCH_I18N_KEYS.degradedTitle) }), _jsx("ul", { style: { margin: 0, paddingInlineStart: spacing[5], fontSize: fontSize.xs.fontSize }, children: props.degradations.map((degradation) => (_jsx("li", { "data-degradation": degradation.raw, children: _jsx(Typography.Text, { type: "secondary", children: say(degradation) }) }, degradation.raw))) })] }));
22
33
  }
23
- return (_jsx(Alert, { type: "warning", showIcon: true, "data-testid": "search-degraded", "data-variant": "banner", message: t(SEARCH_I18N_KEYS.degradedTitle), description: _jsx("ul", { style: { margin: 0, paddingInlineStart: 20 }, children: lines }) }));
34
+ return (_jsx(Alert, { type: "warning", showIcon: true, "data-testid": "search-degraded", "data-variant": "banner", title: t(SEARCH_I18N_KEYS.degradedTitle), description: _jsx("ul", { style: { margin: 0, paddingInlineStart: spacing[5] }, children: lines }) }));
24
35
  }
25
36
  //# sourceMappingURL=DegradationNotice.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DegradationNotice.js","sourceRoot":"","sources":["../../src/default/DegradationNotice.tsx"],"names":[],"mappings":";AA8BA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAiBnD,MAAM,UAAU,iBAAiB,CAC/B,KAA6B;IAE7B,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,OAAO,GAA6B,KAAK,CAAC,OAAO,IAAI,QAAQ,CAAC;IACpE,IAAI,OAAO,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,yEAAyE;IACzE,IAAI,iBAAiB,CAAC,KAAK,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvD,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CACpD,iCAA4C,WAAW,CAAC,GAAG,YACzD,KAAC,UAAU,CAAC,IAAI,IAAC,IAAI,EAAC,WAAW,YAC9B,CAAC,CAAC,WAAW,CAAC,UAAU,EAAE;gBACzB,MAAM,EAAE,WAAW,CAAC,MAAM,IAAI,EAAE;gBAChC,GAAG,EAAE,WAAW,CAAC,GAAG;aACrB,CAAC,GACc,IANX,WAAW,CAAC,GAAG,CAOnB,CACN,CAAC,CAAC;IAEH,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,MAAC,IAAI,IAAC,QAAQ,QAAC,GAAG,EAAE,CAAC,iBAAc,iBAAiB,kBAAc,QAAQ,aACxE,KAAC,UAAU,CAAC,IAAI,IAAC,IAAI,EAAC,WAAW,EAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,YACtD,CAAC,CAAC,gBAAgB,CAAC,aAAa,CAAC,GAClB,EAClB,aAAI,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,YAC3D,KAAK,GACH,IACA,CACR,CAAC;IACJ,CAAC;IAED,OAAO,CACL,KAAC,KAAK,IACJ,IAAI,EAAC,SAAS,EACd,QAAQ,uBACI,iBAAiB,kBAChB,QAAQ,EACrB,OAAO,EAAE,CAAC,CAAC,gBAAgB,CAAC,aAAa,CAAC,EAC1C,WAAW,EACT,aAAI,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,EAAE,EAAE,YAAG,KAAK,GAAM,GAEhE,CACH,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"DegradationNotice.js","sourceRoot":"","sources":["../../src/default/DegradationNotice.tsx"],"names":[],"mappings":";AA8BA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAwBnD,MAAM,UAAU,iBAAiB,CAC/B,KAA6B;IAE7B,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;IACjB,MAAM,OAAO,GAA6B,KAAK,CAAC,OAAO,IAAI,QAAQ,CAAC;IACpE,IAAI,OAAO,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,yEAAyE;IACzE,IAAI,iBAAiB,CAAC,KAAK,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvD,MAAM,GAAG,GAAG,CAAC,WAA8B,EAAU,EAAE;QACrD,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC;QAChC,OAAO,CAAC,CAAC,WAAW,CAAC,UAAU,EAAE;YAC/B,MAAM,EACJ,IAAI,KAAK,SAAS;gBAChB,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;YACxC,GAAG,EAAE,WAAW,CAAC,GAAG;SACrB,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,mEAAmE;IACnE,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CACpD,iCAA4C,WAAW,CAAC,GAAG,YACzD,KAAC,UAAU,CAAC,IAAI,cAAE,GAAG,CAAC,WAAW,CAAC,GAAmB,IAD9C,WAAW,CAAC,GAAG,CAEnB,CACN,CAAC,CAAC;IAEH,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,MAAC,IAAI,IAAC,QAAQ,QAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,iBAAc,iBAAiB,kBAAc,QAAQ,aACjF,KAAC,UAAU,CAAC,IAAI,IAAC,IAAI,EAAC,WAAW,EAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YACxE,CAAC,CAAC,gBAAgB,CAAC,aAAa,CAAC,GAClB,EAClB,aAAI,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,YACrF,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CACvC,iCAA4C,WAAW,CAAC,GAAG,YACzD,KAAC,UAAU,CAAC,IAAI,IAAC,IAAI,EAAC,WAAW,YAAE,GAAG,CAAC,WAAW,CAAC,GAAmB,IAD/D,WAAW,CAAC,GAAG,CAEnB,CACN,CAAC,GACC,IACA,CACR,CAAC;IACJ,CAAC;IAED,OAAO,CACL,KAAC,KAAK,IACJ,IAAI,EAAC,SAAS,EACd,QAAQ,uBACI,iBAAiB,kBAChB,QAAQ,EACrB,KAAK,EAAE,CAAC,CAAC,gBAAgB,CAAC,aAAa,CAAC,EACxC,WAAW,EACT,aAAI,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,YAAG,KAAK,GAAM,GAExE,CACH,CAAC;AACJ,CAAC"}
@@ -1,5 +1,11 @@
1
1
  /**
2
- * `<FacetPanelPane>` — the antd facet panel.
2
+ * `<FacetPanelPane>` — the antd FILTER panel: everything that narrows a
3
+ * search, in the order a person narrows it.
4
+ *
5
+ * Category → language → location → numeric ranges → facet checkboxes. The last
6
+ * of those was, for three releases, the only one that existed: six of the nine
7
+ * state setters had no control at all, so a price range or a location could
8
+ * only be applied by editing the address bar (audit S-2, S-7).
3
9
  *
4
10
  * Three things it is obliged to render, all of which a naive panel drops:
5
11
  *
@@ -12,15 +18,60 @@
12
18
  * - `skipped` — the slugs the server did not count at all. Their options show
13
19
  * "not counted", never `0`. A silent zero there is the same defect class as
14
20
  * `data ?? []`: a number that looks like an answer and is not one.
21
+ *
22
+ * ── Two slots, and why they are slots ─────────────────────────────────────
23
+ *
24
+ * `renderCategoryFilter` and `renderGeoFilter` are filled by OTHER pairs:
25
+ * choosing a category means walking the catalogue tree (`categories-react`),
26
+ * and turning an address into a coordinate needs a geocoder and a map
27
+ * (`geo-react`). Neither belongs in a search package, and importing either
28
+ * would tie a storefront's search to a catalogue it might not have.
29
+ *
30
+ * What this panel does NOT do is pretend the slot is optional. An unfilled
31
+ * slot renders `SlotPlaceholder` in development — a named, visible hole rather
32
+ * than a silent absence — and, in every build, any constraint the URL already
33
+ * carries gets a control that REMOVES it. A shared link that narrows to a
34
+ * category or a point must never leave a person with no way to widen it again.
15
35
  */
16
- import type { ReactElement } from "react";
36
+ import type { ReactElement, ReactNode } from "react";
17
37
  import type { FeatureDef } from "@stapel/attributes-react";
38
+ import type { SearchGeo } from "../api/types.js";
18
39
  import type { ThemeModeProp } from "./types.js";
40
+ /** What a host's category control is handed. */
41
+ export interface CategoryFilterSlotProps {
42
+ /** The `root/leaf` path the search is narrowed to, if any. */
43
+ readonly value: string | undefined;
44
+ /** `null` widens the search back to the whole catalogue. */
45
+ readonly onChange: (path: string | null) => void;
46
+ }
47
+ /** What a host's location control is handed. */
48
+ export interface GeoFilterSlotProps {
49
+ readonly value: SearchGeo | undefined;
50
+ /** `null` clears the location constraint. */
51
+ readonly onChange: (geo: SearchGeo | null) => void;
52
+ }
19
53
  export interface FacetPanelPaneProps extends ThemeModeProp {
20
- /** The category's feature schema — the source of option LABELS. */
54
+ /** The category's feature schema — the source of option LABELS and of which
55
+ * slugs get a numeric range row. */
21
56
  readonly categoryFeatures?: readonly FeatureDef[];
22
57
  readonly locale?: string;
23
58
  readonly enabled?: boolean;
59
+ /** The catalogue picker (`categories-react`'s `CategoryPickerField`, bound
60
+ * to a path). Unfilled, an active category still gets a "clear" control. */
61
+ readonly renderCategoryFilter?: (slot: CategoryFilterSlotProps) => ReactNode;
62
+ /** The location control (`geo-react`). Unfilled, a location that arrived in
63
+ * the URL still gets its radius and a "clear" control. */
64
+ readonly renderGeoFilter?: (slot: GeoFilterSlotProps) => ReactNode;
65
+ /** BCP-47 tags this deployment indexes — see {@link LanguageSelect}. */
66
+ readonly languages?: readonly string[];
67
+ /**
68
+ * What the panel calls itself. `null` draws no title at all — for a surface
69
+ * that has ALREADY named it, which the phone sheet has: its dialog title and
70
+ * the panel's own heading both said "Filters", one under the other, in every
71
+ * shot of the open sheet. The row itself stays either way, because the
72
+ * "Clear all" control lives in it.
73
+ */
74
+ readonly heading?: ReactNode;
24
75
  }
25
76
  export declare function FacetPanelPane(props: FacetPanelPaneProps): ReactElement;
26
77
  //# sourceMappingURL=FacetPanelPane.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FacetPanelPane.d.ts","sourceRoot":"","sources":["../../src/default/FacetPanelPane.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAM3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,mEAAmE;IACnE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAsCD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,CAwGvE"}
1
+ {"version":3,"file":"FacetPanelPane.d.ts","sourceRoot":"","sources":["../../src/default/FacetPanelPane.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAmBrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAQjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,gDAAgD;AAChD,MAAM,WAAW,uBAAuB;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAClD;AAED,gDAAgD;AAChD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IACtC,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD;wCACoC;IACpC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;gFAC4E;IAC5E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,SAAS,CAAC;IAC7E;8DAC0D;IAC1D,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,SAAS,CAAC;IACnE,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;CAC9B;AA4MD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,CAkJvE"}
@@ -1,32 +1,132 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Alert, Button, Checkbox, Empty, Flex, Spin, Tag, Typography } from "antd";
3
- import { matchList, toFlowError, useDescribeFlowError, useT } from "@stapel/core";
2
+ import { Alert, Button, Checkbox, Divider, Flex, InputNumber, Typography, } from "antd";
3
+ import { SlotPlaceholder, isDevBuild, useT } from "@stapel/core";
4
+ import { EmptyState, ErrorAlert, LoadList, SkinTheme, } from "@stapel/tokens-antd/skin";
5
+ import { spacing } from "@stapel/tokens";
6
+ import { featureName } from "@stapel/attributes-react";
4
7
  import { FacetPanel } from "../headless/FacetPanel.js";
8
+ import { useSearchState } from "../headless/SearchStateProvider.js";
9
+ import { buildRangeGroups } from "../state/ranges.js";
5
10
  import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
6
- import { ErrorAlert } from "./ErrorAlert.js";
7
- import { SearchSkinTheme } from "./theme.js";
11
+ import { LanguageSelect } from "./LanguageSelect.js";
12
+ import { RangeFilterRow } from "./RangeFilterRow.js";
8
13
  function OptionRow(props) {
9
14
  const t = useT();
10
15
  const { option, group } = props;
11
- return (_jsxs(Flex, { justify: "space-between", align: "center", gap: 8, children: [_jsx(Checkbox, { checked: option.selected, "data-testid": `facet-option-${group.slug}-${option.value}`, "data-analytics": "none", "data-analytics-reason": "a filter is a read, not a flow step", onChange: () => {
16
+ return (_jsxs(Flex, { justify: "space-between", align: "center", gap: spacing[2], children: [_jsx(Checkbox, { checked: option.selected, "data-testid": `facet-option-${group.slug}-${option.value}`, "data-analytics": "none", "data-analytics-reason": "a filter is a read, not a flow step", onChange: () => {
12
17
  props.onToggle(group.slug, option.value);
13
- }, children: option.label }), option.count === null ? (_jsx(Tag, { "data-testid": `facet-count-${group.slug}-${option.value}`, children: t(SEARCH_I18N_KEYS.facetsNotCounted) })) : (_jsx(Typography.Text, { type: "secondary", "data-testid": `facet-count-${group.slug}-${option.value}`, children: option.count }))] }));
18
+ }, children: option.label }), option.count === null ? (_jsx(Typography.Text, { type: "secondary", "data-testid": `facet-count-${group.slug}-${option.value}`, children: t(SEARCH_I18N_KEYS.facetsNotCounted) })) : (_jsx(Typography.Text, { type: "secondary", "data-testid": `facet-count-${group.slug}-${option.value}`, children: option.count }))] }));
19
+ }
20
+ /** The category constraint: the host's control, or the door out of it. */
21
+ function CategoryFilter(props) {
22
+ const t = useT();
23
+ const { state, setCategory } = useSearchState();
24
+ const value = state.category;
25
+ if (props.render !== undefined) {
26
+ return (_jsxs(Flex, { vertical: true, gap: spacing[1], "data-testid": "search-category", children: [_jsx(Typography.Text, { strong: true, children: t(SEARCH_I18N_KEYS.categoryTitle) }), props.render({
27
+ value,
28
+ onChange: (path) => {
29
+ setCategory(path);
30
+ },
31
+ })] }));
32
+ }
33
+ if (value === undefined) {
34
+ return _jsx(SlotPlaceholder, { name: "renderCategoryFilter", "data-testid": "search-category-slot" });
35
+ }
36
+ return (_jsxs(Flex, { vertical: true, gap: spacing[1], "data-testid": "search-category", children: [_jsx(Typography.Text, { strong: true, children: t(SEARCH_I18N_KEYS.categoryTitle) }), _jsx(Typography.Text, { type: "secondary", children: t(SEARCH_I18N_KEYS.categoryCurrent, { path: value }) }), _jsx(Button, { style: { alignSelf: "flex-start" }, "data-testid": "search-category-clear", "data-analytics": "none", "data-analytics-reason": "a filter is a read, not a flow step", onClick: () => {
37
+ setCategory(null);
38
+ }, children: t(SEARCH_I18N_KEYS.categoryClear) })] }));
39
+ }
40
+ /**
41
+ * The location constraint.
42
+ *
43
+ * SETTING a centre needs a geocoder, which is the deployment's and
44
+ * `geo-react`'s. ADJUSTING one that already exists does not: the radius is a
45
+ * number in the URL, and a link shared with `lat/lon/radius_km` is a link this
46
+ * panel can widen, tighten and clear without knowing what a map is. That is the
47
+ * difference between a slot and a hole.
48
+ */
49
+ function GeoFilter(props) {
50
+ const t = useT();
51
+ const { state, setGeo } = useSearchState();
52
+ const geo = state.geo;
53
+ const slot = props.render !== undefined
54
+ ? props.render({
55
+ value: geo,
56
+ onChange: (next) => {
57
+ setGeo(next);
58
+ },
59
+ })
60
+ : geo === undefined
61
+ ? _jsx(SlotPlaceholder, { name: "renderGeoFilter", "data-testid": "search-geo-slot" })
62
+ : null;
63
+ if (geo === undefined) {
64
+ // An unfilled slot is a NAMED hole in development and nothing at all in a
65
+ // production build — so the heading has to follow the placeholder rather
66
+ // than outlive it. It did not, and the live desktop panel printed
67
+ // "Location" over empty space with no location control under it
68
+ // (class NC-ORPHANFIELD): a label is a promise that a control follows.
69
+ if (props.render === undefined && !isDevBuild())
70
+ return null;
71
+ return slot === null ? null : (_jsxs(Flex, { vertical: true, gap: spacing[1], "data-testid": "search-geo", children: [_jsx(Typography.Text, { strong: true, children: t(SEARCH_I18N_KEYS.geoTitle) }), slot] }));
72
+ }
73
+ return (_jsxs(Flex, { vertical: true, gap: spacing[1], "data-testid": "search-geo", children: [_jsx(Typography.Text, { strong: true, children: t(SEARCH_I18N_KEYS.geoTitle) }), slot, _jsx(Typography.Text, { type: "secondary", "data-testid": "search-geo-summary", children: geo.kind === "bbox"
74
+ ? t(SEARCH_I18N_KEYS.geoBox)
75
+ : t(SEARCH_I18N_KEYS.geoCenter, {
76
+ lat: geo.lat.toFixed(3),
77
+ lon: geo.lon.toFixed(3),
78
+ }) }), geo.kind === "center" && (_jsxs(Flex, { gap: spacing[2], align: "center", wrap: true, children: [_jsx(Typography.Text, { type: "secondary", "aria-hidden": "true", children: t(SEARCH_I18N_KEYS.geoRadiusLabel) }), _jsx(InputNumber, { min: 1, value: geo.radiusKm ?? null, "aria-label": t(SEARCH_I18N_KEYS.geoRadiusLabel), "data-testid": "search-geo-radius", onChange: (value) => {
79
+ setGeo({
80
+ kind: "center",
81
+ lat: geo.lat,
82
+ lon: geo.lon,
83
+ ...(typeof value === "number" ? { radiusKm: value } : {}),
84
+ });
85
+ } })] })), _jsx(Button, { style: { alignSelf: "flex-start" }, "data-testid": "search-geo-clear", "data-analytics": "none", "data-analytics-reason": "a filter is a read, not a flow step", onClick: () => {
86
+ setGeo(null);
87
+ }, children: t(SEARCH_I18N_KEYS.geoClear) })] }));
88
+ }
89
+ /**
90
+ * The slugs the server skipped, named the way the panel names everything else.
91
+ *
92
+ * `facet_meta.skipped` is a list of index slugs (`power_w`); the sentence that
93
+ * reports them was printing exactly that, so a shopper read "These filters
94
+ * were not counted for this search: power_w". The category schema is already
95
+ * in this component for the option labels — it names these too. A slug the
96
+ * schema does not know stays as it is, because a made-up name would be worse
97
+ * than an honest identifier.
98
+ */
99
+ function skippedNames(slugs, features, t) {
100
+ return slugs
101
+ .map((slug) => {
102
+ const feature = features?.find((candidate) => candidate.slug === slug);
103
+ if (feature === undefined)
104
+ return slug;
105
+ const name = t(featureName(feature));
106
+ return name.length > 0 ? name : slug;
107
+ })
108
+ .join(", ");
14
109
  }
15
110
  export function FacetPanelPane(props) {
16
111
  const t = useT();
17
- const describe = useDescribeFlowError();
18
- return (_jsx(SearchSkinTheme, { ...(props.mode !== undefined ? { mode: props.mode } : {}), children: _jsx(FacetPanel, { ...(props.categoryFeatures !== undefined
112
+ const { state } = useSearchState();
113
+ const ranges = buildRangeGroups({
114
+ state,
115
+ ...(props.categoryFeatures !== undefined
116
+ ? { categoryFeatures: props.categoryFeatures }
117
+ : {}),
118
+ t,
119
+ });
120
+ return (_jsx(SkinTheme, { ...(props.mode !== undefined ? { mode: props.mode } : {}), children: _jsx(FacetPanel, { ...(props.categoryFeatures !== undefined
19
121
  ? { categoryFeatures: props.categoryFeatures }
20
- : {}), ...(props.locale !== undefined ? { locale: props.locale } : {}), ...(props.enabled !== undefined ? { enabled: props.enabled } : {}), children: (bag) => (_jsxs(Flex, { vertical: true, gap: 12, "data-testid": "search-facets", children: [_jsxs(Flex, { justify: "space-between", align: "center", gap: 8, children: [_jsx(Typography.Title, { level: 5, style: { margin: 0 }, children: t(SEARCH_I18N_KEYS.facetsTitle) }), bag.activeFilters > 0 && (_jsx(Button, { size: "small", onClick: bag.clearAll, "data-analytics": "none", "data-analytics-reason": "a filter is a read, not a flow step", "data-testid": "facets-clear-all", children: t(SEARCH_I18N_KEYS.facetsClearAll, { count: bag.activeFilters }) }))] }), bag.approximate && (_jsx(Alert, { type: "info", showIcon: true, "data-testid": "facets-approximate", message: t(SEARCH_I18N_KEYS.facetsApproximate) })), bag.skipped.length > 0 && (_jsx(Alert, { type: "warning", showIcon: true, "data-testid": "facets-skipped", message: t(SEARCH_I18N_KEYS.facetsSkipped, {
21
- slugs: bag.skipped.join(", "),
22
- }) })), matchList(bag.state, {
23
- loading: () => (_jsx(Flex, { justify: "center", style: { padding: 16 }, children: _jsx(Spin, { "data-testid": "facets-loading" }) })),
24
- failed: (error) => (_jsx(ErrorAlert, { testId: "facets-failed", error: {
25
- ...describe(toFlowError(error)),
26
- message: t(SEARCH_I18N_KEYS.facetsLoadFailed),
27
- } })),
28
- empty: () => (_jsx(Empty, { "data-testid": "facets-empty", description: t(SEARCH_I18N_KEYS.facetsEmpty) })),
29
- ready: (groups) => (_jsxs(Flex, { vertical: true, gap: 16, children: [groups.map((group) => (_jsxs(Flex, { vertical: true, gap: 4, "data-testid": `facet-group-${group.slug}`, "data-counted": group.counted ? "true" : "false", children: [_jsx(Typography.Text, { strong: true, children: group.label }), group.options.map((option) => (_jsx(OptionRow, { group: group, option: option, onToggle: bag.toggle }, option.value)))] }, group.slug))), _jsx(Typography.Text, { type: "secondary", style: { fontSize: 12 }, children: t(SEARCH_I18N_KEYS.facetsDrillDownHint) })] })),
30
- })] })) }) }));
122
+ : {}), ...(props.locale !== undefined ? { locale: props.locale } : {}), ...(props.enabled !== undefined ? { enabled: props.enabled } : {}), children: (bag) => (_jsxs(Flex, { vertical: true, gap: spacing[3], "data-testid": "search-facets", children: [_jsxs(Flex, { justify: "space-between", align: "center", gap: spacing[2], children: [props.heading === null ? (_jsx("span", {})) : (_jsx(Typography.Title, { level: 5, style: { margin: 0 }, children: props.heading ?? t(SEARCH_I18N_KEYS.facetsTitle) })), bag.activeFilters > 0 && (_jsx(Button, { onClick: bag.clearAll, "data-analytics": "none", "data-analytics-reason": "a filter is a read, not a flow step", "data-testid": "facets-clear-all", children: t(SEARCH_I18N_KEYS.facetsClearAll, { count: bag.activeFilters }) }))] }), _jsx(CategoryFilter, { ...(props.renderCategoryFilter !== undefined
123
+ ? { render: props.renderCategoryFilter }
124
+ : {}) }), _jsx(LanguageSelect, { ...(props.languages !== undefined ? { languages: props.languages } : {}) }), _jsx(GeoFilter, { ...(props.renderGeoFilter !== undefined
125
+ ? { render: props.renderGeoFilter }
126
+ : {}) }), bag.approximate && (_jsx(Alert, { type: "info", showIcon: true, "data-testid": "facets-approximate", title: t(SEARCH_I18N_KEYS.facetsApproximate) })), bag.skipped.length > 0 && (_jsx(Alert, { type: "warning", showIcon: true, "data-testid": "facets-skipped", title: t(SEARCH_I18N_KEYS.facetsSkipped, {
127
+ slugs: skippedNames(bag.skipped, props.categoryFeatures, t),
128
+ }) })), ranges.length > 0 && (_jsx(Flex, { vertical: true, gap: spacing[3], "data-testid": "search-ranges", children: ranges.map((group) => (_jsx(RangeFilterRow, { group: group, onApply: bag.setRange }, group.slug))) })), ranges.length > 0 && _jsx(Divider, { style: { margin: 0 } }), _jsx(LoadList, { state: bag.state, testId: "facets", skeletonRows: 4, empty: _jsx(EmptyState, { compact: true, title: t(SEARCH_I18N_KEYS.facetsEmpty), testId: "facets-empty" }), failed: (error) => (_jsx(ErrorAlert, { testId: "facets-failed", thrown: error, message: t(SEARCH_I18N_KEYS.facetsLoadFailed) })), children: (groups) => (_jsxs(Flex, { vertical: true, gap: spacing[4], children: [groups
129
+ .filter((group) => group.options.length > 0)
130
+ .map((group) => (_jsxs(Flex, { vertical: true, gap: spacing[1], "data-testid": `facet-group-${group.slug}`, "data-counted": group.counted ? "true" : "false", children: [_jsx(Typography.Text, { strong: true, children: group.label }), group.options.map((option) => (_jsx(OptionRow, { group: group, option: option, onToggle: bag.toggle }, option.value)))] }, group.slug))), _jsx(Typography.Text, { type: "secondary", children: t(SEARCH_I18N_KEYS.facetsDrillDownHint) })] })) })] })) }) }));
31
131
  }
32
132
  //# sourceMappingURL=FacetPanelPane.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"FacetPanelPane.js","sourceRoot":"","sources":["../../src/default/FacetPanelPane.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FacetPanelPane.js","sourceRoot":"","sources":["../../src/default/FacetPanelPane.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { ReactElement } from "react";
2
+ /** Width of the language select — a language name plus room for a long one. */
3
+ export declare const LANGUAGE_SELECT_MIN_WIDTH = 176;
4
+ export interface LanguageSelectProps {
5
+ /** BCP-47 tags the deployment indexes (e.g. `["ru", "en"]`). */
6
+ readonly languages?: readonly string[];
7
+ }
8
+ export declare function LanguageSelect(props: LanguageSelectProps): ReactElement | null;
9
+ //# sourceMappingURL=LanguageSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LanguageSelect.d.ts","sourceRoot":"","sources":["../../src/default/LanguageSelect.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAO1C,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAgBD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,GAAG,IAAI,CAwC9E"}
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The query's LANGUAGE — `lang`, and the reason it is a filter rather than a
4
+ * display preference.
5
+ *
6
+ * `lang` does two things server-side (`query.py`): it picks the ANALYZER (so a
7
+ * Russian word stems as Russian rather than as an unknown token) and it
8
+ * NARROWS the corpus to documents indexed in that language. Getting it wrong therefore changes
9
+ * which results exist, not how they are written — which is why the control
10
+ * lives in the filter panel beside the facets and not in a settings menu.
11
+ *
12
+ * ── Which languages a deployment has is not this pair's fact ───────────────
13
+ *
14
+ * The index's languages come from what has been indexed; there is no endpoint
15
+ * that lists them. So the host names them (`languages`), and the labels come
16
+ * from `Intl.DisplayNames` in the reader's own locale — each language named
17
+ * the way THIS reader would name it — rather than from a translation table this
18
+ * pair would have to carry for every language a deployment might index.
19
+ *
20
+ * With no `languages` prop the control renders only when the URL already
21
+ * carries one, and then only to REMOVE it: a constraint that arrived in a
22
+ * shared link must always have a control that widens it again.
23
+ */
24
+ import { useMemo } from "react";
25
+ import { Flex, Select, Typography } from "antd";
26
+ import { useOptionalI18n, useT } from "@stapel/core";
27
+ import { spacing } from "@stapel/tokens";
28
+ import { useSearchState } from "../headless/SearchStateProvider.js";
29
+ import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
30
+ /** Width of the language select — a language name plus room for a long one. */
31
+ export const LANGUAGE_SELECT_MIN_WIDTH = 176;
32
+ /**
33
+ * A language tag as a person reads it, in THEIR locale. Falls back to the tag
34
+ * itself where the runtime has no name for it — a tag is at least honest,
35
+ * where a blank option is unclickable.
36
+ */
37
+ function languageName(tag, locale) {
38
+ try {
39
+ const names = new Intl.DisplayNames([locale], { type: "language" });
40
+ return names.of(tag) ?? tag;
41
+ }
42
+ catch {
43
+ return tag;
44
+ }
45
+ }
46
+ export function LanguageSelect(props) {
47
+ const t = useT();
48
+ // The reader's locale names the languages. Optional on purpose: every other
49
+ // control in this skin renders outside a provider (English floor) rather
50
+ // than throwing, and a language list is not the place to break that.
51
+ const locale = useOptionalI18n()?.locale ?? "en";
52
+ const { state, setLanguage } = useSearchState();
53
+ const active = state.lang;
54
+ const languages = props.languages;
55
+ const options = useMemo(() => {
56
+ const tags = [...(languages ?? [])];
57
+ if (active !== undefined && !tags.includes(active))
58
+ tags.push(active);
59
+ return [
60
+ { value: "", label: t(SEARCH_I18N_KEYS.languageAny) },
61
+ ...tags.map((tag) => ({ value: tag, label: languageName(tag, locale) })),
62
+ ];
63
+ }, [languages, active, locale, t]);
64
+ // Nothing offered and nothing applied: a select with one option ("any") is a
65
+ // control that cannot do anything, which §83 calls a dead control.
66
+ if ((languages ?? []).length === 0 && active === undefined)
67
+ return null;
68
+ return (_jsxs(Flex, { gap: spacing[2], align: "center", wrap: true, children: [_jsx(Typography.Text, { type: "secondary", "aria-hidden": "true", children: t(SEARCH_I18N_KEYS.languageLabel) }), _jsx(Select, { "data-testid": "search-language", "aria-label": t(SEARCH_I18N_KEYS.languageLabel), style: { minWidth: LANGUAGE_SELECT_MIN_WIDTH }, value: active ?? "", onChange: (next) => {
69
+ setLanguage(next === "" ? null : next);
70
+ }, options: options })] }));
71
+ }
72
+ //# sourceMappingURL=LanguageSelect.js.map