Files
relicario/docs/superpowers/coordination/v0.5.1-dev-a-prompt.md
adlee-was-taken 450de33c0a docs(coordination): architecture-review kickoff prompts + followup planning
Adds the four kickoff prompts that drove the 2026-05-04 whole-codebase
architecture audit (PM + DEV-A/B/C reviewers), the planning prompt
that converts the synthesis into three implementation plans, and the
PM + DEV-A/B/C kickoff prompts for executing those plans in parallel.

Also updates the existing v0.5.1-* prompts with the relay-server
fallback section that references the new tools/relay/call.py shim.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 17:49:34 -04:00

1465 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 A1A7
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<void>;
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:** A1A7 (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.
## Relay server
A message-bus MCP server is running on `localhost:7331`. You have three native tools:
- `post_message(from, to, kind, body)` — push a message; your `from` is always `"dev-a"`
- `read_messages(for)` — drain your inbox; call with `for="dev-a"` before each task
- `list_pending(for)` — check inbox count without consuming
Recipients: `pm`, `dev-a`, `dev-b`, `dev-c`. Use these instead of asking the user to copy-paste. Before starting each task: `read_messages(for="dev-a")`. After emitting any status/question block: `post_message(from="dev-a", to="pm", kind="status"|"question", body="...")`.
## Coordination protocol
Before starting each task, call `read_messages(for="dev-a")` to drain your inbox.
When posting a status update, call `post_message(from="dev-a", to="pm", kind="status", body="...")` with the body:
```
## STATUS UPDATE — DEV-A
Time: <iso8601>
Task: <N of 14>
Status: IN-PROGRESS | BLOCKED | REVIEW-READY
Summary: <one line>
Next: <next task or "waiting for PM">
```
---
## 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 `&#x2934;` 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:
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">&#x2934;</button>
// New:
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">${GLYPH_VAULT_TAB}</button>
```
- [ ] **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 &#x2934; 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 ~1626) 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 ? ' <span class="entry-row__attach-indicator" title="has attachments">📎</span>' : ''}
// New:
${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">⊕</span>' : ''}
```
- [ ] **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 `'<div class="empty">no items</div>'`. Split into:
```ts
function buildRowsHtml(): string {
const state = getState();
const filtered = getFilteredEntries();
if (filtered.length === 0) {
if (state.searchQuery) {
return `
<div class="empty-state">
<span class="empty-state__icon" aria-hidden="true">⊘</span>
<div class="empty-state__title">No results for "${escapeHtml(state.searchQuery)}"</div>
<div class="empty-state__hint">Try a shorter search term.</div>
</div>
`;
}
return `
<div class="empty-state">
<span class="empty-state__icon" aria-hidden="true">◈</span>
<div class="empty-state__title">No items yet</div>
<div class="empty-state__hint">Press <kbd>+</kbd> to add your first item.</div>
</div>
`;
}
return filtered.map(([id, e], i) => `
<div class="entry-row ${i === state.selectedIndex ? 'selected' : ''}" data-id="${escapeHtml(id)}" data-index="${i}">
<span class="entry-name"><span class="type-icon" aria-hidden="true">${typeIcon(e.type)}</span> ${escapeHtml(e.title)}${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">⊕</span>' : ''}</span>
<span class="entry-meta">${escapeHtml(metaLine(e))}</span>
</div>
`).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 = `
<div class="pad">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
<button class="btn" id="back-btn">◂ back</button>
<span style="font-size:14px; font-weight:600;">New item</span>
<span style="flex:1;"></span>
${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⧉</button>'}
</div>
<div class="type-card-grid">
${TYPE_OPTIONS.map((opt) => `
<button class="type-card" data-type="${opt.type}">
<span class="type-card__icon" aria-hidden="true">${opt.icon}</span>
<span class="type-card__label">${escapeHtml(opt.label)}</span>
<span class="type-card__desc">${escapeHtml(opt.description)}</span>
</button>
`).join('')}
</div>
<div class="keyhints"><span><kbd>Esc</kbd> back</span></div>
</div>
`;
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<HTMLButtonElement>('[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<HTMLElement>('.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 = `
<div class="vault-shell">
<div class="vault-sidebar">
<div class="vault-sidebar__header">
<img class="brand-logo" src="icons/relicario-logo-16.svg" alt="">
<span class="brand">Relicario</span>
</div>
<div class="vault-sidebar__search">
<input type="text" id="vault-search" placeholder="/ search…" />
</div>
<nav class="vault-sidebar__categories" id="vault-categories" aria-label="Item types"></nav>
<div class="vault-sidebar__nav">
<button class="vault-sidebar__nav-item" data-nav="add" title="New item">+ new item</button>
<button class="vault-sidebar__nav-item" data-nav="trash" title="Trash">${GLYPH_TRASH} <span class="vault-sidebar__nav-label">trash</span></button>
<button class="vault-sidebar__nav-item" data-nav="settings" title="Settings">${GLYPH_SETTINGS} <span class="vault-sidebar__nav-label">settings</span></button>
<button class="vault-sidebar__nav-item" data-nav="lock" title="Lock">${GLYPH_LOCK} <span class="vault-sidebar__nav-label">lock</span></button>
</div>
</div>
<div class="vault-list-pane" id="vault-list-pane"></div>
<div class="vault-drawer" id="vault-drawer"></div>
<div class="vault-bottom-sheet-scrim" id="vault-sheet-scrim"></div>
<div class="vault-bottom-sheet" id="vault-bottom-sheet"></div>
</div>
`;
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 = `
<button class="vault-category-row ${isAllActive ? 'vault-category-row--active' : ''}" data-group="">
<span class="vault-category-row__icon">◈</span>
<span class="vault-category-row__label">All items</span>
<span class="vault-category-row__count">${allCount}</span>
</button>
`;
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 += `
<button class="vault-category-row ${isActive ? 'vault-category-row--active' : ''}" data-group="${t}">
<span class="vault-category-row__icon">${typeIcon(t)}</span>
<span class="vault-category-row__label vault-sidebar__category-label">${typeLabel(t)}</span>
<span class="vault-category-row__count vault-sidebar__category-count">${count}</span>
</button>
`;
}
container.innerHTML = html;
container.querySelectorAll<HTMLButtonElement>('.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 = `
<div class="empty-state">
<span class="empty-state__icon" aria-hidden="true">${state.searchQuery ? '⊘' : '◈'}</span>
<div class="empty-state__title">${state.searchQuery ? `No results for "${escapeHtml(state.searchQuery)}"` : 'No items yet'}</div>
<div class="empty-state__hint">${state.searchQuery ? 'Try a shorter search term.' : 'Click + new item to get started.'}</div>
</div>
`;
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 `
<div class="vault-list-row${sel}" data-id="${escapeHtml(id)}">
<div class="vault-list-row__icon" aria-hidden="true">${typeIcon(e.type)}</div>
<div class="vault-list-row__text">
<div class="vault-list-row__title">${escapeHtml(e.title)}</div>
${subtitle ? `<div class="vault-list-row__subtitle">${escapeHtml(subtitle)}</div>` : ''}
</div>
${modifiedAgo ? `<div class="vault-list-row__age">${escapeHtml(modifiedAgo)}</div>` : ''}
</div>
`;
}).join('');
pane.querySelectorAll<HTMLElement>('.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<void> {
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 = `
<div class="vault-drawer__header">
<span class="vault-drawer__type-pill">${item.type.replace('_', ' ').toUpperCase()}</span>
<div class="vault-drawer__actions">
<button class="btn" id="drawer-edit-btn" style="font-size:11px;">edit</button>
<button class="vault-drawer__close" id="drawer-close-btn" title="Close (Esc)">✕</button>
</div>
</div>
<div class="vault-drawer__body">
<div class="vault-drawer__title">${escapeHtml(item.title)}</div>
${item.core && 'url' in item.core ? `<div class="vault-drawer__subtitle">${escapeHtml((item.core as { url?: string }).url ?? '')}</div>` : ''}
<div class="vault-drawer__field-grid">
${coreFields.map(([label, value, full]) => `
<div class="vault-drawer__field${full ? ' vault-drawer__field--full' : ''}">
<div class="vault-drawer__field-label">${escapeHtml(label)}</div>
<div class="vault-drawer__field-value">${escapeHtml(value)}</div>
</div>
`).join('')}
</div>
</div>
`;
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<string, unknown>;
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 = `
<div class="vault-bottom-sheet__handle"></div>
<div class="vault-bottom-sheet__title">New item — choose type</div>
<div class="vault-type-grid">
${BOTTOM_SHEET_TYPES.map((t) => `
<button class="vault-type-card" data-type="${t.type}">
<span class="vault-type-card__icon" aria-hidden="true">${typeIcon(t.type)}</span>
<span class="vault-type-card__name">${escapeHtml(t.label)}</span>
</button>
`).join('')}
</div>
`;
sheet.classList.add('vault-bottom-sheet--open');
scrim.classList.add('vault-bottom-sheet-scrim--visible');
state.bottomSheetOpen = true;
sheet.querySelectorAll<HTMLButtonElement>('[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**
Call `post_message(from="dev-a", to="pm", kind="status", body="...")` with:
```
## STATUS UPDATE — DEV-A
Time: <iso8601>
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
```