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:
52
extension/src/vault/vault-list.ts
Normal file
52
extension/src/vault/vault-list.ts
Normal 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!);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user