added favorites and performance improvements
This commit is contained in:
parent
74903b64ba
commit
c1de0c84a1
|
|
@ -51,6 +51,7 @@
|
||||||
- **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")
|
- **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
|
||||||
|
|
||||||
|
|
|
||||||
213
frontend/app.js
213
frontend/app.js
|
|
@ -58,8 +58,27 @@ function noteApp() {
|
||||||
linkCopied: false,
|
linkCopied: false,
|
||||||
zenMode: false,
|
zenMode: false,
|
||||||
previousViewMode: 'split',
|
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: [],
|
||||||
|
|
@ -308,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
|
||||||
|
|
@ -398,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;
|
||||||
|
|
@ -730,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) {
|
||||||
|
|
@ -737,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 {
|
||||||
|
|
@ -940,6 +1017,88 @@ 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 current note's last modified time as relative string
|
||||||
get lastEditedText() {
|
get lastEditedText() {
|
||||||
if (!this.currentNote) return '';
|
if (!this.currentNote) return '';
|
||||||
|
|
@ -1885,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
|
||||||
|
|
@ -2753,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({}, '', '/');
|
||||||
}
|
}
|
||||||
|
|
@ -2901,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('---')) {
|
||||||
|
|
@ -2923,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');
|
||||||
|
|
@ -3031,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(() => {
|
||||||
|
|
@ -3059,6 +3210,10 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
// Cache the result for performance
|
||||||
|
this._lastRenderedContent = this.noteContent;
|
||||||
|
this._cachedRenderedHTML = html;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1215,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>
|
||||||
|
|
@ -1938,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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue