From 735aaa668a3f334eb9211e628262ce3d01ecfed5 Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Tue, 1 Feb 2022 20:58:24 -0800 Subject: [PATCH] fix: add toc to mobile views and account for branding on shared view layouts (#2997) * fix: add toc to mobile views and center the branding * add padding to bottom of sidebar * put the mobile branding inline * finesse the padding * make spelling of sign-in email less crazy looking * move mobile sidebar button into header * adds scene to search and 404 pages * fix title alignment * make filter buttons tight * clean up unused imports * lint Co-authored-by: Tom Moor --- app/components/Branding.tsx | 16 +++-- app/components/FilterOptions.tsx | 4 +- app/components/Header.tsx | 71 ++++++++++++++++----- app/components/Layout.tsx | 26 -------- app/components/Scene.tsx | 3 +- app/components/Sidebar/Shared.tsx | 9 ++- app/scenes/Document/components/Document.tsx | 10 ++- app/scenes/Document/components/Header.tsx | 41 ++++++------ app/scenes/Error404.tsx | 10 ++- app/scenes/Search/Search.tsx | 19 ++---- server/emails/SigninEmail.tsx | 6 +- shared/i18n/locales/en_US/translation.json | 3 +- 12 files changed, 119 insertions(+), 99 deletions(-) diff --git a/app/components/Branding.tsx b/app/components/Branding.tsx index 21dd5005a..9087de147 100644 --- a/app/components/Branding.tsx +++ b/app/components/Branding.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import styled from "styled-components"; +import breakpoint from "styled-components-breakpoint"; import env from "~/env"; import OutlineLogo from "./OutlineLogo"; @@ -17,10 +18,8 @@ function Branding({ href = env.URL }: Props) { } const Link = styled.a` - z-index: ${(props) => props.theme.depths.sidebar + 1}; - position: fixed; - bottom: 0; - left: 0; + justify-content: center; + padding-bottom: 16px; font-weight: 600; font-size: 14px; @@ -29,7 +28,6 @@ const Link = styled.a` color: ${(props) => props.theme.text}; display: flex; align-items: center; - padding: 16px; svg { fill: ${(props) => props.theme.text}; @@ -38,6 +36,14 @@ const Link = styled.a` &:hover { background: ${(props) => props.theme.sidebarBackground}; } + + ${breakpoint("tablet")` + z-index: ${(props: any) => props.theme.depths.sidebar + 1}; + position: fixed; + bottom: 0; + left: 0; + padding: 16px; + `}; `; export default Branding; diff --git a/app/components/FilterOptions.tsx b/app/components/FilterOptions.tsx index 30caeae83..b288edc39 100644 --- a/app/components/FilterOptions.tsx +++ b/app/components/FilterOptions.tsx @@ -94,13 +94,15 @@ const StyledButton = styled(Button)` box-shadow: none; text-transform: none; border-color: transparent; + height: auto; &:hover { background: transparent; } ${Inner} { - line-height: 28px; + line-height: 24px; + min-height: auto; } `; diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 71a143e8e..299fb2d20 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -1,19 +1,31 @@ import { throttle } from "lodash"; import { observer } from "mobx-react"; +import { MenuIcon } from "outline-icons"; import { transparentize } from "polished"; import * as React from "react"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; +import Button from "~/components/Button"; import Fade from "~/components/Fade"; import Flex from "~/components/Flex"; +import useMobile from "~/hooks/useMobile"; +import useStores from "~/hooks/useStores"; type Props = { breadcrumb?: React.ReactNode; title: React.ReactNode; actions?: React.ReactNode; + hasSidebar?: boolean; }; -function Header({ breadcrumb, title, actions }: Props) { +function Header({ breadcrumb, title, actions, hasSidebar }: Props) { + const { ui } = useStores(); + const isMobile = useMobile(); + + const hasMobileSidebar = hasSidebar && isMobile; + + const passThrough = !actions && !breadcrumb && !title; + const [isScrolled, setScrolled] = React.useState(false); const handleScroll = React.useCallback( throttle(() => setScrolled(window.scrollY > 75), 50), @@ -33,8 +45,21 @@ function Header({ breadcrumb, title, actions }: Props) { }, []); return ( - - {breadcrumb ? {breadcrumb} : null} + + {breadcrumb || hasMobileSidebar ? ( + + {hasMobileSidebar && ( + } + iconColor="currentColor" + neutral + /> + )} + {breadcrumb} + + ) : null} + {isScrolled ? ( <Fade>{title}</Fade> @@ -42,11 +67,9 @@ function Header({ breadcrumb, title, actions }: Props) { ) : ( <div /> )} - {actions && ( - <Actions align="center" justify="flex-end"> - {actions} - </Actions> - )} + <Actions align="center" justify="flex-end"> + {actions} + </Actions> </Wrapper> ); } @@ -56,12 +79,7 @@ const Breadcrumbs = styled("div")` flex-basis: 0; align-items: center; padding-right: 8px; - - /* Don't show breadcrumbs on mobile */ - display: none; - ${breakpoint("tablet")` display: flex; -`}; `; const Actions = styled(Flex)` @@ -75,11 +93,23 @@ const Actions = styled(Flex)` `}; `; -const Wrapper = styled(Flex)` - position: sticky; +const Wrapper = styled(Flex)<{ $passThrough?: boolean }>` top: 0; z-index: ${(props) => props.theme.depths.header}; + position: sticky; background: ${(props) => props.theme.background}; + + ${(props) => + props.$passThrough + ? ` + background: transparent; + pointer-events: none; + ` + : ` + background: ${transparentize(0.2, props.theme.background)}; + backdrop-filter: blur(20px); + `}; + padding: 12px; transition: all 100ms ease-out; transform: translate3d(0, 0, 0); @@ -111,7 +141,7 @@ const Title = styled("div")` cursor: pointer; min-width: 0; - ${breakpoint("tablet")` + ${breakpoint("tablet")` padding-left: 0; display: block; `}; @@ -126,4 +156,13 @@ const Title = styled("div")` } `; +const MobileMenuButton = styled(Button)` + margin-right: 8px; + pointer-events: auto; + + @media print { + display: none; + } +`; + export default observer(Header); diff --git a/app/components/Layout.tsx b/app/components/Layout.tsx index 7e9b9ae88..043699351 100644 --- a/app/components/Layout.tsx +++ b/app/components/Layout.tsx @@ -1,10 +1,8 @@ import { observer } from "mobx-react"; -import { MenuIcon } from "outline-icons"; import * as React from "react"; import { Helmet } from "react-helmet"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; -import Button from "~/components/Button"; import Flex from "~/components/Flex"; import { LoadingIndicatorBar } from "~/components/LoadingIndicator"; import SkipNavContent from "~/components/SkipNavContent"; @@ -41,15 +39,6 @@ function Layout({ title, children, sidebar, rightRail }: Props) { {ui.progressBarVisible && <LoadingIndicatorBar />} - {sidebar && ( - <MobileMenuButton - onClick={ui.toggleMobileSidebar} - icon={<MenuIcon />} - iconColor="currentColor" - neutral - /> - )} - <Container auto> {sidebar} @@ -85,21 +74,6 @@ const Container = styled(Flex)` min-height: 100%; `; -const MobileMenuButton = styled(Button)` - position: fixed; - top: 12px; - left: 12px; - z-index: ${(props) => props.theme.depths.sidebar - 1}; - - ${breakpoint("tablet")` - display: none; - `}; - - @media print { - display: none; - } -`; - const Content = styled(Flex)<{ $isResizing?: boolean; $sidebarCollapsed?: boolean; diff --git a/app/components/Scene.tsx b/app/components/Scene.tsx index 775827934..73b3f0722 100644 --- a/app/components/Scene.tsx +++ b/app/components/Scene.tsx @@ -6,7 +6,7 @@ import PageTitle from "~/components/PageTitle"; type Props = { icon?: React.ReactNode; - title: React.ReactNode; + title?: React.ReactNode; textTitle?: string; children: React.ReactNode; breadcrumb?: React.ReactNode; @@ -27,6 +27,7 @@ function Scene({ <FillWidth> <PageTitle title={textTitle || title} /> <Header + hasSidebar title={ icon ? ( <> diff --git a/app/components/Sidebar/Shared.tsx b/app/components/Sidebar/Shared.tsx index f0f106071..0fcbc86f9 100644 --- a/app/components/Sidebar/Shared.tsx +++ b/app/components/Sidebar/Shared.tsx @@ -1,5 +1,6 @@ import { observer } from "mobx-react"; import * as React from "react"; +import styled from "styled-components"; import Scrollable from "~/components/Scrollable"; import useStores from "~/hooks/useStores"; import { NavigationNode } from "~/types"; @@ -17,7 +18,7 @@ function SharedSidebar({ rootNode, shareId }: Props) { return ( <Sidebar> - <Scrollable flex> + <ScrollContainer flex> <Section> <DocumentLink index={0} @@ -27,9 +28,13 @@ function SharedSidebar({ rootNode, shareId }: Props) { activeDocument={documents.active} /> </Section> - </Scrollable> + </ScrollContainer> </Sidebar> ); } +const ScrollContainer = styled(Scrollable)` + padding-bottom: 16px; +`; + export default observer(SharedSidebar); diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 364477eb2..1a10fcb80 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -394,10 +394,12 @@ class DocumentScene extends React.Component<Props> { const value = revision ? revision.text : document.text; const embedsDisabled = (team && team.documentEmbeds === false) || document.embedsDisabled; + const headings = this.editor.current ? // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'. this.editor.current.getHeadings() : []; + const showContents = ui.tocVisible && (readOnly || team?.collaborativeEditing); const collaborativeEditing = @@ -615,11 +617,11 @@ class DocumentScene extends React.Component<Props> { </Flex> </React.Suspense> </MaxWidth> + {isShare && !isCustomDomain() && ( + <Branding href="//www.getoutline.com?ref=sharelink" /> + )} </Container> </Background> - {isShare && !isCustomDomain() && ( - <Branding href="//www.getoutline.com?ref=sharelink" /> - )} {!isShare && ( <> <KeyboardShortcutsButton /> @@ -666,6 +668,8 @@ const MaxWidth = styled(Flex)<MaxWidthProps>` max-width: 100vw; width: 100%; + padding-bottom: 16px; + ${breakpoint("tablet")` margin: 4px auto 12px; max-width: ${(props: MaxWidthProps) => diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index f401126b0..efd90fcc5 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -164,14 +164,19 @@ function DocumentHeader({ return ( <Header title={document.title} + hasSidebar={!!sharedTree} breadcrumb={ - <PublicBreadcrumb - documentId={document.id} - shareId={shareId} - sharedTree={sharedTree} - > - {toc} - </PublicBreadcrumb> + isMobile ? ( + <TableOfContentsMenu headings={headings} /> + ) : ( + <PublicBreadcrumb + documentId={document.id} + shareId={shareId} + sharedTree={sharedTree} + > + {toc} + </PublicBreadcrumb> + ) } actions={ <> @@ -186,10 +191,15 @@ function DocumentHeader({ return ( <> <Header + hasSidebar breadcrumb={ - <DocumentBreadcrumb document={document}> - {!isEditing && toc} - </DocumentBreadcrumb> + isMobile ? ( + <TableOfContentsMenu headings={headings} /> + ) : ( + <DocumentBreadcrumb document={document}> + {!isEditing && toc} + </DocumentBreadcrumb> + ) } title={ <> @@ -200,11 +210,7 @@ function DocumentHeader({ actions={ <> <ObservingBanner /> - {isMobile && ( - <TocWrapper> - <TableOfContentsMenu headings={headings} /> - </TocWrapper> - )} + {!isPublishing && isSaving && !team?.collaborativeEditing && ( <Status>{t("Saving")}…</Status> )} @@ -328,9 +334,4 @@ const Status = styled(Action)` color: ${(props) => props.theme.slate}; `; -const TocWrapper = styled(Action)` - position: absolute; - left: 42px; -`; - export default observer(DocumentHeader); diff --git a/app/scenes/Error404.tsx b/app/scenes/Error404.tsx index 209367657..a78c0c37c 100644 --- a/app/scenes/Error404.tsx +++ b/app/scenes/Error404.tsx @@ -1,23 +1,21 @@ import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; import { Link } from "react-router-dom"; -import CenteredContent from "~/components/CenteredContent"; import Empty from "~/components/Empty"; -import PageTitle from "~/components/PageTitle"; +import Scene from "~/components/Scene"; const Error404 = () => { const { t } = useTranslation(); return ( - <CenteredContent> - <PageTitle title={t("Not found")} /> - <h1>{t("Not found")}</h1> + <Scene title={t("Not Found")}> + <h1>{t("Not Found")}</h1> <Empty> <Trans> We were unable to find the page you’re looking for. Go to the{" "} <Link to="/home">homepage</Link>? </Trans> </Empty> - </CenteredContent> + </Scene> ); }; diff --git a/app/scenes/Search/Search.tsx b/app/scenes/Search/Search.tsx index 023ae3c3d..e0f8445cb 100644 --- a/app/scenes/Search/Search.tsx +++ b/app/scenes/Search/Search.tsx @@ -14,15 +14,14 @@ import { DateFilter as TDateFilter } from "@shared/types"; import { DEFAULT_PAGINATION_LIMIT } from "~/stores/BaseStore"; import { SearchParams } from "~/stores/DocumentsStore"; import RootStore from "~/stores/RootStore"; -import CenteredContent from "~/components/CenteredContent"; import DocumentListItem from "~/components/DocumentListItem"; import Empty from "~/components/Empty"; import Fade from "~/components/Fade"; import Flex from "~/components/Flex"; import HelpText from "~/components/HelpText"; import LoadingIndicator from "~/components/LoadingIndicator"; -import PageTitle from "~/components/PageTitle"; import RegisterKeyDown from "~/components/RegisterKeyDown"; +import Scene from "~/components/Scene"; import withStores from "~/components/withStores"; import { searchUrl } from "~/utils/routeHelpers"; import { decodeURIComponentSafe } from "~/utils/urls"; @@ -259,8 +258,7 @@ class Search extends React.Component<Props> { const showEmpty = !this.isLoading && this.query && results.length === 0; return ( - <Container> - <PageTitle title={this.title} /> + <Scene textTitle={this.title}> <RegisterKeyDown trigger="Escape" handler={this.goBack} /> {this.isLoading && <LoadingIndicator />} {notFound && ( @@ -351,7 +349,7 @@ class Search extends React.Component<Props> { )} </ResultList> </ResultsWrapper> - </Container> + </Scene> ); } } @@ -363,15 +361,8 @@ const Centered = styled(Flex)` transform: translateY(-50%); `; -const Container = styled(CenteredContent)` - > div { - position: relative; - height: 100%; - } -`; - const ResultsWrapper = styled(Flex)` - ${breakpoint("tablet")` + ${breakpoint("tablet")` margin-top: 40px; `}; `; @@ -394,7 +385,7 @@ const Filters = styled(Flex)` overflow-x: auto; padding: 8px 0; - ${breakpoint("tablet")` + ${breakpoint("tablet")` padding: 0; `}; diff --git a/server/emails/SigninEmail.tsx b/server/emails/SigninEmail.tsx index 7e0773e85..cc9357af2 100644 --- a/server/emails/SigninEmail.tsx +++ b/server/emails/SigninEmail.tsx @@ -27,8 +27,8 @@ export const SigninEmail = ({ token, teamUrl }: Props) => { <Header /> <Body> - <Heading>Magic signin link</Heading> - <p>Click the button below to signin to Outline.</p> + <Heading>Magic Sign-in Link</Heading> + <p>Click the button below to sign in to Outline.</p> <EmptySpace height={10} /> <p> <Button @@ -40,7 +40,7 @@ export const SigninEmail = ({ token, teamUrl }: Props) => { <EmptySpace height={10} /> <p> If your magic link expired you can request a new one from your team’s - signin page at: <a href={teamUrl}>{teamUrl}</a> + sign-in page at: <a href={teamUrl}>{teamUrl}</a> </p> </Body> diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 87d6bccbd..7f195cf07 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -417,7 +417,7 @@ "Search documents": "Search documents", "No documents found for your filters.": "No documents found for your filters.", "You’ve not got any drafts at the moment.": "You’ve not got any drafts at the moment.", - "Not found": "Not found", + "Not Found": "Not Found", "We were unable to find the page you’re looking for. Go to the <2>homepage</2>?": "We were unable to find the page you’re looking for. Go to the <2>homepage</2>?", "Offline": "Offline", "We were unable to load the document while offline.": "We were unable to load the document while offline.", @@ -510,7 +510,6 @@ "Include documents that are in the archive": "Include documents that are in the archive", "Any author": "Any author", "Author": "Author", - "Not Found": "Not Found", "We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.", "No documents found for your search filters.": "No documents found for your search filters.", "Processing": "Processing",