Migrate to twenty-ui - navigation/menu-item (#8213)

This PR was created by [GitStart](https://gitstart.com/) to address the
requirements from this ticket:
[TWNTY-7536](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7536).

 --- 

### Description

Migrate all menu items components to twenty ui and update imports.

```typescript
MenuItem
MenuItemAvata
MenuItemCommand
MenuItemCommandHotKeys
MenuItemDraggable
MenuItemMultiSelect
MenuItemMultiSelectAvatar
MenuItemMultiSelectTag
MenuItemNavigate
MenuItemSelect
MenuItemSelectAvatar
MenuItemSelectColor
MenuItemSelectTag
MenuItemSuggestion
MenuItemToggle
```

\
Also migrate all other dependent components and utilities like
`Checkbox` & `Toggle`\
\
Fixes twentyhq/private-issues#82

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
gitstart-app[bot]
2024-11-07 16:51:39 +00:00
committed by GitHub
parent f9a136ab6d
commit 6264d509bd
106 changed files with 326 additions and 256 deletions

View File

@ -1,89 +0,0 @@
import { useTheme } from '@emotion/react';
import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react';
import {
IconChevronRight,
IconComponent,
LightIconButtonGroup,
LightIconButtonProps,
} from 'twenty-ui';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
StyledHoverableMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
export type MenuItemIconButton = {
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
Icon: IconComponent;
accent?: LightIconButtonProps['accent'];
onClick?: (event: MouseEvent<any>) => void;
};
export type MenuItemProps = {
accent?: MenuItemAccent;
className?: string;
iconButtons?: MenuItemIconButton[];
isIconDisplayedOnHoverOnly?: boolean;
isTooltipOpen?: boolean;
LeftIcon?: IconComponent | null;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
testId?: string;
text: ReactNode;
hasSubMenu?: boolean;
};
export const MenuItem = ({
accent = 'default',
className,
iconButtons,
isIconDisplayedOnHoverOnly = true,
LeftIcon,
onClick,
onMouseEnter,
onMouseLeave,
testId,
text,
hasSubMenu = false,
}: MenuItemProps) => {
const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
const handleMenuItemClick = (event: MouseEvent<HTMLDivElement>) => {
if (!onClick) return;
event.preventDefault();
event.stopPropagation();
onClick?.(event);
};
return (
<StyledHoverableMenuItemBase
data-testid={testId ?? undefined}
onClick={handleMenuItemClick}
className={className}
accent={accent}
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
</StyledMenuItemLeftContent>
<div className="hoverable-buttons">
{showIconButtons && (
<LightIconButtonGroup iconButtons={iconButtons} size="small" />
)}
</div>
{hasSubMenu && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledHoverableMenuItemBase>
);
};

View File

@ -1,105 +0,0 @@
import { useTheme } from '@emotion/react';
import { FunctionComponent, MouseEvent, ReactElement } from 'react';
import {
Avatar,
AvatarProps,
IconChevronRight,
IconComponent,
LightIconButtonGroup,
LightIconButtonProps,
OverflowingTextWithTooltip,
isDefined,
} from 'twenty-ui';
import {
StyledHoverableMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
export type MenuItemIconButton = {
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
Icon: IconComponent;
accent?: LightIconButtonProps['accent'];
onClick?: (event: MouseEvent<any>) => void;
};
export type MenuItemAvatarProps = {
accent?: MenuItemAccent;
className?: string;
iconButtons?: MenuItemIconButton[];
isIconDisplayedOnHoverOnly?: boolean;
isTooltipOpen?: boolean;
avatar?: Pick<
AvatarProps,
'avatarUrl' | 'placeholderColorSeed' | 'placeholder' | 'size' | 'type'
> | null;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
testId?: string;
text: string;
hasSubMenu?: boolean;
};
// TODO: merge with MenuItem
export const MenuItemAvatar = ({
accent = 'default',
className,
iconButtons,
isIconDisplayedOnHoverOnly = true,
onClick,
onMouseEnter,
onMouseLeave,
testId,
avatar,
hasSubMenu = false,
text,
}: MenuItemAvatarProps) => {
const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
const handleMenuItemClick = (event: MouseEvent<HTMLDivElement>) => {
if (!onClick) return;
event.preventDefault();
event.stopPropagation();
onClick?.(event);
};
return (
<StyledHoverableMenuItemBase
data-testid={testId ?? undefined}
onClick={handleMenuItemClick}
className={className}
accent={accent}
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<StyledMenuItemLeftContent>
{isDefined(avatar) && (
<Avatar
placeholder={avatar.placeholder}
avatarUrl={avatar.avatarUrl}
placeholderColorSeed={avatar.placeholderColorSeed}
size={avatar.size}
type={avatar.type}
/>
)}
<OverflowingTextWithTooltip text={text ?? ''} />
</StyledMenuItemLeftContent>
<div className="hoverable-buttons">
{showIconButtons && (
<LightIconButtonGroup iconButtons={iconButtons} size="small" />
)}
</div>
{hasSubMenu && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledHoverableMenuItemBase>
);
};

View File

@ -1,114 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import {
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemCommandHotKeys } from './MenuItemCommandHotKeys';
const StyledMenuItemLabelText = styled(StyledMenuItemLabel)`
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledBigIconContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: row;
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledMenuItemCommandContainer = styled.div<{ isSelected?: boolean }>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
background: ${({ isSelected, theme }) =>
isSelected
? theme.background.transparent.light
: theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
padding: var(--vertical-padding) var(--horizontal-padding);
position: relative;
transition: all 150ms ease;
transition-property: none;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
&[data-selected='true'] {
background: ${({ theme }) => theme.background.tertiary};
}
&[data-disabled='true'] {
color: ${({ theme }) => theme.font.color.light};
cursor: not-allowed;
}
svg {
height: 16px;
width: 16px;
}
`;
export type MenuItemCommandProps = {
LeftIcon?: IconComponent;
text: string;
firstHotKey?: string;
secondHotKey?: string;
className?: string;
isSelected?: boolean;
onClick?: () => void;
};
export const MenuItemCommand = ({
LeftIcon,
text,
firstHotKey,
secondHotKey,
className,
isSelected,
onClick,
}: MenuItemCommandProps) => {
const theme = useTheme();
const isMobile = useIsMobile();
return (
<StyledMenuItemCommandContainer
onClick={onClick}
className={className}
isSelected={isSelected}
>
<StyledMenuItemLeftContent>
{LeftIcon && (
<StyledBigIconContainer>
<LeftIcon size={theme.icon.size.sm} />
</StyledBigIconContainer>
)}
<StyledMenuItemLabelText hasLeftIcon={!!LeftIcon}>
{text}
</StyledMenuItemLabelText>
</StyledMenuItemLeftContent>
{!isMobile && (
<MenuItemCommandHotKeys
firstHotKey={firstHotKey}
secondHotKey={secondHotKey}
/>
)}
</StyledMenuItemCommandContainer>
);
};

View File

@ -1,62 +0,0 @@
import styled from '@emotion/styled';
const StyledCommandTextContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
`;
const StyledCommandText = styled.div`
color: ${({ theme }) => theme.font.color.light};
padding-bottom: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(1)};
white-space: nowrap;
`;
const StyledCommandKey = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.strong};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) => theme.boxShadow.underline};
display: flex;
flex-direction: column;
height: ${({ theme }) => theme.spacing(5)};
height: 18px;
justify-content: center;
text-align: center;
width: ${({ theme }) => theme.spacing(4)};
`;
export type MenuItemCommandHotKeysProps = {
firstHotKey?: string;
joinLabel?: string;
secondHotKey?: string;
};
export const MenuItemCommandHotKeys = ({
firstHotKey,
secondHotKey,
joinLabel = 'then',
}: MenuItemCommandHotKeysProps) => {
return (
<StyledCommandText>
{firstHotKey && (
<StyledCommandTextContainer>
<StyledCommandKey>{firstHotKey}</StyledCommandKey>
{secondHotKey && (
<>
{joinLabel}
<StyledCommandKey>{secondHotKey}</StyledCommandKey>
</>
)}
</StyledCommandTextContainer>
)}
</StyledCommandText>
);
};

View File

@ -1,65 +0,0 @@
import { IconComponent, LightIconButtonGroup } from 'twenty-ui';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
import { MenuItemIconButton } from './MenuItem';
import { ReactNode } from 'react';
export type MenuItemDraggableProps = {
LeftIcon?: IconComponent | undefined;
accent?: MenuItemAccent;
iconButtons?: MenuItemIconButton[];
isTooltipOpen?: boolean;
onClick?: () => void;
text: ReactNode;
className?: string;
isIconDisplayedOnHoverOnly?: boolean;
showGrip?: boolean;
isDragDisabled?: boolean;
isHoverDisabled?: boolean;
};
export const MenuItemDraggable = ({
LeftIcon,
accent = 'default',
iconButtons,
onClick,
text,
isDragDisabled = false,
className,
isIconDisplayedOnHoverOnly = true,
showGrip = false,
}: MenuItemDraggableProps) => {
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
const cursorType = showGrip
? isDragDisabled
? 'not-allowed'
: 'drag'
: 'default';
return (
<StyledHoverableMenuItemBase
onClick={onClick}
accent={accent}
className={className}
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
cursor={cursorType}
>
<MenuItemLeftContent
LeftIcon={LeftIcon}
text={text}
isDisabled={isDragDisabled}
showGrip={showGrip}
/>
{showIconButtons && (
<LightIconButtonGroup
className="hoverable-buttons"
iconButtons={iconButtons}
/>
)}
</StyledHoverableMenuItemBase>
);
};

View File

@ -1,54 +0,0 @@
import styled from '@emotion/styled';
import { Checkbox, IconComponent, Tag, ThemeColor } from 'twenty-ui';
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
const StyledLeftContentWithCheckboxContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
type MenuItemMultiSelectProps = {
color?: ThemeColor;
LeftIcon?: IconComponent;
selected: boolean;
isKeySelected?: boolean;
text: string;
className: string;
onSelectChange?: (selected: boolean) => void;
};
export const MenuItemMultiSelect = ({
color,
LeftIcon,
text,
selected,
isKeySelected,
className,
onSelectChange,
}: MenuItemMultiSelectProps) => {
const handleOnClick = () => {
onSelectChange?.(!selected);
};
return (
<StyledMenuItemBase
isKeySelected={isKeySelected}
className={className}
onClick={handleOnClick}
>
<StyledLeftContentWithCheckboxContainer>
<Checkbox checked={selected} />
{color ? (
<Tag color={color} text={text} Icon={LeftIcon} />
) : (
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
)}
</StyledLeftContentWithCheckboxContainer>
</StyledMenuItemBase>
);
};

View File

@ -1,57 +0,0 @@
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { Checkbox, OverflowingTextWithTooltip } from 'twenty-ui';
import {
StyledMenuItemBase,
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
const StyledLeftContentWithCheckboxContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
type MenuItemMultiSelectAvatarProps = {
avatar?: ReactNode;
selected: boolean;
isKeySelected?: boolean;
text?: string;
className?: string;
onSelectChange?: (selected: boolean) => void;
};
export const MenuItemMultiSelectAvatar = ({
avatar,
text,
selected,
className,
isKeySelected,
onSelectChange,
}: MenuItemMultiSelectAvatarProps) => {
const handleOnClick = () => {
onSelectChange?.(!selected);
};
return (
<StyledMenuItemBase
className={className}
onClick={handleOnClick}
isKeySelected={isKeySelected}
>
<StyledLeftContentWithCheckboxContainer>
<Checkbox checked={selected} />
<StyledMenuItemLeftContent>
{avatar}
<StyledMenuItemLabel hasLeftIcon={!!avatar}>
<OverflowingTextWithTooltip text={text} />
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
</StyledLeftContentWithCheckboxContainer>
</StyledMenuItemBase>
);
};

View File

@ -1,47 +0,0 @@
import {
Checkbox,
CheckboxShape,
CheckboxSize,
Tag,
ThemeColor,
} from 'twenty-ui';
import {
StyledMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
type MenuItemMultiSelectTagProps = {
selected: boolean;
className?: string;
isKeySelected?: boolean;
onClick?: () => void;
color: ThemeColor;
text: string;
};
export const MenuItemMultiSelectTag = ({
color,
selected,
className,
onClick,
isKeySelected,
text,
}: MenuItemMultiSelectTagProps) => {
return (
<StyledMenuItemBase
isKeySelected={isKeySelected}
onClick={onClick}
className={className}
>
<Checkbox
size={CheckboxSize.Small}
shape={CheckboxShape.Squared}
checked={selected}
/>
<StyledMenuItemLeftContent>
<Tag color={color} text={text} />
</StyledMenuItemLeftContent>
</StyledMenuItemBase>
);
};

View File

@ -1,36 +0,0 @@
import { useTheme } from '@emotion/react';
import { IconChevronRight, IconComponent } from 'twenty-ui';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
StyledMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
export type MenuItemNavigateProps = {
LeftIcon?: IconComponent;
text: string;
onClick?: () => void;
className?: string;
};
export const MenuItemNavigate = ({
LeftIcon,
text,
className,
onClick,
}: MenuItemNavigateProps) => {
const theme = useTheme();
return (
<StyledMenuItemBase onClick={onClick} className={className}>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
</StyledMenuItemLeftContent>
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
</StyledMenuItemBase>
);
};

View File

@ -1,81 +0,0 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck, IconChevronRight, IconComponent } from 'twenty-ui';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
selected: boolean;
disabled?: boolean;
hovered?: boolean;
}>`
${({ theme, selected, disabled, hovered }) => {
if (selected) {
return css`
background: ${theme.background.transparent.light};
&:hover {
background: ${theme.background.transparent.medium};
}
`;
} else if (disabled === true) {
return css`
background: inherit;
&:hover {
background: inherit;
}
color: ${theme.font.color.tertiary};
cursor: default;
`;
} else if (hovered === true) {
return css`
background: ${theme.background.transparent.light};
`;
}
}}
`;
type MenuItemSelectProps = {
LeftIcon: IconComponent | null | undefined;
selected: boolean;
text: string;
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
hasSubMenu?: boolean;
};
export const MenuItemSelect = ({
LeftIcon,
text,
selected,
className,
onClick,
disabled,
hovered,
hasSubMenu = false,
}: MenuItemSelectProps) => {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
{selected && <IconCheck size={theme.icon.size.md} />}
{hasSubMenu && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledMenuItemSelect>
);
};

View File

@ -1,53 +0,0 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import { IconCheck, OverflowingTextWithTooltip } from 'twenty-ui';
import {
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { StyledMenuItemSelect } from './MenuItemSelect';
type MenuItemSelectAvatarProps = {
avatar?: ReactNode;
selected: boolean;
text: string;
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
testId?: string;
};
export const MenuItemSelectAvatar = ({
avatar,
text,
selected,
className,
onClick,
disabled,
hovered,
testId,
}: MenuItemSelectAvatarProps) => {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
data-testid={testId}
>
<StyledMenuItemLeftContent>
{avatar}
<StyledMenuItemLabel hasLeftIcon={!!avatar}>
<OverflowingTextWithTooltip text={text} />
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
{selected && <IconCheck size={theme.icon.size.md} />}
</StyledMenuItemSelect>
);
};

View File

@ -1,67 +0,0 @@
import { useTheme } from '@emotion/react';
import {
ColorSample,
ColorSampleVariant,
IconCheck,
ThemeColor,
} from 'twenty-ui';
import {
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { StyledMenuItemSelect } from './MenuItemSelect';
type MenuItemSelectColorProps = {
selected: boolean;
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
color: ThemeColor;
variant?: ColorSampleVariant;
};
export const colorLabels: Record<ThemeColor, string> = {
green: 'Green',
turquoise: 'Turquoise',
sky: 'Sky',
blue: 'Blue',
purple: 'Purple',
pink: 'Pink',
red: 'Red',
orange: 'Orange',
yellow: 'Yellow',
gray: 'Gray',
};
export const MenuItemSelectColor = ({
color,
selected,
className,
onClick,
disabled,
hovered,
variant = 'default',
}: MenuItemSelectColorProps) => {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
>
<StyledMenuItemLeftContent>
<ColorSample colorName={color} variant={variant} />
<StyledMenuItemLabel hasLeftIcon={true}>
{colorLabels[color]}
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
{selected && <IconCheck size={theme.icon.size.md} />}
</StyledMenuItemSelect>
);
};

View File

@ -1,42 +0,0 @@
import { useTheme } from '@emotion/react';
import { IconCheck, Tag, ThemeColor } from 'twenty-ui';
import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase';
import { StyledMenuItemSelect } from './MenuItemSelect';
type MenuItemSelectTagProps = {
selected: boolean;
isKeySelected?: boolean;
className?: string;
onClick?: () => void;
color: ThemeColor | 'transparent';
text: string;
variant?: 'solid' | 'outline';
};
export const MenuItemSelectTag = ({
color,
selected,
isKeySelected,
className,
onClick,
text,
variant = 'solid',
}: MenuItemSelectTagProps) => {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
isKeySelected={isKeySelected}
>
<StyledMenuItemLeftContent>
<Tag variant={variant} color={color} text={text} />
</StyledMenuItemLeftContent>
{selected && <IconCheck size={theme.icon.size.sm} />}
</StyledMenuItemSelect>
);
};

View File

@ -1,77 +0,0 @@
import styled from '@emotion/styled';
import { MouseEvent } from 'react';
import { HOVER_BACKGROUND, IconComponent } from 'twenty-ui';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase';
export type MenuItemSuggestionProps = {
LeftIcon?: IconComponent | null;
text: string;
selected?: boolean;
className?: string;
onClick?: (event: MouseEvent<HTMLLIElement>) => void;
};
const StyledSuggestionMenuItem = styled.li<{
selected?: boolean;
}>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
height: calc(32px - 2 * var(--vertical-padding));
justify-content: space-between;
padding: var(--vertical-padding) var(--horizontal-padding);
background: ${({ selected, theme }) =>
selected ? theme.background.transparent.medium : ''};
${HOVER_BACKGROUND};
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
`;
export const MenuItemSuggestion = ({
LeftIcon,
text,
className,
selected,
onClick,
}: MenuItemSuggestionProps) => {
const handleMenuItemClick = (event: MouseEvent<HTMLLIElement>) => {
if (!onClick) return;
event.preventDefault();
event.stopPropagation();
onClick?.(event);
};
return (
<StyledSuggestionMenuItem
onClick={handleMenuItemClick}
className={className}
selected={selected}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
</StyledMenuItemLeftContent>
</StyledSuggestionMenuItem>
);
};

View File

@ -1,38 +0,0 @@
import { IconComponent, Toggle, ToggleSize } from 'twenty-ui';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
StyledMenuItemBase,
StyledMenuItemRightContent,
} from '../internals/components/StyledMenuItemBase';
type MenuItemToggleProps = {
LeftIcon?: IconComponent;
toggled: boolean;
text: string;
className?: string;
onToggleChange?: (toggled: boolean) => void;
toggleSize?: ToggleSize;
};
export const MenuItemToggle = ({
LeftIcon,
text,
toggled,
className,
onToggleChange,
toggleSize,
}: MenuItemToggleProps) => {
return (
<StyledMenuItemBase className={className}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<StyledMenuItemRightContent>
<Toggle
value={toggled}
onChange={onToggleChange}
toggleSize={toggleSize}
/>
</StyledMenuItemRightContent>
</StyledMenuItemBase>
);
};

View File

@ -1,3 +1,5 @@
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useTheme } from '@emotion/react';
import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react';
import {
@ -6,16 +8,11 @@ import {
IconDotsVertical,
LightIconButton,
LightIconButtonProps,
} from 'twenty-ui';
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
MenuItemAccent,
MenuItemLeftContent,
StyledHoverableMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
} from 'twenty-ui';
export type MenuItemIconButton = {
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;

View File

@ -1,112 +0,0 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogStory,
ComponentDecorator,
IconBell,
} from 'twenty-ui';
import { MenuItemAccent } from '../../types/MenuItemAccent';
import { MenuItem } from '../MenuItem';
const meta: Meta<typeof MenuItem> = {
title: 'UI/Navigation/MenuItem/MenuItem',
component: MenuItem,
};
export default meta;
type Story = StoryObj<typeof MenuItem>;
export const Default: Story = {
args: {
text: 'Menu item text',
LeftIcon: IconBell,
accent: 'default',
iconButtons: [
{ Icon: IconBell, onClick: action('Clicked') },
{ Icon: IconBell, onClick: action('Clicked') },
],
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItem> = {
args: { ...Default.args },
argTypes: {
accent: { control: false },
className: { control: false },
iconButtons: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'accents',
values: ['default', 'danger'] satisfies MenuItemAccent[],
props: (accent: MenuItemAccent) => ({ accent }),
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
{
name: 'iconButtons',
values: ['no icon button', 'two icon buttons'],
props: (choice: string) => {
switch (choice) {
case 'no icon button': {
return {
iconButtons: [],
};
}
case 'two icon buttons': {
return {
iconButtons: [
{
Icon: IconBell,
onClick: action('Clicked on first icon button'),
},
{
Icon: IconBell,
onClick: action('Clicked on second icon button'),
},
],
};
}
default:
return {};
}
},
},
],
options: {
elementContainer: {
width: 200,
},
},
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,96 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogStory,
ComponentDecorator,
IconBell,
} from 'twenty-ui';
import { MenuItemCommand } from '../MenuItemCommand';
const meta: Meta<typeof MenuItemCommand> = {
title: 'UI/Navigation/MenuItem/MenuItemCommand',
component: MenuItemCommand,
};
export default meta;
type Story = StoryObj<typeof MenuItemCommand>;
export const Default: Story = {
args: {
text: 'First option',
firstHotKey: '⌘',
secondHotKey: '1',
},
render: (props) => (
<MenuItemCommand
LeftIcon={props.LeftIcon}
text={props.text}
firstHotKey={props.firstHotKey}
secondHotKey={props.secondHotKey}
className={props.className}
onClick={props.onClick}
isSelected={false}
></MenuItemCommand>
),
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemCommand> = {
args: {
text: 'Menu item',
firstHotKey: '⌘',
secondHotKey: '1',
},
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
],
options: {
elementContainer: {
width: 200,
},
},
},
},
render: (props) => (
<MenuItemCommand
LeftIcon={props.LeftIcon}
text={props.text}
firstHotKey={props.firstHotKey}
secondHotKey={props.secondHotKey}
className={props.className}
onClick={props.onClick}
isSelected={false}
></MenuItemCommand>
),
decorators: [CatalogDecorator],
};

View File

@ -1,114 +0,0 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
ComponentDecorator,
IconBell,
IconMinus,
} from 'twenty-ui';
import { MenuItemAccent } from '../../types/MenuItemAccent';
import { MenuItemDraggable } from '../MenuItemDraggable';
const meta: Meta<typeof MenuItemDraggable> = {
title: 'ui/Navigation/MenuItem/MenuItemDraggable',
component: MenuItemDraggable,
};
export default meta;
type Story = StoryObj<typeof MenuItemDraggable>;
export const Default: Story = {
args: {
LeftIcon: IconBell,
accent: 'default',
iconButtons: [{ Icon: IconMinus, onClick: action('Clicked') }],
onClick: action('Clicked'),
text: 'Menu item draggable',
isDragDisabled: false,
},
decorators: [ComponentDecorator],
};
export const Catalog: Story = {
args: { ...Default.args },
argTypes: {
accent: { control: false },
iconButtons: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'] },
catalog: {
dimensions: [
{
name: 'isDragDisabled',
values: [true, false],
props: (isDragDisabled: boolean) => ({
isDragDisabled: isDragDisabled,
}),
labels: (isDragDisabled: boolean) =>
isDragDisabled ? 'Without drag icon' : 'With drag icon',
},
{
name: 'accents',
values: ['default', 'danger', 'placeholder'] as MenuItemAccent[],
props: (accent: MenuItemAccent) => ({ accent }),
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
{
name: 'iconButtons',
values: ['no icon button', 'minus icon buttons'],
props: (choice: string) => {
switch (choice) {
case 'no icon button': {
return {
iconButtons: [],
};
}
case 'minus icon buttons': {
return {
iconButtons: [
{
Icon: IconMinus,
onClick: action('Clicked on minus icon button'),
},
],
};
}
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};
export const Grip: Story = {
args: { ...Default.args, showGrip: true, isDragDisabled: false },
};
export const HoverDisabled: Story = {
args: { ...Default.args, isHoverDisabled: true },
};

View File

@ -1,77 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ComponentDecorator,
IconBell,
} from 'twenty-ui';
import { MenuItemMultiSelect } from '../MenuItemMultiSelect';
const meta: Meta<typeof MenuItemMultiSelect> = {
title: 'UI/Navigation/MenuItem/MenuItemMultiSelect',
component: MenuItemMultiSelect,
};
export default meta;
type Story = StoryObj<typeof MenuItemMultiSelect>;
export const Default: Story = {
args: {
text: 'First option',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemMultiSelect> = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'selected',
values: [true, false],
props: (selected: boolean) => ({ selected }),
labels: (selected: boolean) =>
selected ? 'Selected' : 'Not selected',
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,84 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
Avatar,
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ComponentDecorator,
} from 'twenty-ui';
import { avatarUrl } from '~/testing/mock-data/users';
import { MenuItemMultiSelectAvatar } from '../MenuItemMultiSelectAvatar';
const meta: Meta<typeof MenuItemMultiSelectAvatar> = {
title: 'UI/Navigation/MenuItem/MenuItemMultiSelectAvatar',
component: MenuItemMultiSelectAvatar,
};
export default meta;
type Story = StoryObj<typeof MenuItemMultiSelectAvatar>;
export const Default: Story = {
args: {
text: 'First option',
avatar: <Avatar avatarUrl={avatarUrl} placeholder="L" />,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemMultiSelectAvatar> = {
args: { text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withAvatar',
values: [true, false],
props: (withAvatar: boolean) => ({
avatar: withAvatar ? (
<Avatar avatarUrl={avatarUrl} placeholder="L" />
) : (
<Avatar avatarUrl={''} placeholder="L" />
),
}),
labels: (withAvatar: boolean) =>
withAvatar ? 'With avatar' : 'Without avatar',
},
{
name: 'selected',
values: [true, false],
props: (selected: boolean) => ({ selected }),
labels: (selected: boolean) =>
selected ? 'Selected' : 'Not selected',
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,70 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ComponentDecorator,
IconBell,
} from 'twenty-ui';
import { MenuItemNavigate } from '../MenuItemNavigate';
const meta: Meta<typeof MenuItemNavigate> = {
title: 'UI/Navigation/MenuItem/MenuItemNavigate',
component: MenuItemNavigate,
};
export default meta;
type Story = StoryObj<typeof MenuItemNavigate>;
export const Default: Story = {
args: {
text: 'First option',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemNavigate> = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,78 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ComponentDecorator,
IconBell,
} from 'twenty-ui';
import { MenuItemSelect } from '../MenuItemSelect';
const meta: Meta<typeof MenuItemSelect> = {
title: 'UI/Navigation/MenuItem/MenuItemSelect',
component: MenuItemSelect,
};
export default meta;
type Story = StoryObj<typeof MenuItemSelect>;
export const Default: Story = {
args: {
text: 'First option',
LeftIcon: IconBell,
},
argTypes: {
className: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemSelect> = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'states',
values: ['default', 'hover', 'selected', 'hover+selected'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: 'hover' };
case 'selected':
return { selected: true };
case 'hover+selected':
return { className: 'hover', selected: true };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,93 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
Avatar,
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ComponentDecorator,
} from 'twenty-ui';
import { avatarUrl } from '~/testing/mock-data/users';
import { MenuItemSelectAvatar } from '../MenuItemSelectAvatar';
const meta: Meta<typeof MenuItemSelectAvatar> = {
title: 'UI/Navigation/MenuItem/MenuItemSelectAvatar',
component: MenuItemSelectAvatar,
};
export default meta;
type Story = StoryObj<typeof MenuItemSelectAvatar>;
export const Default: Story = {
args: {
text: 'First option',
avatar: <Avatar avatarUrl={avatarUrl} placeholder="L" />,
},
argTypes: {
className: { control: false },
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemSelectAvatar> = {
args: { text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withAvatar',
values: [true, false],
props: (withAvatar: boolean) => ({
avatar: withAvatar ? (
<Avatar avatarUrl={avatarUrl} placeholder="L" />
) : (
<Avatar avatarUrl={''} placeholder="L" />
),
}),
labels: (withAvatar: boolean) =>
withAvatar ? 'With avatar' : 'Without avatar',
},
{
name: 'states',
values: [
'default',
'hover',
'disabled',
'selected',
'hover+selected',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: 'hover' };
case 'disabled':
return { disabled: true };
case 'selected':
return { selected: true };
case 'hover+selected':
return { className: 'hover', selected: true };
default:
return {};
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,84 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ColorSampleVariant,
ComponentDecorator,
MAIN_COLOR_NAMES,
ThemeColor,
} from 'twenty-ui';
import { MenuItemSelectColor } from '../MenuItemSelectColor';
const meta: Meta<typeof MenuItemSelectColor> = {
title: 'UI/Navigation/MenuItem/MenuItemSelectColor',
component: MenuItemSelectColor,
};
export default meta;
type Story = StoryObj<typeof MenuItemSelectColor>;
export const Default: Story = {
args: { color: 'green' },
argTypes: { className: { control: false } },
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemSelectColor> = {
argTypes: { className: { control: false } },
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'color',
values: MAIN_COLOR_NAMES,
props: (color: ThemeColor) => ({ color }),
labels: (color: ThemeColor) => color,
},
{
name: 'states',
values: [
'default',
'hover',
'disabled',
'selected',
'hover+selected',
],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: 'hover' };
case 'disabled':
return { disabled: true };
case 'selected':
return { selected: true };
case 'hover+selected':
return { className: 'hover', selected: true };
default:
return {};
}
},
},
{
name: 'variant',
values: ['default', 'pipeline'],
props: (variant: ColorSampleVariant) => ({ variant }),
labels: (variant: ColorSampleVariant) => variant,
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,94 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ComponentDecorator,
ThemeColor,
} from 'twenty-ui';
import { MenuItemSelectTag } from '../MenuItemSelectTag';
const meta: Meta<typeof MenuItemSelectTag> = {
title: 'UI/Navigation/MenuItem/MenuItemSelectTag',
component: MenuItemSelectTag,
};
export default meta;
type Story = StoryObj<typeof MenuItemSelectTag>;
export const Default: Story = {
args: {
selected: false,
onClick: undefined,
text: 'Item A',
},
argTypes: {
selected: {
control: 'boolean',
defaultValue: false,
},
onClick: {
control: false,
},
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemSelectTag> = {
args: {
text: 'Item A',
},
parameters: {
pseudo: {
hover: ['.hover'],
active: ['.pressed'],
focus: ['.focus'],
},
catalog: {
dimensions: [
{
name: 'color',
values: [
'green',
'turquoise',
'sky',
'blue',
'purple',
'pink',
'red',
'orange',
'yellow',
'gray',
],
props: (color: ThemeColor) => ({ color }),
},
{
name: 'states',
values: ['default', 'hover', 'selected', 'hover+selected'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: 'hover' };
case 'selected':
return { selected: true };
case 'hover+selected':
return { className: 'hover', selected: true };
default:
return {};
}
},
labels: (state: string) => `State: ${state}`,
},
] as CatalogDimension[],
options: {
elementContainer: { width: 200 },
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,76 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
CatalogStory,
ComponentDecorator,
IconBell,
} from 'twenty-ui';
import { MenuItemToggle } from '../MenuItemToggle';
const meta: Meta<typeof MenuItemToggle> = {
title: 'UI/Navigation/MenuItem/MenuItemToggle',
component: MenuItemToggle,
};
export default meta;
type Story = StoryObj<typeof MenuItemToggle>;
export const Default: Story = {
args: {
text: 'First option',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemToggle> = {
args: { LeftIcon: IconBell, text: 'Menu item' },
argTypes: {
className: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'withIcon',
values: [true, false],
props: (withIcon: boolean) => ({
LeftIcon: withIcon ? IconBell : undefined,
}),
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'toggled',
values: [true, false],
props: (toggled: boolean) => ({ toggled }),
labels: (toggled: boolean) => (toggled ? 'Toggled' : 'Not toggled'),
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: state };
default:
return {};
}
},
},
] satisfies CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} satisfies CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -1,69 +0,0 @@
import { useTheme } from '@emotion/react';
import { isString } from '@sniptt/guards';
import { ReactNode } from 'react';
import {
IconComponent,
IconGripVertical,
OverflowingTextWithTooltip,
} from 'twenty-ui';
import {
StyledDraggableItem,
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from './StyledMenuItemBase';
type MenuItemLeftContentProps = {
className?: string;
LeftIcon: IconComponent | null | undefined;
showGrip?: boolean;
isDisabled?: boolean;
text: ReactNode;
};
export const MenuItemLeftContent = ({
className,
LeftIcon,
text,
showGrip = false,
isDisabled = false,
}: MenuItemLeftContentProps) => {
const theme = useTheme();
return (
<StyledMenuItemLeftContent className={className}>
{showGrip &&
(isDisabled ? (
<StyledDraggableItem>
<IconGripVertical
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={
isDisabled
? theme.font.color.extraLight
: theme.font.color.light
}
/>
</StyledDraggableItem>
) : (
<StyledDraggableItem>
<IconGripVertical
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={
isDisabled
? theme.font.color.extraLight
: theme.font.color.light
}
/>
</StyledDraggableItem>
))}
{LeftIcon && (
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
<StyledMenuItemLabel hasLeftIcon={!!LeftIcon}>
{isString(text) ? <OverflowingTextWithTooltip text={text} /> : text}
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
);
};

View File

@ -1,149 +0,0 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { HOVER_BACKGROUND } from 'twenty-ui';
import { MenuItemAccent } from '../../types/MenuItemAccent';
export type MenuItemBaseProps = {
accent?: MenuItemAccent;
isKeySelected?: boolean;
isHoverBackgroundDisabled?: boolean;
hovered?: boolean;
};
export const StyledMenuItemBase = styled.div<MenuItemBaseProps>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
height: calc(32px - 2 * var(--vertical-padding));
justify-content: space-between;
padding: var(--vertical-padding) var(--horizontal-padding);
${({ theme, isKeySelected }) =>
isKeySelected ? `background: ${theme.background.transparent.light};` : ''}
${({ isHoverBackgroundDisabled }) =>
isHoverBackgroundDisabled ?? HOVER_BACKGROUND};
${({ theme, accent }) => {
switch (accent) {
case 'danger': {
return css`
color: ${theme.font.color.danger};
&:hover {
background: ${theme.background.transparent.danger};
}
`;
}
case 'placeholder': {
return css`
color: ${theme.font.color.tertiary};
`;
}
case 'default':
default: {
return css`
color: ${theme.font.color.secondary};
`;
}
}
}}
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
`;
export const StyledMenuItemLabel = styled.div<{ hasLeftIcon: boolean }>`
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
overflow: hidden;
padding-left: ${({ theme, hasLeftIcon }) =>
hasLeftIcon ? '' : theme.spacing(1)};
white-space: nowrap;
`;
export const StyledNoIconFiller = styled.div`
width: ${({ theme }) => theme.spacing(1)};
`;
export const StyledMenuItemLeftContent = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
min-width: 0;
width: 100%;
& > svg {
flex-shrink: 0;
}
`;
export const StyledMenuItemRightContent = styled.div`
align-items: center;
display: flex;
flex-direction: row;
`;
export const StyledDraggableItem = styled.div`
cursor: grab;
align-items: center;
display: flex;
`;
export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{
isIconDisplayedOnHoverOnly?: boolean;
cursor?: 'drag' | 'default' | 'not-allowed';
}>`
${({ isIconDisplayedOnHoverOnly, theme }) =>
isIconDisplayedOnHoverOnly &&
css`
& .hoverable-buttons {
opacity: 0;
position: fixed;
right: ${theme.spacing(2)};
}
&:hover {
& .hoverable-buttons {
opacity: 1;
position: static;
}
}
`};
& .hoverable-buttons {
transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease;
}
cursor: ${({ cursor }) => {
switch (cursor) {
case 'drag':
return 'grab';
case 'not-allowed':
return 'not-allowed';
default:
return 'pointer';
}
}};
`;

View File

@ -1 +0,0 @@
export type MenuItemAccent = 'default' | 'danger' | 'placeholder';

View File

@ -3,7 +3,6 @@ import { Workspaces } from '@/auth/states/workspaces';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId';
@ -14,7 +13,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconChevronDown } from 'twenty-ui';
import { IconChevronDown, MenuItemSelectAvatar } from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
const StyledLogo = styled.div<{ logo: string }>`