Move the serach box to teh sidebar
This commit is contained in:
@@ -212,10 +212,10 @@ export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
{shouldShowHeader && (
|
{shouldShowHeader && (
|
||||||
<Header
|
<Header
|
||||||
onMenuClick={toggleSidebar}
|
onMenuClick={toggleSidebar}
|
||||||
sidebarCollapsed={isSidebarCollapsed}
|
sidebarCollapsed={isSidebarCollapsed}
|
||||||
className="z-[var(--z-fixed)]"
|
className="z-[var(--z-fixed)]"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 relative">
|
<div className="flex flex-1 relative">
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel'
|
|||||||
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
|
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
Search,
|
|
||||||
Bell,
|
Bell,
|
||||||
X
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -59,7 +58,7 @@ export interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HeaderRef {
|
export interface HeaderRef {
|
||||||
focusSearch: () => void;
|
// No search-related methods anymore
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,63 +99,28 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|||||||
clearAll
|
clearAll
|
||||||
} = useNotifications();
|
} = useNotifications();
|
||||||
|
|
||||||
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
|
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
|
||||||
|
|
||||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
const defaultSearchPlaceholder = searchPlaceholder || t('common:forms.search_placeholder', 'Search...');
|
const defaultSearchPlaceholder = searchPlaceholder || t('common:forms.search_placeholder', 'Search...');
|
||||||
|
|
||||||
// Focus search input
|
|
||||||
const focusSearch = useCallback(() => {
|
|
||||||
searchInputRef.current?.focus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Expose ref methods
|
// Expose ref methods
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
focusSearch,
|
// No search functions available anymore
|
||||||
}), [focusSearch]);
|
}), []);
|
||||||
|
|
||||||
// Handle search
|
|
||||||
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
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();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
// Cmd/Ctrl + K for search
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
||||||
e.preventDefault();
|
|
||||||
focusSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape to close menus
|
// Escape to close menus
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
setIsNotificationPanelOpen(false);
|
setIsNotificationPanelOpen(false);
|
||||||
if (isSearchFocused) {
|
|
||||||
searchInputRef.current?.blur();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [focusSearch, isSearchFocused]);
|
}, []);
|
||||||
|
|
||||||
// Close menus when clicking outside
|
// Close menus when clicking outside
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -238,71 +202,18 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Search */}
|
{/* Space for potential future content */ }
|
||||||
{showSearch && isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<form
|
<div className="hidden md:flex items-center flex-1 max-w-md mx-4">
|
||||||
onSubmit={handleSearchSubmit}
|
{/* Empty space to maintain layout consistency */}
|
||||||
className="hidden md:flex items-center flex-1 max-w-md mx-4"
|
</div>
|
||||||
>
|
|
||||||
<div className="relative w-full">
|
|
||||||
<div className="absolute left-3 top-0 bottom-0 flex items-center pointer-events-none">
|
|
||||||
<Search className="h-4 w-4 text-[var(--text-tertiary)]" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref={searchInputRef}
|
|
||||||
type="text"
|
|
||||||
value={searchValue}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
onFocus={() => 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 ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={clearSearch}
|
|
||||||
className="absolute right-3 top-0 bottom-0 flex items-center p-1 hover:bg-[var(--bg-tertiary)] rounded-full transition-colors"
|
|
||||||
aria-label={t('common:actions.clear', 'Clear search')}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3 text-[var(--text-tertiary)]" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="absolute right-3 top-0 bottom-0 flex items-center pointer-events-none">
|
|
||||||
<kbd className="hidden lg:inline-flex items-center justify-center h-5 px-1.5 text-xs text-[var(--text-tertiary)] font-mono bg-[var(--bg-tertiary)] rounded border border-[var(--border-primary)]">
|
|
||||||
⌘K
|
|
||||||
</kbd>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right section */}
|
{/* Right section */}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{/* Mobile search */}
|
{/* Placeholder for potential future items */ }
|
||||||
{showSearch && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={focusSearch}
|
|
||||||
className="md:hidden w-10 h-10 p-0 flex items-center justify-center"
|
|
||||||
aria-label={t('common:actions.search', 'Search')}
|
|
||||||
>
|
|
||||||
<Search className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Language selector */}
|
{/* Language selector */}
|
||||||
<CompactLanguageSelector className="w-auto min-w-[60px]" />
|
<CompactLanguageSelector className="w-auto min-w-[60px]" />
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
LogOut,
|
LogOut,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
X
|
X,
|
||||||
|
Search
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface SidebarProps {
|
||||||
@@ -143,6 +144,9 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
|||||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||||
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
|
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
|
||||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||||
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
const sidebarRef = React.useRef<HTMLDivElement>(null);
|
const sidebarRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Get subscription-aware navigation routes
|
// Get subscription-aware navigation routes
|
||||||
@@ -260,6 +264,29 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
|||||||
}
|
}
|
||||||
}, [navigate, onClose]);
|
}, [navigate, onClose]);
|
||||||
|
|
||||||
|
// Handle search
|
||||||
|
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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
|
// Handle logout
|
||||||
const handleLogout = useCallback(async () => {
|
const handleLogout = useCallback(async () => {
|
||||||
await logout();
|
await logout();
|
||||||
@@ -389,6 +416,42 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
|||||||
}
|
}
|
||||||
}, [isProfileMenuOpen]);
|
}, [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
|
// Render submenu overlay for collapsed sidebar
|
||||||
const renderSubmenuOverlay = (item: NavigationItem) => {
|
const renderSubmenuOverlay = (item: NavigationItem) => {
|
||||||
if (!item.children || item.children.length === 0) return null;
|
if (!item.children || item.children.length === 0) return null;
|
||||||
@@ -597,6 +660,55 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
|||||||
)}
|
)}
|
||||||
aria-label={t('common:accessibility.menu', 'Main navigation')}
|
aria-label={t('common:accessibility.menu', 'Main navigation')}
|
||||||
>
|
>
|
||||||
|
{/* Search */}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="px-4 pt-4" data-search>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSearchSubmit}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div className="absolute left-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||||
|
<Search className="h-4 w-4 text-[var(--text-tertiary)]" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
|
type="text"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
onFocus={() => 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 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearSearch}
|
||||||
|
className="absolute right-3 top-0 bottom-0 flex items-center p-1 hover:bg-[var(--bg-tertiary)] rounded-full transition-colors"
|
||||||
|
aria-label={t('common:actions.clear', 'Clear search')}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3 text-[var(--text-tertiary)]" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="absolute right-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||||
|
<kbd className="hidden lg:inline-flex items-center justify-center h-5 px-1.5 text-xs text-[var(--text-tertiary)] font-mono bg-[var(--bg-tertiary)] rounded border border-[var(--border-primary)]">
|
||||||
|
⌘K
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className={clsx('flex-1 overflow-y-auto', isCollapsed ? 'px-2 py-4' : 'p-4')}>
|
<nav className={clsx('flex-1 overflow-y-auto', isCollapsed ? 'px-2 py-4' : 'p-4')}>
|
||||||
<ul className={clsx(isCollapsed ? 'space-y-2' : 'space-y-2')}>
|
<ul className={clsx(isCollapsed ? 'space-y-2' : 'space-y-2')}>
|
||||||
@@ -750,6 +862,53 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile search - always visible in mobile view */}
|
||||||
|
<div className="p-4 border-b border-[var(--border-primary)]">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSearchSubmit}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div className="absolute left-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||||
|
<Search className="h-4 w-4 text-[var(--text-tertiary)]" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={searchInputRef}
|
||||||
|
type="text"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
onFocus={() => 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 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearSearch}
|
||||||
|
className="absolute right-3 top-0 bottom-0 flex items-center p-1 hover:bg-[var(--bg-tertiary)] rounded-full transition-colors"
|
||||||
|
aria-label={t('common:actions.clear', 'Clear search')}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3 text-[var(--text-tertiary)]" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="absolute right-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||||
|
<kbd className="hidden lg:inline-flex items-center justify-center h-5 px-1.5 text-xs text-[var(--text-tertiary)] font-mono bg-[var(--bg-tertiary)] rounded border border-[var(--border-primary)]">
|
||||||
|
⌘K
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 p-4 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
<nav className="flex-1 p-4 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
||||||
<ul className="space-y-2 pb-4">
|
<ul className="space-y-2 pb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user