diff --git a/extension/src/vault/__tests__/sidebar-glyphs.test.ts b/extension/src/vault/__tests__/sidebar-glyphs.test.ts
index 98415db..586a2b1 100644
--- a/extension/src/vault/__tests__/sidebar-glyphs.test.ts
+++ b/extension/src/vault/__tests__/sidebar-glyphs.test.ts
@@ -4,7 +4,7 @@ import * as path from 'path';
describe('vault sidebar glyphs', () => {
const vaultSrc = fs.readFileSync(
- path.resolve(__dirname, '../vault-shell.ts'),
+ path.resolve(__dirname, '../vault-sidebar.ts'),
'utf-8',
);
diff --git a/extension/src/vault/vault-shell.ts b/extension/src/vault/vault-shell.ts
index fb5f34c..a7e1b7a 100644
--- a/extension/src/vault/vault-shell.ts
+++ b/extension/src/vault/vault-shell.ts
@@ -6,13 +6,11 @@
import type { ItemType } from '../shared/types';
import { lookupErrorCopy } from '../shared/error-copy';
-import {
- GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_HISTORY,
-} from '../shared/glyphs';
import { applyColorScheme } from '../shared/color-scheme';
import {
type VaultController, escapeHtml, typeIcon,
} from './vault-context';
+import { renderSidebarShell } from './vault-sidebar';
// ---------------------------------------------------------------------------
// Type picker (right side panel)
@@ -110,24 +108,7 @@ export function renderShell(ctx: VaultController, app: HTMLElement): void {
if (!app.querySelector('.vault-shell')) {
app.innerHTML = `
-
+ ${renderSidebarShell()}
diff --git a/extension/src/vault/vault-sidebar.ts b/extension/src/vault/vault-sidebar.ts
new file mode 100644
index 0000000..b7db7b7
--- /dev/null
+++ b/extension/src/vault/vault-sidebar.ts
@@ -0,0 +1,174 @@
+// 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 — never from vault-shell or vault.ts.
+
+import type { ItemType } from '../shared/types';
+import {
+ GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_HISTORY, GLYPH_LOCK,
+} from '../shared/glyphs';
+import {
+ type VaultController, typeIcon, typeLabel, getFilteredEntries,
+} from './vault-context';
+
+const SEARCH_DEBOUNCE_MS = 80;
+
+// ---------------------------------------------------------------------------
+// Sidebar markup
+// ---------------------------------------------------------------------------
+
+export function renderSidebarShell(): string {
+ return `
+ `;
+}
+
+// ---------------------------------------------------------------------------
+// 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();
+ }
+ });
+}
+
+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 += `
+
+ ${typeIcon(t)}
+
+
+
+ `;
+ }
+
+ 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();
+ });
+ });
+}
diff --git a/extension/src/vault/vault.ts b/extension/src/vault/vault.ts
index 26c86c0..1521dec 100644
--- a/extension/src/vault/vault.ts
+++ b/extension/src/vault/vault.ts
@@ -23,13 +23,14 @@ import { renderBackupPanel, teardown as teardownBackup } from './components/back
import { renderImportPanel, teardown as teardownImport } from './components/import-panel';
import {
type VaultController, type VaultState, type VaultView, type HashRoute,
- escapeHtml, typeIcon, typeLabel, getFilteredEntries,
+ escapeHtml, typeIcon, getFilteredEntries,
} from './vault-context';
import {
render, applyShellViewClass,
openTypePanel, closeTypePanel, applyVaultColorScheme,
wireSessionExpiredListener,
} from './vault-shell';
+import { wireSidebar, renderSidebarCategories } from './vault-sidebar';
// ---------------------------------------------------------------------------
// Helpers
@@ -144,7 +145,7 @@ const ctx: VaultController = {
render: () => render(ctx),
renderPane: () => renderPane(),
renderListPane: () => renderListPane(),
- renderSidebarCategories: () => renderSidebarCategories(),
+ renderSidebarCategories: () => renderSidebarCategories(ctx),
renderDrawer: (item) => renderDrawer(item),
applyShellViewClass: () => applyShellViewClass(ctx),
setHash,
@@ -153,7 +154,7 @@ const ctx: VaultController = {
selectItemForDrawer: (id) => selectItemForDrawer(id),
openTypePanel: () => openTypePanel(ctx),
closeTypePanel: () => closeTypePanel(ctx),
- wireSidebar: () => wireSidebar(),
+ wireSidebar: () => wireSidebar(ctx),
loadManifest: () => loadManifest(),
};
@@ -171,7 +172,7 @@ registerHost({
Object.assign(state, { view, error: null, loading: false, ...extras });
setHash(view as VaultView);
applyShellViewClass(ctx);
- renderSidebarCategories();
+ renderSidebarCategories(ctx);
if (state.view === 'list') renderListPane();
renderPane();
},
@@ -302,137 +303,12 @@ async function selectItemForDrawer(id: string): Promise {
state.selectedId = id;
state.selectedItem = data.item;
state.drawerOpen = true;
- renderSidebarCategories();
+ renderSidebarCategories(ctx);
renderListPane();
renderDrawer(data.item);
openDrawer();
}
-// ---------------------------------------------------------------------------
-// Sidebar wiring
-// ---------------------------------------------------------------------------
-
-function wireSidebar(): void {
- // Search
- const searchInput = document.getElementById('vault-search') as HTMLInputElement | null;
- searchInput?.addEventListener('input', () => {
- state.searchQuery = searchInput.value;
- renderSidebarCategories();
- renderListPane();
- });
-
- // 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 sendMessage({ type: 'lock' });
- state.unlocked = false;
- state.selectedId = null;
- state.selectedItem = null;
- state.entries = [];
- render(ctx);
- return;
- }
- if (nav === 'add') {
- state.selectedId = null;
- state.selectedItem = null;
- state.newType = null;
- state.drawerOpen = false;
- closeDrawer();
- openTypePanel(ctx);
- return;
- }
- if (nav === 'trash' || nav === 'devices' || nav === 'settings' || nav === 'history') {
- state.selectedId = null;
- state.selectedItem = null;
- state.newType = null;
- state.drawerOpen = false;
- state.view = nav;
- setHash(nav);
- 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' && state.drawerOpen) {
- closeDrawer();
- renderListPane();
- }
- });
-}
-
-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
-// ---------------------------------------------------------------------------
-
-function renderSidebarCategories(): void {
- const container = document.getElementById('vault-categories');
- if (!container) return;
-
- const filtered = getFilteredEntries(state);
- const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp'];
-
- const allCount = filtered.length;
- const isAllActive = !state.activeGroup && 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 = state.activeGroup === t;
- html += `
-
- ${typeIcon(t)}
-
-
-
- `;
- }
-
- container.innerHTML = html;
-
- container.querySelectorAll('.vault-category-row').forEach((btn) => {
- btn.addEventListener('click', () => {
- state.activeGroup = btn.dataset.group || null;
- state.drawerOpen = false;
- state.selectedId = null;
- state.selectedItem = null;
- state.view = 'list';
- setHash('list');
- applyShellViewClass(ctx);
- renderSidebarCategories();
- renderListPane();
- closeDrawer();
- });
- });
-}
-
// ---------------------------------------------------------------------------
// List pane
// ---------------------------------------------------------------------------
@@ -712,7 +588,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if ((route.view === 'detail' || route.view === 'edit') && route.id) {
if (state.selectedId === route.id && state.selectedItem) {
renderPane();
- renderSidebarCategories();
+ renderSidebarCategories(ctx);
if (state.view === 'list') renderListPane();
return;
}
@@ -724,7 +600,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// For non-item views, just re-render the pane
state.selectedId = null;
state.selectedItem = null;
- renderSidebarCategories();
+ renderSidebarCategories(ctx);
if (state.view === 'list') renderListPane();
renderPane();
});
@@ -743,7 +619,7 @@ async function selectItem(id: ItemId): Promise {
state.selectedItem = data.item;
state.loading = false;
setHash('detail', id);
- renderSidebarCategories();
+ renderSidebarCategories(ctx);
renderListPane();
renderPane();
} else {