From f1c615c0ed9fef700cae02b014ca2099a7927037 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 15:06:42 -0400 Subject: [PATCH] feat(ext/vault): fullscreen form header with dirty-state subtitle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Title left ('new login' / 'edit login'), subtitle below cycles between 'no changes' and 'unsaved · esc to cancel' on input events. Right side shows the platform-aware save hint ('⌘+S to save' / 'Ctrl+S to save'). The actual ⌘+S keymap arrives in Phase 3 — this is a visual hint only. --- .../src/vault/__tests__/form-wrapper.test.ts | 45 +++++++++++++++++++ extension/src/vault/vault.css | 23 ++++++++++ 2 files changed, 68 insertions(+) create mode 100644 extension/src/vault/__tests__/form-wrapper.test.ts diff --git a/extension/src/vault/__tests__/form-wrapper.test.ts b/extension/src/vault/__tests__/form-wrapper.test.ts new file mode 100644 index 0000000..791c34d --- /dev/null +++ b/extension/src/vault/__tests__/form-wrapper.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('fullscreen form dirty subtitle', () => { + const vaultSrc = fs.readFileSync( + path.resolve(__dirname, '../vault.ts'), + 'utf-8', + ); + + it('contains renderFormWrapped function', () => { + expect(vaultSrc).toContain('function renderFormWrapped'); + }); + + it('starts pristine: renders "no changes" subtitle', () => { + expect(vaultSrc).toContain("'no changes'"); + }); + + it('switches to dirty on first input event', () => { + expect(vaultSrc).toContain("'unsaved · esc to cancel'"); + }); + + it('listens on input and change events on the scroll element', () => { + expect(vaultSrc).toContain("scrollEl.addEventListener('input', markDirty, true)"); + expect(vaultSrc).toContain("scrollEl.addEventListener('change', markDirty, true)"); + }); + + it('marks clean on save', () => { + expect(vaultSrc).toContain('markClean()'); + }); + + it('contains platform-aware SAVE_HINT', () => { + expect(vaultSrc).toContain('SAVE_HINT'); + expect(vaultSrc).toContain('⌘+S to save'); + expect(vaultSrc).toContain('Ctrl+S to save'); + }); + + it('renders fullscreen-form-header element', () => { + expect(vaultSrc).toContain('fullscreen-form-header'); + }); + + it('renders form-dirty-sub element', () => { + expect(vaultSrc).toContain('form-dirty-sub'); + }); +}); diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index a507dbd..1cfb456 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1606,6 +1606,29 @@ textarea { margin-bottom: 12px; } +/* Phase 2B: fullscreen form header */ +.fullscreen-form-header { + padding: 14px 24px; + border-bottom: 1px solid var(--border-mid); + display: flex; + justify-content: space-between; + align-items: flex-start; +} +.fullscreen-form-header .title { + font-size: 16px; + font-weight: 500; + color: var(--text); +} +.fullscreen-form-header .sub { + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} +.fullscreen-form-header .hint { + font-size: 11px; + color: var(--text-dim); +} + /* Phase 2B: sticky save bar + scrollable form pane */ .form-pane { display: flex;