@redocly/theme 0.42.3 → 0.44.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 (179) hide show
  1. package/lib/components/Buttons/CopyButton.js +2 -2
  2. package/lib/components/Buttons/EditPageButton.js +1 -1
  3. package/lib/components/Catalog/CatalogActions.js +1 -1
  4. package/lib/components/Dropdown/DropdownMenu.d.ts +2 -0
  5. package/lib/components/Dropdown/DropdownMenu.js +3 -1
  6. package/lib/components/Feedback/Comment.js +6 -6
  7. package/lib/components/Feedback/Mood.js +7 -7
  8. package/lib/components/Feedback/Rating.js +4 -4
  9. package/lib/components/Feedback/Reasons.js +3 -3
  10. package/lib/components/Feedback/Scale.js +10 -10
  11. package/lib/components/Feedback/Sentiment.js +5 -5
  12. package/lib/components/Filter/FilterContent.js +2 -2
  13. package/lib/components/Filter/FilterInput.js +1 -1
  14. package/lib/components/Filter/FilterPopover.js +2 -2
  15. package/lib/components/Filter/FilterSelect.js +1 -1
  16. package/lib/components/Footer/FooterCopyright.js +2 -2
  17. package/lib/components/LastUpdated/LastUpdated.js +1 -1
  18. package/lib/components/Loaders/SpinnerLoader.d.ts +5 -0
  19. package/lib/components/Loaders/SpinnerLoader.js +32 -0
  20. package/lib/components/PageNavigation/NextButton.js +1 -1
  21. package/lib/components/PageNavigation/PreviousButton.js +1 -1
  22. package/lib/components/Product/ProductPicker.js +1 -1
  23. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.d.ts +12 -0
  24. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +113 -0
  25. package/lib/components/Search/FilterFields/SearchFilterFieldTags.d.ts +10 -0
  26. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +37 -0
  27. package/lib/components/Search/Search.js +1 -1
  28. package/lib/components/Search/SearchDialog.js +113 -31
  29. package/lib/components/Search/SearchFilter.d.ts +11 -0
  30. package/lib/components/Search/SearchFilter.js +71 -0
  31. package/lib/components/Search/SearchFilterField.d.ts +11 -0
  32. package/lib/components/Search/SearchFilterField.js +43 -0
  33. package/lib/components/Search/SearchGroups.d.ts +9 -0
  34. package/lib/components/Search/SearchGroups.js +69 -0
  35. package/lib/components/Search/SearchHighlight.d.ts +1 -1
  36. package/lib/components/Search/SearchHighlight.js +28 -5
  37. package/lib/components/Search/SearchInput.d.ts +1 -1
  38. package/lib/components/Search/SearchInput.js +5 -2
  39. package/lib/components/Search/SearchItem.d.ts +2 -2
  40. package/lib/components/Search/SearchItem.js +24 -15
  41. package/lib/components/Search/SearchRecent.js +1 -1
  42. package/lib/components/Search/SearchSuggestedPages.js +1 -1
  43. package/lib/components/Search/SearchTrigger.js +2 -2
  44. package/lib/components/Search/variables.js +48 -2
  45. package/lib/components/Segmented/Segmented.d.ts +2 -5
  46. package/lib/components/Select/Select.d.ts +2 -36
  47. package/lib/components/Select/Select.js +136 -98
  48. package/lib/components/Select/SelectInput.d.ts +23 -0
  49. package/lib/components/Select/SelectInput.js +129 -0
  50. package/lib/components/Select/variables.js +12 -1
  51. package/lib/components/SidebarActions/ChangeViewButton.js +1 -1
  52. package/lib/components/SidebarActions/SidebarActions.js +2 -2
  53. package/lib/components/TableOfContent/TableOfContent.js +1 -1
  54. package/lib/components/Tag/Tag.d.ts +4 -2
  55. package/lib/components/Tag/Tag.js +40 -4
  56. package/lib/components/Tag/variables.dark.js +20 -5
  57. package/lib/components/Tag/variables.js +49 -17
  58. package/lib/components/UserMenu/LoginButton.js +1 -1
  59. package/lib/components/UserMenu/LogoutMenuItem.js +1 -1
  60. package/lib/components/UserMenu/UserMenu.js +1 -1
  61. package/lib/components/VersionPicker/VersionPicker.d.ts +2 -3
  62. package/lib/components/VersionPicker/VersionPicker.js +14 -31
  63. package/lib/core/hooks/__mocks__/index.d.ts +2 -1
  64. package/lib/core/hooks/__mocks__/index.js +2 -1
  65. package/lib/core/hooks/__mocks__/search/use-search-filter.d.ts +9 -0
  66. package/lib/core/hooks/__mocks__/search/use-search-filter.js +14 -0
  67. package/lib/core/hooks/__mocks__/use-theme-hooks.d.ts +6 -1
  68. package/lib/core/hooks/__mocks__/use-theme-hooks.js +6 -1
  69. package/lib/core/hooks/feedback/use-report-dialog.js +3 -3
  70. package/lib/core/hooks/index.d.ts +2 -1
  71. package/lib/core/hooks/index.js +2 -1
  72. package/lib/core/hooks/menu/use-mobile-menu-items.js +1 -1
  73. package/lib/core/hooks/menu/use-mobile-menu-levels.js +2 -2
  74. package/lib/core/hooks/search/use-recent-searches.js +2 -0
  75. package/lib/core/hooks/{use-search.d.ts → search/use-search-dialog.d.ts} +1 -1
  76. package/lib/core/hooks/{use-search.js → search/use-search-dialog.js} +5 -5
  77. package/lib/core/hooks/search/use-search-filter.d.ts +9 -0
  78. package/lib/core/hooks/search/use-search-filter.js +50 -0
  79. package/lib/core/types/hooks.d.ts +17 -4
  80. package/lib/core/types/index.d.ts +1 -1
  81. package/lib/core/types/index.js +1 -1
  82. package/lib/core/types/l10n.d.ts +1 -2
  83. package/lib/core/types/search.d.ts +42 -2
  84. package/lib/core/types/select.d.ts +31 -0
  85. package/lib/core/types/{select-option.js → select.js} +1 -1
  86. package/lib/core/utils/index.d.ts +1 -0
  87. package/lib/core/utils/index.js +1 -0
  88. package/lib/core/utils/menu.js +1 -1
  89. package/lib/core/utils/text-trimmer.d.ts +1 -0
  90. package/lib/core/utils/text-trimmer.js +16 -0
  91. package/lib/icons/ResetIcon/ResetIcon.d.ts +9 -0
  92. package/lib/icons/ResetIcon/ResetIcon.js +22 -0
  93. package/lib/icons/SettingsIcon/SettingsIcon.d.ts +9 -0
  94. package/lib/icons/SettingsIcon/SettingsIcon.js +23 -0
  95. package/lib/index.d.ts +8 -1
  96. package/lib/index.js +8 -1
  97. package/lib/layouts/Forbidden.js +2 -2
  98. package/lib/layouts/NotFound.js +3 -3
  99. package/lib/layouts/OIDCForbidden.js +1 -1
  100. package/lib/markdoc/tags/partial.js +1 -1
  101. package/package.json +9 -9
  102. package/src/components/Buttons/CopyButton.tsx +2 -2
  103. package/src/components/Buttons/EditPageButton.tsx +2 -2
  104. package/src/components/Catalog/CatalogActions.tsx +2 -2
  105. package/src/components/Dropdown/DropdownMenu.tsx +2 -1
  106. package/src/components/Feedback/Comment.tsx +8 -8
  107. package/src/components/Feedback/Mood.tsx +8 -8
  108. package/src/components/Feedback/Rating.tsx +5 -5
  109. package/src/components/Feedback/Reasons.tsx +4 -4
  110. package/src/components/Feedback/Scale.tsx +13 -13
  111. package/src/components/Feedback/Sentiment.tsx +6 -6
  112. package/src/components/Filter/FilterContent.tsx +3 -3
  113. package/src/components/Filter/FilterInput.tsx +1 -1
  114. package/src/components/Filter/FilterPopover.tsx +3 -3
  115. package/src/components/Filter/FilterSelect.tsx +5 -5
  116. package/src/components/Footer/FooterCopyright.tsx +3 -3
  117. package/src/components/LastUpdated/LastUpdated.tsx +1 -2
  118. package/src/components/Loaders/SpinnerLoader.tsx +31 -0
  119. package/src/components/PageNavigation/NextButton.tsx +1 -1
  120. package/src/components/PageNavigation/PreviousButton.tsx +1 -1
  121. package/src/components/Product/ProductPicker.tsx +2 -2
  122. package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +135 -0
  123. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +61 -0
  124. package/src/components/Search/Search.tsx +2 -2
  125. package/src/components/Search/SearchDialog.tsx +190 -51
  126. package/src/components/Search/SearchFilter.tsx +90 -0
  127. package/src/components/Search/SearchFilterField.tsx +84 -0
  128. package/src/components/Search/SearchGroups.tsx +81 -0
  129. package/src/components/Search/SearchHighlight.tsx +29 -2
  130. package/src/components/Search/SearchInput.tsx +9 -3
  131. package/src/components/Search/SearchItem.tsx +39 -24
  132. package/src/components/Search/SearchRecent.tsx +2 -2
  133. package/src/components/Search/SearchSuggestedPages.tsx +2 -2
  134. package/src/components/Search/SearchTrigger.tsx +2 -2
  135. package/src/components/Search/variables.ts +48 -2
  136. package/src/components/Segmented/Segmented.tsx +2 -2
  137. package/src/components/Select/Select.tsx +208 -157
  138. package/src/components/Select/SelectInput.tsx +201 -0
  139. package/src/components/Select/variables.ts +12 -1
  140. package/src/components/SidebarActions/ChangeViewButton.tsx +1 -1
  141. package/src/components/SidebarActions/SidebarActions.tsx +2 -2
  142. package/src/components/TableOfContent/TableOfContent.tsx +2 -2
  143. package/src/components/Tag/Tag.tsx +57 -6
  144. package/src/components/Tag/variables.dark.ts +20 -5
  145. package/src/components/Tag/variables.ts +49 -17
  146. package/src/components/UserMenu/LoginButton.tsx +2 -2
  147. package/src/components/UserMenu/LogoutMenuItem.tsx +2 -2
  148. package/src/components/UserMenu/UserMenu.tsx +2 -2
  149. package/src/components/VersionPicker/VersionPicker.tsx +18 -42
  150. package/src/core/hooks/__mocks__/index.ts +2 -1
  151. package/src/core/hooks/__mocks__/search/use-search-filter.ts +10 -0
  152. package/src/core/hooks/__mocks__/use-theme-hooks.ts +6 -1
  153. package/src/core/hooks/feedback/use-report-dialog.ts +3 -3
  154. package/src/core/hooks/index.ts +2 -1
  155. package/src/core/hooks/menu/use-mobile-menu-items.ts +1 -1
  156. package/src/core/hooks/menu/use-mobile-menu-levels.ts +2 -2
  157. package/src/core/hooks/search/use-recent-searches.ts +3 -0
  158. package/src/core/hooks/{use-search.ts → search/use-search-dialog.ts} +1 -1
  159. package/src/core/hooks/search/use-search-filter.ts +57 -0
  160. package/src/core/types/hooks.ts +25 -4
  161. package/src/core/types/index.ts +1 -1
  162. package/src/core/types/l10n.ts +169 -97
  163. package/src/core/types/search.ts +53 -2
  164. package/src/core/types/select.ts +33 -0
  165. package/src/core/utils/index.ts +1 -0
  166. package/src/core/utils/menu.ts +1 -1
  167. package/src/core/utils/text-trimmer.ts +7 -0
  168. package/src/icons/ResetIcon/ResetIcon.tsx +26 -0
  169. package/src/icons/SettingsIcon/SettingsIcon.tsx +30 -0
  170. package/src/index.ts +8 -1
  171. package/src/layouts/Forbidden.tsx +4 -9
  172. package/src/layouts/NotFound.tsx +6 -6
  173. package/src/layouts/OIDCForbidden.tsx +2 -2
  174. package/src/markdoc/tags/partial.ts +1 -1
  175. package/lib/core/types/select-option.d.ts +0 -4
  176. package/src/core/types/select-option.ts +0 -4
  177. /package/lib/components/{Loading → Loaders}/Loading.d.ts +0 -0
  178. /package/lib/components/{Loading → Loaders}/Loading.js +0 -0
  179. /package/src/components/{Loading → Loaders}/Loading.tsx +0 -0
