@redocly/theme 0.40.6 → 0.41.0-rc.1

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 (101) hide show
  1. package/lib/components/Dropdown/DropdownMenu.d.ts +2 -0
  2. package/lib/components/Dropdown/DropdownMenu.js +3 -1
  3. package/lib/components/Loaders/SpinnerLoader.d.ts +5 -0
  4. package/lib/components/Loaders/SpinnerLoader.js +32 -0
  5. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.d.ts +12 -0
  6. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +113 -0
  7. package/lib/components/Search/FilterFields/SearchFilterFieldTags.d.ts +10 -0
  8. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +37 -0
  9. package/lib/components/Search/Search.js +1 -1
  10. package/lib/components/Search/SearchDialog.js +103 -26
  11. package/lib/components/Search/SearchFilter.d.ts +11 -0
  12. package/lib/components/Search/SearchFilter.js +71 -0
  13. package/lib/components/Search/SearchFilterField.d.ts +11 -0
  14. package/lib/components/Search/SearchFilterField.js +43 -0
  15. package/lib/components/Search/SearchGroups.d.ts +9 -0
  16. package/lib/components/Search/SearchGroups.js +69 -0
  17. package/lib/components/Search/SearchHighlight.d.ts +1 -1
  18. package/lib/components/Search/SearchHighlight.js +28 -5
  19. package/lib/components/Search/SearchInput.d.ts +3 -2
  20. package/lib/components/Search/SearchInput.js +11 -3
  21. package/lib/components/Search/SearchItem.d.ts +2 -2
  22. package/lib/components/Search/SearchItem.js +23 -15
  23. package/lib/components/Search/variables.js +48 -2
  24. package/lib/components/Segmented/Segmented.d.ts +2 -5
  25. package/lib/components/Select/Select.d.ts +2 -36
  26. package/lib/components/Select/Select.js +110 -98
  27. package/lib/components/Select/SelectInput.d.ts +22 -0
  28. package/lib/components/Select/SelectInput.js +118 -0
  29. package/lib/components/Select/variables.js +11 -1
  30. package/lib/components/Tag/Tag.d.ts +4 -2
  31. package/lib/components/Tag/Tag.js +40 -4
  32. package/lib/components/Tag/variables.dark.js +20 -5
  33. package/lib/components/Tag/variables.js +49 -17
  34. package/lib/components/VersionPicker/VersionPicker.d.ts +2 -3
  35. package/lib/components/VersionPicker/VersionPicker.js +13 -30
  36. package/lib/core/hooks/__mocks__/index.d.ts +1 -1
  37. package/lib/core/hooks/__mocks__/index.js +1 -1
  38. package/lib/core/hooks/__mocks__/use-theme-hooks.d.ts +1 -1
  39. package/lib/core/hooks/__mocks__/use-theme-hooks.js +1 -1
  40. package/lib/core/hooks/index.d.ts +2 -1
  41. package/lib/core/hooks/index.js +2 -1
  42. package/lib/core/hooks/search/use-recent-searches.js +2 -0
  43. package/lib/core/hooks/{use-search.d.ts → search/use-search-dialog.d.ts} +1 -1
  44. package/lib/core/hooks/{use-search.js → search/use-search-dialog.js} +5 -5
  45. package/lib/core/hooks/search/use-search-filter.d.ts +9 -0
  46. package/lib/core/hooks/search/use-search-filter.js +50 -0
  47. package/lib/core/types/hooks.d.ts +16 -4
  48. package/lib/core/types/index.d.ts +1 -1
  49. package/lib/core/types/index.js +1 -1
  50. package/lib/core/types/l10n.d.ts +1 -1
  51. package/lib/core/types/search.d.ts +43 -2
  52. package/lib/core/types/select.d.ts +29 -0
  53. package/lib/core/types/{select-option.js → select.js} +1 -1
  54. package/lib/icons/ResetIcon/ResetIcon.d.ts +9 -0
  55. package/lib/icons/ResetIcon/ResetIcon.js +22 -0
  56. package/lib/icons/SettingsIcon/SettingsIcon.d.ts +9 -0
  57. package/lib/icons/SettingsIcon/SettingsIcon.js +23 -0
  58. package/lib/index.d.ts +7 -1
  59. package/lib/index.js +7 -1
  60. package/package.json +2 -2
  61. package/src/components/Dropdown/DropdownMenu.tsx +2 -1
  62. package/src/components/Filter/FilterSelect.tsx +3 -3
  63. package/src/components/Loaders/SpinnerLoader.tsx +31 -0
  64. package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +134 -0
  65. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +61 -0
  66. package/src/components/Search/Search.tsx +2 -2
  67. package/src/components/Search/SearchDialog.tsx +168 -42
  68. package/src/components/Search/SearchFilter.tsx +90 -0
  69. package/src/components/Search/SearchFilterField.tsx +84 -0
  70. package/src/components/Search/SearchGroups.tsx +80 -0
  71. package/src/components/Search/SearchHighlight.tsx +29 -2
  72. package/src/components/Search/SearchInput.tsx +17 -3
  73. package/src/components/Search/SearchItem.tsx +38 -24
  74. package/src/components/Search/variables.ts +48 -2
  75. package/src/components/Segmented/Segmented.tsx +2 -2
  76. package/src/components/Select/Select.tsx +170 -157
  77. package/src/components/Select/SelectInput.tsx +184 -0
  78. package/src/components/Select/variables.ts +11 -1
  79. package/src/components/Tag/Tag.tsx +57 -6
  80. package/src/components/Tag/variables.dark.ts +20 -5
  81. package/src/components/Tag/variables.ts +49 -17
  82. package/src/components/VersionPicker/VersionPicker.tsx +15 -39
  83. package/src/core/hooks/__mocks__/index.ts +1 -1
  84. package/src/core/hooks/__mocks__/use-theme-hooks.ts +1 -1
  85. package/src/core/hooks/index.ts +2 -1
  86. package/src/core/hooks/search/use-recent-searches.ts +3 -0
  87. package/src/core/hooks/{use-search.ts → search/use-search-dialog.ts} +1 -1
  88. package/src/core/hooks/search/use-search-filter.ts +57 -0
  89. package/src/core/types/hooks.ts +24 -4
  90. package/src/core/types/index.ts +1 -1
  91. package/src/core/types/l10n.ts +7 -1
  92. package/src/core/types/search.ts +54 -2
  93. package/src/core/types/select.ts +31 -0
  94. package/src/icons/ResetIcon/ResetIcon.tsx +26 -0
  95. package/src/icons/SettingsIcon/SettingsIcon.tsx +30 -0
  96. package/src/index.ts +7 -1
  97. package/lib/core/types/select-option.d.ts +0 -4
  98. package/src/core/types/select-option.ts +0 -4
  99. /package/lib/components/{Loading → Loaders}/Loading.d.ts +0 -0
  100. /package/lib/components/{Loading → Loaders}/Loading.js +0 -0
  101. /package/src/components/{Loading → Loaders}/Loading.tsx +0 -0
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { SearchFacet, SearchFacetCount } from '@redocly/theme/core/types';
5
+
6
+ import { Tag, type TagProps } from '@redocly/theme/components/Tag/Tag';
7
+
8
+ type SearchFilterFieldTagsProps = {
9
+ className?: string;
10
+ facet: SearchFacet;
11
+ selectedValues: string[];
12
+ onChange: (value: string | string[]) => void;
13
+ };
14
+
15
+ export function SearchFilterFieldTags({
16
+ className,
17
+ facet,
18
+ selectedValues,
19
+ onChange,
20
+ }: SearchFilterFieldTagsProps) {
21
+ return (
22
+ <FilterTagsWrapper
23
+ data-component-name="Search/FilterFields/SearchFilterFieldTags"
24
+ className={className}
25
+ >
26
+ {facet.values.map((facetCount, index) => {
27
+ const { value, count, isCounterVisible } = facetCount as SearchFacetCount;
28
+ const active = selectedValues.includes(value);
29
+ return (
30
+ <FilterTagWrapper
31
+ key={`${count}-${index}`}
32
+ color={value}
33
+ onClick={() => {
34
+ const values = active
35
+ ? selectedValues.filter((item) => item !== value)
36
+ : [...selectedValues, value];
37
+ onChange(values);
38
+ }}
39
+ active={active}
40
+ borderless
41
+ >
42
+ {value} {isCounterVisible && <span>{count}</span>}
43
+ </FilterTagWrapper>
44
+ );
45
+ })}
46
+ </FilterTagsWrapper>
47
+ );
48
+ }
49
+
50
+ const FilterTagsWrapper = styled.div`
51
+ display: flex;
52
+ flex-wrap: wrap;
53
+ gap: var(--search-filter-field-tags-gap);
54
+ `;
55
+
56
+ const FilterTagWrapper = styled(Tag)<{ color: TagProps['color'] }>`
57
+ text-transform: uppercase;
58
+ cursor: pointer;
59
+ ${({ color }) => color && `background-color: var(--tag-operation-bg-color-${color});`}
60
+ margin: var(--search-filter-field-tags-tag-margin);
61
+ `;
@@ -3,14 +3,14 @@ import styled from 'styled-components';
3
3
 
