diff --git a/frontend/js/palette.js b/frontend/js/palette.js index ffe00a1..730ad8e 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -8,225 +8,271 @@ import { showToast, toggleTheme, TabManager } from './ui.js'; import { openEditor } from './utils.js'; import { state } from './state.js'; -// ── Helper: trigger goHome by clicking the logo ── +// ── Helper: trigger goHome ── function triggerGoHome() { - const logo = document.getElementById('header-logo'); - if (logo) logo.click(); + const el = document.getElementById('header-logo'); + if (el) el.click(); } -// ── Get current active file from tabs ── +// ── Get current active file ── function getCurrentFile() { - const activeId = TabManager._activeTabId; - if (activeId) { - const tab = TabManager._tabs.find(t => t.id === activeId); + const id = TabManager._activeTabId; + if (id) { + const tab = TabManager._tabs.find(t => t.id === id); if (tab) return { vault: tab.vault, path: tab.path }; } - // Fallback to state - if (state.currentVault && state.currentPath) { - return { vault: state.currentVault, path: state.currentPath }; - } + if (state.currentVault && state.currentPath) return { vault: state.currentVault, path: state.currentPath }; return null; } -// ── Get current vault from tabs ── function getCurrentVault() { - const file = getCurrentFile(); - if (file) return file.vault; + const f = getCurrentFile(); + if (f) return f.vault; if (state.currentVault) return state.currentVault; if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name; return null; } -// ── Prompt for vault if ambiguous ── -function promptVault(hint) { - const vault = getCurrentVault(); - if (vault) return vault; - return prompt(`${hint} — Nom de la vault :`) || ''; -} - -// ── DOM cache ── +// ── DOM / state ── let _overlay = null; let _input = null; let _results = null; -let _mode = 'files'; +let _mode = 'files'; // 'files' | 'commands' | 'browse' let _selectedIndex = 0; let _searchTimeout = null; let _lastResults = []; -// ── Available commands ── +// Browse mode state +let _browse = { + action: null, // 'create-file' | 'create-dir' | 'delete-dir' + vault: null, + path: '', + name: '', +}; + +// ── Commands ── const COMMANDS = [ - // ── Navigation ── - { id: 'home', label: '🏠 Accueil', description: 'Retour à l\'accueil', category: 'Navigation', - action: () => { close(); triggerGoHome(); } }, - - // ── Fichiers ── - { id: 'edit-current', label: '✏️ Éditer fichier courant', description: 'Ouvrir le fichier actif dans l\'éditeur', category: 'Fichiers', - action: () => { close(); const f = getCurrentFile(); if (f) openEditor(f.vault, f.path); else showToast('Aucun fichier ouvert', 'error'); } }, - - { id: 'popout', label: '🪟 Pop-out', description: 'Ouvrir le document courant dans une nouvelle fenêtre', category: 'Fichiers', - action: () => { close(); const f = getCurrentFile(); if (f) window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`, `popout_${f.vault}_${f.path.replace(/[^a-zA-Z0-9]/g, '_')}`, 'width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no'); else showToast('Aucun fichier ouvert', 'error'); } }, - - { id: 'create-file', label: '📄 Créer fichier markdown', description: 'Créer un nouveau fichier .md et l\'ouvrir dans l\'éditeur', category: 'Fichiers', - action: async () => { close(); await createFile(); } }, - - { id: 'delete-current', label: '🗑️ Supprimer fichier courant', description: 'Supprimer le fichier ouvert dans l\'onglet actif', category: 'Fichiers', - action: async () => { close(); await deleteCurrentFile(); } }, - - // ── Répertoires ── - { id: 'create-dir', label: '📁 Créer répertoire', description: 'Créer un nouveau dossier', category: 'Répertoires', - action: async () => { close(); await createDirectory(); } }, - - { id: 'delete-dir', label: '🗑️ Supprimer répertoire', description: 'Supprimer un dossier et son contenu', category: 'Répertoires', - action: async () => { close(); await deleteDirectory(); } }, - - // ── Actions ── - { id: 'theme', label: '🌓 Changer le thème', description: 'Basculer entre thème clair et sombre', category: 'Actions', - action: () => { close(); toggleTheme(); } }, - - { id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète des vaults', category: 'Actions', - action: async () => { close(); try { await api('/api/index/reload'); showToast('Index rechargé', 'success'); } catch { showToast('Erreur de réindexation', 'error'); } } }, - - // ── Aide & Config ── - { id: 'help', label: '❓ Aide', description: 'Ouvrir le guide d\'utilisation', category: 'Système', - action: () => { close(); const btn = document.getElementById('help-open-btn'); if (btn) btn.click(); } }, - - { id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', category: 'Système', - action: () => { close(); const btn = document.getElementById('config-open-btn'); if (btn) btn.click(); } }, + { id: 'home', label: '🏠 Accueil', description: "Retour à l'accueil", category: 'Navigation', action: () => { close(); triggerGoHome(); } }, + { id: 'edit-current',label: '✏️ Éditer fichier courant',description: 'Ouvrir le fichier actif dans l\'éditeur', category: 'Fichiers', action: () => { close(); const f=getCurrentFile(); if(f) openEditor(f.vault, f.path); else showToast('Aucun fichier ouvert','error'); } }, + { id: 'popout', label: '🪟 Pop-out', description: 'Ouvrir le document dans une fenêtre détachée', category: 'Fichiers', action: () => { close(); const f=getCurrentFile(); if(f) window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`,'popout','width=1000,height=700'); else showToast('Aucun fichier ouvert','error'); } }, + { id: 'create-file', label: '📄 Créer fichier markdown',description: 'Naviguer vers le dossier parent, puis nommer le fichier', category: 'Fichiers', action: () => { startBrowse('create-file'); } }, + { id: 'delete-current',label: '🗑️ Supprimer fichier courant',description: 'Supprimer le fichier de l\'onglet actif', category: 'Fichiers', action: async () => { close(); await deleteCurrentFile(); } }, + { id: 'create-dir', label: '📁 Créer répertoire', description: 'Naviguer vers le dossier parent, puis nommer le dossier', category: 'Répertoires', action: () => { startBrowse('create-dir'); } }, + { id: 'delete-dir', label: '🗑️ Supprimer répertoire', description: 'Naviguer et sélectionner le dossier à supprimer', category: 'Répertoires', action: () => { startBrowse('delete-dir'); } }, + { id: 'theme', label: '🌓 Changer le thème', description: 'Basculer clair/sombre', category: 'Actions', action: () => { close(); toggleTheme(); } }, + { id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète', category: 'Actions', action: async () => { close(); try{await api('/api/index/reload');showToast('Index rechargé','success');}catch{showToast('Erreur de réindexation','error');} } }, + { id: 'help', label: '❓ Aide', description: "Ouvrir le guide d'utilisation", category: 'Système', action: () => { close(); const b=document.getElementById('help-open-btn'); if(b)b.click(); } }, + { id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', category: 'Système', action: () => { close(); const b=document.getElementById('config-open-btn'); if(b)b.click(); } }, ]; -// ── Create file action ── -async function createFile() { - const vault = promptVault('Créer un fichier'); - if (!vault) return; +// ── Browse mode helpers ── +function startBrowse(action) { + const vault = getCurrentVault(); + if (!vault) { showToast('Aucune vault trouvée', 'error'); return; } + _browse = { action, vault, path: '', name: '' }; + switchMode('browse'); + _input.value = ''; + renderBrowse(); +} - let path = prompt('Chemin du fichier (ex: notes/mon-fichier) :'); - if (!path) return; - - // Add .md extension if none - if (!path.includes('.')) path += '.md'; - - try { - const data = await api(`/api/file/${encodeURIComponent(vault)}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path, content: `# ${path.split('/').pop().replace('.md', '')}\n\n` }), - }); - if (data?.status === 'ok' || data?.success) { - showToast(`✅ ${path} créé`, 'success'); - openEditor(vault, path); - } else { - showToast('Erreur de création', 'error'); - } - } catch (e) { - showToast(`Erreur : ${e.message || e}`, 'error'); +function switchMode(newMode) { + _mode = newMode; + if (_mode === 'browse') { + _input.placeholder = 'Naviguez avec ↑↓, Enter pour entrer, Retour arrière pour monter'; + _input.disabled = true; + _input.style.display = 'none'; + } else { + _input.disabled = false; + _input.style.display = ''; + const bc = document.getElementById('cp-breadcrumb'); + if (bc) bc.style.display = 'none'; } } -// ── Create directory action ── -async function createDirectory() { - const vault = promptVault('Créer un dossier'); - if (!vault) return; +// ── Render directory browser ── +async function renderBrowse() { + _selectedIndex = 0; + _lastResults = []; + const { vault, path } = _browse; + const breadcrumbEl = document.getElementById('cp-breadcrumb'); - const path = prompt('Chemin du dossier (ex: notes/projets) :'); - if (!path) return; + // Show breadcrumb + if (breadcrumbEl) { + breadcrumbEl.style.display = 'flex'; + const segments = path ? path.split('/') : []; + let html = `${escapeHtml(vault)}`; + let cumul = ''; + for (const seg of segments) { + cumul = cumul ? cumul + '/' + seg : seg; + html += `/${escapeHtml(seg)}`; + } + breadcrumbEl.innerHTML = html; + + // Click on breadcrumb segment to jump + breadcrumbEl.querySelectorAll('.cp-bc-item[data-path]').forEach(el => { + el.addEventListener('click', () => { + _browse.path = el.dataset.path; + renderBrowse(); + }); + }); + } try { - const data = await api(`/api/directory/${encodeURIComponent(vault)}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path }), - }); - if (data?.status === 'ok' || data?.success) { - showToast(`📁 ${path} créé`, 'success'); - } else { - showToast('Erreur de création', 'error'); + const data = await api(`/api/browse/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`); + const items = data.items || []; + const dirs = items.filter(i => i.type === 'directory'); + const files = items.filter(i => i.type === 'file'); + + // Build result list + const segments = path ? path.split('/') : []; + const breadcrumb = vault + (path ? '/' + path : ''); + + // ── Action row (first result) ── + if (_browse.action === 'delete-dir' && path) { + _lastResults.push({ + type: '_action', id: 'confirm-delete', + label: '🗑️ ' + (path.split('/').pop() || path), + sublabel: 'Supprimer ce dossier et tout son contenu (confirmation demandée)', + action: () => executeBrowseDelete(path), + }); + } else if (_browse.action === 'create-file' || _browse.action === 'create-dir') { + const actionLabel = _browse.action === 'create-file' ? '📄 Créer un fichier ici' : '📁 Créer un dossier ici'; + _lastResults.push({ + type: '_action', id: 'confirm-create', + label: actionLabel, + sublabel: 'Dans ' + (breadcrumb || 'racine de la vault'), + action: () => executeBrowseCreate(), + }); } - } catch (e) { - showToast(`Erreur : ${e.message || e}`, 'error'); + + // ── Parent entry ── + if (path) { + const parent = segments.slice(0, -1).join('/'); + _lastResults.push({ + type: '_navigate', id: 'parent', + label: '📂 .. (dossier parent)', + sublabel: '', + go: parent, + }); + } + + // ── Subdirectories ── + for (const d of dirs) { + const full = path ? path + '/' + d.name : d.name; + _lastResults.push({ + type: '_navigate', id: 'dir-' + full, + label: '📁 ' + d.name, + sublabel: '', + go: full, + }); + } + + // ── Files (for info, not clickable for creating) ── + for (const f of files) { + _lastResults.push({ + type: '_file', + label: '📄 ' + f.name, + sublabel: '', + }); + } + + if (_lastResults.length === 0) { + _lastResults.push({ type: '_empty', label: '📂 Dossier vide', sublabel: '' }); + } + } catch { + _lastResults = [{ type: '_error', label: 'Erreur de chargement', sublabel: '' }]; + } + + renderResults(); +} + +async function executeBrowseDelete(path) { + if (!confirm(`Supprimer définitivement le dossier "${path}" et TOUT son contenu ?`)) return; + try { + const data = await api(`/api/directory/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method: 'DELETE' }); + if (data?.status === 'ok' || data?.success) { + showToast(`🗑️ ${path} supprimé`, 'success'); + close(); + } else showToast('Erreur de suppression', 'error'); + } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); } +} + +async function executeBrowseCreate() { + const action = _browse.action; + const label = action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier'; + const name = prompt(label + ' dans ' + (_browse.path || 'racine') + ' :'); + if (!name) return; + if (action === 'create-file') { + let filePath = _browse.path ? _browse.path + '/' + name : name; + if (!filePath.includes('.')) filePath += '.md'; + try { + const data = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: filePath, content: `# ${name.replace('.md','')}\n\n` }), + }); + if (data?.status === 'ok' || data?.success) { + showToast(`✅ ${filePath} créé`, 'success'); + close(); + openEditor(_browse.vault, filePath); + } else showToast('Erreur de création', 'error'); + } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); } + } else { + const dirPath = _browse.path ? _browse.path + '/' + name : name; + try { + const data = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: dirPath }), + }); + if (data?.status === 'ok' || data?.success) { + showToast(`📁 ${dirPath} créé`, 'success'); + close(); + } else showToast('Erreur de création', 'error'); + } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); } } } -// ── Delete current file action ── +// ── Delete current file ── async function deleteCurrentFile() { const f = getCurrentFile(); if (!f) { showToast('Aucun fichier ouvert', 'error'); return; } - if (!confirm(`Supprimer définitivement "${f.path}" ?`)) return; - try { - const data = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { - method: 'DELETE', - }); + const data = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method: 'DELETE' }); if (data?.status === 'ok' || data?.success) { showToast(`🗑️ ${f.path} supprimé`, 'success'); - // Close the tab const tab = TabManager._tabs.find(t => t.vault === f.vault && t.path === f.path); if (tab) TabManager.close(tab.id); triggerGoHome(); - } else { - showToast('Erreur de suppression', 'error'); - } - } catch (e) { - showToast(`Erreur : ${e.message || e}`, 'error'); - } + } else showToast('Erreur de suppression', 'error'); + } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); } } -// ── Delete directory action ── -async function deleteDirectory() { - const vault = promptVault('Supprimer un dossier'); - if (!vault) return; - - const path = prompt('Chemin du dossier à supprimer (ex: notes/obsolète) :'); - if (!path) return; - - if (!confirm(`Supprimer définitivement le dossier "${path}" et TOUT son contenu ?`)) return; - - try { - const data = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { - method: 'DELETE', - }); - if (data?.status === 'ok' || data?.success) { - showToast(`🗑️ ${path} supprimé`, 'success'); - } else { - showToast('Erreur de suppression', 'error'); - } - } catch (e) { - showToast(`Erreur : ${e.message || e}`, 'error'); - } -} - -// ── Create DOM structure ── +// ── Create DOM ── function createDOM() { if (_overlay) return; - _overlay = document.createElement('div'); _overlay.className = 'command-palette-overlay'; _overlay.innerHTML = `
+ Ctrl+Shift+Espace
`; - _input = _overlay.querySelector('.cp-input'); _results = _overlay.querySelector('.cp-results'); - - _overlay.addEventListener('click', (e) => { - if (e.target === _overlay) close(); - }); - + _overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); }); _input.addEventListener('keydown', onKeyDown); _input.addEventListener('input', onInput); - document.body.appendChild(_overlay); } @@ -238,6 +284,8 @@ export function open(initialQuery = '') { _selectedIndex = 0; _lastResults = []; _results.innerHTML = ''; + _input.disabled = false; + _input.style.display = ''; _input.focus(); onInput(); } @@ -247,180 +295,136 @@ export function close() { _overlay.classList.remove('active'); if (_searchTimeout) clearTimeout(_searchTimeout); _searchTimeout = null; - const searchInput = document.querySelector('.search-input'); - if (searchInput) searchInput.focus(); + _mode = 'files'; + const si = document.querySelector('.search-input'); + if (si) si.focus(); } -function isOpen() { - return _overlay && _overlay.classList.contains('active'); -} +function isOpen() { return _overlay && _overlay.classList.contains('active'); } function onInput() { + if (_mode === 'browse') return; const val = _input.value; const newMode = val.startsWith('>') ? 'commands' : 'files'; if (newMode !== _mode) { _mode = newMode; - _input.placeholder = _mode === 'commands' - ? 'Tapez une commande...' - : 'Rechercher un fichier... (tapez > pour les commandes)'; + _input.placeholder = _mode === 'commands' ? 'Tapez une commande...' : 'Rechercher un fichier... (tapez > pour les commandes)'; } - if (_searchTimeout) clearTimeout(_searchTimeout); _searchTimeout = setTimeout(() => { _searchTimeout = null; - if (_mode === 'commands') { - searchCommands(val.slice(1).trim()); - } else { - searchFiles(val.trim()); - } + if (_mode === 'commands') searchCommands(val.slice(1).trim()); + else searchFiles(val.trim()); }, 150); } async function searchFiles(query) { - if (!query) { - _results.innerHTML = '
Commencez à taper pour chercher un fichier
'; - _lastResults = []; - return; - } - + if (!query) { _results.innerHTML = '
Commencez à taper pour chercher un fichier
'; _lastResults = []; return; } try { const data = await api(`/api/suggest?q=${encodeURIComponent(query)}&vault=all`); const suggestions = data.suggestions || []; - _lastResults = suggestions.map(s => ({ - type: 'file', - vault: s.vault, - path: s.path, - title: s.title, - label: s.title || s.path.split('/').pop(), - sublabel: `${s.vault}/${s.path}`, - })); + _lastResults = suggestions.map(s => ({ type: 'file', vault: s.vault, path: s.path, title: s.title, label: s.title || s.path.split('/').pop(), sublabel: `${s.vault}/${s.path}` })); renderResults(); - } catch { - _results.innerHTML = '
Erreur de recherche
'; - } + } catch { _results.innerHTML = '
Erreur de recherche
'; } } function searchCommands(query) { - if (!query) { - _lastResults = COMMANDS.map(c => ({ type: 'command', ...c })); - renderResults(); - return; - } - + if (!query) { _lastResults = COMMANDS.map(c => ({ type: 'command', ...c })); renderResults(); return; } const lower = query.toLowerCase(); - _lastResults = COMMANDS - .filter(c => c.label.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower) || (c.category && c.category.toLowerCase().includes(lower))) - .map(c => ({ type: 'command', ...c })); + _lastResults = COMMANDS.filter(c => c.label.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower) || (c.category && c.category.toLowerCase().includes(lower))).map(c => ({ type: 'command', ...c })); renderResults(); } +// ── Render ── function renderResults() { _selectedIndex = 0; - - if (_lastResults.length === 0) { - _results.innerHTML = '
Aucun résultat
'; - return; - } - - _results.innerHTML = _lastResults.map((item, i) => ` -
-
${escapeHtml(item.label)}
-
${escapeHtml(item.sublabel || item.description || '')}
-
- `).join(''); + if (_lastResults.length === 0) { _results.innerHTML = '
Aucun résultat
'; return; } + _results.innerHTML = _lastResults.map((item, i) => { + const cls = i === 0 ? 'cp-selected' : ''; + return `
${escapeHtml(item.label)}
${escapeHtml(item.sublabel || '')}
`; + }).join(''); _results.querySelectorAll('.cp-item').forEach(el => { - el.addEventListener('click', () => { - const idx = parseInt(el.dataset.index); - executeItem(idx); - }); + el.addEventListener('click', () => { executeItem(parseInt(el.dataset.index)); }); el.addEventListener('mouseenter', () => { document.querySelectorAll('.cp-item').forEach(e => e.classList.remove('cp-selected')); el.classList.add('cp-selected'); _selectedIndex = parseInt(el.dataset.index); }); }); - scrollToSelected(); } +// ── Keyboard ── function onKeyDown(e) { const items = _results.querySelectorAll('.cp-item'); - if (e.key === 'Escape') { - e.preventDefault(); - close(); - } else if (e.key === 'Enter') { - e.preventDefault(); - executeItem(_selectedIndex); - } else if (e.key === 'ArrowDown') { - e.preventDefault(); - _selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1); - updateSelection(items); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - _selectedIndex = Math.max(_selectedIndex - 1, 0); - updateSelection(items); - } else if (e.key === 'Tab') { - e.preventDefault(); - if (_mode === 'files') { - _input.value = '> '; - setTimeout(() => { - _input.selectionStart = _input.selectionEnd = _input.value.length; - }, 0); - } else { - _input.value = ''; + if (_mode === 'browse') { + if (e.key === 'Escape') { + e.preventDefault(); + close(); + } else if (e.key === 'Backspace') { + e.preventDefault(); + // Go up one level + const segs = _browse.path ? _browse.path.split('/') : []; + if (segs.length > 0) { + _browse.path = segs.slice(0, -1).join('/'); + renderBrowse(); + } else { + close(); + } + } else if (e.key === 'Enter') { + e.preventDefault(); + executeItem(_selectedIndex); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + _selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1); + updateSelection(items); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + _selectedIndex = Math.max(_selectedIndex - 1, 0); + updateSelection(items); } + return; + } + + // Non-browse mode + if (e.key === 'Escape') { e.preventDefault(); close(); } + else if (e.key === 'Enter') { e.preventDefault(); executeItem(_selectedIndex); } + else if (e.key === 'ArrowDown') { e.preventDefault(); _selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1); updateSelection(items); } + else if (e.key === 'ArrowUp') { e.preventDefault(); _selectedIndex = Math.max(_selectedIndex - 1, 0); updateSelection(items); } + else if (e.key === 'Tab') { + e.preventDefault(); + if (_mode === 'files') { _input.value = '> '; setTimeout(() => { _input.selectionStart = _input.selectionEnd = _input.value.length; }, 0); } + else { _input.value = ''; } onInput(); } } -function updateSelection(items) { - items.forEach((el, i) => { - el.classList.toggle('cp-selected', i === _selectedIndex); - }); - scrollToSelected(); -} +function updateSelection(items) { items.forEach((el, i) => el.classList.toggle('cp-selected', i === _selectedIndex)); scrollToSelected(); } -function scrollToSelected() { - const selected = _results.querySelector('.cp-selected'); - if (selected) { - selected.scrollIntoView({ block: 'nearest' }); - } -} +function scrollToSelected() { const sel = _results.querySelector('.cp-selected'); if (sel) sel.scrollIntoView({ block: 'nearest' }); } function executeItem(index) { const item = _lastResults[index]; if (!item) return; - if (item.type === 'file') { - close(); - openFile(item.vault, item.path); - } else if (item.type === 'command') { - item.action(); - } + if (item.type === 'file') { close(); openFile(item.vault, item.path); } + else if (item.type === 'command') { item.action(); } + else if (item.type === '_navigate') { _browse.path = item.go; renderBrowse(); } + else if (item.type === '_action') { item.action(); } + // _file and _empty are informational only } function escapeHtml(str) { if (!str) return ''; - return String(str) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } +// ── Init ── export function initCommandPalette() { document.addEventListener('keydown', (e) => { - // Ctrl+Shift+Espace — file palette - if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && (e.key === ' ' || e.code === 'Space')) { - e.preventDefault(); - if (isOpen()) { close(); } else { open(''); } - } - // Ctrl+Alt+Espace — command palette - if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === ' ' || e.code === 'Space')) { - e.preventDefault(); - if (isOpen()) { close(); } else { open('> '); } - } + if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && (e.key === ' ' || e.code === 'Space')) { e.preventDefault(); if (isOpen()) close(); else open(''); } + if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === ' ' || e.code === 'Space')) { e.preventDefault(); if (isOpen()) close(); else open('> '); } }); } \ No newline at end of file diff --git a/frontend/style.css b/frontend/style.css index 7c2d245..4edcc5c 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -6288,3 +6288,23 @@ body.popup-mode .content-area { } .cp-footer span:first-child::before { display: none; } + +/* ── Breadcrumb ── */ +.cp-breadcrumb { + display: flex; + align-items: center; + gap: 2px; + padding: 8px 14px; + font-size: 12px; + color: var(--text-secondary); + background: var(--bg-primary); + border-bottom: 1px solid var(--border); + overflow-x: auto; + white-space: nowrap; + scrollbar-width: none; +} +.cp-breadcrumb::-webkit-scrollbar { display: none; } +.cp-bc-root { font-weight: 600; color: var(--accent); } +.cp-bc-sep { color: var(--text-muted); padding: 0 2px; } +.cp-bc-item { cursor: pointer; padding: 1px 4px; border-radius: 3px; } +.cp-bc-item:hover { background: var(--bg-hover); }