diff --git a/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx b/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx index 1895371da..122023af6 100644 --- a/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx +++ b/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx @@ -18,7 +18,7 @@ export function useFileEditorRightRailButtons({ onDeselectAll, onCloseSelected, }: FileEditorRightRailButtonsParams) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const buttons = useMemo(() => [ { @@ -54,7 +54,7 @@ export function useFileEditorRightRailButtons({ visible: totalItems > 0, onClick: onCloseSelected, }, - ], [t, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected]); + ], [t, i18n.language, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected]); useRightRailButtons(buttons); } diff --git a/frontend/src/core/components/onboarding/OnboardingTour.css b/frontend/src/core/components/onboarding/OnboardingTour.css index 9667e835a..54ad69d68 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.css +++ b/frontend/src/core/components/onboarding/OnboardingTour.css @@ -31,3 +31,15 @@ inset 0 0 30px rgba(59, 130, 246, 0.2); } } + +/* RTL: mirror step indicator and controls in Reactour popovers */ +:root[dir='rtl'] .reactour__popover { + direction: rtl; +} + +/* Minimal overrides retained for glow only */ + +:root[dir='rtl'] .reactour__badge { + left: auto; + right: 16px; +} diff --git a/frontend/src/core/components/onboarding/OnboardingTour.tsx b/frontend/src/core/components/onboarding/OnboardingTour.tsx index 2bec9cd4f..e06fa41d5 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.tsx +++ b/frontend/src/core/components/onboarding/OnboardingTour.tsx @@ -7,6 +7,7 @@ import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext'; import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import CheckIcon from '@mui/icons-material/Check'; import TourWelcomeModal from '@app/components/onboarding/TourWelcomeModal'; import '@app/components/onboarding/OnboardingTour.css'; @@ -70,6 +71,7 @@ export default function OnboardingTour() { const { t } = useTranslation(); const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour, tourType, isOpen } = useOnboarding(); const { openFilesModal, closeFilesModal } = useFilesModalContext(); + const isRTL = typeof document !== 'undefined' ? document.documentElement.dir === 'rtl' : false; // Helper to add glow to multiple elements const addGlowToElements = (selectors: string[]) => { @@ -438,6 +440,7 @@ export default function OnboardingTour() { handleCloseTour(clickProps); } }} + rtl={isRTL} styles={{ popover: (base) => ({ ...base, @@ -466,21 +469,19 @@ export default function OnboardingTour() { showBadge={false} showCloseButton={true} disableInteraction={true} - disableDotsNavigation={true} + disableDotsNavigation={false} prevButton={() => null} nextButton={({ currentStep, stepsLength, setCurrentStep, setIsOpen }) => { const isLast = currentStep === stepsLength - 1; - + const ArrowIcon = isRTL ? ArrowBackIcon : ArrowForwardIcon; return ( { - advanceTour({ setCurrentStep, currentStep, steps, setIsOpen }); - }} + onClick={() => advanceTour({ setCurrentStep, currentStep, steps, setIsOpen })} variant="subtle" size="lg" aria-label={isLast ? t('onboarding.finish', 'Finish') : t('onboarding.next', 'Next')} > - {isLast ? : } + {isLast ? : } ); }} diff --git a/frontend/src/core/components/pageEditor/pageEditorRightRailButtons.tsx b/frontend/src/core/components/pageEditor/pageEditorRightRailButtons.tsx index 4aae339a7..3036982ac 100644 --- a/frontend/src/core/components/pageEditor/pageEditorRightRailButtons.tsx +++ b/frontend/src/core/components/pageEditor/pageEditorRightRailButtons.tsx @@ -41,7 +41,7 @@ export function usePageEditorRightRailButtons(params: PageEditorRightRailButtons closePdf, } = params; - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); // Lift i18n labels out of memo for clarity const selectAllLabel = t('rightRail.selectAll', 'Select All'); @@ -144,6 +144,7 @@ export function usePageEditorRightRailButtons(params: PageEditorRightRailButtons ]; }, [ t, + i18n.language, selectAllLabel, deselectAllLabel, selectByNumberLabel, diff --git a/frontend/src/core/components/shared/AllToolsNavButton.tsx b/frontend/src/core/components/shared/AllToolsNavButton.tsx index f5ba4a97c..cc7a8777c 100644 --- a/frontend/src/core/components/shared/AllToolsNavButton.tsx +++ b/frontend/src/core/components/shared/AllToolsNavButton.tsx @@ -10,9 +10,14 @@ import { handleUnlessSpecialClick } from '@app/utils/clickHandlers'; interface AllToolsNavButtonProps { activeButton: string; setActiveButton: (id: string) => void; + tooltipPosition?: 'left' | 'right' | 'top' | 'bottom'; } -const AllToolsNavButton: React.FC = ({ activeButton, setActiveButton }) => { +const AllToolsNavButton: React.FC = ({ + activeButton, + setActiveButton, + tooltipPosition = 'right' +}) => { const { t } = useTranslation(); const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow(); const { getHomeNavigation } = useSidebarNavigation(); @@ -40,7 +45,13 @@ const AllToolsNavButton: React.FC = ({ activeButton, set ); return ( - +
= ({ activeButton, set export default AllToolsNavButton; - diff --git a/frontend/src/core/components/shared/LanguageSelector.tsx b/frontend/src/core/components/shared/LanguageSelector.tsx index 9acaeba36..94a9ab0a4 100644 --- a/frontend/src/core/components/shared/LanguageSelector.tsx +++ b/frontend/src/core/components/shared/LanguageSelector.tsx @@ -185,6 +185,11 @@ const LanguageSelector: React.FC = ({ position = 'bottom- // Clear ripple effect setTimeout(() => setRippleEffect(null), 100); + + // Force a full reload so RTL/LTR layout and tooltips re-evaluate correctly + if (typeof window !== 'undefined') { + window.location.reload(); + } }, 300); }, 200); }; diff --git a/frontend/src/core/components/shared/QuickAccessBar.tsx b/frontend/src/core/components/shared/QuickAccessBar.tsx index f8a1d1649..af42d040d 100644 --- a/frontend/src/core/components/shared/QuickAccessBar.tsx +++ b/frontend/src/core/components/shared/QuickAccessBar.tsx @@ -39,6 +39,8 @@ const QuickAccessBar = forwardRef((_, ref) => { const scrollableRef = useRef(null); const isOverflow = useIsOverflowing(scrollableRef); + const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; + // Open modal if URL is at /settings/* useEffect(() => { const isSettings = location.pathname.startsWith('/settings'); @@ -189,9 +191,6 @@ const QuickAccessBar = forwardRef((_, ref) => { ref={ref} data-sidebar="quick-access" className={`h-screen flex flex-col w-16 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`} - style={{ - borderRight: '1px solid var(--border-default)' - }} > {/* Fixed header outside scrollable area */}
@@ -278,7 +277,7 @@ const QuickAccessBar = forwardRef((_, ref) => { // If admin, show menu with both options return (
- +
{renderNavButton(buttonConfig, index)}
diff --git a/frontend/src/core/components/shared/RightRail.tsx b/frontend/src/core/components/shared/RightRail.tsx index bec38b0fe..3cca4edef 100644 --- a/frontend/src/core/components/shared/RightRail.tsx +++ b/frontend/src/core/components/shared/RightRail.tsx @@ -19,19 +19,22 @@ import LightModeIcon from '@mui/icons-material/LightMode'; import { useSidebarContext } from '@app/contexts/SidebarContext'; import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail'; +import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom']; function renderWithTooltip( node: React.ReactNode, - tooltip: React.ReactNode | undefined + tooltip: React.ReactNode | undefined, + position: 'left' | 'right', + offset: number ) { if (!tooltip) return node; const portalTarget = typeof document !== 'undefined' ? document.body : undefined; return ( - +
{node}
); @@ -39,6 +42,7 @@ function renderWithTooltip( export default function RightRail() { const { sidebarRefs } = useSidebarContext(); + const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs); const { t } = useTranslation(); const viewerContext = React.useContext(ViewerContext); const { toggleTheme, themeMode } = useRainbowThemeContext(); @@ -117,9 +121,9 @@ export default function RightRail() { ); - return renderWithTooltip(buttonNode, btn.tooltip); + return renderWithTooltip(buttonNode, btn.tooltip, tooltipPosition, tooltipOffset); }, - [actions, allButtonsDisabled, disableForFullscreen] + [actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset] ); const handleExportAll = useCallback(async () => { @@ -203,14 +207,18 @@ export default function RightRail() { )} , - t('rightRail.toggleTheme', 'Toggle Theme') + t('rightRail.toggleTheme', 'Toggle Theme'), + tooltipPosition, + tooltipOffset )} {renderWithTooltip(
, - t('rightRail.language', 'Language') + t('rightRail.language', 'Language'), + tooltipPosition, + tooltipOffset )} {renderWithTooltip( @@ -226,7 +234,9 @@ export default function RightRail() { > , - downloadTooltip + downloadTooltip, + tooltipPosition, + tooltipOffset )}
diff --git a/frontend/src/core/components/shared/Tooltip.tsx b/frontend/src/core/components/shared/Tooltip.tsx index a2767e176..7e7ee5750 100644 --- a/frontend/src/core/components/shared/Tooltip.tsx +++ b/frontend/src/core/components/shared/Tooltip.tsx @@ -37,7 +37,7 @@ export interface TooltipProps { export const Tooltip: React.FC = ({ sidebarTooltip = false, - position = 'right', + position, content, tips, children, @@ -81,6 +81,16 @@ export const Tooltip: React.FC = ({ const isControlled = controlledOpen !== undefined; const open = (isControlled ? !!controlledOpen : internalOpen) && !disabled; + const resolvedPosition: NonNullable = useMemo(() => { + const htmlDir = typeof document !== 'undefined' ? document.documentElement.dir : 'ltr'; + const isRTL = htmlDir === 'rtl'; + const base = position ?? 'right'; + if (!isRTL) return base as NonNullable; + if (base === 'left') return 'right'; + if (base === 'right') return 'left'; + return base as NonNullable; + }, [position]); + const setOpen = useCallback( (newOpen: boolean) => { if (newOpen === open) return; // avoid churn @@ -94,7 +104,7 @@ export const Tooltip: React.FC = ({ const { coords, positionReady } = useTooltipPosition({ open, sidebarTooltip, - position, + position: resolvedPosition, gap, triggerRef, tooltipRef, @@ -145,8 +155,8 @@ export const Tooltip: React.FC = ({ left: 'tooltip-arrow-left', right: 'tooltip-arrow-right', }; - return map[position] || map.right; - }, [position, sidebarTooltip]); + return map[resolvedPosition] || map.right; + }, [resolvedPosition, sidebarTooltip]); const getArrowStyleClass = useCallback( (key: string) => @@ -332,7 +342,7 @@ export const Tooltip: React.FC = ({ className={`${styles['tooltip-arrow']} ${getArrowStyleClass(arrowClass!)}`} style={ coords.arrowOffset !== null - ? { [position === 'top' || position === 'bottom' ? 'left' : 'top']: coords.arrowOffset } + ? { [resolvedPosition === 'top' || resolvedPosition === 'bottom' ? 'left' : 'top']: coords.arrowOffset } : undefined } /> diff --git a/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx b/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx index 81ed8f43a..273c09d0e 100644 --- a/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx +++ b/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx @@ -24,11 +24,12 @@ import { Tooltip } from '@app/components/shared/Tooltip'; interface ActiveToolButtonProps { activeButton: string; setActiveButton: (id: string) => void; + tooltipPosition?: 'left' | 'right' | 'top' | 'bottom'; } const NAV_IDS = ['read', 'sign', 'automate']; -const ActiveToolButton: React.FC = ({ setActiveButton }) => { +const ActiveToolButton: React.FC = ({ setActiveButton, tooltipPosition = 'right' }) => { const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow(); const { getHomeNavigation } = useSidebarNavigation(); @@ -139,7 +140,12 @@ const ActiveToolButton: React.FC = ({ setActiveButton }) {indicatorTool && (
- + = ({ setActiveButton }) export default ActiveToolButton; - diff --git a/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css b/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css index cbea90655..2c4d3004a 100644 --- a/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css +++ b/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css @@ -43,6 +43,10 @@ max-width: 4rem; position: relative; z-index: 10; + border-right: 1px solid var(--border-default); + flex-shrink: 0; + box-sizing: border-box; + direction: ltr; /* keep layout stable when document is rtl */ } /* Rainbow mode container */ @@ -53,6 +57,17 @@ max-width: 4rem; position: relative; z-index: 10; + border-right: 1px solid var(--border-default); + flex-shrink: 0; + box-sizing: border-box; + direction: ltr; +} + +/* RTL adjustments keep the bar on-screen and separated from content */ +:root[dir='rtl'] .quick-access-bar-main, +:root[dir='rtl'] .quick-access-bar-main.rainbow-mode { + border-right: none; + border-left: 1px solid var(--border-default); } /* Header padding */ @@ -276,4 +291,4 @@ margin-top: 0.5rem; margin-bottom: 0.5rem; } -} \ No newline at end of file +} diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index ba49ffb70..cd95b90b9 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -11,6 +11,8 @@ import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils'; import { createProcessedFile } from '@app/contexts/file/fileActions'; import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext'; import { useNavigationState } from '@app/contexts/NavigationContext'; +import { useSidebarContext } from '@app/contexts/SidebarContext'; +import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; interface ViewerAnnotationControlsProps { currentView: string; @@ -19,6 +21,8 @@ interface ViewerAnnotationControlsProps { export default function ViewerAnnotationControls({ currentView, disabled = false }: ViewerAnnotationControlsProps) { const { t } = useTranslation(); + const { sidebarRefs } = useSidebarContext(); + const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs); const [selectedColor, setSelectedColor] = useState('#000000'); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false); @@ -53,7 +57,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false return ( <> {/* Annotation Visibility Toggle */} - + ) : ( // When inactive: Show "Draw" tooltip - + + ); -} \ No newline at end of file +} diff --git a/frontend/src/core/components/tools/FullscreenToolSurface.tsx b/frontend/src/core/components/tools/FullscreenToolSurface.tsx index 7ac5d82c9..beebaefe0 100644 --- a/frontend/src/core/components/tools/FullscreenToolSurface.tsx +++ b/frontend/src/core/components/tools/FullscreenToolSurface.tsx @@ -47,6 +47,7 @@ const FullscreenToolSurface = ({ const { colorScheme } = useMantineColorScheme(); const [isExiting, setIsExiting] = useState(false); const surfaceRef = useRef(null); + const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; // Enable focus trap when surface is active useFocusTrap(surfaceRef, !isExiting); @@ -114,7 +115,10 @@ const FullscreenToolSurface = ({ aria-label={toggleLabel} style={{ color: 'var(--right-rail-icon)' }} > - +
@@ -156,4 +160,3 @@ const FullscreenToolSurface = ({ export default FullscreenToolSurface; - diff --git a/frontend/src/core/components/tools/ToolPanel.css b/frontend/src/core/components/tools/ToolPanel.css index 0e6861abd..8d642214f 100644 --- a/frontend/src/core/components/tools/ToolPanel.css +++ b/frontend/src/core/components/tools/ToolPanel.css @@ -111,6 +111,14 @@ animation: tool-panel-fullscreen-slide-out var(--fullscreen-anim-out-duration) ease forwards; } +:root[dir='rtl'] .tool-panel__fullscreen-surface-inner { + animation-name: tool-panel-fullscreen-slide-in-rtl; +} + +:root[dir='rtl'] .tool-panel__fullscreen-surface-inner.tool-panel__fullscreen-surface-inner--exiting { + animation-name: tool-panel-fullscreen-slide-out-rtl; +} + .tool-panel__fullscreen-header { display: flex; justify-content: space-between; @@ -502,6 +510,28 @@ } } +@keyframes tool-panel-fullscreen-slide-in-rtl { + from { + transform: translateX(6%) scaleX(0.85); + opacity: 0; + } + to { + transform: translateX(0) scaleX(1); + opacity: 1; + } +} + +@keyframes tool-panel-fullscreen-slide-out-rtl { + from { + transform: translateX(0) scaleX(1); + opacity: 1; + } + to { + transform: translateX(6%) scaleX(0.85); + opacity: 0; + } +} + @media (prefers-reduced-motion: reduce) { .tool-panel__fullscreen-surface-inner { animation: none !important; @@ -543,4 +573,3 @@ } - diff --git a/frontend/src/core/components/tools/ToolPanel.tsx b/frontend/src/core/components/tools/ToolPanel.tsx index a61739d00..3fcac4988 100644 --- a/frontend/src/core/components/tools/ToolPanel.tsx +++ b/frontend/src/core/components/tools/ToolPanel.tsx @@ -50,6 +50,7 @@ export default function ToolPanel() { const isFullscreenMode = toolPanelMode === 'fullscreen'; const toolPickerVisible = !readerMode; const fullscreenExpanded = isFullscreenMode && leftPanelView === 'toolPicker' && !isMobile && toolPickerVisible; + const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; // Disable right rail buttons when fullscreen mode is active @@ -150,7 +151,10 @@ export default function ToolPanel() { aria-label={toggleLabel} className="tool-panel__mode-toggle" > - + )} diff --git a/frontend/src/core/components/tools/compare/hooks/useCompareRightRailButtons.tsx b/frontend/src/core/components/tools/compare/hooks/useCompareRightRailButtons.tsx index f87e43ae1..525957d95 100644 --- a/frontend/src/core/components/tools/compare/hooks/useCompareRightRailButtons.tsx +++ b/frontend/src/core/components/tools/compare/hooks/useCompareRightRailButtons.tsx @@ -50,7 +50,7 @@ export const useCompareRightRailButtons = ({ baseScrollRef, comparisonScrollRef, }: UseCompareRightRailButtonsOptions): RightRailButtonWithAction[] => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const isMobile = useIsMobile(); return useMemo(() => [ @@ -184,6 +184,7 @@ export const useCompareRightRailButtons = ({ setIsScrollLinked, zoomLimits, t, + i18n.language, isMobile, ]); }; diff --git a/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx b/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx index c853079d9..c02de9278 100644 --- a/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx +++ b/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx @@ -7,11 +7,15 @@ import LocalIcon from '@app/components/shared/LocalIcon'; import { Tooltip } from '@app/components/shared/Tooltip'; import { SearchInterface } from '@app/components/viewer/SearchInterface'; import ViewerAnnotationControls from '@app/components/shared/rightRail/ViewerAnnotationControls'; +import { useSidebarContext } from '@app/contexts/SidebarContext'; +import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; export function useViewerRightRailButtons() { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const viewer = useViewer(); const [isPanning, setIsPanning] = useState(() => viewer.getPanState()?.isPanning ?? false); + const { sidebarRefs } = useSidebarContext(); + const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12); // Lift i18n labels out of memo for clarity const searchLabel = t('rightRail.search', 'Search PDF'); @@ -30,8 +34,8 @@ export function useViewerRightRailButtons() { section: 'top' as const, order: 10, render: ({ disabled }) => ( - - + +
( - + { const rect = panelEl.getBoundingClientRect(); const rail = rightRailEl(); - const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; - const width = Math.max(360, window.innerWidth - rect.left - rightOffset); + const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; + const railRect = rail?.getBoundingClientRect(); + const railIsOnRight = railRect ? railRect.right > window.innerWidth / 2 : false; + const rightOffset = railRect && railIsOnRight ? Math.max(0, window.innerWidth - railRect.right) : 0; + let width: number; + let left: number; + + if (isRTL) { + // In RTL, QuickAccessBar is on the right, so start after it (using rect.right as the right edge) + const quickAccessRect = quickAccessRef.current?.getBoundingClientRect(); + const quickAccessWidth = quickAccessRect ? quickAccessRect.width : 0; + width = Math.max(360, window.innerWidth - quickAccessWidth - rightOffset); + left = quickAccessWidth; + } else { + width = Math.max(360, window.innerWidth - rect.left - rightOffset); + left = rect.left; + } const height = Math.max(rect.height, window.innerHeight - rect.top); setGeometry({ - left: rect.left, + left, top: rect.top, width, height, diff --git a/frontend/src/core/hooks/useRightRailTooltipSide.ts b/frontend/src/core/hooks/useRightRailTooltipSide.ts new file mode 100644 index 000000000..634d04113 --- /dev/null +++ b/frontend/src/core/hooks/useRightRailTooltipSide.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; +import { SidebarRefs } from '@app/types/sidebar'; + +export function useRightRailTooltipSide( + sidebarRefs?: SidebarRefs, + defaultOffset: number = 16 +): { position: 'left' | 'right'; offset: number } { + const [position, setPosition] = useState<'left' | 'right'>('left'); + + useEffect(() => { + const computePosition = () => { + const rail = sidebarRefs?.rightRailRef?.current; + const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; + + // Fallback to left if we can't measure + if (!rail || typeof window === 'undefined') { + setPosition(isRTL ? 'left' : 'left'); + return; + } + + const rect = rail.getBoundingClientRect(); + const center = rect.left + rect.width / 2; + const preferred = center > window.innerWidth / 2 ? 'left' : 'right'; + setPosition(isRTL ? 'left' : preferred); + }; + + computePosition(); + window.addEventListener('resize', computePosition); + return () => window.removeEventListener('resize', computePosition); + }, [sidebarRefs]); + + return { position, offset: defaultOffset }; +} diff --git a/frontend/src/core/hooks/useTooltipPosition.ts b/frontend/src/core/hooks/useTooltipPosition.ts index 2510713c3..506ce562b 100644 --- a/frontend/src/core/hooks/useTooltipPosition.ts +++ b/frontend/src/core/hooks/useTooltipPosition.ts @@ -74,6 +74,7 @@ export function useTooltipPosition({ // Fallback sidebar position (only used as last resort) const sidebarLeft = 240; + const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; const updatePosition = () => { if (!triggerRef.current || !open) return; @@ -93,7 +94,11 @@ export function useTooltipPosition({ } const sidebarInfo = getSidebarInfo(sidebarRefs, sidebarState); - const currentSidebarRight = sidebarInfo.rect ? sidebarInfo.rect.right : sidebarLeft; + const rect = sidebarInfo.rect; + if (!rect) { + setPositionReady(false); + return; + } // Only show tooltip if we have the tool panel active if (!sidebarInfo.isToolPanelActive) { @@ -102,13 +107,18 @@ export function useTooltipPosition({ return; } - // Position to the right of active sidebar with 20px gap - left = currentSidebarRight + 20; + const tooltipRect = tooltipRef.current?.getBoundingClientRect() || null; + + // Position adjacent to sidebar; mirror for RTL + if (isRTL) { + left = rect.left - (tooltipRect?.width || 0) - 20; + } else { + left = rect.right + 20; + } top = triggerRect.top; // Align top of tooltip with trigger element // Only clamp if we have tooltip dimensions - if (tooltipRef.current) { - const tooltipRect = tooltipRef.current.getBoundingClientRect(); + if (tooltipRect) { const maxTop = window.innerHeight - tooltipRect.height - 4; const originalTop = top; top = clamp(top, 4, maxTop);