diff --git a/frontend/js/palette.js b/frontend/js/palette.js index d8f50eb..536bd03 100644 --- a/frontend/js/palette.js +++ b/frontend/js/palette.js @@ -17,6 +17,7 @@ function getCurrentFile() { // ── DOM / state ── let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = []; let _browse = { action: null, vault: null, path: '' }; +let _browseFilterEl = null, _browseFilter = '', _allItems = []; // ── Commands ── const CMDS = [ @@ -36,6 +37,7 @@ const CMDS = [ // ── Browse: vault picker ── function startBrowse(action) { _browse = { action, vault: null, path: '' }; + _browseFilter = ''; _allItems = []; switchMode('browse-vault'); renderVaultPicker(); } @@ -98,9 +100,57 @@ async function renderBrowse() { if (_items.length === 0) _items.push({ type:'_empty', label: '📂 Dossier vide', sublabel: '' }); } catch { _items = [{ type:'_error', label: 'Erreur', sublabel: '' }]; } + + // Store all items for filtering + _allItems = _items; + applyBrowseFilter(); +} + +// ── Browse filter ── +function applyBrowseFilter() { + const q = _browseFilter.toLowerCase().trim(); + const shouldKeep = (it) => { + if (!q) return true; + const label = (it.label || '').toLowerCase(); + const sublabel = (it.sublabel || '').toLowerCase(); + return label.includes(q) || sublabel.includes(q); + }; + // Always keep action row and parent nav + _items = _allItems.filter(it => it.type === '_action' || it.type === '_nav' && it.label.includes('..') || shouldKeep(it)); render(); } +function onBrowseFilterInput() { + _browseFilter = _browseFilterEl ? _browseFilterEl.value : ''; + applyBrowseFilter(); +} + +function onBrowseFilterKey(e) { + if (e.key === 'Escape') { + e.preventDefault(); + if (_browseFilter) { + // Clear filter + _browseFilter = ''; + if (_browseFilterEl) _browseFilterEl.value = ''; + applyBrowseFilter(); + } else { + close(); + } + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + const first = _results.querySelector('.cp-item'); + if (first) { first.classList.add('cp-selected'); _sel = 0; first.focus ? first.focus() : _overlay.focus(); } + } else if (e.key === 'Enter') { + e.preventDefault(); + if (_items.length > 0) exec(_sel); + } else if (e.key === 'Backspace' && !_browseFilter && !_browseFilterEl?.value) { + e.preventDefault(); + const segs = _browse.path ? _browse.path.split('/') : []; + if (segs.length > 0) { _browse.path = segs.slice(0, -1).join('/'); renderBrowse(); } + else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); } + } +} + // ── Name input modal ── function askName() { const label = _browse.action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier'; @@ -199,13 +249,20 @@ function createDOM() { _overlay.tabIndex = -1; // needed to receive keyboard events when input is hidden (browse mode) _overlay.innerHTML = `
Ctrl+Shift+Espace
+
-
`; + `; _input = _overlay.querySelector('.cp-input'); + _browseFilterEl = _overlay.querySelector('#cp-browse-filter'); _results = _overlay.querySelector('.cp-results'); _overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); }); _input.addEventListener('keydown', onKeyDown); _input.addEventListener('input', onInput); + // Browse filter events + if (_browseFilterEl) { + _browseFilterEl.addEventListener('input', onBrowseFilterInput); + _browseFilterEl.addEventListener('keydown', onBrowseFilterKey); + } // Also listen on overlay for browse modes (input is hidden) _overlay.addEventListener('keydown', onOverlayKeyDown); document.body.appendChild(_overlay); @@ -227,7 +284,10 @@ export function close() { _overlay.classList.remove('active'); if (_to) clearTimeout(_to); _to = null; _mode = 'files'; + _browseFilter = ''; + _allItems = []; cleanupNameInput(); + if (_browseFilterEl) { _browseFilterEl.style.display = 'none'; _browseFilterEl.value = ''; } const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none'; const si = document.querySelector('.search-input'); if (si) si.focus(); } @@ -239,9 +299,16 @@ function switchMode(m) { if (m === 'browse' || m === 'browse-vault') { _input.style.display = 'none'; _input.disabled = true; _overlay.focus(); // overlay receives keyboard events when input is hidden + if (m === 'browse') { + // Show browse filter for directory navigation + if (_browseFilterEl) { _browseFilterEl.style.display = ''; _browseFilterEl.value = _browseFilter; _browseFilterEl.focus(); } + } else { + if (_browseFilterEl) _browseFilterEl.style.display = 'none'; + } } else { _input.style.display = ''; _input.disabled = false; _input.focus(); // restore focus to search input + if (_browseFilterEl) _browseFilterEl.style.display = 'none'; const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none'; } } @@ -312,8 +379,9 @@ function scroll() { const s = _results.querySelector('.cp-selected'); if (s) s.s // ── Overlay keydown handler (for browse modes where input is hidden) ── function onOverlayKeyDown(e) { if (_mode !== 'browse' && _mode !== 'browse-vault') return; - // Don't handle if name input is active (it has its own keydown handler) + // Don't handle if name input or browse filter is active (they have their own handlers) if (document.getElementById('cp-name-input')) return; + if (document.activeElement === _browseFilterEl) return; onKeyDown(e); } diff --git a/frontend/style.css b/frontend/style.css index df333ed..0da9f35 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -6423,6 +6423,23 @@ body.popup-mode .content-area { margin-top: 6px; } +/* Browse filter in command palette */ +.cp-browse-filter { + width: calc(100% - 24px); + margin: 8px 12px 4px; + padding: 7px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.8rem; + font-family: inherit; + outline: none; + transition: border-color 0.15s; +} +.cp-browse-filter:focus { border-color: var(--accent); } +.cp-browse-filter::placeholder { color: var(--text-muted); } + /* ── Help Modal: Mobile responsive ── */ @media (max-width: 768px) { .help-container {