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:
@@ -5,6 +5,12 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
|
||||
<body>
|
||||
|
||||
52
src/App.tsx
52
src/App.tsx
@@ -22,16 +22,36 @@ function App() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
|
||||
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>('light');
|
||||
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(() => {
|
||||
const savedServer = localStorage.getItem('serverURL');
|
||||
const savedUsername = localStorage.getItem('username');
|
||||
const savedPassword = localStorage.getItem('password');
|
||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | 'system' | null;
|
||||
const savedEditorFont = localStorage.getItem('editorFont');
|
||||
const savedPreviewFont = localStorage.getItem('previewFont');
|
||||
|
||||
if (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) {
|
||||
const apiInstance = new NextcloudAPI({
|
||||
@@ -117,6 +137,26 @@ function App() {
|
||||
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 () => {
|
||||
if (!api) return;
|
||||
try {
|
||||
@@ -207,6 +247,14 @@ function App() {
|
||||
onLogout={handleLogout}
|
||||
theme={theme}
|
||||
onThemeChange={handleThemeChange}
|
||||
editorFont={editorFont}
|
||||
onEditorFontChange={handleEditorFontChange}
|
||||
editorFontSize={editorFontSize}
|
||||
onEditorFontSizeChange={handleEditorFontSizeChange}
|
||||
previewFont={previewFont}
|
||||
onPreviewFontChange={handlePreviewFontChange}
|
||||
previewFontSize={previewFontSize}
|
||||
onPreviewFontSizeChange={handlePreviewFontSizeChange}
|
||||
/>
|
||||
<NotesList
|
||||
notes={filteredNotes}
|
||||
@@ -231,6 +279,10 @@ function App() {
|
||||
categories={categories}
|
||||
isFocusMode={isFocusMode}
|
||||
onToggleFocusMode={() => setIsFocusMode(!isFocusMode)}
|
||||
editorFont={editorFont}
|
||||
editorFontSize={editorFontSize}
|
||||
previewFont={previewFont}
|
||||
previewFontSize={previewFontSize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
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;
|
||||
@@ -11,6 +26,14 @@ interface CategoriesSidebarProps {
|
||||
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({
|
||||
@@ -24,6 +47,14 @@ export function CategoriesSidebar({
|
||||
onLogout,
|
||||
theme,
|
||||
onThemeChange,
|
||||
editorFont,
|
||||
onEditorFontChange,
|
||||
editorFontSize,
|
||||
onEditorFontSizeChange,
|
||||
previewFont,
|
||||
onPreviewFontChange,
|
||||
previewFontSize,
|
||||
onPreviewFontSizeChange,
|
||||
}: CategoriesSidebarProps) {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newCategoryName, setNewCategoryName] = useState('');
|
||||
@@ -174,7 +205,7 @@ export function CategoriesSidebar({
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="flex items-center space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
|
||||
<button
|
||||
@@ -218,6 +249,78 @@ export function CategoriesSidebar({
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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 && !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';
|
||||
}
|
||||
}, [localContent]);
|
||||
}, 0);
|
||||
}
|
||||
}, [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');
|
||||
@@ -159,6 +169,15 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges, cat
|
||||
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({
|
||||
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' : ''}`}>
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user