// 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; }