# Phase 2B: Polish Foundation + Form Layout — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Land the patina palette, polish vocabulary (backdrop, glass cards, button hierarchy, arrow glyph), and the two-column login form layout across popup, setup wizard, and fullscreen vault. **Architecture:** Foundation CSS tokens + shared classes go into `popup/styles.css` and `vault/vault.css` first. Each surface (login, setup, vault) is then updated to consume the new classes. The two-column login form gets a `surface: 'popup' | 'fullscreen'` flag on `renderForm()` so the same component renders single-column in popup and two-column in fullscreen. **Tech Stack:** TypeScript, vanilla DOM, vitest + happy-dom, plain CSS (no preprocessor). **Spec:** `docs/superpowers/specs/2026-05-02-phase-2b-form-layout-design.md` --- ## File Structure | File | Change | Purpose | |------|--------|---------| | `extension/src/shared/glyphs.ts` | Modify | Add `GLYPH_NEXT = '▸'` | | `extension/src/popup/styles.css` | Modify | Patina tokens, `.surface-backdrop`, `.glass`, `.btn-primary/secondary` | | `extension/src/vault/vault.css` | Modify | Same tokens + form-grid + sticky save bar + header treatment | | `extension/src/popup/components/unlock.ts` | Modify | Logo lockup, glass card, primary unlock button | | `extension/src/popup/components/settings-vault.ts:164,171` | Modify | Replace `→` with `▸` | | `extension/src/setup/setup.ts` | Modify | Backdrop wrapper, glass step cards, `▸` on next buttons | | `extension/src/vault/vault.ts` | Modify | Backdrop wrapper, surface flag passed to login renderer, dirty subtitle | | `extension/src/popup/components/types/login.ts` | Modify | `surface` param on renderForm; column wrapping for fullscreen | | `extension/src/popup/components/__tests__/unlock.test.ts` | Create | Unlock view structure tests | | `extension/src/setup/__tests__/setup.test.ts` | Create | Setup wizard structure tests | | `extension/src/popup/components/types/__tests__/login.test.ts` | Modify | Surface flag + two-column rendering tests | --- ## Task 1: Add patina color tokens to popup/styles.css **Files:** - Modify: `extension/src/popup/styles.css:3-28` - [ ] **Step 1: Read the current `:root` block in styles.css** The existing tokens are at lines 3-28. We're adding patina tokens alongside, keeping the `--accent` alias for backwards compatibility. - [ ] **Step 2: Replace the `:root` block with patina tokens** In `extension/src/popup/styles.css`, replace lines 3-28 with: ```css :root { /* Patina gold (Phase 2B) */ --gold-base: #a88a4a; --gold-mid: #cdb47a; --gold-shadow: #5a3f12; --gold-text: #c9a868; --gold-soft: rgba(184, 149, 86, 0.14); --gold-ring: rgba(184, 149, 86, 0.18); --gold-stroke: #b89556; --gold-hi-end: #dac8a0; /* Brand alias (kept for backwards compatibility) */ --accent: var(--gold-base); --accent-soft: var(--gold-soft); --accent-strong: var(--gold-shadow); /* Surfaces */ --bg-page: #0a0e14; --bg-pane: #11161e; --bg-elevated: #1c2330; --bg-card: rgba(22, 27, 34, 0.55); --bg-input: #0a0e14; --border-soft: rgba(255, 255, 255, 0.05); --border-mid: #262d36; --border-subtle: var(--border-mid); /* Text */ --text: #c9d1d9; --text-muted: #8b949e; --text-dim: #6b7888; /* Status */ --danger: #ab2b20; --danger-bg: #791111; --success: #6cb37a; /* Focus */ --focus-ring: 0 0 0 2px var(--gold-ring); } ``` - [ ] **Step 3: Update `body` background to use new token** Find the `body` rule (around line 36) and replace `background: #0d1117;` with `background: var(--bg-page);`. - [ ] **Step 4: Update `.brand` color to use `--gold-text`** Find `.brand` (around line 62) and replace `color: #d2ab43;` with `color: var(--gold-text);`. - [ ] **Step 5: Build extension to verify no CSS errors** Run from the `extension/` directory: ```bash npm run build 2>&1 | tail -5 ``` Expected: webpack compiles successfully. - [ ] **Step 6: Commit** ```bash git add extension/src/popup/styles.css git commit -m "$(cat <<'EOF' style(ext/popup): add patina palette tokens Replaces bright amber #d2ab43 with patina gold #a88a4a as the new base. Keeps --accent as alias for backwards compatibility. Adds --bg-card and --border-soft for upcoming glass card class. EOF )" ``` --- ## Task 2: Add patina tokens to vault.css **Files:** - Modify: `extension/src/vault/vault.css:3-28` - [ ] **Step 1: Apply the same token block to vault.css** In `extension/src/vault/vault.css`, replace lines 3-28 with the same `:root` block from Task 1 Step 2. - [ ] **Step 2: Update `body` background and `.brand` color** Same updates as Task 1 Steps 3-4 but in `vault.css`. - [ ] **Step 3: Build to verify** ```bash cd extension && npm run build 2>&1 | tail -5 ``` - [ ] **Step 4: Commit** ```bash git add extension/src/vault/vault.css git commit -m "$(cat <<'EOF' style(ext/vault): add patina palette tokens Mirrors popup/styles.css token block so the two surfaces share a consistent color vocabulary. EOF )" ``` --- ## Task 3: Add `.surface-backdrop` class to both stylesheets **Files:** - Modify: `extension/src/popup/styles.css` (append) - Modify: `extension/src/vault/vault.css` (append) - [ ] **Step 1: Append `.surface-backdrop` to popup/styles.css** Add at the end of `extension/src/popup/styles.css`: ```css /* Phase 2B: surface backdrop — subtle radial top-glow + grid texture. Apply to body or a top-level wrapper. Children must sit above the ::before. */ .surface-backdrop { position: relative; background: radial-gradient(ellipse 700px 240px at 50% -40px, rgba(184, 149, 86, 0.05), transparent 65%), linear-gradient(180deg, #11161e 0%, #0a0e14 100%); } .surface-backdrop::before { content: ''; position: absolute; inset: 0; background-image: linear-gradient(rgba(255, 255, 255, 0.012) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.012) 1px, transparent 1px); background-size: 18px 18px; pointer-events: none; z-index: 0; } .surface-backdrop > * { position: relative; z-index: 1; } ``` - [ ] **Step 2: Append the same block to vault.css** Append the identical block to `extension/src/vault/vault.css`. - [ ] **Step 3: Commit** ```bash git add extension/src/popup/styles.css extension/src/vault/vault.css git commit -m "$(cat <<'EOF' style(ext): add .surface-backdrop class Subtle radial top-glow + 18px grid texture. Used as the backdrop for the login popup, setup wizard, and fullscreen vault shell. EOF )" ``` --- ## Task 4: Add `.glass` card class to both stylesheets **Files:** - Modify: `extension/src/popup/styles.css` (append) - Modify: `extension/src/vault/vault.css` (append) - [ ] **Step 1: Append `.glass` to both stylesheets** Add at the end of both `extension/src/popup/styles.css` and `extension/src/vault/vault.css`: ```css /* Phase 2B: glass card. Translucent panel with backdrop blur for the unlock card, setup step card, and form section panels. Falls back gracefully on browsers without backdrop-filter (just stays translucent). */ .glass { background: var(--bg-card); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid var(--border-soft); border-radius: 10px; box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 6px 18px rgba(0, 0, 0, 0.35); } ``` - [ ] **Step 2: Commit** ```bash git add extension/src/popup/styles.css extension/src/vault/vault.css git commit -m "$(cat <<'EOF' style(ext): add .glass card class Translucent fill, soft border, inner highlight, drop shadow. Used for the unlock card, setup step cards, and form section panels. EOF )" ``` --- ## Task 5: Add `.btn-primary` / `.btn-secondary` classes **Files:** - Modify: `extension/src/popup/styles.css` (append) - Modify: `extension/src/vault/vault.css` (append) - [ ] **Step 1: Append button hierarchy to both stylesheets** ```css /* Phase 2B: button hierarchy. Existing .btn class kept for backwards compatibility; .btn-primary and .btn-secondary express clearer intent and are used in updated views. */ .btn-primary { background: var(--gold-base); color: var(--bg-page); border: none; padding: 9px 14px; font-size: 12px; font-weight: 600; border-radius: 6px; font-family: inherit; cursor: pointer; letter-spacing: 0.3px; display: inline-flex; align-items: center; gap: 8px; transition: background-color 0.15s; } .btn-primary:hover { background: var(--gold-stroke); } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary:focus-visible { outline: none; box-shadow: var(--focus-ring); } .btn-secondary { background: transparent; border: 1px solid rgba(255, 255, 255, 0.06); color: var(--text-muted); padding: 6px 12px; font-size: 11px; border-radius: 5px; font-family: inherit; cursor: pointer; } .btn-secondary:hover { border-color: rgba(255, 255, 255, 0.12); color: var(--text); } .btn-secondary:focus-visible { outline: none; box-shadow: var(--focus-ring); } ``` - [ ] **Step 2: Commit** ```bash git add extension/src/popup/styles.css extension/src/vault/vault.css git commit -m "$(cat <<'EOF' style(ext): add .btn-primary and .btn-secondary classes Two-tier button hierarchy. .btn-primary uses patina gold fill; .btn-secondary is a ghost button with muted border. Existing .btn class kept for backwards compatibility. EOF )" ``` --- ## Task 6: Add `GLYPH_NEXT` and apply to existing arrow uses **Files:** - Modify: `extension/src/shared/glyphs.ts` - Modify: `extension/src/popup/components/settings-vault.ts:164,171` - Test: `extension/src/shared/__tests__/glyphs.test.ts` (create or extend) - [ ] **Step 1: Add `GLYPH_NEXT` constant** In `extension/src/shared/glyphs.ts`, add after `GLYPH_LOCK`: ```typescript export const GLYPH_NEXT = '▸'; // forward / next button (matches ▾/▸ disclosure family) ``` - [ ] **Step 2: Write a snapshot test for the constants** Check whether `extension/src/shared/__tests__/glyphs.test.ts` exists. If not, create it: ```typescript import { describe, it, expect } from 'vitest'; import { GLYPH_REVEAL, GLYPH_HIDE, GLYPH_GENERATE, GLYPH_FILL_FROM_TAB, GLYPH_QR, GLYPH_MONO, GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_NEXT, } from '../glyphs'; describe('glyph constants', () => { it('uses single unicode codepoints (no emoji multi-codepoint)', () => { const all = [ GLYPH_REVEAL, GLYPH_HIDE, GLYPH_GENERATE, GLYPH_FILL_FROM_TAB, GLYPH_QR, GLYPH_MONO, GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_NEXT, ]; for (const g of all) { expect([...g].length).toBe(1); } }); it('GLYPH_NEXT is the small right triangle (U+25B8)', () => { expect(GLYPH_NEXT).toBe('▸'); }); }); ``` - [ ] **Step 3: Run the test** ```bash cd extension && npx vitest run src/shared/__tests__/glyphs.test.ts ``` Expected: PASS. - [ ] **Step 4: Replace `→` in settings-vault.ts** In `extension/src/popup/components/settings-vault.ts`, change: ```typescript // Line 164: // becomes: // Line 171: // becomes: ``` Add the import at the top of the file: ```typescript import { GLYPH_NEXT } from '../../shared/glyphs'; ``` - [ ] **Step 5: Run vitest to confirm nothing broke** ```bash cd extension && npx vitest run ``` Expected: all existing tests pass. - [ ] **Step 6: Commit** ```bash git add extension/src/shared/glyphs.ts extension/src/shared/__tests__/glyphs.test.ts extension/src/popup/components/settings-vault.ts git commit -m "$(cat <<'EOF' feat(ext): add GLYPH_NEXT and replace ASCII arrows with ▸ Replaces the ASCII rightwards arrow → with U+25B8 ▸ in settings-vault buttons. Matches the existing ▾/▸ disclosure-glyph family. EOF )" ``` --- ## Task 7: Restructure unlock view with logo lockup, glass card, primary button **Files:** - Modify: `extension/src/popup/components/unlock.ts` - Modify: `extension/src/popup/index.html` (body wrapper class) - Test: `extension/src/popup/components/__tests__/unlock.test.ts` (create) - [ ] **Step 1: Apply `.surface-backdrop` to popup body** In `extension/src/popup/index.html`, change the `` tag to: ```html ``` - [ ] **Step 2: Write the unlock view structure test** Create `extension/src/popup/components/__tests__/unlock.test.ts`: ```typescript import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderUnlock } from '../unlock'; vi.mock('../../../shared/state', () => ({ getState: () => ({ loading: false, error: null }), setState: vi.fn(), sendMessage: vi.fn(), navigate: vi.fn(), escapeHtml: (s: string) => s, openVaultTab: vi.fn(), })); describe('renderUnlock', () => { let app: HTMLElement; beforeEach(() => { document.body.innerHTML = '
'; app = document.getElementById('app')!; }); it('renders the logo lockup (logo + brand + tagline)', () => { renderUnlock(app); expect(app.querySelector('.brand-logo')).toBeTruthy(); expect(app.querySelector('.brand')?.textContent).toBe('Relicario'); expect(app.querySelector('.tagline')?.textContent).toContain('two-factor'); }); it('renders the unlock form inside a .glass card', () => { renderUnlock(app); const glass = app.querySelector('.glass'); expect(glass).toBeTruthy(); expect(glass!.querySelector('#passphrase-input')).toBeTruthy(); expect(glass!.querySelector('.btn-primary')).toBeTruthy(); }); it('renders open-vault and settings as secondary buttons outside the card', () => { renderUnlock(app); const vaultBtn = app.querySelector('#vault-btn'); const settingsBtn = app.querySelector('#settings-btn'); expect(vaultBtn?.classList.contains('btn-secondary')).toBe(true); expect(settingsBtn?.classList.contains('btn-secondary')).toBe(true); // They should NOT be inside the .glass card const glass = app.querySelector('.glass'); expect(glass!.contains(vaultBtn!)).toBe(false); }); }); ``` - [ ] **Step 3: Run tests to verify they fail** ```bash cd extension && npx vitest run src/popup/components/__tests__/unlock.test.ts ``` Expected: FAIL (current unlock view doesn't have these classes / structure). - [ ] **Step 4: Rewrite renderUnlock** Replace the entire `renderUnlock` function in `extension/src/popup/components/unlock.ts`: ```typescript /// Unlock view — passphrase input with ENTER to submit. import { getState, setState, sendMessage, navigate, escapeHtml, openVaultTab } from '../../shared/state'; import type { ItemId, ManifestEntry } from '../../shared/types'; export function renderUnlock(app: HTMLElement): void { const state = getState(); app.innerHTML = `
Relicario

