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
This commit is contained in:
drelich
2026-03-17 20:39:33 +01:00
parent 28914207f6
commit f06fc640b6
4 changed files with 192 additions and 12 deletions

View File

@@ -13,10 +13,14 @@ interface NoteEditorProps {
categories: string[];
isFocusMode?: boolean;
onToggleFocusMode?: () => void;
editorFont?: string;
editorFontSize?: number;
previewFont?: string;
previewFontSize?: number;
}
export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, categories, isFocusMode, onToggleFocusMode }: NoteEditorProps) {
export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, categories, isFocusMode, onToggleFocusMode, editorFont = 'Source Code Pro', editorFontSize = 14, previewFont = 'Merriweather', previewFontSize = 16 }: NoteEditorProps) {
const [localTitle, setLocalTitle] = useState('');
const [localContent, setLocalContent] = useState('');
const [localCategory, setLocalCategory] = useState('');
@@ -45,13 +49,18 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isFocusMode, onToggleFocusMode]);
// Auto-resize textarea when content changes
// Auto-resize textarea when content changes, switching from preview to edit, or font size changes
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
if (textareaRef.current && !isPreviewMode) {
// Use setTimeout to ensure DOM has updated
setTimeout(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
}
}, 0);
}
}, [localContent]);
}, [localContent, isPreviewMode, editorFontSize]);
useEffect(() => {
const loadNewNote = () => {
@@ -136,7 +145,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
try {
const container = document.createElement('div');
container.style.fontFamily = 'Arial, sans-serif';
container.style.fontFamily = `"${previewFont}", Georgia, serif`;
container.style.fontSize = '12px';
container.style.lineHeight = '1.6';
container.style.color = '#000000';
@@ -149,6 +158,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
titleElement.style.fontWeight = 'bold';
titleElement.style.color = '#000000';
titleElement.style.textAlign = 'center';
titleElement.style.fontFamily = `"${previewFont}", Georgia, serif`;
container.appendChild(titleElement);
const contentElement = document.createElement('div');
@@ -158,6 +168,15 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
contentElement.style.lineHeight = '1.6';
contentElement.style.color = '#000000';
container.appendChild(contentElement);
// Apply monospace font to code elements
const style = document.createElement('style');
style.textContent = `
code, pre { font-family: "Source Code Pro", ui-monospace, monospace !important; }
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }
code { background: #f0f0f0; padding: 2px 4px; border-radius: 2px; }
`;
container.appendChild(style);
// Create PDF using jsPDF's html() method (like dompdf)
const pdf = new jsPDF({
@@ -561,8 +580,8 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
<div className={`min-h-full ${isFocusMode ? 'max-w-3xl mx-auto w-full' : ''}`}>
{isPreviewMode ? (
<div
className={`prose prose-slate dark:prose-invert p-8 ${isFocusMode ? '' : 'max-w-none'}`}
style={{ fontSize: `${fontSize}px` }}
className={`prose prose-slate dark:prose-invert p-8 ${isFocusMode ? '' : 'max-w-none'} [&_code]:font-mono [&_pre]:font-mono`}
style={{ fontSize: `${previewFontSize}px`, fontFamily: previewFont }}
dangerouslySetInnerHTML={{
__html: marked.parse(localContent || '', { async: false }) as string
}}
@@ -579,8 +598,8 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
e.target.style.height = 'auto';
e.target.style.height = e.target.scrollHeight + 'px';
}}
className="w-full resize-none border-none outline-none focus:ring-0 bg-transparent text-gray-900 dark:text-gray-100 font-mono overflow-hidden"
style={{ fontSize: `${fontSize}px`, lineHeight: '1.6', minHeight: '100%' }}
className="w-full resize-none border-none outline-none focus:ring-0 bg-transparent text-gray-900 dark:text-gray-100 overflow-hidden"
style={{ fontSize: `${editorFontSize}px`, lineHeight: '1.6', minHeight: '100%', fontFamily: editorFont }}
placeholder="Start writing in markdown..."
/>
</div>