@stapel/search-react 0.4.0 → 0.8.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 (154) hide show
  1. package/CHANGELOG.md +191 -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/FacetGroupControl.d.ts +40 -0
  8. package/dist/default/FacetGroupControl.d.ts.map +1 -0
  9. package/dist/default/FacetGroupControl.js +176 -0
  10. package/dist/default/FacetGroupControl.js.map +1 -0
  11. package/dist/default/FacetPanelPane.d.ts +54 -3
  12. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  13. package/dist/default/FacetPanelPane.js +116 -22
  14. package/dist/default/FacetPanelPane.js.map +1 -1
  15. package/dist/default/FilterChips.d.ts +28 -0
  16. package/dist/default/FilterChips.d.ts.map +1 -0
  17. package/dist/default/FilterChips.js +204 -0
  18. package/dist/default/FilterChips.js.map +1 -0
  19. package/dist/default/LanguageSelect.d.ts +9 -0
  20. package/dist/default/LanguageSelect.d.ts.map +1 -0
  21. package/dist/default/LanguageSelect.js +72 -0
  22. package/dist/default/LanguageSelect.js.map +1 -0
  23. package/dist/default/PageSizeSelect.d.ts +29 -0
  24. package/dist/default/PageSizeSelect.d.ts.map +1 -0
  25. package/dist/default/PageSizeSelect.js +25 -0
  26. package/dist/default/PageSizeSelect.js.map +1 -0
  27. package/dist/default/RangeFilterRow.d.ts +12 -0
  28. package/dist/default/RangeFilterRow.d.ts.map +1 -0
  29. package/dist/default/RangeFilterRow.js +75 -0
  30. package/dist/default/RangeFilterRow.js.map +1 -0
  31. package/dist/default/RankingDisclosurePane.d.ts +5 -0
  32. package/dist/default/RankingDisclosurePane.d.ts.map +1 -1
  33. package/dist/default/RankingDisclosurePane.js +15 -15
  34. package/dist/default/RankingDisclosurePane.js.map +1 -1
  35. package/dist/default/SearchBox.d.ts +13 -0
  36. package/dist/default/SearchBox.d.ts.map +1 -0
  37. package/dist/default/SearchBox.js +67 -0
  38. package/dist/default/SearchBox.js.map +1 -0
  39. package/dist/default/SearchPage.d.ts +94 -16
  40. package/dist/default/SearchPage.d.ts.map +1 -1
  41. package/dist/default/SearchPage.js +191 -8
  42. package/dist/default/SearchPage.js.map +1 -1
  43. package/dist/default/SearchResultCard.d.ts +19 -18
  44. package/dist/default/SearchResultCard.d.ts.map +1 -1
  45. package/dist/default/SearchResultCard.js +178 -5
  46. package/dist/default/SearchResultCard.js.map +1 -1
  47. package/dist/default/SearchResultsPane.d.ts +61 -3
  48. package/dist/default/SearchResultsPane.d.ts.map +1 -1
  49. package/dist/default/SearchResultsPane.js +51 -21
  50. package/dist/default/SearchResultsPane.js.map +1 -1
  51. package/dist/default/SortSelect.d.ts +10 -2
  52. package/dist/default/SortSelect.d.ts.map +1 -1
  53. package/dist/default/SortSelect.js +27 -24
  54. package/dist/default/SortSelect.js.map +1 -1
  55. package/dist/default/UrlIssueNotice.d.ts.map +1 -1
  56. package/dist/default/UrlIssueNotice.js +19 -1
  57. package/dist/default/UrlIssueNotice.js.map +1 -1
  58. package/dist/default/ViewSwitch.d.ts +66 -0
  59. package/dist/default/ViewSwitch.d.ts.map +1 -0
  60. package/dist/default/ViewSwitch.js +49 -0
  61. package/dist/default/ViewSwitch.js.map +1 -0
  62. package/dist/default/index.d.ts +38 -15
  63. package/dist/default/index.d.ts.map +1 -1
  64. package/dist/default/index.js +29 -12
  65. package/dist/default/index.js.map +1 -1
  66. package/dist/default/sortLabels.d.ts +5 -0
  67. package/dist/default/sortLabels.d.ts.map +1 -0
  68. package/dist/default/sortLabels.js +32 -0
  69. package/dist/default/sortLabels.js.map +1 -0
  70. package/dist/headless/FacetPanel.d.ts +17 -0
  71. package/dist/headless/FacetPanel.d.ts.map +1 -1
  72. package/dist/headless/FacetPanel.js +23 -2
  73. package/dist/headless/FacetPanel.js.map +1 -1
  74. package/dist/headless/useAppliedCount.d.ts +8 -0
  75. package/dist/headless/useAppliedCount.d.ts.map +1 -0
  76. package/dist/headless/useAppliedCount.js +30 -0
  77. package/dist/headless/useAppliedCount.js.map +1 -0
  78. package/dist/headless/useScorerNames.d.ts +4 -0
  79. package/dist/headless/useScorerNames.d.ts.map +1 -0
  80. package/dist/headless/useScorerNames.js +34 -0
  81. package/dist/headless/useScorerNames.js.map +1 -0
  82. package/dist/headless/useSearchBox.d.ts +44 -0
  83. package/dist/headless/useSearchBox.d.ts.map +1 -0
  84. package/dist/headless/useSearchBox.js +116 -0
  85. package/dist/headless/useSearchBox.js.map +1 -0
  86. package/dist/i18n/es.d.ts.map +1 -1
  87. package/dist/i18n/es.js +38 -4
  88. package/dist/i18n/es.js.map +1 -1
  89. package/dist/i18n/keys.d.ts +55 -0
  90. package/dist/i18n/keys.d.ts.map +1 -1
  91. package/dist/i18n/keys.js +101 -4
  92. package/dist/i18n/keys.js.map +1 -1
  93. package/dist/i18n/ru.d.ts.map +1 -1
  94. package/dist/i18n/ru.js +42 -4
  95. package/dist/i18n/ru.js.map +1 -1
  96. package/dist/index.d.ts +7 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +5 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/model/queries.d.ts +26 -2
  101. package/dist/model/queries.d.ts.map +1 -1
  102. package/dist/model/queries.js +38 -1
  103. package/dist/model/queries.js.map +1 -1
  104. package/dist/state/limits.d.ts +44 -0
  105. package/dist/state/limits.d.ts.map +1 -0
  106. package/dist/state/limits.js +44 -0
  107. package/dist/state/limits.js.map +1 -0
  108. package/dist/state/ranges.d.ts +52 -0
  109. package/dist/state/ranges.d.ts.map +1 -0
  110. package/dist/state/ranges.js +108 -0
  111. package/dist/state/ranges.js.map +1 -0
  112. package/llms.txt +16 -4
  113. package/manifest.json +268 -22
  114. package/nav-manifest.json +1 -1
  115. package/package.json +15 -10
  116. package/src/analytics/generated/events.json +1 -1
  117. package/src/default/DegradationNotice.tsx +34 -12
  118. package/src/default/FacetGroupControl.tsx +312 -0
  119. package/src/default/FacetPanelPane.tsx +320 -87
  120. package/src/default/FilterChips.tsx +415 -0
  121. package/src/default/LanguageSelect.tsx +93 -0
  122. package/src/default/PageSizeSelect.tsx +80 -0
  123. package/src/default/RangeFilterRow.tsx +148 -0
  124. package/src/default/RankingDisclosurePane.tsx +67 -35
  125. package/src/default/SearchBox.tsx +130 -0
  126. package/src/default/SearchPage.tsx +454 -44
  127. package/src/default/SearchResultCard.tsx +231 -24
  128. package/src/default/SearchResultsPane.tsx +158 -71
  129. package/src/default/SortSelect.tsx +50 -37
  130. package/src/default/UrlIssueNotice.tsx +21 -3
  131. package/src/default/ViewSwitch.tsx +147 -0
  132. package/src/default/index.ts +55 -15
  133. package/src/default/sortLabels.ts +34 -0
  134. package/src/headless/FacetPanel.tsx +30 -2
  135. package/src/headless/useAppliedCount.ts +41 -0
  136. package/src/headless/useScorerNames.ts +36 -0
  137. package/src/headless/useSearchBox.ts +189 -0
  138. package/src/i18n/es.ts +46 -4
  139. package/src/i18n/keys.ts +114 -4
  140. package/src/i18n/ru.ts +50 -4
  141. package/src/index.ts +22 -2
  142. package/src/model/queries.ts +51 -1
  143. package/src/state/limits.ts +50 -0
  144. package/src/state/ranges.ts +144 -0
  145. package/dist/default/ErrorAlert.d.ts +0 -9
  146. package/dist/default/ErrorAlert.d.ts.map +0 -1
  147. package/dist/default/ErrorAlert.js +0 -27
  148. package/dist/default/ErrorAlert.js.map +0 -1
  149. package/dist/default/theme.d.ts +0 -24
  150. package/dist/default/theme.d.ts.map +0 -1
  151. package/dist/default/theme.js +0 -39
  152. package/dist/default/theme.js.map +0 -1
  153. package/src/default/ErrorAlert.tsx +0 -46
  154. package/src/default/theme.tsx +0 -66
