mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
feat: RTL layout (#12107)
* First pass * Remove prop drilling, fix comment layout * Revert dev:watch to use dev:backend Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;"};
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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")`
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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)<ContentProps>`
|
||||
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;`}
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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] &,
|
||||
|
||||
@@ -69,7 +69,7 @@ function AppSidebar() {
|
||||
model={team}
|
||||
size={24}
|
||||
alt={t("Logo")}
|
||||
style={{ marginLeft: 4 }}
|
||||
style={{ insetInlineStart: 4 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
@@ -18,25 +19,25 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
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"
|
||||
>
|
||||
<Position style={style} column>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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<HTMLDivElement, Props>(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<HTMLDivElement, Props>(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<HTMLDivElement, Props>(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<HTMLDivElement, Props>(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<HTMLDivElement, Props>(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<HTMLDivElement, Props>(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)<ContainerProps>`
|
||||
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)<ContainerProps>`
|
||||
? `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 {
|
||||
|
||||
@@ -118,7 +118,7 @@ const DynamicDropCursor = observer(
|
||||
);
|
||||
|
||||
const Loading = styled(PlaceholderCollections)`
|
||||
margin-left: 44px;
|
||||
margin-inline-start: 44px;
|
||||
min-height: 90px;
|
||||
`;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -91,7 +91,7 @@ function HistoryNavigation(props: React.ComponentProps<typeof Flex>) {
|
||||
|
||||
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)`
|
||||
|
||||
@@ -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()}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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()}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ const ToggleButton = styled(SidebarButton)`
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[dir="rtl"] & svg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
`;
|
||||
|
||||
export default ToggleButton;
|
||||
|
||||
@@ -54,5 +54,5 @@ export default function Version() {
|
||||
}
|
||||
|
||||
const LilBadge = styled(Badge)`
|
||||
margin-left: 0;
|
||||
margin-inline-start: 0;
|
||||
`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,7 +54,6 @@ const tabStyles = `
|
||||
font-size: 14px;
|
||||
cursor: var(--pointer);
|
||||
user-select: none;
|
||||
margin-right: 24px;
|
||||
padding: 6px 0;
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
|
||||
+18
-11
@@ -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 (
|
||||
<ThemeProvider theme={theme}>
|
||||
<>
|
||||
<GlobalStyles
|
||||
useCursorPointer={
|
||||
// Default to showing the cursor pointer if no user is logged in (public share)
|
||||
auth.user?.getPreference(UserPreference.UseCursorPointer) ?? true
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
</ThemeProvider>
|
||||
<DirectionProvider dir={direction}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<>
|
||||
<GlobalStyles
|
||||
useCursorPointer={
|
||||
// Default to showing the cursor pointer if no user is logged in (public share)
|
||||
auth.user?.getPreference(UserPreference.UseCursorPointer) ?? true
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
</ThemeProvider>
|
||||
</DirectionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
`}
|
||||
`;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ type BaseMenuItemProps = {
|
||||
const BaseMenuItemCSS = css<BaseMenuItemProps>`
|
||||
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;
|
||||
`;
|
||||
|
||||
|
||||
@@ -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_({
|
||||
<CollectionIcon collection={collection} size={40} expanded />
|
||||
) : null;
|
||||
|
||||
const dir = isRTL(collection.name) ? "rtl" : "ltr";
|
||||
|
||||
return (
|
||||
<StyledHeading>
|
||||
<IconTitleWrapper>
|
||||
<StyledHeading dir={dir}>
|
||||
<IconTitleWrapper dir={dir}>
|
||||
{canEdit ? (
|
||||
<Suspense fallback={fallbackIcon}>
|
||||
<IconPicker
|
||||
|
||||
@@ -51,8 +51,6 @@ type Props = {
|
||||
animatePresence?: boolean;
|
||||
/** Text to highlight at the top of the comment */
|
||||
highlightedText?: string;
|
||||
/** The text direction of the editor */
|
||||
dir?: "rtl" | "ltr";
|
||||
/** Callback when the editor is focused */
|
||||
onFocus?: () => 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}
|
||||
/>
|
||||
</VisuallyHidden.Root>
|
||||
<Flex gap={8} align="flex-start" reverse={dir === "rtl"}>
|
||||
<Flex gap={8} align="flex-start">
|
||||
<Avatar model={user} size={24} style={{ marginTop: 8 }} />
|
||||
<Bubble
|
||||
gap={10}
|
||||
@@ -341,7 +338,7 @@ function CommentForm({
|
||||
/>
|
||||
</React.Suspense>
|
||||
{(inputFocused || draft) && (
|
||||
<Flex justify="space-between" reverse={dir === "rtl"} gap={8}>
|
||||
<Flex justify="space-between" gap={8}>
|
||||
<HStack>
|
||||
<ButtonSmall type="submit" borderOnHover>
|
||||
{thread && !thread.isNew ? t("Reply") : t("Post")}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Flex gap={8} align="flex-start" reverse={dir === "rtl"}>
|
||||
<Flex gap={8} align="flex-start">
|
||||
{firstOfAuthor && (
|
||||
<AvatarSpacer>
|
||||
<Avatar model={comment.createdBy} size={24} />
|
||||
@@ -210,12 +207,11 @@ function CommentThreadItem({
|
||||
$firstOfThread={firstOfThread}
|
||||
$firstOfAuthor={firstOfAuthor}
|
||||
$lastOfThread={lastOfThread}
|
||||
$dir={dir}
|
||||
$canReply={canReply}
|
||||
column
|
||||
>
|
||||
{(showAuthor || showTime) && (
|
||||
<Meta size="xsmall" type="secondary" dir={dir}>
|
||||
<Meta size="xsmall" type="secondary">
|
||||
{showAuthor && <em>{comment.createdBy.name}</em>}
|
||||
{showAuthor && showTime && <> · </>}
|
||||
{showTime && (
|
||||
@@ -277,7 +273,7 @@ function CommentThreadItem({
|
||||
</Body>
|
||||
<EventBoundary>
|
||||
{!isEditing && (
|
||||
<Actions gap={4} dir={dir}>
|
||||
<Actions gap={4}>
|
||||
{!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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")};
|
||||
|
||||
@@ -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 <RightSidebar>{inner}</RightSidebar>;
|
||||
return <Aside>{inner}</Aside>;
|
||||
}
|
||||
|
||||
const ForwardIcon = styled(BackIcon)`
|
||||
transform: rotate(180deg);
|
||||
flex-shrink: 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled(Flex)`
|
||||
|
||||
@@ -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 (
|
||||
<RightSidebar skipInitialAnimation={skipInitialAnimation}>
|
||||
<Aside skipInitialAnimation={skipInitialAnimation}>
|
||||
<RightSidebarWrappedContext.Provider value={true}>
|
||||
{inner}
|
||||
</RightSidebarWrappedContext.Provider>
|
||||
</RightSidebar>
|
||||
</Aside>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
|
||||
@@ -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")};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user