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

@@ -5,6 +5,12 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title> <title>Tauri + React + Typescript</title>
<!-- Editor fonts (monospace) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@200..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<!-- Preview fonts (serif) -->
<link href="https://fonts.googleapis.com/css2?family=Average&family=Crimson+Pro:ital,wght@0,200..900;1,200..900&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&display=swap" rel="stylesheet">
</head> </head>
<body> <body>

View File

@@ -22,16 +22,36 @@ function App() {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system'); const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>('light'); const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>('light');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [editorFont, setEditorFont] = useState('Source Code Pro');
const [editorFontSize, setEditorFontSize] = useState(14);
const [previewFont, setPreviewFont] = useState('Merriweather');
const [previewFontSize, setPreviewFontSize] = useState(16);
useEffect(() => { useEffect(() => {
const savedServer = localStorage.getItem('serverURL'); const savedServer = localStorage.getItem('serverURL');
const savedUsername = localStorage.getItem('username'); const savedUsername = localStorage.getItem('username');
const savedPassword = localStorage.getItem('password'); const savedPassword = localStorage.getItem('password');
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | 'system' | null; const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | 'system' | null;
const savedEditorFont = localStorage.getItem('editorFont');
const savedPreviewFont = localStorage.getItem('previewFont');
if (savedTheme) { if (savedTheme) {
setTheme(savedTheme); setTheme(savedTheme);
} }
if (savedEditorFont) {
setEditorFont(savedEditorFont);
}
if (savedPreviewFont) {
setPreviewFont(savedPreviewFont);
}
const savedEditorFontSize = localStorage.getItem('editorFontSize');
const savedPreviewFontSize = localStorage.getItem('previewFontSize');
if (savedEditorFontSize) {
setEditorFontSize(parseInt(savedEditorFontSize, 10));
}
if (savedPreviewFontSize) {
setPreviewFontSize(parseInt(savedPreviewFontSize, 10));
}
if (savedServer && savedUsername && savedPassword) { if (savedServer && savedUsername && savedPassword) {
const apiInstance = new NextcloudAPI({ const apiInstance = new NextcloudAPI({
@@ -117,6 +137,26 @@ function App() {
localStorage.setItem('theme', newTheme); localStorage.setItem('theme', newTheme);
}; };
const handleEditorFontChange = (font: string) => {
setEditorFont(font);
localStorage.setItem('editorFont', font);
};
const handlePreviewFontChange = (font: string) => {
setPreviewFont(font);
localStorage.setItem('previewFont', font);
};
const handleEditorFontSizeChange = (size: number) => {
setEditorFontSize(size);
localStorage.setItem('editorFontSize', size.toString());
};
const handlePreviewFontSizeChange = (size: number) => {
setPreviewFontSize(size);
localStorage.setItem('previewFontSize', size.toString());
};
const handleCreateNote = async () => { const handleCreateNote = async () => {
if (!api) return; if (!api) return;
try { try {
@@ -207,6 +247,14 @@ function App() {
onLogout={handleLogout} onLogout={handleLogout}
theme={theme} theme={theme}
onThemeChange={handleThemeChange} onThemeChange={handleThemeChange}
editorFont={editorFont}
onEditorFontChange={handleEditorFontChange}
editorFontSize={editorFontSize}
onEditorFontSizeChange={handleEditorFontSizeChange}
previewFont={previewFont}
onPreviewFontChange={handlePreviewFontChange}
previewFontSize={previewFontSize}
onPreviewFontSizeChange={handlePreviewFontSizeChange}
/> />
<NotesList <NotesList
notes={filteredNotes} notes={filteredNotes}
@@ -231,6 +279,10 @@ function App() {
categories={categories} categories={categories}
isFocusMode={isFocusMode} isFocusMode={isFocusMode}
onToggleFocusMode={() => setIsFocusMode(!isFocusMode)} onToggleFocusMode={() => setIsFocusMode(!isFocusMode)}
editorFont={editorFont}
editorFontSize={editorFontSize}
previewFont={previewFont}
previewFontSize={previewFontSize}
/> />
</div> </div>
); );

View File

@@ -1,5 +1,20 @@
import { useState, useEffect, useRef } from 'react'; 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 { interface CategoriesSidebarProps {
categories: string[]; categories: string[];
selectedCategory: string; selectedCategory: string;
@@ -11,6 +26,14 @@ interface CategoriesSidebarProps {
onLogout: () => void; onLogout: () => void;
theme: 'light' | 'dark' | 'system'; theme: 'light' | 'dark' | 'system';
onThemeChange: (theme: 'light' | 'dark' | 'system') => void; 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({ export function CategoriesSidebar({
@@ -24,6 +47,14 @@ export function CategoriesSidebar({
onLogout, onLogout,
theme, theme,
onThemeChange, onThemeChange,
editorFont,
onEditorFontChange,
editorFontSize,
onEditorFontSizeChange,
previewFont,
onPreviewFontChange,
previewFontSize,
onPreviewFontSizeChange,
}: CategoriesSidebarProps) { }: CategoriesSidebarProps) {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [newCategoryName, setNewCategoryName] = useState(''); const [newCategoryName, setNewCategoryName] = useState('');
@@ -174,7 +205,7 @@ export function CategoriesSidebar({
</div> </div>
{/* Theme Toggle */} {/* Theme Toggle */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-500 dark:text-gray-400">Theme</span> <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"> <div className="flex items-center space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
<button <button
@@ -218,6 +249,78 @@ export function CategoriesSidebar({
</button> </button>
</div> </div>
</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>
</div> </div>
); );

View File

@@ -13,10 +13,14 @@ interface NoteEditorProps {
categories: string[]; categories: string[];
isFocusMode?: boolean; isFocusMode?: boolean;
onToggleFocusMode?: () => void; 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 [localTitle, setLocalTitle] = useState('');
const [localContent, setLocalContent] = useState(''); const [localContent, setLocalContent] = useState('');
const [localCategory, setLocalCategory] = useState(''); const [localCategory, setLocalCategory] = useState('');
@@ -45,13 +49,18 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
return () => document.removeEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown);
}, [isFocusMode, onToggleFocusMode]); }, [isFocusMode, onToggleFocusMode]);
// Auto-resize textarea when content changes // Auto-resize textarea when content changes, switching from preview to edit, or font size changes
useEffect(() => { useEffect(() => {
if (textareaRef.current) { if (textareaRef.current && !isPreviewMode) {
textareaRef.current.style.height = 'auto'; // Use setTimeout to ensure DOM has updated
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; setTimeout(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
}
}, 0);
} }
}, [localContent]); }, [localContent, isPreviewMode, editorFontSize]);
useEffect(() => { useEffect(() => {
const loadNewNote = () => { const loadNewNote = () => {
@@ -136,7 +145,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
try { try {
const container = document.createElement('div'); const container = document.createElement('div');
container.style.fontFamily = 'Arial, sans-serif'; container.style.fontFamily = `"${previewFont}", Georgia, serif`;
container.style.fontSize = '12px'; container.style.fontSize = '12px';
container.style.lineHeight = '1.6'; container.style.lineHeight = '1.6';
container.style.color = '#000000'; container.style.color = '#000000';
@@ -149,6 +158,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
titleElement.style.fontWeight = 'bold'; titleElement.style.fontWeight = 'bold';
titleElement.style.color = '#000000'; titleElement.style.color = '#000000';
titleElement.style.textAlign = 'center'; titleElement.style.textAlign = 'center';
titleElement.style.fontFamily = `"${previewFont}", Georgia, serif`;
container.appendChild(titleElement); container.appendChild(titleElement);
const contentElement = document.createElement('div'); const contentElement = document.createElement('div');
@@ -159,6 +169,15 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
contentElement.style.color = '#000000'; contentElement.style.color = '#000000';
container.appendChild(contentElement); 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) // Create PDF using jsPDF's html() method (like dompdf)
const pdf = new jsPDF({ const pdf = new jsPDF({
orientation: 'portrait', orientation: 'portrait',
@@ -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' : ''}`}> <div className={`min-h-full ${isFocusMode ? 'max-w-3xl mx-auto w-full' : ''}`}>
{isPreviewMode ? ( {isPreviewMode ? (
<div <div
className={`prose prose-slate dark:prose-invert p-8 ${isFocusMode ? '' : 'max-w-none'}`} className={`prose prose-slate dark:prose-invert p-8 ${isFocusMode ? '' : 'max-w-none'} [&_code]:font-mono [&_pre]:font-mono`}
style={{ fontSize: `${fontSize}px` }} style={{ fontSize: `${previewFontSize}px`, fontFamily: previewFont }}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: marked.parse(localContent || '', { async: false }) as string __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 = 'auto';
e.target.style.height = e.target.scrollHeight + 'px'; 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" 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: `${fontSize}px`, lineHeight: '1.6', minHeight: '100%' }} style={{ fontSize: `${editorFontSize}px`, lineHeight: '1.6', minHeight: '100%', fontFamily: editorFont }}
placeholder="Start writing in markdown..." placeholder="Start writing in markdown..."
/> />
</div> </div>