// Vault-tab shell: render entry point, lock screen, 3-column shell // scaffolding, the right-side type-picker panel, color-scheme apply, and the // session_expired listener. Each function receives the VaultController (`ctx`) // and reaches sibling concerns through it; pure helpers come from // vault-context. vault.ts owns the state singleton and assembles the ctx. import type { ItemType } from '../shared/types'; import { lookupErrorCopy } from '../shared/error-copy'; import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_HISTORY, } from '../shared/glyphs'; import { applyColorScheme } from '../shared/color-scheme'; import { type VaultController, escapeHtml, typeIcon, } from './vault-context'; // --------------------------------------------------------------------------- // Type picker (right side panel) // --------------------------------------------------------------------------- const PICKER_TYPES: Array<{ type: ItemType; label: string }> = [ { type: 'login', label: 'Login' }, { type: 'secure_note', label: 'Secure Note' }, { type: 'totp', label: 'TOTP' }, { type: 'card', label: 'Card' }, { type: 'identity', label: 'Identity' }, { type: 'key', label: 'SSH / API Key' }, { type: 'document', label: 'Document' }, ]; // --------------------------------------------------------------------------- // Render entry point // --------------------------------------------------------------------------- export function render(ctx: VaultController): void { const app = document.getElementById('vault-app'); if (!app) return; if (!ctx.state.unlocked) { renderLockScreen(ctx, app); } else { renderShell(ctx, app); } } // --------------------------------------------------------------------------- // Lock screen // --------------------------------------------------------------------------- function renderErrorBlock(code: string | null | undefined): string { if (!code) return ''; const copy = lookupErrorCopy(code); const ctaHtml = copy.cta ? `` : ''; return `
${escapeHtml(copy.title)}
${escapeHtml(copy.body)}
${ctaHtml}
`; } export function renderLockScreen(ctx: VaultController, app: HTMLElement): void { app.innerHTML = `
Relicario
${renderErrorBlock(ctx.state.error)}
`; const input = document.getElementById('vault-passphrase') as HTMLInputElement; const btn = document.getElementById('vault-unlock-btn')!; const doUnlock = async () => { const passphrase = input.value; if (!passphrase) return; btn.textContent = 'unlocking...'; btn.setAttribute('disabled', 'true'); const resp = await ctx.sendMessage({ type: 'unlock', passphrase }); if (resp.ok) { ctx.state.unlocked = true; ctx.state.error = null; await ctx.loadManifest(); render(ctx); } else { ctx.state.error = resp.error ?? 'unlock failed'; render(ctx); } }; btn.addEventListener('click', doUnlock); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doUnlock(); }); input.focus(); } // --------------------------------------------------------------------------- // Shell (3-column: sidebar + list pane + drawer) // --------------------------------------------------------------------------- export function renderShell(ctx: VaultController, app: HTMLElement): void { if (!app.querySelector('.vault-shell')) { app.innerHTML = `
Relicario
`; ctx.wireSidebar(); wireTypePanel(ctx); } applyShellViewClass(ctx); ctx.renderSidebarCategories(); if (ctx.state.view === 'list') { ctx.renderListPane(); if (ctx.state.drawerOpen && ctx.state.selectedItem) { ctx.renderDrawer(ctx.state.selectedItem); } } else { ctx.renderPane(); } } // Toggle which middle column is visible based on the current view. // list view → list-pane (+ optional drawer); other views → vault-pane. export function applyShellViewClass(ctx: VaultController): void { const shell = document.querySelector('.vault-shell'); if (!shell) return; shell.classList.toggle('vault-shell--list', ctx.state.view === 'list'); shell.classList.toggle('vault-shell--pane', ctx.state.view !== 'list'); } // --------------------------------------------------------------------------- // Right-side type picker panel // --------------------------------------------------------------------------- export function wireTypePanel(ctx: VaultController): void { document.getElementById('vault-type-scrim')?.addEventListener('click', () => closeTypePanel(ctx)); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && ctx.state.typePanelOpen) closeTypePanel(ctx); }); } export function openTypePanel(ctx: VaultController): void { const panel = document.getElementById('vault-type-panel'); const scrim = document.getElementById('vault-type-scrim'); if (!panel || !scrim) return; panel.innerHTML = `
New item
Choose a type
`; panel.classList.add('vault-type-panel--open'); scrim.classList.add('vault-type-panel-scrim--visible'); ctx.state.typePanelOpen = true; panel.querySelector('#vault-type-close')?.addEventListener('click', () => closeTypePanel(ctx)); panel.querySelectorAll('[data-type]').forEach((btn) => { btn.addEventListener('click', () => { const type = btn.dataset.type as ItemType; closeTypePanel(ctx); // Use the host's navigate hook so view + hash + visibility all update // together. This was the bug: bare setHash + renderPane left the // shell stuck in list view with #vault-pane hidden. ctx.state.newType = type; ctx.state.selectedId = null; ctx.state.selectedItem = null; ctx.state.drawerOpen = false; ctx.state.view = 'add'; ctx.setHash('add', type); applyShellViewClass(ctx); ctx.renderSidebarCategories(); ctx.renderPane(); }); }); // Focus first item for keyboard users (panel.querySelector('.vault-type-item') as HTMLElement | null)?.focus(); } export function closeTypePanel(ctx: VaultController): void { document.getElementById('vault-type-panel')?.classList.remove('vault-type-panel--open'); document.getElementById('vault-type-scrim')?.classList.remove('vault-type-panel-scrim--visible'); ctx.state.typePanelOpen = false; } // --------------------------------------------------------------------------- // Color scheme + session-expired wiring (bootstrap helpers) // --------------------------------------------------------------------------- export async function applyVaultColorScheme(): Promise { await applyColorScheme(); chrome.storage.onChanged.addListener((changes, area) => { if (area === 'sync' && 'password_display_scheme' in changes) { void applyColorScheme(); } }); } export function wireSessionExpiredListener(ctx: VaultController): void { chrome.runtime.onMessage.addListener((msg) => { if (msg.type === 'session_expired') { ctx.state.unlocked = false; ctx.state.selectedId = null; ctx.state.selectedItem = null; ctx.state.entries = []; ctx.state.error = null; render(ctx); } }); }