diff --git a/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md b/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md new file mode 100644 index 0000000..addd42c --- /dev/null +++ b/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md @@ -0,0 +1,948 @@ +# Fullscreen UX redesign — Phase 1: Visual Foundation + +> **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:** Establish the shared visual language (glyph constants, color tokens, focus ring, required pill, header subtitle) and clean up vestigial popup-only UI in the fullscreen vault. No structural or behavioral changes; pure visual foundation that the next three phases will build on. + +**Architecture:** A new `extension/src/shared/glyphs.ts` module exports unicode glyph constants and a `REQUIRED_PILL` HTML snippet, consumed by both popup and fullscreen surfaces. CSS custom properties added to `popup/styles.css` and `vault/vault.css` provide the shared color/focus tokens. All eight type forms migrate from `*` to the pill; sidebar nav buttons replace emoji with glyph constants; the popout-to-tab button is gated behind `!isInTab()` so it disappears in fullscreen. Fullscreen forms gain a static "esc to cancel" subtitle (dynamic dirty-state lands in Phase 3). + +**Tech stack:** TypeScript, vanilla DOM (no framework), Vitest + happy-dom for unit tests. No new runtime dependencies. + +**Spec:** [`docs/superpowers/specs/2026-04-30-relicario-fullscreen-ux-redesign-design.md`](../specs/2026-04-30-relicario-fullscreen-ux-redesign-design.md) + +--- + +## Task 1: shared/glyphs.ts module + snapshot test + +**Files:** +- Create: `extension/src/shared/glyphs.ts` +- Create: `extension/src/shared/__tests__/glyphs.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +// extension/src/shared/__tests__/glyphs.test.ts +import { describe, it, expect } from 'vitest'; +import * as glyphs from '../glyphs'; + +describe('glyphs', () => { + it('exports the documented glyph constants', () => { + expect(glyphs.GLYPH_REVEAL).toBe('⊙'); + expect(glyphs.GLYPH_HIDE).toBe('⊘'); + expect(glyphs.GLYPH_GENERATE).toBe('↻'); + expect(glyphs.GLYPH_FILL_FROM_TAB).toBe('⤓'); + expect(glyphs.GLYPH_QR).toBe('◫'); + expect(glyphs.GLYPH_MONO).toBe('≡'); + expect(glyphs.GLYPH_TRASH).toBe('▦'); + expect(glyphs.GLYPH_DEVICES).toBe('⌬'); + expect(glyphs.GLYPH_SETTINGS).toBe('⚙'); + expect(glyphs.GLYPH_LOCK).toBe('⏻'); + }); + + it('exports REQUIRED_PILL as an HTML snippet', () => { + expect(glyphs.REQUIRED_PILL).toBe('required'); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd extension && ./node_modules/.bin/vitest run src/shared/__tests__/glyphs.test.ts` +Expected: FAIL with module-not-found / unresolved-import error. + +- [ ] **Step 3: Create the glyphs module** + +```typescript +// extension/src/shared/glyphs.ts +// +// Unicode glyph constants used across popup and fullscreen surfaces. All +// glyphs are monochrome unicode (no emoji) so they render identically in the +// codebase's monospace font. Pair each button glyph with a `title=` tooltip +// at the call site for accessibility — the constants here are the visual, +// not the affordance. + +export const GLYPH_REVEAL = '⊙'; // password reveal toggle (hidden state) +export const GLYPH_HIDE = '⊘'; // password reveal toggle (revealed state) +export const GLYPH_GENERATE = '↻'; // password / passphrase generate +export const GLYPH_FILL_FROM_TAB = '⤓'; // pull URL from active browser tab +export const GLYPH_QR = '◫'; // paste / upload QR image (TOTP) +export const GLYPH_MONO = '≡'; // toggle notes monospace font + +export const GLYPH_TRASH = '▦'; // sidebar trash nav +export const GLYPH_DEVICES = '⌬'; // sidebar devices nav +export const GLYPH_SETTINGS = '⚙'; // sidebar settings nav +export const GLYPH_LOCK = '⏻'; // sidebar lock nav + +/// Inline HTML snippet for the required-field pill. Use after a label's text: +/// `` +export const REQUIRED_PILL = 'required'; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd extension && ./node_modules/.bin/vitest run src/shared/__tests__/glyphs.test.ts` +Expected: PASS, 2/2 tests green. + +- [ ] **Step 5: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/shared/glyphs.ts extension/src/shared/__tests__/glyphs.test.ts +git -C /home/alee/Sources/relicario commit -m "feat(ext/shared): glyph constants module for unified icon language + +Centralizes the unicode glyphs used by sidebar nav and form action buttons +so popup and fullscreen surfaces stay in sync. Includes the REQUIRED_PILL +snippet used to replace the trailing-asterisk required-field marker. + +Plan 2026-04-30 fullscreen UX phase 1 task 1. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Task 2: Color tokens + focus ring (popup styles.css) + +**Files:** +- Modify: `extension/src/popup/styles.css:1-150` + +- [ ] **Step 1: Add color tokens at the top of the file** + +Open `extension/src/popup/styles.css` and add a `:root` block immediately after the leading comment (before the `*` reset on line 3): + +```css +/* relicario extension — terminal dark theme */ + +:root { + /* Brand */ + --accent: #d2ab43; + --accent-soft: rgba(210, 171, 67, 0.18); + --accent-strong: #aa812a; + + /* Surfaces */ + --bg-page: #0d1117; + --bg-pane: #161b22; + --bg-elevated: #21262d; + --border-subtle: #30363d; + + /* Text */ + --text: #c9d1d9; + --text-muted: #8b949e; + --text-dim: #484f58; + + /* Status */ + --danger: #ab2b20; + --danger-bg: #791111; + --success: #6cb37a; + + /* Focus */ + --focus-ring: 0 0 0 2px rgba(210, 171, 67, 0.35); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} +``` + +- [ ] **Step 2: Update input focus to use the ring token** + +Find the existing input focus rule (around line 136) and replace it: + +Before: +```css +input:focus, textarea:focus, select:focus { + border-color: #d2ab43; +} +``` + +After: +```css +input:focus-visible, textarea:focus-visible, select:focus-visible { + border-color: var(--accent); + box-shadow: var(--focus-ring); + outline: none; +} +``` + +- [ ] **Step 3: Update button focus to match** + +Find the `.btn:focus` rule (around line 97) and replace: + +Before: +```css +.btn:focus { + outline: 1px solid #d2ab43; + outline-offset: 1px; +} +``` + +After: +```css +.btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} +``` + +- [ ] **Step 4: Add the required-field pill style** + +Find the `.label .req` rule (around line 58) and add the pill rule immediately after it: + +```css +.label .req { + color: var(--accent-strong); + margin-left: 2px; + font-weight: 600; +} + +.req-pill { + display: inline-block; + font-size: 9px; + padding: 1px 5px; + background: var(--accent-soft); + color: var(--accent); + border-radius: 2px; + margin-left: 6px; + vertical-align: middle; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; +} +``` + +- [ ] **Step 5: Build the popup to verify CSS parses** + +Run: `cd /home/alee/Sources/relicario/extension && ./node_modules/.bin/webpack --mode production 2>&1 | tail -5` +Expected: `webpack ... compiled with 2 warnings` (the existing wasm size warnings; no CSS errors). + +- [ ] **Step 6: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/popup/styles.css +git -C /home/alee/Sources/relicario commit -m "style(ext/popup): add color tokens, focus ring, required-pill class + +Establishes :root CSS custom properties (accent, surfaces, status, focus +ring) and applies the focus ring to inputs/buttons via :focus-visible. +Adds .req-pill class used by Task 4 to replace the bare-asterisk required +marker. Existing .label .req kept for backward compatibility during the +migration window. + +Plan 2026-04-30 fullscreen UX phase 1 task 2. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Task 3: Color tokens + focus ring (vault.css) + +**Files:** +- Modify: `extension/src/vault/vault.css` + +- [ ] **Step 1: Add the same `:root` block to vault.css** + +Open `extension/src/vault/vault.css` and add the same `:root` block at the top (above any existing content). Use the **identical** token block from Task 2 Step 1 so the two stylesheets stay in sync: + +```css +:root { + /* Brand */ + --accent: #d2ab43; + --accent-soft: rgba(210, 171, 67, 0.18); + --accent-strong: #aa812a; + + /* Surfaces */ + --bg-page: #0d1117; + --bg-pane: #161b22; + --bg-elevated: #21262d; + --border-subtle: #30363d; + + /* Text */ + --text: #c9d1d9; + --text-muted: #8b949e; + --text-dim: #484f58; + + /* Status */ + --danger: #ab2b20; + --danger-bg: #791111; + --success: #6cb37a; + + /* Focus */ + --focus-ring: 0 0 0 2px rgba(210, 171, 67, 0.35); +} +``` + +- [ ] **Step 2: Find existing input focus rule and migrate it** + +Run: `grep -n "input:focus\|textarea:focus\|:focus" extension/src/vault/vault.css | head -10` + +For each focus rule that sets `border-color: #d2ab43` (or similar accent-color border), update it to use `:focus-visible` and add the ring: + +```css +input:focus-visible, textarea:focus-visible, select:focus-visible { + border-color: var(--accent); + box-shadow: var(--focus-ring); + outline: none; +} +``` + +(If no equivalent rule exists in vault.css today, add the rule above; vault inputs currently inherit popup styles or have their own — check what `grep` returns.) + +- [ ] **Step 3: Add the .req-pill rule** + +Append to vault.css (anywhere; group near `.label` if present): + +```css +.req-pill { + display: inline-block; + font-size: 9px; + padding: 1px 5px; + background: var(--accent-soft); + color: var(--accent); + border-radius: 2px; + margin-left: 6px; + vertical-align: middle; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; +} +``` + +- [ ] **Step 4: Build to verify** + +Run: `cd /home/alee/Sources/relicario/extension && ./node_modules/.bin/webpack --mode production 2>&1 | tail -5` +Expected: `webpack ... compiled with 2 warnings`. + +- [ ] **Step 5: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/vault/vault.css +git -C /home/alee/Sources/relicario commit -m "style(ext/vault): mirror color tokens, focus ring, required-pill class + +Same :root block and .req-pill rule as popup/styles.css so the two +stylesheets share visual tokens. Vault input focus migrated to +:focus-visible + box-shadow ring. + +Plan 2026-04-30 fullscreen UX phase 1 task 3. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Task 4: Migrate required-marker sites to REQUIRED_PILL + +**Files (10 sites across 7 files):** +- Modify: `extension/src/popup/components/types/card.ts:182` +- Modify: `extension/src/popup/components/types/document.ts:94, 98` +- Modify: `extension/src/popup/components/types/identity.ts:142` +- Modify: `extension/src/popup/components/types/key.ts:131, 133` +- Modify: `extension/src/popup/components/types/login.ts:252` +- Modify: `extension/src/popup/components/types/secure-note.ts:120` +- Modify: `extension/src/popup/components/types/totp.ts:221, 230` + +- [ ] **Step 1: Create a regression test for the login form's title label** + +Create `extension/src/popup/components/types/__tests__/required-pill.test.ts`: + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../../../../shared/state', () => ({ + sendMessage: vi.fn(), + getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }), + setState: vi.fn(), + navigate: vi.fn(), + escapeHtml: (s: string) => s, + popOutToTab: vi.fn(), + isInTab: () => false, + openVaultTab: vi.fn(), + registerHost: vi.fn(), +})); + +vi.mock('../../generator-panel', () => ({ + openGeneratorPanel: vi.fn(), + closeGeneratorPanel: vi.fn(), + isGeneratorPanelOpen: () => false, +})); + +import { renderForm } from '../login'; + +describe('required-pill migration', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it('login form title uses the required pill', () => { + renderForm(document.getElementById('app')!, 'add', null); + const titleLabel = document.querySelector('label[for="f-title"]'); + expect(titleLabel?.innerHTML).toContain('required'); + expect(titleLabel?.innerHTML).not.toContain('*'); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cd extension && ./node_modules/.bin/vitest run src/popup/components/types/__tests__/required-pill.test.ts` +Expected: FAIL — `*` is currently present, `required` text is not. + +- [ ] **Step 3: Migrate `login.ts`** + +In `extension/src/popup/components/types/login.ts`: + +Add an import near the top (after the existing imports): + +```typescript +import { REQUIRED_PILL } from '../../../shared/glyphs'; +``` + +Find line 252: +```typescript +
+``` + +Replace with: +```typescript +
+``` + +- [ ] **Step 4: Run the test to verify it passes for login** + +Run: `cd extension && ./node_modules/.bin/vitest run src/popup/components/types/__tests__/required-pill.test.ts` +Expected: PASS. + +- [ ] **Step 5: Migrate the remaining six files** + +Apply the same pattern to each of these six files. For each: +1. Add `import { REQUIRED_PILL } from '../../../shared/glyphs';` +2. Replace each `*` with `${REQUIRED_PILL}` + +| File | Line(s) | +|---|---| +| `extension/src/popup/components/types/card.ts` | 182 | +| `extension/src/popup/components/types/document.ts` | 94, 98 | +| `extension/src/popup/components/types/identity.ts` | 142 | +| `extension/src/popup/components/types/key.ts` | 131, 133 | +| `extension/src/popup/components/types/secure-note.ts` | 120 | +| `extension/src/popup/components/types/totp.ts` | 221, 230 | + +After editing each file, verify no remaining `*` strings exist: + +Run: `grep -rn 'class="req"' extension/src --include="*.ts" 2>/dev/null` +Expected: empty output. + +- [ ] **Step 6: Run the full extension test suite** + +Run: `cd extension && ./node_modules/.bin/vitest run` +Expected: all 220+ tests pass (the new test brings it to 221+; no regressions). + +- [ ] **Step 7: Build to verify TypeScript compiles** + +Run: `cd /home/alee/Sources/relicario/extension && ./node_modules/.bin/webpack --mode production 2>&1 | tail -5` +Expected: `compiled with 2 warnings`. + +- [ ] **Step 8: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/popup/components/types/ extension/src/shared/ +git -C /home/alee/Sources/relicario commit -m "refactor(ext/popup): migrate required-field markers to REQUIRED_PILL + +Replaces ten * sites across all seven type +forms with the shared REQUIRED_PILL snippet ('required' badge). Adds a +regression test pinning the new HTML in the login form. + +Plan 2026-04-30 fullscreen UX phase 1 task 4. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Task 5: Migrate vault sidebar nav glyphs + +**Files:** +- Modify: `extension/src/vault/vault.ts:251-254` + +- [ ] **Step 1: Write a regression test** + +Open `extension/src/vault/components/__tests__/import-panel.test.ts` for reference on how vault tests mock state. Create a new test file: + +`extension/src/vault/__tests__/sidebar-glyphs.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest'; +import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK } from '../../shared/glyphs'; + +// vault.ts injects HTML into document.getElementById('vault-app'); we +// don't need to invoke render() — we just need to scan the source for the +// emoji we removed. +import * as fs from 'fs'; +import * as path from 'path'; + +describe('vault sidebar glyphs', () => { + const vaultSrc = fs.readFileSync( + path.resolve(__dirname, '../vault.ts'), + 'utf-8', + ); + + it('uses GLYPH_TRASH instead of the trash emoji', () => { + expect(vaultSrc).not.toMatch(/\u{1F5D1}/u); + expect(vaultSrc).toContain('GLYPH_TRASH'); + }); + + it('uses GLYPH_DEVICES instead of the devices emoji', () => { + expect(vaultSrc).not.toMatch(/\u{1F4F1}/u); + expect(vaultSrc).toContain('GLYPH_DEVICES'); + }); + + it('uses GLYPH_LOCK instead of the lock emoji', () => { + expect(vaultSrc).not.toMatch(/\u{1F512}/u); + expect(vaultSrc).toContain('GLYPH_LOCK'); + }); + + it('uses GLYPH_SETTINGS for the settings nav', () => { + expect(vaultSrc).toContain('GLYPH_SETTINGS'); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cd extension && ./node_modules/.bin/vitest run src/vault/__tests__/sidebar-glyphs.test.ts` +Expected: FAIL — the emojis are still present, the GLYPH constants are not. + +- [ ] **Step 3: Add the import to vault.ts** + +In `extension/src/vault/vault.ts`, add to the imports section (near the top, after other shared imports): + +```typescript +import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK } from '../shared/glyphs'; +``` + +- [ ] **Step 4: Replace the sidebar nav buttons** + +Find the block at lines 249-255 in `vault.ts`: + +```typescript +
+ + + + + +
+``` + +Replace with: + +```typescript +
+ + + + + +
+``` + +- [ ] **Step 5: Run the test to verify it passes** + +Run: `cd extension && ./node_modules/.bin/vitest run src/vault/__tests__/sidebar-glyphs.test.ts` +Expected: PASS, 4/4 tests green. + +- [ ] **Step 6: Run the full suite + build** + +```bash +cd /home/alee/Sources/relicario/extension +./node_modules/.bin/vitest run 2>&1 | tail -5 +./node_modules/.bin/webpack --mode production 2>&1 | tail -5 +``` +Expected: all tests pass; webpack compiles with 2 warnings. + +- [ ] **Step 7: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/vault/vault.ts extension/src/vault/__tests__/sidebar-glyphs.test.ts +git -C /home/alee/Sources/relicario commit -m "style(ext/vault): replace sidebar emoji nav with monochrome glyphs + +▦ trash · ⌬ devices · ⚙ settings · ⏻ lock — all imported from the new +shared/glyphs module so popup and fullscreen stay in sync. Regression +test scans the source for the old escape-coded emoji to prevent +backsliding. + +Plan 2026-04-30 fullscreen UX phase 1 task 5. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Task 6: Migrate popup settings nav glyphs + +**Files:** +- Modify: `extension/src/popup/components/settings.ts:58-59` + +- [ ] **Step 1: Verify the existing emojis** + +Run: `grep -n "🗑\|🔐" extension/src/popup/components/settings.ts` +Expected output (line 58 trash, line 59 devices): +``` +58: +59: +``` + +- [ ] **Step 2: Add the import** + +In `extension/src/popup/components/settings.ts`, add to the imports near the top: + +```typescript +import { GLYPH_TRASH, GLYPH_DEVICES } from '../../shared/glyphs'; +``` + +- [ ] **Step 3: Replace the buttons** + +Replace lines 58-59: + +Before: +```typescript + + +``` + +After: +```typescript + + +``` + +(Lowercased "trash" / "devices" to match the brand's lowercase aesthetic established in Phase 1.) + +- [ ] **Step 4: Verify no emojis remain** + +Run: `grep -n "🗑\|🔐\|🔒\|📺" extension/src/popup/components/settings.ts` +Expected: empty output. + +- [ ] **Step 5: Run tests + build** + +```bash +cd /home/alee/Sources/relicario/extension +./node_modules/.bin/vitest run 2>&1 | tail -5 +./node_modules/.bin/webpack --mode production 2>&1 | tail -5 +``` +Expected: all tests pass; webpack compiles with 2 warnings. + +- [ ] **Step 6: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/popup/components/settings.ts +git -C /home/alee/Sources/relicario commit -m "style(ext/popup): replace settings nav emoji with shared glyphs + +▦ trash and ⌬ devices in the popup settings panel now match the +fullscreen sidebar's glyph language. Lowercased labels match the brand. + +Plan 2026-04-30 fullscreen UX phase 1 task 6. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Task 7: Hide popout-to-tab button in fullscreen forms + +**Files (8 sites):** +- Modify: `extension/src/popup/components/item-form.ts:61` +- Modify: `extension/src/popup/components/types/card.ts:179` +- Modify: `extension/src/popup/components/types/document.ts:90` +- Modify: `extension/src/popup/components/types/identity.ts:139` +- Modify: `extension/src/popup/components/types/key.ts:128` +- Modify: `extension/src/popup/components/types/login.ts:249` +- Modify: `extension/src/popup/components/types/secure-note.ts:117` +- Modify: `extension/src/popup/components/types/totp.ts:218` + +- [ ] **Step 1: Confirm `isInTab()` is exported and used** + +Run: `grep -n "export.*isInTab\|import.*isInTab" extension/src/shared/state.ts extension/src/popup/components/types/login.ts` +Expected: `state.ts` exports `isInTab`; `login.ts` already imports it. + +- [ ] **Step 2: Write a test for the login form behavior in fullscreen** + +Append to `extension/src/popup/components/types/__tests__/required-pill.test.ts` (or create a new file `popout-button.test.ts` next to it): + +```typescript +// Append to required-pill.test.ts + +describe('popout-to-tab button visibility', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it('renders the popout button when isInTab() is false (popup context)', async () => { + // The default mock at the top of this file sets isInTab: () => false. + // Re-render with that. + const { renderForm } = await import('../login'); + renderForm(document.getElementById('app')!, 'add', null); + expect(document.getElementById('popout-btn')).not.toBeNull(); + }); +}); +``` + +For the fullscreen variant (isInTab → true), add a separate test file because vi.mock is module-level. Create `extension/src/popup/components/types/__tests__/popout-fullscreen.test.ts`: + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../../../../shared/state', () => ({ + sendMessage: vi.fn(), + getState: () => ({ newType: 'login', generatorDefaults: null, error: null, loading: false, vaultSettings: null, entries: [] }), + setState: vi.fn(), + navigate: vi.fn(), + escapeHtml: (s: string) => s, + popOutToTab: vi.fn(), + isInTab: () => true, // FULLSCREEN context + openVaultTab: vi.fn(), + registerHost: vi.fn(), +})); + +vi.mock('../../generator-panel', () => ({ + openGeneratorPanel: vi.fn(), + closeGeneratorPanel: vi.fn(), + isGeneratorPanelOpen: () => false, +})); + +import { renderForm } from '../login'; + +describe('popout-to-tab button (fullscreen context)', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it('does NOT render the popout button when isInTab() is true', () => { + renderForm(document.getElementById('app')!, 'add', null); + expect(document.getElementById('popout-btn')).toBeNull(); + }); +}); +``` + +- [ ] **Step 3: Run tests to verify the fullscreen test fails** + +Run: `cd extension && ./node_modules/.bin/vitest run src/popup/components/types/__tests__/popout-fullscreen.test.ts` +Expected: FAIL — popout button is currently rendered unconditionally. + +- [ ] **Step 4: Gate the popout button in `login.ts`** + +In `extension/src/popup/components/types/login.ts`, find line 249: + +```typescript + +``` + +Replace with: + +```typescript + ${isInTab() ? '' : ''} +``` + +- [ ] **Step 5: Repeat for the other seven files** + +Apply the same conditional wrap to each remaining popout button site. For each, the surrounding context is `` — wrap that single line with the ternary. + +For `extension/src/popup/components/item-form.ts:61` (the type-selection screen's popout button), use the same pattern: + +```typescript + ${isInTab() ? '' : ''} +``` + +If `isInTab` is not already imported in a given file, add it to the existing import from `../../../shared/state` (or `../../shared/state` for `item-form.ts`). + +After editing each file, also remove or guard the corresponding `document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);` line — or leave it as-is since `getElementById` returns `null` and the optional-chain handles it. **Leave the listener wiring untouched** to keep the diff minimal; it's a no-op when the button isn't present. + +- [ ] **Step 6: Run all popout tests + full suite** + +```bash +cd /home/alee/Sources/relicario/extension +./node_modules/.bin/vitest run 2>&1 | tail -8 +``` +Expected: all tests pass, including both `popout-button` and `popout-fullscreen` cases. + +- [ ] **Step 7: Build to verify** + +Run: `cd /home/alee/Sources/relicario/extension && ./node_modules/.bin/webpack --mode production 2>&1 | tail -5` +Expected: `compiled with 2 warnings`. + +- [ ] **Step 8: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/popup/ +git -C /home/alee/Sources/relicario commit -m "feat(ext/popup): hide popout-to-tab button in fullscreen forms + +The ⤴ popout button is meaningless when the form is already in +vault.html — gate it on !isInTab(). Affects all seven type forms plus +the type-selection screen. Regression tests cover both popup (button +present) and fullscreen (button absent) contexts. + +Plan 2026-04-30 fullscreen UX phase 1 task 7. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Task 8: Static "esc to cancel" subtitle in fullscreen forms + +**Files:** +- Modify: same eight files as Task 7 (header markup region, ~3-4 lines above the popout button site) +- Modify: `extension/src/popup/styles.css` (one new CSS class — shared, since the fullscreen inherits popup styles via vault's own stylesheet only loading vault.css) +- Modify: `extension/src/vault/vault.css` (one new CSS class) + +- [ ] **Step 1: Add the `.form-subtitle` CSS class to popup/styles.css** + +Append to `extension/src/popup/styles.css` (anywhere — group near `.muted`): + +```css +.form-subtitle { + font-size: 11px; + color: var(--text-dim); + margin-top: 2px; + margin-bottom: 14px; + letter-spacing: 0.02em; +} +``` + +- [ ] **Step 2: Add the same class to vault.css** + +Append the **identical** `.form-subtitle` rule to `extension/src/vault/vault.css`. + +- [ ] **Step 3: Write a test for the subtitle in fullscreen context** + +Append to `extension/src/popup/components/types/__tests__/popout-fullscreen.test.ts`: + +```typescript +describe('form subtitle (fullscreen context)', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it('renders "esc to cancel" subtitle in the login form header', () => { + renderForm(document.getElementById('app')!, 'add', null); + const subtitle = document.querySelector('.form-subtitle'); + expect(subtitle).not.toBeNull(); + expect(subtitle?.textContent).toContain('esc to cancel'); + }); +}); +``` + +And add a *negative* test in `required-pill.test.ts` (popup context): + +```typescript +describe('form subtitle (popup context)', () => { + beforeEach(() => { document.body.innerHTML = '
'; }); + + it('does NOT render the "esc to cancel" subtitle in popup context', async () => { + const { renderForm } = await import('../login'); + renderForm(document.getElementById('app')!, 'add', null); + expect(document.querySelector('.form-subtitle')).toBeNull(); + }); +}); +``` + +- [ ] **Step 4: Run tests to verify the fullscreen subtitle test fails** + +Run: `cd extension && ./node_modules/.bin/vitest run src/popup/components/types/__tests__/popout-fullscreen.test.ts` +Expected: FAIL — no `.form-subtitle` element rendered today. + +- [ ] **Step 5: Update `login.ts` header** + +In `extension/src/popup/components/types/login.ts`, find the header markup (lines 246-250): + +```typescript +
+
${mode === 'add' ? 'new login' : 'edit login'}
+ + ${isInTab() ? '' : ''} +
+``` + +Replace with: + +```typescript +
+
${mode === 'add' ? 'new login' : 'edit login'}
+ + ${isInTab() ? '' : ''} +
+ ${isInTab() ? '
esc to cancel
' : '
'} +``` + +(The header's `margin-bottom:16px` moves to the conditional spacer so the subtitle gets to sit right under the title.) + +- [ ] **Step 6: Run the test to verify it passes for login** + +Run: `cd extension && ./node_modules/.bin/vitest run src/popup/components/types/__tests__/popout-fullscreen.test.ts src/popup/components/types/__tests__/required-pill.test.ts` +Expected: PASS — both fullscreen and popup variants of the subtitle test. + +- [ ] **Step 7: Repeat for the remaining six type forms** + +Apply the same header restructuring to each of: +- `card.ts` (around line 179) +- `document.ts` (around line 90) +- `identity.ts` (around line 139) +- `key.ts` (around line 128) +- `secure-note.ts` (around line 117) +- `totp.ts` (around line 218) + +For each, find the existing header `
` block that contains the title + popout button, and add the subtitle line below it using the same conditional pattern. The title text differs per type ("new identity" / "new card" etc.) — preserve whatever the current expression is. + +For `extension/src/popup/components/item-form.ts` (the type-selection screen), apply the same pattern around line 60-63. + +- [ ] **Step 8: Run the full suite + build** + +```bash +cd /home/alee/Sources/relicario/extension +./node_modules/.bin/vitest run 2>&1 | tail -5 +./node_modules/.bin/webpack --mode production 2>&1 | tail -5 +``` +Expected: all tests pass; webpack compiles with 2 warnings. + +- [ ] **Step 9: Commit** + +```bash +git -C /home/alee/Sources/relicario add extension/src/popup/ extension/src/vault/ +git -C /home/alee/Sources/relicario commit -m "feat(ext): static 'esc to cancel' subtitle in fullscreen form headers + +All seven type forms plus the type-selection screen now show a small +'esc to cancel' subtitle under the heading when rendered in the +fullscreen vault tab (isInTab() === true). The subtitle is suppressed +in the popup, where esc has the more general meaning of closing the +popup. .form-subtitle class is shared between popup and vault +stylesheets so future hooks can reuse it. + +Dynamic dirty-state ('unsaved · esc to cancel') wiring is deferred to +Phase 3 (unsaved-changes guard). + +Plan 2026-04-30 fullscreen UX phase 1 task 8. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +## Final verification + +- [ ] **Run the full extension test suite one more time** + +```bash +cd /home/alee/Sources/relicario/extension +./node_modules/.bin/vitest run 2>&1 | tail -10 +``` +Expected: all tests pass (count = previous baseline + the new tests added by this plan). + +- [ ] **Build all variants** + +```bash +cd /home/alee/Sources/relicario/extension +./node_modules/.bin/webpack --mode production 2>&1 | tail -5 +./node_modules/.bin/webpack --config webpack.firefox.config.js --mode production 2>&1 | tail -5 +``` +Expected: both compile with 2 warnings. + +- [ ] **Manual smoke test** + +Load the unpacked extension in Chrome: +1. Open the popup: confirm sidebar settings panel shows `▦ trash` / `⌬ devices` (no emoji), required pill on title fields, focus ring is amber. +2. Open vault.html: confirm sidebar shows `▦ trash · ⌬ devices · ⚙ settings · ⏻ lock`, no popout button on the form header, "esc to cancel" subtitle visible under "new login". +3. Tab through fields with keyboard: confirm focus ring renders consistently. + +(If anything looks off, the symptom is almost certainly a CSS specificity issue — vault.css may need an `!important` or scoped selector. Note the issue and fix in a follow-up commit.)