@@ -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,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,61 +99,115 @@ 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
- placeholder={translate('theme.search.label', 'Search docs...')}
68
- isLoading={isLoading}
69
- data-translation-key="theme.search.label"
113
+ placeholder={translate('search.label', 'Search docs...')}
114
+ isLoading={isSearchLoading}
115
+ data-translation-key="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="search.showMore"
139
+ onClick={() =>
140
+ setLoadMore({ groupKey: key, offset: items[key]?.length || 0 })
141
+ }
142
+ >
143
+ {translate('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('search.loading', 'Loading...')}
153
+ </SearchMessage>
154
+ ) : (
155
+ <SearchMessage data-translation-key="search.noResults">
156
+ <b>{translate('search.noResults.title', 'No results')}</b>
157
+ {translate('search.noResults.description', 'Prease, try with a different query.')}
158
+ </SearchMessage>
159
+ )
82
160
  ) : (
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
- </>
161
+ <>
162
+ <SearchRecent onSelect={setQuery} />
163
+ <SearchSuggestedPages />
164
+ </>
165
+ )}
166
+ </SearchDialogBodyMainView>
167
+ {advancedSearch && isFilterOpen && (
168
+ <SearchDialogBodyFilterView>
169
+ <SearchFilter
170
+ facets={facets}
171
+ filter={filter}
172
+ query={query}
173
+ onFilterChange={onFilterChange}
174
+ onFilterReset={onFilterReset}
175
+ onFacetReset={onFacetReset}
176
+ />
177
+ </SearchDialogBodyFilterView>
92
178
  )}
