feat: Major UI improvements - categories sidebar, floating toolbar, focus mode
Categories sidebar: - Collapsible first column with category management - Create new categories directly from sidebar - Thin tab when collapsed - Moved user info, logout, and theme selector here Note editor redesign: - Clean toolbar with pill-style buttons - Category dropdown with folder icon - Preview toggle in toolbar - Streamlined action buttons Floating formatting toolbar: - Appears on text selection - Bold, italic, strikethrough, code, code block, quote, lists, link, headings - Active state highlighting for applied formats - Toggle behavior removes formatting if already applied Focus mode: - Hides sidebars for distraction-free writing - Content centered with max-width - Escape key to exit - Scrolling works from anywhere in viewport
This commit is contained in:
224
src/components/CategoriesSidebar.tsx
Normal file
224
src/components/CategoriesSidebar.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface CategoriesSidebarProps {
|
||||
categories: string[];
|
||||
selectedCategory: string;
|
||||
onSelectCategory: (category: string) => void;
|
||||
onCreateCategory: (name: string) => void;
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
username: string;
|
||||
onLogout: () => void;
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
onThemeChange: (theme: 'light' | 'dark' | 'system') => void;
|
||||
}
|
||||
|
||||
export function CategoriesSidebar({
|
||||
categories,
|
||||
selectedCategory,
|
||||
onSelectCategory,
|
||||
onCreateCategory,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
username,
|
||||
onLogout,
|
||||
theme,
|
||||
onThemeChange,
|
||||
}: CategoriesSidebarProps) {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newCategoryName, setNewCategoryName] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreating && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isCreating]);
|
||||
|
||||
const handleCreateCategory = () => {
|
||||
if (newCategoryName.trim()) {
|
||||
onCreateCategory(newCategoryName.trim());
|
||||
setNewCategoryName('');
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreateCategory();
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsCreating(false);
|
||||
setNewCategoryName('');
|
||||
}
|
||||
};
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="w-6 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 border-r border-gray-300 dark:border-gray-600 flex items-center justify-center transition-colors group relative"
|
||||
title="Show Categories"
|
||||
>
|
||||
<div className="absolute inset-y-0 left-0 w-1 bg-blue-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<svg className="w-4 h-4 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-gray-100 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Categories</h2>
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
title="Collapse"
|
||||
>
|
||||
<svg className="w-4 h-4 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="w-full px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors flex items-center justify-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
New Category
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
onClick={() => onSelectCategory('')}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center ${
|
||||
selectedCategory === ''
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
||||
: 'hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">All Notes</span>
|
||||
</button>
|
||||
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => onSelectCategory(category)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center ${
|
||||
selectedCategory === category
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
||||
: 'hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<span className="text-sm truncate">{category}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{isCreating && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-white dark:bg-gray-700 rounded-lg border border-gray-300 dark:border-gray-600">
|
||||
<svg className="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={newCategoryName}
|
||||
onChange={(e) => setNewCategoryName(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => {
|
||||
if (newCategoryName.trim()) {
|
||||
handleCreateCategory();
|
||||
} else {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}}
|
||||
placeholder="Category name..."
|
||||
className="flex-1 text-sm px-0 py-0 border-none bg-transparent text-gray-900 dark:text-gray-100 focus:ring-0 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Info and Settings */}
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-white dark:bg-gray-900">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0">
|
||||
{username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200 truncate font-medium">{username}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex-shrink-0"
|
||||
title="Logout"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Theme</span>
|
||||
<div className="flex items-center space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => onThemeChange('light')}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
theme === 'light'
|
||||
? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
|
||||
}`}
|
||||
title="Light mode"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onThemeChange('dark')}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
theme === 'dark'
|
||||
? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
|
||||
}`}
|
||||
title="Dark mode"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onThemeChange('system')}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
theme === 'system'
|
||||
? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
|
||||
}`}
|
||||
title="System theme"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user