Files
relicario/extension/src/vault/vault-context.ts
adlee-was-taken 51255b3583 refactor(ext/vault): extract vault-shell.ts + introduce VaultController ctx (Plan C Phase 4)
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 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00

123 lines
3.8 KiB
TypeScript

// 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<Response>;
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<void>;
openTypePanel(): void;
closeTypePanel(): void;
wireSidebar(): void;
loadManifest(): Promise<void>;
}
// --- pure helpers (no state, no DOM dependencies beyond the args) ---
export function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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<ItemType, string> = {
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;
}