feat: implement platform-specific keyboard shortcuts and refactor shortcut data management

This commit is contained in:
Firman Ramdhani
2026-07-13 10:17:07 +07:00
parent ad5be7056a
commit 164ec284ae
7 changed files with 58 additions and 28 deletions
@@ -6,6 +6,7 @@ import { ChevronsLeft, ChevronsRight, PanelTopClose, PanelTopOpen, Search, X } f
import type { MenuItemType } from '../types/menu.types'; import type { MenuItemType } from '../types/menu.types';
import type { SidebarVariant } from '@repo/ui/components'; import type { SidebarVariant } from '@repo/ui/components';
import { useTranslation } from '@repo/core-i18n'; import { useTranslation } from '@repo/core-i18n';
import { shortcutsData } from '@repo/ui/constants';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Props // Props
@@ -333,13 +334,20 @@ export const SidebarMenu = memo(function SidebarMenu({
} }
}, [isMini, setSidebarVariant]); }, [isMini, setSidebarVariant]);
const [isMac, setIsMac] = useState(true); const [isMac, setIsMac] = useState(false);
useEffect(() => { useEffect(() => {
setIsMac(typeof window !== 'undefined' && navigator.userAgent.includes('Mac')); setIsMac(typeof window !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent));
}, []); }, []);
const filterMenuShortcutText = isMac ? '⇧⌘M' : 'Ctrl+Shift+M'; const filterMenuShortcutText = useMemo(() => {
const toggleShortcutText = isMac ? '⇧⌘B' : 'Ctrl+Shift+B'; const shortcutData = shortcutsData.find((s) => s.key === 'search_menu');
return isMac ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
}, [isMac]);
const toggleShortcutText = useMemo(() => {
const shortcutData = shortcutsData.find((s) => s.key === 'collapse_sidebar');
return isMac ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
}, [isMac]);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -442,7 +450,7 @@ export const SidebarMenu = memo(function SidebarMenu({
radius="md" radius="md"
size="xs" size="xs"
style={{ flex: 1 }} style={{ flex: 1 }}
rightSectionWidth={searchQuery ? 30 : 48} rightSectionWidth={searchQuery ? 30 : isMac ? 48 : 68}
rightSection={ rightSection={
searchQuery ? ( searchQuery ? (
<ActionIcon variant="subtle" color="gray" onClick={() => setSearchQuery('')} size="sm"> <ActionIcon variant="subtle" color="gray" onClick={() => setSearchQuery('')} size="sm">
@@ -1,8 +1,8 @@
import { useTranslation } from '@repo/core-i18n'; import { useTranslation } from '@repo/core-i18n';
import { Group, Kbd, Table, Text, TextInput, Box, Center, ScrollArea } from '@repo/ui/components'; import { Group, Kbd, Table, Text, TextInput, Box, Center, ScrollArea } from '@repo/ui/components';
import { shortcutsData } from '@repo/ui/constants';
import { Search, Keyboard } from 'lucide-react'; import { Search, Keyboard } from 'lucide-react';
import { useEffect, useState, useMemo } from 'react'; import { useEffect, useState, useMemo, Fragment } from 'react';
import { shortcuts } from './data';
export function Shortcut() { export function Shortcut() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -11,15 +11,14 @@ export function Shortcut() {
// OS Detection for rendering appropriate keyboard shortcuts // OS Detection for rendering appropriate keyboard shortcuts
useEffect(() => { useEffect(() => {
setIsMac(typeof window !== 'undefined' && navigator.userAgent.includes('Mac')); setIsMac(typeof window !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent));
}, []); }, []);
// Filter logika pencarian berdasarkan label ATAU deskripsi yang sudah diterjemahkan
const filteredShortcuts = useMemo(() => { const filteredShortcuts = useMemo(() => {
const query = searchQuery.toLowerCase().trim(); const query = searchQuery.toLowerCase().trim();
if (!query) return shortcuts; if (!query) return shortcutsData;
return shortcuts.filter((item) => { return shortcutsData.filter((item) => {
const labelMatch = t(item.label).toLowerCase().includes(query); const labelMatch = t(item.label).toLowerCase().includes(query);
const descMatch = t(item.desc).toLowerCase().includes(query); const descMatch = t(item.desc).toLowerCase().includes(query);
return labelMatch || descMatch; return labelMatch || descMatch;
@@ -34,23 +33,30 @@ export function Shortcut() {
<Text size="sm" fw={600} style={{ color: 'var(--mantine-color-text)' }}> <Text size="sm" fw={600} style={{ color: 'var(--mantine-color-text)' }}>
{t(element.label)} {t(element.label)}
</Text> </Text>
<Text size="xs" c="dimmed" mt={4} style={{ maxWidth: '600px', lineHeight: 1.4 }}> <Text size="xs" c="dimmed" mt={4} style={{ lineHeight: 1.4 }}>
{t(element.desc)} {t(element.desc)}
</Text> </Text>
</Table.Td> </Table.Td>
<Table.Td w={250}> <Table.Td w={250}>
<Group gap={6} wrap="nowrap"> <Group gap={6} wrap="nowrap" align="center">
{keys.map((key, i) => ( {keys.map((key, i) => (
<Kbd key={i} size="md" style={{ fontSize: '12px' }}> <Fragment key={i}>
<Kbd size="md" style={{ fontSize: '12px' }}>
{key} {key}
</Kbd> </Kbd>
{i < keys.length - 1 && (
<Text size="sm" c="dimmed" fw={500}>
+
</Text>
)}
</Fragment>
))} ))}
</Group> </Group>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
); );
}); });
return ( return (
<Box> <Box>
<Box p="md" style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}> <Box p="md" style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
@@ -76,8 +82,8 @@ export function Shortcut() {
> >
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>{t('information:info.table.action')}</Table.Th> <Table.Th>{t('information:info.table.action').toUpperCase()}</Table.Th>
<Table.Th>{t('information:info.table.shortcut')}</Table.Th> <Table.Th>{t('information:info.table.shortcut').toUpperCase()}</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"extends": "@repo/typescript-config/react-app.json", "extends": "@repo/typescript-config/react-app.json",
"include": ["src"], "include": ["src", "../../packages/ui/src/constant/shortcut.data.ts"],
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx" "jsx": "react-jsx"
} }
+2 -1
View File
@@ -8,7 +8,8 @@
"./hooks": "./src/hooks/index.ts", "./hooks": "./src/hooks/index.ts",
"./provider": "./src/provider/index.ts", "./provider": "./src/provider/index.ts",
"./validators": "./src/validators/index.ts", "./validators": "./src/validators/index.ts",
"./foundations": "./src/foundations/index.ts" "./foundations": "./src/foundations/index.ts",
"./constants": "./src/constants/index.ts"
}, },
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1
View File
@@ -0,0 +1 @@
export * from './shortcut.data';
@@ -1,20 +1,29 @@
export const shortcuts = [ export const shortcutsData = [
{ {
key: 'create_data',
label: 'information:shortcuts.create.label', label: 'information:shortcuts.create.label',
desc: 'information:shortcuts.create.desc', desc: 'information:shortcuts.create.desc',
macKeys: ['', '', 'N'], macKeys: ['Cmd', 'Shift', 'N'],
macKeyIcons: ['⌘', '⇧', 'N'],
winKeys: ['Ctrl', 'Shift', 'N'], winKeys: ['Ctrl', 'Shift', 'N'],
winKeyIcons: ['Ctrl', '⇧', 'N'],
}, },
{ {
key: 'search_menu',
label: 'information:shortcuts.searchMenu.label', label: 'information:shortcuts.searchMenu.label',
desc: 'information:shortcuts.searchMenu.desc', desc: 'information:shortcuts.searchMenu.desc',
macKeys: ['', '', 'M'], macKeys: ['Cmd', 'Shift', 'M'],
macKeyIcons: ['⌘', '⇧', 'M'],
winKeys: ['Ctrl', 'Shift', 'M'], winKeys: ['Ctrl', 'Shift', 'M'],
winKeyIcons: ['Ctrl', '⇧', 'M'],
}, },
{ {
key: 'collapse_sidebar',
label: 'information:shortcuts.toggleSidebar.label', label: 'information:shortcuts.toggleSidebar.label',
desc: 'information:shortcuts.toggleSidebar.desc', desc: 'information:shortcuts.toggleSidebar.desc',
macKeys: ['', '', 'B'], macKeys: ['Cmd', 'Shift', 'B'],
macKeyIcons: ['⌘', '⇧', 'B'],
winKeys: ['Ctrl', 'Shift', 'B'], winKeys: ['Ctrl', 'Shift', 'B'],
winKeyIcons: ['Ctrl', '⇧', 'B'],
}, },
]; ];
@@ -10,6 +10,7 @@ import {
useEnterpriseModuleNavigationContext, useEnterpriseModuleNavigationContext,
useEnterpriseModuleTranslationContext, useEnterpriseModuleTranslationContext,
} from '../hooks/use-module.context'; } from '../hooks/use-module.context';
import { shortcutsData } from '../../../constants';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
@@ -18,9 +19,6 @@ import {
/** Detect macOS / iOS for displaying platform-specific shortcut labels. */ /** Detect macOS / iOS for displaying platform-specific shortcut labels. */
const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent); const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
/** Platform-aware shortcut label for the Create action. */
const CREATE_SHORTCUT_LABEL = IS_MAC ? '⇧⌘N' : 'Ctrl+Shift+N';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Component // Component
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -48,6 +46,13 @@ export function EnterpriseIndexPageProvider<E extends BaseEntity = BaseEntity>(p
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Global keyboard shortcut: Ctrl+Shift+N / ⌘+Shift+N → Create // Global keyboard shortcut: Ctrl+Shift+N / ⌘+Shift+N → Create
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
/** Platform-aware shortcut label for the Create action. */
const CREATE_SHORTCUT_LABEL = useMemo(() => {
const shortcutData = shortcutsData.find((s) => s.key === 'collapse_sidebar');
return IS_MAC ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
}, [IS_MAC]);
useEffect(() => { useEffect(() => {
function onKeyDown(e: KeyboardEvent) { function onKeyDown(e: KeyboardEvent) {
const isShortcut = (e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'n'; const isShortcut = (e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'n';