@stapel/search-react 0.22.0 → 0.24.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 (74) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/MODULE.md +12 -0
  3. package/README.md +122 -6
  4. package/dist/api/types.d.ts +13 -0
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/types.js.map +1 -1
  7. package/dist/default/FacetGroupControl.d.ts +22 -4
  8. package/dist/default/FacetGroupControl.d.ts.map +1 -1
  9. package/dist/default/FacetGroupControl.js +85 -9
  10. package/dist/default/FacetGroupControl.js.map +1 -1
  11. package/dist/default/FacetPanelPane.d.ts +41 -0
  12. package/dist/default/FacetPanelPane.d.ts.map +1 -1
  13. package/dist/default/FacetPanelPane.js +72 -18
  14. package/dist/default/FacetPanelPane.js.map +1 -1
  15. package/dist/default/FilterChips.d.ts +86 -2
  16. package/dist/default/FilterChips.d.ts.map +1 -1
  17. package/dist/default/FilterChips.js +210 -4
  18. package/dist/default/FilterChips.js.map +1 -1
  19. package/dist/default/PartitionChips.d.ts +14 -0
  20. package/dist/default/PartitionChips.d.ts.map +1 -1
  21. package/dist/default/PartitionChips.js +30 -4
  22. package/dist/default/PartitionChips.js.map +1 -1
  23. package/dist/default/RangeFilterRow.d.ts.map +1 -1
  24. package/dist/default/RangeFilterRow.js +65 -11
  25. package/dist/default/RangeFilterRow.js.map +1 -1
  26. package/dist/default/SearchPage.d.ts +65 -1
  27. package/dist/default/SearchPage.d.ts.map +1 -1
  28. package/dist/default/SearchPage.js +47 -5
  29. package/dist/default/SearchPage.js.map +1 -1
  30. package/dist/default/index.d.ts +4 -4
  31. package/dist/default/index.d.ts.map +1 -1
  32. package/dist/default/index.js +3 -3
  33. package/dist/default/index.js.map +1 -1
  34. package/dist/i18n/es.d.ts.map +1 -1
  35. package/dist/i18n/es.js +9 -0
  36. package/dist/i18n/es.js.map +1 -1
  37. package/dist/i18n/keys.d.ts +39 -0
  38. package/dist/i18n/keys.d.ts.map +1 -1
  39. package/dist/i18n/keys.js +49 -0
  40. package/dist/i18n/keys.js.map +1 -1
  41. package/dist/i18n/ru.d.ts.map +1 -1
  42. package/dist/i18n/ru.js +9 -0
  43. package/dist/i18n/ru.js.map +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/state/facets.d.ts +64 -0
  49. package/dist/state/facets.d.ts.map +1 -1
  50. package/dist/state/facets.js +144 -0
  51. package/dist/state/facets.js.map +1 -1
  52. package/dist/state/ranges.d.ts +22 -0
  53. package/dist/state/ranges.d.ts.map +1 -1
  54. package/dist/state/ranges.js +30 -0
  55. package/dist/state/ranges.js.map +1 -1
  56. package/llms.txt +1 -1
  57. package/manifest.json +14 -1
  58. package/nav-manifest.json +1 -1
  59. package/package.json +7 -7
  60. package/src/analytics/generated/events.json +1 -1
  61. package/src/api/types.ts +13 -0
  62. package/src/default/FacetGroupControl.tsx +139 -14
  63. package/src/default/FacetPanelPane.tsx +131 -19
  64. package/src/default/FilterChips.tsx +313 -6
  65. package/src/default/PartitionChips.tsx +47 -4
  66. package/src/default/RangeFilterRow.tsx +116 -1
  67. package/src/default/SearchPage.tsx +94 -3
  68. package/src/default/index.ts +27 -3
  69. package/src/i18n/es.ts +9 -0
  70. package/src/i18n/keys.ts +50 -0
  71. package/src/i18n/ru.ts +9 -0
  72. package/src/index.ts +4 -0
  73. package/src/state/facets.ts +152 -0
  74. package/src/state/ranges.ts +44 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "by scripts/gen-events.mjs — do not edit; drift-gated (pnpm gen:events:check)",
