diff --git a/frontend/src/components/layout/AppShell/AppShell.tsx b/frontend/src/components/layout/AppShell/AppShell.tsx index aa7d0673..ef2ee889 100644 --- a/frontend/src/components/layout/AppShell/AppShell.tsx +++ b/frontend/src/components/layout/AppShell/AppShell.tsx @@ -212,10 +212,10 @@ export const AppShell = forwardRef(({ {/* Header */} {shouldShowHeader && (
+ onMenuClick={toggleSidebar} + sidebarCollapsed={isSidebarCollapsed} + className="z-[var(--z-fixed)]" + /> )}
diff --git a/frontend/src/components/layout/Header/Header.tsx b/frontend/src/components/layout/Header/Header.tsx index ad603509..b633beb8 100644 --- a/frontend/src/components/layout/Header/Header.tsx +++ b/frontend/src/components/layout/Header/Header.tsx @@ -13,7 +13,6 @@ import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel' import { CompactLanguageSelector } from '../../ui/LanguageSelector'; import { Menu, - Search, Bell, X } from 'lucide-react'; @@ -59,7 +58,7 @@ export interface HeaderProps { } export interface HeaderRef { - focusSearch: () => void; + // No search-related methods anymore } /** @@ -100,63 +99,28 @@ export const Header = forwardRef(({ clearAll } = useNotifications(); - const [isSearchFocused, setIsSearchFocused] = useState(false); - const [searchValue, setSearchValue] = useState(''); const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false); - const searchInputRef = React.useRef(null); const defaultSearchPlaceholder = searchPlaceholder || t('common:forms.search_placeholder', 'Search...'); - // Focus search input - const focusSearch = useCallback(() => { - searchInputRef.current?.focus(); - }, []); - // Expose ref methods React.useImperativeHandle(ref, () => ({ - focusSearch, - }), [focusSearch]); - - // Handle search - const handleSearchChange = useCallback((e: React.ChangeEvent) => { - setSearchValue(e.target.value); - }, []); - - const handleSearchSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (searchValue.trim()) { - // TODO: Implement search functionality - console.log('Search:', searchValue); - } - }, [searchValue]); - - const clearSearch = useCallback(() => { - setSearchValue(''); - searchInputRef.current?.focus(); - }, []); + // No search functions available anymore + }), []); // Keyboard shortcuts React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - // Cmd/Ctrl + K for search - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); - focusSearch(); - } - // Escape to close menus if (e.key === 'Escape') { setIsNotificationPanelOpen(false); - if (isSearchFocused) { - searchInputRef.current?.blur(); - } } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [focusSearch, isSearchFocused]); + }, []); // Close menus when clicking outside React.useEffect(() => { @@ -238,71 +202,18 @@ export const Header = forwardRef(({
)} - {/* Search */} - {showSearch && isAuthenticated && ( -
-
-
- -
- setIsSearchFocused(true)} - onBlur={() => setIsSearchFocused(false)} - placeholder={defaultSearchPlaceholder} - className={clsx( - 'w-full pl-10 pr-12 py-2.5 text-sm', - 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]', - 'rounded-lg transition-colors duration-200', - 'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20', - 'focus:border-[var(--color-primary)]', - 'placeholder:text-[var(--text-tertiary)]', - 'h-9' - )} - aria-label={t('common:accessibility.search', 'Search in the application')} - /> - {searchValue ? ( - - ) : ( -
- - ⌘K - -
- )} -
-
+ {/* Space for potential future content */ } + {isAuthenticated && ( +
+   {/* Empty space to maintain layout consistency */} +
)} {/* Right section */} {isAuthenticated && (
- {/* Mobile search */} - {showSearch && ( - - )} + {/* Placeholder for potential future items */ } {/* Language selector */} diff --git a/frontend/src/components/layout/Sidebar/Sidebar.tsx b/frontend/src/components/layout/Sidebar/Sidebar.tsx index 9bd17f00..6b2244a1 100644 --- a/frontend/src/components/layout/Sidebar/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar/Sidebar.tsx @@ -33,7 +33,8 @@ import { Menu, LogOut, MoreHorizontal, - X + X, + Search } from 'lucide-react'; export interface SidebarProps { @@ -143,6 +144,9 @@ export const Sidebar = forwardRef(({ const [expandedItems, setExpandedItems] = useState>(new Set()); const [hoveredItem, setHoveredItem] = useState(null); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); + const [isSearchFocused, setIsSearchFocused] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const searchInputRef = React.useRef(null); const sidebarRef = React.useRef(null); // Get subscription-aware navigation routes @@ -260,6 +264,29 @@ export const Sidebar = forwardRef(({ } }, [navigate, onClose]); + // Handle search + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, []); + + const handleSearchSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + if (searchValue.trim()) { + // TODO: Implement search functionality + console.log('Search:', searchValue); + } + }, [searchValue]); + + const clearSearch = useCallback(() => { + setSearchValue(''); + searchInputRef.current?.focus(); + }, []); + + // Focus search input + const focusSearch = useCallback(() => { + searchInputRef.current?.focus(); + }, []); + // Handle logout const handleLogout = useCallback(async () => { await logout(); @@ -389,6 +416,42 @@ export const Sidebar = forwardRef(({ } }, [isProfileMenuOpen]); + // Keyboard shortcuts for search and other functionality + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Cmd/Ctrl + K for search + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + focusSearch(); + } + + // Escape to close menus + if (e.key === 'Escape') { + setIsProfileMenuOpen(false); + if (onClose) onClose(); // Close mobile sidebar + if (isSearchFocused) { + searchInputRef.current?.blur(); + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [focusSearch, isSearchFocused, setIsProfileMenuOpen, onClose]); + + // Close search and menus when clicking outside + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest('[data-profile-menu]') && !target.closest('[data-search]')) { + setIsProfileMenuOpen(false); + } + }; + + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + }, []); + // Render submenu overlay for collapsed sidebar const renderSubmenuOverlay = (item: NavigationItem) => { if (!item.children || item.children.length === 0) return null; @@ -597,6 +660,55 @@ export const Sidebar = forwardRef(({ )} aria-label={t('common:accessibility.menu', 'Main navigation')} > + {/* Search */} + {!isCollapsed && ( +
+
+
+ +
+ setIsSearchFocused(true)} + onBlur={() => setIsSearchFocused(false)} + placeholder={t('common:forms.search_placeholder', 'Search...')} + className={clsx( + 'w-full pl-10 pr-12 py-2.5 text-sm', + 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]', + 'rounded-lg transition-colors duration-200', + 'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20', + 'focus:border-[var(--color-primary)]', + 'placeholder:text-[var(--text-tertiary)]', + 'h-9' + )} + aria-label={t('common:accessibility.search', 'Search in the application')} + /> + {searchValue ? ( + + ) : ( +
+ + ⌘K + +
+ )} +
+
+ )} + {/* Navigation */}
+ {/* Mobile search - always visible in mobile view */} +
+
+
+ +
+ setIsSearchFocused(true)} + onBlur={() => setIsSearchFocused(false)} + placeholder={t('common:forms.search_placeholder', 'Search...')} + className={clsx( + 'w-full pl-10 pr-12 py-2.5 text-sm', + 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]', + 'rounded-lg transition-colors duration-200', + 'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20', + 'focus:border-[var(--color-primary)]', + 'placeholder:text-[var(--text-tertiary)]', + 'h-9' + )} + aria-label={t('common:accessibility.search', 'Search in the application')} + /> + {searchValue ? ( + + ) : ( +
+ + ⌘K + +
+ )} +
+
+ {/* Navigation */}