feat: navigateur de dossiers dans la palette — créer/supprimer fichier/dossier par sélection visuelle
CI / lint (push) Failing after 4s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Successful in 9s

This commit is contained in:
2026-06-02 15:27:47 -04:00
parent 9f00455064
commit e405bdc929
2 changed files with 287 additions and 263 deletions
+267 -263
View File
@@ -8,225 +8,271 @@ import { showToast, toggleTheme, TabManager } from './ui.js';
import { openEditor } from './utils.js'; import { openEditor } from './utils.js';
import { state } from './state.js'; import { state } from './state.js';
// ── Helper: trigger goHome by clicking the logo ── // ── Helper: trigger goHome ──
function triggerGoHome() { function triggerGoHome() {
const logo = document.getElementById('header-logo'); const el = document.getElementById('header-logo');
if (logo) logo.click(); if (el) el.click();
} }
// ── Get current active file from tabs ── // ── Get current active file ──
function getCurrentFile() { function getCurrentFile() {
const activeId = TabManager._activeTabId; const id = TabManager._activeTabId;
if (activeId) { if (id) {
const tab = TabManager._tabs.find(t => t.id === activeId); const tab = TabManager._tabs.find(t => t.id === id);
if (tab) return { vault: tab.vault, path: tab.path }; 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; return null;
} }
// ── Get current vault from tabs ──
function getCurrentVault() { function getCurrentVault() {
const file = getCurrentFile(); const f = getCurrentFile();
if (file) return file.vault; if (f) return f.vault;
if (state.currentVault) return state.currentVault; if (state.currentVault) return state.currentVault;
if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name; if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name;
return null; return null;
} }
// ── Prompt for vault if ambiguous ── // ── DOM / state ──
function promptVault(hint) {
const vault = getCurrentVault();
if (vault) return vault;
return prompt(`${hint} — Nom de la vault :`) || '';
}
// ── DOM cache ──
let _overlay = null; let _overlay = null;
let _input = null; let _input = null;
let _results = null; let _results = null;
let _mode = 'files'; let _mode = 'files'; // 'files' | 'commands' | 'browse'
let _selectedIndex = 0; let _selectedIndex = 0;
let _searchTimeout = null; let _searchTimeout = null;
let _lastResults = []; let _lastResults = [];
// ── Available commands ── // Browse mode state
let _browse = {
action: null, // 'create-file' | 'create-dir' | 'delete-dir'
vault: null,
path: '',
name: '',
};
// ── Commands ──
const COMMANDS = [ const COMMANDS = [
// ── Navigation ── { id: 'home', label: '🏠 Accueil', description: "Retour à l'accueil", category: 'Navigation', action: () => { close(); triggerGoHome(); } },
{ id: 'home', label: '🏠 Accueil', description: 'Retour à l\'accueil', category: 'Navigation', { 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'); } },
action: () => { close(); triggerGoHome(); } }, { 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'); } },
// ── Fichiers ── { id: 'delete-current',label: '🗑️ Supprimer fichier courant',description: 'Supprimer le fichier de l\'onglet actif', category: 'Fichiers', action: async () => { close(); await deleteCurrentFile(); } },
{ id: 'edit-current', label: '✏️ Éditer fichier courant', description: 'Ouvrir le fichier actif dans l\'éditeur', category: 'Fichiers', { 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'); } },
action: () => { close(); const f = getCurrentFile(); if (f) openEditor(f.vault, f.path); else showToast('Aucun fichier ouvert', 'error'); } }, { 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: 'popout', label: '🪟 Pop-out', description: 'Ouvrir le document courant dans une nouvelle fenêtre', category: 'Fichiers', { 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');} } },
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: '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(); } },
{ 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(); } },
]; ];
// ── Create file action ── // ── Browse mode helpers ──
async function createFile() { function startBrowse(action) {
const vault = promptVault('Créer un fichier'); const vault = getCurrentVault();
if (!vault) return; 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) :'); function switchMode(newMode) {
if (!path) return; _mode = newMode;
if (_mode === 'browse') {
// Add .md extension if none _input.placeholder = 'Naviguez avec ↑↓, Enter pour entrer, Retour arrière pour monter';
if (!path.includes('.')) path += '.md'; _input.disabled = true;
_input.style.display = 'none';
try { } else {
const data = await api(`/api/file/${encodeURIComponent(vault)}`, { _input.disabled = false;
method: 'POST', _input.style.display = '';
headers: { 'Content-Type': 'application/json' }, const bc = document.getElementById('cp-breadcrumb');
body: JSON.stringify({ path, content: `# ${path.split('/').pop().replace('.md', '')}\n\n` }), if (bc) bc.style.display = 'none';
});
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');
} }
} }
// ── Create directory action ── // ── Render directory browser ──
async function createDirectory() { async function renderBrowse() {
const vault = promptVault('Créer un dossier'); _selectedIndex = 0;
if (!vault) return; _lastResults = [];
const { vault, path } = _browse;
const breadcrumbEl = document.getElementById('cp-breadcrumb');
const path = prompt('Chemin du dossier (ex: notes/projets) :'); // Show breadcrumb
if (!path) return; if (breadcrumbEl) {
breadcrumbEl.style.display = 'flex';
const segments = path ? path.split('/') : [];
let html = `<span class="cp-bc-item cp-bc-root">${escapeHtml(vault)}</span>`;
let cumul = '';
for (const seg of segments) {
cumul = cumul ? cumul + '/' + seg : seg;
html += `<span class="cp-bc-sep">/</span><span class="cp-bc-item" data-path="${escapeHtml(cumul)}">${escapeHtml(seg)}</span>`;
}
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 { try {
const data = await api(`/api/directory/${encodeURIComponent(vault)}`, { const data = await api(`/api/browse/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`);
method: 'POST', const items = data.items || [];
headers: { 'Content-Type': 'application/json' }, const dirs = items.filter(i => i.type === 'directory');
body: JSON.stringify({ path }), const files = items.filter(i => i.type === 'file');
});
if (data?.status === 'ok' || data?.success) { // Build result list
showToast(`📁 ${path} créé`, 'success'); const segments = path ? path.split('/') : [];
} else { const breadcrumb = vault + (path ? '/' + path : '');
showToast('Erreur de création', 'error');
// ── 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() { async function deleteCurrentFile() {
const f = getCurrentFile(); const f = getCurrentFile();
if (!f) { showToast('Aucun fichier ouvert', 'error'); return; } if (!f) { showToast('Aucun fichier ouvert', 'error'); return; }
if (!confirm(`Supprimer définitivement "${f.path}" ?`)) return; if (!confirm(`Supprimer définitivement "${f.path}" ?`)) return;
try { try {
const data = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { const data = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method: 'DELETE' });
method: 'DELETE',
});
if (data?.status === 'ok' || data?.success) { if (data?.status === 'ok' || data?.success) {
showToast(`🗑️ ${f.path} supprimé`, 'success'); showToast(`🗑️ ${f.path} supprimé`, 'success');
// Close the tab
const tab = TabManager._tabs.find(t => t.vault === f.vault && t.path === f.path); const tab = TabManager._tabs.find(t => t.vault === f.vault && t.path === f.path);
if (tab) TabManager.close(tab.id); if (tab) TabManager.close(tab.id);
triggerGoHome(); triggerGoHome();
} else { } else showToast('Erreur de suppression', 'error');
showToast('Erreur de suppression', 'error'); } catch (e) { showToast('Erreur : ' + (e.message||e), 'error'); }
}
} catch (e) {
showToast(`Erreur : ${e.message || e}`, 'error');
}
} }
// ── Delete directory action ── // ── Create DOM ──
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 ──
function createDOM() { function createDOM() {
if (_overlay) return; if (_overlay) return;
_overlay = document.createElement('div'); _overlay = document.createElement('div');
_overlay.className = 'command-palette-overlay'; _overlay.className = 'command-palette-overlay';
_overlay.innerHTML = ` _overlay.innerHTML = `
<div class="command-palette"> <div class="command-palette">
<div class="cp-header"> <div class="cp-header">
<input type="text" class="cp-input" placeholder="Rechercher un fichier... (tapez > pour les commandes)" spellcheck="false" autocomplete="off"> <input type="text" class="cp-input" placeholder="Rechercher un fichier... (tapez > pour les commandes)" spellcheck="false" autocomplete="off">
<div class="cp-breadcrumb" id="cp-breadcrumb" style="display:none"></div>
<span class="cp-hint">Ctrl+Shift+Espace</span> <span class="cp-hint">Ctrl+Shift+Espace</span>
</div> </div>
<div class="cp-results"></div> <div class="cp-results"></div>
<div class="cp-footer"> <div class="cp-footer">
<span>↑↓ naviguer</span> <span>↑↓ naviguer</span>
<span>↵ ouvrir</span> <span>↵ ouvrir/entrer</span>
<span>Tab commandes</span> <span>⌫ monter</span>
<span>Esc fermer</span> <span>Esc fermer</span>
</div> </div>
</div> </div>
`; `;
_input = _overlay.querySelector('.cp-input'); _input = _overlay.querySelector('.cp-input');
_results = _overlay.querySelector('.cp-results'); _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('keydown', onKeyDown);
_input.addEventListener('input', onInput); _input.addEventListener('input', onInput);
document.body.appendChild(_overlay); document.body.appendChild(_overlay);
} }
@@ -238,6 +284,8 @@ export function open(initialQuery = '') {
_selectedIndex = 0; _selectedIndex = 0;
_lastResults = []; _lastResults = [];
_results.innerHTML = ''; _results.innerHTML = '';
_input.disabled = false;
_input.style.display = '';
_input.focus(); _input.focus();
onInput(); onInput();
} }
@@ -247,180 +295,136 @@ export function close() {
_overlay.classList.remove('active'); _overlay.classList.remove('active');
if (_searchTimeout) clearTimeout(_searchTimeout); if (_searchTimeout) clearTimeout(_searchTimeout);
_searchTimeout = null; _searchTimeout = null;
const searchInput = document.querySelector('.search-input'); _mode = 'files';
if (searchInput) searchInput.focus(); const si = document.querySelector('.search-input');
if (si) si.focus();
} }
function isOpen() { function isOpen() { return _overlay && _overlay.classList.contains('active'); }
return _overlay && _overlay.classList.contains('active');
}
function onInput() { function onInput() {
if (_mode === 'browse') return;
const val = _input.value; const val = _input.value;
const newMode = val.startsWith('>') ? 'commands' : 'files'; const newMode = val.startsWith('>') ? 'commands' : 'files';
if (newMode !== _mode) { if (newMode !== _mode) {
_mode = newMode; _mode = newMode;
_input.placeholder = _mode === 'commands' _input.placeholder = _mode === 'commands' ? 'Tapez une commande...' : 'Rechercher un fichier... (tapez > pour les commandes)';
? 'Tapez une commande...'
: 'Rechercher un fichier... (tapez > pour les commandes)';
} }
if (_searchTimeout) clearTimeout(_searchTimeout); if (_searchTimeout) clearTimeout(_searchTimeout);
_searchTimeout = setTimeout(() => { _searchTimeout = setTimeout(() => {
_searchTimeout = null; _searchTimeout = null;
if (_mode === 'commands') { if (_mode === 'commands') searchCommands(val.slice(1).trim());
searchCommands(val.slice(1).trim()); else searchFiles(val.trim());
} else {
searchFiles(val.trim());
}
}, 150); }, 150);
} }
async function searchFiles(query) { async function searchFiles(query) {
if (!query) { if (!query) { _results.innerHTML = '<div class="cp-empty">Commencez à taper pour chercher un fichier</div>'; _lastResults = []; return; }
_results.innerHTML = '<div class="cp-empty">Commencez à taper pour chercher un fichier</div>';
_lastResults = [];
return;
}
try { try {
const data = await api(`/api/suggest?q=${encodeURIComponent(query)}&vault=all`); const data = await api(`/api/suggest?q=${encodeURIComponent(query)}&vault=all`);
const suggestions = data.suggestions || []; const suggestions = data.suggestions || [];
_lastResults = suggestions.map(s => ({ _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}` }));
type: 'file',
vault: s.vault,
path: s.path,
title: s.title,
label: s.title || s.path.split('/').pop(),
sublabel: `${s.vault}/${s.path}`,
}));
renderResults(); renderResults();
} catch { } catch { _results.innerHTML = '<div class="cp-empty cp-error">Erreur de recherche</div>'; }
_results.innerHTML = '<div class="cp-empty cp-error">Erreur de recherche</div>';
}
} }
function searchCommands(query) { function searchCommands(query) {
if (!query) { if (!query) { _lastResults = COMMANDS.map(c => ({ type: 'command', ...c })); renderResults(); return; }
_lastResults = COMMANDS.map(c => ({ type: 'command', ...c }));
renderResults();
return;
}
const lower = query.toLowerCase(); const lower = query.toLowerCase();
_lastResults = COMMANDS _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 }));
.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(); renderResults();
} }
// ── Render ──
function renderResults() { function renderResults() {
_selectedIndex = 0; _selectedIndex = 0;
if (_lastResults.length === 0) { _results.innerHTML = '<div class="cp-empty">Aucun résultat</div>'; return; }
if (_lastResults.length === 0) { _results.innerHTML = _lastResults.map((item, i) => {
_results.innerHTML = '<div class="cp-empty">Aucun résultat</div>'; const cls = i === 0 ? 'cp-selected' : '';
return; return `<div class="cp-item ${cls}" data-index="${i}"><div class="cp-item-label">${escapeHtml(item.label)}</div><div class="cp-item-sublabel">${escapeHtml(item.sublabel || '')}</div></div>`;
} }).join('');
_results.innerHTML = _lastResults.map((item, i) => `
<div class="cp-item ${i === 0 ? 'cp-selected' : ''}" data-index="${i}">
<div class="cp-item-label">${escapeHtml(item.label)}</div>
<div class="cp-item-sublabel">${escapeHtml(item.sublabel || item.description || '')}</div>
</div>
`).join('');
_results.querySelectorAll('.cp-item').forEach(el => { _results.querySelectorAll('.cp-item').forEach(el => {
el.addEventListener('click', () => { el.addEventListener('click', () => { executeItem(parseInt(el.dataset.index)); });
const idx = parseInt(el.dataset.index);
executeItem(idx);
});
el.addEventListener('mouseenter', () => { el.addEventListener('mouseenter', () => {
document.querySelectorAll('.cp-item').forEach(e => e.classList.remove('cp-selected')); document.querySelectorAll('.cp-item').forEach(e => e.classList.remove('cp-selected'));
el.classList.add('cp-selected'); el.classList.add('cp-selected');
_selectedIndex = parseInt(el.dataset.index); _selectedIndex = parseInt(el.dataset.index);
}); });
}); });
scrollToSelected(); scrollToSelected();
} }
// ── Keyboard ──
function onKeyDown(e) { function onKeyDown(e) {
const items = _results.querySelectorAll('.cp-item'); const items = _results.querySelectorAll('.cp-item');
if (e.key === 'Escape') { if (_mode === 'browse') {
e.preventDefault(); if (e.key === 'Escape') {
close(); e.preventDefault();
} else if (e.key === 'Enter') { close();
e.preventDefault(); } else if (e.key === 'Backspace') {
executeItem(_selectedIndex); e.preventDefault();
} else if (e.key === 'ArrowDown') { // Go up one level
e.preventDefault(); const segs = _browse.path ? _browse.path.split('/') : [];
_selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1); if (segs.length > 0) {
updateSelection(items); _browse.path = segs.slice(0, -1).join('/');
} else if (e.key === 'ArrowUp') { renderBrowse();
e.preventDefault(); } else {
_selectedIndex = Math.max(_selectedIndex - 1, 0); close();
updateSelection(items); }
} else if (e.key === 'Tab') { } else if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
if (_mode === 'files') { executeItem(_selectedIndex);
_input.value = '> '; } else if (e.key === 'ArrowDown') {
setTimeout(() => { e.preventDefault();
_input.selectionStart = _input.selectionEnd = _input.value.length; _selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1);
}, 0); updateSelection(items);
} else { } else if (e.key === 'ArrowUp') {
_input.value = ''; 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(); onInput();
} }
} }
function updateSelection(items) { function updateSelection(items) { items.forEach((el, i) => el.classList.toggle('cp-selected', i === _selectedIndex)); scrollToSelected(); }
items.forEach((el, i) => {
el.classList.toggle('cp-selected', i === _selectedIndex);
});
scrollToSelected();
}
function scrollToSelected() { function scrollToSelected() { const sel = _results.querySelector('.cp-selected'); if (sel) sel.scrollIntoView({ block: 'nearest' }); }
const selected = _results.querySelector('.cp-selected');
if (selected) {
selected.scrollIntoView({ block: 'nearest' });
}
}
function executeItem(index) { function executeItem(index) {
const item = _lastResults[index]; const item = _lastResults[index];
if (!item) return; if (!item) return;
if (item.type === 'file') { if (item.type === 'file') { close(); openFile(item.vault, item.path); }
close(); else if (item.type === 'command') { item.action(); }
openFile(item.vault, item.path); else if (item.type === '_navigate') { _browse.path = item.go; renderBrowse(); }
} else if (item.type === 'command') { else if (item.type === '_action') { item.action(); }
item.action(); // _file and _empty are informational only
}
} }
function escapeHtml(str) { function escapeHtml(str) {
if (!str) return ''; if (!str) return '';
return String(str) return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
} }
// ── Init ──
export function initCommandPalette() { export function initCommandPalette() {
document.addEventListener('keydown', (e) => { 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(''); }
if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && (e.key === ' ' || e.code === 'Space')) { if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === ' ' || e.code === 'Space')) { e.preventDefault(); if (isOpen()) close(); else open('> '); }
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('> '); }
}
}); });
} }
+20
View File
@@ -6288,3 +6288,23 @@ body.popup-mode .content-area {
} }
.cp-footer span:first-child::before { display: none; } .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); }