refactor(ext/popup): extract renderFormHeader + .form-header CSS

Code-review feedback on Task 8: the conditional empty
<div style="margin-bottom:16px;"> spacer was an inline-styled magic
number and the 6-line header pattern was duplicated across all 7 typed
forms.

Now:
- .form-header class owns the bottom margin in both stylesheets.
- :has(+ .form-subtitle) selector drops the margin when a subtitle
  follows, so spacing tokens stay in CSS instead of inline styles.
- renderFormHeader(titleText) shared helper collapses the 6-line
  duplication to a one-liner per form. item-form.ts (type-selection
  screen) is unaffected — it uses a different header structure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-01 14:26:16 -04:00
parent 381e8ed496
commit 1454cd8165
10 changed files with 63 additions and 47 deletions

View File

@@ -0,0 +1,20 @@
/// Shared header chrome for typed form views (login, secure-note, identity, card,
/// key, totp, document). Renders the title row plus a fullscreen-only "esc to
/// cancel" subtitle. Use the existing `${...}` template-literal interpolation
/// at call sites: `${renderFormHeader('new login')}`.
///
/// item-form.ts (the type-selection screen) uses a different header structure
/// and does NOT consume this helper.
import { isInTab } from '../../shared/state';
export function renderFormHeader(titleText: string): string {
return `
<div class="form-header">
<div class="detail-title">${titleText}</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>' : ''}
`;
}

View File

@@ -1,7 +1,8 @@
/// Card: number / holder / expiry MonthYear / cvv / pin / kind.
/// Detail view has a styled card-silhouette signature block.
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types';
import {
@@ -174,12 +175,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<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>'}
${renderFormHeader(mode === 'add' ? 'new card' : 'edit card')}
${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

@@ -2,7 +2,8 @@
/// notes/tags + optional supplementary attachments.
/// Primary attachment is referenced by ID from the item's attachments array.
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
import {
@@ -85,12 +86,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<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>'}
${renderFormHeader(isEdit ? 'edit document' : 'new document')}
${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

@@ -1,7 +1,8 @@
/// Identity: full_name, address (multiline), phone, email, date_of_birth.
/// Detail view shows a "profile card" signature block + plain rows.
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
import {
@@ -134,12 +135,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<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>'}
${renderFormHeader(mode === 'add' ? 'new identity' : 'edit identity')}
${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

@@ -2,7 +2,8 @@
/// Form's key_material textarea uses CSS text-security to mask characters
/// since <textarea type="password"> isn't a thing.
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
import {
@@ -123,12 +124,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<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>'}
${renderFormHeader(mode === 'add' ? 'new key' : 'edit key')}
${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

@@ -2,6 +2,7 @@
/// field helpers introduced in Slice 2.
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig, AttachmentRef } from '../../../shared/types';
import { DEFAULT_PASSWORD_REQUEST } from '../../../shared/types';
@@ -244,12 +245,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<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>'}
${renderFormHeader(mode === 'add' ? 'new login' : 'edit login')}
${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

@@ -2,6 +2,7 @@
/// detail view; the form is just a big <textarea>.
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
import {
@@ -112,12 +113,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
app.innerHTML = `
<div class="pad">
<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>'}
${renderFormHeader(mode === 'add' ? 'new secure note' : 'edit secure note')}
${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

@@ -2,7 +2,8 @@
/// signature block with a thin SVG countdown ring; form has a kind toggle
/// (TOTP vs Steam Guard) and a single secret input.
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types';
import { base32Decode, base32Encode } from '../../../shared/base32';
@@ -213,12 +214,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
const renderInner = (): string => `
<div class="pad">
<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>'}
${renderFormHeader(mode === 'add' ? 'new totp' : 'edit totp')}
${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,18 @@ body {
font-size: 11px;
}
.form-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
/* When the header is followed by a subtitle, drop the bottom margin so the
subtitle's own margin owns the spacing. */
.form-header:has(+ .form-subtitle) {
margin-bottom: 0;
}
.form-subtitle {
font-size: 11px;
color: var(--text-dim);