refactor(ext/vault): extract vault-list.ts (Plan C Phase 4)

Moves the list-pane rendering (renderListPane: row markup, empty state, and
row-click → selectItemForDrawer) out of vault.ts into vault-list.ts, taking
the VaultController ctx. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-31 17:09:45 -04:00
parent 9049512e0d
commit 68cada5593
2 changed files with 61 additions and 55 deletions

View File

@@ -0,0 +1,52 @@
// Vault-tab list column: renders the middle list pane (row markup, empty
// state, and the row-click → drawer selection). Receives the VaultController
// (`ctx`) and reaches sibling concerns through it; pure helpers come from
// vault-context. Imports only from shared/ and vault-context.
import type { ItemId, ManifestEntry, ItemType } from '../shared/types';
import { relativeTime } from '../shared/relative-time';
import {
type VaultController, escapeHtml, typeIcon, getFilteredEntries,
} from './vault-context';
export function renderListPane(ctx: VaultController): void {
const pane = document.getElementById('vault-list-pane');
if (!pane) return;
const group = ctx.state.activeGroup as ItemType | null;
let items = getFilteredEntries(ctx.state);
if (group) items = items.filter(([, e]) => e.type === group);
if (items.length === 0) {
pane.innerHTML = `
<div class="empty-state">
<span class="empty-state__icon" aria-hidden="true">${ctx.state.searchQuery ? '⊘' : '◈'}</span>
<div class="empty-state__title">${ctx.state.searchQuery ? `No results for "${escapeHtml(ctx.state.searchQuery)}"` : 'No items yet'}</div>
<div class="empty-state__hint">${ctx.state.searchQuery ? 'Try a shorter search term.' : 'Click + new item to get started.'}</div>
</div>
`;
return;
}
pane.innerHTML = items.map(([id, e]: [ItemId, ManifestEntry]) => {
const sel = id === ctx.state.selectedId ? ' vault-list-row--selected' : '';
const subtitle = (e as any).icon_hint ?? (e.tags?.length > 0 ? e.tags.join(', ') : '');
const modifiedAgo = e.modified ? relativeTime(e.modified) : '';
return `
<div class="vault-list-row${sel}" data-id="${escapeHtml(id)}">
<div class="vault-list-row__icon" aria-hidden="true">${typeIcon(e.type)}</div>
<div class="vault-list-row__text">
<div class="vault-list-row__title">${escapeHtml(e.title)}</div>
${subtitle ? `<div class="vault-list-row__subtitle">${escapeHtml(subtitle)}</div>` : ''}
</div>
${modifiedAgo ? `<div class="vault-list-row__age">${escapeHtml(modifiedAgo)}</div>` : ''}
</div>
`;
}).join('');
pane.querySelectorAll<HTMLElement>('.vault-list-row').forEach((row) => {
row.addEventListener('click', async () => {
await ctx.selectItemForDrawer(row.dataset.id!);
});
});
}

View File

