@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.
- package/CHANGELOG.md +42 -0
- package/README.md +21 -3
- package/dist/default/FacetGroupControl.d.ts +33 -6
- package/dist/default/FacetGroupControl.d.ts.map +1 -1
- package/dist/default/FacetGroupControl.js +167 -4
- package/dist/default/FacetGroupControl.js.map +1 -1
- package/dist/default/FacetPanelPane.d.ts +5 -3
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +1 -1
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/SearchPage.d.ts +10 -6
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +1 -1
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/index.d.ts +1 -1
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -1
- package/dist/default/index.js.map +1 -1
- package/dist/headless/FacetPanel.d.ts +9 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +1 -0
- package/dist/headless/FacetPanel.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +3 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +15 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +18 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +3 -0
- package/dist/i18n/ru.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +4 -1
- package/nav-manifest.json +1 -1
- package/package.json +10 -10
- package/src/analytics/generated/events.json +1 -1
- package/src/default/FacetGroupControl.tsx +278 -22
- package/src/default/FacetPanelPane.tsx +6 -3
- package/src/default/SearchPage.tsx +12 -8
- package/src/default/index.ts +1 -0
- package/src/headless/FacetPanel.tsx +10 -0
- package/src/i18n/es.ts +3 -0
- package/src/i18n/keys.ts +18 -0
- 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.
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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; `"
|
|
319
|
-
*
|
|
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 `"
|
|
276
|
-
*
|
|
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
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
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" ? "
|
|
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.
|
package/src/default/index.ts
CHANGED
|
@@ -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": "Все",
|