import { sendMessage, escapeHtml, openVaultTab } from '../../shared/state'; import type { VaultSettings, DeviceSettings, TrashRetention, HistoryRetention } from '../../shared/types'; import type { ColorScheme } from '../../shared/color-scheme'; import { loadColorScheme, saveColorScheme, resetColorScheme, DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR, } from '../../shared/color-scheme'; import { colorizePassword } from '../../shared/password-coloring'; import { openGeneratorPanel, closeGeneratorPanel, isGeneratorPanelOpen } from './generator-panel'; import { renderSecuritySection, teardownSecuritySection } from './settings-security'; type SettingsSection = | 'autofill' | 'display' | 'security' | 'generator' | 'retention' | 'backup' | 'import'; const NAV_ITEMS: Array<{ id: SettingsSection; icon: string; label: string; group: 'device' | 'vault' }> = [ { id: 'autofill', icon: '⊙', label: 'Autofill', group: 'device' }, { id: 'display', icon: '◈', label: 'Display', group: 'device' }, { id: 'security', icon: '◉', label: 'Security', group: 'vault' }, { id: 'generator', icon: '↻', label: 'Generator', group: 'vault' }, { id: 'retention', icon: '▦', label: 'Retention', group: 'vault' }, { id: 'backup', icon: '⤓', label: 'Backup', group: 'vault' }, { id: 'import', icon: '≡', label: 'Import', group: 'vault' }, ]; let activeSection: SettingsSection = 'autofill'; let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null; let pendingVaultSettings: VaultSettings | null = null; let sessionHandle: number | null = null; export async function renderSettings(container: HTMLElement): Promise { container.innerHTML = `
`; const unlockedResp = await sendMessage({ type: 'is_unlocked' }); sessionHandle = (unlockedResp.ok && unlockedResp.data && (unlockedResp.data as { unlocked: boolean }).unlocked) ? 1 : null; wireNav(); await renderSection(activeSection); } export function teardownSettings(): void { closeGeneratorPanel(); teardownSecuritySection(); if (activeKeyHandler) { document.removeEventListener('keydown', activeKeyHandler); activeKeyHandler = null; } pendingVaultSettings = null; sessionHandle = null; } function navItemHtml(item: (typeof NAV_ITEMS)[0]): string { const active = item.id === activeSection ? ' settings-nav__item--active' : ''; return ` `; } function wireNav(): void { document.getElementById('settings-nav')?.querySelectorAll('[data-section]') .forEach((btn) => { btn.addEventListener('click', async () => { teardownSecuritySection(); closeGeneratorPanel(); activeSection = btn.dataset.section as SettingsSection; document.querySelectorAll('.settings-nav__item').forEach(b => b.classList.remove('settings-nav__item--active')); btn.classList.add('settings-nav__item--active'); await renderSection(activeSection); }); }); } async function renderSection(section: SettingsSection): Promise { const content = document.getElementById('settings-content'); if (!content) return; switch (section) { case 'autofill': return renderAutofillSection(content); case 'display': return renderDisplaySection(content); case 'security': return renderSecuritySection(content, sessionHandle); case 'generator': return renderGeneratorSection(content); case 'retention': return renderRetentionSection(content); case 'backup': return renderBackupSection(content); case 'import': return renderImportSection(content); } } // --- Section stubs (filled in by Tasks 3-9) --- async function renderAutofillSection(content: HTMLElement): Promise { const [settingsResp, blacklistResp] = await Promise.all([ sendMessage({ type: 'get_settings' }), sendMessage({ type: 'get_blacklist' }), ]); const settings: DeviceSettings = settingsResp.ok ? (settingsResp.data as { settings: DeviceSettings }).settings : { captureEnabled: false, captureStyle: 'bar' }; const blacklist: string[] = blacklistResp.ok ? (blacklistResp.data as { blacklist: string[] }).blacklist : []; content.innerHTML = `

Capture

Auto-detect logins
Show a prompt when a login form is detected.
Prompt style
How to prompt when a login is detected.

Blocked sites

