From 51255b3583dacb39430bec546157d091c488e0d9 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 31 May 2026 11:44:03 -0400 Subject: [PATCH] refactor(ext/vault): extract vault-shell.ts + introduce VaultController ctx (Plan C Phase 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces vault-context.ts (VaultView/HashRoute/VaultState types, the VaultController contract, and the pure helpers escapeHtml/typeIcon/typeLabel/ getFilteredEntries). Extracts the shell concerns — render entry, lock screen, 3-column shell scaffolding, type picker panel, color-scheme apply, and the session_expired listener — into vault-shell.ts. vault.ts now assembles the ctx object and delegates shell rendering through it. No behavior change. Co-Authored-By: Claude Opus 4.8 --- .../vault/__tests__/sidebar-glyphs.test.ts | 2 +- extension/src/vault/vault-context.ts | 122 ++++++ extension/src/vault/vault-shell.ts | 255 ++++++++++++ extension/src/vault/vault.ts | 376 +++--------------- 4 files changed, 424 insertions(+), 331 deletions(-) create mode 100644 extension/src/vault/vault-context.ts create mode 100644 extension/src/vault/vault-shell.ts diff --git a/extension/src/vault/__tests__/sidebar-glyphs.test.ts b/extension/src/vault/__tests__/sidebar-glyphs.test.ts index 1800bcd..98415db 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.ts'), + path.resolve(__dirname, '../vault-shell.ts'), 'utf-8', ); diff --git a/extension/src/vault/vault-context.ts b/extension/src/vault/vault-context.ts new file mode 100644 index 0000000..8d38d0a --- /dev/null +++ b/extension/src/vault/vault-context.ts @@ -0,0 +1,122 @@ +// Shared contract for the vault-tab modules. vault.ts owns the state +// singleton and assembles the VaultController; each vault-* module receives +// it as `ctx`. This module sits at the bottom of the dependency graph — +// it imports only from shared/, never from vault.ts or its sibling modules. + +import type { Request, Response } from '../shared/messages'; +import type { + ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest, +} from '../shared/types'; +import { + GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP, + GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, +} from '../shared/glyphs'; + +export type VaultView = + | 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings' + | 'settings-vault' | 'field-history' | 'history' | 'backup' | 'import'; + +export interface HashRoute { + view: VaultView; + id?: string; + type?: string; +} + +export interface VaultState { + unlocked: boolean; + view: VaultView; + entries: Array<[ItemId, ManifestEntry]>; + selectedId: ItemId | null; + selectedItem: Item | null; + selectedIndex: number; + searchQuery: string; + activeGroup: string | null; + drawerOpen: boolean; + typePanelOpen: boolean; + vaultSettings: VaultSettings | null; + generatorDefaults: GeneratorRequest | null; + error: string | null; + loading: boolean; + newType: ItemType | null; + capturedTabId: number | null; + capturedUrl: string; + historyItemId: ItemId | null; +} + +// The controller passed to every vault-* module. vault.ts builds one instance +// and wires each hook to the function that currently lives in vault.ts (later +// Phase-4 tasks repoint individual hooks at the extracted module functions). +export interface VaultController { + readonly state: VaultState; + sendMessage(request: Request): Promise; + render(): void; + renderPane(): void; + renderListPane(): void; + renderSidebarCategories(): void; + renderDrawer(item: Item): void; + applyShellViewClass(): void; + setHash(view: VaultView, param?: string): void; + openDrawer(): void; + closeDrawer(): void; + selectItemForDrawer(id: string): Promise; + openTypePanel(): void; + closeTypePanel(): void; + wireSidebar(): void; + loadManifest(): Promise; +} + +// --- pure helpers (no state, no DOM dependencies beyond the args) --- + +export function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export function typeIcon(t: ItemType): string { + switch (t) { + case 'login': return GLYPH_TYPE_LOGIN; + case 'secure_note': return GLYPH_TYPE_SECURE_NOTE; + case 'identity': return GLYPH_TYPE_IDENTITY; + case 'card': return GLYPH_TYPE_CARD; + case 'key': return GLYPH_TYPE_KEY; + case 'document': return GLYPH_TYPE_DOCUMENT; + case 'totp': return GLYPH_TYPE_TOTP; + } +} + +export function typeLabel(t: ItemType): string { + const labels: Record = { + login: 'Login', + secure_note: 'Secure Note', + identity: 'Identity', + card: 'Card', + key: 'SSH / API Key', + document: 'Document', + totp: 'TOTP', + }; + return labels[t]; +} + +export function getFilteredEntries( + state: VaultState, +): Array<[ItemId, ManifestEntry]> { + let filtered = state.entries.filter( + ([, e]) => e.trashed_at === undefined || e.trashed_at === null, + ); + if (state.searchQuery) { + const q = state.searchQuery.toLowerCase(); + filtered = filtered.filter(([, e]) => { + if (e.title.toLowerCase().includes(q)) return true; + if (e.icon_hint?.toLowerCase().includes(q)) return true; + if (e.group?.toLowerCase().includes(q)) return true; + if (e.tags.some((t) => t.toLowerCase().includes(q))) return true; + return false; + }); + } + filtered.sort((a, b) => a[1].title.localeCompare(b[1].title)); + return filtered; +} diff --git a/extension/src/vault/vault-shell.ts b/extension/src/vault/vault-shell.ts new file mode 100644 index 0000000..fb5f34c --- /dev/null +++ b/extension/src/vault/vault-shell.ts @@ -0,0 +1,255 @@ +// Vault-tab shell: render entry point, lock screen, 3-column shell +// scaffolding, the right-side type-picker panel, color-scheme apply, and the +// session_expired listener. Each function receives the VaultController (`ctx`) +// and reaches sibling concerns through it; pure helpers come from +// vault-context. vault.ts owns the state singleton and assembles the ctx. + +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'; + +// --------------------------------------------------------------------------- +// Type picker (right side panel) +// --------------------------------------------------------------------------- + +const PICKER_TYPES: Array<{ type: ItemType; label: string }> = [ + { type: 'login', label: 'Login' }, + { type: 'secure_note', label: 'Secure Note' }, + { type: 'totp', label: 'TOTP' }, + { type: 'card', label: 'Card' }, + { type: 'identity', label: 'Identity' }, + { type: 'key', label: 'SSH / API Key' }, + { type: 'document', label: 'Document' }, +]; + +// --------------------------------------------------------------------------- +// Render entry point +// --------------------------------------------------------------------------- + +export function render(ctx: VaultController): void { + const app = document.getElementById('vault-app'); + if (!app) return; + + if (!ctx.state.unlocked) { + renderLockScreen(ctx, app); + } else { + renderShell(ctx, app); + } +} + +// --------------------------------------------------------------------------- +// Lock screen +// --------------------------------------------------------------------------- + +function renderErrorBlock(code: string | null | undefined): string { + if (!code) return ''; + const copy = lookupErrorCopy(code); + const ctaHtml = copy.cta + ? `` + : ''; + return ` +
+
${escapeHtml(copy.title)}
+
${escapeHtml(copy.body)}
+ ${ctaHtml} +
+ `; +} + +export function renderLockScreen(ctx: VaultController, app: HTMLElement): void { + app.innerHTML = ` +
+ + Relicario +
+ + + ${renderErrorBlock(ctx.state.error)} +
+
+ `; + + const input = document.getElementById('vault-passphrase') as HTMLInputElement; + const btn = document.getElementById('vault-unlock-btn')!; + + const doUnlock = async () => { + const passphrase = input.value; + if (!passphrase) return; + btn.textContent = 'unlocking...'; + btn.setAttribute('disabled', 'true'); + const resp = await ctx.sendMessage({ type: 'unlock', passphrase }); + if (resp.ok) { + ctx.state.unlocked = true; + ctx.state.error = null; + await ctx.loadManifest(); + render(ctx); + } else { + ctx.state.error = resp.error ?? 'unlock failed'; + render(ctx); + } + }; + + btn.addEventListener('click', doUnlock); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') doUnlock(); + }); + input.focus(); +} + +// --------------------------------------------------------------------------- +// Shell (3-column: sidebar + list pane + drawer) +// --------------------------------------------------------------------------- + +export function renderShell(ctx: VaultController, app: HTMLElement): void { + if (!app.querySelector('.vault-shell')) { + app.innerHTML = ` +
+
+
+ + Relicario +
+ + +
+ + + + + + +
+
+
+
+
+
+ +
+ `; + ctx.wireSidebar(); + wireTypePanel(ctx); + } + + applyShellViewClass(ctx); + ctx.renderSidebarCategories(); + if (ctx.state.view === 'list') { + ctx.renderListPane(); + if (ctx.state.drawerOpen && ctx.state.selectedItem) { + ctx.renderDrawer(ctx.state.selectedItem); + } + } else { + ctx.renderPane(); + } +} + +// Toggle which middle column is visible based on the current view. +// list view → list-pane (+ optional drawer); other views → vault-pane. +export function applyShellViewClass(ctx: VaultController): void { + const shell = document.querySelector('.vault-shell'); + if (!shell) return; + shell.classList.toggle('vault-shell--list', ctx.state.view === 'list'); + shell.classList.toggle('vault-shell--pane', ctx.state.view !== 'list'); +} + +// --------------------------------------------------------------------------- +// Right-side type picker panel +// --------------------------------------------------------------------------- + +export function wireTypePanel(ctx: VaultController): void { + document.getElementById('vault-type-scrim')?.addEventListener('click', () => closeTypePanel(ctx)); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && ctx.state.typePanelOpen) closeTypePanel(ctx); + }); +} + +export function openTypePanel(ctx: VaultController): void { + const panel = document.getElementById('vault-type-panel'); + const scrim = document.getElementById('vault-type-scrim'); + if (!panel || !scrim) return; + + panel.innerHTML = ` +
+
New item
+ +
+
Choose a type
+ + `; + + panel.classList.add('vault-type-panel--open'); + scrim.classList.add('vault-type-panel-scrim--visible'); + ctx.state.typePanelOpen = true; + + panel.querySelector('#vault-type-close')?.addEventListener('click', () => closeTypePanel(ctx)); + + panel.querySelectorAll('[data-type]').forEach((btn) => { + btn.addEventListener('click', () => { + const type = btn.dataset.type as ItemType; + closeTypePanel(ctx); + // Use the host's navigate hook so view + hash + visibility all update + // together. This was the bug: bare setHash + renderPane left the + // shell stuck in list view with #vault-pane hidden. + ctx.state.newType = type; + ctx.state.selectedId = null; + ctx.state.selectedItem = null; + ctx.state.drawerOpen = false; + ctx.state.view = 'add'; + ctx.setHash('add', type); + applyShellViewClass(ctx); + ctx.renderSidebarCategories(); + ctx.renderPane(); + }); + }); + + // Focus first item for keyboard users + (panel.querySelector('.vault-type-item') as HTMLElement | null)?.focus(); +} + +export function closeTypePanel(ctx: VaultController): void { + document.getElementById('vault-type-panel')?.classList.remove('vault-type-panel--open'); + document.getElementById('vault-type-scrim')?.classList.remove('vault-type-panel-scrim--visible'); + ctx.state.typePanelOpen = false; +} + +// --------------------------------------------------------------------------- +// Color scheme + session-expired wiring (bootstrap helpers) +// --------------------------------------------------------------------------- + +export async function applyVaultColorScheme(): Promise { + await applyColorScheme(); + + chrome.storage.onChanged.addListener((changes, area) => { + if (area === 'sync' && 'password_display_scheme' in changes) { + void applyColorScheme(); + } + }); +} + +export function wireSessionExpiredListener(ctx: VaultController): void { + chrome.runtime.onMessage.addListener((msg) => { + if (msg.type === 'session_expired') { + ctx.state.unlocked = false; + ctx.state.selectedId = null; + ctx.state.selectedItem = null; + ctx.state.entries = []; + ctx.state.error = null; + render(ctx); + } + }); +} diff --git a/extension/src/vault/vault.ts b/extension/src/vault/vault.ts index 421c918..26c86c0 100644 --- a/extension/src/vault/vault.ts +++ b/extension/src/vault/vault.ts @@ -9,13 +9,8 @@ import type { ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest, } from '../shared/types'; import { registerHost } from '../shared/state'; -import { lookupErrorCopy, type ErrorCta } from '../shared/error-copy'; +import { type ErrorCta } from '../shared/error-copy'; import { relativeTime } from '../shared/relative-time'; -import { - GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_HISTORY, - GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP, - GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, -} from '../shared/glyphs'; import { renderItemDetail } from '../popup/components/item-detail'; import { renderItemForm } from '../popup/components/item-form'; import { renderTrash, teardown as teardownTrash } from '../popup/components/trash'; @@ -26,21 +21,15 @@ import { renderFieldHistory, teardown as teardownFieldHistory } from '../popup/c import { renderItemHistoryIndex, teardown as teardownHistoryIndex } from '../popup/components/item-history-index'; import { renderBackupPanel, teardown as teardownBackup } from './components/backup-panel'; import { renderImportPanel, teardown as teardownImport } from './components/import-panel'; -import { applyColorScheme } from '../shared/color-scheme'; - -// --------------------------------------------------------------------------- -// Type picker (right side panel) -// --------------------------------------------------------------------------- - -const PICKER_TYPES: Array<{ type: ItemType; label: string }> = [ - { type: 'login', label: 'Login' }, - { type: 'secure_note', label: 'Secure Note' }, - { type: 'totp', label: 'TOTP' }, - { type: 'card', label: 'Card' }, - { type: 'identity', label: 'Identity' }, - { type: 'key', label: 'SSH / API Key' }, - { type: 'document', label: 'Document' }, -]; +import { + type VaultController, type VaultState, type VaultView, type HashRoute, + escapeHtml, typeIcon, typeLabel, getFilteredEntries, +} from './vault-context'; +import { + render, applyShellViewClass, + openTypePanel, closeTypePanel, applyVaultColorScheme, + wireSessionExpiredListener, +} from './vault-shell'; // --------------------------------------------------------------------------- // Helpers @@ -68,74 +57,17 @@ function sendMessage(request: Request): Promise { state.selectedItem = null; state.entries = []; state.error = 'Session expired — please unlock again.'; - render(); + render(ctx); } resolve(response); }); }); } -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -function renderErrorBlock(code: string | null | undefined): string { - if (!code) return ''; - const copy = lookupErrorCopy(code); - const ctaHtml = copy.cta - ? `` - : ''; - return ` -
-
${escapeHtml(copy.title)}
-
${escapeHtml(copy.body)}
- ${ctaHtml} -
- `; -} - -function typeIcon(t: ItemType): string { - switch (t) { - case 'login': return GLYPH_TYPE_LOGIN; - case 'secure_note': return GLYPH_TYPE_SECURE_NOTE; - case 'identity': return GLYPH_TYPE_IDENTITY; - case 'card': return GLYPH_TYPE_CARD; - case 'key': return GLYPH_TYPE_KEY; - case 'document': return GLYPH_TYPE_DOCUMENT; - case 'totp': return GLYPH_TYPE_TOTP; - } -} - -function typeLabel(t: ItemType): string { - const labels: Record = { - login: 'Login', - secure_note: 'Secure Note', - identity: 'Identity', - card: 'Card', - key: 'SSH / API Key', - document: 'Document', - totp: 'TOTP', - }; - return labels[t]; -} - // --------------------------------------------------------------------------- // Hash routing // --------------------------------------------------------------------------- -type VaultView = 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings' | 'settings-vault' | 'field-history' | 'history' | 'backup' | 'import'; - -interface HashRoute { - view: VaultView; - id?: string; - type?: string; -} - function parseHash(): HashRoute { let raw = window.location.hash.replace(/^#\/?/, ''); if (!raw) return { view: 'list' }; @@ -181,27 +113,6 @@ function setHash(view: VaultView, param?: string): void { // State // --------------------------------------------------------------------------- -interface VaultState { - unlocked: boolean; - view: VaultView; - entries: Array<[ItemId, ManifestEntry]>; - selectedId: ItemId | null; - selectedItem: Item | null; - selectedIndex: number; - searchQuery: string; - activeGroup: string | null; - drawerOpen: boolean; - typePanelOpen: boolean; - vaultSettings: VaultSettings | null; - generatorDefaults: GeneratorRequest | null; - error: string | null; - loading: boolean; - newType: ItemType | null; - capturedTabId: number | null; - capturedUrl: string; - historyItemId: ItemId | null; -} - const state: VaultState = { unlocked: false, view: 'list', @@ -223,6 +134,29 @@ const state: VaultState = { historyItemId: null, }; +// --------------------------------------------------------------------------- +// Controller — carries state + cross-module re-render hooks +// --------------------------------------------------------------------------- + +const ctx: VaultController = { + state, + sendMessage, + render: () => render(ctx), + renderPane: () => renderPane(), + renderListPane: () => renderListPane(), + renderSidebarCategories: () => renderSidebarCategories(), + renderDrawer: (item) => renderDrawer(item), + applyShellViewClass: () => applyShellViewClass(ctx), + setHash, + openDrawer: () => openDrawer(), + closeDrawer: () => closeDrawer(), + selectItemForDrawer: (id) => selectItemForDrawer(id), + openTypePanel: () => openTypePanel(ctx), + closeTypePanel: () => closeTypePanel(ctx), + wireSidebar: () => wireSidebar(), + loadManifest: () => loadManifest(), +}; + // --------------------------------------------------------------------------- // Register as shared state host // --------------------------------------------------------------------------- @@ -236,7 +170,7 @@ registerHost({ navigate: (view, extras) => { Object.assign(state, { view, error: null, loading: false, ...extras }); setHash(view as VaultView); - applyShellViewClass(); + applyShellViewClass(ctx); renderSidebarCategories(); if (state.view === 'list') renderListPane(); renderPane(); @@ -248,190 +182,6 @@ registerHost({ openVaultTab: () => {}, }); -// --------------------------------------------------------------------------- -// Render entry point -// --------------------------------------------------------------------------- - -function render(): void { - const app = document.getElementById('vault-app'); - if (!app) return; - - if (!state.unlocked) { - renderLockScreen(app); - } else { - renderShell(app); - } -} - -// --------------------------------------------------------------------------- -// Lock screen -// --------------------------------------------------------------------------- - -function renderLockScreen(app: HTMLElement): void { - app.innerHTML = ` -
- - Relicario -
- - - ${renderErrorBlock(state.error)} -
-
- `; - - const input = document.getElementById('vault-passphrase') as HTMLInputElement; - const btn = document.getElementById('vault-unlock-btn')!; - - const doUnlock = async () => { - const passphrase = input.value; - if (!passphrase) return; - btn.textContent = 'unlocking...'; - btn.setAttribute('disabled', 'true'); - const resp = await sendMessage({ type: 'unlock', passphrase }); - if (resp.ok) { - state.unlocked = true; - state.error = null; - await loadManifest(); - render(); - } else { - state.error = resp.error ?? 'unlock failed'; - render(); - } - }; - - btn.addEventListener('click', doUnlock); - input.addEventListener('keydown', (e) => { - if (e.key === 'Enter') doUnlock(); - }); - input.focus(); -} - -// --------------------------------------------------------------------------- -// Shell (3-column: sidebar + list pane + drawer) -// --------------------------------------------------------------------------- - -function renderShell(app: HTMLElement): void { - if (!app.querySelector('.vault-shell')) { - app.innerHTML = ` -
-
-
- - Relicario -
- - -
- - - - - - -
-
-
-
-
-
- -
- `; - wireSidebar(); - wireTypePanel(); - } - - applyShellViewClass(); - renderSidebarCategories(); - if (state.view === 'list') { - renderListPane(); - if (state.drawerOpen && state.selectedItem) { - renderDrawer(state.selectedItem); - } - } else { - renderPane(); - } -} - -// Toggle which middle column is visible based on the current view. -// list view → list-pane (+ optional drawer); other views → vault-pane. -function applyShellViewClass(): void { - const shell = document.querySelector('.vault-shell'); - if (!shell) return; - shell.classList.toggle('vault-shell--list', state.view === 'list'); - shell.classList.toggle('vault-shell--pane', state.view !== 'list'); -} - -// --------------------------------------------------------------------------- -// Right-side type picker panel -// --------------------------------------------------------------------------- - -function wireTypePanel(): void { - document.getElementById('vault-type-scrim')?.addEventListener('click', closeTypePanel); - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && state.typePanelOpen) closeTypePanel(); - }); -} - -function openTypePanel(): void { - const panel = document.getElementById('vault-type-panel'); - const scrim = document.getElementById('vault-type-scrim'); - if (!panel || !scrim) return; - - panel.innerHTML = ` -
-
New item
- -
-
Choose a type
- - `; - - panel.classList.add('vault-type-panel--open'); - scrim.classList.add('vault-type-panel-scrim--visible'); - state.typePanelOpen = true; - - panel.querySelector('#vault-type-close')?.addEventListener('click', closeTypePanel); - - panel.querySelectorAll('[data-type]').forEach((btn) => { - btn.addEventListener('click', () => { - const type = btn.dataset.type as ItemType; - closeTypePanel(); - // Use the host's navigate hook so view + hash + visibility all update - // together. This was the bug: bare setHash + renderPane left the - // shell stuck in list view with #vault-pane hidden. - state.newType = type; - state.selectedId = null; - state.selectedItem = null; - state.drawerOpen = false; - state.view = 'add'; - setHash('add', type); - applyShellViewClass(); - renderSidebarCategories(); - renderPane(); - }); - }); - - // Focus first item for keyboard users - (panel.querySelector('.vault-type-item') as HTMLElement | null)?.focus(); -} - -function closeTypePanel(): void { - document.getElementById('vault-type-panel')?.classList.remove('vault-type-panel--open'); - document.getElementById('vault-type-scrim')?.classList.remove('vault-type-panel-scrim--visible'); - state.typePanelOpen = false; -} - // --------------------------------------------------------------------------- // Drawer (implemented in Task 10) // --------------------------------------------------------------------------- @@ -581,7 +331,7 @@ function wireSidebar(): void { state.selectedId = null; state.selectedItem = null; state.entries = []; - render(); + render(ctx); return; } if (nav === 'add') { @@ -590,7 +340,7 @@ function wireSidebar(): void { state.newType = null; state.drawerOpen = false; closeDrawer(); - openTypePanel(); + openTypePanel(ctx); return; } if (nav === 'trash' || nav === 'devices' || nav === 'settings' || nav === 'history') { @@ -600,7 +350,7 @@ function wireSidebar(): void { state.drawerOpen = false; state.view = nav; setHash(nav); - applyShellViewClass(); + applyShellViewClass(ctx); renderPane(); return; } @@ -633,29 +383,11 @@ function isEditableTarget(target: EventTarget | null): boolean { // Sidebar category nav // --------------------------------------------------------------------------- -function getFilteredEntries(): Array<[ItemId, ManifestEntry]> { - let filtered = state.entries.filter( - ([, e]) => e.trashed_at === undefined || e.trashed_at === null, - ); - if (state.searchQuery) { - const q = state.searchQuery.toLowerCase(); - filtered = filtered.filter(([, e]) => { - if (e.title.toLowerCase().includes(q)) return true; - if (e.icon_hint?.toLowerCase().includes(q)) return true; - if (e.group?.toLowerCase().includes(q)) return true; - if (e.tags.some((t) => t.toLowerCase().includes(q))) return true; - return false; - }); - } - filtered.sort((a, b) => a[1].title.localeCompare(b[1].title)); - return filtered; -} - function renderSidebarCategories(): void { const container = document.getElementById('vault-categories'); if (!container) return; - const filtered = getFilteredEntries(); + const filtered = getFilteredEntries(state); const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp']; const allCount = filtered.length; @@ -693,7 +425,7 @@ function renderSidebarCategories(): void { state.selectedItem = null; state.view = 'list'; setHash('list'); - applyShellViewClass(); + applyShellViewClass(ctx); renderSidebarCategories(); renderListPane(); closeDrawer(); @@ -710,7 +442,7 @@ function renderListPane(): void { if (!pane) return; const group = state.activeGroup as ItemType | null; - let items = getFilteredEntries(); + let items = getFilteredEntries(state); if (group) items = items.filter(([, e]) => e.type === group); if (items.length === 0) { @@ -835,7 +567,7 @@ function renderPane(): void { const route = parseHash(); // Keep state.view in sync with hash for components that read it state.view = route.view; - applyShellViewClass(); + applyShellViewClass(ctx); pane.className = 'vault-pane'; @@ -930,13 +662,7 @@ async function loadManifest(): Promise { // --------------------------------------------------------------------------- document.addEventListener('DOMContentLoaded', async () => { - await applyColorScheme(); - - chrome.storage.onChanged.addListener((changes, area) => { - if (area === 'sync' && 'password_display_scheme' in changes) { - void applyColorScheme(); - } - }); + await applyVaultColorScheme(); // Delegated handler for .error-cta buttons — set up once on the stable root. const app = document.getElementById('vault-app')!; @@ -970,19 +696,9 @@ document.addEventListener('DOMContentLoaded', async () => { } } - render(); + render(ctx); - // Session expired listener - chrome.runtime.onMessage.addListener((msg) => { - if (msg.type === 'session_expired') { - state.unlocked = false; - state.selectedId = null; - state.selectedItem = null; - state.entries = []; - state.error = null; - render(); - } - }); + wireSessionExpiredListener(ctx); // Hash change listener window.addEventListener('hashchange', () => { @@ -990,7 +706,7 @@ document.addEventListener('DOMContentLoaded', async () => { const route = parseHash(); state.view = route.view; - applyShellViewClass(); + applyShellViewClass(ctx); // If navigating to a detail/edit view for an item we already have loaded if ((route.view === 'detail' || route.view === 'edit') && route.id) {