93
179
  </SearchDialogBody>
94
180
  <SearchDialogFooter>
95
181
  <SearchShortcuts>
96
182
  <SearchShortcut
97
- data-translation-key="theme.search.keys.navigate"
183
+ data-translation-key="search.keys.navigate"
98
184
  combination="Tab"
99
- text={translate('theme.search.keys.navigate', 'to navigate')}
185
+ text={translate('search.keys.navigate', 'to navigate')}
100
186
  />
101
187
  <SearchShortcut
102
- data-translation-key="theme.search.keys.select"
188
+ data-translation-key="search.keys.select"
103
189
  combination="⏎"
104
- text={translate('theme.search.keys.select', 'to select')}
190
+ text={translate('search.keys.select', 'to select')}
105
191
  />
106
192
  <SearchShortcut
107
- data-translation-key="theme.search.keys.exit"
193
+ data-translation-key="search.keys.exit"
108
194
  combination="Esc"
109
- text={translate('theme.search.keys.exit', 'to exit')}
195
+ text={translate('search.keys.exit', 'to exit')}
110
196
  />
111
197
  </SearchShortcuts>
198
+ {isSearchLoading && (
199
+ <SearchLoading>
200
+ <SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
201
+ {translate('search.loading', 'Loading...')}
202
+ </SearchLoading>
203
+ )}
112
204
  <SearchCancelButton
