feature: add more to project pane

This commit is contained in:
Jordan Knott
2020-05-26 19:53:31 -05:00
parent 7e78ee36b4
commit fba4de631f
64 changed files with 1845 additions and 582 deletions

View File

@ -16,7 +16,7 @@ export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
`;
export const ListCard = styled.div`
background-color: #fff;
background-color: ${props => mixin.lighten('#262c49', 0.05)};
border-radius: 3px;
${mixin.boxShadowCard}
cursor: pointer;
@ -55,9 +55,10 @@ export const ListCardEditor = styled(TextareaAutosize)`
padding: 0;
font-size: 14px;
line-height: 20px;
&:focus {
border: none;
outline: none;
color: #c2c6dc;
l &:focus {
background-color: ${props => mixin.lighten('#262c49', 0.05)};
}
`;

View File

@ -4,8 +4,8 @@ export const Container = styled.div<{ left: number; top: number }>`
position: absolute;
left: ${props => props.left}px;
top: ${props => props.top}px;
padding-top: 10px;
position: absolute;
padding-top: 10px;
height: auto;
width: auto;
transform: translate(-100%);
@ -18,12 +18,12 @@ export const Wrapper = styled.div`
padding-top: 8px;
border-radius: 5px;
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;

View File

@ -21,7 +21,7 @@ export const AddCardContainer = styled.div`
export const AddCardButton = styled.a`
border-radius: 3px;
color: #5e6c84;
color: #c2c6dc;
display: flex;
align-items: center;
cursor: pointer;
@ -32,9 +32,9 @@ export const AddCardButton = styled.a`
text-decoration: none;
user-select: none;
&:hover {
background-color: rgba(9, 30, 66, 0.08);
color: #172b4d;
color: #c2c6dc;
text-decoration: none;
background: rgb(115, 103, 240);
}
`;
export const Wrapper = styled.div`
@ -125,4 +125,5 @@ export const ListExtraMenuButtonWrapper = styled.div`
top: 4px;
z-index: 1;
padding: 6px;
padding-bottom: 0;
`;

View File

@ -26,7 +26,7 @@ type Props = {
wrapperProps?: any;
headerProps?: any;
index?: number;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
};
const List = React.forwardRef(
@ -78,20 +78,7 @@ const List = React.forwardRef(
const handleExtraMenuOpen = () => {
if ($extraActionsRef && $extraActionsRef.current) {
const pos = $extraActionsRef.current.getBoundingClientRect();
onExtraMenuOpen(
id,
{
top: pos.top,
left: pos.left,
right: pos.right,
bottom: pos.bottom,
},
{
width: pos.width,
height: pos.height,
},
);
onExtraMenuOpen(id, $extraActionsRef);
}
};
useOnEscapeKeyDown(isEditingTitle, onEscape);
@ -116,7 +103,7 @@ const List = React.forwardRef(
{children && children}
<AddCardContainer hidden={isComposerOpen}>
<AddCardButton onClick={() => onOpenComposer(id)}>
<Plus size={12} color="#42526e" />
<Plus size={12} color="#c2c6dc" />
<AddCardButtonText>Add another card</AddCardButtonText>
</AddCardButton>
</AddCardContainer>

View File

@ -14,19 +14,19 @@ export const ListActionItem = styled.span`
cursor: pointer;
display: block;
font-size: 14px;
color: #172b4d;
color: #c2c6dc;
font-weight: 400;
padding: 6px 12px;
position: relative;
margin: 0 -12px;
text-decoration: none;
&:hover {
background-color: rgba(9, 30, 66, 0.04);
background: rgb(115, 103, 240);
}
`;
export const ListSeparator = styled.hr`
background-color: rgba(9, 30, 66, 0.13);
background-color: #414561;
border: 0;
height: 1px;
margin: 8px 0;

View File

