@stapel/search-react 0.28.0 → 0.29.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 (45) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +21 -3
  3. package/dist/default/FacetGroupControl.d.ts +33 -6
  4. package/dist/default/FacetGroupControl.d.ts.map +1 -1
  5. package/dist/default/FacetGroupControl.js +167 -4
  6. package/dist/default/FacetGroupControl.js.map +1 -1
  7. package/dist/default/FacetPanelPane.d.ts +5 -3
  8. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  9. package/dist/default/FacetPanelPane.js +1 -1
  10. package/dist/default/FacetPanelPane.js.map +1 -1
  11. package/dist/default/SearchPage.d.ts +10 -6
  12. package/dist/default/SearchPage.d.ts.map +1 -1
  13. package/dist/default/SearchPage.js +1 -1
  14. package/dist/default/SearchPage.js.map +1 -1
  15. package/dist/default/index.d.ts +1 -1
  16. package/dist/default/index.d.ts.map +1 -1
  17. package/dist/default/index.js +1 -1
  18. package/dist/default/index.js.map +1 -1
  19. package/dist/headless/FacetPanel.d.ts +9 -0
  20. package/dist/headless/FacetPanel.d.ts.map +1 -1
  21. package/dist/headless/FacetPanel.js +1 -0
  22. package/dist/headless/FacetPanel.js.map +1 -1
  23. package/dist/i18n/es.d.ts.map +1 -1
  24. package/dist/i18n/es.js +3 -0
  25. package/dist/i18n/es.js.map +1 -1
  26. package/dist/i18n/keys.d.ts +15 -0
  27. package/dist/i18n/keys.d.ts.map +1 -1
  28. package/dist/i18n/keys.js +18 -0
  29. package/dist/i18n/keys.js.map +1 -1
  30. package/dist/i18n/ru.d.ts.map +1 -1
  31. package/dist/i18n/ru.js +3 -0
  32. package/dist/i18n/ru.js.map +1 -1
  33. package/llms.txt +1 -1
  34. package/manifest.json +4 -1
  35. package/nav-manifest.json +1 -1
  36. package/package.json +10 -10
  37. package/src/analytics/generated/events.json +1 -1
  38. package/src/default/FacetGroupControl.tsx +278 -22
  39. package/src/default/FacetPanelPane.tsx +6 -3
  40. package/src/default/SearchPage.tsx +12 -8
  41. package/src/default/index.ts +1 -0
  42. package/src/headless/FacetPanel.tsx +10 -0
  43. package/src/i18n/es.ts +3 -0
  44. package/src/i18n/keys.ts +18 -0
  45. package/src/i18n/ru.ts +3 -0
@@ -62,10 +62,12 @@
62
62
  * them visible as before: folding everything would leave a heading over
63
63
  * nothing. Chosen options are always visible, wherever their count went.
64
64
  */
65
- import { useState } from "react";
65
+ import { useMemo, useState } from "react";
66
66
  import type { CSSProperties, ReactElement } from "react";
67
67
  import { Button, Checkbox, Flex, Input, Typography } from "antd";
68
68
  import { useT } from "@stapel/core";
69
+ import { SkinPickerSheet } from "@stapel/tokens-antd/skin";
70
+ import type { PickerGroup, PickerOption } from "@stapel/tokens-antd/skin";
69
71
  import { controls, cssVar, radii, spacing } from "@stapel/tokens";
70
72
  import { featureConfig, featureType } from "@stapel/attributes-react";
71
73
  import type { FeatureDef } from "@stapel/attributes-react";
@@ -632,6 +634,246 @@ function DictionaryField(props: {
632
634
  );
633
635
  }
634
636
 
