/// Vault tab entry point — full "desktop-like" sidebar + pane layout. /// /// Registers as the shared state host so popup components (item-detail, /// item-form, trash, devices, settings, etc.) render natively in the /// vault tab's pane area. import type { Request, Response } from '../shared/messages'; import { registerHost, sendMessage } from '../shared/state'; import { type ErrorCta } from '../shared/error-copy'; import { type VaultController, type VaultState, type VaultView, escapeHtml, } from './vault-context'; import { render, applyShellViewClass, openTypePanel, closeTypePanel, applyVaultColorScheme, wireSessionExpiredListener, } from './vault-shell'; import { wireSidebar, renderSidebarCategories } from './vault-sidebar'; import { renderListPane } from './vault-list'; import { openDrawer, closeDrawer, renderDrawer, selectItemForDrawer, } from './vault-drawer'; import { parseHash, setHash, renderPane, loadManifest, selectItem } from './vault-router'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- // Plain transport to the service worker, registered as the host's sendMessage. // The shared sendMessage() wrapper (shared/state.ts) layers the session-lost // → lock-screen intercept on top of this for every UI RPC. function postToServiceWorker(request: Request): Promise { return new Promise((resolve) => { chrome.runtime.sendMessage(request, (response: Response) => resolve(response)); }); } // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- const state: VaultState = { unlocked: false, view: 'list', entries: [], selectedId: null, selectedItem: null, selectedIndex: 0, searchQuery: '', activeGroup: null, drawerOpen: false, typePanelOpen: false, vaultSettings: null, generatorDefaults: null, error: null, loading: false, newType: null, capturedTabId: null, capturedUrl: '', historyItemId: null, }; // --------------------------------------------------------------------------- // Controller — carries state + cross-module re-render hooks // --------------------------------------------------------------------------- const ctx: VaultController = { state, sendMessage, render: () => render(ctx), renderPane: () => renderPane(ctx), renderListPane: () => renderListPane(ctx), renderSidebarCategories: () => renderSidebarCategories(ctx), renderDrawer: (item) => renderDrawer(ctx, item), applyShellViewClass: () => applyShellViewClass(ctx), setHash, openDrawer: () => openDrawer(), closeDrawer: () => closeDrawer(ctx), selectItemForDrawer: (id) => selectItemForDrawer(ctx, id), openTypePanel: () => openTypePanel(ctx), closeTypePanel: () => closeTypePanel(ctx), wireSidebar: () => wireSidebar(ctx), loadManifest: () => loadManifest(ctx), }; // --------------------------------------------------------------------------- // Register as shared state host // --------------------------------------------------------------------------- registerHost({ getState: () => state, setState: (partial) => { Object.assign(state, partial); renderPane(ctx); }, navigate: (view, extras) => { if (view === 'locked') { // Session lost (SW evicted mid-session). The vault shows its lock // screen off state.unlocked, so flip it and drop the in-memory data. state.unlocked = false; state.selectedId = null; state.selectedItem = null; state.entries = []; state.error = (extras?.error as string | undefined) ?? null; render(ctx); return; } Object.assign(state, { view, error: null, loading: false, ...extras }); setHash(view as VaultView); applyShellViewClass(ctx); renderSidebarCategories(ctx); if (state.view === 'list') renderListPane(ctx); renderPane(ctx); }, sendMessage: postToServiceWorker, escapeHtml, popOutToTab: () => {}, isInTab: () => true, openVaultTab: () => {}, }); // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- document.addEventListener('DOMContentLoaded', async () => { await applyVaultColorScheme(); // Delegated handler for .error-cta buttons — set up once on the stable root. const app = document.getElementById('vault-app')!; app.addEventListener('click', (e) => { const btn = (e.target as HTMLElement).closest('.error-cta'); if (!btn) return; const cta = btn.dataset.cta as ErrorCta['action']; switch (cta) { case 'unlock': { document.getElementById('vault-passphrase')?.focus(); break; } case 'open_setup': { void chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') }); break; } case 'reload_extension': { chrome.runtime.reload(); break; } } }); // Check if already unlocked const resp = await sendMessage({ type: 'is_unlocked' }); if (resp.ok) { const data = resp.data as { unlocked: boolean }; if (data.unlocked) { state.unlocked = true; await loadManifest(ctx); } } render(ctx); wireSessionExpiredListener(ctx); // Hash change listener window.addEventListener('hashchange', () => { if (!state.unlocked) return; const route = parseHash(); state.view = route.view; 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) { if (state.selectedId === route.id && state.selectedItem) { renderPane(ctx); renderSidebarCategories(ctx); if (state.view === 'list') renderListPane(ctx); return; } // Need to fetch the item selectItem(ctx, route.id); return; } // For non-item views, just re-render the pane state.selectedId = null; state.selectedItem = null; renderSidebarCategories(ctx); if (state.view === 'list') renderListPane(ctx); renderPane(ctx); }); });