@@ -0,0 +1,52 @@
1
+ import type { FeatureDef } from "@stapel/attributes-react";
2
+ import type { SearchQueryState, SearchRange } from "../api/types.js";
3
+ /**
4
+ * Value types a numeric range row is drawn for (`config.type`, the
5
+ * stapel-attributes value-type slug).
6
+ */
7
+ export declare const RANGE_FEATURE_TYPES: readonly string[];
8
+ /** One `r.<slug>` row of the filter panel. */
9
+ export interface RangeGroup {
10
+ readonly slug: string;
11
+ /** The feature's display name (translated when it is a key), else the slug. */
12
+ readonly label: string;
13
+ /** The schema entry behind the slug, when the host supplied one. */
14
+ readonly feature: FeatureDef | undefined;
15
+ /** The applied bounds, exactly as the URL carries them (strings — the wire
16
+ * never promised a number, and re-formatting one would rewrite the link). */
17
+ readonly from: string | undefined;
18
+ readonly to: string | undefined;
19
+ /** Bounds the schema declares, for the input's own limits. */
20
+ readonly min: number | undefined;
21
+ readonly max: number | undefined;
22
+ /** Unit suffix the schema declares (`postfix`, or a convertible unit). */
23
+ readonly unit: string | undefined;
24
+ /** `1` for an integer feature — a whole-number input for a whole number. */
25
+ readonly step: number | undefined;
26
+ /** Whether the URL currently constrains this slug. */
27
+ readonly active: boolean;
28
+ }
29
+ export interface BuildRangeGroupsInput {
30
+ readonly state: SearchQueryState;
31
+ readonly categoryFeatures?: readonly FeatureDef[];
32
+ /** Translator for label keys (the schema's `name` is often one). */
33
+ readonly t?: (key: string) => string;
34
+ }
35
+ /** Is this feature one a numeric range row is drawn for? */
36
+ export declare function isRangeFeature(feature: FeatureDef): boolean;
37
+ /**
38
+ * The range rows for the current search: every numeric feature of the
39
+ * category, in the schema's own order, plus any slug the URL constrains that
40
+ * the schema does not explain.
41
+ */
42
+ export declare function buildRangeGroups(input: BuildRangeGroupsInput): readonly RangeGroup[];
43
+ /**
44
+ * Is a from/to pair one the server could ever match?
45
+ *
46
+ * `100..50` is syntactically fine and semantically empty, and the backend does
47
+ * not refuse it — it answers zero results, which reads as "there is nothing
48
+ * like this" rather than "you typed it backwards". So the panel refuses to
49
+ * apply it and says why, beside the button (`search.facets.range_invalid`).
50
+ */
51
+ export declare function isRangeUsable(range: SearchRange): boolean;
52
+ //# sourceMappingURL=ranges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranges.d.ts","sourceRoot":"","sources":["../../src/state/ranges.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAIhD,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC;IACzC;iFAC6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,8DAA8D;IAC9D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,oEAAoE;IACpE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAgBD,4DAA4D;AAC5D,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAG3D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,qBAAqB,GAC3B,SAAS,UAAU,EAAE,CA8BvB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAMzD"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The RANGE half of the filter model — `r.<slug>=from..to`.
3
+ *
4
+ * The codec has carried ranges since 0.1 (`setRangeValue`, `RANGE_PREFIX`) and
5
+ * nothing ever drew one: a marketplace shipped without a price filter because
6
+ * the panel only knew how to draw checkboxes. This module is the missing half:
7
+ * which slugs a range row exists for, and what the row is called.
8
+ *
9
+ * ── Where a range row comes from, and why it is not the response ───────────
10
+ *
11
+ * A facet answer (`facets: {slug: {value: count}}`) enumerates DISCRETE values;
12
+ * a range is not enumerable and the server never sends one. So the rows come
13
+ * from the CATEGORY SCHEMA — the same `categoryFeatures` slot that gives the
14
+ * checkboxes their labels — filtered to the numeric value types, plus every
15
+ * slug the URL already carries a range for.
16
+ *
17
+ * That last clause is the same rule `buildFacetGroups` follows for a filtered
18
+ * slug that fell out of the plan: a constraint that is ACTIVE must always have
19
+ * a control that removes it, even when the schema no longer explains it.
20
+ * Otherwise a shared link narrows the search with nothing on screen to widen it
21
+ * again.
22
+ *
23
+ * `date` is range-capable on the wire and is deliberately NOT here: a date
24
+ * range needs a date editor, the editor lives in `@stapel/attributes-react`'s
25
+ * registry, and a numeric input over a Unix timestamp would be worse than no
26
+ * control at all. The codec still round-trips one a host sets.
27
+ */
28
+ import { featureConfig, featureName, featureType } from "@stapel/attributes-react";
29
+ /**
30
+ * Value types a numeric range row is drawn for (`config.type`, the
31
+ * stapel-attributes value-type slug).
32
+ */
33
+ export const RANGE_FEATURE_TYPES = [
34
+ "int",
35
+ "float",
36
+ "convertible_unit",
37
+ ];
38
+ function num(value) {
39
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
40
+ }
41
+ function str(value) {
42
+ return typeof value === "string" && value.length > 0 ? value : undefined;
43
+ }
44
+ function translate(t, key) {
45
+ if (t === undefined)
46
+ return key;
47
+ const resolved = t(key);
48
+ return resolved.length > 0 ? resolved : key;
49
+ }
50
+ /** Is this feature one a numeric range row is drawn for? */
51
+ export function isRangeFeature(feature) {
52
+ const type = featureType(feature);
53
+ return type !== undefined && RANGE_FEATURE_TYPES.includes(type);
54
+ }
55
+ /**
56
+ * The range rows for the current search: every numeric feature of the
57
+ * category, in the schema's own order, plus any slug the URL constrains that
58
+ * the schema does not explain.
59
+ */
60
+ export function buildRangeGroups(input) {
61
+ const bySlug = new Map();
62
+ for (const feature of input.categoryFeatures ?? [])
63
+ bySlug.set(feature.slug, feature);
64
+ const slugs = [];
65
+ for (const feature of input.categoryFeatures ?? []) {
66
+ if (isRangeFeature(feature))
67
+ slugs.push(feature.slug);
68
+ }
69
+ for (const slug of Object.keys(input.state.ranges)) {
70
+ if (!slugs.includes(slug))
71
+ slugs.push(slug);
72
+ }
73
+ return slugs.map((slug) => {
74
+ const feature = bySlug.get(slug);
75
+ const config = feature === undefined ? {} : featureConfig(feature);
76
+ const applied = input.state.ranges[slug];
77
+ return {
78
+ slug,
79
+ label: feature === undefined ? slug : translate(input.t, featureName(feature)),
80
+ feature,
81
+ from: applied?.from,
82
+ to: applied?.to,
83
+ min: num(config["min"]),
84
+ max: num(config["max"]),
85
+ unit: str(config["postfix"]) ?? str(config["unit_m"]) ?? str(config["unit_i"]),
86
+ step: feature !== undefined && featureType(feature) === "int" ? 1 : undefined,
87
+ active: applied !== undefined,
88
+ };
89
+ });
90
+ }
91
+ /**
92
+ * Is a from/to pair one the server could ever match?
93
+ *
94
+ * `100..50` is syntactically fine and semantically empty, and the backend does
95
+ * not refuse it — it answers zero results, which reads as "there is nothing
96
+ * like this" rather than "you typed it backwards". So the panel refuses to
97
+ * apply it and says why, beside the button (`search.facets.range_invalid`).
98
+ */
99
+ export function isRangeUsable(range) {
100
+ if (range.from === undefined || range.to === undefined)
101
+ return true;
102
+ const from = Number(range.from);
103
+ const to = Number(range.to);
104
+ if (!Number.isFinite(from) || !Number.isFinite(to))
105
+ return true;
106
+ return from <= to;
107
+ }
108
+ //# sourceMappingURL=ranges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranges.js","sourceRoot":"","sources":["../../src/state/ranges.ts"],"names":[],"mappings":"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"}
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @stapel/search-react 0.4.0
1
+ # @stapel/search-react 0.8.0
2
2
 
