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';
|
} from '../shared/types';
|
||||||
import { registerHost } from '../shared/state';
|
import { registerHost } from '../shared/state';
|
||||||
import { type ErrorCta } from '../shared/error-copy';
|
import { type ErrorCta } from '../shared/error-copy';
|
||||||
import { relativeTime } from '../shared/relative-time';
|
|
||||||
import { renderItemDetail } from '../popup/components/item-detail';
|
import { renderItemDetail } from '../popup/components/item-detail';
|
||||||
import { renderItemForm } from '../popup/components/item-form';
|
import { renderItemForm } from '../popup/components/item-form';
|
||||||
import { renderTrash, teardown as teardownTrash } from '../popup/components/trash';
|
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 { renderImportPanel, teardown as teardownImport } from './components/import-panel';
|
||||||
import {
|
import {
|
||||||
type VaultController, type VaultState, type VaultView, type HashRoute,
|
type VaultController, type VaultState, type VaultView, type HashRoute,
|
||||||
escapeHtml, typeIcon, getFilteredEntries,
|
escapeHtml,
|
||||||
} from './vault-context';
|
} from './vault-context';
|
||||||
import {
|
import {
|
||||||
render, applyShellViewClass,
|
render, applyShellViewClass,
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
wireSessionExpiredListener,
|
wireSessionExpiredListener,
|
||||||
} from './vault-shell';
|
} from './vault-shell';
|
||||||
import { wireSidebar, renderSidebarCategories } from './vault-sidebar';
|
import { wireSidebar, renderSidebarCategories } from './vault-sidebar';
|
||||||
|
import { renderListPane } from './vault-list';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -144,7 +144,7 @@ const ctx: VaultController = {
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
render: () => render(ctx),
|
render: () => render(ctx),
|
||||||
renderPane: () => renderPane(),
|
renderPane: () => renderPane(),
|
||||||
renderListPane: () => renderListPane(),
|
renderListPane: () => renderListPane(ctx),
|
||||||
renderSidebarCategories: () => renderSidebarCategories(ctx),
|
renderSidebarCategories: () => renderSidebarCategories(ctx),
|
||||||
renderDrawer: (item) => renderDrawer(item),
|
renderDrawer: (item) => renderDrawer(item),
|
||||||
applyShellViewClass: () => applyShellViewClass(ctx),
|
applyShellViewClass: () => applyShellViewClass(ctx),
|
||||||
@@ -173,7 +173,7 @@ registerHost({
|
|||||||
setHash(view as VaultView);
|
setHash(view as VaultView);
|
||||||
applyShellViewClass(ctx);
|
applyShellViewClass(ctx);
|
||||||
renderSidebarCategories(ctx);
|
renderSidebarCategories(ctx);
|
||||||
if (state.view === 'list') renderListPane();
|
if (state.view === 'list') renderListPane(ctx);
|
||||||
renderPane();
|
renderPane();
|
||||||
},
|
},
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -281,7 +281,7 @@ function renderDrawer(item: Item): void {
|
|||||||
|
|
||||||
document.getElementById('drawer-close-btn')?.addEventListener('click', () => {
|
document.getElementById('drawer-close-btn')?.addEventListener('click', () => {
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
renderListPane();
|
renderListPane(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('drawer-edit-btn')?.addEventListener('click', () => {
|
document.getElementById('drawer-edit-btn')?.addEventListener('click', () => {
|
||||||
@@ -304,57 +304,11 @@ async function selectItemForDrawer(id: string): Promise<void> {
|
|||||||
state.selectedItem = data.item;
|
state.selectedItem = data.item;
|
||||||
state.drawerOpen = true;
|
state.drawerOpen = true;
|
||||||
renderSidebarCategories(ctx);
|
renderSidebarCategories(ctx);
|
||||||
renderListPane();
|
renderListPane(ctx);
|
||||||
renderDrawer(data.item);
|
renderDrawer(data.item);
|
||||||
openDrawer();
|
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
|
// Platform-aware save hint
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -589,7 +543,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
if (state.selectedId === route.id && state.selectedItem) {
|
if (state.selectedId === route.id && state.selectedItem) {
|
||||||
renderPane();
|
renderPane();
|
||||||
renderSidebarCategories(ctx);
|
renderSidebarCategories(ctx);
|
||||||
if (state.view === 'list') renderListPane();
|
if (state.view === 'list') renderListPane(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Need to fetch the item
|
// Need to fetch the item
|
||||||
@@ -601,7 +555,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
state.selectedId = null;
|
state.selectedId = null;
|
||||||
state.selectedItem = null;
|
state.selectedItem = null;
|
||||||
renderSidebarCategories(ctx);
|
renderSidebarCategories(ctx);
|
||||||
if (state.view === 'list') renderListPane();
|
if (state.view === 'list') renderListPane(ctx);
|
||||||
renderPane();
|
renderPane();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -620,7 +574,7 @@ async function selectItem(id: ItemId): Promise<void> {
|
|||||||
state.loading = false;
|
state.loading = false;
|
||||||
setHash('detail', id);
|
setHash('detail', id);
|
||||||
renderSidebarCategories(ctx);
|
renderSidebarCategories(ctx);
|
||||||
renderListPane();
|
renderListPane(ctx);
|
||||||
renderPane();
|
renderPane();
|
||||||
} else {
|
} else {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user