Bug/v2/fix rtl (#4958)

This commit is contained in:
Reece Browne
2025-11-24 16:57:36 +00:00
committed by GitHub
parent 5d18184e46
commit 861e4394df
20 changed files with 225 additions and 54 deletions
@@ -18,7 +18,7 @@ export function useFileEditorRightRailButtons({
onDeselectAll, onDeselectAll,
onCloseSelected, onCloseSelected,
}: FileEditorRightRailButtonsParams) { }: FileEditorRightRailButtonsParams) {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const buttons = useMemo<RightRailButtonWithAction[]>(() => [ const buttons = useMemo<RightRailButtonWithAction[]>(() => [
{ {
@@ -54,7 +54,7 @@ export function useFileEditorRightRailButtons({
visible: totalItems > 0, visible: totalItems > 0,
onClick: onCloseSelected, onClick: onCloseSelected,
}, },
], [t, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected]); ], [t, i18n.language, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected]);
useRightRailButtons(buttons); useRightRailButtons(buttons);
} }
@@ -31,3 +31,15 @@
inset 0 0 30px rgba(59, 130, 246, 0.2); 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;
}
@@ -7,6 +7,7 @@ import { useFilesModalContext } from '@app/contexts/FilesModalContext';
import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext'; import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext';
import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext'; import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import TourWelcomeModal from '@app/components/onboarding/TourWelcomeModal'; import TourWelcomeModal from '@app/components/onboarding/TourWelcomeModal';
import '@app/components/onboarding/OnboardingTour.css'; import '@app/components/onboarding/OnboardingTour.css';
@@ -70,6 +71,7 @@ export default function OnboardingTour() {
const { t } = useTranslation(); const { t } = useTranslation();
const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour, tourType, isOpen } = useOnboarding(); const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour, tourType, isOpen } = useOnboarding();
const { openFilesModal, closeFilesModal } = useFilesModalContext(); const { openFilesModal, closeFilesModal } = useFilesModalContext();
const isRTL = typeof document !== 'undefined' ? document.documentElement.dir === 'rtl' : false;
// Helper to add glow to multiple elements // Helper to add glow to multiple elements
const addGlowToElements = (selectors: string[]) => { const addGlowToElements = (selectors: string[]) => {
@@ -438,6 +440,7 @@ export default function OnboardingTour() {
handleCloseTour(clickProps); handleCloseTour(clickProps);
} }
}} }}
rtl={isRTL}
styles={{ styles={{
popover: (base) => ({ popover: (base) => ({
...base, ...base,
@@ -466,21 +469,19 @@ export default function OnboardingTour() {
showBadge={false} showBadge={false}
showCloseButton={true} showCloseButton={true}
disableInteraction={true} disableInteraction={true}
disableDotsNavigation={true} disableDotsNavigation={false}
prevButton={() => null} prevButton={() => null}
nextButton={({ currentStep, stepsLength, setCurrentStep, setIsOpen }) => { nextButton={({ currentStep, stepsLength, setCurrentStep, setIsOpen }) => {
const isLast = currentStep === stepsLength - 1; const isLast = currentStep === stepsLength - 1;
const ArrowIcon = isRTL ? ArrowBackIcon : ArrowForwardIcon;
return ( return (
<ActionIcon <ActionIcon
onClick={() => { onClick={() => advanceTour({ setCurrentStep, currentStep, steps, setIsOpen })}
advanceTour({ setCurrentStep, currentStep, steps, setIsOpen });
}}
variant="subtle" variant="subtle"
size="lg" size="lg"
aria-label={isLast ? t('onboarding.finish', 'Finish') : t('onboarding.next', 'Next')} aria-label={isLast ? t('onboarding.finish', 'Finish') : t('onboarding.next', 'Next')}
> >
{isLast ? <CheckIcon /> : <ArrowForwardIcon />} {isLast ? <CheckIcon /> : <ArrowIcon />}
</ActionIcon> </ActionIcon>
); );
}} }}
@@ -41,7 +41,7 @@ export function usePageEditorRightRailButtons(params: PageEditorRightRailButtons
closePdf, closePdf,
} = params; } = params;
const { t } = useTranslation(); const { t, i18n } = useTranslation();
// Lift i18n labels out of memo for clarity // Lift i18n labels out of memo for clarity
const selectAllLabel = t('rightRail.selectAll', 'Select All'); const selectAllLabel = t('rightRail.selectAll', 'Select All');
@@ -144,6 +144,7 @@ export function usePageEditorRightRailButtons(params: PageEditorRightRailButtons
]; ];
}, [ }, [
t, t,
i18n.language,
selectAllLabel, selectAllLabel,
deselectAllLabel, deselectAllLabel,
selectByNumberLabel, selectByNumberLabel,
@@ -10,9 +10,14 @@ import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
interface AllToolsNavButtonProps { interface AllToolsNavButtonProps {
activeButton: string; activeButton: string;
setActiveButton: (id: string) => void; setActiveButton: (id: string) => void;
tooltipPosition?: 'left' | 'right' | 'top' | 'bottom';
} }
const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({ activeButton, setActiveButton }) => { const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({
activeButton,
setActiveButton,
tooltipPosition = 'right'
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow(); const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow();
const { getHomeNavigation } = useSidebarNavigation(); const { getHomeNavigation } = useSidebarNavigation();
@@ -40,7 +45,13 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({ activeButton, set
); );
return ( return (
<Tooltip content={t("quickAccess.allTools", "Tools")} position="right" arrow containerStyle={{ marginTop: "-1rem" }} maxWidth={200}> <Tooltip
content={t("quickAccess.allTools", "Tools")}
position={tooltipPosition}
arrow
containerStyle={{ marginTop: "-1rem" }}
maxWidth={200}
>
<div className="flex flex-col items-center gap-1 mt-4 mb-2"> <div className="flex flex-col items-center gap-1 mt-4 mb-2">
<ActionIcon <ActionIcon
component="a" component="a"
@@ -70,4 +81,3 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({ activeButton, set
export default AllToolsNavButton; export default AllToolsNavButton;
@@ -185,6 +185,11 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-
// Clear ripple effect // Clear ripple effect
setTimeout(() => setRippleEffect(null), 100); 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); }, 300);
}, 200); }, 200);
}; };
@@ -39,6 +39,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
const scrollableRef = useRef<HTMLDivElement>(null); const scrollableRef = useRef<HTMLDivElement>(null);
const isOverflow = useIsOverflowing(scrollableRef); const isOverflow = useIsOverflowing(scrollableRef);
const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl';
// Open modal if URL is at /settings/* // Open modal if URL is at /settings/*
useEffect(() => { useEffect(() => {
const isSettings = location.pathname.startsWith('/settings'); const isSettings = location.pathname.startsWith('/settings');
@@ -189,9 +191,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
ref={ref} ref={ref}
data-sidebar="quick-access" data-sidebar="quick-access"
className={`h-screen flex flex-col w-16 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`} 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 */} {/* Fixed header outside scrollable area */}
<div className="quick-access-header"> <div className="quick-access-header">
@@ -278,7 +277,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
// If admin, show menu with both options // If admin, show menu with both options
return ( return (
<div key={buttonConfig.id} data-tour="help-button"> <div key={buttonConfig.id} data-tour="help-button">
<Menu position="right" offset={10} zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}> <Menu position={isRTL ? 'left' : 'right'} offset={10} zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}>
<Menu.Target> <Menu.Target>
<div>{renderNavButton(buttonConfig, index)}</div> <div>{renderNavButton(buttonConfig, index)}</div>
</Menu.Target> </Menu.Target>
@@ -19,19 +19,22 @@ import LightModeIcon from '@mui/icons-material/LightMode';
import { useSidebarContext } from '@app/contexts/SidebarContext'; import { useSidebarContext } from '@app/contexts/SidebarContext';
import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail'; import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom']; const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom'];
function renderWithTooltip( function renderWithTooltip(
node: React.ReactNode, node: React.ReactNode,
tooltip: React.ReactNode | undefined tooltip: React.ReactNode | undefined,
position: 'left' | 'right',
offset: number
) { ) {
if (!tooltip) return node; if (!tooltip) return node;
const portalTarget = typeof document !== 'undefined' ? document.body : undefined; const portalTarget = typeof document !== 'undefined' ? document.body : undefined;
return ( return (
<Tooltip content={tooltip} position="left" offset={12} arrow portalTarget={portalTarget}> <Tooltip content={tooltip} position={position} offset={offset} arrow portalTarget={portalTarget}>
<div className="right-rail-tooltip-wrapper">{node}</div> <div className="right-rail-tooltip-wrapper">{node}</div>
</Tooltip> </Tooltip>
); );
@@ -39,6 +42,7 @@ function renderWithTooltip(
export default function RightRail() { export default function RightRail() {
const { sidebarRefs } = useSidebarContext(); const { sidebarRefs } = useSidebarContext();
const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs);
const { t } = useTranslation(); const { t } = useTranslation();
const viewerContext = React.useContext(ViewerContext); const viewerContext = React.useContext(ViewerContext);
const { toggleTheme, themeMode } = useRainbowThemeContext(); const { toggleTheme, themeMode } = useRainbowThemeContext();
@@ -117,9 +121,9 @@ export default function RightRail() {
</ActionIcon> </ActionIcon>
); );
return renderWithTooltip(buttonNode, btn.tooltip); return renderWithTooltip(buttonNode, btn.tooltip, tooltipPosition, tooltipOffset);
}, },
[actions, allButtonsDisabled, disableForFullscreen] [actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset]
); );
const handleExportAll = useCallback(async () => { const handleExportAll = useCallback(async () => {
@@ -203,14 +207,18 @@ export default function RightRail() {
<DarkModeIcon sx={{ fontSize: '1.5rem' }} /> <DarkModeIcon sx={{ fontSize: '1.5rem' }} />
)} )}
</ActionIcon>, </ActionIcon>,
t('rightRail.toggleTheme', 'Toggle Theme') t('rightRail.toggleTheme', 'Toggle Theme'),
tooltipPosition,
tooltipOffset
)} )}
{renderWithTooltip( {renderWithTooltip(
<div style={{ display: 'inline-flex' }}> <div style={{ display: 'inline-flex' }}>
<LanguageSelector position="left-start" offset={6} compact /> <LanguageSelector position="left-start" offset={6} compact />
</div>, </div>,
t('rightRail.language', 'Language') t('rightRail.language', 'Language'),
tooltipPosition,
tooltipOffset
)} )}
{renderWithTooltip( {renderWithTooltip(
@@ -226,7 +234,9 @@ export default function RightRail() {
> >
<LocalIcon icon="download" width="1.5rem" height="1.5rem" /> <LocalIcon icon="download" width="1.5rem" height="1.5rem" />
</ActionIcon>, </ActionIcon>,
downloadTooltip downloadTooltip,
tooltipPosition,
tooltipOffset
)} )}
</div> </div>
@@ -37,7 +37,7 @@ export interface TooltipProps {
export const Tooltip: React.FC<TooltipProps> = ({ export const Tooltip: React.FC<TooltipProps> = ({
sidebarTooltip = false, sidebarTooltip = false,
position = 'right', position,
content, content,
tips, tips,
children, children,
@@ -81,6 +81,16 @@ export const Tooltip: React.FC<TooltipProps> = ({
const isControlled = controlledOpen !== undefined; const isControlled = controlledOpen !== undefined;
const open = (isControlled ? !!controlledOpen : internalOpen) && !disabled; const open = (isControlled ? !!controlledOpen : internalOpen) && !disabled;
const resolvedPosition: NonNullable<TooltipProps['position']> = useMemo(() => {
const htmlDir = typeof document !== 'undefined' ? document.documentElement.dir : 'ltr';
const isRTL = htmlDir === 'rtl';
const base = position ?? 'right';
if (!isRTL) return base as NonNullable<TooltipProps['position']>;
if (base === 'left') return 'right';
if (base === 'right') return 'left';
return base as NonNullable<TooltipProps['position']>;
}, [position]);
const setOpen = useCallback( const setOpen = useCallback(
(newOpen: boolean) => { (newOpen: boolean) => {
if (newOpen === open) return; // avoid churn if (newOpen === open) return; // avoid churn
@@ -94,7 +104,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
const { coords, positionReady } = useTooltipPosition({ const { coords, positionReady } = useTooltipPosition({
open, open,
sidebarTooltip, sidebarTooltip,
position, position: resolvedPosition,
gap, gap,
triggerRef, triggerRef,
tooltipRef, tooltipRef,
@@ -145,8 +155,8 @@ export const Tooltip: React.FC<TooltipProps> = ({
left: 'tooltip-arrow-left', left: 'tooltip-arrow-left',
right: 'tooltip-arrow-right', right: 'tooltip-arrow-right',
}; };
return map[position] || map.right; return map[resolvedPosition] || map.right;
}, [position, sidebarTooltip]); }, [resolvedPosition, sidebarTooltip]);
const getArrowStyleClass = useCallback( const getArrowStyleClass = useCallback(
(key: string) => (key: string) =>
@@ -332,7 +342,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
className={`${styles['tooltip-arrow']} ${getArrowStyleClass(arrowClass!)}`} className={`${styles['tooltip-arrow']} ${getArrowStyleClass(arrowClass!)}`}
style={ style={
coords.arrowOffset !== null coords.arrowOffset !== null
? { [position === 'top' || position === 'bottom' ? 'left' : 'top']: coords.arrowOffset } ? { [resolvedPosition === 'top' || resolvedPosition === 'bottom' ? 'left' : 'top']: coords.arrowOffset }
: undefined : undefined
} }
/> />
@@ -24,11 +24,12 @@ import { Tooltip } from '@app/components/shared/Tooltip';
interface ActiveToolButtonProps { interface ActiveToolButtonProps {
activeButton: string; activeButton: string;
setActiveButton: (id: string) => void; setActiveButton: (id: string) => void;
tooltipPosition?: 'left' | 'right' | 'top' | 'bottom';
} }
const NAV_IDS = ['read', 'sign', 'automate']; const NAV_IDS = ['read', 'sign', 'automate'];
const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton }) => { const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton, tooltipPosition = 'right' }) => {
const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow(); const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow();
const { getHomeNavigation } = useSidebarNavigation(); const { getHomeNavigation } = useSidebarNavigation();
@@ -139,7 +140,12 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
{indicatorTool && ( {indicatorTool && (
<div className="current-tool-content"> <div className="current-tool-content">
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<Tooltip content={isBackHover ? 'Back to all tools' : indicatorTool.name} position="right" arrow maxWidth={140}> <Tooltip
content={isBackHover ? 'Back to all tools' : indicatorTool.name}
position={tooltipPosition}
arrow
maxWidth={140}
>
<ActionIcon <ActionIcon
component="a" component="a"
href={getHomeNavigation().href} href={getHomeNavigation().href}
@@ -189,4 +195,3 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
export default ActiveToolButton; export default ActiveToolButton;
@@ -43,6 +43,10 @@
max-width: 4rem; max-width: 4rem;
position: relative; position: relative;
z-index: 10; 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 */ /* Rainbow mode container */
@@ -53,6 +57,17 @@
max-width: 4rem; max-width: 4rem;
position: relative; position: relative;
z-index: 10; 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 */ /* Header padding */
@@ -11,6 +11,8 @@ import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils';
import { createProcessedFile } from '@app/contexts/file/fileActions'; import { createProcessedFile } from '@app/contexts/file/fileActions';
import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext'; import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext';
import { useNavigationState } from '@app/contexts/NavigationContext'; import { useNavigationState } from '@app/contexts/NavigationContext';
import { useSidebarContext } from '@app/contexts/SidebarContext';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
interface ViewerAnnotationControlsProps { interface ViewerAnnotationControlsProps {
currentView: string; currentView: string;
@@ -19,6 +21,8 @@ interface ViewerAnnotationControlsProps {
export default function ViewerAnnotationControls({ currentView, disabled = false }: ViewerAnnotationControlsProps) { export default function ViewerAnnotationControls({ currentView, disabled = false }: ViewerAnnotationControlsProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { sidebarRefs } = useSidebarContext();
const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs);
const [selectedColor, setSelectedColor] = useState('#000000'); const [selectedColor, setSelectedColor] = useState('#000000');
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false); const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false);
@@ -53,7 +57,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
return ( return (
<> <>
{/* Annotation Visibility Toggle */} {/* Annotation Visibility Toggle */}
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position="left" offset={12} arrow portalTarget={document.body}> <Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
radius="md" radius="md"
@@ -130,7 +134,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
</div> </div>
) : ( ) : (
// When inactive: Show "Draw" tooltip // When inactive: Show "Draw" tooltip
<Tooltip content={t('rightRail.draw', 'Draw')} position="left" offset={12} arrow portalTarget={document.body}> <Tooltip content={t('rightRail.draw', 'Draw')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
radius="md" radius="md"
@@ -156,7 +160,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false
)} )}
{/* Save PDF with Annotations */} {/* Save PDF with Annotations */}
<Tooltip content={t('rightRail.save', 'Save')} position="left" offset={12} arrow portalTarget={document.body}> <Tooltip content={t('rightRail.save', 'Save')} position={tooltipPosition} offset={tooltipOffset} arrow portalTarget={document.body}>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
radius="md" radius="md"
@@ -47,6 +47,7 @@ const FullscreenToolSurface = ({
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const [isExiting, setIsExiting] = useState(false); const [isExiting, setIsExiting] = useState(false);
const surfaceRef = useRef<HTMLDivElement>(null); const surfaceRef = useRef<HTMLDivElement>(null);
const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl';
// Enable focus trap when surface is active // Enable focus trap when surface is active
useFocusTrap(surfaceRef, !isExiting); useFocusTrap(surfaceRef, !isExiting);
@@ -114,7 +115,10 @@ const FullscreenToolSurface = ({
aria-label={toggleLabel} aria-label={toggleLabel}
style={{ color: 'var(--right-rail-icon)' }} style={{ color: 'var(--right-rail-icon)' }}
> >
<DoubleArrowIcon fontSize="small" style={{ transform: 'rotate(180deg)' }} /> <DoubleArrowIcon
fontSize="small"
style={{ transform: isRTL ? undefined : 'rotate(180deg)' }}
/>
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</div> </div>
@@ -156,4 +160,3 @@ const FullscreenToolSurface = ({
export default FullscreenToolSurface; export default FullscreenToolSurface;
@@ -111,6 +111,14 @@
animation: tool-panel-fullscreen-slide-out var(--fullscreen-anim-out-duration) ease forwards; 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 { .tool-panel__fullscreen-header {
display: flex; display: flex;
justify-content: space-between; 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) { @media (prefers-reduced-motion: reduce) {
.tool-panel__fullscreen-surface-inner { .tool-panel__fullscreen-surface-inner {
animation: none !important; animation: none !important;
@@ -543,4 +573,3 @@
} }
@@ -50,6 +50,7 @@ export default function ToolPanel() {
const isFullscreenMode = toolPanelMode === 'fullscreen'; const isFullscreenMode = toolPanelMode === 'fullscreen';
const toolPickerVisible = !readerMode; const toolPickerVisible = !readerMode;
const fullscreenExpanded = isFullscreenMode && leftPanelView === 'toolPicker' && !isMobile && toolPickerVisible; 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 // Disable right rail buttons when fullscreen mode is active
@@ -150,7 +151,10 @@ export default function ToolPanel() {
aria-label={toggleLabel} aria-label={toggleLabel}
className="tool-panel__mode-toggle" className="tool-panel__mode-toggle"
> >
<DoubleArrowIcon fontSize="small" /> <DoubleArrowIcon
fontSize="small"
style={{ transform: isRTL ? 'scaleX(-1)' : undefined }}
/>
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}
@@ -50,7 +50,7 @@ export const useCompareRightRailButtons = ({
baseScrollRef, baseScrollRef,
comparisonScrollRef, comparisonScrollRef,
}: UseCompareRightRailButtonsOptions): RightRailButtonWithAction[] => { }: UseCompareRightRailButtonsOptions): RightRailButtonWithAction[] => {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return useMemo<RightRailButtonWithAction[]>(() => [ return useMemo<RightRailButtonWithAction[]>(() => [
@@ -184,6 +184,7 @@ export const useCompareRightRailButtons = ({
setIsScrollLinked, setIsScrollLinked,
zoomLimits, zoomLimits,
t, t,
i18n.language,
isMobile, isMobile,
]); ]);
}; };
@@ -7,11 +7,15 @@ import LocalIcon from '@app/components/shared/LocalIcon';
import { Tooltip } from '@app/components/shared/Tooltip'; import { Tooltip } from '@app/components/shared/Tooltip';
import { SearchInterface } from '@app/components/viewer/SearchInterface'; import { SearchInterface } from '@app/components/viewer/SearchInterface';
import ViewerAnnotationControls from '@app/components/shared/rightRail/ViewerAnnotationControls'; import ViewerAnnotationControls from '@app/components/shared/rightRail/ViewerAnnotationControls';
import { useSidebarContext } from '@app/contexts/SidebarContext';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
export function useViewerRightRailButtons() { export function useViewerRightRailButtons() {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const viewer = useViewer(); const viewer = useViewer();
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false); const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
const { sidebarRefs } = useSidebarContext();
const { position: tooltipPosition } = useRightRailTooltipSide(sidebarRefs, 12);
// Lift i18n labels out of memo for clarity // Lift i18n labels out of memo for clarity
const searchLabel = t('rightRail.search', 'Search PDF'); const searchLabel = t('rightRail.search', 'Search PDF');
@@ -30,8 +34,8 @@ export function useViewerRightRailButtons() {
section: 'top' as const, section: 'top' as const,
order: 10, order: 10,
render: ({ disabled }) => ( render: ({ disabled }) => (
<Tooltip content={searchLabel} position="left" offset={12} arrow portalTarget={document.body}> <Tooltip content={searchLabel} position={tooltipPosition} offset={12} arrow portalTarget={document.body}>
<Popover position="left" withArrow shadow="md" offset={8}> <Popover position={tooltipPosition} withArrow shadow="md" offset={8}>
<Popover.Target> <Popover.Target>
<div style={{ display: 'inline-flex' }}> <div style={{ display: 'inline-flex' }}>
<ActionIcon <ActionIcon
@@ -61,7 +65,7 @@ export function useViewerRightRailButtons() {
section: 'top' as const, section: 'top' as const,
order: 20, order: 20,
render: ({ disabled }) => ( render: ({ disabled }) => (
<Tooltip content={panLabel} position="left" offset={12} arrow portalTarget={document.body}> <Tooltip content={panLabel} position={tooltipPosition} offset={12} arrow portalTarget={document.body}>
<ActionIcon <ActionIcon
variant={isPanning ? 'default' : 'subtle'} variant={isPanning ? 'default' : 'subtle'}
color={undefined} color={undefined}
@@ -132,7 +136,7 @@ export function useViewerRightRailButtons() {
) )
} }
]; ];
}, [t, viewer, isPanning, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel, bookmarkLabel]); }, [t, i18n.language, viewer, isPanning, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel, bookmarkLabel, tooltipPosition]);
useRightRailButtons(viewerButtons); useRightRailButtons(viewerButtons);
} }
@@ -42,11 +42,26 @@ export function useToolPanelGeometry({
const computeAndSetGeometry = () => { const computeAndSetGeometry = () => {
const rect = panelEl.getBoundingClientRect(); const rect = panelEl.getBoundingClientRect();
const rail = rightRailEl(); const rail = rightRailEl();
const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl';
const width = Math.max(360, window.innerWidth - rect.left - rightOffset); 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); const height = Math.max(rect.height, window.innerHeight - rect.top);
setGeometry({ setGeometry({
left: rect.left, left,
top: rect.top, top: rect.top,
width, width,
height, height,
@@ -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 };
}
+15 -5
View File
@@ -74,6 +74,7 @@ export function useTooltipPosition({
// Fallback sidebar position (only used as last resort) // Fallback sidebar position (only used as last resort)
const sidebarLeft = 240; const sidebarLeft = 240;
const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl';
const updatePosition = () => { const updatePosition = () => {
if (!triggerRef.current || !open) return; if (!triggerRef.current || !open) return;
@@ -93,7 +94,11 @@ export function useTooltipPosition({
} }
const sidebarInfo = getSidebarInfo(sidebarRefs, sidebarState); 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 // Only show tooltip if we have the tool panel active
if (!sidebarInfo.isToolPanelActive) { if (!sidebarInfo.isToolPanelActive) {
@@ -102,13 +107,18 @@ export function useTooltipPosition({
return; return;
} }
// Position to the right of active sidebar with 20px gap const tooltipRect = tooltipRef.current?.getBoundingClientRect() || null;
left = currentSidebarRight + 20;
// 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 top = triggerRect.top; // Align top of tooltip with trigger element
// Only clamp if we have tooltip dimensions // Only clamp if we have tooltip dimensions
if (tooltipRef.current) { if (tooltipRect) {
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const maxTop = window.innerHeight - tooltipRect.height - 4; const maxTop = window.innerHeight - tooltipRect.height - 4;
const originalTop = top; const originalTop = top;
top = clamp(top, 4, maxTop); top = clamp(top, 4, maxTop);