/// Popup entry point — state machine with view routing. /// /// Views: setup | locked | list | detail | add | edit /// Navigation works by updating `currentState` and calling `render()`. import type { Request, Response } from '../shared/messages'; import type { ManifestEntry, Entry } from '../shared/types'; import { renderUnlock } from './components/unlock'; import { renderEntryList } from './components/entry-list'; import { renderEntryDetail } from './components/entry-detail'; import { renderEntryForm } from './components/entry-form'; import { renderSetupWizard } from './components/setup-wizard'; // --- Escape HTML to prevent XSS --- export function escapeHtml(str: string): string { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // --- State --- export type View = 'setup' | 'locked' | 'list' | 'detail' | 'add' | 'edit'; export interface PopupState { view: View; entries: Array<[string, ManifestEntry]>; selectedId: string | null; selectedEntry: Entry | null; selectedIndex: number; searchQuery: string; activeGroup: string | null; error: string | null; loading: boolean; } let currentState: PopupState = { view: 'locked', entries: [], selectedId: null, selectedEntry: null, selectedIndex: 0, searchQuery: '', activeGroup: null, error: null, loading: false, }; export function getState(): PopupState { return currentState; } export function setState(partial: Partial): void { currentState = { ...currentState, ...partial }; render(); } // --- Messaging --- export function sendMessage(request: Request): Promise { return new Promise((resolve) => { chrome.runtime.sendMessage(request, (response: Response) => { resolve(response); }); }); } // --- Navigation --- export function navigate(view: View, extras?: Partial): void { setState({ view, error: null, loading: false, ...extras }); } // --- Render --- function render(): void { const app = document.getElementById('app'); if (!app) return; switch (currentState.view) { case 'setup': renderSetupWizard(app); break; case 'locked': renderUnlock(app); break; case 'list': renderEntryList(app); break; case 'detail': renderEntryDetail(app); break; case 'add': renderEntryForm(app, 'add'); break; case 'edit': renderEntryForm(app, 'edit'); break; } } // --- Init --- async function init(): Promise { // Check if extension is configured. const setupResp = await sendMessage({ type: 'get_setup_state' }); if (setupResp.ok) { const data = setupResp.data as { isConfigured: boolean }; if (!data.isConfigured) { navigate('setup'); return; } } // Check if vault is unlocked. const unlockResp = await sendMessage({ type: 'is_unlocked' }); if (unlockResp.ok) { const data = unlockResp.data as { unlocked: boolean }; if (data.unlocked) { // Load entries and go to list. const listResp = await sendMessage({ type: 'list_entries' }); if (listResp.ok) { const listData = listResp.data as { entries: Array<[string, ManifestEntry]> }; navigate('list', { entries: listData.entries }); return; } } } navigate('locked'); } document.addEventListener('DOMContentLoaded', init);