@redocly/theme 0.55.0-next.1 → 0.55.0-next.3

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 (47) hide show
  1. package/lib/components/JsonViewer/JsonViewer.d.ts +2 -0
  2. package/lib/components/JsonViewer/JsonViewer.js +3 -1
  3. package/lib/components/Marker/Marker.d.ts +10 -0
  4. package/lib/components/Marker/Marker.js +62 -0
  5. package/lib/core/contexts/CodeWalkthrough/CodeWalkthroughStepsContext.d.ts +1 -1
  6. package/lib/core/contexts/CodeWalkthrough/CodeWalkthroughStepsContext.js +5 -2
  7. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.d.ts +17 -9
  8. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +242 -47
  9. package/lib/core/hooks/code-walkthrough/use-code-walkthrough.d.ts +9 -2
  10. package/lib/core/hooks/code-walkthrough/use-code-walkthrough.js +2 -2
  11. package/lib/core/hooks/index.d.ts +1 -1
  12. package/lib/core/hooks/index.js +1 -1
  13. package/lib/core/hooks/use-active-page-version.d.ts +1 -0
  14. package/lib/core/hooks/{use-page-active-version.js → use-active-page-version.js} +3 -3
  15. package/lib/core/types/code-walkthrough.d.ts +13 -0
  16. package/lib/core/types/code-walkthrough.js +3 -0
  17. package/lib/core/types/index.d.ts +2 -0
  18. package/lib/core/types/index.js +2 -0
  19. package/lib/core/types/marker.d.ts +4 -0
  20. package/lib/core/types/marker.js +3 -0
  21. package/lib/core/utils/js-utils.d.ts +18 -0
  22. package/lib/core/utils/js-utils.js +31 -0
  23. package/lib/index.d.ts +1 -0
  24. package/lib/index.js +1 -0
  25. package/lib/markdoc/components/CodeWalkthrough/CodeStep.d.ts +1 -2
  26. package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +26 -20
  27. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +32 -3
  28. package/lib/markdoc/tags/code-step.js +0 -3
  29. package/lib/markdoc/tags/code-walkthrough.js +0 -1
  30. package/package.json +1 -1
  31. package/src/components/JsonViewer/JsonViewer.tsx +6 -0
  32. package/src/components/Marker/Marker.tsx +53 -0
  33. package/src/core/contexts/CodeWalkthrough/CodeWalkthroughStepsContext.tsx +6 -3
  34. package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +326 -65
  35. package/src/core/hooks/code-walkthrough/use-code-walkthrough.ts +9 -6
  36. package/src/core/hooks/index.ts +1 -1
  37. package/src/core/hooks/{use-page-active-version.ts → use-active-page-version.ts} +1 -1
  38. package/src/core/types/code-walkthrough.ts +15 -0
  39. package/src/core/types/index.ts +2 -0
  40. package/src/core/types/marker.ts +4 -0
  41. package/src/core/utils/js-utils.ts +31 -0
  42. package/src/index.ts +1 -0
  43. package/src/markdoc/components/CodeWalkthrough/CodeStep.tsx +76 -36
  44. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +8 -3
  45. package/src/markdoc/tags/code-step.ts +0 -3
  46. package/src/markdoc/tags/code-walkthrough.ts +0 -1
  47. package/lib/core/hooks/use-page-active-version.d.ts +0 -1
@@ -2,28 +2,31 @@ import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
2
2
  import { useLocation, useNavigate } from 'react-router-dom';
3
3
 
4
4
  import type { CodeWalkthroughStepAttr } from '@redocly/config';
5
+ import type { ActiveStep, MarkerArea, WalkthroughStepsState } from '@redocly/theme/core/types';
5
6
 
6
- import { getAdjacentValues } from '@redocly/theme/core/utils';
7
+ import { getAdjacentValues, insertAt, isBrowser, removeElement } from '@redocly/theme/core/utils';
7
8
  import { ACTIVE_STEP_QUERY_PARAM } from '@redocly/theme/core/constants';
8
9
 
