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:
20
extension/src/popup/components/form-header.ts
Normal file
20
extension/src/popup/components/form-header.ts
Normal 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>' : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
/// Card: number / holder / expiry MonthYear / cvv / pin / kind.
|
/// Card: number / holder / expiry MonthYear / cvv / pin / kind.
|
||||||
/// Detail view has a styled card-silhouette signature block.
|
/// 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 { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||||
import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, CardKind, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
@@ -174,12 +175,7 @@ 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;">
|
${renderFormHeader(mode === 'add' ? 'new card' : 'edit card')}
|
||||||
<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>` : ''}
|
${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>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
/// notes/tags + optional supplementary attachments.
|
/// notes/tags + optional supplementary attachments.
|
||||||
/// Primary attachment is referenced by ID from the item's attachments array.
|
/// 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 { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
@@ -85,12 +86,7 @@ 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;">
|
${renderFormHeader(isEdit ? 'edit document' : 'new document')}
|
||||||
<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>` : ''}
|
${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>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/// Identity: full_name, address (multiline), phone, email, date_of_birth.
|
/// Identity: full_name, address (multiline), phone, email, date_of_birth.
|
||||||
/// Detail view shows a "profile card" signature block + plain rows.
|
/// 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 { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
@@ -134,12 +135,7 @@ 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;">
|
${renderFormHeader(mode === 'add' ? 'new identity' : 'edit identity')}
|
||||||
<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>` : ''}
|
${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>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
/// Form's key_material textarea uses CSS text-security to mask characters
|
/// Form's key_material textarea uses CSS text-security to mask characters
|
||||||
/// since <textarea type="password"> isn't a thing.
|
/// 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 { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
@@ -123,12 +124,7 @@ 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;">
|
${renderFormHeader(mode === 'add' ? 'new key' : 'edit key')}
|
||||||
<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>` : ''}
|
${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>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/// field helpers introduced in Slice 2.
|
/// field helpers introduced in Slice 2.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
||||||
|
import { renderFormHeader } from '../form-header';
|
||||||
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||||
import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig, AttachmentRef } from '../../../shared/types';
|
||||||
import { DEFAULT_PASSWORD_REQUEST } 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 = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div style="display:flex; align-items:center;">
|
${renderFormHeader(mode === 'add' ? 'new login' : 'edit login')}
|
||||||
<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>` : ''}
|
${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>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/// detail view; the form is just a big <textarea>.
|
/// detail view; the form is just a big <textarea>.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab, isInTab } from '../../../shared/state';
|
||||||
|
import { renderFormHeader } from '../form-header';
|
||||||
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
@@ -112,12 +113,7 @@ 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;">
|
${renderFormHeader(mode === 'add' ? 'new secure note' : 'edit secure note')}
|
||||||
<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>` : ''}
|
${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>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
/// signature block with a thin SVG countdown ring; form has a kind toggle
|
/// signature block with a thin SVG countdown ring; form has a kind toggle
|
||||||
/// (TOTP vs Steam Guard) and a single secret input.
|
/// (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 { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
|
||||||
import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types';
|
||||||
import { base32Decode, base32Encode } from '../../../shared/base32';
|
import { base32Decode, base32Encode } from '../../../shared/base32';
|
||||||
@@ -213,12 +214,7 @@ 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;">
|
${renderFormHeader(mode === 'add' ? 'new totp' : 'edit totp')}
|
||||||
<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>` : ''}
|
${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,18 @@ body {
|
|||||||
font-size: 11px;
|
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 {
|
.form-subtitle {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
|
|||||||
@@ -104,6 +104,18 @@ body {
|
|||||||
font-size: 11px;
|
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 {
|
.form-subtitle {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
|
|||||||
Reference in New Issue
Block a user