@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.
- package/CHANGELOG.md +1 -0
- package/MODULE.md +203 -0
- package/README.md +175 -0
- package/dist/api/generated/schema.d.ts +375 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/searchApi.d.ts +77 -0
- package/dist/api/searchApi.d.ts.map +1 -0
- package/dist/api/searchApi.js +79 -0
- package/dist/api/searchApi.js.map +1 -0
- package/dist/api/types.d.ts +131 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +20 -0
- package/dist/api/types.js.map +1 -0
- package/dist/default/DegradationNotice.d.ts +19 -0
- package/dist/default/DegradationNotice.d.ts.map +1 -0
- package/dist/default/DegradationNotice.js +14 -0
- package/dist/default/DegradationNotice.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +9 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +27 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/FacetPanelPane.d.ts +26 -0
- package/dist/default/FacetPanelPane.d.ts.map +1 -0
- package/dist/default/FacetPanelPane.js +32 -0
- package/dist/default/FacetPanelPane.js.map +1 -0
- package/dist/default/RankingDisclosurePane.d.ts +17 -0
- package/dist/default/RankingDisclosurePane.d.ts.map +1 -0
- package/dist/default/RankingDisclosurePane.js +23 -0
- package/dist/default/RankingDisclosurePane.js.map +1 -0
- package/dist/default/SearchPage.d.ts +34 -0
- package/dist/default/SearchPage.d.ts.map +1 -0
- package/dist/default/SearchPage.js +16 -0
- package/dist/default/SearchPage.js.map +1 -0
- package/dist/default/SearchResultCard.d.ts +34 -0
- package/dist/default/SearchResultCard.d.ts.map +1 -0
- package/dist/default/SearchResultCard.js +38 -0
- package/dist/default/SearchResultCard.js.map +1 -0
- package/dist/default/SearchResultsPane.d.ts +32 -0
- package/dist/default/SearchResultsPane.d.ts.map +1 -0
- package/dist/default/SearchResultsPane.js +46 -0
- package/dist/default/SearchResultsPane.js.map +1 -0
- package/dist/default/SortSelect.d.ts +22 -0
- package/dist/default/SortSelect.d.ts.map +1 -0
- package/dist/default/SortSelect.js +39 -0
- package/dist/default/SortSelect.js.map +1 -0
- package/dist/default/UrlIssueNotice.d.ts +12 -0
- package/dist/default/UrlIssueNotice.d.ts.map +1 -0
- package/dist/default/UrlIssueNotice.js +13 -0
- package/dist/default/UrlIssueNotice.js.map +1 -0
- package/dist/default/index.d.ts +40 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +34 -0
- package/dist/default/index.js.map +1 -0
- package/dist/default/theme.d.ts +24 -0
- package/dist/default/theme.d.ts.map +1 -0
- package/dist/default/theme.js +39 -0
- package/dist/default/theme.js.map +1 -0
- package/dist/default/types.d.ts +12 -0
- package/dist/default/types.d.ts.map +1 -0
- package/dist/default/types.js +2 -0
- package/dist/default/types.js.map +1 -0
- package/dist/headless/FacetPanel.d.ts +61 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -0
- package/dist/headless/FacetPanel.js +57 -0
- package/dist/headless/FacetPanel.js.map +1 -0
- package/dist/headless/RankingDisclosure.d.ts +39 -0
- package/dist/headless/RankingDisclosure.d.ts.map +1 -0
- package/dist/headless/RankingDisclosure.js +28 -0
- package/dist/headless/RankingDisclosure.js.map +1 -0
- package/dist/headless/SearchProvider.d.ts +19 -0
- package/dist/headless/SearchProvider.d.ts.map +1 -0
- package/dist/headless/SearchProvider.js +15 -0
- package/dist/headless/SearchProvider.js.map +1 -0
- package/dist/headless/SearchResults.d.ts +68 -0
- package/dist/headless/SearchResults.d.ts.map +1 -0
- package/dist/headless/SearchResults.js +70 -0
- package/dist/headless/SearchResults.js.map +1 -0
- package/dist/headless/SearchStateProvider.d.ts +69 -0
- package/dist/headless/SearchStateProvider.d.ts.map +1 -0
- package/dist/headless/SearchStateProvider.js +71 -0
- package/dist/headless/SearchStateProvider.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +27 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +37 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +13 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +85 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +9 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +63 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +359 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +183 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +9 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +63 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +107 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +185 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +29 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +101 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/queries.d.ts +48 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +65 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +25 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +22 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/runtime.d.ts +25 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +6 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/nav/manifest.d.ts +21 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +34 -0
- package/dist/nav/manifest.js.map +1 -0
- package/dist/router/index.d.ts +10 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +34 -0
- package/dist/router/index.js.map +1 -0
- package/dist/state/degradations.d.ts +30 -0
- package/dist/state/degradations.d.ts.map +1 -0
- package/dist/state/degradations.js +52 -0
- package/dist/state/degradations.js.map +1 -0
- package/dist/state/facets.d.ts +68 -0
- package/dist/state/facets.d.ts.map +1 -0
- package/dist/state/facets.js +171 -0
- package/dist/state/facets.js.map +1 -0
- package/dist/state/urlState.d.ts +143 -0
- package/dist/state/urlState.d.ts.map +1 -0
- package/dist/state/urlState.js +319 -0
- package/dist/state/urlState.js.map +1 -0
- package/llms.txt +87 -0
- package/manifest.json +652 -0
- package/nav-manifest.json +44 -0
- package/package.json +133 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/generated/schema.ts +377 -0
- package/src/api/searchApi.ts +169 -0
- package/src/api/types.ts +160 -0
- package/src/default/DegradationNotice.tsx +50 -0
- package/src/default/ErrorAlert.tsx +46 -0
- package/src/default/FacetPanelPane.tsx +174 -0
- package/src/default/RankingDisclosurePane.tsx +116 -0
- package/src/default/SearchPage.tsx +87 -0
- package/src/default/SearchResultCard.tsx +96 -0
- package/src/default/SearchResultsPane.tsx +173 -0
- package/src/default/SortSelect.tsx +77 -0
- package/src/default/UrlIssueNotice.tsx +37 -0
- package/src/default/index.ts +48 -0
- package/src/default/theme.tsx +66 -0
- package/src/default/types.ts +12 -0
- package/src/headless/FacetPanel.tsx +113 -0
- package/src/headless/RankingDisclosure.tsx +58 -0
- package/src/headless/SearchProvider.tsx +20 -0
- package/src/headless/SearchResults.tsx +149 -0
- package/src/headless/SearchStateProvider.tsx +175 -0
- package/src/i18n/errorsMap.ts +50 -0
- package/src/i18n/es.ts +110 -0
- package/src/i18n/generated/errors.es.gen.ts +69 -0
- package/src/i18n/generated/errors.gen.ts +214 -0
- package/src/i18n/generated/errors.json +423 -0
- package/src/i18n/generated/errors.ru.gen.ts +69 -0
- package/src/i18n/keys.ts +220 -0
- package/src/i18n/ru.ts +126 -0
- package/src/index.ts +154 -0
- package/src/model/context.tsx +28 -0
- package/src/model/queries.ts +79 -0
- package/src/model/queryKeys.ts +48 -0
- package/src/model/runtime.ts +33 -0
- package/src/nav/manifest.ts +53 -0
- package/src/router/index.ts +38 -0
- package/src/state/degradations.ts +75 -0
- package/src/state/facets.ts +219 -0
- package/src/state/urlState.ts +458 -0
- 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
|
+
};
|
package/src/i18n/keys.ts
ADDED
|
@@ -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;
|