feat(ext/popup): Totp view + form (countdown ring, Steam toggle)

Detail view renders a signature block with a large monospace rotating code and
a thin SVG countdown ring that sweeps via CSS transition. The ticker polls
get_totp every second and is stopped on teardown (back/edit/trash/Escape/e/d/t).

Form has a two-button kind toggle (TOTP / Steam Guard) that re-renders in place
while preserving entered values. TOTP uses digits=6 kind='totp'; Steam uses
digits=5 kind='steam'. Both default to algorithm='sha1' period_seconds=30.

Keyboard shortcuts on detail: Escape=back, e=edit, d=trash, t=copy-code.
Guarded against stealing keystrokes from editable targets.

Wires totp.renderDetail / totp.renderForm into both dispatchers and calls
totp.teardown() alongside the other types so tickers can't leak across views.

Closes T8 of the extension 1C-β1 plan (5/5 typed-item modules in place;
only T9 picker and T10 acceptance remain).
This commit is contained in:
adlee-was-taken
2026-04-23 22:54:49 -04:00
parent e084790756
commit 673981379e
4 changed files with 433 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ import * as secureNote from './types/secure-note';
import * as identity from './types/identity';
import * as card from './types/card';
import * as key from './types/key';
import * as totp from './types/totp';
export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
login.teardown(); // detail-view's ticker/listener don't leak into form
@@ -15,6 +16,7 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
identity.teardown();
card.teardown();
key.teardown();
totp.teardown();
const state = getState();
const existing = mode === 'edit' ? state.selectedItem : null;
const type: ItemType = existing?.type ?? state.newType ?? 'login';
@@ -25,7 +27,7 @@ export function renderItemForm(app: HTMLElement, mode: 'add' | 'edit'): void {
case 'identity': return identity.renderForm(app, mode, existing);
case 'card': return card.renderForm(app, mode, existing);
case 'key': return key.renderForm(app, mode, existing);
case 'totp':
case 'totp': return totp.renderForm(app, mode, existing);
case 'document': return renderComingSoon(app, type);
}
}