ext(affordances): wirePasswordStrength via scheduleRate
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
form = document.createElement('div');
|
||||
form.innerHTML = `
|
||||
<input id="f-password" type="password" value="" />
|
||||
<div id="strength-bar-row" class="strength-bar-row" hidden>
|
||||
<div class="strength-bar"><span></span><span></span><span></span><span></span><span></span></div>
|
||||
<div class="strength-label"></div>
|
||||
</div>
|
||||
`;
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<HTMLInputElement>('#f-password');
|
||||
const row = form.querySelector<HTMLElement>('#strength-bar-row');
|
||||
if (!input || !row) return;
|
||||
const bar = row.querySelector<HTMLElement>('.strength-bar');
|
||||
const label = row.querySelector<HTMLElement>('.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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user