// Vault-tab sidebar column: its static markup, the category nav rendering, // nav-button wiring, and the (now debounced) search input. Each function // receives the VaultController (`ctx`) and reaches sibling concerns through it; // pure helpers come from vault-context. Imports only from shared/ and // vault-context, plus the leaf renderer vault-status — never from vault-shell // or vault.ts. import type { ItemType } from '../shared/types'; import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_HISTORY, GLYPH_LOCK, GLYPH_REFRESH, } from '../shared/glyphs'; import { renderStatusIndicator, type VaultStatus } from './vault-status'; import { type VaultController, typeIcon, typeLabel, getFilteredEntries, } from './vault-context'; const SEARCH_DEBOUNCE_MS = 80; // --------------------------------------------------------------------------- // Sidebar markup // --------------------------------------------------------------------------- export function renderSidebarShell(): string { return `
Relicario
`; } // --------------------------------------------------------------------------- // Sidebar wiring // --------------------------------------------------------------------------- export function wireSidebar(ctx: VaultController): void { // Search (debounced — trailing edge) const searchInput = document.getElementById('vault-search') as HTMLInputElement | null; let searchTimer: number | undefined; searchInput?.addEventListener('input', () => { if (searchTimer !== undefined) clearTimeout(searchTimer); searchTimer = window.setTimeout(() => { ctx.state.searchQuery = searchInput.value; renderSidebarCategories(ctx); ctx.renderListPane(); }, SEARCH_DEBOUNCE_MS); }); // Nav buttons document.querySelectorAll('.vault-sidebar__nav-item').forEach((btn) => { btn.addEventListener('click', async () => { const nav = (btn as HTMLElement).dataset.nav; if (nav === 'lock') { await ctx.sendMessage({ type: 'lock' }); ctx.state.unlocked = false; ctx.state.selectedId = null; ctx.state.selectedItem = null; ctx.state.entries = []; ctx.render(); return; } if (nav === 'add') { ctx.state.selectedId = null; ctx.state.selectedItem = null; ctx.state.newType = null; ctx.state.drawerOpen = false; ctx.closeDrawer(); ctx.openTypePanel(); return; } if (nav === 'trash' || nav === 'devices' || nav === 'settings' || nav === 'history') { ctx.state.selectedId = null; ctx.state.selectedItem = null; ctx.state.newType = null; ctx.state.drawerOpen = false; ctx.state.view = nav; ctx.setHash(nav); ctx.applyShellViewClass(); ctx.renderPane(); return; } }); }); // Global "/" shortcut to focus search; Esc to close drawer document.addEventListener('keydown', (e) => { if (e.key === '/' && !isEditableTarget(e.target)) { e.preventDefault(); searchInput?.focus(); return; } if (e.key === 'Escape' && ctx.state.drawerOpen) { ctx.closeDrawer(); ctx.renderListPane(); } }); // Vault status indicator — refresh on mount + on the manual button only. // No timer polling: get_vault_status returns cached state and sync is // user-initiated (spec 2026-05-04, Phase 6). const refreshStatus = async (): Promise => { const resp = await ctx.sendMessage({ type: 'get_vault_status' }); if (!resp.ok) return; const slot = document.getElementById('vault-status-slot'); if (slot) renderStatusIndicator(slot, resp.data as VaultStatus); }; void refreshStatus(); document.getElementById('status-refresh-btn')?.addEventListener('click', () => { void refreshStatus(); }); } function isEditableTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; const tag = target.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; if (target.isContentEditable) return true; return false; } // --------------------------------------------------------------------------- // Sidebar category nav // --------------------------------------------------------------------------- export function renderSidebarCategories(ctx: VaultController): void { const container = document.getElementById('vault-categories'); if (!container) return; const filtered = getFilteredEntries(ctx.state); const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp']; const allCount = filtered.length; const isAllActive = !ctx.state.activeGroup && ctx.state.view === 'list'; let html = ` `; for (const t of typeOrder) { const count = filtered.filter(([, e]) => e.type === t).length; // Always show Login (staple type); hide other types when empty. if (count === 0 && t !== 'login') continue; const isActive = ctx.state.activeGroup === t; html += ` `; } container.innerHTML = html; container.querySelectorAll('.vault-category-row').forEach((btn) => { btn.addEventListener('click', () => { ctx.state.activeGroup = btn.dataset.group || null; ctx.state.drawerOpen = false; ctx.state.selectedId = null; ctx.state.selectedItem = null; ctx.state.view = 'list'; ctx.setHash('list'); ctx.applyShellViewClass(); renderSidebarCategories(ctx); ctx.renderListPane(); ctx.closeDrawer(); }); }); }