feat: onglet de navigation avec facettes, tris et retour Home complet #158
CI / lint (push) Successful in 2m46s
CI / security (push) Successful in 1m57s
CI / build (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / test (push) Canceled after 1h50m30s

- clic sur un repertoire de l'arbre (modes focus vault et All) ouvre un onglet de navigation dedie qui coexiste avec les onglets de fichiers ouverts
- vue: sous-repertoires cliquables, facettes Vaults/Tags/Extensions (meme mecanisme que la recherche), tris Pertinence/Date, bouton Sauver; tags indexes exposes par GET /api/vault/{v}/files
- BUG-100: showWelcome re-injecte la section Raccourcis & Astuces et goHome efface la recherche globale + le filtre de la sidebar
This commit is contained in:
2026-10-02 12:27:27 -04:00
parent 5ffc9d851a
commit a50227849d
26 changed files with 870 additions and 232 deletions
+416 -214
View File
@@ -1,17 +1,19 @@
/**
* Vault Home Page module for ObsiGate.
* Vault Home / navigation page module for ObsiGate (#158).
*
* When a specific vault is selected (not "All"), replaces the dashboard
* with a non-recursive file listing sorted by modification time (newest first).
* Only files from the selected directory are shown (not subdirectories)
* to keep the listing fast and lightweight.
* The listing updates automatically when browsing into subdirectories.
* The view is mounted by `TabManager.openNav(vault, dir)` as a dedicated
* **navigation tab**: it opens on every directory click (vault root included,
* vault focus mode *and* "All" mode), coexists with open file tabs, and
* re-renders when the user drills into a subdirectory or the breadcrumb.
*
* The visual style mirrors the search results (.search-result-item) for
* consistency.
* Content: breadcrumb, sort/save toolbar, facets panel (Vaults · Tags ·
* Extensions — same mechanism and look as the search page), subdirectory list,
* recent files (root only) and the non-recursive file listing of the current
* directory.
*
* Usage:
* import { showVaultHome, updateVaultHomeDir, isVaultHomeActive } from './vaulthome.js';
* import { showVaultHome, isVaultHomeActive } from './vaulthome.js';
* // …but in practice: TabManager.openNav(vault, dirPath)
*/
import { api } from './auth.js';
@@ -19,6 +21,7 @@ import { el } from './viewer.js';
import { getFileIcon, safeCreateIcons } from './utils.js';
import { TabManager } from './ui.js';
import { t, getLocale } from './i18n.js';
import { buildNavFacets, filterNavFiles, sortNavFiles, navDisplayName } from './navfacets.js';
// ---------------------------------------------------------------------------
// State
@@ -27,25 +30,35 @@ import { t, getLocale } from './i18n.js';
const state = {
active: false,
currentVault: null,
currentDir: '', // empty = vault root
currentDir: '', // empty = vault root
sort: 'modified', // 'modified' (Date) | 'relevance' (Pertinence)
filterTag: '',
filterExt: '',
files: [], // last listing, unfiltered
dirs: [], // subdirectories of the current directory
};
// Handles of the mounted view (only one navigation view exists at a time).
let _view = null;
let _renderSeq = 0;
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** Check if vault home is currently displayed. */
/** Check if the navigation view is currently displayed. */
export function isVaultHomeActive() {
return state.active;
}
/**
* Display the vault home page for a vault directory.
* Display the navigation view for a vault directory.
* Called by TabManager when the navigation tab is activated.
* @param {string} vaultName - Vault name
* @param {string} dirPath - Directory path (empty for root)
*/
export async function showVaultHome(vaultName, dirPath = '') {
if (vaultName === 'all' || !vaultName) {
if (!vaultName || vaultName === 'all') {
state.active = false;
return;
}
@@ -53,6 +66,9 @@ export async function showVaultHome(vaultName, dirPath = '') {
state.active = true;
state.currentVault = vaultName;
state.currentDir = dirPath || '';
state.filterTag = '';
state.filterExt = '';
state.sort = 'modified';
const area = document.getElementById('content-area');
if (!area) return;
@@ -61,7 +77,7 @@ export async function showVaultHome(vaultName, dirPath = '') {
const home = document.getElementById('dashboard-home');
if (home) home.remove();
// Build vault home container
const seq = ++_renderSeq;
area.innerHTML = '';
const container = el('div', { class: 'vault-home', id: 'vault-home' });
@@ -70,117 +86,399 @@ export async function showVaultHome(vaultName, dirPath = '') {
// Breadcrumb
renderBreadcrumb(container, vaultName, state.currentDir);
// Sort + save toolbar (same classes as the search results header)
container.appendChild(buildToolbar());
// Facets host (filled once the listing is loaded)
const facetsHost = el('div', { id: 'vault-home-facets' });
container.appendChild(facetsHost);
// Results container
const resultsArea = el('div', { class: 'vault-home-results', id: 'vault-home-results' });
container.appendChild(resultsArea);
// Show loading
resultsArea.innerHTML = `
<div class="tree-loading" style="padding:2rem;text-align:center">
<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>
<p data-i18n="vaulthome.loading" style="margin-top:0.75rem;color:var(--text-muted);font-size:0.85rem">${t('vaulthome.loading')}</p>
</div>`;
safeCreateIcons();
await loadAndRender(vaultName, state.currentDir, resultsArea);
_view = { container, facetsHost, resultsArea, vault: vaultName, dirs: [] };
// Also load recent files for this vault (only at root level)
if (!state.currentDir) {
loadRecentSection(vaultName, container);
}
}
/**
* Update the vault home directory without rebuilding the whole page.
* Called when navigating into a subdirectory.
* @param {string} vaultName
* @param {string} dirPath
*/
export async function updateVaultHomeDir(vaultName, dirPath) {
if (!state.active || state.currentVault !== vaultName) return;
state.currentDir = dirPath || '';
const container = document.getElementById('vault-home');
if (!container) return;
// Update breadcrumb
const bc = container.querySelector('.vault-home-breadcrumb');
if (bc) {
bc.innerHTML = '';
buildBreadcrumbHTML(bc, vaultName, state.currentDir);
safeCreateIcons();
}
// Remove recent section when navigating into subdirectory
const recentSection = container.querySelector('.vault-home-recent');
if (recentSection) recentSection.remove();
// Reload results
const resultsArea = document.getElementById('vault-home-results');
if (resultsArea) {
// Listing + subdirectories
let files;
try {
const data = await api(
`/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(state.currentDir)}&limit=200&recursive=false`,
);
files = data.files || [];
} catch (err) {
if (seq !== _renderSeq) return;
const errMsg = escapeHtml(
t('vaulthome.error_loading') + ' ' + (err?.detail || err?.message || t('common.unknown')),
);
resultsArea.innerHTML = `
<div class="tree-loading" style="padding:2rem;text-align:center">
<div class="loading-spinner" style="width:20px;height:20px;border-width:2px;margin:0 auto"></div>
<div class="vault-home-error">
<p data-i18n="vaulthome.error_loading">${errMsg}</p>
</div>`;
await loadAndRender(vaultName, state.currentDir, resultsArea);
}
}
// ---------------------------------------------------------------------------
// Tree click delegation — update vault home on directory clicks
// ---------------------------------------------------------------------------
let _treeDelegationInstalled = false;
function _initTreeDelegation() {
if (_treeDelegationInstalled) return;
const tree = document.getElementById('vault-tree');
if (!tree) {
// Retry once when DOM might not be ready — use MutationObserver as fallback
const observer = new MutationObserver(() => {
const t = document.getElementById('vault-tree');
if (t) {
observer.disconnect();
_attachTreeListener(t);
}
});
observer.observe(document.body, { childList: true, subtree: true });
return;
}
_attachTreeListener(tree);
try {
const browse = await api(
`/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(state.currentDir)}`,
);
state.dirs = (browse.items || []).filter((i) => i.type === 'directory');
} catch {
state.dirs = []; // no subdirectory list — the file listing still renders
}
if (seq !== _renderSeq) return;
state.files = files;
_view.dirs = state.dirs;
const facets = buildFacets();
if (facets) facetsHost.appendChild(facets);
renderResults();
// Recent files for this vault (root level only)
if (!state.currentDir) {
loadRecentSection(vaultName, container, resultsArea);
}
}
function _attachTreeListener(tree) {
if (_treeDelegationInstalled) return;
_treeDelegationInstalled = true;
tree.addEventListener('click', (e) => {
if (!state.active) return;
// Find the closest tree-item that is a directory
const item = e.target.closest('.tree-item');
if (!item || !item.dataset.vault || item.dataset.path === undefined) return;
const vault = item.dataset.vault;
const path = item.dataset.path;
// Only react to directory clicks (items that have a child container)
const hasChildContainer = item.nextElementSibling
&& item.nextElementSibling.classList.contains('tree-children');
if (!hasChildContainer) return;
// Update vault home dir
updateVaultHomeDir(vault, path);
});
}
// Initialize delegation when module loads
_initTreeDelegation();
// ---------------------------------------------------------------------------
// Rendering
// Sort + save toolbar
// ---------------------------------------------------------------------------
function buildToolbar() {
const header = el('div', { class: 'search-results-header' });
const sortDiv = el('div', { class: 'search-sort' });
const btnRelevance = el('button', {
class: 'search-sort__btn' + (state.sort === 'relevance' ? ' active' : ''),
type: 'button',
});
btnRelevance.textContent = 'Pertinence';
btnRelevance.addEventListener('click', () => {
state.sort = 'relevance';
btnRelevance.classList.add('active');
btnDate.classList.remove('active');
renderResults();
});
const btnDate = el('button', {
class: 'search-sort__btn' + (state.sort === 'modified' ? ' active' : ''),
type: 'button',
});
btnDate.textContent = 'Date';
btnDate.addEventListener('click', () => {
state.sort = 'modified';
btnDate.classList.add('active');
btnRelevance.classList.remove('active');
renderResults();
});
sortDiv.appendChild(btnRelevance);
sortDiv.appendChild(btnDate);
header.appendChild(sortDiv);
// Save the current directory as a saved search — same payload as the search
// page's "save directory" action (query vide + include_paths).
const saveBtn = el('button', { class: 'search-save-btn', type: 'button', title: t('search.save_search') });
saveBtn.innerHTML = '<i data-lucide="bookmark-plus" style="width:14px;height:14px"></i> Sauver';
saveBtn.addEventListener('click', async () => {
try {
await api('/api/saved-searches', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: '',
vault: state.currentVault,
case_sensitive: false,
whole_word: false,
regex: false,
include_paths: state.currentDir ? state.currentDir + '/**' : '**',
exclude_paths: '',
}),
});
flashSaveBtn(saveBtn, false);
import('./viewer.js').then((m) => m.loadSavedSearches());
} catch (err) {
flashSaveBtn(saveBtn, true, err);
}
});
header.appendChild(saveBtn);
safeCreateIcons();
return header;
}
/** Tiny inline feedback on the save button (no toast module dependency). */
function flashSaveBtn(btn, isError, err) {
const original = btn.innerHTML;
if (isError) {
btn.title = String(err?.detail || err?.message || err);
}
btn.textContent = isError ? '✕' : '✓';
setTimeout(() => {
btn.innerHTML = original;
safeCreateIcons();
}, 1200);
}
// ---------------------------------------------------------------------------
// Facets panel — same mechanism and visuals as the search page (#157/#158)
// ---------------------------------------------------------------------------
function buildFacets() {
const facets = buildNavFacets(state.files);
// Parité avec la page de recherche : pas de panneau vide (listing sans fichier).
if (!Object.keys(facets.vaults).length && !Object.keys(facets.tags).length
&& !Object.keys(facets.extensions).length) {
return null;
}
const collapsed = localStorage.getItem('obsigate_facets_collapsed') === '1';
const facetsDiv = el('div', { class: 'search-facets', 'data-collapsed': collapsed ? 'true' : 'false' });
const toggleBtn = el('button', {
class: 'search-facets__toggle',
type: 'button',
'aria-expanded': String(!collapsed),
title: collapsed ? t('search.facets_expand') : t('search.facets_collapse'),
});
toggleBtn.innerHTML = '<i data-lucide="chevron-down" style="width:12px;height:12px"></i>';
toggleBtn.addEventListener('click', () => {
const nowCollapsed = facetsDiv.getAttribute('data-collapsed') !== 'true';
facetsDiv.setAttribute('data-collapsed', nowCollapsed ? 'true' : 'false');
toggleBtn.setAttribute('aria-expanded', String(!nowCollapsed));
toggleBtn.title = nowCollapsed ? t('search.facets_expand') : t('search.facets_collapse');
localStorage.setItem('obsigate_facets_collapsed', nowCollapsed ? '1' : '0');
});
facetsDiv.appendChild(toggleBtn);
const body = el('div', { class: 'search-facets__body' });
// Vaults — the view is scoped to one vault; clicking the chip jumps to the
// vault root (never a dead control).
if (Object.keys(facets.vaults).length > 0) {
const group = el('div', { class: 'search-facets__group' });
group.appendChild(facetLabel('Vaults'));
for (const [vaultName, count] of Object.entries(facets.vaults)) {
const item = facetItem(vaultName, count, false);
item.addEventListener('click', () => TabManager.openNav(vaultName, ''));
group.appendChild(item);
}
body.appendChild(group);
}
// Tags — filter the listing client-side (toggle).
if (Object.keys(facets.tags).length > 0) {
const group = el('div', { class: 'search-facets__group' });
group.appendChild(facetLabel('Tags'));
for (const [tagName, count] of Object.entries(facets.tags).slice(0, 12)) {
const item = facetItem('#' + tagName, count, state.filterTag === tagName);
item.addEventListener('click', () => {
state.filterTag = state.filterTag === tagName ? '' : tagName;
refreshView();
});
group.appendChild(item);
}
body.appendChild(group);
}
// Extensions — filter the listing client-side (toggle).
if (Object.keys(facets.extensions).length > 0) {
const group = el('div', { class: 'search-facets__group', 'data-facet': 'extensions' });
group.appendChild(facetLabel('Extensions'));
for (const [extName, count] of Object.entries(facets.extensions)) {
const item = facetItem('.' + extName, count, state.filterExt === extName);
item.addEventListener('click', () => {
state.filterExt = state.filterExt === extName ? '' : extName;
refreshView();
});
group.appendChild(item);
}
body.appendChild(group);
}
facetsDiv.appendChild(body);
return facetsDiv;
}
function facetLabel(text) {
const label = el('span', { class: 'search-facets__label' });
label.textContent = text;
return label;
}
function facetItem(label, count, active) {
// textContent: facet labels come from user data (names, tags, extensions),
// never innerHTML.
const item = el('span', { class: 'search-facets__item' + (active ? ' active' : '') });
item.appendChild(document.createTextNode(label + ' '));
const countEl = el('span', { class: 'facet-count' });
countEl.textContent = String(count);
item.appendChild(countEl);
return item;
}
/** Re-render facets + listing after a filter change (no refetch). */
function refreshView() {
if (!_view) return;
_view.facetsHost.replaceChildren();
const facets = buildFacets();
if (facets) _view.facetsHost.appendChild(facets);
renderResults();
safeCreateIcons();
}
// ---------------------------------------------------------------------------
// Listing: subdirectories + files
// ---------------------------------------------------------------------------
function renderResults() {
if (!_view) return;
const container = _view.resultsArea;
container.innerHTML = '';
const files = filterNavFiles(state.files, { tag: state.filterTag, ext: state.filterExt });
const isEmptyListing = state.files.length === 0;
if (isEmptyListing && state.dirs.length === 0) {
container.innerHTML = `
<div class="vault-home-empty">
<i data-lucide="folder-open" style="width:32px;height:32px;color:var(--text-muted)"></i>
<p data-i18n="vaulthome.empty">${t('vaulthome.empty')}</p>
</div>`;
safeCreateIcons();
return;
}
// Header: count + directory label
const header = el('div', { class: 'vault-home-header' });
const shown = isEmptyListing ? state.dirs.length : files.length;
const countKey = shown > 1 ? 'vaulthome.file_count_plural' : 'vaulthome.file_count';
header.appendChild(el('span', { class: 'vault-home-count', 'data-i18n': countKey }, [
document.createTextNode(t(countKey, { count: shown })),
]));
header.appendChild(el('span', { class: 'vault-home-dir-info' }, [
document.createTextNode(state.currentDir ? `/${state.currentDir}` : ` ${t('common.root')}`),
]));
container.appendChild(header);
// Active filter chips (same badges as the search page)
if (state.filterTag || state.filterExt) {
const activeFilters = el('div', { class: 'search-results-active-tags' });
const addChip = (label, onRemove) => {
const removeBtn = el('button', { class: 'search-results-active-tag-remove', title: label }, [
document.createTextNode('×'),
]);
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
onRemove();
});
activeFilters.appendChild(el('span', { class: 'search-results-active-tag' }, [
document.createTextNode(label), removeBtn,
]));
};
if (state.filterTag) addChip('#' + state.filterTag, () => { state.filterTag = ''; refreshView(); });
if (state.filterExt) addChip('.' + state.filterExt, () => { state.filterExt = ''; refreshView(); });
container.appendChild(activeFilters);
}
// Subdirectories — clickable, navigate deeper
if (state.dirs.length > 0) {
const section = el('div', { class: 'vault-home-recent vault-home-dirs' });
const secHeader = el('div', { class: 'vault-home-recent-header' });
secHeader.innerHTML = `
<i data-lucide="folders" style="width:14px;height:14px"></i>
<span data-i18n="vaulthome.directories">${t('vaulthome.directories')}</span>`;
section.appendChild(secHeader);
const list = el('div', { class: 'vault-home-recent-list' });
state.dirs.forEach((d) => {
const item = el('div', { class: 'vault-home-recent-item', 'data-path': d.path });
item.innerHTML = `
<i data-lucide="folder" style="width:14px;height:14px;opacity:0.6;flex-shrink:0"></i>
<span class="vault-home-recent-title">${escapeHtml(d.name)}</span>
<span class="vault-home-recent-time">(${d.children_count})</span>`;
item.addEventListener('click', () => TabManager.openNav(state.currentVault, d.path));
list.appendChild(item);
});
section.appendChild(list);
container.appendChild(section);
}
// File list — search-result-item style
if (files.length === 0) {
if (!isEmptyListing) {
container.appendChild(el('p', { style: 'color:var(--text-muted);margin-top:20px' }, [
document.createTextNode(t('search.no_results')),
]));
}
safeCreateIcons();
return;
}
const list = el('div', { class: 'search-results' });
const locale = getLocale();
sortNavFiles(files, state.sort).forEach((f) => {
list.appendChild(buildFileItem(f, locale));
});
container.appendChild(list);
safeCreateIcons();
}
function buildFileItem(f, locale) {
const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file';
const ext = f.extension || '';
const titleDiv = el('div', { class: 'search-result-title' }, [
document.createTextNode(navDisplayName(f)),
]);
const modDate = new Date(f.modified * 1000);
const dateStr = modDate.toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
const metaDiv = el('div', { class: 'search-result-vault' }, [
document.createTextNode([dateStr, formatSize(f.size)].join(' · ')),
]);
const item = el('div', {
class: 'search-result-item vault-home-item',
'data-vault': f.vault,
'data-path': f.path,
}, [
el('span', { class: 'search-result-ext' }, [document.createTextNode(ext)]),
el('i', { 'data-lucide': fileIconName, style: 'width:14px;height:14px;margin-right:4px;opacity:0.6' }),
titleDiv,
metaDiv,
]);
// Tags — clicking one applies the tag filter (like the search page)
if (f.tags && f.tags.length > 0) {
const tagsDiv = el('div', { class: 'search-result-tags' });
f.tags.forEach((tag) => {
const tagEl = el('span', { class: 'file-tag' }, [document.createTextNode('#' + tag)]);
tagEl.addEventListener('click', (e) => {
e.stopPropagation();
state.filterTag = tag;
refreshView();
});
tagsDiv.appendChild(tagEl);
});
item.appendChild(tagsDiv);
}
item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
item.addEventListener('dblclick', (e) => {
e.preventDefault();
TabManager.openPersistent(f.vault, f.path);
});
return item;
}
// ---------------------------------------------------------------------------
// Breadcrumb
// ---------------------------------------------------------------------------
function renderBreadcrumb(container, vaultName, dirPath) {
@@ -190,12 +488,13 @@ function renderBreadcrumb(container, vaultName, dirPath) {
}
function buildBreadcrumbHTML(parentEl, vaultName, dirPath) {
// Vault icon + name (root)
// Vault icon + name (root) — routed through the navigation tab so the tab
// cache and the view never drift apart.
const vaultLink = el('span', { class: 'vault-home-breadcrumb-item vault-home-breadcrumb-root' }, [
el('i', { 'data-lucide': 'database', style: 'width:14px;height:14px' }),
el('span', {}, [document.createTextNode(vaultName)]),
]);
vaultLink.addEventListener('click', () => updateVaultHomeDir(vaultName, ''));
vaultLink.addEventListener('click', () => TabManager.openNav(vaultName, ''));
vaultLink.style.cursor = 'pointer';
parentEl.appendChild(vaultLink);
@@ -205,126 +504,31 @@ function buildBreadcrumbHTML(parentEl, vaultName, dirPath) {
let cumulative = '';
for (const seg of segments) {
cumulative = cumulative ? cumulative + '/' + seg : seg;
const sep = el('span', { class: 'vault-home-breadcrumb-sep' }, [document.createTextNode(' / ')]);
parentEl.appendChild(sep);
parentEl.appendChild(el('span', { class: 'vault-home-breadcrumb-sep' }, [document.createTextNode(' / ')]));
const target = cumulative;
const segLink = el('span', { class: 'vault-home-breadcrumb-item' }, [
el('i', { 'data-lucide': 'folder', style: 'width:14px;height:14px' }),
el('span', {}, [document.createTextNode(seg)]),
]);
segLink.addEventListener('click', () => updateVaultHomeDir(vaultName, cumulative));
segLink.addEventListener('click', () => TabManager.openNav(vaultName, target));
segLink.style.cursor = 'pointer';
parentEl.appendChild(segLink);
}
}
async function loadAndRender(vaultName, dirPath, container) {
try {
const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200&recursive=false`;
const data = await api(url);
if (!data.files || data.files.length === 0) {
container.innerHTML = `
<div class="vault-home-empty">
<i data-lucide="folder-open" style="width:32px;height:32px;color:var(--text-muted)"></i>
<p data-i18n="vaulthome.empty">${t('vaulthome.empty')}</p>
</div>`;
safeCreateIcons();
return;
}
// Header
const header = el('div', { class: 'vault-home-header' });
const countKey = data.files.length > 1 ? 'vaulthome.file_count_plural' : 'vaulthome.file_count';
const countStr = t(countKey, { count: data.files.length });
const count = el('span', { class: 'vault-home-count', 'data-i18n': countKey }, [
document.createTextNode(countStr),
]);
header.appendChild(count);
// Directory label
const dirLabel = dirPath ? `/${dirPath}` : ` ${t('common.root')}`;
const dirInfo = el('span', { class: 'vault-home-dir-info' }, [
document.createTextNode(dirLabel),
]);
header.appendChild(dirInfo);
container.innerHTML = '';
container.appendChild(header);
// File list — using search-result-item style
const list = el('div', { class: 'search-results' });
const locale = getLocale();
data.files.forEach((f) => {
const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file';
const displayName = f.name.endsWith('.md') ? f.name.slice(0, -3) : f.name;
const ext = f.extension || '';
const titleDiv = el('div', { class: 'search-result-title' }, [
document.createTextNode(displayName),
]);
// Format date using current locale
const modDate = new Date(f.modified * 1000);
const dateStr = modDate.toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
// Build metadata line: date + size
const metaParts = [];
metaParts.push(dateStr);
metaParts.push(formatSize(f.size));
const metaDiv = el('div', { class: 'search-result-vault' }, [
document.createTextNode(metaParts.join(' · ')),
]);
const item = el('div', {
class: 'search-result-item vault-home-item',
'data-vault': f.vault,
'data-path': f.path,
}, [
el('span', { class: 'search-result-ext' }, [document.createTextNode(ext)]),
el('i', { 'data-lucide': fileIconName, style: 'width:14px;height:14px;margin-right:4px;opacity:0.6' }),
titleDiv,
metaDiv,
]);
item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
item.addEventListener('dblclick', (e) => {
e.preventDefault();
TabManager.openPersistent(f.vault, f.path);
});
list.appendChild(item);
});
container.appendChild(list);
safeCreateIcons();
} catch (err) {
const errMsg = t('vaulthome.error_loading') + ' ' + escapeHtml(err?.detail || err?.message || t('common.unknown'));
container.innerHTML = `
<div class="vault-home-error">
<p data-i18n="vaulthome.error_loading">${errMsg}</p>
</div>`;
}
}
// ---------------------------------------------------------------------------
// Recent files section (loaded at vault root only)
// Recent files section (root only)
// ---------------------------------------------------------------------------
async function loadRecentSection(vaultName, container) {
async function loadRecentSection(vaultName, container, resultsArea) {
try {
const data = await api(`/api/recent?mode=opened&vault=${encodeURIComponent(vaultName)}&limit=5`);
const files = data.files || [];
if (files.length === 0) return;
// The view may have been replaced while fetching
if (!container.isConnected) return;
// Insert recent section BEFORE the results area
const resultsArea = document.getElementById('vault-home-results');
const section = el('div', { class: 'vault-home-recent' });
const header = el('div', { class: 'vault-home-recent-header' });
header.innerHTML = `
<i data-lucide="clock" style="width:14px;height:14px"></i>
@@ -351,16 +555,14 @@ async function loadRecentSection(vaultName, container) {
});
list.appendChild(item);
});
section.appendChild(list);
// Insert before the results area
if (resultsArea) {
if (resultsArea && resultsArea.parentNode === container) {
container.insertBefore(section, resultsArea);
} else {
container.appendChild(section);
}
safeCreateIcons();
} catch {
// Silently skip if recent files can't be loaded