// 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 = `
`;
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 = `
`;
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 = `
Choose a type
${PICKER_TYPES.map((t) => `
`).join('')}
`;
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);
}
});
}