@redocly/theme 0.47.1 → 0.48.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 (117) hide show
  1. package/lib/components/Feedback/Mood.d.ts +2 -2
  2. package/lib/components/Feedback/Mood.js +20 -15
  3. package/lib/components/Feedback/Rating.d.ts +2 -2
  4. package/lib/components/Feedback/Rating.js +6 -6
  5. package/lib/components/Feedback/Scale.d.ts +2 -2
  6. package/lib/components/Feedback/Scale.js +6 -6
  7. package/lib/components/Feedback/Sentiment.d.ts +2 -2
  8. package/lib/components/Feedback/Sentiment.js +6 -6
  9. package/lib/components/Search/SearchDialog.js +17 -11
  10. package/lib/components/Search/SearchFilter.d.ts +3 -2
  11. package/lib/components/Search/SearchFilter.js +2 -2
  12. package/lib/components/Search/SearchFilterField.d.ts +3 -2
  13. package/lib/components/Search/SearchFilterField.js +2 -2
  14. package/lib/components/Search/SearchGroups.d.ts +5 -4
  15. package/lib/components/Search/SearchGroups.js +3 -3
  16. package/lib/components/Search/variables.js +4 -0
  17. package/lib/components/Segmented/Segmented.d.ts +4 -4
  18. package/lib/components/Segmented/Segmented.js +4 -7
  19. package/lib/components/Tag/Tag.d.ts +1 -0
  20. package/lib/components/Tag/Tag.js +3 -2
  21. package/lib/core/hooks/__mocks__/search/use-search-filter.d.ts +1 -1
  22. package/lib/core/hooks/__mocks__/search/use-search-filter.js +1 -1
  23. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.d.ts +1 -1
  24. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.js +10 -5
  25. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.d.ts +1 -2
  26. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +20 -15
  27. package/lib/core/hooks/code-walkthrough/use-code-walkthrough.d.ts +2 -7
  28. package/lib/core/hooks/code-walkthrough/use-code-walkthrough.js +10 -3
  29. package/lib/core/hooks/code-walkthrough/use-renderable-files.d.ts +9 -0
  30. package/lib/core/hooks/code-walkthrough/use-renderable-files.js +28 -0
  31. package/lib/core/hooks/index.d.ts +1 -0
  32. package/lib/core/hooks/index.js +1 -0
  33. package/lib/core/hooks/search/use-search-filter.d.ts +2 -2
  34. package/lib/core/hooks/search/use-search-filter.js +5 -5
  35. package/lib/core/types/hooks.d.ts +1 -0
  36. package/lib/core/types/l10n.d.ts +1 -1
  37. package/lib/core/types/search.d.ts +1 -2
  38. package/lib/core/utils/download-code-walkthrough.js +9 -1
  39. package/lib/core/utils/find-closest-common-directory.d.ts +6 -0
  40. package/lib/core/utils/find-closest-common-directory.js +51 -0
  41. package/lib/core/utils/get-file-icon.js +6 -0
  42. package/lib/core/utils/index.d.ts +1 -0
  43. package/lib/core/utils/index.js +1 -0
  44. package/lib/core/utils/replace-inputs-with-value.d.ts +1 -1
  45. package/lib/core/utils/replace-inputs-with-value.js +9 -10
  46. package/lib/icons/DocumentJavaIcon/DocumentJavaIcon.d.ts +9 -0
  47. package/lib/icons/DocumentJavaIcon/DocumentJavaIcon.js +22 -0
  48. package/lib/icons/DocumentJavaIcon/index.d.ts +1 -0
  49. package/lib/icons/DocumentJavaIcon/index.js +6 -0
  50. package/lib/icons/DocumentPythonIcon/DocumentPythonIcon.d.ts +9 -0
  51. package/lib/icons/DocumentPythonIcon/DocumentPythonIcon.js +23 -0
  52. package/lib/icons/DocumentPythonIcon/index.d.ts +1 -0
  53. package/lib/icons/DocumentPythonIcon/index.js +6 -0
  54. package/lib/icons/DocumentShellIcon/DocumentShellIcon.d.ts +9 -0
  55. package/lib/icons/DocumentShellIcon/DocumentShellIcon.js +22 -0
  56. package/lib/icons/DocumentShellIcon/index.d.ts +1 -0
  57. package/lib/icons/DocumentShellIcon/index.js +6 -0
  58. package/lib/icons/__tests__/IconTestUtils.d.ts +7 -0
  59. package/lib/icons/__tests__/IconTestUtils.js +33 -0
  60. package/lib/layouts/CodeWalkthroughLayout.js +4 -1
  61. package/lib/markdoc/components/CodeWalkthrough/CodeContainer.js +1 -1
  62. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.js +15 -2
  63. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +1 -1
  64. package/lib/markdoc/components/CodeWalkthrough/CodePanelHeader.js +29 -23
  65. package/lib/markdoc/components/CodeWalkthrough/CodePanelPreview.js +1 -1
  66. package/lib/markdoc/components/CodeWalkthrough/CodePanelToolbar.js +1 -1
  67. package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +6 -3
  68. package/lib/markdoc/components/CodeWalkthrough/CodeToggle.js +1 -1
  69. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +1 -1
  70. package/lib/markdoc/components/CodeWalkthrough/Input.js +4 -2
  71. package/lib/markdoc/tags/code-walkthrough.js +5 -0
  72. package/package.json +3 -3
  73. package/src/components/Feedback/Mood.tsx +25 -17
  74. package/src/components/Feedback/Rating.tsx +9 -10
  75. package/src/components/Feedback/Scale.tsx +9 -10
  76. package/src/components/Feedback/Sentiment.tsx +9 -10
  77. package/src/components/Search/SearchDialog.tsx +63 -42
  78. package/src/components/Search/SearchFilter.tsx +6 -3
  79. package/src/components/Search/SearchFilterField.tsx +4 -2
  80. package/src/components/Search/SearchGroups.tsx +13 -8
  81. package/src/components/Search/variables.ts +4 -0
  82. package/src/components/Segmented/Segmented.tsx +10 -10
  83. package/src/components/Tag/Tag.tsx +1 -1
  84. package/src/core/hooks/__mocks__/search/use-search-filter.ts +1 -1
  85. package/src/core/hooks/code-walkthrough/use-code-walkthrough-controls.ts +9 -3
  86. package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +30 -18
  87. package/src/core/hooks/code-walkthrough/use-code-walkthrough.ts +13 -13
  88. package/src/core/hooks/code-walkthrough/use-renderable-files.ts +51 -0
  89. package/src/core/hooks/index.ts +1 -0
  90. package/src/core/hooks/search/use-search-filter.ts +9 -5
  91. package/src/core/types/hooks.ts +1 -0
  92. package/src/core/types/l10n.ts +5 -3
  93. package/src/core/types/search.ts +1 -2
  94. package/src/core/utils/download-code-walkthrough.ts +14 -2
  95. package/src/core/utils/find-closest-common-directory.ts +51 -0
  96. package/src/core/utils/get-file-icon.ts +7 -0
  97. package/src/core/utils/index.ts +1 -0
  98. package/src/core/utils/replace-inputs-with-value.ts +12 -9
  99. package/src/icons/DocumentJavaIcon/DocumentJavaIcon.tsx +33 -0
  100. package/src/icons/DocumentJavaIcon/index.ts +1 -0
  101. package/src/icons/DocumentPythonIcon/DocumentPythonIcon.tsx +37 -0
  102. package/src/icons/DocumentPythonIcon/index.ts +1 -0
  103. package/src/icons/DocumentShellIcon/DocumentShellIcon.tsx +33 -0
  104. package/src/icons/DocumentShellIcon/index.ts +1 -0
  105. package/src/icons/__tests__/IconTestUtils.tsx +31 -0
  106. package/src/layouts/CodeWalkthroughLayout.tsx +5 -1
  107. package/src/markdoc/components/CodeWalkthrough/CodeContainer.tsx +1 -0
  108. package/src/markdoc/components/CodeWalkthrough/CodeFilters.tsx +19 -3
  109. package/src/markdoc/components/CodeWalkthrough/CodePanel.tsx +1 -1
  110. package/src/markdoc/components/CodeWalkthrough/CodePanelHeader.tsx +64 -47
  111. package/src/markdoc/components/CodeWalkthrough/CodePanelPreview.tsx +1 -1
  112. package/src/markdoc/components/CodeWalkthrough/CodePanelToolbar.tsx +1 -1
  113. package/src/markdoc/components/CodeWalkthrough/CodeStep.tsx +6 -2
  114. package/src/markdoc/components/CodeWalkthrough/CodeToggle.tsx +1 -1
  115. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +4 -1
  116. package/src/markdoc/components/CodeWalkthrough/Input.tsx +4 -2
  117. package/src/markdoc/tags/code-walkthrough.ts +5 -0
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
  import { useEffect } from 'react';
