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>
123 lines
3.8 KiB
TypeScript
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, '&')
|
|
.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<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;
|
|
}
|