diff --git a/front/src/modules/comments/components/comments/CommentHeader.tsx b/front/src/modules/comments/components/comments/CommentHeader.tsx index 9db3677e1e..7eb6f6745e 100644 --- a/front/src/modules/comments/components/comments/CommentHeader.tsx +++ b/front/src/modules/comments/components/comments/CommentHeader.tsx @@ -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 ( - props.theme.spacing(2)}; `; diff --git a/front/src/modules/ui/components/menu/DropdownMenu.tsx b/front/src/modules/ui/components/menu/DropdownMenu.tsx new file mode 100644 index 0000000000..8b92bcf622 --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenu.tsx @@ -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); +`; diff --git a/front/src/modules/ui/components/menu/DropdownMenuButton.tsx b/front/src/modules/ui/components/menu/DropdownMenuButton.tsx new file mode 100644 index 0000000000..38573c6002 --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenuButton.tsx @@ -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}; +`; diff --git a/front/src/modules/ui/components/menu/DropdownMenuCheckableItem.tsx b/front/src/modules/ui/components/menu/DropdownMenuCheckableItem.tsx new file mode 100644 index 0000000000..5e9aca79fd --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenuCheckableItem.tsx @@ -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) { + function handleClick() { + onChange?.(!checked); + } + + return ( + + + + {children} + + + ); +} diff --git a/front/src/modules/ui/components/menu/DropdownMenuItem.tsx b/front/src/modules/ui/components/menu/DropdownMenuItem.tsx new file mode 100644 index 0000000000..b34c730295 --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenuItem.tsx @@ -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}; +`; diff --git a/front/src/modules/ui/components/menu/DropdownMenuItemContainer.tsx b/front/src/modules/ui/components/menu/DropdownMenuItemContainer.tsx new file mode 100644 index 0000000000..a9a109e84d --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenuItemContainer.tsx @@ -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; +`; diff --git a/front/src/modules/ui/components/menu/DropdownMenuSearch.tsx b/front/src/modules/ui/components/menu/DropdownMenuSearch.tsx new file mode 100644 index 0000000000..976f70d569 --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenuSearch.tsx @@ -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, +) { + return ( + + + + ); +} diff --git a/front/src/modules/ui/components/menu/DropdownMenuSelectableItem.tsx b/front/src/modules/ui/components/menu/DropdownMenuSelectableItem.tsx new file mode 100644 index 0000000000..a4d83ba6f6 --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenuSelectableItem.tsx @@ -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)` + ${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) { + return ( + + {children} + {selected && } + + ); +} diff --git a/front/src/modules/ui/components/menu/DropdownMenuSeparator.tsx b/front/src/modules/ui/components/menu/DropdownMenuSeparator.tsx new file mode 100644 index 0000000000..4262c2de1a --- /dev/null +++ b/front/src/modules/ui/components/menu/DropdownMenuSeparator.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +export const DropdownMenuSeparator = styled.div` + width: 100%; + height: 1px; + + background-color: ${(props) => props.theme.mediumBorder}; +`; diff --git a/front/src/modules/ui/components/menu/stories/DropdownMenu.stories.tsx b/front/src/modules/ui/components/menu/stories/DropdownMenu.stories.tsx new file mode 100644 index 0000000000..e26d9bb0a9 --- /dev/null +++ b/front/src/modules/ui/components/menu/stories/DropdownMenu.stories.tsx @@ -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 = { + title: 'Components/DropdownMenu', + component: DropdownMenu, +}; + +export default meta; +type Story = StoryObj; + +const FakeContentBelow = () => ( +
+ askjdlaksjdlaksjdlakjsdlkj lkajsldkjalskd jalksdj alksjd alskjd alksjd + alksjd laksjd askjdlaksjdlaksjdlakjsdlkj lkajsldkjalskd jalksdj alksjd +
+); + +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 = () => ( + <> + Company A + Company B + Company C + Person 2 + Company D + Person 1 + +); + +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( + + + , + ), +}; + +const DropdownMenuStoryWrapper = ({ + children, +}: React.PropsWithChildren) => ( + + + + {children} + + +); + +export const EmptyWithContentBelow: Story = { + render: getRenderWrapperForComponent( + + + , + ), +}; + +export const SimpleMenuItem: Story = { + render: getRenderWrapperForComponent( + + + + + , + ), +}; + +export const Search: Story = { + render: getRenderWrapperForComponent( + + + + + + + + + + + + , + ), +}; + +const FakeSelectableMenuItemList = () => { + const [selectedItem, setSelectedItem] = useState(null); + + return ( + <> + {mockSelectArray.map((item) => ( + setSelectedItem(item.id)} + > + {item.name} + + ))} + + ); +}; + +export const Button: Story = { + render: getRenderWrapperForComponent( + + + + + + + +
Create new
+
+
+ + + + +
+
+
, + ), +}; + +export const SelectableMenuItem: Story = { + render: getRenderWrapperForComponent( + + + + + + + + + + , + ), +}; + +const FakeSelectableMenuItemWithAvatarList = () => { + const [selectedItem, setSelectedItem] = useState(null); + + return ( + <> + {mockSelectArray.map((item) => ( + setSelectedItem(item.id)} + > + + {item.name} + + ))} + + ); +}; + +export const SelectableMenuItemWithAvatar: Story = { + render: getRenderWrapperForComponent( + + + + + + + + + + , + ), +}; + +const FakeCheckableMenuItemList = () => { + const [selectedItems, setSelectedItems] = useState([]); + + return ( + <> + {mockSelectArray.map((item) => ( + { + if (checked) { + setSelectedItems([...selectedItems, item.id]); + } else { + setSelectedItems(selectedItems.filter((id) => id !== item.id)); + } + }} + > + {item.name} + + ))} + + ); +}; + +export const CheckableMenuItem: Story = { + render: getRenderWrapperForComponent( + + + + + + + + + + , + ), +}; + +const FakeCheckableMenuItemWithAvatarList = () => { + const [selectedItems, setSelectedItems] = useState([]); + + return ( + <> + {mockSelectArray.map((item) => ( + { + if (checked) { + setSelectedItems([...selectedItems, item.id]); + } else { + setSelectedItems(selectedItems.filter((id) => id !== item.id)); + } + }} + > + + {item.name} + + ))} + + ); +}; + +export const CheckableMenuItemWithAvatar: Story = { + render: getRenderWrapperForComponent( + + + + + + + + + + , + ), +}; diff --git a/front/src/modules/ui/icons/index.ts b/front/src/modules/ui/icons/index.ts index 3be39c9e38..07d7cb2e49 100644 --- a/front/src/modules/ui/icons/index.ts +++ b/front/src/modules/ui/icons/index.ts @@ -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'; diff --git a/front/src/modules/ui/layout/styles/themes.ts b/front/src/modules/ui/layout/styles/themes.ts index c8c1feeb15..ac2ac99a43 100644 --- a/front/src/modules/ui/layout/styles/themes.ts +++ b/front/src/modules/ui/layout/styles/themes.ts @@ -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 }; diff --git a/front/src/modules/users/components/UserAvatar.tsx b/front/src/modules/users/components/Avatar.tsx similarity index 78% rename from front/src/modules/users/components/UserAvatar.tsx rename to front/src/modules/users/components/Avatar.tsx index 6c78846f73..5b8404934c 100644 --- a/front/src/modules/users/components/UserAvatar.tsx +++ b/front/src/modules/users/components/Avatar.tsx @@ -6,12 +6,13 @@ type OwnProps = { avatarUrl: string | null | undefined; size: number; placeholderLetter: string; + type?: 'squared' | 'rounded'; }; -export const StyledUserAvatar = styled.div>` +export const StyledAvatar = styled.div>` 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` 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 ( - + {noAvatarUrl && ( {placeholderLetter} )} - + ); } diff --git a/front/src/modules/users/components/__stories__/UserAvatar.stories.tsx b/front/src/modules/users/components/__stories__/Avatar.stories.tsx similarity index 54% rename from front/src/modules/users/components/__stories__/UserAvatar.stories.tsx rename to front/src/modules/users/components/__stories__/Avatar.stories.tsx index d524c63412..c933efd58a 100644 --- a/front/src/modules/users/components/__stories__/UserAvatar.stories.tsx +++ b/front/src/modules/users/components/__stories__/Avatar.stories.tsx @@ -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 = { - title: 'Users/UserAvatar', - component: UserAvatar, +const meta: Meta = { + title: 'Components/Common/Avatar', + component: Avatar, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; 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( - , + , ), }; -export const Size32: Story = { +export const Squared: Story = { render: getRenderWrapperForComponent( - , + , ), }; -export const Size16: Story = { +export const NoAvatarPictureRounded: Story = { render: getRenderWrapperForComponent( - , + , ), }; -export const NoAvatarPicture: Story = { +export const NoAvatarPictureSquared: Story = { render: getRenderWrapperForComponent( - , + , ), };