3
3
  "package": "@stapel/search-react",
4
- "version": "0.22.0",
4
+ "version": "0.24.0",
5
5
  "defined": [],
6
6
  "flows": []
7
7
  }
package/src/api/types.ts CHANGED
@@ -30,6 +30,19 @@ export type Schemas = components["schemas"];
30
30
  */
31
31
  export type FacetLabels = Schemas["FacetLabels"] & {
32
32
  readonly label?: string | null;
33
+ /**
34
+ * Whether {@link label} is a translation KEY rather than a caption, the way
35
+ * `translatable` says it for the values.
36
+ *
37
+ * Measured on a live answer (`category=141/151`, 2026-09-04): the server
38
+ * sends it beside every label — a make caption with
39
+ * `label_translatable: false`, a condition caption with `true` — and the
40
+ * describe it. Declared, not consumed: this pair passes the group heading
41
+ * through its translator either way, and a caption that is not a key comes
42
+ * back unchanged (`translate()` keeps a key it cannot resolve). Typed so a
43
+ * fixture captured from the wire is not a type error.
44
+ */
45
+ readonly label_translatable?: boolean;
33
46
  };
34
47
 
35
48
  /** `facet_labels` as a whole: `{slug: {label, translatable, values}}`. */
@@ -66,7 +66,7 @@ import { 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 { radii, spacing } from "@stapel/tokens";
69
+ import { controls, cssVar, radii, spacing } from "@stapel/tokens";
70
70
  import {
71
71
  VOCABULARY_BACKED_TYPES,
72
72
  featureConfig,
@@ -181,18 +181,28 @@ export function isDictionaryFacet(group: FacetGroup): boolean {
181
181
  /**
182
182
  * Which of the four shapes a group takes.
183
183
  *
184
- * Order matters: a hierarchical facet is nested even when it is single-choice,
185
- * because losing the tree costs more than losing the pills; and a dictionary
186
- * is a dictionary before it is a checkbox list, because the checkbox list is
187
- * the shape it was drawn as when nobody could pick a make.
184
+ * Order matters, and it changed in one place after a live measurement:
185
+ *
186
+ * - a hierarchical facet is nested even when it is single-choice, because
187
+ * losing the tree costs more than losing the pills;
188
+ * - a DICTIONARY outranks the pills. The make axis on the live cars leaf
189
+ * `maxSelected: 1` over a 418-value vocabulary, so "pick one" won and the
190
+ * control it produced was four hundred pills in a 280px rail — a wall
191
+ * with a different border radius. Above the fold the shape a person needs
192
+ * is a search box, whether or not they may tick two; below it,
193
+ * single-choice still means pills, because `isDictionaryFacet` requires
194
+ * more than {@link FACET_DICTIONARY_THRESHOLD} counted buckets;
195
+ * - and a dictionary is a dictionary before it is a checkbox list, because
196
+ * the checkbox list is the shape it was drawn as when nobody could pick a
197
+ * make.
188
198
  */
189
199
  export function facetGroupShape(group: FacetGroup): FacetGroupShape {
190
200
  const feature = group.feature;
191
201
  if (feature !== undefined && featureType(feature) === "hierarchical_select") {
192
202
  return "nested";
193
203
  }
194
- if (singleChoice(feature)) return "segmented";
195
- return isDictionaryFacet(group) ? "dictionary" : "checkbox";
204
+ if (isDictionaryFacet(group)) return "dictionary";
205
+ return singleChoice(feature) ? "segmented" : "checkbox";
196
206
  }
197
207
 
198
208
  /**
@@ -519,6 +529,105 @@ function DictionaryBody(props: {
519
529
  );
520
530
  }
521
531
 
532
+ /**
533
+ * The closed face of a dictionary group on DESKTOP: a select-shaped field
534
+ * that reads what is chosen, or "Any".
535
+ *
536
+ * A 418-value vocabulary rendered as a permanently-open box plus a scrolling
537
+ * list is right in a phone sheet, where the sheet IS the disclosure and there
538
+ * is one group on screen. In a 280px rail it is the whole rail: the reference
539
+ * classified draws the make as a field reading "Any" that opens the
540
+ * searchable list, and every axis under it stays reachable at a glance.
541
+ *
542
+ * A native `<button role="combobox">` rather than antd's `Select`, for the
543
+ * same reason the option pills are native buttons: the list underneath is
544
+ * this component's — it carries per-option counts, a chosen block and a fold
545
+ * — and a `Select` that only lends its trigger is a dependency on a popup
546
+ * layer for a border. `aria-expanded` on a real button is the disclosure
547
+ * pattern; Escape closes, and the field keeps focus so the next Tab goes
548
+ * where the person expects.
549
+ */
550
+ const DICTIONARY_FIELD: CSSProperties = {
551
+ display: "flex",
552
+ alignItems: "center",
553
+ gap: spacing[1],
554
+ inlineSize: "100%",
555
+ minBlockSize: controls.height,
556
+ paddingInline: spacing[2],
557
+ paddingBlock: spacing[1],
558
+ border: `1px solid ${cssVar("border")}`,
559
+ borderRadius: cssVar("radius-md"),
560
+ background: cssVar("surface"),
561
+ color: "inherit",
562
+ font: "inherit",
563
+ textAlign: "start",
564
+ cursor: "pointer",
565
+ };
566
+
567
+ /** The chosen values, or the word for "no constraint on this axis". Never a
568
+ * count: "3 chosen" makes a person open the field to find out which three. */
569
+ const DICTIONARY_FIELD_TEXT: CSSProperties = {
570
+ flex: "1 1 auto",
571
+ minInlineSize: 0,
572
+ overflow: "hidden",
573
+ textOverflow: "ellipsis",
574
+ whiteSpace: "nowrap",
575
+ };
576
+
577
+ function DictionaryField(props: {
578
+ readonly group: FacetGroup;
579
+ readonly onToggle: (slug: string, value: string) => void;
580
+ readonly visible: number;
581
+ }): ReactElement {
582
+ const t = useT();
583
+ const { group } = props;
584
+ const [open, setOpen] = useState(false);
585
+ const chosen = group.options.filter((option) => option.selected);
586
+ const text =
587
+ chosen.length > 0
588
+ ? chosen.map((option) => option.label).join(", ")
589
+ : t(SEARCH_I18N_KEYS.facetsDictionaryAny);
590
+ return (
591
+ <Flex vertical gap={spacing[1]}>
592
+ <button
593
+ type="button"
594
+ role="combobox"
595
+ aria-expanded={open}
596
+ aria-haspopup="listbox"
597
+ aria-label={group.label}
598
+ style={DICTIONARY_FIELD}
599
+ data-testid={`facet-dictionary-field-${group.slug}`}
600
+ data-chosen={chosen.length}
601
+ data-analytics="none"
602
+ data-analytics-reason="opening a filter group is a read, not a flow step"
603
+ onKeyDown={(event) => {
604
+ if (event.key === "Escape" && open) {
605
+ event.preventDefault();
606
+ setOpen(false);
607
+ }
608
+ if (event.key === "ArrowDown" && !open) {
609
+ event.preventDefault();
610
+ setOpen(true);
611
+ }
612
+ }}
613
+ onClick={() => {
614
+ setOpen((was) => !was);
615
+ }}
616
+ >
617
+ <span style={DICTIONARY_FIELD_TEXT}>{text}</span>
618
+ <ChevronGlyph open={open} />
619
+ </button>
620
+ {open && (
621
+ <DictionaryBody
622
+ group={group}
623
+ onToggle={props.onToggle}
624
+ visible={props.visible}
625
+ />
626
+ )}
627
+ </Flex>
628
+ );
629
+ }
630
+
522
631
  export interface FacetGroupControlProps {
523
632
  readonly group: FacetGroup;
524
633
  readonly onToggle: (slug: string, value: string) => void;
@@ -538,6 +647,14 @@ export interface FacetGroupControlProps {
538
647
  /** Whether a `collapsible` group STARTS open. Default `true`. The initial
539
648
  * value only — the person owns the state after the first click. */
540
649
  readonly defaultOpen?: boolean;
650
+ /**
651
+ * How a `"dictionary"` group is drawn. `"field"` is the desktop shape — a
652
+ * select-style field reading its chosen values or "Any", which opens the
653
+ * searchable list; `"inline"` (the default) keeps the list open, the shape
654
+ * a phone sheet wants because the sheet is already the disclosure.
655
+ * Meaningless for the other three shapes.
656
+ */
657
+ readonly dictionaryMode?: "field" | "inline";
541
658
  }
542
659
 
543
660
  export function FacetGroupControl(props: FacetGroupControlProps): ReactElement {
@@ -618,13 +735,21 @@ export function FacetGroupControl(props: FacetGroupControlProps): ReactElement {
618
735
  {/* Closed means NOT RENDERED, not hidden: a hundred `display:none`
619
736
  checkboxes are still a hundred stops for a screen reader, and the
620
737
  measured rail held 118 of them. */}
621
- {open && shape === "dictionary" && (
622
- <DictionaryBody
623
- group={group}
624
- onToggle={props.onToggle}
625
- visible={limit ?? FACET_VISIBLE_OPTIONS}
626
- />
627
- )}
738
+ {open &&
739
+ shape === "dictionary" &&
740
+ (props.dictionaryMode === "field" ? (
741
+ <DictionaryField
742
+ group={group}
743
+ onToggle={props.onToggle}
744
+ visible={limit ?? FACET_VISIBLE_OPTIONS}
745
+ />
746
+ ) : (
747
+ <DictionaryBody
748
+ group={group}
749
+ onToggle={props.onToggle}
750
+ visible={limit ?? FACET_VISIBLE_OPTIONS}
751
+ />
752
+ ))}
628
753
 
629
754
  {open && shape !== "dictionary" && (
630
755
  <>
@@ -95,7 +95,11 @@ import type { FacetPanelBag } from "../headless/FacetPanel.js";
95
95
  import type { FacetLabelResolver } from "../headless/useFacetLabels.js";
96
96
  import { useSearchState } from "../headless/SearchStateProvider.js";
97
97
  import { useAppliedCount } from "../headless/useAppliedCount.js";
98
- import { facetCoverage, orderFacetGroups } from "../state/facets.js";
98
+ import {
99
+ facetCoverage,
100
+ facetGroupIsDrawable,
101
+ orderFacetGroupsBySchema,
102
+ } from "../state/facets.js";
99
103
  import type { FacetGroup } from "../state/facets.js";
100
104
  import { FacetGroupControl } from "./FacetGroupControl.js";
101
105
  import { buildRangeGroups } from "../state/ranges.js";
@@ -176,6 +180,17 @@ export const FACET_OPEN_GROUPS = 5;
176
180
  */
177
181
  export const FACET_SEARCH_THRESHOLD = 6;
178
182
 
183
+ /**
184
+ * How many groups the rail draws before the rest go behind one control.
185
+ *
186
+ * The reference classified shows a make, a price, a year and a handful of
187
+ * body axes, and then the word "all filters" — eight is the count that fills
188
+ * a 900px rail once the partition row and the price have taken their share,
189
+ * and it is the point past which a person is scanning rather than reading.
190
+ * The tail is not hidden: `facetsAllFilters` names how many are in it.
191
+ */
192
+ export const FACET_VISIBLE_GROUPS = 8;
193
+
179
194
  /** What a host's category control is handed. */
180
195
  export interface CategoryFilterSlotProps {
181
196
  /** The `root/leaf` path the search is narrowed to, if any. */
@@ -256,6 +271,37 @@ export interface FacetPanelPaneProps extends ThemeModeProp {
256
271
  * count-bearing bar above that one would be the same sentence twice.
257
272
  */
258
273
  readonly footerBar?: boolean;
274
+ /**
275
+ * The partition control, drawn at the TOP of the panel — above the price,
276
+ * above every facet.
277
+ *
278
+ * A partition (`children_as: "chips"`) is not a filter among filters: it is
279
+ * which of one template's halves the page is about, and the reference
280
+ * classified puts it first for that reason (a car-type row: all, used,
281
+ * new). It is a slot rather than a component because the
282
+ * children come from the catalogue tree, which is `categories-react`'s;
283
+ * `<PartitionChips variant="segmented">` is what a host usually puts here.
284
+ */
285
+ readonly partition?: ReactNode;
286
+ /**
287
+ * Slugs pinned above every other group, in the order given — the axis a
288
+ * page has already decided is its subject. See
289
+ * {@link orderFacetGroupsBySchema}.
290
+ */
291
+ readonly pinnedFacets?: readonly string[];
292
+ /**
293
+ * How many groups before the tail folds under "All filters (K)". Default
294
+ * {@link FACET_VISIBLE_GROUPS}; `null` draws every group, which is what a
295
+ * phone sheet devoted to filtering wants.
296
+ */
297
+ readonly visibleGroups?: number | null;
298
+ /**
299
+ * How a DICTIONARY group is drawn. `"field"` is the desktop shape — a
300
+ * select-style field reading its chosen values or "Any", which opens the
301
+ * searchable list; `"inline"` (the default) keeps the list open, which is
302
+ * the phone sheet's shape because a sheet is already a disclosure.
303
+ */
304
+ readonly dictionaryMode?: "field" | "inline";
259
305
  }
260
306
 
261
307
  /**
@@ -463,6 +509,10 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
463
509
  // panel is drawn, never what the search is, so it must not survive into a
464
510
  // shared link the way everything in `useSearchState` does.
465
511
  const [filterQuery, setFilterQuery] = useState("");
512
+ // Whether the tail past `visibleGroups` is open. Presentation, like the
513
+ // panel's own search box: the URL is the search, and how much of the rail a
514
+ // person has unfolded is not part of it.
515
+ const [tailOpen, setTailOpen] = useState(false);
466
516
 
467
517
  return (
468
518
  <SkinTheme {...(props.mode !== undefined ? { mode: props.mode } : {})}>
@@ -540,6 +590,12 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
540
590
  )}
541
591
  </Flex>
542
592
 
593
+ {/* The partition first: which half of one template this page is
594
+ about is not a filter among filters. */}
595
+ {props.partition !== undefined && (
596
+ <div data-testid="search-partition">{props.partition}</div>
597
+ )}
598
+
543
599
  <CategoryFilter
544
600
  {...(props.renderCategoryFilter !== undefined
545
601
  ? { render: props.renderCategoryFilter }
@@ -617,25 +673,34 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
617
673
  // case: a `ref_select` whose config is a bare pointer into
618
674
  // a vocabulary this pair cannot read. A heading with no
619
675
  // control under it names nothing, so it is not drawn.
620
- // Evidence order, the chip row's rule applied to the rail:
621
- // answered axes first, then the ones this corpus actually
622
- // fills. Schema order is the catalogue importer's, and an
623
- // imported catalogue's is alphabetical-by-accident.
624
- const drawable = orderFacetGroups(
625
- groups.filter((group) => group.options.length > 0)
626
- );
676
+ // SCHEMA order, required first see
677
+ // `orderFacetGroupsBySchema`. The rail ranked by evidence for
678
+ // two releases, which on a three-listing cars leaf put
679
+ // condition and colour above make, model and year:
680
+ // the busiest axis is the right question for a chip row with
681
+ // room for four and the wrong one for the column a person
682
+ // narrows a catalogue in. Groups the schema does not name
683
+ // keep evidence order among themselves.
684
+ const drawable = orderFacetGroupsBySchema({
685
+ groups: groups.filter(facetGroupIsDrawable),
686
+ ...(props.categoryFeatures !== undefined
687
+ ? { categoryFeatures: props.categoryFeatures }
688
+ : {}),
689
+ ...(props.pinnedFacets !== undefined
690
+ ? { pinned: props.pinnedFacets }
691
+ : {}),
692
+ });
627
693
  // Which groups OPEN — see the module note. Chosen groups are
628
- // open unconditionally below; here the answer's evidence
629
- // picks the rest: the top counted groups by coverage, and a
630
- // group the server never counted sums to zero, so the wall
631
- // of "not counted" rows starts as headers.
632
- const openByEvidence = new Set(
694
+ // open unconditionally below; the rest are the first
695
+ // FACET_OPEN_GROUPS of the order above, so the panel's first
696
+ // screen is the axes the category itself calls required. A
697
+ // group the server never counted starts as a header, which is
698
+ // what keeps the wall of "not counted" rows folded.
699
+ const openByOrder = new Set(
633
700
  drawable
634
- .filter((group) => group.counted)
635
- .map((group) => [group, facetCoverage(group)] as const)
636
- .sort((a, b) => b[1] - a[1])
701
+ .filter((group) => group.counted || facetCoverage(group) > 0)
637
702
  .slice(0, FACET_OPEN_GROUPS)
638
- .map(([group]) => group.slug)
703
+ .map((group) => group.slug)
639
704
  );
640
705
  const searchable = drawable.length >= FACET_SEARCH_THRESHOLD;
641
706
  const needle = searchable
@@ -648,6 +713,33 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
648
713
  );
649
714
  const listed =
650
715
  needle === "" ? drawable : drawable.filter(matches);
716
+ // The tail. Only while nothing is typed: a query has already
717
+ // narrowed the list, and folding its answer would hide the
718
+ // thing that was looked for. One group over the limit is not
719
+ // folded — a control that reveals exactly one heading costs
720
+ // more than it saves.
721
+ const groupLimit =
722
+ props.visibleGroups === null
723
+ ? null
724
+ : (props.visibleGroups ?? FACET_VISIBLE_GROUPS);
725
+ const tailFolded =
726
+ groupLimit !== null &&
727
+ needle === "" &&
728
+ listed.length > groupLimit + 1;
729
+ // A CONSTRAINT NEVER FOLDS. The fold hides axes a person has
730
+ // not touched; a group they have chosen a value in stays in
731
+ // the visible band wherever the schema put it, because the
732
+ // control that removes a filter is the one they came back
733
+ // for. (The rail used to rank answered axes to the top for
734
+ // this; schema order is stable under a click, which a rail
735
+ // that reshuffles as you tick is not.)
736
+ const shownGroups =
737
+ tailFolded && !tailOpen && groupLimit !== null
738
+ ? listed.filter(
739
+ (group, index) =>
740
+ index < groupLimit || group.selected.length > 0
741
+ )
742
+ : listed;
651
743
  return (
652
744
  <Flex vertical gap={spacing[4]}>
653
745
  {searchable && (
@@ -684,7 +776,7 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
684
776
  a closed header is not an answer. Remounting is the
685
777
  honest way to re-ask the question; the person's own
686
778
  opens and closes come back when the box clears. */}
687
- {listed.map((group) => (
779
+ {shownGroups.map((group) => (
688
780
  <FacetGroupControl
689
781
  key={needle === "" ? group.slug : `${group.slug}:match`}
690
782
  group={group}
@@ -693,10 +785,30 @@ export function FacetPanelPane(props: FacetPanelPaneProps): ReactElement {
693
785
  defaultOpen={
694
786
  needle !== "" ||
695
787
  group.selected.length > 0 ||
696
- openByEvidence.has(group.slug)
788
+ openByOrder.has(group.slug)
697
789
  }
790
+ {...(props.dictionaryMode !== undefined
791
+ ? { dictionaryMode: props.dictionaryMode }
792
+ : {})}
698
793
  />
699
794
  ))}
795
+ {tailFolded && (
796
+ <Button
797
+ style={{ alignSelf: "flex-start" }}
798
+ data-testid="facets-all-filters"
799
+ data-analytics="none"
800
+ data-analytics-reason="opening the filter tail is a read, not a flow step"
801
+ onClick={() => {
802
+ setTailOpen((was) => !was);
803
+ }}
804
+ >
805
+ {tailOpen
806
+ ? t(SEARCH_I18N_KEYS.facetsShowLess)
807
+ : t(SEARCH_I18N_KEYS.facetsAllFilters, {
808
+ count: listed.length - (groupLimit ?? 0),
809
+ })}
810
+ </Button>
811
+ )}
700
812
  <Typography.Text type="secondary">
701
813
  {t(SEARCH_I18N_KEYS.facetsDrillDownHint)}
702
814
  </Typography.Text>