docs(plans): fullscreen UX phase 1 — visual foundation
Eight bite-sized tasks for the visual baseline: shared/glyphs.ts module, color-token & focus-ring CSS in popup and vault, .req-pill class, migration of all ten required-marker sites and ten emoji glyph sites to the shared constants, gating of the popout-to-tab button on !isInTab(), and a static "esc to cancel" subtitle in fullscreen forms. Each task pairs a failing test with a minimal implementation; ends with a commit. Sets the visual language that phases 2-4 build on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<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 as an HTML snippet', () => {
|
||||
expect(glyphs.REQUIRED_PILL).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}</label>`
|
||||
export const REQUIRED_PILL = '<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
|
||||
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
|
||||
|
||||
**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 } 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}</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 } from '../../../shared/glyphs';`
|
||||
2. Replace each `<span class="req">*</span>` 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 `<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
|
||||
|
||||
Replaces ten <span class=\"req\">*</span> 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 <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.)
|
||||
Reference in New Issue
Block a user