From 34d1dff0f6690248b5918817918a0bd0bb681e5e Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:27:28 +0700 Subject: [PATCH] feat(sidebar): add expand/collapse functionality and search menu with keyboard shortcuts feat(i18n): add translations for expand/collapse menu and search menu --- .../modules/layouts/components/sidebar.tsx | 146 +++++++++++++++--- packages/core-i18n/src/locales/en/common.json | 5 +- packages/core-i18n/src/locales/id/common.json | 5 +- 3 files changed, 132 insertions(+), 24 deletions(-) diff --git a/apps/web/src/apps/modules/layouts/components/sidebar.tsx b/apps/web/src/apps/modules/layouts/components/sidebar.tsx index 744c396..1f855f4 100644 --- a/apps/web/src/apps/modules/layouts/components/sidebar.tsx +++ b/apps/web/src/apps/modules/layouts/components/sidebar.tsx @@ -1,10 +1,11 @@ -import { memo, useCallback, useMemo, useState, useRef } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Box, NavLink, Stack, Tooltip, ActionIcon, Divider, Menu, TextInput } from '@repo/ui/components'; import { useCoreAppShell } from '@repo/ui/components'; -import { ChevronsLeft, ChevronsRight, Search, X } from 'lucide-react'; +import { ChevronsLeft, ChevronsRight, PanelTopClose, PanelTopOpen, Search, X } from 'lucide-react'; import type { MenuItemType } from '../types/menu.types'; import type { SidebarVariant } from '@repo/ui/components'; +import { useTranslation } from '@repo/core-i18n'; // --------------------------------------------------------------------------- // Props @@ -32,9 +33,17 @@ interface MenuItemExpandedProps { item: MenuItemType; activeKeys: Set; isSearching?: boolean; + expandVersion?: number; + collapseVersion?: number; } -const MenuItemExpanded = memo(function MenuItemExpanded({ item, activeKeys, isSearching }: MenuItemExpandedProps) { +const MenuItemExpanded = memo(function MenuItemExpanded({ + item, + activeKeys, + isSearching, + expandVersion = 0, + collapseVersion = 0, +}: MenuItemExpandedProps) { const Icon = item.icon; const isActive = activeKeys.has(item.key); const hasChildren = item.children && item.children.length > 0; @@ -45,6 +54,14 @@ const MenuItemExpanded = memo(function MenuItemExpanded({ item, activeKeys, isSe const [opened, setOpened] = useState(isActive); const isOpened = isSearching || opened; + useEffect(() => { + if (expandVersion > 0) setOpened(true); + }, [expandVersion]); + + useEffect(() => { + if (collapseVersion > 0) setOpened(false); + }, [collapseVersion]); + return ( {hasChildren && item.children!.map((child) => ( - + ))} ); @@ -195,6 +219,8 @@ export const SidebarMenu = memo(function SidebarMenu({ withToggle = false, withMenuFilter = true, }: SidebarMenuProps) { + const { t } = useTranslation(); + const { pathname } = useLocation(); const { sidebarVariant: contextVariant, setSidebarVariant } = useCoreAppShell(); @@ -205,6 +231,20 @@ export const SidebarMenu = memo(function SidebarMenu({ const inputRef = useRef(null); const isSearching = searchQuery.trim().length > 0; + const [expandVersion, setExpandVersion] = useState(0); + const [collapseVersion, setCollapseVersion] = useState(0); + const [isAllExpanded, setIsAllExpanded] = useState(false); + + const handleToggleExpandAll = useCallback(() => { + if (isAllExpanded) { + setCollapseVersion((v) => v + 1); + setIsAllExpanded(false); + } else { + setExpandVersion((v) => v + 1); + setIsAllExpanded(true); + } + }, [isAllExpanded]); + // Recursive active state calculation const activeKeys = useMemo(() => { const keys = new Set(); @@ -291,6 +331,28 @@ export const SidebarMenu = memo(function SidebarMenu({ } }, [isMini, setSidebarVariant]); + const [isMac, setIsMac] = useState(true); + useEffect(() => { + setIsMac(typeof window !== 'undefined' && navigator.userAgent.includes('Mac')); + }, []); + + const shortcutText = isMac ? '⌘K' : 'Ctrl+K'; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + if (isMini) { + handleExpandAndSearch(); + } else { + inputRef.current?.focus(); + } + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isMini, handleExpandAndSearch]); + // -- Mini (Collapsed) Mode ------------------------------------------------ if (isMini) { return ( @@ -352,29 +414,69 @@ export const SidebarMenu = memo(function SidebarMenu({ borderBottom: '1px solid var(--mantine-color-default-border)', }} > - ) => setSearchQuery(e.currentTarget.value)} - leftSection={} - variant="filled" - radius="md" - size="xs" - rightSection={ - searchQuery ? ( - setSearchQuery('')} size="sm"> - - - ) : null - } - /> + + ) => setSearchQuery(e.currentTarget.value)} + leftSection={} + variant="filled" + radius="md" + size="xs" + style={{ flex: 1 }} + rightSectionWidth={searchQuery ? 30 : 48} + rightSection={ + searchQuery ? ( + setSearchQuery('')} size="sm"> + + + ) : ( + + {shortcutText} + + ) + } + /> + + + {isAllExpanded ? : } + + + )} {filteredItems.map((item) => ( - + ))} diff --git a/packages/core-i18n/src/locales/en/common.json b/packages/core-i18n/src/locales/en/common.json index 9e88c68..3bdf019 100644 --- a/packages/core-i18n/src/locales/en/common.json +++ b/packages/core-i18n/src/locales/en/common.json @@ -32,6 +32,9 @@ "systemInformation": "System Information", "systemInformationDesc": "View details about the system version and status.", "systemNotifications": "All Notifications", - "systemNotificationsDesc": "View and manage all system notifications." + "systemNotificationsDesc": "View and manage all system notifications.", + "expandAll": "Expand all menu", + "collapseAll": "Collapse all menu", + "searchMenu": "Search menu" } } \ No newline at end of file diff --git a/packages/core-i18n/src/locales/id/common.json b/packages/core-i18n/src/locales/id/common.json index 0e90c76..987ac7a 100644 --- a/packages/core-i18n/src/locales/id/common.json +++ b/packages/core-i18n/src/locales/id/common.json @@ -32,6 +32,9 @@ "systemInformation": "Informasi Sistem", "systemInformationDesc": "Lihat detail tentang versi dan status sistem.", "systemNotifications": "Semua Notifikasi", - "systemNotificationsDesc": "Lihat dan kelola semua notifikasi sistem." + "systemNotificationsDesc": "Lihat dan kelola semua notifikasi sistem.", + "expandAll": "Buka semua menu", + "collapseAll": "Tutup semua menu", + "searchMenu": "Cari menu" } } \ No newline at end of file