refactor(ext/popup): rename entry-* → item-* components
Git-moves the three popup components so history survives the content rewrite that follows in Tasks 22–24: - entry-list.ts → item-list.ts - entry-detail.ts → item-detail.ts - entry-form.ts → item-form.ts Also renames the exported render functions (renderEntryList → renderItemList, etc.) and updates popup.ts imports + render switch. The files still wear @ts-nocheck and reference the old Entry type; content rewriting happens in Tasks 22–24. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
256
extension/src/popup/components/item-detail.ts
Normal file
256
extension/src/popup/components/item-detail.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
// @ts-nocheck — transitional: downstream files updated in Slice 6 (item-* rewrites) / Slice 4 (vitest setup) / Slice 5 (content + setup rewires)
|
||||
/// Entry detail view — shows fields, TOTP countdown, copy/fill shortcuts.
|
||||
|
||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../popup';
|
||||
import type { ManifestEntry } from '../../shared/types';
|
||||
|
||||
let totpInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function stopTotpTimer(): void {
|
||||
if (totpInterval !== null) {
|
||||
clearInterval(totpInterval);
|
||||
totpInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToClipboard(text: string): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
// Fallback for older browsers.
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderItemDetail(app: HTMLElement): void {
|
||||
const state = getState();
|
||||
const entry = state.selectedEntry;
|
||||
const id = state.selectedId;
|
||||
if (!entry || !id) {
|
||||
navigate('list');
|
||||
return;
|
||||
}
|
||||
|
||||
stopTotpTimer();
|
||||
|
||||
let html = `
|
||||
<div class="detail-header">
|
||||
<span class="detail-title">${escapeHtml(entry.name)}</span>
|
||||
<button class="btn" id="back-btn" style="font-size:11px;">esc</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// URL
|
||||
if (entry.url) {
|
||||
html += `
|
||||
<div class="field">
|
||||
<div class="label">url</div>
|
||||
<div class="field-value">${escapeHtml(entry.url)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Username
|
||||
if (entry.username) {
|
||||
html += `
|
||||
<div class="field">
|
||||
<div class="label">username</div>
|
||||
<div class="field-value" id="username-val">${escapeHtml(entry.username)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Password (masked by default)
|
||||
html += `
|
||||
<div class="field">
|
||||
<div class="label">password</div>
|
||||
<div class="field-value" id="password-val" style="cursor:pointer;">
|
||||
<span id="password-display">********</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// TOTP
|
||||
if (entry.totp_secret) {
|
||||
html += `
|
||||
<div class="field">
|
||||
<div class="label">totp</div>
|
||||
<div class="totp-code" id="totp-code">------</div>
|
||||
<div class="totp-bar"><div class="totp-bar-fill" id="totp-bar-fill" style="width:100%;"></div></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Notes
|
||||
if (entry.notes) {
|
||||
html += `
|
||||
<div class="field">
|
||||
<div class="label">notes</div>
|
||||
<div class="field-value">${escapeHtml(entry.notes)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Group
|
||||
if (entry.group) {
|
||||
html += `
|
||||
<div class="field">
|
||||
<div class="label">group</div>
|
||||
<div class="field-value">${escapeHtml(entry.group)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Metadata
|
||||
html += `
|
||||
<div class="field">
|
||||
<div class="muted">updated ${escapeHtml(entry.updated_at)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Key hints
|
||||
html += `
|
||||
<div class="keyhints">
|
||||
<span><kbd>c</kbd> copy user</span>
|
||||
<span><kbd>p</kbd> copy pass</span>
|
||||
${entry.totp_secret ? '<span><kbd>t</kbd> copy totp</span>' : ''}
|
||||
<span><kbd>f</kbd> autofill</span>
|
||||
<span><kbd>e</kbd> edit</span>
|
||||
<span><kbd>d</kbd> delete</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
app.innerHTML = html;
|
||||
|
||||
// --- Password toggle ---
|
||||
let passwordVisible = false;
|
||||
const passwordDisplay = document.getElementById('password-display')!;
|
||||
const passwordVal = document.getElementById('password-val')!;
|
||||
passwordVal?.addEventListener('click', () => {
|
||||
passwordVisible = !passwordVisible;
|
||||
passwordDisplay.textContent = passwordVisible ? entry.password : '********';
|
||||
});
|
||||
|
||||
// --- Back button ---
|
||||
document.getElementById('back-btn')?.addEventListener('click', goBack);
|
||||
|
||||
// --- TOTP timer ---
|
||||
if (entry.totp_secret) {
|
||||
refreshTotp(id);
|
||||
totpInterval = setInterval(() => refreshTotp(id), 1000);
|
||||
}
|
||||
|
||||
// --- Keyboard shortcuts ---
|
||||
const handler = async (e: KeyboardEvent) => {
|
||||
// Ignore if typing in an input.
|
||||
if ((e.target as HTMLElement).tagName === 'INPUT') return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
document.removeEventListener('keydown', handler);
|
||||
goBack();
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
if (entry.username) await copyToClipboard(entry.username);
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
await copyToClipboard(entry.password);
|
||||
break;
|
||||
|
||||
case 't':
|
||||
if (entry.totp_secret) {
|
||||
const codeEl = document.getElementById('totp-code');
|
||||
if (codeEl) await copyToClipboard(codeEl.textContent ?? '');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'f': {
|
||||
const resp = await sendMessage({
|
||||
type: 'fill_credentials',
|
||||
username: entry.username ?? '',
|
||||
password: entry.password,
|
||||
});
|
||||
if (!resp.ok) setState({ error: resp.error });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'e':
|
||||
document.removeEventListener('keydown', handler);
|
||||
stopTotpTimer();
|
||||
navigate('edit');
|
||||
break;
|
||||
|
||||
case 'd':
|
||||
e.preventDefault();
|
||||
showDeleteConfirm(id, entry.name, handler);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handler);
|
||||
}
|
||||
|
||||
async function refreshTotp(id: string): Promise<void> {
|
||||
const resp = await sendMessage({ type: 'get_totp', id });
|
||||
if (resp.ok) {
|
||||
const data = resp.data as { code: string; remaining_seconds: number };
|
||||
const codeEl = document.getElementById('totp-code');
|
||||
const barEl = document.getElementById('totp-bar-fill');
|
||||
if (codeEl) codeEl.textContent = data.code;
|
||||
if (barEl) barEl.style.width = `${(data.remaining_seconds / 30) * 100}%`;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack(): void {
|
||||
stopTotpTimer();
|
||||
// Reload the entry list.
|
||||
sendMessage({ type: 'list_entries' }).then(resp => {
|
||||
if (resp.ok) {
|
||||
const data = resp.data as { entries: Array<[string, ManifestEntry]> };
|
||||
navigate('list', {
|
||||
entries: data.entries,
|
||||
selectedId: null,
|
||||
selectedEntry: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showDeleteConfirm(id: string, name: string, parentHandler: (e: KeyboardEvent) => void): void {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'confirm-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="confirm-box">
|
||||
<p>Delete <strong>${escapeHtml(name)}</strong>?</p>
|
||||
<button class="btn" id="cancel-delete">cancel</button>
|
||||
<button class="btn btn-danger" id="confirm-delete">delete</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
document.getElementById('cancel-delete')?.addEventListener('click', () => {
|
||||
overlay.remove();
|
||||
});
|
||||
|
||||
document.getElementById('confirm-delete')?.addEventListener('click', async () => {
|
||||
overlay.remove();
|
||||
setState({ loading: true });
|
||||
const resp = await sendMessage({ type: 'delete_entry', id });
|
||||
if (resp.ok) {
|
||||
document.removeEventListener('keydown', parentHandler);
|
||||
stopTotpTimer();
|
||||
goBack();
|
||||
} else {
|
||||
setState({ loading: false, error: resp.error });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user