frontend > app
This commit is contained in:
37
app/components/Alert/Alert.js
Normal file
37
app/components/Alert/Alert.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
type Props = {
|
||||
children: React.Element<*>,
|
||||
type?: 'info' | 'success' | 'warning' | 'danger' | 'offline',
|
||||
};
|
||||
|
||||
@observer class Alert extends React.Component {
|
||||
props: Props;
|
||||
defaultProps = {
|
||||
type: 'info',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container align="center" justify="center" type={this.props.type}>
|
||||
{this.props.children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
height: $headerHeight;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
background-color: ${({ type }) => color[type]};
|
||||
`;
|
||||
|
||||
export default Alert;
|
||||
3
app/components/Alert/index.js
Normal file
3
app/components/Alert/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Alert from './Alert';
|
||||
export default Alert;
|
||||
10
app/components/Avatar/Avatar.js
Normal file
10
app/components/Avatar/Avatar.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Avatar = styled.img`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
export default Avatar;
|
||||
3
app/components/Avatar/index.js
Normal file
3
app/components/Avatar/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Avatar from './Avatar';
|
||||
export default Avatar;
|
||||
115
app/components/Button/Button.js
Normal file
115
app/components/Button/Button.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import { darken, lighten } from 'polished';
|
||||
|
||||
const RealButton = styled.button`
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: ${color.primary};
|
||||
color: ${color.white};
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
outline: none;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: ${darken(0.05, color.primary)};
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: .05em;
|
||||
}
|
||||
|
||||
${props => props.light && `
|
||||
color: ${color.text};
|
||||
background: ${lighten(0.08, color.slateLight)};
|
||||
|
||||
&:hover {
|
||||
background: ${color.slateLight};
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.neutral && `
|
||||
background: ${color.slate};
|
||||
|
||||
&:hover {
|
||||
background: ${darken(0.05, color.slate)};
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.danger && `
|
||||
background: ${color.danger};
|
||||
|
||||
&:hover {
|
||||
background: ${darken(0.05, color.danger)};
|
||||
}
|
||||
`}
|
||||
|
||||
&:disabled {
|
||||
background: ${color.slateLight};
|
||||
}
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
${props => props.hasIcon && 'padding-left: 2px;'}
|
||||
`;
|
||||
|
||||
const Inner = styled.span`
|
||||
padding: 4px 16px;
|
||||
display: flex;
|
||||
line-height: 28px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
${props => props.small && `
|
||||
padding: 1px 10px;
|
||||
`}
|
||||
|
||||
${props => props.hasIcon && (props.small ? 'padding-left: 6px;' : 'padding-left: 10px;')}
|
||||
`;
|
||||
|
||||
export type Props = {
|
||||
type?: string,
|
||||
value?: string,
|
||||
small?: boolean,
|
||||
icon?: React$Element<any>,
|
||||
className?: string,
|
||||
children?: React$Element<any>,
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
type = 'text',
|
||||
icon,
|
||||
children,
|
||||
small,
|
||||
value,
|
||||
...rest
|
||||
}: Props) {
|
||||
const hasText = children !== undefined || value !== undefined;
|
||||
const hasIcon = icon !== undefined;
|
||||
|
||||
return (
|
||||
<RealButton {...rest}>
|
||||
<Inner hasIcon={hasIcon} small={small}>
|
||||
{hasIcon && icon}
|
||||
{hasText && <Label hasIcon={hasIcon}>{children || value}</Label>}
|
||||
</Inner>
|
||||
</RealButton>
|
||||
);
|
||||
}
|
||||
3
app/components/Button/index.js
Normal file
3
app/components/Button/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Button from './Button';
|
||||
export default Button;
|
||||
29
app/components/CenteredContent/CenteredContent.js
Normal file
29
app/components/CenteredContent/CenteredContent.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
children?: React.Element<any>,
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
margin: 60px 20px;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
max-width: 50em;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const CenteredContent = ({ children, ...rest }: Props) => {
|
||||
return (
|
||||
<Container {...rest}>
|
||||
<Content>
|
||||
{children}
|
||||
</Content>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default CenteredContent;
|
||||
3
app/components/CenteredContent/index.js
Normal file
3
app/components/CenteredContent/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import CenteredContent from './CenteredContent';
|
||||
export default CenteredContent;
|
||||
60
app/components/Collaborators/Collaborators.js
Normal file
60
app/components/Collaborators/Collaborators.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import Flex from 'components/Flex';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Document from 'models/Document';
|
||||
|
||||
const Collaborators = function({ document }: { document: Document }) {
|
||||
const {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
collaborators,
|
||||
} = document;
|
||||
let tooltip;
|
||||
|
||||
if (createdAt === updatedAt) {
|
||||
tooltip = `${createdBy.name} published ${moment(createdAt).fromNow()}`;
|
||||
} else {
|
||||
tooltip = `${updatedBy.name} modified ${moment(updatedAt).fromNow()}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatars>
|
||||
<StyledTooltip tooltip={tooltip} placement="bottom">
|
||||
{collaborators.map(user => (
|
||||
<Avatar key={user.id} src={user.avatarUrl} />
|
||||
))}
|
||||
</StyledTooltip>
|
||||
</Avatars>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
`;
|
||||
|
||||
const Avatars = styled(Flex)`
|
||||
flex-direction: row-reverse;
|
||||
height: 26px;
|
||||
`;
|
||||
|
||||
const Avatar = styled.img`
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${color.white};
|
||||
margin-right: -10px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Collaborators;
|
||||
3
app/components/Collaborators/index.js
Normal file
3
app/components/Collaborators/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Collaborators from './Collaborators';
|
||||
export default Collaborators;
|
||||
36
app/components/CopyToClipboard/CopyToClipboard.js
Normal file
36
app/components/CopyToClipboard/CopyToClipboard.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
import React, { PureComponent } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
children?: React.Element<any>,
|
||||
onClick?: () => void,
|
||||
onCopy: () => void,
|
||||
};
|
||||
|
||||
class CopyToClipboard extends PureComponent {
|
||||
props: Props;
|
||||
|
||||
onClick = (ev: SyntheticEvent) => {
|
||||
const { text, onCopy, children } = this.props;
|
||||
const elem = React.Children.only(children);
|
||||
copy(text, {
|
||||
debug: __DEV__,
|
||||
});
|
||||
|
||||
if (onCopy) onCopy();
|
||||
|
||||
if (elem && elem.props && typeof elem.props.onClick === 'function') {
|
||||
elem.props.onClick(ev);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { text: _text, onCopy: _onCopy, children, ...rest } = this.props;
|
||||
const elem = React.Children.only(children);
|
||||
return React.cloneElement(elem, { ...rest, onClick: this.onClick });
|
||||
}
|
||||
}
|
||||
|
||||
export default CopyToClipboard;
|
||||
3
app/components/CopyToClipboard/index.js
Normal file
3
app/components/CopyToClipboard/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import CopyToClipboard from './CopyToClipboard';
|
||||
export default CopyToClipboard;
|
||||
17
app/components/Divider/Divider.js
Normal file
17
app/components/Divider/Divider.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Divider = () => {
|
||||
return <Flex auto justify="center"><Content /></Flex>;
|
||||
};
|
||||
|
||||
const Content = styled.span`
|
||||
display: flex;
|
||||
width: 50%;
|
||||
margin: 20px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
`;
|
||||
|
||||
export default Divider;
|
||||
3
app/components/Divider/index.js
Normal file
3
app/components/Divider/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Divider from './Divider';
|
||||
export default Divider;
|
||||
27
app/components/DocumentList/DocumentList.js
Normal file
27
app/components/DocumentList/DocumentList.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Document from 'models/Document';
|
||||
import DocumentPreview from 'components/DocumentPreview';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
|
||||
class DocumentList extends React.Component {
|
||||
props: {
|
||||
documents: Array<Document>,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{this.props.documents &&
|
||||
this.props.documents.map(document => (
|
||||
<DocumentPreview key={document.id} document={document} />
|
||||
))}
|
||||
</ArrowKeyNavigation>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentList;
|
||||
3
app/components/DocumentList/index.js
Normal file
3
app/components/DocumentList/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import DocumentList from './DocumentList';
|
||||
export default DocumentList;
|
||||
109
app/components/DocumentPreview/DocumentPreview.js
Normal file
109
app/components/DocumentPreview/DocumentPreview.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Document from 'models/Document';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import StarredIcon from 'components/Icon/StarredIcon';
|
||||
import PublishingInfo from './components/PublishingInfo';
|
||||
|
||||
type Props = {
|
||||
document: Document,
|
||||
highlight?: ?string,
|
||||
showCollection?: boolean,
|
||||
innerRef?: Function,
|
||||
};
|
||||
|
||||
const StyledStar = styled(({ solid, ...props }) => (
|
||||
<StarredIcon color={solid ? color.black : color.text} {...props} />
|
||||
))`
|
||||
position: absolute;
|
||||
opacity: ${props => (props.solid ? '1 !important' : 0)};
|
||||
transition: all 100ms ease-in-out;
|
||||
margin-left: 2px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
`;
|
||||
|
||||
const DocumentLink = styled(Link)`
|
||||
display: block;
|
||||
margin: 0 -16px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
max-height: 50vh;
|
||||
min-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: ${color.smokeLight};
|
||||
border: 2px solid ${color.smoke};
|
||||
outline: none;
|
||||
|
||||
${StyledStar} {
|
||||
opacity: .5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 2px solid ${color.slateDark};
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
`;
|
||||
|
||||
@observer class DocumentPreview extends Component {
|
||||
props: Props;
|
||||
|
||||
star = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.star();
|
||||
};
|
||||
|
||||
unstar = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.document.unstar();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { document, showCollection, innerRef, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<DocumentLink to={document.url} innerRef={innerRef} {...rest}>
|
||||
<h3>
|
||||
{document.title}
|
||||
{document.starred
|
||||
? <a onClick={this.unstar}>
|
||||
<StyledStar solid />
|
||||
</a>
|
||||
: <a onClick={this.star}>
|
||||
<StyledStar />
|
||||
</a>}
|
||||
</h3>
|
||||
<PublishingInfo
|
||||
document={document}
|
||||
collection={showCollection ? document.collection : undefined}
|
||||
/>
|
||||
</DocumentLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentPreview;
|
||||
62
app/components/DocumentPreview/components/PublishingInfo.js
Normal file
62
app/components/DocumentPreview/components/PublishingInfo.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import moment from 'moment';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import Collection from 'models/Collection';
|
||||
import Document from 'models/Document';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Container = styled(Flex)`
|
||||
color: ${color.slate};
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
const Modified = styled.span`
|
||||
color: ${props => (props.highlight ? color.slateDark : color.slate)};
|
||||
font-weight: ${props => (props.highlight ? '600' : '400')};
|
||||
`;
|
||||
|
||||
class PublishingInfo extends Component {
|
||||
props: {
|
||||
collection?: Collection,
|
||||
document: Document,
|
||||
views?: number,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collection, document } = this.props;
|
||||
const {
|
||||
modifiedSinceViewed,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
} = document;
|
||||
|
||||
return (
|
||||
<Container align="center">
|
||||
{createdAt === updatedAt
|
||||
? <span>
|
||||
{createdBy.name}
|
||||
{' '}
|
||||
published
|
||||
{' '}
|
||||
{moment(createdAt).fromNow()}
|
||||
</span>
|
||||
: <span>
|
||||
{updatedBy.name}
|
||||
<Modified highlight={modifiedSinceViewed}>
|
||||
{' '}
|
||||
modified
|
||||
{' '}
|
||||
{moment(updatedAt).fromNow()}
|
||||
</Modified>
|
||||
</span>}
|
||||
{collection && <span> in <strong>{collection.name}</strong></span>}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishingInfo;
|
||||
3
app/components/DocumentPreview/index.js
Normal file
3
app/components/DocumentPreview/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import DocumentPreview from './DocumentPreview';
|
||||
export default DocumentPreview;
|
||||
41
app/components/DocumentViews/DocumentViewersStore.js
Normal file
41
app/components/DocumentViews/DocumentViewersStore.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
import { observable, action } from 'mobx';
|
||||
import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import type { User } from 'types';
|
||||
|
||||
type View = {
|
||||
user: User,
|
||||
count: number,
|
||||
};
|
||||
|
||||
class DocumentViewersStore {
|
||||
documentId: string;
|
||||
@observable viewers: Array<View>;
|
||||
@observable isFetching: boolean;
|
||||
|
||||
@action fetchViewers = async () => {
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const res = await client.post(
|
||||
'/views.list',
|
||||
{
|
||||
id: this.documentId,
|
||||
},
|
||||
{ cache: true }
|
||||
);
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
this.viewers = res.data.users;
|
||||
} catch (e) {
|
||||
console.error('Something went wrong');
|
||||
}
|
||||
this.isFetching = false;
|
||||
};
|
||||
|
||||
constructor(documentId: string) {
|
||||
this.documentId = documentId;
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentViewersStore;
|
||||
76
app/components/DocumentViews/DocumentViews.js
Normal file
76
app/components/DocumentViews/DocumentViews.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Popover from 'components/Popover';
|
||||
import styled from 'styled-components';
|
||||
import DocumentViewers from './components/DocumentViewers';
|
||||
import DocumentViewersStore from './DocumentViewersStore';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
const Container = styled(Flex)`
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
|
||||
a {
|
||||
color: #ccc;
|
||||
|
||||
&:hover {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
documentId: string,
|
||||
count: number,
|
||||
};
|
||||
|
||||
@observer class DocumentViews extends Component {
|
||||
anchor: HTMLElement;
|
||||
store: DocumentViewersStore;
|
||||
props: Props;
|
||||
state: {
|
||||
opened: boolean,
|
||||
};
|
||||
state = {};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.store = new DocumentViewersStore(props.documentId);
|
||||
}
|
||||
|
||||
openPopover = () => {
|
||||
this.setState({ opened: true });
|
||||
};
|
||||
|
||||
closePopover = () => {
|
||||
this.setState({ opened: false });
|
||||
};
|
||||
|
||||
setRef = (ref: HTMLElement) => {
|
||||
this.anchor = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container align="center">
|
||||
<a ref={this.setRef} onClick={this.openPopover}>
|
||||
Viewed
|
||||
{' '}
|
||||
{this.props.count}
|
||||
{' '}
|
||||
{this.props.count === 1 ? 'time' : 'times'}
|
||||
</a>
|
||||
{this.state.opened &&
|
||||
<Popover anchor={this.anchor} onClose={this.closePopover}>
|
||||
<DocumentViewers
|
||||
onMount={this.store.fetchViewers}
|
||||
viewers={this.store.viewers}
|
||||
/>
|
||||
</Popover>}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentViews;
|
||||
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import Flex from 'components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import map from 'lodash/map';
|
||||
import Avatar from 'components/Avatar';
|
||||
import Scrollable from 'components/Scrollable';
|
||||
|
||||
type Props = {
|
||||
viewers: Array<Object>,
|
||||
onMount: Function,
|
||||
};
|
||||
|
||||
const List = styled.ul`
|
||||
list-style: none;
|
||||
font-size: 13px;
|
||||
margin: -4px 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 4px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const UserName = styled.span`
|
||||
padding-left: 8px;
|
||||
`;
|
||||
|
||||
class DocumentViewers extends Component {
|
||||
props: Props;
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onMount();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Scrollable>
|
||||
<List>
|
||||
{map(this.props.viewers, view => (
|
||||
<li key={view.user.id}>
|
||||
<Flex align="center">
|
||||
<Avatar src={view.user.avatarUrl} />
|
||||
{' '}
|
||||
<UserName>{view.user.name}</UserName>
|
||||
</Flex>
|
||||
</li>
|
||||
))}
|
||||
</List>
|
||||
</Scrollable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentViewers;
|
||||
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import DocumentViewers from './DocumentViewers';
|
||||
export default DocumentViewers;
|
||||
3
app/components/DocumentViews/index.js
Normal file
3
app/components/DocumentViews/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import DocumentViews from './DocumentViews';
|
||||
export default DocumentViews;
|
||||
110
app/components/DropToImport/DropToImport.js
Normal file
110
app/components/DropToImport/DropToImport.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { inject } from 'mobx-react';
|
||||
import invariant from 'invariant';
|
||||
import _ from 'lodash';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import Document from 'models/Document';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
|
||||
type Props = {
|
||||
children?: React$Element<any>,
|
||||
collectionId: string,
|
||||
documentId?: string,
|
||||
activeClassName?: string,
|
||||
rejectClassName?: string,
|
||||
documents: DocumentsStore,
|
||||
disabled: boolean,
|
||||
dropzoneRef: Function,
|
||||
history: Object,
|
||||
};
|
||||
|
||||
class DropToImport extends Component {
|
||||
state: {
|
||||
isImporting: boolean,
|
||||
};
|
||||
props: Props;
|
||||
state = {
|
||||
isImporting: false,
|
||||
};
|
||||
|
||||
importFile = async ({ file, documentId, collectionId, redirect }) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async ev => {
|
||||
const text = ev.target.result;
|
||||
let data = {
|
||||
parentDocument: undefined,
|
||||
collection: { id: collectionId },
|
||||
text,
|
||||
};
|
||||
|
||||
if (documentId) data.parentDocument = documentId;
|
||||
|
||||
let document = new Document(data);
|
||||
document = await document.save();
|
||||
this.props.documents.add(document);
|
||||
|
||||
if (redirect && this.props.history) {
|
||||
this.props.history.push(document.url);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
onDropAccepted = async (files = []) => {
|
||||
this.setState({ isImporting: true });
|
||||
|
||||
try {
|
||||
let collectionId = this.props.collectionId;
|
||||
const documentId = this.props.documentId;
|
||||
const redirect = files.length === 1;
|
||||
|
||||
if (documentId && !collectionId) {
|
||||
const document = await this.props.documents.fetch(documentId);
|
||||
invariant(document, 'Document not available');
|
||||
collectionId = document.collection.id;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
await this.importFile({ file, documentId, collectionId, redirect });
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: show error alert.
|
||||
} finally {
|
||||
this.setState({ isImporting: false });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const props = _.omit(
|
||||
this.props,
|
||||
'history',
|
||||
'documentId',
|
||||
'collectionId',
|
||||
'documents',
|
||||
'disabled'
|
||||
);
|
||||
|
||||
if (this.props.disabled) return this.props.children;
|
||||
|
||||
return (
|
||||
<Dropzone
|
||||
accept="text/markdown, text/plain"
|
||||
onDropAccepted={this.onDropAccepted}
|
||||
style={{}}
|
||||
disableClick
|
||||
disablePreview
|
||||
multiple
|
||||
ref={this.props.dropzoneRef}
|
||||
{...props}
|
||||
>
|
||||
{this.state.isImporting && <LoadingIndicator />}
|
||||
{this.props.children}
|
||||
</Dropzone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('documents')(DropToImport);
|
||||
3
app/components/DropToImport/index.js
Normal file
3
app/components/DropToImport/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import DropToImport from './DropToImport';
|
||||
export default DropToImport;
|
||||
103
app/components/DropdownMenu/DropdownMenu.js
Normal file
103
app/components/DropdownMenu/DropdownMenu.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import invariant from 'invariant';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import Portal from 'react-portal';
|
||||
import Flex from 'components/Flex';
|
||||
import { color } from 'styles/constants';
|
||||
import { fadeAndScaleIn } from 'styles/animations';
|
||||
|
||||
type Props = {
|
||||
label: React.Element<*>,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
children?: React.Element<*>,
|
||||
style?: Object,
|
||||
};
|
||||
|
||||
@observer class DropdownMenu extends Component {
|
||||
props: Props;
|
||||
actionRef: Object;
|
||||
@observable open: boolean = false;
|
||||
@observable top: number;
|
||||
@observable left: number;
|
||||
@observable right: number;
|
||||
|
||||
handleClick = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const currentTarget = ev.currentTarget;
|
||||
invariant(document.body, 'why you not here');
|
||||
|
||||
if (currentTarget instanceof HTMLDivElement) {
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const targetRect = currentTarget.getBoundingClientRect();
|
||||
this.open = true;
|
||||
this.top = targetRect.bottom - bodyRect.top;
|
||||
this.right = bodyRect.width - targetRect.left - targetRect.width;
|
||||
if (this.props.onOpen) this.props.onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = (ev: SyntheticEvent) => {
|
||||
this.open = false;
|
||||
if (this.props.onClose) this.props.onClose();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Label
|
||||
onClick={this.handleClick}
|
||||
innerRef={ref => (this.actionRef = ref)}
|
||||
>
|
||||
{this.props.label}
|
||||
</Label>
|
||||
<Portal
|
||||
closeOnEsc
|
||||
closeOnOutsideClick
|
||||
isOpened={this.open}
|
||||
onClose={this.handleClose}
|
||||
>
|
||||
<Menu
|
||||
onClick={this.handleClose}
|
||||
style={this.props.style}
|
||||
left={this.left}
|
||||
top={this.top}
|
||||
right={this.right}
|
||||
>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
</Portal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Label = styled(Flex).attrs({
|
||||
justify: 'center',
|
||||
align: 'center',
|
||||
})`
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Menu = styled.div`
|
||||
animation: ${fadeAndScaleIn} 200ms ease;
|
||||
transform-origin: 75% 0;
|
||||
|
||||
position: absolute;
|
||||
right: ${({ right }) => right}px;
|
||||
top: ${({ top }) => top}px;
|
||||
z-index: 1000;
|
||||
border: ${color.slateLight};
|
||||
background: ${color.white};
|
||||
border-radius: 2px;
|
||||
min-width: 160px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,.05), 0 4px 8px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.08);
|
||||
`;
|
||||
|
||||
export default DropdownMenu;
|
||||
51
app/components/DropdownMenu/DropdownMenuItem.js
Normal file
51
app/components/DropdownMenu/DropdownMenuItem.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'components/Flex';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const DropdownMenuItem = ({
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
onClick?: SyntheticEvent => void,
|
||||
children?: React.Element<any>,
|
||||
}) => {
|
||||
return (
|
||||
<MenuItem onClick={onClick}>
|
||||
{children}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItem = styled(Flex)`
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
height: 32px;
|
||||
|
||||
color: ${color.slateDark};
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
|
||||
svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${color.white};
|
||||
background: ${color.primary};
|
||||
|
||||
svg {
|
||||
fill: ${color.white};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default DropdownMenuItem;
|
||||
3
app/components/DropdownMenu/index.js
Normal file
3
app/components/DropdownMenu/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
export { default as DropdownMenu } from './DropdownMenu';
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem';
|
||||
331
app/components/Editor/Editor.js
Normal file
331
app/components/Editor/Editor.js
Normal file
@@ -0,0 +1,331 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Editor, Plain } from 'slate';
|
||||
import keydown from 'react-keydown';
|
||||
import type { State, Editor as EditorType } from './types';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Flex from 'components/Flex';
|
||||
import ClickablePadding from './components/ClickablePadding';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import BlockInsert from './components/BlockInsert';
|
||||
import Placeholder from './components/Placeholder';
|
||||
import Contents from './components/Contents';
|
||||
import Markdown from './serializer';
|
||||
import createSchema from './schema';
|
||||
import createPlugins from './plugins';
|
||||
import insertImage from './insertImage';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
text: string,
|
||||
onChange: Function,
|
||||
onSave: Function,
|
||||
onCancel: Function,
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
emoji?: string,
|
||||
readOnly: boolean,
|
||||
};
|
||||
|
||||
type KeyData = {
|
||||
isMeta: boolean,
|
||||
key: string,
|
||||
};
|
||||
|
||||
@observer class MarkdownEditor extends Component {
|
||||
props: Props;
|
||||
editor: EditorType;
|
||||
schema: Object;
|
||||
plugins: Array<Object>;
|
||||
@observable editorState: State;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.schema = createSchema();
|
||||
this.plugins = createPlugins({
|
||||
onImageUploadStart: props.onImageUploadStart,
|
||||
onImageUploadStop: props.onImageUploadStop,
|
||||
});
|
||||
|
||||
if (props.text.trim().length) {
|
||||
this.editorState = Markdown.deserialize(props.text);
|
||||
} else {
|
||||
this.editorState = Plain.deserialize('');
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.readOnly) {
|
||||
if (this.props.text) {
|
||||
this.focusAtEnd();
|
||||
} else {
|
||||
this.focusAtStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.readOnly && !this.props.readOnly) {
|
||||
this.focusAtEnd();
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (editorState: State) => {
|
||||
if (this.editorState !== editorState) {
|
||||
this.props.onChange(Markdown.serialize(editorState));
|
||||
}
|
||||
|
||||
this.editorState = editorState;
|
||||
};
|
||||
|
||||
handleDrop = async (ev: SyntheticEvent) => {
|
||||
if (this.props.readOnly) return;
|
||||
// check if this event was already handled by the Editor
|
||||
if (ev.isDefaultPrevented()) return;
|
||||
|
||||
// otherwise we'll handle this
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const files = getDataTransferFiles(ev);
|
||||
for (const file of files) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
await this.insertImageFile(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
insertImageFile = async (file: window.File) => {
|
||||
const state = this.editor.getState();
|
||||
let transform = state.transform();
|
||||
|
||||
transform = await insertImage(
|
||||
transform,
|
||||
file,
|
||||
this.editor,
|
||||
this.props.onImageUploadStart,
|
||||
this.props.onImageUploadStop
|
||||
);
|
||||
this.editor.onChange(transform.apply());
|
||||
};
|
||||
|
||||
cancelEvent = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
// Handling of keyboard shortcuts outside of editor focus
|
||||
@keydown('meta+s')
|
||||
onSave(ev: SyntheticKeyboardEvent) {
|
||||
if (this.props.readOnly) return;
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onSave();
|
||||
}
|
||||
|
||||
@keydown('meta+enter')
|
||||
onSaveAndExit(ev: SyntheticKeyboardEvent) {
|
||||
if (this.props.readOnly) return;
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onSave({ redirect: false });
|
||||
}
|
||||
|
||||
@keydown('esc')
|
||||
onCancel() {
|
||||
if (this.props.readOnly) return;
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
// Handling of keyboard shortcuts within editor focus
|
||||
onKeyDown = (ev: SyntheticKeyboardEvent, data: KeyData, state: State) => {
|
||||
if (!data.isMeta) return;
|
||||
|
||||
switch (data.key) {
|
||||
case 's':
|
||||
this.onSave(ev);
|
||||
return state;
|
||||
case 'enter':
|
||||
this.onSaveAndExit(ev);
|
||||
return state;
|
||||
case 'escape':
|
||||
this.onCancel();
|
||||
return state;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
focusAtStart = () => {
|
||||
const state = this.editor.getState();
|
||||
const transform = state.transform();
|
||||
transform.collapseToStartOf(state.document);
|
||||
transform.focus();
|
||||
this.editorState = transform.apply();
|
||||
};
|
||||
|
||||
focusAtEnd = () => {
|
||||
const state = this.editor.getState();
|
||||
const transform = state.transform();
|
||||
transform.collapseToEndOf(state.document);
|
||||
transform.focus();
|
||||
this.editorState = transform.apply();
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { readOnly, emoji, onSave } = this.props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onDrop={this.handleDrop}
|
||||
onDragOver={this.cancelEvent}
|
||||
onDragEnter={this.cancelEvent}
|
||||
align="flex-start"
|
||||
justify="center"
|
||||
auto
|
||||
>
|
||||
<MaxWidth column auto>
|
||||
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
||||
<Contents state={this.editorState} />
|
||||
{!readOnly &&
|
||||
<Toolbar state={this.editorState} onChange={this.onChange} />}
|
||||
{!readOnly &&
|
||||
<BlockInsert
|
||||
state={this.editorState}
|
||||
onChange={this.onChange}
|
||||
onInsertImage={this.insertImageFile}
|
||||
/>}
|
||||
<StyledEditor
|
||||
innerRef={ref => (this.editor = ref)}
|
||||
placeholder="Start with a title…"
|
||||
bodyPlaceholder="…the rest is your canvas"
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
emoji={emoji}
|
||||
state={this.editorState}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
onSave={onSave}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<ClickablePadding
|
||||
onClick={!readOnly ? this.focusAtEnd : undefined}
|
||||
grow
|
||||
/>
|
||||
</MaxWidth>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const MaxWidth = styled(Flex)`
|
||||
padding: 0 60px;
|
||||
max-width: 50em;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Header = styled(Flex)`
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
align-items: flex-end;
|
||||
${({ readOnly }) => !readOnly && 'cursor: text;'}
|
||||
`;
|
||||
|
||||
const StyledEditor = styled(Editor)`
|
||||
font-weight: 400;
|
||||
font-size: 1em;
|
||||
line-height: 1.7em;
|
||||
width: 100%;
|
||||
color: #1b2830;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:nth-child(2) {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 1em 0.1em;
|
||||
padding-left: 1em;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: ${({ readOnly }) => (readOnly ? 'underline' : 'none')};
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.todoList {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
.todoList {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.todo {
|
||||
span:last-child:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #efefef;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
`;
|
||||
|
||||
export default MarkdownEditor;
|
||||
196
app/components/Editor/components/BlockInsert.js
Normal file
196
app/components/Editor/components/BlockInsert.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import EditList from '../plugins/EditList';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Portal from 'react-portal';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import PlusIcon from 'components/Icon/PlusIcon';
|
||||
import BlockMenu from 'menus/BlockMenu';
|
||||
import type { State } from '../types';
|
||||
|
||||
const { transforms } = EditList;
|
||||
|
||||
type Props = {
|
||||
state: State,
|
||||
onChange: Function,
|
||||
onInsertImage: File => Promise<*>,
|
||||
};
|
||||
|
||||
@observer
|
||||
export default class BlockInsert extends Component {
|
||||
props: Props;
|
||||
mouseMoveTimeout: number;
|
||||
file: HTMLInputElement;
|
||||
|
||||
@observable active: boolean = false;
|
||||
@observable menuOpen: boolean = false;
|
||||
@observable top: number;
|
||||
@observable left: number;
|
||||
@observable mouseX: number;
|
||||
|
||||
componentDidMount = () => {
|
||||
this.update();
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
};
|
||||
|
||||
componentWillUpdate = (nextProps: Props) => {
|
||||
this.update(nextProps);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
};
|
||||
|
||||
setInactive = () => {
|
||||
if (this.menuOpen) return;
|
||||
this.active = false;
|
||||
};
|
||||
|
||||
handleMouseMove = (ev: SyntheticMouseEvent) => {
|
||||
const windowWidth = window.innerWidth / 3;
|
||||
let active = ev.clientX < windowWidth;
|
||||
|
||||
if (active !== this.active) {
|
||||
this.active = active || this.menuOpen;
|
||||
}
|
||||
if (active) {
|
||||
clearTimeout(this.mouseMoveTimeout);
|
||||
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
handleMenuOpen = () => {
|
||||
this.menuOpen = true;
|
||||
};
|
||||
|
||||
handleMenuClose = () => {
|
||||
this.menuOpen = false;
|
||||
};
|
||||
|
||||
update = (props?: Props) => {
|
||||
if (!document.activeElement) return;
|
||||
const { state } = props || this.props;
|
||||
const boxRect = document.activeElement.getBoundingClientRect();
|
||||
const selection = window.getSelection();
|
||||
if (!selection.focusNode) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
if (rect.top <= 0 || boxRect.left <= 0) return;
|
||||
|
||||
if (state.startBlock.type === 'heading1') {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
this.top = Math.round(rect.top + window.scrollY);
|
||||
this.left = Math.round(boxRect.left + window.scrollX - 20);
|
||||
};
|
||||
|
||||
insertBlock = (
|
||||
ev: SyntheticEvent,
|
||||
options: {
|
||||
type: string | Object,
|
||||
wrapper?: string | Object,
|
||||
append?: string | Object,
|
||||
}
|
||||
) => {
|
||||
ev.preventDefault();
|
||||
const { type, wrapper, append } = options;
|
||||
let { state } = this.props;
|
||||
let transform = state.transform();
|
||||
const { document } = state;
|
||||
const parent = document.getParent(state.startBlock.key);
|
||||
|
||||
// lists get some special treatment
|
||||
if (parent && parent.type === 'list-item') {
|
||||
transform = transforms.unwrapList(
|
||||
transforms
|
||||
.splitListItem(transform.collapseToStart())
|
||||
.collapseToEndOfPreviousBlock()
|
||||
);
|
||||
}
|
||||
|
||||
transform = transform.insertBlock(type);
|
||||
|
||||
if (wrapper) transform = transform.wrapBlock(wrapper);
|
||||
if (append) transform = transform.insertBlock(append);
|
||||
|
||||
state = transform.focus().apply();
|
||||
this.props.onChange(state);
|
||||
this.active = false;
|
||||
};
|
||||
|
||||
onPickImage = (ev: SyntheticEvent) => {
|
||||
// simulate a click on the file upload input element
|
||||
this.file.click();
|
||||
};
|
||||
|
||||
onChooseImage = async (ev: SyntheticEvent) => {
|
||||
const files = getDataTransferFiles(ev);
|
||||
for (const file of files) {
|
||||
await this.props.onInsertImage(file);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = { top: `${this.top}px`, left: `${this.left}px` };
|
||||
const todo = { type: 'list-item', data: { checked: false } };
|
||||
const rule = { type: 'horizontal-rule', isVoid: true };
|
||||
|
||||
return (
|
||||
<Portal isOpened>
|
||||
<Trigger active={this.active} style={style}>
|
||||
<HiddenInput
|
||||
type="file"
|
||||
innerRef={ref => (this.file = ref)}
|
||||
onChange={this.onChooseImage}
|
||||
accept="image/*"
|
||||
/>
|
||||
<BlockMenu
|
||||
label={<PlusIcon />}
|
||||
onPickImage={this.onPickImage}
|
||||
onInsertList={ev =>
|
||||
this.insertBlock(ev, {
|
||||
type: 'list-item',
|
||||
wrapper: 'bulleted-list',
|
||||
})}
|
||||
onInsertTodoList={ev =>
|
||||
this.insertBlock(ev, { type: todo, wrapper: 'todo-list' })}
|
||||
onInsertBreak={ev =>
|
||||
this.insertBlock(ev, { type: rule, append: 'paragraph' })}
|
||||
onOpen={this.handleMenuOpen}
|
||||
onClose={this.handleMenuClose}
|
||||
/>
|
||||
</Trigger>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HiddenInput = styled.input`
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
const Trigger = styled.div`
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
background-color: ${color.white};
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
line-height: 0;
|
||||
margin-top: -2px;
|
||||
margin-left: -4px;
|
||||
transform: scale(.9);
|
||||
|
||||
${({ active }) => active && `
|
||||
transform: scale(1);
|
||||
opacity: .9;
|
||||
`}
|
||||
`;
|
||||
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
onClick?: ?Function,
|
||||
grow?: boolean,
|
||||
};
|
||||
|
||||
const ClickablePadding = (props: Props) => {
|
||||
return <Container grow={props.grow} onClick={props.onClick} />;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
min-height: 150px;
|
||||
padding-top: 50px;
|
||||
cursor: ${({ onClick }) => (onClick ? 'text' : 'default')};
|
||||
|
||||
${({ grow }) => grow && `flex-grow: 1;`}
|
||||
`;
|
||||
|
||||
export default ClickablePadding;
|
||||
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import ClickablePadding from './ClickablePadding';
|
||||
export default ClickablePadding;
|
||||
42
app/components/Editor/components/Code.js
Normal file
42
app/components/Editor/components/Code.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import CopyButton from './CopyButton';
|
||||
import { color } from 'styles/constants';
|
||||
import type { Props } from '../types';
|
||||
|
||||
export default function Code({ children, node, readOnly, attributes }: Props) {
|
||||
const language = node.data.get('language') || 'javascript';
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{readOnly && <CopyButton text={node.text} />}
|
||||
<Pre className={`language-${language}`}>
|
||||
<code {...attributes} className={`language-${language}`}>
|
||||
{children}
|
||||
</code>
|
||||
</Pre>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Pre = styled.pre`
|
||||
padding: .5em 1em;
|
||||
background: ${color.smokeLight};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${color.smokeDark};
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
> span {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
149
app/components/Editor/components/Contents.js
Normal file
149
app/components/Editor/components/Contents.js
Normal file
@@ -0,0 +1,149 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { List } from 'immutable';
|
||||
import { color } from 'styles/constants';
|
||||
import headingToSlug from '../headingToSlug';
|
||||
import type { State, Block } from '../types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
state: State,
|
||||
};
|
||||
|
||||
@observer class Contents extends Component {
|
||||
props: Props;
|
||||
@observable activeHeading: ?string;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('scroll', this.updateActiveHeading);
|
||||
this.updateActiveHeading();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('scroll', this.updateActiveHeading);
|
||||
}
|
||||
|
||||
updateActiveHeading = () => {
|
||||
const elements = this.headingElements;
|
||||
if (!elements.length) return;
|
||||
|
||||
let activeHeading = elements[0].id;
|
||||
|
||||
for (const element of elements) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
if (bounds.top <= 0) activeHeading = element.id;
|
||||
}
|
||||
|
||||
this.activeHeading = activeHeading;
|
||||
};
|
||||
|
||||
get headingElements(): HTMLElement[] {
|
||||
const elements = [];
|
||||
const tagNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
for (const tagName of tagNames) {
|
||||
for (const ele of document.getElementsByTagName(tagName)) {
|
||||
elements.push(ele);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
get headings(): List<Block> {
|
||||
const { state } = this.props;
|
||||
|
||||
return state.document.nodes.filter((node: Block) => {
|
||||
if (!node.text) return false;
|
||||
return node.type.match(/^heading/);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
// If there are one or less headings in the document no need for a minimap
|
||||
if (this.headings.size <= 1) return null;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Sections>
|
||||
{this.headings.map(heading => {
|
||||
const slug = headingToSlug(heading);
|
||||
const active = this.activeHeading === slug;
|
||||
|
||||
return (
|
||||
<ListItem type={heading.type} active={active}>
|
||||
<Anchor href={`#${slug}`} active={active}>
|
||||
{heading.text}
|
||||
</Anchor>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</Sections>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 150px;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||
font-weight: ${props => (props.active ? 500 : 400)};
|
||||
opacity: 0;
|
||||
transition: all 100ms ease-in-out;
|
||||
margin-right: -5px;
|
||||
padding: 2px 0;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
color: ${color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItem = styled.li`
|
||||
position: relative;
|
||||
margin-left: ${props => (props.type.match(/heading[12]/) ? '8px' : '16px')};
|
||||
text-align: right;
|
||||
color: ${color.slate};
|
||||
padding-right: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:after {
|
||||
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||
content: "${props => (props.type.match(/heading[12]/) ? '—' : '–')}";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Sections = styled.ol`
|
||||
margin: 0 0 0 -8px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 13px;
|
||||
width: 100px;
|
||||
transition-delay: 1s;
|
||||
transition: width 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
width: 300px;
|
||||
transition-delay: 0s;
|
||||
|
||||
${Anchor} {
|
||||
opacity: 1;
|
||||
margin-right: 0;
|
||||
background: ${color.white};
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Contents;
|
||||
49
app/components/Editor/components/CopyButton.js
Normal file
49
app/components/Editor/components/CopyButton.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { color } from 'styles/constants';
|
||||
import styled from 'styled-components';
|
||||
import CopyToClipboard from 'components/CopyToClipboard';
|
||||
|
||||
@observer class CopyButton extends Component {
|
||||
@observable copied: boolean = false;
|
||||
copiedTimeout: ?number;
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.copiedTimeout);
|
||||
}
|
||||
|
||||
handleCopy = () => {
|
||||
this.copied = true;
|
||||
this.copiedTimeout = setTimeout(() => (this.copied = false), 3000);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledCopyToClipboard onCopy={this.handleCopy} {...this.props}>
|
||||
<span>{this.copied ? 'Copied!' : 'Copy to clipboard'}</span>
|
||||
</StyledCopyToClipboard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledCopyToClipboard = styled(CopyToClipboard)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 50ms ease-in-out;
|
||||
z-index: 1;
|
||||
font-size: 12px;
|
||||
background: ${color.slateLight};
|
||||
border-radius: 2px;
|
||||
padding: 1px 6px;
|
||||
|
||||
&:hover {
|
||||
background: ${color.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
export default CopyButton;
|
||||
97
app/components/Editor/components/Heading.js
Normal file
97
app/components/Editor/components/Heading.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import styled from 'styled-components';
|
||||
import headingToSlug from '../headingToSlug';
|
||||
import type { Node, Editor } from '../types';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
type Props = {
|
||||
children: React$Element<*>,
|
||||
placeholder?: boolean,
|
||||
parent: Node,
|
||||
node: Node,
|
||||
editor: Editor,
|
||||
readOnly: boolean,
|
||||
component?: string,
|
||||
};
|
||||
|
||||
function Heading(props: Props) {
|
||||
const {
|
||||
parent,
|
||||
placeholder,
|
||||
node,
|
||||
editor,
|
||||
readOnly,
|
||||
children,
|
||||
component = 'h1',
|
||||
...rest
|
||||
} = props;
|
||||
const parentIsDocument = parent instanceof Document;
|
||||
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||
const slugish = headingToSlug(node);
|
||||
const showHash = readOnly && !!slugish;
|
||||
const Component = component;
|
||||
const emoji = editor.props.emoji || '';
|
||||
const title = node.text.trim();
|
||||
const startsWithEmojiAndSpace =
|
||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||
|
||||
return (
|
||||
<Component {...rest} id={slugish}>
|
||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>
|
||||
{children}
|
||||
</Wrapper>
|
||||
{showPlaceholder &&
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.placeholder}
|
||||
</Placeholder>}
|
||||
{showHash && <Anchor name={slugish} href={`#${slugish}`}>#</Anchor>}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: inline;
|
||||
margin-left: ${(props: Props) => (props.hasEmoji ? '-1.2em' : 0)}
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
visibility: hidden;
|
||||
padding-left: .25em;
|
||||
color: #dedede;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledHeading = styled(Heading)`
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
${Anchor} {
|
||||
visibility: visible;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const Heading1 = (props: Props) => (
|
||||
<StyledHeading component="h1" {...props} />
|
||||
);
|
||||
export const Heading2 = (props: Props) => (
|
||||
<StyledHeading component="h2" {...props} />
|
||||
);
|
||||
export const Heading3 = (props: Props) => (
|
||||
<StyledHeading component="h3" {...props} />
|
||||
);
|
||||
export const Heading4 = (props: Props) => (
|
||||
<StyledHeading component="h4" {...props} />
|
||||
);
|
||||
export const Heading5 = (props: Props) => (
|
||||
<StyledHeading component="h5" {...props} />
|
||||
);
|
||||
export const Heading6 = (props: Props) => (
|
||||
<StyledHeading component="h6" {...props} />
|
||||
);
|
||||
17
app/components/Editor/components/HorizontalRule.js
Normal file
17
app/components/Editor/components/HorizontalRule.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { Props } from '../types';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
function HorizontalRule(props: Props) {
|
||||
const { state, node } = props;
|
||||
const active = state.isFocused && state.selection.hasEdgeIn(node);
|
||||
return <StyledHr active={active} />;
|
||||
}
|
||||
|
||||
const StyledHr = styled.hr`
|
||||
border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)};
|
||||
`;
|
||||
|
||||
export default HorizontalRule;
|
||||
87
app/components/Editor/components/Image.js
Normal file
87
app/components/Editor/components/Image.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { Props } from '../types';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
class Image extends Component {
|
||||
props: Props;
|
||||
|
||||
handleChange = (ev: SyntheticInputEvent) => {
|
||||
const alt = ev.target.value;
|
||||
const { editor, node } = this.props;
|
||||
const data = node.data.toObject();
|
||||
const state = editor
|
||||
.getState()
|
||||
.transform()
|
||||
.setNodeByKey(node.key, { data: { ...data, alt } })
|
||||
.apply();
|
||||
|
||||
editor.onChange(state);
|
||||
};
|
||||
|
||||
handleClick = (ev: SyntheticInputEvent) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { attributes, state, node, readOnly } = this.props;
|
||||
const loading = node.data.get('loading');
|
||||
const caption = node.data.get('alt');
|
||||
const src = node.data.get('src');
|
||||
const active = state.isFocused && state.selection.hasEdgeIn(node);
|
||||
const showCaption = !readOnly || caption;
|
||||
|
||||
return (
|
||||
<CenteredImage>
|
||||
<StyledImg
|
||||
{...attributes}
|
||||
src={src}
|
||||
alt={caption}
|
||||
active={active}
|
||||
loading={loading}
|
||||
/>
|
||||
{showCaption &&
|
||||
<Caption
|
||||
type="text"
|
||||
placeholder="Write a caption"
|
||||
onChange={this.handleChange}
|
||||
onClick={this.handleClick}
|
||||
defaultValue={caption}
|
||||
contentEditable={false}
|
||||
disabled={readOnly}
|
||||
tabIndex={-1}
|
||||
/>}
|
||||
</CenteredImage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StyledImg = styled.img`
|
||||
box-shadow: ${props => (props.active ? `0 0 0 2px ${color.slate}` : '0')};
|
||||
border-radius: ${props => (props.active ? `2px` : '0')};
|
||||
opacity: ${props => (props.loading ? 0.5 : 1)};
|
||||
`;
|
||||
|
||||
const CenteredImage = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Caption = styled.input`
|
||||
border: 0;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
color: ${color.slate};
|
||||
padding: 2px 0;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: ${color.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Image;
|
||||
12
app/components/Editor/components/InlineCode.js
Normal file
12
app/components/Editor/components/InlineCode.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const InlineCode = styled.code`
|
||||
padding: .25em;
|
||||
background: ${color.smoke};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${color.smokeDark};
|
||||
`;
|
||||
|
||||
export default InlineCode;
|
||||
38
app/components/Editor/components/Link.js
Normal file
38
app/components/Editor/components/Link.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Link as InternalLink } from 'react-router-dom';
|
||||
import type { Props } from '../types';
|
||||
|
||||
function getPathFromUrl(href: string) {
|
||||
if (href[0] === '/') return href;
|
||||
|
||||
try {
|
||||
const parsed = new URL(href);
|
||||
return parsed.pathname;
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isAtlasUrl(href: string) {
|
||||
if (href[0] === '/') return true;
|
||||
|
||||
try {
|
||||
const atlas = new URL(BASE_URL);
|
||||
const parsed = new URL(href);
|
||||
return parsed.hostname === atlas.hostname;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Link({ attributes, node, children, readOnly }: Props) {
|
||||
const href = node.data.get('href');
|
||||
const path = getPathFromUrl(href);
|
||||
|
||||
if (isAtlasUrl(href) && readOnly) {
|
||||
return <InternalLink {...attributes} to={path}>{children}</InternalLink>;
|
||||
} else {
|
||||
return <a {...attributes} href={href} target="_blank">{children}</a>;
|
||||
}
|
||||
}
|
||||
16
app/components/Editor/components/ListItem.js
Normal file
16
app/components/Editor/components/ListItem.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import type { Props } from '../types';
|
||||
import TodoItem from './TodoItem';
|
||||
|
||||
export default function ListItem({ children, node, ...props }: Props) {
|
||||
const checked = node.data.get('checked');
|
||||
if (checked !== undefined) {
|
||||
return (
|
||||
<TodoItem checked={checked} node={node} {...props}>
|
||||
{children}
|
||||
</TodoItem>
|
||||
);
|
||||
}
|
||||
return <li>{children}</li>;
|
||||
}
|
||||
34
app/components/Editor/components/Paragraph.js
Normal file
34
app/components/Editor/components/Paragraph.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import type { Props } from '../types';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
export default function Link({
|
||||
attributes,
|
||||
editor,
|
||||
node,
|
||||
parent,
|
||||
children,
|
||||
readOnly,
|
||||
}: Props) {
|
||||
const parentIsDocument = parent instanceof Document;
|
||||
const firstParagraph = parent && parent.nodes.get(1) === node;
|
||||
const lastParagraph = parent && parent.nodes.last() === node;
|
||||
const showPlaceholder =
|
||||
!readOnly &&
|
||||
parentIsDocument &&
|
||||
firstParagraph &&
|
||||
lastParagraph &&
|
||||
!node.text;
|
||||
|
||||
return (
|
||||
<p>
|
||||
{children}
|
||||
{showPlaceholder &&
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.bodyPlaceholder}
|
||||
</Placeholder>}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
11
app/components/Editor/components/Placeholder.js
Normal file
11
app/components/Editor/components/Placeholder.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default styled.span`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #B1BECC;
|
||||
`;
|
||||
51
app/components/Editor/components/TodoItem.js
Normal file
51
app/components/Editor/components/TodoItem.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import type { Props } from '../types';
|
||||
|
||||
export default class TodoItem extends Component {
|
||||
props: Props & { checked: boolean };
|
||||
|
||||
handleChange = (ev: SyntheticInputEvent) => {
|
||||
const checked = ev.target.checked;
|
||||
const { editor, node } = this.props;
|
||||
const state = editor
|
||||
.getState()
|
||||
.transform()
|
||||
.setNodeByKey(node.key, { data: { checked } })
|
||||
.apply();
|
||||
|
||||
editor.onChange(state);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, checked, readOnly } = this.props;
|
||||
|
||||
return (
|
||||
<ListItem checked={checked}>
|
||||
<Input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={this.handleChange}
|
||||
disabled={readOnly}
|
||||
contentEditable={false}
|
||||
/>
|
||||
{children}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ListItem = styled.li`
|
||||
padding-left: 1.4em;
|
||||
position: relative;
|
||||
text-decoration: ${props => (props.checked ? 'line-through' : 'none')};
|
||||
color: ${props => (props.checked ? color.slateDark : 'inherit')};
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.4em;
|
||||
`;
|
||||
13
app/components/Editor/components/TodoList.js
Normal file
13
app/components/Editor/components/TodoList.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
const TodoList = styled.ul`
|
||||
list-style: none;
|
||||
padding: 0 !important;
|
||||
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
`;
|
||||
|
||||
export default TodoList;
|
||||
159
app/components/Editor/components/Toolbar/Toolbar.js
Normal file
159
app/components/Editor/components/Toolbar/Toolbar.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import Portal from 'react-portal';
|
||||
import styled from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import type { State } from '../../types';
|
||||
import FormattingToolbar from './components/FormattingToolbar';
|
||||
import LinkToolbar from './components/LinkToolbar';
|
||||
|
||||
export default class Toolbar extends Component {
|
||||
props: {
|
||||
state: State,
|
||||
onChange: Function,
|
||||
};
|
||||
|
||||
menu: HTMLElement;
|
||||
state: {
|
||||
active: boolean,
|
||||
focused: boolean,
|
||||
link: React$Element<any>,
|
||||
top: string,
|
||||
left: string,
|
||||
};
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
focused: false,
|
||||
link: null,
|
||||
top: '',
|
||||
left: '',
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this.update();
|
||||
};
|
||||
|
||||
componentDidUpdate = () => {
|
||||
this.update();
|
||||
};
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ focused: true });
|
||||
};
|
||||
|
||||
handleBlur = () => {
|
||||
this.setState({ focused: false });
|
||||
};
|
||||
|
||||
get linkInSelection(): any {
|
||||
const { state } = this.props;
|
||||
|
||||
try {
|
||||
const selectedLinks = state.startBlock
|
||||
.getInlinesAtRange(state.selection)
|
||||
.filter(node => node.type === 'link');
|
||||
if (selectedLinks.size) {
|
||||
return selectedLinks.first();
|
||||
}
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
update = () => {
|
||||
const { state } = this.props;
|
||||
const link = this.linkInSelection;
|
||||
|
||||
if (state.isBlurred || (state.isCollapsed && !link)) {
|
||||
if (this.state.active && !this.state.focused) {
|
||||
this.setState({ active: false, link: null, top: '', left: '' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// don't display toolbar for document title
|
||||
const firstNode = state.document.nodes.first();
|
||||
if (firstNode === state.startBlock) return;
|
||||
|
||||
// don't display toolbar for code blocks
|
||||
if (state.startBlock.type === 'code') return;
|
||||
|
||||
const data = {
|
||||
...this.state,
|
||||
active: true,
|
||||
link,
|
||||
focused: !!link,
|
||||
};
|
||||
|
||||
if (!_.isEqual(data, this.state)) {
|
||||
const padding = 16;
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
if (rect.top === 0 && rect.left === 0) {
|
||||
this.setState(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const left =
|
||||
rect.left + window.scrollX - this.menu.offsetWidth / 2 + rect.width / 2;
|
||||
data.top = `${Math.round(rect.top + window.scrollY - this.menu.offsetHeight)}px`;
|
||||
data.left = `${Math.round(Math.max(padding, left))}px`;
|
||||
this.setState(data);
|
||||
}
|
||||
};
|
||||
|
||||
setRef = (ref: HTMLElement) => {
|
||||
this.menu = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const link = this.state.link;
|
||||
|
||||
const style = {
|
||||
top: this.state.top,
|
||||
left: this.state.left,
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal isOpened>
|
||||
<Menu active={this.state.active} innerRef={this.setRef} style={style}>
|
||||
{link &&
|
||||
<LinkToolbar
|
||||
{...this.props}
|
||||
link={link}
|
||||
onBlur={this.handleBlur}
|
||||
/>}
|
||||
{!link &&
|
||||
<FormattingToolbar
|
||||
onCreateLink={this.handleFocus}
|
||||
{...this.props}
|
||||
/>}
|
||||
</Menu>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Menu = styled.div`
|
||||
padding: 8px 16px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
opacity: 0;
|
||||
background-color: #2F3336;
|
||||
border-radius: 4px;
|
||||
transform: scale(.95);
|
||||
transition: opacity 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275), transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
line-height: 0;
|
||||
height: 40px;
|
||||
min-width: 260px;
|
||||
|
||||
${({ active }) => active && `
|
||||
transform: translateY(-6px) scale(1);
|
||||
opacity: 1;
|
||||
`}
|
||||
`;
|
||||
@@ -0,0 +1,47 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { fontWeight, color } from 'styles/constants';
|
||||
import Document from 'models/Document';
|
||||
import NextIcon from 'components/Icon/NextIcon';
|
||||
|
||||
type Props = {
|
||||
innerRef?: Function,
|
||||
onClick: SyntheticEvent => void,
|
||||
document: Document,
|
||||
};
|
||||
|
||||
function DocumentResult({ document, ...rest }: Props) {
|
||||
return (
|
||||
<ListItem {...rest} href="">
|
||||
<i><NextIcon light /></i>
|
||||
{document.title}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
const ListItem = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 4px 8px 4px 0;
|
||||
color: ${color.white};
|
||||
font-size: 15px;
|
||||
|
||||
i {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
font-weight: ${fontWeight.medium};
|
||||
outline: none;
|
||||
|
||||
i {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default DocumentResult;
|
||||
@@ -0,0 +1,118 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { State } from '../../../types';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import BoldIcon from 'components/Icon/BoldIcon';
|
||||
import CodeIcon from 'components/Icon/CodeIcon';
|
||||
import Heading1Icon from 'components/Icon/Heading1Icon';
|
||||
import Heading2Icon from 'components/Icon/Heading2Icon';
|
||||
import ItalicIcon from 'components/Icon/ItalicIcon';
|
||||
import LinkIcon from 'components/Icon/LinkIcon';
|
||||
import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
|
||||
|
||||
class FormattingToolbar extends Component {
|
||||
props: {
|
||||
state: State,
|
||||
onChange: Function,
|
||||
onCreateLink: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current selection has a mark with `type` in it.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasMark = (type: string) => {
|
||||
return this.props.state.marks.some(mark => mark.type === type);
|
||||
};
|
||||
|
||||
isBlock = (type: string) => {
|
||||
return this.props.state.startBlock.type === type;
|
||||
};
|
||||
|
||||
/**
|
||||
* When a mark button is clicked, toggle the current mark.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @param {String} type
|
||||
*/
|
||||
onClickMark = (ev: SyntheticEvent, type: string) => {
|
||||
ev.preventDefault();
|
||||
let { state } = this.props;
|
||||
|
||||
state = state.transform().toggleMark(type).apply();
|
||||
this.props.onChange(state);
|
||||
};
|
||||
|
||||
onClickBlock = (ev: SyntheticEvent, type: string) => {
|
||||
ev.preventDefault();
|
||||
let { state } = this.props;
|
||||
|
||||
state = state.transform().setBlock(type).apply();
|
||||
this.props.onChange(state);
|
||||
};
|
||||
|
||||
onCreateLink = (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
let { state } = this.props;
|
||||
const data = { href: '' };
|
||||
state = state.transform().wrapInline({ type: 'link', data }).apply();
|
||||
this.props.onChange(state);
|
||||
this.props.onCreateLink();
|
||||
};
|
||||
|
||||
renderMarkButton = (type: string, IconClass: Function) => {
|
||||
const isActive = this.hasMark(type);
|
||||
const onMouseDown = ev => this.onClickMark(ev, type);
|
||||
|
||||
return (
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
renderBlockButton = (type: string, IconClass: Function) => {
|
||||
const isActive = this.isBlock(type);
|
||||
const onMouseDown = ev =>
|
||||
this.onClickBlock(ev, isActive ? 'paragraph' : type);
|
||||
|
||||
return (
|
||||
<ToolbarButton onMouseDown={onMouseDown} active={isActive}>
|
||||
<IconClass light />
|
||||
</ToolbarButton>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
{this.renderMarkButton('bold', BoldIcon)}
|
||||
{this.renderMarkButton('italic', ItalicIcon)}
|
||||
{this.renderMarkButton('deleted', StrikethroughIcon)}
|
||||
{this.renderMarkButton('code', CodeIcon)}
|
||||
<Separator />
|
||||
{this.renderBlockButton('heading1', Heading1Icon)}
|
||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||
<Separator />
|
||||
<ToolbarButton onMouseDown={this.onCreateLink}>
|
||||
<LinkIcon light />
|
||||
</ToolbarButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Separator = styled.div`
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: #FFF;
|
||||
opacity: .2;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
export default FormattingToolbar;
|
||||
@@ -0,0 +1,212 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { observable, action } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { withRouter } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import DocumentResult from './DocumentResult';
|
||||
import type { State } from '../../../types';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import keydown from 'react-keydown';
|
||||
import CloseIcon from 'components/Icon/CloseIcon';
|
||||
import OpenIcon from 'components/Icon/OpenIcon';
|
||||
import TrashIcon from 'components/Icon/TrashIcon';
|
||||
import Flex from 'components/Flex';
|
||||
|
||||
@keydown
|
||||
@observer
|
||||
class LinkToolbar extends Component {
|
||||
input: HTMLElement;
|
||||
firstDocument: HTMLElement;
|
||||
|
||||
props: {
|
||||
state: State,
|
||||
link: Object,
|
||||
documents: DocumentsStore,
|
||||
onBlur: () => void,
|
||||
onChange: State => void,
|
||||
};
|
||||
|
||||
@observable isEditing: boolean = false;
|
||||
@observable isFetching: boolean = false;
|
||||
@observable resultIds: string[] = [];
|
||||
@observable searchTerm: ?string = null;
|
||||
|
||||
componentWillMount() {
|
||||
this.isEditing = !!this.props.link.data.get('href');
|
||||
}
|
||||
|
||||
@action search = async () => {
|
||||
this.isFetching = true;
|
||||
|
||||
if (this.searchTerm) {
|
||||
try {
|
||||
this.resultIds = await this.props.documents.search(this.searchTerm);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
this.resultIds = [];
|
||||
}
|
||||
|
||||
this.isFetching = false;
|
||||
};
|
||||
|
||||
selectDocument = (ev, document) => {
|
||||
ev.preventDefault();
|
||||
this.save(document.url);
|
||||
};
|
||||
|
||||
onKeyDown = (ev: SyntheticKeyboardEvent & SyntheticInputEvent) => {
|
||||
switch (ev.keyCode) {
|
||||
case 13: // enter
|
||||
ev.preventDefault();
|
||||
return this.save(ev.target.value);
|
||||
case 27: // escape
|
||||
return this.input.blur();
|
||||
case 40: // down
|
||||
ev.preventDefault();
|
||||
if (this.firstDocument) {
|
||||
const element = ReactDOM.findDOMNode(this.firstDocument);
|
||||
if (element instanceof HTMLElement) element.focus();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (ev: SyntheticKeyboardEvent & SyntheticInputEvent) => {
|
||||
try {
|
||||
new URL(ev.target.value);
|
||||
} catch (err) {
|
||||
// this is not a valid url, show search suggestions
|
||||
this.searchTerm = ev.target.value;
|
||||
this.search();
|
||||
return;
|
||||
}
|
||||
this.resultIds = [];
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
if (!this.resultIds.length) {
|
||||
if (this.input.value) {
|
||||
this.props.onBlur();
|
||||
} else {
|
||||
this.removeLink();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
removeLink = () => {
|
||||
this.save('');
|
||||
};
|
||||
|
||||
openLink = () => {
|
||||
const href = this.props.link.data.get('href');
|
||||
window.open(href, '_blank');
|
||||
};
|
||||
|
||||
save = (href: string) => {
|
||||
href = href.trim();
|
||||
const { state } = this.props;
|
||||
const transform = state.transform();
|
||||
|
||||
if (href) {
|
||||
transform.setInline({ type: 'link', data: { href } });
|
||||
} else {
|
||||
transform.unwrapInline('link');
|
||||
}
|
||||
|
||||
this.props.onChange(transform.apply());
|
||||
this.props.onBlur();
|
||||
};
|
||||
|
||||
setFirstDocumentRef = ref => {
|
||||
this.firstDocument = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const href = this.props.link.data.get('href');
|
||||
const hasResults = this.resultIds.length > 0;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<LinkEditor>
|
||||
<Input
|
||||
innerRef={ref => (this.input = ref)}
|
||||
defaultValue={href}
|
||||
placeholder="Search or paste a link…"
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
autoFocus
|
||||
/>
|
||||
{this.isEditing &&
|
||||
<ToolbarButton onMouseDown={this.openLink}>
|
||||
<OpenIcon light />
|
||||
</ToolbarButton>}
|
||||
<ToolbarButton onMouseDown={this.removeLink}>
|
||||
{this.isEditing ? <TrashIcon light /> : <CloseIcon light />}
|
||||
</ToolbarButton>
|
||||
</LinkEditor>
|
||||
{hasResults &&
|
||||
<SearchResults>
|
||||
<ArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
defaultActiveChildIndex={0}
|
||||
>
|
||||
{this.resultIds.map((id, index) => {
|
||||
const document = this.props.documents.getById(id);
|
||||
if (!document) return null;
|
||||
|
||||
return (
|
||||
<DocumentResult
|
||||
innerRef={ref =>
|
||||
index === 0 && this.setFirstDocumentRef(ref)}
|
||||
document={document}
|
||||
key={document.id}
|
||||
onClick={ev => this.selectDocument(ev, document)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ArrowKeyNavigation>
|
||||
</SearchResults>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SearchResults = styled.div`
|
||||
background: #2F3336;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
margin-top: -3px;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
`;
|
||||
|
||||
const LinkEditor = styled(Flex)`
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
font-size: 15px;
|
||||
background: rgba(255,255,255,.1);
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export default withRouter(inject('documents')(LinkToolbar));
|
||||
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default styled.button`
|
||||
display: inline-block;
|
||||
flex: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
padding: 0;
|
||||
opacity: .7;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${({ active }) => active && 'opacity: 1;'}
|
||||
`;
|
||||
3
app/components/Editor/components/Toolbar/index.js
Normal file
3
app/components/Editor/components/Toolbar/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Toolbar from './Toolbar';
|
||||
export default Toolbar;
|
||||
9
app/components/Editor/headingToSlug.js
Normal file
9
app/components/Editor/headingToSlug.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
import { escape } from 'lodash';
|
||||
import type { Node } from './types';
|
||||
import slug from 'slug';
|
||||
|
||||
export default function headingToSlug(node: Node) {
|
||||
const level = node.type.replace('heading', 'h');
|
||||
return escape(`${level}-${slug(node.text)}-${node.key}`);
|
||||
}
|
||||
3
app/components/Editor/index.js
Normal file
3
app/components/Editor/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Editor from './Editor';
|
||||
export default Editor;
|
||||
56
app/components/Editor/insertImage.js
Normal file
56
app/components/Editor/insertImage.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// @flow
|
||||
import uuid from 'uuid';
|
||||
import uploadFile from 'utils/uploadFile';
|
||||
import type { Editor, Transform } from './types';
|
||||
|
||||
export default async function insertImageFile(
|
||||
transform: Transform,
|
||||
file: window.File,
|
||||
editor: Editor,
|
||||
onImageUploadStart: () => void,
|
||||
onImageUploadStop: () => void
|
||||
) {
|
||||
onImageUploadStart();
|
||||
|
||||
try {
|
||||
// load the file as a data URL
|
||||
const id = uuid.v4();
|
||||
const alt = '';
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
const src = reader.result;
|
||||
|
||||
// insert into document as uploading placeholder
|
||||
const state = transform
|
||||
.insertBlock({
|
||||
type: 'image',
|
||||
isVoid: true,
|
||||
data: { src, id, alt, loading: true },
|
||||
})
|
||||
.apply();
|
||||
editor.onChange(state);
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// now we have a placeholder, start the upload
|
||||
const asset = await uploadFile(file);
|
||||
const src = asset.url;
|
||||
|
||||
// we dont use the original transform provided to the callback here
|
||||
// as the state may have changed significantly in the time it took to
|
||||
// upload the file.
|
||||
const state = editor.getState();
|
||||
const finalTransform = state.transform();
|
||||
const placeholder = state.document.findDescendant(
|
||||
node => node.data && node.data.get('id') === id
|
||||
);
|
||||
|
||||
return finalTransform.setNodeByKey(placeholder.key, {
|
||||
data: { src, alt, loading: false },
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
onImageUploadStop();
|
||||
}
|
||||
}
|
||||
57
app/components/Editor/plugins.js
Normal file
57
app/components/Editor/plugins.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// @flow
|
||||
import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images';
|
||||
import PasteLinkify from 'slate-paste-linkify';
|
||||
import CollapseOnEscape from 'slate-collapse-on-escape';
|
||||
import TrailingBlock from 'slate-trailing-block';
|
||||
import EditCode from 'slate-edit-code';
|
||||
import Prism from 'slate-prism';
|
||||
import EditList from './plugins/EditList';
|
||||
import KeyboardShortcuts from './plugins/KeyboardShortcuts';
|
||||
import MarkdownShortcuts from './plugins/MarkdownShortcuts';
|
||||
import insertImage from './insertImage';
|
||||
|
||||
const onlyInCode = node => node.type === 'code';
|
||||
|
||||
type Options = {
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
};
|
||||
|
||||
const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => {
|
||||
return [
|
||||
PasteLinkify({
|
||||
type: 'link',
|
||||
collapseTo: 'end',
|
||||
}),
|
||||
DropOrPasteImages({
|
||||
extensions: ['png', 'jpg', 'gif'],
|
||||
applyTransform: (transform, file, editor) => {
|
||||
return insertImage(
|
||||
transform,
|
||||
file,
|
||||
editor,
|
||||
onImageUploadStart,
|
||||
onImageUploadStop
|
||||
);
|
||||
},
|
||||
}),
|
||||
EditList,
|
||||
EditCode({
|
||||
onlyIn: onlyInCode,
|
||||
containerType: 'code',
|
||||
lineType: 'code-line',
|
||||
exitBlocktype: 'paragraph',
|
||||
selectAll: true,
|
||||
}),
|
||||
Prism({
|
||||
onlyIn: onlyInCode,
|
||||
getSyntax: node => 'javascript',
|
||||
}),
|
||||
CollapseOnEscape({ toEdge: 'end' }),
|
||||
TrailingBlock({ type: 'paragraph' }),
|
||||
KeyboardShortcuts(),
|
||||
MarkdownShortcuts(),
|
||||
];
|
||||
};
|
||||
|
||||
export default createPlugins;
|
||||
7
app/components/Editor/plugins/EditList.js
Normal file
7
app/components/Editor/plugins/EditList.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
import EditList from 'slate-edit-list';
|
||||
|
||||
export default EditList({
|
||||
types: ['ordered-list', 'bulleted-list', 'todo-list'],
|
||||
typeItem: 'list-item',
|
||||
});
|
||||
44
app/components/Editor/plugins/KeyboardShortcuts.js
Normal file
44
app/components/Editor/plugins/KeyboardShortcuts.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// @flow
|
||||
|
||||
export default function KeyboardShortcuts() {
|
||||
return {
|
||||
/**
|
||||
* On key down, check for our specific key shortcuts.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Data} data
|
||||
* @param {State} state
|
||||
* @return {State or Null} state
|
||||
*/
|
||||
onKeyDown(ev: SyntheticEvent, data: Object, state: Object) {
|
||||
if (!data.isMeta) return null;
|
||||
|
||||
switch (data.key) {
|
||||
case 'b':
|
||||
return this.toggleMark(state, 'bold');
|
||||
case 'i':
|
||||
return this.toggleMark(state, 'italic');
|
||||
case 'u':
|
||||
return this.toggleMark(state, 'underlined');
|
||||
case 'd':
|
||||
return this.toggleMark(state, 'deleted');
|
||||
case 'k':
|
||||
return state
|
||||
.transform()
|
||||
.wrapInline({ type: 'link', data: { href: '' } })
|
||||
.apply();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
toggleMark(state: Object, type: string) {
|
||||
// don't allow formatting of document title
|
||||
const firstNode = state.document.nodes.first();
|
||||
if (firstNode === state.startBlock) return;
|
||||
|
||||
state = state.transform().toggleMark(type).apply();
|
||||
return state;
|
||||
},
|
||||
};
|
||||
}
|
||||
290
app/components/Editor/plugins/MarkdownShortcuts.js
Normal file
290
app/components/Editor/plugins/MarkdownShortcuts.js
Normal file
@@ -0,0 +1,290 @@
|
||||
// @flow
|
||||
const inlineShortcuts = [
|
||||
{ mark: 'bold', shortcut: '**' },
|
||||
{ mark: 'bold', shortcut: '__' },
|
||||
{ mark: 'italic', shortcut: '*' },
|
||||
{ mark: 'italic', shortcut: '_' },
|
||||
{ mark: 'code', shortcut: '`' },
|
||||
{ mark: 'added', shortcut: '++' },
|
||||
{ mark: 'deleted', shortcut: '~~' },
|
||||
];
|
||||
|
||||
export default function MarkdownShortcuts() {
|
||||
return {
|
||||
/**
|
||||
* On key down, check for our specific key shortcuts.
|
||||
*/
|
||||
onKeyDown(ev: SyntheticEvent, data: Object, state: Object) {
|
||||
switch (data.key) {
|
||||
case '-':
|
||||
return this.onDash(ev, state);
|
||||
case '`':
|
||||
return this.onBacktick(ev, state);
|
||||
case 'tab':
|
||||
return this.onTab(ev, state);
|
||||
case 'space':
|
||||
return this.onSpace(ev, state);
|
||||
case 'backspace':
|
||||
return this.onBackspace(ev, state);
|
||||
case 'enter':
|
||||
return this.onEnter(ev, state);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On space, if it was after an auto-markdown shortcut, convert the current
|
||||
* node into the shortcut's corresponding type.
|
||||
*/
|
||||
onSpace(ev: SyntheticEvent, state: Object) {
|
||||
if (state.isExpanded) return;
|
||||
const { startBlock, startOffset } = state;
|
||||
const chars = startBlock.text.slice(0, startOffset).trim();
|
||||
const type = this.getType(chars);
|
||||
|
||||
if (type) {
|
||||
if (type === 'list-item' && startBlock.type === 'list-item') return;
|
||||
ev.preventDefault();
|
||||
|
||||
let checked;
|
||||
if (chars === '[x]') checked = true;
|
||||
if (chars === '[ ]') checked = false;
|
||||
const transform = state
|
||||
.transform()
|
||||
.setBlock({ type, data: { checked } });
|
||||
|
||||
if (type === 'list-item') {
|
||||
if (checked !== undefined) {
|
||||
transform.wrapBlock('todo-list');
|
||||
} else if (chars === '1.') {
|
||||
transform.wrapBlock('ordered-list');
|
||||
} else {
|
||||
transform.wrapBlock('bulleted-list');
|
||||
}
|
||||
}
|
||||
|
||||
state = transform.extendToStartOf(startBlock).delete().apply();
|
||||
return state;
|
||||
}
|
||||
|
||||
for (const key of inlineShortcuts) {
|
||||
// find all inline characters
|
||||
let { mark, shortcut } = key;
|
||||
let inlineTags = [];
|
||||
|
||||
// only add tags if they have spaces around them or the tag is beginning or the end of the block
|
||||
for (let i = 0; i < startBlock.text.length; i++) {
|
||||
const { text } = startBlock;
|
||||
const start = i;
|
||||
const end = i + shortcut.length;
|
||||
const beginningOfBlock = start === 0;
|
||||
const endOfBlock = end === text.length;
|
||||
const surroundedByWhitespaces = [
|
||||
text.slice(start - 1, start),
|
||||
text.slice(end, end + 1),
|
||||
].includes(' ');
|
||||
|
||||
if (
|
||||
text.slice(start, end) === shortcut &&
|
||||
(beginningOfBlock || endOfBlock || surroundedByWhitespaces)
|
||||
)
|
||||
inlineTags.push(i);
|
||||
}
|
||||
|
||||
// if we have multiple tags then mark the text between as inline code
|
||||
if (inlineTags.length > 1) {
|
||||
const transform = state.transform();
|
||||
const firstText = startBlock.getFirstText();
|
||||
const firstCodeTagIndex = inlineTags[0];
|
||||
const lastCodeTagIndex = inlineTags[inlineTags.length - 1];
|
||||
transform.removeTextByKey(
|
||||
firstText.key,
|
||||
lastCodeTagIndex,
|
||||
shortcut.length
|
||||
);
|
||||
transform.removeTextByKey(
|
||||
firstText.key,
|
||||
firstCodeTagIndex,
|
||||
shortcut.length
|
||||
);
|
||||
transform.moveOffsetsTo(
|
||||
firstCodeTagIndex,
|
||||
lastCodeTagIndex - shortcut.length
|
||||
);
|
||||
transform.addMark(mark);
|
||||
state = transform.collapseToEnd().removeMark(mark).apply();
|
||||
return state;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onDash(ev: SyntheticEvent, state: Object) {
|
||||
if (state.isExpanded) return;
|
||||
const { startBlock, startOffset } = state;
|
||||
const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, '');
|
||||
|
||||
if (chars === '--') {
|
||||
ev.preventDefault();
|
||||
return state
|
||||
.transform()
|
||||
.extendToStartOf(startBlock)
|
||||
.delete()
|
||||
.setBlock({
|
||||
type: 'horizontal-rule',
|
||||
isVoid: true,
|
||||
})
|
||||
.collapseToStartOfNextBlock()
|
||||
.insertBlock('paragraph')
|
||||
.apply();
|
||||
}
|
||||
},
|
||||
|
||||
onBacktick(ev: SyntheticEvent, state: Object) {
|
||||
if (state.isExpanded) return;
|
||||
const { startBlock, startOffset } = state;
|
||||
const chars = startBlock.text.slice(0, startOffset).replace(/\s*/g, '');
|
||||
|
||||
if (chars === '``') {
|
||||
ev.preventDefault();
|
||||
return state
|
||||
.transform()
|
||||
.extendToStartOf(startBlock)
|
||||
.delete()
|
||||
.setBlock({
|
||||
type: 'code',
|
||||
})
|
||||
.apply();
|
||||
}
|
||||
},
|
||||
|
||||
onBackspace(ev: SyntheticEvent, state: Object) {
|
||||
if (state.isExpanded) return;
|
||||
const { startBlock, selection, startOffset } = state;
|
||||
|
||||
// If at the start of a non-paragraph, convert it back into a paragraph
|
||||
if (startOffset === 0) {
|
||||
if (startBlock.type === 'paragraph') return;
|
||||
ev.preventDefault();
|
||||
|
||||
const transform = state.transform().setBlock('paragraph');
|
||||
|
||||
if (startBlock.type === 'list-item')
|
||||
transform.unwrapBlock('bulleted-list');
|
||||
|
||||
state = transform.apply();
|
||||
return state;
|
||||
}
|
||||
|
||||
// If at the end of a code mark hitting backspace should remove the mark
|
||||
if (selection.isCollapsed) {
|
||||
const marksAtCursor = startBlock.getMarksAtRange(selection);
|
||||
const codeMarksAtCursor = marksAtCursor.filter(
|
||||
mark => mark.type === 'code'
|
||||
);
|
||||
|
||||
if (codeMarksAtCursor.size > 0) {
|
||||
ev.preventDefault();
|
||||
|
||||
const textNode = startBlock.getTextAtOffset(startOffset);
|
||||
const charsInCodeBlock = textNode.characters
|
||||
.takeUntil((v, k) => k === startOffset)
|
||||
.reverse()
|
||||
.takeUntil((v, k) => !v.marks.some(mark => mark.type === 'code'));
|
||||
|
||||
const transform = state.transform();
|
||||
transform.removeMarkByKey(
|
||||
textNode.key,
|
||||
state.startOffset - charsInCodeBlock.size,
|
||||
state.startOffset,
|
||||
'code'
|
||||
);
|
||||
state = transform.apply();
|
||||
return state;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On tab, if at the end of the heading jump to the main body content
|
||||
* as if it is another input field (act the same as enter).
|
||||
*/
|
||||
onTab(ev: SyntheticEvent, state: Object) {
|
||||
if (state.startBlock.type === 'heading1') {
|
||||
ev.preventDefault();
|
||||
return state.transform().splitBlock().setBlock('paragraph').apply();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On return, if at the end of a node type that should not be extended,
|
||||
* create a new paragraph below it.
|
||||
*/
|
||||
onEnter(ev: SyntheticEvent, state: Object) {
|
||||
if (state.isExpanded) return;
|
||||
const { startBlock, startOffset, endOffset } = state;
|
||||
if (startOffset === 0 && startBlock.length === 0)
|
||||
return this.onBackspace(ev, state);
|
||||
if (endOffset !== startBlock.length) return;
|
||||
|
||||
// Hitting enter while an image is selected should jump caret below and
|
||||
// insert a new paragraph
|
||||
if (startBlock.type === 'image') {
|
||||
ev.preventDefault();
|
||||
return state
|
||||
.transform()
|
||||
.collapseToEnd()
|
||||
.insertBlock('paragraph')
|
||||
.apply();
|
||||
}
|
||||
|
||||
// Hitting enter in a heading or blockquote will split the node at that
|
||||
// point and make the new node a paragraph
|
||||
if (
|
||||
startBlock.type !== 'heading1' &&
|
||||
startBlock.type !== 'heading2' &&
|
||||
startBlock.type !== 'heading3' &&
|
||||
startBlock.type !== 'heading4' &&
|
||||
startBlock.type !== 'heading5' &&
|
||||
startBlock.type !== 'heading6' &&
|
||||
startBlock.type !== 'block-quote'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
return state.transform().splitBlock().setBlock('paragraph').apply();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the block type for a series of auto-markdown shortcut `chars`.
|
||||
*/
|
||||
getType(chars: string) {
|
||||
switch (chars) {
|
||||
case '*':
|
||||
case '-':
|
||||
case '+':
|
||||
case '1.':
|
||||
case '[ ]':
|
||||
case '[x]':
|
||||
return 'list-item';
|
||||
case '>':
|
||||
return 'block-quote';
|
||||
case '#':
|
||||
return 'heading1';
|
||||
case '##':
|
||||
return 'heading2';
|
||||
case '###':
|
||||
return 'heading3';
|
||||
case '####':
|
||||
return 'heading4';
|
||||
case '#####':
|
||||
return 'heading5';
|
||||
case '######':
|
||||
return 'heading6';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
106
app/components/Editor/schema.js
Normal file
106
app/components/Editor/schema.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Code from './components/Code';
|
||||
import HorizontalRule from './components/HorizontalRule';
|
||||
import InlineCode from './components/InlineCode';
|
||||
import Image from './components/Image';
|
||||
import Link from './components/Link';
|
||||
import ListItem from './components/ListItem';
|
||||
import TodoList from './components/TodoList';
|
||||
import {
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Heading5,
|
||||
Heading6,
|
||||
} from './components/Heading';
|
||||
import Paragraph from './components/Paragraph';
|
||||
import type { Props, Node, Transform } from './types';
|
||||
|
||||
const createSchema = () => {
|
||||
return {
|
||||
marks: {
|
||||
bold: (props: Props) => <strong>{props.children}</strong>,
|
||||
code: (props: Props) => <InlineCode>{props.children}</InlineCode>,
|
||||
italic: (props: Props) => <em>{props.children}</em>,
|
||||
underlined: (props: Props) => <u>{props.children}</u>,
|
||||
deleted: (props: Props) => <del>{props.children}</del>,
|
||||
added: (props: Props) => <mark>{props.children}</mark>,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
paragraph: (props: Props) => <Paragraph {...props} />,
|
||||
'block-quote': (props: Props) => (
|
||||
<blockquote>{props.children}</blockquote>
|
||||
),
|
||||
'horizontal-rule': HorizontalRule,
|
||||
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
|
||||
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
|
||||
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,
|
||||
table: (props: Props) => <table>{props.children}</table>,
|
||||
'table-row': (props: Props) => <tr>{props.children}</tr>,
|
||||
'table-head': (props: Props) => <th>{props.children}</th>,
|
||||
'table-cell': (props: Props) => <td>{props.children}</td>,
|
||||
code: Code,
|
||||
image: Image,
|
||||
link: Link,
|
||||
'list-item': ListItem,
|
||||
heading1: (props: Props) => <Heading1 placeholder {...props} />,
|
||||
heading2: (props: Props) => <Heading2 {...props} />,
|
||||
heading3: (props: Props) => <Heading3 {...props} />,
|
||||
heading4: (props: Props) => <Heading4 {...props} />,
|
||||
heading5: (props: Props) => <Heading5 {...props} />,
|
||||
heading6: (props: Props) => <Heading6 {...props} />,
|
||||
},
|
||||
|
||||
rules: [
|
||||
// ensure first node is always a heading
|
||||
{
|
||||
match: (node: Node) => {
|
||||
return node.kind === 'document';
|
||||
},
|
||||
validate: (document: Node) => {
|
||||
const firstNode = document.nodes.first();
|
||||
return firstNode && firstNode.type === 'heading1' ? null : firstNode;
|
||||
},
|
||||
normalize: (transform: Transform, document: Node, firstNode: Node) => {
|
||||
transform.setBlock({ type: 'heading1' });
|
||||
},
|
||||
},
|
||||
|
||||
// automatically removes any marks in first heading
|
||||
{
|
||||
match: (node: Node) => {
|
||||
return node.kind === 'heading1';
|
||||
},
|
||||
validate: (heading: Node) => {
|
||||
const hasMarks = heading.getMarks().isEmpty();
|
||||
const hasInlines = heading.getInlines().isEmpty();
|
||||
|
||||
return !(hasMarks && hasInlines);
|
||||
},
|
||||
normalize: (transform: Transform, heading: Node) => {
|
||||
transform.unwrapInlineByKey(heading.key);
|
||||
|
||||
heading.getMarks().forEach(mark => {
|
||||
heading.nodes.forEach(textNode => {
|
||||
if (textNode.kind === 'text') {
|
||||
transform.removeMarkByKey(
|
||||
textNode.key,
|
||||
0,
|
||||
textNode.text.length,
|
||||
mark
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return transform;
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default createSchema;
|
||||
3
app/components/Editor/serializer.js
Normal file
3
app/components/Editor/serializer.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import MarkdownSerializer from 'slate-markdown-serializer';
|
||||
export default new MarkdownSerializer();
|
||||
113
app/components/Editor/types.js
Normal file
113
app/components/Editor/types.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// @flow
|
||||
import { List, Set, Map } from 'immutable';
|
||||
import { Selection } from 'slate';
|
||||
|
||||
export type NodeTransform = {
|
||||
addMarkByKey: Function,
|
||||
insertNodeByKey: Function,
|
||||
insertTextByKey: Function,
|
||||
moveNodeByKey: Function,
|
||||
removeMarkByKey: Function,
|
||||
removeNodeByKey: Function,
|
||||
removeTextByKey: Function,
|
||||
setMarkByKey: Function,
|
||||
setNodeByKey: Function,
|
||||
splitNodeByKey: Function,
|
||||
unwrapInlineByKey: Function,
|
||||
unwrapBlockByKey: Function,
|
||||
unwrapNodeByKey: Function,
|
||||
wrapBlockByKey: Function,
|
||||
wrapInlineByKey: Function,
|
||||
};
|
||||
|
||||
export type StateTransform = {
|
||||
deleteBackward: Function,
|
||||
deleteForward: Function,
|
||||
delete: Function,
|
||||
insertBlock: Function,
|
||||
insertFragment: Function,
|
||||
insertInline: Function,
|
||||
insertText: Function,
|
||||
addMark: Function,
|
||||
setBlock: Function,
|
||||
setInline: Function,
|
||||
splitBlock: Function,
|
||||
splitInline: Function,
|
||||
removeMark: Function,
|
||||
toggleMark: Function,
|
||||
unwrapBlock: Function,
|
||||
unwrapInline: Function,
|
||||
wrapBlock: Function,
|
||||
wrapInline: Function,
|
||||
wrapText: Function,
|
||||
};
|
||||
|
||||
export type Transform = NodeTransform & StateTransform;
|
||||
|
||||
export type Editor = {
|
||||
props: Object,
|
||||
className: string,
|
||||
onChange: Function,
|
||||
onDocumentChange: Function,
|
||||
onSelectionChange: Function,
|
||||
plugins: Array<Object>,
|
||||
readOnly: boolean,
|
||||
state: Object,
|
||||
style: Object,
|
||||
placeholder?: string,
|
||||
placeholderClassName?: string,
|
||||
placeholderStyle?: string,
|
||||
blur: Function,
|
||||
focus: Function,
|
||||
getSchema: Function,
|
||||
getState: Function,
|
||||
};
|
||||
|
||||
export type Node = {
|
||||
key: string,
|
||||
kind: string,
|
||||
type: string,
|
||||
length: number,
|
||||
text: string,
|
||||
data: Map<string, any>,
|
||||
nodes: List<Node>,
|
||||
getMarks: Function,
|
||||
getBlocks: Function,
|
||||
getParent: Function,
|
||||
getInlines: Function,
|
||||
getInlinesAtRange: Function,
|
||||
setBlock: Function,
|
||||
};
|
||||
|
||||
export type Block = Node & {
|
||||
type: string,
|
||||
};
|
||||
|
||||
export type Document = Node;
|
||||
|
||||
export type State = {
|
||||
document: Document,
|
||||
selection: Selection,
|
||||
startBlock: Block,
|
||||
endBlock: Block,
|
||||
startText: Node,
|
||||
endText: Node,
|
||||
marks: Set<*>,
|
||||
blocks: List<Block>,
|
||||
fragment: Document,
|
||||
lines: List<Node>,
|
||||
tests: List<Node>,
|
||||
startBlock: Block,
|
||||
transform: Function,
|
||||
isBlurred: Function,
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
node: Node,
|
||||
parent?: Node,
|
||||
attributes?: Object,
|
||||
state: State,
|
||||
editor: Editor,
|
||||
readOnly?: boolean,
|
||||
children?: React$Element<any>,
|
||||
};
|
||||
21
app/components/Empty/Empty.js
Normal file
21
app/components/Empty/Empty.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
type Props = {
|
||||
children: string,
|
||||
};
|
||||
|
||||
const Empty = (props: Props) => {
|
||||
const { children, ...rest } = props;
|
||||
return <Container {...rest}>{children}</Container>;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
color: ${color.slate};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export default Empty;
|
||||
3
app/components/Empty/index.js
Normal file
3
app/components/Empty/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Empty from './Empty';
|
||||
export default Empty;
|
||||
41
app/components/Flex/Flex.js
Normal file
41
app/components/Flex/Flex.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type JustifyValues =
|
||||
| 'center'
|
||||
| 'space-around'
|
||||
| 'space-between'
|
||||
| 'flex-start'
|
||||
| 'flex-end';
|
||||
|
||||
type AlignValues =
|
||||
| 'stretch'
|
||||
| 'center'
|
||||
| 'baseline'
|
||||
| 'flex-start'
|
||||
| 'flex-end';
|
||||
|
||||
type Props = {
|
||||
column?: ?boolean,
|
||||
align?: AlignValues,
|
||||
justify?: JustifyValues,
|
||||
auto?: ?boolean,
|
||||
className?: string,
|
||||
children?: React.Element<any>,
|
||||
};
|
||||
|
||||
const Flex = (props: Props) => {
|
||||
const { children, ...restProps } = props;
|
||||
return <Container {...restProps}>{children}</Container>;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: ${({ auto }) => (auto ? '1 1 auto' : 'initial')};
|
||||
flex-direction: ${({ column }) => (column ? 'column' : 'row')};
|
||||
align-items: ${({ align }) => align};
|
||||
justify-content: ${({ justify }) => justify};
|
||||
`;
|
||||
|
||||
export default Flex;
|
||||
3
app/components/Flex/index.js
Normal file
3
app/components/Flex/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Flex from './Flex';
|
||||
export default Flex;
|
||||
10
app/components/HelpText/HelpText.js
Normal file
10
app/components/HelpText/HelpText.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
const HelpText = styled.p`
|
||||
margin-top: 0;
|
||||
color: ${color.slateDark};
|
||||
`;
|
||||
|
||||
export default HelpText;
|
||||
3
app/components/HelpText/index.js
Normal file
3
app/components/HelpText/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import HelpText from './HelpText';
|
||||
export default HelpText;
|
||||
15
app/components/Icon/BackIcon.js
Normal file
15
app/components/Icon/BackIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BackIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M7.20710678,8.79289322 C6.81658249,8.40236893 6.18341751,8.40236893 5.79289322,8.79289322 C5.40236893,9.18341751 5.40236893,9.81658249 5.79289322,10.2071068 L10.7928932,15.2071068 C11.1834175,15.5976311 11.8165825,15.5976311 12.2071068,15.2071068 L17.2071068,10.2071068 C17.5976311,9.81658249 17.5976311,9.18341751 17.2071068,8.79289322 C16.8165825,8.40236893 16.1834175,8.40236893 15.7928932,8.79289322 L11.5,13.0857864 L7.20710678,8.79289322 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/BoldIcon.js
Normal file
12
app/components/Icon/BoldIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BoldIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M18,15 C18,17.209139 16.209139,19 14,19 L8,19 C7.44771525,19 7,18.5522847 7,18 L7,6 C7,5.44771525 7.44771525,5 8,5 L13,5 C15.209139,5 17,6.790861 17,9 C17,9.9796381 16.6478342,10.8770235 16.0631951,11.5724638 C17.2238614,12.2726251 18,13.5456741 18,15 Z M9,17 L14,17 C15.1045695,17 16,16.1045695 16,15 C16,13.8954305 15.1045695,13 14,13 L9,13 L9,17 Z M9,11 L13,11 C14.1045695,11 15,10.1045695 15,9 C15,7.8954305 14.1045695,7 13,7 L9,7 L9,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/BulletedListIcon.js
Normal file
12
app/components/Icon/BulletedListIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function BulletedListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M10,6 L19,6 C19.5522847,6 20,6.44771525 20,7 L20,7 C20,7.55228475 19.5522847,8 19,8 L10,8 C9.44771525,8 9,7.55228475 9,7 L9,7 L9,7 C9,6.44771525 9.44771525,6 10,6 Z M10,16 L19,16 C19.5522847,16 20,16.4477153 20,17 C20,17.5522847 19.5522847,18 19,18 L10,18 C9.44771525,18 9,17.5522847 9,17 C9,16.4477153 9.44771525,16 10,16 Z M10,11 L19,11 C19.5522847,11 20,11.4477153 20,12 C20,12.5522847 19.5522847,13 19,13 L10,13 C9.44771525,13 9,12.5522847 9,12 C9,11.4477153 9.44771525,11 10,11 Z M5,10.5 L5,10.5 C5.82842712,10.5 6.5,11.1715729 6.5,12 C6.5,12.8284271 5.82842712,13.5 5,13.5 C4.17157288,13.5 3.5,12.8284271 3.5,12 C3.5,11.1715729 4.17157288,10.5 5,10.5 L5,10.5 Z M5,5.5 L5,5.5 C5.82842712,5.5 6.5,6.17157288 6.5,7 L6.5,7 C6.5,7.82842712 5.82842712,8.5 5,8.5 C4.17157288,8.5 3.5,7.82842712 3.5,7 L3.5,7 L3.5,7 C3.5,6.17157288 4.17157288,5.5 5,5.5 L5,5.5 Z M5,15.5 L5,15.5 C5.82842712,15.5 6.5,16.1715729 6.5,17 C6.5,17.8284271 5.82842712,18.5 5,18.5 C4.17157288,18.5 3.5,17.8284271 3.5,17 C3.5,16.1715729 4.17157288,15.5 5,15.5 L5,15.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
20
app/components/Icon/CheckboxIcon.js
Normal file
20
app/components/Icon/CheckboxIcon.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CheckboxIcon({
|
||||
checked,
|
||||
...rest
|
||||
}: Props & { checked: boolean }) {
|
||||
return (
|
||||
<Icon {...rest}>
|
||||
{checked
|
||||
? <path d="M8,5 L16,5 L16,5 C17.6568542,5 19,6.34314575 19,8 L19,16 C19,17.6568542 17.6568542,19 16,19 L8,19 L8,19 C6.34314575,19 5,17.6568542 5,16 L5,8 L5,8 C5,6.34314575 6.34314575,5 8,5 L8,5 Z M10.958729,12.8883948 L9.26824635,10.8598156 C8.91468227,10.4355387 8.28411757,10.3782146 7.85984067,10.7317787 C7.43556378,11.0853428 7.37823971,11.7159075 7.73180379,12.1401844 L10.2318038,15.1401844 C10.6450125,15.6360348 11.4127535,15.616362 11.8000251,15.1 L16.3000251,9.1 C16.6313959,8.6581722 16.5418529,8.03137085 16.1000251,7.7 C15.6581973,7.36862915 15.0313959,7.4581722 14.7000251,7.9 L10.958729,12.8883948 Z" />
|
||||
: <path
|
||||
d="M8,5 L16,5 L16,5 C17.6568542,5 19,6.34314575 19,8 L19,16 C19,17.6568542 17.6568542,19 16,19 L8,19 L8,19 C6.34314575,19 5,17.6568542 5,16 L5,8 L5,8 C5,6.34314575 6.34314575,5 8,5 L8,5 Z M8,7 C7.44771525,7 7,7.44771525 7,8 L7,16 C7,16.5522847 7.44771525,17 8,17 L16,17 C16.5522847,17 17,16.5522847 17,16 L17,8 C17,7.44771525 16.5522847,7 16,7 L8,7 Z"
|
||||
id="path-1"
|
||||
/>}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/CloseIcon.js
Normal file
15
app/components/Icon/CloseIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CloseIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M12,10.5857864 L8.70710678,7.29289322 C8.31658249,6.90236893 7.68341751,6.90236893 7.29289322,7.29289322 C6.90236893,7.68341751 6.90236893,8.31658249 7.29289322,8.70710678 L10.5857864,12 L7.29289322,15.2928932 C6.90236893,15.6834175 6.90236893,16.3165825 7.29289322,16.7071068 C7.68341751,17.0976311 8.31658249,17.0976311 8.70710678,16.7071068 L12,13.4142136 L15.2928932,16.7071068 C15.6834175,17.0976311 16.3165825,17.0976311 16.7071068,16.7071068 C17.0976311,16.3165825 17.0976311,15.6834175 16.7071068,15.2928932 L13.4142136,12 L16.7071068,8.70710678 C17.0976311,8.31658249 17.0976311,7.68341751 16.7071068,7.29289322 C16.3165825,6.90236893 15.6834175,6.90236893 15.2928932,7.29289322 L12,10.5857864 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/CodeIcon.js
Normal file
12
app/components/Icon/CodeIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CodeIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M11.9805807,17.1961161 C11.8722687,17.7376759 11.3454436,18.0888926 10.8038839,17.9805807 C10.2623241,17.8722687 9.91110737,17.3454436 10.0194193,16.8038839 L12.0194193,6.80388386 C12.1277313,6.26232411 12.6545564,5.91110737 13.1961161,6.01941932 C13.7376759,6.12773127 14.0888926,6.65455638 13.9805807,7.19611614 L11.9805807,17.1961161 Z M6.41421356,12 L8.70710678,14.2928932 C9.09763107,14.6834175 9.09763107,15.3165825 8.70710678,15.7071068 C8.31658249,16.0976311 7.68341751,16.0976311 7.29289322,15.7071068 L4.29289322,12.7071068 C3.90236893,12.3165825 3.90236893,11.6834175 4.29289322,11.2928932 L7.29289322,8.29289322 C7.68341751,7.90236893 8.31658249,7.90236893 8.70710678,8.29289322 C9.09763107,8.68341751 9.09763107,9.31658249 8.70710678,9.70710678 L6.41421356,12 Z M15.2928932,14.2928932 L17.5857864,12 L15.2928932,9.70710678 C14.9023689,9.31658249 14.9023689,8.68341751 15.2928932,8.29289322 C15.6834175,7.90236893 16.3165825,7.90236893 16.7071068,8.29289322 L19.7071068,11.2928932 C20.0976311,11.6834175 20.0976311,12.3165825 19.7071068,12.7071068 L16.7071068,15.7071068 C16.3165825,16.0976311 15.6834175,16.0976311 15.2928932,15.7071068 C14.9023689,15.3165825 14.9023689,14.6834175 15.2928932,14.2928932 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/CollapsedIcon.js
Normal file
12
app/components/Icon/CollapsedIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CollapsedIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M8.23823905,10.6097108 L11.207376,14.4695888 L11.207376,14.4695888 C11.54411,14.907343 12.1719566,14.989236 12.6097108,14.652502 C12.6783439,14.5997073 12.7398293,14.538222 12.792624,14.4695888 L15.761761,10.6097108 L15.761761,10.6097108 C16.0984949,10.1719566 16.0166019,9.54410997 15.5788477,9.20737601 C15.4040391,9.07290785 15.1896811,9 14.969137,9 L9.03086304,9 L9.03086304,9 C8.47857829,9 8.03086304,9.44771525 8.03086304,10 C8.03086304,10.2205442 8.10377089,10.4349022 8.23823905,10.6097108 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
17
app/components/Icon/CollectionIcon.js
Normal file
17
app/components/Icon/CollectionIcon.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function CollectionIcon({
|
||||
expanded,
|
||||
...rest
|
||||
}: Props & { expanded: boolean }) {
|
||||
return (
|
||||
<Icon {...rest}>
|
||||
{expanded
|
||||
? <path d="M14,3.28571429 C15.1045695,3.12791864 16,3.8954305 16,5 L16,19 C16,20.1045695 15.1045695,20.8720814 14,20.7142857 L7,19.2857143 C5.8954305,19.1279186 5,18.3284271 5,17.5 L5,6.5 C5,5.67157288 5.8954305,4.87208136 7,4.71428571 L14,3.28571429 Z M7.5,6.47598949 L8.5,6.37337629 C8.77614237,6.34504044 9,6.49817875 9,6.71542029 L9,17.2845797 C9,17.5018212 8.77614237,17.6549596 8.5,17.6266237 L7.5,17.5240105 C7.22385763,17.4956747 7,17.3042518 7,17.0964555 L7,6.90354448 C7,6.69574823 7.22385763,6.50432534 7.5,6.47598949 Z M17,4 C18.1045695,4 19,4.8954305 19,6 L19,18 C19,19.1045695 18.1045695,20 17,20 L17,4 Z" />
|
||||
: <path d="M7,4 L17,4 C18.1045695,4 19,4.8954305 19,6 L19,18 C19,19.1045695 18.1045695,20 17,20 L7,20 C5.8954305,20 5,19.1045695 5,18 L5,6 L5,6 C5,4.8954305 5.8954305,4 7,4 L7,4 Z M7.5,6 C7.22385763,6 7,6.22385763 7,6.5 L7,17.5 C7,17.7761424 7.22385763,18 7.5,18 L8.5,18 C8.77614237,18 9,17.7761424 9,17.5 L9,6.5 C9,6.22385763 8.77614237,6 8.5,6 L7.5,6 Z" />}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/DocumentIcon.js
Normal file
15
app/components/Icon/DocumentIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function DocumentIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M13.5,7.5 L16.5,10.5 L9.66719477,17.3328052 L9.66719477,17.3328052 C9.55724743,17.4427526 9.42317605,17.5255464 9.27563076,17.5746098 L6.42375483,18.5229479 L6.42375483,18.5229479 C6.03070288,18.65365 5.60611633,18.4409735 5.47541424,18.0479215 C5.42427386,17.8941303 5.42432697,17.727911 5.47556562,17.5741526 L5.47556562,17.5741526 L6.42535348,14.7240015 L6.42535348,14.7240015 C6.47444294,14.5766924 6.55716155,14.4428385 6.66695621,14.3330438 L13.5,7.5 Z M14.5,6.5 L15.7928932,5.20710678 L15.7928932,5.20710678 C16.1834175,4.81658249 16.8165825,4.81658249 17.2071068,5.20710678 L18.7928932,6.79289322 L18.7928932,6.79289322 C19.1834175,7.18341751 19.1834175,7.81658249 18.7928932,8.20710678 L17.5,9.5 L14.5,6.5 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/EditIcon.js
Normal file
12
app/components/Icon/EditIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function EditIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M13.5,7.5 L16.5,10.5 L9.66719477,17.3328052 L9.66719477,17.3328052 C9.55724743,17.4427526 9.42317605,17.5255464 9.27563076,17.5746098 L6.42375483,18.5229479 L6.42375483,18.5229479 C6.03070288,18.65365 5.60611633,18.4409735 5.47541424,18.0479215 C5.42427386,17.8941303 5.42432697,17.727911 5.47556562,17.5741526 L5.47556562,17.5741526 L6.42535348,14.7240015 L6.42535348,14.7240015 C6.47444294,14.5766924 6.55716155,14.4428385 6.66695621,14.3330438 L13.5,7.5 Z M14.5,6.5 L15.7928932,5.20710678 L15.7928932,5.20710678 C16.1834175,4.81658249 16.8165825,4.81658249 17.2071068,5.20710678 L18.7928932,6.79289322 L18.7928932,6.79289322 C19.1834175,7.18341751 19.1834175,7.81658249 18.7928932,8.20710678 L17.5,9.5 L14.5,6.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/GoToIcon.js
Normal file
15
app/components/Icon/GoToIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function GoToIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M14.080855,4.6060807 L8.08085497,18.6060807 C7.86329935,19.1137105 8.09845092,19.7015894 8.6060807,19.919145 C9.11371048,20.1367007 9.70158941,19.9015491 9.91914503,19.3939193 L15.919145,5.3939193 C16.1367007,4.88628952 15.9015491,4.29841059 15.3939193,4.08085497 C14.8862895,3.86329935 14.2984106,4.09845092 14.080855,4.6060807 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/Heading1Icon.js
Normal file
12
app/components/Icon/Heading1Icon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function Heading1Icon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M7,4 L7,4 C7.55228475,4 8,4.44771525 8,5 L8,19 C8,19.5522847 7.55228475,20 7,20 C6.44771525,20 6,19.5522847 6,19 L6,5 L6,5 C6,4.44771525 6.44771525,4 7,4 L7,4 Z M8,11 L16,11 L16,13 L8,13 L8,11 Z M17,4 C17.5522847,4 18,4.44771525 18,5 L18,19 C18,19.5522847 17.5522847,20 17,20 C16.4477153,20 16,19.5522847 16,19 L16,5 L16,5 C16,4.44771525 16.4477153,4 17,4 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/Heading2Icon.js
Normal file
12
app/components/Icon/Heading2Icon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function Heading2Icon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M14,13 L10,13 L10,16 C10,16.5522847 9.55228475,17 9,17 C8.44771525,17 8,16.5522847 8,16 L8,8 L8,8 C8,7.44771525 8.44771525,7 9,7 L9,7 L9,7 C9.55228475,7 10,7.44771525 10,8 L10,11 L14,11 L14,8 L14,8 C14,7.44771525 14.4477153,7 15,7 C15.5522847,7 16,7.44771525 16,8 L16,16 C16,16.5522847 15.5522847,17 15,17 C14.4477153,17 14,16.5522847 14,16 L14,13 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/HomeIcon.js
Normal file
12
app/components/Icon/HomeIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function HomeIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M7,18 L9,18 L9,18 C9.55228475,18 10,17.5522847 10,17 L10,13.5 L10,13.5 C10,12.3954305 10.8954305,11.5 12,11.5 L12,11.5 L12,11.5 C13.1045695,11.5 14,12.3954305 14,13.5 L14,17 L14,17 C14,17.5522847 14.4477153,18 15,18 L17,18 L17,18 C17.5522847,18 18,17.5522847 18,17 L18,10.9367499 L18,10.9367499 C18,10.3431902 17.736354,9.78029498 17.2803688,9.40030733 L12.6401844,5.533487 L12.6401844,5.533487 C12.2693384,5.22444871 11.7306616,5.22444871 11.3598156,5.533487 L6.7196312,9.40030733 L6.7196312,9.40030733 C6.26364602,9.78029498 6,10.3431902 6,10.9367499 L6,17 L6,17 C6,17.5522847 6.44771525,18 7,18 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/HorizontalRuleIcon.js
Normal file
15
app/components/Icon/HorizontalRuleIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function HorizontalRuleIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M5,11 L19,11 C19.5522847,11 20,11.4477153 20,12 C20,12.5522847 19.5522847,13 19,13 L5,13 C4.44771525,13 4,12.5522847 4,12 C4,11.4477153 4.44771525,11 5,11 L5,11 Z M7,6 L17,6 C17.5522847,6 18,6.44771525 18,7 L18,8 C18,8.55228475 17.5522847,9 17,9 L7,9 C6.44771525,9 6,8.55228475 6,8 L6,7 L6,7 C6,6.44771525 6.44771525,6 7,6 Z M7,15 L17,15 C17.5522847,15 18,15.4477153 18,16 L18,17 C18,17.5522847 17.5522847,18 17,18 L7,18 C6.44771525,18 6,17.5522847 6,17 L6,16 C6,15.4477153 6.44771525,15 7,15 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
46
app/components/Icon/Icon.js
Normal file
46
app/components/Icon/Icon.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
export type Props = {
|
||||
className?: string,
|
||||
light?: boolean,
|
||||
black?: boolean,
|
||||
primary?: boolean,
|
||||
color?: string,
|
||||
size?: number,
|
||||
onClick?: Function,
|
||||
};
|
||||
|
||||
type BaseProps = {
|
||||
children?: React$Element<*>,
|
||||
};
|
||||
|
||||
export default function Icon({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
...rest
|
||||
}: Props & BaseProps) {
|
||||
const size = rest.size ? rest.size + 'px' : '24px';
|
||||
|
||||
let fill = color.slateDark;
|
||||
if (rest.color) fill = rest.color;
|
||||
if (rest.light) fill = color.white;
|
||||
if (rest.black) fill = color.black;
|
||||
if (rest.primary) fill = color.primary;
|
||||
|
||||
return (
|
||||
<svg
|
||||
fill={fill}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/ImageIcon.js
Normal file
12
app/components/Icon/ImageIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function ImageIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M19,14.5857864 L13.7071068,9.29289322 C13.3165825,8.90236893 12.6834175,8.90236893 12.2928932,9.29289322 L8,13.5857864 L5,10.5857864 L5,7 L5,7 C5,5.8954305 5.8954305,5 7,5 L7,5 L17,5 L17,5 C18.1045695,5 19,5.8954305 19,7 L19,14.5857864 Z M18.9642423,17.3784559 C18.7873485,18.3020643 17.9751801,19 17,19 L7,19 L7,19 C5.8954305,19 5,18.1045695 5,17 L5,13.4142136 L7.29289322,15.7071068 C7.68341751,16.0976311 8.31658249,16.0976311 8.70710678,15.7071068 L13,11.4142136 L18.9642423,17.3784559 Z M8.5,10 C9.32842712,10 10,9.32842712 10,8.5 C10,7.67157288 9.32842712,7 8.5,7 C7.67157288,7 7,7.67157288 7,8.5 C7,9.32842712 7.67157288,10 8.5,10 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/ItalicIcon.js
Normal file
12
app/components/Icon/ItalicIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function ItalicIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M12.5,6 C11.6715729,6 11,5.32842712 11,4.5 C11,3.67157288 11.6715729,3 12.5,3 C13.3284271,3 14,3.67157288 14,4.5 C14,5.32842712 13.3284271,6 12.5,6 Z M10.7801961,10 L10,10 C9.44771525,10 9,9.55228475 9,9 C9,8.44771525 9.44771525,8 10,8 L12,8 C12.6310464,8 13.1043391,8.57732421 12.9805807,9.19611614 L11.4590271,16.8038839 C11.3507152,17.3454436 11.7019319,17.8722687 12.2434917,17.9805807 C12.3080649,17.9934953 12.3737558,18 12.4396078,18 L13,18 C13.5522847,18 14,18.4477153 14,19 C14,19.5522847 13.5522847,20 13,20 L12.4396078,20 C12.2420518,20 12.044979,19.9804859 11.8512594,19.941742 C10.2265801,19.6168062 9.17292993,18.0363309 9.49786578,16.4116516 L10.7801961,10 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/LinkIcon.js
Normal file
12
app/components/Icon/LinkIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function LinkIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M11.0745387,14.3927712 C10.6944229,13.9921087 10.7110794,13.3591628 11.1117419,12.9790471 C11.5124045,12.5989313 12.1453503,12.6155878 12.5254661,13.0162503 C12.9108834,13.422501 13.5526571,13.4393898 13.9780169,13.0353595 L17.3813571,9.63201937 C18.2122318,8.80114469 18.2122318,7.45403068 17.3813571,6.62315601 C16.5504824,5.79228133 15.2033684,5.79228133 14.3724938,6.62315601 L13.5071091,7.48854062 C13.1165848,7.87906491 12.4834199,7.87906491 12.0928956,7.48854062 C11.7023713,7.09801633 11.7023713,6.46485135 12.0928956,6.07432706 L12.9582802,5.20894244 C14.5702035,3.59701919 17.1836474,3.59701919 18.7955707,5.20894244 C20.4074939,6.8208657 20.4074939,9.43430967 18.7955707,11.0462329 L15.3922305,14.4495731 C15.3640722,14.4774856 15.3640722,14.4774856 15.3354286,14.5048999 C14.1278529,15.6505487 12.2201874,15.6003469 11.0745387,14.3927712 Z M12.9299745,9.60955606 C13.3100902,10.0102186 13.2934337,10.6431644 12.8927712,11.0232802 C12.4921087,11.4033959 11.8591628,11.3867395 11.4790471,10.9860769 C11.0936298,10.5798262 10.4518561,10.5629375 10.0264962,10.9669677 L6.62315601,14.3703079 C5.79228133,15.2011826 5.79228133,16.5482966 6.62315601,17.3791712 C7.45403068,18.2100459 8.80114469,18.2100459 9.63201937,17.3791712 L10.497404,16.5137866 C10.8879283,16.1232623 11.5210933,16.1232623 11.9116175,16.5137866 C12.3021418,16.9043109 12.3021418,17.5374759 11.9116175,17.9280002 L11.0462329,18.7933848 C9.43430967,20.4053081 6.8208657,20.4053081 5.20894244,18.7933848 C3.59701919,17.1814616 3.59701919,14.5680176 5.20894244,12.9560943 L8.61228261,9.55275416 C8.64044095,9.52484169 8.64044095,9.52484169 8.66908451,9.49742738 C9.87666026,8.35177859 11.7843257,8.40198031 12.9299745,9.60955606 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/MoreIcon.js
Normal file
15
app/components/Icon/MoreIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function MoreIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M12,14 C10.8954305,14 10,13.1045695 10,12 C10,10.8954305 10.8954305,10 12,10 C13.1045695,10 14,10.8954305 14,12 C14,13.1045695 13.1045695,14 12,14 Z M18,14 C16.8954305,14 16,13.1045695 16,12 C16,10.8954305 16.8954305,10 18,10 C19.1045695,10 20,10.8954305 20,12 C20,13.1045695 19.1045695,14 18,14 Z M6,14 C4.8954305,14 4,13.1045695 4,12 C4,10.8954305 4.8954305,10 6,10 C7.1045695,10 8,10.8954305 8,12 C8,13.1045695 7.1045695,14 6,14 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/NewDocumentIcon.js
Normal file
15
app/components/Icon/NewDocumentIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function NewDocumentIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M19,18 L20,18 C20.5522847,18 21,18.4477153 21,19 C21,19.5522847 20.5522847,20 20,20 L19,20 L19,21 C19,21.5522847 18.5522847,22 18,22 C17.4477153,22 17,21.5522847 17,21 L17,20 L16,20 C15.4477153,20 15,19.5522847 15,19 C15,18.4477153 15.4477153,18 16,18 L17,18 L17,17 C17,16.4477153 17.4477153,16 18,16 C18.5522847,16 19,16.4477153 19,17 L19,18 Z M13.1000181,20 L7,20 C5.8954305,20 5,19.1045695 5,18 L5,6 L5,6 C5,4.8954305 5.8954305,4 7,4 L7,4 L14.5,4 L12,4 L12,9 C12,10.1045695 12.8954305,11 14,11 L19,11 L19,8.5 L19,14.1000181 C18.6768901,14.0344303 18.3424658,14 18,14 C15.2385763,14 13,16.2385763 13,19 C13,19.3424658 13.0344303,19.6768901 13.1000181,20 Z M14,4 L19,9 L14,9 L14,4 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/NextIcon.js
Normal file
15
app/components/Icon/NextIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function NextIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M9.29289322,16.2928932 C8.90236893,16.6834175 8.90236893,17.3165825 9.29289322,17.7071068 C9.68341751,18.0976311 10.3165825,18.0976311 10.7071068,17.7071068 L15.7071068,12.7071068 C16.0976311,12.3165825 16.0976311,11.6834175 15.7071068,11.2928932 L10.7071068,6.29289322 C10.3165825,5.90236893 9.68341751,5.90236893 9.29289322,6.29289322 C8.90236893,6.68341751 8.90236893,7.31658249 9.29289322,7.70710678 L13.5857864,12 L9.29289322,16.2928932 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/OpenIcon.js
Normal file
12
app/components/Icon/OpenIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function OpenIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M18,7.41421356 L11.7071068,13.7071068 C11.3165825,14.0976311 10.6834175,14.0976311 10.2928932,13.7071068 C9.90236893,13.3165825 9.90236893,12.6834175 10.2928932,12.2928932 L16.5857864,6 L14,6 C13.4477153,6 13,5.55228475 13,5 C13,4.44771525 13.4477153,4 14,4 L19,4 C19.5522847,4 20,4.44771525 20,5 L20,10 C20,10.5522847 19.5522847,11 19,11 C18.4477153,11 18,10.5522847 18,10 L18,7.41421356 Z M9,6 C9.55228475,6 10,6.44771525 10,7 C10,7.55228475 9.55228475,8 9,8 L6,8 L6,18 L16,18 L16,15 C16,14.4477153 16.4477153,14 17,14 C17.5522847,14 18,14.4477153 18,15 L18,18 C18,19.1045695 17.1045695,20 16,20 L6,20 C4.8954305,20 4,19.1045695 4,18 L4,8 C4,6.8954305 4.8954305,6 6,6 L9,6 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/OrderedListIcon.js
Normal file
12
app/components/Icon/OrderedListIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function OrderedListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M1,3.99978522 L1,2.70798687 L0.853553391,2.85442299 C0.658291245,3.04967116 0.341708755,3.04967116 0.146446609,2.85442299 C-0.0488155365,2.65917483 -0.0488155365,2.342615 0.146446609,2.14736684 L1.14644661,1.14743843 C1.46142904,0.83247855 2,1.05554597 2,1.50096651 L2,3.99978522 L2.5000358,3.99978522 L2.5000358,3.99978522 C2.7761584,3.99978522 3,4.22362682 3,4.49974942 C3,4.77587203 2.7761584,4.99971363 2.5000358,4.99971363 L1.53191883,4.99971363 C1.52136474,5.00037848 1.51072178,5.00071593 1.5,5.00071593 C1.48927822,5.00071593 1.47863526,5.00037848 1.46808117,4.99971363 L0.499964203,4.99971363 L0.499964203,4.99971363 C0.223841598,4.99971363 3.38152664e-17,4.77587203 0,4.49974942 C-3.38152664e-17,4.22362682 0.223841598,3.99978522 0.499964203,3.99978522 L0.499964203,3.99978522 L1,3.99978522 Z M5.99992841,1.99992841 L15.0000716,1.99992841 L15.0000716,1.99992841 C15.5523168,1.99992841 16,2.4476116 16,2.99985681 L16,2.99985681 L16,2.99985681 C16,3.55210202 15.5523168,3.99978522 15.0000716,3.99978522 L5.99992841,3.99978522 L5.99992841,3.99978522 C5.4476832,3.99978522 5,3.55210202 5,2.99985681 L5,2.99985681 L5,2.99985681 C5,2.4476116 5.4476832,1.99992841 5.99992841,1.99992841 Z M5.99992841,11.9992125 L15.0000716,11.9992125 L15.0000716,11.9992125 C15.5523168,11.9992125 16,12.4468957 16,12.9991409 L16,12.9991409 L16,12.9991409 C16,13.5513861 15.5523168,13.9990693 15.0000716,13.9990693 L5.99992841,13.9990693 C5.4476832,13.9990693 5,13.5513861 5,12.9991409 C5,12.4468957 5.4476832,11.9992125 5.99992841,11.9992125 Z M5.99992841,6.99957044 L15.0000716,6.99957044 L15.0000716,6.99957044 C15.5523168,6.99957044 16,7.44725364 16,7.99949885 L16,7.99949885 C16,8.55174406 15.5523168,8.99942725 15.0000716,8.99942725 L5.99992841,8.99942725 L5.99992841,8.99942725 C5.4476832,8.99942725 5,8.55174406 5,7.99949885 C5,7.44725364 5.4476832,6.99957044 5.99992841,6.99957044 Z M0.646446609,12.6466151 L1.29289322,12.0002148 L0.5,12.0002148 C0.223857625,12.0002148 0,11.7763732 0,11.5002506 C0,11.224128 0.223857625,11.0002864 0.5,11.0002864 L2.5,11.0002864 C2.94545243,11.0002864 3.16853582,11.5388188 2.85355339,11.8537787 L2.14380887,12.5634724 C2.64120863,12.728439 3,13.1973672 3,13.7500895 C3,14.440396 2.44035594,15 1.75,15 L0.5,15 C0.223857625,15 0,14.7761584 0,14.5000358 C0,14.2239132 0.223857625,14.0000716 0.5,14.0000716 L1.75,14.0000716 C1.88807119,14.0000716 2,13.8881508 2,13.7500895 C2,13.6120282 1.88807119,13.5001074 1.75,13.5001074 L1,13.5001074 C0.554547575,13.5001074 0.331464179,12.961575 0.646446609,12.6466151 Z M2.40096969,8.70045104 L2.00096969,9.00042956 L2.50096969,9.00042956 C2.77711207,9.00042956 3.00096969,9.22427116 3.00096969,9.50039376 C3.00096969,9.77651637 2.77711207,10.000358 2.50096969,10.000358 L0.500969693,10.000358 C0.0204635467,10.000358 -0.183435224,9.38870545 0.200969693,9.1004224 L1.80096969,7.90050831 C1.92687261,7.80608788 2.00096969,7.65790433 2.00096969,7.50053695 L2.00096969,7.25055485 C2.00096969,7.11249355 1.88904088,7.00057275 1.75096969,7.00057275 L1.50096969,7.00057275 C1.22482732,7.00057275 1.00096969,7.22441434 1.00096969,7.50053695 C1.00096969,7.77665955 0.777112068,8.00050115 0.500969693,8.00050115 C0.224827319,8.00050115 0.000969693445,7.77665955 0.000969693445,7.50053695 C0.000969693445,6.67216913 0.672542569,6.00064434 1.50096969,6.00064434 L1.75096969,6.00064434 C2.44132563,6.00064434 3.00096969,6.56024834 3.00096969,7.25055485 L3.00096969,7.50053695 C3.00096969,7.9726391 2.77867846,8.41718975 2.40096969,8.70045104 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/PlusIcon.js
Normal file
12
app/components/Icon/PlusIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function PlusIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M13,11 L13,6 C13,5.44771525 12.5522847,5 12,5 C11.4477153,5 11,5.44771525 11,6 L11,6 L11,11 L6,11 C5.44771525,11 5,11.4477153 5,12 C5,12.5522847 5.44771525,13 6,13 L11,13 L11,18 C11,18.5522847 11.4477153,19 12,19 C12.5522847,19 13,18.5522847 13,18 L13,13 L18,13 C18.5522847,13 19,12.5522847 19,12 C19,11.4477153 18.5522847,11 18,11 L13,11 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/SearchIcon.js
Normal file
12
app/components/Icon/SearchIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function SearchIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M16.1692714,14.047951 L19.0606602,16.9393398 C19.6464466,17.5251263 19.6464466,18.4748737 19.0606602,19.0606602 C18.4748737,19.6464466 17.5251263,19.6464466 16.9393398,19.0606602 L14.047951,16.1692714 C13.1546811,16.6971059 12.1127129,17 11,17 C7.6862915,17 5,14.3137085 5,11 C5,7.6862915 7.6862915,5 11,5 C14.3137085,5 17,7.6862915 17,11 C17,12.1127129 16.6971059,13.1546811 16.1692714,14.047951 Z M11,8 C9.34314575,8 8,9.34314575 8,11 C8,12.6568542 9.34314575,14 11,14 C12.6568542,14 14,12.6568542 14,11 C14,9.34314575 12.6568542,8 11,8 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/StarredIcon.js
Normal file
12
app/components/Icon/StarredIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function StarredIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M12,16.1500001 L8.79729751,17.8337604 L8.79729751,17.8337604 C8.30845292,18.0907612 7.70382577,17.9028147 7.44682496,17.4139701 C7.34448589,17.2193097 7.30917121,16.9963416 7.34634806,16.779584 L7.95800981,13.2133223 L5.36696906,10.6876818 L5.36696906,10.6876818 C4.97148548,10.3021806 4.96339318,9.66906733 5.34889439,9.27358375 C5.50240299,9.11610012 5.70354541,9.01361294 5.92118244,8.98198843 L9.50191268,8.46167787 L11.1032639,5.21698585 L11.1032639,5.21698585 C11.3476862,4.72173219 11.9473121,4.51839319 12.4425657,4.76281548 C12.6397783,4.86014572 12.7994058,5.01977324 12.8967361,5.21698585 L14.4980873,8.46167787 L18.0788176,8.98198843 L18.0788176,8.98198843 C18.6253624,9.06140605 19.0040439,9.5688489 18.9246263,10.1153938 C18.8930018,10.3330308 18.7905146,10.5341732 18.6330309,10.6876818 L16.0419902,13.2133223 L16.6536519,16.779584 L16.6536519,16.779584 C16.747013,17.3239204 16.3814251,17.8408763 15.8370887,17.9342373 C15.620331,17.9714142 15.397363,17.9360995 15.2027025,17.8337604 L12,16.1500001 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/StrikethroughIcon.js
Normal file
12
app/components/Icon/StrikethroughIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function StrikethroughIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M9.26756439,10 C9.09739429,9.70582663 9,9.36428714 9,9 C9,7.8954305 9.8954305,7 11,7 L16,7 C16.5522847,7 17,6.55228475 17,6 C17,5.44771525 16.5522847,5 16,5 L11,5 C8.790861,5 7,6.790861 7,9 C7,9.34529957 7.043753,9.68038008 7.12601749,10 L9.26756439,10 Z M16.8739825,14 C16.956247,14.3196199 17,14.6547004 17,15 C17,17.209139 15.209139,19 13,19 L8,19 C7.44771525,19 7,18.5522847 7,18 C7,17.4477153 7.44771525,17 8,17 L13,17 C14.1045695,17 15,16.1045695 15,15 C15,14.6357129 14.9026057,14.2941734 14.7324356,14 L16.8739825,14 Z M5.5,11.5 L18.5,11.5 C18.7761424,11.5 19,11.7238576 19,12 C19,12.2761424 18.7761424,12.5 18.5,12.5 L5.5,12.5 C5.22385763,12.5 5,12.2761424 5,12 C5,11.7238576 5.22385763,11.5 5.5,11.5 L5.5,11.5 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
15
app/components/Icon/TableIcon.js
Normal file
15
app/components/Icon/TableIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TableIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M6,5 L18,5 C19.1045695,5 20,5.8954305 20,7 L20,17 C20,18.1045695 19.1045695,19 18,19 L6,19 C4.8954305,19 4,18.1045695 4,17 L4,7 C4,5.8954305 4.8954305,5 6,5 Z M6,7 L6,9 L11,9 L11,7 L6,7 Z M13,7 L13,9 L18,9 L18,7 L13,7 Z M6,11 L6,13 L11,13 L11,11 L6,11 Z M13,11 L13,13 L18,13 L18,11 L13,11 Z M6,15 L6,17 L11,17 L11,15 L6,15 Z M13,15 L13,17 L18,17 L18,15 L13,15 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/TodoListIcon.js
Normal file
12
app/components/Icon/TodoListIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TodoListIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M9.99992841,5.99992841 L19.0000716,5.99992841 L19.0000716,5.99992841 C19.5523168,5.99992841 20,6.4476116 20,6.99985681 L20,6.99985681 C20,7.55210202 19.5523168,7.99978522 19.0000716,7.99978522 L9.99992841,7.99978522 L9.99992841,7.99978522 C9.4476832,7.99978522 9,7.55210202 9,6.99985681 C9,6.4476116 9.4476832,5.99992841 9.99992841,5.99992841 L9.99992841,5.99992841 Z M9.99992841,15.9992125 L19.0000716,15.9992125 L19.0000716,15.9992125 C19.5523168,15.9992125 20,16.4468957 20,16.9991409 L20,16.9991409 L20,16.9991409 C20,17.5513861 19.5523168,17.9990693 19.0000716,17.9990693 L9.99992841,17.9990693 C9.4476832,17.9990693 9,17.5513861 9,16.9991409 C9,16.4468957 9.4476832,15.9992125 9.99992841,15.9992125 Z M9.99992841,10.9995704 L19.0000716,10.9995704 L19.0000716,10.9995704 C19.5523168,10.9995704 20,11.4472536 20,11.9994988 L20,11.9994988 C20,12.5517441 19.5523168,12.9994273 19.0000716,12.9994273 L9.99992841,12.9994273 C9.4476832,12.9994273 9,12.5517441 9,11.9994988 C9,11.4472536 9.4476832,10.9995704 9.99992841,10.9995704 Z M5.22935099,7.69420576 L7.09998441,5.20002786 C7.26566855,4.97911569 7.57906677,4.93434451 7.79997895,5.10002864 C8.02089112,5.26571278 8.0656623,5.579111 7.89997817,5.80002318 L5.64999574,8.79999974 C5.45636149,9.05817875 5.07249394,9.06801504 4.86589123,8.82009178 L3.61590099,7.3201035 C3.43912033,7.10796671 3.46778214,6.79268682 3.67991893,6.61590616 C3.89205572,6.4391255 4.20733561,6.46778731 4.38411627,6.6799241 L5.22935099,7.69420576 Z M5.22935099,12.6942058 L7.09998441,10.2000279 C7.26566855,9.97911569 7.57906677,9.93434451 7.79997895,10.1000286 C8.02089112,10.2657128 8.0656623,10.579111 7.89997817,10.8000232 L5.64999574,13.7999997 C5.45636149,14.0581787 5.07249394,14.068015 4.86589123,13.8200918 L3.61590099,12.3201035 C3.43912033,12.1079667 3.46778214,11.7926868 3.67991893,11.6159062 C3.89205572,11.4391255 4.20733561,11.4677873 4.38411627,11.6799241 L5.22935099,12.6942058 Z M5.22935099,17.6942058 L7.09998441,15.2000279 C7.26566855,14.9791157 7.57906677,14.9343445 7.79997895,15.1000286 C8.02089112,15.2657128 8.0656623,15.579111 7.89997817,15.8000232 L5.64999574,18.7999997 C5.45636149,19.0581787 5.07249394,19.068015 4.86589123,18.8200918 L3.61590099,17.3201035 C3.43912033,17.1079667 3.46778214,16.7926868 3.67991893,16.6159062 C3.89205572,16.4391255 4.20733561,16.4677873 4.38411627,16.6799241 L5.22935099,17.6942058 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
12
app/components/Icon/TrashIcon.js
Normal file
12
app/components/Icon/TrashIcon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function TrashIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path d="M10,6 L10,5 L10,5 C10,4.44771525 10.4477153,4 11,4 L13,4 C13.5522847,4 14,4.44771525 14,5 L14,6 L18,6 C18.5522847,6 19,6.44771525 19,7 C19,7.55228475 18.5522847,8 18,8 L17.8571429,8 L17.132679,18.1424941 C17.0579211,19.1891049 16.1870389,20 15.1377616,20 L8.86223841,20 C7.81296107,20 6.94207892,19.1891049 6.86732101,18.1424941 L6.14285714,8 L6,8 C5.44771525,8 5,7.55228475 5,7 C5,6.44771525 5.44771525,6 6,6 L6,6 L10,6 Z M8.86223841,18 L15.1377616,18 L15.8520473,8 L8.14795269,8 L8.86223841,18 Z" />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
3
app/components/Icon/index.js
Normal file
3
app/components/Icon/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Icon from './Icon';
|
||||
export default Icon;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user