@@ -1,17 +1,19 @@
/**
* Vault Home P age module for ObsiGate.
* Vault Home / navigation p age module for ObsiGate (#158) .
*
* W hen a specific vault is selected (not "All"), replaces the dashboar d
* 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.
* T he view is mounted by `TabManager.openNav(vault, dir)` as a dedicate d
* **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:cente r">
<div class="loading-spinner" style="width:20px;height:20px;border-width:2px;margin:0 auto"></div >
<div class="vault-home-erro r">
<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