fix(ext/vault): friendly error block in fullscreen tab (closes B2)

Replaces raw escapeHtml(state.error) renders with lookupErrorCopy()-driven
title/body/CTA blocks. vault_locked specifically gets an 'Unlock vault'
CTA that refocuses the passphrase input. Other CTAs route to setup.html
or chrome.runtime.reload().

Closes B2; concludes P4.
This commit is contained in:
adlee-was-taken
2026-05-02 16:37:16 -04:00
parent 214e1e49f8
commit 1c641b4911
2 changed files with 59 additions and 1 deletions

View File

@@ -144,6 +144,30 @@ body {
margin-top: 8px;
}
.error-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 16px;
border: 1px solid rgba(171, 43, 32, 0.4);
border-radius: 6px;
background: rgba(171, 43, 32, 0.08);
margin-top: 12px;
}
.error-block .error-title {
font-weight: 600;
color: var(--danger);
}
.error-block .error-body {
color: var(--text);
font-size: 12px;
text-align: center;
}
.error-block .error-cta {
margin-top: 6px;
}
/* Buttons */
.btn {
display: inline-block;

View File

@@ -9,6 +9,7 @@ import type {
ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest,
} from '../shared/types';
import { registerHost } from '../shared/state';
import { lookupErrorCopy, type ErrorCta } from '../shared/error-copy';
import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK } from '../shared/glyphs';
import { renderItemDetail } from '../popup/components/item-detail';
import { renderItemForm } from '../popup/components/item-form';
@@ -41,6 +42,21 @@ function escapeHtml(str: string): string {
.replace(/'/g, ''');
}
function renderErrorBlock(code: string | null | undefined): string {
if (!code) return '';
const copy = lookupErrorCopy(code);
const ctaHtml = copy.cta
? `<button class="btn btn-primary error-cta" data-cta="${escapeHtml(copy.cta.action ?? '')}">${escapeHtml(copy.cta.label)}</button>`
: '';
return `
<div class="error error-block">
<div class="error-title">${escapeHtml(copy.title)}</div>
<div class="error-body">${escapeHtml(copy.body)}</div>
${ctaHtml}
</div>
`;
}
function typeIcon(t: ItemType): string {
switch (t) {
case 'login': return '\u{1F511}'; // key
@@ -199,11 +215,29 @@ function renderLockScreen(app: HTMLElement): void {
<div class="vault-lock-screen__form">
<input type="password" id="vault-passphrase" placeholder="passphrase" autocomplete="off" />
<button class="btn btn-primary" id="vault-unlock-btn" style="width:100%;">unlock</button>
${state.error ? `<div class="error" style="text-align:center;">${escapeHtml(state.error)}</div>` : ''}
${renderErrorBlock(state.error)}
</div>
</div>
`;
app.querySelector<HTMLButtonElement>('.error-cta')?.addEventListener('click', (e) => {
const cta = (e.currentTarget as HTMLElement).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;
}
}
});
const input = document.getElementById('vault-passphrase') as HTMLInputElement;
const btn = document.getElementById('vault-unlock-btn')!;