@stapel/search-react 0.1.0 → 0.3.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 +94 -0
- package/LICENSE +21 -0
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +2 -5
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +20 -0
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +26 -7
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/SortSelect.d.ts +2 -1
- package/dist/default/SortSelect.d.ts.map +1 -1
- package/dist/default/SortSelect.js +12 -3
- package/dist/default/SortSelect.js.map +1 -1
- package/dist/default/index.d.ts +3 -1
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +2 -0
- package/dist/default/index.js.map +1 -1
- package/dist/headless/useAppliedSort.d.ts +2 -0
- package/dist/headless/useAppliedSort.d.ts.map +1 -0
- package/dist/headless/useAppliedSort.js +25 -0
- package/dist/headless/useAppliedSort.js.map +1 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +4 -2
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +19 -2
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +26 -4
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +11 -3
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +7 -1
- package/nav-manifest.json +1 -1
- package/package.json +15 -15
- package/src/analytics/generated/events.json +1 -1
- package/src/default/SearchPage.tsx +11 -16
- package/src/default/SearchResultsPane.tsx +68 -16
- package/src/default/SortSelect.tsx +14 -5
- package/src/default/index.ts +6 -1
- package/src/headless/useAppliedSort.ts +25 -0
- package/src/i18n/es.ts +4 -2
- package/src/i18n/keys.ts +27 -4
- package/src/i18n/ru.ts +10 -2
- package/src/index.ts +2 -0
package/dist/i18n/ru.js
CHANGED
|
@@ -7,7 +7,7 @@ export { searchErrorBundleRu } from "./generated/errors.ru.gen.js";
|
|
|
7
7
|
* entry does not import this module — gated by size-limit + the bundle-purity
|
|
8
8
|
* test).
|
|
9
9
|
*
|
|
10
|
-
* This is the storefront's DEFAULT language (
|
|
10
|
+
* This is the storefront's DEFAULT language (storefront spec verdict F1:
|
|
11
11
|
* ru-first), which is why it is the fullest of the three bundles and why the
|
|
12
12
|
* wording is checked against what a marketplace visitor is actually being
|
|
13
13
|
* told — "counts are approximate" has to sound like an explanation, not an
|
|
@@ -30,8 +30,16 @@ export const searchI18nBundleRu = {
|
|
|
30
30
|
"search.results.load_failed": "Не удалось выполнить поиск.",
|
|
31
31
|
"search.results.empty": "По этому запросу ничего не нашлось.",
|
|
32
32
|
"search.results.retry": "Повторить",
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Four forms, because Russian has four (1 / 2-4 / 5-20 / fractions). One
|
|
34
|
+
// string here is wrong on most pages the storefront serves.
|
|
35
|
+
"search.results.count_approximate.one": "Примерно {count} объявление",
|
|
36
|
+
"search.results.count_approximate.few": "Примерно {count} объявления",
|
|
37
|
+
"search.results.count_approximate.many": "Примерно {count} объявлений",
|
|
38
|
+
"search.results.count_approximate.other": "Примерно {count} объявления",
|
|
39
|
+
"search.results.count_exact.one": "{count} объявление",
|
|
40
|
+
"search.results.count_exact.few": "{count} объявления",
|
|
41
|
+
"search.results.count_exact.many": "{count} объявлений",
|
|
42
|
+
"search.results.count_exact.other": "{count} объявления",
|
|
35
43
|
"search.results.took_ms": "{ms} мс",
|
|
36
44
|
"search.results.next": "Следующая страница",
|
|
37
45
|
"search.results.prev": "Предыдущая страница",
|
package/dist/i18n/ru.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ru.js","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,GAAG,mBAAmB;IAEtB,sBAAsB,EAAE,iCAAiC;IAEzD,sBAAsB,EAAE,YAAY;IACpC,wBAAwB,EAAE,OAAO;IACjC,4BAA4B,EAAE,6BAA6B;IAC3D,sBAAsB,EAAE,qCAAqC;IAC7D,sBAAsB,EAAE,WAAW;IACnC,
|
|
1
|
+
{"version":3,"file":"ru.js","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,GAAG,mBAAmB;IAEtB,sBAAsB,EAAE,iCAAiC;IAEzD,sBAAsB,EAAE,YAAY;IACpC,wBAAwB,EAAE,OAAO;IACjC,4BAA4B,EAAE,6BAA6B;IAC3D,sBAAsB,EAAE,qCAAqC;IAC7D,sBAAsB,EAAE,WAAW;IACnC,yEAAyE;IACzE,4DAA4D;IAC5D,sCAAsC,EAAE,6BAA6B;IACrE,sCAAsC,EAAE,6BAA6B;IACrE,uCAAuC,EAAE,6BAA6B;IACtE,wCAAwC,EAAE,6BAA6B;IACvE,gCAAgC,EAAE,oBAAoB;IACtD,gCAAgC,EAAE,oBAAoB;IACtD,iCAAiC,EAAE,oBAAoB;IACvD,kCAAkC,EAAE,oBAAoB;IACxD,wBAAwB,EAAE,SAAS;IACnC,qBAAqB,EAAE,oBAAoB;IAC3C,qBAAqB,EAAE,qBAAqB;IAC5C,+BAA+B,EAAE,wBAAwB;IACzD,iCAAiC,EAAE,qBAAqB;IACxD,gCAAgC,EAC9B,uFAAuF;IACzF,yBAAyB,EAAE,cAAc;IACzC,8BAA8B,EAC5B,+GAA+G;IACjH,4BAA4B,EAAE,gBAAgB;IAC9C,yBAAyB,EAAE,cAAc;IACzC,qBAAqB,EAAE,SAAS;IAEhC,mBAAmB,EAAE,YAAY;IACjC,uBAAuB,EAAE,kBAAkB;IAC3C,oBAAoB,EAAE,eAAe;IACrC,uBAAuB,EAAE,iBAAiB;IAC1C,wBAAwB,EAAE,gBAAgB;IAC1C,sBAAsB,EAAE,iBAAiB;IACzC,0BAA0B,EAAE,uBAAuB;IAEnD,qBAAqB,EAAE,SAAS;IAChC,uBAAuB,EAAE,oBAAoB;IAC7C,2BAA2B,EAAE,+BAA+B;IAC5D,qBAAqB,EAAE,gCAAgC;IACvD,qBAAqB,EAAE,UAAU;IACjC,yBAAyB,EAAE,gCAAgC;IAC3D,2BAA2B,EACzB,wFAAwF;IAC1F,uBAAuB,EACrB,uDAAuD;IACzD,2BAA2B,EAAE,cAAc;IAC3C,+BAA+B,EAC7B,6EAA6E;IAC/E,0BAA0B,EAAE,IAAI;IAChC,wBAAwB,EAAE,IAAI;IAC9B,2BAA2B,EAAE,WAAW;IAExC,kBAAkB,EAAE,YAAY;IAChC,sBAAsB,EAAE,mBAAmB;IAC3C,kBAAkB,EAAE,OAAO;IAC3B,gBAAgB,EAAE,sBAAsB;IACxC,mBAAmB,EAAE,sBAAsB;IAE3C,yBAAyB,EAAE,wCAAwC;IACnE,+BAA+B,EAAE,uCAAuC;IACxE,iCAAiC,EAC/B,kDAAkD;IACpD,iCAAiC,EAC/B,kFAAkF;IACpF,kCAAkC,EAChC,wDAAwD;IAE1D,uBAAuB,EAAE,gCAAgC;IACzD,gCAAgC,EAC9B,0EAA0E;IAC5E,iCAAiC,EAC/B,2EAA2E;IAC7E,6BAA6B,EAAE,+CAA+C;IAC9E,oCAAoC,EAAE,oCAAoC;IAC1E,iCAAiC,EAC/B,0EAA0E;IAC5E,wBAAwB,EACtB,4FAA4F;IAC9F,yBAAyB,EACvB,oFAAoF;IAEtF,sBAAsB,EAAE,4BAA4B;IACpD,sBAAsB,EACpB,8EAA8E;IAChF,wBAAwB,EAAE,mCAAmC;IAC7D,4BAA4B,EAAE,8CAA8C;IAC5E,sBAAsB,EAAE,uDAAuD;IAC/E,0BAA0B,EAAE,UAAU;IACtC,uBAAuB,EAAE,KAAK;IAC9B,2BAA2B,EAAE,eAAe;IAC5C,yBAAyB,EAAE,0BAA0B;IACrD,sBAAsB,EAAE,YAAY;IACpC,qBAAqB,EAAE,4BAA4B;CACpD,CAAC;AAEF;yDACyD;AACzD,MAAM,UAAU,oBAAoB,CAAC,MAAkB;IACrD,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;AAClD,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -62,10 +62,11 @@ export { SearchResults } from "./headless/SearchResults.js";
|
|
|
62
62
|
export type { SearchPageInfo, SearchResultsBag } from "./headless/SearchResults.js";
|
|
63
63
|
export { FacetPanel } from "./headless/FacetPanel.js";
|
|
64
64
|
export type { FacetPanelBag } from "./headless/FacetPanel.js";
|
|
65
|
+
export { useAppliedSort } from "./headless/useAppliedSort.js";
|
|
65
66
|
export { RankingDisclosure } from "./headless/RankingDisclosure.js";
|
|
66
67
|
export type { RankingDisclosureBag } from "./headless/RankingDisclosure.js";
|
|
67
68
|
export { navEntries } from "./nav/manifest.js";
|
|
68
|
-
export { SEARCH_I18N_KEYS, searchI18nBundleEn, registerSearchI18n, } from "./i18n/keys.js";
|
|
69
|
+
export { SEARCH_I18N_KEYS, SEARCH_I18N_PLURAL_KEYS, searchI18nBundleEn, registerSearchI18n, } from "./i18n/keys.js";
|
|
69
70
|
export type { SearchI18nKey } from "./i18n/keys.js";
|
|
70
71
|
export { SEARCH_ERRORS, SEARCH_ERROR_CODES, SEARCH_BACKEND_UNAVAILABLE, SEARCH_WINDOW_EXCEEDED, searchErrorBundleEn, explainSearchError, } from "./i18n/errorsMap.js";
|
|
71
72
|
export type { SearchErrorCode, SearchErrorSpec, Remediation, } from "./i18n/errorsMap.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACxE,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,MAAM,EACN,iBAAiB,EACjB,qBAAqB,EACrB,SAAS,EACT,YAAY,EACZ,eAAe,EACf,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,aAAa,EACb,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEpG,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,YAAY,EACV,qBAAqB,EACrB,UAAU,EACV,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EACV,aAAa,EACb,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAG1E,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACxF,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,wBAAwB,GACzB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAG5E,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG/C,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,eAAe,EACf,eAAe,EACf,WAAW,GACZ,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAGH,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACxE,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,MAAM,EACN,iBAAiB,EACjB,qBAAqB,EACrB,SAAS,EACT,YAAY,EACZ,eAAe,EACf,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,aAAa,EACb,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEpG,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,YAAY,EACV,qBAAqB,EACrB,UAAU,EACV,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,YAAY,EACV,aAAa,EACb,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAG1E,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACxF,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,wBAAwB,GACzB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAG5E,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAG/C,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,eAAe,EACf,eAAe,EACf,WAAW,GACZ,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -56,11 +56,12 @@ export { SearchProvider } from "./headless/SearchProvider.js";
|
|
|
56
56
|
export { SearchStateProvider, useSearchState } from "./headless/SearchStateProvider.js";
|
|
57
57
|
export { SearchResults } from "./headless/SearchResults.js";
|
|
58
58
|
export { FacetPanel } from "./headless/FacetPanel.js";
|
|
59
|
+
export { useAppliedSort } from "./headless/useAppliedSort.js";
|
|
59
60
|
export { RankingDisclosure } from "./headless/RankingDisclosure.js";
|
|
60
61
|
// ── nav manifest (the pair's public surface declaration) ─────────────────────
|
|
61
62
|
export { navEntries } from "./nav/manifest.js";
|
|
62
63
|
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
63
|
-
export { SEARCH_I18N_KEYS, searchI18nBundleEn, registerSearchI18n, } from "./i18n/keys.js";
|
|
64
|
+
export { SEARCH_I18N_KEYS, SEARCH_I18N_PLURAL_KEYS, searchI18nBundleEn, registerSearchI18n, } from "./i18n/keys.js";
|
|
64
65
|
// ── errors map (code → status/params/remediation/en; generated) ──────────────
|
|
65
66
|
export { SEARCH_ERRORS, SEARCH_ERROR_CODES, SEARCH_BACKEND_UNAVAILABLE, SEARCH_WINDOW_EXCEEDED, searchErrorBundleEn, explainSearchError, } from "./i18n/errorsMap.js";
|
|
66
67
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,gFAAgF;AAChF,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAmB9C,gFAAgF;AAChF,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAS7B,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEpG,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAOvE,gFAAgF;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAKzD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE1E,gFAAgF;AAChF,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAMxF,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAGpE,gFAAgF;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,gFAAgF;AAChF,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAGxB,gFAAgF;AAChF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,gFAAgF;AAChF,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAmB9C,gFAAgF;AAChF,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAS7B,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEpG,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAOvE,gFAAgF;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAKzD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE1E,gFAAgF;AAChF,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAMxF,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAGpE,gFAAgF;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,gFAAgF;AAChF,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAGxB,gFAAgF;AAChF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC"}
|
package/llms.txt
CHANGED
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
|
|
3
3
|
"package": "@stapel/search-react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"backend": {
|
|
6
6
|
"module": "stapel-search",
|
|
7
7
|
"contract": ">=0.1 <0.2"
|
|
@@ -532,7 +532,11 @@
|
|
|
532
532
|
"search.results.blocked.at_end",
|
|
533
533
|
"search.results.blocked.at_start",
|
|
534
534
|
"search.results.count_approximate",
|
|
535
|
+
"search.results.count_approximate.one",
|
|
536
|
+
"search.results.count_approximate.other",
|
|
535
537
|
"search.results.count_exact",
|
|
538
|
+
"search.results.count_exact.one",
|
|
539
|
+
"search.results.count_exact.other",
|
|
536
540
|
"search.results.distance_km",
|
|
537
541
|
"search.results.empty",
|
|
538
542
|
"search.results.load_failed",
|
|
@@ -570,6 +574,7 @@
|
|
|
570
574
|
"SEARCH_ERRORS",
|
|
571
575
|
"SEARCH_ERROR_CODES",
|
|
572
576
|
"SEARCH_I18N_KEYS",
|
|
577
|
+
"SEARCH_I18N_PLURAL_KEYS",
|
|
573
578
|
"SEARCH_PARAM",
|
|
574
579
|
"SEARCH_SORTS",
|
|
575
580
|
"SEARCH_WINDOW_EXCEEDED",
|
|
@@ -599,6 +604,7 @@
|
|
|
599
604
|
"setFilterValues",
|
|
600
605
|
"setRangeValue",
|
|
601
606
|
"toggleFilterValue",
|
|
607
|
+
"useAppliedSort",
|
|
602
608
|
"useRankingDisclosure",
|
|
603
609
|
"useSearchAnalytics",
|
|
604
610
|
"useSearchApi",
|
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/search-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Headless React pair for stapel-search: a typed query client, TanStack Query hooks, and a URL-first state codec that makes a search shareable by construction (filters, ranges, geo, sort and the keyset cursor all live in the query string). Drill-down facets rendered with their remaining counts and with the server's own honesty flags — approximate, skipped, degraded — never swallowed; keyset pagination with the window refusal named; DSA Art. 26 `promoted` marking carried into every card slot and the P2B Art. 5 ranking disclosure exposed as data. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin, and /router binds the codec to react-router's useSearchParams.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -50,13 +50,6 @@
|
|
|
50
50
|
"nav-manifest.json",
|
|
51
51
|
"llms.txt"
|
|
52
52
|
],
|
|
53
|
-
"scripts": {
|
|
54
|
-
"build": "tsc -p tsconfig.json",
|
|
55
|
-
"test": "tsc -p tsconfig.test.json && vitest run --exclude test/prodBundlePurity.test.ts",
|
|
56
|
-
"lint": "eslint .",
|
|
57
|
-
"size": "size-limit",
|
|
58
|
-
"test:pack": "vitest run test/prodBundlePurity.test.ts"
|
|
59
|
-
},
|
|
60
53
|
"size-limit": [
|
|
61
54
|
{
|
|
62
55
|
"name": "index — the headless pair (client + hooks + URL codec + facet model): no antd, no react-router",
|
|
@@ -106,11 +99,6 @@
|
|
|
106
99
|
},
|
|
107
100
|
"devDependencies": {
|
|
108
101
|
"@size-limit/preset-small-lib": "^11.2.0",
|
|
109
|
-
"@stapel/attributes-react": "workspace:^",
|
|
110
|
-
"@stapel/core": "workspace:^",
|
|
111
|
-
"@stapel/showcase": "workspace:^",
|
|
112
|
-
"@stapel/tokens": "workspace:^",
|
|
113
|
-
"@stapel/tokens-antd": "workspace:^",
|
|
114
102
|
"@tanstack/react-query": "^5.81.0",
|
|
115
103
|
"@testing-library/react": "^16.3.0",
|
|
116
104
|
"@types/react": "^19.1.0",
|
|
@@ -122,12 +110,24 @@
|
|
|
122
110
|
"react-router": "^7.18.1",
|
|
123
111
|
"size-limit": "^11.2.0",
|
|
124
112
|
"typescript": "^5.8.3",
|
|
125
|
-
"vitest": "^3.2.4"
|
|
113
|
+
"vitest": "^3.2.4",
|
|
114
|
+
"@stapel/attributes-react": "^0.2.0",
|
|
115
|
+
"@stapel/core": "^0.17.0",
|
|
116
|
+
"@stapel/showcase": "^0.2.0",
|
|
117
|
+
"@stapel/tokens": "^0.5.0",
|
|
118
|
+
"@stapel/tokens-antd": "^0.5.0"
|
|
126
119
|
},
|
|
127
120
|
"engines": {
|
|
128
121
|
"node": ">=22"
|
|
129
122
|
},
|
|
130
123
|
"publishConfig": {
|
|
131
124
|
"access": "public"
|
|
125
|
+
},
|
|
126
|
+
"scripts": {
|
|
127
|
+
"build": "tsc -p tsconfig.json",
|
|
128
|
+
"test": "tsc -p tsconfig.test.json && vitest run --exclude test/prodBundlePurity.test.ts",
|
|
129
|
+
"lint": "eslint .",
|
|
130
|
+
"size": "size-limit",
|
|
131
|
+
"test:pack": "vitest run test/prodBundlePurity.test.ts"
|
|
132
132
|
}
|
|
133
|
-
}
|
|
133
|
+
}
|
|
@@ -15,13 +15,11 @@
|
|
|
15
15
|
* optional behaviour in disguise — every one has a working default.
|
|
16
16
|
*/
|
|
17
17
|
import type { ReactElement, ReactNode } from "react";
|
|
18
|
-
import { Col, Flex, Row
|
|
19
|
-
import { useT } from "@stapel/core";
|
|
18
|
+
import { Col, Flex, Row } from "antd";
|
|
20
19
|
import type { FeatureDef } from "@stapel/attributes-react";
|
|
21
20
|
import { SearchStateProvider } from "../headless/SearchStateProvider.js";
|
|
22
21
|
import type { SearchParamsAdapter } from "../headless/SearchStateProvider.js";
|
|
23
22
|
import type { ParseSearchStateOptions } from "../state/urlState.js";
|
|
24
|
-
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
25
23
|
import { FacetPanelPane } from "./FacetPanelPane.js";
|
|
26
24
|
import { SearchResultsPane } from "./SearchResultsPane.js";
|
|
27
25
|
import { SortSelect } from "./SortSelect.js";
|
|
@@ -42,7 +40,6 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
export function SearchPage(props: SearchPageProps): ReactElement {
|
|
45
|
-
const t = useT();
|
|
46
43
|
const {
|
|
47
44
|
adapter,
|
|
48
45
|
renderCard,
|
|
@@ -66,18 +63,16 @@ export function SearchPage(props: SearchPageProps): ReactElement {
|
|
|
66
63
|
/>
|
|
67
64
|
</Col>
|
|
68
65
|
<Col xs={24} md={17}>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/>
|
|
80
|
-
</Flex>
|
|
66
|
+
{/* ONE heading and ONE sort control. The page used to caption
|
|
67
|
+
the toolbar "Results" and then mount a pane whose own heading
|
|
68
|
+
says "Results" again — the live /s page printed both, one
|
|
69
|
+
above the other. The pane owns the heading row; the page puts
|
|
70
|
+
the sort control INTO it. */}
|
|
71
|
+
<SearchResultsPane
|
|
72
|
+
toolbar={<SortSelect />}
|
|
73
|
+
{...(renderCard !== undefined ? { renderCard } : {})}
|
|
74
|
+
{...(footer !== undefined ? { footer } : {})}
|
|
75
|
+
/>
|
|
81
76
|
</Col>
|
|
82
77
|
</Row>
|
|
83
78
|
</Flex>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* with an empty body, which is precisely how it would otherwise render as an
|
|
14
14
|
* empty page.
|
|
15
15
|
*/
|
|
16
|
-
import type { ReactElement, ReactNode } from "react";
|
|
16
|
+
import type { CSSProperties, ReactElement, ReactNode } from "react";
|
|
17
17
|
import { Button, Empty, Flex, Spin, Typography } from "antd";
|
|
18
18
|
import {
|
|
19
19
|
errorCode,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
toFlowError,
|
|
22
22
|
useDescribeFlowError,
|
|
23
23
|
useT,
|
|
24
|
+
useTPlural,
|
|
24
25
|
} from "@stapel/core";
|
|
25
26
|
import type { SearchItem } from "../api/types.js";
|
|
26
27
|
import { SearchResults } from "../headless/SearchResults.js";
|
|
@@ -34,6 +35,32 @@ import type { SearchCardRenderer } from "./SearchResultCard.js";
|
|
|
34
35
|
import { SearchSkinTheme } from "./theme.js";
|
|
35
36
|
import type { ThemeModeProp } from "./types.js";
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The whole result LAYOUT, for a container that wants its own — a map beside
|
|
40
|
+
* the list, a masonry wall, a table. It receives the loaded rows; the pane
|
|
41
|
+
* still owns the four load arms around it, so "nothing found" and "we could
|
|
42
|
+
* not run this search" stay the pane's sentences rather than the slot's
|
|
43
|
+
* problem.
|
|
44
|
+
*/
|
|
45
|
+
export type SearchResultsRenderer = (
|
|
46
|
+
items: readonly SearchItem[]
|
|
47
|
+
) => ReactNode;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The results grid. `auto-fill` + `minmax(280px, 1fr)`: as many columns as fit,
|
|
51
|
+
* each at least a readable card and never wider than its share — a catalogue
|
|
52
|
+
* on a 1400px desktop is four columns, not four full-bleed rows, and the same
|
|
53
|
+
* declaration collapses to one column on a phone with no breakpoint to
|
|
54
|
+
* maintain. 280px is the width below which the default card's title, price and
|
|
55
|
+
* location stop fitting on their own lines.
|
|
56
|
+
*/
|
|
57
|
+
const RESULTS_GRID: CSSProperties = {
|
|
58
|
+
display: "grid",
|
|
59
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
|
|
60
|
+
gap: 12,
|
|
61
|
+
alignItems: "stretch",
|
|
62
|
+
};
|
|
63
|
+
|
|
37
64
|
export interface SearchResultsPaneProps extends ThemeModeProp {
|
|
38
65
|
/**
|
|
39
66
|
* The card slot (spec §6.2 item 1). A storefront passes
|
|
@@ -42,20 +69,36 @@ export interface SearchResultsPaneProps extends ThemeModeProp {
|
|
|
42
69
|
* default honours it.
|
|
43
70
|
*/
|
|
44
71
|
readonly renderCard?: SearchCardRenderer;
|
|
72
|
+
/**
|
|
73
|
+
* The layout slot: replaces the grid entirely, `renderCard` and all. Use it
|
|
74
|
+
* when the container's arrangement is not "cards in a grid".
|
|
75
|
+
*/
|
|
76
|
+
readonly renderResults?: SearchResultsRenderer;
|
|
77
|
+
/**
|
|
78
|
+
* Rendered beside the count in the heading row — the sort control, a view
|
|
79
|
+
* switch. The pane owns the heading, so a screen that composes it hands its
|
|
80
|
+
* toolbar in here instead of printing a second caption above the pane.
|
|
81
|
+
*/
|
|
82
|
+
readonly toolbar?: ReactNode;
|
|
45
83
|
/** Rendered under the pager — where the container puts the ranking link. */
|
|
46
84
|
readonly footer?: ReactNode;
|
|
47
85
|
readonly enabled?: boolean;
|
|
48
86
|
}
|
|
49
87
|
|
|
50
88
|
function Count(props: { bag: SearchResultsBag }): ReactElement | null {
|
|
51
|
-
|
|
89
|
+
// A COUNTED sentence: `tPlural` asks Intl.PluralRules for the locale's
|
|
90
|
+
// category, so Russian gets three different endings for 1, 3 and 17 instead
|
|
91
|
+
// of the single one that was right for 5-20 and wrong everywhere else.
|
|
92
|
+
const tPlural = useTPlural();
|
|
52
93
|
const page = props.bag.page;
|
|
53
94
|
if (page === null) return null;
|
|
54
95
|
return (
|
|
55
96
|
<Typography.Text type="secondary" data-testid="search-count">
|
|
56
97
|
{page.countIsEstimate
|
|
57
|
-
?
|
|
58
|
-
|
|
98
|
+
? tPlural(SEARCH_I18N_KEYS.resultsCountApproximate, {
|
|
99
|
+
count: page.count,
|
|
100
|
+
})
|
|
101
|
+
: tPlural(SEARCH_I18N_KEYS.resultsCountExact, { count: page.count })}
|
|
59
102
|
</Typography.Text>
|
|
60
103
|
);
|
|
61
104
|
}
|
|
@@ -92,7 +135,7 @@ function Pager(props: { bag: SearchResultsBag }): ReactElement {
|
|
|
92
135
|
export function SearchResultsPane(props: SearchResultsPaneProps): ReactElement {
|
|
93
136
|
const t = useT();
|
|
94
137
|
const describe = useDescribeFlowError();
|
|
95
|
-
const renderCard = props
|
|
138
|
+
const { renderCard, renderResults } = props;
|
|
96
139
|
|
|
97
140
|
return (
|
|
98
141
|
<SearchSkinTheme {...(props.mode !== undefined ? { mode: props.mode } : {})}>
|
|
@@ -103,7 +146,10 @@ export function SearchResultsPane(props: SearchResultsPaneProps): ReactElement {
|
|
|
103
146
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
|
104
147
|
{t(SEARCH_I18N_KEYS.resultsTitle)}
|
|
105
148
|
</Typography.Title>
|
|
106
|
-
<
|
|
149
|
+
<Flex align="center" wrap gap={12}>
|
|
150
|
+
<Count bag={bag} />
|
|
151
|
+
{props.toolbar}
|
|
152
|
+
</Flex>
|
|
107
153
|
</Flex>
|
|
108
154
|
|
|
109
155
|
<DegradationNotice degradations={bag.degradations} />
|
|
@@ -149,17 +195,23 @@ export function SearchResultsPane(props: SearchResultsPaneProps): ReactElement {
|
|
|
149
195
|
/>
|
|
150
196
|
),
|
|
151
197
|
ready: (items) => (
|
|
152
|
-
<
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
<
|
|
159
|
-
|
|
198
|
+
<div data-testid="search-results">
|
|
199
|
+
{renderResults !== undefined ? (
|
|
200
|
+
renderResults(items)
|
|
201
|
+
) : (
|
|
202
|
+
<div style={RESULTS_GRID} data-testid="search-results-grid">
|
|
203
|
+
{items.map((item: SearchItem) => (
|
|
204
|
+
<div key={item.key}>
|
|
205
|
+
{renderCard !== undefined ? (
|
|
206
|
+
renderCard(item)
|
|
207
|
+
) : (
|
|
208
|
+
<SearchResultCard item={item} />
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
))}
|
|
160
212
|
</div>
|
|
161
|
-
)
|
|
162
|
-
</
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
163
215
|
),
|
|
164
216
|
})}
|
|
165
217
|
|
|
@@ -17,6 +17,7 @@ import type { ReactElement } from "react";
|
|
|
17
17
|
import { Flex, Select, Typography } from "antd";
|
|
18
18
|
import { useT } from "@stapel/core";
|
|
19
19
|
import { SEARCH_SORTS } from "../api/types.js";
|
|
20
|
+
import { useAppliedSort } from "../headless/useAppliedSort.js";
|
|
20
21
|
import { useSearchState } from "../headless/SearchStateProvider.js";
|
|
21
22
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
22
23
|
|
|
@@ -29,19 +30,28 @@ const SORT_LABEL_KEY: Readonly<Record<string, string>> = {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export interface SortSelectProps {
|
|
32
|
-
/** The sort the SERVER applied, shown when the URL names none.
|
|
33
|
+
/** The sort the SERVER applied, shown when the URL names none. Omitted, it
|
|
34
|
+
* is read from the page already in cache — see {@link useAppliedSort}. */
|
|
33
35
|
readonly appliedSort?: string | undefined;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function SortSelect(props: SortSelectProps): ReactElement {
|
|
37
39
|
const t = useT();
|
|
38
40
|
const { state, setSort } = useSearchState();
|
|
41
|
+
// What the control SAYS must be what the results are ordered by. With no
|
|
42
|
+
// `sort` in the URL the select used to fall through to its placeholder and
|
|
43
|
+
// show nothing at all, while the server had already sorted the page and
|
|
44
|
+
// said so in the envelope.
|
|
45
|
+
const applied = useAppliedSort();
|
|
39
46
|
|
|
47
|
+
// What the page is ACTUALLY ordered by: the URL's sort, else the one the
|
|
48
|
+
// container states, else the one the server reported for the page in cache.
|
|
49
|
+
const active = state.sort ?? props.appliedSort ?? applied;
|
|
40
50
|
const hasCentre = state.geo !== undefined;
|
|
41
51
|
const known = new Set(SEARCH_SORTS);
|
|
42
52
|
const values =
|
|
43
|
-
|
|
44
|
-
? [...SEARCH_SORTS,
|
|
53
|
+
active !== undefined && !known.has(active)
|
|
54
|
+
? [...SEARCH_SORTS, active]
|
|
45
55
|
: SEARCH_SORTS;
|
|
46
56
|
|
|
47
57
|
return (
|
|
@@ -52,8 +62,7 @@ export function SortSelect(props: SortSelectProps): ReactElement {
|
|
|
52
62
|
<Select<string>
|
|
53
63
|
data-testid="search-sort"
|
|
54
64
|
style={{ minWidth: 180 }}
|
|
55
|
-
value={
|
|
56
|
-
placeholder={t(SEARCH_I18N_KEYS.sortLabel)}
|
|
65
|
+
value={active ?? null}
|
|
57
66
|
onChange={(next) => {
|
|
58
67
|
setSort(next);
|
|
59
68
|
}}
|
package/src/default/index.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* 1. **`renderCard`** — the card slot. A storefront passes
|
|
12
12
|
* `<ListingCard>` from `@stapel/listings-react/default`; the generic card
|
|
13
13
|
* here is the documented default, not the intended end state (spec §3.7).
|
|
14
|
+
* `renderResults` is the level above it: the whole layout, for a container
|
|
15
|
+
* whose arrangement is not "cards in a grid".
|
|
14
16
|
* 2. **`categoryFeatures`** — the facet-label slot. The server sends
|
|
15
17
|
* `{value: count}` and no labels; the schema that names them lives in
|
|
16
18
|
* categories, and the container hands it in (spec §6.2 item 2).
|
|
@@ -24,7 +26,10 @@ export { SearchPage } from "./SearchPage.js";
|
|
|
24
26
|
export type { SearchPageProps } from "./SearchPage.js";
|
|
25
27
|
|
|
26
28
|
export { SearchResultsPane } from "./SearchResultsPane.js";
|
|
27
|
-
export type {
|
|
29
|
+
export type {
|
|
30
|
+
SearchResultsPaneProps,
|
|
31
|
+
SearchResultsRenderer,
|
|
32
|
+
} from "./SearchResultsPane.js";
|
|
28
33
|
|
|
29
34
|
export { FacetPanelPane } from "./FacetPanelPane.js";
|
|
30
35
|
export type { FacetPanelPaneProps } from "./FacetPanelPane.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sort the SERVER applied to the page currently in cache.
|
|
3
|
+
*
|
|
4
|
+
* A search with no `sort` in its URL is still sorted — the backend picks
|
|
5
|
+
* relevance when there is text and newest when there is not, and says so in
|
|
6
|
+
* the envelope's `sort`. A control that showed nothing in that case was not
|
|
7
|
+
* being cautious: it was hiding the answer the response already carried, and
|
|
8
|
+
* the live /s page rendered a sort box with no value while the results were
|
|
9
|
+
* plainly in an order.
|
|
10
|
+
*
|
|
11
|
+
* `enabled: false` is the whole trick. The key is the one `<SearchResults>`
|
|
12
|
+
* populates, so this subscribes to the SAME cache entry and re-renders when
|
|
13
|
+
* the page lands — but it never issues a request of its own, which is what
|
|
14
|
+
* keeps a control from turning into a second search (including on a pane
|
|
15
|
+
* mounted with `enabled={false}`, where a request would be exactly the thing
|
|
16
|
+
* the host switched off).
|
|
17
|
+
*/
|
|
18
|
+
import { useSearchQuery } from "../model/queries.js";
|
|
19
|
+
import { useSearchState } from "./SearchStateProvider.js";
|
|
20
|
+
|
|
21
|
+
export function useAppliedSort(): string | undefined {
|
|
22
|
+
const { state } = useSearchState();
|
|
23
|
+
const query = useSearchQuery(state, { enabled: false });
|
|
24
|
+
return query.data?.sort;
|
|
25
|
+
}
|
package/src/i18n/es.ts
CHANGED
|
@@ -20,8 +20,10 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
20
20
|
"search.results.load_failed": "No pudimos ejecutar esta búsqueda.",
|
|
21
21
|
"search.results.empty": "No hay nada que coincida con esta búsqueda.",
|
|
22
22
|
"search.results.retry": "Reintentar",
|
|
23
|
-
"search.results.count_approximate": "
|
|
24
|
-
"search.results.
|
|
23
|
+
"search.results.count_approximate.one": "Aproximadamente {count} resultado",
|
|
24
|
+
"search.results.count_approximate.other": "Unos {count} resultados",
|
|
25
|
+
"search.results.count_exact.one": "{count} resultado",
|
|
26
|
+
"search.results.count_exact.other": "{count} resultados",
|
|
25
27
|
"search.results.took_ms": "{ms} ms",
|
|
26
28
|
"search.results.next": "Página siguiente",
|
|
27
29
|
"search.results.prev": "Página anterior",
|
package/src/i18n/keys.ts
CHANGED
|
@@ -28,9 +28,10 @@ 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.
|
|
31
|
+
/** "About N results" — the sentence for an ESTIMATED count. A PLURAL
|
|
32
|
+
* FAMILY: render with `tPlural`, never `t` (see SEARCH_I18N_PLURAL_KEYS). */
|
|
32
33
|
resultsCountApproximate: "search.results.count_approximate",
|
|
33
|
-
/** "N results" — only when `exact_total` is true. */
|
|
34
|
+
/** "N results" — only when `exact_total` is true. A PLURAL FAMILY. */
|
|
34
35
|
resultsCountExact: "search.results.count_exact",
|
|
35
36
|
resultsTookMs: "search.results.took_ms",
|
|
36
37
|
resultsNext: "search.results.next",
|
|
@@ -111,6 +112,26 @@ export const SEARCH_I18N_KEYS = {
|
|
|
111
112
|
export type SearchI18nKey =
|
|
112
113
|
(typeof SEARCH_I18N_KEYS)[keyof typeof SEARCH_I18N_KEYS];
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The keys above that are PLURAL FAMILIES rather than single messages.
|
|
117
|
+
*
|
|
118
|
+
* A family is catalogued as one flat key per CLDR category
|
|
119
|
+
* (`<family>.one`, `…few`, `…many`, `…other`) and rendered with core's
|
|
120
|
+
* `tPlural(family, { count })`, which asks `Intl.PluralRules` for the current
|
|
121
|
+
* locale's category. Which categories a bundle carries is a fact about the
|
|
122
|
+
* language — `en` needs two, `ru` needs four — so the pair's parity test asks
|
|
123
|
+
* `Intl.PluralRules` which forms each locale can select and demands exactly
|
|
124
|
+
* those, rather than checking a list somebody typed.
|
|
125
|
+
*
|
|
126
|
+
* The live defect this closes: the Russian estimate sentence was ONE string
|
|
127
|
+
* doing the work of four, correct only for 5-20 and wrong for every 1, 2, 3
|
|
128
|
+
* and 4 a result page actually shows.
|
|
129
|
+
*/
|
|
130
|
+
export const SEARCH_I18N_PLURAL_KEYS: readonly SearchI18nKey[] = [
|
|
131
|
+
SEARCH_I18N_KEYS.resultsCountApproximate,
|
|
132
|
+
SEARCH_I18N_KEYS.resultsCountExact,
|
|
133
|
+
];
|
|
134
|
+
|
|
114
135
|
/**
|
|
115
136
|
* English fallback bundle for search-react UI keys + backend error codes.
|
|
116
137
|
* The generated backend texts are spread FIRST so coverage of the error
|
|
@@ -126,8 +147,10 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
126
147
|
"search.results.load_failed": "We could not run this search",
|
|
127
148
|
"search.results.empty": "Nothing matches this search",
|
|
128
149
|
"search.results.retry": "Try again",
|
|
129
|
-
"search.results.count_approximate": "About {count}
|
|
130
|
-
"search.results.
|
|
150
|
+
"search.results.count_approximate.one": "About {count} result",
|
|
151
|
+
"search.results.count_approximate.other": "About {count} results",
|
|
152
|
+
"search.results.count_exact.one": "{count} result",
|
|
153
|
+
"search.results.count_exact.other": "{count} results",
|
|
131
154
|
"search.results.took_ms": "{ms} ms",
|
|
132
155
|
"search.results.next": "Next page",
|
|
133
156
|
"search.results.prev": "Previous page",
|
package/src/i18n/ru.ts
CHANGED
|
@@ -35,8 +35,16 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
35
35
|
"search.results.load_failed": "Не удалось выполнить поиск.",
|
|
36
36
|
"search.results.empty": "По этому запросу ничего не нашлось.",
|
|
37
37
|
"search.results.retry": "Повторить",
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Four forms, because Russian has four (1 / 2-4 / 5-20 / fractions). One
|
|
39
|
+
// string here is wrong on most pages the storefront serves.
|
|
40
|
+
"search.results.count_approximate.one": "Примерно {count} объявление",
|
|
41
|
+
"search.results.count_approximate.few": "Примерно {count} объявления",
|
|
42
|
+
"search.results.count_approximate.many": "Примерно {count} объявлений",
|
|
43
|
+
"search.results.count_approximate.other": "Примерно {count} объявления",
|
|
44
|
+
"search.results.count_exact.one": "{count} объявление",
|
|
45
|
+
"search.results.count_exact.few": "{count} объявления",
|
|
46
|
+
"search.results.count_exact.many": "{count} объявлений",
|
|
47
|
+
"search.results.count_exact.other": "{count} объявления",
|
|
40
48
|
"search.results.took_ms": "{ms} мс",
|
|
41
49
|
"search.results.next": "Следующая страница",
|
|
42
50
|
"search.results.prev": "Предыдущая страница",
|
package/src/index.ts
CHANGED
|
@@ -124,6 +124,7 @@ export { SearchResults } from "./headless/SearchResults.js";
|
|
|
124
124
|
export type { SearchPageInfo, SearchResultsBag } from "./headless/SearchResults.js";
|
|
125
125
|
export { FacetPanel } from "./headless/FacetPanel.js";
|
|
126
126
|
export type { FacetPanelBag } from "./headless/FacetPanel.js";
|
|
127
|
+
export { useAppliedSort } from "./headless/useAppliedSort.js";
|
|
127
128
|
export { RankingDisclosure } from "./headless/RankingDisclosure.js";
|
|
128
129
|
export type { RankingDisclosureBag } from "./headless/RankingDisclosure.js";
|
|
129
130
|
|
|
@@ -133,6 +134,7 @@ export { navEntries } from "./nav/manifest.js";
|
|
|
133
134
|
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
134
135
|
export {
|
|
135
136
|
SEARCH_I18N_KEYS,
|
|
137
|
+
SEARCH_I18N_PLURAL_KEYS,
|
|
136
138
|
searchI18nBundleEn,
|
|
137
139
|
registerSearchI18n,
|
|
138
140
|
} from "./i18n/keys.js";
|