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:
804
docs/superpowers/plans/2026-05-01-password-coloring.md
Normal file
804
docs/superpowers/plans/2026-05-01-password-coloring.md
Normal 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 1–5, 7–9 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.
|
||||
1791
docs/superpowers/plans/2026-05-01-recovery-qr-and-entropy-floor.md
Normal file
1791
docs/superpowers/plans/2026-05-01-recovery-qr-and-entropy-floor.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user