diff --git a/web/package.json b/web/package.json
index 548db58..ca93c15 100644
--- a/web/package.json
+++ b/web/package.json
@@ -58,7 +58,8 @@
"eject": "react-scripts eject",
"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public",
- "generate": "graphql-codegen"
+ "generate": "graphql-codegen",
+ "lint": "eslint --ext js,ts,tsx src"
},
"eslintConfig": {
"extends": "react-app"
diff --git a/web/src/citadel.d.ts b/web/src/citadel.d.ts
index b838401..e84a5da 100644
--- a/web/src/citadel.d.ts
+++ b/web/src/citadel.d.ts
@@ -11,6 +11,7 @@ type Task = {
name: string;
position: number;
labels: Label[];
+ description?: string;
};
type TaskGroup = {
diff --git a/web/src/shared/components/Modal/Modal.stories.tsx b/web/src/shared/components/Modal/Modal.stories.tsx
new file mode 100644
index 0000000..5a7eeb2
--- /dev/null
+++ b/web/src/shared/components/Modal/Modal.stories.tsx
@@ -0,0 +1,33 @@
+import React, { useState } from 'react';
+import { action } from '@storybook/addon-actions';
+import NormalizeStyles from 'App/NormalizeStyles';
+import BaseStyles from 'App/BaseStyles';
+import styled from 'styled-components';
+import Modal from './index';
+
+export default {
+ component: Modal,
+ title: 'Modal',
+ parameters: {
+ backgrounds: [
+ { name: 'white', value: '#ffffff' },
+ { name: 'gray', value: '#cdd3e1', default: true },
+ ],
+ },
+};
+
+export const Default = () => {
+ return (
+ <>
+
+
+ {
+ return Hello!
;
+ }}
+ />
+ >
+ );
+};
diff --git a/web/src/shared/components/Modal/Styles.ts b/web/src/shared/components/Modal/Styles.ts
new file mode 100644
index 0000000..b6ec642
--- /dev/null
+++ b/web/src/shared/components/Modal/Styles.ts
@@ -0,0 +1,33 @@
+import styled from 'styled-components';
+import { mixin } from 'shared/utils/styles';
+
+export const ScrollOverlay = styled.div`
+ z-index: 1000000;
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+`;
+
+export const ClickableOverlay = styled.div`
+ min-height: 100%;
+ background: rgba(9, 30, 66, 0.54);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 50px;
+`;
+
+export const StyledModal = styled.div<{ width: number }>`
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ background: #fff;
+ max-width: ${props => props.width}px;
+ vertical-align: middle;
+ border-radius: 3px;
+ ${mixin.boxShadowMedium}
+`;
diff --git a/web/src/shared/components/Modal/index.tsx b/web/src/shared/components/Modal/index.tsx
new file mode 100644
index 0000000..70b4e69
--- /dev/null
+++ b/web/src/shared/components/Modal/index.tsx
@@ -0,0 +1,36 @@
+import React, { useRef } from 'react';
+import ReactDOM from 'react-dom';
+
+import useOnOutsideClick from 'shared/hooks/onOutsideClick';
+import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
+
+import { ScrollOverlay, ClickableOverlay, StyledModal } from './Styles';
+
+const $root: HTMLElement = document.getElementById('root')!;
+
+type ModalProps = {
+ width: number;
+ onClose: () => void;
+ renderContent: () => JSX.Element;
+};
+
+const Modal: React.FC = ({ width, onClose: tellParentToClose, renderContent }) => {
+ const $modalRef = useRef(null);
+ const $clickableOverlayRef = useRef(null);
+
+ useOnOutsideClick($modalRef, true, tellParentToClose, $clickableOverlayRef);
+ useOnEscapeKeyDown(true, tellParentToClose);
+
+ return ReactDOM.createPortal(
+
+
+
+ {renderContent()}
+
+
+ ,
+ $root,
+ );
+};
+
+export default Modal;
diff --git a/web/src/shared/components/TaskDetails/Styles.ts b/web/src/shared/components/TaskDetails/Styles.ts
new file mode 100644
index 0000000..e5ae0c9
--- /dev/null
+++ b/web/src/shared/components/TaskDetails/Styles.ts
@@ -0,0 +1,181 @@
+import styled from 'styled-components';
+import TextareaAutosize from 'react-autosize-textarea/lib';
+
+export const TaskHeader = styled.div`
+ display: flex;
+ -webkit-box-pack: justify;
+ justify-content: space-between;
+ padding: 21px 18px 0px;
+`;
+
+export const TaskMeta = styled.div`
+ position: relative;
+ cursor: pointer;
+ font-size: 14px;
+ display: inline-block;
+ border-radius: 4px;
+`;
+
+export const TaskActions = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+export const TaskAction = styled.button`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 32px;
+ vertical-align: middle;
+ line-height: 1;
+ white-space: nowrap;
+ cursor: pointer;
+ user-select: none;
+ font-size: 14.5px;
+ color: rgb(66, 82, 110);
+ font-family: CircularStdBook;
+ font-weight: normal;
+ padding: 0px 9px;
+ border-radius: 3px;
+ transition: all 0.1s ease 0s;
+ background: rgb(255, 255, 255);
+`;
+
+export const TaskDetailsWrapper = styled.div`
+ display: flex;
+ padding: 0px 30px 60px;
+`;
+
+export const TaskDetailsContent = styled.div`
+ width: 65%;
+ padding-right: 50px;
+`;
+
+export const TaskDetailsSidebar = styled.div`
+ width: 35%;
+ padding-top: 5px;
+`;
+
+export const TaskDetailsTitleWrapper = styled.div`
+ height: 44px;
+ width: 100%;
+ margin: 18px 0px 0px -8px;
+ display: inline-block;
+`;
+
+export const TaskDetailsTitle = styled(TextareaAutosize)`
+ line-height: 1.28;
+ resize: none;
+ box-shadow: transparent 0px 0px 0px 1px;
+ font-size: 24px;
+ font-family: 'Droid Sans';
+ font-weight: 700;
+ padding: 7px 7px 8px;
+ background: rgb(255, 255, 255);
+ border-width: 1px;
+ border-style: solid;
+ border-color: transparent;
+ border-image: initial;
+ transition: background 0.1s ease 0s;
+ overflow-y: hidden;
+ width: 100%;
+ color: rgb(23, 43, 77);
+ &:hover {
+ background: rgb(235, 236, 240);
+ }
+ &:focus {
+ box-shadow: rgb(76, 154, 255) 0px 0px 0px 1px;
+ background: rgb(255, 255, 255);
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgb(76, 154, 255);
+ border-image: initial;
+ }
+`;
+
+export const TaskDetailsLabel = styled.div`
+ padding: 20px 0px 12px;
+ font-size: 15px;
+ font-weight: 600;
+`;
+
+export const TaskDetailsAddDetailsButton = styled.div`
+ background-color: rgba(9, 30, 66, 0.04);
+ box-shadow: none;
+ border: none;
+ border-radius: 3px;
+ display: block;
+ min-height: 56px;
+ padding: 8px 12px;
+ text-decoration: none;
+ font-size: 14px;
+ cursor: pointer;
+ &:hover {
+ background-color: rgba(9, 30, 66, 0.08);
+ box-shadow: none;
+ border: none;
+ }
+`;
+
+export const TaskDetailsEditorWrapper = styled.div`
+ display: block;
+ float: left;
+ padding-bottom: 9px;
+ z-index: 50;
+ width: 100%;
+`;
+
+export const TaskDetailsEditor = styled(TextareaAutosize)`
+ width: 100%;
+ min-height: 108px;
+ background: #fff;
+ box-shadow: none;
+ border-color: rgba(9, 30, 66, 0.13);
+ border-radius: 3px;
+ line-height: 20px;
+ padding: 8px 12px;
+ outline: none;
+ border: none;
+
+ &:focus {
+ background: #fff;
+ border: none;
+ box-shadow: inset 0 0 0 2px #0079bf;
+ }
+`;
+
+export const TaskDetailsMarkdown = styled.div`
+ width: 100%;
+ cursor: pointer;
+`;
+
+export const TaskDetailsControls = styled.div`
+ clear: both;
+ margin-top: 8px;
+`;
+
+export const ConfirmSave = styled.div`
+ background-color: #5aac44;
+ box-shadow: none;
+ border: none;
+ color: #fff;
+ float: left;
+ margin: 0 4px 0 0;
+ cursor: pointer;
+ display: inline-block;
+ font-weight: 400;
+ line-height: 20px;
+ padding: 6px 12px;
+ text-align: center;
+ border-radius: 3px;
+ font-size: 14px;
+`;
+
+export const CancelEdit = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 32px;
+ width: 32px;
+ cursor: pointer;
+`;
diff --git a/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx
new file mode 100644
index 0000000..101c835
--- /dev/null
+++ b/web/src/shared/components/TaskDetails/TaskDetails.stories.tsx
@@ -0,0 +1,47 @@
+import React, { useState } from 'react';
+import { action } from '@storybook/addon-actions';
+import NormalizeStyles from 'App/NormalizeStyles';
+import BaseStyles from 'App/BaseStyles';
+import styled from 'styled-components';
+import Modal from 'shared/components/Modal';
+import TaskDetails from './';
+
+export default {
+ component: TaskDetails,
+ title: 'TaskDetails',
+ parameters: {
+ backgrounds: [
+ { name: 'white', value: '#ffffff' },
+ { name: 'gray', value: '#cdd3e1', default: true },
+ ],
+ },
+};
+
+export const Default = () => {
+ const [description, setDescription] = useState('');
+ return (
+ <>
+
+
+ {
+ return (
+ setDescription(desc)}
+ />
+ );
+ }}
+ />
+ >
+ );
+};
diff --git a/web/src/shared/components/TaskDetails/index.tsx b/web/src/shared/components/TaskDetails/index.tsx
new file mode 100644
index 0000000..0e43368
--- /dev/null
+++ b/web/src/shared/components/TaskDetails/index.tsx
@@ -0,0 +1,130 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { Bin, Cross } from 'shared/icons';
+import useOnOutsideClick from 'shared/hooks/onOutsideClick';
+
+import {
+ TaskActions,
+ TaskAction,
+ TaskMeta,
+ TaskHeader,
+ TaskDetailsContent,
+ TaskDetailsWrapper,
+ TaskDetailsSidebar,
+ TaskDetailsTitleWrapper,
+ TaskDetailsTitle,
+ TaskDetailsLabel,
+ TaskDetailsAddDetailsButton,
+ TaskDetailsEditor,
+ TaskDetailsEditorWrapper,
+ TaskDetailsMarkdown,
+ TaskDetailsControls,
+ ConfirmSave,
+ CancelEdit,
+} from './Styles';
+
+type TaskContentProps = {
+ onEditContent: () => void;
+ description: string;
+};
+
+const TaskContent: React.FC = ({ description, onEditContent }) => {
+ return description === '' ? (
+ Add a more detailed description
+ ) : (
+ {description}
+ );
+};
+
+type DetailsEditorProps = {
+ description: string;
+ onTaskDescriptionChange: (newDescription: string) => void;
+ onCancel: () => void;
+};
+
+const DetailsEditor: React.FC = ({
+ description: initialDescription,
+ onTaskDescriptionChange,
+ onCancel,
+}) => {
+ const [description, setDescription] = useState(initialDescription);
+ const $editorWrapperRef = useRef(null);
+ const $editorRef = useRef(null);
+ const handleOutsideClick = () => {
+ onTaskDescriptionChange(description);
+ };
+ useEffect(() => {
+ if ($editorRef && $editorRef.current) {
+ $editorRef.current.focus();
+ $editorRef.current.select();
+ }
+ }, []);
+
+ useOnOutsideClick($editorWrapperRef, true, handleOutsideClick, null);
+ return (
+
+ ) => setDescription(e.currentTarget.value)}
+ />
+
+ Save
+
+
+
+
+
+ );
+};
+
+type TaskDetailsProps = {
+ task: Task;
+ onTaskDescriptionChange: (task: Task, newDescription: string) => void;
+};
+
+const TaskDetails: React.FC = ({ task, onTaskDescriptionChange }) => {
+ const [editorOpen, setEditorOpen] = useState(false);
+ const handleClick = () => {
+ setEditorOpen(!editorOpen);
+ };
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Description
+ {editorOpen ? (
+ {
+ setEditorOpen(false);
+ onTaskDescriptionChange(task, newDescription);
+ }}
+ onCancel={() => {
+ setEditorOpen(false);
+ }}
+ />
+ ) : (
+
+ )}
+
+
+
+ >
+ );
+};
+
+export default TaskDetails;
diff --git a/web/src/shared/icons/Bin.tsx b/web/src/shared/icons/Bin.tsx
new file mode 100644
index 0000000..31b3637
--- /dev/null
+++ b/web/src/shared/icons/Bin.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+type Props = {
+ size: number | string;
+ color: string;
+};
+
+const Bin = ({ size, color }: Props) => {
+ return (
+
+ );
+};
+
+Bin.defaultProps = {
+ size: 16,
+ color: '#000',
+};
+
+export default Bin;
diff --git a/web/src/shared/icons/index.ts b/web/src/shared/icons/index.ts
index 66349e4..b0c006c 100644
--- a/web/src/shared/icons/index.ts
+++ b/web/src/shared/icons/index.ts
@@ -1,5 +1,6 @@
import Cross from './Cross';
import Bell from './Bell';
+import Bin from './Bin';
import Pencil from './Pencil';
import Checkmark from './Checkmark';
import User from './User';
@@ -11,4 +12,4 @@ import Stack from './Stack';
import Question from './Question';
import Exit from './Exit';
-export { Cross, Bell, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };
+export { Cross, Bell, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };