@redocly/theme 0.48.0 → 0.48.2

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 (98) hide show
  1. package/lib/components/DatePicker/variables.js +1 -1
  2. package/lib/components/Feedback/Mood.js +14 -9
  3. package/lib/components/Search/SearchDialog.js +9 -3
  4. package/lib/components/Search/variables.js +4 -0
  5. package/lib/components/Segmented/Segmented.d.ts +4 -4
  6. package/lib/components/Segmented/Segmented.js +4 -7
  7. package/lib/components/Tag/Tag.d.ts +1 -0
  8. package/lib/components/Tag/Tag.js +3 -2
  9. package/lib/core/contexts/CodeWalkthrough/CodeWalkthroughControlsContext.js +2 -6
  10. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.d.ts +7 -10
  11. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.js +63 -97
  12. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.d.ts +1 -2
  13. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +20 -15
  14. package/lib/core/hooks/code-walkthrough/use-code-walkthrough.d.ts +2 -7
  15. package/lib/core/hooks/code-walkthrough/use-code-walkthrough.js +10 -3
  16. package/lib/core/hooks/code-walkthrough/use-renderable-files.d.ts +9 -0
  17. package/lib/core/hooks/code-walkthrough/use-renderable-files.js +28 -0
  18. package/lib/core/hooks/index.d.ts +1 -0
  19. package/lib/core/hooks/index.js +1 -0
  20. package/lib/core/styles/global.js +18 -0
  21. package/lib/core/types/l10n.d.ts +1 -1
  22. package/lib/core/utils/download-code-walkthrough.d.ts +4 -2
  23. package/lib/core/utils/download-code-walkthrough.js +9 -1
  24. package/lib/core/utils/find-closest-common-directory.d.ts +6 -0
  25. package/lib/core/utils/find-closest-common-directory.js +51 -0
  26. package/lib/core/utils/get-code-walkthrough-file-text.d.ts +4 -2
  27. package/lib/core/utils/get-file-icon.js +6 -0
  28. package/lib/core/utils/index.d.ts +1 -0
  29. package/lib/core/utils/index.js +1 -0
  30. package/lib/core/utils/replace-inputs-with-value.d.ts +1 -1
  31. package/lib/core/utils/replace-inputs-with-value.js +9 -10
  32. package/lib/icons/DocumentJavaIcon/DocumentJavaIcon.d.ts +9 -0
  33. package/lib/icons/DocumentJavaIcon/DocumentJavaIcon.js +22 -0
  34. package/lib/icons/DocumentJavaIcon/index.d.ts +1 -0
  35. package/lib/icons/DocumentJavaIcon/index.js +6 -0
  36. package/lib/icons/DocumentPythonIcon/DocumentPythonIcon.d.ts +9 -0
  37. package/lib/icons/DocumentPythonIcon/DocumentPythonIcon.js +23 -0
  38. package/lib/icons/DocumentPythonIcon/index.d.ts +1 -0
  39. package/lib/icons/DocumentPythonIcon/index.js +6 -0
  40. package/lib/icons/DocumentShellIcon/DocumentShellIcon.d.ts +9 -0
  41. package/lib/icons/DocumentShellIcon/DocumentShellIcon.js +22 -0
  42. package/lib/icons/DocumentShellIcon/index.d.ts +1 -0
  43. package/lib/icons/DocumentShellIcon/index.js +6 -0
  44. package/lib/icons/__tests__/IconTestUtils.d.ts +7 -0
  45. package/lib/icons/__tests__/IconTestUtils.js +33 -0
  46. package/lib/layouts/CodeWalkthroughLayout.js +4 -1
  47. package/lib/markdoc/components/CodeWalkthrough/CodeContainer.js +25 -4
  48. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.d.ts +5 -4
  49. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.js +15 -2
  50. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +1 -1
  51. package/lib/markdoc/components/CodeWalkthrough/CodePanelHeader.js +29 -23
  52. package/lib/markdoc/components/CodeWalkthrough/CodePanelPreview.js +1 -1
  53. package/lib/markdoc/components/CodeWalkthrough/CodePanelToolbar.js +1 -1
  54. package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +5 -2
  55. package/lib/markdoc/components/CodeWalkthrough/CodeToggle.js +5 -5
  56. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +3 -3
  57. package/lib/markdoc/components/CodeWalkthrough/Input.js +7 -5
  58. package/lib/markdoc/tags/code-walkthrough.js +5 -0
  59. package/package.json +3 -3
  60. package/src/components/DatePicker/variables.ts +1 -1
  61. package/src/components/Feedback/Mood.tsx +16 -7
  62. package/src/components/Search/SearchDialog.tsx +52 -36
  63. package/src/components/Search/variables.ts +4 -0
  64. package/src/components/Segmented/Segmented.tsx +10 -10
  65. package/src/components/Tag/Tag.tsx +1 -1
  66. package/src/core/contexts/CodeWalkthrough/CodeWalkthroughControlsContext.tsx +2 -8
  67. package/src/core/hooks/code-walkthrough/use-code-walkthrough-controls.ts +90 -142
  68. package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +30 -18
  69. package/src/core/hooks/code-walkthrough/use-code-walkthrough.ts +13 -13
  70. package/src/core/hooks/code-walkthrough/use-renderable-files.ts +51 -0
  71. package/src/core/hooks/index.ts +1 -0
  72. package/src/core/styles/global.ts +18 -0
  73. package/src/core/types/l10n.ts +3 -1
  74. package/src/core/utils/download-code-walkthrough.ts +16 -4
  75. package/src/core/utils/find-closest-common-directory.ts +51 -0
  76. package/src/core/utils/get-code-walkthrough-file-text.ts +3 -3
  77. package/src/core/utils/get-file-icon.ts +7 -0
  78. package/src/core/utils/index.ts +1 -0
  79. package/src/core/utils/replace-inputs-with-value.ts +12 -9
  80. package/src/icons/DocumentJavaIcon/DocumentJavaIcon.tsx +33 -0
  81. package/src/icons/DocumentJavaIcon/index.ts +1 -0
  82. package/src/icons/DocumentPythonIcon/DocumentPythonIcon.tsx +37 -0
  83. package/src/icons/DocumentPythonIcon/index.ts +1 -0
  84. package/src/icons/DocumentShellIcon/DocumentShellIcon.tsx +33 -0
  85. package/src/icons/DocumentShellIcon/index.ts +1 -0
  86. package/src/icons/__tests__/IconTestUtils.tsx +31 -0
  87. package/src/layouts/CodeWalkthroughLayout.tsx +5 -1
  88. package/src/markdoc/components/CodeWalkthrough/CodeContainer.tsx +28 -3
  89. package/src/markdoc/components/CodeWalkthrough/CodeFilters.tsx +21 -4
  90. package/src/markdoc/components/CodeWalkthrough/CodePanel.tsx +1 -1
  91. package/src/markdoc/components/CodeWalkthrough/CodePanelHeader.tsx +64 -47
  92. package/src/markdoc/components/CodeWalkthrough/CodePanelPreview.tsx +1 -1
  93. package/src/markdoc/components/CodeWalkthrough/CodePanelToolbar.tsx +1 -1
  94. package/src/markdoc/components/CodeWalkthrough/CodeStep.tsx +5 -1
  95. package/src/markdoc/components/CodeWalkthrough/CodeToggle.tsx +5 -5
  96. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +11 -5
  97. package/src/markdoc/components/CodeWalkthrough/Input.tsx +8 -6
  98. package/src/markdoc/tags/code-walkthrough.ts +5 -0
