diff --git a/ROADMAP.md b/ROADMAP.md
index c04a83e..bf0dc87 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -153,13 +153,24 @@
- **Effort** : 0.5 jour | **Impact** : 🟢
- `indexer.py` et `watcher.py` ont des définitions séparées → mutualiser
-### 11. Indicateur AI Actif
+### 11. Indicateur AI Actif — FAIT
- **Effort** : 0.5 jour | **Impact** : 🟡
-- Dans le bouton/indicateur à droite du titre, en cliquant il y a un panneau avec les statut de syncro. Ajouter un indicateur visuel dans ce panneau pour indiquer si l'IA est actif, configuré, et fonctionnelle. Durant la phase de traitement de l'IA, le bouton/indicateur devrait changer de couleur pour un blanc qui flashindiquant le statut de la tâche AI en cours.
+- Dans le bouton/indicateur à droite du titre, en cliquant il y a un panneau avec les statuts de syncro. Ajouter un indicateur visuel dans ce panneau pour indiquer si l'IA est active, configurée, et fonctionnelle. Durant la phase de traitement de l'IA, le bouton/indicateur devrait changer de couleur pour un blanc qui flash indiquant le statut de la tâche AI en cours. ✅
+- **Fichiers** : `backend/main.py` (`/api/ai/status`), `frontend/js/ai.js`, `frontend/js/sync.js`, `frontend/style.css`
+- [x] **Badge header flash blanc** : le `#sync-badge` devient blanc avec animation flash quand une tâche AI est en cours
+- [x] **Événements** : `ai-processing-start` / `ai-processing-end` dispatchés depuis `ai.js`
+- [x] **Section IA dans panneau sync** : statut (configurée/non configurée/traitement en cours) avec icône et couleur
+- [x] **Endpoint `/api/ai/status`** : vérifie si au moins un provider AI est configuré
-### 11. Page d'accueil de voute
+### 11b. Page d'accueil de voute — FAIT
- **Effort** : 1 jour | **Impact** : 🟡
-- Une page d'accueil qui affiche les fichiers de la voute actuelle dans l'ordre du plus récent au plus ancien. Si l'utilisateur change de répertoire dans cette voute, la liste devrait se mettre à jour automatiquement.
+- Une page d'accueil qui affiche les fichiers de la voute actuelle dans l'ordre du plus récent au plus ancien. Si l'utilisateur change de répertoire dans cette voute, la liste devrait se mettre à jour automatiquement. ✅
+- **Fichiers** : `backend/main.py` (`/api/vault/{vault}/files`), `frontend/js/vaulthome.js`, `frontend/js/sidebar.js`, `frontend/style.css`
+- [x] **Endpoint `/api/vault/{vault}/files?dir=...`** : liste les fichiers d'un répertoire triés par date de modification décroissante
+- [x] **Affichage style résultats de recherche** : chaque fichier utilise le même look `.search-result-item` (extension, icône, titre, métadonnées)
+- [x] **Breadcrumb navigable** : fil d'Ariane cliquable (vault → dossier → sous-dossier)
+- [x] **Mise Ă jour automatique** : quand on clique sur un dossier dans l'arborescence, la page se met Ă jour
+- [x] **Déclenchement** : affiché automatiquement quand un vault spécifique est sélectionné
---
diff --git a/backend/main.py b/backend/main.py
index fa85e85..7b70738 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -2162,6 +2162,21 @@ async def api_file_restore(
raise HTTPException(status_code=500, detail=f"Error restoring file: {str(e)}")
+@app.get("/api/ai/status")
+async def api_ai_status(current_user=Depends(require_auth)):
+ """Return the AI configuration status.
+
+ Checks whether any AI provider API key is configured.
+ Does NOT expose the key itself, only a boolean 'configured' flag.
+ """
+ providers = ["DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY"]
+ configured = any(
+ os.environ.get(env_var, "").strip()
+ for env_var in providers
+ )
+ return {"configured": configured, "providers_checked": providers}
+
+
@app.get("/api/file/{vault_name}/backlinks")
async def api_file_backlinks(
vault_name: str,
@@ -3068,6 +3083,82 @@ async def api_update_vault_settings(vault_name: str, body: dict = Body(...), cur
return updated
+@app.get("/api/vault/{vault_name}/files")
+async def api_vault_recent_files(
+ vault_name: str,
+ dir: str = Query("", description="Directory path within the vault (empty = root)"),
+ limit: int = Query(100, description="Maximum number of files to return"),
+ current_user=Depends(require_auth),
+):
+ """List files in a vault directory sorted by modification time (newest first).
+
+ Returns file metadata suitable for a vault home page display.
+ Unlike /api/browse, this endpoint sorts by mtime and returns
+ additional metadata (size, modified time, extension).
+
+ Args:
+ vault_name: Name of the vault.
+ dir: Relative directory path within the vault (empty for root).
+ limit: Maximum files to return (default 100).
+
+ Returns:
+ JSON with vault, directory, and list of file entries.
+ """
+ if not check_vault_access(vault_name, current_user):
+ raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
+
+ vault_data = get_vault_data(vault_name)
+ if not vault_data:
+ raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
+
+ vault_root = Path(vault_data["path"])
+ dir_path = _resolve_safe_path(vault_root, dir) if dir else vault_root
+
+ if not dir_path.exists() or not dir_path.is_dir():
+ raise HTTPException(status_code=404, detail=f"Directory not found: {dir}")
+
+ files = []
+ ignored = {'.obsidian', '.trash', '.git', '.obsigate-backup', '__pycache__', 'node_modules'}
+
+ try:
+ for entry in dir_path.iterdir():
+ if entry.name.startswith('.') and entry.name not in ('.', '..'):
+ continue
+ if entry.name in ignored:
+ continue
+ if not entry.is_file():
+ continue
+
+ stat = entry.stat()
+ rel_path = str(entry.relative_to(vault_root)).replace("\\", "/")
+ ext = entry.suffix.lower() if entry.suffix else ""
+
+ files.append({
+ "name": entry.name,
+ "path": rel_path,
+ "vault": vault_name,
+ "size": stat.st_size,
+ "modified": stat.st_mtime,
+ "modified_iso": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
+ "extension": ext.lstrip(".") if ext else "",
+ })
+ except PermissionError:
+ raise HTTPException(status_code=403, detail="Permission denied reading directory")
+
+ # Sort by modification time, newest first
+ files.sort(key=lambda f: f["modified"], reverse=True)
+
+ # Apply limit
+ files = files[:limit]
+
+ return {
+ "vault": vault_name,
+ "directory": dir,
+ "count": len(files),
+ "files": files,
+ }
+
+
@app.get("/api/vaults/settings/all")
async def api_get_all_vault_settings(current_user=Depends(require_auth)):
"""Get UI display settings for all vaults.
diff --git a/frontend/js/ai.js b/frontend/js/ai.js
index 34da007..5468ce8 100644
--- a/frontend/js/ai.js
+++ b/frontend/js/ai.js
@@ -347,6 +347,8 @@ export async function createAIToolbar(container, getEditorView) {
return;
}
showToast('⏳ AI: traitement en cours...', 'info');
+ // Dispatch AI processing start event
+ document.dispatchEvent(new CustomEvent('ai-processing-start', { detail: { endpoint, mode } }));
try {
const result = await aiAction(endpoint, text, extra);
replaceSelection(v, result, mode);
@@ -360,9 +362,11 @@ export async function createAIToolbar(container, getEditorView) {
} else {
showToast('AI: ' + msg, 'error');
}
+ } finally {
+ // Dispatch AI processing end event
+ document.dispatchEvent(new CustomEvent('ai-processing-end'));
}
}
-
// ── Keyboard shortcut Ctrl+J for inline completion ──
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'j') {
diff --git a/frontend/js/sidebar.js b/frontend/js/sidebar.js
index be4cd6e..b265077 100644
--- a/frontend/js/sidebar.js
+++ b/frontend/js/sidebar.js
@@ -43,6 +43,10 @@ async function setSelectedVaultContext(vaultName, options) {
}
showWelcome();
+ // If a specific vault is selected, switch to vault home page
+ if (vaultName !== "all") {
+ import('./vaulthome.js').then(m => m.showVaultHome(vaultName, ''));
+ }
if (options && options.focusVault && vaultName !== "all") {
await focusVaultInSidebar(vaultName);
}
diff --git a/frontend/js/sync.js b/frontend/js/sync.js
index 33ac33d..adeac36 100644
--- a/frontend/js/sync.js
+++ b/frontend/js/sync.js
@@ -246,6 +246,9 @@ function initSyncStatus() {
});
IndexUpdateManager.connect();
+
+ // Initialize AI status tracking
+ _initAIStatus();
}
function toggleSyncPanel() {
@@ -323,6 +326,118 @@ function _renderSyncPanel(panel) {
}
panel.innerHTML = html;
+
+ // ── AI Status section ──
+ _renderAIStatusSection(panel);
+}
+
+// ---------------------------------------------------------------------------
+// AI Status Indicator (inside sync panel + header badge animation)
+// ---------------------------------------------------------------------------
+
+let _aiProcessing = false;
+let _aiStatusChecked = false;
+let _aiConfigured = null; // null = unknown, true/false
+
+/** Check if AI is configured by calling the status endpoint. */
+async function _checkAIStatus() {
+ if (_aiStatusChecked) return;
+ try {
+ const resp = await api('/api/ai/status');
+ _aiConfigured = !!(resp && resp.configured);
+ } catch (e) {
+ _aiConfigured = false;
+ }
+ _aiStatusChecked = true;
+}
+
+function _renderAIStatusSection(panel) {
+ const section = document.createElement('div');
+ section.className = 'sync-panel__ai-section';
+ section.id = 'sync-panel-ai';
+
+ let statusIcon, statusText, statusClass;
+ if (_aiProcessing) {
+ statusIcon = 'loader';
+ statusText = 'Traitement en cours…';
+ statusClass = 'sync-panel__ai--processing';
+ } else if (_aiConfigured === true) {
+ statusIcon = 'check-circle';
+ statusText = 'IA configurée et active';
+ statusClass = 'sync-panel__ai--active';
+ } else if (_aiConfigured === false) {
+ statusIcon = 'x-circle';
+ statusText = 'IA non configurée';
+ statusClass = 'sync-panel__ai--inactive';
+ } else {
+ statusIcon = 'help-circle';
+ statusText = 'Statut IA inconnu';
+ statusClass = 'sync-panel__ai--unknown';
+ }
+
+ section.innerHTML = `
+
+ `;
+
+ panel.appendChild(section);
+ // Trigger icon refresh via global
+ if (window._obsigateRefreshIcons) window._obsigateRefreshIcons();
+}
+
+/** Update the AI section in the open sync panel. */
+function _updateAIStatusInPanel() {
+ const section = document.getElementById('sync-panel-ai');
+ if (!section) return;
+ section.innerHTML = '';
+ const panel = document.getElementById('sync-panel');
+ if (panel) {
+ // Re-render just the AI section
+ const oldSection = panel.querySelector('.sync-panel__ai-section');
+ if (oldSection) oldSection.remove();
+ _renderAIStatusSection(panel);
+ }
+}
+
+/** Flash the sync badge white during AI processing. */
+function _setBadgeAIProcessing(active) {
+ const badge = document.getElementById('sync-badge');
+ if (!badge) return;
+ if (active) {
+ badge.classList.add('sync-badge--ai-processing');
+ } else {
+ badge.classList.remove('sync-badge--ai-processing');
+ }
+}
+
+/** Initialize AI status tracking. */
+function _initAIStatus() {
+ // Listen for AI processing events from ai.js
+ document.addEventListener('ai-processing-start', () => {
+ _aiProcessing = true;
+ _setBadgeAIProcessing(true);
+ _updateAIStatusInPanel();
+ });
+
+ document.addEventListener('ai-processing-end', () => {
+ _aiProcessing = false;
+ _setBadgeAIProcessing(false);
+ _updateAIStatusInPanel();
+ });
+
+ // Check AI status on first sync panel open
+ const badge = document.getElementById('sync-badge');
+ if (badge) {
+ const origClick = badge.onclick;
+ badge.addEventListener('click', () => {
+ _checkAIStatus().then(() => _updateAIStatusInPanel());
+ }, { once: false });
+ }
}
// ---------------------------------------------------------------------------
diff --git a/frontend/js/vaulthome.js b/frontend/js/vaulthome.js
new file mode 100644
index 0000000..179be25
--- /dev/null
+++ b/frontend/js/vaulthome.js
@@ -0,0 +1,284 @@
+/**
+ * Vault Home Page module for ObsiGate.
+ *
+ * When a specific vault is selected (not "All"), replaces the dashboard
+ * with a file listing sorted by modification time (newest first).
+ * The listing updates automatically when browsing into subdirectories.
+ *
+ * The visual style mirrors the search results (.search-result-item) for
+ * consistency.
+ *
+ * Usage:
+ * import { showVaultHome, updateVaultHomeDir, isVaultHomeActive } from './vaulthome.js';
+ */
+
+import { api } from './auth.js';
+import { el, safeCreateIcons, getFileIcon } from './viewer.js';
+import { TabManager } from './ui.js';
+
+// ---------------------------------------------------------------------------
+// State
+// ---------------------------------------------------------------------------
+
+const state = {
+ active: false,
+ currentVault: null,
+ currentDir: '', // empty = vault root
+};
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+/** Check if vault home is currently displayed. */
+export function isVaultHomeActive() {
+ return state.active;
+}
+
+/**
+ * Display the vault home page for a vault directory.
+ * @param {string} vaultName - Vault name
+ * @param {string} dirPath - Directory path (empty for root)
+ */
+export async function showVaultHome(vaultName, dirPath = '') {
+ if (vaultName === 'all' || !vaultName) {
+ state.active = false;
+ return;
+ }
+
+ state.active = true;
+ state.currentVault = vaultName;
+ state.currentDir = dirPath || '';
+
+ const area = document.getElementById('content-area');
+ if (!area) return;
+
+ // Remove any existing dashboard
+ const home = document.getElementById('dashboard-home');
+ if (home) home.remove();
+
+ // Build vault home container
+ area.innerHTML = '';
+
+ const container = el('div', { class: 'vault-home', id: 'vault-home' });
+ area.appendChild(container);
+
+ // Breadcrumb
+ renderBreadcrumb(container, vaultName, state.currentDir);
+
+ // Results container
+ const resultsArea = el('div', { class: 'vault-home-results', id: 'vault-home-results' });
+ container.appendChild(resultsArea);
+
+ // Show loading
+ resultsArea.innerHTML = `
+
+
+
Chargement des fichiers...
+
`;
+
+ await loadAndRender(vaultName, state.currentDir, resultsArea);
+}
+
+/**
+ * Update the vault home directory without rebuilding the whole page.
+ * Called when navigating into a subdirectory.
+ * @param {string} vaultName
+ * @param {string} dirPath
+ */
+export async function updateVaultHomeDir(vaultName, dirPath) {
+ if (!state.active || state.currentVault !== vaultName) return;
+
+ state.currentDir = dirPath || '';
+
+ const container = document.getElementById('vault-home');
+ if (!container) return;
+
+ // Update breadcrumb
+ const bc = container.querySelector('.vault-home-breadcrumb');
+ if (bc) {
+ bc.innerHTML = '';
+ buildBreadcrumbHTML(bc, vaultName, state.currentDir);
+ safeCreateIcons();
+ }
+
+ // Reload results
+ const resultsArea = document.getElementById('vault-home-results');
+ if (resultsArea) {
+ resultsArea.innerHTML = `
+ `;
+ await loadAndRender(vaultName, state.currentDir, resultsArea);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Tree click delegation — update vault home on directory clicks
+// ---------------------------------------------------------------------------
+
+function _initTreeDelegation() {
+ const tree = document.getElementById('vault-tree');
+ if (!tree) return;
+
+ tree.addEventListener('click', (e) => {
+ if (!state.active) return;
+
+ // Find the closest tree-item that is a directory
+ const item = e.target.closest('.tree-item');
+ if (!item || !item.dataset.vault || item.dataset.path === undefined) return;
+
+ const vault = item.dataset.vault;
+ const path = item.dataset.path;
+
+ // Only react to directory clicks (items that have a child container)
+ const hasChildContainer = item.nextElementSibling
+ && item.nextElementSibling.classList.contains('tree-children');
+ if (!hasChildContainer) return;
+
+ // Update vault home dir
+ updateVaultHomeDir(vault, path);
+ });
+}
+
+// Initialize delegation when module loads
+_initTreeDelegation();
+
+// ---------------------------------------------------------------------------
+// Rendering
+// ---------------------------------------------------------------------------
+
+function renderBreadcrumb(container, vaultName, dirPath) {
+ const bc = el('div', { class: 'vault-home-breadcrumb' });
+ buildBreadcrumbHTML(bc, vaultName, dirPath);
+ container.appendChild(bc);
+}
+
+function buildBreadcrumbHTML(parentEl, vaultName, dirPath) {
+ // Vault icon + name (root)
+ const vaultLink = el('span', { class: 'vault-home-breadcrumb-item vault-home-breadcrumb-root' }, [
+ el('i', { 'data-lucide': 'database', style: 'width:14px;height:14px' }),
+ el('span', {}, [document.createTextNode(vaultName)]),
+ ]);
+ vaultLink.addEventListener('click', () => updateVaultHomeDir(vaultName, ''));
+ vaultLink.style.cursor = 'pointer';
+ parentEl.appendChild(vaultLink);
+
+ if (!dirPath) return;
+
+ const segments = dirPath.split('/').filter(Boolean);
+ let cumulative = '';
+ for (const seg of segments) {
+ cumulative = cumulative ? cumulative + '/' + seg : seg;
+ const sep = el('span', { class: 'vault-home-breadcrumb-sep' }, [document.createTextNode(' / ')]);
+ parentEl.appendChild(sep);
+ const segLink = el('span', { class: 'vault-home-breadcrumb-item' }, [
+ el('i', { 'data-lucide': 'folder', style: 'width:14px;height:14px' }),
+ el('span', {}, [document.createTextNode(seg)]),
+ ]);
+ segLink.addEventListener('click', () => updateVaultHomeDir(vaultName, cumulative));
+ segLink.style.cursor = 'pointer';
+ parentEl.appendChild(segLink);
+ }
+}
+
+async function loadAndRender(vaultName, dirPath, container) {
+ try {
+ const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200`;
+ const data = await api(url);
+
+ if (!data.files || data.files.length === 0) {
+ container.innerHTML = `
+
+
+
Aucun fichier dans ce répertoire
+
`;
+ safeCreateIcons();
+ return;
+ }
+
+ // Header
+ const header = el('div', { class: 'vault-home-header' });
+ const count = el('span', { class: 'vault-home-count' }, [
+ document.createTextNode(`${data.files.length} fichier${data.files.length > 1 ? 's' : ''}`),
+ ]);
+ header.appendChild(count);
+
+ // Sort dropdown (already sorted by mtime from backend)
+ const dirLabel = dirPath ? `/${dirPath}` : ' (racine)';
+ const dirInfo = el('span', { class: 'vault-home-dir-info' }, [
+ document.createTextNode(dirLabel),
+ ]);
+ header.appendChild(dirInfo);
+
+ container.innerHTML = '';
+ container.appendChild(header);
+
+ // File list — using search-result-item style
+ const list = el('div', { class: 'search-results' });
+
+ data.files.forEach((f) => {
+ const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file';
+ const displayName = f.name.match(/\.md$/i) ? f.name.replace(/\.md$/i, '') : f.name;
+ const ext = f.extension || '';
+
+ const titleDiv = el('div', { class: 'search-result-title' }, [
+ document.createTextNode(displayName),
+ ]);
+
+ // Format date
+ const modDate = new Date(f.modified * 1000);
+ const dateStr = modDate.toLocaleDateString('fr-CA', {
+ year: 'numeric', month: 'short', day: 'numeric',
+ hour: '2-digit', minute: '2-digit',
+ });
+
+ const metaDiv = el('div', { class: 'search-result-vault' }, [
+ document.createTextNode(`${dateStr} · ${formatSize(f.size)}`),
+ ]);
+
+ const item = el('div', {
+ class: 'search-result-item vault-home-item',
+ 'data-vault': f.vault,
+ 'data-path': f.path,
+ }, [
+ el('span', { class: 'search-result-ext' }, [document.createTextNode(ext)]),
+ el('i', { 'data-lucide': fileIconName, style: 'width:14px;height:14px;margin-right:4px;opacity:0.6' }),
+ titleDiv,
+ metaDiv,
+ ]);
+
+ item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
+ item.addEventListener('dblclick', (e) => {
+ e.preventDefault();
+ TabManager.openPersistent(f.vault, f.path);
+ });
+
+ list.appendChild(item);
+ });
+
+ container.appendChild(list);
+ safeCreateIcons();
+ } catch (err) {
+ container.innerHTML = `
+
+
Erreur de chargement: ${escapeHtml(err?.detail || err?.message || 'Inconnue')}
+
`;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Utility
+// ---------------------------------------------------------------------------
+
+function formatSize(bytes) {
+ if (bytes < 1024) return bytes + ' o';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko';
+ return (bytes / (1024 * 1024)).toFixed(1) + ' Mo';
+}
+
+function escapeHtml(str) {
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+}
diff --git a/frontend/style.css b/frontend/style.css
index b8b8adc..0930cff 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -3872,6 +3872,184 @@ body.resizing-v {
}
}
+/* AI processing — white flash on the sync badge */
+.sync-badge--ai-processing {
+ background: #ffffff !important;
+ box-shadow: 0 0 6px 3px rgba(255, 255, 255, 0.6) !important;
+ animation: ai-flash 0.6s ease-in-out infinite;
+}
+
+@keyframes ai-flash {
+ 0%, 100% { opacity: 1; box-shadow: 0 0 6px 3px rgba(255,255,255,0.6); }
+ 50% { opacity: 0.3; box-shadow: 0 0 12px 6px rgba(255,255,255,0.9); }
+}
+
+/* ── AI Status section inside sync panel ── */
+
+.sync-panel__ai-section {
+ border-top: 1px solid var(--border);
+ padding: 10px 14px;
+}
+
+.sync-panel__ai-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.sync-panel__ai-title {
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.sync-panel__ai-state {
+ font-size: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 2px 8px;
+ border-radius: 4px;
+}
+
+.sync-panel__ai-state i {
+ flex-shrink: 0;
+}
+
+.sync-panel__ai--active {
+ color: #22c55e;
+ background: rgba(34, 197, 94, 0.1);
+}
+
+.sync-panel__ai--inactive {
+ color: #f59e0b;
+ background: rgba(245, 158, 11, 0.1);
+}
+
+.sync-panel__ai--processing {
+ color: #ffffff;
+ background: rgba(59, 130, 246, 0.2);
+ animation: ai-status-pulse 1s ease-in-out infinite;
+}
+
+.sync-panel__ai--unknown {
+ color: var(--text-muted);
+ background: var(--bg-tertiary);
+}
+
+@keyframes ai-status-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+/* ---------------------------------------------------------------------------
+ Vault Home Page
+ --------------------------------------------------------------------------- */
+
+.vault-home {
+ padding: 16px 24px;
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.vault-home-breadcrumb {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 2px;
+ font-size: 0.85rem;
+ margin-bottom: 16px;
+ padding: 8px 12px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.vault-home-breadcrumb-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ color: var(--text-secondary);
+ transition: background 0.15s, color 0.15s;
+ user-select: none;
+}
+
+.vault-home-breadcrumb-item:hover {
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
+ color: var(--text-primary);
+}
+
+.vault-home-breadcrumb-root {
+ font-weight: 600;
+ color: var(--accent);
+}
+
+.vault-home-breadcrumb-sep {
+ color: var(--text-muted);
+ user-select: none;
+ font-size: 0.8rem;
+}
+
+.vault-home-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--border);
+}
+
+.vault-home-count {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ background: var(--bg-secondary);
+ padding: 3px 10px;
+ border-radius: 4px;
+}
+
+.vault-home-dir-info {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+}
+
+.vault-home-results {
+ min-height: 200px;
+}
+
+.vault-home-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem;
+ color: var(--text-muted);
+ gap: 8px;
+}
+
+.vault-home-empty p {
+ font-size: 0.85rem;
+ margin: 0;
+}
+
+.vault-home-error {
+ padding: 2rem;
+ text-align: center;
+ color: var(--danger, #ef4444);
+ font-size: 0.85rem;
+}
+
+.vault-home-item {
+ cursor: pointer;
+}
+
+.vault-home-item:hover {
+ background: var(--bg-secondary);
+}
+
/* ---------------------------------------------------------------------------
Sync Panel — event log overlay
--------------------------------------------------------------------------- */