@stapel/search-react 0.1.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 (195) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +203 -0
  3. package/README.md +175 -0
  4. package/dist/api/generated/schema.d.ts +375 -0
  5. package/dist/api/generated/schema.d.ts.map +1 -0
  6. package/dist/api/generated/schema.js +2 -0
  7. package/dist/api/generated/schema.js.map +1 -0
  8. package/dist/api/searchApi.d.ts +77 -0
  9. package/dist/api/searchApi.d.ts.map +1 -0
  10. package/dist/api/searchApi.js +79 -0
  11. package/dist/api/searchApi.js.map +1 -0
  12. package/dist/api/types.d.ts +131 -0
  13. package/dist/api/types.d.ts.map +1 -0
  14. package/dist/api/types.js +20 -0
  15. package/dist/api/types.js.map +1 -0
  16. package/dist/default/DegradationNotice.d.ts +19 -0
  17. package/dist/default/DegradationNotice.d.ts.map +1 -0
  18. package/dist/default/DegradationNotice.js +14 -0
  19. package/dist/default/DegradationNotice.js.map +1 -0
  20. package/dist/default/ErrorAlert.d.ts +9 -0
  21. package/dist/default/ErrorAlert.d.ts.map +1 -0
  22. package/dist/default/ErrorAlert.js +27 -0
  23. package/dist/default/ErrorAlert.js.map +1 -0
  24. package/dist/default/FacetPanelPane.d.ts +26 -0
  25. package/dist/default/FacetPanelPane.d.ts.map +1 -0
  26. package/dist/default/FacetPanelPane.js +32 -0
  27. package/dist/default/FacetPanelPane.js.map +1 -0
  28. package/dist/default/RankingDisclosurePane.d.ts +17 -0
  29. package/dist/default/RankingDisclosurePane.d.ts.map +1 -0
  30. package/dist/default/RankingDisclosurePane.js +23 -0
  31. package/dist/default/RankingDisclosurePane.js.map +1 -0
  32. package/dist/default/SearchPage.d.ts +34 -0
  33. package/dist/default/SearchPage.d.ts.map +1 -0
  34. package/dist/default/SearchPage.js +16 -0
  35. package/dist/default/SearchPage.js.map +1 -0
  36. package/dist/default/SearchResultCard.d.ts +34 -0
  37. package/dist/default/SearchResultCard.d.ts.map +1 -0
  38. package/dist/default/SearchResultCard.js +38 -0
  39. package/dist/default/SearchResultCard.js.map +1 -0
  40. package/dist/default/SearchResultsPane.d.ts +32 -0
  41. package/dist/default/SearchResultsPane.d.ts.map +1 -0
  42. package/dist/default/SearchResultsPane.js +46 -0
  43. package/dist/default/SearchResultsPane.js.map +1 -0
  44. package/dist/default/SortSelect.d.ts +22 -0
  45. package/dist/default/SortSelect.d.ts.map +1 -0
  46. package/dist/default/SortSelect.js +39 -0
  47. package/dist/default/SortSelect.js.map +1 -0
  48. package/dist/default/UrlIssueNotice.d.ts +12 -0
  49. package/dist/default/UrlIssueNotice.d.ts.map +1 -0
  50. package/dist/default/UrlIssueNotice.js +13 -0
  51. package/dist/default/UrlIssueNotice.js.map +1 -0
  52. package/dist/default/index.d.ts +40 -0
  53. package/dist/default/index.d.ts.map +1 -0
  54. package/dist/default/index.js +34 -0
  55. package/dist/default/index.js.map +1 -0
  56. package/dist/default/theme.d.ts +24 -0
  57. package/dist/default/theme.d.ts.map +1 -0
  58. package/dist/default/theme.js +39 -0
  59. package/dist/default/theme.js.map +1 -0
  60. package/dist/default/types.d.ts +12 -0
  61. package/dist/default/types.d.ts.map +1 -0
  62. package/dist/default/types.js +2 -0
  63. package/dist/default/types.js.map +1 -0
  64. package/dist/headless/FacetPanel.d.ts +61 -0
  65. package/dist/headless/FacetPanel.d.ts.map +1 -0
  66. package/dist/headless/FacetPanel.js +57 -0
  67. package/dist/headless/FacetPanel.js.map +1 -0
  68. package/dist/headless/RankingDisclosure.d.ts +39 -0
  69. package/dist/headless/RankingDisclosure.d.ts.map +1 -0
  70. package/dist/headless/RankingDisclosure.js +28 -0
  71. package/dist/headless/RankingDisclosure.js.map +1 -0
  72. package/dist/headless/SearchProvider.d.ts +19 -0
  73. package/dist/headless/SearchProvider.d.ts.map +1 -0
  74. package/dist/headless/SearchProvider.js +15 -0
  75. package/dist/headless/SearchProvider.js.map +1 -0
  76. package/dist/headless/SearchResults.d.ts +68 -0
  77. package/dist/headless/SearchResults.d.ts.map +1 -0
  78. package/dist/headless/SearchResults.js +70 -0
  79. package/dist/headless/SearchResults.js.map +1 -0
  80. package/dist/headless/SearchStateProvider.d.ts +69 -0
  81. package/dist/headless/SearchStateProvider.d.ts.map +1 -0
  82. package/dist/headless/SearchStateProvider.js +71 -0
  83. package/dist/headless/SearchStateProvider.js.map +1 -0
  84. package/dist/i18n/errorsMap.d.ts +27 -0
  85. package/dist/i18n/errorsMap.d.ts.map +1 -0
  86. package/dist/i18n/errorsMap.js +37 -0
  87. package/dist/i18n/errorsMap.js.map +1 -0
  88. package/dist/i18n/es.d.ts +13 -0
  89. package/dist/i18n/es.d.ts.map +1 -0
  90. package/dist/i18n/es.js +85 -0
  91. package/dist/i18n/es.js.map +1 -0
  92. package/dist/i18n/generated/errors.es.gen.d.ts +9 -0
  93. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  94. package/dist/i18n/generated/errors.es.gen.js +63 -0
  95. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  96. package/dist/i18n/generated/errors.gen.d.ts +359 -0
  97. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  98. package/dist/i18n/generated/errors.gen.js +183 -0
  99. package/dist/i18n/generated/errors.gen.js.map +1 -0
  100. package/dist/i18n/generated/errors.ru.gen.d.ts +9 -0
  101. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  102. package/dist/i18n/generated/errors.ru.gen.js +63 -0
  103. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  104. package/dist/i18n/keys.d.ts +107 -0
  105. package/dist/i18n/keys.d.ts.map +1 -0
  106. package/dist/i18n/keys.js +185 -0
  107. package/dist/i18n/keys.js.map +1 -0
  108. package/dist/i18n/ru.d.ts +29 -0
  109. package/dist/i18n/ru.d.ts.map +1 -0
  110. package/dist/i18n/ru.js +101 -0
  111. package/dist/i18n/ru.js.map +1 -0
  112. package/dist/index.d.ts +72 -0
  113. package/dist/index.d.ts.map +1 -0
  114. package/dist/index.js +66 -0
  115. package/dist/index.js.map +1 -0
  116. package/dist/model/context.d.ts +11 -0
  117. package/dist/model/context.d.ts.map +1 -0
  118. package/dist/model/context.js +16 -0
  119. package/dist/model/context.js.map +1 -0
  120. package/dist/model/queries.d.ts +48 -0
  121. package/dist/model/queries.d.ts.map +1 -0
  122. package/dist/model/queries.js +65 -0
  123. package/dist/model/queries.js.map +1 -0
  124. package/dist/model/queryKeys.d.ts +25 -0
  125. package/dist/model/queryKeys.d.ts.map +1 -0
  126. package/dist/model/queryKeys.js +22 -0
  127. package/dist/model/queryKeys.js.map +1 -0
  128. package/dist/model/runtime.d.ts +25 -0
  129. package/dist/model/runtime.d.ts.map +1 -0
  130. package/dist/model/runtime.js +6 -0
  131. package/dist/model/runtime.js.map +1 -0
  132. package/dist/nav/manifest.d.ts +21 -0
  133. package/dist/nav/manifest.d.ts.map +1 -0
  134. package/dist/nav/manifest.js +34 -0
  135. package/dist/nav/manifest.js.map +1 -0
  136. package/dist/router/index.d.ts +10 -0
  137. package/dist/router/index.d.ts.map +1 -0
  138. package/dist/router/index.js +34 -0
  139. package/dist/router/index.js.map +1 -0
  140. package/dist/state/degradations.d.ts +30 -0
  141. package/dist/state/degradations.d.ts.map +1 -0
  142. package/dist/state/degradations.js +52 -0
  143. package/dist/state/degradations.js.map +1 -0
  144. package/dist/state/facets.d.ts +68 -0
  145. package/dist/state/facets.d.ts.map +1 -0
  146. package/dist/state/facets.js +171 -0
  147. package/dist/state/facets.js.map +1 -0
  148. package/dist/state/urlState.d.ts +143 -0
  149. package/dist/state/urlState.d.ts.map +1 -0
  150. package/dist/state/urlState.js +319 -0
  151. package/dist/state/urlState.js.map +1 -0
  152. package/llms.txt +87 -0
  153. package/manifest.json +652 -0
  154. package/nav-manifest.json +44 -0
  155. package/package.json +133 -0
  156. package/src/analytics/generated/events.json +7 -0
  157. package/src/api/generated/schema.ts +377 -0
  158. package/src/api/searchApi.ts +169 -0
  159. package/src/api/types.ts +160 -0
  160. package/src/default/DegradationNotice.tsx +50 -0
  161. package/src/default/ErrorAlert.tsx +46 -0
  162. package/src/default/FacetPanelPane.tsx +174 -0
  163. package/src/default/RankingDisclosurePane.tsx +116 -0
  164. package/src/default/SearchPage.tsx +87 -0
  165. package/src/default/SearchResultCard.tsx +96 -0
  166. package/src/default/SearchResultsPane.tsx +173 -0
  167. package/src/default/SortSelect.tsx +77 -0
  168. package/src/default/UrlIssueNotice.tsx +37 -0
  169. package/src/default/index.ts +48 -0
  170. package/src/default/theme.tsx +66 -0
  171. package/src/default/types.ts +12 -0
  172. package/src/headless/FacetPanel.tsx +113 -0
  173. package/src/headless/RankingDisclosure.tsx +58 -0
  174. package/src/headless/SearchProvider.tsx +20 -0
  175. package/src/headless/SearchResults.tsx +149 -0
  176. package/src/headless/SearchStateProvider.tsx +175 -0
  177. package/src/i18n/errorsMap.ts +50 -0
  178. package/src/i18n/es.ts +110 -0
  179. package/src/i18n/generated/errors.es.gen.ts +69 -0
  180. package/src/i18n/generated/errors.gen.ts +214 -0
  181. package/src/i18n/generated/errors.json +423 -0
  182. package/src/i18n/generated/errors.ru.gen.ts +69 -0
  183. package/src/i18n/keys.ts +220 -0
  184. package/src/i18n/ru.ts +126 -0
  185. package/src/index.ts +154 -0
  186. package/src/model/context.tsx +28 -0
  187. package/src/model/queries.ts +79 -0
  188. package/src/model/queryKeys.ts +48 -0
  189. package/src/model/runtime.ts +33 -0
  190. package/src/nav/manifest.ts +53 -0
  191. package/src/router/index.ts +38 -0
  192. package/src/state/degradations.ts +75 -0
  193. package/src/state/facets.ts +219 -0
  194. package/src/state/urlState.ts +458 -0
  195. package/tsconfig.json +26 -0
