Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,67 @@
import { MouseEvent } from 'react';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { FloatingIconButtonGroup } from '@/ui/input/button/components/FloatingIconButtonGroup';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
StyledHoverableMenuItemBase,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
export type MenuItemIconButton = {
Icon: IconComponent;
onClick?: (event: MouseEvent<any>) => void;
};
export type MenuItemProps = {
LeftIcon?: IconComponent | null;
accent?: MenuItemAccent;
text: string;
iconButtons?: MenuItemIconButton[];
isTooltipOpen?: boolean;
className?: string;
testId?: string;
onClick?: (event: MouseEvent<HTMLLIElement>) => void;
};
export const MenuItem = ({
LeftIcon,
accent = 'default',
text,
iconButtons,
isTooltipOpen,
className,
testId,
onClick,
}: MenuItemProps) => {
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
const handleMenuItemClick = (event: MouseEvent<HTMLLIElement>) => {
if (!onClick) return;
event.preventDefault();
event.stopPropagation();
onClick?.(event);
};
return (
<StyledHoverableMenuItemBase
data-testid={testId ?? undefined}
onClick={handleMenuItemClick}
className={className}
accent={accent}
isMenuOpen={!!isTooltipOpen}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
</StyledMenuItemLeftContent>
<div className="hoverable-buttons">
{showIconButtons && (
<FloatingIconButtonGroup iconButtons={iconButtons} size="small" />
)}
</div>
</StyledHoverableMenuItemBase>
);
};

View File

@ -0,0 +1,108 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
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.primary};
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();
return (
<StyledMenuItemCommandContainer
onClick={onClick}
className={className}
isSelected={isSelected}
>
<StyledMenuItemLeftContent>
{LeftIcon && (
<StyledBigIconContainer>
<LeftIcon size={theme.icon.size.sm} />
</StyledBigIconContainer>
)}
<StyledMenuItemLabelText hasLeftIcon={!!LeftIcon}>
{text}
</StyledMenuItemLabelText>
</StyledMenuItemLeftContent>
<MenuItemCommandHotKeys
firstHotKey={firstHotKey}
secondHotKey={secondHotKey}
/>
</StyledMenuItemCommandContainer>
);
};

View File

@ -0,0 +1,62 @@
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

@ -0,0 +1,52 @@
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { FloatingIconButtonGroup } from '@/ui/input/button/components/FloatingIconButtonGroup';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuItemBase';
import { MenuItemAccent } from '../types/MenuItemAccent';
import { MenuItemIconButton } from './MenuItem';
export type MenuItemDraggableProps = {
LeftIcon: IconComponent | undefined;
accent?: MenuItemAccent;
iconButtons?: MenuItemIconButton[];
isTooltipOpen?: boolean;
onClick?: () => void;
text: string;
isDragDisabled?: boolean;
className?: string;
};
export const MenuItemDraggable = ({
LeftIcon,
accent = 'default',
iconButtons,
isTooltipOpen,
onClick,
text,
isDragDisabled = false,
className,
}: MenuItemDraggableProps) => {
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
return (
<StyledHoverableMenuItemBase
onClick={onClick}
accent={accent}
className={className}
isMenuOpen={!!isTooltipOpen}
>
<MenuItemLeftContent
LeftIcon={LeftIcon}
text={text}
showGrip={!isDragDisabled}
/>
{showIconButtons && (
<FloatingIconButtonGroup
className="hoverable-buttons"
iconButtons={iconButtons}
/>
)}
</StyledHoverableMenuItemBase>
);
};

View File

@ -0,0 +1,43 @@
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { Checkbox } from '@/ui/input/components/Checkbox';
import { MenuItemLeftContent } from '../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 = {
LeftIcon?: IconComponent;
selected: boolean;
text: string;
className: string;
onSelectChange?: (selected: boolean) => void;
};
export const MenuItemMultiSelect = ({
LeftIcon,
text,
selected,
className,
onSelectChange,
}: MenuItemMultiSelectProps) => {
const handleOnClick = () => {
onSelectChange?.(!selected);
};
return (
<StyledMenuItemBase className={className} onClick={handleOnClick}>
<StyledLeftContentWithCheckboxContainer>
<Checkbox checked={selected} />
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
</StyledLeftContentWithCheckboxContainer>
</StyledMenuItemBase>
);
};

View File

