Design fixes (#665)

This commit is contained in:
Charles Bochet
2023-07-14 18:43:16 -07:00
committed by GitHub
parent 0a319bcf86
commit b971464fe5
22 changed files with 464 additions and 138 deletions

View File

@ -10,24 +10,23 @@ import {
IconTargetArrow, IconTargetArrow,
IconUser, IconUser,
} from '@/ui/icons/index'; } from '@/ui/icons/index';
import NavItemsContainer from '@/ui/layout/navbar/NavItemsContainer'; import { useIsSubNavbarDisplayed } from '@/ui/layout/hooks/useIsSubNavbarDisplayed';
import MainNavbar from '@/ui/layout/navbar/MainNavbar';
import NavItem from './modules/ui/layout/navbar/NavItem'; import NavItem from './modules/ui/layout/navbar/NavItem';
import NavTitle from './modules/ui/layout/navbar/NavTitle'; import NavTitle from './modules/ui/layout/navbar/NavTitle';
import NavWorkspaceButton from './modules/ui/layout/navbar/NavWorkspaceButton';
export function AppNavbar() { export function AppNavbar() {
const theme = useTheme(); const theme = useTheme();
const currentPath = useLocation().pathname; const currentPath = useLocation().pathname;
const shouldDiplaySubNavBar = currentPath.match(/\/settings\//g) !== null; const isSubNavbarDisplayed = useIsSubNavbarDisplayed();
return ( return (
<> <>
{!shouldDiplaySubNavBar ? ( {!isSubNavbarDisplayed ? (
<> <MainNavbar>
<NavWorkspaceButton /> <>
<NavItemsContainer>
<NavItem <NavItem
label="Search" label="Search"
to="/search" to="/search"
@ -64,8 +63,8 @@ export function AppNavbar() {
icon={<IconTargetArrow size={theme.icon.size.md} />} icon={<IconTargetArrow size={theme.icon.size.md} />}
active={currentPath === '/opportunities'} active={currentPath === '/opportunities'}
/> />
</NavItemsContainer> </>
</> </MainNavbar>
) : ( ) : (
<SettingsNavbar /> <SettingsNavbar />
)} )}

View File

@ -6,7 +6,7 @@ import { useOpenCommentThreadRightDrawer } from '@/comments/hooks/useOpenComment
import { useOpenCreateCommentThreadDrawer } from '@/comments/hooks/useOpenCreateCommentThreadDrawer'; import { useOpenCreateCommentThreadDrawer } from '@/comments/hooks/useOpenCreateCommentThreadDrawer';
import { CommentableEntity } from '@/comments/types/CommentableEntity'; import { CommentableEntity } from '@/comments/types/CommentableEntity';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer'; import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import { IconCirclePlus, IconNotes } from '@/ui/icons/index'; import { IconNotes } from '@/ui/icons/index';
import { import {
beautifyExactDate, beautifyExactDate,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
@ -221,10 +221,6 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
<StyledMainContainer> <StyledMainContainer>
<StyledTopActionBar> <StyledTopActionBar>
<StyledTimelineItemContainer> <StyledTimelineItemContainer>
<StyledIconContainer>
<IconCirclePlus />
</StyledIconContainer>
<CommentThreadCreateButton <CommentThreadCreateButton
onNoteClick={() => openCreateCommandThread(entity)} onNoteClick={() => openCreateCommandThread(entity)}
/> />

View File

@ -11,9 +11,8 @@ import {
IconUsers, IconUsers,
} from '@/ui/icons/index'; } from '@/ui/icons/index';
import NavItem from '@/ui/layout/navbar/NavItem'; import NavItem from '@/ui/layout/navbar/NavItem';
import NavItemsContainer from '@/ui/layout/navbar/NavItemsContainer';
import NavTitle from '@/ui/layout/navbar/NavTitle'; import NavTitle from '@/ui/layout/navbar/NavTitle';
import SubNavbarContainer from '@/ui/layout/navbar/sub-navbar/SubNavBarContainer'; import SubNavbar from '@/ui/layout/navbar/sub-navbar/SubNavbar';
export function SettingsNavbar() { export function SettingsNavbar() {
const theme = useTheme(); const theme = useTheme();
@ -25,8 +24,8 @@ export function SettingsNavbar() {
}, [logout]); }, [logout]);
return ( return (
<SubNavbarContainer backButtonTitle="Settings"> <SubNavbar backButtonTitle="Settings">
<NavItemsContainer> <>
<NavTitle label="User" /> <NavTitle label="User" />
<NavItem <NavItem
label="Profile" label="Profile"
@ -81,7 +80,7 @@ export function SettingsNavbar() {
icon={<IconLogout size={theme.icon.size.md} />} icon={<IconLogout size={theme.icon.size.md} />}
danger={true} danger={true}
/> />
</NavItemsContainer> </>
</SubNavbarContainer> </SubNavbar>
); );
} }

View File

@ -1,33 +1,118 @@
import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
const StyledIconButton = styled.button` export type IconButtonVariant = 'transparent' | 'border' | 'shadow' | 'white';
export type IconButtonSize = 'large' | 'medium' | 'small';
export type ButtonProps = {
icon?: React.ReactNode;
variant?: IconButtonVariant;
size?: IconButtonSize;
} & React.ComponentProps<'button'>;
const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
align-items: center; align-items: center;
background: ${({ theme }) => theme.color.blue}; background: ${({ theme, variant, disabled }) => {
border: none; switch (variant) {
case 'shadow':
case 'white':
return theme.background.transparent.lighter;
case 'transparent':
case 'border':
default:
return 'transparent';
}
}};
border-color: ${({ theme, variant }) => {
switch (variant) {
case 'border':
return theme.border.color.medium;
case 'shadow':
case 'white':
case 'transparent':
default:
return 'none';
}
}};
transition: background 0.1s ease;
border-radius: ${({ theme }) => {
return theme.border.radius.sm;
}};
border-width: ${({ variant }) => {
switch (variant) {
case 'border':
return '1px';
case 'shadow':
case 'white':
case 'transparent':
default:
return 0;
}
}};
color: ${({ theme, disabled }) => {
if (disabled) {
return theme.font.color.extraLight;
}
border-radius: 50%; return theme.font.color.tertiary;
color: ${({ theme }) => theme.font.color.inverted}; }};
border-style: solid;
cursor: pointer; cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
height: ${({ size }) => {
switch (size) {
case 'large':
return '32px';
case 'medium':
return '24px';
case 'small':
default:
return '20px';
}
}};
display: flex; display: flex;
height: 20px;
justify-content: center; justify-content: center;
padding: 0; padding: 0;
transition: color 0.1s ease-in-out, background 0.1s ease-in-out; width: ${({ size }) => {
width: 20px; switch (size) {
case 'large':
&:disabled { return '32px';
background: ${({ theme }) => theme.background.quaternary}; case 'medium':
color: ${({ theme }) => theme.font.color.tertiary}; return '24px';
cursor: default; case 'small':
default:
return '20px';
}
}};
flex-shrink: 0;
&:hover {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.light;
}};
} }
user-select: none;
&:active {
background: ${({ theme, disabled }) => {
return disabled ? 'auto' : theme.background.transparent.medium;
}};
`; `;
export function IconButton({ export function IconButton({
icon, icon,
title,
variant = 'transparent',
size = 'medium',
disabled = false,
...props ...props
}: { icon: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) { }: ButtonProps) {
return <StyledIconButton {...props}>{icon}</StyledIconButton>; return (
<StyledIconButton
variant={variant}
size={size}
disabled={disabled}
{...props}
>
{icon}
</StyledIconButton>
);
} }

View File

@ -0,0 +1,33 @@
import styled from '@emotion/styled';
const StyledIconButton = styled.button`
align-items: center;
background: ${({ theme }) => theme.color.blue};
border: none;
border-radius: 50%;
color: ${({ theme }) => theme.font.color.inverted};
cursor: pointer;
display: flex;
height: 20px;
justify-content: center;
padding: 0;
transition: color 0.1s ease-in-out, background 0.1s ease-in-out;
width: 20px;
&:disabled {
background: ${({ theme }) => theme.background.quaternary};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: default;
}
`;
export function RoundedIconButton({
icon,
...props
}: { icon: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <StyledIconButton {...props}>{icon}</StyledIconButton>;
}

View File

@ -1,33 +1,153 @@
import React from 'react';
import styled from '@emotion/styled';
import { withKnobs } from '@storybook/addon-knobs';
import { expect, jest } from '@storybook/jest'; import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library'; import { userEvent, within } from '@storybook/testing-library';
import { IconArrowRight } from '@/ui/icons'; import { IconUser } from '@/ui/icons';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { IconButton } from '../IconButton'; import { IconButton } from '../IconButton';
type IconButtonProps = React.ComponentProps<typeof IconButton>;
const StyledContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
width: 800px;
> * + * {
margin-top: ${({ theme }) => theme.spacing(4)};
}
`;
const StyledTitle = styled.h1`
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-align: center;
text-transform: uppercase;
`;
const StyledLine = styled.div`
display: flex;
flex: 1;
flex-direction: row;
`;
const StyledIconButtonContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
width: 50px;
`;
const meta: Meta<typeof IconButton> = { const meta: Meta<typeof IconButton> = {
title: 'UI/Buttons/IconButton', title: 'UI/Buttons/IconButton',
component: IconButton, component: IconButton,
decorators: [withKnobs],
}; };
export default meta; export default meta;
type Story = StoryObj<typeof IconButton>; type Story = StoryObj<typeof IconButton>;
const variants: IconButtonProps['variant'][] = [
'transparent',
'border',
'shadow',
'white',
];
const clickJestFn = jest.fn(); const clickJestFn = jest.fn();
export const Default: Story = { const states = {
default: {
description: 'Default',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-default`,
onClick: clickJestFn,
}),
},
hover: {
description: 'Hover',
extraProps: (variant: string) => ({
id: `${variant}-button-hover`,
'data-testid': `${variant}-button-hover`,
}),
},
pressed: {
description: 'Pressed',
extraProps: (variant: string) => ({
id: `${variant}-button-pressed`,
'data-testid': `${variant}-button-pressed`,
}),
},
disabled: {
description: 'Disabled',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-disabled`,
disabled: true,
}),
},
};
function IconButtonRow({ variant, size, ...props }: IconButtonProps) {
const iconSize = size === 'small' ? 14 : 16;
return (
<>
{Object.entries(states).map(([state, { description, extraProps }]) => (
<StyledIconButtonContainer key={`${variant}-container-${state}`}>
<StyledDescription>{description}</StyledDescription>
<IconButton
{...props}
{...extraProps(variant ?? '')}
variant={variant}
size={size}
icon={<IconUser size={iconSize} />}
/>
</StyledIconButtonContainer>
))}
</>
);
}
const generateStory = (
size: IconButtonProps['size'],
LineComponent: React.ComponentType<IconButtonProps>,
): Story => ({
render: getRenderWrapperForComponent( render: getRenderWrapperForComponent(
<IconButton onClick={clickJestFn} icon={<IconArrowRight size={15} />} />, <StyledContainer>
{variants.map((variant) => (
<div key={variant}>
<StyledTitle>{variant}</StyledTitle>
<StyledLine>
<LineComponent size={size} variant={variant} />
</StyledLine>
</div>
))}
</StyledContainer>,
), ),
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0); const button = canvas.getByTestId(`transparent-button-default`);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1); const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
}, },
}; });
export const LargeSize = generateStory('large', IconButtonRow);
export const MediumSize = generateStory('medium', IconButtonRow);
export const SmallSize = generateStory('small', IconButtonRow);

View File

@ -0,0 +1,36 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconArrowRight } from '@/ui/icons';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { RoundedIconButton } from '../RoundedIconButton';
const meta: Meta<typeof RoundedIconButton> = {
title: 'UI/Buttons/RoundedIconButton',
component: RoundedIconButton,
};
export default meta;
type Story = StoryObj<typeof RoundedIconButton>;
const clickJestFn = jest.fn();
export const Default: Story = {
render: getRenderWrapperForComponent(
<RoundedIconButton
onClick={clickJestFn}
icon={<IconArrowRight size={15} />}
/>,
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickJestFn).toHaveBeenCalledTimes(0);
const button = canvas.getByRole('button');
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(1);
},
};

View File

@ -4,7 +4,7 @@ import { HotkeysEvent } from 'react-hotkeys-hook/dist/types';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconButton } from '@/ui/components/buttons/IconButton'; import { RoundedIconButton } from '@/ui/components/buttons/RoundedIconButton';
import { IconArrowRight } from '@/ui/icons/index'; import { IconArrowRight } from '@/ui/icons/index';
const MAX_ROWS = 5; const MAX_ROWS = 5;
@ -47,7 +47,7 @@ const StyledTextArea = styled(TextareaAutosize)`
`; `;
// TODO: this messes with the layout, fix it // TODO: this messes with the layout, fix it
const StyledBottomRightIconButton = styled.div` const StyledBottomRightRoundedIconButton = styled.div`
height: 0; height: 0;
position: relative; position: relative;
right: 26px; right: 26px;
@ -129,13 +129,13 @@ export function AutosizeTextInput({
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
/> />
<StyledBottomRightIconButton> <StyledBottomRightRoundedIconButton>
<IconButton <RoundedIconButton
onClick={handleOnClickSendButton} onClick={handleOnClickSendButton}
icon={<IconArrowRight size={15} />} icon={<IconArrowRight size={15} />}
disabled={isSendButtonDisabled} disabled={isSendButtonDisabled}
/> />
</StyledBottomRightIconButton> </StyledBottomRightRoundedIconButton>
</StyledContainer> </StyledContainer>
</> </>
); );

View File

@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
import { TableColumn } from '@/people/table/components/peopleColumns'; import { TableColumn } from '@/people/table/components/peopleColumns';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState'; import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState';
import { RowContext } from '@/ui/tables/states/RowContext'; import { RowContext } from '@/ui/tables/states/RowContext';
import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState'; import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState';
@ -11,13 +12,15 @@ import { EntityTableRow } from './EntityTableRow';
export function EntityTableBody({ columns }: { columns: Array<TableColumn> }) { export function EntityTableBody({ columns }: { columns: Array<TableColumn> }) {
const rowIds = useRecoilValue(tableRowIdsState); const rowIds = useRecoilValue(tableRowIdsState);
const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState);
const isFetchingEntityTableData = useRecoilValue( const isFetchingEntityTableData = useRecoilValue(
isFetchingEntityTableDataState, isFetchingEntityTableDataState,
); );
return ( return (
<tbody> <tbody>
{!isFetchingEntityTableData {!isFetchingEntityTableData && !isNavbarSwitchingSize
? rowIds.map((rowId, index) => ( ? rowIds.map((rowId, index) => (
<RecoilScope SpecificContext={RowContext} key={rowId}> <RecoilScope SpecificContext={RowContext} key={rowId}>
<EntityTableRow columns={columns} rowId={rowId} index={index} /> <EntityTableRow columns={columns} rowId={rowId} index={index} />

View File

@ -7,7 +7,7 @@ import { AppNavbar } from '~/AppNavbar';
import { MOBILE_VIEWPORT } from '../themes/themes'; import { MOBILE_VIEWPORT } from '../themes/themes';
import { NavbarContainer } from './navbar/NavbarContainer'; import { NavbarAnimatedContainer } from './navbar/NavbarAnimatedContainer';
import { isNavbarOpenedState } from './states/isNavbarOpenedState'; import { isNavbarOpenedState } from './states/isNavbarOpenedState';
const StyledLayout = styled.div` const StyledLayout = styled.div`
@ -47,9 +47,9 @@ export function DefaultLayout({ children }: OwnProps) {
{userIsAuthenticated ? ( {userIsAuthenticated ? (
<> <>
<CommandMenu /> <CommandMenu />
<NavbarContainer> <NavbarAnimatedContainer>
<AppNavbar /> <AppNavbar />
</NavbarContainer> </NavbarAnimatedContainer>
<MainContainer>{children}</MainContainer> <MainContainer>{children}</MainContainer>
</> </>
) : ( ) : (

View File

@ -0,0 +1,6 @@
import { useLocation } from 'react-router-dom';
export function useIsSubNavbarDisplayed() {
const currentPath = useLocation().pathname;
return currentPath.match(/\/settings\//g) !== null;
}

View File

@ -0,0 +1,21 @@
import styled from '@emotion/styled';
import NavItemsContainer from './NavItemsContainer';
import NavWorkspaceButton from './NavWorkspaceButton';
type OwnProps = {
children: JSX.Element;
};
const StyledContainer = styled.div`
width: 220px;
`;
export default function MainNavbar({ children }: OwnProps) {
return (
<StyledContainer>
<NavWorkspaceButton />
<NavItemsContainer>{children}</NavItemsContainer>
</StyledContainer>
);
}

View File

@ -8,7 +8,6 @@ const StyledNavItemsContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-top: 40px; margin-top: 40px;
min-width: 220px;
`; `;
function NavItemsContainer({ children }: OwnProps) { function NavItemsContainer({ children }: OwnProps) {

View File

@ -0,0 +1,57 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MOBILE_VIEWPORT } from '@/ui/themes/themes';
import { useIsSubNavbarDisplayed } from '../hooks/useIsSubNavbarDisplayed';
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
import { isNavbarSwitchingSizeState } from '../states/isNavbarSwitchingSizeState';
const StyledNavbarContainer = styled(motion.div)`
align-items: end;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
padding: ${({ theme }) => theme.spacing(2)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: ${(props) =>
useRecoilValue(isNavbarOpenedState)
? `calc(100% - ` + props.theme.spacing(4) + `)`
: '0'};
}
`;
type NavbarProps = {
children: React.ReactNode;
layout?: string;
};
export function NavbarAnimatedContainer({ children, layout }: NavbarProps) {
const isMenuOpened = useRecoilValue(isNavbarOpenedState);
const [, setIsNavbarSwitchingSize] = useRecoilState(
isNavbarSwitchingSizeState,
);
const isSubNavbarDisplayed = useIsSubNavbarDisplayed();
const theme = useTheme();
return (
<StyledNavbarContainer
onAnimationComplete={() => {
setIsNavbarSwitchingSize(false);
}}
animate={{
width: isMenuOpened ? (isSubNavbarDisplayed ? '520px' : '220px') : '0',
opacity: isMenuOpened ? 1 : 0,
}}
transition={{
duration: theme.animation.duration.visible,
}}
>
{children}
</StyledNavbarContainer>
);
}

View File

@ -1,38 +0,0 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { MOBILE_VIEWPORT } from '@/ui/themes/themes';
import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
const StyledNavbarContainer = styled.div`
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
padding: ${({ theme }) => theme.spacing(2)};
width: ${(props) => (useRecoilValue(isNavbarOpenedState) ? 'auto' : '0')};
@media (max-width: ${MOBILE_VIEWPORT}px) {
width: ${(props) =>
useRecoilValue(isNavbarOpenedState)
? `calc(100% - ` + props.theme.spacing(4) + `)`
: '0'};
}
`;
const NavbarContent = styled.div`
display: ${() => (useRecoilValue(isNavbarOpenedState) ? 'block' : 'none')};
`;
type NavbarProps = {
children: React.ReactNode;
layout?: string;
};
export function NavbarContainer({ children, layout }: NavbarProps) {
return (
<StyledNavbarContainer>
<NavbarContent>{children}</NavbarContent>
</StyledNavbarContainer>
);
}

View File

@ -1,8 +1,10 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { IconChevronLeft } from '@/ui/icons/index'; import { IconChevronLeft } from '@/ui/icons/index';
import { isNavbarSwitchingSizeState } from '../../states/isNavbarSwitchingSizeState';
import NavCollapseButton from '../NavCollapseButton'; import NavCollapseButton from '../NavCollapseButton';
type OwnProps = { type OwnProps = {
@ -32,12 +34,18 @@ const StyledContainer = styled.div`
export default function NavBackButton({ title }: OwnProps) { export default function NavBackButton({ title }: OwnProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [, setIsNavbarSwitchingSize] = useRecoilState(
isNavbarSwitchingSizeState,
);
return ( return (
<> <>
<StyledContainer> <StyledContainer>
<IconAndButtonContainer <IconAndButtonContainer
onClick={() => navigate('/', { replace: true })} onClick={() => {
setIsNavbarSwitchingSize(true);
navigate('/', { replace: true });
}}
> >
<IconChevronLeft /> <IconChevronLeft />
<span>{title}</span> <span>{title}</span>

View File

@ -1,5 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import NavItemsContainer from '../NavItemsContainer';
import NavBackButton from './NavBackButton'; import NavBackButton from './NavBackButton';
type OwnProps = { type OwnProps = {
@ -10,23 +12,15 @@ type OwnProps = {
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: 300px;
padding-top: ${({ theme }) => theme.spacing(6)}; padding-top: ${({ theme }) => theme.spacing(6)};
width: 220px;
`; `;
const StyledNavItemsContainer = styled.div` export default function SubNavbar({ children, backButtonTitle }: OwnProps) {
display: flex;
flex-direction: column;
`;
export default function SubNavbarContainer({
children,
backButtonTitle,
}: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<NavBackButton title={backButtonTitle} /> <NavBackButton title={backButtonTitle} />
<StyledNavItemsContainer>{children}</StyledNavItemsContainer> <NavItemsContainer>{children}</NavItemsContainer>
</StyledContainer> </StyledContainer>
); );
} }

View File

@ -1,5 +1,7 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { import {
@ -13,16 +15,15 @@ import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerRouter } from './RightDrawerRouter'; import { RightDrawerRouter } from './RightDrawerRouter';
const StyledContainer = styled.div` const StyledContainer = styled(motion.div)`
background: ${({ theme }) => theme.background.primary}; background: ${({ theme }) => theme.background.primary};
box-shadow: ${({ theme }) => theme.boxShadow.strong}; box-shadow: ${({ theme }) => theme.boxShadow.strong};
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
transition: width 0.5s;
width: ${({ theme }) => theme.rightDrawerWidth};
z-index: 2; z-index: 2;
`; `;
@ -45,17 +46,23 @@ export function RightDrawer() {
callback: () => setIsRightDrawerOpen(false), callback: () => setIsRightDrawerOpen(false),
mode: OutsideClickAlerterMode.absolute, mode: OutsideClickAlerterMode.absolute,
}); });
if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) { const theme = useTheme();
if (!isDefined(rightDrawerPage)) {
return <></>; return <></>;
} }
return ( return (
<> <StyledContainer
<StyledContainer> animate={{
<StyledRightDrawer ref={rightDrawerRef}> width: isRightDrawerOpen ? theme.rightDrawerWidth : '0',
<RightDrawerRouter /> }}
</StyledRightDrawer> transition={{
</StyledContainer> duration: theme.animation.duration.visible,
</> }}
>
<StyledRightDrawer ref={rightDrawerRef}>
<RightDrawerRouter />
</StyledRightDrawer>
</StyledContainer>
); );
} }

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isNavbarSwitchingSizeState = atom({
key: 'ui/isNavbarSwitchingSizeState',
default: true,
});

View File

@ -1,6 +1,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconButton } from '@/ui/components/buttons/IconButton';
import { IconPlus } from '@/ui/icons/index'; import { IconPlus } from '@/ui/icons/index';
import NavCollapseButton from '../navbar/NavCollapseButton'; import NavCollapseButton from '../navbar/NavCollapseButton';
@ -16,6 +17,7 @@ const TopBarContainer = styled.div`
font-size: 14px; font-size: 14px;
min-height: ${TOP_BAR_MIN_HEIGHT}px; min-height: ${TOP_BAR_MIN_HEIGHT}px;
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(3)};
`; `;
const TitleContainer = styled.div` const TitleContainer = styled.div`
@ -26,22 +28,6 @@ const TitleContainer = styled.div`
width: 100%; width: 100%;
`; `;
const AddButtonContainer = styled.div`
align-items: center;
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex;
flex-shrink: 0;
height: 28px;
justify-content: center;
justify-self: flex-end;
margin-right: ${({ theme }) => theme.spacing(1)};
user-select: none;
width: 28px;
`;
type OwnProps = { type OwnProps = {
title: string; title: string;
icon: ReactNode; icon: ReactNode;
@ -56,12 +42,13 @@ export function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
{icon} {icon}
<TitleContainer data-testid="top-bar-title">{title}</TitleContainer> <TitleContainer data-testid="top-bar-title">{title}</TitleContainer>
{onAddButtonClick && ( {onAddButtonClick && (
<AddButtonContainer <IconButton
icon={<IconPlus size={16} />}
size="large"
data-testid="add-button" data-testid="add-button"
onClick={onAddButtonClick} onClick={onAddButtonClick}
> variant="border"
<IconPlus size={16} /> />
</AddButtonContainer>
)} )}
</TopBarContainer> </TopBarContainer>
</> </>

View File

@ -0,0 +1,6 @@
export const animation = {
duration: {
instant: 0.1,
visible: 0.3,
},
};

View File

@ -1,3 +1,4 @@
import { animation } from './animation';
import { backgroundDark, backgroundLight } from './background'; import { backgroundDark, backgroundLight } from './background';
import { blur } from './blur'; import { blur } from './blur';
import { borderDark, borderLight } from './border'; import { borderDark, borderLight } from './border';
@ -13,6 +14,7 @@ const common = {
icon: icon, icon: icon,
text: text, text: text,
blur: blur, blur: blur,
animation: animation,
snackBar: { snackBar: {
success: { success: {
background: '#16A26B', background: '#16A26B',