From 861eb1e103cbd4cac5e4bccb9b62373af19cfff9 Mon Sep 17 00:00:00 2001 From: drelich Date: Mon, 23 Mar 2026 16:08:26 +0100 Subject: [PATCH] feat: add custom category color picker with visual improvements - Add custom color picker for categories (10 pastel colors) - Store category colors in localStorage - Add real-time color updates across components using custom events - Change folder icons to filled/solid style for better visibility - Use vibrant darker shades for folder icon colors - Add 'Remove Color' option to reset category to default - Add color indicator dots (replaced with filled icons) - Improve hash distribution using FNV-1a algorithm for auto-assigned colors - Expand auto-assigned color palette from 10 to 20 colors --- src/components/CategoriesSidebar.tsx | 139 +++++++++++++++++++++------ src/components/NotesList.tsx | 53 ++++++++-- 2 files changed, 155 insertions(+), 37 deletions(-) diff --git a/src/components/CategoriesSidebar.tsx b/src/components/CategoriesSidebar.tsx index 8623f70..a9a82fa 100644 --- a/src/components/CategoriesSidebar.tsx +++ b/src/components/CategoriesSidebar.tsx @@ -37,6 +37,19 @@ interface CategoriesSidebarProps { onPreviewFontSizeChange: (size: number) => void; } +const CATEGORY_COLORS = [ + { name: 'Blue', bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', preview: '#dbeafe', dot: '#3b82f6' }, + { name: 'Green', bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300', preview: '#dcfce7', dot: '#22c55e' }, + { name: 'Purple', bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300', preview: '#f3e8ff', dot: '#a855f7' }, + { name: 'Pink', bg: 'bg-pink-100 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300', preview: '#fce7f3', dot: '#ec4899' }, + { name: 'Yellow', bg: 'bg-yellow-100 dark:bg-yellow-900/30', text: 'text-yellow-700 dark:text-yellow-300', preview: '#fef9c3', dot: '#eab308' }, + { name: 'Red', bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300', preview: '#fee2e2', dot: '#ef4444' }, + { name: 'Orange', bg: 'bg-orange-100 dark:bg-orange-900/30', text: 'text-orange-700 dark:text-orange-300', preview: '#ffedd5', dot: '#f97316' }, + { name: 'Teal', bg: 'bg-teal-100 dark:bg-teal-900/30', text: 'text-teal-700 dark:text-teal-300', preview: '#ccfbf1', dot: '#14b8a6' }, + { name: 'Indigo', bg: 'bg-indigo-100 dark:bg-indigo-900/30', text: 'text-indigo-700 dark:text-indigo-300', preview: '#e0e7ff', dot: '#6366f1' }, + { name: 'Cyan', bg: 'bg-cyan-100 dark:bg-cyan-900/30', text: 'text-cyan-700 dark:text-cyan-300', preview: '#cffafe', dot: '#06b6d4' }, +]; + export function CategoriesSidebar({ categories, selectedCategory, @@ -62,10 +75,34 @@ export function CategoriesSidebar({ const [newCategoryName, setNewCategoryName] = useState(''); const [renamingCategory, setRenamingCategory] = useState(null); const [renameCategoryValue, setRenameCategoryValue] = useState(''); + const [categoryColors, setCategoryColors] = useState>({}); + const [colorPickerCategory, setColorPickerCategory] = useState(null); const [isSettingsCollapsed, setIsSettingsCollapsed] = useState(true); const inputRef = useRef(null); const renameInputRef = useRef(null); + // Load category colors from localStorage + useEffect(() => { + const saved = localStorage.getItem('categoryColors'); + if (saved) { + setCategoryColors(JSON.parse(saved)); + } + }, []); + + const setCategoryColor = (category: string, colorIndex: number | null) => { + const updated = { ...categoryColors }; + if (colorIndex === null) { + delete updated[category]; + } else { + updated[category] = colorIndex; + } + setCategoryColors(updated); + localStorage.setItem('categoryColors', JSON.stringify(updated)); + setColorPickerCategory(null); + // Dispatch event to notify other components + window.dispatchEvent(new Event('categoryColorChanged')); + }; + useEffect(() => { if (isCreating && inputRef.current) { inputRef.current.focus(); @@ -198,35 +235,81 @@ export function CategoriesSidebar({ ) : ( -
- - + +
+ + +
+
+ {colorPickerCategory === category && ( +
+
+ {CATEGORY_COLORS.map((color, idx) => ( +
+ +
+ )} ) ))} diff --git a/src/components/NotesList.tsx b/src/components/NotesList.tsx index aa92a43..0533bfd 100644 --- a/src/components/NotesList.tsx +++ b/src/components/NotesList.tsx @@ -42,8 +42,29 @@ export function NotesList({ return saved ? parseInt(saved, 10) : 320; }); const [isResizing, setIsResizing] = React.useState(false); + const [, forceUpdate] = React.useReducer(x => x + 1, 0); const containerRef = React.useRef(null); + // Listen for category color changes + React.useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'categoryColors') { + forceUpdate(); + } + }; + + window.addEventListener('storage', handleStorageChange); + + // Also listen for changes in the same tab + const handleCustomEvent = () => forceUpdate(); + window.addEventListener('categoryColorChanged', handleCustomEvent); + + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('categoryColorChanged', handleCustomEvent); + }; + }, []); + const handleSync = async () => { setIsSyncing(true); await onSync(); @@ -121,25 +142,39 @@ export function NotesList({ }; const getCategoryColor = (category: string) => { - // Generate consistent pastel color based on category name - let hash = 0; - for (let i = 0; i < category.length; i++) { - hash = category.charCodeAt(i) + ((hash << 5) - hash); - } - - // Pastel color palette (light, subtle tones) + // Color palette matching CategoriesSidebar const colors = [ { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300' }, { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300' }, { bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300' }, { bg: 'bg-pink-100 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300' }, { bg: 'bg-yellow-100 dark:bg-yellow-900/30', text: 'text-yellow-700 dark:text-yellow-300' }, - { bg: 'bg-indigo-100 dark:bg-indigo-900/30', text: 'text-indigo-700 dark:text-indigo-300' }, { bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300' }, - { bg: 'bg-teal-100 dark:bg-teal-900/30', text: 'text-teal-700 dark:text-teal-300' }, { bg: 'bg-orange-100 dark:bg-orange-900/30', text: 'text-orange-700 dark:text-orange-300' }, + { bg: 'bg-teal-100 dark:bg-teal-900/30', text: 'text-teal-700 dark:text-teal-300' }, + { bg: 'bg-indigo-100 dark:bg-indigo-900/30', text: 'text-indigo-700 dark:text-indigo-300' }, { bg: 'bg-cyan-100 dark:bg-cyan-900/30', text: 'text-cyan-700 dark:text-cyan-300' }, ]; + + // Check for custom color in localStorage first + const savedColors = localStorage.getItem('categoryColors'); + if (savedColors) { + try { + const customColors = JSON.parse(savedColors); + if (customColors[category] !== undefined) { + return colors[customColors[category]]; + } + } catch (e) { + // Fall through to hash-based color + } + } + + // Fall back to hash-based color assignment + let hash = 2166136261; // FNV offset basis + for (let i = 0; i < category.length; i++) { + hash ^= category.charCodeAt(i); + hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); + } const index = Math.abs(hash) % colors.length; return colors[index];