113
- data-translation-key="theme.search.cancel"
205
+ data-translation-key="search.cancel"
114
206
  variant="secondary"
115
207
  size="small"
116
208
  onClick={onClose}
117
209
  >
118
- {translate('theme.search.cancel', 'Cancel')}
210
+ {translate('search.cancel', 'Cancel')}
119
211
  </SearchCancelButton>
120
212
  </SearchDialogFooter>
121
213
  </SearchDialogWrapper>
@@ -139,7 +231,6 @@ const SearchOverlay = styled.div`
139
231
  const SearchDialogWrapper = styled.div`
140
232
  display: flex;
141
233
  flex-direction: column;
142
- justify-content: space-between;
143
234
  overflow: auto;
144
235
  width: 100vw;
145
236
  height: 100vh;
@@ -155,39 +246,51 @@ const SearchDialogWrapper = styled.div`
155
246
  max-height: 95vh;
156
247
  height: auto;
157
248
  resize: both;
158
- min-width: 300px;
159
- min-height: 200px;
160
249
  }
161
250
  `;
162
251
 
163
252
  const SearchDialogHeader = styled.header`
164
253
  display: flex;
165
254
  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);
255
+ border-bottom: var(--search-modal-border);
256
+ background-color: var(--search-modal-header-bg-color);
257
+ padding: var(--search-modal-header-padding);
169
258
  `;
