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:
@@ -54,12 +54,13 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
|
|||||||
function renderTypeSelection(app: HTMLElement): void {
|
function renderTypeSelection(app: HTMLElement): void {
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<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>
|
<button class="btn" id="back-btn">← back</button>
|
||||||
<h3 style="margin:0;">new item</h3>
|
<h3 style="margin:0;">new item</h3>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</div>
|
||||||
|
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
|
||||||
<div class="type-select-list">
|
<div class="type-select-list">
|
||||||
${TYPE_OPTIONS.map((opt) => `
|
${TYPE_OPTIONS.map((opt) => `
|
||||||
<button class="type-select-row" data-type="${opt.type}">
|
<button class="type-select-row" data-type="${opt.type}">
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -174,11 +174,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<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>
|
<div class="detail-title">${mode === 'add' ? 'new card' : 'edit card'}</div>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</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>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
<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>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Amex Gold"></div>
|
||||||
|
|||||||
@@ -85,11 +85,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<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>
|
<div class="detail-title">${isEdit ? 'edit document' : 'new document'}</div>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</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>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
<label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
||||||
|
|||||||
@@ -134,11 +134,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<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>
|
<div class="detail-title">${mode === 'add' ? 'new identity' : 'edit identity'}</div>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</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>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
<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>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="Aaron Lee · personal"></div>
|
||||||
|
|||||||
@@ -123,11 +123,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<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>
|
<div class="detail-title">${mode === 'add' ? 'new key' : 'edit key'}</div>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</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>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
<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>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="github ssh"></div>
|
||||||
|
|||||||
@@ -244,11 +244,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<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>
|
<div class="detail-title">${mode === 'add' ? 'new login' : 'edit login'}</div>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</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>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
<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>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>
|
||||||
|
|||||||
@@ -112,11 +112,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<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>
|
<div class="detail-title">${mode === 'add' ? 'new secure note' : 'edit secure note'}</div>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</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>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
<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>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="My recovery codes"></div>
|
||||||
|
|||||||
@@ -213,11 +213,12 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
|
|
||||||
const renderInner = (): string => `
|
const renderInner = (): string => `
|
||||||
<div class="pad">
|
<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>
|
<div class="detail-title">${mode === 'add' ? 'new totp' : 'edit totp'}</div>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
|
||||||
</div>
|
</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>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
|
<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>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>
|
||||||
|
|||||||
@@ -104,6 +104,14 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-subtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: #ab2b20;
|
color: #ab2b20;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -104,6 +104,14 @@ body {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-subtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: #ab2b20;
|
color: #ab2b20;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user