From 7bd1a9dd7dea8771b0f7be5bec72f7d194981142 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 19:50:18 -0400 Subject: [PATCH] ext(affordances): wirePasswordStrength via scheduleRate Co-Authored-By: Claude Opus 4.7 --- extension/src/popup/styles.css | 26 ++++++++++ .../__tests__/password-tools.test.ts | 49 ++++++++++++++++++- .../shared/form-affordances/password-tools.ts | 39 +++++++++++++++ extension/src/vault/vault.css | 26 ++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 5e5dcb6..3f278fd 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1385,3 +1385,29 @@ textarea { .hostname-text { font-family: ui-monospace, monospace; } +.strength-bar-row { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 4px; +} +.strength-bar { + display: flex; + gap: 3px; + height: 4px; +} +.strength-bar > span { + flex: 1; + background: var(--border-subtle); + border-radius: 2px; +} +.strength-bar.s-very-weak > span.lit { background: #c75a4f; } +.strength-bar.s-weak > span.lit { background: #c75a4f; } +.strength-bar.s-fair > span.lit { background: #d49b3a; } +.strength-bar.s-good > span.lit { background: #d49b3a; } +.strength-bar.s-strong > span.lit { background: #6cb37a; } +.strength-label { + font-size: 11px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} diff --git a/extension/src/shared/form-affordances/__tests__/password-tools.test.ts b/extension/src/shared/form-affordances/__tests__/password-tools.test.ts index 168acc4..7e16210 100644 --- a/extension/src/shared/form-affordances/__tests__/password-tools.test.ts +++ b/extension/src/shared/form-affordances/__tests__/password-tools.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { wirePasswordReveal } from '../password-tools'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { wirePasswordReveal, wirePasswordStrength } from '../password-tools'; describe('wirePasswordReveal', () => { let form: HTMLElement; @@ -47,3 +47,48 @@ describe('wirePasswordReveal', () => { expect(btn.title).toBe('reveal'); }); }); + +describe('wirePasswordStrength', () => { + let form: HTMLElement; + let scheduleRate: ReturnType; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` + + + `; + document.body.appendChild(form); + scheduleRate = vi.fn(); + }); + + afterEach(() => { + document.body.removeChild(form); + }); + + it('shows bar with score class on input', () => { + scheduleRate.mockImplementation((_pw, cb) => cb({ score: 3, guessesLog10: 11.4 })); + wirePasswordStrength(form, { scheduleRate }); + const input = form.querySelector('#f-password') as HTMLInputElement; + input.value = 'CorrectHorseBatteryStaple'; + input.dispatchEvent(new Event('input')); + const row = form.querySelector('#strength-bar-row') as HTMLElement; + expect(row.hidden).toBe(false); + expect(row.querySelector('.strength-bar')?.className).toContain('s-good'); + expect(row.querySelector('.strength-label')?.textContent).toContain('good'); + expect(row.querySelector('.strength-label')?.textContent).toContain('10^11'); + }); + + it('hides bar when input is empty', () => { + scheduleRate.mockImplementation((_pw, cb) => cb({ score: -1, guessesLog10: -1 })); + wirePasswordStrength(form, { scheduleRate }); + const input = form.querySelector('#f-password') as HTMLInputElement; + input.value = ''; + input.dispatchEvent(new Event('input')); + const row = form.querySelector('#strength-bar-row') as HTMLElement; + expect(row.hidden).toBe(true); + }); +}); diff --git a/extension/src/shared/form-affordances/password-tools.ts b/extension/src/shared/form-affordances/password-tools.ts index dacf023..f1061e0 100644 --- a/extension/src/shared/form-affordances/password-tools.ts +++ b/extension/src/shared/form-affordances/password-tools.ts @@ -1,4 +1,5 @@ import { GLYPH_REVEAL, GLYPH_HIDE } from '../glyphs'; +import { STRENGTH_LABELS, entropyText, type Strength } from '../../setup/setup-helpers'; /// Returns a teardown fn the caller must invoke on unmount. export function wirePasswordReveal(form: HTMLElement): () => void { @@ -26,3 +27,41 @@ export function wirePasswordReveal(form: HTMLElement): () => void { btn.title = 'reveal'; }; } + +export interface PasswordStrengthOpts { + scheduleRate: (passphrase: string, cb: (s: Strength) => void) => void; +} + +export function wirePasswordStrength(form: HTMLElement, opts: PasswordStrengthOpts): void { + const input = form.querySelector('#f-password'); + const row = form.querySelector('#strength-bar-row'); + if (!input || !row) return; + const bar = row.querySelector('.strength-bar'); + const label = row.querySelector('.strength-label'); + if (!bar || !label) return; + + const update = () => { + const v = input.value; + if (!v) { + row.hidden = true; + return; + } + opts.scheduleRate(v, (s) => { + if (s.score < 0) { row.hidden = true; return; } + row.hidden = false; + // Reset score classes, then add the current one to the bar element. + bar.className = 'strength-bar'; + const cls = STRENGTH_LABELS[s.score]?.cls ?? 's-very-weak'; + bar.classList.add(cls); + // Light up segments 0..score (5-segment bar). + Array.from(bar.children).forEach((seg, i) => { + (seg as HTMLElement).classList.toggle('lit', i <= s.score); + }); + const text = STRENGTH_LABELS[s.score]?.text ?? '?'; + label.textContent = `${text} ยท ${entropyText(s.guessesLog10)}`; + }); + }; + + input.addEventListener('input', update); + update(); +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index bb808e8..b7099c8 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1415,3 +1415,29 @@ textarea { .hostname-text { font-family: ui-monospace, monospace; } +.strength-bar-row { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 4px; +} +.strength-bar { + display: flex; + gap: 3px; + height: 4px; +} +.strength-bar > span { + flex: 1; + background: var(--border-subtle); + border-radius: 2px; +} +.strength-bar.s-very-weak > span.lit { background: #c75a4f; } +.strength-bar.s-weak > span.lit { background: #c75a4f; } +.strength-bar.s-fair > span.lit { background: #d49b3a; } +.strength-bar.s-good > span.lit { background: #d49b3a; } +.strength-bar.s-strong > span.lit { background: #6cb37a; } +.strength-label { + font-size: 11px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +}