- 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>
1449 lines
43 KiB
Markdown
1449 lines
43 KiB
Markdown
# 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<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.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: <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 `⤴` 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)">⤴</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 ⤴ 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 ? ' <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**
|
||
|
||
```
|
||
## 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
|
||
```
|