@redocly/theme 0.63.0-next.2 → 0.63.0-next.4

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 (36) hide show
  1. package/lib/components/Buttons/CopyButton.d.ts +1 -1
  2. package/lib/components/Buttons/CopyButton.js +1 -1
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.js +2 -2
  4. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityVersionItem.js +6 -13
  5. package/lib/components/CodeBlock/CodeBlockControls.js +3 -3
  6. package/lib/components/PageActions/PageActions.js +1 -1
  7. package/lib/components/SidebarActions/styled.d.ts +1 -1
  8. package/lib/components/Tooltip/AnchorTooltip.d.ts +7 -0
  9. package/lib/components/Tooltip/AnchorTooltip.js +230 -0
  10. package/lib/components/Tooltip/JsTooltip.d.ts +3 -0
  11. package/lib/components/Tooltip/JsTooltip.js +274 -0
  12. package/lib/components/Tooltip/Tooltip.d.ts +2 -13
  13. package/lib/components/Tooltip/Tooltip.js +14 -190
  14. package/lib/core/hooks/use-page-actions.js +16 -9
  15. package/lib/core/hooks/use-theme-hooks.js +1 -0
  16. package/lib/core/types/hooks.d.ts +3 -0
  17. package/lib/core/types/index.d.ts +1 -0
  18. package/lib/core/types/tooltip.d.ts +13 -0
  19. package/lib/core/types/tooltip.js +3 -0
  20. package/lib/core/utils/transform-revisions-to-version-history.js +13 -20
  21. package/package.json +3 -3
  22. package/src/components/Buttons/CopyButton.tsx +2 -1
  23. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.tsx +2 -2
  24. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityVersionItem.tsx +5 -21
  25. package/src/components/CodeBlock/CodeBlockControls.tsx +3 -0
  26. package/src/components/PageActions/PageActions.tsx +6 -1
  27. package/src/components/Tooltip/AnchorTooltip.tsx +255 -0
  28. package/src/components/Tooltip/JsTooltip.tsx +292 -0
  29. package/src/components/Tooltip/Tooltip.tsx +18 -257
  30. package/src/core/hooks/__mocks__/use-theme-hooks.ts +3 -0
  31. package/src/core/hooks/use-page-actions.ts +17 -6
  32. package/src/core/hooks/use-theme-hooks.ts +1 -0
  33. package/src/core/types/hooks.ts +1 -0
  34. package/src/core/types/index.ts +1 -0
  35. package/src/core/types/tooltip.ts +14 -0
  36. package/src/core/utils/transform-revisions-to-version-history.ts +13 -21
@@ -1,264 +1,25 @@
1
- import React, { memo, useEffect, useRef, useId } from 'react';
2
- import styled, { css } from 'styled-components';
1
+ import React, { memo } from 'react';
3
2
 
4
- import type { JSX, PropsWithChildren, ReactNode } from 'react';
3
+ import { Tooltip as AnchorTooltip } from '@redocly/theme/components/Tooltip/AnchorTooltip';
4
+ import { Tooltip as JsTooltip } from '@redocly/theme/components/Tooltip/JsTooltip';
5
+ import { TooltipProps } from '@redocly/theme/core/types';
6
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
5
7
 