3
3
  import styled from 'styled-components';
4
4
 
5
- import type { ReasonsSettingsSchema, AnonymousUserEmailSettings } from '@redocly/config';
5
+ import type { OptionalEmailSettings, ReasonsSettingsSchema } from '@redocly/config';
6
6
  import type { ReasonsProps } from '@redocly/theme/components/Feedback/Reasons';
7
7
 
8
8
  import { Reasons } from '@redocly/theme/components/Feedback/Reasons';
@@ -32,7 +32,7 @@ export type SentimentProps = {
32
32
  like?: ReasonsSettingsSchema;
33
33
  dislike?: ReasonsSettingsSchema;
34
34
  };
35
- anonymousUserEmail?: AnonymousUserEmailSettings;
35
+ optionalEmail?: OptionalEmailSettings;
36
36
  };
37
37
  className?: string;
38
38
  };
@@ -43,7 +43,7 @@ export function Sentiment({ settings, onSubmit, className }: SentimentProps): JS
43
43
  submitText,
44
44
  comment: commentSettings,
45
45
  reasons: reasonsSettings,
46
- anonymousUserEmail: anonymousUserEmailSettings,
46
+ optionalEmail: optionalEmailSettings,
47
47
  } = settings || {};
48
48
  const [isSubmitted, setIsSubmitted] = React.useState(false);
