diff --git a/app/components/ColorPicker/ColorPicker.js b/app/components/ColorPicker/ColorPicker.js
new file mode 100644
index 000000000..f95d857ec
--- /dev/null
+++ b/app/components/ColorPicker/ColorPicker.js
@@ -0,0 +1,184 @@
+// @flow
+import React from 'react';
+import { observable, computed, action } from 'mobx';
+import { observer } from 'mobx-react';
+import styled from 'styled-components';
+import Flex from 'shared/components/Flex';
+import { LabelText, Outline } from 'components/Input';
+import { color, fonts, fontWeight } from 'shared/styles/constants';
+import { validateColorHex } from 'shared/utils/color';
+
+const colors = [
+ '#4E5C6E',
+ '#19B7FF',
+ '#7F6BFF',
+ '#FC7419',
+ '#FC2D2D',
+ '#FFE100',
+ '#14CF9F',
+ '#EE84F0',
+ '#2F362F',
+];
+
+type Props = {
+ onSelect: (color: string) => void,
+ value?: string,
+};
+
+@observer class ColorPicker extends React.Component {
+ props: Props;
+
+ @observable selectedColor: string = colors[0];
+ @observable customColorValue: string = '';
+ @observable customColorSelected: boolean;
+
+ componentWillMount() {
+ const { value } = this.props;
+ if (value && colors.includes(value)) {
+ this.selectedColor = value;
+ } else if (value) {
+ this.customColorSelected = true;
+ this.customColorValue = value.replace('#', '');
+ }
+ }
+
+ componentDidMount() {
+ this.fireCallback();
+ }
+
+ fireCallback = () => {
+ this.props.onSelect(
+ this.customColorSelected ? this.customColor : this.selectedColor
+ );
+ };
+
+ @computed get customColor(): string {
+ return this.customColorValue &&
+ validateColorHex(`#${this.customColorValue}`)
+ ? `#${this.customColorValue}`
+ : colors[0];
+ }
+
+ @action setColor = (color: string) => {
+ this.selectedColor = color;
+ this.customColorSelected = false;
+ this.fireCallback();
+ };
+
+ @action focusOnCustomColor = (event: SyntheticEvent) => {
+ this.selectedColor = '';
+ this.customColorSelected = true;
+ this.fireCallback();
+ };
+
+ @action setCustomColor = (event: SyntheticEvent) => {
+ let target = event.target;
+ if (target instanceof HTMLInputElement) {
+ const color = target.value;
+ this.customColorValue = color.replace('#', '');
+ this.fireCallback();
+ }
+ };
+
+ render() {
+ return (
+
+ Color
+
+
+ {colors.map(color => (
+ this.setColor(color)}
+ />
+ ))}
+
+
+ Custom color:
+ #
+
+
+
+
+
+ );
+ }
+}
+
+type SwatchProps = {
+ onClick?: () => void,
+ color?: string,
+ active?: boolean,
+};
+
+const Swatch = ({ onClick, ...props }: SwatchProps) => (
+
+
+
+);
+
+const SwatchOutset = styled(Flex)`
+ width: 24px;
+ height: 24px;
+ margin-right: 5px;
+ border: 2px solid ${({ active, color }) => (active ? color : 'transparent')};
+ border-radius: 2px;
+ background: ${({ color }) => color};
+ ${({ onClick }) => onClick && `cursor: pointer;`}
+
+ &:last-child {
+ margin-right: 0;
+ }
+`;
+
+const SwatchInset = styled(Flex)`
+ width: 20px;
+ height: 20px;
+ border: 1px solid ${({ active, color }) => (active ? 'white' : 'transparent')};
+ border-radius: 2px;
+ background: ${({ color }) => color};
+`;
+
+const StyledOutline = styled(Outline)`
+ padding: 5px;
+`;
+
+const HexHash = styled.div`
+ margin-left: 12px;
+ padding-bottom: 0;
+ font-weight: ${fontWeight.medium};
+ user-select: none;
+`;
+
+const CustomColorInput = styled.input`
+ border: 0;
+ flex: 1;
+ width: 65px;
+ margin-right: 12px;
+ padding-bottom: 0;
+ outline: none;
+ background: none;
+ font-family: ${fonts.monospace};
+ font-weight: ${fontWeight.medium};
+
+ &::placeholder {
+ color: ${color.slate};
+ font-family: ${fonts.monospace};
+ font-weight: ${fontWeight.medium};
+ }
+`;
+
+export default ColorPicker;
diff --git a/app/components/ColorPicker/index.js b/app/components/ColorPicker/index.js
new file mode 100644
index 000000000..84f7ebb8d
--- /dev/null
+++ b/app/components/ColorPicker/index.js
@@ -0,0 +1,3 @@
+// @flow
+import ColorPicker from './ColorPicker';
+export default ColorPicker;
diff --git a/app/components/Input/Input.js b/app/components/Input/Input.js
index 5fbe34106..7897b3b3e 100644
--- a/app/components/Input/Input.js
+++ b/app/components/Input/Input.js
@@ -32,7 +32,7 @@ const Wrapper = styled.div`
`;
-const Outline = styled(Flex)`
+export const Outline = styled(Flex)`
display: flex;
flex: 1;
margin: 0 0 ${size.large};
@@ -48,7 +48,7 @@ const Outline = styled(Flex)`
}
`;
-const LabelText = styled.div`
+export const LabelText = styled.div`
font-weight: 500;
padding-bottom: 4px;
`;
diff --git a/app/components/Input/index.js b/app/components/Input/index.js
index e005a8af8..e9b21f08b 100644
--- a/app/components/Input/index.js
+++ b/app/components/Input/index.js
@@ -1,3 +1,4 @@
// @flow
-import Input from './Input';
+import Input, { LabelText, Outline } from './Input';
export default Input;
+export { LabelText, Outline };
diff --git a/app/components/Layout/components/SidebarCollections.js b/app/components/Layout/components/SidebarCollections.js
index 37f064db2..38507c3aa 100644
--- a/app/components/Layout/components/SidebarCollections.js
+++ b/app/components/Layout/components/SidebarCollections.js
@@ -102,7 +102,7 @@ type Props = {
}
+ icon={}
>
{collection.name}
diff --git a/app/models/Collection.js b/app/models/Collection.js
index 93923e9c0..77158d866 100644
--- a/app/models/Collection.js
+++ b/app/models/Collection.js
@@ -19,6 +19,7 @@ class Collection extends BaseModel {
description: ?string;
id: string;
name: string;
+ color: string;
type: 'atlas' | 'journal';
documents: Array;
updatedAt: string;
@@ -57,19 +58,21 @@ class Collection extends BaseModel {
if (this.isSaving) return this;
this.isSaving = true;
+ const params = {
+ name: this.name,
+ color: this.color,
+ description: this.description,
+ };
+
try {
let res;
if (this.id) {
res = await client.post('/collections.update', {
id: this.id,
- name: this.name,
- description: this.description,
+ ...params,
});
} else {
- res = await client.post('/collections.create', {
- name: this.name,
- description: this.description,
- });
+ res = await client.post('/collections.create', params);
}
runInAction('Collection#save', () => {
invariant(res && res.data, 'Data should be available');
diff --git a/app/models/Document.js b/app/models/Document.js
index 4f4c760ef..5df71a8f8 100644
--- a/app/models/Document.js
+++ b/app/models/Document.js
@@ -5,7 +5,7 @@ import invariant from 'invariant';
import { client } from 'utils/ApiClient';
import stores from 'stores';
import ErrorsStore from 'stores/ErrorsStore';
-import parseTitle from '../../shared/parseTitle';
+import parseTitle from '../../shared/utils/parseTitle';
import type { User } from 'types';
import BaseModel from './BaseModel';
diff --git a/app/scenes/CollectionEdit/CollectionEdit.js b/app/scenes/CollectionEdit/CollectionEdit.js
index 17fc570b8..db5b07117 100644
--- a/app/scenes/CollectionEdit/CollectionEdit.js
+++ b/app/scenes/CollectionEdit/CollectionEdit.js
@@ -7,6 +7,7 @@ import Button from 'components/Button';
import Input from 'components/Input';
import Flex from 'shared/components/Flex';
import HelpText from 'components/HelpText';
+import ColorPicker from 'components/ColorPicker';
import Collection from 'models/Collection';
type Props = {
@@ -18,6 +19,7 @@ type Props = {
@observer class CollectionEdit extends Component {
props: Props;
@observable name: string;
+ @observable color: string = '';
@observable isSaving: boolean;
componentWillMount() {
@@ -28,7 +30,7 @@ type Props = {
ev.preventDefault();
this.isSaving = true;
- this.props.collection.updateData({ name: this.name });
+ this.props.collection.updateData({ name: this.name, color: this.color });
const success = await this.props.collection.save();
if (success) {
@@ -42,6 +44,10 @@ type Props = {
this.name = ev.target.value;
};
+ handleColor = (color: string) => {
+ this.color = color;
+ };
+
render() {
return (
@@ -58,6 +64,10 @@ type Props = {
required
autoFocus
/>
+