# 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.