@@ -10,7 +10,6 @@ import type {
} from '../shared/types';
import { registerHost } from '../shared/state';
import { type ErrorCta } from '../shared/error-copy';
import { relativeTime } from '../shared/relative-time';
import { renderItemDetail } from '../popup/components/item-detail';
import { renderItemForm } from '../popup/components/item-form';
import { renderTrash, teardown as teardownTrash } from '../popup/components/trash';
@@ -23,7 +22,7 @@ import { renderBackupPanel, teardown as teardownBackup } from './components/back
import { renderImportPanel, teardown as teardownImport } from './components/import-panel';
import {
type VaultController, type VaultState, type VaultView, type HashRoute,
escapeHtml, typeIcon, getFilteredEntries,
escapeHtml,
} from './vault-context';
import {
render, applyShellViewClass,
@@ -31,6 +30,7 @@ import {
wireSessionExpiredListener,
} from './vault-shell';
import { wireSidebar, renderSidebarCategories } from './vault-sidebar';
import { renderListPane } from './vault-list';
// ---------------------------------------------------------------------------
// Helpers
@@ -144,7 +144,7 @@ const ctx: VaultController = {
sendMessage,
render: () => render(ctx),
renderPane: () => renderPane(),
renderListPane: () => renderListPane(),
renderListPane: () => renderListPane(ctx),
renderSidebarCategories: () => renderSidebarCategories(ctx),
renderDrawer: (item) => renderDrawer(item),
applyShellViewClass: () => applyShellViewClass(ctx),
@@ -173,7 +173,7 @@ registerHost({
setHash(view as VaultView);
applyShellViewClass(ctx);
renderSidebarCategories(ctx);
if (state.view === 'list') renderListPane();
if (state.view === 'list') renderListPane(ctx);
renderPane();
},
sendMessage,
@@ -281,7 +281,7 @@ function renderDrawer(item: Item): void {
document.getElementById('drawer-close-btn')?.addEventListener('click', () => {
closeDrawer();
renderListPane();
renderListPane(ctx);
});
document.getElementById('drawer-edit-btn')?.addEventListener('click', () => {
@@ -304,57 +304,11 @@ async function selectItemForDrawer(id: string): Promise<void> {
state.selectedItem = data.item;
state.drawerOpen = true;
renderSidebarCategories(ctx);
renderListPane();
renderListPane(ctx);
renderDrawer(data.item);
openDrawer();
}
// ---------------------------------------------------------------------------
// List pane
// ---------------------------------------------------------------------------
function renderListPane(): void {
const pane = document.getElementById('vault-list-pane');
if (!pane) return;
const group = state.activeGroup as ItemType | null;
let items = getFilteredEntries(state);
if (group) items = items.filter(([, e]) => e.type === group);
if (items.length === 0) {
pane.innerHTML = `
<div class="empty-state">
<span class="empty-state__icon" aria-hidden="true">${state.searchQuery ? '⊘' : '◈'}</span>
<div class="empty-state__title">${state.searchQuery ? `No results for "${escapeHtml(state.searchQuery)}"` : 'No items yet'}</div>
<div class="empty-state__hint">${state.searchQuery ? 'Try a shorter search term.' : 'Click + new item to get started.'}</div>
</div>
`;
return;
}
pane.innerHTML = items.map(([id, e]) => {
const sel = id === state.selectedId ? ' vault-list-row--selected' : '';
const subtitle = (e as any).icon_hint ?? (e.tags?.length > 0 ? e.tags.join(', ') : '');
const modifiedAgo = e.modified ? relativeTime(e.modified) : '';
return `
<div class="vault-list-row${sel}" data-id="${escapeHtml(id)}">
<div class="vault-list-row__icon" aria-hidden="true">${typeIcon(e.type)}</div>
<div class="vault-list-row__text">
<div class="vault-list-row__title">${escapeHtml(e.title)}</div>
${subtitle ? `<div class="vault-list-row__subtitle">${escapeHtml(subtitle)}</div>` : ''}
</div>
${modifiedAgo ? `<div class="vault-list-row__age">${escapeHtml(modifiedAgo)}</div>` : ''}
</div>
`;
}).join('');
pane.querySelectorAll<HTMLElement>('.vault-list-row').forEach((row) => {
row.addEventListener('click', async () => {
await selectItemForDrawer(row.dataset.id!);
});
});
}
// ---------------------------------------------------------------------------
// Platform-aware save hint
// ---------------------------------------------------------------------------
@@ -589,7 +543,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (state.selectedId === route.id && state.selectedItem) {
renderPane();
renderSidebarCategories(ctx);
if (state.view === 'list') renderListPane();
if (state.view === 'list') renderListPane(ctx);
return;
}
// Need to fetch the item
@@ -601,7 +555,7 @@ document.addEventListener('DOMContentLoaded', async () => {
state.selectedId = null;
state.selectedItem = null;
renderSidebarCategories(ctx);
if (state.view === 'list') renderListPane();
if (state.view === 'list') renderListPane(ctx);
renderPane();
});
});
@@ -620,7 +574,7 @@ async function selectItem(id: ItemId): Promise<void> {
state.loading = false;
setHash('detail', id);
renderSidebarCategories(ctx);
renderListPane();
renderListPane(ctx);
renderPane();
} else {
state.loading = false;