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 <tom.moor@gmail.com>
This commit is contained in:
Nan Yu
2022-02-01 20:58:24 -08:00
committed by GitHub
parent 516e2f1b6e
commit 735aaa668a
12 changed files with 119 additions and 99 deletions

View File

@@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import env from "~/env"; import env from "~/env";
import OutlineLogo from "./OutlineLogo"; import OutlineLogo from "./OutlineLogo";
@@ -17,10 +18,8 @@ function Branding({ href = env.URL }: Props) {
} }
const Link = styled.a` const Link = styled.a`
z-index: ${(props) => props.theme.depths.sidebar + 1}; justify-content: center;
position: fixed; padding-bottom: 16px;
bottom: 0;
left: 0;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
@@ -29,7 +28,6 @@ const Link = styled.a`
color: ${(props) => props.theme.text}; color: ${(props) => props.theme.text};
display: flex; display: flex;
align-items: center; align-items: center;
padding: 16px;
svg { svg {
fill: ${(props) => props.theme.text}; fill: ${(props) => props.theme.text};
@@ -38,6 +36,14 @@ const Link = styled.a`
&:hover { &:hover {
background: ${(props) => props.theme.sidebarBackground}; 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; export default Branding;

View File

@@ -94,13 +94,15 @@ const StyledButton = styled(Button)`
box-shadow: none; box-shadow: none;
text-transform: none; text-transform: none;
border-color: transparent; border-color: transparent;
height: auto;
&:hover { &:hover {
background: transparent; background: transparent;
} }
${Inner} { ${Inner} {
line-height: 28px; line-height: 24px;
min-height: auto;
} }
`; `;

View File

@@ -1,19 +1,31 @@
import { throttle } from "lodash"; import { throttle } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { MenuIcon } from "outline-icons";
import { transparentize } from "polished"; import { transparentize } from "polished";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Button from "~/components/Button";
import Fade from "~/components/Fade"; import Fade from "~/components/Fade";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import useMobile from "~/hooks/useMobile";
import useStores from "~/hooks/useStores";
type Props = { type Props = {
breadcrumb?: React.ReactNode; breadcrumb?: React.ReactNode;
title: React.ReactNode; title: React.ReactNode;
actions?: 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 [isScrolled, setScrolled] = React.useState(false);
const handleScroll = React.useCallback( const handleScroll = React.useCallback(
throttle(() => setScrolled(window.scrollY > 75), 50), throttle(() => setScrolled(window.scrollY > 75), 50),
@@ -33,8 +45,21 @@ function Header({ breadcrumb, title, actions }: Props) {
}, []); }, []);
return ( return (
<Wrapper align="center" shrink={false}> <Wrapper align="center" shrink={false} $passThrough={passThrough}>
{breadcrumb ? <Breadcrumbs>{breadcrumb}</Breadcrumbs> : null} {breadcrumb || hasMobileSidebar ? (
<Breadcrumbs>
{hasMobileSidebar && (
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
iconColor="currentColor"
neutral
/>
)}
{breadcrumb}
</Breadcrumbs>
) : null}
{isScrolled ? ( {isScrolled ? (
<Title onClick={handleClickTitle}> <Title onClick={handleClickTitle}>
<Fade>{title}</Fade> <Fade>{title}</Fade>
@@ -42,11 +67,9 @@ function Header({ breadcrumb, title, actions }: Props) {
) : ( ) : (
<div /> <div />
)} )}
{actions && ( <Actions align="center" justify="flex-end">
<Actions align="center" justify="flex-end"> {actions}
{actions} </Actions>
</Actions>
)}
</Wrapper> </Wrapper>
); );
} }
@@ -56,12 +79,7 @@ const Breadcrumbs = styled("div")`
flex-basis: 0; flex-basis: 0;
align-items: center; align-items: center;
padding-right: 8px; padding-right: 8px;
/* Don't show breadcrumbs on mobile */
display: none;
${breakpoint("tablet")`
display: flex; display: flex;
`};
`; `;
const Actions = styled(Flex)` const Actions = styled(Flex)`
@@ -75,11 +93,23 @@ const Actions = styled(Flex)`
`}; `};
`; `;
const Wrapper = styled(Flex)` const Wrapper = styled(Flex)<{ $passThrough?: boolean }>`
position: sticky;
top: 0; top: 0;
z-index: ${(props) => props.theme.depths.header}; z-index: ${(props) => props.theme.depths.header};
position: sticky;
background: ${(props) => props.theme.background}; 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; padding: 12px;
transition: all 100ms ease-out; transition: all 100ms ease-out;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
@@ -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); export default observer(Header);

View File

@@ -1,10 +1,8 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { MenuIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import Button from "~/components/Button";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import { LoadingIndicatorBar } from "~/components/LoadingIndicator"; import { LoadingIndicatorBar } from "~/components/LoadingIndicator";
import SkipNavContent from "~/components/SkipNavContent"; import SkipNavContent from "~/components/SkipNavContent";
@@ -41,15 +39,6 @@ function Layout({ title, children, sidebar, rightRail }: Props) {
{ui.progressBarVisible && <LoadingIndicatorBar />} {ui.progressBarVisible && <LoadingIndicatorBar />}
{sidebar && (
<MobileMenuButton
onClick={ui.toggleMobileSidebar}
icon={<MenuIcon />}
iconColor="currentColor"
neutral
/>
)}
<Container auto> <Container auto>
{sidebar} {sidebar}
@@ -85,21 +74,6 @@ const Container = styled(Flex)`
min-height: 100%; 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)<{ const Content = styled(Flex)<{
$isResizing?: boolean; $isResizing?: boolean;
$sidebarCollapsed?: boolean; $sidebarCollapsed?: boolean;

View File

@@ -6,7 +6,7 @@ import PageTitle from "~/components/PageTitle";
type Props = { type Props = {
icon?: React.ReactNode; icon?: React.ReactNode;
title: React.ReactNode; title?: React.ReactNode;
textTitle?: string; textTitle?: string;
children: React.ReactNode; children: React.ReactNode;
breadcrumb?: React.ReactNode; breadcrumb?: React.ReactNode;
@@ -27,6 +27,7 @@ function Scene({
<FillWidth> <FillWidth>
<PageTitle title={textTitle || title} /> <PageTitle title={textTitle || title} />
<Header <Header
hasSidebar
title={ title={
icon ? ( icon ? (
<> <>

View File

@@ -1,5 +1,6 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import styled from "styled-components";
import Scrollable from "~/components/Scrollable"; import Scrollable from "~/components/Scrollable";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import { NavigationNode } from "~/types"; import { NavigationNode } from "~/types";
@@ -17,7 +18,7 @@ function SharedSidebar({ rootNode, shareId }: Props) {
return ( return (
<Sidebar> <Sidebar>
<Scrollable flex> <ScrollContainer flex>
<Section> <Section>
<DocumentLink <DocumentLink
index={0} index={0}
@@ -27,9 +28,13 @@ function SharedSidebar({ rootNode, shareId }: Props) {
activeDocument={documents.active} activeDocument={documents.active}
/> />
</Section> </Section>
</Scrollable> </ScrollContainer>
</Sidebar> </Sidebar>
); );
} }
const ScrollContainer = styled(Scrollable)`
padding-bottom: 16px;
`;
export default observer(SharedSidebar); export default observer(SharedSidebar);

View File

@@ -394,10 +394,12 @@ class DocumentScene extends React.Component<Props> {
const value = revision ? revision.text : document.text; const value = revision ? revision.text : document.text;
const embedsDisabled = const embedsDisabled =
(team && team.documentEmbeds === false) || document.embedsDisabled; (team && team.documentEmbeds === false) || document.embedsDisabled;
const headings = this.editor.current const headings = this.editor.current
? // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'. ? // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
this.editor.current.getHeadings() this.editor.current.getHeadings()
: []; : [];
const showContents = const showContents =
ui.tocVisible && (readOnly || team?.collaborativeEditing); ui.tocVisible && (readOnly || team?.collaborativeEditing);
const collaborativeEditing = const collaborativeEditing =
@@ -615,11 +617,11 @@ class DocumentScene extends React.Component<Props> {
</Flex> </Flex>
</React.Suspense> </React.Suspense>
</MaxWidth> </MaxWidth>
{isShare && !isCustomDomain() && (
<Branding href="//www.getoutline.com?ref=sharelink" />
)}
</Container> </Container>
</Background> </Background>
{isShare && !isCustomDomain() && (
<Branding href="//www.getoutline.com?ref=sharelink" />
)}
{!isShare && ( {!isShare && (
<> <>
<KeyboardShortcutsButton /> <KeyboardShortcutsButton />
@@ -666,6 +668,8 @@ const MaxWidth = styled(Flex)<MaxWidthProps>`
max-width: 100vw; max-width: 100vw;
width: 100%; width: 100%;
padding-bottom: 16px;
${breakpoint("tablet")` ${breakpoint("tablet")`
margin: 4px auto 12px; margin: 4px auto 12px;
max-width: ${(props: MaxWidthProps) => max-width: ${(props: MaxWidthProps) =>

View File

@@ -164,14 +164,19 @@ function DocumentHeader({
return ( return (
<Header <Header
title={document.title} title={document.title}
hasSidebar={!!sharedTree}
breadcrumb={ breadcrumb={
<PublicBreadcrumb isMobile ? (
documentId={document.id} <TableOfContentsMenu headings={headings} />
shareId={shareId} ) : (
sharedTree={sharedTree} <PublicBreadcrumb
> documentId={document.id}
{toc} shareId={shareId}
</PublicBreadcrumb> sharedTree={sharedTree}
>
{toc}
</PublicBreadcrumb>
)
} }
actions={ actions={
<> <>
@@ -186,10 +191,15 @@ function DocumentHeader({
return ( return (
<> <>
<Header <Header
hasSidebar
breadcrumb={ breadcrumb={
<DocumentBreadcrumb document={document}> isMobile ? (
{!isEditing && toc} <TableOfContentsMenu headings={headings} />
</DocumentBreadcrumb> ) : (
<DocumentBreadcrumb document={document}>
{!isEditing && toc}
</DocumentBreadcrumb>
)
} }
title={ title={
<> <>
@@ -200,11 +210,7 @@ function DocumentHeader({
actions={ actions={
<> <>
<ObservingBanner /> <ObservingBanner />
{isMobile && (
<TocWrapper>
<TableOfContentsMenu headings={headings} />
</TocWrapper>
)}
{!isPublishing && isSaving && !team?.collaborativeEditing && ( {!isPublishing && isSaving && !team?.collaborativeEditing && (
<Status>{t("Saving")}</Status> <Status>{t("Saving")}</Status>
)} )}
@@ -328,9 +334,4 @@ const Status = styled(Action)`
color: ${(props) => props.theme.slate}; color: ${(props) => props.theme.slate};
`; `;
const TocWrapper = styled(Action)`
position: absolute;
left: 42px;
`;
export default observer(DocumentHeader); export default observer(DocumentHeader);

View File

@@ -1,23 +1,21 @@
import * as React from "react"; import * as React from "react";
import { useTranslation, Trans } from "react-i18next"; import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import CenteredContent from "~/components/CenteredContent";
import Empty from "~/components/Empty"; import Empty from "~/components/Empty";
import PageTitle from "~/components/PageTitle"; import Scene from "~/components/Scene";
const Error404 = () => { const Error404 = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<CenteredContent> <Scene title={t("Not Found")}>
<PageTitle title={t("Not found")} /> <h1>{t("Not Found")}</h1>
<h1>{t("Not found")}</h1>
<Empty> <Empty>
<Trans> <Trans>
We were unable to find the page youre looking for. Go to the{" "} We were unable to find the page youre looking for. Go to the{" "}
<Link to="/home">homepage</Link>? <Link to="/home">homepage</Link>?
</Trans> </Trans>
</Empty> </Empty>
</CenteredContent> </Scene>
); );
}; };

View File

@@ -14,15 +14,14 @@ import { DateFilter as TDateFilter } from "@shared/types";
import { DEFAULT_PAGINATION_LIMIT } from "~/stores/BaseStore"; import { DEFAULT_PAGINATION_LIMIT } from "~/stores/BaseStore";
import { SearchParams } from "~/stores/DocumentsStore"; import { SearchParams } from "~/stores/DocumentsStore";
import RootStore from "~/stores/RootStore"; import RootStore from "~/stores/RootStore";
import CenteredContent from "~/components/CenteredContent";
import DocumentListItem from "~/components/DocumentListItem"; import DocumentListItem from "~/components/DocumentListItem";
import Empty from "~/components/Empty"; import Empty from "~/components/Empty";
import Fade from "~/components/Fade"; import Fade from "~/components/Fade";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import HelpText from "~/components/HelpText"; import HelpText from "~/components/HelpText";
import LoadingIndicator from "~/components/LoadingIndicator"; import LoadingIndicator from "~/components/LoadingIndicator";
import PageTitle from "~/components/PageTitle";
import RegisterKeyDown from "~/components/RegisterKeyDown"; import RegisterKeyDown from "~/components/RegisterKeyDown";
import Scene from "~/components/Scene";
import withStores from "~/components/withStores"; import withStores from "~/components/withStores";
import { searchUrl } from "~/utils/routeHelpers"; import { searchUrl } from "~/utils/routeHelpers";
import { decodeURIComponentSafe } from "~/utils/urls"; import { decodeURIComponentSafe } from "~/utils/urls";
@@ -259,8 +258,7 @@ class Search extends React.Component<Props> {
const showEmpty = !this.isLoading && this.query && results.length === 0; const showEmpty = !this.isLoading && this.query && results.length === 0;
return ( return (
<Container> <Scene textTitle={this.title}>
<PageTitle title={this.title} />
<RegisterKeyDown trigger="Escape" handler={this.goBack} /> <RegisterKeyDown trigger="Escape" handler={this.goBack} />
{this.isLoading && <LoadingIndicator />} {this.isLoading && <LoadingIndicator />}
{notFound && ( {notFound && (
@@ -351,7 +349,7 @@ class Search extends React.Component<Props> {
)} )}
</ResultList> </ResultList>
</ResultsWrapper> </ResultsWrapper>
</Container> </Scene>
); );
} }
} }
@@ -363,13 +361,6 @@ const Centered = styled(Flex)`
transform: translateY(-50%); transform: translateY(-50%);
`; `;
const Container = styled(CenteredContent)`
> div {
position: relative;
height: 100%;
}
`;
const ResultsWrapper = styled(Flex)` const ResultsWrapper = styled(Flex)`
${breakpoint("tablet")` ${breakpoint("tablet")`
margin-top: 40px; margin-top: 40px;

View File

@@ -27,8 +27,8 @@ export const SigninEmail = ({ token, teamUrl }: Props) => {
<Header /> <Header />
<Body> <Body>
<Heading>Magic signin link</Heading> <Heading>Magic Sign-in Link</Heading>
<p>Click the button below to signin to Outline.</p> <p>Click the button below to sign in to Outline.</p>
<EmptySpace height={10} /> <EmptySpace height={10} />
<p> <p>
<Button <Button
@@ -40,7 +40,7 @@ export const SigninEmail = ({ token, teamUrl }: Props) => {
<EmptySpace height={10} /> <EmptySpace height={10} />
<p> <p>
If your magic link expired you can request a new one from your teams If your magic link expired you can request a new one from your teams
signin page at: <a href={teamUrl}>{teamUrl}</a> sign-in page at: <a href={teamUrl}>{teamUrl}</a>
</p> </p>
</Body> </Body>

View File

@@ -417,7 +417,7 @@
"Search documents": "Search documents", "Search documents": "Search documents",
"No documents found for your filters.": "No documents found for your filters.", "No documents found for your filters.": "No documents found for your filters.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.", "Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.",
"Not found": "Not found", "Not Found": "Not Found",
"We were unable to find the page youre looking for. Go to the <2>homepage</2>?": "We were unable to find the page youre looking for. Go to the <2>homepage</2>?", "We were unable to find the page youre looking for. Go to the <2>homepage</2>?": "We were unable to find the page youre looking for. Go to the <2>homepage</2>?",
"Offline": "Offline", "Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while 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", "Include documents that are in the archive": "Include documents that are in the archive",
"Any author": "Any author", "Any author": "Any author",
"Author": "Author", "Author": "Author",
"Not Found": "Not Found",
"We were unable to find the page youre looking for.": "We were unable to find the page youre looking for.", "We were unable to find the page youre looking for.": "We were unable to find the page youre looking for.",
"No documents found for your search filters.": "No documents found for your search filters.", "No documents found for your search filters.": "No documents found for your search filters.",
"Processing": "Processing", "Processing": "Processing",