@@ -0,0 +1,69 @@
1
+ // AUTO-GENERATED by scripts/gen-errors.mjs — do not edit by hand.
2
+ // Source: stapel-auth canonical errors.json (backend codegen artifact).
3
+ // Regenerate: pnpm gen:errors · Drift gate: pnpm gen:errors:check
4
+ // Locale source: backend translations/errors.ru.json (locale catalog
5
+ // beside the canonical errors.json — i18n-shipping.md §1).
6
+ import type { SearchErrorCode } from "./errors.gen.js";
7
+
8
+ /**
9
+ * `ru` texts for every backend error code — complete over the registry by
10
+ * construction (the generator fails on a gap; the Record type fails compilation
11
+ * on drift). Import from the pair's `./i18n/ru` subpath, NOT the main
12
+ * entry, so the locale stays out of hosts that don't ship it.
13
+ */
14
+ export const searchErrorBundleRu: Record<SearchErrorCode, string> = {
15
+ "error.400.bad_request": "Некорректный запрос",
16
+ "error.400.captcha_invalid": "Проверка капчи не пройдена. Пожалуйста, попробуйте ещё раз.",
17
+ "error.400.captcha_required": "Требуется токен капчи.",
18
+ "error.400.expected_list": "Ожидался список элементов",
19
+ "error.400.field.blank": "Поле «{field}» не может быть пустым",
20
+ "error.400.field.does_not_exist": "«{field}» не существует",
21
+ "error.400.field.invalid": "Поле «{field}» содержит недопустимое значение",
22
+ "error.400.field.invalid_choice": "Недопустимый вариант для поля «{field}»",
23
+ "error.400.field.max_length": "Поле «{field}» должно содержать не более {max_length} символов",
24
+ "error.400.field.max_value": "Значение поля «{field}» должно быть не больше {max_value}",
25
+ "error.400.field.min_length": "Поле «{field}» должно содержать не менее {min_length} символов",
26
+ "error.400.field.min_value": "Значение поля «{field}» должно быть не меньше {min_value}",
27
+ "error.400.field.null": "Поле «{field}» не может быть null",
28
+ "error.400.field.required": "Поле «{field}» обязательно",
29
+ "error.400.field.unique": "Значение поля «{field}» должно быть уникальным",
30
+ "error.400.invalid_ad_id": "Недопустимый идентификатор объявления",
31
+ "error.400.search_bad_cursor": "Некорректный курсор постраничной навигации",
32
+ "error.400.search_bad_geo": "Некорректный географический фильтр: {reason}",
33
+ "error.400.search_bad_range": "Некорректный диапазон «{slug}»: {reason}",
34
+ "error.400.search_query_too_long": "Слишком длинный поисковый запрос",
35
+ "error.400.search_sort_needs_center": "Сортировка по расстоянию требует координат (lat и lon)",
36
+ "error.400.search_too_many_facets": "Слишком много фильтров-фасетов (предел {limit})",
37
+ "error.400.search_too_many_ranges": "Слишком много диапазонных фильтров (предел {limit})",
38
+ "error.400.search_unknown_doc_type": "Неизвестный тип поиска «{doc_type}»",
39
+ "error.400.search_unknown_sort": "Неизвестная сортировка «{sort}»",
40
+ "error.400.search_window_exceeded": "Эта страница выдачи выходит за предел в {window} результатов. Уточните запрос вместо перелистывания вглубь.",
41
+ "error.400.validation_error": "Ошибка валидации",
42
+ "error.400.verification_failed": "Проверка не пройдена",
43
+ "error.400.verification_invalid_factor": "Этот способ подтверждения недоступен",
44
+ "error.401.unauthorized": "Требуется аутентификация",
45
+ "error.402.payment_required": "Требуется оплата",
46
+ "error.403.forbidden": "У вас нет прав для выполнения этого действия",
47
+ "error.403.network_blocked": "Запросы из этой сети не разрешены.",
48
+ "error.403.search_forbidden": "У вас нет прав управлять поисковым индексом",
49
+ "error.403.verification_enrollment_required": "Требуется регистрация фактора подтверждения.",
50
+ "error.403.verification_required": "Требуется дополнительная проверка",
51
+ "error.404.ad_not_found": "Объявление не найдено",
52
+ "error.404.not_found": "Запрошенный ресурс не найден",
53
+ "error.404.verification_challenge_not_found": "Запрос на подтверждение не найден или истёк",
54
+ "error.405.method_not_allowed": "Метод не разрешён",
55
+ "error.406.not_acceptable": "Недопустимый формат ответа",
56
+ "error.408.request_timeout": "Время ожидания запроса истекло",
57
+ "error.409.conflict": "Ресурс уже существует",
58
+ "error.410.gone": "Ресурс был безвозвратно удалён",
59
+ "error.413.payload_too_large": "Тело запроса слишком большое",
60
+ "error.415.unsupported_media_type": "Неподдерживаемый тип данных",
61
+ "error.422.unprocessable_entity": "Невозможно обработать данные запроса",
62
+ "error.423.locked": "Ресурс заблокирован",
63
+ "error.423.verification_locked": "Слишком много неудачных попыток — подтверждение заблокировано",
64
+ "error.429.rate_limit": "Слишком много попыток. Повторите попытку через {retry_after_minutes} мин.",
65
+ "error.429.too_many_requests": "Слишком много запросов. Пожалуйста, повторите попытку позже.",
66
+ "error.500.internal": "Что-то пошло не так",
67
+ "error.503.mandate_unavailable": "Не удалось проверить доступ к рабочему пространству",
68
+ "error.503.search_backend_unavailable": "Поисковый движок недоступен",
69
+ };
@@ -0,0 +1,220 @@
1
+ import type { I18nDictionary, I18nEngine } from "@stapel/core";
2
+ import { searchErrorBundleEn } from "./generated/errors.gen.js";
3
+
4
+ /**
5
+ * search-react's own translation KEYS (frontend-standard §4.2): headless
6
+ * components never render literal strings — hosts resolve these via core's
7
+ * i18n engine (`useT`). Backend error codes flow through the SAME contour: a
8
+ * `StapelApiError.code` is already a key, so the default bundle below ships
9
+ * English fallbacks for the 54 backend error codes (generated) plus the
10
+ * pair's own UI keys. All UI keys live under the `search.` namespace.
11
+ *
12
+ * ── What is NOT an i18n key here ───────────────────────────────────────────
13
+ *
14
+ * A FACET VALUE is not one. `facets.py` returns `{value: count}` — index
15
+ * terms, not keys. Their captions come from the category's feature schema
16
+ * (`config.options[].label`, which IS a key when `translatable_options` is
17
+ * on) and are resolved by `facetOptionLabel` through
18
+ * `@stapel/attributes-react`, i.e. through the same formatter the card and
19
+ * the spec table use. Nothing in this file names a catalogue value, because
20
+ * a catalogue is a deployment's content, not a library's chrome.
21
+ */
22
+ export const SEARCH_I18N_KEYS = {
23
+ unknownError: "search.error.unknown",
24
+
25
+ // ── results ──────────────────────────────────────────────────────────────
26
+ resultsTitle: "search.results.title",
27
+ resultsLoading: "search.results.loading",
28
+ resultsLoadFailed: "search.results.load_failed",
29
+ resultsEmpty: "search.results.empty",
30
+ resultsRetry: "search.results.retry",
31
+ /** "About N results" — the sentence for an ESTIMATED count. */
32
+ resultsCountApproximate: "search.results.count_approximate",
33
+ /** "N results" — only when `exact_total` is true. */
34
+ resultsCountExact: "search.results.count_exact",
35
+ resultsTookMs: "search.results.took_ms",
36
+ resultsNext: "search.results.next",
37
+ resultsPrev: "search.results.prev",
38
+ resultsBlockedAtEnd: "search.results.blocked.at_end",
39
+ resultsBlockedAtStart: "search.results.blocked.at_start",
40
+ /** The refusal that must NOT read as "nothing found". */
41
+ resultsWindowExceeded: "search.results.window_exceeded",
42
+ resultsPromoted: "search.results.promoted",
43
+ resultsPromotedHint: "search.results.promoted_hint",
44
+ resultsDistanceKm: "search.results.distance_km",
45
+ resultsUntitled: "search.results.untitled",
46
+ resultsOpen: "search.results.open",
47
+
48
+ // ── sort ─────────────────────────────────────────────────────────────────
49
+ sortLabel: "search.sort.label",
50
+ sortRelevance: "search.sort.relevance",
51
+ sortNewest: "search.sort.newest",
52
+ sortPriceAsc: "search.sort.price_asc",
53
+ sortPriceDesc: "search.sort.price_desc",
54
+ sortDistance: "search.sort.distance",
55
+ sortServerChose: "search.sort.server_chose",
56
+
57
+ // ── facets ───────────────────────────────────────────────────────────────
58
+ facetsTitle: "search.facets.title",
59
+ facetsLoading: "search.facets.loading",
60
+ facetsLoadFailed: "search.facets.load_failed",
61
+ facetsEmpty: "search.facets.empty",
62
+ facetsClear: "search.facets.clear",
63
+ facetsClearAll: "search.facets.clear_all",
64
+ facetsApproximate: "search.facets.approximate",
65
+ facetsSkipped: "search.facets.skipped",
66
+ /** Shown INSTEAD of a count for a slug the server did not count. */
67
+ facetsNotCounted: "search.facets.not_counted",
68
+ facetsDrillDownHint: "search.facets.drill_down_hint",
69
+ facetsRangeFrom: "search.facets.range_from",
70
+ facetsRangeTo: "search.facets.range_to",
71
+ facetsRangeApply: "search.facets.range_apply",
72
+
73
+ // ── geo ──────────────────────────────────────────────────────────────────
74
+ geoTitle: "search.geo.title",
75
+ geoRadiusKm: "search.geo.radius_km",
76
+ geoClear: "search.geo.clear",
77
+ geoBox: "search.geo.box",
78
+ geoCenter: "search.geo.center",
79
+
80
+ // ── the URL that could not be read ───────────────────────────────────────
81
+ urlIssuesTitle: "search.url.issues_title",
82
+ urlIssueNotANumber: "search.url.issue.not_a_number",
83
+ urlIssueGeoIncomplete: "search.url.issue.geo_incomplete",
84
+ urlIssueBboxMalformed: "search.url.issue.bbox_malformed",
85
+ urlIssueRangeMalformed: "search.url.issue.range_malformed",
86
+
87
+ // ── degradations (the envelope's `degraded[]`) ───────────────────────────
88
+ degradedTitle: "search.degraded.title",
89
+ degradedTypoTolerance: "search.degraded.typo_tolerance",
90
+ degradedPhraseSynonyms: "search.degraded.phrase_synonyms",
91
+ degradedExactTotal: "search.degraded.exact_total",
92
+ degradedExactFacetCounts: "search.degraded.exact_facet_counts",
93
+ degradedCategoryRollup: "search.degraded.category_rollup",
94
+ degradedScorer: "search.degraded.scorer",
95
+ degradedUnknown: "search.degraded.unknown",
96
+
97
+ // ── ranking disclosure (P2B Art. 5) ──────────────────────────────────────
98
+ rankingTitle: "search.ranking.title",
99
+ rankingIntro: "search.ranking.intro",
100
+ rankingLoading: "search.ranking.loading",
101
+ rankingLoadFailed: "search.ranking.load_failed",
102
+ rankingEmpty: "search.ranking.empty",
103
+ rankingParameter: "search.ranking.parameter",
104
+ rankingWeight: "search.ranking.weight",
105
+ rankingAppliesTo: "search.ranking.applies_to",
106
+ rankingInactive: "search.ranking.inactive",
107
+ rankingNotes: "search.ranking.notes",
108
+ rankingLink: "search.ranking.link",
109
+ } as const;
110
+
111
+ export type SearchI18nKey =
112
+ (typeof SEARCH_I18N_KEYS)[keyof typeof SEARCH_I18N_KEYS];
113
+
114
+ /**
115
+ * English fallback bundle for search-react UI keys + backend error codes.
116
+ * The generated backend texts are spread FIRST so coverage of the error
117
+ * registry is by construction; the pair's own copy follows.
118
+ */
119
+ export const searchI18nBundleEn: Record<string, string> = {
120
+ ...searchErrorBundleEn,
121
+
122
+ "search.error.unknown": "Something went wrong with the search",
123
+
124
+ "search.results.title": "Results",
125
+ "search.results.loading": "Searching…",
126
+ "search.results.load_failed": "We could not run this search",
127
+ "search.results.empty": "Nothing matches this search",
128
+ "search.results.retry": "Try again",
129
+ "search.results.count_approximate": "About {count} results",
130
+ "search.results.count_exact": "{count} results",
131
+ "search.results.took_ms": "{ms} ms",
132
+ "search.results.next": "Next page",
133
+ "search.results.prev": "Previous page",
134
+ "search.results.blocked.at_end": "This is the last page",
135
+ "search.results.blocked.at_start": "This is the first page",
136
+ "search.results.window_exceeded":
137
+ "This page is deeper than the search can go. Narrow the search instead of paging further.",
138
+ "search.results.promoted": "Promoted",
139
+ "search.results.promoted_hint":
140
+ "This placement is paid for. It is marked because the law requires it, and it does not change what the other results are.",
141
+ "search.results.distance_km": "{km} km away",
142
+ "search.results.untitled": "Untitled",
143
+ "search.results.open": "Open",
144
+
145
+ "search.sort.label": "Sort",
146
+ "search.sort.relevance": "Most relevant",
147
+ "search.sort.newest": "Newest first",
148
+ "search.sort.price_asc": "Price: low to high",
149
+ "search.sort.price_desc": "Price: high to low",
150
+ "search.sort.distance": "Nearest first",
151
+ "search.sort.server_chose": "Sorted by {sort}",
152
+
153
+ "search.facets.title": "Filters",
154
+ "search.facets.loading": "Loading filters…",
155
+ "search.facets.load_failed": "We could not load the filters",
156
+ "search.facets.empty": "This search offers no filters",
157
+ "search.facets.clear": "Clear",
158
+ "search.facets.clear_all": "Clear all filters ({count})",
159
+ "search.facets.approximate":
160
+ "Counts are approximate — there were too many candidates to count them all.",
161
+ "search.facets.skipped":
162
+ "These filters were not counted for this search: {slugs}",
163
+ "search.facets.not_counted": "not counted",
164
+ "search.facets.drill_down_hint":
165
+ "Each count is what you would get by choosing that value instead of the one you have.",
166
+ "search.facets.range_from": "From",
167
+ "search.facets.range_to": "To",
168
+ "search.facets.range_apply": "Apply",
169
+
170
+ "search.geo.title": "Location",
171
+ "search.geo.radius_km": "Within {km} km",
172
+ "search.geo.clear": "Anywhere",
173
+ "search.geo.box": "Inside the shown area",
174
+ "search.geo.center": "Around {lat}, {lon}",
175
+
176
+ "search.url.issues_title": "Part of this link could not be read",
177
+ "search.url.issue.not_a_number": "{param} is not a number and was ignored",
178
+ "search.url.issue.geo_incomplete":
179
+ "the location needs both lat and lon, so it was ignored",
180
+ "search.url.issue.bbox_malformed":
181
+ "the area needs four numbers (minLat,minLon,maxLat,maxLon) and was ignored",
182
+ "search.url.issue.range_malformed":
183
+ "{param} is not a from..to range and was ignored",
184
+
185
+ "search.degraded.title": "What this search could not do",
186
+ "search.degraded.typo_tolerance":
187
+ "Typos were not corrected — the search engine in use cannot do it.",
188
+ "search.degraded.phrase_synonyms":
189
+ "Synonyms were not expanded — the search engine in use cannot do it.",
190
+ "search.degraded.exact_total": "The number of results is an estimate.",
191
+ "search.degraded.exact_facet_counts": "The filter counts are approximate.",
192
+ "search.degraded.category_rollup":
193
+ "Subcategories may be missing from these results — the category service did not answer.",
194
+ "search.degraded.scorer":
195
+ "The ranking parameter “{scorer}” was not applied — the search engine in use cannot evaluate it.",
196
+ "search.degraded.unknown":
197
+ "The search engine reported a limitation this page has no wording for: {raw}",
198
+
199
+ "search.ranking.title": "How these results are ordered",
200
+ "search.ranking.intro":
201
+ "These are the parameters that determine the order of the results, with their relative weight.",
202
+ "search.ranking.loading": "Loading the ranking disclosure…",
203
+ "search.ranking.load_failed": "We could not load the ranking disclosure",
204
+ "search.ranking.empty": "This deployment declares no ranking parameters",
205
+ "search.ranking.parameter": "Parameter",
206
+ "search.ranking.weight": "Weight",
207
+ "search.ranking.applies_to": "Applies to",
208
+ "search.ranking.inactive": "Not applied: {reason}",
209
+ "search.ranking.notes": "Notes",
210
+ "search.ranking.link": "How these results are ordered",
211
+ };
212
+
213
+ /**
214
+ * Register the English bundle into a core i18n engine. Locale bundles ship as
215
+ * opt-in subpaths (`@stapel/search-react/i18n/ru`, `…/es`) so a host that
216
+ * needs only English never carries them.
217
+ */
218
+ export function registerSearchI18n(i18n: I18nEngine): void {
219
+ i18n.registerBundle("en", searchI18nBundleEn as I18nDictionary);
220
+ }
package/src/i18n/ru.ts ADDED
@@ -0,0 +1,126 @@
1
+ import type { I18nDictionary, I18nEngine } from "@stapel/core";
2
+ import { searchErrorBundleRu } from "./generated/errors.ru.gen.js";
3
+
4
+ export { searchErrorBundleRu } from "./generated/errors.ru.gen.js";
5
+
6
+ /**
7
+ * Russian bundle for search-react — the pair's `ru` locale, shipped as the
8
+ * `@stapel/search-react/i18n/ru` subpath (i18n-shipping.md §2) so the locale
9
+ * is opt-in: hosts that don't register it never carry these strings (the main
10
+ * entry does not import this module — gated by size-limit + the bundle-purity
11
+ * test).
12
+ *
13
+ * This is the storefront's DEFAULT language (storefront spec verdict F1:
14
+ * ru-first), which is why it is the fullest of the three bundles and why the
15
+ * wording is checked against what a marketplace visitor is actually being
16
+ * told — "counts are approximate" has to sound like an explanation, not an
17
+ * apology.
18
+ *
19
+ * Composition mirrors `searchI18nBundleEn`: the GENERATED backend error texts
20
+ * (stapel-search's `translations/errors.ru.json` merged under stapel-core's
21
+ * cross-cutting catalogue — `pnpm gen:errors`) are spread first for coverage
22
+ * by construction; the ru UI copy follows.
23
+ *
24
+ * PROVENANCE, stated rather than implied: the backend catalogues ship
25
+ * `origin=seed:authored` and are UNREVIEWED; the pair-authored strings below
26
+ * are the same grade. Neither is a claim of review.
27
+ */
28
+ export const searchI18nBundleRu: I18nDictionary = {
29
+ ...searchErrorBundleRu,
30
+
31
+ "search.error.unknown": "Что-то пошло не так при поиске.",
32
+
33
+ "search.results.title": "Результаты",
34
+ "search.results.loading": "Ищем…",
35
+ "search.results.load_failed": "Не удалось выполнить поиск.",
36
+ "search.results.empty": "По этому запросу ничего не нашлось.",
37
+ "search.results.retry": "Повторить",
38
+ "search.results.count_approximate": "Примерно {count} объявлений",
39
+ "search.results.count_exact": "{count} объявлений",
40
+ "search.results.took_ms": "{ms} мс",
41
+ "search.results.next": "Следующая страница",
42
+ "search.results.prev": "Предыдущая страница",
43
+ "search.results.blocked.at_end": "Это последняя страница",
44
+ "search.results.blocked.at_start": "Это первая страница",
45
+ "search.results.window_exceeded":
46
+ "Эта страница глубже, чем поиск умеет выдавать. Уточните запрос вместо перелистывания.",
47
+ "search.results.promoted": "Продвигается",
48
+ "search.results.promoted_hint":
49
+ "Это платное размещение. Мы помечаем его, потому что так требует закон; на остальные результаты это не влияет.",
50
+ "search.results.distance_km": "{km} км от вас",
51
+ "search.results.untitled": "Без названия",
52
+ "search.results.open": "Открыть",
53
+
54
+ "search.sort.label": "Сортировка",
55
+ "search.sort.relevance": "По релевантности",
56
+ "search.sort.newest": "Сначала новые",
57
+ "search.sort.price_asc": "Сначала дешевле",
58
+ "search.sort.price_desc": "Сначала дороже",
59
+ "search.sort.distance": "Сначала ближние",
60
+ "search.sort.server_chose": "Отсортировано: {sort}",
61
+
62
+ "search.facets.title": "Фильтры",
63
+ "search.facets.loading": "Загружаем фильтры…",
64
+ "search.facets.load_failed": "Не удалось загрузить фильтры.",
65
+ "search.facets.empty": "Для этого поиска фильтров нет.",
66
+ "search.facets.clear": "Сбросить",
67
+ "search.facets.clear_all": "Сбросить все фильтры ({count})",
68
+ "search.facets.approximate":
69
+ "Счётчики приблизительные — подходящих объявлений слишком много, чтобы пересчитать все.",
70
+ "search.facets.skipped":
71
+ "Эти фильтры для текущего поиска не посчитаны: {slugs}",
72
+ "search.facets.not_counted": "не посчитано",
73
+ "search.facets.drill_down_hint":
74
+ "Рядом с каждым значением — сколько будет, если выбрать его вместо текущего.",
75
+ "search.facets.range_from": "От",
76
+ "search.facets.range_to": "До",
77
+ "search.facets.range_apply": "Применить",
78
+
79
+ "search.geo.title": "Где искать",
80
+ "search.geo.radius_km": "В радиусе {km} км",
81
+ "search.geo.clear": "Везде",
82
+ "search.geo.box": "В показанной области",
83
+ "search.geo.center": "Рядом с {lat}, {lon}",
84
+
85
+ "search.url.issues_title": "Часть этой ссылки прочитать не удалось",
86
+ "search.url.issue.not_a_number": "{param} — не число, параметр пропущен",
87
+ "search.url.issue.geo_incomplete":
88
+ "для точки нужны и lat, и lon — параметр пропущен",
89
+ "search.url.issue.bbox_malformed":
90
+ "для области нужны четыре числа (minLat,minLon,maxLat,maxLon) — параметр пропущен",
91
+ "search.url.issue.range_malformed":
92
+ "{param} — не диапазон вида from..to, параметр пропущен",
93
+
94
+ "search.degraded.title": "Что этот поиск не смог сделать",
95
+ "search.degraded.typo_tolerance":
96
+ "Опечатки не исправлялись — используемый поисковый движок этого не умеет.",
97
+ "search.degraded.phrase_synonyms":
98
+ "Синонимы не подставлялись — используемый поисковый движок этого не умеет.",
99
+ "search.degraded.exact_total": "Число результатов — оценка, а не точный счёт.",
100
+ "search.degraded.exact_facet_counts": "Счётчики фильтров приблизительные.",
101
+ "search.degraded.category_rollup":
102
+ "В выдаче могут отсутствовать подкатегории — сервис категорий не ответил.",
103
+ "search.degraded.scorer":
104
+ "Параметр ранжирования «{scorer}» не применялся — используемый движок не умеет его считать.",
105
+ "search.degraded.unknown":
106
+ "Поиск сообщил об ограничении, для которого у этой страницы нет формулировки: {raw}",
107
+
108
+ "search.ranking.title": "Как упорядочены результаты",
109
+ "search.ranking.intro":
110
+ "Ниже — параметры, которые определяют порядок выдачи, и их относительный вес.",
111
+ "search.ranking.loading": "Загружаем раскрытие ранжирования…",
112
+ "search.ranking.load_failed": "Не удалось загрузить раскрытие ранжирования.",
113
+ "search.ranking.empty": "В этой установке параметры ранжирования не объявлены.",
114
+ "search.ranking.parameter": "Параметр",
115
+ "search.ranking.weight": "Вес",
116
+ "search.ranking.applies_to": "Применяется к",
117
+ "search.ranking.inactive": "Не применяется: {reason}",
118
+ "search.ranking.notes": "Примечания",
119
+ "search.ranking.link": "Как упорядочены результаты",
120
+ };
121
+
122
+ /** Register the ru bundle. Call AFTER `registerSearchI18n` so it layers over
123
+ * the en floor (merge priority = registration order). */
124
+ export function registerSearchI18nRu(engine: I18nEngine): void {
125
+ engine.registerBundle("ru", searchI18nBundleRu);
126
+ }
package/src/index.ts ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * `@stapel/search-react` — the headless React pair for stapel-search
3
+ * (frontend-standard §2). Business + state only, zero visual opinion; the
4
+ * antd skin lives behind the `./default` subpath and the react-router binding
5
+ * behind `./router`, so a host that renders its own visuals carries neither.
6
+ *
7
+ * ── The one-liner ──────────────────────────────────────────────────────────
8
+ *
9
+ * ```tsx
10
+ * const runtime = createSearchRuntime({ baseUrl: "/search/api/v1/" });
11
+ * <SearchProvider runtime={runtime}>
12
+ * <SearchPage adapter={useRouterSearchParams()} defaultType="listing" />
13
+ * </SearchProvider>
14
+ * ```
15
+ *
16
+ * No session, no workspace id, no auth client: every endpoint this pair calls
17
+ * is `AllowAny`, so a storefront's catalogue renders for a visitor who will
18
+ * never sign in.
19
+ *
20
+ * ── The three properties this pair exists to guarantee ─────────────────────
21
+ *
22
+ * 1. **The URL is the state.** Text, category, facet filters, ranges, geo,
23
+ * sort, page size and the keyset cursor all live in the query string,
24
+ * under the BACKEND's own parameter names, and no component keeps a second
25
+ * copy. Copying the address into another tab reproduces the page; Back
26
+ * removes the last filter; a reload loses nothing. See `state/urlState.ts`.
27
+ * 2. **The server's honesty survives to the screen.** `facet_meta.approximate`,
28
+ * `facet_meta.skipped`, `exact_total` and `degraded[]` are parsed and
29
+ * surfaced, never swallowed. A skipped facet's options carry `count: null`,
30
+ * not `0` — "we did not count this" and "there are none" are different
31
+ * sentences.
32
+ * 3. **`promoted` is carried, not optional.** DSA Art. 26 marking rides every
33
+ * item under every sort; the card slot receives the whole item and the
34
+ * default card renders the tag. The P2B Art. 5 ranking disclosure ships as
35
+ * a headless bag and a page.
36
+ *
37
+ * Layers: api → model → state → headless → i18n. Generated surfaces (the
38
+ * typed schema, the error map, the manifest, the nav manifest, llms.txt) are
39
+ * produced by the monorepo `gen:*` drivers from stapel-search's own `docs/`
40
+ * artifacts and stand under drift gates.
41
+ */
42
+
43
+ // ── api ──────────────────────────────────────────────────────────────────────
44
+ export { createSearchApi, searchQueryParams } from "./api/searchApi.js";
45
+ export type { SearchApi } from "./api/searchApi.js";
46
+ export type { Schemas } from "./api/types.js";
47
+ export { SEARCH_SORTS } from "./api/types.js";
48
+ export type {
49
+ FacetMeta,
50
+ FacetSelection,
51
+ RankingResponse,
52
+ Scorer,
53
+ SearchDegradation,
54
+ SearchDegradationKind,
55
+ SearchGeo,
56
+ SearchGeoBox,
57
+ SearchGeoCenter,
58
+ SearchItem,
59
+ SearchQueryState,
60
+ SearchRange,
61
+ SearchResponse,
62
+ SuggestParams,
63
+ SuggestResponse,
64
+ } from "./api/types.js";
65
+
66
+ // ── state (pure: no React, no router) ────────────────────────────────────────
67
+ export {
68
+ FILTER_PREFIX,
69
+ RANGE_PREFIX,
70
+ SEARCH_PARAM,
71
+ activeFilterCount,
72
+ clearFilters,
73
+ ownsParam,
74
+ parseSearchState,
75
+ patchSearchState,
76
+ setFilterValues,
77
+ setRangeValue,
78
+ toggleFilterValue,
79
+ writeSearchState,
80
+ } from "./state/urlState.js";
81
+ export type {
82
+ ParseSearchStateOptions,
83
+ ParsedSearchState,
84
+ SearchStateIssue,
85
+ SearchStateIssueCode,
86
+ SearchStatePatch,
87
+ } from "./state/urlState.js";
88
+
89
+ export { countIsEstimate, degradationMessageKey, parseDegradations } from "./state/degradations.js";
90
+
91
+ export { buildFacetGroups, facetOptionLabel } from "./state/facets.js";
92
+ export type {
93
+ BuildFacetGroupsInput,
94
+ FacetGroup,
95
+ FacetOption,
96
+ } from "./state/facets.js";
97
+
98
+ // ── model (runtime wiring, query keys, context, hooks) ───────────────────────
99
+ export { createSearchRuntime } from "./model/runtime.js";
100
+ export type {
101
+ SearchRuntime,
102
+ CreateSearchRuntimeOptions,
103
+ } from "./model/runtime.js";
104
+ export {
105
+ SearchRuntimeContext,
106
+ useSearchRuntime,
107
+ useSearchApi,
108
+ useSearchAnalytics,
109
+ } from "./model/context.js";
110
+ export { searchQueryKeys } from "./model/queryKeys.js";
111
+ export type { SearchQueryKeyParams } from "./model/queryKeys.js";
112
+
113
+ export { useRankingDisclosure, useSearchQuery } from "./model/queries.js";
114
+
115
+ // ── headless (renderless components) ─────────────────────────────────────────
116
+ export { SearchProvider } from "./headless/SearchProvider.js";
117
+ export { SearchStateProvider, useSearchState } from "./headless/SearchStateProvider.js";
118
+ export type {
119
+ SearchParamsAdapter,
120
+ SearchStateBag,
121
+ SearchStateProviderProps,
122
+ } from "./headless/SearchStateProvider.js";
123
+ export { SearchResults } from "./headless/SearchResults.js";
124
+ export type { SearchPageInfo, SearchResultsBag } from "./headless/SearchResults.js";
125
+ export { FacetPanel } from "./headless/FacetPanel.js";
126
+ export type { FacetPanelBag } from "./headless/FacetPanel.js";
127
+ export { RankingDisclosure } from "./headless/RankingDisclosure.js";
128
+ export type { RankingDisclosureBag } from "./headless/RankingDisclosure.js";
129
+
130
+ // ── nav manifest (the pair's public surface declaration) ─────────────────────
131
+ export { navEntries } from "./nav/manifest.js";
132
+
133
+ // ── i18n ─────────────────────────────────────────────────────────────────────
134
+ export {
135
+ SEARCH_I18N_KEYS,
136
+ searchI18nBundleEn,
137
+ registerSearchI18n,
138
+ } from "./i18n/keys.js";
139
+ export type { SearchI18nKey } from "./i18n/keys.js";
140
+
141
+ // ── errors map (code → status/params/remediation/en; generated) ──────────────
142
+ export {
143
+ SEARCH_ERRORS,
144
+ SEARCH_ERROR_CODES,
145
+ SEARCH_BACKEND_UNAVAILABLE,
146
+ SEARCH_WINDOW_EXCEEDED,
147
+ searchErrorBundleEn,
148
+ explainSearchError,
149
+ } from "./i18n/errorsMap.js";
150
+ export type {
151
+ SearchErrorCode,
152
+ SearchErrorSpec,
153
+ Remediation,
154
+ } from "./i18n/errorsMap.js";
@@ -0,0 +1,28 @@
1
+ import type { Context } from "react";
2
+ import { createModuleContext } from "@stapel/core";
3
+ import type { Analytics, ModuleContextKit } from "@stapel/core";
4
+ import type { SearchApi } from "../api/searchApi.js";
5
+ import type { SearchRuntime } from "./runtime.js";
6
+
7
+ /**
8
+ * The wired SearchRuntime shared through React context by
9
+ * `<SearchProvider>`. Hooks in `model/` and `headless/` read the singletons
10
+ * from here. One reviewed copy of this plumbing lives in `@stapel/core`
11
+ * (`createModuleContext`, slim wave §21/S2); this module binds it under the
12
+ * pair's public names.
13
+ */
14
+ const kit: ModuleContextKit<SearchRuntime> =
15
+ createModuleContext<SearchRuntime>("Search");
16
+
17
+ export const SearchRuntimeContext: Context<SearchRuntime | null> =
18
+ kit.RuntimeContext;
19
+
20
+ export const useSearchRuntime: () => SearchRuntime = kit.useRuntime;
21
+
22
+ export const useSearchApi: () => SearchApi = kit.useApi;
23
+
24
+ export const useSearchAnalytics: () => Analytics | null = kit.useAnalytics;
25
+
26
+ /** @internal Re-exported as `<SearchProvider>` from `headless/`. */
27
+ export const ModuleProvider: ModuleContextKit<SearchRuntime>["Provider"] =
28
+ kit.Provider;