From 1fdfac4f0db58b9b3c3dda30cefe3d83f67583da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:14:32 +0200 Subject: [PATCH] Update menu items across the app (#14724) Closes https://github.com/twentyhq/core-team-issues/issues/1568 https://github.com/user-attachments/assets/a0fe6538-8405-4b52-a6ce-7afa846ee5f3 --- .../components/CommandMenuItem.tsx | 9 +- .../CommandMenuPageLayoutGraphTypeSelect.tsx | 5 +- .../CommandMenuPageLayoutWidgetTypeSelect.tsx | 11 +- .../components/WorkflowActionMenuItems.tsx | 5 +- ...ndMenuWorkflowSelectTriggerTypeContent.tsx | 8 +- .../ObjectOptionsDropdownCustomView.tsx | 5 + .../ObjectOptionsDropdownDefaultView.tsx | 2 + .../ObjectOptionsDropdownLayoutContent.tsx | 5 + ...jectOptionsDropdownRecordGroupsContent.tsx | 2 + packages/twenty-ui/src/navigation/index.ts | 8 +- .../menu/menu-item/components/MenuItem.tsx | 82 ++++++++-- .../menu-item/components/MenuItemCommand.tsx | 131 ---------------- .../components/MenuItemDraggable.tsx | 3 + ...CommandHotKeys.tsx => MenuItemHotKeys.tsx} | 6 +- .../components/MenuItemMultiSelect.tsx | 8 +- .../menu-item/components/MenuItemNavigate.tsx | 8 +- .../menu-item/components/MenuItemSelect.tsx | 52 ++++-- .../components/MenuItemSuggestion.tsx | 8 +- .../menu-item/components/MenuItemToggle.tsx | 8 +- .../__stories__/MenuItem.stories.tsx | 148 ++++++++++++++++++ .../__stories__/MenuItemCommand.stories.tsx | 101 ------------ .../__stories__/MenuItemDraggable.stories.tsx | 9 ++ .../MenuItemMultiSelect.stories.tsx | 9 ++ .../__stories__/MenuItemNavigate.stories.tsx | 9 ++ .../__stories__/MenuItemSelect.stories.tsx | 9 ++ .../__stories__/MenuItemToggle.stories.tsx | 9 ++ .../menu/menu-item/components/index.ts | 3 +- .../components/MenuItemLeftContent.tsx | 48 ++++-- .../components/StyledMenuItemBase.tsx | 25 ++- 29 files changed, 429 insertions(+), 307 deletions(-) delete mode 100644 packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemCommand.tsx rename packages/twenty-ui/src/navigation/menu/menu-item/components/{MenuItemCommandHotKeys.tsx => MenuItemHotKeys.tsx} (92%) delete mode 100644 packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemCommand.stories.tsx diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx index 3105a2fe126..691a3f95181 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx @@ -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 ( - onItemClick({ @@ -51,7 +52,7 @@ export const CommandMenuItem = ({ to, }) } - isSelected={isSelectedItemId} + focused={isSelectedItemId} RightComponent={RightComponent} /> ); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx index 127f3ecdef5..921a25701ac 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphTypeSelect.tsx @@ -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 = () => { Graph type {graphTypeOptions.map((option) => ( - { Widget type {widgetTypeOptions.map((option) => { - const MenuItem = ( - { return option.disabled ? ( - {MenuItem} + {MenuItemComponent} ) : ( - MenuItem + MenuItemComponent ); })} diff --git a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/WorkflowActionMenuItems.tsx b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/WorkflowActionMenuItems.tsx index 9a7174eb736..e47efdfdfc3 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/WorkflowActionMenuItems.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/workflow/action/components/WorkflowActionMenuItems.tsx @@ -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 ( - ( { const Icon = getIcon(action.icon); return ( - } text={action.defaultLabel} @@ -86,7 +87,8 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({ {OTHER_TRIGGER_TYPES.map((action) => { const Icon = getIcon(action.icon); return ( - } text={action.defaultLabel} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx index 03984199cdd..b2c7d995be0 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx @@ -137,6 +137,7 @@ export const ObjectOptionsDropdownCustomView = ({ )} text={t`Layout`} contextualText={`${capitalize(customViewData?.type ?? '')}`} + contextualTextPosition="right" hasSubMenu /> @@ -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" /> @@ -195,6 +198,7 @@ export const ObjectOptionsDropdownCustomView = ({ LeftIcon={IconListDetails} text={t`Fields`} contextualText={`${visibleBoardFields.length} shown`} + contextualTextPosition="right" hasSubMenu /> @@ -222,6 +226,7 @@ export const ObjectOptionsDropdownCustomView = ({ ? t`Not available on Default View` : recordGroupFieldMetadata?.label } + contextualTextPosition="right" hasSubMenu disabled={isDefaultView} /> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownDefaultView.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownDefaultView.tsx index e1fa63ab709..d20ddc5b89f 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownDefaultView.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownDefaultView.tsx @@ -87,6 +87,7 @@ export const ObjectOptionsDropdownDefaultView = () => { LeftIcon={IconListDetails} text={t`Fields`} contextualText={`${visibleBoardFields.length} shown`} + contextualTextPosition="right" hasSubMenu /> @@ -119,6 +120,7 @@ export const ObjectOptionsDropdownDefaultView = () => { onClick={handleCreateCustomView} LeftIcon={IconLayout} text={t`Create custom view`} + contextualTextPosition="right" /> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx index 81c2cb2225d..b56c879b138 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx @@ -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 /> @@ -247,6 +249,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { ? t`Week` : t`Day` } + contextualTextPosition="right" hasSubMenu /> @@ -274,6 +277,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { ? t`Side Panel` : t`Record Page` } + contextualTextPosition="right" hasSubMenu /> @@ -296,6 +300,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { LeftIcon={IconLayoutList} text={t`Group`} contextualText={recordGroupFieldMetadata?.label} + contextualTextPosition="right" hasSubMenu /> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx index f43f8d7e50f..b251ce6d41b 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx @@ -125,6 +125,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { LeftIcon={IconLayoutList} text={t`Group by`} contextualText={recordGroupFieldMetadata?.label} + contextualTextPosition="right" hasSubMenu /> @@ -138,6 +139,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { LeftIcon={IconSortDescending} text={t`Sort`} contextualText={recordGroupSort} + contextualTextPosition="right" hasSubMenu /> diff --git a/packages/twenty-ui/src/navigation/index.ts b/packages/twenty-ui/src/navigation/index.ts index 72580cf6eb0..8c48cd68994 100644 --- a/packages/twenty-ui/src/navigation/index.ts +++ b/packages/twenty-ui/src/navigation/index.ts @@ -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'; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx index 63331dfccde..dd6153fd5c6 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItem.tsx @@ -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) => void; onMouseEnter?: (event: MouseEvent) => void; onMouseLeave?: (event: MouseEvent) => 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 = ({ -
- {showIconButtons && ( - + + {contextualTextPosition === 'right' && ( + + {isString(contextualText) ? ( + + + + ) : ( + contextualText + )} + )} -
- {RightIcon && ( - - )} - {hasSubMenu && !disabled && ( - - )} + {iconButtons && ( +
+ {showIconButtons && ( + + )} +
+ )} + {hotKeys && } + {RightIcon && ( + + )} + {RightComponent} + {hasSubMenu && !disabled && ( + + + + )} + ); }; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemCommand.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemCommand.tsx deleted file mode 100644 index 5c8ba8f7996..00000000000 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemCommand.tsx +++ /dev/null @@ -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 ( - - - {LeftIcon && ( - - - - )} - - - - - {description && {description}} - - {RightComponent} - - {!isMobile && } - - ); -}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemDraggable.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemDraggable.tsx index ad951f0f7d0..5984f31f48b 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemDraggable.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemDraggable.tsx @@ -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 = ({ diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemCommandHotKeys.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.tsx similarity index 92% rename from packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemCommandHotKeys.tsx rename to packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.tsx index e5222bdff5f..7922315b183 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemCommandHotKeys.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemHotKeys.tsx @@ -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 ( {hotKeys && ( diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.tsx index e8facc52391..ec5bb0dc22a 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemMultiSelect.tsx @@ -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 ? ( ) : ( - + )} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemNavigate.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemNavigate.tsx index 7c113a422ae..7e951a21082 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemNavigate.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemNavigate.tsx @@ -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 ( - + { const theme = useTheme(); @@ -70,16 +85,33 @@ export const MenuItemSelect = ({ - {selected && needIconCheck && } + + {contextualTextPosition === 'right' && ( + + {isString(contextualText) ? ( + + + + ) : ( + contextualText + )} + + )} - {hasSubMenu && ( - - )} + {selected && needIconCheck && } + + {hasSubMenu && ( + + )} + ); }; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx index 20a69485d54..9ef2f8f2a52 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx @@ -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} > - + ); diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx index 04ba4a274a5..7b98c586477 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemToggle.tsx @@ -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 ( - + = { 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 = { }, decorators: [CatalogDecorator], }; + +export const HotKeysCatalog: CatalogStory = { + 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 = { + 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 = { + 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], +}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemCommand.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemCommand.stories.tsx deleted file mode 100644 index bd99fec3a2e..00000000000 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemCommand.stories.tsx +++ /dev/null @@ -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 = { - title: 'UI/Navigation/Menu/MenuItem/MenuItemCommand', - component: MenuItemCommand, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - text: 'First option', - hotKeys: ['⌘', '1'], - }, - render: (props) => ( - - ), - decorators: [ComponentDecorator], -}; - -export const WithDescription: Story = { - args: { - text: 'Menu item', - hotKeys: ['⌘', '1'], - description: 'Description', - }, - decorators: [ComponentDecorator], -}; - -export const Catalog: CatalogStory = { - 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) => ( - - ), - decorators: [CatalogDecorator], -}; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemDraggable.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemDraggable.stories.tsx index 215cbe21e2d..46f9ebf0923 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemDraggable.stories.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemDraggable.stories.tsx @@ -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], diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelect.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelect.stories.tsx index cdff264367d..fea7924af2e 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelect.stories.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemMultiSelect.stories.tsx @@ -44,6 +44,15 @@ export const Catalog: CatalogStory = { 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], diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemNavigate.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemNavigate.stories.tsx index 15fd5ece242..ea629f4ae51 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemNavigate.stories.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemNavigate.stories.tsx @@ -45,6 +45,15 @@ export const Catalog: CatalogStory = { 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'], diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelect.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelect.stories.tsx index 6117dc13982..43aedf7b1d5 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelect.stories.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelect.stories.tsx @@ -48,6 +48,15 @@ export const Catalog: CatalogStory = { 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'], diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemToggle.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemToggle.stories.tsx index 48aba2d84b1..3e371cadde4 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemToggle.stories.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemToggle.stories.tsx @@ -44,6 +44,15 @@ export const Catalog: CatalogStory = { 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], diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/index.ts b/packages/twenty-ui/src/navigation/menu/menu-item/components/index.ts index 9bee971d34b..390e5120fd8 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/components/index.ts +++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/index.ts @@ -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'; diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.tsx index f32cb71ed7a..88fa9a978f1 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/MenuItemLeftContent.tsx @@ -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 } /> )} - {LeftIcon && ( - - )} + {LeftIcon && + (withIconContainer ? ( + + + + ) : ( + + ))} {LeftComponent} {isString(text) ? ( @@ -82,7 +90,13 @@ export const MenuItemLeftContent = ({ text )} {isString(contextualText) ? ( - {`· ${contextualText}`} + <> + {isNonEmptyString(contextualText) && ( + + + + )} + ) : ( contextualText )} diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.tsx index 6ae784315a8..e07e1f4de85 100644 --- a/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.tsx +++ b/packages/twenty-ui/src/navigation/menu/menu-item/internals/components/StyledMenuItemBase.tsx @@ -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)}; +`;