docs(plans): recovery QR + entropy floor; password coloring

Two implementation plans, one per spec landed in 00da7e7. Each plan
decomposes its spec into bite-sized TDD tasks with exact file paths,
complete code, and per-task commits.

- recovery-qr-and-entropy-floor.md (15 tasks, 6 phases): core crypto
  module + wasm bindings + CLI subcommands (imgsecret embed, recovery-qr
  generate/unlock, --force-weak-passphrase) + extension popup window
  with canvas QR + vault-tab button + unlock-flow recovery link +
  zxcvbn>=3 hard gate at init (CLI + setup wizard) + soft warning at
  unlock for grandfathered weak vaults.
- password-coloring.md (9 tasks, 6 phases): pure colorizePassword()
  utility + chrome.storage.sync round-trip + applyColorScheme() boot
  step + four reveal-surface integrations (field history, popup item
  detail, fullscreen item detail, generator preview) + settings UI
  with color pickers and live-preview swatch. Task 6 (fullscreen)
  flagged for coordination with in-flight Phase 1 UX work.

Both plans follow the subagent-driven execution preference per
feedback_subagent_default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-01 16:25:33 -04:00
parent 00da7e7931
commit eb443c38b4
2 changed files with 2595 additions and 0 deletions

View File

@@ -0,0 +1,804 @@
# Password Display Character-Class Coloring — Implementation Plan
> **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.
**Goal:** Color revealed passwords in the extension UI by character class (digits, symbols, letters), defaulting to digits-blue / symbols-red / letters-inherit, with user-configurable colors persisted in `chrome.storage.sync`.
**Architecture:** A single pure utility `colorizePassword(text)` that returns a `DocumentFragment` of class-named `<span>` runs. CSS rules in the existing extension stylesheet(s) bind those classes to CSS custom properties (`--relicario-pwd-digit-color`, `--relicario-pwd-symbol-color`). User overrides are stored in `chrome.storage.sync` and applied on popup/vault startup by setting the custom properties on `document.documentElement`. All four password-revealing surfaces (popup field-history viewer, popup item detail, fullscreen item detail, generator preview) call the same utility.
**Tech Stack:** TypeScript, Vitest with JSDOM for unit tests, existing `chrome.storage.sync` plumbing in the extension, existing settings UI patterns in `extension/src/popup/components/settings*.ts`.
**Spec:** `docs/superpowers/specs/2026-05-01-password-coloring-design.md`
---
## File Structure
### Created
- `extension/src/shared/password-coloring.ts` — pure `colorizePassword()` utility + class-name constants.
- `extension/src/shared/__tests__/password-coloring.test.ts` — Vitest unit tests for the utility.
- `extension/src/shared/color-scheme.ts` — read/write/apply helpers for the user's stored color scheme.
- `extension/src/shared/__tests__/color-scheme.test.ts` — Vitest unit tests for storage round-trip + apply.
(If `extension/src/shared/` does not exist, create it. Otherwise place under whatever the extension's existing shared/utility directory is — match the established convention.)
### Modified
- The popup stylesheet (`extension/src/popup/styles.css` and any vault stylesheet): add `:root` defaults + `.pwd-digit/.pwd-symbol/.pwd-letter` rules.
- `extension/src/popup/components/field-history.ts:72` — replace text-content assignment with `colorizePassword()` fragment.
- The popup's vault item detail component (find via `grep -n "password.*reveal\|passwordCell" extension/src/popup/`).
- `extension/src/vault/` item-detail component — same change, fullscreen surface.
- The generator preview component — same change.
- The popup's bootstrap (`extension/src/popup/popup.ts` or `index.ts`) — call `applyColorScheme()` once at startup.
- The vault's bootstrap (`extension/src/vault/vault.ts`) — same `applyColorScheme()` call.
- A settings page component — add the Display section with two color pickers, preview swatch, reset button.
---
## Phase A — Core utility
### Task 1: `colorizePassword()` pure utility
**Files:**
- Create: `extension/src/shared/password-coloring.ts`
- Create: `extension/src/shared/__tests__/password-coloring.test.ts`
- [ ] **Step 1: Write the failing tests**
`extension/src/shared/__tests__/password-coloring.test.ts`:
```ts
import { describe, it, expect, beforeEach } from 'vitest';
import { JSDOM } from 'jsdom';
import { colorizePassword, PWD_DIGIT, PWD_SYMBOL, PWD_LETTER } from '../password-coloring';
describe('colorizePassword', () => {
beforeEach(() => {
const dom = new JSDOM('<!DOCTYPE html><body></body>');
(global as any).document = dom.window.document;
});
function classes(frag: DocumentFragment): string[] {
return Array.from(frag.querySelectorAll('span')).map(s => s.className);
}
function texts(frag: DocumentFragment): string[] {
return Array.from(frag.querySelectorAll('span')).map(s => s.textContent ?? '');
}
it('returns empty fragment for empty input', () => {
const frag = colorizePassword('');
expect(frag.childNodes.length).toBe(0);
});
it('classifies a mixed-class run', () => {
const frag = colorizePassword('aB3$xY');
expect(classes(frag)).toEqual([PWD_LETTER, PWD_DIGIT, PWD_SYMBOL, PWD_LETTER]);
expect(texts(frag)).toEqual(['aB', '3', '$', 'xY']);
});
it('all-letters produces a single letter span', () => {
const frag = colorizePassword('passwd');
expect(classes(frag)).toEqual([PWD_LETTER]);
expect(texts(frag)).toEqual(['passwd']);
});
it('all-digits produces a single digit span', () => {
const frag = colorizePassword('123456');
expect(classes(frag)).toEqual([PWD_DIGIT]);
expect(texts(frag)).toEqual(['123456']);
});
it('all-symbols produces a single symbol span', () => {
const frag = colorizePassword('!@#$%^');
expect(classes(frag)).toEqual([PWD_SYMBOL]);
expect(texts(frag)).toEqual(['!@#$%^']);
});
it('classifies unicode letters as letters', () => {
const frag = colorizePassword('áñü');
expect(classes(frag)).toEqual([PWD_LETTER]);
});
it('classifies whitespace as symbol', () => {
const frag = colorizePassword('a b');
expect(classes(frag)).toEqual([PWD_LETTER, PWD_SYMBOL, PWD_LETTER]);
expect(texts(frag)).toEqual(['a', ' ', 'b']);
});
it('representative password snapshot: aB3$xY7&_!', () => {
const frag = colorizePassword('aB3$xY7&_!');
expect(classes(frag)).toEqual([
PWD_LETTER, PWD_DIGIT, PWD_SYMBOL, PWD_LETTER, PWD_DIGIT, PWD_SYMBOL,
]);
expect(texts(frag)).toEqual(['aB', '3', '$', 'xY', '7', '&_!']);
});
});
```
- [ ] **Step 2: Run — expect compile failure (module missing)**
```
cd extension && npm run test -- password-coloring
```
Expected: `Cannot find module '../password-coloring'`.
- [ ] **Step 3: Implement the utility**
`extension/src/shared/password-coloring.ts`:
```ts
export const PWD_DIGIT = 'pwd-digit';
export const PWD_SYMBOL = 'pwd-symbol';
export const PWD_LETTER = 'pwd-letter';
type Class = typeof PWD_DIGIT | typeof PWD_SYMBOL | typeof PWD_LETTER;
function classify(ch: string): Class {
if (/^\d$/.test(ch)) return PWD_DIGIT;
if (/^\p{L}$/u.test(ch)) return PWD_LETTER;
return PWD_SYMBOL;
}
/**
* Split `text` into runs of same-class codepoints and return a DocumentFragment
* of class-named <span> nodes (one span per run). Returns an empty fragment
* for empty input.
*
* Pure: does not mutate any DOM outside the returned fragment, does not perform
* I/O. Safe to call on every render.
*/
export function colorizePassword(text: string): DocumentFragment {
const frag = document.createDocumentFragment();
if (text.length === 0) return frag;
// Iterate by codepoint so unicode letters classify correctly.
const codepoints = Array.from(text);
let runStart = 0;
let runClass = classify(codepoints[0]);
for (let i = 1; i <= codepoints.length; i++) {
const c = i < codepoints.length ? classify(codepoints[i]) : null;
if (c !== runClass) {
const span = document.createElement('span');
span.className = runClass;
span.textContent = codepoints.slice(runStart, i).join('');
frag.appendChild(span);
if (c !== null) {
runStart = i;
runClass = c;
}
}
}
return frag;
}
```
- [ ] **Step 4: Run — expect pass**
```
cd extension && npm run test -- password-coloring
```
Expected: all 8 PASS.
- [ ] **Step 5: Commit**
```
git add extension/src/shared/password-coloring.ts extension/src/shared/__tests__/password-coloring.test.ts
git commit -m "feat(ext/shared): add colorizePassword utility"
```
---
## Phase B — Color scheme storage + apply
### Task 2: `applyColorScheme()` + storage round-trip
**Files:**
- Create: `extension/src/shared/color-scheme.ts`
- Create: `extension/src/shared/__tests__/color-scheme.test.ts`
- [ ] **Step 1: Write the failing tests**
`extension/src/shared/__tests__/color-scheme.test.ts`:
```ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { JSDOM } from 'jsdom';
import {
loadColorScheme, saveColorScheme, resetColorScheme, applyColorScheme,
DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR,
} from '../color-scheme';
function mockChromeStorage(initial: any = {}) {
const store = { ...initial };
(global as any).chrome = {
storage: {
sync: {
get: vi.fn((key: string) => Promise.resolve(
key in store ? { [key]: store[key] } : {})),
set: vi.fn((kv: any) => { Object.assign(store, kv); return Promise.resolve(); }),
remove: vi.fn((key: string) => { delete store[key]; return Promise.resolve(); }),
},
},
};
return store;
}
describe('color-scheme storage', () => {
beforeEach(() => {
const dom = new JSDOM('<!DOCTYPE html><body></body>');
(global as any).document = dom.window.document;
});
it('load returns defaults when storage is empty', async () => {
mockChromeStorage();
const scheme = await loadColorScheme();
expect(scheme.digit_color).toBe(DEFAULT_DIGIT_COLOR);
expect(scheme.symbol_color).toBe(DEFAULT_SYMBOL_COLOR);
});
it('load returns stored values when present', async () => {
mockChromeStorage({
password_display_scheme: { digit_color: '#123456', symbol_color: '#abcdef' },
});
const scheme = await loadColorScheme();
expect(scheme.digit_color).toBe('#123456');
expect(scheme.symbol_color).toBe('#abcdef');
});
it('save round-trips', async () => {
mockChromeStorage();
await saveColorScheme({ digit_color: '#111111', symbol_color: '#222222' });
const scheme = await loadColorScheme();
expect(scheme).toEqual({ digit_color: '#111111', symbol_color: '#222222' });
});
it('reset removes the storage key', async () => {
const store = mockChromeStorage({
password_display_scheme: { digit_color: '#000', symbol_color: '#fff' },
});
await resetColorScheme();
expect(store.password_display_scheme).toBeUndefined();
const scheme = await loadColorScheme();
expect(scheme.digit_color).toBe(DEFAULT_DIGIT_COLOR);
});
it('apply sets CSS custom properties on document.documentElement', async () => {
mockChromeStorage({
password_display_scheme: { digit_color: '#deadbe', symbol_color: '#feed00' },
});
await applyColorScheme();
const root = document.documentElement.style;
expect(root.getPropertyValue('--relicario-pwd-digit-color').trim()).toBe('#deadbe');
expect(root.getPropertyValue('--relicario-pwd-symbol-color').trim()).toBe('#feed00');
});
it('save rejects malformed hex values', async () => {
mockChromeStorage();
await expect(saveColorScheme({ digit_color: 'not-a-color', symbol_color: '#ffffff' }))
.rejects.toThrow();
});
});
```
- [ ] **Step 2: Run — expect compile failure**
```
cd extension && npm run test -- color-scheme
```
Expected: missing module.
- [ ] **Step 3: Implement**
`extension/src/shared/color-scheme.ts`:
```ts
export const DEFAULT_DIGIT_COLOR = '#2563eb';
export const DEFAULT_SYMBOL_COLOR = '#dc2626';
const STORAGE_KEY = 'password_display_scheme';
const HEX_RE = /^#[0-9a-fA-F]{6}$/;
export interface ColorScheme {
digit_color: string;
symbol_color: string;
}
export const DEFAULT_SCHEME: ColorScheme = {
digit_color: DEFAULT_DIGIT_COLOR,
symbol_color: DEFAULT_SYMBOL_COLOR,
};
function isValid(s: ColorScheme): boolean {
return HEX_RE.test(s.digit_color) && HEX_RE.test(s.symbol_color);
}
export async function loadColorScheme(): Promise<ColorScheme> {
const result = await chrome.storage.sync.get(STORAGE_KEY);
const stored = result[STORAGE_KEY] as Partial<ColorScheme> | undefined;
if (!stored) return { ...DEFAULT_SCHEME };
const merged: ColorScheme = {
digit_color: typeof stored.digit_color === 'string' && HEX_RE.test(stored.digit_color)
? stored.digit_color : DEFAULT_DIGIT_COLOR,
symbol_color: typeof stored.symbol_color === 'string' && HEX_RE.test(stored.symbol_color)
? stored.symbol_color : DEFAULT_SYMBOL_COLOR,
};
return merged;
}
export async function saveColorScheme(scheme: ColorScheme): Promise<void> {
if (!isValid(scheme)) {
throw new Error('Invalid color values; expected #rrggbb hex strings.');
}
await chrome.storage.sync.set({ [STORAGE_KEY]: scheme });
}
export async function resetColorScheme(): Promise<void> {
await chrome.storage.sync.remove(STORAGE_KEY);
}
/**
* Read the user's stored scheme (or defaults) and apply the colors as inline
* CSS custom properties on `document.documentElement`. Idempotent — safe to
* call on every popup/vault boot, and from a chrome.storage.onChanged handler
* to react to live edits from another open extension surface.
*/
export async function applyColorScheme(): Promise<void> {
const scheme = await loadColorScheme();
const root = document.documentElement.style;
root.setProperty('--relicario-pwd-digit-color', scheme.digit_color);
root.setProperty('--relicario-pwd-symbol-color', scheme.symbol_color);
}
```
- [ ] **Step 4: Run — expect pass**
```
cd extension && npm run test -- color-scheme
```
Expected: 6 PASS.
- [ ] **Step 5: Commit**
```
git add extension/src/shared/color-scheme.ts extension/src/shared/__tests__/color-scheme.test.ts
git commit -m "feat(ext/shared): color-scheme storage + applyColorScheme"
```
---
## Phase C — Stylesheet integration
### Task 3: Add CSS rules + custom-property defaults
**Files:**
- Modify: `extension/src/popup/styles.css`
- Modify: `extension/src/vault/vault.css` (and any other extension stylesheet that styles password reveal cells)
- [ ] **Step 1: Add the rules**
Append to each stylesheet (or to a single shared partial if the build supports CSS imports):
```css
:root {
--relicario-pwd-digit-color: #2563eb;
--relicario-pwd-symbol-color: #dc2626;
}
.pwd-digit { color: var(--relicario-pwd-digit-color); }
.pwd-symbol { color: var(--relicario-pwd-symbol-color); }
.pwd-letter { color: inherit; }
```
- [ ] **Step 2: Build the extension**
```
cd extension && npm run build
```
Expected: clean build, no CSS errors.
- [ ] **Step 3: Commit**
```
git add extension/src/popup/styles.css extension/src/vault/vault.css
git commit -m "style(ext): add password-coloring CSS rules + custom property defaults"
```
---
## Phase D — Wire into reveal surfaces
### Task 4: Field-history viewer
**Files:**
- Modify: `extension/src/popup/components/field-history.ts`
- [ ] **Step 1: Locate the text-content assignment**
```
grep -n "history-entry__value\|displayValue" extension/src/popup/components/field-history.ts
```
The line near 72 reads roughly:
```ts
<div class="history-entry__value ${isRevealed ? 'revealed' : 'masked'}">${displayValue}</div>
```
This is template-string interpolation, so `displayValue` is escaped HTML. The change requires switching from a string-template render to an imperative DOM patch (since `colorizePassword()` returns DOM, not HTML strings).
- [ ] **Step 2: Update the render to imperatively set content**
After the template renders the entry's outer markup, query the `.history-entry__value` element for revealed entries and replace its `textContent` with `colorizePassword(value)`:
```ts
import { colorizePassword } from '../../shared/password-coloring';
// existing render ...
container.querySelectorAll('.history-entry__value.revealed').forEach((el, idx) => {
el.textContent = '';
el.appendChild(colorizePassword(revealedValues[idx]));
});
```
(`revealedValues` here stands in for whatever array of revealed-entry values was already computed; adapt to actual variable names.)
- [ ] **Step 3: Update or add a test for this surface**
If `extension/src/popup/components/__tests__/field-history.test.ts` exists, add a case asserting that a revealed password's DOM contains `.pwd-*` spans. Otherwise just verify by running the existing test suite + a manual check.
```ts
it('revealed entry colorizes by character class', () => {
const dom = render(/* item with password "aB3$" in field history, revealed */);
const revealed = dom.querySelector('.history-entry__value.revealed')!;
expect(revealed.querySelector('.pwd-digit')?.textContent).toBe('3');
expect(revealed.querySelector('.pwd-symbol')?.textContent).toBe('$');
});
```
- [ ] **Step 4: Run tests + manual visual check**
```
cd extension && npm run test
```
Expected: PASS. Then build and load the extension to verify a revealed password in the field-history viewer is colored.
- [ ] **Step 5: Commit**
```
git add extension/src/popup/components/field-history.ts \
extension/src/popup/components/__tests__/field-history.test.ts
git commit -m "feat(ext/popup/field-history): colorize revealed password entries"
```
---
### Task 5: Popup vault item detail (password reveal)
**Files:**
- Modify: the popup component that renders the password field's revealed value (find via `grep -rn "field.*Password\|FieldKind.Password\|reveal" extension/src/popup/components/`)
- [ ] **Step 1: Find the surface**
Read the matched files and identify the line(s) that set the password text when revealed. The likely shape is a function `renderField(field)` with a branch on `field.kind === FieldKind.Password`.
- [ ] **Step 2: Apply the same imperative pattern**
Replace whatever currently sets the password's text content with:
```ts
import { colorizePassword } from '../../shared/password-coloring';
passwordValueEl.textContent = '';
if (revealed) {
passwordValueEl.appendChild(colorizePassword(field.value));
} else {
passwordValueEl.textContent = '••••••••';
}
```
- [ ] **Step 3: Run tests + manual check**
```
cd extension && npm run test
```
Build, load, reveal a password — confirm coloring.
- [ ] **Step 4: Commit**
```
git add extension/src/popup/components/
git commit -m "feat(ext/popup/item-detail): colorize revealed password field"
```
---
### Task 6: Fullscreen vault item detail
**Files:**
- Modify: the equivalent component under `extension/src/vault/`
The fullscreen vault is currently undergoing a Phase 1 redesign (see `9ed7e7c` and the Phase 1 plan in `docs/superpowers/plans/2026-04-30-fullscreen-ux-phase-1-visual-foundation.md`). Coordinate with that work — if the password-reveal surface is in active flux, land this change after Phase 1 settles, or fold it into Phase 2 if the user is doing that work themselves.
- [ ] **Step 1: Find the fullscreen reveal surface**
```
grep -rn "FieldKind.Password\|password.*reveal\|reveal.*password" extension/src/vault/
```
- [ ] **Step 2: Apply the same pattern as Task 5**
Same code shape. Different file.
- [ ] **Step 3: Run tests + manual check**
Open the fullscreen vault, reveal a password, confirm coloring.
- [ ] **Step 4: Commit**
```
git add extension/src/vault/
git commit -m "feat(ext/vault): colorize revealed password field in fullscreen view"
```
---
### Task 7: Generator preview
**Files:**
- Modify: the generator component (find via `grep -rn "generate_password\|generator.*preview" extension/src/`)
- [ ] **Step 1: Find the surface**
The generator likely has a live preview element that updates as the user adjusts character-class toggles, length, etc.
- [ ] **Step 2: Apply the imperative pattern**
```ts
import { colorizePassword } from '../../shared/password-coloring';
previewEl.textContent = '';
previewEl.appendChild(colorizePassword(generatedPassword));
```
- [ ] **Step 3: Run tests + manual check**
Open the generator, click roll/regenerate a few times — confirm the preview updates with coloring intact.
- [ ] **Step 4: Commit**
```
git add extension/src/popup/components/ # or wherever the generator lives
git commit -m "feat(ext/generator): colorize live password preview"
```
---
## Phase E — Boot wiring
### Task 8: Call `applyColorScheme()` on popup + vault startup
**Files:**
- Modify: `extension/src/popup/popup.ts` (or `popup/index.ts` — the popup's bootstrap)
- Modify: `extension/src/vault/vault.ts` — the fullscreen vault's bootstrap
- [ ] **Step 1: Add the call in popup boot**
Near the top of the popup's `init()` / `main()` function:
```ts
import { applyColorScheme } from '../shared/color-scheme';
await applyColorScheme();
```
The `await` is fine — it runs once per popup open, the storage round-trip is cheap (sub-millisecond).
Also wire a `chrome.storage.onChanged` listener so live edits from another open extension surface (e.g., the settings page) reflect immediately:
```ts
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && 'password_display_scheme' in changes) {
void applyColorScheme();
}
});
```
- [ ] **Step 2: Add the call in vault boot**
Same pattern in the fullscreen vault's bootstrap.
- [ ] **Step 3: Manual verification**
Open both surfaces, edit the colors via the (about-to-exist) settings page, observe the change reflect in real time.
- [ ] **Step 4: Commit**
```
git add extension/src/popup/popup.ts extension/src/vault/vault.ts
git commit -m "feat(ext): apply color scheme on popup + vault startup, react to storage changes"
```
---
## Phase F — Settings UI
### Task 9: Display section in settings with color pickers + preview swatch + reset
**Files:**
- Modify: an existing settings component — best candidate is `extension/src/popup/components/settings.ts` (general settings) or a new dedicated section if settings are split. Read the existing settings layout before deciding.
- Test: `extension/src/popup/components/__tests__/settings.test.ts` (extend existing tests)
- [ ] **Step 1: Find the existing settings shape**
```
grep -n "render\|section\|setting" extension/src/popup/components/settings.ts | head -30
```
Identify the pattern used to render a settings group (likely a `section` builder + child controls).
- [ ] **Step 2: Add the Display section**
Following the existing pattern:
```ts
import {
loadColorScheme, saveColorScheme, resetColorScheme,
DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR,
} from '../../shared/color-scheme';
import { colorizePassword } from '../../shared/password-coloring';
async function renderDisplaySection(parent: HTMLElement) {
const section = createSection('Display');
parent.appendChild(section);
const scheme = await loadColorScheme();
const digitInput = createColorInput('Digit color', scheme.digit_color);
const symbolInput = createColorInput('Symbol color', scheme.symbol_color);
const swatch = document.createElement('div');
swatch.className = 'color-preview-swatch';
const SAMPLE = 'Abc123!@#xyz';
const updateSwatch = () => {
swatch.style.setProperty('--relicario-pwd-digit-color', digitInput.value);
swatch.style.setProperty('--relicario-pwd-symbol-color', symbolInput.value);
swatch.textContent = '';
swatch.appendChild(colorizePassword(SAMPLE));
};
updateSwatch();
const onChange = async () => {
updateSwatch();
try {
await saveColorScheme({
digit_color: digitInput.value, symbol_color: symbolInput.value,
});
} catch (e) {
// Show inline error; keep current swatch.
}
};
digitInput.addEventListener('change', onChange);
symbolInput.addEventListener('change', onChange);
const resetBtn = document.createElement('button');
resetBtn.textContent = 'Reset to defaults';
resetBtn.addEventListener('click', async () => {
digitInput.value = DEFAULT_DIGIT_COLOR;
symbolInput.value = DEFAULT_SYMBOL_COLOR;
await resetColorScheme();
updateSwatch();
});
section.append(digitInput, symbolInput, swatch, resetBtn);
}
function createColorInput(label: string, value: string): HTMLInputElement & { label: string } {
// simple <label><input type=color>...
const input = document.createElement('input') as HTMLInputElement & { label: string };
input.type = 'color';
input.value = value;
input.label = label;
return input;
}
```
(Adapt to the existing component-creation idioms — the snippet above is illustrative.)
- [ ] **Step 3: Add the swatch styling**
In the popup stylesheet:
```css
.color-preview-swatch {
font-family: ui-monospace, monospace;
font-size: 1.1rem;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 8px;
background: #fff;
}
.color-preview-swatch .pwd-digit { color: var(--relicario-pwd-digit-color); }
.color-preview-swatch .pwd-symbol { color: var(--relicario-pwd-symbol-color); }
.color-preview-swatch .pwd-letter { color: inherit; }
```
(The custom properties are scoped to `.color-preview-swatch` itself via `style.setProperty`, so the swatch's preview is independent of the global root scheme — handy for previewing changes without committing them.)
- [ ] **Step 4: Add a settings test**
In `extension/src/popup/components/__tests__/settings.test.ts`, add:
```ts
it('Display section round-trips color scheme to storage', async () => {
// mock chrome.storage.sync, render settings, change the digit color picker,
// assert chrome.storage.sync.set was called with the new value.
// (Detailed scaffolding follows the existing tests in this file.)
});
it('Reset button clears storage and restores swatch defaults', async () => {
// render, change colors, click reset, assert chrome.storage.sync.remove
// was called and swatch reverts.
});
```
- [ ] **Step 5: Run all extension tests**
```
cd extension && npm run test
```
Expected: PASS.
- [ ] **Step 6: Commit**
```
git add extension/src/popup/components/settings.ts \
extension/src/popup/components/__tests__/settings.test.ts \
extension/src/popup/styles.css
git commit -m "feat(ext/settings): Display section with color pickers + swatch + reset"
```
---
## Self-Review Notes
Spec coverage check:
- **`colorizePassword` utility, single source of truth:** Task 1.
- **Three character classes (digit / symbol / letter), Unicode-letter classification:** Task 1.
- **CSS rules with custom properties + defaults:** Task 3.
- **Storage shape (`password_display_scheme`), default fallbacks, hex validation:** Task 2.
- **`applyColorScheme()` boot step on popup + vault:** Task 8.
- **Live updates via `chrome.storage.onChanged`:** Task 8.
- **Wire into field-history viewer:** Task 4.
- **Wire into popup item detail:** Task 5.
- **Wire into fullscreen item detail:** Task 6.
- **Wire into generator preview:** Task 7.
- **Settings UI with pickers + preview swatch + reset:** Task 9.
- **WCAG AA contrast warning:** spec says non-blocking; this is a small follow-up not gated by anything in this plan, so it is **not** included as a separate task. Either add a tiny inline contrast check in Task 9's `onChange` (left as an exercise — the contrast formula is `(L1 + 0.05) / (L2 + 0.05)`; show a `.contrast-warning` element when below 4.5) or open a follow-up issue.
No placeholders. No type drift (the `ColorScheme` interface and `PWD_*` constants are referenced consistently).
---
## Coordination note
The fullscreen UX redesign (Phase 1, recently merged in `87e63c2`) is in flight. **Task 6** (fullscreen reveal surface) touches code that may also be touched by ongoing UX work — coordinate with the user before landing it. Tasks 15, 79 are independent of fullscreen work and can land standalone.
---
## Execution Handoff
Plan complete and saved to `docs/superpowers/plans/2026-05-01-password-coloring.md`.
When ready to execute, the user's preference per `feedback_subagent_default` is **subagent-driven**: a fresh subagent per task, with two-stage review between tasks.

File diff suppressed because it is too large Load Diff