diff --git a/app/components/Actions/Actions.js b/app/components/Actions/Actions.js
index b1b56ec1d..3ac624b85 100644
--- a/app/components/Actions/Actions.js
+++ b/app/components/Actions/Actions.js
@@ -7,6 +7,7 @@ export const Action = styled(Flex)`
justify-content: center;
align-items: center;
padding: 0 0 0 12px;
+ font-size: 15px;
a {
color: ${props => props.theme.text};
@@ -28,7 +29,7 @@ const Actions = styled(Flex)`
left: 0;
border-radius: 3px;
background: rgba(255, 255, 255, 0.9);
- padding: 16px;
+ padding: 12px;
-webkit-backdrop-filter: blur(20px);
@media print {
@@ -37,8 +38,7 @@ const Actions = styled(Flex)`
${breakpoint('tablet')`
left: auto;
- padding: ${props => props.theme.vpadding} ${props =>
- props.theme.hpadding} 8px 8px;
+ padding: 24px;
`};
`;
diff --git a/app/components/Collaborators/Collaborators.js b/app/components/Collaborators/Collaborators.js
index 463e08c1a..0fbeaf1c2 100644
--- a/app/components/Collaborators/Collaborators.js
+++ b/app/components/Collaborators/Collaborators.js
@@ -31,22 +31,21 @@ const Collaborators = ({ document }: Props) => {
return (
-
- {collaborators.map(user => (
-
+ {collaborators.map(user => (
+ 1 ? user.name : tooltip}
+ placement="bottom"
+ key={user.id}
+ >
+
- ))}
-
+
+ ))}
);
};
-const StyledTooltip = styled(Tooltip)`
- display: flex;
- flex-direction: row-reverse;
-`;
-
const AvatarWrapper = styled.div`
width: 24px;
height: 24px;
diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js
index 1b0a89102..f69ac1620 100644
--- a/app/components/Sidebar/Sidebar.js
+++ b/app/components/Sidebar/Sidebar.js
@@ -59,7 +59,7 @@ const Container = styled(Flex)`
background: ${props => props.theme.smoke};
transition: left 100ms ease-out;
margin-left: ${props => (props.mobileSidebarVisible ? 0 : '-100%')};
- z-index: 1;
+ z-index: 2;
@media print {
display: none;
@@ -101,7 +101,7 @@ const Toggle = styled.a`
left: ${props => (props.mobileSidebarVisible ? 'auto' : 0)};
right: ${props => (props.mobileSidebarVisible ? 0 : 'auto')};
z-index: 1;
- margin: 16px;
+ margin: 12px;
${breakpoint('tablet')`
display: none;
diff --git a/app/models/Document.js b/app/models/Document.js
index 0b9c9cfad..65b5c953f 100644
--- a/app/models/Document.js
+++ b/app/models/Document.js
@@ -8,7 +8,7 @@ import UiStore from 'stores/UiStore';
import parseTitle from '../../shared/utils/parseTitle';
import unescape from '../../shared/utils/unescape';
-import type { User } from 'types';
+import type { NavigationNode, User } from 'types';
import BaseModel from './BaseModel';
import Collection from './Collection';
@@ -52,17 +52,11 @@ class Document extends BaseModel {
}
@computed
- get pathToDocument(): Array<{ id: string, title: string }> {
+ get pathToDocument(): NavigationNode[] {
let path;
const traveler = (nodes, previousPath) => {
nodes.forEach(childNode => {
- const newPath = [
- ...previousPath,
- {
- id: childNode.id,
- title: childNode.title,
- },
- ];
+ const newPath = [...previousPath, childNode];
if (childNode.id === this.id) {
path = newPath;
return;
diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js
index 23ef41cf8..937ccf08c 100644
--- a/app/scenes/Document/Document.js
+++ b/app/scenes/Document/Document.js
@@ -21,7 +21,7 @@ import { uploadFile } from 'utils/uploadFile';
import isInternalUrl from 'utils/isInternalUrl';
import Document from 'models/Document';
-import Actions from './components/Actions';
+import Header from './components/Header';
import DocumentMove from './components/DocumentMove';
import UiStore from 'stores/UiStore';
import AuthStore from 'stores/AuthStore';
@@ -278,7 +278,7 @@ class DocumentScene extends React.Component {
) : (
-
+
{this.isEditing && (
{
)}
+ {document &&
+ !isShare && (
+
+ )}
{
toc
/>
- {document &&
- !isShare && (
-
- )}
-
+
)}
);
@@ -328,13 +328,14 @@ class DocumentScene extends React.Component {
}
const MaxWidth = styled(Flex)`
- padding: 0 20px;
+ padding: 0 16px;
max-width: 100vw;
+ width: 100%;
height: 100%;
${breakpoint('tablet')`
- padding: 0;
- margin: 60px;
+ padding: 0 24px;
+ margin: 60px auto;
max-width: 46em;
`};
`;
diff --git a/app/scenes/Document/components/Actions.js b/app/scenes/Document/components/Actions.js
deleted file mode 100644
index dd90aa88a..000000000
--- a/app/scenes/Document/components/Actions.js
+++ /dev/null
@@ -1,130 +0,0 @@
-// @flow
-import * as React from 'react';
-import styled from 'styled-components';
-import { NewDocumentIcon } from 'outline-icons';
-import Document from 'models/Document';
-import { documentEditUrl, documentNewUrl } from 'utils/routeHelpers';
-
-import DocumentMenu from 'menus/DocumentMenu';
-import Collaborators from 'components/Collaborators';
-import Actions, { Action, Separator } from 'components/Actions';
-
-type Props = {
- document: Document,
- isDraft: boolean,
- isEditing: boolean,
- isSaving: boolean,
- isPublishing: boolean,
- savingIsDisabled: boolean,
- onDiscard: () => *,
- onSave: ({
- done?: boolean,
- publish?: boolean,
- autosave?: boolean,
- }) => *,
- history: Object,
-};
-
-class DocumentActions extends React.Component {
- handleNewDocument = () => {
- this.props.history.push(documentNewUrl(this.props.document));
- };
-
- handleEdit = () => {
- this.props.history.push(documentEditUrl(this.props.document));
- };
-
- handleSave = () => {
- this.props.onSave({ done: true });
- };
-
- handlePublish = () => {
- this.props.onSave({ done: true, publish: true });
- };
-
- render() {
- const {
- document,
- isEditing,
- isDraft,
- isPublishing,
- isSaving,
- savingIsDisabled,
- } = this.props;
-
- return (
-
- {!isDraft && !isEditing && }
- {isDraft && (
-
-
- {isPublishing ? 'Publishing…' : 'Publish'}
-
-
- )}
- {isEditing && (
-
-
-
- {isSaving && !isPublishing ? 'Saving…' : 'Save'}
-
-
- {isDraft && }
-
- )}
- {!isEditing && (
-
- Edit
-
- )}
- {isEditing && (
-
-
- {document.hasPendingChanges ? 'Discard' : 'Done'}
-
-
- )}
- {!isEditing && (
-
-
-
- )}
- {!isEditing &&
- !isDraft && (
-
-
-
-
-
-
-
-
- )}
-
- );
- }
-}
-
-const Link = styled.a`
- display: flex;
- align-items: center;
- font-weight: ${props => (props.highlight ? 500 : 'inherit')};
- color: ${props =>
- props.highlight ? `${props.theme.primary} !important` : 'inherit'};
- opacity: ${props => (props.disabled ? 0.5 : 1)};
- pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
- cursor: ${props => (props.disabled ? 'default' : 'pointer')};
-`;
-
-export default DocumentActions;
diff --git a/app/scenes/Document/components/Breadcrumb.js b/app/scenes/Document/components/Breadcrumb.js
new file mode 100644
index 000000000..914b4bea8
--- /dev/null
+++ b/app/scenes/Document/components/Breadcrumb.js
@@ -0,0 +1,76 @@
+// @flow
+import * as React from 'react';
+import { observer, inject } from 'mobx-react';
+import breakpoint from 'styled-components-breakpoint';
+import styled from 'styled-components';
+import { Link } from 'react-router-dom';
+import { CollectionIcon, GoToIcon } from 'outline-icons';
+
+import Document from 'models/Document';
+import CollectionsStore from 'stores/CollectionsStore';
+import { collectionUrl } from 'utils/routeHelpers';
+import Flex from 'shared/components/Flex';
+
+type Props = {
+ document: Document,
+ collections: CollectionsStore,
+};
+
+const Breadcrumb = observer(({ document, collections }: Props) => {
+ const path = document.pathToDocument.slice(0, -1);
+ const collection =
+ collections.getById(document.collection.id) || document.collection;
+
+ return (
+
+
+ {' '}
+ {collection.name}
+
+ {path.map(n => (
+
+ {n.title}
+
+ ))}
+
+ );
+});
+
+const Wrapper = styled(Flex)`
+ width: 33.3%;
+ display: none;
+
+ ${breakpoint('tablet')`
+ display: flex;
+ `};
+`;
+
+const Slash = styled(GoToIcon)`
+ flex-shrink: 0;
+ opacity: 0.25;
+`;
+
+const Crumb = styled(Link)`
+ color: ${props => props.theme.text};
+ font-size: 15px;
+ height: 24px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+const CollectionName = styled(Link)`
+ display: flex;
+ flex-shrink: 0;
+ color: ${props => props.theme.text};
+ font-size: 15px;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+`;
+
+export default inject('collections')(Breadcrumb);
diff --git a/app/scenes/Document/components/Header.js b/app/scenes/Document/components/Header.js
new file mode 100644
index 000000000..6428f66fb
--- /dev/null
+++ b/app/scenes/Document/components/Header.js
@@ -0,0 +1,231 @@
+// @flow
+import * as React from 'react';
+import { throttle } from 'lodash';
+import { observable } from 'mobx';
+import { observer } from 'mobx-react';
+import styled from 'styled-components';
+import breakpoint from 'styled-components-breakpoint';
+import { NewDocumentIcon } from 'outline-icons';
+import Document from 'models/Document';
+import { documentEditUrl, documentNewUrl } from 'utils/routeHelpers';
+
+import Flex from 'shared/components/Flex';
+import Breadcrumb from './Breadcrumb';
+import DocumentMenu from 'menus/DocumentMenu';
+import Collaborators from 'components/Collaborators';
+import { Action, Separator } from 'components/Actions';
+
+type Props = {
+ document: Document,
+ isDraft: boolean,
+ isEditing: boolean,
+ isSaving: boolean,
+ isPublishing: boolean,
+ savingIsDisabled: boolean,
+ onDiscard: () => *,
+ onSave: ({
+ done?: boolean,
+ publish?: boolean,
+ autosave?: boolean,
+ }) => *,
+ history: Object,
+};
+
+@observer
+class Header extends React.Component {
+ @observable isScrolled = false;
+
+ componentDidMount() {
+ window.addEventListener('scroll', this.handleScroll);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('scroll', this.handleScroll);
+ }
+
+ updateIsScrolled = () => {
+ this.isScrolled = window.scrollY > 75;
+ };
+
+ handleScroll = throttle(this.updateIsScrolled, 50);
+
+ handleNewDocument = () => {
+ this.props.history.push(documentNewUrl(this.props.document));
+ };
+
+ handleEdit = () => {
+ this.props.history.push(documentEditUrl(this.props.document));
+ };
+
+ handleSave = () => {
+ this.props.onSave({ done: true });
+ };
+
+ handlePublish = () => {
+ this.props.onSave({ done: true, publish: true });
+ };
+
+ handleClickTitle = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ };
+
+ render() {
+ const {
+ document,
+ isEditing,
+ isDraft,
+ isPublishing,
+ isSaving,
+ savingIsDisabled,
+ } = this.props;
+
+ return (
+
+
+
+ {document.title}
+
+
+ {!isDraft && !isEditing && }
+ {isSaving &&
+ !isPublishing && (
+
+ Saving…
+
+ )}
+ {isDraft && (
+
+
+ {isPublishing ? 'Publishing…' : 'Publish'}
+
+
+ )}
+ {isEditing && (
+
+
+
+ {isDraft ? 'Save Draft' : 'Done'}
+
+
+
+ )}
+ {!isEditing && (
+
+ Edit
+
+ )}
+ {isEditing &&
+ !isSaving &&
+ document.hasPendingChanges && (
+
+ Discard
+
+ )}
+ {!isEditing && (
+
+
+
+ )}
+ {!isEditing &&
+ !isDraft && (
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+ }
+}
+
+const Status = styled.div`
+ color: ${props => props.theme.slate};
+`;
+
+const Wrapper = styled(Flex)`
+ width: 100%;
+ align-self: flex-end;
+
+ ${breakpoint('tablet')`
+ width: 33.3%;
+ `};
+`;
+
+const Actions = styled(Flex)`
+ position: sticky;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1;
+ background: rgba(255, 255, 255, 0.9);
+ border-bottom: 1px solid
+ ${props => (props.isCompact ? props.theme.smoke : 'transparent')};
+ padding: 12px;
+ transition: padding 100ms ease-out;
+ -webkit-backdrop-filter: blur(20px);
+
+ @media print {
+ display: none;
+ }
+
+ ${breakpoint('tablet')`
+ padding: ${props => (props.isCompact ? '12px' : `24px 24px 0`)};
+ `};
+`;
+
+const Title = styled.div`
+ font-size: 16px;
+ font-weight: 600;
+ text-align: center;
+ justify-content: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ transition: opacity 100ms ease-in-out;
+ opacity: ${props => (props.isHidden ? '0' : '1')};
+ cursor: ${props => (props.isHidden ? 'default' : 'pointer')};
+ display: none;
+ width: 0;
+
+ ${breakpoint('tablet')`
+ display: block;
+ width: 33.3%;
+ `};
+`;
+
+const Link = styled.a`
+ display: flex;
+ align-items: center;
+ font-weight: ${props => (props.highlight ? 500 : 'inherit')};
+ color: ${props =>
+ props.highlight ? `${props.theme.primary} !important` : 'inherit'};
+ opacity: ${props => (props.disabled ? 0.5 : 1)};
+ pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
+ cursor: ${props => (props.disabled ? 'default' : 'pointer')};
+`;
+
+export default Header;