/// Field rendering primitives used by every typed-item detail view. /// /// Pure functions that return HTML strings. Caller is responsible for /// mounting the strings into the DOM (typically via `app.innerHTML = ...`). /// After mounting, call `wireFieldHandlers(scope)` once to bind reveal + /// copy click handlers on any rendered rows. import { escapeHtml } from '../popup'; export interface RowOpts { label: string; value: string; copyable?: boolean; href?: string; monospace?: boolean; multiline?: boolean; } /// Plain label/value row. Optional copy button, optional anchor wrap, /// optional monospace styling, optional multiline (renders in a
).
export function renderRow(opts: RowOpts): string {
  const { label, value, copyable, href, monospace, multiline } = opts;
  const valueClass = `field-row__value${monospace ? ' monospace' : ''}`;
  let valueHtml: string;
  if (multiline) {
    valueHtml = `
${escapeHtml(value)}
`; } else if (href) { valueHtml = `${escapeHtml(value)}`; } else { valueHtml = escapeHtml(value); } const actions = copyable ? `` : ''; return `
${escapeHtml(label)} ${valueHtml} ${actions}
`; } export interface ConcealedRowOpts { id: string; label: string; value: string; monospace?: boolean; multiline?: boolean; } /// Concealed row — value rendered hidden until the user clicks "show". /// Plaintext is stored in `data-field-value` on the row element and copied /// to the visible value span on reveal. Copy button always copies plaintext. export function renderConcealedRow(opts: ConcealedRowOpts): string { const { id, label, value, monospace, multiline } = opts; const placeholder = multiline ? `•••• (${value.length} chars)` : '••••'; const valueClass = `field-row__value${monospace ? ' monospace' : ''}`; return `
${escapeHtml(label)} ${escapeHtml(placeholder)}
`; } export interface SignatureBlockOpts { accent?: 'blue' | 'green' | 'amber' | 'red'; children: string; } /// Container for the type-specific signature panel. `children` is HTML /// the caller has already produced (and escaped where needed). export function renderSignatureBlock(opts: SignatureBlockOpts): string { const accent = opts.accent ?? 'blue'; return `
${opts.children}
`; } /// Wire reveal-toggle + copy click handlers within `scope`. Idempotent — /// safe to call multiple times against the same scope. export function wireFieldHandlers(scope: HTMLElement): void { scope.querySelectorAll('[data-field-action="reveal"]').forEach((btn) => { btn.addEventListener('click', () => { const row = btn.closest('[data-field-id]') as HTMLElement | null; if (!row) return; const valueEl = row.querySelector('[data-field-role="value"]') as HTMLElement | null; if (!valueEl) return; const revealed = row.getAttribute('data-revealed') === 'true'; const plaintext = row.getAttribute('data-field-value') ?? ''; const multiline = row.getAttribute('data-field-multiline') === 'true'; if (revealed) { const placeholder = multiline ? `•••• (${plaintext.length} chars)` : '••••'; valueEl.textContent = placeholder; row.setAttribute('data-revealed', 'false'); btn.textContent = 'show'; } else { valueEl.textContent = plaintext; row.setAttribute('data-revealed', 'true'); btn.textContent = 'hide'; } }); }); scope.querySelectorAll('[data-field-action="copy"]').forEach((btn) => { btn.addEventListener('click', async () => { const value = btn.getAttribute('data-field-value') ?? ''; try { await navigator.clipboard.writeText(value); } catch { /* swallow — UX is the visual flash below */ } const original = btn.textContent; btn.textContent = 'copied'; setTimeout(() => { if (btn.textContent === 'copied') btn.textContent = original; }, 1500); }); }); }