170
259
 
171
260
  const SearchDialogBody = styled.div`
261
+ display: flex;
262
+ flex-direction: row;
172
263
  flex-grow: 1;
173
- overflow-y: scroll;
174
- overscroll-behavior: contain;
175
264
 
176
265
  @media screen and (min-width: ${breakpoints.small}) {
177
266
  height: var(--search-modal-min-height);
178
267
  }
179
268
  `;
180
269
 
270
+ const SearchDialogBodyMainView = styled.div`
271
+ flex: 2;
272
+ flex-grow: 2;
273
+ overflow-y: scroll;
274
+ overscroll-behavior: contain;
275
+ border-right: var(--search-modal-border);
276
+ `;
277
+
278
+ const SearchDialogBodyFilterView = styled.div`
279
+ overflow: scroll;
280
+ `;
281
+
181
282
  const SearchDialogFooter = styled.footer`
182
- padding: var(--spacing-sm);
183
- border-top: 1px solid var(--border-color-secondary);
283
+ display: flex;
284
+ gap: var(--search-modal-footer-gap);
285
+ padding: var(--search-modal-footer-padding);
286
+ border-top: var(--search-modal-border);
184
287
  `;
185
288
 
186
289
  const SearchShortcuts = styled.div`
187
290
  display: none;
188
291
  justify-content: flex-start;
189
292
  align-items: center;
190
- gap: var(--spacing-xs);
293
+ gap: var(--search-shortcuts-gap);
191
294
 
192
295
  @media screen and (min-width: ${breakpoints.small}) {
193
296
  display: flex;
@@ -195,16 +298,28 @@ const SearchShortcuts = styled.div`
195
298
  `;
196
299
 
197
300
  const SearchMessage = styled.div`
198
- padding: var(--spacing-lg);
199
- color: var(--search-item-title-text-color);
301
+ display: flex;
302
+ height: 40%;
303
+ justify-content: center;
304
+ align-items: center;
305
+ flex-direction: column;
306
+ font-size: var(--search-message-font-size);
307
+ font-weight: var(--search-message-font-weight);
308
+ line-height: var(--search-message-line-height);
309
+ color: var(--search-message-text-color);
310
+ gap: var(--search-message-gap);
200
311
  `;
201
312
 
202
313
  const SearchProductTag = styled(Tag)`
203
314
  --tag-border-radius: var(--border-radius);
204
-
315
+ border: none;
205
316
  margin: var(--spacing-xs) var(--spacing-sm) !important;
206
317
  `;
207
318
 
319
+ const SearchFilterToggleButton = styled(Button)`
320
+ margin-left: 0;
321
+ `;
322
+
208
323
  const SearchCancelButton = styled(Button)`
209
324
  width: 100%;
210
325
 
@@ -212,3 +327,27 @@ const SearchCancelButton = styled(Button)`
212
327
  display: none;
213
328
  }
214
329
  `;
330
+
331
+ const SearchGroupTitle = styled.div`
332
+ border-bottom: var(--search-modal-border);
333
+ padding: var(--search-group-title-padding);
334
+ background-color: var(--search-group-title-bg-color);
335
+ `;
336
+
337
+ const SearchGroupFooter = styled.div`
338
+ display: flex;
339
+ justify-content: center;
340
+ padding: var(--search-group-footer-padding);
341
+ color: var(--search-group-footer-text-color);
342
+ cursor: pointer;
343
+ `;
344
+
345
+ const SearchLoading = styled.div`
346
+ display: none;
347
+ align-items: center;
348
+ gap: var(--spacing-xs);
349
+
350
+ @media screen and (min-width: ${breakpoints.small}) {
351
+ display: flex;
352
+ }
353
+ `;
@@ -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="search.filter.title">
36
+ {translate('search.filter.title', 'Advanced filter')}
37
+ </span>
38
+ <Button
39
+ data-translation-key="search.filter.reset"
40
+ onClick={onFilterReset}
41
+ variant="ghost"
42
+ icon={<CleanIcon />}
43
+ >
44
+ {translate('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="search.filter.field.reset"
48
+ icon={<ResetIcon />}
49
+ variant="ghost"
50
+ size="small"
51
+ onClick={onReset}
52
+ >
53
+ {translate('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 (