Design fixes (#665)
This commit is contained in:
@ -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 />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function useIsSubNavbarDisplayed() {
|
||||||
|
const currentPath = useLocation().pathname;
|
||||||
|
return currentPath.match(/\/settings\//g) !== null;
|
||||||
|
}
|
||||||
21
front/src/modules/ui/layout/navbar/MainNavbar.tsx
Normal file
21
front/src/modules/ui/layout/navbar/MainNavbar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const isNavbarSwitchingSizeState = atom({
|
||||||
|
key: 'ui/isNavbarSwitchingSizeState',
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
6
front/src/modules/ui/themes/animation.ts
Normal file
6
front/src/modules/ui/themes/animation.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const animation = {
|
||||||
|
duration: {
|
||||||
|
instant: 0.1,
|
||||||
|
visible: 0.3,
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user