Merge pull request #92 from gamosoft/features/new-features
Features/new features
This commit is contained in:
commit
bcdc10a5d5
|
|
@ -7,7 +7,7 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Optional, Tuple
|
from typing import List, Dict, Optional, Tuple
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
# In-memory cache for parsed tags
|
# In-memory cache for parsed tags
|
||||||
|
|
@ -201,7 +201,7 @@ def get_all_notes(notes_dir: str) -> List[Dict]:
|
||||||
"name": md_file.stem,
|
"name": md_file.stem,
|
||||||
"path": str(relative_path.as_posix()),
|
"path": str(relative_path.as_posix()),
|
||||||
"folder": str(relative_path.parent.as_posix()) if str(relative_path.parent) != "." else "",
|
"folder": str(relative_path.parent.as_posix()) if str(relative_path.parent) != "." else "",
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"type": "note",
|
"type": "note",
|
||||||
"tags": tags
|
"tags": tags
|
||||||
|
|
@ -354,8 +354,8 @@ def create_note_metadata(notes_dir: str, note_path: str) -> Dict:
|
||||||
line_count = sum(1 for _ in f)
|
line_count = sum(1 for _ in f)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"created": datetime.fromtimestamp(stat.st_ctime).isoformat(),
|
"created": datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc).isoformat(),
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"lines": line_count
|
"lines": line_count
|
||||||
}
|
}
|
||||||
|
|
@ -477,7 +477,7 @@ def get_all_images(notes_dir: str) -> List[Dict]:
|
||||||
"name": image_file.name,
|
"name": image_file.name,
|
||||||
"path": str(relative_path.as_posix()),
|
"path": str(relative_path.as_posix()),
|
||||||
"folder": str(relative_path.parent.as_posix()),
|
"folder": str(relative_path.parent.as_posix()),
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"type": "image"
|
"type": "image"
|
||||||
})
|
})
|
||||||
|
|
@ -667,7 +667,7 @@ def get_notes_by_tag(notes_dir: str, tag: str) -> List[Dict]:
|
||||||
"name": md_file.stem,
|
"name": md_file.stem,
|
||||||
"path": str(relative_path.as_posix()),
|
"path": str(relative_path.as_posix()),
|
||||||
"folder": str(relative_path.parent.as_posix()) if str(relative_path.parent) != "." else "",
|
"folder": str(relative_path.parent.as_posix()) if str(relative_path.parent) != "." else "",
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"tags": tags
|
"tags": tags
|
||||||
})
|
})
|
||||||
|
|
@ -712,7 +712,7 @@ def get_templates(notes_dir: str) -> List[Dict]:
|
||||||
templates.append({
|
templates.append({
|
||||||
"name": template_file.stem,
|
"name": template_file.stem,
|
||||||
"path": str(template_file.relative_to(notes_dir).as_posix()),
|
"path": str(template_file.relative_to(notes_dir).as_posix()),
|
||||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat()
|
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error reading template {template_file}: {e}")
|
print(f"Error reading template {template_file}: {e}")
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@
|
||||||
- **Shareable links** - Bookmark or share direct links to notes with highlighted terms
|
- **Shareable links** - Bookmark or share direct links to notes with highlighted terms
|
||||||
- **Refresh safe** - Page reload keeps you on the same note with search context
|
- **Refresh safe** - Page reload keeps you on the same note with search context
|
||||||
- **Copy link button** - One-click copy of note URL to clipboard
|
- **Copy link button** - One-click copy of note URL to clipboard
|
||||||
|
- **Last edited indicator** - Shows relative time since last edit (e.g., "Edited 2h ago")
|
||||||
|
- **Favorites** - Star notes for quick access; displayed at top of sidebar
|
||||||
|
|
||||||
## 🎨 Customization
|
## 🎨 Customization
|
||||||
|
|
||||||
|
|
@ -265,7 +267,9 @@ date: {{date}}
|
||||||
| `Ctrl+Alt+N` | `Cmd+Option+N` | New note |
|
| `Ctrl+Alt+N` | `Cmd+Option+N` | New note |
|
||||||
| `Ctrl+Alt+F` | `Cmd+Option+F` | New folder |
|
| `Ctrl+Alt+F` | `Cmd+Option+F` | New folder |
|
||||||
| `Ctrl+Z` | `Cmd+Z` | Undo |
|
| `Ctrl+Z` | `Cmd+Z` | Undo |
|
||||||
| `Ctrl+Y` or `Ctrl+Shift+Z` | `Cmd+Y` or `Cmd+Shift+Z` | Redo |
|
| `Ctrl+Y` | `Cmd+Y` | Redo |
|
||||||
|
| `Ctrl+Shift+Z` | `Cmd+Shift+Z` | Toggle Zen Mode |
|
||||||
|
| `Esc` | `Esc` | Exit Zen Mode |
|
||||||
| `F3` | `F3` | Next search match |
|
| `F3` | `F3` | Next search match |
|
||||||
| `Shift+F3` | `Shift+F3` | Previous search match |
|
| `Shift+F3` | `Shift+F3` | Previous search match |
|
||||||
|
|
||||||
|
|
@ -278,6 +282,18 @@ date: {{date}}
|
||||||
| `Ctrl+K` | `Cmd+K` | Insert link | `[text](url)` |
|
| `Ctrl+K` | `Cmd+K` | Insert link | `[text](url)` |
|
||||||
| `Ctrl+Alt+T` | `Cmd+Option+T` | Insert table | 3x3 table placeholder |
|
| `Ctrl+Alt+T` | `Cmd+Option+T` | Insert table | 3x3 table placeholder |
|
||||||
|
|
||||||
|
## 🧘 Zen Mode
|
||||||
|
|
||||||
|
Full immersive distraction-free writing experience:
|
||||||
|
|
||||||
|
- **Full screen** - Uses browser Fullscreen API for true immersion
|
||||||
|
- **Hidden UI** - Sidebar, toolbar, and stats bar disappear
|
||||||
|
- **Centered editor** - Comfortable width for optimal reading
|
||||||
|
- **Larger text** - 18px font size with relaxed line spacing
|
||||||
|
- **Quick access** - Button in toolbar or `Ctrl+Shift+Z` shortcut
|
||||||
|
- **Easy exit** - Press `Esc`, click exit button, or use shortcut again
|
||||||
|
- **State preserved** - Returns to your previous view mode on exit
|
||||||
|
|
||||||
## 🚀 Performance
|
## 🚀 Performance
|
||||||
|
|
||||||
- **Instant loading** - No lag, no loading spinners
|
- **Instant loading** - No lag, no loading spinners
|
||||||
|
|
|
||||||
312
frontend/app.js
312
frontend/app.js
|
|
@ -56,8 +56,29 @@ function noteApp() {
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
lastSaved: false,
|
lastSaved: false,
|
||||||
linkCopied: false,
|
linkCopied: false,
|
||||||
|
zenMode: false,
|
||||||
|
previousViewMode: 'split',
|
||||||
|
favorites: [],
|
||||||
|
favoritesSet: new Set(), // For O(1) lookups
|
||||||
|
favoritesExpanded: true,
|
||||||
saveTimeout: null,
|
saveTimeout: null,
|
||||||
|
|
||||||
|
// Note lookup maps for O(1) wikilink resolution (built on loadNotes)
|
||||||
|
_noteLookup: {
|
||||||
|
byPath: new Map(), // path -> true
|
||||||
|
byPathLower: new Map(), // path.toLowerCase() -> true
|
||||||
|
byName: new Map(), // name (without .md) -> true
|
||||||
|
byNameLower: new Map(), // name.toLowerCase() -> true
|
||||||
|
byEndPath: new Map(), // '/filename' and '/filename.md' -> true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Preview rendering debounce
|
||||||
|
_previewDebounceTimeout: null,
|
||||||
|
_lastRenderedContent: '',
|
||||||
|
_cachedRenderedHTML: '',
|
||||||
|
_mathDebounceTimeout: null,
|
||||||
|
_mermaidDebounceTimeout: null,
|
||||||
|
|
||||||
// Theme state
|
// Theme state
|
||||||
currentTheme: 'light',
|
currentTheme: 'light',
|
||||||
availableThemes: [],
|
availableThemes: [],
|
||||||
|
|
@ -306,6 +327,8 @@ function noteApp() {
|
||||||
this.loadEditorWidth();
|
this.loadEditorWidth();
|
||||||
this.loadViewMode();
|
this.loadViewMode();
|
||||||
this.loadTagsExpanded();
|
this.loadTagsExpanded();
|
||||||
|
this.loadFavorites();
|
||||||
|
this.loadFavoritesExpanded();
|
||||||
this.loadSyntaxHighlightSetting();
|
this.loadSyntaxHighlightSetting();
|
||||||
|
|
||||||
// Parse URL and load specific note if provided
|
// Parse URL and load specific note if provided
|
||||||
|
|
@ -396,6 +419,11 @@ function noteApp() {
|
||||||
this.saveTagsExpanded();
|
this.saveTagsExpanded();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch favorites expanded state and save to localStorage
|
||||||
|
this.$watch('favoritesExpanded', () => {
|
||||||
|
this.saveFavoritesExpanded();
|
||||||
|
});
|
||||||
|
|
||||||
// Setup keyboard shortcuts (only once to prevent double triggers)
|
// Setup keyboard shortcuts (only once to prevent double triggers)
|
||||||
if (!window.__noteapp_shortcuts_initialized) {
|
if (!window.__noteapp_shortcuts_initialized) {
|
||||||
window.__noteapp_shortcuts_initialized = true;
|
window.__noteapp_shortcuts_initialized = true;
|
||||||
|
|
@ -424,8 +452,8 @@ function noteApp() {
|
||||||
this.undo();
|
this.undo();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl/Cmd + Y or Ctrl/Cmd + Shift + Z for redo
|
// Ctrl/Cmd + Y for redo (Ctrl+Shift+Z now opens Zen mode)
|
||||||
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.redo();
|
this.redo();
|
||||||
}
|
}
|
||||||
|
|
@ -468,6 +496,18 @@ function noteApp() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.insertTable();
|
this.insertTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl/Cmd + Shift + Z for Zen mode (only when note is open)
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'Z') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.toggleZenMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape to exit Zen mode (works anywhere)
|
||||||
|
if (e.key === 'Escape' && this.zenMode) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.toggleZenMode();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -482,6 +522,15 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for fullscreen changes (to sync zen mode state)
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
if (!document.fullscreenElement && this.zenMode) {
|
||||||
|
// User exited fullscreen manually, exit zen mode too
|
||||||
|
this.zenMode = false;
|
||||||
|
this.viewMode = this.previousViewMode;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load app configuration
|
// Load app configuration
|
||||||
|
|
@ -707,6 +756,7 @@ function noteApp() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.notes = data.notes;
|
this.notes = data.notes;
|
||||||
this.allFolders = data.folders || [];
|
this.allFolders = data.folders || [];
|
||||||
|
this.buildNoteLookupMaps(); // Build O(1) lookup maps
|
||||||
this.buildFolderTree();
|
this.buildFolderTree();
|
||||||
await this.loadTags(); // Load tags after notes are loaded
|
await this.loadTags(); // Load tags after notes are loaded
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -714,6 +764,56 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Build lookup maps for O(1) wikilink resolution
|
||||||
|
buildNoteLookupMaps() {
|
||||||
|
// Clear existing maps
|
||||||
|
this._noteLookup.byPath.clear();
|
||||||
|
this._noteLookup.byPathLower.clear();
|
||||||
|
this._noteLookup.byName.clear();
|
||||||
|
this._noteLookup.byNameLower.clear();
|
||||||
|
this._noteLookup.byEndPath.clear();
|
||||||
|
|
||||||
|
for (const note of this.notes) {
|
||||||
|
const path = note.path;
|
||||||
|
const pathLower = path.toLowerCase();
|
||||||
|
const name = note.name;
|
||||||
|
const nameLower = name.toLowerCase();
|
||||||
|
const nameWithoutMd = name.replace(/\.md$/i, '');
|
||||||
|
const nameWithoutMdLower = nameWithoutMd.toLowerCase();
|
||||||
|
|
||||||
|
// Store all variations for fast lookup
|
||||||
|
this._noteLookup.byPath.set(path, true);
|
||||||
|
this._noteLookup.byPath.set(path.replace(/\.md$/i, ''), true);
|
||||||
|
this._noteLookup.byPathLower.set(pathLower, true);
|
||||||
|
this._noteLookup.byPathLower.set(pathLower.replace(/\.md$/i, ''), true);
|
||||||
|
this._noteLookup.byName.set(name, true);
|
||||||
|
this._noteLookup.byName.set(nameWithoutMd, true);
|
||||||
|
this._noteLookup.byNameLower.set(nameLower, true);
|
||||||
|
this._noteLookup.byNameLower.set(nameWithoutMdLower, true);
|
||||||
|
|
||||||
|
// End path matching (for /folder/note style links)
|
||||||
|
this._noteLookup.byEndPath.set('/' + nameWithoutMdLower, true);
|
||||||
|
this._noteLookup.byEndPath.set('/' + nameLower, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fast O(1) check if a wikilink target exists
|
||||||
|
wikiLinkExists(linkTarget) {
|
||||||
|
const targetLower = linkTarget.toLowerCase();
|
||||||
|
|
||||||
|
// Check all lookup maps
|
||||||
|
return (
|
||||||
|
this._noteLookup.byPath.has(linkTarget) ||
|
||||||
|
this._noteLookup.byPath.has(linkTarget + '.md') ||
|
||||||
|
this._noteLookup.byPathLower.has(targetLower) ||
|
||||||
|
this._noteLookup.byPathLower.has(targetLower + '.md') ||
|
||||||
|
this._noteLookup.byName.has(linkTarget) ||
|
||||||
|
this._noteLookup.byNameLower.has(targetLower) ||
|
||||||
|
this._noteLookup.byEndPath.has('/' + targetLower) ||
|
||||||
|
this._noteLookup.byEndPath.has('/' + targetLower + '.md')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
// Load all tags
|
// Load all tags
|
||||||
async loadTags() {
|
async loadTags() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -917,6 +1017,111 @@ function noteApp() {
|
||||||
return note && note.tags ? note.tags : [];
|
return note && note.tags ? note.tags : [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ==================== FAVORITES ====================
|
||||||
|
|
||||||
|
// Load favorites from localStorage
|
||||||
|
loadFavorites() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('noteFavorites');
|
||||||
|
if (stored) {
|
||||||
|
this.favorites = JSON.parse(stored);
|
||||||
|
this.favoritesSet = new Set(this.favorites);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.favorites = [];
|
||||||
|
this.favoritesSet = new Set();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save favorites to localStorage
|
||||||
|
saveFavorites() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('noteFavorites', JSON.stringify(this.favorites));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not save favorites to localStorage');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if a note is favorited (O(1) lookup)
|
||||||
|
isFavorite(notePath) {
|
||||||
|
return this.favoritesSet.has(notePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toggle favorite status for a note
|
||||||
|
toggleFavorite(notePath = null) {
|
||||||
|
const path = notePath || this.currentNote;
|
||||||
|
if (!path) return;
|
||||||
|
|
||||||
|
if (this.favoritesSet.has(path)) {
|
||||||
|
// Remove from favorites
|
||||||
|
this.favoritesSet.delete(path);
|
||||||
|
this.favorites = this.favorites.filter(f => f !== path);
|
||||||
|
} else {
|
||||||
|
// Add to favorites
|
||||||
|
this.favoritesSet.add(path);
|
||||||
|
this.favorites.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveFavorites();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get favorite notes with full details (for display)
|
||||||
|
get favoriteNotes() {
|
||||||
|
return this.favorites
|
||||||
|
.map(path => {
|
||||||
|
const note = this.notes.find(n => n.path === path);
|
||||||
|
if (!note) return null;
|
||||||
|
return {
|
||||||
|
path: note.path,
|
||||||
|
name: note.path.split('/').pop().replace('.md', ''),
|
||||||
|
folder: note.folder || ''
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean); // Remove nulls (deleted notes)
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFavoritesExpanded() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('favoritesExpanded');
|
||||||
|
if (saved !== null) {
|
||||||
|
this.favoritesExpanded = saved === 'true';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading favorites expanded state:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveFavoritesExpanded() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('favoritesExpanded', this.favoritesExpanded.toString());
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error saving favorites expanded state:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get current note's last modified time as relative string
|
||||||
|
get lastEditedText() {
|
||||||
|
if (!this.currentNote) return '';
|
||||||
|
const note = this.notes.find(n => n.path === this.currentNote);
|
||||||
|
if (!note || !note.modified) return '';
|
||||||
|
|
||||||
|
const modified = new Date(note.modified);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now - modified;
|
||||||
|
const diffSecs = Math.floor(diffMs / 1000);
|
||||||
|
const diffMins = Math.floor(diffSecs / 60);
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
|
||||||
|
if (diffSecs < 60) return 'just now';
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
if (diffDays < 7) return `${diffDays}d ago`;
|
||||||
|
|
||||||
|
// For older dates, show the date
|
||||||
|
return modified.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||||
|
},
|
||||||
|
|
||||||
// Parse tags from markdown content (matches backend logic)
|
// Parse tags from markdown content (matches backend logic)
|
||||||
parseTagsFromContent(content) {
|
parseTagsFromContent(content) {
|
||||||
if (!content || !content.trim().startsWith('---')) {
|
if (!content || !content.trim().startsWith('---')) {
|
||||||
|
|
@ -1839,6 +2044,8 @@ function noteApp() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
this.currentNote = notePath;
|
this.currentNote = notePath;
|
||||||
|
this._lastRenderedContent = ''; // Clear render cache for new note
|
||||||
|
this._cachedRenderedHTML = '';
|
||||||
this.noteContent = data.content;
|
this.noteContent = data.content;
|
||||||
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
||||||
this.currentImage = ''; // Clear image viewer when loading a note
|
this.currentImage = ''; // Clear image viewer when loading a note
|
||||||
|
|
@ -2707,6 +2914,8 @@ function noteApp() {
|
||||||
this.currentNote = '';
|
this.currentNote = '';
|
||||||
this.noteContent = '';
|
this.noteContent = '';
|
||||||
this.currentNoteName = '';
|
this.currentNoteName = '';
|
||||||
|
this._lastRenderedContent = ''; // Clear render cache
|
||||||
|
this._cachedRenderedHTML = '';
|
||||||
// Redirect to root
|
// Redirect to root
|
||||||
window.history.replaceState({}, '', '/');
|
window.history.replaceState({}, '', '/');
|
||||||
}
|
}
|
||||||
|
|
@ -2855,6 +3064,11 @@ function noteApp() {
|
||||||
get renderedMarkdown() {
|
get renderedMarkdown() {
|
||||||
if (!this.noteContent) return '<p style="color: var(--text-tertiary);">Nothing to preview yet...</p>';
|
if (!this.noteContent) return '<p style="color: var(--text-tertiary);">Nothing to preview yet...</p>';
|
||||||
|
|
||||||
|
// Performance: Return cached HTML if content hasn't changed
|
||||||
|
if (this.noteContent === this._lastRenderedContent && this._cachedRenderedHTML) {
|
||||||
|
return this._cachedRenderedHTML;
|
||||||
|
}
|
||||||
|
|
||||||
// Strip YAML frontmatter from content before rendering
|
// Strip YAML frontmatter from content before rendering
|
||||||
let contentToRender = this.noteContent;
|
let contentToRender = this.noteContent;
|
||||||
if (contentToRender.trim().startsWith('---')) {
|
if (contentToRender.trim().startsWith('---')) {
|
||||||
|
|
@ -2877,37 +3091,18 @@ function noteApp() {
|
||||||
|
|
||||||
// Convert Obsidian-style wikilinks: [[note]] or [[note|display text]]
|
// Convert Obsidian-style wikilinks: [[note]] or [[note|display text]]
|
||||||
// Must be done before marked.parse() to avoid conflicts with markdown syntax
|
// Must be done before marked.parse() to avoid conflicts with markdown syntax
|
||||||
const notes = this.notes; // Reference for closure
|
const self = this; // Reference for closure
|
||||||
contentToRender = contentToRender.replace(
|
contentToRender = contentToRender.replace(
|
||||||
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||||
(match, target, displayText) => {
|
(match, target, displayText) => {
|
||||||
const linkTarget = target.trim();
|
const linkTarget = target.trim();
|
||||||
const linkText = displayText ? displayText.trim() : linkTarget;
|
const linkText = displayText ? displayText.trim() : linkTarget;
|
||||||
const linkTargetLower = linkTarget.toLowerCase();
|
|
||||||
|
|
||||||
// Check if note exists (by path or by name, case-insensitive)
|
// Fast O(1) check using pre-built lookup maps
|
||||||
const noteExists = notes.some(n => {
|
// Handle section anchors: extract base note path
|
||||||
const pathLower = n.path.toLowerCase();
|
const hashIndex = linkTarget.indexOf('#');
|
||||||
const nameLower = n.name.toLowerCase();
|
const basePath = hashIndex !== -1 ? linkTarget.substring(0, hashIndex) : linkTarget;
|
||||||
return (
|
const noteExists = basePath === '' || self.wikiLinkExists(basePath);
|
||||||
// Exact path match
|
|
||||||
n.path === linkTarget ||
|
|
||||||
n.path === linkTarget + '.md' ||
|
|
||||||
// Case-insensitive path match
|
|
||||||
pathLower === linkTargetLower ||
|
|
||||||
pathLower === linkTargetLower + '.md' ||
|
|
||||||
// Name match (with or without .md)
|
|
||||||
n.name === linkTarget ||
|
|
||||||
n.name === linkTarget + '.md' ||
|
|
||||||
nameLower === linkTargetLower ||
|
|
||||||
nameLower === linkTargetLower + '.md' ||
|
|
||||||
// Match by filename at end of path
|
|
||||||
n.path.endsWith('/' + linkTarget) ||
|
|
||||||
n.path.endsWith('/' + linkTarget + '.md') ||
|
|
||||||
pathLower.endsWith('/' + linkTargetLower) ||
|
|
||||||
pathLower.endsWith('/' + linkTargetLower + '.md')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Escape special chars: href needs quote escaping, text needs HTML escaping
|
// Escape special chars: href needs quote escaping, text needs HTML escaping
|
||||||
const safeHref = linkTarget.replace(/"/g, '%22');
|
const safeHref = linkTarget.replace(/"/g, '%22');
|
||||||
|
|
@ -2985,11 +3180,13 @@ function noteApp() {
|
||||||
|
|
||||||
html = tempDiv.innerHTML;
|
html = tempDiv.innerHTML;
|
||||||
|
|
||||||
// Trigger MathJax rendering after DOM updates
|
// Debounced MathJax rendering (avoid re-running on every keystroke)
|
||||||
this.typesetMath();
|
if (this._mathDebounceTimeout) clearTimeout(this._mathDebounceTimeout);
|
||||||
|
this._mathDebounceTimeout = setTimeout(() => this.typesetMath(), 300);
|
||||||
|
|
||||||
// Render Mermaid diagrams
|
// Debounced Mermaid rendering
|
||||||
this.renderMermaid();
|
if (this._mermaidDebounceTimeout) clearTimeout(this._mermaidDebounceTimeout);
|
||||||
|
this._mermaidDebounceTimeout = setTimeout(() => this.renderMermaid(), 300);
|
||||||
|
|
||||||
// Apply syntax highlighting and add copy buttons to code blocks
|
// Apply syntax highlighting and add copy buttons to code blocks
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -3013,6 +3210,10 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
// Cache the result for performance
|
||||||
|
this._lastRenderedContent = this.noteContent;
|
||||||
|
this._cachedRenderedHTML = html;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -4043,6 +4244,55 @@ function noteApp() {
|
||||||
}, 1500);
|
}, 1500);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Toggle Zen Mode (full immersive writing experience)
|
||||||
|
async toggleZenMode() {
|
||||||
|
if (!this.zenMode) {
|
||||||
|
// Entering Zen Mode
|
||||||
|
this.previousViewMode = this.viewMode;
|
||||||
|
this.viewMode = 'edit';
|
||||||
|
this.mobileSidebarOpen = false;
|
||||||
|
this.zenMode = true;
|
||||||
|
|
||||||
|
// Request fullscreen
|
||||||
|
try {
|
||||||
|
const elem = document.documentElement;
|
||||||
|
if (elem.requestFullscreen) {
|
||||||
|
await elem.requestFullscreen();
|
||||||
|
} else if (elem.webkitRequestFullscreen) {
|
||||||
|
await elem.webkitRequestFullscreen();
|
||||||
|
} else if (elem.msRequestFullscreen) {
|
||||||
|
await elem.msRequestFullscreen();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fullscreen not supported or denied, continue anyway
|
||||||
|
console.log('Fullscreen not available:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus editor after transition
|
||||||
|
setTimeout(() => {
|
||||||
|
const editor = document.getElementById('note-editor');
|
||||||
|
if (editor) editor.focus();
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
// Exiting Zen Mode
|
||||||
|
this.zenMode = false;
|
||||||
|
this.viewMode = this.previousViewMode;
|
||||||
|
|
||||||
|
// Exit fullscreen
|
||||||
|
try {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
} else if (document.webkitExitFullscreen) {
|
||||||
|
await document.webkitExitFullscreen();
|
||||||
|
} else if (document.msExitFullscreen) {
|
||||||
|
await document.msExitFullscreen();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Exit fullscreen error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Homepage folder navigation methods
|
// Homepage folder navigation methods
|
||||||
goToHomepageFolder(folderPath) {
|
goToHomepageFolder(folderPath) {
|
||||||
this.showGraph = false; // Close graph when navigating
|
this.showGraph = false; // Close graph when navigating
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,67 @@
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Zen Mode Styles */
|
||||||
|
.zen-mode-active {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
.zen-mode-active .zen-hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.zen-mode-active .zen-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.zen-mode-active .zen-editor-container .editor-wrapper {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
.zen-mode-active .zen-editor-container .editor-textarea {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.zen-mode-active .zen-editor-container .syntax-overlay {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.zen-exit-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease, transform 0.2s ease;
|
||||||
|
z-index: 9999;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.zen-exit-button:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.zen-mode-active:hover .zen-exit-button {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zen mode fade transition */
|
||||||
|
.zen-fade-enter {
|
||||||
|
animation: zenFadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes zenFadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Syntax highlight colors using theme variables */
|
/* Syntax highlight colors using theme variables */
|
||||||
.syntax-overlay .md-heading { color: var(--accent-primary); font-weight: 600; }
|
.syntax-overlay .md-heading { color: var(--accent-primary); font-weight: 600; }
|
||||||
.syntax-overlay .md-bold { color: var(--text-primary); font-weight: 700; }
|
.syntax-overlay .md-bold { color: var(--text-primary); font-weight: 700; }
|
||||||
|
|
@ -1000,7 +1061,23 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body x-data="noteApp()" x-init="init()" style="background-color: var(--bg-primary);">
|
<body x-data="noteApp()" x-init="init()" style="background-color: var(--bg-primary);" :class="{'zen-mode-active': zenMode}">
|
||||||
|
|
||||||
|
<!-- Zen Mode Exit Button -->
|
||||||
|
<button
|
||||||
|
x-show="zenMode"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4"
|
||||||
|
x-transition:enter-end="opacity-60 translate-y-0"
|
||||||
|
@click="toggleZenMode()"
|
||||||
|
class="zen-exit-button"
|
||||||
|
title="Exit Zen Mode (Esc)"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Exit Zen</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Mobile Overlay -->
|
<!-- Mobile Overlay -->
|
||||||
<div
|
<div
|
||||||
|
|
@ -1014,7 +1091,7 @@
|
||||||
|
|
||||||
<!-- Sidebar with Icon Rail -->
|
<!-- Sidebar with Icon Rail -->
|
||||||
<div
|
<div
|
||||||
class="flex mobile-sidebar"
|
class="flex mobile-sidebar zen-hide"
|
||||||
:class="{'mobile-sidebar-open': mobileSidebarOpen}"
|
:class="{'mobile-sidebar-open': mobileSidebarOpen}"
|
||||||
:style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); min-width: 200px; max-width: 600px;'"
|
:style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); min-width: 200px; max-width: 600px;'"
|
||||||
>
|
>
|
||||||
|
|
@ -1138,6 +1215,62 @@
|
||||||
|
|
||||||
<!-- Folder Tree -->
|
<!-- Folder Tree -->
|
||||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-2">
|
<div class="flex-1 overflow-y-auto custom-scrollbar p-2">
|
||||||
|
|
||||||
|
<!-- Favorites Section -->
|
||||||
|
<div x-show="favoriteNotes.length > 0" class="mb-3">
|
||||||
|
<div
|
||||||
|
@click="favoritesExpanded = !favoritesExpanded"
|
||||||
|
class="flex items-center justify-between px-2 py-1 cursor-pointer rounded hover:bg-opacity-50 transition-colors"
|
||||||
|
style="color: var(--text-tertiary);"
|
||||||
|
@mouseenter="$el.style.backgroundColor = 'var(--bg-hover)'"
|
||||||
|
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24" style="color: var(--warning, #eab308);">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wide">Favorites</span>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 transition-transform"
|
||||||
|
:class="{'rotate-90': favoritesExpanded}"
|
||||||
|
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div x-show="favoritesExpanded" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0 -translate-y-1" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="mt-1">
|
||||||
|
<template x-for="fav in favoriteNotes" :key="fav.path">
|
||||||
|
<div
|
||||||
|
@click="loadNote(fav.path)"
|
||||||
|
class="flex items-center justify-between px-2 py-1.5 rounded cursor-pointer group"
|
||||||
|
:class="{'bg-opacity-50': currentNote === fav.path}"
|
||||||
|
:style="currentNote === fav.path ? 'background-color: var(--accent-primary); color: white;' : 'color: var(--text-secondary);'"
|
||||||
|
@mouseenter="if (currentNote !== fav.path) $el.style.backgroundColor = 'var(--bg-hover)'"
|
||||||
|
@mouseleave="if (currentNote !== fav.path) $el.style.backgroundColor = 'transparent'"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
||||||
|
<svg class="w-3.5 h-3.5 flex-shrink-0" style="color: var(--warning, #eab308);" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-xs truncate" x-text="fav.name"></span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click.stop="toggleFavorite(fav.path)"
|
||||||
|
class="opacity-0 group-hover:opacity-100 p-0.5 rounded transition-opacity"
|
||||||
|
title="Remove from favorites"
|
||||||
|
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||||
|
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||||
|
>
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-xs px-2 py-1 mb-2" style="color: var(--text-tertiary);">
|
<div class="text-xs px-2 py-1 mb-2" style="color: var(--text-tertiary);">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
<span>Folders & Notes</span>
|
<span>Folders & Notes</span>
|
||||||
|
|
@ -1741,9 +1874,9 @@
|
||||||
|
|
||||||
<template x-if="currentNote || currentImage">
|
<template x-if="currentNote || currentImage">
|
||||||
<!-- Editor Area -->
|
<!-- Editor Area -->
|
||||||
<div class="flex-1 flex flex-col overflow-hidden" style="background-color: var(--bg-primary);">
|
<div class="flex-1 flex flex-col overflow-hidden" :class="{'zen-editor-container': zenMode}" style="background-color: var(--bg-primary);">
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="px-4 py-3 flex items-center justify-between mobile-toolbar" style="background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-primary);">
|
<div class="px-4 py-3 flex items-center justify-between mobile-toolbar zen-hide" style="background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-primary);">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<!-- Mobile Menu Button -->
|
<!-- Mobile Menu Button -->
|
||||||
<button
|
<button
|
||||||
|
|
@ -1812,6 +1945,7 @@
|
||||||
</button>
|
</button>
|
||||||
<span x-show="isSaving" class="text-xs" style="color: var(--text-secondary);">Saving...</span>
|
<span x-show="isSaving" class="text-xs" style="color: var(--text-secondary);">Saving...</span>
|
||||||
<span x-show="!isSaving && lastSaved" class="text-xs" style="color: var(--success);">✓ Saved</span>
|
<span x-show="!isSaving && lastSaved" class="text-xs" style="color: var(--success);">✓ Saved</span>
|
||||||
|
<span x-show="!isSaving && !lastSaved && lastEditedText" class="text-xs hidden md:inline" style="color: var(--text-tertiary);" x-text="'Edited ' + lastEditedText"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Demo Mode Warning Banner -->
|
<!-- Demo Mode Warning Banner -->
|
||||||
|
|
@ -1860,6 +1994,24 @@
|
||||||
class="flex items-center rounded overflow-hidden"
|
class="flex items-center rounded overflow-hidden"
|
||||||
style="background-color: var(--bg-tertiary); border: 1px solid var(--border-primary);"
|
style="background-color: var(--bg-tertiary); border: 1px solid var(--border-primary);"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
@click="toggleFavorite()"
|
||||||
|
class="p-2 transition"
|
||||||
|
:title="isFavorite(currentNote) ? 'Remove from favorites' : 'Add to favorites'"
|
||||||
|
:style="'color: ' + (isFavorite(currentNote) ? 'var(--warning, #eab308)' : 'var(--text-secondary)') + ';'"
|
||||||
|
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||||
|
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||||
|
>
|
||||||
|
<!-- Filled star (favorited) -->
|
||||||
|
<svg x-show="isFavorite(currentNote)" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<!-- Empty star (not favorited) -->
|
||||||
|
<svg x-show="!isFavorite(currentNote)" x-cloak class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
|
||||||
<button
|
<button
|
||||||
@click="exportToHTML()"
|
@click="exportToHTML()"
|
||||||
class="p-2 transition"
|
class="p-2 transition"
|
||||||
|
|
@ -1890,6 +2042,20 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div style="width: 1px; height: 20px; background-color: var(--border-primary);"></div>
|
||||||
|
<button
|
||||||
|
@click="toggleZenMode()"
|
||||||
|
class="p-2 transition"
|
||||||
|
style="color: var(--text-secondary);"
|
||||||
|
title="Zen Mode (Ctrl+Shift+Z)"
|
||||||
|
@mouseenter="$el.style.backgroundColor = 'var(--bg-secondary)'"
|
||||||
|
@mouseleave="$el.style.backgroundColor = 'transparent'"
|
||||||
|
>
|
||||||
|
<!-- Expand/Focus icon -->
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2082,7 +2248,7 @@
|
||||||
|
|
||||||
<!-- Stats Status Bar (only shows if plugin enabled) -->
|
<!-- Stats Status Bar (only shows if plugin enabled) -->
|
||||||
<div x-show="statsPluginEnabled && currentNote && noteStats"
|
<div x-show="statsPluginEnabled && currentNote && noteStats"
|
||||||
class="border-t"
|
class="border-t zen-hide"
|
||||||
style="background-color: var(--bg-secondary); border-color: var(--border-primary);">
|
style="background-color: var(--bg-secondary); border-color: var(--border-primary);">
|
||||||
|
|
||||||
<!-- Collapsed State (default) -->
|
<!-- Collapsed State (default) -->
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue