refactor(ext/vault): extract vault-sidebar.ts with debounced search (Plan C Phase 4)
Moves the sidebar column out of vault.ts/vault-shell.ts into vault-sidebar.ts: its markup (now incl. an empty #vault-status-slot footer for Phase 6), the category nav rendering, nav-button wiring, and search. The search input gains an 80ms trailing-edge debounce (P2 fix — it re-filtered on every keystroke). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|||||||
|
|
||||||
describe('vault sidebar glyphs', () => {
|
describe('vault sidebar glyphs', () => {
|
||||||
const vaultSrc = fs.readFileSync(
|
const vaultSrc = fs.readFileSync(
|
||||||
path.resolve(__dirname, '../vault-shell.ts'),
|
path.resolve(__dirname, '../vault-sidebar.ts'),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@
|
|||||||
|
|
||||||
import type { ItemType } from '../shared/types';
|
import type { ItemType } from '../shared/types';
|
||||||
import { lookupErrorCopy } from '../shared/error-copy';
|
import { lookupErrorCopy } from '../shared/error-copy';
|
||||||
import {
|
|
||||||
GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_HISTORY,
|
|
||||||
} from '../shared/glyphs';
|
|
||||||
import { applyColorScheme } from '../shared/color-scheme';
|
import { applyColorScheme } from '../shared/color-scheme';
|
||||||
import {
|
import {
|
||||||
type VaultController, escapeHtml, typeIcon,
|
type VaultController, escapeHtml, typeIcon,
|
||||||
} from './vault-context';
|
} from './vault-context';
|
||||||
|
import { renderSidebarShell } from './vault-sidebar';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Type picker (right side panel)
|
// Type picker (right side panel)
|
||||||
@@ -110,24 +108,7 @@ export function renderShell(ctx: VaultController, app: HTMLElement): void {
|
|||||||
if (!app.querySelector('.vault-shell')) {
|
if (!app.querySelector('.vault-shell')) {
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="vault-shell">
|
<div class="vault-shell">
|
||||||
<div class="vault-sidebar">
|
${renderSidebarShell()}
|
||||||
<div class="vault-sidebar__header">
|
|
||||||
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
|
||||||
<span class="brand">Relicario</span>
|
|
||||||
</div>
|
|
||||||
<div class="vault-sidebar__search">
|
|
||||||
<input type="text" id="vault-search" placeholder="/ search…" />
|
|
||||||
</div>
|
|
||||||
<nav class="vault-sidebar__categories" id="vault-categories" aria-label="Item types"></nav>
|
|
||||||
<div class="vault-sidebar__nav">
|
|
||||||
<button class="vault-sidebar__nav-item vault-sidebar__nav-item--primary" data-nav="add" title="New item">+ new item</button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="trash" title="Trash">${GLYPH_TRASH} <span class="vault-sidebar__nav-label">trash</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="devices" title="Devices">${GLYPH_DEVICES} <span class="vault-sidebar__nav-label">devices</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="settings" title="Settings">${GLYPH_SETTINGS} <span class="vault-sidebar__nav-label">settings</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="history" title="History">${GLYPH_HISTORY} <span class="vault-sidebar__nav-label">history</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="lock" title="Lock">${GLYPH_LOCK} <span class="vault-sidebar__nav-label">lock</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="vault-list-pane" id="vault-list-pane"></div>
|
<div class="vault-list-pane" id="vault-list-pane"></div>
|
||||||
<div class="vault-pane" id="vault-pane"></div>
|
<div class="vault-pane" id="vault-pane"></div>
|
||||||
<div class="vault-drawer" id="vault-drawer"></div>
|
<div class="vault-drawer" id="vault-drawer"></div>
|
||||||
|
|||||||
174
extension/src/vault/vault-sidebar.ts
Normal file
174
extension/src/vault/vault-sidebar.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// Vault-tab sidebar column: its static markup, the category nav rendering,
|
||||||
|
// nav-button wiring, and the (now debounced) search input. Each function
|
||||||
|
// receives the VaultController (`ctx`) and reaches sibling concerns through it;
|
||||||
|
// pure helpers come from vault-context. Imports only from shared/ and
|
||||||
|
// vault-context — never from vault-shell or vault.ts.
|
||||||
|
|
||||||
|
import type { ItemType } from '../shared/types';
|
||||||
|
import {
|
||||||
|
GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_HISTORY, GLYPH_LOCK,
|
||||||
|
} from '../shared/glyphs';
|
||||||
|
import {
|
||||||
|
type VaultController, typeIcon, typeLabel, getFilteredEntries,
|
||||||
|
} from './vault-context';
|
||||||
|
|
||||||
|
const SEARCH_DEBOUNCE_MS = 80;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sidebar markup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function renderSidebarShell(): string {
|
||||||
|
return `
|
||||||
|
<div class="vault-sidebar">
|
||||||
|
<div class="vault-sidebar__header">
|
||||||
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||||
|
<span class="brand">Relicario</span>
|
||||||
|
</div>
|
||||||
|
<div class="vault-sidebar__search">
|
||||||
|
<input type="text" id="vault-search" placeholder="/ search…" />
|
||||||
|
</div>
|
||||||
|
<nav class="vault-sidebar__categories" id="vault-categories" aria-label="Item types"></nav>
|
||||||
|
<div class="vault-sidebar__nav">
|
||||||
|
<button class="vault-sidebar__nav-item vault-sidebar__nav-item--primary" data-nav="add" title="New item">+ new item</button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="trash" title="Trash">${GLYPH_TRASH} <span class="vault-sidebar__nav-label">trash</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="devices" title="Devices">${GLYPH_DEVICES} <span class="vault-sidebar__nav-label">devices</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="settings" title="Settings">${GLYPH_SETTINGS} <span class="vault-sidebar__nav-label">settings</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="history" title="History">${GLYPH_HISTORY} <span class="vault-sidebar__nav-label">history</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="lock" title="Lock">${GLYPH_LOCK} <span class="vault-sidebar__nav-label">lock</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="vault-sidebar__footer">
|
||||||
|
<!-- Phase 6 (Dev-C Task 6.3) wires the sync-status indicator into this slot. -->
|
||||||
|
<div id="vault-status-slot"></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sidebar wiring
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function wireSidebar(ctx: VaultController): void {
|
||||||
|
// Search (debounced — trailing edge)
|
||||||
|
const searchInput = document.getElementById('vault-search') as HTMLInputElement | null;
|
||||||
|
let searchTimer: number | undefined;
|
||||||
|
searchInput?.addEventListener('input', () => {
|
||||||
|
if (searchTimer !== undefined) clearTimeout(searchTimer);
|
||||||
|
searchTimer = window.setTimeout(() => {
|
||||||
|
ctx.state.searchQuery = searchInput.value;
|
||||||
|
renderSidebarCategories(ctx);
|
||||||
|
ctx.renderListPane();
|
||||||
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nav buttons
|
||||||
|
document.querySelectorAll('.vault-sidebar__nav-item').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const nav = (btn as HTMLElement).dataset.nav;
|
||||||
|
if (nav === 'lock') {
|
||||||
|
await ctx.sendMessage({ type: 'lock' });
|
||||||
|
ctx.state.unlocked = false;
|
||||||
|
ctx.state.selectedId = null;
|
||||||
|
ctx.state.selectedItem = null;
|
||||||
|
ctx.state.entries = [];
|
||||||
|
ctx.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nav === 'add') {
|
||||||
|
ctx.state.selectedId = null;
|
||||||
|
ctx.state.selectedItem = null;
|
||||||
|
ctx.state.newType = null;
|
||||||
|
ctx.state.drawerOpen = false;
|
||||||
|
ctx.closeDrawer();
|
||||||
|
ctx.openTypePanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nav === 'trash' || nav === 'devices' || nav === 'settings' || nav === 'history') {
|
||||||
|
ctx.state.selectedId = null;
|
||||||
|
ctx.state.selectedItem = null;
|
||||||
|
ctx.state.newType = null;
|
||||||
|
ctx.state.drawerOpen = false;
|
||||||
|
ctx.state.view = nav;
|
||||||
|
ctx.setHash(nav);
|
||||||
|
ctx.applyShellViewClass();
|
||||||
|
ctx.renderPane();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global "/" shortcut to focus search; Esc to close drawer
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === '/' && !isEditableTarget(e.target)) {
|
||||||
|
e.preventDefault();
|
||||||
|
searchInput?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape' && ctx.state.drawerOpen) {
|
||||||
|
ctx.closeDrawer();
|
||||||
|
ctx.renderListPane();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditableTarget(target: EventTarget | null): boolean {
|
||||||
|
if (!(target instanceof HTMLElement)) return false;
|
||||||
|
const tag = target.tagName;
|
||||||
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
||||||
|
if (target.isContentEditable) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Sidebar category nav
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function renderSidebarCategories(ctx: VaultController): void {
|
||||||
|
const container = document.getElementById('vault-categories');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const filtered = getFilteredEntries(ctx.state);
|
||||||
|
const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp'];
|
||||||
|
|
||||||
|
const allCount = filtered.length;
|
||||||
|
const isAllActive = !ctx.state.activeGroup && ctx.state.view === 'list';
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<button class="vault-category-row ${isAllActive ? 'vault-category-row--active' : ''}" data-group="">
|
||||||
|
<span class="vault-category-row__icon">◈</span>
|
||||||
|
<span class="vault-category-row__label vault-sidebar__category-label">All items</span>
|
||||||
|
<span class="vault-category-row__count vault-sidebar__category-count">${allCount}</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const t of typeOrder) {
|
||||||
|
const count = filtered.filter(([, e]) => e.type === t).length;
|
||||||
|
// Always show Login (staple type); hide other types when empty.
|
||||||
|
if (count === 0 && t !== 'login') continue;
|
||||||
|
const isActive = ctx.state.activeGroup === t;
|
||||||
|
html += `
|
||||||
|
<button class="vault-category-row ${isActive ? 'vault-category-row--active' : ''}" data-group="${t}">
|
||||||
|
<span class="vault-category-row__icon">${typeIcon(t)}</span>
|
||||||
|
<span class="vault-category-row__label vault-sidebar__category-label">${typeLabel(t)}</span>
|
||||||
|
<span class="vault-category-row__count vault-sidebar__category-count">${count}</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
container.querySelectorAll<HTMLButtonElement>('.vault-category-row').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
ctx.state.activeGroup = btn.dataset.group || null;
|
||||||
|
ctx.state.drawerOpen = false;
|
||||||
|
ctx.state.selectedId = null;
|
||||||
|
ctx.state.selectedItem = null;
|
||||||
|
ctx.state.view = 'list';
|
||||||
|
ctx.setHash('list');
|
||||||
|
ctx.applyShellViewClass();
|
||||||
|
renderSidebarCategories(ctx);
|
||||||
|
ctx.renderListPane();
|
||||||
|
ctx.closeDrawer();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -23,13 +23,14 @@ 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, typeLabel, getFilteredEntries,
|
escapeHtml, typeIcon, getFilteredEntries,
|
||||||
} from './vault-context';
|
} from './vault-context';
|
||||||
import {
|
import {
|
||||||
render, applyShellViewClass,
|
render, applyShellViewClass,
|
||||||
openTypePanel, closeTypePanel, applyVaultColorScheme,
|
openTypePanel, closeTypePanel, applyVaultColorScheme,
|
||||||
wireSessionExpiredListener,
|
wireSessionExpiredListener,
|
||||||
} from './vault-shell';
|
} from './vault-shell';
|
||||||
|
import { wireSidebar, renderSidebarCategories } from './vault-sidebar';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -144,7 +145,7 @@ const ctx: VaultController = {
|
|||||||
render: () => render(ctx),
|
render: () => render(ctx),
|
||||||
renderPane: () => renderPane(),
|
renderPane: () => renderPane(),
|
||||||
renderListPane: () => renderListPane(),
|
renderListPane: () => renderListPane(),
|
||||||
renderSidebarCategories: () => renderSidebarCategories(),
|
renderSidebarCategories: () => renderSidebarCategories(ctx),
|
||||||
renderDrawer: (item) => renderDrawer(item),
|
renderDrawer: (item) => renderDrawer(item),
|
||||||
applyShellViewClass: () => applyShellViewClass(ctx),
|
applyShellViewClass: () => applyShellViewClass(ctx),
|
||||||
setHash,
|
setHash,
|
||||||
@@ -153,7 +154,7 @@ const ctx: VaultController = {
|
|||||||
selectItemForDrawer: (id) => selectItemForDrawer(id),
|
selectItemForDrawer: (id) => selectItemForDrawer(id),
|
||||||
openTypePanel: () => openTypePanel(ctx),
|
openTypePanel: () => openTypePanel(ctx),
|
||||||
closeTypePanel: () => closeTypePanel(ctx),
|
closeTypePanel: () => closeTypePanel(ctx),
|
||||||
wireSidebar: () => wireSidebar(),
|
wireSidebar: () => wireSidebar(ctx),
|
||||||
loadManifest: () => loadManifest(),
|
loadManifest: () => loadManifest(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ registerHost({
|
|||||||
Object.assign(state, { view, error: null, loading: false, ...extras });
|
Object.assign(state, { view, error: null, loading: false, ...extras });
|
||||||
setHash(view as VaultView);
|
setHash(view as VaultView);
|
||||||
applyShellViewClass(ctx);
|
applyShellViewClass(ctx);
|
||||||
renderSidebarCategories();
|
renderSidebarCategories(ctx);
|
||||||
if (state.view === 'list') renderListPane();
|
if (state.view === 'list') renderListPane();
|
||||||
renderPane();
|
renderPane();
|
||||||
},
|
},
|
||||||
@@ -302,137 +303,12 @@ async function selectItemForDrawer(id: string): Promise<void> {
|
|||||||
state.selectedId = id;
|
state.selectedId = id;
|
||||||
state.selectedItem = data.item;
|
state.selectedItem = data.item;
|
||||||
state.drawerOpen = true;
|
state.drawerOpen = true;
|
||||||
renderSidebarCategories();
|
renderSidebarCategories(ctx);
|
||||||
renderListPane();
|
renderListPane();
|
||||||
renderDrawer(data.item);
|
renderDrawer(data.item);
|
||||||
openDrawer();
|
openDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Sidebar wiring
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function wireSidebar(): void {
|
|
||||||
// Search
|
|
||||||
const searchInput = document.getElementById('vault-search') as HTMLInputElement | null;
|
|
||||||
searchInput?.addEventListener('input', () => {
|
|
||||||
state.searchQuery = searchInput.value;
|
|
||||||
renderSidebarCategories();
|
|
||||||
renderListPane();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Nav buttons
|
|
||||||
document.querySelectorAll('.vault-sidebar__nav-item').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
const nav = (btn as HTMLElement).dataset.nav;
|
|
||||||
if (nav === 'lock') {
|
|
||||||
await sendMessage({ type: 'lock' });
|
|
||||||
state.unlocked = false;
|
|
||||||
state.selectedId = null;
|
|
||||||
state.selectedItem = null;
|
|
||||||
state.entries = [];
|
|
||||||
render(ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (nav === 'add') {
|
|
||||||
state.selectedId = null;
|
|
||||||
state.selectedItem = null;
|
|
||||||
state.newType = null;
|
|
||||||
state.drawerOpen = false;
|
|
||||||
closeDrawer();
|
|
||||||
openTypePanel(ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (nav === 'trash' || nav === 'devices' || nav === 'settings' || nav === 'history') {
|
|
||||||
state.selectedId = null;
|
|
||||||
state.selectedItem = null;
|
|
||||||
state.newType = null;
|
|
||||||
state.drawerOpen = false;
|
|
||||||
state.view = nav;
|
|
||||||
setHash(nav);
|
|
||||||
applyShellViewClass(ctx);
|
|
||||||
renderPane();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Global "/" shortcut to focus search; Esc to close drawer
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === '/' && !isEditableTarget(e.target)) {
|
|
||||||
e.preventDefault();
|
|
||||||
searchInput?.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === 'Escape' && state.drawerOpen) {
|
|
||||||
closeDrawer();
|
|
||||||
renderListPane();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEditableTarget(target: EventTarget | null): boolean {
|
|
||||||
if (!(target instanceof HTMLElement)) return false;
|
|
||||||
const tag = target.tagName;
|
|
||||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
||||||
if (target.isContentEditable) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Sidebar category nav
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function renderSidebarCategories(): void {
|
|
||||||
const container = document.getElementById('vault-categories');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const filtered = getFilteredEntries(state);
|
|
||||||
const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp'];
|
|
||||||
|
|
||||||
const allCount = filtered.length;
|
|
||||||
const isAllActive = !state.activeGroup && state.view === 'list';
|
|
||||||
|
|
||||||
let html = `
|
|
||||||
<button class="vault-category-row ${isAllActive ? 'vault-category-row--active' : ''}" data-group="">
|
|
||||||
<span class="vault-category-row__icon">◈</span>
|
|
||||||
<span class="vault-category-row__label vault-sidebar__category-label">All items</span>
|
|
||||||
<span class="vault-category-row__count vault-sidebar__category-count">${allCount}</span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (const t of typeOrder) {
|
|
||||||
const count = filtered.filter(([, e]) => e.type === t).length;
|
|
||||||
// Always show Login (staple type); hide other types when empty.
|
|
||||||
if (count === 0 && t !== 'login') continue;
|
|
||||||
const isActive = state.activeGroup === t;
|
|
||||||
html += `
|
|
||||||
<button class="vault-category-row ${isActive ? 'vault-category-row--active' : ''}" data-group="${t}">
|
|
||||||
<span class="vault-category-row__icon">${typeIcon(t)}</span>
|
|
||||||
<span class="vault-category-row__label vault-sidebar__category-label">${typeLabel(t)}</span>
|
|
||||||
<span class="vault-category-row__count vault-sidebar__category-count">${count}</span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
|
|
||||||
container.querySelectorAll<HTMLButtonElement>('.vault-category-row').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
state.activeGroup = btn.dataset.group || null;
|
|
||||||
state.drawerOpen = false;
|
|
||||||
state.selectedId = null;
|
|
||||||
state.selectedItem = null;
|
|
||||||
state.view = 'list';
|
|
||||||
setHash('list');
|
|
||||||
applyShellViewClass(ctx);
|
|
||||||
renderSidebarCategories();
|
|
||||||
renderListPane();
|
|
||||||
closeDrawer();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// List pane
|
// List pane
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -712,7 +588,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
if ((route.view === 'detail' || route.view === 'edit') && route.id) {
|
if ((route.view === 'detail' || route.view === 'edit') && route.id) {
|
||||||
if (state.selectedId === route.id && state.selectedItem) {
|
if (state.selectedId === route.id && state.selectedItem) {
|
||||||
renderPane();
|
renderPane();
|
||||||
renderSidebarCategories();
|
renderSidebarCategories(ctx);
|
||||||
if (state.view === 'list') renderListPane();
|
if (state.view === 'list') renderListPane();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -724,7 +600,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// For non-item views, just re-render the pane
|
// For non-item views, just re-render the pane
|
||||||
state.selectedId = null;
|
state.selectedId = null;
|
||||||
state.selectedItem = null;
|
state.selectedItem = null;
|
||||||
renderSidebarCategories();
|
renderSidebarCategories(ctx);
|
||||||
if (state.view === 'list') renderListPane();
|
if (state.view === 'list') renderListPane();
|
||||||
renderPane();
|
renderPane();
|
||||||
});
|
});
|
||||||
@@ -743,7 +619,7 @@ async function selectItem(id: ItemId): Promise<void> {
|
|||||||
state.selectedItem = data.item;
|
state.selectedItem = data.item;
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
setHash('detail', id);
|
setHash('detail', id);
|
||||||
renderSidebarCategories();
|
renderSidebarCategories(ctx);
|
||||||
renderListPane();
|
renderListPane();
|
||||||
renderPane();
|
renderPane();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user