diff --git a/package.json b/package.json index 5bfd3f3..8f78054 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "helixnotes", "private": true, "license": "AGPL-3.0-or-later", - "version": "1.2.2", + "version": "1.2.3", "type": "module", "scripts": { "dev": "vite dev", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 97a581b..1034241 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1850,7 +1850,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "helixnotes" -version = "1.2.2" +version = "1.2.3" dependencies = [ "arboard", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b253928..abcc4a6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helixnotes" -version = "1.2.2" +version = "1.2.3" description = "Local markdown note-taking app" authors = ["HelixNotes"] license = "AGPL-3.0-or-later" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b845a7a..4270e9c 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -287,8 +287,11 @@ pub fn create_daily_note(state: State<'_, AppState>) -> Result Result { - operations::rename_note(&path, &new_title) +pub fn rename_note(state: State<'_, AppState>, path: String, new_title: String) -> Result { + let config = state.config.lock().map_err(|e| e.to_string())?; + let vault_path = config.active_vault.as_ref().ok_or("No active vault")?.clone(); + drop(config); + operations::rename_note(&path, &new_title, &vault_path) } #[tauri::command] diff --git a/src-tauri/src/vault/operations.rs b/src-tauri/src/vault/operations.rs index a9f8a28..b322573 100644 --- a/src-tauri/src/vault/operations.rs +++ b/src-tauri/src/vault/operations.rs @@ -276,7 +276,7 @@ fn read_note_entry_metadata_only(path: &Path, vault_root: &Path) -> Result Result Result<(), Stri Ok(()) } -pub fn rename_note(path: &str, new_title: &str) -> Result { +pub fn rename_note(path: &str, new_title: &str, vault_path: &str) -> Result { let src = Path::new(path); if !src.exists() { return Err("Note does not exist".to_string()); } - // Update frontmatter + // Read old title before renaming let raw = fs::read_to_string(src).map_err(|e| e.to_string())?; let filename = src .file_name() @@ -637,6 +639,10 @@ pub fn rename_note(path: &str, new_title: &str) -> Result { .to_string_lossy() .to_string(); let (mut meta, content) = frontmatter::parse_note(&raw, &filename); + let old_title = meta.title.clone(); + let old_path_str = src.to_string_lossy().to_string(); + + // Update frontmatter meta.title = new_title.to_string(); meta.modified = Utc::now(); if meta.id.is_empty() { @@ -653,7 +659,134 @@ pub fn rename_note(path: &str, new_title: &str) -> Result { fs::rename(src, &new_path).map_err(|e| e.to_string())?; } - Ok(new_path.to_string_lossy().to_string()) + let new_path_str = new_path.to_string_lossy().to_string(); + + // Update wikilinks in other notes that reference this note + update_wikilinks_after_rename(vault_path, &old_path_str, &new_path_str, &old_title, new_title); + + Ok(new_path_str) +} + +/// Walk all .md files in the vault and update wikilink references after a note rename. +/// Updates both HTML data-attributes (data-path, data-title) and raw [[old_title]] references. +fn update_wikilinks_after_rename( + vault_path: &str, + old_path: &str, + new_path: &str, + old_title: &str, + new_title: &str, +) { + let vault = Path::new(vault_path); + let vault_prefix = format!("{}/", vault_path); + + // Compute vault-relative path refs (without .md) for path-based wikilink refs + let old_rel_ref = old_path.strip_prefix(&vault_prefix) + .unwrap_or(old_path) + .strip_suffix(".md") + .unwrap_or(old_path); + let new_rel_ref = new_path.strip_prefix(&vault_prefix) + .unwrap_or(new_path) + .strip_suffix(".md") + .unwrap_or(new_path); + + // Check if another note with the same old_title exists in the vault. + // If so, title-based replacements (rules 1-3) are ambiguous and must be skipped, + // because we can't tell which [[Old Title]] ref points to the renamed note + // vs. the other note with the same title. + let title_is_unique = !WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .any(|e| { + let p = e.path(); + if !p.is_file() || p.to_string_lossy().as_ref() == new_path { + return false; + } + if p.extension().and_then(|ext| ext.to_str()) != Some("md") { + return false; + } + let p_str = p.to_string_lossy(); + if p_str.contains("/.helixnotes/") || p_str.contains("/.trash/") { + return false; + } + // Check if this note's filename (without .md) matches the old title + p.file_stem() + .and_then(|s| s.to_str()) + .map(|s| s.eq_ignore_ascii_case(old_title)) + .unwrap_or(false) + }); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if !path.is_file() { continue; } + let path_str = path.to_string_lossy(); + if path_str.contains("/.helixnotes/") || path_str.contains("/.trash/") { continue; } + if path.extension().and_then(|e| e.to_str()) != Some("md") { continue; } + // Skip the renamed note itself + if *path_str == *new_path { continue; } + + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(_) => continue, + }; + + let mut result = content.clone(); + + // Notes are saved as markdown with [[ref]] or [[ref|display]] syntax. + // Update all wikilink forms that reference the renamed note: + + // Title-based rules only apply when the old title is unique in the vault. + // If another note shares the same title, these would be ambiguous. + if old_title != new_title && title_is_unique { + // 1. Short title ref: [[Old Title]] → [[New Title]] + result = result.replace( + &format!("[[{}]]", old_title), + &format!("[[{}]]", new_title), + ); + + // 2. Short title with alias: [[Old Title|display]] → [[New Title|display]] + result = result.replace( + &format!("[[{}|", old_title), + &format!("[[{}|", new_title), + ); + + // 3. Short title as alias display: [[ref|Old Title]] → [[ref|New Title]] + result = result.replace( + &format!("|{}]]", old_title), + &format!("|{}]]", new_title), + ); + } + + // Path-based rules are always safe (paths are unique). + if old_rel_ref != new_rel_ref { + // 4. Path-based ref: [[folder/Old Name]] → [[folder/New Name]] + result = result.replace( + &format!("[[{}]]", old_rel_ref), + &format!("[[{}]]", new_rel_ref), + ); + + // 5. Path-based ref with alias: [[folder/Old Name|display]] → [[folder/New Name|display]] + // Also update the alias if it matches the old title. + // e.g. [[folder/Old Name|Old Title]] → [[folder/New Name|New Title]] + if old_title != new_title { + result = result.replace( + &format!("[[{}|{}]]", old_rel_ref, old_title), + &format!("[[{}|{}]]", new_rel_ref, new_title), + ); + } + // For aliases that DON'T match the old title, just update the ref part + result = result.replace( + &format!("[[{}|", old_rel_ref), + &format!("[[{}|", new_rel_ref), + ); + } + + if result != content { + let _ = fs::write(path, &result); + } + } } pub fn rename_notebook(path: &str, new_name: &str) -> Result { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 65be1cc..ecb2a13 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "HelixNotes", - "version": "1.2.2", + "version": "1.2.3", "identifier": "com.helixnotes.app", "build": { "frontendDist": "../build", diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 4dd87d0..b78cfe3 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -742,8 +742,17 @@ let wikiLinkSelectedIndex = $state(0); let wikiLinkTitlesCache = $state([]); let wikiLinkTypedByUser = false; + // Disambiguation state: when ]] auto-close finds multiple matches + let wikiLinkDisambigEntries = $state(null); + let wikiLinkDisambigRef = $state(null); + let wikiLinkDisambigDisplay = $state(null); + // Navigation disambiguation: when clicking a wikilink with multiple matches + let wikiLinkNavDisambig = $state<{ entries: NoteTitleEntry[]; x: number; y: number } | null>(null); + let wikiLinkNavDisambigIndex = $state(0); let wikiLinkFiltered = $derived.by(() => { + // When disambiguating, show only the exact matches + if (wikiLinkDisambigEntries) return wikiLinkDisambigEntries; if (!wikiLinkMenu) return wikiLinkTitlesCache; let q = wikiLinkMenu.query.toLowerCase(); if (!q) return wikiLinkTitlesCache; @@ -757,6 +766,29 @@ ); }); + // Set of lowercase titles that appear more than once (for disambiguation display) + let wikiLinkDuplicateTitles = $derived.by(() => { + const counts = new Map(); + for (const e of wikiLinkTitlesCache) { + const key = e.title.toLowerCase(); + counts.set(key, (counts.get(key) ?? 0) + 1); + } + const dupes = new Set(); + for (const [key, count] of counts) { + if (count > 1) dupes.add(key); + } + return dupes; + }); + + function wikiLinkFolderPath(entry: NoteTitleEntry): string { + const vaultRoot = $appConfig?.active_vault; + if (!vaultRoot || !entry.path) return ''; + const rel = entry.path.startsWith(vaultRoot + '/') ? entry.path.slice(vaultRoot.length + 1) : entry.path; + const parts = rel.split('/'); + // Return parent folder(s), excluding the filename + return parts.length > 1 ? parts.slice(0, -1).join('/') + '/' : ''; + } + async function refreshWikiLinkTitles() { try { wikiLinkTitlesCache = await getAllNoteTitles(); @@ -768,6 +800,15 @@ function closeWikiLinkMenu() { wikiLinkMenu = null; wikiLinkSelectedIndex = 0; + wikiLinkDisambigEntries = null; + wikiLinkDisambigRef = null; + wikiLinkDisambigDisplay = null; + } + + function wikiLinkRelPath(entry: NoteTitleEntry): string | null { + const vaultRoot = $appConfig?.active_vault; + if (!vaultRoot || !entry.path || !entry.path.startsWith(vaultRoot + '/')) return null; + return entry.path.slice(vaultRoot.length + 1).replace(/\.md$/, ''); } function insertWikiLink(entry: NoteTitleEntry, originalRef?: string) { @@ -777,9 +818,17 @@ const to = editor.state.selection.from; editor.chain().focus().deleteRange({ from, to }).run(); // Insert the wiki-link mark - // entry.title is the display text, originalRef (if provided) is the full reference (e.g. "note#heading") + // For ambiguous titles, use vault-relative path as the ref so it survives source-mode roundtrips const displayText = entry.title; - const titleAttr = originalRef || entry.title; + let titleAttr = originalRef || entry.title; + if (entry.path && wikiLinkDuplicateTitles.has(entry.title.toLowerCase())) { + const relPath = wikiLinkRelPath(entry); + if (relPath) { + // Preserve any #heading or ^block anchors from the original ref + const anchor = originalRef ? originalRef.replace(/^[^#^]*/, '') : ''; + titleAttr = relPath + anchor; + } + } tick().then(() => { if (!editor) return; editor.chain().focus() @@ -796,7 +845,12 @@ function executeWikiLinkCommand(index: number) { const items = wikiLinkFiltered; if (index < 0 || index >= items.length) return; - insertWikiLink(items[index]); + if (wikiLinkDisambigEntries) { + // In disambiguation mode: use stored display/ref + insertWikiLink({ ...items[index], title: wikiLinkDisambigDisplay || items[index].title }, wikiLinkDisambigRef || undefined); + } else { + insertWikiLink(items[index]); + } } const WikiLink = TiptapMark.create({ @@ -849,7 +903,7 @@ event.stopPropagation(); const path = wikiLinkEl.getAttribute('data-path') || ''; const title = wikiLinkEl.getAttribute('data-title') || wikiLinkEl.textContent || ''; - navigateToWikiLink(path, title); + navigateToWikiLink(path, title, event as MouseEvent); return true; } return false; @@ -917,9 +971,18 @@ const display = (pipeIdx >= 0 ? rawQuery.slice(pipeIdx + 1) : noteRef).trim(); // Strip #heading and ^block for title matching const titleForLookup = noteRef.replace(/#.*$/, '').replace(/\^.*$/, '').trim(); - const match = wikiLinkTitlesCache.find(e => e.title.toLowerCase() === titleForLookup.toLowerCase()); - if (match) { - insertWikiLink({ ...match, title: display }, noteRef); + const matches = wikiLinkTitlesCache.filter(e => e.title.toLowerCase() === titleForLookup.toLowerCase()); + if (matches.length === 1) { + insertWikiLink({ ...matches[0], title: display }, noteRef); + } else if (matches.length > 1) { + // Multiple matches — show disambiguation picker + // Keep the menu open but filter to only the matching entries + wikiLinkMenu = { ...wikiLinkMenu!, query: titleForLookup }; + // Override filtered results to only show exact matches + wikiLinkDisambigEntries = matches; + wikiLinkDisambigRef = noteRef; + wikiLinkDisambigDisplay = display; + wikiLinkSelectedIndex = 0; } else { // Insert as unresolved wiki-link (no path) const menuFrom = wikiLinkMenu.from; @@ -991,22 +1054,57 @@ wikiLinkSelectedIndex = 0; } - async function navigateToWikiLink(path: string, title: string) { + async function navigateToWikiLink(path: string, title: string, clickEvent?: MouseEvent) { // title may contain #heading or ^block anchors — strip for note lookup const noteTitle = title.replace(/#.*$/, '').replace(/\^.*$/, '').trim(); + if (!path) { + // Try path-based resolution first (for disambiguated refs like "folder/note") + const vaultRoot = $appConfig?.active_vault; + if (noteTitle.includes('/') && vaultRoot) { + const fullPath = vaultRoot + '/' + noteTitle + '.md'; + const pathMatch = wikiLinkTitlesCache.find(e => e.path === fullPath); + if (pathMatch) { + path = pathMatch.path; + } else { + // Path not found — try title of last segment + const lastSegment = noteTitle.split('/').pop()!; + const segMatches = wikiLinkTitlesCache.filter(e => e.title.toLowerCase() === lastSegment.toLowerCase()); + if (segMatches.length === 1) path = segMatches[0].path; + else if (segMatches.length > 1) { + let x = clickEvent ? clickEvent.clientX : window.innerWidth / 2 - 140; + let y = clickEvent ? clickEvent.clientY + 8 : window.innerHeight / 2 - 100; + if (x + 280 > window.innerWidth) x = window.innerWidth - 290; + if (y + 200 > window.innerHeight) y = Math.max(4, window.innerHeight - 200); + wikiLinkNavDisambig = { entries: segMatches, x, y }; + wikiLinkNavDisambigIndex = 0; + return; + } + } + } + } if (!path) { // Unresolved link — try to find by title - const match = wikiLinkTitlesCache.find(e => e.title.toLowerCase() === noteTitle.toLowerCase()); - if (match) { - path = match.path; + const matches = wikiLinkTitlesCache.filter(e => e.title.toLowerCase() === noteTitle.toLowerCase()); + if (matches.length === 1) { + path = matches[0].path; + } else if (matches.length > 1) { + // Multiple matches — show disambiguation popup + let x = clickEvent ? clickEvent.clientX : window.innerWidth / 2 - 140; + let y = clickEvent ? clickEvent.clientY + 8 : window.innerHeight / 2 - 100; + if (x + 280 > window.innerWidth) x = window.innerWidth - 290; + if (y + 200 > window.innerHeight) y = Math.max(4, window.innerHeight - 200); + wikiLinkNavDisambig = { entries: matches, x, y }; + wikiLinkNavDisambigIndex = 0; + return; } else { // Offer to create the note (use clean title, not the anchor ref) + const cleanTitle = noteTitle.includes('/') ? noteTitle.split('/').pop()! : noteTitle; const notebookRel = $activeNotePath ? $activeNotePath.substring(($appConfig?.active_vault?.length ?? 0) + 1).split('/').slice(0, -1).join('/') : null; try { const { createNote } = await import('$lib/api'); - const newNote = await createNote(notebookRel || null, noteTitle); + const newNote = await createNote(notebookRel || null, cleanTitle); // Navigate to the new note const content = await readNote(newNote.path); $activeNote = { ...content, content: content.content }; @@ -1026,6 +1124,17 @@ } } + async function navigateToWikiLinkDirect(entry: NoteTitleEntry) { + wikiLinkNavDisambig = null; + try { + const content = await readNote(entry.path); + $activeNote = { ...content, content: content.content }; + $activeNotePath = entry.path; + } catch (e) { + console.error('Failed to navigate to wiki-link:', e); + } + } + const textColors = [ { name: 'Default', value: '' }, { name: 'Red', value: '#ef4444' }, @@ -1917,8 +2026,28 @@ const display = (pipeIdx >= 0 ? raw.slice(pipeIdx + 1) : noteRef).trim(); // Strip #heading and ^block anchors for title matching const titleForLookup = noteRef.replace(/#.*$/, '').replace(/\^.*$/, '').trim(); - // Try to resolve the title to a path from cache - const match = wikiLinkTitlesCache.find(e => e.title.toLowerCase() === titleForLookup.toLowerCase()); + // Try to resolve: first by vault-relative path (for disambiguated links), then by title + const vaultRoot = $appConfig?.active_vault ?? ''; + let match: NoteTitleEntry | undefined; + if (titleForLookup.includes('/') && vaultRoot) { + // Ref contains a path — resolve by matching vault-relative path + const fullPath = vaultRoot + '/' + titleForLookup + '.md'; + match = wikiLinkTitlesCache.find(e => e.path === fullPath); + } + if (!match) { + // Fallback: resolve by title (use the last segment if path-based) + const titleOnly = titleForLookup.includes('/') ? titleForLookup.split('/').pop()! : titleForLookup; + const titleLower = titleOnly.toLowerCase(); + const titleMatches = wikiLinkTitlesCache.filter(e => e.title.toLowerCase() === titleLower); + if (titleMatches.length === 1) { + match = titleMatches[0]; + } else if (titleMatches.length > 1) { + // Multiple matches — prefer the shallowest path (closest to vault root) + match = titleMatches.reduce((a, b) => + a.path.split('/').length <= b.path.split('/').length ? a : b + ); + } + } const path = match ? match.path : ''; return `${escapeHtml(display)}`; }); @@ -2339,7 +2468,7 @@ event.stopPropagation(); const path = wikiLinkEl.getAttribute('data-path') || ''; const title = wikiLinkEl.getAttribute('data-title') || wikiLinkEl.textContent || ''; - navigateToWikiLink(path, title); + navigateToWikiLink(path, title, event); return; } @@ -3266,6 +3395,8 @@ ? { ...n, path: newPath, relative_path: n.relative_path.replace(/[^/]+$/, newTitle + '.md'), meta: { ...n.meta, title: newTitle } } : n )); + // Refresh wiki-link titles cache so links resolve to renamed note + refreshWikiLinkTitles(); } catch (err) { console.error('Failed to rename note file:', err); notes.update(list => list.map(n => @@ -4467,10 +4598,15 @@ class="wiki-link-item" class:selected={i === wikiLinkSelectedIndex} onmouseenter={() => wikiLinkSelectedIndex = i} - onmousedown={(e) => { e.preventDefault(); insertWikiLink(entry); }} + onmousedown={(e) => { e.preventDefault(); if (wikiLinkDisambigEntries) { insertWikiLink({ ...entry, title: wikiLinkDisambigDisplay || entry.title }, wikiLinkDisambigRef || undefined); } else { insertWikiLink(entry); } }} > - {entry.title} + + {entry.title} + {#if wikiLinkDuplicateTitles.has(entry.title.toLowerCase())} + {wikiLinkFolderPath(entry) || '(vault root)'} + {/if} + {/each} {/if} @@ -4478,6 +4614,30 @@ {/if} +{#if wikiLinkNavDisambig} + + +{/if} + {#if aiMenu}
@@ -5248,7 +5408,7 @@ color: var(--text-primary); font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: var(--editor-font-size, 14px); - line-height: var(--editor-line-height, 1.6); + line-height: 1.3; resize: none; outline: none; padding: 0; @@ -5276,7 +5436,7 @@ padding-top: 8px; font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: var(--editor-font-size, 14px); - line-height: var(--editor-line-height, 1.6); + line-height: 1.3; color: var(--text-secondary); opacity: 0.5; text-align: right; @@ -6835,12 +6995,34 @@ color: var(--accent); } + .wiki-link-title-col { + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; + } + .wiki-link-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .wiki-link-folder { + font-size: 11px; + color: var(--text-tertiary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .wiki-link-disambig-header { + padding: 8px 10px 4px; + font-size: 11px; + color: var(--text-tertiary); + font-weight: 500; + } + /* ═══ MOBILE (class-based, not media-query, for Android high-DPI) ═══ */ .editor-container.mobile { height: 100%; diff --git a/src/lib/components/NoteList.svelte b/src/lib/components/NoteList.svelte index 8a22e4b..41bd1d1 100644 --- a/src/lib/components/NoteList.svelte +++ b/src/lib/components/NoteList.svelte @@ -44,6 +44,8 @@ const modKey = navigator.platform.startsWith('Mac') ? '⌘' : 'Ctrl'; const isMobile = /android|ios/i.test(navigator.userAgent); + const isAndroid = /android/i.test(navigator.userAgent); + let multiSelectMode = $state(false); /** Derive tags from current $notes store (avoids re-scanning files on mobile) */ function deriveTagsFromNotes() { @@ -154,6 +156,7 @@ selectedPaths = new Set(); lastClickedPath = null; batchMovePicker = false; + multiSelectMode = false; } // Clear selection and reset scroll when view/notebook changes @@ -556,8 +559,8 @@ e.preventDefault(); return; } - // Mobile: if in selection mode, tap toggles selection - if (isMobile && selectedPaths.size > 0) { + // Mobile: if in selection mode (or Android multi-select mode), tap toggles selection + if (isMobile && (selectedPaths.size > 0 || multiSelectMode)) { const next = new Set(selectedPaths); if (next.has(note.path)) { next.delete(note.path); @@ -693,6 +696,13 @@
{viewTitle}
+ {#if isAndroid} + + {/if}
{/if} + {#if batchTagEdit && !contextMenu} + +
batchTagEdit = false}> + +
e.stopPropagation()}> +
+ Tags for {selectedPaths.size} notes + +
+
+
+ { + if (e.key === 'Enter') { e.preventDefault(); addTagToBatch(tagEditValue); } + if (e.key === 'Escape') { e.preventDefault(); batchTagEdit = false; } + }} + autofocus + /> +
+ {#if tagEditTags.length > 0} +
+ {#each tagEditTags as tag} +
+ #{tag} + +
+ {/each} +
+ {:else} +
No common tags
+ {/if} +
+
+
+ {/if} +
{#if $sortedNotes.length === 0}
@@ -793,10 +848,10 @@ class:qa-drag-above={$viewMode === 'quickaccess' && qaDragOver === noteIndex && qaDragFrom !== null && qaDragHalf === 'top'} class:qa-drag-below={$viewMode === 'quickaccess' && qaDragOver === noteIndex && qaDragFrom !== null && qaDragHalf === 'bottom'} onclick={(e) => handleNoteClick(e, note)} - ontouchstart={(e) => { if (isMobile) handleTouchStart(e, note); }} - ontouchmove={(e) => { if (isMobile) handleTouchMove(e); }} - ontouchend={() => { if (isMobile) handleTouchEnd(); }} - ontouchcancel={() => { if (isMobile) handleTouchEnd(); }} + ontouchstart={(e) => { if (isMobile && !isAndroid) handleTouchStart(e, note); }} + ontouchmove={(e) => { if (isMobile && !isAndroid) handleTouchMove(e); }} + ontouchend={() => { if (isMobile && !isAndroid) handleTouchEnd(); }} + ontouchcancel={() => { if (isMobile && !isAndroid) handleTouchEnd(); }} oncontextmenu={(e) => { e.preventDefault(); const pos = clampMenu(e.clientX, e.clientY); @@ -846,13 +901,6 @@ }} ondragend={() => { qaDragFrom = null; qaDragOver = null; }} > - {#if isMobile && selectedPaths.size > 0} -
- {#if selectedPaths.has(note.path)} - - {/if} -
- {/if} {#if compact}
@@ -1172,6 +1220,11 @@ color: var(--text-primary); } + .icon-btn.active-toggle { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 15%, transparent); + } + .list-content { flex: 1; overflow-y: auto; @@ -1721,6 +1774,9 @@ .note-list.mobile .selection-bar { padding: 8px 16px; + border-radius: 12px; + margin: 4px 8px; + border-bottom: none; } .note-list.mobile .selection-count { @@ -1731,6 +1787,7 @@ padding: 6px 14px; font-size: 13px; min-height: 36px; + border-radius: 10px; } .note-list.mobile .context-menu {