feat(ext/vault): detail drawer — open/close state + core fields display
This commit is contained in:
@@ -316,25 +316,129 @@ function wireBottomSheet(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Drawer stubs (implemented in Task 10)
|
// Drawer (implemented in Task 10)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function openDrawer(): void {
|
||||||
|
document.getElementById('vault-drawer')?.classList.add('vault-drawer--open');
|
||||||
|
}
|
||||||
|
|
||||||
function closeDrawer(): void {
|
function closeDrawer(): void {
|
||||||
state.drawerOpen = false;
|
state.drawerOpen = false;
|
||||||
|
state.selectedId = null;
|
||||||
|
state.selectedItem = null;
|
||||||
document.getElementById('vault-drawer')?.classList.remove('vault-drawer--open');
|
document.getElementById('vault-drawer')?.classList.remove('vault-drawer--open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDrawer(_item: unknown): void {
|
function getDrawerCoreFields(item: Item): Array<[string, string, boolean]> {
|
||||||
// implemented in Task 10
|
const core = item.core as Record<string, unknown>;
|
||||||
|
if (!core) return [];
|
||||||
|
const fields: Array<[string, string, boolean]> = [];
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case 'login':
|
||||||
|
if ('username' in core) fields.push(['username', String(core.username ?? ''), false]);
|
||||||
|
if ('password' in core) fields.push(['password', '••••••••', false]);
|
||||||
|
if ('url' in core) fields.push(['url', String(core.url ?? ''), true]);
|
||||||
|
break;
|
||||||
|
case 'card': {
|
||||||
|
if ('number' in core) fields.push(['number', String(core.number ?? ''), false]);
|
||||||
|
if ('holder' in core) fields.push(['holder', String(core.holder ?? ''), false]);
|
||||||
|
if ('expiry' in core && core.expiry) {
|
||||||
|
const exp = core.expiry as { month: number; year: number };
|
||||||
|
fields.push(['expiry', `${String(exp.month).padStart(2, '0')}/${exp.year}`, false]);
|
||||||
|
}
|
||||||
|
if ('cvv' in core) fields.push(['cvv', '•••', false]);
|
||||||
|
if ('pin' in core) fields.push(['pin', '••••', false]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'identity':
|
||||||
|
if ('full_name' in core) fields.push(['full name', String(core.full_name ?? ''), true]);
|
||||||
|
if ('email' in core) fields.push(['email', String(core.email ?? ''), true]);
|
||||||
|
if ('phone' in core) fields.push(['phone', String(core.phone ?? ''), false]);
|
||||||
|
if ('address' in core) fields.push(['address', String(core.address ?? ''), true]);
|
||||||
|
if ('date_of_birth' in core) fields.push(['date of birth', String(core.date_of_birth ?? ''), false]);
|
||||||
|
break;
|
||||||
|
case 'key':
|
||||||
|
if ('label' in core) fields.push(['label', String(core.label ?? ''), true]);
|
||||||
|
if ('algorithm' in core) fields.push(['algorithm', String(core.algorithm ?? ''), false]);
|
||||||
|
if ('public_key' in core) fields.push(['public key', String(core.public_key ?? ''), true]);
|
||||||
|
break;
|
||||||
|
case 'secure_note':
|
||||||
|
if ('body' in core) fields.push(['body', String(core.body ?? ''), true]);
|
||||||
|
break;
|
||||||
|
case 'totp':
|
||||||
|
if ('issuer' in core) fields.push(['issuer', String(core.issuer ?? ''), false]);
|
||||||
|
if ('label' in core) fields.push(['label', String(core.label ?? ''), false]);
|
||||||
|
break;
|
||||||
|
case 'document':
|
||||||
|
if ('filename' in core) fields.push(['filename', String(core.filename ?? ''), true]);
|
||||||
|
if ('mime_type' in core) fields.push(['type', String(core.mime_type ?? ''), false]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.notes) fields.push(['notes', item.notes, true]);
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDrawer(item: Item): void {
|
||||||
|
const drawer = document.getElementById('vault-drawer');
|
||||||
|
if (!drawer) return;
|
||||||
|
|
||||||
|
const coreFields = getDrawerCoreFields(item);
|
||||||
|
|
||||||
|
drawer.innerHTML = `
|
||||||
|
<div class="vault-drawer__header">
|
||||||
|
<span class="vault-drawer__type-pill">${item.type.replace('_', ' ').toUpperCase()}</span>
|
||||||
|
<div class="vault-drawer__actions">
|
||||||
|
<button class="btn" id="drawer-edit-btn" style="font-size:11px;">edit</button>
|
||||||
|
<button class="vault-drawer__close" id="drawer-close-btn" title="Close (Esc)">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vault-drawer__body">
|
||||||
|
<div class="vault-drawer__title">${escapeHtml(item.title)}</div>
|
||||||
|
${item.type === 'login' && (item.core as { url?: string }).url
|
||||||
|
? `<div class="vault-drawer__subtitle">${escapeHtml((item.core as { url?: string }).url ?? '')}</div>`
|
||||||
|
: ''}
|
||||||
|
<div class="vault-drawer__field-grid">
|
||||||
|
${coreFields.map(([label, value, full]) => `
|
||||||
|
<div class="vault-drawer__field${full ? ' vault-drawer__field--full' : ''}">
|
||||||
|
<div class="vault-drawer__field-label">${escapeHtml(label)}</div>
|
||||||
|
<div class="vault-drawer__field-value">${escapeHtml(value)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('drawer-close-btn')?.addEventListener('click', () => {
|
||||||
|
closeDrawer();
|
||||||
|
renderListPane();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('drawer-edit-btn')?.addEventListener('click', () => {
|
||||||
|
if (state.selectedId) {
|
||||||
|
setHash('edit', state.selectedId);
|
||||||
|
renderPane();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Item selection stub (implemented in Task 10)
|
// Item selection (implemented in Task 10)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function selectItemForDrawer(id: string): Promise<void> {
|
async function selectItemForDrawer(id: string): Promise<void> {
|
||||||
// implemented in Task 10
|
const resp = await sendMessage({ type: 'get_item', id });
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const data = resp.data as { item: Item };
|
||||||
state.selectedId = id;
|
state.selectedId = id;
|
||||||
|
state.selectedItem = data.item;
|
||||||
|
state.drawerOpen = true;
|
||||||
|
renderSidebarCategories();
|
||||||
|
renderListPane();
|
||||||
|
renderDrawer(data.item);
|
||||||
|
openDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -382,11 +486,16 @@ function wireSidebar(): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global "/" shortcut to focus search
|
// Global "/" shortcut to focus search; Esc to close drawer
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === '/' && !isEditableTarget(e.target)) {
|
if (e.key === '/' && !isEditableTarget(e.target)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
searchInput?.focus();
|
searchInput?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape' && state.drawerOpen) {
|
||||||
|
closeDrawer();
|
||||||
|
renderListPane();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user