637
+ /**
638
+ * How many values the sheet's «All values» band adds per page.
639
+ *
640
+ * Fifty is the vocabulary endpoint's own page size, and it is the number past
641
+ * which a phone list stops being scrolled and starts being searched. Reaching
642
+ * the end of the list asks for the next fifty, so a 418-make level is a
643
+ * scroll rather than 418 mounted rows.
644
+ */
645
+ export const FACET_SHEET_PAGE = 50;
646
+
647
+ /** One facet option as the shared picker reads it. The count rides along as
648
+ * the row's second line: a filter without its remaining count is the
649
+ * drill-down facet turned naive, and that rule does not stop at a sheet. */
650
+ function pickerRow(option: FacetOption, notCounted: string): PickerOption {
651
+ return {
652
+ value: option.value,
653
+ label: option.label,
654
+ description: option.count === null ? notCounted : String(option.count),
655
+ };
656
+ }
657
+
658
+ /**
659
+ * A dictionary group on a PHONE: a trigger row, and a nested sheet.
660
+ *
661
+ * ── What the buyer was looking at ─────────────────────────────────────────
662
+ *
663
+ * The composer's own vocabulary picker (`@stapel/attributes-react`'s
664
+ * `ref_select` editor) is a trigger that opens a sheet with a search box, a
665
+ * recommended band and the rest — zero checkboxes. The buyer's filter sheet
666
+ * drew the SAME axis as a wall of eight checkboxes with a "Find a value" box
667
+ * and a "Show all (38)" under it, and no way to say "any". Two halves of one
668
+ * product teaching two different gestures for one dictionary is the defect;
669
+ * this mode is the half that moved.
670
+ *
671
+ * ── Why the shared `SkinPickerSheet` and not a local one ──────────────────
672
+ *
673
+ * It is the same component the composer's picker draws: the search box pinned
674
+ * above the list, the checkmarks, the commit button above the home indicator,
675
+ * the swipe/Esc/back dismissal, the skeleton and the empty arm. A pair that
676
+ * re-derives that gets a near-miss of it, which is exactly how the two halves
677
+ * drifted the first time.
678
+ *
679
+ * ── The two bands ─────────────────────────────────────────────────────────
680
+ *
681
+ * «Recommended» is the busiest values BY COUNT — the answer's own evidence,
682
+ * capped at {@link FACET_VISIBLE_OPTIONS} — with the chosen values in front
683
+ * of it, because a filter a person cannot see is a filter they cannot remove.
684
+ * «All values» is everything else, alphabetically, a page at a time. Once
685
+ * something is typed the bands collapse into one: a "Recommended" heading
686
+ * over rows that do not answer the box is the stale-list defect wearing a
687
+ * hat.
688
+ *
689
+ * The box filters LOCALLY and across alphabets (`translitPrefixMatch`, the
690
+ * same matcher the desktop field uses), so a Cyrillic spelling of a Latin
691
+ * make finds it. Nothing here touches the URL: what a person typed to FIND a
692
+ * filter is not part of the search they would share. The COMMIT does — the
693
+ * whole draft is written to the slug's URL key at once.
694
+ */
695
+ function DictionarySheet(props: {
696
+ readonly group: FacetGroup;
697
+ readonly onSetValues: (slug: string, values: readonly string[]) => void;
698
+ }): ReactElement {
699
+ const t = useT();
700
+ const { group } = props;
701
+ const [open, setOpen] = useState(false);
702
+ const [needle, setNeedle] = useState("");
703
+ const [page, setPage] = useState(FACET_SHEET_PAGE);
704
+
705
+ const chosen = group.options.filter((option) => option.selected);
706
+ const query = needle.trim();
707
+
708
+ const { groups, total } = useMemo(() => {
709
+ const notCounted = t(SEARCH_I18N_KEYS.facetsNotCounted);
710
+ const hit = (option: FacetOption): boolean =>
711
+ query === "" ||
712
+ translitPrefixMatch(query, option.label) ||
713
+ translitPrefixMatch(query, option.value);
714
+ const selected = group.options.filter((option) => option.selected);
715
+ const byCount = [...group.options].sort(
716
+ (a, b) => (b.count ?? 0) - (a.count ?? 0)
717
+ );
718
+
719
+ if (query !== "") {
720
+ // One band while the box holds something: the chosen values first (they
721
+ // are the ones with an off-switch to reach), then the evidence order.
722
+ const hits = [
723
+ ...selected.filter(hit),
724
+ ...byCount.filter((option) => !option.selected && hit(option)),
725
+ ];
726
+ return {
727
+ total: hits.length,
728
+ groups: [
729
+ {
730
+ key: "all",
731
+ label: t(SEARCH_I18N_KEYS.facetsDictionaryAllValues),
732
+ options: hits.slice(0, page).map((o) => pickerRow(o, notCounted)),
733
+ },
734
+ ] as readonly PickerGroup[],
735
+ };
736
+ }
737
+
738
+ // The band never drops a chosen value, however cold it is: its cap grows
739
+ // to hold them rather than pushing one of them into the alphabet.
740
+ const cap = Math.max(FACET_VISIBLE_OPTIONS, selected.length);
741
+ const band: FacetOption[] = [];
742
+ const banded = new Set<string>();
743
+ for (const option of [...selected, ...byCount]) {
744
+ if (banded.has(option.value) || band.length >= cap) continue;
745
+ band.push(option);
746
+ banded.add(option.value);
747
+ }
748
+ const rest = group.options
749
+ .filter((option) => !banded.has(option.value))
750
+ .sort((a, b) => a.label.localeCompare(b.label));
751
+ return {
752
+ total: band.length + Math.min(rest.length, page),
753
+ groups: [
754
+ {
755
+ key: "band",
756
+ label: t(SEARCH_I18N_KEYS.facetsDictionaryRecommended),
757
+ options: band.map((o) => pickerRow(o, notCounted)),
758
+ },
759
+ {
760
+ key: "all",
761
+ label: t(SEARCH_I18N_KEYS.facetsDictionaryAllValues),
762
+ options: rest.slice(0, page).map((o) => pickerRow(o, notCounted)),
763
+ },
764
+ ] as readonly PickerGroup[],
765
+ };
766
+ }, [group, query, page, t]);
767
+
768
+ // How many rows exist behind the current page — what "there is more" means.
769
+ const available = group.options.length;
770
+
771
+ const text =
772
+ chosen.length > 0
773
+ ? chosen.map((option) => option.label).join(", ")
774
+ : t(SEARCH_I18N_KEYS.facetsDictionaryAny);
775
+
776
+ return (
777
+ <>
778
+ <button
779
+ type="button"
780
+ aria-haspopup="dialog"
781
+ aria-expanded={open}
782
+ aria-label={group.label}
783
+ style={DICTIONARY_FIELD}
784
+ data-testid={`facet-dictionary-trigger-${group.slug}`}
785
+ data-chosen={chosen.length}
786
+ data-analytics="none"
787
+ data-analytics-reason="opening a filter group is a read, not a flow step"
788
+ onClick={() => {
789
+ setOpen(true);
790
+ }}
791
+ >
792
+ <span style={DICTIONARY_FIELD_TEXT}>{text}</span>
793
+ {chosen.length > 0 && (
794
+ <Typography.Text
795
+ type="secondary"
796
+ data-testid={`facet-dictionary-trigger-count-${group.slug}`}
797
+ >
798
+ {chosen.length}
799
+ </Typography.Text>
800
+ )}
801
+ <ChevronGlyph open={open} />
802
+ </button>
803
+ <SkinPickerSheet
804
+ mode="multi"
805
+ open={open}
806
+ onClose={() => {
807
+ setOpen(false);
808
+ setNeedle("");
809
+ setPage(FACET_SHEET_PAGE);
810
+ }}
811
+ title={group.label}
812
+ testId={`facet-dictionary-sheet-${group.slug}`}
813
+ doneLabel={t(SEARCH_I18N_KEYS.facetsDictionaryDone)}
814
+ searchPlaceholder={t(SEARCH_I18N_KEYS.facetsDictionarySearch)}
815
+ emptyLabel={t(SEARCH_I18N_KEYS.facetsDictionaryEmpty)}
816
+ // The caller owns the filtering — the sheet's own local filter matches
817
+ // on the label only, and this axis is searched across alphabets.
818
+ searchValue={needle}
819
+ onSearchChange={(next) => {
820
+ setNeedle(next);
821
+ setPage(FACET_SHEET_PAGE);
822
+ }}
823
+ groups={groups}
824
+ // Everything handed over is drawn: the paging above is this
825
+ // component's, so the sheet's own row cap must not fold it again.
826
+ maxRows={Math.max(1, total)}
827
+ onEndReached={() => {
828
+ setPage((current) =>
829
+ current >= available ? current : current + FACET_SHEET_PAGE
830
+ );
831
+ }}
832
+ values={group.selected}
833
+ onChange={(values) => {
834
+ props.onSetValues(group.slug, values);
835
+ }}
836
+ />
837
+ </>
838
+ );
839
+ }
840
+
841
+ /**
842
+ * The three faces of one dictionary, chosen by mode.
843
+ *
844
+ * `"sheet"` without a bulk setter falls back to the FIELD rather than to the
845
+ * inline wall: a trigger that opens a list is the shape both surfaces are
846
+ * moving to, and the wall is the thing the pass named.
847
+ */
848
+ function DictionaryControl(props: {
849
+ readonly group: FacetGroup;
850
+ readonly mode: "field" | "inline" | "sheet" | undefined;
851
+ readonly onToggle: (slug: string, value: string) => void;
852
+ readonly onSetValues?: (slug: string, values: readonly string[]) => void;
853
+ readonly visible: number;
854
+ }): ReactElement {
855
+ const { onSetValues } = props;
856
+ if (props.mode === "sheet" && onSetValues !== undefined) {
857
+ return <DictionarySheet group={props.group} onSetValues={onSetValues} />;
858
+ }
859
+ if (props.mode === "field" || props.mode === "sheet") {
860
+ return (
861
+ <DictionaryField
862
+ group={props.group}
863
+ onToggle={props.onToggle}
864
+ visible={props.visible}
865
+ />
866
+ );
867
+ }
868
+ return (
869
+ <DictionaryBody
870
+ group={props.group}
871
+ onToggle={props.onToggle}
872
+ visible={props.visible}
873
+ />
874
+ );
875
+ }
876
+
635
877
  export interface FacetGroupControlProps {
636
878
  readonly group: FacetGroup;
637
879
  readonly onToggle: (slug: string, value: string) => void;
@@ -652,13 +894,31 @@ export interface FacetGroupControlProps {
652
894
  * value only — the person owns the state after the first click. */
653
895
  readonly defaultOpen?: boolean;
654
896
  /**
655
- * How a `"dictionary"` group is drawn. `"field"` is the desktop shape — a
656
- * select-style field reading its chosen values or "Any", which opens the
657
- * searchable list; `"inline"` (the default) keeps the list open, the shape
658
- * a phone sheet wants because the sheet is already the disclosure.
659
- * Meaningless for the other three shapes.
897
+ * How a `"dictionary"` group is drawn. Meaningless for the other three
898
+ * shapes.
899
+ *
900
+ * - `"field"` the desktop shape: a select-style field reading its chosen
901
+ * values or "Any", which opens the searchable list under it;
902
+ * - `"sheet"` — the PHONE shape: a trigger row reading the same sentence,
903
+ * opening a nested picker sheet with a search box, a recommended band
904
+ * and the rest. The same control the composer's vocabulary picker is, so
905
+ * one dictionary is one gesture on both halves of the product. Needs
906
+ * {@link FacetGroupControlProps.onSetValues} — the sheet commits a whole
907
+ * draft at once, and a per-value toggle cannot apply one; without it the
908
+ * group falls back to `"field"`;
909
+ * - `"inline"` (the default) keeps the list open — a group that is already
910
+ * the only thing on its surface, such as a per-chip sheet.
660
911
  */
661
- readonly dictionaryMode?: "field" | "inline";
912
+ readonly dictionaryMode?: "field" | "inline" | "sheet";
913
+ /**
914
+ * Write a slug's chosen values in ONE go — `useFacetPanel`'s `setValues`.
915
+ *
916
+ * `onToggle` reads the current state to flip one value, so N toggles in one
917
+ * tick collapse into the last one; a sheet that commits a draft of several
918
+ * ticks needs the bulk write. Optional so no existing host changes, and
919
+ * only `dictionaryMode: "sheet"` reads it.
920
+ */
921
+ readonly onSetValues?: (slug: string, values: readonly string[]) => void;
662
922
  }
663
923
 
664
924
  export function FacetGroupControl(props: FacetGroupControlProps): ReactElement {
@@ -739,21 +999,17 @@ export function FacetGroupControl(props: FacetGroupControlProps): ReactElement {
739
999
  {/* Closed means NOT RENDERED, not hidden: a hundred `display:none`
740
1000
  checkboxes are still a hundred stops for a screen reader, and the
741
1001
  measured rail held 118 of them. */}
742
- {open &&
743
- shape === "dictionary" &&
744
- (props.dictionaryMode === "field" ? (
745
- <DictionaryField
746
- group={group}
747
- onToggle={props.onToggle}
748
- visible={limit ?? FACET_VISIBLE_OPTIONS}
749
- />
750
- ) : (
751
- <DictionaryBody
752
- group={group}
753
- onToggle={props.onToggle}
754
- visible={limit ?? FACET_VISIBLE_OPTIONS}
755
- />
756
- ))}
1002
+ {open && shape === "dictionary" && (
1003
+ <DictionaryControl
1004
+ group={group}
1005
+ mode={props.dictionaryMode}
1006
+ onToggle={props.onToggle}
1007
+ {...(props.onSetValues !== undefined
1008
+ ? { onSetValues: props.onSetValues }
1009
+ : {})}
1010
+ visible={limit ?? FACET_VISIBLE_OPTIONS}
1011
+ />
1012
+ )}
757
1013
 
758
1014
  {open && shape !== "dictionary" && (
759
1015
  <>
@@ -315,10 +315,12 @@ export interface FacetPanelPaneProps extends ThemeModeProp {
315
315
  /**
316
316
  * How a DICTIONARY group is drawn. `"field"` is the desktop shape — a
317
317
  * select-style field reading its chosen values or "Any", which opens the
318
- * searchable list; `"inline"` (the default) keeps the list open, which is
319
- * the phone sheet's shape because a sheet is already a disclosure.
318
+ * searchable list under it; `"sheet"` is the phone's the same trigger
319
+ * row, opening a nested picker sheet with a search box, a recommended band
320
+ * and the rest, which is the control the composer's vocabulary picker
321
+ * already is; `"inline"` (the default) keeps the list open.
320
322
  */
321
- readonly dictionaryMode?: "field" | "inline";
323
+ readonly dictionaryMode?: "field" | "inline" | "sheet";
322
324
  }
323
325
 
324
326
  /**
@@ -828,6 +830,7 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
828
830
  key={needle === "" ? group.slug : `${group.slug}:match`}
829
831
  group={group}
830
832
  onToggle={bag.toggle}
833
+ onSetValues={bag.setValues}
831
834
  collapsible
832
835
  defaultOpen={
833
836
  needle !== "" ||
@@ -272,14 +272,18 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
272
272
  * the two frames want opposite shapes and only this component knows which
273
273
  * one it is drawing: the desktop rail gets `"field"` (a select-style «Any»
274
274
  * that opens the searchable list — a 418-value vocabulary held open in a
275
- * 280px column is the whole column), the phone sheet gets `"inline"`
276
- * (the sheet is already the disclosure). Set it to override both.
275
+ * 280px column is the whole column), the phone sheet gets `"sheet"` — the
276
+ * same trigger row, opening a nested picker with a search box, a
277
+ * recommended band and the rest.
277
278
  *
278
- * It was unreachable through this component until now: the panel had the
279
- * prop, `<SearchPage>` forwarded nothing, and a storefront that mounts the
280
- * page rather than the pane could not get the field at all.
279
+ * The phone default was `"inline"`, which drew the axis as a wall of
280
+ * checkboxes with a "Find a value" box over it while the COMPOSER's picker
281
+ * for the very same vocabulary was a trigger and a sheet. One dictionary,
282
+ * two gestures, depending on which half of the product you were in. Set it
283
+ * to override both; `"inline"` is still there for a surface that is
284
+ * already devoted to one group.
281
285
  */
282
- readonly dictionaryMode?: "field" | "inline";
286
+ readonly dictionaryMode?: "field" | "inline" | "sheet";
283
287
  /**
284
288
  * How many groups the rail draws before the rest fold behind "All filters"
285
289
  * — see {@link FacetPanelPaneProps.visibleGroups}.
@@ -455,7 +459,7 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
455
459
 
456
460
  interface SearchPageBodyProps {
457
461
  readonly renderCard?: SearchCardRenderer;
458
- readonly dictionaryMode?: "field" | "inline";
462
+ readonly dictionaryMode?: "field" | "inline" | "sheet";
459
463
  readonly visibleGroups?: number | null;
460
464
  readonly categoryFeatures?: readonly FeatureDef[];
461
465
  readonly renderEmptyExits?: () => ReactNode;
@@ -587,7 +591,7 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
587
591
  : // STATIC, not sticky: the rail scrolls with the page, and a bar
588
592
  // pinned to the port's floor sat on top of the last groups.
589
593
  { footerBar: "static" as const })}
590
- dictionaryMode={props.dictionaryMode ?? (layout === "sheet" ? "inline" : "field")}
594
+ dictionaryMode={props.dictionaryMode ?? (layout === "sheet" ? "sheet" : "field")}
591
595
  // `??` would treat an explicit `null` ("never fold") the same as
592
596
  // "not set": `visibleGroups` uses `null` as a real value, unlike
593
597
  // `dictionaryMode` above, so only `undefined` falls through.
@@ -107,6 +107,7 @@ export {
107
107
  facetOptionNodes,
108
108
  isDictionaryFacet,
109
109
  FACET_DICTIONARY_THRESHOLD,
110
+ FACET_SHEET_PAGE,
110
111
  FACET_VISIBLE_OPTIONS,
111
112
  } from "./FacetGroupControl.js";
112
113
  export type {
@@ -89,6 +89,15 @@ export interface FacetPanelBag {
89
89
  readonly activeFilters: number;
90
90
 
91
91
  toggle(slug: string, value: string): void;
92
+ /**
93
+ * Write one slug's chosen values WHOLE — the bulk half of `toggle`.
94
+ *
95
+ * `toggle` reads the state it flips, so several of them in one tick
96
+ * collapse into the last. A control that holds a draft and applies it once
97
+ * — the phone's dictionary sheet — needs to say the whole list, and a
98
+ * storefront replaying a saved search needs the same call.
99
+ */
100
+ setValues(slug: string, values: readonly string[]): void;
92
101
  setRange(slug: string, range: SearchRange | null): void;
93
102
  clear(slug: string): void;
94
103
  clearAll(): void;
@@ -224,6 +233,7 @@ export function useFacetPanel(props: {
224
233
  envelope.data.degraded.includes(FACET_PLAN_EVIDENCE),
225
234
  activeFilters,
226
235
  toggle: toggleFilter,
236
+ setValues: setFilter,
227
237
  setRange,
228
238
  clear: (slug) => {
229
239
  setFilter(slug, []);
package/src/i18n/es.ts CHANGED
@@ -93,6 +93,9 @@ export const searchI18nBundleEs: I18nDictionary = {
93
93
  "search.facets.dictionary_empty": "Ningún valor coincide con esto",
94
94
  "search.facets.dictionary_chosen": "Elegido",
95
95
  "search.facets.dictionary_any": "Cualquiera",
96
+ "search.facets.dictionary_recommended": "Recomendados",
97
+ "search.facets.dictionary_all_values": "Todos los valores",
98
+ "search.facets.dictionary_done": "Listo",
96
99
  "search.facets.all_filters": "Todos los filtros ({count})",
97
100
  "search.facets.range_bounds": "de {min} a {max}",
98
101
  "search.facets.popular_all": "Todos",
package/src/i18n/keys.ts CHANGED
@@ -183,6 +183,21 @@ export const SEARCH_I18N_KEYS = {
183
183
  * diverge in every language with gender or case.
184
184
  */
185
185
  facetsDictionaryAny: "search.facets.dictionary_any",
186
+ /**
187
+ * The heading over a dictionary sheet's first band: the busiest values of
188
+ * this axis, which is the EVIDENCE the answer carries and not an editorial
189
+ * pick. Distinct from `facetsPopularValues`, which names a block on the
190
+ * results page rather than a section inside one picker.
191
+ */
192
+ facetsDictionaryRecommended: "search.facets.dictionary_recommended",
193
+ /** The heading over the rest of the dictionary sheet — everything the band
194
+ * did not take, alphabetically. Not `facetsShowAll`: that is a fold's
195
+ * button, this is a section a person scrolls into. */
196
+ facetsDictionaryAllValues: "search.facets.dictionary_all_values",
197
+ /** The dictionary sheet's commit. A multi-select sheet holds a draft and
198
+ * applies it once, so this word is the difference between keeping the
199
+ * ticks and dismissing them. */
200
+ facetsDictionaryDone: "search.facets.dictionary_done",
186
201
  /**
187
202
  * The tail of the rail, folded: how many groups are behind one control.
188
203
  * `{count}` is a number in parentheses, not a counted noun, so this is not
@@ -563,6 +578,9 @@ export const searchI18nBundleEn: Record<string, string> = {
563
578
  "search.facets.dictionary_empty": "No value matches this",
564
579
  "search.facets.dictionary_chosen": "Chosen",
565
580
  "search.facets.dictionary_any": "Any",
581
+ "search.facets.dictionary_recommended": "Recommended",
582
+ "search.facets.dictionary_all_values": "All values",
583
+ "search.facets.dictionary_done": "Done",
566
584
  "search.facets.all_filters": "All filters ({count})",
567
585
  "search.facets.range_bounds": "from {min} to {max}",
568
586
  "search.facets.popular_all": "All",
package/src/i18n/ru.ts CHANGED
@@ -118,6 +118,9 @@ export const searchI18nBundleRu: I18nDictionary = {
118
118
  "search.facets.dictionary_empty": "Такого значения здесь нет",
119
119
  "search.facets.dictionary_chosen": "Выбрано",
120
120
  "search.facets.dictionary_any": "Любая",
121
+ "search.facets.dictionary_recommended": "Рекомендуемые",
122
+ "search.facets.dictionary_all_values": "Все варианты",
123
+ "search.facets.dictionary_done": "Готово",
121
124
  "search.facets.all_filters": "Все фильтры ({count})",
122
125
  "search.facets.range_bounds": "от {min} до {max}",
123
126
  "search.facets.popular_all": "Все",