- 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>
1075 lines
37 KiB
Markdown
1075 lines
37 KiB
Markdown
# Dev B Kickoff Prompt — v0.5.1 Stream B (Settings UX Redesign)
|
||
|
||
> **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 B for the Relicario v0.5.1 release. Stream B replaces the current flat settings dump with a unified left-nav sectioned settings page, integrating device and vault settings into a single coherent view.
|
||
|
||
**Goal:** Replace `settings.ts` (flat dump) + `settings-vault.ts` (flat dump) with a single settings component that has a left-nav sidebar (Autofill, Display, Security, Generator, Retention, Backup, Import). Wire into `vault.ts` via the agreed interface. Wire `settings-security.ts` from DEV-C into the Security section.
|
||
|
||
**Architecture:** All changes are in the extension. The settings component lives in `settings.ts` (rewrite). Section content for vault-level settings moves from `settings-vault.ts` into inline render functions within the new layout. DEV-C owns `settings-security.ts`; you stub it and import it.
|
||
|
||
**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-b -b feature/v0.5.1-stream-b-settings
|
||
cd ../relicario.v0.5.1-stream-b
|
||
pwd # should print /home/alee/Sources/relicario.v0.5.1-stream-b
|
||
```
|
||
|
||
**ALL subsequent work happens in `/home/alee/Sources/relicario.v0.5.1-stream-b`**. Every subagent prompt MUST begin with `cd /home/alee/Sources/relicario.v0.5.1-stream-b`.
|
||
|
||
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 B1–B8
|
||
3. `extension/src/popup/components/settings.ts` — current flat settings (your rewrite target)
|
||
4. `extension/src/popup/components/settings-vault.ts` — vault settings content to decompose
|
||
5. `extension/src/popup/styles.css` — existing CSS (add to, don't break)
|
||
|
||
## 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-b`.
|
||
|
||
## Interface contracts
|
||
|
||
### With DEV-A (vault.ts wiring)
|
||
|
||
DEV-A imports these exact exports from `settings.ts` in vault.ts. **You must use exactly these signatures:**
|
||
|
||
```ts
|
||
export async function renderSettings(container: HTMLElement): Promise<void>;
|
||
export function teardownSettings(): void;
|
||
```
|
||
|
||
DEV-A calls `renderSettings(pane)` when the settings view is active and `teardownSettings()` when navigating away.
|
||
|
||
### With DEV-C (settings-security.ts)
|
||
|
||
DEV-C owns `settings-security.ts`. You import it for the Security section. The agreed interface:
|
||
|
||
```ts
|
||
// extension/src/popup/components/settings-security.ts
|
||
export async function renderSecuritySection(
|
||
container: HTMLElement,
|
||
sessionHandle: number | null,
|
||
): Promise<void>;
|
||
export function teardownSecuritySection(): void;
|
||
```
|
||
|
||
**Task 1** creates a stub `settings-security.ts` with this interface (no-op implementations). DEV-C will replace it on their branch; the real implementation lands when C merges.
|
||
|
||
## Scope and boundaries
|
||
|
||
**In scope:** B1–B8 (settings skeleton, all 7 sections, cleanup of old settings-vault.ts surface).
|
||
|
||
**Out of scope:** Stream A and C work. The Security section's QR and device UI is DEV-C's responsibility — you only wire the import.
|
||
|
||
**Hard rules:**
|
||
- `renderSettings` / `teardownSettings` must be exported with these exact names and signatures.
|
||
- Device sections read/write `chrome.storage.local`. Vault sections call `sendMessage` to the service worker.
|
||
- Don't merge to main. The PM owns merges.
|
||
|
||
## Coordination protocol
|
||
|
||
```
|
||
## STATUS UPDATE — DEV-B
|
||
Time: <iso8601>
|
||
Task: <N of 10>
|
||
Status: IN-PROGRESS | BLOCKED | REVIEW-READY
|
||
Summary: <one line>
|
||
Next: <next task or "waiting for PM">
|
||
```
|
||
|
||
---
|
||
|
||
## Files
|
||
|
||
**Create:**
|
||
- `extension/src/popup/components/settings-security.ts` — stub (DEV-C replaces real implementation)
|
||
- `extension/src/popup/components/__tests__/settings-nav.test.ts` — left-nav structure tests
|
||
|
||
**Modify:**
|
||
- `extension/src/popup/components/settings.ts` — full rewrite as sectioned layout
|
||
- `extension/src/popup/components/settings-vault.ts` — decompose into section functions; may be removed at the end
|
||
- `extension/src/popup/styles.css` — settings-nav CSS + section styles
|
||
|
||
---
|
||
|
||
### Task 1: Stub `settings-security.ts`
|
||
|
||
This stub satisfies the import used in the Security section (Task 6). DEV-C replaces it with the real implementation.
|
||
|
||
**Files:**
|
||
- Create: `extension/src/popup/components/settings-security.ts`
|
||
|
||
- [ ] **Step 1: Write the stub**
|
||
|
||
```ts
|
||
// extension/src/popup/components/settings-security.ts
|
||
// Stub — real implementation provided by Stream C (DEV-C).
|
||
|
||
export async function renderSecuritySection(
|
||
container: HTMLElement,
|
||
_sessionHandle: number | null,
|
||
): Promise<void> {
|
||
container.innerHTML = `
|
||
<div class="settings-section-placeholder">
|
||
<span class="muted">Security settings — loading…</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
export function teardownSecuritySection(): void {
|
||
// no-op in stub
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Build to confirm no TS errors**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings-security.ts
|
||
git commit -m "chore(ext/settings): stub settings-security.ts (DEV-C replaces implementation)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Settings left-nav skeleton
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
- Modify: `extension/src/popup/styles.css`
|
||
|
||
The new settings.ts is a full rewrite. It replaces the current flat dump with a two-panel layout: a 148px left-nav sidebar + content area.
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Create `extension/src/popup/components/__tests__/settings-nav.test.ts`:
|
||
|
||
```ts
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
||
// Mock chrome.storage.local
|
||
const storageMock: Record<string, unknown> = {};
|
||
vi.stubGlobal('chrome', {
|
||
storage: {
|
||
local: {
|
||
get: vi.fn((keys, cb) => cb(typeof keys === 'string' ? { [keys]: storageMock[keys] } : Object.fromEntries((keys as string[]).map(k => [k, storageMock[k]])))),
|
||
set: vi.fn((data, cb) => { Object.assign(storageMock, data); cb?.(); }),
|
||
},
|
||
},
|
||
});
|
||
|
||
// We can't easily test the full render (no DOM env yet), so we test the
|
||
// structural contract — the exported function names exist.
|
||
import * as settingsMod from '../settings';
|
||
|
||
describe('settings module contract', () => {
|
||
it('exports renderSettings as an async function', () => {
|
||
expect(typeof settingsMod.renderSettings).toBe('function');
|
||
});
|
||
|
||
it('exports teardownSettings as a function', () => {
|
||
expect(typeof settingsMod.teardownSettings).toBe('function');
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run to confirm the test currently passes (contract already met)**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run test 2>&1 | grep "settings-nav"
|
||
```
|
||
|
||
The test may already pass if current settings.ts exports `renderSettings`. If so, proceed — the test ensures the contract is preserved through the rewrite.
|
||
|
||
- [ ] **Step 3: Rewrite settings.ts with the new sectioned layout**
|
||
|
||
```ts
|
||
// extension/src/popup/components/settings.ts
|
||
|
||
import { sendMessage, escapeHtml } from '../../shared/state';
|
||
import type { DeviceSettings, VaultSettings } from '../../shared/types';
|
||
import {
|
||
loadColorScheme, saveColorScheme, resetColorScheme,
|
||
DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR,
|
||
} from '../../shared/color-scheme';
|
||
import { colorizePassword } from '../../shared/password-coloring';
|
||
import { openGeneratorPanel, closeGeneratorPanel } from './generator-panel';
|
||
import { renderSecuritySection, teardownSecuritySection } from './settings-security';
|
||
|
||
type SettingsSection =
|
||
| 'autofill'
|
||
| 'display'
|
||
| 'security'
|
||
| 'generator'
|
||
| 'retention'
|
||
| 'backup'
|
||
| 'import';
|
||
|
||
const NAV_ITEMS: Array<{ id: SettingsSection; icon: string; label: string; group: 'device' | 'vault' }> = [
|
||
{ id: 'autofill', icon: '⊙', label: 'Autofill', group: 'device' },
|
||
{ id: 'display', icon: '◈', label: 'Display', group: 'device' },
|
||
{ id: 'security', icon: '◉', label: 'Security', group: 'vault' },
|
||
{ id: 'generator', icon: '↻', label: 'Generator', group: 'vault' },
|
||
{ id: 'retention', icon: '▦', label: 'Retention', group: 'vault' },
|
||
{ id: 'backup', icon: '⤓', label: 'Backup', group: 'vault' },
|
||
{ id: 'import', icon: '≡', label: 'Import', group: 'vault' },
|
||
];
|
||
|
||
let activeSection: SettingsSection = 'autofill';
|
||
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
||
let pendingVaultSettings: VaultSettings | null = null;
|
||
|
||
export async function renderSettings(container: HTMLElement): Promise<void> {
|
||
container.innerHTML = `
|
||
<div class="settings-layout">
|
||
<nav class="settings-nav" id="settings-nav">
|
||
<div class="settings-nav__group-label">Device</div>
|
||
${NAV_ITEMS.filter(n => n.group === 'device').map(navItemHtml).join('')}
|
||
<div class="settings-nav__group-label">Vault</div>
|
||
${NAV_ITEMS.filter(n => n.group === 'vault').map(navItemHtml).join('')}
|
||
</nav>
|
||
<div class="settings-content" id="settings-content"></div>
|
||
</div>
|
||
`;
|
||
|
||
wireNav();
|
||
await renderSection(activeSection);
|
||
}
|
||
|
||
export function teardownSettings(): void {
|
||
closeGeneratorPanel();
|
||
teardownSecuritySection();
|
||
if (activeKeyHandler) {
|
||
document.removeEventListener('keydown', activeKeyHandler);
|
||
activeKeyHandler = null;
|
||
}
|
||
pendingVaultSettings = null;
|
||
}
|
||
|
||
function navItemHtml(item: (typeof NAV_ITEMS)[0]): string {
|
||
const active = item.id === activeSection ? ' settings-nav__item--active' : '';
|
||
return `
|
||
<button class="settings-nav__item${active}" data-section="${item.id}">
|
||
<span class="settings-nav__icon" aria-hidden="true">${item.icon}</span>
|
||
<span class="settings-nav__label">${escapeHtml(item.label)}</span>
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
function wireNav(): void {
|
||
document.getElementById('settings-nav')?.querySelectorAll<HTMLButtonElement>('[data-section]')
|
||
.forEach((btn) => {
|
||
btn.addEventListener('click', async () => {
|
||
teardownSecuritySection();
|
||
closeGeneratorPanel();
|
||
activeSection = btn.dataset.section as SettingsSection;
|
||
// Update active state
|
||
document.querySelectorAll('.settings-nav__item').forEach(b => b.classList.remove('settings-nav__item--active'));
|
||
btn.classList.add('settings-nav__item--active');
|
||
await renderSection(activeSection);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function renderSection(section: SettingsSection): Promise<void> {
|
||
const content = document.getElementById('settings-content');
|
||
if (!content) return;
|
||
|
||
switch (section) {
|
||
case 'autofill': return renderAutofillSection(content);
|
||
case 'display': return renderDisplaySection(content);
|
||
case 'security': return renderSecuritySection(content, null); // sessionHandle: null until wired from vault.ts
|
||
case 'generator': return renderGeneratorSection(content);
|
||
case 'retention': return renderRetentionSection(content);
|
||
case 'backup': return renderBackupSection(content);
|
||
case 'import': return renderImportSection(content);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Add settings layout CSS**
|
||
|
||
In `extension/src/popup/styles.css`:
|
||
|
||
```css
|
||
/* === Settings layout === */
|
||
.settings-layout {
|
||
display: flex;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.settings-nav {
|
||
width: 148px;
|
||
min-width: 148px;
|
||
border-right: 1px solid var(--border, #30363d);
|
||
padding: 12px 0;
|
||
overflow-y: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.settings-nav__group-label {
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--text-muted, #8b949e);
|
||
padding: 8px 12px 4px;
|
||
}
|
||
|
||
.settings-nav__item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
padding: 7px 12px;
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: inherit;
|
||
text-align: left;
|
||
}
|
||
|
||
.settings-nav__item:hover { background: var(--bg-hover, #161b22); }
|
||
.settings-nav__item--active { background: var(--bg-selected, #1c2d41); }
|
||
|
||
.settings-nav__icon { font-size: 14px; flex-shrink: 0; }
|
||
|
||
.settings-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px 24px;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Setting row (label + description + control) */
|
||
.setting-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid var(--border-subtle, #21262d);
|
||
}
|
||
|
||
.setting-row:last-child { border-bottom: none; }
|
||
|
||
.setting-row__info { flex: 1; }
|
||
.setting-row__title { font-size: 13px; font-weight: 500; }
|
||
.setting-row__desc { font-size: 11px; color: var(--text-muted, #8b949e); margin-top: 2px; }
|
||
.setting-row__control { flex-shrink: 0; }
|
||
|
||
/* Section headings */
|
||
.settings-section-title {
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--text-muted, #8b949e);
|
||
margin: 0 0 12px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px solid var(--border, #30363d);
|
||
}
|
||
|
||
/* Setting card (used by Security section) */
|
||
.setting-card {
|
||
padding: 12px 16px;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--border, #30363d);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.setting-card--ok { border-color: var(--success, #238636); background: rgba(35, 134, 54, 0.06); }
|
||
.setting-card--warn { border-color: var(--gold, #b8860b); background: rgba(184, 134, 11, 0.06); }
|
||
|
||
.setting-card__status { font-size: 13px; margin-bottom: 8px; }
|
||
.setting-card__actions { display: flex; gap: 8px; }
|
||
```
|
||
|
||
- [ ] **Step 5: Build and run tests**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
bun run test 2>&1 | grep "settings-nav"
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts extension/src/popup/styles.css extension/src/popup/components/__tests__/settings-nav.test.ts
|
||
git commit -m "feat(ext/settings): settings left-nav skeleton with section routing"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Autofill section (Device)
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
|
||
- [ ] **Step 1: Implement `renderAutofillSection()`**
|
||
|
||
Add this function to settings.ts:
|
||
|
||
```ts
|
||
async function renderAutofillSection(content: HTMLElement): Promise<void> {
|
||
const [settingsResp, blacklistResp] = await Promise.all([
|
||
sendMessage({ type: 'get_settings' }),
|
||
sendMessage({ type: 'get_blacklist' }),
|
||
]);
|
||
|
||
const settings: DeviceSettings = settingsResp.ok
|
||
? (settingsResp.data as { settings: DeviceSettings }).settings
|
||
: { captureEnabled: false, captureStyle: 'bar' };
|
||
|
||
const blacklist: string[] = blacklistResp.ok
|
||
? (blacklistResp.data as { blacklist: string[] }).blacklist
|
||
: [];
|
||
|
||
content.innerHTML = `
|
||
<h3 class="settings-section-title">Capture</h3>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Auto-detect logins</div>
|
||
<div class="setting-row__desc">Show a prompt when a login form is detected.</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<label class="toggle">
|
||
<input type="checkbox" id="capture-enabled" ${settings.captureEnabled ? 'checked' : ''}>
|
||
<span class="toggle__track"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Prompt style</div>
|
||
<div class="setting-row__desc">How to prompt when a login is detected.</div>
|
||
</div>
|
||
<div class="setting-row__control" style="display:flex; gap:6px;">
|
||
<button class="btn ${settings.captureStyle === 'bar' ? 'btn-active' : ''}" id="style-bar" style="font-size:11px;">bar</button>
|
||
<button class="btn ${settings.captureStyle === 'toast' ? 'btn-active' : ''}" id="style-toast" style="font-size:11px;">toast</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 class="settings-section-title" style="margin-top:20px;">Blocked sites</h3>
|
||
<div id="blacklist-container">
|
||
${blacklist.length > 0
|
||
? blacklist.map((h) => `
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">${escapeHtml(h)}</div>
|
||
</div>
|
||
<button class="btn btn-danger remove-bl" data-hostname="${escapeHtml(h)}" style="font-size:11px;">remove</button>
|
||
</div>
|
||
`).join('')
|
||
: '<p class="muted" style="font-size:12px;">No blocked sites.</p>'}
|
||
</div>
|
||
<div style="display:flex; gap:6px; margin-top:8px;">
|
||
<input type="text" id="bl-add-input" placeholder="example.com" style="flex:1; font-size:12px;">
|
||
<button class="btn" id="bl-add-btn" style="font-size:11px;">Add</button>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('capture-enabled')?.addEventListener('change', async (e) => {
|
||
const enabled = (e.target as HTMLInputElement).checked;
|
||
await sendMessage({ type: 'save_settings', settings: { ...settings, captureEnabled: enabled } });
|
||
});
|
||
|
||
document.getElementById('style-bar')?.addEventListener('click', async () => {
|
||
await sendMessage({ type: 'save_settings', settings: { ...settings, captureStyle: 'bar' } });
|
||
renderAutofillSection(content);
|
||
});
|
||
|
||
document.getElementById('style-toast')?.addEventListener('click', async () => {
|
||
await sendMessage({ type: 'save_settings', settings: { ...settings, captureStyle: 'toast' } });
|
||
renderAutofillSection(content);
|
||
});
|
||
|
||
content.querySelectorAll<HTMLButtonElement>('.remove-bl').forEach((btn) => {
|
||
btn.addEventListener('click', async () => {
|
||
const host = btn.dataset.hostname!;
|
||
await sendMessage({ type: 'remove_blacklist_entry', hostname: host });
|
||
renderAutofillSection(content);
|
||
});
|
||
});
|
||
|
||
document.getElementById('bl-add-btn')?.addEventListener('click', async () => {
|
||
const input = document.getElementById('bl-add-input') as HTMLInputElement;
|
||
const val = input.value.trim();
|
||
if (!val) return;
|
||
await sendMessage({ type: 'add_blacklist_entry', hostname: val });
|
||
input.value = '';
|
||
renderAutofillSection(content);
|
||
});
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Build**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts
|
||
git commit -m "feat(ext/settings): autofill section (capture toggle + blacklist)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Display section (Device)
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
|
||
- [ ] **Step 1: Implement `renderDisplaySection()`**
|
||
|
||
Extract the existing password-coloring UI from the old settings.ts and move it into the Display section:
|
||
|
||
```ts
|
||
function renderDisplaySection(content: HTMLElement): void {
|
||
const scheme = loadColorScheme();
|
||
content.innerHTML = `
|
||
<h3 class="settings-section-title">Password coloring</h3>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Digit color</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<input type="color" id="digit-color" value="${scheme.digitColor}">
|
||
</div>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Symbol color</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<input type="color" id="symbol-color" value="${scheme.symbolColor}">
|
||
</div>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Preview</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<span id="color-preview" style="font-family:monospace; font-size:13px;">${colorizePassword('Aa1!Bb2@', scheme)}</span>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex; gap:6px; margin-top:12px;">
|
||
<button class="btn" id="reset-colors" style="font-size:11px;">Reset defaults</button>
|
||
</div>
|
||
`;
|
||
|
||
function updatePreview(): void {
|
||
const scheme2 = loadColorScheme();
|
||
const preview = document.getElementById('color-preview');
|
||
if (preview) preview.innerHTML = colorizePassword('Aa1!Bb2@', scheme2);
|
||
}
|
||
|
||
document.getElementById('digit-color')?.addEventListener('input', (e) => {
|
||
const color = (e.target as HTMLInputElement).value;
|
||
saveColorScheme({ ...loadColorScheme(), digitColor: color });
|
||
updatePreview();
|
||
});
|
||
|
||
document.getElementById('symbol-color')?.addEventListener('input', (e) => {
|
||
const color = (e.target as HTMLInputElement).value;
|
||
saveColorScheme({ ...loadColorScheme(), symbolColor: color });
|
||
updatePreview();
|
||
});
|
||
|
||
document.getElementById('reset-colors')?.addEventListener('click', () => {
|
||
resetColorScheme();
|
||
renderDisplaySection(content);
|
||
});
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Build**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts
|
||
git commit -m "feat(ext/settings): display section (password coloring)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Security section stub wire-up
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
|
||
The Security section calls `renderSecuritySection()` from `settings-security.ts` (stub from Task 1; DEV-C's real implementation replaces it).
|
||
|
||
The stub's signature is `renderSecuritySection(container, sessionHandle)`. For now, pass `null` as the session handle — the vault.ts wiring from DEV-A will eventually thread the real handle through. This is acceptable for the stub phase.
|
||
|
||
- [ ] **Step 1: Verify `renderSection('security')` calls `renderSecuritySection`**
|
||
|
||
The `renderSection()` function already has:
|
||
|
||
```ts
|
||
case 'security': return renderSecuritySection(content, null);
|
||
```
|
||
|
||
This is already wired in Task 2's settings.ts rewrite. Confirm it builds cleanly.
|
||
|
||
- [ ] **Step 2: Build and confirm**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
```
|
||
|
||
- [ ] **Step 3: Commit (only if additional wiring was needed)**
|
||
|
||
If you had to add anything, commit:
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts
|
||
git commit -m "feat(ext/settings): wire security section to settings-security.ts stub"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Generator section (Vault)
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
|
||
Extract the generator defaults content from `settings-vault.ts`. No functional changes — just consistent styling using `setting-row` pattern.
|
||
|
||
- [ ] **Step 1: Read the current generator content in settings-vault.ts**
|
||
|
||
```bash
|
||
grep -n "generator\|generate\|Generator" extension/src/popup/components/settings-vault.ts | head -20
|
||
```
|
||
|
||
- [ ] **Step 2: Implement `renderGeneratorSection()`**
|
||
|
||
```ts
|
||
async function renderGeneratorSection(content: HTMLElement): Promise<void> {
|
||
content.innerHTML = '<p class="muted" style="font-size:12px;">Loading…</p>';
|
||
const resp = await sendMessage({ type: 'get_vault_settings' });
|
||
if (!resp.ok) {
|
||
content.innerHTML = `<p class="muted">Failed to load: ${escapeHtml(resp.error ?? 'unknown')}</p>`;
|
||
return;
|
||
}
|
||
const settings = (resp.data as { settings: VaultSettings }).settings;
|
||
|
||
content.innerHTML = `
|
||
<h3 class="settings-section-title">Generator defaults</h3>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Configure generator</div>
|
||
<div class="setting-row__desc">Password length, character classes, passphrase word count.</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<button class="btn" id="open-generator-panel" style="font-size:11px;">Configure ▸</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('open-generator-panel')?.addEventListener('click', (e) => {
|
||
openGeneratorPanel(e.currentTarget as HTMLElement, settings.generator_defaults, async (req) => {
|
||
await sendMessage({ type: 'save_vault_settings', settings: { ...settings, generator_defaults: req } });
|
||
});
|
||
});
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts
|
||
git commit -m "feat(ext/settings): generator section (vault defaults)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: Retention section (Vault)
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
|
||
Extract retention content from `settings-vault.ts`.
|
||
|
||
- [ ] **Step 1: Read current retention helpers in settings-vault.ts**
|
||
|
||
```bash
|
||
grep -n "retention\|Retention\|trash\|history" extension/src/popup/components/settings-vault.ts | head -30
|
||
```
|
||
|
||
Note the helper functions: `trashRetentionToValue`, `valueToTrashRetention`, `historyRetentionToValue`, `valueToHistoryRetention`. Copy these into settings.ts or import from settings-vault.ts if it stays as a helper module.
|
||
|
||
- [ ] **Step 2: Implement `renderRetentionSection()`**
|
||
|
||
```ts
|
||
async function renderRetentionSection(content: HTMLElement): Promise<void> {
|
||
content.innerHTML = '<p class="muted" style="font-size:12px;">Loading…</p>';
|
||
const resp = await sendMessage({ type: 'get_vault_settings' });
|
||
if (!resp.ok) {
|
||
content.innerHTML = `<p class="muted">Failed to load: ${escapeHtml(resp.error ?? 'unknown')}</p>`;
|
||
return;
|
||
}
|
||
const settings = (resp.data as { settings: VaultSettings }).settings;
|
||
pendingVaultSettings = { ...settings };
|
||
|
||
content.innerHTML = `
|
||
<h3 class="settings-section-title">Trash retention</h3>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Keep deleted items for</div>
|
||
<div class="setting-row__desc">Items in trash older than this are permanently deleted on the next sync.</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<select id="trash-retention" style="font-size:12px;">
|
||
<option value="days:7" ${trashRetentionToValue(settings.trash_retention) === 'days:7' ? 'selected' : ''}>7 days</option>
|
||
<option value="days:30" ${trashRetentionToValue(settings.trash_retention) === 'days:30' ? 'selected' : ''}>30 days</option>
|
||
<option value="days:90" ${trashRetentionToValue(settings.trash_retention) === 'days:90' ? 'selected' : ''}>90 days</option>
|
||
<option value="forever" ${trashRetentionToValue(settings.trash_retention) === 'forever' ? 'selected' : ''}>Forever</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 class="settings-section-title" style="margin-top:20px;">Field history retention</h3>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Keep password history for</div>
|
||
<div class="setting-row__desc">History entries older than this are pruned on save.</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<select id="history-retention" style="font-size:12px;">
|
||
<option value="last_n:5" ${historyRetentionToValue(settings.history_retention) === 'last_n:5' ? 'selected' : ''}>Last 5</option>
|
||
<option value="last_n:10" ${historyRetentionToValue(settings.history_retention) === 'last_n:10' ? 'selected' : ''}>Last 10</option>
|
||
<option value="days:90" ${historyRetentionToValue(settings.history_retention) === 'days:90' ? 'selected' : ''}>90 days</option>
|
||
<option value="days:365" ${historyRetentionToValue(settings.history_retention) === 'days:365' ? 'selected' : ''}>1 year</option>
|
||
<option value="forever" ${historyRetentionToValue(settings.history_retention) === 'forever' ? 'selected' : ''}>Forever</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:12px;">
|
||
<button class="btn btn-primary" id="save-retention" style="font-size:11px;">Save retention settings</button>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('trash-retention')?.addEventListener('change', (e) => {
|
||
if (pendingVaultSettings) {
|
||
pendingVaultSettings.trash_retention = valueToTrashRetention((e.target as HTMLSelectElement).value);
|
||
}
|
||
});
|
||
|
||
document.getElementById('history-retention')?.addEventListener('change', (e) => {
|
||
if (pendingVaultSettings) {
|
||
pendingVaultSettings.history_retention = valueToHistoryRetention((e.target as HTMLSelectElement).value);
|
||
}
|
||
});
|
||
|
||
document.getElementById('save-retention')?.addEventListener('click', async () => {
|
||
if (!pendingVaultSettings) return;
|
||
const r = await sendMessage({ type: 'save_vault_settings', settings: pendingVaultSettings });
|
||
if (!r.ok) alert(`Save failed: ${r.error}`);
|
||
});
|
||
}
|
||
|
||
// Copy retention helpers from settings-vault.ts:
|
||
function trashRetentionToValue(r: import('../../shared/types').TrashRetention): string {
|
||
if (r.kind === 'forever') return 'forever';
|
||
return `days:${r.value}`;
|
||
}
|
||
|
||
function valueToTrashRetention(v: string): import('../../shared/types').TrashRetention {
|
||
if (v === 'forever') return { kind: 'forever' };
|
||
const m = /^days:(\d+)$/.exec(v);
|
||
if (m) return { kind: 'days', value: Number(m[1]) };
|
||
return { kind: 'forever' };
|
||
}
|
||
|
||
function historyRetentionToValue(r: import('../../shared/types').HistoryRetention): string {
|
||
if (r.kind === 'forever') return 'forever';
|
||
if (r.kind === 'last_n') return `last_n:${r.value}`;
|
||
return `days:${r.value}`;
|
||
}
|
||
|
||
function valueToHistoryRetention(v: string): import('../../shared/types').HistoryRetention {
|
||
if (v === 'forever') return { kind: 'forever' };
|
||
const mLast = /^last_n:(\d+)$/.exec(v);
|
||
if (mLast) return { kind: 'last_n', value: Number(mLast[1]) };
|
||
const mDays = /^days:(\d+)$/.exec(v);
|
||
if (mDays) return { kind: 'days', value: Number(mDays[1]) };
|
||
return { kind: 'forever' };
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts
|
||
git commit -m "feat(ext/settings): retention section (trash + field history)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: Backup section (Vault)
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
|
||
- [ ] **Step 1: Read current backup content in settings-vault.ts**
|
||
|
||
```bash
|
||
grep -n "backup\|Backup\|restore\|Restore" extension/src/popup/components/settings-vault.ts | head -20
|
||
```
|
||
|
||
- [ ] **Step 2: Implement `renderBackupSection()`**
|
||
|
||
Extract the backup/restore content from settings-vault.ts. Keep the same functionality, just restyled with `setting-row` pattern:
|
||
|
||
```ts
|
||
function renderBackupSection(content: HTMLElement): void {
|
||
content.innerHTML = `
|
||
<h3 class="settings-section-title">Backup</h3>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Export backup</div>
|
||
<div class="setting-row__desc">Download an encrypted backup of your entire vault.</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<button class="btn" id="backup-export-btn" style="font-size:11px;">Download backup</button>
|
||
</div>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Restore backup</div>
|
||
<div class="setting-row__desc">Restore from a previously exported backup file.</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<button class="btn" id="backup-restore-btn" style="font-size:11px;">Restore…</button>
|
||
<input type="file" id="backup-file-input" accept=".rbak" style="display:none;">
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Wire the backup export button — same logic as settings-vault.ts
|
||
document.getElementById('backup-export-btn')?.addEventListener('click', async () => {
|
||
const btn = document.getElementById('backup-export-btn') as HTMLButtonElement;
|
||
btn.disabled = true;
|
||
btn.textContent = 'Exporting…';
|
||
const resp = await sendMessage({ type: 'export_backup' });
|
||
btn.disabled = false;
|
||
btn.textContent = 'Download backup';
|
||
if (!resp.ok) { alert(`Export failed: ${resp.error}`); return; }
|
||
const data = resp.data as { backup_b64: string; filename: string };
|
||
const bytes = Uint8Array.from(atob(data.backup_b64), c => c.charCodeAt(0));
|
||
const blob = new Blob([bytes], { type: 'application/octet-stream' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url; a.download = data.filename; a.click();
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
|
||
document.getElementById('backup-restore-btn')?.addEventListener('click', () => {
|
||
document.getElementById('backup-file-input')?.click();
|
||
});
|
||
|
||
document.getElementById('backup-file-input')?.addEventListener('change', async (e) => {
|
||
const file = (e.target as HTMLInputElement).files?.[0];
|
||
if (!file) return;
|
||
const arrayBuf = await file.arrayBuffer();
|
||
const b64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuf)));
|
||
const resp = await sendMessage({ type: 'import_backup', backup_b64: b64 });
|
||
if (resp.ok) {
|
||
alert('Backup restored. The extension will reload.');
|
||
window.location.reload();
|
||
} else {
|
||
alert(`Restore failed: ${resp.error}`);
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts
|
||
git commit -m "feat(ext/settings): backup section (export + restore)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: Import section (Vault)
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings.ts`
|
||
|
||
- [ ] **Step 1: Read current import content in settings-vault.ts**
|
||
|
||
```bash
|
||
grep -n "import\|Import\|lastpass\|LastPass" extension/src/popup/components/settings-vault.ts | head -20
|
||
```
|
||
|
||
- [ ] **Step 2: Implement `renderImportSection()`**
|
||
|
||
```ts
|
||
function renderImportSection(content: HTMLElement): void {
|
||
content.innerHTML = `
|
||
<h3 class="settings-section-title">Import</h3>
|
||
<div class="setting-row">
|
||
<div class="setting-row__info">
|
||
<div class="setting-row__title">Import from LastPass</div>
|
||
<div class="setting-row__desc">Import a LastPass CSV export file.</div>
|
||
</div>
|
||
<div class="setting-row__control">
|
||
<button class="btn" id="import-lp-btn" style="font-size:11px;">Choose file…</button>
|
||
<input type="file" id="import-lp-input" accept=".csv" style="display:none;">
|
||
</div>
|
||
</div>
|
||
<div id="import-result" style="margin-top:12px;"></div>
|
||
`;
|
||
|
||
document.getElementById('import-lp-btn')?.addEventListener('click', () => {
|
||
document.getElementById('import-lp-input')?.click();
|
||
});
|
||
|
||
document.getElementById('import-lp-input')?.addEventListener('change', async (e) => {
|
||
const file = (e.target as HTMLInputElement).files?.[0];
|
||
if (!file) return;
|
||
const text = await file.text();
|
||
const resultDiv = document.getElementById('import-result')!;
|
||
resultDiv.textContent = 'Importing…';
|
||
const resp = await sendMessage({ type: 'import_lastpass', csv: text });
|
||
if (resp.ok) {
|
||
const data = resp.data as { imported: number; warnings: string[] };
|
||
resultDiv.innerHTML = `
|
||
<p style="color:var(--success,#238636);">Imported ${data.imported} items.</p>
|
||
${data.warnings.length > 0 ? `<p style="color:var(--gold,#b8860b);">${data.warnings.length} warning(s): ${escapeHtml(data.warnings.slice(0,3).join('; '))}${data.warnings.length > 3 ? '…' : ''}</p>` : ''}
|
||
`;
|
||
} else {
|
||
resultDiv.innerHTML = `<p style="color:var(--danger,#ab2b20);">Import failed: ${escapeHtml(resp.error ?? 'unknown')}</p>`;
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build and run all tests**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
|
||
bun run test 2>&1 | tail -15
|
||
```
|
||
|
||
Expected: all tests pass.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add extension/src/popup/components/settings.ts
|
||
git commit -m "feat(ext/settings): import section (LastPass CSV)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: Cleanup + full build pass
|
||
|
||
**Files:**
|
||
- Modify: `extension/src/popup/components/settings-vault.ts`
|
||
|
||
The old `renderVaultSettings()` exported function was called from popup.ts and vault.ts. Check all call sites.
|
||
|
||
- [ ] **Step 1: Find all callers of settings-vault.ts exports**
|
||
|
||
```bash
|
||
grep -rn "renderVaultSettings\|settings-vault\|settingsVault" extension/src/ | grep -v ".test.ts" | grep -v "__tests__"
|
||
```
|
||
|
||
- [ ] **Step 2: Assess whether settings-vault.ts can be deleted**
|
||
|
||
If all callers have been migrated to the new settings.ts sections, and no other code imports from it, it can be deleted. If it's still imported somewhere, keep it as a thin stub or migrate the remaining callers first.
|
||
|
||
If safe to delete:
|
||
|
||
```bash
|
||
git rm extension/src/popup/components/settings-vault.ts
|
||
git commit -m "refactor(ext/settings): remove settings-vault.ts (content merged into sectioned settings)"
|
||
```
|
||
|
||
If still needed (some callers remain), leave it and note the remaining callers in your status update to PM.
|
||
|
||
- [ ] **Step 3: Final build check — both targets**
|
||
|
||
```bash
|
||
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | tail -10
|
||
bun run build:firefox 2>&1 | tail -10
|
||
```
|
||
|
||
Expected: both build clean.
|
||
|
||
- [ ] **Step 4: Run all tests**
|
||
|
||
```bash
|
||
bun run test 2>&1 | tail -15
|
||
```
|
||
|
||
Expected: all pass.
|
||
|
||
- [ ] **Step 5: Open PR**
|
||
|
||
```bash
|
||
gh pr create --title "feat: settings UX redesign — left-nav sectioned layout (Stream B)" --base main
|
||
```
|
||
|
||
- [ ] **Step 6: Post status to PM**
|
||
|
||
```
|
||
## STATUS UPDATE — DEV-B
|
||
Time: <iso8601>
|
||
Task: 10 of 10
|
||
Status: REVIEW-READY
|
||
Summary: All 10 tasks complete. PR open. All sections implemented. Build clean. Tests pass.
|
||
Next: waiting for PM
|
||
```
|