@stapel/search-react 0.3.1 → 0.4.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 (57) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/MODULE.md +43 -4
  3. package/dist/api/generated/schema.d.ts +5 -1
  4. package/dist/api/generated/schema.d.ts.map +1 -1
  5. package/dist/default/DegradationNotice.d.ts +27 -0
  6. package/dist/default/DegradationNotice.d.ts.map +1 -1
  7. package/dist/default/DegradationNotice.js +17 -6
  8. package/dist/default/DegradationNotice.js.map +1 -1
  9. package/dist/default/SearchPage.d.ts +4 -0
  10. package/dist/default/SearchPage.d.ts.map +1 -1
  11. package/dist/default/SearchPage.js +4 -2
  12. package/dist/default/SearchPage.js.map +1 -1
  13. package/dist/default/SearchResultsPane.d.ts +12 -0
  14. package/dist/default/SearchResultsPane.d.ts.map +1 -1
  15. package/dist/default/SearchResultsPane.js +8 -5
  16. package/dist/default/SearchResultsPane.js.map +1 -1
  17. package/dist/default/index.d.ts +1 -1
  18. package/dist/default/index.d.ts.map +1 -1
  19. package/dist/default/index.js.map +1 -1
  20. package/dist/headless/SearchResults.d.ts +20 -3
  21. package/dist/headless/SearchResults.d.ts.map +1 -1
  22. package/dist/headless/SearchResults.js +3 -1
  23. package/dist/headless/SearchResults.js.map +1 -1
  24. package/dist/i18n/es.d.ts.map +1 -1
  25. package/dist/i18n/es.js +2 -0
  26. package/dist/i18n/es.js.map +1 -1
  27. package/dist/i18n/keys.d.ts +11 -2
  28. package/dist/i18n/keys.d.ts.map +1 -1
  29. package/dist/i18n/keys.js +14 -2
  30. package/dist/i18n/keys.js.map +1 -1
  31. package/dist/i18n/ru.d.ts.map +1 -1
  32. package/dist/i18n/ru.js +4 -0
  33. package/dist/i18n/ru.js.map +1 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/state/degradations.d.ts +41 -0
  39. package/dist/state/degradations.d.ts.map +1 -1
  40. package/dist/state/degradations.js +41 -0
  41. package/dist/state/degradations.js.map +1 -1
  42. package/llms.txt +2 -2
  43. package/manifest.json +8 -2
  44. package/nav-manifest.json +1 -1
  45. package/package.json +3 -3
  46. package/src/analytics/generated/events.json +1 -1
  47. package/src/api/generated/schema.ts +5 -1
  48. package/src/default/DegradationNotice.tsx +61 -13
  49. package/src/default/SearchPage.tsx +8 -0
  50. package/src/default/SearchResultsPane.tsx +27 -6
  51. package/src/default/index.ts +4 -1
  52. package/src/headless/SearchResults.tsx +32 -4
  53. package/src/i18n/es.ts +2 -0
  54. package/src/i18n/keys.ts +14 -2
  55. package/src/i18n/ru.ts +4 -0
  56. package/src/index.ts +8 -1
  57. package/src/state/degradations.ts +60 -0
package/src/i18n/keys.ts CHANGED
@@ -28,9 +28,18 @@ export const SEARCH_I18N_KEYS = {
28
28
  resultsLoadFailed: "search.results.load_failed",
29
29
  resultsEmpty: "search.results.empty",
30
30
  resultsRetry: "search.results.retry",
31
- /** "About N results" — the sentence for an ESTIMATED count. A PLURAL
32
- * FAMILY: render with `tPlural`, never `t` (see SEARCH_I18N_PLURAL_KEYS). */
31
+ /**
32
+ * "About N results" the sentence for an ESTIMATED count. A PLURAL
33
+ * FAMILY: render with `tPlural`, never `t` (see SEARCH_I18N_PLURAL_KEYS).
34
+ *
35
+ * @deprecated stapel-search 0.2.0 stopped producing estimates: a count is
36
+ * exact, a FLOOR (`resultsCountAtLeast`), or absent. Kept so a host that
37
+ * translated it keeps a working key, and no longer rendered by the skin.
38
+ */
33
39
  resultsCountApproximate: "search.results.count_approximate",
40
+ /** "N+ results" — a LOWER BOUND: at least this many match, possibly more.
41
+ * A PLURAL FAMILY. Never render a floor as a plain number. */
42
+ resultsCountAtLeast: "search.results.count_at_least",
34
43
  /** "N results" — only when `exact_total` is true. A PLURAL FAMILY. */
35
44
  resultsCountExact: "search.results.count_exact",
36
45
  resultsTookMs: "search.results.took_ms",
@@ -129,6 +138,7 @@ export type SearchI18nKey =
129
138
  */
130
139
  export const SEARCH_I18N_PLURAL_KEYS: readonly SearchI18nKey[] = [
131
140
  SEARCH_I18N_KEYS.resultsCountApproximate,
141
+ SEARCH_I18N_KEYS.resultsCountAtLeast,
132
142
  SEARCH_I18N_KEYS.resultsCountExact,
133
143
  ];
134
144
 
@@ -149,6 +159,8 @@ export const searchI18nBundleEn: Record<string, string> = {
149
159
  "search.results.retry": "Try again",
150
160
  "search.results.count_approximate.one": "About {count} result",
151
161
  "search.results.count_approximate.other": "About {count} results",
162
+ "search.results.count_at_least.one": "{count}+ result",
163
+ "search.results.count_at_least.other": "{count}+ results",
152
164
  "search.results.count_exact.one": "{count} result",
153
165
  "search.results.count_exact.other": "{count} results",
154
166
  "search.results.took_ms": "{ms} ms",
package/src/i18n/ru.ts CHANGED
@@ -41,6 +41,10 @@ export const searchI18nBundleRu: I18nDictionary = {
41
41
  "search.results.count_approximate.few": "Примерно {count} объявления",
42
42
  "search.results.count_approximate.many": "Примерно {count} объявлений",
43
43
  "search.results.count_approximate.other": "Примерно {count} объявления",
44
+ "search.results.count_at_least.one": "{count}+ объявление",
45
+ "search.results.count_at_least.few": "{count}+ объявления",
46
+ "search.results.count_at_least.many": "{count}+ объявлений",
47
+ "search.results.count_at_least.other": "{count}+ объявления",
44
48
  "search.results.count_exact.one": "{count} объявление",
45
49
  "search.results.count_exact.few": "{count} объявления",
46
50
  "search.results.count_exact.many": "{count} объявлений",
package/src/index.ts CHANGED
@@ -86,7 +86,14 @@ export type {
86
86
  SearchStatePatch,
87
87
  } from "./state/urlState.js";
88
88
 
89
- export { countIsEstimate, degradationMessageKey, parseDegradations } from "./state/degradations.js";
89
+ export {
90
+ countIsEstimate,
91
+ countKind,
92
+ degradationMessageKey,
93
+ isCountNuanceOnly,
94
+ parseDegradations,
95
+ } from "./state/degradations.js";
96
+ export type { SearchCountKind } from "./state/degradations.js";
90
97
 
91
98
  export { buildFacetGroups, facetOptionLabel } from "./state/facets.js";
92
99
  export type {
@@ -66,6 +66,11 @@ export function parseDegradations(
66
66
  * the envelope's own `exact_total: false`, and the `"exact_total"`
67
67
  * degradation. They agree in practice; treating either as decisive is what
68
68
  * keeps "≈1 200" from ever being rendered as "1 200".
69
+ *
70
+ * @deprecated Since stapel-search 0.2.0 the envelope answers this directly
71
+ * with `count_is_lower_bound`, and "not exact" now means a FLOOR rather than
72
+ * a fuzzy number. Use {@link countKind}, which reads the wire field and also
73
+ * handles `count: null`.
69
74
  */
70
75
  export function countIsEstimate(
71
76
  exactTotal: boolean,
@@ -73,3 +78,58 @@ export function countIsEstimate(
73
78
  ): boolean {
74
79
  return !exactTotal || degradations.some((d) => d.kind === "exact_total");
75
80
  }
81
+
82
+ /**
83
+ * How a count may be spoken: as a number, as a floor, or not at all.
84
+ *
85
+ * The three states stapel-search 0.2.0 made explicit, because the version
86
+ * before it had only two and used `0` for both "none match" and "we do not
87
+ * know" — which is how a storefront came to print "About 0 listings" over
88
+ * four visible cards.
89
+ */
90
+ export type SearchCountKind = "exact" | "at_least" | "unknown";
91
+
92
+ /**
93
+ * Read the envelope's three count fields as one decision.
94
+ *
95
+ * `null` is UNKNOWN and renders as no count line — never as `0`, and never
96
+ * as "about 0". Anything the server declines to call exact is rendered as a
97
+ * floor ("N+"), including the defensive case of an `exact_total: false` from
98
+ * a server that predates `count_is_lower_bound`: claiming "at least N" over
99
+ * a page that shows N rows is the one reading that cannot be contradicted by
100
+ * what the reader can see.
101
+ */
102
+ export function countKind(
103
+ count: number | null | undefined,
104
+ isLowerBound: boolean | undefined,
105
+ exactTotal: boolean,
106
+ degradations: readonly SearchDegradation[]
107
+ ): SearchCountKind {
108
+ if (count === null || count === undefined) return "unknown";
109
+ if (isLowerBound === true) return "at_least";
110
+ return countIsEstimate(exactTotal, degradations) ? "at_least" : "exact";
111
+ }
112
+
113
+ /**
114
+ * Is `exact_total` the ONLY thing the engine could not do?
115
+ *
116
+ * It is a count NUANCE, not a failed search: the answer is complete, the
117
+ * rows are the right rows, and the single consequence — that the total is a
118
+ * floor — is already spoken by the count itself as "N+". Raising a warning
119
+ * banner over it teaches a reader that a perfectly good result page is
120
+ * broken, and a banner that cries wolf on every landing page is a banner
121
+ * nobody reads on the day `category_rollup` shows up in it.
122
+ *
123
+ * Every other degradation changes what the page MEANS ("typos were not
124
+ * corrected", "subcategories may be missing", "counts are approximate") and
125
+ * still renders. So does `exact_total` when it arrives BESIDE one of them —
126
+ * the list is then a description of a genuinely degraded answer.
127
+ */
128
+ export function isCountNuanceOnly(
129
+ degradations: readonly SearchDegradation[]
130
+ ): boolean {
131
+ return (
132
+ degradations.length > 0 &&
133
+ degradations.every((degradation) => degradation.kind === "exact_total")
134
+ );
135
+ }