From 7303970118bfb0d0b9cc5ad244472b4e79cb9f46 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 14 Sep 2025 23:09:33 +0200 Subject: [PATCH] feat: Display right sidebar as drawer on mobile (#10175) * wip * Stack metadata on mobile * fix: Allow viewing history --- app/components/AuthenticatedLayout.tsx | 11 ++-- app/components/DocumentMeta.tsx | 21 +++++-- app/components/Sidebar/Right.tsx | 14 ++--- app/components/primitives/Drawer.tsx | 12 ++-- app/scenes/Document/components/Comments.tsx | 11 +++- .../Document/components/DocumentMeta.tsx | 17 +++++- app/scenes/Document/components/History.tsx | 6 ++ .../Document/components/SidebarLayout.tsx | 59 +++++++++---------- 8 files changed, 86 insertions(+), 65 deletions(-) diff --git a/app/components/AuthenticatedLayout.tsx b/app/components/AuthenticatedLayout.tsx index c565c1cd65..2289b74242 100644 --- a/app/components/AuthenticatedLayout.tsx +++ b/app/components/AuthenticatedLayout.tsx @@ -13,7 +13,6 @@ import ErrorSuspended from "~/scenes/Errors/ErrorSuspended"; import Layout from "~/components/Layout"; import RegisterKeyDown from "~/components/RegisterKeyDown"; import Sidebar from "~/components/Sidebar"; -import SidebarRight from "~/components/Sidebar/Right"; import SettingsSidebar from "~/components/Sidebar/Settings"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import { usePostLoginPath } from "~/hooks/useLastVisitedPath"; @@ -109,12 +108,10 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => { > {(showHistory || showComments) && ( - - - {showHistory && } - {showComments && } - - + + {showHistory && } + {showComments && } + )} diff --git a/app/components/DocumentMeta.tsx b/app/components/DocumentMeta.tsx index 496e96faaf..8bd9d6b6be 100644 --- a/app/components/DocumentMeta.tsx +++ b/app/components/DocumentMeta.tsx @@ -155,14 +155,16 @@ const DocumentMeta: React.FC = ({ } return ( - • {t("Never viewed")} + + {t("Never viewed")} ); } return ( - • {t("Viewed")} ); }; @@ -186,16 +188,17 @@ const DocumentMeta: React.FC = ({ )} {showParentDocuments && nestedDocumentsCount > 0 && ( -  • {nestedDocumentsCount}{" "} + + {nestedDocumentsCount}{" "} {t("nested document", { count: nestedDocumentsCount, })} )} -  {timeSinceNow()} + {timeSinceNow()} {canShowProgressBar && ( <> -  •  + )} @@ -204,6 +207,14 @@ const DocumentMeta: React.FC = ({ ); }; +export const Separator = styled.span` + padding: 0 0.4em; + + &::after { + content: "•"; + } +`; + const Strong = styled.strong` font-weight: 550; `; diff --git a/app/components/Sidebar/Right.tsx b/app/components/Sidebar/Right.tsx index b70e2e3189..779b1168c3 100644 --- a/app/components/Sidebar/Right.tsx +++ b/app/components/Sidebar/Right.tsx @@ -7,7 +7,6 @@ import { depths, s } from "@shared/styles"; import ErrorBoundary from "~/components/ErrorBoundary"; import Flex from "~/components/Flex"; import ResizeBorder from "~/components/Sidebar/components/ResizeBorder"; -import useMobile from "~/hooks/useMobile"; import useStores from "~/hooks/useStores"; import { sidebarAppearDuration } from "~/styles/animations"; @@ -20,7 +19,6 @@ function Right({ children, border, className }: Props) { const theme = useTheme(); const { ui } = useStores(); const [isResizing, setResizing] = React.useState(false); - const isMobile = useMobile(); const maxWidth = theme.sidebarMaxWidth; const minWidth = theme.sidebarMinWidth + 16; // padding @@ -100,13 +98,11 @@ function Right({ children, border, className }: Props) { {children} - {!isMobile && ( - - )} + ); diff --git a/app/components/primitives/Drawer.tsx b/app/components/primitives/Drawer.tsx index 8ea6553d5f..d0ffa68ceb 100644 --- a/app/components/primitives/Drawer.tsx +++ b/app/components/primitives/Drawer.tsx @@ -18,6 +18,8 @@ Drawer.displayName = "Drawer"; /** Drawer's trigger. */ const DrawerTrigger = DrawerPrimitive.Trigger; +const DrawerHandle = DrawerPrimitive.Handle; + /** Drawer's content - renders the overlay and the actual content. */ const DrawerContent = React.forwardRef< React.ElementRef, @@ -56,11 +58,9 @@ const DrawerTitle = React.forwardRef< const { hidden, children, ...rest } = props; const title = ( - - - {children} - - + + {children} + ); return ( @@ -100,4 +100,4 @@ const TitleWrapper = styled(Flex)` padding: 8px 0; `; -export { Drawer, DrawerTrigger, DrawerContent, DrawerTitle }; +export { Drawer, DrawerTrigger, DrawerHandle, DrawerContent, DrawerTitle }; diff --git a/app/scenes/Document/components/Comments.tsx b/app/scenes/Document/components/Comments.tsx index 8c075eb2f6..5277daece2 100644 --- a/app/scenes/Document/components/Comments.tsx +++ b/app/scenes/Document/components/Comments.tsx @@ -23,6 +23,7 @@ import CommentForm from "./CommentForm"; import CommentSortMenu from "./CommentSortMenu"; import CommentThread from "./CommentThread"; import Sidebar from "./SidebarLayout"; +import useMobile from "~/hooks/useMobile"; import { ArrowDownIcon } from "~/components/Icons/ArrowIcon"; function Comments() { @@ -34,6 +35,8 @@ function Comments() { const document = documents.get(match.params.documentSlug); const focusedComment = useFocusedComment(); const can = usePolicy(document); + const isMobile = useMobile(); + const query = useQuery(); const [viewingResolved, setViewingResolved] = useState( query.get("resolved") !== null || focusedComment?.isResolved || false @@ -130,8 +133,10 @@ function Comments() { return ( - {t("Comments")} + +
+ {t("Comments")} +
{ @@ -184,7 +189,7 @@ function Comments() { - {!focusedComment && can.comment && !viewingResolved && ( + {(!focusedComment || isMobile) && can.comment && !viewingResolved && ( {commentingEnabled && can.comment && ( <> -  •  + -  •  + {t("Viewed by")}{" "} {onlyYou @@ -108,6 +109,16 @@ export const Meta = styled(DocumentMeta)<{ rtl?: boolean }>` user-select: none; z-index: 1; + ${breakpoint("mobile", "tablet")` + flex-direction: column; + align-items: flex-start; + line-height: 1.6; + + ${Separator} { + display: none; + } + `} + a { color: inherit; cursor: var(--pointer); diff --git a/app/scenes/Document/components/History.tsx b/app/scenes/Document/components/History.tsx index 6474006733..3be656da09 100644 --- a/app/scenes/Document/components/History.tsx +++ b/app/scenes/Document/components/History.tsx @@ -15,6 +15,7 @@ import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext"; import useStores from "~/hooks/useStores"; import { documentPath } from "~/utils/routeHelpers"; import Sidebar from "./SidebarLayout"; +import useMobile from "~/hooks/useMobile"; const DocumentEvents = [ "documents.publish", @@ -37,6 +38,7 @@ function History() { const document = documents.get(match.params.documentSlug); const [revisionsOffset, setRevisionsOffset] = React.useState(0); const [eventsOffset, setEventsOffset] = React.useState(0); + const isMobile = useMobile(); const fetchHistory = React.useCallback(async () => { if (!document) { @@ -125,6 +127,10 @@ function History() { }, [revisions, document, revisionEvents, nonRevisionEvents]); const onCloseHistory = React.useCallback(() => { + if (isMobile) { + // Allow closing the history drawer on mobile to view revision content + return; + } if (document) { history.push({ pathname: documentPath(document), diff --git a/app/scenes/Document/components/SidebarLayout.tsx b/app/scenes/Document/components/SidebarLayout.tsx index 1091f26198..20a3771047 100644 --- a/app/scenes/Document/components/SidebarLayout.tsx +++ b/app/scenes/Document/components/SidebarLayout.tsx @@ -3,15 +3,19 @@ import { BackIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; -import { depths, s, ellipsis } from "@shared/styles"; +import { s, ellipsis } from "@shared/styles"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; -import { Portal } from "~/components/Portal"; import Scrollable from "~/components/Scrollable"; import Tooltip from "~/components/Tooltip"; import useMobile from "~/hooks/useMobile"; import { draggableOnDesktop } from "~/styles"; -import { fadeIn } from "~/styles/animations"; +import RightSidebar from "~/components/Sidebar/Right"; +import { + Drawer, + DrawerContent, + DrawerTitle, +} from "~/components/primitives/Drawer"; type Props = Omit, "title"> & { /* The title of the sidebar */ @@ -19,7 +23,7 @@ type Props = Omit, "title"> & { /* The content of the sidebar */ children: React.ReactNode; /* Called when the sidebar is closed */ - onClose: React.MouseEventHandler; + onClose: () => void; /* Whether the sidebar should be scrollable */ scrollable?: boolean; }; @@ -28,8 +32,23 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) { const { t } = useTranslation(); const isMobile = useMobile(); - return ( - <> + const content = scrollable ? ( + + {children} + + ) : ( + children + ); + + return isMobile ? ( + + + {title} + {content} + + + ) : ( +
{title} @@ -41,35 +60,11 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) { />
- {scrollable ? ( - - {children} - - ) : ( - children - )} - - {isMobile && ( - - - - )} - + {content} +
); } -const Backdrop = styled.a` - animation: ${fadeIn} 250ms ease-in-out; - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - cursor: default; - z-index: ${depths.mobileSidebar - 1}; - background: ${s("backdrop")}; -`; - const ForwardIcon = styled(BackIcon)` transform: rotate(180deg); flex-shrink: 0;