@ -0,0 +1,51 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { Checkbox } from '@/ui/input/components/Checkbox';
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)};
`;
type MenuItemMultiSelectAvatarProps = {
avatar?: ReactNode;
selected: boolean;
text: string;
className?: string;
onSelectChange?: (selected: boolean) => void;
};
export const MenuItemMultiSelectAvatar = ({
avatar,
text,
selected,
className,
onSelectChange,
}: MenuItemMultiSelectAvatarProps) => {
const handleOnClick = () => {
onSelectChange?.(!selected);
};
return (
<StyledMenuItemBase className={className} onClick={handleOnClick}>
<StyledLeftContentWithCheckboxContainer>
<Checkbox checked={selected} />
<StyledMenuItemLeftContent>
{avatar}
<StyledMenuItemLabel hasLeftIcon={!!avatar}>
{text}
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
</StyledLeftContentWithCheckboxContainer>
</StyledMenuItemBase>
);
};

View File

@ -0,0 +1,35 @@
import { useTheme } from '@emotion/react';
import { IconChevronRight } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
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} />
</StyledMenuItemBase>
);
};

View File

@ -0,0 +1,75 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
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) {
return css`
background: inherit;
&:hover {
background: inherit;
}
color: ${theme.font.color.tertiary};
cursor: default;
`;
} else if (hovered) {
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;
};
export const MenuItemSelect = ({
LeftIcon,
text,
selected,
className,
onClick,
disabled,
hovered,
}: 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.sm} />}
</StyledMenuItemSelect>
);
};

View File

@ -0,0 +1,55 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import { IconCheck } from '@/ui/display/icon';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
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.sm} />}
</StyledMenuItemSelect>
);
};

View File

@ -0,0 +1,68 @@
import { useTheme } from '@emotion/react';
import {
ColorSample,
ColorSampleVariant,
} from '@/ui/display/color/components/ColorSample';
import { IconCheck } from '@/ui/display/icon';
import { ThemeColor } from '@/ui/theme/constants/colors';
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.sm} />}
</StyledMenuItemSelect>
);
};

View File

@ -0,0 +1,43 @@
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { Toggle, ToggleSize } from '@/ui/input/components/Toggle';
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) => {
const handleOnClick = () => {
onToggleChange?.(!toggled);
};
return (
<StyledMenuItemBase className={className} onClick={handleOnClick}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<StyledMenuItemRightContent>
<Toggle
value={toggled}
onChange={onToggleChange}
toggleSize={toggleSize}
/>
</StyledMenuItemRightContent>
</StyledMenuItemBase>
);
};

View File

@ -0,0 +1,112 @@
import { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/display/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
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: () => console.log('Clicked') },
{ Icon: IconBell, onClick: () => console.log('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: () =>
console.log('Clicked on first icon button'),
},
{
Icon: IconBell,
onClick: () =>
console.log('Clicked on second icon button'),
},
],
};
}
default:
return {};
}
},
},
],
options: {
elementContainer: {
width: 200,
},
},
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,95 @@
import { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/display/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
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

@ -0,0 +1,106 @@
import { Meta, StoryObj } from '@storybook/react';
import { IconBell, IconMinus } from '@/ui/display/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
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: () => console.log('Clicked') }],
onClick: () => console.log('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: () =>
console.log('Clicked on minus icon button'),
},
],
};
}
}
},
},
] as CatalogDimension[],
options: {
elementContainer: {
width: 200,
},
} as CatalogOptions,
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,78 @@
import { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/display/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
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

@ -0,0 +1,84 @@
import { Meta, StoryObj } from '@storybook/react';
import { Avatar } from '@/users/components/Avatar';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { avatarUrl } from '~/testing/mock-data/users';
import { CatalogStory } from '~/testing/types';
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

@ -0,0 +1,71 @@
import { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/display/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
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

@ -0,0 +1,79 @@
import { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/display/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
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

@ -0,0 +1,93 @@
import { Meta, StoryObj } from '@storybook/react';
import { Avatar } from '@/users/components/Avatar';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { avatarUrl } from '~/testing/mock-data/users';
import { CatalogStory } from '~/testing/types';
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

@ -0,0 +1,84 @@
import { Meta, StoryObj } from '@storybook/react';
import { ColorSampleVariant } from '@/ui/display/color/components/ColorSample';
import { mainColorNames, ThemeColor } from '@/ui/theme/constants/colors';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
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: mainColorNames,
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

@ -0,0 +1,77 @@
import { Meta, StoryObj } from '@storybook/react';
import { IconBell } from '@/ui/display/icon';
import {
CatalogDecorator,
CatalogDimension,
CatalogOptions,
} from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CatalogStory } from '~/testing/types';
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

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

View File

@ -0,0 +1,113 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { hoverBackground } from '@/ui/theme/constants/effects';
import { MenuItemAccent } from '../../types/MenuItemAccent';
export type MenuItemBaseProps = {
accent?: MenuItemAccent;
};
export const StyledMenuItemBase = styled.li<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);
${hoverBackground};
${({ 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.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
overflow: hidden;
padding-left: ${({ theme, hasLeftIcon }) =>
hasLeftIcon ? '' : theme.spacing(1)};
text-overflow: ellipsis;
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%;
`;
export const StyledMenuItemRightContent = styled.div`
align-items: center;
display: flex;
flex-direction: row;
`;
export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{
isMenuOpen: boolean;
}>`
& .hoverable-buttons {
opacity: ${({ isMenuOpen }) => (isMenuOpen ? 1 : 0)};
pointer-events: none;
position: fixed;
right: ${({ theme }) => theme.spacing(2)};
transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease;
}
&:hover {
& .hoverable-buttons {
opacity: 1;
pointer-events: auto;
}
}
`;

View File

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