@ -29,7 +29,7 @@ type Props = {
onCardCreate: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateList: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, pos: ElementPosition, size: ElementSize) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
};
const Lists: React.FC<Props> = ({

View File

@ -1,5 +1,6 @@
import styled from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea/lib';
import { mixin } from 'shared/utils/styles';
export const MemberManagerWrapper = styled.div``;
@ -11,17 +12,27 @@ export const MemberManagerSearchWrapper = styled.div`
export const MemberManagerSearch = styled(TextareaAutosize)`
margin: 4px 0 12px;
width: 100%;
background-color: #ebecf0;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
color: #172b4d;
font-family: 'Droid Sans';
font-weight: 400;
background: #262c49;
outline: none;
color: #c2c6dc;
border-color: #414561;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const BoardMembersLabel = styled.h4`
color: #5e6c84;
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -52,7 +63,7 @@ export const BoardMemberListItemContent = styled.div`
white-space: nowrap;
padding: 4px;
margin-bottom: 2px;
color: #172b4d;
color: #c2c6dc;
`;
export const ProfileIcon = styled.div`
@ -62,7 +73,7 @@ export const ProfileIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
color: #fff;
color: #c2c6dc;
font-weight: 700;
background: rgb(115, 103, 240);
cursor: pointer;

View File

@ -43,7 +43,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
)
.map(member => {
return (
<BoardMembersListItem>
<BoardMembersListItem key={member.userID}>
<BoardMemberListItemContent
onClick={() => {
const isActive = activeMembers.findIndex(m => m.userID === member.userID) !== -1;

View File

@ -11,7 +11,11 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
margin: 2px;
background-color: ${props => props.bgColor};
border-radius: 25em;
display: block;
font-size: 16px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 50px;
overflow: hidden;
position: relative;
@ -20,6 +24,7 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
`;
export const ProfileInfo = styled.div`
color: #c2c6dc;
margin: 0 0 0 64px;
word-wrap: break-word;
`;
@ -29,11 +34,11 @@ export const InfoTitle = styled.h3`
font-size: 16px;
font-weight: 600;
line-height: 20px;
color: #172b4d;
color: #c2c6dc;
`;
export const InfoUsername = styled.p`
color: #5e6c84;
color: #c2c6dc;
font-size: 14px;
line-height: 20px;
`;
@ -41,10 +46,29 @@ export const InfoUsername = styled.p`
export const InfoBio = styled.p`
font-size: 14px;
line-height: 20px;
color: #5e6c84;
color: #c2c6dc;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
padding: 0;
`;
export const MiniProfileActions = styled.ul`
list-style-type: none;
`;
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span`
color: #c2c6dc;
cursor: pointer;
display: block;
font-weight: 400;
padding: 6px 12px;
position: relative;
text-decoration: none;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -1,14 +1,25 @@
import React from 'react';
import { Profile, ProfileIcon, ProfileInfo, InfoTitle, InfoUsername, InfoBio } from './Styles';
import {
Profile,
ProfileIcon,
ProfileInfo,
InfoTitle,
InfoUsername,
InfoBio,
MiniProfileActions,
MiniProfileActionWrapper,
MiniProfileActionItem,
} from './Styles';
type MiniProfileProps = {
displayName: string;
username: string;
bio: string;
profileIcon: ProfileIcon;
onRemoveFromTask: () => void;
};
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon }) => {
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon, onRemoveFromTask }) => {
return (
<>
<Profile>
@ -19,6 +30,17 @@ const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, p
<InfoBio>{bio}</InfoBio>
</ProfileInfo>
</Profile>
<MiniProfileActions>
<MiniProfileActionWrapper>
<MiniProfileActionItem
onClick={() => {
onRemoveFromTask();
}}
>
Remove from card
</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
</>
);
};

View File