49
49
  const [score, setScore] = React.useState(0);
@@ -87,8 +87,7 @@ export function Sentiment({ settings, onSubmit, className }: SentimentProps): JS
87
87
  const displayReasons = checkIfShouldDisplayReasons(score);
88
88
  const displayComment = !!(score && !commentSettings?.hide);
89
89
  const displaySubmitBnt = !!(score && (displayReasons || displayComment));
90
- const displayFeedbackEmail =
91
- !!score && anonymousUserEmailSettings?.enabled && !userData.isAuthenticated;
90
+ const displayFeedbackEmail = !!score && !optionalEmailSettings?.hide && !userData.isAuthenticated;
92
91
 
93
92
  const commentLabel =
94
93
  score === 1
@@ -214,18 +213,18 @@ export function Sentiment({ settings, onSubmit, className }: SentimentProps): JS
214
213
 
215
214
  {displayFeedbackEmail && (
216
215
  <StyledFormOptionalFields>
217
- <Label data-translation-key="feedback.settings.anonymousUserEmail.label">
218
- {anonymousUserEmailSettings?.label ||
216
+ <Label data-translation-key="feedback.settings.optionalEmail.label">
217
+ {optionalEmailSettings?.label ||
219
218
  translate(
220
- 'feedback.settings.anonymousUserEmail.label',
219
+ 'feedback.settings.optionalEmail.label',
221
220
  'Your email (optional, for follow-up)',
222
221
  )}
223
222
  </Label>
224
223
  <EmailInput
225
224
  onChange={onEmailChange}
226
225
  placeholder={
227
- anonymousUserEmailSettings?.placeholder ||
228
- translate('feedback.settings.anonymousUserEmail.placeholder', 'username@mail.com')
226
+ optionalEmailSettings?.placeholder ||
227
+ translate('feedback.settings.optionalEmail.placeholder', 'yourname@example.com')
229
228
  }
230
229
  type="email"
231
230
  required={!!email}
@@ -44,6 +44,7 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
44
44
  setLoadMore,
45
45
  advancedSearch,
46
46
  askAi,
47
+ groupField,
47
48
  } = useSearch(product?.name, autoSearchDisabled);
48
49
  const {
49
50
  isFilterOpen,
@@ -51,7 +52,7 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
51
52
  onFilterChange,
52
53
  onFilterReset,
53
54
  onFacetReset,
54
- onTopFacetsReset,
55
+ onQuickFilterReset,
55
56
  } = useSearchFilter(filter, setFilter);
56
57
  const aiSearch = useAiSearch({ filter });
57
58
 
@@ -83,10 +84,10 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
83
84
  };
84
85
 
85
86
  const showLoadMore = (groupKey: string, currentCount: number = 0) => {
86
- const topFacet = facets.find((facet) => facet.isTop);
87
+ const groupFacet = facets.find((facet) => facet.field === groupField);
87
88
  let needLoadMore = false;
88
- if (topFacet) {
89
- const groupValue = topFacet.values.find((value) => {
89
+ if (groupFacet) {
90
+ const groupValue = groupFacet.values.find((value) => {
90
91
  if (typeof value === 'object') {
91
92
  return value.value === groupKey;
92
93
  } else return false;
@@ -152,7 +153,7 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
152
153
  }
153
154
  }}
154
155
  >
155
- {translate('search.aiButton', 'Search with AI')}
156
+ {translate('search.ai.button', 'Search with AI')}
156
157
  </SearchAiButton>
157
158
  ) : null}
158
159
  {showSearchFilterButton && (
@@ -170,14 +171,17 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
170
171
  facets={facets}
171
172
  searchFilter={filter}
172
173
  onFilterChange={onFilterChange}
173
- onTopFacetsReset={onTopFacetsReset}
174
+ onQuickFilterReset={onQuickFilterReset}
175
+ groupField={groupField}
174
176
  />
175
177
  {showResults ? (
176
178
  items && Object.keys(items).some((key) => items[key]?.length) ? (
177
179
  Object.keys(items).map((key) =>
178
180
  items[key]?.length ? (
179
181
  <Fragment key={key}>
180
- <SearchGroupTitle>{key}</SearchGroupTitle>
182
+ <SearchGroupTitle data-testid="search-group-title">
183
+ {key}
184
+ </SearchGroupTitle>
181
185
  {items[key]?.map(mapItem)}
182
186
  {showLoadMore(key, items[key]?.length || 0) && (
183
187
  <SearchGroupFooter
@@ -215,6 +219,7 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
215
219
  facets={facets}
216
220
  filter={filter}
217
221
  query={query}
222
+ quickFilterFields={[groupField]}
218
223
  onFilterChange={onFilterChange}
219
224
  onFilterReset={onFilterReset}
220
225
  onFacetReset={onFacetReset}
@@ -231,41 +236,50 @@ export function SearchDialog({ onClose, className }: SearchDialogProps): JSX.Ele
231
236
  />
232
237
  )}
233
238
  </SearchDialogBody>
234
- {mode === 'search' && (
235
- <SearchDialogFooter>
236
- <SearchShortcuts>
237
- <SearchShortcut
238
- data-translation-key="search.keys.navigate"
239
- combination="Tab"
240
- text={translate('search.keys.navigate', 'to navigate')}
241
- />
242
- <SearchShortcut
243
- data-translation-key="search.keys.select"
244
- combination="⏎"
245
- text={translate('search.keys.select', 'to select')}
246
- />
247
- <SearchShortcut
248
- data-translation-key="search.keys.exit"
249
- combination="Esc"
250
- text={translate('search.keys.exit', 'to exit')}
251
- />
252
- </SearchShortcuts>
253
- {isSearchLoading && (
254
- <SearchLoading>
255
- <SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
256
- {translate('search.loading', 'Loading...')}
257
- </SearchLoading>
258
- )}
259
- <SearchCancelButton
260
- data-translation-key="search.cancel"
261
- variant="secondary"
262
- size="small"
263
- onClick={onClose}
264
- >
265
- {translate('search.cancel', 'Cancel')}
266
- </SearchCancelButton>
267
- </SearchDialogFooter>
268
- )}
239
+ <SearchDialogFooter>
240
+ {mode === 'ai-dialog' ? (
241
+ <AiDisclaimer>
242
+ {translate(
243
+ 'search.ai.disclaimer',
244
+ 'AI search might provide incomplete or incorrect results. Verify important information.',
245
+ )}
246
+ </AiDisclaimer>
247
+ ) : (
248
+ <>
249
+ <SearchShortcuts>
250
+ <SearchShortcut
251
+ data-translation-key="search.keys.navigate"
252
+ combination="Tab"
253
+ text={translate('search.keys.navigate', 'to navigate')}
254
+ />
255
+ <SearchShortcut
256
+ data-translation-key="search.keys.select"
257
+ combination="⏎"
258
+ text={translate('search.keys.select', 'to select')}
259
+ />
260
+ <SearchShortcut
261
+ data-translation-key="search.keys.exit"
262
+ combination="Esc"
263
+ text={translate('search.keys.exit', 'to exit')}
264
+ />
265
+ </SearchShortcuts>
266
+ {isSearchLoading && (
267
+ <SearchLoading>
268
+ <SpinnerLoader size="16px" color="var(--search-input-icon-color)" />
269
+ {translate('search.loading', 'Loading...')}
270
+ </SearchLoading>
271
+ )}
272
+ <SearchCancelButton
273
+ data-translation-key="search.cancel"
274
+ variant="secondary"
275
+ size="small"
276
+ onClick={onClose}
277
+ >
278
+ {translate('search.cancel', 'Cancel')}
279
+ </SearchCancelButton>
280
+ </>
281
+ )}
282
+ </SearchDialogFooter>
269
283
  </SearchDialogWrapper>
270
284
  </SearchOverlay>
271
285
  );
@@ -434,3 +448,10 @@ const SearchHeaderButtons = styled.div`
434
448
  padding-left: var(--search-header-buttons-padding-left);
435
449
  border-left: var(--search-header-buttons-border-left);
436
450
  `;
451
+
452
+ const AiDisclaimer = styled.div`
453
+ font-size: var(--search-ai-disclaimer-font-size);
454
+ line-height: var(--search-ai-disclaimer-line-height);
455
+ color: var(--search-ai-disclaimer-text-color);
456
+ margin: 0 auto;
457
+ `;
@@ -13,7 +13,8 @@ export type SearchFilterProps = {
13
13
  facets: SearchFacet[];
14
14
  filter: SearchFilterItem[];
15
15
  query: string;
16
- onFilterChange: (field: string, value: string | string[], isTop?: boolean) => void;
16
+ quickFilterFields: string[];
17
+ onFilterChange: (field: string, value: string | string[], isQuickFilter?: boolean) => void;
17
18
  onFilterReset: () => void;
18
19
  onFacetReset: (field: string) => void;
19
20
  };
@@ -23,6 +24,7 @@ export function SearchFilter({
23
24
  facets,
24
25
  filter,
25
26
  query,
27
+ quickFilterFields,
26
28
  onFilterChange,
27
29
  onFilterReset,
28
30
  onFacetReset,
@@ -50,10 +52,11 @@ export function SearchFilter({
50
52
  <SearchFilterField
51
53
  key={`${facet.field}-${index}`}
52
54
  facet={facet}
53
- onFilterChange={onFilterChange}
54
- onFacetReset={onFacetReset}
55
55
  filter={filter}
56
56
  query={query}
57
+ quickFilterFields={quickFilterFields}
58
+ onFilterChange={onFilterChange}
59
+ onFacetReset={onFacetReset}
57
60
  />
58
61
  ))}
59
62
  </SearchFilterFields>
@@ -14,7 +14,8 @@ type SearchFilterFieldProps = {
14
14
  facet: SearchFacet;
15
15
  filter: SearchFilterItem[];
16
16
  query: string;
17
- onFilterChange: (field: string, value: string | string[], isTop?: boolean) => void;
17
+ quickFilterFields: string[];
18
+ onFilterChange: (field: string, value: string | string[], isQuickFilter?: boolean) => void;
18
19
  onFacetReset: (filed: string) => void;
19
20
  };
20
21
 
@@ -23,6 +24,7 @@ export function SearchFilterField({
23
24
  facet,
24
25
  filter,
25
26
  query,
27
+ quickFilterFields,
26
28
  onFilterChange,
27
29
  onFacetReset,
28
30
  }: SearchFilterFieldProps): JSX.Element {
@@ -31,7 +33,7 @@ export function SearchFilterField({
31
33
  const selectedValues = filter.find((item) => item.field === facet.field)?.values || [];
32
34
 
33
35
  const onChange = (value: string | string[]) => {
34
- onFilterChange(facet.field, value, facet.isTop);
36
+ onFilterChange(facet.field, value, facet.field in quickFilterFields);
35
37
  };
36
38
 
37
39
  const onReset = () => {
@@ -1,24 +1,29 @@
1
1
  import * as React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
- import type { SearchFacet, SearchFacetCount, SearchFilterItem } from '@redocly/theme';
5
-
4
+ import {
5
+ type SearchFacet,
6
+ type SearchFacetCount,
7
+ type SearchFilterItem,
8
+ } from '@redocly/theme/core/types';
6
9
  import { Tag } from '@redocly/theme/components/Tag/Tag';
7
10
 
8
11
  type SearchGroupsProps = {
9
12
  facets: SearchFacet[];
10
13
  searchFilter: SearchFilterItem[];
11
- onFilterChange: (field: string, value: string[], isTop?: boolean) => void;
12
- onTopFacetsReset: () => void;
14
+ groupField: string;
15
+ onFilterChange: (field: string, value: string[], isQuickFilter?: boolean) => void;
16
+ onQuickFilterReset: () => void;
13
17
  };
14
18
 
15
19
  export function SearchGroups({
16
20
  facets,
17
21
  searchFilter,
22
+ groupField,
18
23
  onFilterChange,
19
- onTopFacetsReset,
24
+ onQuickFilterReset,
20
25
  }: SearchGroupsProps): JSX.Element {
21
- const groupFacets = facets.filter((facet) => facet.isTop);
26
+ const groupFacets = facets.filter((facet) => facet.field === groupField);
22
27
 
23
28
  const handleGroupTagClick = (
24
29
  value: string,
@@ -36,8 +41,8 @@ export function SearchGroups({
36
41
  <SearchGroupsWrapper>
37
42
  <GroupTag
38
43
  borderless
39
- active={!searchFilter.some((item) => item.isTop)}
40
- onClick={() => searchFilter.some((item) => item.isTop) && onTopFacetsReset()}
44
+ active={!searchFilter.some((item) => item.isQuickFilter)}
45
+ onClick={() => searchFilter.some((item) => item.isQuickFilter) && onQuickFilterReset()}
41
46
  >
42
47
  All
43
48
  </GroupTag>
@@ -176,5 +176,9 @@ export const search = css`
176
176
  --search-ai-resource-tag-text-color: var(--text-color-secondary);
177
177
  --search-ai-resource-tag-icon-color: var(--text-color-secondary);
178
178
 
179
+ --search-ai-disclaimer-font-size: var(--font-size-sm);
180
+ --search-ai-disclaimer-line-height: var(--line-height-sm);
181
+ --search-ai-disclaimer-text-color: var(--text-color-secondary);
182
+
179
183
  // @tokens End
180
184
  `;
@@ -1,7 +1,7 @@
1
- import React from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
- import type { ReactElement } from 'react';
4
+ import type { ForwardedRef, ReactElement } from 'react';
5
5
  import type { SelectOption } from '@redocly/theme/core/types/select';
6
6
 
7
7
  import { typedMemo } from '@redocly/theme/core/hoc/typedMemo';
@@ -14,15 +14,13 @@ export type SegmentedProps<T> = {
14
14
  size?: 'regular' | 'small';
15
15
  };
16
16
 
17
- function SegmentedComponent<T>({
18
- options,
19
- onChange,
20
- value,
21
- className = '',
22
- size = 'regular',
23
- }: SegmentedProps<T>): ReactElement {
17
+ function SegmentedComponent<T>(
18
+ { options, onChange, value, className = '', size = 'regular' }: SegmentedProps<T>,
19
+ ref?: ForwardedRef<HTMLDivElement>,
20
+ ): ReactElement {
24
21
  return (
25
22
  <SegmentedGroup
23
+ ref={ref}
26
24
  data-component-name="Segmented/Segmented"
27
25
  className={`tag-grey ${size} ${className}`}
28
26
  role="tablist"
@@ -43,7 +41,9 @@ function SegmentedComponent<T>({
43
41
  );
44
42
  }
45
43
 
46
- export const Segmented = typedMemo(SegmentedComponent);
44
+ export const Segmented = typedMemo(forwardRef(SegmentedComponent)) as <T>(
45
+ props: SegmentedProps<T> & { ref?: ForwardedRef<HTMLDivElement> },
46
+ ) => ReactElement;
47
47
 
48
48
  const SegmentedGroup = styled.div`
49
49
  display: flex;
@@ -83,7 +83,7 @@ export function Tag({
83
83
  );
84
84
  }
85
85
 
86
- const ContentWrapper = styled.div`
86
+ export const ContentWrapper = styled.div`
87
87
  display: inline-flex;
88
88
  align-items: center;
89
89
  justify-content: center;
@@ -5,6 +5,6 @@ export const useSearchFilter = jest.fn(() => {
5
5
  onFilterChange: () => {},
6
6
  onFilterToggle: () => {},
7
7
  onFilterReset: () => {},
8
- onTopFacetsReset: () => {},
8
+ onQuickFilterReset: () => {},
9
9
  };
10
10
  });
@@ -60,6 +60,7 @@ export function useCodeWalkthroughControls(
60
60
  filters: Record<string, CodeWalkthroughFilter>,
61
61
  inputs: InputsMarkdocAttr,
62
62
  toggles: TogglesMarkdocAttr,
63
+ enableDeepLink: boolean,
63
64
  ): WalkthroughControlsState {
64
65
  const location = useLocation();
65
66
  const navigate = useNavigate();
@@ -72,7 +73,7 @@ export function useCodeWalkthroughControls(
72
73
  ...toggle,
73
74
  render: true,
74
75
  type: 'toggle',
75
- value: searchParams.get(id) === 'true',
76
+ value: enableDeepLink ? searchParams.get(id) === 'true' : false,
76
77
  };
77
78
  }
78
79
  return initialState;
@@ -116,7 +117,7 @@ export function useCodeWalkthroughControls(
116
117
  ...input,
117
118
  render: true,
118
119
  type: 'input',
119
- value: searchParams.get(id) || input.value,
120
+ value: enableDeepLink ? (searchParams.get(id) ?? input.value) : input.value,
120
121
  };
121
122
  }
122
123
  return initialState;
@@ -156,11 +157,12 @@ export function useCodeWalkthroughControls(
156
157
  const initialState: FiltersState = {};
157
158
 
158
159
  for (const [id, filter] of Object.entries(filters)) {
160
+ const defaultValue = filter?.items?.[0]?.value || '';
159
161
  initialState[id] = {
160
162
  ...filter,
161
163
  render: true,
162
164
  type: 'filter',
163
- value: searchParams.get(id) || filter?.items?.[0]?.value || '',
165
+ value: enableDeepLink ? (searchParams.get(id) ?? defaultValue) : defaultValue,
164
166
  };
165
167
  }
166
168
 
@@ -270,6 +272,10 @@ export function useCodeWalkthroughControls(
270
272
  * Update the URL search params with the current state of the filters and inputs
271
273
  */
272
274
  useEffect(() => {
275
+ if (!enableDeepLink) {
276
+ return;
277
+ }
278
+
273
279
  const newSearchParams = new URLSearchParams(Array.from(searchParams.entries()));
274
280
 
275
281
  for (const [id, { value }] of Object.entries(state)) {
@@ -8,9 +8,9 @@ import { ACTIVE_STEP_QUERY_PARAM } from '@redocly/theme/core/constants';
8
8
 
9
9
  type ActiveStep = string | null;
10
10
  type CodeWalkthroughStep = CodeWalkthroughStepAttr & {
11
- active?: boolean;
12
11
  compRef?: HTMLElement;
13
12
  };
13
+
14
14
  export type WalkthroughStepsState = {
15
15
  activeStep: ActiveStep;
16
16
  setActiveStep: (stepId: ActiveStep) => void;
@@ -20,7 +20,10 @@ export type WalkthroughStepsState = {
20
20
  filtersElementRef?: React.RefObject<HTMLDivElement>;
21
21
  };
22
22
 
23
- export function useCodeWalkthroughSteps(steps: CodeWalkthroughStep[]): WalkthroughStepsState {
23
+ export function useCodeWalkthroughSteps(
24
+ steps: CodeWalkthroughStep[],
25
+ enableDeepLink: boolean,
26
+ ): WalkthroughStepsState {
24
27
  const location = useLocation();
25
28
  const navigate = useNavigate();
26
29
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
@@ -32,7 +35,7 @@ export function useCodeWalkthroughSteps(steps: CodeWalkthroughStep[]): Walkthrou
32
35
  const observedElementsRef = useRef(new Set<HTMLElement>());
33
36
 
34
37
  const [activeStep, setActiveStep] = useState<ActiveStep>(
35
- searchParams.get(ACTIVE_STEP_QUERY_PARAM),
38
+ enableDeepLink ? searchParams.get(ACTIVE_STEP_QUERY_PARAM) : null,
36
39
  );
37
40
 
38
41
  const register = useCallback(
@@ -74,28 +77,23 @@ export function useCodeWalkthroughSteps(steps: CodeWalkthroughStep[]): Walkthrou
74
77
  return;
75
78
  }
76
79
 
77
- const stepsEntries = [];
80
+ const renderedSteps = steps.filter((step) => Boolean(step.compRef));
81
+
82
+ if (renderedSteps.length < 2) {
83
+ setActiveStep(renderedSteps[0]?.id || null);
84
+ return;
85
+ }
86
+
78
87
  for (const entry of entries) {
79
- const { target } = entry;
80
- const stepKey = Number((target as HTMLElement)?.dataset?.stepKey);
81
- const stepActive = (target as HTMLElement)?.dataset?.stepActive === 'true';
88
+ const stepKey = Number((entry.target as HTMLElement)?.dataset?.stepKey);
82
89
 
83
90
  if (!Number.isInteger(stepKey) || stepKey < 0) {
84
91
  continue;
85
92
  }
86
93
 
94
+ const { intersectionRatio, boundingClientRect, rootBounds, isIntersecting } = entry;
87
95
  const step = steps[stepKey];
88
- step.active = stepActive;
89
- stepsEntries.push(entry);
90
- }
91
96
 
92
- for (const stepEntry of stepsEntries) {
93
- const { target, intersectionRatio, boundingClientRect, rootBounds, isIntersecting } =
94
- stepEntry;
95
- const stepKey = Number((target as HTMLElement)?.dataset?.stepKey);
96
- const step = steps[stepKey];
97
-
98
- const renderedSteps = steps.filter((step) => Boolean(step.compRef));
99
97
  const stepIndex = renderedSteps.findIndex(
100
98
  (renderedStep) => renderedStep.stepKey === step.stepKey,
101
99
  );
@@ -105,6 +103,16 @@ export function useCodeWalkthroughSteps(steps: CodeWalkthroughStep[]): Walkthrou
105
103
  rootBounds?.bottom !== undefined && boundingClientRect.top < rootBounds.top;
106
104
  const stepGoesIn = isIntersecting;
107
105
 
106
+ if (
107
+ intersectionRatio > 0.8 &&
108
+ intersectionRatio < 1 &&
109
+ intersectionAtTop &&
110
+ activeStep === null
111
+ ) {
112
+ setActiveStep(step.id);
113
+ break;
114
+ }
115
+
108
116
  if (intersectionRatio < 1 && intersectionRatio !== 0 && intersectionAtTop) {
109
117
  let newStep: string | null = null;
110
118
 
@@ -128,7 +136,7 @@ export function useCodeWalkthroughSteps(steps: CodeWalkthroughStep[]): Walkthrou
128
136
  const filtersElementHeight = filtersElementRef.current?.clientHeight || 0;
129
137
  const navbarHeight = document.querySelector('nav')?.clientHeight || 0;
130
138
  const newObserver = new IntersectionObserver(observerCallback, {
131
- threshold: [0.8],
139
+ threshold: [0.8, 0.85, 0.9, 0.95],
132
140
  rootMargin: `-${filtersElementHeight + navbarHeight}px 0px 0px 0px`,
133
141
  });
134
142
 
@@ -145,6 +153,10 @@ export function useCodeWalkthroughSteps(steps: CodeWalkthroughStep[]): Walkthrou
145
153
  * Update the URL search params with the current state of the filters and inputs
146
154
  */
147
155
  useEffect(() => {
156
+ if (!enableDeepLink) {
157
+ return;
158
+ }
159
+
148
160
  const newSearchParams = new URLSearchParams(Array.from(searchParams.entries()));
149
161
 
150
162
  if (activeStep) {
@@ -1,10 +1,7 @@
1
1
  import type {
2
- CodeWalkthroughFileset,
3
2
  CodeWalkthroughFile,
4
3
  CodeWalkthroughStepAttr,
5
- CodeWalkthroughFilter,
6
- InputsMarkdocAttr,
7
- TogglesMarkdocAttr,
4
+ CodeWalkthroughAttr,
8
5
  } from '@redocly/config';
9
6
 
10
7
  import {
@@ -23,16 +20,19 @@ export type WalkthroughState = {
23
20
 
24
21
  export function useCodeWalkthrough(
25
22
  steps: CodeWalkthroughStepAttr[],
26
- attributes: {
27
- filters: Record<string, CodeWalkthroughFilter>;
28
- filesets: CodeWalkthroughFileset[];
29
- inputs: InputsMarkdocAttr;
30
- toggles: TogglesMarkdocAttr;
31
- },
23
+ attributes: Omit<CodeWalkthroughAttr, 'steps' | 'preview'>,
32
24
  ): WalkthroughState {
33
- const { filters, filesets, inputs, toggles } = attributes;
34
- const stepsState = useCodeWalkthroughSteps(steps);
35
- const controlsState = useCodeWalkthroughControls(filters, inputs, toggles);
25
+ const { filters, filesets, inputs, toggles, __idx } = attributes;
26
+ /*
27
+ We only enable deep linking for the first CodeWalkthrough,
28
+ because we don't expect more than one on the same page.
29
+ Any subsequent walkthroughs have it disabled to avoid
30
+ collisions/conflicts in the URL.
31
+ */
32
+ const enableDeepLink = __idx === 1;
33
+
34
+ const stepsState = useCodeWalkthroughSteps(steps, enableDeepLink);
35
+ const controlsState = useCodeWalkthroughControls(filters, inputs, toggles, enableDeepLink);
36
36
 
37
37
  const files: CodeWalkthroughFile[] = filesets
38
38
  .filter((fileset) => controlsState.areConditionsMet(fileset))
@@ -0,0 +1,51 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import type { CodeWalkthroughFile } from '@redocly/config';
4
+ import type { IconProps } from '@redocly/theme/icons/types';
5
+
6
+ import {
7
+ getFileIconByExt,
8
+ removeLeadingSlash,
9
+ findClosestCommonDirectory,
10
+ } from '@redocly/theme/core/utils';
11
+
12
+ export type RenderableFile = CodeWalkthroughFile & {
13
+ FileIcon: React.FunctionComponent<IconProps>;
14
+ parentFolder: string;
15
+ isNameDuplicate: boolean;
16
+ inRootDir: boolean;
17
+ };
18
+
19
+ export function useRenderableFiles(files: CodeWalkthroughFile[]): RenderableFile[] {
20
+ return useMemo(
21
+ function () {
22
+ const filePaths = files.map(({ path }) => path);
23
+ const rootDir = findClosestCommonDirectory(filePaths);
24
+
25
+ const renderableFiles = files.map((file) => {
26
+ const FileIcon = getFileTypeIcon(file.basename);
27
+ const parentFolder = file.path.split('/').slice(-2, -1)[0];
28
+ const isNameDuplicate = files.some(
29
+ (_file) => file.basename === _file.basename && file.path !== _file.path,
30
+ );
31
+ const inRootDir = file.path === `${removeLeadingSlash(rootDir)}/${file.basename}`;
32
+
33
+ return {
34
+ ...file,
35
+ FileIcon,
36
+ inRootDir,
37
+ parentFolder,
38
+ isNameDuplicate,
39
+ };
40
+ });
41
+
42
+ return renderableFiles;
43
+ },
44
+ [files],
45
+ );
46
+ }
47
+
48
+ function getFileTypeIcon(basename: string) {
49
+ const extension = basename.split('.').pop()?.toLowerCase() || '';
50
+ return getFileIconByExt(extension);
51
+ }
@@ -32,3 +32,4 @@ export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-walkthrough';
32
32
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-walkthrough-steps';
33
33
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-walkthrough-controls';
34
34
  export * from '@redocly/theme/core/hooks/code-walkthrough/use-code-panel';
35
+ export * from '@redocly/theme/core/hooks/code-walkthrough/use-renderable-files';