Refactor/dropdown menu (#279)
* Created dropdown menu UI component with story * Added all components for composing Dropdown Menus * Better component naming and reordered stories * Solved comment thread from review
This commit is contained in:
@ -2,7 +2,7 @@ import { Tooltip } from 'react-tooltip';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
|
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
|
||||||
import { UserAvatar } from '@/users/components/UserAvatar';
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
import {
|
import {
|
||||||
beautifyExactDate,
|
beautifyExactDate,
|
||||||
beautifyPastDateRelativeToNow,
|
beautifyPastDateRelativeToNow,
|
||||||
@ -75,7 +75,7 @@ export function CommentHeader({ comment }: OwnProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<UserAvatar
|
<Avatar
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
size={16}
|
size={16}
|
||||||
placeholderLetter={capitalizedFirstUsernameLetter}
|
placeholderLetter={capitalizedFirstUsernameLetter}
|
||||||
|
|||||||
@ -15,5 +15,5 @@ export const EditableRelationCreateButton = styled.button`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
background: none;
|
background: none;
|
||||||
gap: 8px;
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|||||||
20
front/src/modules/ui/components/menu/DropdownMenu.tsx
Normal file
20
front/src/modules/ui/components/menu/DropdownMenu.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const DropdownMenu = styled.div`
|
||||||
|
width: 200px;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background: ${(props) => props.theme.secondaryBackgroundTransparent};
|
||||||
|
|
||||||
|
border: 1px solid ${(props) => props.theme.lightBorder};
|
||||||
|
|
||||||
|
border-radius: calc(${(props) => props.theme.borderRadius} * 2);
|
||||||
|
|
||||||
|
box-shadow: ${(props) => props.theme.modalBoxShadow};
|
||||||
|
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
`;
|
||||||
30
front/src/modules/ui/components/menu/DropdownMenuButton.tsx
Normal file
30
front/src/modules/ui/components/menu/DropdownMenuButton.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { hoverBackground } from '@/ui/layout/styles/themes';
|
||||||
|
|
||||||
|
export const DropdownMenuButton = styled.div`
|
||||||
|
--horizontal-padding: ${(props) => props.theme.spacing(1.5)};
|
||||||
|
--vertical-padding: ${(props) => props.theme.spacing(2)};
|
||||||
|
|
||||||
|
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||||
|
|
||||||
|
width: calc(100% - 2 * var(--horizontal-padding));
|
||||||
|
height: calc(32px - 2 * var(--vertical-padding));
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
|
||||||
|
border-radius: ${(props) => props.theme.borderRadius};
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
${hoverBackground};
|
||||||
|
|
||||||
|
color: ${(props) => props.theme.text60};
|
||||||
|
font-size: ${(props) => props.theme.fontSizeSmall};
|
||||||
|
`;
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { Checkbox } from '../form/Checkbox';
|
||||||
|
|
||||||
|
import { DropdownMenuButton } from './DropdownMenuButton';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
checked: boolean;
|
||||||
|
onChange?: (newCheckedValue: boolean) => void;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownMenuCheckableItemContainer = styled(DropdownMenuButton)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLeftContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledChildrenContainer = styled.div`
|
||||||
|
font-size: ${(props) => props.theme.fontSizeSmall};
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function DropdownMenuCheckableItem({
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<Props>) {
|
||||||
|
function handleClick() {
|
||||||
|
onChange?.(!checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckableItemContainer onClick={handleClick}>
|
||||||
|
<StyledLeftContainer>
|
||||||
|
<Checkbox onChange={onChange} id={id} name={id} checked={checked} />
|
||||||
|
<StyledChildrenContainer>{children}</StyledChildrenContainer>
|
||||||
|
</StyledLeftContainer>
|
||||||
|
</DropdownMenuCheckableItemContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
front/src/modules/ui/components/menu/DropdownMenuItem.tsx
Normal file
22
front/src/modules/ui/components/menu/DropdownMenuItem.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const DropdownMenuItem = styled.div`
|
||||||
|
--horizontal-padding: ${(props) => props.theme.spacing(1.5)};
|
||||||
|
--vertical-padding: ${(props) => props.theme.spacing(2)};
|
||||||
|
|
||||||
|
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||||
|
|
||||||
|
width: calc(100% - 2 * var(--horizontal-padding));
|
||||||
|
height: calc(32px - 2 * var(--vertical-padding));
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
|
||||||
|
border-radius: ${(props) => props.theme.borderRadius};
|
||||||
|
|
||||||
|
color: ${(props) => props.theme.text60};
|
||||||
|
font-size: ${(props) => props.theme.fontSizeSmall};
|
||||||
|
`;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const DropdownMenuItemContainer = styled.div`
|
||||||
|
--padding: ${(props) => props.theme.spacing(1 / 2)};
|
||||||
|
|
||||||
|
width: calc(100% - 2 * var(--padding));
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: var(--padding);
|
||||||
|
gap: 2px;
|
||||||
|
`;
|
||||||
40
front/src/modules/ui/components/menu/DropdownMenuSearch.tsx
Normal file
40
front/src/modules/ui/components/menu/DropdownMenuSearch.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { InputHTMLAttributes } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { textInputStyle } from '@/ui/layout/styles/themes';
|
||||||
|
|
||||||
|
export const DropdownMenuSearchContainer = styled.div`
|
||||||
|
--horizontal-padding: ${(props) => props.theme.spacing(2)};
|
||||||
|
--vertical-padding: ${(props) => props.theme.spacing(1)};
|
||||||
|
|
||||||
|
width: calc(100% - 2 * var(--horizontal-padding));
|
||||||
|
height: calc(36px - 2 * var(--vertical-padding));
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||||
|
|
||||||
|
border-bottom: 1px solid ${(props) => props.theme.lightBorder};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEditModeSearchInput = styled.input`
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
${textInputStyle}
|
||||||
|
|
||||||
|
font-size: ${(props) => props.theme.fontSizeSmall};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function DropdownMenuSearch(
|
||||||
|
props: InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuSearchContainer>
|
||||||
|
<StyledEditModeSearchInput
|
||||||
|
{...props}
|
||||||
|
placeholder={props.placeholder ?? 'Search'}
|
||||||
|
/>
|
||||||
|
</DropdownMenuSearchContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconCheck } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { hoverBackground } from '@/ui/layout/styles/themes';
|
||||||
|
|
||||||
|
import { DropdownMenuButton } from './DropdownMenuButton';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownMenuSelectableItemContainer = styled(DropdownMenuButton)<Props>`
|
||||||
|
${hoverBackground};
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLeftContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRightIcon = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function DropdownMenuSelectableItem({
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<Props>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuSelectableItemContainer onClick={onClick} selected={selected}>
|
||||||
|
<StyledLeftContainer>{children}</StyledLeftContainer>
|
||||||
|
<StyledRightIcon>{selected && <IconCheck size={16} />}</StyledRightIcon>
|
||||||
|
</DropdownMenuSelectableItemContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const DropdownMenuSeparator = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
|
||||||
|
background-color: ${(props) => props.theme.mediumBorder};
|
||||||
|
`;
|
||||||
@ -0,0 +1,331 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { IconPlus } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
|
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
|
import { DropdownMenu } from '../DropdownMenu';
|
||||||
|
import { DropdownMenuButton } from '../DropdownMenuButton';
|
||||||
|
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
|
||||||
|
import { DropdownMenuItem } from '../DropdownMenuItem';
|
||||||
|
import { DropdownMenuItemContainer } from '../DropdownMenuItemContainer';
|
||||||
|
import { DropdownMenuSearch } from '../DropdownMenuSearch';
|
||||||
|
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
|
||||||
|
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
|
||||||
|
|
||||||
|
const meta: Meta<typeof DropdownMenu> = {
|
||||||
|
title: 'Components/DropdownMenu',
|
||||||
|
component: DropdownMenu,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof DropdownMenu>;
|
||||||
|
|
||||||
|
const FakeContentBelow = () => (
|
||||||
|
<div style={{ position: 'absolute' }}>
|
||||||
|
askjdlaksjdlaksjdlakjsdlkj lkajsldkjalskd jalksdj alksjd alskjd alksjd
|
||||||
|
alksjd laksjd askjdlaksjdlaksjdlakjsdlkj lkajsldkjalskd jalksdj alksjd
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatarUrl =
|
||||||
|
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
|
||||||
|
|
||||||
|
const FakeMenuContent = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FakeBelowContainer = styled.div`
|
||||||
|
width: 300px;
|
||||||
|
height: 600px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuAbsolutePositionWrapper = styled.div`
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FakeMenuItemList = () => (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem>Company A</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Company B</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Company C</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Person 2</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Company D</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Person 1</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockSelectArray = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Company A',
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Company B',
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Company C',
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Person 2',
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'Company D',
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
name: 'Person 1',
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Empty: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<DropdownMenu>
|
||||||
|
<FakeMenuContent />
|
||||||
|
</DropdownMenu>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownMenuStoryWrapper = ({
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<unknown>) => (
|
||||||
|
<FakeBelowContainer>
|
||||||
|
<FakeContentBelow />
|
||||||
|
<MenuAbsolutePositionWrapper>
|
||||||
|
<DropdownMenu>{children}</DropdownMenu>
|
||||||
|
</MenuAbsolutePositionWrapper>
|
||||||
|
</FakeBelowContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EmptyWithContentBelow: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<DropdownMenuStoryWrapper>
|
||||||
|
<FakeMenuContent />
|
||||||
|
</DropdownMenuStoryWrapper>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SimpleMenuItem: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<DropdownMenuStoryWrapper>
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<FakeMenuItemList />
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenuStoryWrapper>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Search: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<FakeBelowContainer>
|
||||||
|
<FakeContentBelow />
|
||||||
|
<MenuAbsolutePositionWrapper>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuSearch />
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<FakeMenuItemList />
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MenuAbsolutePositionWrapper>
|
||||||
|
</FakeBelowContainer>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const FakeSelectableMenuItemList = () => {
|
||||||
|
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mockSelectArray.map((item) => (
|
||||||
|
<DropdownMenuSelectableItem
|
||||||
|
key={item.id}
|
||||||
|
selected={selectedItem === item.id}
|
||||||
|
onClick={() => setSelectedItem(item.id)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</DropdownMenuSelectableItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Button: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<FakeBelowContainer>
|
||||||
|
<FakeContentBelow />
|
||||||
|
<MenuAbsolutePositionWrapper>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<DropdownMenuButton>
|
||||||
|
<IconPlus size={16} />
|
||||||
|
<div>Create new</div>
|
||||||
|
</DropdownMenuButton>
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<FakeSelectableMenuItemList />
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MenuAbsolutePositionWrapper>
|
||||||
|
</FakeBelowContainer>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectableMenuItem: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<FakeBelowContainer>
|
||||||
|
<FakeContentBelow />
|
||||||
|
<MenuAbsolutePositionWrapper>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<FakeSelectableMenuItemList />
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MenuAbsolutePositionWrapper>
|
||||||
|
</FakeBelowContainer>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const FakeSelectableMenuItemWithAvatarList = () => {
|
||||||
|
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mockSelectArray.map((item) => (
|
||||||
|
<DropdownMenuSelectableItem
|
||||||
|
key={item.id}
|
||||||
|
selected={selectedItem === item.id}
|
||||||
|
onClick={() => setSelectedItem(item.id)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
placeholderLetter="A"
|
||||||
|
avatarUrl={item.avatarUrl}
|
||||||
|
size={16}
|
||||||
|
type="squared"
|
||||||
|
/>
|
||||||
|
{item.name}
|
||||||
|
</DropdownMenuSelectableItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectableMenuItemWithAvatar: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<FakeBelowContainer>
|
||||||
|
<FakeContentBelow />
|
||||||
|
<MenuAbsolutePositionWrapper>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<FakeSelectableMenuItemWithAvatarList />
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MenuAbsolutePositionWrapper>
|
||||||
|
</FakeBelowContainer>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const FakeCheckableMenuItemList = () => {
|
||||||
|
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mockSelectArray.map((item) => (
|
||||||
|
<DropdownMenuCheckableItem
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
checked={selectedItems.includes(item.id)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedItems([...selectedItems, item.id]);
|
||||||
|
} else {
|
||||||
|
setSelectedItems(selectedItems.filter((id) => id !== item.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</DropdownMenuCheckableItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CheckableMenuItem: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<FakeBelowContainer>
|
||||||
|
<FakeContentBelow />
|
||||||
|
<MenuAbsolutePositionWrapper>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<FakeCheckableMenuItemList />
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MenuAbsolutePositionWrapper>
|
||||||
|
</FakeBelowContainer>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const FakeCheckableMenuItemWithAvatarList = () => {
|
||||||
|
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mockSelectArray.map((item) => (
|
||||||
|
<DropdownMenuCheckableItem
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
checked={selectedItems.includes(item.id)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedItems([...selectedItems, item.id]);
|
||||||
|
} else {
|
||||||
|
setSelectedItems(selectedItems.filter((id) => id !== item.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
placeholderLetter="A"
|
||||||
|
avatarUrl={item.avatarUrl}
|
||||||
|
size={16}
|
||||||
|
type="squared"
|
||||||
|
/>
|
||||||
|
{item.name}
|
||||||
|
</DropdownMenuCheckableItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CheckableMenuItemWithAvatar: Story = {
|
||||||
|
render: getRenderWrapperForComponent(
|
||||||
|
<FakeBelowContainer>
|
||||||
|
<FakeContentBelow />
|
||||||
|
<MenuAbsolutePositionWrapper>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
<FakeCheckableMenuItemWithAvatarList />
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</DropdownMenu>
|
||||||
|
</MenuAbsolutePositionWrapper>
|
||||||
|
</FakeBelowContainer>,
|
||||||
|
),
|
||||||
|
};
|
||||||
@ -3,3 +3,4 @@ export { IconComment } from './components/IconComment';
|
|||||||
export { IconSidebarLeftCollapse } from './components/IconSidebarLeftCollapse';
|
export { IconSidebarLeftCollapse } from './components/IconSidebarLeftCollapse';
|
||||||
export { IconSidebarRightCollapse } from './components/IconSidebarRightCollapse';
|
export { IconSidebarRightCollapse } from './components/IconSidebarRightCollapse';
|
||||||
export { IconAward } from '@tabler/icons-react';
|
export { IconAward } from '@tabler/icons-react';
|
||||||
|
export { IconCheck } from '@tabler/icons-react';
|
||||||
|
|||||||
@ -49,6 +49,7 @@ const lightThemeSpecific = {
|
|||||||
|
|
||||||
primaryBorder: 'rgba(0, 0, 0, 0.08)',
|
primaryBorder: 'rgba(0, 0, 0, 0.08)',
|
||||||
lightBorder: '#f5f5f5',
|
lightBorder: '#f5f5f5',
|
||||||
|
mediumBorder: '#ebebeb',
|
||||||
|
|
||||||
clickableElementBackgroundHover: 'rgba(0, 0, 0, 0.04)',
|
clickableElementBackgroundHover: 'rgba(0, 0, 0, 0.04)',
|
||||||
clickableElementBackgroundTransition: 'background 0.1s ease',
|
clickableElementBackgroundTransition: 'background 0.1s ease',
|
||||||
@ -72,6 +73,8 @@ const lightThemeSpecific = {
|
|||||||
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
|
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
|
||||||
|
|
||||||
boxShadow: '0px 2px 4px 0px #0F0F0F0A',
|
boxShadow: '0px 2px 4px 0px #0F0F0F0A',
|
||||||
|
|
||||||
|
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const darkThemeSpecific: typeof lightThemeSpecific = {
|
const darkThemeSpecific: typeof lightThemeSpecific = {
|
||||||
@ -94,6 +97,7 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
|
|||||||
|
|
||||||
primaryBorder: 'rgba(255, 255, 255, 0.08)',
|
primaryBorder: 'rgba(255, 255, 255, 0.08)',
|
||||||
lightBorder: '#222222',
|
lightBorder: '#222222',
|
||||||
|
mediumBorder: '#141414',
|
||||||
|
|
||||||
text100: '#ffffff',
|
text100: '#ffffff',
|
||||||
text80: '#cccccc',
|
text80: '#cccccc',
|
||||||
@ -113,13 +117,14 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
|
|||||||
blueHighTransparency: 'rgba(104, 149, 236, 0.03)',
|
blueHighTransparency: 'rgba(104, 149, 236, 0.03)',
|
||||||
blueLowTransparency: 'rgba(104, 149, 236, 0.32)',
|
blueLowTransparency: 'rgba(104, 149, 236, 0.32)',
|
||||||
boxShadow: '0px 2px 4px 0px #0F0F0F0A', // TODO change color for dark theme
|
boxShadow: '0px 2px 4px 0px #0F0F0F0A', // TODO change color for dark theme
|
||||||
|
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)', // TODO change color for dark theme
|
||||||
};
|
};
|
||||||
|
|
||||||
export const overlayBackground = (props: any) =>
|
export const overlayBackground = (props: any) =>
|
||||||
css`
|
css`
|
||||||
background: ${props.theme.secondaryBackgroundTransparent};
|
background: ${props.theme.secondaryBackgroundTransparent};
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
|
box-shadow: ${props.theme.modalBoxShadow};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const textInputStyle = (props: any) =>
|
export const textInputStyle = (props: any) =>
|
||||||
@ -137,6 +142,14 @@ export const textInputStyle = (props: any) =>
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const hoverBackground = (props: any) =>
|
||||||
|
css`
|
||||||
|
transition: background 0.1s ease;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
|
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
|
||||||
export const darkTheme = { ...commonTheme, ...darkThemeSpecific };
|
export const darkTheme = { ...commonTheme, ...darkThemeSpecific };
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,13 @@ type OwnProps = {
|
|||||||
avatarUrl: string | null | undefined;
|
avatarUrl: string | null | undefined;
|
||||||
size: number;
|
size: number;
|
||||||
placeholderLetter: string;
|
placeholderLetter: string;
|
||||||
|
type?: 'squared' | 'rounded';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledUserAvatar = styled.div<Omit<OwnProps, 'placeholderLetter'>>`
|
export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholderLetter'>>`
|
||||||
width: ${(props) => props.size}px;
|
width: ${(props) => props.size}px;
|
||||||
height: ${(props) => props.size}px;
|
height: ${(props) => props.size}px;
|
||||||
border-radius: 50%;
|
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
|
||||||
background-image: url(${(props) =>
|
background-image: url(${(props) =>
|
||||||
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
|
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
@ -46,16 +47,21 @@ export const StyledPlaceholderLetter = styled.div<StyledPlaceholderLetterProps>`
|
|||||||
color: ${(props) => props.theme.text80};
|
color: ${(props) => props.theme.text80};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function UserAvatar({ avatarUrl, size, placeholderLetter }: OwnProps) {
|
export function Avatar({
|
||||||
|
avatarUrl,
|
||||||
|
size,
|
||||||
|
placeholderLetter,
|
||||||
|
type = 'squared',
|
||||||
|
}: OwnProps) {
|
||||||
const noAvatarUrl = !isNonEmptyString(avatarUrl);
|
const noAvatarUrl = !isNonEmptyString(avatarUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledUserAvatar avatarUrl={avatarUrl} size={size}>
|
<StyledAvatar avatarUrl={avatarUrl} size={size} type={type}>
|
||||||
{noAvatarUrl && (
|
{noAvatarUrl && (
|
||||||
<StyledPlaceholderLetter size={size}>
|
<StyledPlaceholderLetter size={size}>
|
||||||
{placeholderLetter}
|
{placeholderLetter}
|
||||||
</StyledPlaceholderLetter>
|
</StyledPlaceholderLetter>
|
||||||
)}
|
)}
|
||||||
</StyledUserAvatar>
|
</StyledAvatar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2,39 +2,49 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|||||||
|
|
||||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
import { UserAvatar } from '../UserAvatar';
|
import { Avatar } from '../Avatar';
|
||||||
|
|
||||||
const meta: Meta<typeof UserAvatar> = {
|
const meta: Meta<typeof Avatar> = {
|
||||||
title: 'Users/UserAvatar',
|
title: 'Components/Common/Avatar',
|
||||||
component: UserAvatar,
|
component: Avatar,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof UserAvatar>;
|
type Story = StoryObj<typeof Avatar>;
|
||||||
|
|
||||||
const avatarUrl =
|
const avatarUrl =
|
||||||
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
|
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
|
||||||
|
|
||||||
export const Size40: Story = {
|
export const Rounded: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<UserAvatar avatarUrl={avatarUrl} size={40} placeholderLetter="L" />,
|
<Avatar
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
size={16}
|
||||||
|
placeholderLetter="L"
|
||||||
|
type="rounded"
|
||||||
|
/>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Size32: Story = {
|
export const Squared: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<UserAvatar avatarUrl={avatarUrl} size={32} placeholderLetter="L" />,
|
<Avatar
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
size={16}
|
||||||
|
placeholderLetter="L"
|
||||||
|
type="squared"
|
||||||
|
/>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Size16: Story = {
|
export const NoAvatarPictureRounded: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<UserAvatar avatarUrl={avatarUrl} size={16} placeholderLetter="L" />,
|
<Avatar avatarUrl={''} size={16} placeholderLetter="L" type="rounded" />,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoAvatarPicture: Story = {
|
export const NoAvatarPictureSquared: Story = {
|
||||||
render: getRenderWrapperForComponent(
|
render: getRenderWrapperForComponent(
|
||||||
<UserAvatar avatarUrl={''} size={16} placeholderLetter="L" />,
|
<Avatar avatarUrl={''} size={16} placeholderLetter="L" type="squared" />,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user