@@ -7,14 +7,9 @@ import type {
7
7
  InputsMarkdocAttr,
8
8
  TogglesMarkdocAttr,
9
9
  CodeWalkthroughControls,
10
- TogglesState,
11
- InputsState,
12
- FiltersState,
13
10
  CodeWalkthroughFilterItem,
14
- ControlState,
15
- ControlTypeValue,
16
- ControlType,
17
11
  CodeWalkthroughConditionsObject,
12
+ CodeWalkthroughControlsState,
18
13
  } from '@redocly/config';
19
14
 
20
15
  import {
@@ -24,8 +19,6 @@ import {
24
19
  replaceInputsWithValue,
25
20
  } from '@redocly/theme/core/utils';
26
21
 
27
- export type getState<T extends ControlType> = (id: string) => Omit<ControlState<T>, 'type'> | null;
28
- export type changeState<T extends ControlType> = (id: string, value: ControlTypeValue<T>) => void;
29
22
  export type ActiveFilter = {
30
23
  id: string;
31
24
  label?: string;
@@ -34,15 +27,8 @@ export type ActiveFilter = {
34
27
 
35
28
  export type WalkthroughControlsState = {
36
29
  activeFilters: ActiveFilter[];
37
- /* Toggle State Controls */
38
- getToggleState: getState<'toggle'>;
39
- changeToggleState: changeState<'toggle'>;
40
- /* Input State Controls */
41
- getInputState: getState<'input'>;
42
- changeInputState: changeState<'input'>;
43
- /* Filter State Controls */
44
- getFilterState: getState<'filter'>;
45
- changeFilterState: changeState<'filter'>;
30
+ getControlState: (id: string) => { value: string | boolean; render: boolean } | null;
31
+ changeControlState: (id: string, value: string | boolean) => void;
46
32
  /* Utility */
47
33
  areConditionsMet: (conditions: CodeWalkthroughConditionsObject) => boolean;
48
34
  handleDownloadCode: (files: CodeWalkthroughFile[]) => Promise<void>;
@@ -60,163 +46,126 @@ export function useCodeWalkthroughControls(
60
46
  filters: Record<string, CodeWalkthroughFilter>,
61
47
  inputs: InputsMarkdocAttr,
62
48
  toggles: TogglesMarkdocAttr,
49
+ enableDeepLink: boolean,
63
50
  ): WalkthroughControlsState {
64
51
  const location = useLocation();
65
52
  const navigate = useNavigate();
66
53
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
67
54
 
68
- const [togglesState, setTogglesState] = useState(() => {
69
- const initialState: TogglesState = {};
55
+ const [controlsState, setControlsState] = useState(() => {
56
+ const initialState: CodeWalkthroughControlsState = {};
57
+
70
58
  for (const [id, toggle] of Object.entries(toggles)) {
71
59
  initialState[id] = {
72
60
  ...toggle,
73
61
  render: true,
74
62
  type: 'toggle',
75
- value: searchParams.get(id) === 'true',
63
+ value: enableDeepLink ? searchParams.get(id) === 'true' : false,
76
64
  };
77
65
  }
78
- return initialState;
79
- });
80
-
81
- const changeToggleState = (toggleId: string, checked: boolean) => {
82
- setTogglesState((prev) => {
83
- const toggle = prev[toggleId];
84
- if (toggle) {
85
- return {
86
- ...prev,
87
- [toggleId]: {
88
- ...toggle,
89
- value: checked,
90
- },
91
- };
92
- } else {
93
- return prev;
94
- }
95
- });
96
- };
97
-
98
- const getToggleState = (toggleId: string) => {
99
- const toggleState = togglesState[toggleId];
100
-
101
- if (toggleState) {
102
- return {
103
- render: toggleState.render,
104
- value: toggleState.value,
105
- };
106
- } else {
107
- return null;
108
- }
109
- };
110
-
111
- const [inputsState, setInputsState] = useState(() => {
112
- const initialState: InputsState = {};
113
66
 
114
67
  for (const [id, input] of Object.entries(inputs)) {
115
68
  initialState[id] = {
116
69
  ...input,
117
70
  render: true,
118
71
  type: 'input',
119
- value: searchParams.get(id) || input.value,
72
+ value: enableDeepLink ? (searchParams.get(id) ?? input.value) : input.value,
120
73
  };
121
74
  }
122
- return initialState;
123
- });
124
-
125
- const changeInputState = (inputId: string, value: string) => {
126
- setInputsState((prev) => {
127
- const input = prev[inputId];
128
- if (input) {
129
- return {
130
- ...prev,
131
- [inputId]: {
132
- ...input,
133
- value,
134
- },
135
- };
136
- } else {
137
- return prev;
138
- }
139
- });
140
- };
141
-
142
- const getInputState = (inputId: string) => {
143
- const inputState = inputsState[inputId];
144
-
145
- if (inputState) {
146
- return {
147
- render: inputState.render,
148
- value: inputState.value,
149
- };
150
- } else {
151
- return null;
152
- }
153
- };
154
-
155
- const [filtersState, setFiltersState] = useState(() => {
156
- const initialState: FiltersState = {};
157
75
 
158
76
  for (const [id, filter] of Object.entries(filters)) {
77
+ const defaultValue = filter?.items?.[0]?.value || '';
159
78
  initialState[id] = {
160
79
  ...filter,
161
80
  render: true,
162
81
  type: 'filter',
163
- value: searchParams.get(id) || filter?.items?.[0]?.value || '',
82
+ value: enableDeepLink ? (searchParams.get(id) ?? defaultValue) : defaultValue,
164
83
  };
165
84
  }
166
85
 
167
86
  return initialState;
168
87
  });
169
88
 
170
- const changeFilterState = (filterId: string, value: string) => {
171
- setFiltersState((prev) => {
172
- const filter = prev[filterId];
173
- if (filter) {
174
- return {
175
- ...prev,
176
- [filterId]: {
177
- ...filter,
178
- value,
179
- },
180
- };
181
- } else {
89
+ const changeControlState = (id: string, value: string | boolean) => {
90
+ setControlsState((prev) => {
91
+ const control = prev[id];
92
+ if (!control) {
93
+ console.error(`Control with id "${id}" not found.`);
182
94
  return prev;
183
95
  }
96
+
97
+ switch (control.type) {
98
+ case 'input':
99
+ if (typeof value !== 'string') {
100
+ console.error(
101
+ `Invalid value type for input "${id}". Input control type requires a string value.`,
102
+ );
103
+ return prev;
104
+ }
105
+ break;
106
+
107
+ case 'toggle':
108
+ if (typeof value !== 'boolean') {
109
+ console.error(
110
+ `Invalid value type for toggle "${id}". Toggle control type requires a boolean value.`,
111
+ );
112
+ return prev;
113
+ }
114
+ break;
115
+
116
+ case 'filter':
117
+ if (typeof value !== 'string') {
118
+ console.error(
119
+ `Invalid value type for filter "${id}". Filter control type requires a string value.`,
120
+ );
121
+ return prev;
122
+ }
123
+ break;
124
+
125
+ default:
126
+ console.error(
127
+ `Invalid control type "${(control as { type: string })?.type}" for control "${id}". Allowed types are "toggle", "input", or "filter".`,
128
+ );
129
+ return prev;
130
+ }
131
+
132
+ return {
133
+ ...prev,
134
+ [id]: {
135
+ ...control,
136
+ value,
137
+ },
138
+ } as CodeWalkthroughControlsState;
184
139
  });
185
140
  };
186
141
 
187
- const getFilterState = (filterId: string) => {
188
- const filterState = filtersState[filterId];
142
+ const getControlState = (id: string) => {
143
+ const controlState = controlsState[id];
189
144
 
190
- if (filterState) {
145
+ if (controlState) {
191
146
  return {
192
- render: filterState.render,
193
- value: filterState.value,
147
+ render: controlState.render,
148
+ value: controlState.value,
194
149
  };
195
150
  } else {
196
151
  return null;
197
152
  }
198
153
  };
199
154
 
200
- const state = {
201
- ...filtersState,
202
- ...togglesState,
203
- ...inputsState,
204
- };
205
-
206
155
  const walkthroughContext = useMemo(() => {
207
156
  const areConditionsMet = (conditions: CodeWalkthroughConditionsObject) =>
208
- matchCodeWalkthroughConditions(conditions, state);
157
+ matchCodeWalkthroughConditions(conditions, controlsState);
209
158
 
210
- for (const [id, element] of Object.entries(state)) {
211
- if (element && !areConditionsMet(element)) {
212
- state[id].render = false;
213
- state[id].value = defaultControlsValues[element.type];
159
+ for (const [id, control] of Object.entries(controlsState)) {
160
+ if (control && !areConditionsMet(control)) {
161
+ controlsState[id].render = false;
162
+ controlsState[id].value = defaultControlsValues[control.type];
214
163
  }
215
164
  }
216
165
 
217
166
  const activeFilters = [];
218
167
  for (const [id, filter] of Object.entries(filters)) {
219
- if (!filtersState[id].render) {
168
+ if (!controlsState[id].render) {
220
169
  continue;
221
170
  }
222
171
 
@@ -226,18 +175,18 @@ export function useCodeWalkthroughControls(
226
175
  const activeItems = items.filter((item) => areConditionsMet(item));
227
176
 
228
177
  if (activeItems.length === 0) {
229
- filtersState[id].render = false;
230
- filtersState[id].value = defaultControlsValues['filter'];
178
+ controlsState[id].render = false;
179
+ controlsState[id].value = defaultControlsValues['filter'];
231
180
  continue;
232
181
  }
233
182
 
234
- const currentValue = filtersState[id].value;
183
+ const currentValue = controlsState[id].value;
235
184
  if (currentValue) {
236
185
  const isValueInActiveItems =
237
186
  activeItems.findIndex(({ value }) => value === currentValue) !== -1;
238
- filtersState[id].value = isValueInActiveItems ? currentValue : activeItems[0].value;
187
+ controlsState[id].value = isValueInActiveItems ? currentValue : activeItems[0].value;
239
188
  } else {
240
- filtersState[id].value = activeItems[0].value;
189
+ controlsState[id].value = activeItems[0].value;
241
190
  }
242
191
 
243
192
  activeFilters.push({
@@ -247,11 +196,15 @@ export function useCodeWalkthroughControls(
247
196
  });
248
197
  }
249
198
 
199
+ const inputsState = Object.fromEntries(
200
+ Object.entries(controlsState).filter(([_, controlState]) => controlState.type === 'input'),
201
+ ) as Record<string, { value: string }>;
202
+
250
203
  const handleDownloadCode = (files: CodeWalkthroughFile[]) =>
251
- downloadCodeWalkthrough(files, state, inputsState);
204
+ downloadCodeWalkthrough(files, controlsState, inputsState);
252
205
 
253
206
  const getFileText = (file: CodeWalkthroughFile) =>
254
- getCodeWalkthroughFileText(file, state, inputsState);
207
+ getCodeWalkthroughFileText(file, controlsState, inputsState);
255
208
 
256
209
  const populateInputsWithValue = (node: string) => replaceInputsWithValue(node, inputsState);
257
210
 
@@ -262,17 +215,19 @@ export function useCodeWalkthroughControls(
262
215
  getFileText,
263
216
  populateInputsWithValue,
264
217
  };
265
- // Ignore state in dependency array as it's simply a combination of toggles, filters and inputs.
266
- // eslint-disable-next-line react-hooks/exhaustive-deps
267
- }, [filters, filtersState, togglesState, inputsState]);
218
+ }, [filters, controlsState]);
268
219
 
269
220
  /**
270
221
  * Update the URL search params with the current state of the filters and inputs
271
222
  */
272
223
  useEffect(() => {
224
+ if (!enableDeepLink) {
225
+ return;
226
+ }
227
+
273
228
  const newSearchParams = new URLSearchParams(Array.from(searchParams.entries()));
274
229
 
275
- for (const [id, { value }] of Object.entries(state)) {
230
+ for (const [id, { value }] of Object.entries(controlsState)) {
276
231
  if (value) {
277
232
  newSearchParams.set(id, value.toString());
278
233
  } else {
@@ -285,18 +240,11 @@ export function useCodeWalkthroughControls(
285
240
  navigate({ search: newSearch });
286
241
  // Ignore searchParams in dependency array to avoid infinite re-renders
287
242
  // eslint-disable-next-line react-hooks/exhaustive-deps
288
- }, [filters, filtersState, togglesState, inputsState, navigate, location, state]);
243
+ }, [filters, controlsState, navigate, location]);
289
244
 
290
245
  return {
291
- getInputState,
292
- changeInputState,
293
-
294
- getToggleState,
295
- changeToggleState,
296
-
297
- getFilterState,
298
- changeFilterState,
299
-
246
+ changeControlState,
247
+ getControlState,
300
248
  ...walkthroughContext,
301
249
  };
302
250
  }
@@ -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';
@@ -1019,6 +1019,24 @@ const error = css`
1019
1019
  --detailed-error-message-width: 100%;
1020
1020
  --detailed-error-message-font-size: var(--font-size-base);
1021
1021
  --detailed-error-message-font-family: var(--code-block-controls-font-family);
1022
+
1023
+ --compilation-error-description-padding: 0 0 var(--spacing-base);
1024
+ --compilation-error-codeframe-margin: var(--spacing-xs) 0 0 0;
1025
+ --compilation-error-codeframe-padding: var(--spacing-xs);
1026
+ --compilation-error-block-padding: var(--spacing-base);
1027
+ --compilation-error-block-margin: var(--spacing-xl) 0;
1028
+ --compilation-error-title-font-size: var(--font-size-xl);
1029
+ --compilation-error-header-font-size: var(--font-size-lg);
1030
+ --compilation-error-text-font-size: var(--font-size-base);
1031
+ --compilation-error-header-padding: var(--spacing-sm);
1032
+ --compilation-error-container-margin: var(--spacing-xxl) auto;
1033
+ --compilation-error-container-padding: var(--spacing-lg);
1034
+ --compilation-error-page-max-width: 800px;
1035
+ --compilation-error-font-family: var(--font-family-base);
1036
+ --compilation-error-fix-instruction-margin: var(--spacing-sm) 0 0 0;
1037
+ --compilation-error-highlighted-text-padding: 0 var(--spacing-xxs);
1038
+ --compilation-error-divider-margin: var(--spacing-base) 0;
1039
+ --compilation-error-file-header-margin: 0 0 var(--spacing-xs) 0;
1022
1040
  `;
1023
1041
 
1024
1042
  const modal = css`
@@ -85,8 +85,9 @@ export type TranslationKey =
85
85
  | 'search.filter.field.reset'
86
86
  | 'search.ai.thinkingText'
87
87
  | 'search.ai.resourcesFound'
88
- | 'search.aiButton'
88
+ | 'search.ai.button'
89
89
  | 'search.ai.label'
90
+ | 'search.ai.disclaimer'
90
91
  | 'toc.header'
91
92
  | 'footer.copyrightText'
92
93
  | 'page.homeButton'
@@ -196,6 +197,7 @@ export type TranslationKey =
196
197
  | 'openapi.expandAll'
197
198
  | 'openapi.collapseAll'
198
199
  | 'openapi.noResponseExample'
200
+ | 'openapi.noResponseContent'
199
201
  | 'openapi.noRequestPayload'
200
202
  | 'openapi.hidePattern'
201
203
  | 'openapi.showPattern'
@@ -1,9 +1,13 @@
1
1
  import { saveAs } from 'file-saver';
2
2
  import JSZip from 'jszip';
3
3
 
4
- import type { CodeWalkthroughFile, InputsState } from '@redocly/config';
4
+ import type { CodeWalkthroughFile } from '@redocly/config';
5
5
 
6
- import { getCodeWalkthroughFileText } from '@redocly/theme/core/utils';
6
+ import {
7
+ findClosestCommonDirectory,
8
+ getCodeWalkthroughFileText,
9
+ removeLeadingSlash,
10
+ } from '@redocly/theme/core/utils';
7
11
 
8
12
  // https://github.com/Stuk/jszip/issues/196#issuecomment-69503828
9
13
  JSZip.support.nodebuffer = false;
@@ -11,13 +15,21 @@ JSZip.support.nodebuffer = false;
11
15
  export async function downloadCodeWalkthrough(
12
16
  files: CodeWalkthroughFile[],
13
17
  state: Record<string, { value: string | boolean }>,
14
- inputsState: InputsState,
18
+ inputsState: Record<string, { value: string }>,
15
19
  ) {
16
20
  const zip = new JSZip();
17
21
 
22
+ const filePaths = files.map(({ path }) => path);
23
+ const commonClosestDirectory = findClosestCommonDirectory(filePaths);
24
+
18
25
  for (const file of files) {
19
26
  const fileContent = getCodeWalkthroughFileText(file, state, inputsState);
20
- zip.file(file.basename, fileContent);
27
+ if (commonClosestDirectory === '/') {
28
+ zip.file(file.path, fileContent);
29
+ } else {
30
+ const filePath = file.path.replace(removeLeadingSlash(`${commonClosestDirectory}/`), '');
31
+ zip.file(filePath, fileContent);
32
+ }
21
33
  }
22
34
 
23
35
  const zipContent = await zip.generateAsync({ type: 'blob' });