feat: add popup state machine and all components
View router (setup/locked/list/detail/add/edit), unlock screen with passphrase input, entry list with search/group tabs/keyboard nav, entry detail with TOTP countdown and copy shortcuts, add/edit form with password generation, and 3-step setup wizard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
extension/src/popup/components/unlock.ts
Normal file
56
extension/src/popup/components/unlock.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/// Unlock view — passphrase input with ENTER to submit.
|
||||
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../popup';
|
||||
import type { ManifestEntry } from '../../shared/types';
|
||||
|
||||
export function renderUnlock(app: HTMLElement): void {
|
||||
const state = getState();
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="pad" style="text-align:center; padding-top:40px;">
|
||||
<div class="brand">idfoto</div>
|
||||
<p class="muted" style="margin:8px 0 24px;">two-factor vault</p>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="password"
|
||||
id="passphrase-input"
|
||||
placeholder="passphrase"
|
||||
autocomplete="off"
|
||||
${state.loading ? 'disabled' : ''}
|
||||
>
|
||||
</div>
|
||||
${state.loading ? '<div style="margin:12px 0;"><span class="spinner"></span></div>' : ''}
|
||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||
<div style="margin-top:24px;">
|
||||
<button class="btn" id="settings-btn" style="font-size:11px;">settings</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const input = document.getElementById('passphrase-input') as HTMLInputElement;
|
||||
if (input && !state.loading) {
|
||||
input.focus();
|
||||
input.addEventListener('keydown', async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const passphrase = input.value;
|
||||
if (!passphrase) return;
|
||||
setState({ loading: true, error: null });
|
||||
const resp = await sendMessage({ type: 'unlock', passphrase });
|
||||
if (resp.ok) {
|
||||
const listResp = await sendMessage({ type: 'list_entries' });
|
||||
if (listResp.ok) {
|
||||
const data = listResp.data as { entries: Array<[string, ManifestEntry]> };
|
||||
navigate('list', { entries: data.entries });
|
||||
} else {
|
||||
setState({ loading: false, error: listResp.error });
|
||||
}
|
||||
} else {
|
||||
setState({ loading: false, error: resp.error });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const settingsBtn = document.getElementById('settings-btn');
|
||||
settingsBtn?.addEventListener('click', () => navigate('setup'));
|
||||
}
|
||||
Reference in New Issue
Block a user