mirror of
https://github.com/outline/outline.git
synced 2026-01-06 11:09:55 -06:00
feat: Display right sidebar as drawer on mobile (#10175)
* wip * Stack metadata on mobile * fix: Allow viewing history
This commit is contained in:
@@ -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) && (
|
||||
<Route path={`/doc/${slug}`}>
|
||||
<SidebarRight>
|
||||
<React.Suspense fallback={null}>
|
||||
{showHistory && <DocumentHistory />}
|
||||
{showComments && <DocumentComments />}
|
||||
</React.Suspense>
|
||||
</SidebarRight>
|
||||
<React.Suspense fallback={null}>
|
||||
{showHistory && <DocumentHistory />}
|
||||
{showComments && <DocumentComments />}
|
||||
</React.Suspense>
|
||||
</Route>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -155,14 +155,16 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
}
|
||||
return (
|
||||
<Viewed>
|
||||
• <Modified highlight>{t("Never viewed")}</Modified>
|
||||
<Separator />
|
||||
<Modified highlight>{t("Never viewed")}</Modified>
|
||||
</Viewed>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Viewed>
|
||||
• {t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
|
||||
<Separator />
|
||||
{t("Viewed")} <Time dateTime={lastViewedAt} addSuffix shorten />
|
||||
</Viewed>
|
||||
);
|
||||
};
|
||||
@@ -186,16 +188,17 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
)}
|
||||
{showParentDocuments && nestedDocumentsCount > 0 && (
|
||||
<span>
|
||||
• {nestedDocumentsCount}{" "}
|
||||
<Separator />
|
||||
{nestedDocumentsCount}{" "}
|
||||
{t("nested document", {
|
||||
count: nestedDocumentsCount,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{timeSinceNow()}
|
||||
{timeSinceNow()}
|
||||
{canShowProgressBar && (
|
||||
<>
|
||||
•
|
||||
<Separator />
|
||||
<DocumentTasks document={document} />
|
||||
</>
|
||||
)}
|
||||
@@ -204,6 +207,14 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const Separator = styled.span`
|
||||
padding: 0 0.4em;
|
||||
|
||||
&::after {
|
||||
content: "•";
|
||||
}
|
||||
`;
|
||||
|
||||
const Strong = styled.strong`
|
||||
font-weight: 550;
|
||||
`;
|
||||
|
||||
@@ -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) {
|
||||
<Sidebar {...animationProps} $border={border} className={className}>
|
||||
<Position style={style} column>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
{!isMobile && (
|
||||
<ResizeBorder
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={handleReset}
|
||||
dir="right"
|
||||
/>
|
||||
)}
|
||||
<ResizeBorder
|
||||
onMouseDown={handleMouseDown}
|
||||
onDoubleClick={handleReset}
|
||||
dir="right"
|
||||
/>
|
||||
</Position>
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@@ -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<typeof DrawerPrimitive.Content>,
|
||||
@@ -56,11 +58,9 @@ const DrawerTitle = React.forwardRef<
|
||||
const { hidden, children, ...rest } = props;
|
||||
|
||||
const title = (
|
||||
<TitleWrapper justify="center">
|
||||
<Text size="medium" weight="bold">
|
||||
{children}
|
||||
</Text>
|
||||
</TitleWrapper>
|
||||
<Text size="medium" weight="bold" as={TitleWrapper} justify="center">
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -100,4 +100,4 @@ const TitleWrapper = styled(Flex)`
|
||||
padding: 8px 0;
|
||||
`;
|
||||
|
||||
export { Drawer, DrawerTrigger, DrawerContent, DrawerTitle };
|
||||
export { Drawer, DrawerTrigger, DrawerHandle, DrawerContent, DrawerTitle };
|
||||
|
||||
@@ -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 (
|
||||
<Sidebar
|
||||
title={
|
||||
<Flex align="center" justify="space-between" auto>
|
||||
<span>{t("Comments")}</span>
|
||||
<Flex align="center" justify="space-between" gap={8} auto>
|
||||
<div style={isMobile ? { padding: "0 8px" } : undefined}>
|
||||
{t("Comments")}
|
||||
</div>
|
||||
<CommentSortMenu
|
||||
viewingResolved={viewingResolved}
|
||||
onChange={(val) => {
|
||||
@@ -184,7 +189,7 @@ function Comments() {
|
||||
</Wrapper>
|
||||
</Scrollable>
|
||||
<AnimatePresence initial={false}>
|
||||
{!focusedComment && can.comment && !viewingResolved && (
|
||||
{(!focusedComment || isMobile) && can.comment && !viewingResolved && (
|
||||
<NewCommentForm
|
||||
draft={draft}
|
||||
onSaveDraft={onSaveDraft}
|
||||
|
||||
@@ -9,12 +9,13 @@ import { TeamPreference } from "@shared/types";
|
||||
import Document from "~/models/Document";
|
||||
import Revision from "~/models/Revision";
|
||||
import { openDocumentInsights } from "~/actions/definitions/documents";
|
||||
import DocumentMeta from "~/components/DocumentMeta";
|
||||
import DocumentMeta, { Separator } from "~/components/DocumentMeta";
|
||||
import Fade from "~/components/Fade";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { documentPath } from "~/utils/routeHelpers";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
|
||||
@@ -46,7 +47,7 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
|
||||
<Meta document={document} revision={revision} to={to} replace {...rest}>
|
||||
{commentingEnabled && can.comment && (
|
||||
<>
|
||||
•
|
||||
<Separator />
|
||||
<CommentLink
|
||||
to={{
|
||||
pathname: documentPath(document),
|
||||
@@ -66,7 +67,7 @@ function TitleDocumentMeta({ to, document, revision, ...rest }: Props) {
|
||||
!document.isDraft &&
|
||||
!document.isTemplate ? (
|
||||
<Wrapper>
|
||||
•
|
||||
<Separator />
|
||||
<InsightsButton action={openDocumentInsights}>
|
||||
{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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<React.HTMLAttributes<HTMLDivElement>, "title"> & {
|
||||
/* The title of the sidebar */
|
||||
@@ -19,7 +23,7 @@ type Props = Omit<React.HTMLAttributes<HTMLDivElement>, "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 ? (
|
||||
<Scrollable hiddenScrollbars topShadow>
|
||||
{children}
|
||||
</Scrollable>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<Drawer onClose={onClose} defaultOpen>
|
||||
<DrawerContent>
|
||||
<DrawerTitle>{title}</DrawerTitle>
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
) : (
|
||||
<RightSidebar>
|
||||
<Header>
|
||||
<Title>{title}</Title>
|
||||
<Tooltip content={t("Close")} shortcut="Esc">
|
||||
@@ -41,35 +60,11 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) {
|
||||
/>
|
||||
</Tooltip>
|
||||
</Header>
|
||||
{scrollable ? (
|
||||
<Scrollable hiddenScrollbars topShadow>
|
||||
{children}
|
||||
</Scrollable>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<Portal>
|
||||
<Backdrop onClick={onClose} />
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
{content}
|
||||
</RightSidebar>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user