@redocly/theme 0.64.0 → 0.65.0-next.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.
@@ -38,11 +38,18 @@ const react_1 = __importStar(require("react"));
38
38
  const styled_components_1 = __importStar(require("styled-components"));
39
39
  const hooks_1 = require("../../core/hooks");
40
40
  const Portal_1 = require("../../components/Portal/Portal");
41
- function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }) {
41
+ function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', fallbackPlacements, className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }) {
42
42
  const tooltipWrapperRef = (0, react_1.useRef)(null);
43
43
  const tooltipBodyRef = (0, react_1.useRef)(null);
44
44
  const { isOpened, handleOpen, handleClose } = (0, hooks_1.useControl)(isOpen);
45
45
  const anchorName = `--tooltip${(0, react_1.useId)().replace(/:/g, '')}`;
46
+ const { activePlacement, activeArrowPosition } = (0, hooks_1.useTooltipFallbackPlacement)({
47
+ isOpened,
48
+ placement,
49
+ arrowPosition,
50
+ fallbackPlacements,
51
+ tooltipBodyRef,
52
+ });
46
53
  (0, hooks_1.useOutsideClick)(isOpened ? [tooltipWrapperRef, tooltipBodyRef] : tooltipWrapperRef, handleClose);
47
54
  const isControlled = isOpen !== undefined;
48
55
  (0, react_1.useEffect)(() => {
@@ -70,7 +77,9 @@ function TooltipComponent({ children, isOpen, tip, withArrow = true, placement =
70
77
  return (react_1.default.createElement(TooltipWrapper, Object.assign({ ref: tooltipWrapperRef }, controllers, { className: `tooltip-${className}`, "data-component-name": "Tooltip/Tooltip", anchorName: anchorName }),
71
78
  children,
72
79
  isOpened && !disabled && (react_1.default.createElement(Portal_1.Portal, null,
73
- react_1.default.createElement(TooltipBody, { ref: tooltipBodyRef, "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement: placement, width: width, withArrow: withArrow, arrowPosition: arrowPosition, anchorName: anchorName }, tip)))));
80
+ react_1.default.createElement(TooltipBody, { ref: tooltipBodyRef, "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement: activePlacement, width: width, withArrow: withArrow, arrowPosition: activeArrowPosition === 'left' || activeArrowPosition === 'right'
81
+ ? activeArrowPosition
82
+ : 'center', anchorName: anchorName }, tip)))));
74
83
  }
75
84
  exports.Tooltip = (0, react_1.memo)(TooltipComponent);
76
85
  const PLACEMENTS = {
@@ -38,56 +38,37 @@ const react_1 = __importStar(require("react"));
38
38
  const styled_components_1 = __importStar(require("styled-components"));
39
39
  const hooks_1 = require("../../core/hooks");
40
40
  const Portal_1 = require("../../components/Portal/Portal");
41
- function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }) {
42
- const ref = (0, react_1.useRef)(null);
41
+ const utils_1 = require("../../core/utils");
42
+ function TooltipComponent({ children, isOpen, tip, withArrow = true, placement = 'top', fallbackPlacements, className = 'default', width, dataTestId, disabled = false, arrowPosition = 'center', onClick, }) {
43
+ const wrapperRef = (0, react_1.useRef)(null);
44
+ const tooltipBodyRef = (0, react_1.useRef)(null);
43
45
  const { isOpened, handleOpen, handleClose } = (0, hooks_1.useControl)(isOpen);
44
46
  const [tooltipPosition, setTooltipPosition] = (0, react_1.useState)({ top: 0, left: 0 });
45
- (0, hooks_1.useOutsideClick)(ref, handleClose);
47
+ const [activePlacement, setActivePlacement] = (0, react_1.useState)(placement);
48
+ const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
49
+ (0, hooks_1.useOutsideClick)(wrapperRef, handleClose);
46
50
  const isControlled = isOpen !== undefined;
47
51
  const updateTooltipPosition = (0, react_1.useCallback)(() => {
48
- if (isOpened && ref.current) {
49
- const rect = ref.current.getBoundingClientRect();
50
- let top = 0;
51
- let left = 0;
52
- switch (placement) {
53
- case 'top':
54
- top = rect.top;
55
- if (arrowPosition === 'left') {
56
- left = rect.left - 24;
57
- }
58
- else if (arrowPosition === 'right') {
59
- left = rect.right + 24;
60
- }
61
- else {
62
- left = rect.left + rect.width / 2;
63
- }
64
- break;
65
- case 'bottom':
66
- top = rect.bottom;
67
- if (arrowPosition === 'left') {
68
- left = rect.left - 24;
69
- }
70
- else if (arrowPosition === 'right') {
71
- left = rect.right + 24;
72
- }
73
- else {
74
- left = rect.left + rect.width / 2;
75
- }
76
- break;
77
- case 'left':
78
- top = rect.top + rect.height / 2;
79
- left = rect.left;
80
- break;
81
- case 'right':
82
- top = rect.top + rect.height / 2;
83
- left = rect.right;
84
- break;
85
- }
86
- setTooltipPosition({ top, left });
87
- }
88
- }, [isOpened, placement, arrowPosition]);
52
+ var _a, _b, _c, _d;
53
+ if (!isOpened || !wrapperRef.current)
54
+ return;
55
+ const triggerRect = wrapperRef.current.getBoundingClientRect();
56
+ const tooltipWidth = (_b = (_a = tooltipBodyRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : 0;
57
+ const tooltipHeight = (_d = (_c = tooltipBodyRef.current) === null || _c === void 0 ? void 0 : _c.offsetHeight) !== null && _d !== void 0 ? _d : 0;
58
+ const resolved = (0, utils_1.resolvePlacement)({
59
+ triggerRect,
60
+ tooltipWidth,
61
+ tooltipHeight,
62
+ placement,
63
+ arrowPosition,
64
+ fallbackPlacements,
65
+ });
66
+ const resolvedArrow = resolved === placement ? arrowPosition : 'center';
67
+ setTooltipPosition((0, utils_1.calcAnchorPoint)(triggerRect, resolved, resolvedArrow));
68
+ setActivePlacement(resolved);
69
+ }, [isOpened, placement, arrowPosition, fallbackPlacements]);
89
70
  (0, react_1.useLayoutEffect)(() => {
90
- if (isOpened && ref.current) {
71
+ if (isOpened && wrapperRef.current) {
91
72
  updateTooltipPosition();
92
73
  const handleScroll = () => updateTooltipPosition();
93
74
  const handleResize = () => updateTooltipPosition();
@@ -118,10 +99,10 @@ function TooltipComponent({ children, isOpen, tip, withArrow = true, placement =
118
99
  onFocus: handleOpen,
119
100
  onBlur: handleClose,
120
101
  };
121
- return (react_1.default.createElement(TooltipWrapper, Object.assign({ ref: ref }, controllers, { className: `tooltip-${className}`, "data-component-name": "Tooltip/Tooltip" }),
102
+ return (react_1.default.createElement(TooltipWrapper, Object.assign({ ref: wrapperRef }, controllers, { className: `tooltip-${className}`, "data-component-name": "Tooltip/Tooltip" }),
122
103
  children,
123
104
  isOpened && !disabled && (react_1.default.createElement(Portal_1.Portal, null,
124
- react_1.default.createElement(TooltipBody, { "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement: placement, width: width, withArrow: withArrow, arrowPosition: arrowPosition, style: {
105
+ react_1.default.createElement(TooltipBody, { ref: tooltipBodyRef, "data-testid": dataTestId || (typeof tip === 'string' ? tip : ''), placement: activePlacement, width: width, withArrow: withArrow, arrowPosition: activeArrowPosition, style: {
125
106
  position: 'fixed',
126
107
  top: tooltipPosition.top,
127
108
  left: tooltipPosition.left,
@@ -1,3 +1,3 @@
1
1
  import React from 'react';
2
- import { TooltipProps } from '../../core/types';
2
+ import type { TooltipProps } from '../../core/types';
3
3
  export declare const Tooltip: React.NamedExoticComponent<TooltipProps>;
@@ -38,13 +38,16 @@ const react_1 = __importStar(require("react"));
38
38
  const AnchorTooltip_1 = require("../../components/Tooltip/AnchorTooltip");
39
39
  const JsTooltip_1 = require("../../components/Tooltip/JsTooltip");
40
40
  const hooks_1 = require("../../core/hooks");
41
+ const utils_1 = require("../../core/utils");
41
42
  function TooltipComponent(props) {
43
+ var _a, _b;
42
44
  const { useAnchorPositioning } = (0, hooks_1.useThemeHooks)();
43
45
  const { isSupported } = useAnchorPositioning();
46
+ const fallbackPlacements = (_a = props.fallbackPlacements) !== null && _a !== void 0 ? _a : (0, utils_1.getDefaultFallbackPlacements)((_b = props.placement) !== null && _b !== void 0 ? _b : 'top');
44
47
  if (isSupported) {
45
- return react_1.default.createElement(AnchorTooltip_1.Tooltip, Object.assign({}, props, { arrowPosition: prepareArrowPosition(props.arrowPosition) }));
48
+ return (react_1.default.createElement(AnchorTooltip_1.Tooltip, Object.assign({}, props, { fallbackPlacements: fallbackPlacements, arrowPosition: prepareArrowPosition(props.arrowPosition) })));
46
49
  }
47
- return react_1.default.createElement(JsTooltip_1.Tooltip, Object.assign({}, props));
50
+ return react_1.default.createElement(JsTooltip_1.Tooltip, Object.assign({}, props, { fallbackPlacements: fallbackPlacements }));
48
51
  }
49
52
  exports.Tooltip = (0, react_1.memo)(TooltipComponent);
50
53
  const prepareArrowPosition = (arrowPosition) => {
@@ -54,3 +54,4 @@ export * from './use-is-truncated';
54
54
  export * from './use-toast';
55
55
  export * from './use-toast-logic';
56
56
  export * from './use-banner-telemetry';
57
+ export * from './use-tooltip-fallback-placement';
@@ -70,4 +70,5 @@ __exportStar(require("./use-is-truncated"), exports);
70
70
  __exportStar(require("./use-toast"), exports);
71
71
  __exportStar(require("./use-toast-logic"), exports);
72
72
  __exportStar(require("./use-banner-telemetry"), exports);
73
+ __exportStar(require("./use-tooltip-fallback-placement"), exports);
73
74
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ import type { RefObject } from 'react';
2
+ import type { TooltipPlacement, TooltipProps } from '../../core/types';
3
+ type TooltipFallbackPlacementParams = {
4
+ isOpened: boolean;
5
+ placement: TooltipPlacement;
6
+ arrowPosition: TooltipProps['arrowPosition'];
7
+ fallbackPlacements: TooltipPlacement[] | undefined;
8
+ tooltipBodyRef: RefObject<HTMLElement | null>;
9
+ };
10
+ type TooltipFallbackPlacementResult = {
11
+ activePlacement: TooltipPlacement;
12
+ activeArrowPosition: TooltipProps['arrowPosition'];
13
+ };
14
+ export declare function useTooltipFallbackPlacement({ isOpened, placement, arrowPosition, fallbackPlacements, tooltipBodyRef, }: TooltipFallbackPlacementParams): TooltipFallbackPlacementResult;
15
+ export {};
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useTooltipFallbackPlacement = useTooltipFallbackPlacement;
4
+ const react_1 = require("react");
5
+ function useTooltipFallbackPlacement({ isOpened, placement, arrowPosition, fallbackPlacements, tooltipBodyRef, }) {
6
+ const [activePlacement, setActivePlacement] = (0, react_1.useState)(placement);
7
+ const wasOpenRef = (0, react_1.useRef)(false);
8
+ const candidateIndexRef = (0, react_1.useRef)(0);
9
+ (0, react_1.useLayoutEffect)(() => {
10
+ if (!isOpened) {
11
+ wasOpenRef.current = false;
12
+ candidateIndexRef.current = 0;
13
+ return;
14
+ }
15
+ if (!wasOpenRef.current) {
16
+ wasOpenRef.current = true;
17
+ candidateIndexRef.current = 0;
18
+ if (activePlacement !== placement) {
19
+ setActivePlacement(placement);
20
+ return;
21
+ }
22
+ }
23
+ if (!tooltipBodyRef.current || !(fallbackPlacements === null || fallbackPlacements === void 0 ? void 0 : fallbackPlacements.length))
24
+ return;
25
+ const candidates = [placement, ...fallbackPlacements];
26
+ if (candidateIndexRef.current >= candidates.length)
27
+ return;
28
+ const rect = tooltipBodyRef.current.getBoundingClientRect();
29
+ const overflows = rect.left < 0 ||
30
+ rect.top < 0 ||
31
+ rect.right > window.innerWidth ||
32
+ rect.bottom > window.innerHeight;
33
+ if (!overflows)
34
+ return;
35
+ candidateIndexRef.current++;
36
+ if (candidateIndexRef.current < candidates.length) {
37
+ setActivePlacement(candidates[candidateIndexRef.current]);
38
+ }
39
+ else if (activePlacement !== placement) {
40
+ setActivePlacement(placement);
41
+ }
42
+ }, [isOpened, activePlacement, placement, fallbackPlacements, tooltipBodyRef]);
43
+ const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
44
+ return { activePlacement, activeArrowPosition };
45
+ }
46
+ //# sourceMappingURL=use-tooltip-fallback-placement.js.map
@@ -1232,6 +1232,7 @@ const replay = (0, styled_components_1.css) `
1232
1232
  --replay-runtime-expression-color: rgba(54, 90, 249, 1); // @presenter Color
1233
1233
  --replay-runtime-expression-bg-color: rgba(54, 90, 249, 0.08); // @presenter Color
1234
1234
  --replay-operators-color: rgba(193, 142, 31, 1); // @presenter Color
1235
+ --replay-claude-icon-color: rgba(217, 119, 87, 1); // @presenter Color
1235
1236
 
1236
1237
  --replay-ai-gradient-soft: linear-gradient(62.6deg, rgba(113, 94, 254, 0.16) 0%, rgba(255, 92, 220, 0.16) 100%);
1237
1238
  --replay-ai-gradient-disabled: linear-gradient(62.6deg, rgba(113, 94, 254, 0.6) 0%, rgba(255, 92, 220, 0.6) 100%);
@@ -1,5 +1,5 @@
1
1
  import type { TOptions } from 'i18next';
2
- export type TranslationKey = 'dev.newApp' | 'dev.newApp.text' | 'dev.sidebar.header' | 'dev.sidebar.footer.text' | 'dev.create.app.dialog.appName.placeholder' | 'dev.create.app.dialog.appName.error' | 'dev.create.app.dialog.selectAPIs' | 'dev.create.app.dialog.description' | 'dev.create.app.dialog.description.placeholder' | 'dev.create.app.dialog.create' | 'dev.create.app.dialog.cancel' | 'dev.main.tab.appKeys' | 'dev.main.tab.logs' | 'dev.app.description.title' | 'dev.edit.description.dialog.title' | 'dev.edit.description.dialog.save' | 'dev.edit.description.dialog.cancel' | 'dev.edit.apis.dialog.selectedAPIs' | 'dev.app.key.create' | 'dev.create.key.dialog.title' | 'dev.create.key.dialog.create' | 'dev.create.key.dialog.cancel' | 'dev.app.edit' | 'dev.app.delete' | 'dev.edit.app.dialog.title' | 'dev.edit.app.dialog.save' | 'dev.edit.app.dialog.cancel' | 'dev.delete.app.dialog.title' | 'dev.delete.app.dialog.confirmation' | 'dev.delete.app.dialog.delete' | 'dev.delete.app.dialog.cancel' | 'dev.app.key.roll' | 'dev.roll.key.dialog.title' | 'dev.roll.key.dialog.apiKey' | 'dev.roll.key.dialog.expires' | 'dev.roll.key.dialog.confirmation' | 'dev.roll.key.dialog.cancel' | 'dev.roll.key.dialog.roll' | 'dev.update.key.dialog.title' | 'dev.update.key.dialog.update' | 'dev.update.key.dialog.cancel' | 'dev.app.key.api.name' | 'dev.app.key.api.status' | 'dev.app.key.api.edit' | 'dev.edit.apis.dialog.title' | 'dev.edit.apis.dialog.apiKey' | 'dev.edit.apis.dialog.save' | 'dev.edit.apis.dialog.cancel' | 'dev.select.placeholder' | 'dev.app.overview.status.pending' | 'dev.app.overview.status.approved' | 'dev.app.overview.status.revoked' | 'dev.app.overview.status' | 'dev.app.overview.non-production' | 'dev.app.overview.production' | 'dev.app.overview.clientId' | 'dev.app.overview.apiKey' | 'dev.app.key.revoke' | 'dev.revoke.key.dialog.title' | 'dev.revoke.key.dialog.apiKey' | 'dev.revoke.key.dialog.expires' | 'dev.revoke.key.dialog.confirmation' | 'dev.revoke.key.dialog.revoke' | 'dev.revoke.key.dialog.cancel' | 'dev.app.overview.expires' | 'dev.app.overview.created' | 'dev.app.overview.visibilityToggle.hide' | 'dev.app.overview.visibilityToggle.show' | 'search.loading' | 'search.noResults.title' | 'search.keys.navigate' | 'search.keys.select' | 'search.keys.exit' | 'search.label' | 'search.cancel' | 'search.recent' | 'search.navbar.label' | 'search.suggested' | 'search.showMore' | 'search.filter.title' | 'search.filter.reset' | 'search.filter.field.reset' | 'search.ai.welcomeText' | 'search.ai.newConversation' | 'search.ai.backToSearch' | 'search.ai.back' | 'search.ai.assistant' | 'search.ai.placeholder' | 'search.ai.generatingResponse' | 'search.ai.followUpQuestion' | 'search.ai.suggestionsTitle' | 'search.ai.thinkingText' | 'search.ai.resourcesFound' | 'search.ai.resourcesFound.basedOn' | 'search.ai.resourcesFound.resources' | 'search.ai.feedback.title' | 'search.ai.feedback.detailsPlaceholder' | 'search.ai.feedback.thanks' | 'search.ai.toolCall.executed' | 'search.ai.toolCall.executing' | 'search.ai.toolCall.withArgs' | 'search.ai.button' | 'search.ai.label' | 'search.ai.disclaimer' | 'search.ai.error.description' | 'search.ai.error.description.forbidden' | 'search.ai.error.description.unauthorized' | 'search.ai.error.header' | 'search.ai.error.header.forbidden' | 'search.ai.error.header.unauthorized' | 'search.ai.feedback.more' | 'search.searchItem.deprecated' | 'search.groups.all' | 'search.filter.field.footer' | 'aiAssistant.trigger' | 'toc.header' | 'footer.copyrightText' | 'page.homeButton' | 'page.forbidden.title' | 'page.forbidden.description' | 'page.notFound.title' | 'page.notFound.description' | 'page.lastUpdated.timeago' | 'page.lastUpdated.on' | 'catalog.filters.placeholder' | 'catalog.filters.title' | 'catalog.filters.add' | 'catalog.filters.clearAll' | 'catalog.filters.select.addFilter' | 'catalog.filters.select.all' | 'catalog.filters.done' | 'catalog.catalogs.all.title' | 'catalog.catalogs.all.description' | 'catalog.catalogs.all.switcherLabel' | 'catalog.catalogs.service.title' | 'catalog.catalogs.service.description' | 'catalog.catalogs.service.switcherLabel' | 'catalog.catalogs.user.title' | 'catalog.catalogs.user.description' | 'catalog.catalogs.user.switcherLabel' | 'catalog.catalogs.team.title' | 'catalog.catalogs.team.description' | 'catalog.catalogs.team.switcherLabel' | 'catalog.catalogs.domain.title' | 'catalog.catalogs.domain.description' | 'catalog.catalogs.domain.switcherLabel' | 'catalog.catalogs.apiDescription.title' | 'catalog.catalogs.apiDescription.description' | 'catalog.catalogs.apiDescription.switcherLabel' | 'catalog.catalogs.dataSchema.title' | 'catalog.catalogs.dataSchema.description' | 'catalog.catalogs.dataSchema.switcherLabel' | 'catalog.catalogs.apiOperation.title' | 'catalog.catalogs.apiOperation.description' | 'catalog.catalogs.apiOperation.switcherLabel' | 'catalog.entity.metadata.title' | 'catalog.entity.schema.title' | 'catalog.entity.properties.apiDescription.title' | 'catalog.backToAllLabel' | 'catalog.notConnected' | 'catalog.tags.label' | 'catalog.sort' | 'catalog.catalogs.label' | 'catalog.owners.label' | 'catalog.repositories.label' | 'catalog.email.label' | 'catalog.format.label' | 'catalog.entityType.label' | 'catalog.domains.label' | 'catalog.contact.label' | 'catalog.methodAndPath.label' | 'catalog.links.label' | 'catalog.metadata.domains' | 'catalog.metadata.owners' | 'catalog.history.button.label' | 'catalog.history.sidebar.title' | 'catalog.history.sidebar.close' | 'catalog.history.version.label' | 'catalog.filters.close' | 'catalog.history.version.notSpecified' | 'catalog.history.version.default' | 'catalog.history.revisions.limitMessage' | 'catalog.history.revision.current' | 'catalog.history.revisions.showLess' | 'catalog.history.revisions.showMore' | 'sidebar.menu.backLabel' | 'sidebar.menu.backToLabel' | 'sidebar.actions.show' | 'sidebar.actions.hide' | 'sidebar.actions.changeToSingleColumn' | 'sidebar.actions.changeToTwoColumns' | 'sidebar.actions.singleColumn' | 'sidebar.actions.twoColumns' | 'versionPicker.label' | 'versionPicker.unversioned' | 'codeSnippet.copy.buttonText' | 'codeSnippet.copy.tooltipText' | 'codeSnippet.copy.toasterText' | 'markdown.editPage.text' | 'feedback.settings.comment.submitText' | 'feedback.settings.comment.label' | 'feedback.settings.comment.send' | 'feedback.settings.comment.cancel' | 'feedback.settings.comment.maxLength' | 'feedback.settings.comment.satisfiedLabel' | 'feedback.settings.comment.neutralLabel' | 'feedback.settings.comment.dissatisfiedLabel' | 'feedback.settings.submitText' | 'feedback.settings.label' | 'feedback.settings.reasons.label' | 'feedback.submit' | 'feedback.cancel' | 'feedback.settings.comment.likeLabel' | 'feedback.settings.comment.dislikeLabel' | 'feedback.sentiment.thumbUp' | 'feedback.sentiment.thumbDown' | 'feedback.settings.leftScaleLabel' | 'feedback.settings.rightScaleLabel' | 'feedback.settings.optionalEmail.placeholder' | 'feedback.settings.optionalEmail.label' | 'codeSnippet.report.buttonText' | 'codeSnippet.report.tooltipText' | 'codeSnippet.report.label' | 'codeSnippet.expand.tooltipText' | 'codeSnippet.collapse.tooltipText' | 'userMenu.login' | 'userMenu.logout' | 'userMenu.devOnboardingLabel' | 'mobileMenu.mainMenu' | 'mobileMenu.previous' | 'mobileMenu.products' | 'mobileMenu.version' | 'navbar.products' | 'page.nextButton' | 'page.previousButton' | 'page.actions.copyButtonText' | 'page.actions.copyTitle' | 'page.actions.copyDescription' | 'page.actions.viewAsMdTitle' | 'page.actions.viewAsMdButtonText' | 'page.actions.viewAsMdDescription' | 'page.actions.chatGptTitle' | 'page.actions.chatGptButtonText' | 'page.actions.chatGptDescription' | 'page.actions.claudeTitle' | 'page.actions.claudeButtonText' | 'page.actions.claudeDescription' | 'page.actions.cursorMcpButtonText' | 'page.actions.cursorMcpTitle' | 'page.actions.cursorMcpDescription' | 'page.actions.connectMcp' | 'page.actions.connectMcp.cursor' | 'page.actions.connectMcp.cursorDescription' | 'page.actions.connectMcp.vscode' | 'page.actions.connectMcp.vscodeDescription' | 'page.actions.connectMcp.copyConfig' | 'page.actions.connectMcp.copyConfigDescription' | 'openapi.download.description.title' | 'openapi.info.title' | 'openapi.info.contact.url' | 'openapi.info.contact.name' | 'openapi.info.license' | 'openapi.info.termsOfService' | 'openapi.info.metadata.title' | 'openapi.key' | 'openapi.value' | 'openapi.enum' | 'openapi.items' | 'openapi.default' | 'openapi.variable' | 'openapi.variables' | 'openapi.actions.show' | 'openapi.actions.hide' | 'openapi.actions.more' | 'openapi.languages.title' | 'openapi.languages.moreButton.tooltipText' | 'openapi.servers.title' | 'openapi.operations' | 'openapi.webhooks' | 'openapi.description' | 'openapi.badges.deprecated' | 'openapi.badges.required' | 'openapi.badges.webhook' | 'openapi.request' | 'openapi.path' | 'openapi.query' | 'openapi.cookie' | 'openapi.header' | 'openapi.body' | 'openapi.responses' | 'openapi.response' | 'openapi.callbacks' | 'openapi.callbackRequest' | 'openapi.callbackResponse' | 'openapi.payload' | 'openapi.discriminator' | 'openapi.contentType' | 'openapi.tryIt' | 'openapi.loading' | 'openapi.example' | 'openapi.examples' | 'openapi.additionalProperties' | 'openapi.patternProperties' | 'openapi.required' | 'openapi.recursive' | 'openapi.complex' | 'openapi.hideExample' | 'openapi.showExample' | 'openapi.expandAll' | 'openapi.collapseAll' | 'openapi.viewSecurityDetails' | 'openapi.noResponseExample' | 'openapi.discriminator.searchPlaceholder' | 'openapi.discriminator.searchNoResults' | 'openapi.discriminator.defaultMapping' | 'openapi.discriminator.defaultMappingTooltip' | 'openapi.noResponseContent' | 'openapi.noRequestPayload' | 'openapi.hidePattern' | 'openapi.showPattern' | 'openapi.authorizationUrl' | 'openapi.tokenUrl' | 'openapi.refreshUrl' | 'openapi.showOptionalScopes' | 'openapi.hideOptionalScopes' | 'openapi.security' | 'openapi.httpAuthorizationScheme' | 'openapi.bearerFormat' | 'openapi.parameterName' | 'openapi.flowType' | 'openapi.connectUrl' | 'openapi.requiredScopes' | 'openapi.unsupportedLanguage' | 'openapi.failedToGenerateCodeSample' | 'openapi.schemaCatalogLink.title' | 'openapi.schemaCatalogLink.copyButtonTooltip' | 'openapi.schemaCatalogLink.copiedTooltip' | 'openapi.mcp.title' | 'openapi.mcp.endpoint' | 'openapi.mcp.tools' | 'openapi.mcp.protocolVersion' | 'openapi.mcp.capabilities' | 'openapi.mcp.experimentalCapabilities' | 'openapi.mcp.inputSchema' | 'openapi.mcp.inputExample' | 'openapi.mcp.outputSchema' | 'openapi.mcp.outputExample' | 'asyncapi.download.description.title' | 'asyncapi.info.title' | 'graphql.download.description.title' | 'graphql.info.title' | 'graphql.info.contact.url' | 'graphql.info.contact.name' | 'graphql.info.license' | 'graphql.info.termsOfService' | 'graphql.overview' | 'graphql.metadata' | 'graphql.key' | 'graphql.value' | 'graphql.queries' | 'graphql.mutations' | 'graphql.subscriptions' | 'graphql.directives' | 'graphql.objects' | 'graphql.interfaces' | 'graphql.unions' | 'graphql.enums' | 'graphql.inputs' | 'graphql.scalars' | 'graphql.arguments.label' | 'graphql.arguments.show' | 'graphql.arguments.hide' | 'graphql.arguments.here' | 'graphql.returnTypes.label' | 'graphql.returnTypes.show' | 'graphql.returnTypes.hide' | 'graphql.possibleTypes' | 'graphql.defaultValue' | 'graphql.deprecationReason' | 'graphql.requiredScopes' | 'graphql.viewSecurityDetails' | 'graphql.objectScopes' | 'graphql.fieldScopes' | 'graphql.implementedInterfaces' | 'graphql.nonNull' | 'graphql.required' | 'graphql.deprecated' | 'graphql.variables' | 'graphql.querySample' | 'graphql.mutationSample' | 'graphql.subscriptionSample' | 'graphql.responseSample' | 'graphql.locations' | 'graphql.sample' | 'graphql.referenced' | 'graphql.content.fragment' | 'button.copy.tooltipText' | 'button.download.tooltipText' | 'button.externalLink.tooltipText' | 'button.email.tooltipText' | 'codeWalkthrough.download' | 'codeWalkthrough.preview' | 'time.justNow' | 'time.past.second' | 'time.past.seconds' | 'time.past.minute' | 'time.past.minutes' | 'time.past.hour' | 'time.past.hours' | 'time.past.day' | 'time.past.days' | 'time.past.week' | 'time.past.weeks' | 'time.past.month' | 'time.past.months' | 'time.past.year' | 'time.past.years' | 'page.internalServerError.title' | 'page.internalServerError.description' | 'page.skipToContent.label' | 'select.noResults' | 'loaders.loading' | 'filter.dateRange.from' | 'filter.dateRange.to' | 'diagram.openFullscreen' | 'diagram.zoomIn' | 'diagram.zoomOut' | 'diagram.reset' | 'diagram.close' | 'diagram.viewer';
2
+ export type TranslationKey = 'dev.newApp' | 'dev.newApp.text' | 'dev.sidebar.header' | 'dev.sidebar.footer.text' | 'dev.create.app.dialog.appName.placeholder' | 'dev.create.app.dialog.appName.error' | 'dev.create.app.dialog.selectAPIs' | 'dev.create.app.dialog.description' | 'dev.create.app.dialog.description.placeholder' | 'dev.create.app.dialog.create' | 'dev.create.app.dialog.cancel' | 'dev.main.tab.appKeys' | 'dev.main.tab.logs' | 'dev.app.description.title' | 'dev.edit.description.dialog.title' | 'dev.edit.description.dialog.save' | 'dev.edit.description.dialog.cancel' | 'dev.create.app.dialog.callbackUrls' | 'dev.create.app.dialog.callbackUrls.placeholder' | 'dev.create.app.dialog.callbackUrls.hint' | 'dev.app.callbackUrls.title' | 'dev.edit.callbackUrls.dialog.title' | 'dev.edit.callbackUrls.dialog.placeholder' | 'dev.edit.callbackUrls.dialog.hint' | 'dev.edit.callbackUrls.dialog.save' | 'dev.edit.callbackUrls.dialog.cancel' | 'dev.edit.apis.dialog.selectedAPIs' | 'dev.app.key.create' | 'dev.create.key.dialog.title' | 'dev.create.key.dialog.create' | 'dev.create.key.dialog.cancel' | 'dev.app.edit' | 'dev.app.delete' | 'dev.edit.app.dialog.title' | 'dev.edit.app.dialog.save' | 'dev.edit.app.dialog.cancel' | 'dev.delete.app.dialog.title' | 'dev.delete.app.dialog.confirmation' | 'dev.delete.app.dialog.delete' | 'dev.delete.app.dialog.cancel' | 'dev.app.key.roll' | 'dev.roll.key.dialog.title' | 'dev.roll.key.dialog.apiKey' | 'dev.roll.key.dialog.expires' | 'dev.roll.key.dialog.confirmation' | 'dev.roll.key.dialog.cancel' | 'dev.roll.key.dialog.roll' | 'dev.update.key.dialog.title' | 'dev.update.key.dialog.update' | 'dev.update.key.dialog.cancel' | 'dev.app.key.api.name' | 'dev.app.key.api.status' | 'dev.app.key.api.edit' | 'dev.edit.apis.dialog.title' | 'dev.edit.apis.dialog.apiKey' | 'dev.edit.apis.dialog.save' | 'dev.edit.apis.dialog.cancel' | 'dev.select.placeholder' | 'dev.app.overview.status.pending' | 'dev.app.overview.status.approved' | 'dev.app.overview.status.revoked' | 'dev.app.overview.status' | 'dev.app.overview.non-production' | 'dev.app.overview.production' | 'dev.app.overview.clientId' | 'dev.app.overview.apiKey' | 'dev.app.key.revoke' | 'dev.revoke.key.dialog.title' | 'dev.revoke.key.dialog.apiKey' | 'dev.revoke.key.dialog.expires' | 'dev.revoke.key.dialog.confirmation' | 'dev.revoke.key.dialog.revoke' | 'dev.revoke.key.dialog.cancel' | 'dev.app.overview.expires' | 'dev.app.overview.created' | 'dev.app.overview.visibilityToggle.hide' | 'dev.app.overview.visibilityToggle.show' | 'search.loading' | 'search.noResults.title' | 'search.keys.navigate' | 'search.keys.select' | 'search.keys.exit' | 'search.label' | 'search.cancel' | 'search.recent' | 'search.navbar.label' | 'search.suggested' | 'search.showMore' | 'search.filter.title' | 'search.filter.reset' | 'search.filter.field.reset' | 'search.ai.welcomeText' | 'search.ai.newConversation' | 'search.ai.backToSearch' | 'search.ai.back' | 'search.ai.assistant' | 'search.ai.placeholder' | 'search.ai.generatingResponse' | 'search.ai.followUpQuestion' | 'search.ai.suggestionsTitle' | 'search.ai.thinkingText' | 'search.ai.resourcesFound' | 'search.ai.resourcesFound.basedOn' | 'search.ai.resourcesFound.resources' | 'search.ai.feedback.title' | 'search.ai.feedback.detailsPlaceholder' | 'search.ai.feedback.thanks' | 'search.ai.toolCall.executed' | 'search.ai.toolCall.executing' | 'search.ai.toolCall.withArgs' | 'search.ai.button' | 'search.ai.label' | 'search.ai.disclaimer' | 'search.ai.error.description' | 'search.ai.error.description.forbidden' | 'search.ai.error.description.unauthorized' | 'search.ai.error.header' | 'search.ai.error.header.forbidden' | 'search.ai.error.header.unauthorized' | 'search.ai.feedback.more' | 'search.searchItem.deprecated' | 'search.groups.all' | 'search.filter.field.footer' | 'aiAssistant.trigger' | 'toc.header' | 'footer.copyrightText' | 'page.homeButton' | 'page.forbidden.title' | 'page.forbidden.description' | 'page.notFound.title' | 'page.notFound.description' | 'page.lastUpdated.timeago' | 'page.lastUpdated.on' | 'catalog.filters.placeholder' | 'catalog.filters.title' | 'catalog.filters.add' | 'catalog.filters.clearAll' | 'catalog.filters.select.addFilter' | 'catalog.filters.select.all' | 'catalog.filters.done' | 'catalog.catalogs.all.title' | 'catalog.catalogs.all.description' | 'catalog.catalogs.all.switcherLabel' | 'catalog.catalogs.service.title' | 'catalog.catalogs.service.description' | 'catalog.catalogs.service.switcherLabel' | 'catalog.catalogs.user.title' | 'catalog.catalogs.user.description' | 'catalog.catalogs.user.switcherLabel' | 'catalog.catalogs.team.title' | 'catalog.catalogs.team.description' | 'catalog.catalogs.team.switcherLabel' | 'catalog.catalogs.domain.title' | 'catalog.catalogs.domain.description' | 'catalog.catalogs.domain.switcherLabel' | 'catalog.catalogs.apiDescription.title' | 'catalog.catalogs.apiDescription.description' | 'catalog.catalogs.apiDescription.switcherLabel' | 'catalog.catalogs.dataSchema.title' | 'catalog.catalogs.dataSchema.description' | 'catalog.catalogs.dataSchema.switcherLabel' | 'catalog.catalogs.apiOperation.title' | 'catalog.catalogs.apiOperation.description' | 'catalog.catalogs.apiOperation.switcherLabel' | 'catalog.entity.metadata.title' | 'catalog.entity.schema.title' | 'catalog.entity.properties.apiDescription.title' | 'catalog.backToAllLabel' | 'catalog.notConnected' | 'catalog.tags.label' | 'catalog.sort' | 'catalog.catalogs.label' | 'catalog.owners.label' | 'catalog.repositories.label' | 'catalog.email.label' | 'catalog.format.label' | 'catalog.entityType.label' | 'catalog.domains.label' | 'catalog.contact.label' | 'catalog.methodAndPath.label' | 'catalog.links.label' | 'catalog.metadata.domains' | 'catalog.metadata.owners' | 'catalog.history.button.label' | 'catalog.history.sidebar.title' | 'catalog.history.sidebar.close' | 'catalog.history.version.label' | 'catalog.filters.close' | 'catalog.history.version.notSpecified' | 'catalog.history.version.default' | 'catalog.history.revisions.limitMessage' | 'catalog.history.revision.current' | 'catalog.history.revisions.showLess' | 'catalog.history.revisions.showMore' | 'sidebar.menu.backLabel' | 'sidebar.menu.backToLabel' | 'sidebar.actions.show' | 'sidebar.actions.hide' | 'sidebar.actions.changeToSingleColumn' | 'sidebar.actions.changeToTwoColumns' | 'sidebar.actions.singleColumn' | 'sidebar.actions.twoColumns' | 'versionPicker.label' | 'versionPicker.unversioned' | 'codeSnippet.copy.buttonText' | 'codeSnippet.copy.tooltipText' | 'codeSnippet.copy.toasterText' | 'markdown.editPage.text' | 'feedback.settings.comment.submitText' | 'feedback.settings.comment.label' | 'feedback.settings.comment.send' | 'feedback.settings.comment.cancel' | 'feedback.settings.comment.maxLength' | 'feedback.settings.comment.satisfiedLabel' | 'feedback.settings.comment.neutralLabel' | 'feedback.settings.comment.dissatisfiedLabel' | 'feedback.settings.submitText' | 'feedback.settings.label' | 'feedback.settings.reasons.label' | 'feedback.submit' | 'feedback.cancel' | 'feedback.settings.comment.likeLabel' | 'feedback.settings.comment.dislikeLabel' | 'feedback.sentiment.thumbUp' | 'feedback.sentiment.thumbDown' | 'feedback.settings.leftScaleLabel' | 'feedback.settings.rightScaleLabel' | 'feedback.settings.optionalEmail.placeholder' | 'feedback.settings.optionalEmail.label' | 'codeSnippet.report.buttonText' | 'codeSnippet.report.tooltipText' | 'codeSnippet.report.label' | 'codeSnippet.expand.tooltipText' | 'codeSnippet.collapse.tooltipText' | 'userMenu.login' | 'userMenu.logout' | 'userMenu.devOnboardingLabel' | 'mobileMenu.mainMenu' | 'mobileMenu.previous' | 'mobileMenu.products' | 'mobileMenu.version' | 'navbar.products' | 'page.nextButton' | 'page.previousButton' | 'page.actions.copyButtonText' | 'page.actions.copyTitle' | 'page.actions.copyDescription' | 'page.actions.viewAsMdTitle' | 'page.actions.viewAsMdButtonText' | 'page.actions.viewAsMdDescription' | 'page.actions.chatGptTitle' | 'page.actions.chatGptButtonText' | 'page.actions.chatGptDescription' | 'page.actions.claudeTitle' | 'page.actions.claudeButtonText' | 'page.actions.claudeDescription' | 'page.actions.cursorMcpButtonText' | 'page.actions.cursorMcpTitle' | 'page.actions.cursorMcpDescription' | 'page.actions.connectMcp' | 'page.actions.connectMcp.cursor' | 'page.actions.connectMcp.cursorDescription' | 'page.actions.connectMcp.vscode' | 'page.actions.connectMcp.vscodeDescription' | 'page.actions.connectMcp.copyConfig' | 'page.actions.connectMcp.copyConfigDescription' | 'openapi.download.description.title' | 'openapi.info.title' | 'openapi.info.contact.url' | 'openapi.info.contact.name' | 'openapi.info.license' | 'openapi.info.termsOfService' | 'openapi.info.metadata.title' | 'openapi.key' | 'openapi.value' | 'openapi.enum' | 'openapi.items' | 'openapi.default' | 'openapi.variable' | 'openapi.variables' | 'openapi.actions.show' | 'openapi.actions.hide' | 'openapi.actions.more' | 'openapi.languages.title' | 'openapi.languages.moreButton.tooltipText' | 'openapi.servers.title' | 'openapi.operations' | 'openapi.webhooks' | 'openapi.description' | 'openapi.badges.deprecated' | 'openapi.badges.required' | 'openapi.badges.webhook' | 'openapi.request' | 'openapi.path' | 'openapi.query' | 'openapi.cookie' | 'openapi.header' | 'openapi.body' | 'openapi.responses' | 'openapi.response' | 'openapi.callbacks' | 'openapi.callbackRequest' | 'openapi.callbackResponse' | 'openapi.payload' | 'openapi.discriminator' | 'openapi.contentType' | 'openapi.tryIt' | 'openapi.loading' | 'openapi.example' | 'openapi.examples' | 'openapi.additionalProperties' | 'openapi.patternProperties' | 'openapi.required' | 'openapi.recursive' | 'openapi.complex' | 'openapi.hideExample' | 'openapi.showExample' | 'openapi.expandAll' | 'openapi.collapseAll' | 'openapi.viewSecurityDetails' | 'openapi.noResponseExample' | 'openapi.discriminator.searchPlaceholder' | 'openapi.discriminator.searchNoResults' | 'openapi.discriminator.defaultMapping' | 'openapi.discriminator.defaultMappingTooltip' | 'openapi.noResponseContent' | 'openapi.noRequestPayload' | 'openapi.hidePattern' | 'openapi.showPattern' | 'openapi.authorizationUrl' | 'openapi.tokenUrl' | 'openapi.refreshUrl' | 'openapi.showOptionalScopes' | 'openapi.hideOptionalScopes' | 'openapi.security' | 'openapi.httpAuthorizationScheme' | 'openapi.bearerFormat' | 'openapi.parameterName' | 'openapi.flowType' | 'openapi.connectUrl' | 'openapi.requiredScopes' | 'openapi.unsupportedLanguage' | 'openapi.failedToGenerateCodeSample' | 'openapi.schemaCatalogLink.title' | 'openapi.schemaCatalogLink.copyButtonTooltip' | 'openapi.schemaCatalogLink.copiedTooltip' | 'openapi.mcp.title' | 'openapi.mcp.endpoint' | 'openapi.mcp.tools' | 'openapi.mcp.protocolVersion' | 'openapi.mcp.capabilities' | 'openapi.mcp.experimentalCapabilities' | 'openapi.mcp.inputSchema' | 'openapi.mcp.inputExample' | 'openapi.mcp.outputSchema' | 'openapi.mcp.outputExample' | 'asyncapi.download.description.title' | 'asyncapi.info.title' | 'graphql.download.description.title' | 'graphql.info.title' | 'graphql.info.contact.url' | 'graphql.info.contact.name' | 'graphql.info.license' | 'graphql.info.termsOfService' | 'graphql.overview' | 'graphql.metadata' | 'graphql.key' | 'graphql.value' | 'graphql.queries' | 'graphql.mutations' | 'graphql.subscriptions' | 'graphql.directives' | 'graphql.objects' | 'graphql.interfaces' | 'graphql.unions' | 'graphql.enums' | 'graphql.inputs' | 'graphql.scalars' | 'graphql.arguments.label' | 'graphql.arguments.show' | 'graphql.arguments.hide' | 'graphql.arguments.here' | 'graphql.returnTypes.label' | 'graphql.returnTypes.show' | 'graphql.returnTypes.hide' | 'graphql.possibleTypes' | 'graphql.defaultValue' | 'graphql.deprecationReason' | 'graphql.requiredScopes' | 'graphql.viewSecurityDetails' | 'graphql.objectScopes' | 'graphql.fieldScopes' | 'graphql.implementedInterfaces' | 'graphql.nonNull' | 'graphql.required' | 'graphql.deprecated' | 'graphql.variables' | 'graphql.querySample' | 'graphql.mutationSample' | 'graphql.subscriptionSample' | 'graphql.responseSample' | 'graphql.locations' | 'graphql.sample' | 'graphql.referenced' | 'graphql.content.fragment' | 'button.copy.tooltipText' | 'button.download.tooltipText' | 'button.externalLink.tooltipText' | 'button.email.tooltipText' | 'codeWalkthrough.download' | 'codeWalkthrough.preview' | 'time.justNow' | 'time.past.second' | 'time.past.seconds' | 'time.past.minute' | 'time.past.minutes' | 'time.past.hour' | 'time.past.hours' | 'time.past.day' | 'time.past.days' | 'time.past.week' | 'time.past.weeks' | 'time.past.month' | 'time.past.months' | 'time.past.year' | 'time.past.years' | 'page.internalServerError.title' | 'page.internalServerError.description' | 'page.skipToContent.label' | 'select.noResults' | 'loaders.loading' | 'filter.dateRange.from' | 'filter.dateRange.to' | 'diagram.openFullscreen' | 'diagram.zoomIn' | 'diagram.zoomOut' | 'diagram.reset' | 'diagram.close' | 'diagram.viewer';
3
3
  export type Locale = {
4
4
  code: string;
5
5
  name: string;
@@ -1,10 +1,12 @@
1
1
  import type { ReactNode } from 'react';
2
+ export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
2
3
  export type TooltipProps = {
3
4
  children?: ReactNode;
4
5
  tip: string | ReactNode;
5
6
  isOpen?: boolean;
6
7
  withArrow?: boolean;
7
- placement?: 'top' | 'bottom' | 'left' | 'right';
8
+ placement?: TooltipPlacement;
9
+ fallbackPlacements?: TooltipPlacement[];
8
10
  className?: string;
9
11
  width?: string;
10
12
  dataTestId?: string;
@@ -46,3 +46,4 @@ export * from './build-revision-url';
46
46
  export * from './content-segments';
47
47
  export * from './custom-catalog-options-casing';
48
48
  export * from './get-auto-dismiss-duration';
49
+ export * from './tooltip-placement';
@@ -62,4 +62,5 @@ __exportStar(require("./build-revision-url"), exports);
62
62
  __exportStar(require("./content-segments"), exports);
63
63
  __exportStar(require("./custom-catalog-options-casing"), exports);
64
64
  __exportStar(require("./get-auto-dismiss-duration"), exports);
65
+ __exportStar(require("./tooltip-placement"), exports);
65
66
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,32 @@
1
+ import type { TooltipPlacement, TooltipProps } from '../../core/types';
2
+ export declare function getDefaultFallbackPlacements(placement: TooltipPlacement): TooltipPlacement[];
3
+ export declare function calcAnchorPoint(triggerRect: DOMRect, placement: TooltipPlacement, arrowPosition: TooltipProps['arrowPosition']): {
4
+ top: number;
5
+ left: number;
6
+ };
7
+ type FitsInViewportParams = {
8
+ anchor: {
9
+ top: number;
10
+ left: number;
11
+ };
12
+ tooltipWidth: number;
13
+ tooltipHeight: number;
14
+ placement: TooltipPlacement;
15
+ arrowPosition: TooltipProps['arrowPosition'];
16
+ };
17
+ export declare function fitsInViewport({ anchor, tooltipWidth, tooltipHeight, placement, arrowPosition, }: FitsInViewportParams): boolean;
18
+ type ResolvePlacementParams = {
19
+ triggerRect: DOMRect;
20
+ tooltipWidth: number;
21
+ tooltipHeight: number;
22
+ placement: TooltipPlacement;
23
+ arrowPosition: TooltipProps['arrowPosition'];
24
+ fallbackPlacements: TooltipPlacement[] | undefined;
25
+ };
26
+ /**
27
+ * Given the trigger rect, tooltip dimensions, primary placement/arrow, and
28
+ * fallback list, returns the first placement that keeps the tooltip fully
29
+ * inside the viewport. Falls back to the primary when nothing fits.
30
+ */
31
+ export declare function resolvePlacement({ triggerRect, tooltipWidth, tooltipHeight, placement, arrowPosition, fallbackPlacements, }: ResolvePlacementParams): TooltipPlacement;
32
+ export {};
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDefaultFallbackPlacements = getDefaultFallbackPlacements;
4
+ exports.calcAnchorPoint = calcAnchorPoint;
5
+ exports.fitsInViewport = fitsInViewport;
6
+ exports.resolvePlacement = resolvePlacement;
7
+ const PLACEMENT_MARGIN = 10;
8
+ const COUNTER_CLOCKWISE = ['top', 'left', 'bottom', 'right'];
9
+ function getDefaultFallbackPlacements(placement) {
10
+ const index = COUNTER_CLOCKWISE.indexOf(placement);
11
+ const result = [];
12
+ for (let i = 1; i < COUNTER_CLOCKWISE.length; i++) {
13
+ result.push(COUNTER_CLOCKWISE[(index + i) % COUNTER_CLOCKWISE.length]);
14
+ }
15
+ return result;
16
+ }
17
+ function calcAnchorPoint(triggerRect, placement, arrowPosition) {
18
+ const horizontalLeft = () => arrowPosition === 'left'
19
+ ? triggerRect.left - 24
20
+ : arrowPosition === 'right'
21
+ ? triggerRect.right + 24
22
+ : triggerRect.left + triggerRect.width / 2;
23
+ const verticalTop = () => triggerRect.top + triggerRect.height / 2;
24
+ switch (placement) {
25
+ case 'top':
26
+ return { top: triggerRect.top, left: horizontalLeft() };
27
+ case 'bottom':
28
+ return { top: triggerRect.bottom, left: horizontalLeft() };
29
+ case 'left':
30
+ return { top: verticalTop(), left: triggerRect.left };
31
+ case 'right':
32
+ return { top: verticalTop(), left: triggerRect.right };
33
+ }
34
+ }
35
+ function fitsInViewport({ anchor, tooltipWidth, tooltipHeight, placement, arrowPosition, }) {
36
+ const horizontalLeft = () => arrowPosition === 'left'
37
+ ? anchor.left
38
+ : arrowPosition === 'right'
39
+ ? anchor.left - tooltipWidth
40
+ : anchor.left - tooltipWidth / 2;
41
+ const verticalTop = () => anchor.top - tooltipHeight / 2;
42
+ let top;
43
+ let left;
44
+ switch (placement) {
45
+ case 'top':
46
+ top = anchor.top - tooltipHeight - PLACEMENT_MARGIN;
47
+ left = horizontalLeft();
48
+ break;
49
+ case 'bottom':
50
+ top = anchor.top + PLACEMENT_MARGIN;
51
+ left = horizontalLeft();
52
+ break;
53
+ case 'left':
54
+ top = verticalTop();
55
+ left = anchor.left - tooltipWidth - PLACEMENT_MARGIN;
56
+ break;
57
+ case 'right':
58
+ top = verticalTop();
59
+ left = anchor.left + PLACEMENT_MARGIN;
60
+ break;
61
+ }
62
+ return (top >= 0 &&
63
+ left >= 0 &&
64
+ left + tooltipWidth <= window.innerWidth &&
65
+ top + tooltipHeight <= window.innerHeight);
66
+ }
67
+ /**
68
+ * Given the trigger rect, tooltip dimensions, primary placement/arrow, and
69
+ * fallback list, returns the first placement that keeps the tooltip fully
70
+ * inside the viewport. Falls back to the primary when nothing fits.
71
+ */
72
+ function resolvePlacement({ triggerRect, tooltipWidth, tooltipHeight, placement, arrowPosition, fallbackPlacements, }) {
73
+ if (!(fallbackPlacements === null || fallbackPlacements === void 0 ? void 0 : fallbackPlacements.length) || tooltipWidth === 0 || tooltipHeight === 0) {
74
+ return placement;
75
+ }
76
+ const candidates = [placement, ...fallbackPlacements];
77
+ for (const candidate of candidates) {
78
+ const candidateArrow = candidate === placement ? arrowPosition : 'center';
79
+ const pos = calcAnchorPoint(triggerRect, candidate, candidateArrow);
80
+ if (fitsInViewport({
81
+ anchor: pos,
82
+ tooltipWidth,
83
+ tooltipHeight,
84
+ placement: candidate,
85
+ arrowPosition: candidateArrow,
86
+ })) {
87
+ return candidate;
88
+ }
89
+ }
90
+ return placement;
91
+ }
92
+ //# sourceMappingURL=tooltip-placement.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.64.0",
3
+ "version": "0.65.0-next.1",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -63,7 +63,7 @@
63
63
  "vitest": "4.0.10",
64
64
  "vitest-when": "0.6.2",
65
65
  "webpack": "5.105.2",
66
- "@redocly/realm-asyncapi-sdk": "0.10.0"
66
+ "@redocly/realm-asyncapi-sdk": "0.11.0-next.1"
67
67
  },
68
68
  "dependencies": {
69
69
  "@tanstack/react-query": "5.62.3",
@@ -81,7 +81,7 @@
81
81
  "openapi-sampler": "^1.7.2",
82
82
  "react-calendar": "5.1.0",
83
83
  "react-date-picker": "11.0.0",
84
- "@redocly/config": "0.48.0"
84
+ "@redocly/config": "0.48.1"
85
85
  },
86
86
  "scripts": {
87
87
  "watch": "tsc -p tsconfig.build.json && (concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\")",
@@ -4,7 +4,11 @@ import styled, { css } from 'styled-components';
4
4
  import type { JSX, PropsWithChildren } from 'react';
5
5
  import type { TooltipProps } from '@redocly/theme/core/types';
6
6
 
7
- import { useControl, useOutsideClick } from '@redocly/theme/core/hooks';
7
+ import {
8
+ useControl,
9
+ useOutsideClick,
10
+ useTooltipFallbackPlacement,
11
+ } from '@redocly/theme/core/hooks';
8
12
  import { Portal } from '@redocly/theme/components/Portal/Portal';
9
13
 
10
14
  type Props = Exclude<TooltipProps, 'arrowPosition'> & {
@@ -17,6 +21,7 @@ function TooltipComponent({
17
21
  tip,
18
22
  withArrow = true,
19
23
  placement = 'top',
24
+ fallbackPlacements,
20
25
  className = 'default',
21
26
  width,
22
27
  dataTestId,
@@ -29,6 +34,14 @@ function TooltipComponent({
29
34
  const { isOpened, handleOpen, handleClose } = useControl(isOpen);
30
35
  const anchorName = `--tooltip${useId().replace(/:/g, '')}`;
31
36
 
37
+ const { activePlacement, activeArrowPosition } = useTooltipFallbackPlacement({
38
+ isOpened,
39
+ placement,
40
+ arrowPosition,
41
+ fallbackPlacements,
42
+ tooltipBodyRef,
43
+ });
44
+
32
45
  useOutsideClick(isOpened ? [tooltipWrapperRef, tooltipBodyRef] : tooltipWrapperRef, handleClose);
33
46
 
34
47
  const isControlled = isOpen !== undefined;
@@ -71,10 +84,14 @@ function TooltipComponent({
71
84
  <TooltipBody
72
85
  ref={tooltipBodyRef}
73
86
  data-testid={dataTestId || (typeof tip === 'string' ? tip : '')}
74
- placement={placement}
87
+ placement={activePlacement}
75
88
  width={width}
76
89
  withArrow={withArrow}
77
- arrowPosition={arrowPosition}
90
+ arrowPosition={
91
+ activeArrowPosition === 'left' || activeArrowPosition === 'right'
92
+ ? activeArrowPosition
93
+ : 'center'
94
+ }
78
95
  anchorName={anchorName}
79
96
  >
80
97
  {tip}
@@ -6,6 +6,7 @@ import type { TooltipProps } from '@redocly/theme/core/types';
6
6
 
7
7
  import { useControl, useOutsideClick } from '@redocly/theme/core/hooks';
8
8
  import { Portal } from '@redocly/theme/components/Portal/Portal';
9
+ import { calcAnchorPoint, resolvePlacement } from '@redocly/theme/core/utils';
9
10
 
10
11
  function TooltipComponent({
11
12
  children,
@@ -13,6 +14,7 @@ function TooltipComponent({
13
14
  tip,
14
15
  withArrow = true,
15
16
  placement = 'top',
17
+ fallbackPlacements,
16
18
  className = 'default',
17
19
  width,
18
20
  dataTestId,
@@ -20,58 +22,41 @@ function TooltipComponent({
20
22
  arrowPosition = 'center',
21
23
  onClick,
22
24
  }: PropsWithChildren<TooltipProps>): JSX.Element {
23
- const ref = useRef<HTMLDivElement | null>(null);
25
+ const wrapperRef = useRef<HTMLDivElement | null>(null);
26
+ const tooltipBodyRef = useRef<HTMLSpanElement | null>(null);
24
27
  const { isOpened, handleOpen, handleClose } = useControl(isOpen);
25
28
  const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
29
+ const [activePlacement, setActivePlacement] = useState(placement);
26
30
 
27
- useOutsideClick(ref, handleClose);
31
+ const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
32
+
33
+ useOutsideClick(wrapperRef, handleClose);
28
34
 
29
35
  const isControlled = isOpen !== undefined;
30
36
 
31
- const updateTooltipPosition = useCallback(() => {
32
- if (isOpened && ref.current) {
33
- const rect = ref.current.getBoundingClientRect();
34
-
35
- let top = 0;
36
- let left = 0;
37
-
38
- switch (placement) {
39
- case 'top':
40
- top = rect.top;
41
- if (arrowPosition === 'left') {
42
- left = rect.left - 24;
43
- } else if (arrowPosition === 'right') {
44
- left = rect.right + 24;
45
- } else {
46
- left = rect.left + rect.width / 2;
47
- }
48
- break;
49
- case 'bottom':
50
- top = rect.bottom;
51
- if (arrowPosition === 'left') {
52
- left = rect.left - 24;
53
- } else if (arrowPosition === 'right') {
54
- left = rect.right + 24;
55
- } else {
56
- left = rect.left + rect.width / 2;
57
- }
58
- break;
59
- case 'left':
60
- top = rect.top + rect.height / 2;
61
- left = rect.left;
62
- break;
63
- case 'right':
64
- top = rect.top + rect.height / 2;
65
- left = rect.right;
66
- break;
67
- }
68
-
69
- setTooltipPosition({ top, left });
70
- }
71
- }, [isOpened, placement, arrowPosition]);
37
+ const updateTooltipPosition = useCallback((): void => {
38
+ if (!isOpened || !wrapperRef.current) return;
39
+
40
+ const triggerRect = wrapperRef.current.getBoundingClientRect();
41
+ const tooltipWidth = tooltipBodyRef.current?.offsetWidth ?? 0;
42
+ const tooltipHeight = tooltipBodyRef.current?.offsetHeight ?? 0;
43
+
44
+ const resolved = resolvePlacement({
45
+ triggerRect,
46
+ tooltipWidth,
47
+ tooltipHeight,
48
+ placement,
49
+ arrowPosition,
50
+ fallbackPlacements,
51
+ });
52
+ const resolvedArrow = resolved === placement ? arrowPosition : 'center';
53
+
54
+ setTooltipPosition(calcAnchorPoint(triggerRect, resolved, resolvedArrow));
55
+ setActivePlacement(resolved);
56
+ }, [isOpened, placement, arrowPosition, fallbackPlacements]);
72
57
 
73
58
  useLayoutEffect(() => {
74
- if (isOpened && ref.current) {
59
+ if (isOpened && wrapperRef.current) {
75
60
  updateTooltipPosition();
76
61
 
77
62
  const handleScroll = () => updateTooltipPosition();
@@ -109,7 +94,7 @@ function TooltipComponent({
109
94
 
110
95
  return (
111
96
  <TooltipWrapper
112
- ref={ref}
97
+ ref={wrapperRef}
113
98
  {...controllers}
114
99
  className={`tooltip-${className}`}
115
100
  data-component-name="Tooltip/Tooltip"
@@ -118,11 +103,12 @@ function TooltipComponent({
118
103
  {isOpened && !disabled && (
119
104
  <Portal>
120
105
  <TooltipBody
106
+ ref={tooltipBodyRef}
121
107
  data-testid={dataTestId || (typeof tip === 'string' ? tip : '')}
122
- placement={placement}
108
+ placement={activePlacement}
123
109
  width={width}
124
110
  withArrow={withArrow}
125
- arrowPosition={arrowPosition}
111
+ arrowPosition={activeArrowPosition}
126
112
  style={{
127
113
  position: 'fixed',
128
114
  top: tooltipPosition.top,
@@ -255,7 +241,9 @@ const TooltipWrapper = styled.div`
255
241
  display: flex;
256
242
  `;
257
243
  const TooltipBody = styled.span<
258
- Pick<Required<TooltipProps>, 'placement' | 'withArrow' | 'arrowPosition'> & { width?: string }
244
+ Pick<Required<TooltipProps>, 'placement' | 'withArrow' | 'arrowPosition'> & {
245
+ width?: string;
246
+ }
259
247
  >`
260
248
  display: inline-block;
261
249
 
@@ -1,18 +1,29 @@
1
1
  import React, { memo } from 'react';
2
2
 
3
+ import type { TooltipProps } from '@redocly/theme/core/types';
4
+
3
5
  import { Tooltip as AnchorTooltip } from '@redocly/theme/components/Tooltip/AnchorTooltip';
4
6
  import { Tooltip as JsTooltip } from '@redocly/theme/components/Tooltip/JsTooltip';
5
- import { TooltipProps } from '@redocly/theme/core/types';
6
7
  import { useThemeHooks } from '@redocly/theme/core/hooks';
8
+ import { getDefaultFallbackPlacements } from '@redocly/theme/core/utils';
7
9
 
8
10
  function TooltipComponent(props: TooltipProps): React.ReactElement {
9
11
  const { useAnchorPositioning } = useThemeHooks();
10
12
  const { isSupported } = useAnchorPositioning();
11
13
 
14
+ const fallbackPlacements =
15
+ props.fallbackPlacements ?? getDefaultFallbackPlacements(props.placement ?? 'top');
16
+
12
17
  if (isSupported) {
13
- return <AnchorTooltip {...props} arrowPosition={prepareArrowPosition(props.arrowPosition)} />;
18
+ return (
19
+ <AnchorTooltip
20
+ {...props}
21
+ fallbackPlacements={fallbackPlacements}
22
+ arrowPosition={prepareArrowPosition(props.arrowPosition)}
23
+ />
24
+ );
14
25
  }
15
- return <JsTooltip {...props} />;
26
+ return <JsTooltip {...props} fallbackPlacements={fallbackPlacements} />;
16
27
  }
17
28
 
18
29
  export const Tooltip = memo<TooltipProps>(TooltipComponent);
@@ -26,3 +26,4 @@ export * from '../use-language-picker';
26
26
  export * from './use-element-size';
27
27
  export * from './use-time-ago';
28
28
  export * from './use-input-key-commands';
29
+ export * from '../use-tooltip-fallback-placement';
@@ -54,3 +54,4 @@ export * from './use-is-truncated';
54
54
  export * from './use-toast';
55
55
  export * from './use-toast-logic';
56
56
  export * from './use-banner-telemetry';
57
+ export * from './use-tooltip-fallback-placement';
@@ -0,0 +1,73 @@
1
+ import { useLayoutEffect, useRef, useState } from 'react';
2
+
3
+ import type { RefObject } from 'react';
4
+ import type { TooltipPlacement, TooltipProps } from '@redocly/theme/core/types';
5
+
6
+ type TooltipFallbackPlacementParams = {
7
+ isOpened: boolean;
8
+ placement: TooltipPlacement;
9
+ arrowPosition: TooltipProps['arrowPosition'];
10
+ fallbackPlacements: TooltipPlacement[] | undefined;
11
+ tooltipBodyRef: RefObject<HTMLElement | null>;
12
+ };
13
+
14
+ type TooltipFallbackPlacementResult = {
15
+ activePlacement: TooltipPlacement;
16
+ activeArrowPosition: TooltipProps['arrowPosition'];
17
+ };
18
+
19
+ export function useTooltipFallbackPlacement({
20
+ isOpened,
21
+ placement,
22
+ arrowPosition,
23
+ fallbackPlacements,
24
+ tooltipBodyRef,
25
+ }: TooltipFallbackPlacementParams): TooltipFallbackPlacementResult {
26
+ const [activePlacement, setActivePlacement] = useState<TooltipPlacement>(placement);
27
+ const wasOpenRef = useRef(false);
28
+ const candidateIndexRef = useRef(0);
29
+
30
+ useLayoutEffect(() => {
31
+ if (!isOpened) {
32
+ wasOpenRef.current = false;
33
+ candidateIndexRef.current = 0;
34
+ return;
35
+ }
36
+
37
+ if (!wasOpenRef.current) {
38
+ wasOpenRef.current = true;
39
+ candidateIndexRef.current = 0;
40
+ if (activePlacement !== placement) {
41
+ setActivePlacement(placement);
42
+ return;
43
+ }
44
+ }
45
+
46
+ if (!tooltipBodyRef.current || !fallbackPlacements?.length) return;
47
+
48
+ const candidates: TooltipPlacement[] = [placement, ...fallbackPlacements];
49
+
50
+ if (candidateIndexRef.current >= candidates.length) return;
51
+
52
+ const rect = tooltipBodyRef.current.getBoundingClientRect();
53
+ const overflows =
54
+ rect.left < 0 ||
55
+ rect.top < 0 ||
56
+ rect.right > window.innerWidth ||
57
+ rect.bottom > window.innerHeight;
58
+
59
+ if (!overflows) return;
60
+
61
+ candidateIndexRef.current++;
62
+
63
+ if (candidateIndexRef.current < candidates.length) {
64
+ setActivePlacement(candidates[candidateIndexRef.current]);
65
+ } else if (activePlacement !== placement) {
66
+ setActivePlacement(placement);
67
+ }
68
+ }, [isOpened, activePlacement, placement, fallbackPlacements, tooltipBodyRef]);
69
+
70
+ const activeArrowPosition = activePlacement === placement ? arrowPosition : 'center';
71
+
72
+ return { activePlacement, activeArrowPosition };
73
+ }
@@ -1253,6 +1253,7 @@ const replay = css`
1253
1253
  --replay-runtime-expression-color: rgba(54, 90, 249, 1); // @presenter Color
1254
1254
  --replay-runtime-expression-bg-color: rgba(54, 90, 249, 0.08); // @presenter Color
1255
1255
  --replay-operators-color: rgba(193, 142, 31, 1); // @presenter Color
1256
+ --replay-claude-icon-color: rgba(217, 119, 87, 1); // @presenter Color
1256
1257
 
1257
1258
  --replay-ai-gradient-soft: linear-gradient(62.6deg, rgba(113, 94, 254, 0.16) 0%, rgba(255, 92, 220, 0.16) 100%);
1258
1259
  --replay-ai-gradient-disabled: linear-gradient(62.6deg, rgba(113, 94, 254, 0.6) 0%, rgba(255, 92, 220, 0.6) 100%);
@@ -18,6 +18,15 @@ export type TranslationKey =
18
18
  | 'dev.edit.description.dialog.title'
19
19
  | 'dev.edit.description.dialog.save'
20
20
  | 'dev.edit.description.dialog.cancel'
21
+ | 'dev.create.app.dialog.callbackUrls'
22
+ | 'dev.create.app.dialog.callbackUrls.placeholder'
23
+ | 'dev.create.app.dialog.callbackUrls.hint'
24
+ | 'dev.app.callbackUrls.title'
25
+ | 'dev.edit.callbackUrls.dialog.title'
26
+ | 'dev.edit.callbackUrls.dialog.placeholder'
27
+ | 'dev.edit.callbackUrls.dialog.hint'
28
+ | 'dev.edit.callbackUrls.dialog.save'
29
+ | 'dev.edit.callbackUrls.dialog.cancel'
21
30
  | 'dev.edit.apis.dialog.selectedAPIs'
22
31
  | 'dev.app.key.create'
23
32
  | 'dev.create.key.dialog.title'
@@ -1,11 +1,14 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
+ export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
4
+
3
5
  export type TooltipProps = {
4
6
  children?: ReactNode;
5
7
  tip: string | ReactNode;
6
8
  isOpen?: boolean;
7
9
  withArrow?: boolean;
8
- placement?: 'top' | 'bottom' | 'left' | 'right';
10
+ placement?: TooltipPlacement;
11
+ fallbackPlacements?: TooltipPlacement[];
9
12
  className?: string;
10
13
  width?: string;
11
14
  dataTestId?: string;
@@ -46,3 +46,4 @@ export * from './build-revision-url';
46
46
  export * from './content-segments';
47
47
  export * from './custom-catalog-options-casing';
48
48
  export * from './get-auto-dismiss-duration';
49
+ export * from './tooltip-placement';
@@ -0,0 +1,141 @@
1
+ import type { TooltipPlacement, TooltipProps } from '@redocly/theme/core/types';
2
+
3
+ const PLACEMENT_MARGIN = 10;
4
+ const COUNTER_CLOCKWISE: TooltipPlacement[] = ['top', 'left', 'bottom', 'right'];
5
+
6
+ export function getDefaultFallbackPlacements(placement: TooltipPlacement): TooltipPlacement[] {
7
+ const index = COUNTER_CLOCKWISE.indexOf(placement);
8
+ const result: TooltipPlacement[] = [];
9
+ for (let i = 1; i < COUNTER_CLOCKWISE.length; i++) {
10
+ result.push(COUNTER_CLOCKWISE[(index + i) % COUNTER_CLOCKWISE.length]);
11
+ }
12
+ return result;
13
+ }
14
+
15
+ export function calcAnchorPoint(
16
+ triggerRect: DOMRect,
17
+ placement: TooltipPlacement,
18
+ arrowPosition: TooltipProps['arrowPosition'],
19
+ ): { top: number; left: number } {
20
+ const horizontalLeft = (): number =>
21
+ arrowPosition === 'left'
22
+ ? triggerRect.left - 24
23
+ : arrowPosition === 'right'
24
+ ? triggerRect.right + 24
25
+ : triggerRect.left + triggerRect.width / 2;
26
+
27
+ const verticalTop = (): number => triggerRect.top + triggerRect.height / 2;
28
+
29
+ switch (placement) {
30
+ case 'top':
31
+ return { top: triggerRect.top, left: horizontalLeft() };
32
+ case 'bottom':
33
+ return { top: triggerRect.bottom, left: horizontalLeft() };
34
+ case 'left':
35
+ return { top: verticalTop(), left: triggerRect.left };
36
+ case 'right':
37
+ return { top: verticalTop(), left: triggerRect.right };
38
+ }
39
+ }
40
+
41
+ type FitsInViewportParams = {
42
+ anchor: { top: number; left: number };
43
+ tooltipWidth: number;
44
+ tooltipHeight: number;
45
+ placement: TooltipPlacement;
46
+ arrowPosition: TooltipProps['arrowPosition'];
47
+ };
48
+
49
+ export function fitsInViewport({
50
+ anchor,
51
+ tooltipWidth,
52
+ tooltipHeight,
53
+ placement,
54
+ arrowPosition,
55
+ }: FitsInViewportParams): boolean {
56
+ const horizontalLeft = (): number =>
57
+ arrowPosition === 'left'
58
+ ? anchor.left
59
+ : arrowPosition === 'right'
60
+ ? anchor.left - tooltipWidth
61
+ : anchor.left - tooltipWidth / 2;
62
+
63
+ const verticalTop = (): number => anchor.top - tooltipHeight / 2;
64
+
65
+ let top: number;
66
+ let left: number;
67
+
68
+ switch (placement) {
69
+ case 'top':
70
+ top = anchor.top - tooltipHeight - PLACEMENT_MARGIN;
71
+ left = horizontalLeft();
72
+ break;
73
+ case 'bottom':
74
+ top = anchor.top + PLACEMENT_MARGIN;
75
+ left = horizontalLeft();
76
+ break;
77
+ case 'left':
78
+ top = verticalTop();
79
+ left = anchor.left - tooltipWidth - PLACEMENT_MARGIN;
80
+ break;
81
+ case 'right':
82
+ top = verticalTop();
83
+ left = anchor.left + PLACEMENT_MARGIN;
84
+ break;
85
+ }
86
+
87
+ return (
88
+ top >= 0 &&
89
+ left >= 0 &&
90
+ left + tooltipWidth <= window.innerWidth &&
91
+ top + tooltipHeight <= window.innerHeight
92
+ );
93
+ }
94
+
95
+ type ResolvePlacementParams = {
96
+ triggerRect: DOMRect;
97
+ tooltipWidth: number;
98
+ tooltipHeight: number;
99
+ placement: TooltipPlacement;
100
+ arrowPosition: TooltipProps['arrowPosition'];
101
+ fallbackPlacements: TooltipPlacement[] | undefined;
102
+ };
103
+
104
+ /**
105
+ * Given the trigger rect, tooltip dimensions, primary placement/arrow, and
106
+ * fallback list, returns the first placement that keeps the tooltip fully
107
+ * inside the viewport. Falls back to the primary when nothing fits.
108
+ */
109
+ export function resolvePlacement({
110
+ triggerRect,
111
+ tooltipWidth,
112
+ tooltipHeight,
113
+ placement,
114
+ arrowPosition,
115
+ fallbackPlacements,
116
+ }: ResolvePlacementParams): TooltipPlacement {
117
+ if (!fallbackPlacements?.length || tooltipWidth === 0 || tooltipHeight === 0) {
118
+ return placement;
119
+ }
120
+
121
+ const candidates: TooltipPlacement[] = [placement, ...fallbackPlacements];
122
+
123
+ for (const candidate of candidates) {
124
+ const candidateArrow = candidate === placement ? arrowPosition : 'center';
125
+ const pos = calcAnchorPoint(triggerRect, candidate, candidateArrow);
126
+
127
+ if (
128
+ fitsInViewport({
129
+ anchor: pos,
130
+ tooltipWidth,
131
+ tooltipHeight,
132
+ placement: candidate,
133
+ arrowPosition: candidateArrow,
134
+ })
135
+ ) {
136
+ return candidate;
137
+ }
138
+ }
139
+
140
+ return placement;
141
+ }