mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-29 15:25:45 +03:00
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:
parent
16e1b862d9
commit
3a719001de
@ -2,7 +2,7 @@ import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
|
||||
import { UserAvatar } from '@/users/components/UserAvatar';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyPastDateRelativeToNow,
|
||||
@ -75,7 +75,7 @@ export function CommentHeader({ comment }: OwnProps) {
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<UserAvatar
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
size={16}
|
||||
placeholderLetter={capitalizedFirstUsernameLetter}
|
||||
|
@ -15,5 +15,5 @@ export const EditableRelationCreateButton = styled.button`
|
||||
width: 100%;
|
||||
height: 31px;
|
||||
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 { IconSidebarRightCollapse } from './components/IconSidebarRightCollapse';
|
||||
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)',
|
||||
lightBorder: '#f5f5f5',
|
||||
mediumBorder: '#ebebeb',
|
||||
|
||||
clickableElementBackgroundHover: 'rgba(0, 0, 0, 0.04)',
|
||||
clickableElementBackgroundTransition: 'background 0.1s ease',
|
||||
@ -72,6 +73,8 @@ const lightThemeSpecific = {
|
||||
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
|
||||
|
||||
boxShadow: '0px 2px 4px 0px #0F0F0F0A',
|
||||
|
||||
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)',
|
||||
};
|
||||
|
||||
const darkThemeSpecific: typeof lightThemeSpecific = {
|
||||
@ -94,6 +97,7 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
|
||||
|
||||
primaryBorder: 'rgba(255, 255, 255, 0.08)',
|
||||
lightBorder: '#222222',
|
||||
mediumBorder: '#141414',
|
||||
|
||||
text100: '#ffffff',
|
||||
text80: '#cccccc',
|
||||
@ -113,13 +117,14 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
|
||||
blueHighTransparency: 'rgba(104, 149, 236, 0.03)',
|
||||
blueLowTransparency: 'rgba(104, 149, 236, 0.32)',
|
||||
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) =>
|
||||
css`
|
||||
background: ${props.theme.secondaryBackgroundTransparent};
|
||||
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) =>
|
||||
@ -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 darkTheme = { ...commonTheme, ...darkThemeSpecific };
|
||||
|
||||
|
@ -6,12 +6,13 @@ type OwnProps = {
|
||||
avatarUrl: string | null | undefined;
|
||||
size: number;
|
||||
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;
|
||||
height: ${(props) => props.size}px;
|
||||
border-radius: 50%;
|
||||
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
|
||||
background-image: url(${(props) =>
|
||||
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
|
||||
background-color: ${(props) =>
|
||||
@ -46,16 +47,21 @@ export const StyledPlaceholderLetter = styled.div<StyledPlaceholderLetterProps>`
|
||||
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);
|
||||
|
||||
return (
|
||||
<StyledUserAvatar avatarUrl={avatarUrl} size={size}>
|
||||
<StyledAvatar avatarUrl={avatarUrl} size={size} type={type}>
|
||||
{noAvatarUrl && (
|
||||
<StyledPlaceholderLetter size={size}>
|
||||
{placeholderLetter}
|
||||
</StyledPlaceholderLetter>
|
||||
)}
|
||||
</StyledUserAvatar>
|
||||
</StyledAvatar>
|
||||
);
|
||||
}
|
@ -2,39 +2,49 @@ import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { UserAvatar } from '../UserAvatar';
|
||||
import { Avatar } from '../Avatar';
|
||||
|
||||
const meta: Meta<typeof UserAvatar> = {
|
||||
title: 'Users/UserAvatar',
|
||||
component: UserAvatar,
|
||||
const meta: Meta<typeof Avatar> = {
|
||||
title: 'Components/Common/Avatar',
|
||||
component: Avatar,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof UserAvatar>;
|
||||
type Story = StoryObj<typeof Avatar>;
|
||||
|
||||
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';
|
||||
|
||||
export const Size40: Story = {
|
||||
export const Rounded: Story = {
|
||||
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(
|
||||
<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(
|
||||
<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(
|
||||
<UserAvatar avatarUrl={''} size={16} placeholderLetter="L" />,
|
||||
<Avatar avatarUrl={''} size={16} placeholderLetter="L" type="squared" />,
|
||||
),
|
||||
};
|
Loading…
Reference in New Issue
Block a user