6
- import { useControl, useOutsideClick } from '@redocly/theme/core/hooks';
7
- import { Portal } from '@redocly/theme/components/Portal/Portal';
8
+ function TooltipComponent(props: TooltipProps): React.ReactElement {
9
+ const { useAnchorPositioning } = useThemeHooks();
10
+ const { isSupported } = useAnchorPositioning();
8
11
 
9
- export type TooltipProps = {
10
- tip: string | ReactNode;
11
- isOpen?: boolean;
12
- withArrow?: boolean;
13
- placement?: 'top' | 'bottom' | 'left' | 'right';
14
- className?: string;
15
- width?: string;
16
- dataTestId?: string;
17
- disabled?: boolean;
18
- arrowPosition?: 'left' | 'right' | 'center';
19
- };
20
-
21
- function TooltipComponent({
22
- children,
23
- isOpen,
24
- tip,
25
- withArrow = true,
26
- placement = 'top',
27
- className = 'default',
28
- width,
29
- dataTestId,
30
- disabled = false,
31
- arrowPosition = 'center',
32
- }: PropsWithChildren<TooltipProps>): JSX.Element {
33
- const tooltipWrapperRef = useRef<HTMLDivElement | null>(null);
34
- const tooltipBodyRef = useRef<HTMLDivElement | null>(null);
35
- const { isOpened, handleOpen, handleClose } = useControl(isOpen);
36
- const anchorName = `--tooltip${useId().replace(/:/g, '')}`;
37
-
38
- useOutsideClick(isOpened ? [tooltipWrapperRef, tooltipBodyRef] : tooltipWrapperRef, handleClose);
39
-
40
- const isControlled = isOpen !== undefined;
41
-
42
- useEffect(() => {
43
- if (!isControlled) return;
44
-
45
- if (isOpen && !disabled) {
46
- handleOpen();
47
- } else {
48
- handleClose();
49
- }
50
- }, [isOpen, disabled, isControlled, handleOpen, handleClose]);
51
-
52
- const controllers =
53
- !isControlled && !disabled
54
- ? {
55
- onMouseEnter: handleOpen,
56
- onMouseLeave: handleClose,
57
- onClick: handleClose,
58
- onFocus: handleOpen,
59
- onBlur: handleClose,
60
- }
61
- : {};
62
-
63
- return (
64
- <TooltipWrapper
65
- ref={tooltipWrapperRef}
66
- {...controllers}
67
- className={`tooltip-${className}`}
68
- data-component-name="Tooltip/Tooltip"
69
- anchorName={anchorName}
70
- >
71
- {children}
72
- {isOpened && !disabled && (
73
- <Portal>
74
- <TooltipBody
75
- ref={tooltipBodyRef}
76
- data-testid={dataTestId || (typeof tip === 'string' ? tip : '')}
77
- placement={placement}
78
- width={width}
79
- withArrow={withArrow}
80
- arrowPosition={arrowPosition}
81
- anchorName={anchorName}
82
- >
83
- {tip}
84
- </TooltipBody>
85
- </Portal>
86
- )}
87
- </TooltipWrapper>
88
- );
89
- }
90
-
91
- export const Tooltip = memo<PropsWithChildren<TooltipProps>>(TooltipComponent);
92
-
93
- const PLACEMENTS = {
94
- top: css<Pick<TooltipProps, 'withArrow' | 'arrowPosition'>>`
95
- bottom: anchor(top);
96
- ${({ withArrow, arrowPosition }) =>
97
- withArrow && arrowPosition === 'left'
98
- ? css`
99
- transform: translate(-32px, -6px);
100
- left: anchor(center);
101
- `
102
- : arrowPosition === 'right'
103
- ? css`
104
- transform: translate(32px, -6px);
105
- right: anchor(center);
106
- `
107
- : css`
108
- transform: translate(-50%, -6px);
109
- left: anchor(center);
110
- `}
111
-
112
- ${({ withArrow, arrowPosition }) =>
113
- withArrow &&
114
- css`
115
- &::after {
116
- border-left: 14px solid transparent;
117
- border-right: 14px solid transparent;
118
- border-top-width: 8px;
119
- border-top-style: solid;
120
- border-radius: 2px;
121
- bottom: 0;
122
- ${arrowPosition === 'left' && 'left: 16px; transform: translateY(100%);'}
123
- ${arrowPosition === 'center' && 'left: 50%; transform: translate(-50%, 100%);'}
124
- ${arrowPosition === 'right' && 'right: 16px; transform: translateY(100%);'}
125
- }
126
- `}
127
- `,
128
- bottom: css<Pick<TooltipProps, 'withArrow' | 'arrowPosition'>>`
129
- top: anchor(bottom);
130
- ${({ withArrow, arrowPosition }) =>
131
- withArrow && arrowPosition === 'left'
132
- ? css`
133
- transform: translate(-32px, 6px);
134
- left: anchor(center);
135
- `
136
- : arrowPosition === 'right'
137
- ? css`
138
- transform: translate(32px, 6px);
139
- right: anchor(center);
140
- `
141
- : css`
142
- transform: translate(-50%, 6px);
143
- left: anchor(center);
144
- `}
145
-
146
- ${({ withArrow, arrowPosition }) =>
147
- withArrow &&
148
- css`
149
- &::after {
150
- border-left: 14px solid transparent;
151
- border-right: 14px solid transparent;
152
- border-bottom-width: 8px;
153
- border-bottom-style: solid;
154
- border-radius: 0 0 2px 2px;
155
- top: 0;
156
- ${arrowPosition === 'left' && 'left: 16px; transform: translateY(-100%);'}
157
- ${arrowPosition === 'center' && 'left: 50%; transform: translate(-50%, -100%);'}
158
- ${arrowPosition === 'right' && 'right: 16px; transform: translateY(-100%);'}
159
- }
160
- `}
161
- `,
162
- left: css<Pick<TooltipProps, 'withArrow' | 'arrowPosition'>>`
163
- transform: translate(-100%, -50%);
164
- margin-left: -7px;
165
- top: anchor(center);
166
- left: anchor(left);
167
-
168
- ${({ withArrow }) =>
169
- withArrow &&
170
- css`
171
- &::after {
172
- border-top: 14px solid transparent;
173
- border-bottom: 14px solid transparent;
174
- border-left-width: 8px;
175
- border-left-style: solid;
176
- border-radius: 2px 0 0 2px;
177
- right: -9px;
178
- top: 50%;
179
- transform: translateY(-50%);
180
- }
181
- `}
182
- `,
183
- right: css<Pick<TooltipProps, 'withArrow' | 'arrowPosition'>>`
184
- transform: translate(0, -50%);
185
- margin-left: 7px;
186
- top: anchor(center);
187
- left: anchor(right);
188
-
189
- ${({ withArrow }) =>
190
- withArrow &&
191
- css`
192
- &::after {
193
- border-top: 14px solid transparent;
194
- border-bottom: 14px solid transparent;
195
- border-right-width: 8px;
196
- border-right-style: solid;
197
- border-radius: 0 2px 2px 0;
198
- left: -9px;
199
- top: 50%;
200
- transform: translateY(-50%);
201
- }
202
- `}
203
- `,
204
- };
205
-
206
- const TooltipWrapper = styled.div.attrs<{ anchorName: string }>(({ anchorName }) => ({
207
- style: {
208
- anchorName: anchorName,
209
- } as React.CSSProperties,
210
- }))<{ anchorName: string }>`
211
- display: flex;
212
- `;
213
-
214
- const TooltipBody = styled.span.attrs<{ anchorName: string }>(({ anchorName }) => ({
215
- style: {
216
- positionAnchor: anchorName,
217
- } as React.CSSProperties,
218
- }))<
219
- Pick<Required<TooltipProps>, 'placement' | 'withArrow' | 'arrowPosition'> & {
220
- width?: string;
221
- anchorName: string;
12
+ if (isSupported) {
13
+ return <AnchorTooltip {...props} arrowPosition={prepareArrowPosition(props.arrowPosition)} />;
222
14
  }
223
- >`
224
- position: fixed;
225
- min-width: 64px;
226
- padding: var(--tooltip-padding);
227
- max-width: var(--tooltip-max-width);
228
- white-space: normal;
229
- word-break: normal;
230
- overflow-wrap: break-word;
231
- text-align: left;
232
-
233
- border-radius: var(--border-radius-md);
234
- transition: opacity 0.3s ease-out;
235
-
236
- font-size: var(--font-size-base);
237
- line-height: var(--line-height-base);
238
-
239
- z-index: var(--z-index-overlay);
15
+ return <JsTooltip {...props} />;
16
+ }
240
17
 
241
- &::after {
242
- position: absolute;
18
+ export const Tooltip = memo<TooltipProps>(TooltipComponent);
243
19
 
244
- content: ' ';
245
- display: inline-block;
246
- width: 0;
247
- height: 0;
248
- border-color: var(--tooltip-arrow-color, var(--tooltip-bg-color));
20
+ const prepareArrowPosition = (arrowPosition: TooltipProps['arrowPosition']) => {
21
+ if (arrowPosition === 'bottom' || arrowPosition === 'top') {
22
+ return 'center';
249
23
  }
250
-
251
- background: var(--tooltip-bg-color);
252
- color: var(--tooltip-text-color);
253
- border: var(--tooltip-border-width, 0) var(--tooltip-border-style, solid)
254
- var(--tooltip-border-color, transparent);
255
- box-shadow:
256
- 0px 8px 24px 8px #0000000a,
257
- 0px 4px 12px 0px #00000014;
258
-
259
- width: ${({ width }) => width || 'max-content'};
260
-
261
- ${({ placement }) => css`
262
- ${PLACEMENTS[placement]};
263
- `}
264
- `;
24
+ return arrowPosition;
25
+ };
@@ -3,6 +3,9 @@ import { vi, type MockedFunction } from 'vitest';
3
3
  import type { ThemeHooks } from '../../types/hooks';
4
4
 
5
5
  export const useThemeHooks = vi.fn(() => ({
6
+ useAnchorPositioning: vi.fn(() => ({
7
+ isSupported: true,
8
+ })),
6
9
  useTranslate: vi.fn(() => ({
7
10
  translate: vi.fn((key: string, defaultValue: string | { defaultValue: string }) =>
8
11
  defaultValue
@@ -16,6 +16,12 @@ import { useMCPConfig } from './use-mcp-config';
16
16
  import { ClipboardService } from '../utils/clipboard-service';
17
17
  import { IS_BROWSER } from '../utils/dom';
18
18
  import { generateMCPDeepLink } from '../utils/mcp';
19
+ import {
20
+ addTrailingSlash,
21
+ combineUrls,
22
+ removeTrailingSlash,
23
+ withoutPathPrefix,
24
+ } from '../utils/urls';
19
25
 
20
26
  function createPageActionResource(pageSlug: string, pageUrl: string) {
21
27
  return {
@@ -108,11 +114,12 @@ export function usePageActions(
108
114
  const origin = IS_BROWSER
109
115
  ? window.location.origin
110
116
  : ((globalThis as { SSR_HOSTNAME?: string })['SSR_HOSTNAME'] ?? '');
111
- const normalizedSlug = pageSlug.startsWith('/') ? pageSlug : '/' + pageSlug;
112
- const pageUrl = `${origin}${normalizedSlug}`;
113
- const mdPageUrl = new URL(
114
- origin + normalizedSlug + (normalizedSlug === '/' ? 'index.html.md' : '.md'),
115
- ).toString();
117
+ const pathname = addTrailingSlash(pageSlug);
118
+ const pageUrl = combineUrls(origin, pathname);
119
+ const isRoot = withoutPathPrefix(pathname) === '/';
120
+ const mdPageUrl = isRoot
121
+ ? combineUrls(origin, pathname, 'index.html.md')
122
+ : combineUrls(origin, removeTrailingSlash(pathname) + '.md');
116
123
 
117
124
  const actionHandlers: Record<PageActionType, () => PageAction | null> = {
118
125
  'docs-mcp-cursor': createMCPHandler('cursor', false),
@@ -158,7 +165,6 @@ export function usePageActions(
158
165
  action_type: 'view',
159
166
  },
160
167
  ]);
161
- window.location.href = mdPageUrl;
162
168
  },
163
169
  }),
164
170
 
@@ -284,6 +290,11 @@ function shouldHidePageActions(
284
290
  themeConfig: UiAccessibleConfig,
285
291
  openapiExcludeFromSearch?: boolean,
286
292
  ): boolean {
293
+ // Can't use any actions if search is globally disabled (markdown files are not generated)
294
+ if (themeConfig.search?.hide) {
295
+ return true;
296
+ }
297
+
287
298
  // Can't use any actions if no markdown files are generated for LLMs
288
299
  if (pageProps?.seo?.llmstxt?.hide) {
289
300
  return true;
@@ -6,6 +6,7 @@ import { ThemeDataContext } from '../contexts/ThemeDataContext';
6
6
  import { useTelemetryFallback } from './use-telemetry-fallback';
7
7
 
8
8
  const fallbacks = {
9
+ useAnchorPositioning: () => ({ isSupported: false }),
9
10
  useTranslate: () => ({
10
11
  translate: (value?: string, options?: { defaultValue: string } | string) =>
11
12
  (typeof options === 'string' ? options : options?.defaultValue) || value || '',
@@ -47,6 +47,7 @@ import {
47
47
  import { AiSearchError } from '../constants/search';
48
48
 
49
49
  export type ThemeHooks = {
50
+ useAnchorPositioning: () => { isSupported: boolean };
50
51
  useTranslate: () => { translate: TFunction };
51
52
  /**
52
53
  * @deprecated use `useL10n` instead
@@ -19,3 +19,4 @@ export type * from './code-walkthrough';
19
19
  export type * from './page-actions';
20
20
  export type * from './open-api-info';
21
21
  export type * from './segmented';
22
+ export type * from './tooltip';
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type TooltipProps = {
4
+ children?: ReactNode;
5
+ tip: string | ReactNode;
6
+ isOpen?: boolean;
7
+ withArrow?: boolean;
8
+ placement?: 'top' | 'bottom' | 'left' | 'right';
9
+ className?: string;
10
+ width?: string;
11
+ dataTestId?: string;
12
+ disabled?: boolean;
13
+ arrowPosition?: 'top' | 'bottom' | 'left' | 'right' | 'center';
14
+ };
@@ -53,27 +53,19 @@ export function transformRevisionsToVersionHistory({
53
53
  // Check if any revision in this version group is the default version
54
54
  const isDefaultVersion = versionRevisions.some((rev) => rev.isDefaultVersion === true);
55
55
 
56
- const revisions =
57
- versionRevisions.length > 1
58
- ? versionRevisions.map((rev, index): CatalogEntityRevision => {
59
- const revisionMatches = currentRevisionDate
60
- ? rev.revision === currentRevisionDate
61
- : false;
62
- const isActiveRevision = revisionMatches && versionMatches;
63
- const isCurrentByDefault =
64
- !currentRevisionDate &&
65
- normalizedCurrentVersion === undefined &&
66
- index === 0 &&
67
- isCurrent;
68
- return {
69
- name: `r.${versionRevisions.length - index}`,
70
- date: toLocalizedShortDateTime(rev.revision, locale),
71
- revisionDate: rev.revision,
72
- isActive: isActiveRevision || isCurrentByDefault,
73
- isCurrent: rev.isCurrent ?? false,
74
- };
75
- })
76
- : undefined;
56
+ const revisions = versionRevisions.map((rev, index): CatalogEntityRevision => {
57
+ const revisionMatches = currentRevisionDate ? rev.revision === currentRevisionDate : false;
58
+ const isActiveRevision = revisionMatches && versionMatches;
59
+ const isCurrentByDefault =
60
+ !currentRevisionDate && normalizedCurrentVersion === undefined && index === 0 && isCurrent;
61
+ return {
62
+ name: `r.${versionRevisions.length - index}`,
63
+ date: toLocalizedShortDateTime(rev.revision, locale),
64
+ revisionDate: rev.revision,
65
+ isActive: isActiveRevision || isCurrentByDefault,
66
+ isCurrent: rev.isCurrent ?? false,
67
+ };
68
+ });
77
69
 
78
70
  return {
79
71
  version,