Files
nextcloud-notes-desktop-app/src/components/CategoriesSidebar.tsx
drelich f06fc640b6 feat: Add customizable fonts for editor and preview
- Added Google Fonts: Source Code Pro, Roboto Mono, Inconsolata (editor)
  and Merriweather, Crimson Pro, Roboto Serif, Average (preview)
- Font face and size selectors in Categories sidebar with polished UI
- Editor font applied to markdown textarea
- Preview font applied to preview mode and PDF export
- Code blocks always render in monospace
- Settings persist in localStorage
- Fixed textarea height recalculation when switching from preview to edit
2026-03-17 20:39:33 +01:00

328 lines
15 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
const EDITOR_FONTS = [
{ name: 'Source Code Pro', value: 'Source Code Pro' },
{ name: 'Roboto Mono', value: 'Roboto Mono' },
{ name: 'Inconsolata', value: 'Inconsolata' },
{ name: 'System Mono', value: 'ui-monospace, monospace' },
];
const PREVIEW_FONTS = [
{ name: 'Merriweather', value: 'Merriweather' },
{ name: 'Crimson Pro', value: 'Crimson Pro' },
{ name: 'Roboto Serif', value: 'Roboto Serif' },
{ name: 'Average', value: 'Average' },
{ name: 'System Serif', value: 'ui-serif, Georgia, serif' },
];
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;
editorFont: string;
onEditorFontChange: (font: string) => void;
editorFontSize: number;
onEditorFontSizeChange: (size: number) => void;
previewFont: string;
onPreviewFontChange: (font: string) => void;
previewFontSize: number;
onPreviewFontSizeChange: (size: number) => void;
}
export function CategoriesSidebar({
categories,
selectedCategory,
onSelectCategory,
onCreateCategory,
isCollapsed,
onToggleCollapse,
username,
onLogout,
theme,
onThemeChange,
editorFont,
onEditorFontChange,
editorFontSize,
onEditorFontSizeChange,
previewFont,
onPreviewFontChange,
previewFontSize,
onPreviewFontSizeChange,
}: 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 mb-3">
<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>
{/* Font Settings */}
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700 space-y-3">
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Fonts</span>
{/* Editor Font */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">Editor</span>
</div>
<div className="flex gap-2">
<select
value={editorFont}
onChange={(e) => onEditorFontChange(e.target.value)}
className="flex-1 min-w-0 text-sm px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
style={{ fontFamily: editorFont }}
>
{EDITOR_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>
{font.name}
</option>
))}
</select>
<select
value={editorFontSize}
onChange={(e) => onEditorFontSizeChange(parseInt(e.target.value, 10))}
className="w-16 flex-shrink-0 text-sm px-2 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer text-center"
>
{[12, 13, 14, 15, 16, 17, 18, 20, 22, 24].map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
</div>
{/* Preview Font */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">Preview</span>
</div>
<div className="flex gap-2">
<select
value={previewFont}
onChange={(e) => onPreviewFontChange(e.target.value)}
className="flex-1 min-w-0 text-sm px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer"
style={{ fontFamily: previewFont }}
>
{PREVIEW_FONTS.map((font) => (
<option key={font.value} value={font.value} style={{ fontFamily: font.value }}>
{font.name}
</option>
))}
</select>
<select
value={previewFontSize}
onChange={(e) => onPreviewFontSizeChange(parseInt(e.target.value, 10))}
className="w-16 flex-shrink-0 text-sm px-2 py-1.5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent cursor-pointer text-center"
>
{[12, 13, 14, 15, 16, 17, 18, 20, 22, 24].map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
</div>
</div>
</div>
</div>
);
}