- coordination/v0.5.1-pm-prompt.md — PM coordinates 3 streams, enforces interface contracts (A-B settings signature, B-C security component), owns merge order and pre-tag checklist - coordination/v0.5.1-dev-a-prompt.md — Stream A: fullscreen 3-column layout, sidebar category nav, detail drawer, bottom sheet, popup type- picker polish, per-type glyph icons, empty states, toast system (13 tasks) - coordination/v0.5.1-dev-b-prompt.md — Stream B: settings left-nav redesign (Autofill, Display, Security, Generator, Retention, Backup, Import sections), security component stub (10 tasks) - coordination/v0.5.1-dev-c-prompt.md — Stream C: recovery_qr.rs core, WASM session expansion, CLI subcommand, settings-security.ts three-state component, setup wizard Style C redesign + QR banner (12 tasks) - Archive v0.5.0 coordination files to coordination/archive/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
43 KiB
Dev A Kickoff Prompt — v0.5.1 Stream A (Fullscreen + Popup Layout)
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto 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)
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
CLAUDE.md— project rulesdocs/superpowers/specs/2026-05-03-v0.5.x-ux-polish-and-recovery-qr-design.md— spec sections A1–A7extension/src/vault/vault.ts— current implementation (read fully before editing)extension/src/vault/vault.css— current stylesextension/src/popup/components/item-list.ts— popup item listextension/src/popup/components/item-form.ts— type-pickerextension/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:
// 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: 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.tsis 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: <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 constantsextension/src/shared/__tests__/glyphs.test.ts— add new constants to the testextension/src/popup/components/item-list.ts— glyph vault-btn, type icons, empty states, toastextension/src/popup/components/item-form.ts— TYPE_OPTIONS glyphs, polished type pickerextension/src/vault/vault.ts— full layout rewriteextension/src/vault/vault.css— 3-column layout, drawer, bottom sheet, responsiveextension/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:
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
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:
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
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
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:
import { GLYPH_VAULT_TAB } from '../../shared/glyphs';
- Step 2: Replace the inline HTML entity
In item-list.ts:69, change:
// Old:
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">⤴</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
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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
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:
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():
// 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
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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:
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:
.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
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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:
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
renderTypeSelectionto a 2-column card grid
Replace the current renderTypeSelection function's HTML:
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:
.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
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
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
// 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:
/* 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:
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
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
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:
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:
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
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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:
/* === 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
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
drawerOpenandbottomSheetOpento VaultState
In the VaultState interface, add:
drawerOpen: boolean;
bottomSheetOpen: boolean;
In the initial state object, add:
drawerOpen: false,
bottomSheetOpen: false,
- Step 2: Rewrite
renderShell()for 3-column layout
Replace the existing renderShell() function:
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):
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):
.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()
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
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
- Step 6: Commit
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
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:
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && state.drawerOpen) {
closeDrawer();
renderListPane();
}
});
- Step 3: Build
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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
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'):
if (nav === 'add') {
openBottomSheet();
return;
}
- Step 3: Build and run tests
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
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:
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:
case 'settings':
teardownSettings();
await renderSettings(pane);
return;
And ensure teardownSettings() is called when navigating away from settings (wherever view transitions happen).
- Step 3: Build
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
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
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
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/
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
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: <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