diff --git a/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx b/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx
index ae21df17c..84951d3b2 100644
--- a/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx
@@ -1,7 +1,7 @@
-import { Button } from 'twenty-ui';
+import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Key } from 'ts-key-enum';
-import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
+import { Button, getOsControlSymbol } from 'twenty-ui';
export const CmdEnterActionButton = ({
title,
@@ -24,7 +24,7 @@ export const CmdEnterActionButton = ({
accent="blue"
size="medium"
onClick={onClick}
- shortcut={'⌘⏎'}
+ hotkeys={[getOsControlSymbol(), '⏎']}
/>
);
};
diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx
index 336b4f734..9805b6375 100644
--- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx
@@ -1,7 +1,7 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { IconLayoutSidebarRightExpand } from 'twenty-ui';
+import { IconLayoutSidebarRightExpand, getOsControlSymbol } from 'twenty-ui';
const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm};
@@ -46,7 +46,7 @@ export const RecordIndexActionMenuBarAllActionsButton = () => {
All Actions
- ⌘K
+ {getOsControlSymbol()}K
>
);
diff --git a/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx
index a35f7dbef..f054b12a5 100644
--- a/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx
@@ -14,7 +14,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import { Key } from 'ts-key-enum';
-import { Button, MenuItem } from 'twenty-ui';
+import { Button, MenuItem, getOsControlSymbol } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
export const RightDrawerActionMenuDropdown = () => {
@@ -68,7 +68,9 @@ export const RightDrawerActionMenuDropdown = () => {
RightDrawerActionMenuDropdownHotkeyScope.RightDrawerActionMenuDropdown,
}}
data-select-disable
- clickableComponent={}
+ clickableComponent={
+
+ }
dropdownPlacement="top-end"
dropdownOffset={{
y: parseInt(theme.spacing(2), 10),
diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral.ts b/packages/twenty-front/src/modules/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral.ts
index 2697d33fe..93a72a891 100644
--- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral.ts
+++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/constants/KeyboardShortcutsGeneral.ts
@@ -1,10 +1,11 @@
+import { getOsControlSymbol } from 'twenty-ui';
import { Shortcut, ShortcutType } from '../types/Shortcut';
export const KEYBOARD_SHORTCUTS_GENERAL: Shortcut[] = [
{
label: 'Open search',
type: ShortcutType.General,
- firstHotKey: '⌘',
+ firstHotKey: getOsControlSymbol(),
secondHotKey: 'K',
areSimultaneous: false,
},
diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx
index e42e647a0..de2815388 100644
--- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx
+++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx
@@ -1,6 +1,6 @@
import { useLocation } from 'react-router-dom';
import { useRecoilState, useSetRecoilState } from 'recoil';
-import { IconSearch, IconSettings } from 'twenty-ui';
+import { IconSearch, IconSettings, getOsControlSymbol } from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
@@ -45,7 +45,7 @@ export const MainNavigationDrawerItems = () => {
label="Search"
Icon={IconSearch}
onClick={toggleCommandMenu}
- keyboard={['⌘', 'K']}
+ keyboard={[getOsControlSymbol(), 'K']}
/>
{}}
- keyboard={['⌘', 'K']}
+ keyboard={[getOsControlSymbol(), 'K']}
/>
{
size={isMobile ? 'medium' : 'small'}
variant="secondary"
accent="default"
- shortcut={isMobile ? '' : '⌘K'}
+ hotkeys={[getOsControlSymbol(), 'K']}
ariaLabel="Open command menu"
onClick={openCommandMenu}
/>
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx
index 84d17d5da..d588e7ba5 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx
@@ -19,6 +19,7 @@ import {
IconUser,
IconUserCircle,
IconUsers,
+ getOsControlSymbol,
} from 'twenty-ui';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@@ -88,7 +89,7 @@ export const Default: Story = {
= {
: adornmentName === 'Count'
? { count: 3 }
: adornmentName === 'Keyboard Keys'
- ? { keyboard: ['⌘', 'K'] }
+ ? { keyboard: [getOsControlSymbol(), 'K'] }
: {},
},
],
diff --git a/packages/twenty-ui/src/input/button/components/Button.tsx b/packages/twenty-ui/src/input/button/components/Button.tsx
index 902801126..8f968644d 100644
--- a/packages/twenty-ui/src/input/button/components/Button.tsx
+++ b/packages/twenty-ui/src/input/button/components/Button.tsx
@@ -3,6 +3,8 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Pill } from '@ui/components/Pill/Pill';
import { IconComponent } from '@ui/display/icon/types/IconComponent';
+import { useIsMobile } from '@ui/utilities';
+import { getOsShortcutSeparator } from '@ui/utilities/device/getOsShortcutSeparator';
import React from 'react';
import { Link } from 'react-router-dom';
@@ -29,7 +31,7 @@ export type ButtonProps = {
to?: string;
target?: string;
dataTestId?: string;
- shortcut?: string;
+ hotkeys?: string[];
ariaLabel?: string;
} & React.ComponentProps<'button'>;
@@ -417,11 +419,13 @@ export const Button = ({
to,
target,
dataTestId,
- shortcut,
+ hotkeys,
ariaLabel,
}: ButtonProps) => {
const theme = useTheme();
+ const isMobile = useIsMobile();
+
return (
{Icon && }
{title}
- {shortcut && (
+ {hotkeys && !isMobile && (
<>
- {shortcut}
+ {hotkeys.join(getOsShortcutSeparator())}
>
)}
diff --git a/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx
index d53758a94..a52510fd5 100644
--- a/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx
+++ b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx
@@ -23,7 +23,7 @@ type Story = StoryObj;
export const Default: Story = {
argTypes: {
- shortcut: { control: false },
+ hotkeys: { control: false },
Icon: { control: false },
},
args: {
@@ -44,7 +44,7 @@ export const Default: Story = {
};
export const Catalog: CatalogStory = {
- args: { title: 'Filter', Icon: IconSearch, shortcut: '' },
+ args: { title: 'Filter', Icon: IconSearch, hotkeys: ['⌘', 'O'] },
argTypes: {
size: { control: false },
variant: { control: false },
@@ -127,7 +127,7 @@ export const SoonCatalog: CatalogStory = {
soon: { control: false },
position: { control: false },
className: { control: false },
- shortcut: { control: false },
+ hotkeys: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
@@ -199,7 +199,7 @@ export const PositionCatalog: CatalogStory = {
fullWidth: { control: false },
soon: { control: false },
position: { control: false },
- shortcut: { control: false },
+ hotkeys: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
@@ -266,7 +266,7 @@ export const PositionCatalog: CatalogStory = {
};
export const ShortcutCatalog: CatalogStory = {
- args: { title: 'Actions', shortcut: '⌘O' },
+ args: { title: 'Actions', hotkeys: ['⌘', 'O'] },
argTypes: {
size: { control: false },
variant: { control: false },
diff --git a/packages/twenty-ui/src/utilities/device/__tests__/getOsControlSymbol.test.ts b/packages/twenty-ui/src/utilities/device/__tests__/getOsControlSymbol.test.ts
new file mode 100644
index 000000000..bd97e08a3
--- /dev/null
+++ b/packages/twenty-ui/src/utilities/device/__tests__/getOsControlSymbol.test.ts
@@ -0,0 +1,27 @@
+import { getOsControlSymbol } from '../getOsControlSymbol';
+
+describe('getOsControlSymbol', () => {
+ let userAgentSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ userAgentSpy = jest.spyOn(window.navigator, 'userAgent', 'get');
+ });
+
+ afterEach(() => {
+ userAgentSpy.mockRestore();
+ });
+
+ it('should return Ctrl for Windows', () => {
+ userAgentSpy.mockReturnValue(
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
+ );
+ expect(getOsControlSymbol()).toBe('Ctrl');
+ });
+
+ it('should return ⌘ for Mac', () => {
+ userAgentSpy.mockReturnValue(
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
+ );
+ expect(getOsControlSymbol()).toBe('⌘');
+ });
+});
diff --git a/packages/twenty-ui/src/utilities/device/__tests__/getOsShortcutSeparator.test.ts b/packages/twenty-ui/src/utilities/device/__tests__/getOsShortcutSeparator.test.ts
new file mode 100644
index 000000000..16205be14
--- /dev/null
+++ b/packages/twenty-ui/src/utilities/device/__tests__/getOsShortcutSeparator.test.ts
@@ -0,0 +1,27 @@
+import { getOsShortcutSeparator } from '../getOsShortcutSeparator';
+
+describe('getOsShortcutSeparator', () => {
+ let userAgentSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ userAgentSpy = jest.spyOn(window.navigator, 'userAgent', 'get');
+ });
+
+ afterEach(() => {
+ userAgentSpy.mockRestore();
+ });
+
+ it('should return space for Windows', () => {
+ userAgentSpy.mockReturnValue(
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
+ );
+ expect(getOsShortcutSeparator()).toBe(' ');
+ });
+
+ it('should return empty string for Mac', () => {
+ userAgentSpy.mockReturnValue(
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
+ );
+ expect(getOsShortcutSeparator()).toBe('');
+ });
+});
diff --git a/packages/twenty-ui/src/utilities/device/getOsControlSymbol.ts b/packages/twenty-ui/src/utilities/device/getOsControlSymbol.ts
new file mode 100644
index 000000000..3278c3501
--- /dev/null
+++ b/packages/twenty-ui/src/utilities/device/getOsControlSymbol.ts
@@ -0,0 +1,7 @@
+import { getUserDevice } from '@ui/utilities/device/getUserDevice';
+
+export const getOsControlSymbol = () => {
+ const device = getUserDevice();
+
+ return device === 'mac' ? '⌘' : 'Ctrl';
+};
diff --git a/packages/twenty-ui/src/utilities/device/getOsShortcutSeparator.ts b/packages/twenty-ui/src/utilities/device/getOsShortcutSeparator.ts
new file mode 100644
index 000000000..c7bc029b8
--- /dev/null
+++ b/packages/twenty-ui/src/utilities/device/getOsShortcutSeparator.ts
@@ -0,0 +1,6 @@
+import { getUserDevice } from '@ui/utilities/device/getUserDevice';
+
+export const getOsShortcutSeparator = () => {
+ const device = getUserDevice();
+ return device === 'mac' ? '' : ' ';
+};
diff --git a/packages/twenty-ui/src/utilities/device/getUserDevice.ts b/packages/twenty-ui/src/utilities/device/getUserDevice.ts
new file mode 100644
index 000000000..1e724e079
--- /dev/null
+++ b/packages/twenty-ui/src/utilities/device/getUserDevice.ts
@@ -0,0 +1,26 @@
+export const getUserDevice = () => {
+ const userAgent = navigator.userAgent.toLowerCase();
+
+ if (userAgent.includes('mac os x') || userAgent.includes('macos')) {
+ return 'mac';
+ }
+
+ if (userAgent.includes('windows')) {
+ return 'windows';
+ }
+
+ if (userAgent.includes('linux')) {
+ return 'linux';
+ }
+
+ if (userAgent.includes('android')) return 'android';
+
+ if (
+ userAgent.includes('ios') ||
+ userAgent.includes('iphone') ||
+ userAgent.includes('ipad')
+ )
+ return 'ios';
+
+ return 'unknown';
+};
diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts
index d075abd62..f45ce2d7a 100644
--- a/packages/twenty-ui/src/utilities/index.ts
+++ b/packages/twenty-ui/src/utilities/index.ts
@@ -5,6 +5,9 @@ export * from './animation/components/AnimatedFadeOut';
export * from './animation/components/AnimatedTextWord';
export * from './animation/components/AnimatedTranslation';
export * from './color/utils/stringToHslColor';
+export * from './device/getOsControlSymbol';
+export * from './device/getOsShortcutSeparator';
+export * from './device/getUserDevice';
export * from './dimensions/components/ComputeNodeDimensions';
export * from './isDefined';
export * from './responsive/hooks/useIsMobile';