From 32e674eb405ee935cf56eee7ce02cf4b80fd967b Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 30 May 2026 10:32:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(extension):=20field-history=20pane=20visua?= =?UTF-8?q?l=20polish=20=E2=80=94=20section=20headers=20+=20glyph=20button?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/field-history.test.ts | 12 +++- .../src/popup/components/field-history.ts | 38 +++++------ extension/src/popup/styles.css | 66 ++++--------------- extension/src/vault/vault.css | 66 ++++--------------- 4 files changed, 56 insertions(+), 126 deletions(-) diff --git a/extension/src/popup/components/__tests__/field-history.test.ts b/extension/src/popup/components/__tests__/field-history.test.ts index 33df7cd..18dd275 100644 --- a/extension/src/popup/components/__tests__/field-history.test.ts +++ b/extension/src/popup/components/__tests__/field-history.test.ts @@ -38,7 +38,7 @@ describe('field-history view', () => { expect(app.innerHTML).toContain('No history available'); }); - it('renders history entries masked by default', async () => { + it('renders history entries masked by default with section-header and glyph buttons', async () => { (sendMessage as ReturnType).mockResolvedValueOnce({ ok: true, data: { @@ -53,9 +53,17 @@ describe('field-history view', () => { await renderFieldHistory(app); + // Masked by default expect(app.innerHTML).toContain('••••••••••••'); expect(app.innerHTML).not.toContain('secret123'); - expect(app.innerHTML).toContain('current'); + // Section-header per field with uppercase name + entry count + expect(app.innerHTML).toContain('section-header'); + expect(app.innerHTML).toContain('PASSWORD · 2 entries'); + // Current entry annotation + expect(app.innerHTML).toContain('current · '); + // Explicit glyph buttons (reveal + copy) on each entry + expect(app.querySelectorAll('[data-entry-reveal]').length).toBe(2); + expect(app.querySelectorAll('[data-entry-copy]').length).toBe(2); }); it('back button navigates to detail', async () => { diff --git a/extension/src/popup/components/field-history.ts b/extension/src/popup/components/field-history.ts index 8cf1454..8ebf99d 100644 --- a/extension/src/popup/components/field-history.ts +++ b/extension/src/popup/components/field-history.ts @@ -4,7 +4,7 @@ import { getState, setState, sendMessage, navigate, escapeHtml } from '../../sha import { colorizePassword } from '../../shared/password-coloring'; import type { FieldHistoryView } from '../../shared/types'; import { relativeTime } from '../../shared/relative-time'; -import { GLYPH_COPY } from '../../shared/glyphs'; +import { GLYPH_COPY, GLYPH_REVEAL, GLYPH_HIDE } from '../../shared/glyphs'; const revealedSet = new Set(); @@ -58,27 +58,28 @@ export async function renderFieldHistory(app: HTMLElement): Promise { const isRevealed = revealedSet.has(entryKey); const displayValue = isRevealed ? escapeHtml(value) : '••••••••••••'; valueStore.set(entryKey, value); + const revealGlyph = isRevealed ? GLYPH_HIDE : GLYPH_REVEAL; return `
${displayValue}
- `; } let content = ''; for (const field of history) { - if (history.length > 1) { - content += `
${escapeHtml(field.field_name)}
`; - } - // Current value first + const entryCount = field.entries.length + 1; // +1 for current + content += `
${escapeHtml(field.field_name.toUpperCase())} · ${entryCount} ${entryCount === 1 ? 'entry' : 'entries'}
`; content += renderEntry(field.field_id, field.current_value, item.modified, true); - // Historical values for (const entry of field.entries) { content += renderEntry(field.field_id, entry.value, entry.changed_at, false); } @@ -108,17 +109,14 @@ export async function renderFieldHistory(app: HTMLElement): Promise { // Wire handlers app.querySelector('#back-btn')?.addEventListener('click', () => navigate('detail')); - // Toggle reveal on click - app.querySelectorAll('.history-entry').forEach((el) => { - el.addEventListener('click', (e) => { - if ((e.target as HTMLElement).classList.contains('history-entry__copy')) return; - const key = el.dataset.entry; + // Reveal toggle via explicit glyph button (decoupled from row click) + app.querySelectorAll('[data-entry-reveal]').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const key = btn.dataset.entryReveal; if (!key) return; - if (revealedSet.has(key)) { - revealedSet.delete(key); - } else { - revealedSet.add(key); - } + if (revealedSet.has(key)) revealedSet.delete(key); + else revealedSet.add(key); renderFieldHistory(app); }); }); diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 5abe74a..0ce2d18 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1191,66 +1191,28 @@ textarea { margin-bottom: 12px; } -.history-field-label { - font-size: 11px; - color: #8b949e; - text-transform: uppercase; - margin: 12px 0 6px; -} - .history-entry { - display: flex; + display: grid; + grid-template-columns: 1fr auto; + gap: 6px; align-items: center; - gap: 8px; - padding: 10px; - border-radius: 4px; - background: #161b22; - margin-bottom: 6px; - cursor: pointer; + padding: 8px 0; + border-bottom: 1px solid var(--border-subtle); } - -.history-entry:hover { - background: #1c2128; -} - .history-entry__value { - flex: 1; - font-family: monospace; - font-size: 13px; + font-family: ui-monospace, monospace; + word-break: break-all; } - -.history-entry__value.masked { - color: #8b949e; -} - -.history-entry__value.revealed { - color: #c9d1d9; -} - +.history-entry__value.masked { letter-spacing: 1px; } .history-entry__meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 2px; + grid-column: 1 / 2; font-size: 11px; - color: #8b949e; } - -.history-entry__current { - color: #58a6ff; - font-weight: 500; -} - -.history-entry__copy { - background: none; - border: none; - cursor: pointer; - font-size: 14px; - padding: 4px; -} - -.history-entry__copy:hover { - opacity: 0.8; +.history-entry__actions { + grid-row: 1 / 3; + grid-column: 2 / 3; + display: flex; + gap: 4px; } /* --- Type selection --- */ diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 5ee70ab..23a5e28 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1111,66 +1111,28 @@ textarea { margin-bottom: 12px; } -.history-field-label { - font-size: 11px; - color: #8b949e; - text-transform: uppercase; - margin: 12px 0 6px; -} - .history-entry { - display: flex; + display: grid; + grid-template-columns: 1fr auto; + gap: 6px; align-items: center; - gap: 8px; - padding: 10px; - border-radius: 4px; - background: #161b22; - margin-bottom: 6px; - cursor: pointer; + padding: 8px 0; + border-bottom: 1px solid var(--border-subtle); } - -.history-entry:hover { - background: #1c2128; -} - .history-entry__value { - flex: 1; - font-family: monospace; - font-size: 13px; + font-family: ui-monospace, monospace; + word-break: break-all; } - -.history-entry__value.masked { - color: #8b949e; -} - -.history-entry__value.revealed { - color: #c9d1d9; -} - +.history-entry__value.masked { letter-spacing: 1px; } .history-entry__meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 2px; + grid-column: 1 / 2; font-size: 11px; - color: #8b949e; } - -.history-entry__current { - color: #58a6ff; - font-weight: 500; -} - -.history-entry__copy { - background: none; - border: none; - cursor: pointer; - font-size: 14px; - padding: 4px; -} - -.history-entry__copy:hover { - opacity: 0.8; +.history-entry__actions { + grid-row: 1 / 3; + grid-column: 2 / 3; + display: flex; + gap: 4px; } /* --- Type selection --- */