diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index ab92122..ece1641 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -44,6 +44,7 @@ import { listen } from '@tauri-apps/api/event'; import { debounce } from '$lib/utils/debounce'; import GraphView from './GraphView.svelte'; + import TagSuggestInput from './TagSuggestInput.svelte'; const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; const isMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent); @@ -142,6 +143,7 @@ // Version history let showHistory = $state(false); let showGraph = $state(false); + let tagMenu = $state<{ x: number; y: number } | null>(null); let historyVersions = $state([]); let historyPreview = $state(null); let historySelected = $state(null); @@ -1894,6 +1896,31 @@ } } + // ── Tag editing (active note) ── + function toggleTagMenu(e: MouseEvent) { + if (tagMenu) { tagMenu = null; return; } + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const width = 240; + const x = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)); + tagMenu = { x, y: rect.bottom + 4 }; + } + + function addActiveNoteTag(tag: string) { + if (!$activeNote) return; + const cleaned = tag.trim().toLowerCase(); + if (!cleaned || $activeNote.meta.tags.includes(cleaned)) return; + $activeNote = { ...$activeNote, meta: { ...$activeNote.meta, tags: [...$activeNote.meta.tags, cleaned] } }; + $editorDirty = true; + autoSave(); + } + + function removeActiveNoteTag(tag: string) { + if (!$activeNote) return; + $activeNote = { ...$activeNote, meta: { ...$activeNote.meta, tags: $activeNote.meta.tags.filter((t) => t !== tag) } }; + $editorDirty = true; + autoSave(); + } + // Sync editor editable state when readOnly store changes (from titlebar or editor) $effect(() => { const ro = $readOnly; @@ -4321,6 +4348,17 @@ + {/if} @@ -5516,6 +5552,35 @@ {/if} +{#if tagMenu && $activeNote} + + +
(tagMenu = null)}> + + +
e.stopPropagation()}> + {#if $activeNote.meta.tags.length > 0} +
+ {#each $activeNote.meta.tags as tag} + + #{tag} + + + {/each} +
+ {/if} + addActiveNoteTag(t)} + oncancel={() => (tagMenu = null)} + /> +
+
+{/if} + {#if codeLangDropdown}
@@ -6001,11 +6066,18 @@ user-select: none; } - .note-tags { + .note-tags-trigger { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; + border: none; + background: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + cursor: pointer; } .note-tag { @@ -6017,6 +6089,17 @@ letter-spacing: 0.01em; } + .note-tags-add { + font-size: 11px; + color: var(--text-tertiary); + opacity: 0.65; + } + + .note-tags-trigger:hover .note-tags-add { + opacity: 1; + color: var(--accent); + } + .toolbar-actions { display: flex; align-items: center; @@ -6996,6 +7079,58 @@ flex-shrink: 0; } + .tag-menu-overlay { + position: fixed; + inset: 0; + z-index: 2000; + } + + .tag-menu { + position: fixed; + width: 240px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: var(--shadow-lg); + padding: 8px; + z-index: 2001; + } + + .tag-menu-list { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 8px; + } + + .tag-menu-chip { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 11px; + color: var(--text-secondary); + background: var(--bg-tertiary); + padding: 2px 4px 2px 8px; + border-radius: 10px; + } + + .tag-menu-remove { + display: flex; + align-items: center; + justify-content: center; + border: none; + background: none; + color: var(--text-tertiary); + cursor: pointer; + padding: 1px; + border-radius: 50%; + } + + .tag-menu-remove:hover { + color: var(--danger, #e53e3e); + background: color-mix(in srgb, var(--danger, #e53e3e) 12%, transparent); + } + .code-lang-overlay { position: fixed; inset: 0;