@ -2,7 +2,7 @@ import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const ScrollOverlay = styled.div`
z-index: 1000000;
z-index: 3000;
position: fixed;
top: 0;
left: 0;

View File

@ -4,25 +4,51 @@ import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
type Props = {
label: Label;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
label: Label | null;
onLabelEdit: (labelId: string | null, labelName: string, color: string) => void;
};
const LabelManager = ({ label, onLabelEdit }: Props) => {
const [currentLabel, setCurrentLabel] = useState('');
console.log(label);
const [currentLabel, setCurrentLabel] = useState(label ? label.name : '');
const [currentColor, setCurrentColor] = useState<string | null>(label ? label.color : null);
return (
<EditLabelForm>
<FieldLabel>Name</FieldLabel>
<FieldName id="labelName" type="text" name="name" value={currentLabel} />
<FieldName
id="labelName"
type="text"
name="name"
onChange={e => {
setCurrentLabel(e.currentTarget.value);
}}
value={currentLabel}
/>
<FieldLabel>Select a color</FieldLabel>
<div>
{Object.values(LabelColors).map(labelColor => (
<LabelBox color={labelColor}>
<Checkmark color="#fff" size={12} />
<LabelBox
color={labelColor}
onClick={() => {
setCurrentColor(labelColor);
}}
>
{labelColor === currentColor && <Checkmark color="#fff" size={12} />}
</LabelBox>
))}
</div>
<div>
<SaveButton type="submit" value="Save" />
<SaveButton
onClick={e => {
e.preventDefault();
console.log(currentColor);
if (currentColor) {
onLabelEdit(label ? label.labelId : null, currentLabel, currentColor);
}
}}
type="submit"
value="Save"
/>
<DeleteButton type="submit" value="Delete" />
</div>
</EditLabelForm>

View File

@ -1,46 +1,78 @@
import React, { useState } from 'react';
import { Pencil, Checkmark } from 'shared/icons';
import { LabelSearch, ActiveIcon, Labels, Label, CardLabel, Section, SectionTitle, LabelIcon } from './Styles';
import {
LabelSearch,
ActiveIcon,
Labels,
Label,
CardLabel,
Section,
SectionTitle,
LabelIcon,
CreateLabelButton,
} from './Styles';
type Props = {
labels?: Label[];
onLabelToggle: (labelId: string) => void;
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
onLabelEdit: (labelId: string) => void;
onLabelCreate: () => void;
};
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit }) => {
const LabelManager: React.FC<Props> = ({ labels, onLabelToggle, onLabelEdit, onLabelCreate }) => {
const [currentLabel, setCurrentLabel] = useState('');
const [currentSearch, setCurrentSearch] = useState('');
return (
<>
<LabelSearch type="text" />
<LabelSearch
type="text"
placeholder="search labels..."
onChange={e => {
setCurrentSearch(e.currentTarget.value);
}}
value={currentSearch}
/>
<Section>
<SectionTitle>Labels</SectionTitle>
<Labels>
{labels &&
labels.map(label => (
<Label>
<LabelIcon>
<Pencil />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
labels
.filter(label => currentSearch === '' || label.name.toLowerCase().startsWith(currentSearch.toLowerCase()))
.map(label => (
<Label key={label.labelId}>
<LabelIcon
onClick={() => {
onLabelEdit(label.labelId);
}}
>
<Pencil color="#c2c6dc" />
</LabelIcon>
<CardLabel
key={label.labelId}
color={label.color}
active={currentLabel === label.labelId}
onMouseEnter={() => {
setCurrentLabel(label.labelId);
}}
onClick={() => onLabelToggle(label.labelId)}
>
{label.name}
{label.active && (
<ActiveIcon>
<Checkmark color="#fff" />
</ActiveIcon>
)}
</CardLabel>
</Label>
))}
</Labels>
<CreateLabelButton
onClick={() => {
onLabelCreate();
}}
>
Create a new label
</CreateLabelButton>
</Section>
</>
);

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, createRef } from 'react';
import { action } from '@storybook/addon-actions';
import LabelColors from 'shared/constants/labelColors';
import LabelManager from 'shared/components/PopupMenu/LabelManager';
@ -7,10 +7,12 @@ import ListActions from 'shared/components/ListActions';
import MemberManager from 'shared/components/MemberManager';
import DueDateManager from 'shared/components/DueDateManager';
import MiniProfile from 'shared/components/MiniProfile';
import styled from 'styled-components';
import PopupMenu from '.';
import PopupMenu, { PopupProvider, usePopup, Popup } from '.';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import produce from 'immer';
export default {
component: PopupMenu,
@ -37,19 +39,93 @@ const labelData = [
},
];
const OpenLabelBtn = styled.span``;
type TabProps = {
tab: number;
};
const LabelManagerEditor = () => {
const [labels, setLabels] = useState(labelData);
const [currentLabel, setCurrentLabel] = useState('');
const { setTab } = usePopup();
return (
<>
<Popup title="Labels" tab={0} onClose={action('on close')}>
<LabelManager
labels={labels}
onLabelCreate={() => {
setTab(2);
}}
onLabelEdit={labelId => {
setCurrentLabel(labelId);
setTab(1);
}}
onLabelToggle={labelId => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === labelId);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], active: !labels[idx].active };
}
}),
);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Edit label" tab={1}>
<LabelEditor
label={labels.find(label => label.labelId === currentLabel) ?? null}
onLabelEdit={(_labelId, name, color) => {
setLabels(
produce(labels, draftState => {
const idx = labels.findIndex(label => label.labelId === currentLabel);
if (idx !== -1) {
draftState[idx] = { ...draftState[idx], name, color };
}
}),
);
setTab(0);
}}
/>
</Popup>
<Popup onClose={action('on close')} title="Create new label" tab={2}>
<LabelEditor
label={null}
onLabelEdit={(_labelId, name, color) => {
setLabels([...labels, { labelId: name, name, color, active: false }]);
setTab(0);
}}
/>
</Popup>
</>
);
};
const OpenLabelsButton = () => {
const $buttonRef = createRef<HTMLButtonElement>();
const [currentLabel, setCurrentLabel] = useState('');
const [labels, setLabels] = useState(labelData);
const { showPopup, setTab } = usePopup();
console.log(labels);
return (
<OpenLabelBtn
ref={$buttonRef}
onClick={() => {
showPopup($buttonRef, <LabelManagerEditor />);
}}
>
Open
</OpenLabelBtn>
);
};
export const LabelsPopup = () => {
const [isPopupOpen, setPopupOpen] = useState(false);
return (
<>
{isPopupOpen && (
<PopupMenu title="Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<LabelManager labels={labelData} onLabelToggle={action('label toggle')} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
<button type="submit" onClick={() => setPopupOpen(true)}>
Open
</button>
</>
<PopupProvider>
<OpenLabelsButton />
</PopupProvider>
);
};
@ -58,7 +134,13 @@ export const LabelsLabelEditor = () => {
return (
<>
{isPopupOpen && (
<PopupMenu title="Change Label" top={10} onClose={() => setPopupOpen(false)} left={10}>
<PopupMenu
onPrevious={action('on previous')}
title="Change Label"
top={10}
onClose={() => setPopupOpen(false)}
left={10}
>
<LabelEditor label={labelData[0]} onLabelEdit={action('label edit')} />
</PopupMenu>
)}
@ -201,12 +283,19 @@ export const MiniProfilePopup = () => {
<NormalizeStyles />
<BaseStyles />
{popupData.isOpen && (
<PopupMenu title="Due Date" top={popupData.top} onClose={() => setPopupData(initalState)} left={popupData.left}>
<PopupMenu
noHeader
title="Due Date"
top={popupData.top}
onClose={() => setPopupData(initalState)}
left={popupData.left}
>
<MiniProfile
displayName="Jordan Knott"
profileIcon={{ url: null, bgColor: '#000', initials: 'JK' }}
username="@jordanthedev"
bio="Stuff and things"
onRemoveFromTask={action('mini profile')}
/>
</PopupMenu>
)}
@ -236,3 +325,4 @@ export const MiniProfilePopup = () => {
</>
);
};

View File

@ -1,20 +1,34 @@
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Container = styled.div<{ top: number; left: number; ref: any }>`
export const Container = styled.div<{ invert: boolean; top: number; left: number; ref: any }>`
left: ${props => props.left}px;
top: ${props => props.top}px;
background: #fff;
border-radius: 3px;
box-shadow: 0 8px 16px -4px rgba(9, 30, 66, 0.25), 0 0 0 1px rgba(9, 30, 66, 0.08);
display: block;
position: absolute;
width: 304px;
z-index: 100000000000;
&:focus {
outline: none;
border: none;
}
width: 316px;
padding-top: 10px;
height: auto;
z-index: 40000;
${props =>
props.invert &&
css`
transform: translate(-100%);
`}
`;
export const Wrapper = styled.div`
padding: 5px;
padding-top: 8px;
border-radius: 5px;
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
position: relative;
margin: 0;
color: #c2c6dc;
background: #262c49;
border: 1px solid rgba(0, 0, 0, 0.1);
border-color: #414561;
`;
export const Header = styled.div`
@ -26,10 +40,10 @@ export const Header = styled.div`
export const HeaderTitle = styled.span`
box-sizing: border-box;
color: #5e6c84;
color: #c2c6dc;
display: block;
line-height: 40px;
border-bottom: 1px solid rgba(9, 30, 66, 0.13);
border-bottom: 1px solid #414561;
margin: 0 12px;
overflow: hidden;
padding: 0 32px;
@ -46,23 +60,30 @@ export const Content = styled.div`
padding: 0 12px 12px;
`;
export const LabelSearch = styled.input`
box-sizing: border-box;
display: block;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
display: block;
line-height: 20px;
padding: 8px 12px;
font-size: 14px;
font-family: 'Droid Sans';
font-weight: 400;
transition-property: background-color, border-color, box-shadow;
transition-duration: 85ms;
transition-timing-function: ease;
background: #262c49;
outline: none;
color: #c2c6dc;
border-color: #414561;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const Section = styled.div`
@ -70,7 +91,7 @@ export const Section = styled.div`
`;
export const SectionTitle = styled.h4`
color: #5e6c84;
color: #c2c6dc;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
@ -95,7 +116,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
props.active &&
css`
margin-left: 4px;
box-shadow: -8px 0 ${mixin.darken(props.color, 0.15)};
box-shadow: -8px 0 ${mixin.darken(props.color, 0.12)};
border-radius: 3px;
`}
@ -113,6 +134,7 @@ export const CardLabel = styled.span<{ active: boolean; color: string }>`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 31px;
`;
export const CloseButton = styled.div`
@ -126,8 +148,6 @@ export const CloseButton = styled.div`
align-items: center;
justify-content: center;
z-index: 40;
height: 20px;
width: 20px;
cursor: pointer;
`;
@ -142,14 +162,14 @@ export const LabelIcon = styled.div`
align-items: center;
justify-content: center;
height: 20px;
height: 100%;
font-size: 16px;
line-height: 20px;
width: 20px;
width: auto;
cursor: pointer;
&:hover {
background: rgba(9, 30, 66, 0.08);
background: rgb(115, 103, 240);
}
`;
@ -186,19 +206,27 @@ export const FieldLabel = styled.label`
export const FieldName = styled.input`
margin: 4px 0 12px;
width: 100%;
background-color: #fafbfc;
border: none;
box-shadow: inset 0 0 0 2px #dfe1e6;
color: #172b4d;
box-sizing: border-box;
border-radius: 3px;
display: block;
line-height: 20px;
margin-bottom: 12px;
padding: 8px 12px;
background: #262c49;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
font-size: 12px;
font-weight: 400;
color: #c2c6dc;
&:focus {
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
background: ${mixin.darken('#262c49', 0.15)};
}
`;
export const LabelBox = styled.span<{ color: string }>`
@ -208,6 +236,7 @@ export const LabelBox = styled.span<{ color: string }>`
padding: 0;
width: 48px;
cursor: pointer;
background-color: ${props => props.color};
border-radius: 4px;
color: #fff;
@ -217,6 +246,7 @@ export const LabelBox = styled.span<{ color: string }>`
`;
export const SaveButton = styled.input`
cursor: pointer;
background-color: #5aac44;
box-shadow: none;
border: none;
@ -239,8 +269,7 @@ export const DeleteButton = styled.input`
border: none;
color: #fff;
cursor: pointer;
display: inline-block;
font-weight: 400;
type="submit"font-weight: 400;
line-height: 20px;
margin: 8px 4px 0 0;
padding: 6px 12px;
@ -248,3 +277,53 @@ export const DeleteButton = styled.input`
border-radius: 3px;
float: right;
`;
export const CreateLabelButton = styled.button`
outline: none;
border: none;
width: 100%;
border-radius: 3px;
line-height: 20px;
margin-bottom: 8px;
padding: 6px 12px;
background-color: none;
text-align: center;
color: #c2c6dc;
margin: 8px 4px 0 0;
font-size: 14px;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const PreviousButton = styled.div`
padding: 10px 12px 10px 8px;
position: absolute;
top: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
cursor: pointer;
`;
export const ContainerDiamond = styled.div<{ invert: boolean }>`
top: 10px;
${props => (props.invert ? 'right: 10px; ' : 'left: 15px;')}
position: absolute;
width: 10px;
height: 10px;
display: block;
transform: rotate(45deg) translate(-7px);
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(0, 0, 0, 0.1);
z-index: 10;
background: #262c49;
border-color: #414561;
`;

View File

@ -1,30 +1,219 @@
import React, { useRef } from 'react';
import { Cross } from 'shared/icons';
import React, { useRef, createContext, RefObject, useState, useContext } from 'react';
import { Cross, AngleLeft } from 'shared/icons';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { Container, Header, HeaderTitle, Content, CloseButton } from './Styles';
import { createPortal } from 'react-dom';
import produce from 'immer';
import {
Container,
ContainerDiamond,
Header,
HeaderTitle,
Content,
CloseButton,
PreviousButton,
Wrapper,
} from './Styles';
type Props = {
title: string;
type PopupContextState = {
show: (target: RefObject<HTMLElement>, content: JSX.Element) => void;
setTab: (newTab: number) => void;
getCurrentTab: () => number;
};
type PopupProps = {
title: string | null;
onClose: () => void;
tab: number;
};
type PopupContainerProps = {
top: number;
left: number;
invert: boolean;
onClose: () => void;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, children }) => {
const PopupContainer: React.FC<PopupContainerProps> = ({ top, left, onClose, children, invert }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef} invert={invert}>
{children}
</Container>
);
};
const PopupContext = createContext<PopupContextState>({
show: () => {},
setTab: () => {},
getCurrentTab: () => 0,
});
export const usePopup = () => {
const ctx = useContext<PopupContextState>(PopupContext);
return { showPopup: ctx.show, setTab: ctx.setTab, getCurrentTab: ctx.getCurrentTab };
};
type PopupState = {
isOpen: boolean;
left: number;
top: number;
invert: boolean;
currentTab: number;
previousTab: number;
content: JSX.Element | null;
};
const { Provider, Consumer } = PopupContext;
const canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
const defaultState = {
isOpen: false,
left: 0,
top: 0,
invert: false,
currentTab: 0,
previousTab: 0,
content: null,
};
export const PopupProvider: React.FC = ({ children }) => {
const [currentState, setState] = useState<PopupState>(defaultState);
const show = (target: RefObject<HTMLElement>, content: JSX.Element) => {
console.log(target);
if (target && target.current) {
const bounds = target.current.getBoundingClientRect();
if (bounds.left + 304 + 30 > window.innerWidth) {
console.log('open!');
setState({
isOpen: true,
left: bounds.left + bounds.width,
top: bounds.top + bounds.height,
invert: true,
currentTab: 0,
previousTab: 0,
content,
});
} else {
console.log('open NOT INVERT!');
setState({
isOpen: true,
left: bounds.left,
top: bounds.top + bounds.height,
invert: false,
currentTab: 0,
previousTab: 0,
content,
});
}
}
};
const portalTarget = canUseDOM ? document.body : null; // appease flow
const setTab = (newTab: number) => {
setState((prevState: PopupState) => {
return {
...prevState,
previousTab: currentState.currentTab,
currentTab: newTab,
};
});
};
const getCurrentTab = () => {
return currentState.currentTab;
};
return (
<Provider value={{ show, setTab, getCurrentTab }}>
{portalTarget &&
currentState.isOpen &&
createPortal(
<PopupContainer
invert={currentState.invert}
top={currentState.top}
left={currentState.left}
onClose={() => setState(defaultState)}
>
{currentState.content}
<ContainerDiamond invert={currentState.invert} />
</PopupContainer>,
portalTarget,
)}
{children}
</Provider>
);
};
type Props = {
title: string | null;
top: number;
left: number;
onClose: () => void;
onPrevious?: () => void | null;
noHeader?: boolean | null;
};
const PopupMenu: React.FC<Props> = ({ title, top, left, onClose, noHeader, children, onPrevious }) => {
const $containerRef = useRef();
useOnOutsideClick($containerRef, true, onClose, null);
return (
<Container left={left} top={top} ref={$containerRef}>
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross />
</CloseButton>
</Header>
<Content>{children}</Content>
<Container invert={false} left={left} top={top} ref={$containerRef}>
<Wrapper>
{onPrevious && (
<PreviousButton onClick={onPrevious}>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{noHeader ? (
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
) : (
<Header>
<HeaderTitle>{title}</HeaderTitle>
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
</Header>
)}
<Content>{children}</Content>
</Wrapper>
</Container>
);
};
export const Popup: React.FC<PopupProps> = ({ title, onClose, tab, children }) => {
const { getCurrentTab, setTab } = usePopup();
if (getCurrentTab() !== tab) {
return null;
}
return (
<>
<Wrapper>
{tab > 0 && (
<PreviousButton
onClick={() => {
setTab(0);
}}
>
<AngleLeft color="#c2c6dc" />
</PreviousButton>
)}
{title && (
<Header>
<HeaderTitle>{title}</HeaderTitle>
</Header>
)}
<CloseButton onClick={() => onClose()}>
<Cross color="#c2c6dc" />
</CloseButton>
<Content>{children}</Content>
</Wrapper>
</>
);
};
export default PopupMenu;

View File

@ -61,7 +61,7 @@ export const Default = () => {
onSaveName={action('on save name')}
onOpenComposer={action('on open composer')}
tasks={[]}
onExtraMenuOpen={(taskGroupID, pos, size) => console.log(taskGroupID, pos, size)}
onExtraMenuOpen={(taskGroupID, $targetRef) => console.log(taskGroupID, $targetRef)}
>
<ListCards>
<Card

View File

@ -2,7 +2,7 @@ import styled, { keyframes } from 'styled-components';
import TextareaAutosize from 'react-autosize-textarea';
export const Wrapper = styled.div<{ open: boolean }>`
background: rgba(0, 0, 0, 0.6);
background: rgba(0, 0, 0, 0.4);
bottom: 0;
color: #fff;
left: 0;

View File

@ -286,4 +286,7 @@ export const UnassignedLabel = styled.div`
color: rgb(137, 147, 164);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
height: 32px;
`;

View File

@ -47,6 +47,7 @@ export const Default = () => {
onTaskDescriptionChange={(_task, desc) => setDescription(desc)}
onDeleteTask={action('delete task')}
onCloseModal={action('close modal')}
onMemberProfile={action('profile')}
onOpenAddMemberPopup={action('open add member popup')}
onOpenAddLabelPopup={action('open add label popup')}
/>

View File

@ -93,13 +93,27 @@ const DetailsEditor: React.FC<DetailsEditorProps> = ({
);
};
type TaskAssigneeProps = {
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.userID)} key={member.userID}>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
};
type TaskDetailsProps = {
task: Task;
onTaskNameChange: (task: Task, newName: string) => void;
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
onDeleteTask: (task: Task) => void;
onOpenAddMemberPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddLabelPopup: (task: Task, bounds: ElementBounds) => void;
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
onCloseModal: () => void;
};
@ -111,6 +125,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
onCloseModal,
onOpenAddMemberPopup,
onOpenAddLabelPopup,
onMemberProfile,
}) => {
const [editorOpen, setEditorOpen] = useState(false);
const [description, setDescription] = useState(task.description ?? '');
@ -130,23 +145,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
const $unassignedRef = useRef<HTMLDivElement>(null);
const $addMemberRef = useRef<HTMLDivElement>(null);
const onUnassignedClick = () => {
const bounds = convertDivElementRefToBounds($unassignedRef);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
onOpenAddMemberPopup(task, $unassignedRef);
};
const onAddMember = () => {
const bounds = convertDivElementRefToBounds($addMemberRef);
if (bounds) {
onOpenAddMemberPopup(task, bounds);
}
onOpenAddMemberPopup(task, $addMemberRef);
};
const $addLabelRef = useRef<HTMLDivElement>(null);
const onAddLabel = () => {
const bounds = convertDivElementRefToBounds($addLabelRef);
if (bounds) {
onOpenAddLabelPopup(task, bounds);
}
onOpenAddLabelPopup(task, $addLabelRef);
};
console.log(task);
return (
@ -204,14 +210,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
) : (
<>
{task.members &&
task.members.map(member => {
console.log(member);
return (
<TaskDetailAssignee key={member.userID}>
<ProfileIcon>{member.profileIcon.initials ?? ''}</ProfileIcon>
</TaskDetailAssignee>
);
})}
task.members.map(member => <TaskAssignee member={member} onMemberProfile={onMemberProfile} />)}
<TaskDetailsAddMember ref={$addMemberRef} onClick={onAddMember}>
<TaskDetailsAddMemberIcon>
<Plus size={16} color="#c2c6dc" />

View File

@ -1,4 +1,5 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const NavbarWrapper = styled.div`
width: 100%;
@ -76,8 +77,10 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
`;
export const ProjectMeta = styled.div`
align-items: center;
display: flex;
padding-top: 9px;
margin-left: -14px;
align-items: center;
max-width: 100%;
min-height: 51px;
`;
@ -91,11 +94,11 @@ export const ProjectTabs = styled.div`
max-width: 100%;
`;
export const ProjectTab = styled.span`
export const ProjectTab = styled.span<{ active?: boolean }>`
font-size: 80%;
color: #c2c6dc;
font-size: 15px;
cursor: default;
cursor: pointer;
display: flex;
line-height: normal;
min-width: 1px;
@ -103,16 +106,71 @@ export const ProjectTab = styled.span`
transition-property: box-shadow, color;
white-space: nowrap;
flex: 0 1 auto;
padding-bottom: 12px;
box-shadow: inset 0 -2px #d85dd8;
color: #d85dd8;
&:not(:last-child) {
margin-right: 20px;
}
${props =>
props.active
? css`
box-shadow: inset 0 -2px #d85dd8;
color: #d85dd8;
`
: css`
&:hover {
box-shadow: inset 0 -2px #cbd4db;
color: ${mixin.lighten('#c2c6dc', 0.25)};
}
`}
`;
export const ProjectName = styled.h1`
color: #c2c6dc;
margin-top: 9px;
font-weight: 600;
font-size: 20px;
padding: 6px 10px 6px 8px;
`;
export const ProjectSwitcher = styled.button`
font-size: 20px;
outline: none;
border: none;
width: 100px;
border-radius: 3px;
line-height: 20px;
padding: 6px 4px;
background-color: none;
text-align: center;
color: #c2c6dc;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;
export const Separator = styled.div`
color: #c2c6dc;
font-size: 16px;
padding-left: 4px;
padding-right: 4px;
`;
export const ProjectSettingsButton = styled.button`
outline: none;
border: none;
border-radius: 3px;
line-height: 20px;
width: 28px;
height: 28px;
background-color: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`;

View File

@ -38,6 +38,7 @@ export const Default = () => {
<NormalizeStyles />
<BaseStyles />
<TopNavbar
projectName="Projects"
bgColor="#7367F0"
firstName="Jordan"
lastName="Knott"

View File

@ -1,10 +1,12 @@
import React, { useRef } from 'react';
import { Bell } from 'shared/icons';
import { Star, Bell, Cog, AngleDown } from 'shared/icons';
import {
NotificationContainer,
GlobalActions,
ProjectActions,
ProjectSwitcher,
Separator,
ProjectMeta,
ProjectName,
ProjectTabs,
@ -13,6 +15,7 @@ import {
NavbarHeader,
Breadcrumbs,
BreadcrumpSeparator,
ProjectSettingsButton,
ProfileIcon,
ProfileContainer,
ProfileNameWrapper,
@ -21,6 +24,7 @@ import {
} from './Styles';
type NavBarProps = {
projectName: string;
onProfileClick: (bottom: number, right: number) => void;
onNotificationClick: () => void;
bgColor: string;
@ -29,6 +33,7 @@ type NavBarProps = {
initials: string;
};
const NavBar: React.FC<NavBarProps> = ({
projectName,
onProfileClick,
onNotificationClick,
firstName,
@ -47,10 +52,19 @@ const NavBar: React.FC<NavBarProps> = ({
<NavbarHeader>
<ProjectActions>
<ProjectMeta>
<ProjectName>Production Team</ProjectName>
<ProjectSwitcher>Projects</ProjectSwitcher>
<Separator>»</Separator>
<ProjectName>{projectName}</ProjectName>
<ProjectSettingsButton>
<AngleDown color="#c2c6dc" />
</ProjectSettingsButton>
<Star filled color="#c2c6dc" />
</ProjectMeta>
<ProjectTabs>
<ProjectTab>Board</ProjectTab>
<ProjectTab active>Board</ProjectTab>
<ProjectTab>Calender</ProjectTab>
<ProjectTab>Timeline</ProjectTab>
<ProjectTab>Wiki</ProjectTab>
</ProjectTabs>
</ProjectActions>
<GlobalActions>