Files
units-ui/components/ui/CommandPalette.tsx
Sebastian Krüger eb556ddfce feat: ensure all category components use consistent hex colors
Update all category-related UI components to use getCategoryColorHex() function
instead of CSS variables for consistent color application across the app.

Changes:
- MainConverter: category buttons now use hex colors for background/border
- MainConverter: quick result display uses hex color for text and border
- MainConverter: result cards use hex color for left border
- CommandPalette: measure commands use hex colors for color indicators
- SearchUnits: category color dots use hex colors

This ensures all category colors are consistently applied using the same
hex color values defined in the OKLCH color palette.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 10:36:26 +01:00

225 lines
6.9 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { Command, Hash, Clock, Star, Moon, Sun } from 'lucide-react';
import { useTheme } from '@/components/providers/ThemeProvider';
import {
getAllMeasures,
formatMeasureName,
getCategoryColor,
getCategoryColorHex,
type Measure,
} from '@/lib/units';
import { getHistory, getFavorites } from '@/lib/storage';
import { cn } from '@/lib/utils';
interface CommandPaletteProps {
onSelectMeasure: (measure: Measure) => void;
onSelectUnit: (unit: string, measure: Measure) => void;
}
export default function CommandPalette({
onSelectMeasure,
onSelectUnit,
}: CommandPaletteProps) {
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const { theme, setTheme } = useTheme();
const inputRef = useRef<HTMLInputElement>(null);
// Commands
const commands = [
{
id: 'theme-light',
label: 'Switch to Light Mode',
icon: Sun,
action: () => setTheme('light'),
keywords: ['theme', 'light', 'mode'],
},
{
id: 'theme-dark',
label: 'Switch to Dark Mode',
icon: Moon,
action: () => setTheme('dark'),
keywords: ['theme', 'dark', 'mode'],
},
{
id: 'theme-system',
label: 'Use System Theme',
icon: Command,
action: () => setTheme('system'),
keywords: ['theme', 'system', 'auto'],
},
];
// Add measure commands
const measures = getAllMeasures();
const measureCommands = measures.map(measure => ({
id: `measure-${measure}`,
label: `Convert ${formatMeasureName(measure)}`,
icon: Hash,
action: () => onSelectMeasure(measure),
keywords: ['convert', measure, formatMeasureName(measure).toLowerCase()],
color: getCategoryColorHex(measure),
}));
const allCommands = [...commands, ...measureCommands];
// Filter commands
const filteredCommands = query
? allCommands.filter(cmd =>
cmd.keywords.some(kw => kw.toLowerCase().includes(query.toLowerCase())) ||
cmd.label.toLowerCase().includes(query.toLowerCase())
)
: allCommands;
// Keyboard shortcut to open
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setIsOpen(prev => !prev);
}
if (e.key === 'Escape') {
setIsOpen(false);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
// Focus input when opened
useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
setQuery('');
setSelectedIndex(0);
}
}, [isOpen]);
// Keyboard navigation
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(prev =>
prev < filteredCommands.length - 1 ? prev + 1 : prev
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(prev => (prev > 0 ? prev - 1 : prev));
} else if (e.key === 'Enter') {
e.preventDefault();
const command = filteredCommands[selectedIndex];
if (command) {
command.action();
setIsOpen(false);
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, selectedIndex, filteredCommands]);
// Reset selected index when query changes
useEffect(() => {
setSelectedIndex(0);
}, [query]);
if (!isOpen) return null;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50"
onClick={() => setIsOpen(false)}
/>
{/* Command Palette */}
<div className="fixed left-1/2 top-1/4 -translate-x-1/2 w-full max-w-2xl z-50 animate-scale-in">
<div className="bg-popover border rounded-lg shadow-2xl overflow-hidden">
{/* Search Input */}
<div className="flex items-center border-b px-4">
<Command className="h-5 w-5 text-muted-foreground" />
<input
ref={inputRef}
type="text"
placeholder="Type a command or search..."
value={query}
onChange={e => setQuery(e.target.value)}
className="flex-1 bg-transparent py-4 px-4 outline-none placeholder:text-muted-foreground"
/>
<kbd className="hidden sm:inline-block px-2 py-1 text-xs bg-muted rounded">
ESC
</kbd>
</div>
{/* Commands List */}
<div className="max-h-96 overflow-y-auto p-2">
{filteredCommands.length === 0 ? (
<div className="py-8 text-center text-muted-foreground">
No commands found
</div>
) : (
filteredCommands.map((command, index) => {
const Icon = command.icon;
return (
<button
key={command.id}
onClick={() => {
command.action();
setIsOpen(false);
}}
className={cn(
'w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors text-left',
index === selectedIndex
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent/50'
)}
>
{command.color ? (
<div
className="w-5 h-5 rounded flex-shrink-0"
style={{
backgroundColor: command.color,
}}
/>
) : (
<Icon className="h-5 w-5 flex-shrink-0 text-muted-foreground" />
)}
<span className="flex-1">{command.label}</span>
</button>
);
})
)}
</div>
{/* Footer */}
<div className="border-t px-4 py-2 text-xs text-muted-foreground flex items-center gap-4">
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded"></kbd>
<kbd className="px-1.5 py-0.5 bg-muted rounded"></kbd>
<span>Navigate</span>
</div>
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded">Enter</kbd>
<span>Select</span>
</div>
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-muted rounded">ESC</kbd>
<span>Close</span>
</div>
</div>
</div>
</div>
</>
);
}