two-factor vault

unlock
${state.loading ? '
' : ''} ${state.error ? `
${escapeHtml(state.error)}
` : ''}
`; const input = document.getElementById('passphrase-input') as HTMLInputElement; const unlockBtn = document.getElementById('unlock-btn') as HTMLButtonElement | null; const submit = async () => { const passphrase = input.value; if (!passphrase) return; setState({ loading: true, error: null }); const resp = await sendMessage({ type: 'unlock', passphrase }); if (resp.ok) { const listResp = await sendMessage({ type: 'list_items' }); if (listResp.ok) { const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> }; navigate('list', { entries: data.items }); } else { setState({ loading: false, error: listResp.error }); } } else { setState({ loading: false, error: resp.error }); } }; if (input && !state.loading) { input.focus(); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); }); } unlockBtn?.addEventListener('click', submit); document.getElementById('vault-btn')?.addEventListener('click', () => openVaultTab()); document.getElementById('settings-btn')?.addEventListener('click', () => navigate('settings')); } ``` - [ ] **Step 5: Add `.tagline` and `.logo-lockup` CSS to popup/styles.css** Append to `extension/src/popup/styles.css`: ```css .logo-lockup .brand-logo { width: 42px; height: 42px; margin: 0 auto 10px; } .logo-lockup .brand { font-size: 17px; font-weight: 600; color: var(--gold-text); letter-spacing: 0.5px; } .tagline { color: var(--text-dim); font-size: 11px; margin-top: 4px; letter-spacing: 0.3px; } ``` - [ ] **Step 6: Run tests to verify they pass** ```bash cd extension && npx vitest run src/popup/components/__tests__/unlock.test.ts ``` Expected: PASS. - [ ] **Step 7: Commit** ```bash git add extension/src/popup/index.html extension/src/popup/components/unlock.ts extension/src/popup/components/__tests__/unlock.test.ts extension/src/popup/styles.css git commit -m "$(cat <<'EOF' feat(ext/popup): polish unlock view with logo lockup + glass card Restructures the unlock screen so the form sits in a glass card with a primary 'unlock vault' button. Logo, brand, and tagline are grouped as a lockup. Open-vault and settings are demoted to secondary buttons. Body gets the .surface-backdrop wrapper. EOF )" ``` --- ## Task 8: Apply backdrop + glass cards to setup wizard **Files:** - Modify: `extension/src/setup/setup.ts` - [ ] **Step 1: Find the body wrapper / outer container in setup.ts** The `app.innerHTML = ...` block around line 191-199 wraps content in a `.pad` div. That's where we'll apply the backdrop. - [ ] **Step 2: Wrap setup content in `.surface-backdrop`** In `extension/src/setup/setup.ts`, locate the `render()` function. Change the outer wrapper from: ```typescript app.innerHTML = `
...
`; ``` to: ```typescript app.innerHTML = `
...
`; ``` - [ ] **Step 3: Wrap each step body in a `.glass` card** Each `renderStepN()` function returns a string starting with `
...`. Update each to: ```typescript return `

...

...
`; ``` Apply this to `renderStep0`, `renderStep1`, `renderStep2`, `renderStep3New`, `renderStep3Attach`, `renderStep4`, `renderStep5`. The `wizard-step glass` combination preserves any existing `.wizard-step` rules while adding the glass treatment. - [ ] **Step 4: Update mode-card style to use glass class** In `renderStep0`, the mode cards use ` // Line 445: // Line 546: // Line 921 (continue button — also gets the glyph): ``` For `attach-btn` (~line 301) and `create-btn` (~line 696), keep them as `btn-primary` but no arrow glyph (those are commit-action buttons, not "next"-style). - [ ] **Step 6: Build and visually verify in Chrome** ```bash cd extension && npm run build 2>&1 | tail -3 ``` Open the extension's setup page (or load the dist into Chrome) to confirm rendering. This is a manual visual check — there isn't an existing test harness for setup.ts views. - [ ] **Step 7: Run vitest to confirm nothing regressed** ```bash cd extension && npx vitest run ``` Expected: all existing tests pass. - [ ] **Step 8: Commit** ```bash git add extension/src/setup/setup.ts git commit -m "$(cat <<'EOF' feat(ext/setup): apply polish vocabulary to setup wizard - Wraps setup content in .surface-backdrop - Each wizard step gets a .glass card - Mode-picker cards become glass cards - 'next' / 'continue' buttons get the ▸ glyph - Migrate from .btn .btn-primary to the new .btn-primary class EOF )" ``` --- ## Task 9: Apply backdrop to fullscreen vault shell **Files:** - Modify: `extension/src/vault/vault.html` - Modify: `extension/src/vault/vault.ts` - [ ] **Step 1: Apply `.surface-backdrop` to body in vault.html** In `extension/src/vault/vault.html`, change `` to: ```html ``` - [ ] **Step 2: Build and check the existing layout still works** ```bash cd extension && npm run build 2>&1 | tail -3 ``` The existing vault layout has its own panes / sidebar; the backdrop sits behind everything via the `::before` pseudo-element. - [ ] **Step 3: Commit** ```bash git add extension/src/vault/vault.html git commit -m "$(cat <<'EOF' style(ext/vault): apply .surface-backdrop to fullscreen body Subtle radial top-glow + grid texture behind the existing vault shell. No layout changes — existing panes sit above the backdrop's ::before. EOF )" ``` --- ## Task 10: Add `surface` flag to renderForm and per-surface column wrapping **Files:** - Modify: `extension/src/popup/components/types/login.ts:238` - Modify: `extension/src/popup/popup.ts` (callers of renderForm) - Modify: `extension/src/vault/vault.ts` (callers of renderForm) - Test: `extension/src/popup/components/types/__tests__/login.test.ts` - [ ] **Step 1: Read the current renderForm signature** ```bash grep -n "renderForm" extension/src/popup/components/types/login.ts | head -10 ``` The current signature at line 238 is: ```typescript export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null): void ``` - [ ] **Step 2: Write the surface-flag test** In `extension/src/popup/components/types/__tests__/login.test.ts`, add a test: ```typescript describe('renderForm surface flag', () => { let app: HTMLElement; beforeEach(() => { document.body.innerHTML = '
'; app = document.getElementById('app')!; // ... existing test setup mocks should already be in place above }); it('renders single-column when surface is "popup" (default)', () => { renderForm(app, 'add', null); expect(app.querySelector('.form-grid')).toBeNull(); }); it('renders two-column .form-grid wrapper when surface is "fullscreen"', () => { renderForm(app, 'add', null, { surface: 'fullscreen' }); const grid = app.querySelector('.form-grid'); expect(grid).toBeTruthy(); expect(grid!.querySelector('[data-form-section="identity"]')).toBeTruthy(); expect(grid!.querySelector('[data-form-section="credentials"]')).toBeTruthy(); }); }); ``` - [ ] **Step 3: Run test to verify it fails** ```bash cd extension && npx vitest run src/popup/components/types/__tests__/login.test.ts -t "surface flag" ``` Expected: FAIL — type error on `{ surface }` arg. - [ ] **Step 4: Update renderForm signature with optional surface flag** In `extension/src/popup/components/types/login.ts:238`, change the signature: ```typescript export interface RenderFormOptions { surface?: 'popup' | 'fullscreen'; } export function renderForm( app: HTMLElement, mode: 'add' | 'edit', existing: Item | null, opts: RenderFormOptions = {} ): void { const surface = opts.surface ?? 'popup'; // ... existing function body, with section wrapping below ``` - [ ] **Step 5: Wrap Identity / Credentials sections** Inside `renderForm`, locate the existing form-fields render block (search for the `` / `` blocks). The current implementation builds the form HTML by concatenating field strings or calling `renderRow(...)` per field. Identify the title / url / group fields (Identity) and username / password / totp fields (Credentials), then wrap them in column containers that only become a grid in fullscreen. Below, `<>` etc. are placeholders for whatever the existing code generates for that field — leave that code unchanged, just relocate it inside the new wrappers: ```typescript const identityHtml = ` <div data-form-section="identity" class="${surface === 'fullscreen' ? 'glass form-col' : ''}"> ${surface === 'fullscreen' ? '<div class="col-header">Identity</div>' : ''} <<title field render>> <<url field render>> <<group field render>> </div> `; const credentialsHtml = ` <div data-form-section="credentials" class="${surface === 'fullscreen' ? 'glass form-col' : ''}"> ${surface === 'fullscreen' ? '<div class="col-header">Credentials</div>' : ''} <<username field render>> <<password field render>> <<totp field render>> </div> `; const sectionsHtml = surface === 'fullscreen' ? `<div class="form-grid">${identityHtml}${credentialsHtml}</div>` : `${identityHtml}${credentialsHtml}`; ``` The notes / custom-sections / attachments blocks must remain *outside* the grid — they should sit below the column wrapper as full-width siblings, regardless of surface. - [ ] **Step 6: Add `.form-grid`, `.form-col`, `.col-header` to vault.css** Append to `extension/src/vault/vault.css`: ```css .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; max-width: 960px; margin: 0 auto; } @media (max-width: 720px) { .form-grid { grid-template-columns: 1fr; } } .form-col { padding: 14px 16px; } .col-header { text-transform: uppercase; letter-spacing: 1.2px; font-weight: 500; color: var(--text-muted); font-size: 10px; border-bottom: 1px solid var(--border-mid); padding-bottom: 6px; margin-bottom: 12px; } ``` - [ ] **Step 7: Update the vault.ts caller to pass `surface: 'fullscreen'`** ```bash grep -n "renderForm" extension/src/vault/vault.ts ``` For each call site in vault.ts, add the surface flag: ```typescript renderForm(app, mode, existing, { surface: 'fullscreen' }); ``` The popup.ts callers stay unchanged (default `'popup'`). - [ ] **Step 8: Run tests** ```bash cd extension && npx vitest run src/popup/components/types/__tests__/login.test.ts ``` Expected: PASS. - [ ] **Step 9: Commit** ```bash git add extension/src/popup/components/types/login.ts extension/src/popup/components/types/__tests__/login.test.ts extension/src/vault/vault.ts extension/src/vault/vault.css git commit -m "$(cat <<'EOF' feat(ext/login): add surface flag for two-column fullscreen form renderForm() takes an optional { surface: 'popup' | 'fullscreen' } parameter. When 'fullscreen', the Identity and Credentials field groups render as glass cards inside a .form-grid (two columns, stacks at <=720px). Popup keeps its single-column layout. EOF )" ``` --- ## Task 11: Add sticky save bar in fullscreen forms **Files:** - Modify: `extension/src/vault/vault.css` (append) - Modify: `extension/src/vault/vault.ts` (form rendering wrapper) - [ ] **Step 1: Append sticky save bar CSS to vault.css** ```css .form-pane { display: flex; flex-direction: column; height: 100%; overflow: hidden; } .form-scroll { flex: 1; overflow-y: auto; padding: 20px 24px; } .sticky-save-bar { position: sticky; bottom: 0; background: rgba(17, 22, 30, 0.7); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); border-top: 1px solid var(--border-mid); padding: 12px 24px; display: flex; justify-content: flex-end; gap: 8px; z-index: 10; } .sticky-save-bar::before { content: ''; position: absolute; top: -24px; left: 0; right: 0; height: 24px; background: linear-gradient(to top, rgba(17, 22, 30, 0.7), transparent); pointer-events: none; } ``` - [ ] **Step 2: Wrap fullscreen form rendering with `.form-pane` + `.sticky-save-bar`** In `extension/src/vault/vault.ts`, wherever the form is rendered (look for the call to `renderForm`), wrap with the pane structure: ```typescript function renderFormWrapped(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null) { const wrapper = document.createElement('div'); wrapper.className = 'form-pane'; wrapper.innerHTML = ` <div class="form-scroll" id="form-scroll"></div> <div class="sticky-save-bar"> <button class="btn-secondary" id="form-cancel">cancel</button> <button class="btn-primary" id="form-save">save</button> </div> `; app.replaceChildren(wrapper); const scrollEl = wrapper.querySelector('#form-scroll') as HTMLElement; renderForm(scrollEl, mode, existing, { surface: 'fullscreen' }); wrapper.querySelector('#form-cancel')?.addEventListener('click', () => { // dispatch existing cancel handler — wire up to existing flow document.getElementById('form-cancel-existing')?.click(); }); wrapper.querySelector('#form-save')?.addEventListener('click', () => { document.getElementById('form-save-existing')?.click(); }); } ``` The exact wiring depends on how the existing form save/cancel buttons are structured. The save bar buttons should trigger the same handlers — easiest path is to make the existing form's save/cancel buttons hidden when `surface === 'fullscreen'` and let the sticky bar trigger them. A cleaner alternative: have `renderForm` accept a flag to skip rendering its own action buttons when the wrapper is providing them. Add to `RenderFormOptions`: ```typescript export interface RenderFormOptions { surface?: 'popup' | 'fullscreen'; /** When true, renderForm skips its own save/cancel buttons (caller provides them in a sticky bar). */ externalActions?: boolean; } ``` And inside renderForm, gate the existing save/cancel button render on `!opts.externalActions`. Then the sticky bar buttons can call the existing save/cancel functions directly (export them from `login.ts` if necessary). - [ ] **Step 3: Build to verify CSS / TS compile** ```bash cd extension && npm run build 2>&1 | tail -3 ``` - [ ] **Step 4: Commit** ```bash git add extension/src/vault/vault.css extension/src/vault/vault.ts extension/src/popup/components/types/login.ts git commit -m "$(cat <<'EOF' feat(ext/vault): sticky save bar in fullscreen forms The form pane gets a flex column layout: scrollable content above, sticky save bar at bottom. Bar uses translucent fill with backdrop-blur and a 24px gradient fade so content scrolls under it. Save / cancel buttons reuse the form's existing handlers via externalActions flag. EOF )" ``` --- ## Task 12: Header treatment with dirty-state subtitle **Files:** - Modify: `extension/src/vault/vault.css` (append) - Modify: `extension/src/vault/vault.ts` - [ ] **Step 1: Append form header styles to vault.css** ```css .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); } ``` - [ ] **Step 2: Detect platform for keyboard hint label** Add a small helper near the top of `vault.ts`: ```typescript const isMac = navigator.platform.toLowerCase().includes('mac'); const SAVE_HINT = isMac ? '⌘+S to save' : 'Ctrl+S to save'; ``` - [ ] **Step 3: Render the header above `.form-pane`** Update `renderFormWrapped` from Task 11 to include the header: ```typescript function renderFormWrapped(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null) { const titleText = mode === 'add' ? 'new login' : 'edit login'; const wrapper = document.createElement('div'); wrapper.className = 'form-pane'; wrapper.innerHTML = ` <div class="fullscreen-form-header"> <div> <div class="title">${titleText}</div> <div class="sub" id="form-dirty-sub">no changes</div> </div> <div class="hint">${SAVE_HINT}</div> </div> <div class="form-scroll" id="form-scroll"></div> <div class="sticky-save-bar"> <button class="btn-secondary" id="form-cancel">cancel</button> <button class="btn-primary" id="form-save">save</button> </div> `; app.replaceChildren(wrapper); // ... rest of wiring from Task 11 } ``` - [ ] **Step 4: Wire dirty-state subscription** Add a small dirty-tracker that listens for `input`/`change` events on the form scroll element. When any input fires, set the subtitle to "unsaved · esc to cancel"; on save/cancel, reset to "no changes": ```typescript const scrollEl = wrapper.querySelector('#form-scroll') as HTMLElement; const subEl = wrapper.querySelector('#form-dirty-sub') as HTMLElement; let isDirty = false; const markDirty = () => { if (isDirty) return; isDirty = true; subEl.textContent = 'unsaved · esc to cancel'; }; const markClean = () => { isDirty = false; subEl.textContent = 'no changes'; }; scrollEl.addEventListener('input', markDirty, true); scrollEl.addEventListener('change', markDirty, true); wrapper.querySelector('#form-cancel')?.addEventListener('click', () => { markClean(); // ... existing cancel logic }); wrapper.querySelector('#form-save')?.addEventListener('click', () => { markClean(); // ... existing save logic }); ``` - [ ] **Step 5: Write a happy-dom test for the dirty subtitle** In `extension/src/vault/__tests__/`, create or extend a test file: ```typescript import { describe, it, expect, beforeEach } from 'vitest'; // Pseudo-test — the actual test mounts renderFormWrapped with real wiring. // If renderFormWrapped is unexported, export it from vault.ts for testing. describe('fullscreen form dirty subtitle', () => { let app: HTMLElement; beforeEach(() => { document.body.innerHTML = '<div id="app"></div>'; app = document.getElementById('app')!; }); it('starts pristine and switches to dirty on first input', () => { // Mount renderFormWrapped(app, 'add', null); // const sub = app.querySelector('#form-dirty-sub')!; // expect(sub.textContent).toBe('no changes'); // const titleInput = app.querySelector('input[type=text]') as HTMLInputElement; // titleInput.value = 'x'; // titleInput.dispatchEvent(new Event('input', { bubbles: true })); // expect(sub.textContent).toContain('unsaved'); }); }); ``` If `renderFormWrapped` is internal, expose it via `__test__` export gated on test env: ```typescript // vault.ts export const __test__ = { renderFormWrapped }; ``` - [ ] **Step 6: Build to verify** ```bash cd extension && npm run build 2>&1 | tail -3 ``` - [ ] **Step 7: Commit** ```bash git add extension/src/vault/vault.css extension/src/vault/vault.ts extension/src/vault/__tests__/ git commit -m "$(cat <<'EOF' feat(ext/vault): fullscreen form header with dirty-state subtitle 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. EOF )" ``` --- ## Task 13: Final verification - [ ] **Step 1: Run full test suite** ```bash cd extension && npx vitest run ``` Expected: all tests pass. If any test fails, fix and recommit before proceeding. - [ ] **Step 2: Build for production (Chrome + Firefox)** ```bash cd extension && npm run build:all ``` Expected: webpack compiles both targets with no errors (only the existing 4MB WASM warning). - [ ] **Step 3: Verify rebuilt extension manifest** ```bash grep '"name"' extension/dist/manifest.json extension/dist-firefox/manifest.json ``` Expected: both show `"name": "Relicario"`. - [ ] **Step 4: Manual smoke test (load unpacked dist into Chrome)** Open `chrome://extensions`, enable Developer Mode, "Load unpacked" → `extension/dist/`. Verify: - Click the extension icon: popup opens, shows logo lockup, glass card with passphrase input, primary "unlock vault" button, secondary buttons below. - Open the setup wizard (e.g., via `extension/dist/setup.html`): each step renders inside a glass card, "next" buttons show `▸` glyph. - Open the fullscreen vault (extension menu → "Open Relicario vault"): backdrop visible behind the existing pane layout. - Add a new login item: form renders two-column with Identity and Credentials cards, sticky save bar at bottom, header subtitle changes from "no changes" to "unsaved · esc to cancel" on first input. - [ ] **Step 5: Commit verification log if anything was missed** If the smoke test reveals follow-up tweaks, fix them and commit with message `style(ext): polish smoke-test fixes`. Otherwise, no commit. --- ## Completion Checklist - [ ] Task 1: Patina tokens in popup/styles.css - [ ] Task 2: Patina tokens in vault.css - [ ] Task 3: `.surface-backdrop` class - [ ] Task 4: `.glass` card class - [ ] Task 5: `.btn-primary` / `.btn-secondary` - [ ] Task 6: `GLYPH_NEXT` + arrow replacements - [ ] Task 7: Unlock view restructure - [ ] Task 8: Setup wizard polish - [ ] Task 9: Vault shell backdrop - [ ] Task 10: Surface flag + two-column form - [ ] Task 11: Sticky save bar - [ ] Task 12: Header dirty subtitle - [ ] Task 13: Final verification