Files
relicario/docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md
adlee-was-taken 506ad9711d refactor(ext/shared): rename REQUIRED_PILL → REQUIRED_PILL_HTML
Code-review feedback on Task 1: the _HTML suffix makes the 'this is raw
HTML, do not escape' contract obvious at every call site. Cheap to do
now (zero consumers); would be 8 diffs once Tasks 4-6 wire the constant
into the type forms.

Plan updated in lockstep so Task 4 references the new name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:29:49 -04:00

949 lines
33 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 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 `<span class="req">*</span>` 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_HTML as an HTML snippet', () => {
expect(glyphs.REQUIRED_PILL_HTML).toBe('<span class="req-pill">required</span>');
});
});
```
- [ ] **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:
/// `<label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>`
export const REQUIRED_PILL_HTML = '<span class="req-pill">required</span>';
```
- [ ] **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_HTML
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 <noreply@anthropic.com>"
```
---
## 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 <noreply@anthropic.com>"
```
---
## 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 <noreply@anthropic.com>"
```
---
## Task 4: Migrate required-marker sites to REQUIRED_PILL_HTML
**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 = '<div id="app"></div>'; });
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('<span class="req">*</span>');
});
});
```
- [ ] **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 — `<span class="req">*</span>` 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_HTML } from '../../../shared/glyphs';
```
Find line 252:
```typescript
<div class="form-group"><label class="label" for="f-title">title <span class="req">*</span></label>
```
Replace with:
```typescript
<div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
```
- [ ] **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_HTML } from '../../../shared/glyphs';`
2. Replace each `<span class="req">*</span>` with `${REQUIRED_PILL_HTML}`
| 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 `<span class="req">*</span>` 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_HTML
Replaces ten <span class=\"req\">*</span> sites across all seven type
forms with the shared REQUIRED_PILL_HTML 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 <noreply@anthropic.com>"
```
---
## 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
<div class="vault-sidebar__nav">
<button class="vault-sidebar__nav-item" data-nav="add">+ new item</button>
<button class="vault-sidebar__nav-item" data-nav="trash">\u{1F5D1} trash</button>
<button class="vault-sidebar__nav-item" data-nav="devices">\u{1F4F1} devices</button>
<button class="vault-sidebar__nav-item" data-nav="settings"> settings</button>
<button class="vault-sidebar__nav-item" data-nav="lock">\u{1F512} lock</button>
</div>
```
Replace with:
```typescript
<div class="vault-sidebar__nav">
<button class="vault-sidebar__nav-item" data-nav="add">+ new item</button>
<button class="vault-sidebar__nav-item" data-nav="trash">${GLYPH_TRASH} trash</button>
<button class="vault-sidebar__nav-item" data-nav="devices">${GLYPH_DEVICES} devices</button>
<button class="vault-sidebar__nav-item" data-nav="settings">${GLYPH_SETTINGS} settings</button>
<button class="vault-sidebar__nav-item" data-nav="lock">${GLYPH_LOCK} lock</button>
</div>
```
- [ ] **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 <noreply@anthropic.com>"
```
---
## 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: <button class="btn" id="trash-btn" style="width:100%;margin-bottom:8px;">🗑️ Trash</button>
59: <button class="btn" id="devices-btn" style="width:100%;margin-bottom:8px;">🔐 Devices</button>
```
- [ ] **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
<button class="btn" id="trash-btn" style="width:100%;margin-bottom:8px;">🗑 Trash</button>
<button class="btn" id="devices-btn" style="width:100%;margin-bottom:8px;">🔐 Devices</button>
```
After:
```typescript
<button class="btn" id="trash-btn" style="width:100%;margin-bottom:8px;">${GLYPH_TRASH} trash</button>
<button class="btn" id="devices-btn" style="width:100%;margin-bottom:8px;">${GLYPH_DEVICES} devices</button>
```
(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 <noreply@anthropic.com>"
```
---
## 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 = '<div id="app"></div>'; });
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 = '<div id="app"></div>'; });
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
<button class="btn" id="popout-btn" title="Open in tab"></button>
```
Replace with:
```typescript
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
```
- [ ] **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 `<button class="btn" id="popout-btn" title="Open in tab">⤴</button>` — 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() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
```
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 <noreply@anthropic.com>"
```
---
## 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 = '<div id="app"></div>'; });
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 = '<div id="app"></div>'; });
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
<div style="display:flex; align-items:center; margin-bottom:16px;">
<div class="detail-title">${mode === 'add' ? 'new login' : 'edit login'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
```
Replace with:
```typescript
<div style="display:flex; align-items:center;">
<div class="detail-title">${mode === 'add' ? 'new login' : 'edit login'}</div>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⤴</button>'}
</div>
${isInTab() ? '<div class="form-subtitle">esc to cancel</div>' : '<div style="margin-bottom:16px;"></div>'}
```
(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 `<div>` 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 <noreply@anthropic.com>"
```
---
## 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.)