diff --git a/ROADMAP.md b/ROADMAP.md
index 5abb1f6..f5ef822 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -79,9 +79,9 @@
- Export dans la vue publique `/s/{token}/pdf` β
- GTK/Pango installΓ© dans le Dockerfile β
-### 3. Palette de commandes (Ctrl+P)
-- **Effort** : 1 jour | **Impact** : π‘
-- Navigation rapide style VS Code dans l'interface
+### β
3. Palette de commandes (Ctrl+P) β FAIT
+- Navigation rapide fichiers + commandes (> prefix) β
+- Module `frontend/js/palette.js` + intΓ©grΓ© dans app.js β
### 4. Drag & drop de fichiers
- **Effort** : 1-2 jours | **Impact** : π‘
diff --git a/frontend/js/app.js b/frontend/js/app.js
index 18bf117..0f45232 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -13,6 +13,7 @@ import * as Viewer from './viewer.js';
import * as Sync from './sync.js';
import { initGraphView } from './graph.js';
import * as Legacy from './legacy.js';
+import { initCommandPalette } from './palette.js';
// =========================================================================
// Initialization β mirrors the original app.js init() ordering
@@ -44,6 +45,7 @@ async function init() {
UI.FindInPageManager.init();
UI.ContextMenuManager.init();
UI.TabManager.init();
+ initCommandPalette();
const authOk = await Auth.AuthManager.initAuth();
diff --git a/frontend/js/palette.js b/frontend/js/palette.js
new file mode 100644
index 0000000..8fd72e5
--- /dev/null
+++ b/frontend/js/palette.js
@@ -0,0 +1,287 @@
+/* ObsiGate β Command Palette (Ctrl+P)
+ Quick file opener and command runner.
+ Inspired by VS Code's Ctrl+P / Ctrl+Shift+P.
+*/
+import { api } from './auth.js';
+import { openFile } from './viewer.js';
+import { showToast, toggleTheme } from './ui.js';
+import { state } from './state.js';
+
+// ββ Helper: trigger goHome by clicking the logo ββ
+function triggerGoHome() {
+ const logo = document.getElementById('header-logo');
+ if (logo) logo.click();
+}
+
+// ββ DOM cache (created in open) ββ
+let _overlay = null;
+let _input = null;
+let _results = null;
+let _mode = 'files'; // 'files' | 'commands'
+let _selectedIndex = 0;
+let _searchTimeout = null;
+let _lastResults = [];
+
+// ββ Available commands ββ
+const COMMANDS = [
+ { id: 'home', label: 'π Accueil', description: 'Retour Γ l\'accueil', action: () => { close(); triggerGoHome(); } },
+ { id: 'theme', label: 'π Changer le thΓ¨me', description: 'Basculer clair/sombre', action: () => { close(); toggleTheme(); } },
+ { id: 'reindex', label: 'π RΓ©indexer', description: 'Forcer la rΓ©indexation complΓ¨te', 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 l\'aide', action: () => { close(); const helpBtn = document.getElementById('header-help-btn'); if (helpBtn) helpBtn.click(); } },
+ { id: 'config', label: 'βοΈ Configuration', description: 'Ouvrir les paramΓ¨tres', action: () => { close(); document.querySelector('[data-tab-config]')?.click(); } },
+];
+
+// ββ Create DOM structure ββ
+function createDOM() {
+ if (_overlay) return;
+
+ _overlay = document.createElement('div');
+ _overlay.className = 'command-palette-overlay';
+ _overlay.innerHTML = `
+
+ `;
+
+ _input = _overlay.querySelector('.cp-input');
+ _results = _overlay.querySelector('.cp-results');
+
+ // Click outside to close (on overlay, not the palette dialog)
+ _overlay.addEventListener('click', (e) => {
+ if (e.target === _overlay) close();
+ });
+
+ // Keyboard events
+ _input.addEventListener('keydown', onKeyDown);
+ _input.addEventListener('input', onInput);
+
+ document.body.appendChild(_overlay);
+}
+
+// ββ Open / Close ββ
+export function open(initialQuery = '') {
+ createDOM();
+ _overlay.classList.add('active');
+ _input.value = initialQuery;
+ _mode = initialQuery.startsWith('>') ? 'commands' : 'files';
+ _selectedIndex = 0;
+ _lastResults = [];
+ _results.innerHTML = '';
+ _input.focus();
+ // Trigger initial search
+ onInput();
+}
+
+export function close() {
+ if (!_overlay) return;
+ _overlay.classList.remove('active');
+ if (_searchTimeout) clearTimeout(_searchTimeout);
+ _searchTimeout = null;
+ // Restore focus
+ const searchInput = document.querySelector('.search-input');
+ if (searchInput) searchInput.focus();
+}
+
+function isOpen() {
+ return _overlay && _overlay.classList.contains('active');
+}
+
+// ββ Input handler ββ
+function onInput() {
+ const val = _input.value;
+
+ // Detect mode change
+ 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)';
+ }
+
+ if (_searchTimeout) clearTimeout(_searchTimeout);
+ _searchTimeout = setTimeout(() => {
+ _searchTimeout = null;
+ if (_mode === 'commands') {
+ searchCommands(val.slice(1).trim());
+ } else {
+ searchFiles(val.trim());
+ }
+ }, 150);
+}
+
+// ββ Search files (via /api/suggest) ββ
+async function searchFiles(query) {
+ 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}`,
+ }));
+ renderResults();
+ } catch {
+ _results.innerHTML = 'Erreur de recherche
';
+ }
+}
+
+// ββ Search commands ββ
+function searchCommands(query) {
+ 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))
+ .map(c => ({ type: 'command', ...c }));
+ renderResults();
+}
+
+// ββ Render results list ββ
+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('');
+
+ // Click handler
+ _results.querySelectorAll('.cp-item').forEach(el => {
+ el.addEventListener('click', () => {
+ const idx = parseInt(el.dataset.index);
+ executeItem(idx);
+ });
+ 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 navigation ββ
+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();
+ // Switch between files/commands mode
+ if (_mode === 'files') {
+ _input.value = '> ';
+ // Move cursor to end
+ 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 scrollToSelected() {
+ const selected = _results.querySelector('.cp-selected');
+ if (selected) {
+ selected.scrollIntoView({ block: 'nearest' });
+ }
+}
+
+// ββ Execute ββ
+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();
+ }
+}
+
+// ββ HTML escaping ββ
+function escapeHtml(str) {
+ if (!str) return '';
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+// ββ Init: Register global keyboard shortcut ββ
+export function initCommandPalette() {
+ document.addEventListener('keydown', (e) => {
+ // Ctrl+P or Cmd+P β open palette
+ if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
+ e.preventDefault();
+ if (isOpen()) {
+ close();
+ } else {
+ open('');
+ }
+ }
+ // Ctrl+Shift+P or Cmd+Shift+P β open in command mode
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'p') {
+ e.preventDefault();
+ if (isOpen()) {
+ close();
+ } else {
+ open('> ');
+ }
+ }
+ });
+}
\ No newline at end of file
diff --git a/frontend/style.css b/frontend/style.css
index 5aa937d..8bf5e2e 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -6132,3 +6132,160 @@ body.popup-mode .content-area {
.dashboard-panel.active {
display: block;
}
+
+/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Command Palette (Ctrl+P)
+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+.command-palette-overlay {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: 0 0;
+ z-index: 10000;
+ display: flex;
+ justify-content: center;
+ padding-top: 12vh;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.1s ease, background 0.1s ease;
+}
+
+.command-palette-overlay.active {
+ opacity: 1;
+ background: var(--overlay-bg);
+ pointer-events: auto;
+}
+
+.command-palette {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ box-shadow: 0 8px 40px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.03);
+ width: 580px;
+ max-width: calc(100vw - 40px);
+ max-height: 60vh;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ animation: cp-slide-in 0.12s ease-out;
+}
+
+@keyframes cp-slide-in {
+ from { transform: translateY(-12px); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+
+.cp-header {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid var(--border);
+ padding: 0 14px;
+}
+
+.cp-input {
+ flex: 1;
+ border: none;
+ background: none;
+ padding: 14px 0;
+ font-size: 16px;
+ color: var(--text-primary);
+ outline: none;
+ font-family: inherit;
+}
+
+.cp-input::placeholder {
+ color: var(--text-muted);
+}
+
+.cp-hint {
+ font-size: 11px;
+ color: var(--text-muted);
+ background: var(--bg-hover);
+ padding: 3px 8px;
+ border-radius: 4px;
+ font-family: "JetBrains Mono", monospace;
+ opacity: 0.7;
+}
+
+.cp-results {
+ flex: 1;
+ overflow-y: auto;
+ padding: 6px 0;
+ max-height: 40vh;
+}
+
+.cp-empty {
+ padding: 24px 16px;
+ text-align: center;
+ color: var(--text-muted);
+ font-size: 13px;
+}
+
+.cp-error {
+ color: var(--accent-orange, #d29922);
+}
+
+.cp-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 14px;
+ cursor: pointer;
+ border-left: 3px solid transparent;
+ /* Uncomment for smooth scroll perf */;
+}
+
+.cp-item:hover {
+ background: var(--bg-hover);
+ border-left-color: var(--accent);
+}
+
+.cp-item.cp-selected {
+ background: var(--accent-bg);
+ border-left-color: var(--accent);
+}
+
+.cp-item-label {
+ font-size: 14px;
+ color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cp-item-sublabel {
+ font-size: 11px;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 45%;
+ text-align: right;
+ margin-left: 12px;
+}
+
+.cp-footer {
+ display: flex;
+ gap: 14px;
+ padding: 8px 14px;
+ border-top: 1px solid var(--border);
+ font-size: 11px;
+ color: var(--text-muted);
+}
+
+.cp-footer span {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.cp-footer span::before {
+ content: '';
+ display: inline-block;
+ width: 3px; height: 3px;
+ background: var(--text-muted);
+ border-radius: 50%;
+ margin-right: 4px;
+}
+
+.cp-footer span:first-child::before { display: none; }