feat(ext): static 'esc to cancel' subtitle in fullscreen form headers

All seven type forms plus the type-selection screen now show a small
'esc to cancel' subtitle under the heading when rendered in the
fullscreen vault tab (isInTab() === true). The subtitle is suppressed
in the popup, where esc has the more general meaning of closing the
popup. .form-subtitle class is shared between popup and vault
stylesheets so future hooks can reuse it.

Dynamic dirty-state ('unsaved · esc to cancel') wiring is deferred to
Phase 3 (unsaved-changes guard).

Plan 2026-04-30 fullscreen UX phase 1 task 8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-01 14:17:59 -04:00
parent 38ba31768a
commit 381e8ed496
12 changed files with 94 additions and 8 deletions

View File

@@ -54,12 +54,13 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
function renderTypeSelection(app: HTMLElement): void {
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
<div style="display:flex; align-items:center; gap:12px;">
<button class="btn" id="back-btn">← back</button>
<h3 style="margin:0;">new item</h3>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
<div class="type-select-list">
${TYPE_OPTIONS.map((opt) => `
<button class="type-select-row" data-type="${opt.type}">

View File

@@ -0,0 +1,32 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../../../../shared/state', () => ({
sendMessage: vi.fn(),
getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }),
setState: vi.fn(),
navigate: vi.fn(),
escapeHtml: (s: string) => s,
popOutToTab: vi.fn(),
isInTab: () => true, // FULLSCREEN context
openVaultTab: vi.fn(),
registerHost: vi.fn(),
}));
vi.mock('../../generator-panel', () => ({
openGeneratorPanel: vi.fn(),
closeGeneratorPanel: vi.fn(),
isGeneratorPanelOpen: () => false,
}));
import { TYPED_FORMS } from './_typed-forms';
describe('form subtitle (fullscreen context)', () => {
beforeEach(() => { document.body.innerHTML = '<div id="app"></div>'; });
it.each(TYPED_FORMS)('%s form renders "esc to cancel" subtitle in fullscreen', (_name, render) => {
render(document.getElementById('app')!, 'add', null);
const subtitle = document.querySelector('.form-subtitle');
expect(subtitle).not.toBeNull();
expect(subtitle?.textContent).toContain('esc to cancel');
});
});

View File

@@ -0,0 +1,30 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../../../../shared/state', () => ({
sendMessage: vi.fn(),
getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }),
setState: vi.fn(),
navigate: vi.fn(),
escapeHtml: (s: string) => s,
popOutToTab: vi.fn(),
isInTab: () => false, // POPUP context
openVaultTab: vi.fn(),
registerHost: vi.fn(),
}));
vi.mock('../../generator-panel', () => ({
openGeneratorPanel: vi.fn(),
closeGeneratorPanel: vi.fn(),
isGeneratorPanelOpen: () => false,
}));
import { TYPED_FORMS } from './_typed-forms';
describe('form subtitle (popup context)', () => {
beforeEach(() => { document.body.innerHTML = '<div id="app"></div>'; });
it.each(TYPED_FORMS)('%s form omits the form-subtitle in popup context', (_name, render) => {
render(document.getElementById('app')!, 'add', null);
expect(document.querySelector('.form-subtitle')).toBeNull();
});
});

View File

@@ -174,11 +174,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div style="display:flex; align-items:center;">
<div class="detail-title">${mode === 'add' ? 'new card' : 'edit card'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Amex Gold"></div>

View File

@@ -85,11 +85,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div style="display:flex; align-items:center;">
<div class="detail-title">${isEdit ? 'edit document' : 'new document'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-group">
<label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>

View File

@@ -134,11 +134,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div style="display:flex; align-items:center;">
<div class="detail-title">${mode === 'add' ? 'new identity' : 'edit identity'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Aaron Lee · personal"></div>

View File

@@ -123,11 +123,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div style="display:flex; align-items:center;">
<div class="detail-title">${mode === 'add' ? 'new key' : 'edit key'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="github ssh"></div>

View File

@@ -244,11 +244,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div style="display:flex; align-items:center;">
<div class="detail-title">${mode === 'add' ? 'new login' : 'edit login'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>

View File

@@ -112,11 +112,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div style="display:flex; align-items:center;">
<div class="detail-title">${mode === 'add' ? 'new secure note' : 'edit secure note'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="My recovery codes"></div>

View File

@@ -213,11 +213,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
const renderInner = (): string => `
<div class="pad">
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div style="display:flex; align-items:center;">
<div class="detail-title">${mode === 'add' ? 'new totp' : 'edit totp'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>

View File

@@ -104,6 +104,14 @@ body {
font-size: 11px;
}
.form-subtitle {
font-size: 11px;
color: var(--text-dim);
margin-top: 2px;
margin-bottom: 14px;
letter-spacing: 0.02em;
}
.error {
color: #ab2b20;
font-size: 12px;

View File

@@ -104,6 +104,14 @@ body {
font-size: 11px;
}
.form-subtitle {
font-size: 11px;
color: var(--text-dim);
margin-top: 2px;
margin-bottom: 14px;
letter-spacing: 0.02em;
}
.error {
color: #ab2b20;
font-size: 12px;