4
4
  import { SearchTrigger } from '@redocly/theme/components/Search/SearchTrigger';
5
5
  import { SearchDialog } from '@redocly/theme/components/Search/SearchDialog';
6
- import { useSearch } from '@redocly/theme/core/hooks';
6
+ import { useSearchDialog } from '@redocly/theme/core/hooks';
7
7
 
8
8
  export type SearchProps = {
9
9
  className?: string;
10
10
  };
11
11
 
12
12
  export function Search({ className }: SearchProps): JSX.Element {
13
- const { isOpen, onOpen, onClose } = useSearch();
13
+ const { isOpen, onOpen, onClose } = useSearchDialog();
14
14
 
15
15
  return (
16
16
  <SearchWrapper data-component-name="Search/Search" className={className}>
@@ -1,8 +1,8 @@
1
- import React, { useRef, useState } from 'react';
1
+ import React, { Fragment, useRef, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { MouseEvent } from 'react';
5
- import type { SearchDocument } from '@redocly/theme/core/types';
5
+ import type { SearchFacetCount, SearchItemData } from '@redocly/theme/core/types';
6
6
 
7
7
  import { SearchInput } from '@redocly/theme/components/Search/SearchInput';
8
8
  import { SearchShortcut } from '@redocly/theme/components/Search/SearchShortcut';
@@ -11,9 +11,12 @@ import { breakpoints, concatClassNames } from '@redocly/theme/core/utils';
11
11
  import { SearchItem } from '@redocly/theme/components/Search/SearchItem';
12
12
  import { SearchRecent } from '@redocly/theme/components/Search/SearchRecent';
13
13
  import { SearchSuggestedPages } from '@redocly/theme/components/Search/SearchSuggestedPages';
14
- import { useThemeHooks, useDialogHotKeys } from '@redocly/theme/core/hooks';
14
+ import { useThemeHooks, useDialogHotKeys, useSearchFilter } from '@redocly/theme/core/hooks';
15
15
  import { Tag } from '@redocly/theme/components/Tag/Tag';
16
16
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
17
+ import { SearchFilter } from '@redocly/theme/components/Search/SearchFilter';
18
+ import { SearchGroups } from '@redocly/theme/components/Search/SearchGroups';
19
+ import { SpinnerLoader } from '@redocly/theme/components/Loaders/SpinnerLoader';
17
20
 
18
21
  export type SearchDialogProps = {
19
22
  onClose: () => void;
@@ -21,11 +24,20 @@ export type SearchDialogProps = {
21
24
  };
22
25
 
23
26
  export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Element {
24
- const { useTranslate, useCurrentProduct, useFuseSearch, useProducts } = useThemeHooks();
27
+ const { useTranslate, useCurrentProduct, useSearch, useProducts } = useThemeHooks();
25
28
  const products = useProducts();
26
29
  const currentProduct = useCurrentProduct();
27
30
  const [product, setProduct] = useState(currentProduct);
28
- const { query, setQuery, items, isLoading } = useFuseSearch(product?.name);
31
+ const { query, setQuery, filter, setFilter, items, isSearchLoading, facets, setLoadMore } =
32
+ useSearch(product?.name);
33
+ const {
34
+ isFilterOpen,
35
+ onFilterToggle,
36
+ onFilterChange,
37
+ onFilterReset,
38
+ onFacetReset,
39
+ onTopFacetsReset,
40
+ } = useSearchFilter(filter, setFilter);
29
41
  const modalRef = useRef<HTMLDivElement>(null);
30
42
  const { translate } = useTranslate();
31
43
 
@@ -39,19 +51,35 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
39
51
  }
40
52
  };
41
53
 
42
- const mapItem = (item: SearchDocument) => {
54
+ const mapItem = (item: SearchItemData) => {
43
55
  let itemProduct;
44
- if (!product && item.product) {
56
+ if (!product && item.document.product) {
45
57
  const resolvedProduct = products.find((product) =>
46
- product.slug.match(`/${item.product?.folder}/`),
58
+ product.slug.match(`/${item.document.product?.folder}/`),
47
59
  );
48
60
  itemProduct = resolvedProduct
49
61
  ? { name: resolvedProduct.name, icon: resolvedProduct.icon }
50
62
  : undefined;
51
63
  }
52
- return <SearchItem key={item.id} item={item} product={itemProduct} />;
64
+ return <SearchItem key={item.document.id} item={item} product={itemProduct} />;
53
65
  };
54
66
 
67
+ const showLoadMore = (groupKey: string, currentCount: number = 0) => {
68
+ const topFacet = facets.find((facet) => facet.isTop);
69
+ let needLoadMore = false;
70
+ if (topFacet) {
71
+ const groupValue = topFacet.values.find((value) => {
72
+ if (typeof value === 'object') {
73
+ return value.value === groupKey;
74
+ } else return false;
75
+ }) as SearchFacetCount;
76
+ needLoadMore = groupValue ? groupValue.count > currentCount : false;
77
+ }
78
+ return needLoadMore;
79
+ };
80
+
81
+ const showResults = !!((filter && filter.length) || query);
82
+
55
83
  return (
56
84
  <SearchOverlay
57
85
  data-component-name="Search/SearchDialog"
@@ -61,34 +89,83 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
61
89
  >
62
90
  <SearchDialogWrapper className="scroll-lock" role="dialog">
63
91
  <SearchDialogHeader>
92
+ {product && (
93
+ <>
94
+ <SearchProductTag color="product">
95
+ {product.name}
96
+ <CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
97
+ </SearchProductTag>
98
+ </>
99
+ )}
64
100
  <SearchInput
65
101
  value={query}
66
102
  onChange={setQuery}
103
+ onFilterToggle={onFilterToggle}
67
104
  placeholder={translate('theme.search.label', 'Search docs...')}
68
- isLoading={isLoading}
105
+ isLoading={isSearchLoading}
69
106
  data-translation-key="theme.search.label"
70
107
  />
71
- {product && (
72
- <SearchProductTag color="product">
73
- {product.name}
74
- <CloseIcon onClick={() => setProduct(undefined)} color="--icon-color-additional" />
75
- </SearchProductTag>
76
- )}
77
108
  </SearchDialogHeader>
78
109
  <SearchDialogBody>
79
- {items !== null ? (
80
- items.length ? (
81
- items.map(mapItem)
110
+ <SearchDialogBodyMainView>
111
+ <SearchGroups
112
+ facets={facets}
113
+ searchFilter={filter}
114
+ onFilterChange={onFilterChange}
115
+ onTopFacetsReset={onTopFacetsReset}
116
+ />
117
+ {showResults ? (
118
+ items && Object.keys(items).some((key) => items[key]?.length) ? (
119
+ Object.keys(items).map((key) =>
120
+ items[key]?.length ? (
121
+ <Fragment key={key}>
122
+ <SearchGroupTitle>{key}</SearchGroupTitle>
123
+ {items[key]?.map(mapItem)}
124
+ {showLoadMore(key, items[key]?.length || 0) && (
125
+ <SearchGroupFooter
126
+ data-translation-key="theme.search.showMore"
127
+ onClick={() =>
128
+ setLoadMore({ groupKey: key, offset: items[key]?.length || 0 })
129
+ }
130
+ >
131
+ {translate('theme.search.showMore', 'Show more')}
132
+ </SearchGroupFooter>
133
+ )}
134
+ </Fragment>
135
+ ) : null,
136
+ )
137
+ ) : isSearchLoading ? (
138
+ <SearchMessage>
139
+ <SpinnerLoader size="26px" color="var(--search-input-icon-color)" />
140
+ {translate('theme.search.loading', 'Loading...')}
141
+ </SearchMessage>
142
+ ) : (
143
+ <SearchMessage data-translation-key="theme.search.noResults">
144
+ <b>{translate('theme.search.noResults.title', 'No results')}</b>
145
+ {translate(
146
+ 'theme.search.noResults.description',
147
+ 'Prease, try with a different query.',
148
+ )}
149
+ </SearchMessage>
150
+ )
82
151
  ) : (
83
- <SearchMessage data-translation-key="theme.search.noResults">
84
- {translate('theme.search.noResults', 'No results')}
85
- </SearchMessage>
86
- )
87
- ) : (
88
- <>
89
- <SearchRecent onSelect={setQuery} />
90
- <SearchSuggestedPages />
91
- </>
152
+ <>
153
+ <SearchRecent onSelect={setQuery} />
154
+ <SearchSuggestedPages />
155
+ </>
156
+ )}
157
+ </SearchDialogBodyMainView>
158
+ {isFilterOpen && (
159
+ <SearchDialogBodyFilterView>
160
+ <SearchFilter
161
+ facets={facets}
162
+ filter={filter}
163
+ query={query}
164
+ onFilterChange={onFilterChange}
165
+ onFilterReset={onFilterReset}
166
+ onFacetReset={onFacetReset}
167
+ />
168
+ </SearchDialogBodyFilterView>
92
169
  )}
93
170
  </SearchDialogBody>
94
171
  <SearchDialogFooter>
@@ -109,6 +186,12 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
109
186
  text={translate('theme.search.keys.exit', 'to exit')}
110
187
  />
111
188
  </SearchShortcuts>
189
+ {isSearchLoading && (
190
+ <SearchLoading>
191
+ <SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
192
+ {translate('theme.search.loading', 'Loading...')}
193
+ </SearchLoading>
194
+ )}
112
195
  <SearchCancelButton
113
196
  data-translation-key="theme.search.cancel"
114
197
  variant="secondary"
@@ -139,7 +222,6 @@ const SearchOverlay = styled.div`
139
222
  const SearchDialogWrapper = styled.div`
140
223
  display: flex;
141
224
  flex-direction: column;
142
- justify-content: space-between;
143
225
  overflow: auto;
144
226
  width: 100vw;
145
227
  height: 100vh;
@@ -155,39 +237,51 @@ const SearchDialogWrapper = styled.div`
155
237
  max-height: 95vh;
156
238
  height: auto;
157
239
  resize: both;
158
- min-width: 300px;
159
- min-height: 200px;
160
240
  }
161
241
  `;
162
242
 
163
243
  const SearchDialogHeader = styled.header`
164
244
  display: flex;
165
245
  align-items: center;
166
- border-bottom: 1px solid var(--border-color-secondary);
167
- background-color: var(--search-input-bg-color);
168
- padding: var(--spacing-sm);
246
+ border-bottom: var(--search-modal-border);
247
+ background-color: var(--search-modal-header-bg-color);
248
+ padding: var(--search-modal-header-padding);
169
249
  `;
170
250
 
171
251
  const SearchDialogBody = styled.div`
252
+ display: flex;
253
+ flex-direction: row;
172
254
  flex-grow: 1;
173
- overflow-y: scroll;
174
- overscroll-behavior: contain;
175
255
 
176
256
  @media screen and (min-width: ${breakpoints.small}) {
177
257
  height: var(--search-modal-min-height);
178
258
  }
179
259
  `;
180
260
 
261
+ const SearchDialogBodyMainView = styled.div`
262
+ flex: 2;
263
+ flex-grow: 2;
264
+ overflow-y: scroll;
265
+ overscroll-behavior: contain;
266
+ border-right: var(--search-modal-border);
267
+ `;
268
+
269
+ const SearchDialogBodyFilterView = styled.div`
270
+ overflow: scroll;
271
+ `;
272
+
181
273
  const SearchDialogFooter = styled.footer`
182
- padding: var(--spacing-sm);
183
- border-top: 1px solid var(--border-color-secondary);
274
+ display: flex;
275
+ gap: var(--search-modal-footer-gap);
276
+ padding: var(--search-modal-footer-padding);
277
+ border-top: var(--search-modal-border);
184
278
  `;
185
279
 
186
280
  const SearchShortcuts = styled.div`
187
281
  display: none;
188
282
  justify-content: flex-start;
189
283
  align-items: center;
190
- gap: var(--spacing-xs);
284
+ gap: var(--search-shortcuts-gap);
191
285
 
192
286
  @media screen and (min-width: ${breakpoints.small}) {
193
287
  display: flex;
@@ -195,13 +289,21 @@ const SearchShortcuts = styled.div`
195
289
  `;
196
290
 
197
291
  const SearchMessage = styled.div`
198
- padding: var(--spacing-lg);
199
- color: var(--search-item-title-text-color);
292
+ display: flex;
293
+ height: 40%;
294
+ justify-content: center;
295
+ align-items: center;
296
+ flex-direction: column;
297
+ font-size: var(--search-message-font-size);
298
+ font-weight: var(--search-message-font-weight);
299
+ line-height: var(--search-message-line-height);
300
+ color: var(--search-message-text-color);
301
+ gap: var(--search-message-gap);
200
302
  `;
201
303
 
202
304
  const SearchProductTag = styled(Tag)`
203
305
  --tag-border-radius: var(--border-radius);
204
-
306
+ border: none;
205
307
  margin: var(--spacing-xs) var(--spacing-sm) !important;
206
308
  `;
207
309
 
@@ -212,3 +314,27 @@ const SearchCancelButton = styled(Button)`
212
314
  display: none;
213
315
  }
214
316
  `;
317
+
318
+ const SearchGroupTitle = styled.div`
319
+ border-bottom: var(--search-modal-border);
320
+ padding: var(--search-group-title-padding);
321
+ background-color: var(--search-group-title-bg-color);
322
+ `;
323
+
324
+ const SearchGroupFooter = styled.div`
325
+ display: flex;
326
+ justify-content: center;
327
+ padding: var(--search-group-footer-padding);
328
+ color: var(--search-group-footer-text-color);
329
+ cursor: pointer;
330
+ `;
331
+
332
+ const SearchLoading = styled.div`
333
+ display: none;
334
+ align-items: center;
335
+ gap: var(--spacing-xs);
336
+
337
+ @media screen and (min-width: ${breakpoints.small}) {
338
+ display: flex;
339
+ }
340
+ `;
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { SearchFilterItem, SearchFacet } from '@redocly/theme/core/types';
5
+
6
+ import { Button } from '@redocly/theme/components/Button/Button';
7
+ import { CleanIcon } from '@redocly/theme/icons/CleanIcon/CleanIcon';
8
+ import { SearchFilterField } from '@redocly/theme/components/Search/SearchFilterField';
9
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
10
+
11
+ export type SearchFilterProps = {
12
+ className?: string;
13
+ facets: SearchFacet[];
14
+ filter: SearchFilterItem[];
15
+ query: string;
16
+ onFilterChange: (field: string, value: string | string[], isTop?: boolean) => void;
17
+ onFilterReset: () => void;
18
+ onFacetReset: (field: string) => void;
19
+ };
20
+
21
+ export function SearchFilter({
22
+ className,
23
+ facets,
24
+ filter,
25
+ query,
26
+ onFilterChange,
27
+ onFilterReset,
28
+ onFacetReset,
29
+ }: SearchFilterProps): JSX.Element {
30
+ const { useTranslate } = useThemeHooks();
31
+ const { translate } = useTranslate();
32
+ return (
33
+ <SearchFilterWrapper data-component-name="Search/SearchFilter" className={className}>
34
+ <SearchFilterHeader>
35
+ <span data-translation-key="theme.search.filter.title">
36
+ {translate('theme.search.filter.title', 'Advanced filter')}
37
+ </span>
38
+ <Button
39
+ data-translation-key="theme.search.filter.reset"
40
+ onClick={onFilterReset}
41
+ variant="ghost"
42
+ icon={<CleanIcon />}
43
+ >
44
+ {translate('theme.search.filter.reset', 'Reset filters')}
45
+ </Button>
46
+ </SearchFilterHeader>
47
+
48
+ <SearchFilterFields>
49
+ {facets.map((facet, index) => (
50
+ <SearchFilterField
51
+ key={`${facet.field}-${index}`}
52
+ facet={facet}
53
+ onFilterChange={onFilterChange}
54
+ onFacetReset={onFacetReset}
55
+ filter={filter}
56
+ query={query}
57
+ />
58
+ ))}
59
+ </SearchFilterFields>
60
+ </SearchFilterWrapper>
61
+ );
62
+ }
63
+
64
+ const SearchFilterWrapper = styled.div`
65
+ width: var(--search-filter-width);
66
+ display: flex;
67
+ flex-direction: column;
68
+ padding: var(--search-filter-padding);
69
+ font-size: var(--search-filter-font-size);
70
+ font-weight: var(--search-filter-font-weight);
71
+ line-height: var(--search-filter-line-height);
72
+ `;
73
+
74
+ const SearchFilterHeader = styled.div`
75
+ position: sticky;
76
+ top: 0px;
77
+ display: flex;
78
+ justify-content: space-between;
79
+ align-items: center;
80
+ padding: var(--search-filter-header-padding);
81
+ color: var(--search-filter-header-text-color);
82
+ background-color: var(--search-filter-bg-color);
83
+ z-index: var(--search-filter-header-z-index);
84
+ `;
85
+
86
+ const SearchFilterFields = styled.div`
87
+ display: flex;
88
+ flex-direction: column;
89
+ gap: var(--search-filter-fields-gap);
90
+ `;
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { SearchFacet, SearchFilterItem } from '@redocly/theme/core/types';
5
+
6
+ import { Button } from '@redocly/theme/components/Button/Button';
7
+ import { ResetIcon } from '@redocly/theme/icons/ResetIcon/ResetIcon';
8
+ import { useThemeHooks } from '@redocly/theme/core';
9
+ import { SearchFilterFieldSelect } from '@redocly/theme/components/Search/FilterFields/SearchFilterFieldSelect';
10
+ import { SearchFilterFieldTags } from '@redocly/theme/components/Search/FilterFields/SearchFilterFieldTags';
11
+
12
+ type SearchFilterFieldProps = {
13
+ className?: string;
14
+ facet: SearchFacet;
15
+ filter: SearchFilterItem[];
16
+ query: string;
17
+ onFilterChange: (field: string, value: string | string[], isTop?: boolean) => void;
18
+ onFacetReset: (filed: string) => void;
19
+ };
20
+
21
+ export function SearchFilterField({
22
+ className,
23
+ facet,
24
+ filter,
25
+ query,
26
+ onFilterChange,
27
+ onFacetReset,
28
+ }: SearchFilterFieldProps): JSX.Element {
29
+ const { useTranslate } = useThemeHooks();
30
+ const { translate } = useTranslate();
31
+ const selectedValues = filter.find((item) => item.field === facet.field)?.values || [];
32
+
33
+ const onChange = (value: string | string[]) => {
34
+ onFilterChange(facet.field, value, facet.isTop);
35
+ };
36
+
37
+ const onReset = () => {
38
+ onFacetReset(facet.field);
39
+ };
40
+
41
+ return (
42
+ <FilterFieldWrapper data-component-name="Search/SearchFilterField" className={className}>
43
+ <FilterFieldLabel>
44
+ {facet.name}
45
+ {selectedValues.length > 0 && (
46
+ <Button
47
+ data-translation-key="theme.search.filter.field.reset"
48
+ icon={<ResetIcon />}
49
+ variant="ghost"
50
+ size="small"
51
+ onClick={onReset}
52
+ >
53
+ {translate('theme.search.filter.field.reset', 'Reset')}
54
+ </Button>
55
+ )}
56
+ </FilterFieldLabel>
57
+ {['select', 'multi-select'].includes(facet.type) && (
58
+ <SearchFilterFieldSelect
59
+ facet={facet}
60
+ filter={filter}
61
+ query={query}
62
+ selectedValues={selectedValues}
63
+ onChange={onChange}
64
+ />
65
+ )}
66
+ {/* Special default HTTP Methods filed */}
67
+ {facet.type === 'tags' && (
68
+ <SearchFilterFieldTags facet={facet} selectedValues={selectedValues} onChange={onChange} />
69
+ )}
70
+ </FilterFieldWrapper>
71
+ );
72
+ }
73
+
74
+ const FilterFieldWrapper = styled.div`
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 4px;
78
+ `;
79
+
80
+ const FilterFieldLabel = styled.div`
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ `;
@@ -0,0 +1,80 @@
1
+ import * as React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { type SearchFacet, type SearchFacetCount, type SearchFilterItem } from '@redocly/theme';
5
+ import { Tag } from '@redocly/theme/components/Tag/Tag';
6
+
7
+ type SearchGroupsProps = {
8
+ facets: SearchFacet[];
9
+ searchFilter: SearchFilterItem[];
10
+ onFilterChange: (field: string, value: string[], isTop?: boolean) => void;
11
+ onTopFacetsReset: () => void;
12
+ };
13
+
14
+ export function SearchGroups({
15
+ facets,
16
+ searchFilter,
17
+ onFilterChange,
18
+ onTopFacetsReset,
19
+ }: SearchGroupsProps): JSX.Element {
20
+ const groupFacets = facets.filter((facet) => facet.isTop);
21
+
22
+ const handleGroupTagClick = (
23
+ value: string,
24
+ fieldId: string,
25
+ active: boolean,
26
+ currentValues: string[],
27
+ ) => {
28
+ const values = active
29
+ ? currentValues.filter((item) => item !== value)
30
+ : [...currentValues, value];
31
+ onFilterChange(fieldId, values, true);
32
+ };
33
+
34
+ return (
35
+ <SearchGroupsWrapper>
36
+ <GroupTag
37
+ borderless
38
+ active={!searchFilter.some((item) => item.isTop)}
39
+ onClick={() => searchFilter.some((item) => item.isTop) && onTopFacetsReset()}
40
+ >
41
+ All
42
+ </GroupTag>
43
+ <Divider />
44
+ {groupFacets.flatMap((facet) =>
45
+ facet.values.map((facetCount, index) => {
46
+ const { value, count, isCounterVisible } = facetCount as SearchFacetCount;
47
+ const currentValues =
48
+ searchFilter.find((item) => item.field === facet.field)?.values || [];
49
+ const active = currentValues?.includes(value);
50
+ return (
51
+ <GroupTag
52
+ key={`${facet.field}-${index}`}
53
+ onClick={() => handleGroupTagClick(value, facet.field, active, currentValues)}
54
+ active={active}
55
+ borderless
56
+ >
57
+ {value} {isCounterVisible && <span>{count}</span>}
58
+ </GroupTag>
59
+ );
60
+ }),
61
+ )}
62
+ </SearchGroupsWrapper>
63
+ );
64
+ }
65
+
66
+ const SearchGroupsWrapper = styled.div`
67
+ display: flex;
68
+ gap: 4px;
69
+ padding: var(--spacing-md);
70
+ `;
71
+
72
+ const GroupTag = styled(Tag)`
73
+ cursor: pointer;
74
+ gap: 4px;
75
+ `;
76
+
77
+ const Divider = styled.div`
78
+ border-right: 1px solid var(--border-color-secondary);
79
+ margin: 5px 5px 5px 0;
80
+ `;