diff --git a/app/components/Breadcrumb.tsx b/app/components/Breadcrumb.tsx index 8828d2eb01..a7e4c6369c 100644 --- a/app/components/Breadcrumb.tsx +++ b/app/components/Breadcrumb.tsx @@ -121,7 +121,7 @@ const Item = styled(Link)<{ $highlight: boolean; $withIcon: boolean }>` height: 24px; line-height: 24px; font-weight: ${(props) => (props.$highlight ? "500" : "inherit")}; - margin-left: ${(props) => (props.$withIcon ? "4px" : "0")}; + margin-inline-start: ${(props) => (props.$withIcon ? "4px" : "0")}; max-width: 460px; &:hover { diff --git a/app/components/Button.tsx b/app/components/Button.tsx index e1798957d2..50d0cdd5a6 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -125,7 +125,7 @@ const Label = styled.span<{ hasIcon?: boolean }>` white-space: nowrap; text-overflow: ellipsis; - ${(props) => props.hasIcon && "padding-left: 4px;"}; + ${(props) => props.hasIcon && "padding-inline-start: 4px;"}; `; export const Inner = styled.span<{ @@ -135,13 +135,13 @@ export const Inner = styled.span<{ }>` display: flex; padding: 0 8px; - padding-right: ${(props) => (props.disclosure ? 2 : 8)}px; + padding-inline-end: ${(props) => (props.disclosure ? 2 : 8)}px; line-height: ${(props) => (props.hasIcon ? 24 : 32)}px; justify-content: center; align-items: center; min-height: 32px; - ${(props) => props.hasIcon && props.hasText && "padding-left: 4px;"}; + ${(props) => props.hasIcon && props.hasText && "padding-inline-start: 4px;"}; ${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"}; `; diff --git a/app/components/CommandBar/CommandBarResults.tsx b/app/components/CommandBar/CommandBarResults.tsx index 59de8841b7..fb7e0bfce3 100644 --- a/app/components/CommandBar/CommandBarResults.tsx +++ b/app/components/CommandBar/CommandBarResults.tsx @@ -43,7 +43,8 @@ const Container = styled.div` const Header = styled(Text).attrs({ as: "h3" })` letter-spacing: 0.03em; margin: 0; - padding: 16px 0 4px 20px; + padding-block: 16px 4px; + padding-inline: 20px 0; height: 36px; cursor: default; `; diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 0d3bd56c4f..9ea5388079 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -117,7 +117,7 @@ const Breadcrumbs = styled("div")` flex-grow: 1; flex-basis: 0; align-items: center; - padding-right: 8px; + padding-inline: 0 8px; display: flex; `; @@ -125,7 +125,7 @@ const Actions = styled(Flex)` flex-grow: 1; flex-basis: 0; min-width: auto; - padding-left: 8px; + padding-inline: 8px 0; gap: 12px; ${breakpoint("tablet")` diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 34bca1c3bf..f66573ccc3 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -95,7 +95,7 @@ export const Wrapper = styled.div<{ const IconWrapper = styled.span` position: relative; - left: 4px; + inset-inline-start: 4px; width: 24px; height: 24px; `; @@ -132,11 +132,14 @@ export const Outline = styled(Flex)<{ const CharacterCount = styled.span<{ $warning?: boolean }>` position: absolute; top: 0; - right: 0; + inset-inline-end: 0; font-size: 11px; line-height: 1; padding: 2px 4px; - border-radius: 0 0 0 2px; + border-start-start-radius: 0; + border-start-end-radius: 0; + border-end-end-radius: 0; + border-end-start-radius: 2px; background: ${(props) => props.$warning ? props.theme.warning : props.theme.inputBorder}; color: ${(props) => diff --git a/app/components/InputColor.tsx b/app/components/InputColor.tsx index ba92a05bf5..d0b5075784 100644 --- a/app/components/InputColor.tsx +++ b/app/components/InputColor.tsx @@ -36,7 +36,7 @@ const PositionedSwatchButton = styled(SwatchButton)` border: 1px solid ${(props) => props.theme.inputBorder}; position: absolute; bottom: 21px; - right: 6px; + inset-inline-end: 6px; `; export default InputColor; diff --git a/app/components/InputSearchPage.tsx b/app/components/InputSearchPage.tsx index ebc9d9e570..0721c7af7a 100644 --- a/app/components/InputSearchPage.tsx +++ b/app/components/InputSearchPage.tsx @@ -121,7 +121,7 @@ const Shortcut = styled.span<{ $visible: boolean }>` flex-shrink: 0; font-size: 13px; color: ${s("textTertiary")}; - padding-right: 10px; + padding-inline: 0 10px; pointer-events: none; opacity: ${(props) => (props.$visible ? 1 : 0)}; transition: opacity 100ms ease-in-out; diff --git a/app/components/InputSelect.tsx b/app/components/InputSelect.tsx index be7bad7ee1..b6b1c8e411 100644 --- a/app/components/InputSelect.tsx +++ b/app/components/InputSelect.tsx @@ -365,8 +365,8 @@ const IconWrapper = styled.span` align-items: center; width: 24px; height: 24px; - margin-left: -4px; - margin-right: 4px; + margin-inline-start: -4px; + margin-inline-end: 4px; overflow: hidden; flex-shrink: 0; `; diff --git a/app/components/Layout.tsx b/app/components/Layout.tsx index 2dc3b3ba4c..209817d97f 100644 --- a/app/components/Layout.tsx +++ b/app/components/Layout.tsx @@ -56,7 +56,7 @@ const Layout = React.forwardRef(function Layout_( sidebarCollapsed ? undefined : { - marginLeft: `${ui.sidebarWidth}px`, + marginInlineStart: `${ui.sidebarWidth}px`, } } > @@ -86,21 +86,21 @@ type ContentProps = { const Content = styled(Flex)` margin: 0; transition: ${(props) => - props.$isResizing ? "none" : `margin-left 100ms ease-out`}; + props.$isResizing ? "none" : `margin-inline-start 100ms ease-out`}; @media print { margin: 0 !important; } ${breakpoint("mobile", "tablet")` - margin-left: 0 !important; + margin-inline-start: 0 !important; `} ${breakpoint("tablet")` ${(props: ContentProps) => props.$hasSidebar && props.$sidebarCollapsed && - `margin-left: ${props.theme.sidebarCollapsedWidth}px;`} + `margin-inline-start: ${props.theme.sidebarCollapsedWidth}px;`} `}; `; diff --git a/app/components/List/Item.tsx b/app/components/List/Item.tsx index 6cb7b93be9..6319f94524 100644 --- a/app/components/List/Item.tsx +++ b/app/components/List/Item.tsx @@ -203,7 +203,7 @@ const Wrapper = styled.a<{ `; const Image = styled(Flex)` - padding: 0 8px 0 0; + padding-inline-end: 8px; max-height: 32px; align-items: center; user-select: none; diff --git a/app/components/Notifications/NotificationListItem.tsx b/app/components/Notifications/NotificationListItem.tsx index 2b0b95f58b..2fcf91affe 100644 --- a/app/components/Notifications/NotificationListItem.tsx +++ b/app/components/Notifications/NotificationListItem.tsx @@ -117,8 +117,8 @@ const StyledAvatar = styled(Avatar).attrs({ const Container = styled(Flex)<{ $unread: boolean }>` position: relative; - padding: 8px 12px; - padding-right: 40px; + padding-block: 8px; + padding-inline: 12px 40px; border-radius: 4px; ${StyledLink}[data-state=open] &, diff --git a/app/components/Sidebar/App.tsx b/app/components/Sidebar/App.tsx index a5b6453103..f3c05b2b5b 100644 --- a/app/components/Sidebar/App.tsx +++ b/app/components/Sidebar/App.tsx @@ -69,7 +69,7 @@ function AppSidebar() { model={team} size={24} alt={t("Logo")} - style={{ marginLeft: 4 }} + style={{ insetInlineStart: 4 }} /> } > diff --git a/app/components/Sidebar/Right.tsx b/app/components/Sidebar/Aside.tsx similarity index 87% rename from app/components/Sidebar/Right.tsx rename to app/components/Sidebar/Aside.tsx index b0dccf73a4..d3aa0310f6 100644 --- a/app/components/Sidebar/Right.tsx +++ b/app/components/Sidebar/Aside.tsx @@ -10,6 +10,7 @@ import ResizeBorder from "~/components/Sidebar/components/ResizeBorder"; import useStores from "~/hooks/useStores"; import useWindowScrollbarWidth from "~/hooks/useWindowScrollbarWidth"; import { sidebarAppearDuration } from "~/styles/animations"; +import { useDirection } from "@radix-ui/react-direction"; interface Props extends React.HTMLAttributes { children: React.ReactNode; @@ -18,25 +19,25 @@ interface Props extends React.HTMLAttributes { skipInitialAnimation?: boolean; } -function Right({ children, border, className, skipInitialAnimation }: Props) { +function Aside({ children, border, className, skipInitialAnimation }: Props) { const theme = useTheme(); const { ui } = useStores(); const [isResizing, setResizing] = React.useState(false); const maxWidth = theme.sidebarMaxWidth; const minWidth = theme.sidebarMinWidth + 16; // padding const windowScrollbarWidth = useWindowScrollbarWidth(); + const direction = useDirection(); const handleDrag = React.useCallback( (event: MouseEvent) => { // suppresses text selection event.preventDefault(); - const width = Math.max( - Math.min(window.innerWidth - event.pageX, maxWidth), - minWidth - ); + const distance = + direction === "rtl" ? event.pageX : window.innerWidth - event.pageX; + const width = Math.max(Math.min(distance, maxWidth), minWidth); ui.set({ sidebarRightWidth: width }); }, - [minWidth, maxWidth, ui] + [minWidth, maxWidth, direction, ui] ); const handleReset = React.useCallback(() => { @@ -108,7 +109,7 @@ function Right({ children, border, className, skipInitialAnimation }: Props) { $border={border} className={className} role="complementary" - aria-label="Right sidebar" + aria-label="Aside" > {children} @@ -136,15 +137,15 @@ const Sidebar = styled(m.div)<{ flex-shrink: 0; background: ${s("background")}; max-width: 80%; - border-left: 1px solid ${s("divider")}; - transition: border-left 100ms ease-in-out; + border-inline-start: 1px solid ${s("divider")}; + transition: border-inline-start 100ms ease-in-out; z-index: ${depths.sidebar}; ${breakpoint("mobile", "tablet")` display: flex; position: absolute; top: 0; - right: 0; + inset-inline-end: 0; bottom: 0; z-index: ${depths.mobileSidebar}; `} @@ -154,4 +155,4 @@ const Sidebar = styled(m.div)<{ `} `; -export default observer(Right); +export default observer(Aside); diff --git a/app/components/Sidebar/Settings.tsx b/app/components/Sidebar/Settings.tsx index bfd51ca30b..27b922b302 100644 --- a/app/components/Sidebar/Settings.tsx +++ b/app/components/Sidebar/Settings.tsx @@ -101,7 +101,11 @@ function SettingsSidebar() { } const StyledBackIcon = styled(BackIcon)` - margin-left: 4px; + margin-inline-start: 4px; + + [dir="rtl"] & { + transform: rotate(180deg); + } `; export default observer(SettingsSidebar); diff --git a/app/components/Sidebar/Shared.tsx b/app/components/Sidebar/Shared.tsx index a7fe81c6e5..aa778b73d5 100644 --- a/app/components/Sidebar/Shared.tsx +++ b/app/components/Sidebar/Shared.tsx @@ -137,7 +137,7 @@ const SearchButton = styled.button` const SearchLabel = styled.span` flex-grow: 1; - text-align: left; + text-align: start; `; const Shortcut = styled.span` diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index 978fa4fb95..865dc86785 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -22,6 +22,7 @@ import ResizeBorder from "./components/ResizeBorder"; import SidebarButton from "./components/SidebarButton"; import ToggleButton from "./components/ToggleButton"; import { useTranslation } from "react-i18next"; +import { useDirection } from "@radix-ui/react-direction"; const ANIMATION_MS = 250; @@ -53,6 +54,7 @@ const Sidebar = React.forwardRef(function Sidebar_( const maxWidth = theme.sidebarMaxWidth; const minWidth = theme.sidebarMinWidth + 16; // padding const { trigger } = useWebHaptics(); + const direction = useDirection(); const [offset, setOffset] = React.useState(0); const [isHovering, setHovering] = React.useState(false); @@ -66,8 +68,9 @@ const Sidebar = React.forwardRef(function Sidebar_( (event: MouseEvent) => { // suppresses text selection event.preventDefault(); - // this is simple because the sidebar is always against the left edge - const newWidth = Math.min(event.pageX - offset, maxWidth); + const rawWidth = + direction === "rtl" ? offset - event.pageX : event.pageX - offset; + const newWidth = Math.min(rawWidth, maxWidth); const isSmallerThanCollapsePoint = newWidth < minWidth / 2; if (canCollapse) { @@ -80,7 +83,7 @@ const Sidebar = React.forwardRef(function Sidebar_( ui.set({ sidebarWidth: Math.max(newWidth, minWidth) }); } }, - [ui, theme, offset, minWidth, maxWidth] + [ui, theme, offset, minWidth, maxWidth, direction] ); const handleStopDrag = React.useCallback(() => { @@ -117,11 +120,13 @@ const Sidebar = React.forwardRef(function Sidebar_( return; } - setOffset(event.pageX - width); + setOffset( + direction === "rtl" ? event.pageX + width : event.pageX - width + ); setResizing(true); setAnimating(false); }, - [width] + [width, direction] ); const handlePointerActivity = React.useCallback(() => { @@ -145,16 +150,21 @@ const Sidebar = React.forwardRef(function Sidebar_( // add a short delay when mouse exits the sidebar before closing hoverTimeoutRef.current = setTimeout(() => { + const withinSidebar = + direction === "rtl" + ? ev.pageX > window.innerWidth - width + : ev.pageX < width; + setHovering( document.hasFocus() && - ev.pageX < width && + withinSidebar && ev.pageY < window.innerHeight && ev.pageY > 0 ); }, 500); } }, - [width, hasPointerMoved] + [width, direction, hasPointerMoved] ); React.useEffect(() => { @@ -255,7 +265,7 @@ const Sidebar = React.forwardRef(function Sidebar_( alt={t("Avatar of {{ name }}", { name: user.name })} model={user} size={24} - style={{ marginLeft: 4 }} + style={{ marginInlineStart: 4 }} /> } > @@ -302,7 +312,7 @@ type ContainerProps = { }; const hoverStyles = (props: ContainerProps) => ` - transform: none; + transform: none !important; box-shadow: ${ props.$collapsed ? "rgba(0, 0, 0, 0.2) 1px 0 4px" @@ -320,22 +330,29 @@ const Container = styled(Flex)` position: fixed; top: 0; bottom: 0; + inset-inline-start: 0; width: 100%; background: ${s("sidebarBackground")}; transition: box-shadow 150ms ease-in-out, - transform 150ms ease-out, - ${(props: ContainerProps) => - props.$isAnimating ? `,width ${ANIMATION_MS}ms ease-out` : ""}; + transform 150ms + ease-out${(props: ContainerProps) => + props.$isAnimating ? `, width ${ANIMATION_MS}ms ease-out` : ""}; transform: translateX( ${(props) => (props.$mobileSidebarVisible ? 0 : "-100%")} ); z-index: ${depths.mobileSidebar}; max-width: 80%; min-width: 280px; - padding-left: var(--sal); + padding-inline-start: var(--sal); ${fadeOnDesktopBackgrounded()} + [dir="rtl"] & { + transform: translateX( + ${(props) => (props.$mobileSidebarVisible ? 0 : "100%")} + ); + } + @media print { display: none; transform: none; @@ -367,6 +384,11 @@ const Container = styled(Flex)` ? `calc(-100% + ${Desktop.hasInsetTitlebar() ? 8 : 16}px)` : 0}); + [dir="rtl"] & { + transform: translateX(${(props: ContainerProps) => + props.$collapsed ? `calc(100% - 8px)` : 0}); + } + ${(props: ContainerProps) => props.$isHovering && css(hoverStyles)} &:hover { diff --git a/app/components/Sidebar/components/CollectionLinkChildren.tsx b/app/components/Sidebar/components/CollectionLinkChildren.tsx index bd4de4698a..ecd865297a 100644 --- a/app/components/Sidebar/components/CollectionLinkChildren.tsx +++ b/app/components/Sidebar/components/CollectionLinkChildren.tsx @@ -118,7 +118,7 @@ const DynamicDropCursor = observer( ); const Loading = styled(PlaceholderCollections)` - margin-left: 44px; + margin-inline-start: 44px; min-height: 90px; `; diff --git a/app/components/Sidebar/components/Disclosure.tsx b/app/components/Sidebar/components/Disclosure.tsx index b4b8040c53..0a2dad9004 100644 --- a/app/components/Sidebar/components/Disclosure.tsx +++ b/app/components/Sidebar/components/Disclosure.tsx @@ -28,7 +28,7 @@ function Disclosure({ onClick, expanded, ...rest }: Props) { const Button = styled(NudeButton)` position: absolute; - left: -24px; + inset-inline-start: -24px; flex-shrink: 0; color: ${s("textSecondary")}; margin: 2px; @@ -47,7 +47,14 @@ const StyledCollapsedIcon = styled(CollapsedIcon)<{ opacity 100ms ease, transform 100ms ease, fill 50ms !important; - ${(props) => !props.$expanded && "transform: rotate(-90deg);"}; + + [aria-expanded="false"] & { + transform: rotate(-90deg); + } + + [dir="rtl"] [aria-expanded="false"] & { + transform: rotate(90deg); + } `; // Enables identifying this component within styled components diff --git a/app/components/Sidebar/components/Header.tsx b/app/components/Sidebar/components/Header.tsx index aaf7d057e6..fed0f94b7b 100644 --- a/app/components/Sidebar/components/Header.tsx +++ b/app/components/Sidebar/components/Header.tsx @@ -75,7 +75,8 @@ const Button = styled.button` position: relative; letter-spacing: 0.03em; margin: 0; - padding: 4px 2px 4px 12px; + padding-block: 4px; + padding-inline: 12px 2px; border: 0; background: none; border-radius: 4px; @@ -98,6 +99,10 @@ const Disclosure = styled(CollapsedIcon)<{ $expanded?: boolean }>` fill 50ms !important; ${(props) => !props.$expanded && "transform: rotate(-90deg);"}; opacity: 0; + + [dir="rtl"] & { + ${(props) => !props.$expanded && "transform: rotate(90deg);"}; + } `; const H3 = styled.h3` diff --git a/app/components/Sidebar/components/HistoryNavigation.tsx b/app/components/Sidebar/components/HistoryNavigation.tsx index df61e33292..a5e1e86554 100644 --- a/app/components/Sidebar/components/HistoryNavigation.tsx +++ b/app/components/Sidebar/components/HistoryNavigation.tsx @@ -91,7 +91,7 @@ function HistoryNavigation(props: React.ComponentProps) { const Navigation = styled(Flex)` position: absolute; - right: 12px; + inset-inline-end: 12px; top: 14px; button { @@ -108,11 +108,19 @@ const Forward = styled(ArrowIcon)<{ $enabled: boolean }>` &:hover { opacity: ${(props) => (props.$enabled ? 1 : 0.15)}; } + + [dir="rtl"] & { + transform: rotate(180deg); + } `; const Back = styled(Forward)` transform: rotate(180deg); flex-shrink: 0; + + [dir="rtl"] & { + transform: rotate(0deg); + } `; const StyledClockIcon = styled(ClockIcon)` diff --git a/app/components/Sidebar/components/ResizeBorder.ts b/app/components/Sidebar/components/ResizeBorder.ts index 653ddf0468..913d96befb 100644 --- a/app/components/Sidebar/components/ResizeBorder.ts +++ b/app/components/Sidebar/components/ResizeBorder.ts @@ -6,8 +6,8 @@ const ResizeBorder = styled.div<{ dir?: "left" | "right" }>` position: absolute; top: 0; bottom: 0; - right: ${(props) => (props.dir !== "right" ? "-1px" : "auto")}; - left: ${(props) => (props.dir === "right" ? "-1px" : "auto")}; + inset-inline-end: ${(props) => (props.dir !== "right" ? "-1px" : "auto")}; + inset-inline-start: ${(props) => (props.dir === "right" ? "-1px" : "auto")}; width: 2px; cursor: col-resize; ${undraggableOnDesktop()} @@ -23,7 +23,7 @@ const ResizeBorder = styled.div<{ dir?: "left" | "right" }>` position: absolute; top: 0; bottom: 0; - right: -4px; + inset-inline-end: -4px; width: 10px; ${undraggableOnDesktop()} } diff --git a/app/components/Sidebar/components/SidebarButton.tsx b/app/components/Sidebar/components/SidebarButton.tsx index c83f4f451d..e929596111 100644 --- a/app/components/Sidebar/components/SidebarButton.tsx +++ b/app/components/Sidebar/components/SidebarButton.tsx @@ -100,7 +100,7 @@ const Button = styled(Flex)<{ -webkit-appearance: none; text-decoration: none; - text-align: left; + text-align: start; user-select: none; position: relative; @@ -118,11 +118,11 @@ const Button = styled(Flex)<{ } &:last-child { - margin-right: 8px; + margin-inline-end: 8px; } &:first-child { - margin-left: 8px; + margin-inline-start: 8px; } `; diff --git a/app/components/Sidebar/components/SidebarLink.tsx b/app/components/Sidebar/components/SidebarLink.tsx index ddf8a89ccd..bce7fbbb63 100644 --- a/app/components/Sidebar/components/SidebarLink.tsx +++ b/app/components/Sidebar/components/SidebarLink.tsx @@ -102,15 +102,15 @@ function SidebarLink( const { handleMouseEnter, handleMouseLeave } = useClickIntent(onClickIntent); const style = React.useMemo( () => ({ - paddingLeft: `${(depth || 0) * 16 + (icon ? -8 : 12)}px`, - paddingRight: unreadBadge ? "32px" : undefined, + paddingInlineStart: `${(depth || 0) * 16 + (icon ? -8 : 12)}px`, + paddingInlineEnd: unreadBadge ? "32px" : undefined, }), [depth, icon, unreadBadge] ); const unreadStyle = React.useMemo( () => ({ - right: -20, + insetInlineEnd: -20, }), [] ); @@ -217,7 +217,7 @@ function SidebarLink( // accounts for whitespace around icon export const IconWrapper = styled.span` - margin-left: -4px; + margin-inline-start: -4px; height: 24px; overflow: hidden; flex-shrink: 0; @@ -237,7 +237,7 @@ const Actions = styled(EventBoundary)<{ $showActions?: boolean }>` visibility: ${(props) => (props.$showActions ? "visible" : "hidden")}; position: absolute; top: 3px; - right: 4px; + inset-inline-end: 4px; gap: 4px; color: ${s("textTertiary")}; transition: opacity 50ms; @@ -261,10 +261,10 @@ const Actions = styled(EventBoundary)<{ $showActions?: boolean }>` const HiddenDisclosure = styled(Disclosure)` position: inherit; - left: initial; + inset-inline-start: initial; display: none; - margin-left: -2px; - margin-right: 6px; + margin-inline-start: -2px; + margin-inline-end: 6px; `; const Link = styled(NavLink)<{ @@ -317,10 +317,7 @@ const Link = styled(NavLink)<{ &:after { content: ""; position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; pointer-events: none; border-radius: 4px; border: 1.5px dashed ${props.theme.sidebarDraftBorder}; @@ -344,7 +341,8 @@ const Link = styled(NavLink)<{ } ${breakpoint("tablet")` - padding: 3px 8px 3px 12px; + padding-block: 3px; + padding-inline: 12px 8px; font-size: 14px; `} @@ -379,9 +377,9 @@ const Label = styled.div<{ $ellipsis: boolean }>` position: relative; width: 100%; line-height: 24px; - margin-left: 2px; + margin-inline-start: 2px; min-width: 0; - text-align: left; + text-align: start; ${(props) => props.$ellipsis && ellipsis()} diff --git a/app/components/Sidebar/components/ToggleButton.tsx b/app/components/Sidebar/components/ToggleButton.tsx index b782d4aa0b..47ed4135c6 100644 --- a/app/components/Sidebar/components/ToggleButton.tsx +++ b/app/components/Sidebar/components/ToggleButton.tsx @@ -10,6 +10,10 @@ const ToggleButton = styled(SidebarButton)` &:active { opacity: 1; } + + [dir="rtl"] & svg { + transform: scaleX(-1); + } `; export default ToggleButton; diff --git a/app/components/Sidebar/components/Version.tsx b/app/components/Sidebar/components/Version.tsx index 0d77a98dd4..66f9a8affd 100644 --- a/app/components/Sidebar/components/Version.tsx +++ b/app/components/Sidebar/components/Version.tsx @@ -54,5 +54,5 @@ export default function Version() { } const LilBadge = styled(Badge)` - margin-left: 0; + margin-inline-start: 0; `; diff --git a/app/components/Switch.tsx b/app/components/Switch.tsx index 9bd3f53bd6..c0d00a6c16 100644 --- a/app/components/Switch.tsx +++ b/app/components/Switch.tsx @@ -172,6 +172,10 @@ const StyledSwitchThumb = styled(RadixSwitch.Thumb)<{ &[data-state="checked"] { transform: translateX(${(props) => props.width - props.height}px); } + + [dir="rtl"] &[data-state="checked"] { + transform: translateX(${(props) => -(props.width - props.height)}px); + } `; export default React.forwardRef(Switch); diff --git a/app/components/Tab.tsx b/app/components/Tab.tsx index 4dc208743d..d5eecaa406 100644 --- a/app/components/Tab.tsx +++ b/app/components/Tab.tsx @@ -54,7 +54,6 @@ const tabStyles = ` font-size: 14px; cursor: var(--pointer); user-select: none; - margin-right: 24px; padding: 6px 0; `; diff --git a/app/components/Tabs.tsx b/app/components/Tabs.tsx index 29eb131ef6..b64a0cfce2 100644 --- a/app/components/Tabs.tsx +++ b/app/components/Tabs.tsx @@ -14,6 +14,10 @@ const Nav = styled.nav<{ $shadowVisible?: boolean }>` -ms-overflow-style: none; scrollbar-width: none; + & > * + * { + margin-inline-start: 24px; + } + &::-webkit-scrollbar { display: none; } @@ -52,7 +56,6 @@ export const Separator = styled.span` border-left: 1px solid ${s("divider")}; position: relative; top: 2px; - margin-right: 24px; margin-top: 6px; `; diff --git a/app/components/Theme.tsx b/app/components/Theme.tsx index f5939c1c54..21cd192c12 100644 --- a/app/components/Theme.tsx +++ b/app/components/Theme.tsx @@ -1,8 +1,11 @@ +import { DirectionProvider } from "@radix-ui/react-direction"; import { observer } from "mobx-react"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import { ThemeProvider } from "styled-components"; import GlobalStyles from "@shared/styles/globals"; import { TeamPreference, UserPreference } from "@shared/types"; +import { isRTLLanguage } from "@shared/utils/rtl"; import useBuildTheme from "~/hooks/useBuildTheme"; import useStores from "~/hooks/useStores"; @@ -12,11 +15,13 @@ type Props = { const Theme: React.FC = ({ children }: Props) => { const { auth, ui } = useStores(); + const { i18n } = useTranslation(); const theme = useBuildTheme( auth.team?.getPreference(TeamPreference.CustomTheme) || auth.config?.customTheme || undefined ); + const direction = isRTLLanguage(i18n.language) ? "rtl" : "ltr"; React.useEffect(() => { window.dispatchEvent( @@ -27,17 +32,19 @@ const Theme: React.FC = ({ children }: Props) => { }, [ui.resolvedTheme]); return ( - - <> - - {children} - - + + + <> + + {children} + + + ); }; diff --git a/app/components/primitives/components/InputSelect.tsx b/app/components/primitives/components/InputSelect.tsx index 5fea3a66b3..9ec1afaf33 100644 --- a/app/components/primitives/components/InputSelect.tsx +++ b/app/components/primitives/components/InputSelect.tsx @@ -73,13 +73,13 @@ export const SelectButton = styled(Button)<{ $nude?: boolean }>` ${Inner} { line-height: 28px; - padding-left: 12px; - padding-right: 4px; + padding-inline-start: 12px; + padding-inline-end: 4px; } svg { justify-self: flex-end; - margin-left: auto; + margin-inline-start: auto; } &[data-placeholder=""] { @@ -132,7 +132,7 @@ const ItemContainer = styled(Flex)` ${breakpoint("tablet")` font-size: 14px; padding: 4px; - padding-left: 8px; + padding-inline-start: 8px; `} `; diff --git a/app/components/primitives/components/Menu.tsx b/app/components/primitives/components/Menu.tsx index 766c0ba561..e509f74088 100644 --- a/app/components/primitives/components/Menu.tsx +++ b/app/components/primitives/components/Menu.tsx @@ -16,7 +16,7 @@ type BaseMenuItemProps = { const BaseMenuItemCSS = css` position: relative; display: flex; - justify-content: left; + justify-content: flex-start; align-items: center; width: 100%; min-height: 32px; @@ -135,15 +135,19 @@ export const MenuHeader = styled.h3` export const MenuDisclosure = styled(ExpandedIcon)` transform: rotate(270deg); position: absolute; - right: 8px; + inset-inline-end: 8px; color: ${s("textTertiary")}; + + [dir="rtl"] & { + transform: rotate(90deg); + } `; export const MenuIconWrapper = styled.span` width: 24px; height: 24px; - margin-right: 6px; - margin-left: -4px; + margin-inline-end: 6px; + margin-inline-start: -4px; color: ${s("textSecondary")}; flex-shrink: 0; display: flex; @@ -154,7 +158,7 @@ export const MenuIconWrapper = styled.span` export const SelectedIconWrapper = styled.span` width: 24px; height: 24px; - margin-right: -6px; + margin-inline-end: -6px; color: ${s("textSecondary")}; flex-shrink: 0; display: flex; @@ -169,7 +173,7 @@ export const MenuShortcut = styled.span` font-size: 12px; color: currentColor; opacity: 0.5; - margin-left: 16px; + margin-inline-start: 16px; flex-shrink: 0; `; diff --git a/app/scenes/Collection/components/Header.tsx b/app/scenes/Collection/components/Header.tsx index 31f3d99726..deac81bd7d 100644 --- a/app/scenes/Collection/components/Header.tsx +++ b/app/scenes/Collection/components/Header.tsx @@ -4,6 +4,7 @@ import first from "lodash/first"; import { Suspense, useCallback } from "react"; import styled from "styled-components"; import { CollectionValidation } from "@shared/validations"; +import { isRTL } from "@shared/utils/rtl"; import Heading from "~/components/Heading"; import ContentEditable from "~/components/ContentEditable"; import CollectionIcon from "~/components/Icons/CollectionIcon"; @@ -48,9 +49,11 @@ export const Header = observer(function Header_({ ) : null; + const dir = isRTL(collection.name) ? "rtl" : "ltr"; + return ( - - + + {canEdit ? ( void; /** Callback when the editor is blurred */ @@ -75,7 +73,6 @@ function CommentForm({ placeholder, animatePresence, highlightedText, - dir, ...rest }: Props) { const { editor } = useDocumentContext(); @@ -306,7 +303,7 @@ function CommentForm({ tabIndex={-1} /> - + {(inputFocused || draft) && ( - + {thread && !thread.isNew ? t("Reply") : t("Post")} diff --git a/app/scenes/Document/components/Comments/CommentThread.tsx b/app/scenes/Document/components/Comments/CommentThread.tsx index d93dfcfa33..5b2ec858f5 100644 --- a/app/scenes/Document/components/Comments/CommentThread.tsx +++ b/app/scenes/Document/components/Comments/CommentThread.tsx @@ -219,7 +219,6 @@ function CommentThread({ ref={topRef} $focused={focused} $recessed={recessed} - $dir={document.dir} onClick={handleClickThread} > {commentsInThread.map((comment, index) => { @@ -252,7 +251,6 @@ function CommentThread({ firstOfAuthor={firstOfAuthor} lastOfAuthor={lastOfAuthor} previousCommentCreatedAt={commentsInThread[index - 1]?.createdAt} - dir={document.dir} forceEdit={editingCommentIds.has(comment.id)} onEditStart={() => handleCommentEditStart(comment.id)} onEditEnd={() => handleCommentEditEnd(comment.id)} @@ -270,7 +268,6 @@ function CommentThread({ documentId={document.id} thread={thread} standalone={commentsInThread.length === 0} - dir={document.dir} autoFocus={autoFocus} highlightedText={ commentsInThread.length === 0 ? highlightedText : undefined @@ -298,23 +295,22 @@ const Reply = styled.button` cursor: var(--pointer); transition: opacity 100ms ease-out; position: absolute; - text-align: left; + text-align: start; width: 100%; bottom: -30px; - left: 32px; + inset-inline-start: 32px; ${breakpoint("tablet")` opacity: 0; `} `; -const ShowMore = styled.div<{ $dir?: "rtl" | "ltr" }>` +const ShowMore = styled.div` display: flex; justify-content: space-between; align-items: center; margin-bottom: 1px; - margin-left: ${(props) => (props.$dir === "rtl" ? 0 : 32)}px; - margin-right: ${(props) => (props.$dir !== "rtl" ? 0 : 32)}px; + margin-inline-start: 32px; padding: 8px 12px; color: ${s("textTertiary")}; background: ${(props) => darken(0.015, props.theme.backgroundSecondary)}; @@ -334,11 +330,10 @@ const ShowMore = styled.div<{ $dir?: "rtl" | "ltr" }>` const Thread = styled.div<{ $focused: boolean; $recessed: boolean; - $dir?: "rtl" | "ltr"; }>` margin: 12px 12px 32px; - margin-right: ${(props) => (props.$dir !== "rtl" ? "18px" : "12px")}; - margin-left: ${(props) => (props.$dir === "rtl" ? "18px" : "12px")}; + margin-inline-end: 18px; + margin-inline-start: 12px; position: relative; transition: opacity 100ms ease-out; diff --git a/app/scenes/Document/components/Comments/CommentThreadItem.tsx b/app/scenes/Document/components/Comments/CommentThreadItem.tsx index a98800ad0b..49d93ecd05 100644 --- a/app/scenes/Document/components/Comments/CommentThreadItem.tsx +++ b/app/scenes/Document/components/Comments/CommentThreadItem.tsx @@ -70,8 +70,6 @@ function useShowTime( type Props = { /** The comment to render */ comment: Comment; - /** The text direction of the editor */ - dir?: "rtl" | "ltr"; /** Whether this is the first comment in the thread */ firstOfThread?: boolean; /** Whether this is the last comment in the thread */ @@ -103,7 +101,6 @@ function CommentThreadItem({ firstOfAuthor, firstOfThread, lastOfThread, - dir, previousCommentCreatedAt, canReply, onDelete, @@ -200,7 +197,7 @@ function CommentThreadItem({ }; return ( - + {firstOfAuthor && ( @@ -210,12 +207,11 @@ function CommentThreadItem({ $firstOfThread={firstOfThread} $firstOfAuthor={firstOfAuthor} $lastOfThread={lastOfThread} - $dir={dir} $canReply={canReply} column > {(showAuthor || showTime) && ( - + {showAuthor && {comment.createdBy.name}} {showAuthor && showTime && <> · } {showTime && ( @@ -277,7 +273,7 @@ function CommentThreadItem({ {!isEditing && ( - + {!comment.isResolved && ( <> {firstOfThread && ( @@ -386,14 +382,13 @@ const Action = styled.span<{ $rounded?: boolean }>` } `; -const Actions = styled(Flex)<{ dir?: "rtl" | "ltr" }>` +const Actions = styled(Flex)` position: absolute; - left: ${(props) => (props.dir !== "rtl" ? "auto" : "4px")}; - right: ${(props) => (props.dir === "rtl" ? "auto" : "4px")}; + inset-inline-end: 4px; top: 4px; transition: opacity 100ms ease-in-out; background: ${s("backgroundSecondary")}; - padding-left: 4px; + padding-inline-start: 4px; ${breakpoint("tablet")` opacity: 0; @@ -423,7 +418,6 @@ export const Bubble = styled(Flex)<{ $lastOfThread?: boolean; $canReply?: boolean; $focused?: boolean; - $dir?: "rtl" | "ltr"; }>` position: relative; flex-grow: 1; @@ -440,16 +434,13 @@ export const Bubble = styled(Flex)<{ ${({ $lastOfThread, $canReply }) => $lastOfThread && !$canReply && - "border-bottom-left-radius: 8px; border-bottom-right-radius: 8px"}; + "border-end-start-radius: 8px; border-end-end-radius: 8px"}; ${({ $firstOfThread }) => $firstOfThread && - "border-top-left-radius: 8px; border-top-right-radius: 8px"}; + "border-start-start-radius: 8px; border-start-end-radius: 8px"}; - margin-left: ${(props) => - props.$firstOfAuthor || props.$dir === "rtl" ? 0 : 32}px; - margin-right: ${(props) => - props.$firstOfAuthor || props.$dir !== "rtl" ? 0 : 32}px; + margin-inline-start: ${(props) => (props.$firstOfAuthor ? 0 : 32)}px; p:last-child { margin-bottom: 0; diff --git a/app/scenes/Document/components/Comments/Comments.tsx b/app/scenes/Document/components/Comments/Comments.tsx index d96f80c2d7..9bcd4b0a7b 100644 --- a/app/scenes/Document/components/Comments/Comments.tsx +++ b/app/scenes/Document/components/Comments/Comments.tsx @@ -180,7 +180,6 @@ function Comments() { documentId={document.id} placeholder={`${t("Add a comment")}…`} autoFocus={false} - dir={document.dir} animatePresence standalone /> @@ -245,10 +244,10 @@ const JumpToRecent = styled(ButtonSmall)` } `; -const NewCommentForm = styled(CommentForm)<{ dir?: "ltr" | "rtl" }>` +const NewCommentForm = styled(CommentForm)` padding: 12px; - padding-right: ${(props) => (props.dir !== "rtl" ? "18px" : "12px")}; - padding-left: ${(props) => (props.dir === "rtl" ? "18px" : "12px")}; + padding-inline-end: 18px; + padding-inline-start: 12px; `; export default observer(Comments); diff --git a/app/scenes/Document/components/Comments/HighlightText.ts b/app/scenes/Document/components/Comments/HighlightText.ts index 401d2947c8..b2ae52984a 100644 --- a/app/scenes/Document/components/Comments/HighlightText.ts +++ b/app/scenes/Document/components/Comments/HighlightText.ts @@ -19,7 +19,7 @@ export const HighlightedText = styled(Text)` content: ""; width: 2px; position: absolute; - left: 0; + inset-inline-start: 0; top: 2px; bottom: 2px; background: ${s("commentMarkBackground")}; diff --git a/app/scenes/Document/components/SidebarLayout.tsx b/app/scenes/Document/components/SidebarLayout.tsx index 32119ce9cf..37604f78c0 100644 --- a/app/scenes/Document/components/SidebarLayout.tsx +++ b/app/scenes/Document/components/SidebarLayout.tsx @@ -9,7 +9,7 @@ import Flex from "~/components/Flex"; import { PortalContext } from "~/components/Portal"; import { RightSidebarWrappedContext } from "~/components/RightSidebarContext"; import Scrollable from "~/components/Scrollable"; -import RightSidebar from "~/components/Sidebar/Right"; +import Aside from "~/components/Sidebar/Aside"; import Tooltip from "~/components/Tooltip"; import { Drawer, @@ -79,12 +79,16 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) { return inner; } - return {inner}; + return ; } const ForwardIcon = styled(BackIcon)` transform: rotate(180deg); flex-shrink: 0; + + [dir="rtl"] & { + transform: rotate(0deg); + } `; const Title = styled(Flex)` diff --git a/app/scenes/Document/hooks/useDocumentSidebar.tsx b/app/scenes/Document/hooks/useDocumentSidebar.tsx index e524b5201e..eef16fe66e 100644 --- a/app/scenes/Document/hooks/useDocumentSidebar.tsx +++ b/app/scenes/Document/hooks/useDocumentSidebar.tsx @@ -5,7 +5,7 @@ import { RightSidebarWrappedContext, useSetRightSidebar, } from "~/components/RightSidebarContext"; -import RightSidebar from "~/components/Sidebar/Right"; +import Aside from "~/components/Sidebar/Aside"; import PlaceholderText from "~/components/PlaceholderText"; import useMobile from "~/hooks/useMobile"; import useStores from "~/hooks/useStores"; @@ -31,7 +31,7 @@ interface DocumentSidebarContentProps { /** * Stable component that reads `ui.rightSidebar` and renders the appropriate - * sidebar content. On desktop, wraps content in a single Right sidebar that + * sidebar content. On desktop, wraps content in a single Aside sidebar that * stays mounted across panel switches to avoid re-triggering the open/close * animation. */ @@ -61,11 +61,11 @@ const DocumentSidebarContent = observer(function DocumentSidebarContent({ } return ( - + ); }); diff --git a/app/scenes/KeyboardShortcuts.tsx b/app/scenes/KeyboardShortcuts.tsx index 77a3393dde..866bbc751d 100644 --- a/app/scenes/KeyboardShortcuts.tsx +++ b/app/scenes/KeyboardShortcuts.tsx @@ -625,11 +625,11 @@ const List = styled.dl` `; const Keys = styled.dt` - float: right; + float: inline-end; width: 45%; margin: 0 0 10px; - clear: left; - text-align: right; + clear: inline-start; + text-align: end; font-size: 12px; color: ${s("textSecondary")}; display: flex; @@ -638,7 +638,7 @@ const Keys = styled.dt` `; const Label = styled.dd` - float: left; + float: inline-start; width: 55%; margin: 0 0 10px; display: flex; diff --git a/app/scenes/Search/components/SearchInput.tsx b/app/scenes/Search/components/SearchInput.tsx index 438c8ce1cc..874bbbd991 100644 --- a/app/scenes/Search/components/SearchInput.tsx +++ b/app/scenes/Search/components/SearchInput.tsx @@ -53,7 +53,8 @@ const Wrapper = styled(Flex)` const StyledInput = styled.input` width: 100%; - padding: 10px 10px 10px 60px; + padding-block: 10px 10px; + padding-inline: 60px 10px; font-size: 30px; font-weight: 400; outline: none; @@ -81,7 +82,7 @@ const StyledInput = styled.input` const StyledIcon = styled(SearchIcon)` position: absolute; - left: 8px; + inset-inline-start: 8px; opacity: 0.7; `; diff --git a/app/scenes/Settings/components/ActionRow.tsx b/app/scenes/Settings/components/ActionRow.tsx index d17889d326..00a90dc3bf 100644 --- a/app/scenes/Settings/components/ActionRow.tsx +++ b/app/scenes/Settings/components/ActionRow.tsx @@ -14,7 +14,7 @@ export const ActionRow = styled(HStack).attrs({ bottom: 0; width: 100vw; padding: 16px 12px; - margin-left: -12px; + margin-inline-start: -12px; background: ${s("background")}; diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts index 5da82272f2..d20fe802a5 100644 --- a/app/utils/i18n.ts +++ b/app/utils/i18n.ts @@ -3,6 +3,7 @@ import backend from "i18next-http-backend"; import { initReactI18next } from "react-i18next"; import { languages } from "@shared/i18n"; import { unicodeCLDRtoBCP47, unicodeBCP47toCLDR } from "@shared/utils/date"; +import { isRTLLanguage } from "@shared/utils/rtl"; import { cdnPath } from "@shared/utils/urls"; import Logger from "./Logger"; @@ -17,6 +18,12 @@ import Logger from "./Logger"; export function initI18n(defaultLanguage = "en_US") { const lng = unicodeCLDRtoBCP47(defaultLanguage); + if (typeof document !== "undefined") { + document.documentElement.dir = isRTLLanguage(defaultLanguage) + ? "rtl" + : "ltr"; + } + void i18n .use(backend) .use(initReactI18next) diff --git a/app/utils/language.ts b/app/utils/language.ts index eae9319d27..7095dc7284 100644 --- a/app/utils/language.ts +++ b/app/utils/language.ts @@ -1,6 +1,7 @@ import type { i18n } from "i18next"; import type { locales } from "@shared/utils/date"; import { unicodeCLDRtoBCP47 } from "@shared/utils/date"; +import { isRTLLanguage } from "@shared/utils/rtl"; import Desktop from "./Desktop"; /** @@ -49,6 +50,10 @@ export async function changeLanguage( await instance.changeLanguage(localeBCP); await Desktop.bridge?.setSpellCheckerLanguages(["en-US", localeBCP]); } + + if (typeof document !== "undefined") { + document.documentElement.dir = isRTLLanguage(locale) ? "rtl" : "ltr"; + } } /** diff --git a/package.json b/package.json index bc007c67f8..c71c2e3647 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-one-time-password-field": "^0.1.8", "@radix-ui/react-popover": "^1.1.15", diff --git a/shared/utils/rtl.ts b/shared/utils/rtl.ts index bea1f55631..3015c09dfe 100644 --- a/shared/utils/rtl.ts +++ b/shared/utils/rtl.ts @@ -4,6 +4,8 @@ const rtlChars = "\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC"; // oxlint-disable-next-line no-misleading-character-class const rtlDirCheck = new RegExp("^[^" + ltrChars + "]*[" + rtlChars + "]"); +const rtlLanguageCodes = new Set(["ar", "fa", "he", "ps", "ur", "yi"]); + /** * Returns true if the text is likely written in an RTL language. * @@ -13,3 +15,18 @@ const rtlDirCheck = new RegExp("^[^" + ltrChars + "]*[" + rtlChars + "]"); export function isRTL(text: string) { return rtlDirCheck.test(text); } + +/** + * Returns true if the given locale is an RTL language. Accepts both CLDR + * (`he_IL`) and BCP47 (`he-IL`, `he`) formats. + * + * @param locale The locale to check + * @returns True if the locale is RTL + */ +export function isRTLLanguage(locale: string | null | undefined) { + if (!locale) { + return false; + } + const code = locale.toLowerCase().split(/[-_]/)[0]; + return rtlLanguageCodes.has(code); +} diff --git a/yarn.lock b/yarn.lock index 6d6a6e1b56..b728060f68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4965,7 +4965,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-direction@npm:1.1.1": +"@radix-ui/react-direction@npm:1.1.1, @radix-ui/react-direction@npm:^1.1.1": version: 1.1.1 resolution: "@radix-ui/react-direction@npm:1.1.1" peerDependencies: @@ -16940,6 +16940,7 @@ __metadata: "@radix-ui/react-collapsible": "npm:^1.1.12" "@radix-ui/react-context-menu": "npm:^2.2.16" "@radix-ui/react-dialog": "npm:^1.1.15" + "@radix-ui/react-direction": "npm:^1.1.1" "@radix-ui/react-dropdown-menu": "npm:^2.1.16" "@radix-ui/react-one-time-password-field": "npm:^0.1.8" "@radix-ui/react-popover": "npm:^1.1.15"