3
3
  Headless React flow pair for stapel-search (contract >=0.2 <0.3) — business + state, zero visual opinion.
4
4
  Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
@@ -30,6 +30,7 @@ Paths are relative to `/search/api/v1/`.
30
30
  ## Query hooks (server state; keys come only from the key factory)
31
31
  - useRankingDisclosure (query) → ranking
32
32
  - useSearchQuery (query) → query
33
+ - useSuggest (query) → suggest
33
34
 
34
35
  ## Errors (render t(code, params); UX from remediation)
35
36
  54 keys (full catalog: manifest.json §errors). By remediation: fix_input 26 · retry 15 · verify 5 · wait_and_retry 5 · contact_support 2 · reauthenticate 1.
@@ -74,9 +75,20 @@ const { tracked } = useTracked();
74
75
  ```
75
76
 
76
77
  ## Demos (defineDemo → manifest.demos; compiled, linted, rendered examples)
77
- - search.facets → <FacetPanel> [default] demo/FacetPanel.demo.tsx
78
- - search.ranking → <RankingDisclosure> [default] demo/RankingDisclosure.demo.tsx
79
- - search.results → <SearchResults> [default] demo/SearchResults.demo.tsx
78
+ - search.box → <SearchBox> [empty|typed] demo/SearchBox.demo.tsx
79
+ - search.classified-filters → <FilterChips> [chips|applied|shapes|views] demo/ClassifiedFilters.demo.tsx
80
+ - search.degradation-notice → <DegradationNotice> [banner|inline] demo/DegradationNotice.demo.tsx
81
+ - search.facet-panel-pane → <FacetPanelPane> [open-search|narrowed] demo/FacetPanelPane.demo.tsx
82
+ - search.filter-sheet → <SearchPage> [open] demo/FilterSheet.demo.tsx
83
+ - search.language-select → <LanguageSelect> [offered|from-a-link] demo/LanguageSelect.demo.tsx
84
+ - search.page → <SearchPage> [desktop|phone|unreadable-link] demo/SearchPage.demo.tsx
85
+ - search.page-size-select → <PageSizeSelect> [ladder|off-ladder] demo/PageSizeSelect.demo.tsx
86
+ - search.range-filter-row → <RangeFilterRow> [untouched|applied] demo/RangeFilterRow.demo.tsx
87
+ - search.ranking-pane → <RankingDisclosurePane> [desktop|phone] demo/RankingDisclosurePane.demo.tsx
88
+ - search.result-card → <SearchResultCard> [promoted|plain] demo/SearchResultCard.demo.tsx
89
+ - search.results-pane → <SearchResultsPane> [desktop|phone|empty] demo/SearchResultsPane.demo.tsx
90
+ - search.sort-select → <SortSelect> [no-centre|with-centre] demo/SortSelect.demo.tsx
91
+ - search.url-issue-notice → <UrlIssueNotice> [two-issues|one-issue] demo/UrlIssueNotice.demo.tsx
80
92
  Each source file is the canonical usage snippet (open the default variant).
81
93
 
82
94
  ## Snippets
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
3
3
  "package": "@stapel/search-react",
4
- "version": "0.4.0",
4
+ "version": "0.8.0",
5
5
  "backend": {
6
6
  "module": "stapel-search",
7
7
  "contract": ">=0.2 <0.3"
@@ -70,6 +70,17 @@
70
70
  "query",
71
71
  "{params}"
72
72
  ]
73
+ },
74
+ "useSuggest": {
75
+ "kind": "query",
76
+ "operation": "suggest",
77
+ "queryKey": [
78
+ "search",
79
+ "suggest",
80
+ "{type}",
81
+ "{q}",
82
+ "{…}"
83
+ ]
73
84
  }
74
85
  },
75
86
  "errors": {
@@ -393,47 +404,228 @@
393
404
  },
394
405
  "demos": [
395
406
  {
396
- "id": "search.facets",
397
- "title": "Facet panel",
398
- "description": "Counts are computed with each facet's own filter removed, so picking one value does not zero its siblings. Labels come from the category schema through @stapel/attributes-react; a slug the server skipped shows 'not counted', never 0.",
399
- "component": "FacetPanel",
407
+ "id": "search.box",
408
+ "title": "Search box",
409
+ "description": "Types into a draft and searches after a pause, so the address bar does not chase the keyboard; the URL still wins whenever it moves on its own, so the box can never show a word the results are not about. Capped at the server's own MAX_QUERY_CHARS, which makes 'query too long' a refusal this control cannot cause.",
410
+ "component": "SearchBox",
400
411
  "tokens": [
401
412
  "surface-raised"
402
413
  ],
403
414
  "variants": [
404
- "default"
415
+ "empty",
416
+ "typed"
405
417
  ],
406
- "source": "demo/FacetPanel.demo.tsx"
418
+ "source": "demo/SearchBox.demo.tsx"
407
419
  },
408
420
  {
409
- "id": "search.ranking",
410
- "title": "Ranking disclosure",
411
- "description": "Which parameters order the results and how much each weighs, straight from the scorer registry. Parameters the configured engine cannot evaluate are listed with their reason rather than filtered out.",
412
- "component": "RankingDisclosure",
421
+ "id": "search.classified-filters",
422
+ "title": "Filter chips and group shapes",
423
+ "description": "The phone's filter row one scrolling line of chips, each opening its own bottom sheet, with the whole panel behind the leading icon-only chip and a dot on it when something is applied. Beside it, the three shapes a facet group takes, all three DERIVED from the category schema rather than chosen per screen: pills for a single-choice facet, indented rows for a hierarchical one, and a fold for a long one that says how many are behind it.",
424
+ "component": "FilterChips",
425
+ "covers": [
426
+ "FacetGroupControl",
427
+ "ViewSwitch"
428
+ ],
429
+ "tokens": [
430
+ "surface-raised",
431
+ "brand"
432
+ ],
433
+ "variants": [
434
+ "chips",
435
+ "applied",
436
+ "shapes",
437
+ "views"
438
+ ],
439
+ "source": "demo/ClassifiedFilters.demo.tsx"
440
+ },
441
+ {
442
+ "id": "search.degradation-notice",
443
+ "title": "Degradation notice",
444
+ "description": "What the engine could not do for THIS query, in words: the typo pass it skipped, the facet counts it sampled rather than counted, the scorer it could not evaluate. An unknown degradation still renders — with its raw literal — because a build that predates a limitation should say 'the engine reported something we have no wording for', not nothing.",
445
+ "component": "DegradationNotice",
413
446
  "tokens": [
414
447
  "surface-raised"
415
448
  ],
416
449
  "variants": [
417
- "default"
450
+ "banner",
451
+ "inline"
418
452
  ],
419
- "source": "demo/RankingDisclosure.demo.tsx"
453
+ "source": "demo/DegradationNotice.demo.tsx"
420
454
  },
421
455
  {
422
- "id": "search.results",
423
- "title": "Search results",
424
- "description": "One keyset page, rendered through matchList so 'nothing found' can only be said about a search that actually ran. The count is marked as an estimate when the envelope says so, and degraded[] is surfaced rather than swallowed.",
425
- "component": "SearchResults",
456
+ "id": "search.facet-panel-pane",
457
+ "title": "Filter panel",
458
+ "description": "The drill-down facet panel with every count beside its option including the options you have not chosen, which is what makes them drill-down rather than naive — plus the server's honesty flags said in words: 'these counts are approximate' and the slugs it did not count at all, whose options read 'not counted' and never a zero that looks like an answer.",
459
+ "component": "FacetPanelPane",
426
460
  "covers": [
461
+ "FacetPanel"
462
+ ],
463
+ "tokens": [
464
+ "surface-raised"
465
+ ],
466
+ "variants": [
467
+ "open-search",
468
+ "narrowed"
469
+ ],
470
+ "source": "demo/FacetPanelPane.demo.tsx"
471
+ },
472
+ {
473
+ "id": "search.filter-sheet",
474
+ "title": "Filter sheet",
475
+ "description": "The filters as a phone actually meets them: a bottom sheet over the results, with the drill-down counts beside every option, a Clear all for the two constraints the link arrived with, and a footer that commits by saying how many results the current choices lead to rather than a bare 'Apply'.",
476
+ "component": "SearchPage",
477
+ "tokens": [
478
+ "surface-raised"
479
+ ],
480
+ "variants": [
481
+ "open"
482
+ ],
483
+ "source": "demo/FilterSheet.demo.tsx"
484
+ },
485
+ {
486
+ "id": "search.language-select",
487
+ "title": "Language filter",
488
+ "description": "The languages a deployment indexes, each named the way THIS reader would name it. With nothing offered and nothing applied the control does not render at all: a select whose only option is 'any' is a dead control, and the panel is thinner without it.",
489
+ "component": "LanguageSelect",
490
+ "tokens": [
491
+ "surface-raised"
492
+ ],
493
+ "variants": [
494
+ "offered",
495
+ "from-a-link"
496
+ ],
497
+ "source": "demo/LanguageSelect.demo.tsx"
498
+ },
499
+ {
500
+ "id": "search.page",
501
+ "title": "Search page",
502
+ "description": "The composed /s screen: the query box bound to setText, the filter panel (category and location slots, numeric ranges, drill-down facets), the sort and page-size toolbar, and one keyset page of cards with the DSA promoted marking explained in words rather than in a tooltip.",
503
+ "component": "SearchPage",
504
+ "covers": [
505
+ "SearchStateProvider",
427
506
  "SearchProvider",
428
- "SearchStateProvider"
507
+ "SearchResults",
508
+ "FacetPanel"
509
+ ],
510
+ "tokens": [
511
+ "surface-raised",
512
+ "warning-bg"
429
513
  ],
514
+ "variants": [
515
+ "desktop",
516
+ "phone",
517
+ "unreadable-link"
518
+ ],
519
+ "source": "demo/SearchPage.demo.tsx"
520
+ },
521
+ {
522
+ "id": "search.page-size-select",
523
+ "title": "Page size control",
524
+ "description": "A short ladder around the server's DEFAULT_PAGE_SIZE. Choosing a size REPLACES the history entry rather than pushing one: a page size is a preference, not a step through the results, and Back should still undo the last filter.",
525
+ "component": "PageSizeSelect",
526
+ "tokens": [
527
+ "surface-raised"
528
+ ],
529
+ "variants": [
530
+ "ladder",
531
+ "off-ladder"
532
+ ],
533
+ "source": "demo/PageSizeSelect.demo.tsx"
534
+ },
535
+ {
536
+ "id": "search.range-filter-row",
537
+ "title": "Numeric range filter",
538
+ "description": "A range row per numeric feature of the category schema, plus any slug the URL already constrains — because a constraint that is active must always have a control that removes it, even when the schema no longer explains it. Bounds and step come from the schema; the unit is shown in the field's accessible name.",
539
+ "component": "RangeFilterRow",
540
+ "tokens": [
541
+ "surface-raised"
542
+ ],
543
+ "variants": [
544
+ "untouched",
545
+ "applied"
546
+ ],
547
+ "source": "demo/RangeFilterRow.demo.tsx"
548
+ },
549
+ {
550
+ "id": "search.ranking-pane",
551
+ "title": "Ranking disclosure",
552
+ "description": "The statutory ranking page in the default skin: every scorer with its weight, the sorts it applies to, and — for a scorer the deployment's engine cannot evaluate — the reason it is inactive, so the disclosure describes the ranking that actually ran rather than the one that was configured.",
553
+ "component": "RankingDisclosurePane",
554
+ "covers": [
555
+ "RankingDisclosure"
556
+ ],
557
+ "tokens": [
558
+ "surface-raised"
559
+ ],
560
+ "variants": [
561
+ "desktop",
562
+ "phone"
563
+ ],
564
+ "source": "demo/RankingDisclosurePane.demo.tsx"
565
+ },
566
+ {
567
+ "id": "search.result-card",
568
+ "title": "Result card",
569
+ "description": "The default card a doc type gets for free: a 4:3 photo well drawn before the network answers (via @stapel/image, so a dead URL is a named placeholder rather than a torn-page icon), the title, the price with its currency, the location and distance — and the promoted marking painted from a token role rather than an antd preset, with its explanation as ordinary text beneath it.",
570
+ "component": "SearchResultCard",
571
+ "tokens": [
572
+ "warning-bg",
573
+ "warning-on",
574
+ "warning-border",
575
+ "surface-sunken"
576
+ ],
577
+ "variants": [
578
+ "promoted",
579
+ "plain"
580
+ ],
581
+ "source": "demo/SearchResultCard.demo.tsx"
582
+ },
583
+ {
584
+ "id": "search.results-pane",
585
+ "title": "Results pane",
586
+ "description": "One keyset page in the default skin: a counted heading (a capped count reads 'N+', never a flat number the server did not promise), the degradation banner, an auto-fill card grid, and a pager that is ABSENT when there is nothing to page rather than disabled with its reason in a tooltip.",
587
+ "component": "SearchResultsPane",
588
+ "covers": [
589
+ "SearchResults"
590
+ ],
591
+ "tokens": [
592
+ "surface-raised",
593
+ "warning-bg"
594
+ ],
595
+ "variants": [
596
+ "desktop",
597
+ "phone",
598
+ "empty"
599
+ ],
600
+ "source": "demo/SearchResultsPane.demo.tsx"
601
+ },
602
+ {
603
+ "id": "search.sort-select",
604
+ "title": "Sort control",
605
+ "description": "The five shipped sorts, plus whatever unknown sort a shared link already carries — offered rather than silently reset, because resetting it would rewrite the meaning of somebody's link on load and the server is the one entitled to refuse an unknown sort by name.",
606
+ "component": "SortSelect",
607
+ "tokens": [
608
+ "surface-raised"
609
+ ],
610
+ "variants": [
611
+ "no-centre",
612
+ "with-centre"
613
+ ],
614
+ "source": "demo/SortSelect.demo.tsx"
615
+ },
616
+ {
617
+ "id": "search.url-issue-notice",
618
+ "title": "Unreadable link notice",
619
+ "description": "The parameters a shared link carried that the codec could not read, named one by one. The alternative is not 'no notice' but a quietly WIDER search than the one that was shared, which is the difference between a page that is wrong and a page that says which part of the link it lost.",
620
+ "component": "UrlIssueNotice",
430
621
  "tokens": [
431
622
  "surface-raised"
432
623
  ],
433
624
  "variants": [
434
- "default"
625
+ "two-issues",
626
+ "one-issue"
435
627
  ],
436
- "source": "demo/SearchResults.demo.tsx"
628
+ "source": "demo/UrlIssueNotice.demo.tsx"
437
629
  }
438
630
  ],
439
631
  "i18nKeys": [
@@ -491,6 +683,14 @@
491
683
  "error.500.internal",
492
684
  "error.503.mandate_unavailable",
493
685
  "error.503.search_backend_unavailable",
686
+ "search.box.clear",
687
+ "search.box.label",
688
+ "search.box.placeholder",
689
+ "search.box.submit",
690
+ "search.box.suggestions",
691
+ "search.category.clear",
692
+ "search.category.current",
693
+ "search.category.title",
494
694
  "search.degraded.category_rollup",
495
695
  "search.degraded.exact_facet_counts",
496
696
  "search.degraded.exact_total",
@@ -509,15 +709,39 @@
509
709
  "search.facets.loading",
510
710
  "search.facets.not_counted",
511
711
  "search.facets.range_apply",
712
+ "search.facets.range_clear",
512
713
  "search.facets.range_from",
714
+ "search.facets.range_from_aria",
715
+ "search.facets.range_invalid",
513
716
  "search.facets.range_to",
717
+ "search.facets.range_to_aria",
718
+ "search.facets.show_all",
719
+ "search.facets.show_less",
514
720
  "search.facets.skipped",
515
721
  "search.facets.title",
722
+ "search.filters.all",
723
+ "search.filters.apply",
724
+ "search.filters.chip_more",
725
+ "search.filters.chips_label",
726
+ "search.filters.dismiss",
727
+ "search.filters.open",
728
+ "search.filters.show_count",
729
+ "search.filters.show_count.one",
730
+ "search.filters.show_count.other",
731
+ "search.filters.show_count_at_least",
732
+ "search.filters.show_count_at_least.one",
733
+ "search.filters.show_count_at_least.other",
516
734
  "search.geo.box",
517
735
  "search.geo.center",
518
736
  "search.geo.clear",
519
737
  "search.geo.radius_km",
738
+ "search.geo.radius_label",
520
739
  "search.geo.title",
740
+ "search.language.any",
741
+ "search.language.label",
742
+ "search.limit.from_link",
743
+ "search.limit.label",
744
+ "search.limit.option",
521
745
  "search.ranking.applies_to",
522
746
  "search.ranking.empty",
523
747
  "search.ranking.inactive",
@@ -542,6 +766,7 @@
542
766
  "search.results.count_exact.other",
543
767
  "search.results.distance_km",
544
768
  "search.results.empty",
769
+ "search.results.image_alt",
545
770
  "search.results.load_failed",
546
771
  "search.results.loading",
547
772
  "search.results.next",
@@ -565,28 +790,40 @@
565
790
  "search.url.issue.geo_incomplete",
566
791
  "search.url.issue.not_a_number",
567
792
  "search.url.issue.range_malformed",
568
- "search.url.issues_title"
793
+ "search.url.issues_title",
794
+ "search.view.grid",
795
+ "search.view.label",
796
+ "search.view.list"
569
797
  ],
570
798
  "exports": {
571
799
  "runtime": [
572
800
  "FILTER_PREFIX",
573
801
  "FacetPanel",
802
+ "RANGE_FEATURE_TYPES",
574
803
  "RANGE_PREFIX",
575
804
  "RankingDisclosure",
576
805
  "SEARCH_BACKEND_UNAVAILABLE",
806
+ "SEARCH_BOX_DEBOUNCE_MS",
807
+ "SEARCH_BOX_SUGGEST_DEBOUNCE_MS",
808
+ "SEARCH_DEFAULT_PAGE_SIZE",
577
809
  "SEARCH_ERRORS",
578
810
  "SEARCH_ERROR_CODES",
579
811
  "SEARCH_I18N_KEYS",
580
812
  "SEARCH_I18N_PLURAL_KEYS",
813
+ "SEARCH_MAX_PAGE_SIZE",
581
814
  "SEARCH_PARAM",
815
+ "SEARCH_QUERY_MAX_CHARS",
582
816
  "SEARCH_SORTS",
583
817
  "SEARCH_WINDOW_EXCEEDED",
818
+ "SUGGEST_MAX_LIMIT",
819
+ "SUGGEST_MIN_CHARS",
584
820
  "SearchProvider",
585
821
  "SearchResults",
586
822
  "SearchRuntimeContext",
587
823
  "SearchStateProvider",
588
824
  "activeFilterCount",
589
825
  "buildFacetGroups",
826
+ "buildRangeGroups",
590
827
  "clearFilters",
591
828
  "countIsEstimate",
592
829
  "countKind",
@@ -596,6 +833,8 @@
596
833
  "explainSearchError",
597
834
  "facetOptionLabel",
598
835
  "isCountNuanceOnly",
836
+ "isRangeFeature",
837
+ "isRangeUsable",
599
838
  "navEntries",
600
839
  "ownsParam",
601
840
  "parseDegradations",
@@ -610,16 +849,20 @@
610
849
  "setRangeValue",
611
850
  "toggleFilterValue",
612
851
  "useAppliedSort",
852
+ "useFacetPanel",
613
853
  "useRankingDisclosure",
614
854
  "useSearchAnalytics",
615
855
  "useSearchApi",
856
+ "useSearchBox",
616
857
  "useSearchQuery",
617
858
  "useSearchRuntime",
618
859
  "useSearchState",
860
+ "useSuggest",
619
861
  "writeSearchState"
620
862
  ],
621
863
  "types": [
622
864
  "BuildFacetGroupsInput",
865
+ "BuildRangeGroupsInput",
623
866
  "CreateSearchRuntimeOptions",
624
867
  "FacetGroup",
625
868
  "FacetMeta",
@@ -628,12 +871,14 @@
628
871
  "FacetSelection",
629
872
  "ParseSearchStateOptions",
630
873
  "ParsedSearchState",
874
+ "RangeGroup",
631
875
  "RankingDisclosureBag",
632
876
  "RankingResponse",
633
877
  "Remediation",
634
878
  "Schemas",
635
879
  "Scorer",
636
880
  "SearchApi",
881
+ "SearchBoxBag",
637
882
  "SearchCountKind",
638
883
  "SearchDegradation",
639
884
  "SearchDegradationKind",
@@ -658,7 +903,8 @@
658
903
  "SearchStatePatch",
659
904
  "SearchStateProviderProps",
660
905
  "SuggestParams",
661
- "SuggestResponse"
906
+ "SuggestResponse",
907
+ "UseSearchBoxOptions"
662
908
  ]
663
909
  }
664
910
  }
package/nav-manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@stapel/search-react",
3
- "version": "0.4.0",
3
+ "version": "0.8.0",
4
4
  "entries": [
5
5
  {
6
6
  "id": "search.results",