Raphaël Bosi
2025-09-25 18:14:32 +02:00
committed by GitHub
parent 9b3e2bfb45
commit 1fdfac4f0d
29 changed files with 429 additions and 307 deletions
@@ -5,7 +5,7 @@ import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { type ReactNode } from 'react';
import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
import { MenuItemCommand } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
export type CommandMenuItemProps = {
label: string;
@@ -40,10 +40,11 @@ export const CommandMenuItem = ({
);
return (
<MenuItemCommand
<MenuItem
withIconContainer={true}
LeftIcon={Icon}
text={label}
description={description}
contextualText={description}
hotKeys={hotKeys}
onClick={() =>
onItemClick({
@@ -51,7 +52,7 @@ export const CommandMenuItem = ({
to,
})
}
isSelected={isSelectedItemId}
focused={isSelectedItemId}
RightComponent={RightComponent}
/>
);
@@ -11,7 +11,7 @@ import {
IconGauge,
IconNumber,
} from 'twenty-ui/display';
import { MenuItemCommand } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
const StyledContainer = styled.div`
display: flex;
@@ -74,7 +74,8 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
<StyledSectionTitle>Graph type</StyledSectionTitle>
{graphTypeOptions.map((option) => (
<MenuItemCommand
<MenuItem
withIconContainer={true}
key={option.type}
LeftIcon={option.icon}
text={option.title}
@@ -6,7 +6,7 @@ import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLa
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import styled from '@emotion/styled';
import { IconChartPie, IconFrame, IconList } from 'twenty-ui/display';
import { MenuItemCommand } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
const StyledContainer = styled.div`
display: flex;
@@ -86,8 +86,9 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
<StyledContainer>
<StyledSectionTitle>Widget type</StyledSectionTitle>
{widgetTypeOptions.map((option) => {
const MenuItem = (
<MenuItemCommand
const MenuItemComponent = (
<MenuItem
withIconContainer={true}
key={option.type}
LeftIcon={option.icon}
text={option.title}
@@ -97,10 +98,10 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
return option.disabled ? (
<StyledDisabledMenuItem key={option.type}>
{MenuItem}
{MenuItemComponent}
</StyledDisabledMenuItem>
) : (
MenuItem
MenuItemComponent
);
})}
</StyledContainer>
@@ -2,7 +2,7 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useTheme } from '@emotion/react';
import { useIcons } from 'twenty-ui/display';
import { MenuItemCommand } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
type Action = { type: WorkflowActionType; label: string; icon: string };
@@ -22,7 +22,8 @@ export const WorkflowActionMenuItems = ({
const Icon = getIcon(action.icon);
return (
<MenuItemCommand
<MenuItem
withIconContainer={true}
key={action.type}
LeftIcon={() => (
<Icon
@@ -15,7 +15,7 @@ import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/g
import { useTheme } from '@emotion/react';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { useIcons } from 'twenty-ui/display';
import { MenuItemCommand } from 'twenty-ui/navigation';
import { MenuItem } from 'twenty-ui/navigation';
export const CommandMenuWorkflowSelectTriggerTypeContent = ({
workflow,
@@ -71,7 +71,8 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
{DATABASE_TRIGGER_TYPES.map((action) => {
const Icon = getIcon(action.icon);
return (
<MenuItemCommand
<MenuItem
withIconContainer={true}
key={action.defaultLabel}
LeftIcon={() => <Icon color={theme.color.blue} />}
text={action.defaultLabel}
@@ -86,7 +87,8 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
{OTHER_TRIGGER_TYPES.map((action) => {
const Icon = getIcon(action.icon);
return (
<MenuItemCommand
<MenuItem
withIconContainer={true}
key={action.defaultLabel}
LeftIcon={() => <Icon color={theme.color.purple} />}
text={action.defaultLabel}
@@ -137,6 +137,7 @@ export const ObjectOptionsDropdownCustomView = ({
)}
text={t`Layout`}
contextualText={`${capitalize(customViewData?.type ?? '')}`}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -160,6 +161,7 @@ export const ObjectOptionsDropdownCustomView = ({
? t`Not available on Default View`
: calendarFieldMetadata?.label
}
contextualTextPosition="right"
hasSubMenu
disabled={isDefaultView}
/>
@@ -181,6 +183,7 @@ export const ObjectOptionsDropdownCustomView = ({
? t`Week`
: t`Day`
}
contextualTextPosition="right"
/>
</SelectableListItem>
</>
@@ -195,6 +198,7 @@ export const ObjectOptionsDropdownCustomView = ({
LeftIcon={IconListDetails}
text={t`Fields`}
contextualText={`${visibleBoardFields.length} shown`}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -222,6 +226,7 @@ export const ObjectOptionsDropdownCustomView = ({
? t`Not available on Default View`
: recordGroupFieldMetadata?.label
}
contextualTextPosition="right"
hasSubMenu
disabled={isDefaultView}
/>
@@ -87,6 +87,7 @@ export const ObjectOptionsDropdownDefaultView = () => {
LeftIcon={IconListDetails}
text={t`Fields`}
contextualText={`${visibleBoardFields.length} shown`}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -119,6 +120,7 @@ export const ObjectOptionsDropdownDefaultView = () => {
onClick={handleCreateCustomView}
LeftIcon={IconLayout}
text={t`Create custom view`}
contextualTextPosition="right"
/>
</SelectableListItem>
</DropdownMenuItemsContainer>
@@ -209,6 +209,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
t`Create Select...`
) : undefined
}
contextualTextPosition="right"
selected={currentView?.type === ViewType.Kanban}
onClick={handleSelectKanbanViewType}
/>
@@ -228,6 +229,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
LeftIcon={IconCalendar}
text={t`Date field`}
contextualText={calendarFieldMetadata?.label}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -247,6 +249,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
? t`Week`
: t`Day`
}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -274,6 +277,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
? t`Side Panel`
: t`Record Page`
}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -296,6 +300,7 @@ export const ObjectOptionsDropdownLayoutContent = () => {
LeftIcon={IconLayoutList}
text={t`Group`}
contextualText={recordGroupFieldMetadata?.label}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -125,6 +125,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
LeftIcon={IconLayoutList}
text={t`Group by`}
contextualText={recordGroupFieldMetadata?.label}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
@@ -138,6 +139,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
LeftIcon={IconSortDescending}
text={t`Sort`}
contextualText={recordGroupSort}
contextualTextPosition="right"
hasSubMenu
/>
</SelectableListItem>
+4 -4
View File
@@ -27,12 +27,10 @@ export type {
export { MenuItem } from './menu/menu-item/components/MenuItem';
export type { MenuItemAvatarProps } from './menu/menu-item/components/MenuItemAvatar';
export { MenuItemAvatar } from './menu/menu-item/components/MenuItemAvatar';
export type { MenuItemCommandProps } from './menu/menu-item/components/MenuItemCommand';
export { MenuItemCommand } from './menu/menu-item/components/MenuItemCommand';
export type { MenuItemCommandHotKeysProps } from './menu/menu-item/components/MenuItemCommandHotKeys';
export { MenuItemCommandHotKeys } from './menu/menu-item/components/MenuItemCommandHotKeys';
export type { MenuItemDraggableProps } from './menu/menu-item/components/MenuItemDraggable';
export { MenuItemDraggable } from './menu/menu-item/components/MenuItemDraggable';
export type { MenuItemHotKeysProps } from './menu/menu-item/components/MenuItemHotKeys';
export { MenuItemHotKeys } from './menu/menu-item/components/MenuItemHotKeys';
export { MenuItemMultiSelect } from './menu/menu-item/components/MenuItemMultiSelect';
export { MenuItemMultiSelectAvatar } from './menu/menu-item/components/MenuItemMultiSelectAvatar';
export { MenuItemMultiSelectTag } from './menu/menu-item/components/MenuItemMultiSelectTag';
@@ -62,6 +60,8 @@ export {
StyledDraggableItem,
StyledHoverableMenuItemBase,
StyledMenuItemIconCheck,
StyledMenuItemContextualText,
StyledRightMenuItemContextualText,
} from './menu/menu-item/internals/components/StyledMenuItemBase';
export type { MenuItemAccent } from './menu/menu-item/types/MenuItemAccent';
export { NavigationBar } from './navigation-bar/components/NavigationBar';
@@ -1,5 +1,9 @@
import { useTheme } from '@emotion/react';
import { IconChevronRight, type IconComponent } from '@ui/display';
import {
IconChevronRight,
OverflowingTextWithTooltip,
type IconComponent,
} from '@ui/display';
import { type LightIconButtonProps } from '@ui/input/button/components/LightIconButton';
import { LightIconButtonGroup } from '@ui/input/button/components/LightIconButtonGroup';
import {
@@ -9,10 +13,17 @@ import {
type ReactNode,
} from 'react';
import styled from '@emotion/styled';
import { isString } from '@sniptt/guards';
import { MenuItemHotKeys } from '@ui/navigation/menu/menu-item/components/MenuItemHotKeys';
import { motion } from 'framer-motion';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import {
StyledHoverableMenuItemBase,
StyledMenuItemLabel,
StyledMenuItemLeftContent,
StyledMenuItemRightContent,
StyledRightMenuItemContextualText,
} from '../internals/components/StyledMenuItemBase';
import { type MenuItemAccent } from '../types/MenuItemAccent';
@@ -26,40 +37,56 @@ export type MenuItemIconButton = {
export type MenuItemProps = {
accent?: MenuItemAccent;
className?: string;
withIconContainer?: boolean;
iconButtons?: MenuItemIconButton[];
isIconDisplayedOnHoverOnly?: boolean;
isTooltipOpen?: boolean;
LeftIcon?: IconComponent | null;
LeftComponent?: ReactNode;
RightIcon?: IconComponent | null;
RightComponent?: ReactNode;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
testId?: string;
disabled?: boolean;
text: ReactNode;
contextualTextPosition?: 'left' | 'right';
contextualText?: ReactNode;
hasSubMenu?: boolean;
focused?: boolean;
hotKeys?: string[];
isSubMenuOpened?: boolean;
};
const StyledSubMenuIcon = styled(motion.div)`
display: flex;
align-items: center;
justify-content: center;
`;
export const MenuItem = ({
accent = 'default',
className,
withIconContainer = false,
iconButtons,
isIconDisplayedOnHoverOnly = true,
LeftIcon,
LeftComponent,
RightIcon,
RightComponent,
onClick,
onMouseEnter,
onMouseLeave,
testId,
text,
contextualTextPosition = 'left',
contextualText,
hasSubMenu = false,
disabled = false,
focused = false,
hotKeys,
isSubMenuOpened = false,
}: MenuItemProps) => {
const theme = useTheme();
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
@@ -88,25 +115,50 @@ export const MenuItem = ({
<MenuItemLeftContent
LeftIcon={LeftIcon ?? undefined}
LeftComponent={LeftComponent}
withIconContainer={withIconContainer}
text={text}
contextualText={contextualText}
contextualText={
contextualTextPosition === 'left' ? contextualText : null
}
disabled={disabled}
/>
</StyledMenuItemLeftContent>
<div className="hoverable-buttons">
{showIconButtons && (
<LightIconButtonGroup iconButtons={iconButtons} size="small" />
<StyledMenuItemRightContent>
{contextualTextPosition === 'right' && (
<StyledMenuItemLabel>
{isString(contextualText) ? (
<StyledRightMenuItemContextualText>
<OverflowingTextWithTooltip text={contextualText} />
</StyledRightMenuItemContextualText>
) : (
contextualText
)}
</StyledMenuItemLabel>
)}
</div>
{RightIcon && (
<RightIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{hasSubMenu && !disabled && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
{iconButtons && (
<div className="hoverable-buttons">
{showIconButtons && (
<LightIconButtonGroup iconButtons={iconButtons} size="small" />
)}
</div>
)}
{hotKeys && <MenuItemHotKeys hotKeys={hotKeys} />}
{RightIcon && (
<RightIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{RightComponent}
{hasSubMenu && !disabled && (
<StyledSubMenuIcon
animate={{ rotate: isSubMenuOpened ? 90 : 0 }}
transition={{ duration: theme.animation.duration.normal }}
>
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.light}
/>
</StyledSubMenuIcon>
)}
</StyledMenuItemRightContent>
</StyledHoverableMenuItemBase>
);
};
@@ -1,131 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { type IconComponent, OverflowingTextWithTooltip } from '@ui/display';
import { useIsMobile } from '@ui/utilities/responsive/hooks/useIsMobile';
import { type ReactNode } from 'react';
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 : 'transparent'};
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: 100%;
box-sizing: border-box;
height: 40px;
&:hover {
background: ${({ theme }) => theme.background.transparent.lighter};
}
&[data-selected='true'] {
background: ${({ theme }) => theme.background.transparent.light};
}
&[data-disabled='true'] {
color: ${({ theme }) => theme.font.color.light};
cursor: not-allowed;
}
svg {
height: 16px;
width: 16px;
}
`;
const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.light};
&::before {
content: '·';
margin: ${({ theme }) => theme.spacing(0, 1)};
}
`;
const StyledTextContainer = styled.div`
display: flex;
flex-direction: row;
max-width: calc(100% - 2 * var(--horizontal-padding));
overflow: hidden;
`;
export type MenuItemCommandProps = {
LeftIcon?: IconComponent;
text: string;
description?: string;
hotKeys?: string[];
className?: string;
isSelected?: boolean;
onClick?: () => void;
RightComponent?: ReactNode;
};
export const MenuItemCommand = ({
LeftIcon,
text,
description,
hotKeys,
className,
isSelected,
onClick,
RightComponent,
}: 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>
)}
<StyledTextContainer>
<StyledMenuItemLabelText>
<OverflowingTextWithTooltip text={text} />
</StyledMenuItemLabelText>
{description && <StyledDescription>{description}</StyledDescription>}
</StyledTextContainer>
{RightComponent}
</StyledMenuItemLeftContent>
{!isMobile && <MenuItemCommandHotKeys hotKeys={hotKeys} />}
</StyledMenuItemCommandContainer>
);
};
@@ -9,6 +9,7 @@ import { type MenuItemIconButton } from './MenuItem';
export type MenuItemDraggableProps = {
LeftIcon?: IconComponent | undefined;
withIconContainer?: boolean;
accent?: MenuItemAccent;
iconButtons?: MenuItemIconButton[];
isTooltipOpen?: boolean;
@@ -23,6 +24,7 @@ export type MenuItemDraggableProps = {
export const MenuItemDraggable = ({
LeftIcon,
withIconContainer = false,
accent = 'default',
iconButtons,
onClick,
@@ -51,6 +53,7 @@ export const MenuItemDraggable = ({
<MenuItemLeftContent
LeftIcon={LeftIcon}
text={text}
withIconContainer={withIconContainer}
disabled={isDragDisabled}
showGrip={showGrip}
/>
@@ -31,15 +31,15 @@ const StyledCommandKey = styled.div`
width: ${({ theme }) => theme.spacing(4)};
`;
export type MenuItemCommandHotKeysProps = {
export type MenuItemHotKeysProps = {
hotKeys?: string[];
joinLabel?: string;
};
export const MenuItemCommandHotKeys = ({
export const MenuItemHotKeys = ({
hotKeys,
joinLabel = 'then',
}: MenuItemCommandHotKeysProps) => {
}: MenuItemHotKeysProps) => {
return (
<StyledCommandText>
{hotKeys && (
@@ -19,6 +19,7 @@ type MenuItemMultiSelectProps = {
LeftIcon?: IconComponent;
selected: boolean;
isKeySelected?: boolean;
withIconContainer?: boolean;
text: string;
className: string;
onSelectChange?: (selected: boolean) => void;
@@ -27,6 +28,7 @@ type MenuItemMultiSelectProps = {
export const MenuItemMultiSelect = ({
color,
LeftIcon,
withIconContainer = false,
text,
selected,
isKeySelected,
@@ -48,7 +50,11 @@ export const MenuItemMultiSelect = ({
{color ? (
<Tag color={color} text={text} Icon={LeftIcon} />
) : (
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<MenuItemLeftContent
LeftIcon={LeftIcon}
text={text}
withIconContainer={withIconContainer}
/>
)}
</StyledLeftContentWithCheckboxContainer>
</StyledMenuItemBase>
@@ -9,6 +9,7 @@ import {
export type MenuItemNavigateProps = {
LeftIcon?: IconComponent;
withIconContainer?: boolean;
text: string;
onClick?: () => void;
className?: string;
@@ -16,6 +17,7 @@ export type MenuItemNavigateProps = {
export const MenuItemNavigate = ({
LeftIcon,
withIconContainer = false,
text,
className,
onClick,
@@ -25,7 +27,11 @@ export const MenuItemNavigate = ({
return (
<StyledMenuItemBase onClick={onClick} className={className}>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<MenuItemLeftContent
LeftIcon={LeftIcon}
text={text}
withIconContainer={withIconContainer}
/>
</StyledMenuItemLeftContent>
<IconChevronRight
size={theme.icon.size.sm}
@@ -1,10 +1,21 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck, IconChevronRight, type IconComponent } from '@ui/display';
import { isString } from '@sniptt/guards';
import {
IconCheck,
IconChevronRight,
OverflowingTextWithTooltip,
type IconComponent,
} from '@ui/display';
import { type ReactNode } from 'react';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
import {
StyledMenuItemBase,
StyledMenuItemLabel,
StyledMenuItemRightContent,
StyledRightMenuItemContextualText,
} from '../internals/components/StyledMenuItemBase';
export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
disabled?: boolean;
@@ -32,6 +43,7 @@ export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
type MenuItemSelectProps = {
LeftIcon?: IconComponent | null | undefined;
withIconContainer?: boolean;
selected: boolean;
needIconCheck?: boolean;
text: string;
@@ -41,10 +53,12 @@ type MenuItemSelectProps = {
focused?: boolean;
hasSubMenu?: boolean;
contextualText?: ReactNode;
contextualTextPosition?: 'left' | 'right';
};
export const MenuItemSelect = ({
LeftIcon,
withIconContainer = false,
text,
selected,
needIconCheck = true,
@@ -54,6 +68,7 @@ export const MenuItemSelect = ({
focused,
hasSubMenu = false,
contextualText,
contextualTextPosition = 'left',
}: MenuItemSelectProps) => {
const theme = useTheme();
@@ -70,16 +85,33 @@ export const MenuItemSelect = ({
<MenuItemLeftContent
LeftIcon={LeftIcon}
text={text}
contextualText={contextualText}
contextualText={
contextualTextPosition === 'left' ? contextualText : null
}
withIconContainer={withIconContainer}
/>
{selected && needIconCheck && <IconCheck size={theme.icon.size.md} />}
<StyledMenuItemRightContent>
{contextualTextPosition === 'right' && (
<StyledMenuItemLabel>
{isString(contextualText) ? (
<StyledRightMenuItemContextualText>
<OverflowingTextWithTooltip text={contextualText} />
</StyledRightMenuItemContextualText>
) : (
contextualText
)}
</StyledMenuItemLabel>
)}
{hasSubMenu && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
{selected && needIconCheck && <IconCheck size={theme.icon.size.md} />}
{hasSubMenu && (
<IconChevronRight
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
)}
</StyledMenuItemRightContent>
</StyledMenuItemSelect>
);
};
@@ -8,6 +8,7 @@ import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuIte
export type MenuItemSuggestionProps = {
LeftIcon?: IconComponent | null;
withIconContainer?: boolean;
text: string;
selected?: boolean;
className?: string;
@@ -52,6 +53,7 @@ const StyledSuggestionMenuItem = styled.li<{
export const MenuItemSuggestion = ({
LeftIcon,
withIconContainer = false,
text,
className,
selected,
@@ -72,7 +74,11 @@ export const MenuItemSuggestion = ({
selected={selected}
>
<StyledMenuItemLeftContent>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
<MenuItemLeftContent
LeftIcon={LeftIcon ?? undefined}
text={text}
withIconContainer={withIconContainer}
/>
</StyledMenuItemLeftContent>
</StyledSuggestionMenuItem>
);
@@ -19,6 +19,7 @@ const StyledToggleContainer = styled.label`
type MenuItemToggleProps = {
focused?: boolean;
LeftIcon?: IconComponent;
withIconContainer?: boolean;
toggled: boolean;
text: string;
className?: string;
@@ -29,6 +30,7 @@ type MenuItemToggleProps = {
export const MenuItemToggle = ({
focused,
LeftIcon,
withIconContainer = false,
text,
toggled,
className,
@@ -39,7 +41,11 @@ export const MenuItemToggle = ({
return (
<StyledMenuItemBase className={className} focused={focused}>
<StyledToggleContainer htmlFor={instanceId}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<MenuItemLeftContent
LeftIcon={LeftIcon}
text={text}
withIconContainer={withIconContainer}
/>
<StyledMenuItemRightContent>
<Toggle
id={instanceId}
@@ -52,6 +52,15 @@ export const Catalog: CatalogStory<Story, typeof MenuItem> = {
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'withIconContainer',
values: [true, false],
props: (withIconContainer: boolean) => ({ withIconContainer }),
labels: (withIconContainer: boolean) =>
withIconContainer
? 'With icon container'
: 'Without icon container',
},
{
name: 'accents',
values: ['default', 'danger'] satisfies MenuItemAccent[],
@@ -115,3 +124,142 @@ export const Catalog: CatalogStory<Story, typeof MenuItem> = {
},
decorators: [CatalogDecorator],
};
export const HotKeysCatalog: CatalogStory<Story, typeof MenuItem> = {
args: {
text: 'Menu item with hotkeys',
LeftIcon: IconBell,
hotKeys: ['⌘', 'K'],
},
argTypes: {
className: { control: false },
iconButtons: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
catalog: {
dimensions: [
{
name: 'hotKeyTypes',
values: ['no hotkeys', 'single key', 'modifier + key'],
props: (choice: string) => {
switch (choice) {
case 'no hotkeys':
return { hotKeys: undefined };
case 'single key':
return { hotKeys: ['K'] };
case 'modifier + key':
return { hotKeys: ['⌘', 'K'] };
default:
return {};
}
},
labels: (choice: string) => {
switch (choice) {
case 'no hotkeys':
return 'No hotkeys';
case 'single key':
return 'Single key (K)';
case 'modifier + key':
return 'Modifier + key (⌘K)';
default:
return choice;
}
},
},
{
name: 'states',
values: ['default', 'hover'],
props: (state: string) => {
switch (state) {
case 'default':
return {};
case 'hover':
return { className: 'hover' };
default:
return {};
}
},
},
],
options: {
elementContainer: {
width: 300,
},
},
},
},
decorators: [CatalogDecorator],
};
export const ContextualTextCatalog: CatalogStory<Story, typeof MenuItem> = {
args: {
text: 'Menu item with contextual text',
},
decorators: [CatalogDecorator],
parameters: {
catalog: {
dimensions: [
{
name: 'contextualTextPosition',
values: ['left', 'right'],
props: (contextualTextPosition: 'left' | 'right') => ({
contextualTextPosition,
}),
labels: (contextualTextPosition: 'left' | 'right') =>
contextualTextPosition === 'left' ? 'Left' : 'Right',
},
{
name: 'contextualText',
values: [
'Contextual text',
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet, corporis.',
],
props: (contextualText: string) => ({ contextualText }),
labels: (contextualText: string) => {
switch (contextualText) {
case 'Contextual text':
return 'Contextual text';
case 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet, corporis.':
return 'Long contextual text';
default:
return contextualText;
}
},
},
],
options: {
elementContainer: {
style: { width: '400px', overflow: 'hidden' },
},
},
},
},
};
export const SubMenuCatalog: CatalogStory<Story, typeof MenuItem> = {
args: {
text: 'Menu item with sub menu',
LeftIcon: IconBell,
hasSubMenu: true,
},
parameters: {
catalog: {
dimensions: [
{
name: 'isSubMenuOpened',
values: [true, false],
props: (isSubMenuOpened: boolean) => ({ isSubMenuOpened }),
labels: (isSubMenuOpened: boolean) =>
isSubMenuOpened ? 'Opened' : 'Closed',
},
],
options: {
elementContainer: {
width: 200,
},
},
},
},
decorators: [CatalogDecorator],
};
@@ -1,101 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { IconBell } from '@ui/display';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { MenuItemCommand } from '../MenuItemCommand';
const meta: Meta<typeof MenuItemCommand> = {
title: 'UI/Navigation/Menu/MenuItem/MenuItemCommand',
component: MenuItemCommand,
};
export default meta;
type Story = StoryObj<typeof MenuItemCommand>;
export const Default: Story = {
args: {
text: 'First option',
hotKeys: ['⌘', '1'],
},
render: (props) => (
<MenuItemCommand
LeftIcon={props.LeftIcon}
text={props.text}
hotKeys={props.hotKeys}
className={props.className}
onClick={props.onClick}
isSelected={false}
></MenuItemCommand>
),
decorators: [ComponentDecorator],
};
export const WithDescription: Story = {
args: {
text: 'Menu item',
hotKeys: ['⌘', '1'],
description: 'Description',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof MenuItemCommand> = {
args: {
text: 'Menu item',
hotKeys: ['⌘', '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}
hotKeys={props.hotKeys}
className={props.className}
onClick={props.onClick}
isSelected={false}
></MenuItemCommand>
),
decorators: [CatalogDecorator],
};
@@ -42,6 +42,15 @@ export const Catalog: Story = {
pseudo: { hover: ['.hover'] },
catalog: {
dimensions: [
{
name: 'withIconContainer',
values: [true, false],
props: (withIconContainer: boolean) => ({ withIconContainer }),
labels: (withIconContainer: boolean) =>
withIconContainer
? 'With icon container'
: 'Without icon container',
},
{
name: 'isDragDisabled',
values: [true, false],
@@ -44,6 +44,15 @@ export const Catalog: CatalogStory<Story, typeof MenuItemMultiSelect> = {
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'withIconContainer',
values: [true, false],
props: (withIconContainer: boolean) => ({ withIconContainer }),
labels: (withIconContainer: boolean) =>
withIconContainer
? 'With icon container'
: 'Without icon container',
},
{
name: 'selected',
values: [true, false],
@@ -45,6 +45,15 @@ export const Catalog: CatalogStory<Story, typeof MenuItemNavigate> = {
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'withIconContainer',
values: [true, false],
props: (withIconContainer: boolean) => ({ withIconContainer }),
labels: (withIconContainer: boolean) =>
withIconContainer
? 'With icon container'
: 'Without icon container',
},
{
name: 'states',
values: ['default', 'hover'],
@@ -48,6 +48,15 @@ export const Catalog: CatalogStory<Story, typeof MenuItemSelect> = {
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'withIconContainer',
values: [true, false],
props: (withIconContainer: boolean) => ({ withIconContainer }),
labels: (withIconContainer: boolean) =>
withIconContainer
? 'With icon container'
: 'Without icon container',
},
{
name: 'states',
values: ['default', 'hover', 'focused', 'hover+focused'],
@@ -44,6 +44,15 @@ export const Catalog: CatalogStory<Story, typeof MenuItemToggle> = {
labels: (withIcon: boolean) =>
withIcon ? 'With left icon' : 'Without left icon',
},
{
name: 'withIconContainer',
values: [true, false],
props: (withIconContainer: boolean) => ({ withIconContainer }),
labels: (withIconContainer: boolean) =>
withIconContainer
? 'With icon container'
: 'Without icon container',
},
{
name: 'toggled',
values: [true, false],
@@ -1,9 +1,8 @@
export * from '../../components/MenuPicker';
export * from './MenuItem';
export * from './MenuItemAvatar';
export * from './MenuItemCommand';
export * from './MenuItemCommandHotKeys';
export * from './MenuItemDraggable';
export * from './MenuItemHotKeys';
export * from './MenuItemMultiSelect';
export * from './MenuItemMultiSelectAvatar';
export * from './MenuItemMultiSelectTag';
@@ -1,5 +1,5 @@
import { useTheme } from '@emotion/react';
import { isString } from '@sniptt/guards';
import { isNonEmptyString, isString } from '@sniptt/guards';
import { type ReactNode } from 'react';
import styled from '@emotion/styled';
@@ -10,6 +10,7 @@ import {
} from '@ui/display';
import {
StyledDraggableItem,
StyledMenuItemContextualText,
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from './StyledMenuItemBase';
@@ -22,23 +23,20 @@ const StyledMainText = styled.div`
max-width: 100%;
`;
const StyledContextualText = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: ${({ theme }) => theme.spacing(1)};
flex-shrink: 1;
const StyledIconContainer = styled.div`
align-items: flex-start;
background: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(1)};
`;
type MenuItemLeftContentProps = {
className?: string;
LeftComponent?: ReactNode;
LeftIcon: IconComponent | null | undefined;
withIconContainer?: boolean;
showGrip?: boolean;
disabled?: boolean;
text: ReactNode;
@@ -49,6 +47,7 @@ export const MenuItemLeftContent = ({
className,
LeftComponent,
LeftIcon,
withIconContainer = false,
text,
contextualText,
showGrip = false,
@@ -64,14 +63,23 @@ export const MenuItemLeftContent = ({
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={
disabled ? theme.font.color.extraLight : theme.font.color.light
withIconContainer
? theme.font.color.tertiary
: disabled
? theme.font.color.extraLight
: theme.font.color.light
}
/>
</StyledDraggableItem>
)}
{LeftIcon && (
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
{LeftIcon &&
(withIconContainer ? (
<StyledIconContainer>
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
</StyledIconContainer>
) : (
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
))}
{LeftComponent}
<StyledMenuItemLabel>
{isString(text) ? (
@@ -82,7 +90,13 @@ export const MenuItemLeftContent = ({
text
)}
{isString(contextualText) ? (
<StyledContextualText>{`· ${contextualText}`}</StyledContextualText>
<>
{isNonEmptyString(contextualText) && (
<StyledMenuItemContextualText>
<OverflowingTextWithTooltip text={`· ${contextualText}`} />
</StyledMenuItemContextualText>
)}
</>
) : (
contextualText
)}
@@ -108,17 +108,16 @@ export const StyledMenuItemLeftContent = styled.div`
gap: ${({ theme }) => theme.spacing(2)};
min-width: 0;
width: 100%;
max-width: 100%;
& > svg {
flex-shrink: 0;
}
flex-shrink: 0;
`;
export const StyledMenuItemRightContent = styled.div`
align-items: center;
display: flex;
flex-direction: row;
overflow: hidden;
`;
export const StyledDraggableItem = styled.div`
@@ -170,3 +169,21 @@ export const StyledMenuItemIconCheck = styled(IconCheck)`
flex-shrink: 0;
margin-right: ${({ theme }) => theme.spacing(1)};
`;
export const StyledMenuItemContextualText = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-family: inherit;
font-size: inherit;
font-weight: inherit;
padding-left: ${({ theme }) => theme.spacing(1)};
flex-shrink: 1;
overflow: hidden;
`;
export const StyledRightMenuItemContextualText = styled(
StyledMenuItemContextualText,
)`
display: flex;
text-align: right;
padding-right: ${({ theme }) => theme.spacing(1)};
`;