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