feat: optimize TopBar search with Cmd+K shortcut and improved styling
- Add global Cmd/Ctrl+K keyboard shortcut to focus search input - Show keyboard shortcut hint (⌘K) in search input when empty - Improve input styling: rounded-lg, softer borders, focus transitions - Improve dropdown: rounded-lg, larger shadow, better spacing - Larger result icons (h-8 w-8) with rounded-lg containers - Refined history section with uppercase label and muted icon - Search icon fades in on focus via group-focus-within Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+34
-11
@@ -49,6 +49,19 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Cmd/Ctrl+K 快捷键聚焦搜索框
|
||||||
|
useEffect(() => {
|
||||||
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||||
|
e.preventDefault();
|
||||||
|
inputRef.current?.focus();
|
||||||
|
setShowResults(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleGlobalKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleGlobalKeyDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
storageUtil
|
storageUtil
|
||||||
.get('app/searchHistory', [])
|
.get('app/searchHistory', [])
|
||||||
@@ -153,8 +166,8 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
|
|
||||||
{/* 中间:搜索容器 */}
|
{/* 中间:搜索容器 */}
|
||||||
<div ref={containerRef} className="flex-1 mx-4 max-w-md relative">
|
<div ref={containerRef} className="flex-1 mx-4 max-w-md relative">
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/60 group-focus-within:text-muted-foreground transition-colors pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -168,8 +181,13 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
onFocus={() => setShowResults(true)}
|
onFocus={() => setShowResults(true)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
aria-label={t('common_buttons_search')}
|
aria-label={t('common_buttons_search')}
|
||||||
className="w-full h-9 pl-9 pr-8 text-sm rounded-md border border-input bg-muted/50 transition-all placeholder:text-muted-foreground focus:bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:border-input"
|
className="w-full h-9 pl-9 pr-16 text-sm rounded-lg border border-border/60 bg-muted/40 transition-all placeholder:text-muted-foreground/50 focus:bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:border-input"
|
||||||
/>
|
/>
|
||||||
|
{!searchQuery && (
|
||||||
|
<kbd className="absolute right-3 top-1/2 -translate-y-1/2 hidden sm:inline-flex h-5 items-center gap-0.5 rounded border border-border/60 bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground/60 pointer-events-none">
|
||||||
|
⌘K
|
||||||
|
</kbd>
|
||||||
|
)}
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -187,8 +205,8 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
|
|
||||||
{/* 动态联想结果卡片 */}
|
{/* 动态联想结果卡片 */}
|
||||||
{showResults && (searchQuery.trim() || displayedHistory.length > 0) && (
|
{showResults && (searchQuery.trim() || displayedHistory.length > 0) && (
|
||||||
<div className="absolute top-full left-0 right-0 mt-1 bg-popover text-popover-foreground rounded-md shadow-md border border-border max-h-80 overflow-y-auto z-50 animate-in fade-in slide-in-from-top-1 duration-150">
|
<div className="absolute top-full left-0 right-0 mt-1.5 bg-popover text-popover-foreground rounded-lg shadow-lg border border-border max-h-80 overflow-y-auto z-50 animate-in fade-in slide-in-from-top-2 duration-150">
|
||||||
<ul role="listbox" className="p-1">
|
<ul role="listbox" className="p-1.5">
|
||||||
{searchQuery.trim() ? (
|
{searchQuery.trim() ? (
|
||||||
searchResults.length > 0 ? (
|
searchResults.length > 0 ? (
|
||||||
searchResults.map((feature, index) => (
|
searchResults.map((feature, index) => (
|
||||||
@@ -198,20 +216,25 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
aria-selected={selectedIndex === index}
|
aria-selected={selectedIndex === index}
|
||||||
onClick={() => handleSelectFeature(feature)}
|
onClick={() => handleSelectFeature(feature)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 px-2.5 py-2 rounded-sm cursor-pointer text-sm transition-colors',
|
'flex items-center gap-3 px-3 py-2.5 rounded-md cursor-pointer text-sm transition-colors',
|
||||||
selectedIndex === index
|
selectedIndex === index
|
||||||
? 'bg-accent text-accent-foreground'
|
? 'bg-accent text-accent-foreground'
|
||||||
: 'hover:bg-muted/60',
|
: 'hover:bg-muted/60',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-sm bg-muted text-muted-foreground">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
|
||||||
|
'bg-muted/80 text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{feature.icon && <feature.icon className="h-4 w-4" />}
|
{feature.icon && <feature.icon className="h-4 w-4" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium text-foreground truncate">
|
<p className="font-medium text-foreground truncate">
|
||||||
{t(feature.labelKey)}
|
{t(feature.labelKey)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||||
{t(feature.descriptionKey)}
|
{t(feature.descriptionKey)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,7 +247,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="px-2.5 py-1.5 text-xs font-semibold tracking-wider text-muted-foreground/80">
|
<div className="px-3 py-1.5 text-[11px] font-semibold tracking-wider text-muted-foreground/60 uppercase">
|
||||||
{t('common:buttons.recentSearch')}
|
{t('common:buttons.recentSearch')}
|
||||||
</div>
|
</div>
|
||||||
{displayedHistory.map((item, index) => (
|
{displayedHistory.map((item, index) => (
|
||||||
@@ -237,13 +260,13 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
|||||||
setSelectedIndex(-1);
|
setSelectedIndex(-1);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 px-2.5 py-2 rounded-sm cursor-pointer text-sm transition-colors',
|
'flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm transition-colors',
|
||||||
selectedIndex === index
|
selectedIndex === index
|
||||||
? 'bg-accent text-accent-foreground'
|
? 'bg-accent text-accent-foreground'
|
||||||
: 'hover:bg-muted/60',
|
: 'hover:bg-muted/60',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<History className="h-4 w-4 text-muted-foreground shrink-0" />
|
<History className="h-4 w-4 text-muted-foreground/60 shrink-0" />
|
||||||
<span className="truncate">{item}</span>
|
<span className="truncate">{item}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user