9
- type ActiveStep = string | null;
10
10
  type CodeWalkthroughStep = CodeWalkthroughStepAttr & {
11
11
  compRef?: HTMLElement;
12
+ markerRef?: HTMLElement;
12
13
  };
13
14
 
14
- export type WalkthroughStepsState = {
15
- activeStep: ActiveStep;
16
- setActiveStep: (stepId: ActiveStep) => void;
17
- register: (element: HTMLElement) => void;
18
- unregister: (element: HTMLElement) => void;
19
- lockObserver?: React.RefObject<boolean>;
20
- filtersElementRef?: React.RefObject<HTMLDivElement | null>;
15
+ type StepWithIndex = CodeWalkthroughStep & {
16
+ index: number;
21
17
  };
22
18
 
23
- export function useCodeWalkthroughSteps(
24
- steps: CodeWalkthroughStep[],
25
- enableDeepLink: boolean,
26
- ): WalkthroughStepsState {
19
+ type Params = {
20
+ steps: CodeWalkthroughStep[];
21
+ enableDeepLink: boolean;
22
+ root: React.RefObject<HTMLDivElement | null>;
23
+ };
24
+
25
+ export function useCodeWalkthroughSteps({
26
+ steps,
27
+ enableDeepLink,
28
+ root,
29
+ }: Params): WalkthroughStepsState {
27
30
  const location = useLocation();
28
31
  const navigate = useNavigate();
29
32
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
@@ -38,80 +41,169 @@ export function useCodeWalkthroughSteps(
38
41
  enableDeepLink ? searchParams.get(ACTIVE_STEP_QUERY_PARAM) : null,
39
42
  );
40
43
 
41
- // eslint-disable-next-line react-hooks/exhaustive-deps
42
- const _steps = useMemo(() => steps, [JSON.stringify(steps)]);
43
-
44
- const register = useCallback(
45
- (element: HTMLElement) => {
46
- // for some reason, the observer is not ready immediately
47
- setTimeout(() => {
48
- if (observerRef.current) {
49
- const stepKey = Number(element.dataset.stepKey);
50
- if (Number.isInteger(stepKey) && stepKey >= 0 && _steps[stepKey]) {
51
- _steps[stepKey].compRef = element;
52
- }
44
+ const stepsMap = useMemo(() => {
45
+ const map = new Map<string, StepWithIndex>();
46
+ steps.forEach((step, index) => {
47
+ map.set(step.id, { ...step, index });
48
+ });
49
+
50
+ return map;
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, [JSON.stringify(steps)]);
53
+
54
+ const options = useMemo(() => {
55
+ if (!isBrowser()) {
56
+ return null;
57
+ }
58
+
59
+ const filtersElementHeight = filtersElementRef.current?.clientHeight || 0;
60
+ const navbarHeight = document.querySelector('nav')?.getBoundingClientRect().height || 0;
61
+
62
+ return {
63
+ filtersElementHeight,
64
+ navbarHeight,
65
+ };
66
+ }, []);
67
+
68
+ const [visibleSteps, setVisibleSteps] = useState<StepWithIndex[]>([]);
69
+
70
+ const [markers, setMarkers] = useState<Record<string, MarkerArea>>({});
71
+
72
+ useEffect(() => {
73
+ if (!root.current || !visibleSteps.length || !options) {
74
+ return;
75
+ }
76
+
77
+ const markersMinTopOffset = options.filtersElementHeight + options.navbarHeight;
78
+
79
+ const rootHeight = root.current?.clientHeight ?? 0;
80
+ const lastStepOffset = visibleSteps[visibleSteps.length - 1]?.compRef?.offsetTop ?? 0;
81
+ const deficit = Math.max(lastStepOffset - (rootHeight - window.innerHeight), 0);
82
+
83
+ const groups = getGroups(visibleSteps);
84
+ let markers: number[] = groups.flatMap((group) => getGroupMarkers(group));
85
+
86
+ if (deficit) {
87
+ const startOffset = markersMinTopOffset;
88
+ const endOffset = Math.max(rootHeight - window.innerHeight, 0);
53
89
 
54
- observerRef.current.observe(element);
55
- observedElementsRef.current.add(element);
90
+ markers = distributeMarkers({
91
+ endOffset,
92
+ markers,
93
+ startOffset: markersMinTopOffset < endOffset ? startOffset : 0,
94
+ });
95
+ }
96
+
97
+ setMarkers(
98
+ markers.reduce(
99
+ (acc, marker, index) => {
100
+ const step = visibleSteps[index];
101
+ acc[step.id] = {
102
+ offset: marker,
103
+ height:
104
+ markers[index + 1] || !step.compRef
105
+ ? (markers[index + 1] ?? rootHeight) - marker
106
+ : step.compRef.clientHeight,
107
+ };
108
+ return acc;
109
+ },
110
+ {} as Record<string, MarkerArea>,
111
+ ),
112
+ );
113
+
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ }, [visibleSteps, root.current, options]);
116
+
117
+ const registerMarker = useCallback(
118
+ (stepId: string, element: HTMLElement) => {
119
+ if (observerRef.current) {
120
+ const step = stepsMap.get(stepId);
121
+ if (step) {
122
+ step.markerRef = element;
56
123
  }
57
- }, 10);
124
+
125
+ observerRef.current.observe(element);
126
+ observedElementsRef.current.add(element);
127
+ }
58
128
  },
59
- [_steps],
129
+ [stepsMap],
60
130
  );
61
131
 
62
- const unregister = useCallback(
63
- (element: HTMLElement) => {
132
+ const removeMarker = useCallback(
133
+ (stepId: string, element: HTMLElement) => {
64
134
  if (observerRef.current) {
65
- const stepKey = Number(element.dataset.stepKey);
66
- if (Number.isInteger(stepKey) && stepKey >= 0 && _steps[stepKey]) {
67
- _steps[stepKey].compRef = undefined;
135
+ const step = stepsMap.get(stepId);
136
+ if (step) {
137
+ step.markerRef = undefined;
68
138
  }
69
139
 
70
140
  observerRef.current.unobserve(element);
71
141
  observedElementsRef.current.delete(element);
72
142
  }
73
143
  },
74
- [_steps],
144
+ [stepsMap],
145
+ );
146
+
147
+ const registerStep = useCallback(
148
+ (stepId: string, element: HTMLElement) => {
149
+ const step = stepsMap.get(stepId);
150
+ if (!step) {
151
+ return;
152
+ }
153
+
154
+ step.compRef = element;
155
+ setVisibleSteps((prevSteps) => insertAt(prevSteps, step.index, step));
156
+ },
157
+ [stepsMap],
158
+ );
159
+
160
+ const removeStep = useCallback(
161
+ (stepId: string) => {
162
+ const step = stepsMap.get(stepId);
163
+ if (!step) {
164
+ return;
165
+ }
166
+
167
+ step.compRef = undefined;
168
+ setVisibleSteps((prevSteps) => removeElement(prevSteps, step));
169
+ setActiveStep((prevStep) => (prevStep === stepId ? null : prevStep));
170
+ },
171
+ [stepsMap],
75
172
  );
76
173
 
77
174
  const observerCallback = useCallback(
78
175
  (entries: IntersectionObserverEntry[]) => {
79
- if (lockObserver.current) {
176
+ if (lockObserver.current || !visibleSteps.length) {
80
177
  return;
81
178
  }
82
179
 
83
- const renderedSteps = _steps.filter((step) => Boolean(step.compRef));
84
-
85
- if (renderedSteps.length < 2) {
86
- setActiveStep(renderedSteps[0]?.id || null);
180
+ if (visibleSteps.length < 2) {
181
+ setActiveStep(visibleSteps[0]?.id || null);
87
182
  return;
88
183
  }
89
184
 
90
185
  for (const entry of entries) {
91
- const stepKey = Number((entry.target as HTMLElement)?.dataset?.stepKey);
186
+ const stepId = (entry.target as HTMLElement)?.dataset?.stepId;
92
187
 
93
- if (!Number.isInteger(stepKey) || stepKey < 0) {
188
+ if (!stepId) {
94
189
  continue;
95
190
  }
96
191
 
97
192
  const { intersectionRatio, boundingClientRect, rootBounds, isIntersecting } = entry;
98
- const step = _steps[stepKey];
193
+ const step = stepsMap.get(stepId);
99
194
 
100
- const stepIndex = renderedSteps.findIndex(
101
- (renderedStep) => renderedStep.stepKey === step.stepKey,
102
- );
103
- const { next } = getAdjacentValues(renderedSteps, stepIndex);
195
+ if (!step) {
196
+ continue;
197
+ }
198
+
199
+ const stepIndex = visibleSteps.findIndex((renderedStep) => renderedStep.id === stepId);
200
+ const { next } = getAdjacentValues(visibleSteps, stepIndex);
104
201
 
105
202
  const intersectionAtTop =
106
203
  rootBounds?.bottom !== undefined && boundingClientRect.top < rootBounds.top;
107
204
  const stepGoesIn = isIntersecting;
108
205
 
109
- if (
110
- intersectionRatio > 0.8 &&
111
- intersectionRatio < 1 &&
112
- intersectionAtTop &&
113
- activeStep === null
114
- ) {
206
+ if (intersectionRatio > 0.8 && intersectionRatio < 1 && intersectionAtTop) {
115
207
  setActiveStep(step.id);
116
208
  break;
117
209
  }
@@ -125,32 +217,45 @@ export function useCodeWalkthroughSteps(
125
217
  newStep = next.id;
126
218
  }
127
219
 
128
- if (newStep !== activeStep) {
129
- setActiveStep(newStep);
130
- }
220
+ setActiveStep((prevStep) => newStep || prevStep);
131
221
 
132
222
  break;
133
223
  }
134
224
  }
135
225
  },
136
- [_steps, activeStep],
226
+ [stepsMap, visibleSteps],
137
227
  );
228
+
138
229
  useEffect(() => {
139
- const filtersElementHeight = filtersElementRef.current?.clientHeight || 0;
140
- const navbarHeight = document.querySelector('nav')?.clientHeight || 0;
230
+ if (!options) {
231
+ return;
232
+ }
233
+
141
234
  const newObserver = new IntersectionObserver(observerCallback, {
142
- threshold: [0.8, 0.85, 0.9, 0.95],
143
- rootMargin: `-${filtersElementHeight + navbarHeight}px 0px 0px 0px`,
235
+ threshold: [0.3, 0.8, 0.9, 0.95],
236
+ rootMargin: `-${options.filtersElementHeight + options.navbarHeight}px 0px 0px 0px`,
144
237
  });
145
238
 
146
- for (const observedElement of observedElementsRef.current) {
239
+ for (const observedElement of observedElementsRef.current.values()) {
147
240
  newObserver.observe(observedElement);
148
241
  }
149
242
 
150
- // Unobserve all from the old observer
151
243
  observerRef.current?.disconnect();
152
244
  observerRef.current = newObserver;
153
- }, [observerCallback]);
245
+ }, [observerCallback, markers, options]);
246
+
247
+ useEffect(() => {
248
+ if (!options) {
249
+ return;
250
+ }
251
+
252
+ const rootTopOffset = root.current?.offsetTop ?? 0;
253
+ if (!activeStep && rootTopOffset <= options.navbarHeight) {
254
+ setActiveStep(visibleSteps[0]?.id || null);
255
+ }
256
+
257
+ // eslint-disable-next-line react-hooks/exhaustive-deps
258
+ }, [activeStep, root.current, options, visibleSteps]);
154
259
 
155
260
  /**
156
261
  * Update the URL search params with the current state of the filters and inputs
@@ -176,5 +281,161 @@ export function useCodeWalkthroughSteps(
176
281
  // eslint-disable-next-line react-hooks/exhaustive-deps
177
282
  }, [activeStep]);
178
283
 
179
- return { register, unregister, lockObserver, filtersElementRef, activeStep, setActiveStep };
284
+ return {
285
+ registerStep,
286
+ removeStep,
287
+ markers,
288
+ registerMarker,
289
+ removeMarker,
290
+ lockObserver,
291
+ filtersElementRef,
292
+ activeStep,
293
+ setActiveStep,
294
+ };
295
+ }
296
+
297
+ type StepsGroup = {
298
+ freeSpace: number;
299
+ usedSpace: number;
300
+ offset: number;
301
+ steps: { offset: number; height: number }[];
302
+ };
303
+ /**
304
+ * This function analyzes the offset and height of each step to determine
305
+ * when a new group should be created. A new group is started when there is a free space
306
+ * between the two steps, treating it as the content of the next group header.
307
+ *
308
+ * @param steps - An array of `CodeWalkthroughStep` objects
309
+ *
310
+ * @returns An array of `StepsGroup` objects, each containing the offset from the top of the relative
311
+ * block, the free space at the top of the group, the total space used by the steps within the group
312
+ * and the steps themselves with relative offset and height.
313
+ */
314
+ function getGroups(steps: CodeWalkthroughStep[]): StepsGroup[] {
315
+ if (!steps.length) {
316
+ return [];
317
+ }
318
+
319
+ const firstStepOffset = steps[0]?.compRef?.offsetTop ?? 0;
320
+ const firstStepHeight = steps[0]?.compRef?.clientHeight ?? 0;
321
+ const secondStepOffset = steps[1]?.compRef?.offsetTop ?? 0;
322
+ const margin = Math.max(secondStepOffset - firstStepOffset - firstStepHeight, 0);
323
+
324
+ let groupIndex = 0;
325
+ const groups: StepsGroup[] = [
326
+ {
327
+ offset: 0,
328
+ freeSpace: firstStepOffset,
329
+ usedSpace: 0,
330
+ steps: [],
331
+ },
332
+ ];
333
+
334
+ for (let i = 0; i < steps.length; i++) {
335
+ let currentGroup = groups[groupIndex];
336
+
337
+ const step = steps[i];
338
+ const stepHeight = step.compRef?.clientHeight ?? 0;
339
+ const stepOffset = step.compRef?.offsetTop ?? 0;
340
+
341
+ const prevStepOffset = currentGroup.freeSpace + currentGroup.usedSpace;
342
+
343
+ if (prevStepOffset !== Math.max(stepOffset - currentGroup.offset, 0)) {
344
+ const offset = currentGroup.offset + currentGroup.freeSpace + currentGroup.usedSpace;
345
+
346
+ groupIndex++;
347
+ groups[groupIndex] = {
348
+ offset,
349
+ freeSpace: Math.max(stepOffset - offset, 0),
350
+ usedSpace: 0,
351
+ steps: [],
352
+ };
353
+ currentGroup = groups[groupIndex];
354
+ }
355
+
356
+ currentGroup.steps.push({
357
+ offset: stepOffset - currentGroup.offset,
358
+ height: stepHeight,
359
+ ref: step.compRef,
360
+ } as any);
361
+ currentGroup.usedSpace += stepHeight + margin;
362
+ }
363
+
364
+ return groups;
365
+ }
366
+
367
+ export function getGroupMarkers(group: StepsGroup) {
368
+ if (!group.steps.length) {
369
+ return [];
370
+ }
371
+
372
+ if (group.steps.length === 1) {
373
+ return [group.offset + group.steps[0].offset - group.freeSpace];
374
+ }
375
+
376
+ const availableFreeSpace =
377
+ group.freeSpace > 0.3 * window.innerHeight ? 0.3 * window.innerHeight : group.freeSpace;
378
+ const unusedFreeSpace = group.freeSpace - availableFreeSpace;
379
+ const lastStepOffset = group.steps[group.steps.length - 1].offset;
380
+
381
+ // distribute group free space between steps
382
+ return distributeMarkers({
383
+ startOffset: 0,
384
+ endOffset: lastStepOffset - unusedFreeSpace,
385
+ markers: group.steps.map((step) => step.offset),
386
+ additionalSteps: [(marker) => group.offset + unusedFreeSpace + marker],
387
+ });
388
+ }
389
+
390
+ /**
391
+ * Distribute markers preserving the relationship throughout the available space
392
+ * @param startOffset - the starting point of the available space
393
+ * @param endOffset - the end point of the available space
394
+ * @param markers - the markers to distribute
395
+ * @param additionalSteps - additional steps to apply to the markers
396
+ *
397
+ * @returns array of markers positions
398
+ */
399
+ function distributeMarkers({
400
+ endOffset,
401
+ markers,
402
+ startOffset,
403
+ additionalSteps = [],
404
+ }: {
405
+ startOffset: number;
406
+ endOffset: number;
407
+ markers: number[];
408
+ additionalSteps?: ((marker: number) => number)[];
409
+ }) {
410
+ return markers.map((marker) => {
411
+ const normalizedOffset = getNormalizedNumber({
412
+ min: markers[0],
413
+ max: markers[markers.length - 1],
414
+ value: marker,
415
+ });
416
+
417
+ const availableSpace = endOffset - startOffset;
418
+
419
+ let result = startOffset + normalizedOffset * availableSpace;
420
+
421
+ for (const additionalStep of additionalSteps) {
422
+ result = additionalStep(result);
423
+ }
424
+
425
+ return result;
426
+ });
427
+ }
428
+
429
+ /**
430
+ * Normalize a number between a min and max value
431
+ * @param min - the minimum value of the distribution
432
+ * @param max - the maximum value of the distribution
433
+ * @param value - the value to normalize
434
+ *
435
+ * @returns normalized number between 0 and 1
436
+ */
437
+ function getNormalizedNumber(options: { min: number; max: number; value: number }) {
438
+ const { min, max, value } = options;
439
+
440
+ return (value - min) / (max - min);
180
441
  }
@@ -3,12 +3,12 @@ import type {
3
3
  CodeWalkthroughStepAttr,
4
4
  CodeWalkthroughAttr,
5
5
  } from '@redocly/config';
6
+ import type { WalkthroughStepsState } from '@redocly/theme/core/types';
6
7
 
7
8
  import {
8
9
  useCodeWalkthroughSteps,
9
10
  useCodeWalkthroughControls,
10
11
  type WalkthroughControlsState,
11
- type WalkthroughStepsState,
12
12
  } from '@redocly/theme/core/hooks';
13
13
 
14
14
  export type WalkthroughState = {
@@ -18,10 +18,13 @@ export type WalkthroughState = {
18
18
  files: CodeWalkthroughFile[];
19
19
  };
20
20
 
21
- export function useCodeWalkthrough(
22
- steps: CodeWalkthroughStepAttr[],
23
- attributes: Omit<CodeWalkthroughAttr, 'steps' | 'preview'>,
24
- ): WalkthroughState {
21
+ type Params = {
22
+ steps: CodeWalkthroughStepAttr[];
23
+ attributes: Omit<CodeWalkthroughAttr, 'steps' | 'preview'>;
24
+ root: React.RefObject<HTMLDivElement | null>;
25
+ };
26
+
27
+ export function useCodeWalkthrough({ steps, attributes, root }: Params): WalkthroughState {
25
28
  const { filters, filesets, inputs, toggles, __idx } = attributes;
26
29
  /*
27
30
  We only enable deep linking for the first CodeWalkthrough,
@@ -31,7 +34,7 @@ export function useCodeWalkthrough(
31
34
  */
32
35
  const enableDeepLink = __idx === 1;
33
36
 
34
- const stepsState = useCodeWalkthroughSteps(steps, enableDeepLink);
37
+ const stepsState = useCodeWalkthroughSteps({ steps, enableDeepLink, root });
35
38
  const controlsState = useCodeWalkthroughControls(filters, inputs, toggles, enableDeepLink);
36
39
 
37
40
  const files: CodeWalkthroughFile[] = filesets
@@ -36,5 +36,5 @@ export * from '@redocly/theme/core/hooks/code-walkthrough/use-renderable-files';
36
36
  export * from '@redocly/theme/core/hooks/use-element-size';
37
37
  export * from '@redocly/theme/core/hooks/use-time-ago';
38
38
  export * from '@redocly/theme/core/hooks/use-input-key-commands';
39
- export * from '@redocly/theme/core/hooks/use-page-active-version';
39
+ export * from '@redocly/theme/core/hooks/use-active-page-version';
40
40
  export * from '@redocly/theme/core/hooks/use-page-versions';
@@ -1,6 +1,6 @@
1
1
  import { useThemeHooks } from '@redocly/theme/core/hooks';
2
2
 
3
- export function usePageActiveVersion(): string | undefined {
3
+ export function useActivePageVersion(): string | undefined {
4
4
  const { usePageVersions } = useThemeHooks();
5
5
  const { versions } = usePageVersions();
6
6
  const activeVersion = versions.find((version) => version.active);
@@ -0,0 +1,15 @@
1
+ import type { MarkerArea } from '@redocly/theme/core/types';
2
+
3
+ export type ActiveStep = string | null;
4
+
5
+ export type WalkthroughStepsState = {
6
+ activeStep: ActiveStep;
7
+ setActiveStep: (stepId: ActiveStep) => void;
8
+ markers: Record<string, MarkerArea>;
9
+ registerMarker: (stepId: string, element: HTMLElement) => void;
10
+ removeMarker: (stepId: string, element: HTMLElement) => void;
11
+ registerStep: (stepId: string, element: HTMLElement) => void;
12
+ removeStep: (stepId: string) => void;
13
+ lockObserver?: React.RefObject<boolean>;
14
+ filtersElementRef?: React.RefObject<HTMLDivElement | null>;
15
+ };
@@ -13,3 +13,5 @@ export * from '@redocly/theme/core/types/user-menu';
13
13
  export * from '@redocly/theme/core/types/user-claims';
14
14
  export * from '@redocly/theme/core/types/common';
15
15
  export * from '@redocly/theme/core/types/open-api-server';
16
+ export * from '@redocly/theme/core/types/marker';
17
+ export * from '@redocly/theme/core/types/code-walkthrough';
@@ -0,0 +1,4 @@
1
+ export type MarkerArea = {
2
+ offset: number;
3
+ height: number;
4
+ };
@@ -71,3 +71,34 @@ export function getAdjacentValues<T>(
71
71
 
72
72
  return { prev: prevValue, next: nextValue };
73
73
  }
74
+
75
+ /**
76
+ * Inserts an element at a given index in an array. Returns a new array with the element inserted.
77
+ *
78
+ * @example
79
+ * const array = [10, 20, 30, 40];
80
+ * insertAt(array, 2, 25);
81
+ * // returns: [10, 20, 25, 30, 40]
82
+ */
83
+ export function insertAt<T>(array: T[], index: number, newElement: T): T[] {
84
+ const result = array.concat();
85
+ result.splice(index, 0, newElement);
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Removes an element from an array. Returns a new array with the element removed.
91
+ *
92
+ * @example
93
+ * const array = [10, 20, 30, 40];
94
+ * removeElement(array, 20);
95
+ * // returns: [10, 30, 40]
96
+ */
97
+ export function removeElement<T>(array: T[], item: T): T[] {
98
+ const index = array.indexOf(item);
99
+ if (index === -1) return array;
100
+
101
+ const result = array.slice();
102
+ result.splice(index, 1);
103
+ return result;
104
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export * from '@redocly/theme/components/Tooltip/Tooltip';
23
23
  export * from '@redocly/theme/components/Tags/HttpTag';
24
24
  export * from '@redocly/theme/components/Tags/CounterTag';
25
25
  export * from '@redocly/theme/components/VersionPicker/VersionPicker';
26
+ export * from '@redocly/theme/components/Marker/Marker';
26
27
  /* Buttons */
27
28
  export * from '@redocly/theme/components/Buttons/CopyButton';
28
29
  export * from '@redocly/theme/components/Buttons/EditPageButton';