${blacklist.length > 0 ? blacklist.map((h) => `
${escapeHtml(h)}
`).join('') : '

No blocked sites.

'}
`; document.getElementById('capture-enabled')?.addEventListener('change', async (e) => { const enabled = (e.target as HTMLInputElement).checked; await sendMessage({ type: 'update_settings', settings: { captureEnabled: enabled } }); }); document.getElementById('style-bar')?.addEventListener('click', async () => { await sendMessage({ type: 'update_settings', settings: { captureStyle: 'bar' } }); renderAutofillSection(content); }); document.getElementById('style-toast')?.addEventListener('click', async () => { await sendMessage({ type: 'update_settings', settings: { captureStyle: 'toast' } }); renderAutofillSection(content); }); content.querySelectorAll('.remove-bl').forEach((btn) => { btn.addEventListener('click', async () => { const host = btn.dataset.hostname; if (!host) return; await sendMessage({ type: 'remove_blacklist', hostname: host }); renderAutofillSection(content); }); }); } async function renderDisplaySection(content: HTMLElement): Promise { const scheme = await loadColorScheme(); content.innerHTML = `

Password coloring

Digit color
Symbol color
Preview
`; function refreshPreview(s: ColorScheme): void { const preview = document.getElementById('color-preview'); if (!preview) return; preview.style.setProperty('--relicario-pwd-digit-color', s.digit_color); preview.style.setProperty('--relicario-pwd-symbol-color', s.symbol_color); preview.innerHTML = ''; preview.appendChild(colorizePassword('Abc123!@#')); } refreshPreview(scheme); document.getElementById('digit-color')?.addEventListener('change', async (e) => { const color = (e.target as HTMLInputElement).value; const current = await loadColorScheme(); await saveColorScheme({ ...current, digit_color: color }); refreshPreview({ ...current, digit_color: color }); }); document.getElementById('symbol-color')?.addEventListener('change', async (e) => { const color = (e.target as HTMLInputElement).value; const current = await loadColorScheme(); await saveColorScheme({ ...current, symbol_color: color }); refreshPreview({ ...current, symbol_color: color }); }); document.getElementById('reset-colors')?.addEventListener('click', async () => { await resetColorScheme(); renderDisplaySection(content); }); } async function renderGeneratorSection(content: HTMLElement): Promise { content.innerHTML = '

Loading…

'; const resp = await sendMessage({ type: 'get_vault_settings' }); if (!resp.ok) { const errorMsg = (resp as { ok: false; error: string }).error; content.innerHTML = `

Failed to load: ${escapeHtml(errorMsg)}

`; return; } const settings = (resp.data as { settings: VaultSettings }).settings; content.innerHTML = `

Generator defaults

Configure generator
Password length, character classes, passphrase word count.
`; document.getElementById('open-generator-panel')?.addEventListener('click', (e) => { const trigger = e.currentTarget as HTMLElement; if (isGeneratorPanelOpen()) { closeGeneratorPanel(); return; } openGeneratorPanel({ parent: content, trigger, initial: settings.generator_defaults, context: 'configure-defaults', }); }); } async function renderRetentionSection(content: HTMLElement): Promise { content.innerHTML = '

Loading…

'; const resp = await sendMessage({ type: 'get_vault_settings' }); if (!resp.ok) { content.innerHTML = `

Failed to load: ${escapeHtml(resp.error ?? 'unknown')}

`; return; } const settings = (resp.data as { settings: VaultSettings }).settings; pendingVaultSettings = { ...settings }; content.innerHTML = `

Trash retention

Keep deleted items for
Items in trash older than this are permanently deleted on the next sync.

Field history retention

Keep password history for
History entries older than this are pruned on save.
`; // Set current select values (document.getElementById('trash-retention') as HTMLSelectElement).value = trashRetentionToValue(settings.trash_retention); (document.getElementById('history-retention') as HTMLSelectElement).value = historyRetentionToValue(settings.field_history_retention); document.getElementById('trash-retention')?.addEventListener('change', (e) => { if (pendingVaultSettings) { pendingVaultSettings.trash_retention = valueToTrashRetention((e.target as HTMLSelectElement).value); } }); document.getElementById('history-retention')?.addEventListener('change', (e) => { if (pendingVaultSettings) { pendingVaultSettings.field_history_retention = valueToHistoryRetention((e.target as HTMLSelectElement).value); } }); document.getElementById('save-retention')?.addEventListener('click', async () => { if (!pendingVaultSettings) return; const r = await sendMessage({ type: 'update_vault_settings', settings: pendingVaultSettings }); if (!r.ok) alert(`Save failed: ${r.error}`); }); } function trashRetentionToValue(r: TrashRetention): string { if (r.kind === 'forever') return 'forever'; return `days:${r.value}`; } function valueToTrashRetention(v: string): TrashRetention { if (v === 'forever') return { kind: 'forever' }; const m = /^days:(\d+)$/.exec(v); if (m) return { kind: 'days', value: Number(m[1]) }; return { kind: 'forever' }; } function historyRetentionToValue(r: HistoryRetention): string { if (r.kind === 'forever') return 'forever'; if (r.kind === 'last_n') return `last_n:${r.value}`; return `days:${r.value}`; } function valueToHistoryRetention(v: string): HistoryRetention { if (v === 'forever') return { kind: 'forever' }; const mLast = /^last_n:(\d+)$/.exec(v); if (mLast) return { kind: 'last_n', value: Number(mLast[1]) }; const mDays = /^days:(\d+)$/.exec(v); if (mDays) return { kind: 'days', value: Number(mDays[1]) }; return { kind: 'forever' }; } function renderBackupSection(content: HTMLElement): void { content.innerHTML = `

Backup & restore

Export & restore backup
Download an encrypted backup or restore from a file. Opens in the vault tab.
`; document.getElementById('open-backup-tab')?.addEventListener('click', () => openVaultTab('backup')); } function renderImportSection(content: HTMLElement): void { content.innerHTML = `

Import

Import from LastPass
Import a LastPass CSV export. Opens in the vault tab for review before committing.
`; document.getElementById('open-import-tab')?.addEventListener('click', () => openVaultTab('import')); } export { renderAutofillSection, renderDisplaySection, renderGeneratorSection, renderRetentionSection, renderBackupSection, renderImportSection }; // Suppress unused-import warnings — these are used by Tasks 3-9 void sendMessage; void loadColorScheme; void saveColorScheme; void resetColorScheme; void DEFAULT_DIGIT_COLOR; void DEFAULT_SYMBOL_COLOR; void colorizePassword; void openGeneratorPanel; void pendingVaultSettings; void activeKeyHandler;