# Dev A Kickoff Prompt — v0.5.1 Stream A (Fullscreen + Popup Layout) > **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. Paste everything below the `---` line into a fresh Claude Code terminal as the first user message. --- You are a **senior developer** owning Stream A for the Relicario v0.5.1 release. Stream A is the fullscreen vault tab layout overhaul + popup polish: 3-column layout (sidebar category nav, full-width list, slide-in drawer), bottom sheet for new-item type picker, popup type-picker polish, per-type glyph icons, and a shared toast system. **Goal:** Replace the current 2-column vault tab (sidebar + single pane) with a responsive 3-column layout. All emoji type icons become Unicode monochrome glyphs. A shared toast module replaces ad-hoc status divs. **Architecture:** All changes are in the extension. `vault.ts` is a full layout rewrite. `item-list.ts` and `item-form.ts` get glyph replacements and polish. A new `toast.ts` provides the shared notification API. CSS lives in `vault.css` (fullscreen) and `popup/styles.css` (popup). **Tech Stack:** TypeScript, vitest, webpack/bun. --- ## Setup (do this first) ```bash cd /home/alee/Sources/relicario git fetch git checkout main git pull git worktree add ../relicario.v0.5.1-stream-a -b feature/v0.5.1-stream-a-layout cd ../relicario.v0.5.1-stream-a pwd # should print /home/alee/Sources/relicario.v0.5.1-stream-a ``` **ALL subsequent work happens in `/home/alee/Sources/relicario.v0.5.1-stream-a`**. Every subagent prompt MUST begin with `cd /home/alee/Sources/relicario.v0.5.1-stream-a`. Today: 2026-05-03. Project rules in `CLAUDE.md` apply. ## Required reading 1. `CLAUDE.md` — project rules 2. `docs/superpowers/specs/2026-05-03-v0.5.x-ux-polish-and-recovery-qr-design.md` — spec sections A1–A7 3. `extension/src/vault/vault.ts` — current implementation (read fully before editing) 4. `extension/src/vault/vault.css` — current styles 5. `extension/src/popup/components/item-list.ts` — popup item list 6. `extension/src/popup/components/item-form.ts` — type-picker 7. `extension/src/shared/glyphs.ts` — existing glyph constants ## Execution mode Use **subagent-driven-development**. Fresh subagent per task, two-stage review between tasks. Every subagent prompt MUST start with `cd /home/alee/Sources/relicario.v0.5.1-stream-a`. ## Interface contract with DEV-B (settings component) DEV-B will deliver a settings component with these exports. **You must use exactly this signature** in `vault.ts` when wiring the settings view: ```ts // extension/src/popup/components/settings.ts export async function renderSettings(container: HTMLElement): Promise; export function teardownSettings(): void; ``` Your vault.ts should call `renderSettings(pane)` when the `#settings` route is active, and `teardownSettings()` when navigating away. You can proceed with a stub import while DEV-B's branch is in progress; it will be reconciled at merge time. ## Scope and boundaries **In scope:** A1–A7 (layout, drawer, bottom sheet, type picker, glyphs, empty states, toast). **Out of scope:** Stream B and C work. If you hit a bug outside your scope, file a `## QUESTION TO PM` block. **Hard rules:** - No emoji anywhere in `extension/src/`. If you see one while editing, replace it with the monochrome glyph. - `glyphs.ts` is the single source of truth. No inline Unicode literals at call sites. - Don't merge to main. The PM owns merges. ## Coordination protocol ``` ## STATUS UPDATE — DEV-A Time: Task: Status: IN-PROGRESS | BLOCKED | REVIEW-READY Summary: Next: ``` --- ## Files **Create:** - `extension/src/shared/toast.ts` — shared notification module **Modify:** - `extension/src/shared/glyphs.ts` — add GLYPH_VAULT_TAB + 7 per-type constants - `extension/src/shared/__tests__/glyphs.test.ts` — add new constants to the test - `extension/src/popup/components/item-list.ts` — glyph vault-btn, type icons, empty states, toast - `extension/src/popup/components/item-form.ts` — TYPE_OPTIONS glyphs, polished type picker - `extension/src/vault/vault.ts` — full layout rewrite - `extension/src/vault/vault.css` — 3-column layout, drawer, bottom sheet, responsive - `extension/src/popup/styles.css` — toast styles, type-icon pill, empty-state styles --- ### Task 1: Add `GLYPH_VAULT_TAB` and per-type glyph constants **Files:** - Modify: `extension/src/shared/glyphs.ts` - Modify: `extension/src/shared/__tests__/glyphs.test.ts` - [ ] **Step 1: Update the failing test first** In `extension/src/shared/__tests__/glyphs.test.ts`, add to the existing test: ```ts it('exports GLYPH_VAULT_TAB as U+29C9', () => { expect(glyphs.GLYPH_VAULT_TAB).toBe('⧉'); }); it('exports per-type glyph constants', () => { expect(glyphs.GLYPH_TYPE_LOGIN).toBe('◉'); expect(glyphs.GLYPH_TYPE_SECURE_NOTE).toBe('◫'); expect(glyphs.GLYPH_TYPE_TOTP).toBe('⊡'); expect(glyphs.GLYPH_TYPE_CARD).toBe('▭'); expect(glyphs.GLYPH_TYPE_IDENTITY).toBe('⌬'); expect(glyphs.GLYPH_TYPE_KEY).toBe('⊹'); expect(glyphs.GLYPH_TYPE_DOCUMENT).toBe('≡'); }); it('per-type glyphs are single codepoints (no emoji)', () => { const typeGlyphs = [ glyphs.GLYPH_TYPE_LOGIN, glyphs.GLYPH_TYPE_SECURE_NOTE, glyphs.GLYPH_TYPE_TOTP, glyphs.GLYPH_TYPE_CARD, glyphs.GLYPH_TYPE_IDENTITY, glyphs.GLYPH_TYPE_KEY, glyphs.GLYPH_TYPE_DOCUMENT, ]; for (const g of typeGlyphs) { expect([...g].length).toBe(1); } }); ``` - [ ] **Step 2: Run tests to confirm they fail** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | grep -E "FAIL|✗|×" ``` Expected: the new test cases fail (constants not exported yet). - [ ] **Step 3: Add constants to glyphs.ts** In `extension/src/shared/glyphs.ts`, add after the existing `GLYPH_NEXT` line: ```ts export const GLYPH_VAULT_TAB = '⧉'; // U+29C9 pop-out to fullscreen vault tab export const GLYPH_TYPE_LOGIN = '◉'; // login export const GLYPH_TYPE_SECURE_NOTE = '◫'; // secure note export const GLYPH_TYPE_TOTP = '⊡'; // totp / 2FA export const GLYPH_TYPE_CARD = '▭'; // card export const GLYPH_TYPE_IDENTITY = '⌬'; // identity export const GLYPH_TYPE_KEY = '⊹'; // SSH / API key export const GLYPH_TYPE_DOCUMENT = '≡'; // document ``` - [ ] **Step 4: Run tests to confirm they pass** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | grep -E "PASS|✓|Tests" ``` Expected: all glyph tests pass. - [ ] **Step 5: Commit** ```bash git add extension/src/shared/glyphs.ts extension/src/shared/__tests__/glyphs.test.ts git commit -m "feat(ext/glyphs): add GLYPH_VAULT_TAB and per-type icon constants" ``` --- ### Task 2: Replace `⤴` vault button in `item-list.ts` **Files:** - Modify: `extension/src/popup/components/item-list.ts` - [ ] **Step 1: Update the import** In `item-list.ts`, find the imports section and add `GLYPH_VAULT_TAB` to the glyphs import: ```ts import { GLYPH_VAULT_TAB } from '../../shared/glyphs'; ``` - [ ] **Step 2: Replace the inline HTML entity** In `item-list.ts:69`, change: ```ts // Old: // New: ``` - [ ] **Step 3: Build and check for TS errors** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" ``` - [ ] **Step 4: Commit** ```bash git add extension/src/popup/components/item-list.ts git commit -m "fix(ext/popup): replace inline ⤴ vault-tab button with GLYPH_VAULT_TAB" ``` --- ### Task 3: Replace emoji `typeIcon()` in `item-list.ts` with glyph function **Files:** - Modify: `extension/src/popup/components/item-list.ts` - [ ] **Step 1: Add glyph imports to item-list.ts** ```ts import { GLYPH_VAULT_TAB, GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP, GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, } from '../../shared/glyphs'; ``` - [ ] **Step 2: Replace `typeIcon()` in item-list.ts** The existing `typeIcon()` function (lines ~16–26) uses emoji. Replace entirely: ```ts function typeIcon(t: ItemType): string { switch (t) { case 'login': return GLYPH_TYPE_LOGIN; case 'secure_note': return GLYPH_TYPE_SECURE_NOTE; case 'identity': return GLYPH_TYPE_IDENTITY; case 'card': return GLYPH_TYPE_CARD; case 'key': return GLYPH_TYPE_KEY; case 'document': return GLYPH_TYPE_DOCUMENT; case 'totp': return GLYPH_TYPE_TOTP; } } ``` Also replace the 📎 paperclip emoji in `buildRowsHtml()`: ```ts // Old: ${e.attachment_summaries.length > 0 ? ' 📎' : ''} // New: ${e.attachment_summaries.length > 0 ? ' ' : ''} ``` - [ ] **Step 3: Build and confirm no TS errors** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" ``` - [ ] **Step 4: Commit** ```bash git add extension/src/popup/components/item-list.ts git commit -m "fix(ext/popup): replace emoji typeIcon with glyph constants in item-list" ``` --- ### Task 4: Empty states in `item-list.ts` **Files:** - Modify: `extension/src/popup/components/item-list.ts` - Modify: `extension/src/popup/styles.css` - [ ] **Step 1: Update `buildRowsHtml()` to use two distinct empty states** Current empty state is `'
no items
'`. Split into: ```ts function buildRowsHtml(): string { const state = getState(); const filtered = getFilteredEntries(); if (filtered.length === 0) { if (state.searchQuery) { return `
No results for "${escapeHtml(state.searchQuery)}"
Try a shorter search term.
`; } return `
No items yet
Press + to add your first item.
`; } return filtered.map(([id, e], i) => `
${escapeHtml(e.title)}${e.attachment_summaries.length > 0 ? ' ' : ''}
`).join(''); } ``` - [ ] **Step 2: Add empty-state CSS to styles.css** In `extension/src/popup/styles.css`, add: ```css .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; text-align: center; } .empty-state__icon { font-size: 28px; color: var(--text-muted, #8b949e); margin-bottom: 12px; display: block; } .empty-state__title { font-size: 13px; font-weight: 600; margin-bottom: 4px; } .empty-state__hint { font-size: 11px; color: var(--text-muted, #8b949e); } ``` - [ ] **Step 3: Build** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" ``` - [ ] **Step 4: Commit** ```bash git add extension/src/popup/components/item-list.ts extension/src/popup/styles.css git commit -m "feat(ext/popup): empty states with glyph icons in item-list" ``` --- ### Task 5: Polished type-picker in `item-form.ts` **Files:** - Modify: `extension/src/popup/components/item-form.ts` - [ ] **Step 1: Replace emoji in TYPE_OPTIONS** Current `TYPE_OPTIONS` uses emoji icons. Replace: ```ts import { GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP, GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, } from '../../shared/glyphs'; const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string; description: string }> = [ { type: 'login', icon: GLYPH_TYPE_LOGIN, label: 'Login', description: 'Username + password' }, { type: 'secure_note', icon: GLYPH_TYPE_SECURE_NOTE, label: 'Secure Note', description: 'Encrypted text note' }, { type: 'identity', icon: GLYPH_TYPE_IDENTITY, label: 'Identity', description: 'Personal details' }, { type: 'card', icon: GLYPH_TYPE_CARD, label: 'Card', description: 'Credit / debit card' }, { type: 'key', icon: GLYPH_TYPE_KEY, label: 'SSH / API Key', description: 'Keys and tokens' }, { type: 'document', icon: GLYPH_TYPE_DOCUMENT, label: 'Document', description: 'File attachment' }, { type: 'totp', icon: GLYPH_TYPE_TOTP, label: 'TOTP', description: '2FA authenticator' }, ]; ``` - [ ] **Step 2: Upgrade `renderTypeSelection` to a 2-column card grid** Replace the current `renderTypeSelection` function's HTML: ```ts function renderTypeSelection(app: HTMLElement): void { app.innerHTML = `
New item ${isInTab() ? '' : ''}
${TYPE_OPTIONS.map((opt) => ` `).join('')}
Esc back
`; document.getElementById('back-btn')?.addEventListener('click', () => navigate('list')); document.getElementById('popout-btn')?.addEventListener('click', popOutToTab); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') navigate('list'); }, { once: true }); document.querySelectorAll('[data-type]').forEach((btn) => { btn.addEventListener('click', () => { const type = btn.dataset.type as ItemType; setState({ newType: type }); renderItemForm(app, 'add'); }); }); } ``` - [ ] **Step 3: Add type-card CSS to styles.css** In `extension/src/popup/styles.css`: ```css .type-card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; } .type-card { display: flex; flex-direction: column; align-items: flex-start; padding: 10px 12px; background: var(--bg-elevated, #161b22); border: 1px solid var(--border, #30363d); border-radius: 6px; cursor: pointer; text-align: left; transition: border-color 0.15s; } .type-card:hover { border-color: var(--gold, #b8860b); } .type-card__icon { font-size: 20px; margin-bottom: 4px; } .type-card__label { font-size: 12px; font-weight: 600; } .type-card__desc { font-size: 10px; color: var(--text-muted, #8b949e); margin-top: 2px; } ``` - [ ] **Step 4: Build and run tests** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" bun run test 2>&1 | tail -10 ``` - [ ] **Step 5: Commit** ```bash git add extension/src/popup/components/item-form.ts extension/src/popup/styles.css git commit -m "feat(ext/popup): polished 2-column type-picker with glyph icons" ``` --- ### Task 6: Toast system **Files:** - Create: `extension/src/shared/toast.ts` - Modify: `extension/src/popup/styles.css` - Modify: `extension/src/vault/vault.css` - [ ] **Step 1: Write toast.ts** ```ts // extension/src/shared/toast.ts export function showToast( message: string, type: 'success' | 'error' | 'info' = 'info', durationMs = 2500, ): void { let container = document.querySelector('.relicario-toast-container'); if (!container) { container = document.createElement('div'); container.className = 'relicario-toast-container'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = `relicario-toast relicario-toast--${type}`; toast.textContent = message; container.appendChild(toast); requestAnimationFrame(() => { requestAnimationFrame(() => toast.classList.add('relicario-toast--visible')); }); setTimeout(() => { toast.classList.remove('relicario-toast--visible'); toast.addEventListener('transitionend', () => toast.remove(), { once: true }); }, durationMs); } ``` - [ ] **Step 2: Add toast CSS to both stylesheets** In `extension/src/popup/styles.css` AND `extension/src/vault/vault.css`, add: ```css /* Toast notifications */ .relicario-toast-container { position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 6px; pointer-events: none; z-index: 9999; } /* Vault tab: position bottom-right instead */ .vault-shell .relicario-toast-container { left: auto; right: 24px; transform: none; } .relicario-toast { padding: 8px 16px; border-radius: 6px; font-size: 12px; opacity: 0; transform: translateY(8px); transition: opacity 0.2s, transform 0.2s; pointer-events: none; } .relicario-toast--visible { opacity: 1; transform: translateY(0); } .relicario-toast--success { background: #1f4a24; color: #aff0b5; border: 1px solid #238636; } .relicario-toast--error { background: #4a1f1f; color: #f0afaf; border: 1px solid #ab2b20; } .relicario-toast--info { background: #1f2d4a; color: #afc8f0; border: 1px solid #1f6feb; } ``` - [ ] **Step 3: Replace sync-status div in item-list.ts with toast** In `item-list.ts`, find the sync button handler and replace the manual status update with toast: ```ts import { showToast } from '../../shared/toast'; // In the sync button click handler: document.getElementById('sync-btn')?.addEventListener('click', async () => { setState({ loading: true, error: null }); const resp = await sendMessage({ type: 'sync' }); if (resp.ok) { const listResp = await sendMessage({ type: 'list_items' }); if (listResp.ok) { const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> }; setState({ entries: data.items, loading: false }); showToast('Synced', 'success'); return; } setState({ loading: false, error: listResp.error }); showToast(listResp.error ?? 'Sync failed', 'error'); } else { setState({ loading: false, error: resp.error }); showToast(resp.error ?? 'Sync failed', 'error'); } }); ``` - [ ] **Step 4: Build and run tests** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" bun run test 2>&1 | tail -10 ``` - [ ] **Step 5: Commit** ```bash git add extension/src/shared/toast.ts extension/src/popup/styles.css extension/src/vault/vault.css extension/src/popup/components/item-list.ts git commit -m "feat(ext): shared toast notification system" ``` --- ### Task 7: `vault.ts` — replace emoji `typeIcon()` with glyphs **Files:** - Modify: `extension/src/vault/vault.ts` - [ ] **Step 1: Add glyph imports to vault.ts** Find the glyphs import line in vault.ts and add the type constants: ```ts import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_NEXT, GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP, GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, } from '../shared/glyphs'; ``` - [ ] **Step 2: Replace `typeIcon()` in vault.ts** Current `typeIcon()` (~line 61) uses Unicode escape sequences for emoji. Replace: ```ts function typeIcon(t: ItemType): string { switch (t) { case 'login': return GLYPH_TYPE_LOGIN; case 'secure_note': return GLYPH_TYPE_SECURE_NOTE; case 'identity': return GLYPH_TYPE_IDENTITY; case 'card': return GLYPH_TYPE_CARD; case 'key': return GLYPH_TYPE_KEY; case 'document': return GLYPH_TYPE_DOCUMENT; case 'totp': return GLYPH_TYPE_TOTP; } } ``` - [ ] **Step 3: Build** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" ``` - [ ] **Step 4: Commit** ```bash git add extension/src/vault/vault.ts git commit -m "fix(ext/vault): replace emoji typeIcon with glyph constants" ``` --- ### Task 8: `vault.css` — 3-column layout rules **Files:** - Modify: `extension/src/vault/vault.css` Add or replace the layout section. The existing `.vault-sidebar` and `.vault-pane` rules will be superseded. - [ ] **Step 1: Add 3-column shell + drawer CSS** In `vault.css`, add/replace the shell layout section: ```css /* === 3-column shell === */ .vault-shell { display: flex; height: 100vh; overflow: hidden; background: var(--bg-page, #0d1117); } /* Sidebar */ .vault-sidebar { width: 200px; min-width: 200px; display: flex; flex-direction: column; border-right: 1px solid var(--border, #30363d); background: var(--bg-sidebar, #0d1117); overflow-y: auto; flex-shrink: 0; } /* List pane (flex: 1, fills between sidebar and drawer) */ .vault-list-pane { flex: 1; display: flex; flex-direction: column; overflow-y: auto; min-width: 0; } /* Detail drawer */ .vault-drawer { width: 440px; min-width: 440px; max-width: 440px; border-left: 1px solid var(--border, #30363d); background: var(--bg-elevated, #161b22); overflow-y: auto; transform: translateX(100%); transition: transform 0.2s ease; flex-shrink: 0; } .vault-drawer--open { transform: translateX(0); } /* List rows */ .vault-list-row { display: flex; align-items: center; gap: 10px; padding: 10px 16px; cursor: pointer; border-bottom: 1px solid var(--border-subtle, #21262d); transition: background 0.1s; } .vault-list-row:hover { background: var(--bg-hover, #161b22); } .vault-list-row--selected { background: var(--bg-selected, #1c2d41); border-left: 2px solid var(--gold, #b8860b); } .vault-list-row__icon { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: var(--bg-elevated, #161b22); border-radius: 6px; border: 1px solid var(--border, #30363d); font-size: 14px; flex-shrink: 0; } .vault-list-row--selected .vault-list-row__icon { border-color: var(--gold, #b8860b); } .vault-list-row__text { flex: 1; min-width: 0; } .vault-list-row__title { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .vault-list-row__subtitle { font-size: 11px; color: var(--text-muted, #8b949e); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px; } .vault-list-row__age { font-size: 10px; color: var(--text-dim, #6e7681); flex-shrink: 0; } /* Bottom sheet */ .vault-bottom-sheet-scrim { position: absolute; inset: 0 0 0 200px; /* exclude sidebar */ background: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s; pointer-events: none; z-index: 100; } .vault-bottom-sheet-scrim--visible { opacity: 1; pointer-events: auto; } .vault-bottom-sheet { position: absolute; bottom: 0; left: 200px; /* exclude sidebar */ right: 0; background: var(--bg-elevated, #161b22); border-top: 1px solid var(--border, #30363d); border-radius: 12px 12px 0 0; padding: 16px 24px 24px; transform: translateY(100%); transition: transform 0.25s ease; z-index: 101; max-height: 60vh; overflow-y: auto; } .vault-bottom-sheet--open { transform: translateY(0); } .vault-bottom-sheet__handle { width: 40px; height: 4px; background: var(--border, #30363d); border-radius: 2px; margin: 0 auto 16px; } .vault-bottom-sheet__title { font-size: 14px; font-weight: 600; margin-bottom: 16px; text-align: center; } .vault-type-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px; } .vault-type-card { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 12px 8px; background: var(--bg-page, #0d1117); border: 1px solid var(--border, #30363d); border-radius: 8px; cursor: pointer; transition: border-color 0.15s; gap: 6px; } .vault-type-card:hover { border-color: var(--gold, #b8860b); } .vault-type-card__icon { font-size: 28px; } .vault-type-card__name { font-size: 11px; color: var(--text-muted, #8b949e); } /* Drawer header and body */ .vault-drawer__header { display: flex; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border, #30363d); gap: 8px; } .vault-drawer__type-pill { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; padding: 2px 8px; background: var(--bg-page, #0d1117); border: 1px solid var(--border, #30363d); border-radius: 4px; color: var(--text-muted, #8b949e); } .vault-drawer__actions { display: flex; gap: 6px; margin-left: auto; } .vault-drawer__close { background: transparent; border: none; cursor: pointer; font-size: 16px; color: var(--text-muted, #8b949e); padding: 4px 6px; } .vault-drawer__body { padding: 20px 20px 16px; } .vault-drawer__title { font-size: 18px; font-weight: 700; margin-bottom: 4px; } .vault-drawer__subtitle { font-size: 12px; color: var(--text-muted, #8b949e); margin-bottom: 16px; } .vault-drawer__field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .vault-drawer__field-grid > .vault-drawer__field--full { grid-column: 1 / -1; } .vault-drawer__field-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted, #8b949e); margin-bottom: 2px; } .vault-drawer__field-value { font-size: 13px; word-break: break-all; } /* === Responsive === */ @media (max-width: 960px) { .vault-drawer { position: absolute; right: 0; top: 0; height: 100%; } } @media (max-width: 720px) { .vault-sidebar { width: 48px; min-width: 48px; } .vault-sidebar__category-label, .vault-sidebar__category-count, .vault-sidebar__nav-label { display: none; } .vault-sidebar__nav-item { justify-content: center; padding: 10px 0; } } ``` - [ ] **Step 2: Commit** ```bash git add extension/src/vault/vault.css git commit -m "feat(ext/vault): 3-column layout CSS — drawer, bottom sheet, list rows, responsive" ``` --- ### Task 9: `vault.ts` — 3-column shell + sidebar category nav **Files:** - Modify: `extension/src/vault/vault.ts` This is the largest task. Rewrite `renderShell()`, `renderSidebarList()`, and add the list pane renderer. - [ ] **Step 1: Add `drawerOpen` and `bottomSheetOpen` to VaultState** In the `VaultState` interface, add: ```ts drawerOpen: boolean; bottomSheetOpen: boolean; ``` In the initial state object, add: ```ts drawerOpen: false, bottomSheetOpen: false, ``` - [ ] **Step 2: Rewrite `renderShell()` for 3-column layout** Replace the existing `renderShell()` function: ```ts function renderShell(app: HTMLElement): void { if (!app.querySelector('.vault-shell')) { app.innerHTML = `
Relicario
`; wireSidebar(); wireBottomSheet(); } renderSidebarCategories(); renderListPane(); if (state.drawerOpen && state.selectedItem) { renderDrawer(state.selectedItem); } } ``` **Note:** The `⌬ devices` nav button is intentionally removed here. Once Stream B's Security section lands, devices are accessible via Settings → Security. Until then, users can access devices via the old popup route (still present). Confirm with PM before removing if uncertain. - [ ] **Step 3: Rewrite `renderSidebarList()` → `renderSidebarCategories()`** Rename the function and change it to show type sections with counts (not per-item rows): ```ts function renderSidebarCategories(): void { const container = document.getElementById('vault-categories'); if (!container) return; const filtered = getFilteredEntries(); const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp']; const allCount = filtered.length; const isAllActive = !state.activeGroup && state.view === 'list'; let html = ` `; for (const t of typeOrder) { const count = filtered.filter(([, e]) => e.type === t).length; if (count === 0 && allCount > 0) continue; // hide empty sections unless vault is empty const isActive = state.activeGroup === t; html += ` `; } container.innerHTML = html; container.querySelectorAll('.vault-category-row').forEach((btn) => { btn.addEventListener('click', () => { state.activeGroup = btn.dataset.group || null; state.drawerOpen = false; state.selectedId = null; state.selectedItem = null; renderSidebarCategories(); renderListPane(); closeDrawer(); }); }); } ``` Add to vault.css (after Task 8): ```css .vault-category-row { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 12px; background: transparent; border: none; cursor: pointer; color: inherit; font-size: 13px; text-align: left; } .vault-category-row:hover { background: var(--bg-hover, #161b22); } .vault-category-row--active { background: var(--bg-selected, #1c2d41); } .vault-category-row__icon { font-size: 14px; flex-shrink: 0; } .vault-category-row__label { flex: 1; } .vault-category-row__count { font-size: 11px; color: var(--text-muted, #8b949e); } ``` - [ ] **Step 4: Add `renderListPane()`** ```ts function renderListPane(): void { const pane = document.getElementById('vault-list-pane'); if (!pane) return; const group = state.activeGroup as ItemType | null; let items = getFilteredEntries(); if (group) items = items.filter(([, e]) => e.type === group); if (items.length === 0) { pane.innerHTML = `
${state.searchQuery ? `No results for "${escapeHtml(state.searchQuery)}"` : 'No items yet'}
${state.searchQuery ? 'Try a shorter search term.' : 'Click + new item to get started.'}
`; return; } pane.innerHTML = items.map(([id, e]) => { const sel = id === state.selectedId ? ' vault-list-row--selected' : ''; const subtitle = e.icon_hint ?? (e.tags.length > 0 ? e.tags.join(', ') : ''); const modifiedAgo = e.modified ? relativeTime(e.modified) : ''; return `
${escapeHtml(e.title)}
${subtitle ? `
${escapeHtml(subtitle)}
` : ''}
${modifiedAgo ? `
${escapeHtml(modifiedAgo)}
` : ''}
`; }).join(''); pane.querySelectorAll('.vault-list-row').forEach((row) => { row.addEventListener('click', async () => { await selectItemForDrawer(row.dataset.id!); }); }); } function relativeTime(unixSec: number): string { const diffS = Math.floor(Date.now() / 1000) - unixSec; if (diffS < 60) return 'just now'; if (diffS < 3600) return `${Math.floor(diffS / 60)}m ago`; if (diffS < 86400) return `${Math.floor(diffS / 3600)}h ago`; return `${Math.floor(diffS / 86400)}d ago`; } ``` - [ ] **Step 5: Build** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" ``` - [ ] **Step 6: Commit** ```bash git add extension/src/vault/vault.ts extension/src/vault/vault.css git commit -m "feat(ext/vault): 3-column shell — sidebar category nav + list pane" ``` --- ### Task 10: `vault.ts` — detail drawer **Files:** - Modify: `extension/src/vault/vault.ts` - [ ] **Step 1: Add `selectItemForDrawer()` and drawer render/close functions** ```ts async function selectItemForDrawer(id: ItemId): Promise { const resp = await sendMessage({ type: 'get_item', id }); if (!resp.ok) return; const data = resp.data as { item: Item }; state.selectedId = id; state.selectedItem = data.item; state.drawerOpen = true; renderSidebarCategories(); renderListPane(); renderDrawer(data.item); openDrawer(); } function openDrawer(): void { document.getElementById('vault-drawer')?.classList.add('vault-drawer--open'); } function closeDrawer(): void { state.drawerOpen = false; state.selectedId = null; state.selectedItem = null; document.getElementById('vault-drawer')?.classList.remove('vault-drawer--open'); } function renderDrawer(item: Item): void { const drawer = document.getElementById('vault-drawer'); if (!drawer) return; const coreFields = getDrawerCoreFields(item); drawer.innerHTML = `
${item.type.replace('_', ' ').toUpperCase()}
${escapeHtml(item.title)}
${item.core && 'url' in item.core ? `
${escapeHtml((item.core as { url?: string }).url ?? '')}
` : ''}
${coreFields.map(([label, value, full]) => `
${escapeHtml(label)}
${escapeHtml(value)}
`).join('')}
`; document.getElementById('drawer-close-btn')?.addEventListener('click', () => { closeDrawer(); renderListPane(); }); document.getElementById('drawer-edit-btn')?.addEventListener('click', () => { setHash('edit', state.selectedId!); renderPane(); }); } function getDrawerCoreFields(item: Item): Array<[string, string, boolean]> { // Returns [label, value, fullWidth] tuples for the core type fields const core = item.core as Record; if (!core) return []; const fields: Array<[string, string, boolean]> = []; if ('username' in core) fields.push(['username', String(core.username ?? ''), false]); if ('password' in core) fields.push(['password', '••••••••', false]); if ('url' in core) fields.push(['url', String(core.url ?? ''), true]); if ('number' in core) fields.push(['number', String(core.number ?? ''), false]); if ('expiry' in core) fields.push(['expiry', String(core.expiry ?? ''), false]); if ('cardholder' in core) fields.push(['cardholder', String(core.cardholder ?? ''), true]); if (item.notes) fields.push(['notes', item.notes, true]); return fields; } ``` - [ ] **Step 2: Add Esc key handler to close drawer** In `wireSidebar()` or the shell setup, add: ```ts document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && state.drawerOpen) { closeDrawer(); renderListPane(); } }); ``` - [ ] **Step 3: Build** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" ``` - [ ] **Step 4: Commit** ```bash git add extension/src/vault/vault.ts git commit -m "feat(ext/vault): detail drawer — open/close state + core fields display" ``` --- ### Task 11: `vault.ts` — bottom sheet for new item type picker **Files:** - Modify: `extension/src/vault/vault.ts` - [ ] **Step 1: Add `wireBottomSheet()` and sheet open/close functions** ```ts const BOTTOM_SHEET_TYPES: Array<{ type: ItemType; label: string }> = [ { type: 'login', label: 'Login' }, { type: 'secure_note', label: 'Secure Note' }, { type: 'totp', label: 'TOTP' }, { type: 'card', label: 'Card' }, { type: 'identity', label: 'Identity' }, { type: 'key', label: 'SSH / API Key' }, { type: 'document', label: 'Document' }, ]; function openBottomSheet(): void { const sheet = document.getElementById('vault-bottom-sheet')!; const scrim = document.getElementById('vault-sheet-scrim')!; sheet.innerHTML = `
New item — choose type
${BOTTOM_SHEET_TYPES.map((t) => ` `).join('')}
`; sheet.classList.add('vault-bottom-sheet--open'); scrim.classList.add('vault-bottom-sheet-scrim--visible'); state.bottomSheetOpen = true; sheet.querySelectorAll('[data-type]').forEach((btn) => { btn.addEventListener('click', () => { const type = btn.dataset.type as ItemType; closeBottomSheet(); state.newType = type; setHash('add', type); renderPane(); }); }); } function closeBottomSheet(): void { document.getElementById('vault-bottom-sheet')?.classList.remove('vault-bottom-sheet--open'); document.getElementById('vault-sheet-scrim')?.classList.remove('vault-bottom-sheet-scrim--visible'); state.bottomSheetOpen = false; } function wireBottomSheet(): void { document.getElementById('vault-sheet-scrim')?.addEventListener('click', closeBottomSheet); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && state.bottomSheetOpen) closeBottomSheet(); }); } ``` - [ ] **Step 2: Update the `data-nav="add"` handler to open the sheet** In `wireSidebar()`, find the `nav === 'add'` branch and change it to call `openBottomSheet()` instead of directly calling `setHash('add')`: ```ts if (nav === 'add') { openBottomSheet(); return; } ``` - [ ] **Step 3: Build and run tests** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" bun run test 2>&1 | tail -10 ``` - [ ] **Step 4: Commit** ```bash git add extension/src/vault/vault.ts git commit -m "feat(ext/vault): bottom sheet type picker for new item" ``` --- ### Task 12: Wire settings component (interface contract with DEV-B) **Files:** - Modify: `extension/src/vault/vault.ts` - [ ] **Step 1: Add settings import** Add to vault.ts imports: ```ts import { renderSettings, teardownSettings } from '../popup/components/settings'; ``` - [ ] **Step 2: Wire in `renderPane()` for the settings view** Find the `renderPane()` function and ensure the `settings` case calls `renderSettings`: ```ts case 'settings': teardownSettings(); await renderSettings(pane); return; ``` And ensure `teardownSettings()` is called when navigating away from settings (wherever view transitions happen). - [ ] **Step 3: Build** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS" ``` If DEV-B's settings.ts doesn't yet export `renderSettings` / `teardownSettings`, add a temporary stub to `settings.ts` on this branch to unblock compilation — note it clearly in a comment for DEV-B. This stub will be replaced at merge time. - [ ] **Step 4: Commit** ```bash git add extension/src/vault/vault.ts git commit -m "feat(ext/vault): wire renderSettings / teardownSettings from settings component" ``` --- ### Task 13: Full build + test pass - [ ] **Step 1: Run all tests** ```bash cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | tail -20 ``` Expected: all existing tests pass. If any fail, fix before proceeding. - [ ] **Step 2: Build both targets** ```bash bun run build 2>&1 | tail -10 bun run build:firefox 2>&1 | tail -10 ``` Expected: both build clean (only pre-existing bundle-size warnings). - [ ] **Step 3: Grep for remaining emoji in extension/src/** ```bash grep -rn '\U0001F\|🔑\|📝\|🪪\|💳\|🗝\|📄\|⏱️\|🖥\|🔐\|📎' /home/alee/Sources/relicario.v0.5.1-stream-a/extension/src/ 2>/dev/null ``` Expected: no output (all emoji replaced with glyphs). - [ ] **Step 4: Commit any remaining fixes, then open PR** ```bash gh pr create --title "feat: fullscreen 3-column layout + popup polish (Stream A)" --base main ``` - [ ] **Step 5: Post status to PM** ``` ## STATUS UPDATE — DEV-A Time: Task: 13 of 13 Status: REVIEW-READY Summary: All 13 tasks complete. PR open. Build clean. All tests pass. No emoji remaining in extension/src/. Next: waiting for PM ```