@@ -1,4 +1,4 @@
import { Component , EventEmitter , HostListener , Input , Output , inject } from '@angular/core' ;
import { Component , EventEmitter , HostListener , Input , Output , inject , effect } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { UiModeService } from '../../shared/services/ui-mode.service' ;
import { ResponsiveService } from '../../shared/services/responsive.service' ;
@@ -70,17 +70,30 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
</ng-container>
<ng-template #collapsedRail>
<aside class="border-r border-border dark:border-gray-800 h-full w-14 flex flex-col items-center py-3 gap-3">
<img src="assets/favicon.svg" alt="ObsiViewer" class="h-6 w-6 mb-2" />
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="toggleSidebarRequest.emit()" title="Show Sidebar">☰</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('quick')" (mouseleave)="scheduleCloseFlyout()" title="Quick Links">▦</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('folders')" (mouseleave)="scheduleCloseFlyout()" title="Folders">📁</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('tags')" (mouseleave)="scheduleCloseFlyout()" title="Tags">🏷️</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('trash')" (mouseleave)="scheduleCloseFlyout()" title="Trash">🗑️</button>
<div class="h-px w-8 bg-border dark:bg-gray-800 my-1"></div>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('playground')" (mouseleave)="scheduleCloseFlyout()" title="Markdown Playground">🧪</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('help')" (mouseleave)="scheduleCloseFlyout()" title="Help">🆘</button>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('about')" (mouseleave)="scheduleCloseFlyout()" title="About">ℹ ️</button>
</aside>
<!-- Flyouts -->
<div class="absolute left-14 top-0 bottom-0 w-80 max-w-[70vw] bg-card dark:bg-main border-r border-border dark:border-gray-800 shadow-xl z-50" *ngIf="hoveredFlyout as f" (mouseenter)="cancelCloseFlyout()" (mouseleave)="scheduleCloseFlyout()">
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
<div class="text-sm font-semibold">{{ f === 'quick' ? 'Quick Links' : (f === 'folders' ? 'Folders' : (f === 'tags' ? 'Tags' : 'Trash')) }}</div>
<div class="text-sm font-semibold">{{
f === 'quick' ? 'Quick Links' :
(f === 'folders' ? 'Folders' :
(f === 'tags' ? 'Tags' :
(f === 'trash' ? 'Trash' :
(f === 'help' ? 'Help' :
(f === 'about' ? 'About' :
(f === 'playground' ? 'Playground' : ''))))))
}}</div>
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="hoveredFlyout=null">✕</button>
</div>
<div class="h-[calc(100%-3rem)] overflow-y-auto" appScrollableOverlay>
@@ -96,6 +109,30 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
</li>
</ul>
</div>
<div *ngSwitchCase="'trash'" class="p-2">
<ng-container *ngIf="(vault.trashTree() || []).length > 0; else emptyTrash">
<app-file-explorer
[nodes]="vault.trashTree()"
[selectedNoteId]="selectedNoteId"
[foldersOnly]="true"
[useTrashCounts]="true"
(folderSelected)="onFolderSelected( $ event)"
(fileSelected)="noteSelected.emit( $ event)">
</app-file-explorer>
</ng-container>
<ng-template #emptyTrash>
<div class="px-3 py-2 text-muted text-sm">La corbeille est vide</div>
</ng-template>
</div>
<div *ngSwitchCase="'help'" class="p-3">
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onHelpPageSelected()">🆘 <span>Help Page</span></button>
</div>
<div *ngSwitchCase="'about'" class="p-3">
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onAboutSelected()">ℹ ️ <span>About</span></button>
</div>
<div *ngSwitchCase="'playground'" class="p-3">
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onMarkdownPlaygroundSelected()">🧪 <span>Markdown Playground</span></button>
</div>
<div *ngSwitchDefault class="p-3 text-sm text-muted">Empty</div>
</ng-container>
</div>
@@ -115,7 +152,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
[quickLinkFilter]="quickLinkFilter"
[query]="listQuery"
(openNote)="onOpenNote( $ event)"
(queryChange)="list Query = $ event"
(queryChange)="on QueryChange( $ event) "
/>
</div>
</section>
@@ -190,7 +227,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolderSelected( $ event)" (fileSelected)="onOpenNote( $ event)"></app-file-explorer>
</div>
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="list Query= $ event" (openNote)="onOpenNote( $ event)"></app-notes-list>
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="on QueryChange( $ event) " (openNote)="onOpenNote( $ event)"></app-notes-list>
</div>
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
@@ -218,7 +255,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
@if (mobileNav.activeTab() === 'list') {
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="list Query= $ event" (openNote)="onNoteSelectedMobile( $ event)"></app-notes-list>
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="on QueryChange( $ event) " (openNote)="onNoteSelectedMobile( $ event)"></app-notes-list>
</div>
}
@@ -296,23 +333,78 @@ export class AppShellNimbusLayoutComponent {
folderFilter : string | null = null ;
listQuery : string = '' ;
hoveredFlyout : 'quick' | 'folders' | 'tags' | 'trash' | null = null ;
hoveredFlyout : 'quick' | 'folders' | 'tags' | 'trash' | 'help' | 'about' | 'playground' | null = null ;
private flyoutCloseTimer : any = null ;
tagFilter : string | null = null ;
quickLinkFilter : 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null = null ;
// Auto-select first note when filters change
private autoSelectFirstNote() {
const filteredNotes = this . getFilteredNotes ( ) ;
if ( filteredNotes . length > 0 && filteredNotes [ 0 ] . id !== this . selectedNoteId ) {
this . noteSelected . emit ( filteredNotes [ 0 ] . id ) ;
}
}
private getFilteredNotes ( ) : Note [ ] {
const q = ( this . listQuery || '' ) . toLowerCase ( ) . trim ( ) ;
const folder = ( this . folderFilter || '' ) . toLowerCase ( ) . replace ( /^\/+|\/+$/g , '' ) ;
const tag = ( this . tagFilter || '' ) . toLowerCase ( ) ;
const quickLink = this . quickLinkFilter ;
let list = this . vault . allNotes ( ) ;
if ( folder ) {
if ( folder === '.trash' ) {
// All files anywhere under .trash (including subfolders)
list = list . filter ( n = > {
const filePath = ( n . filePath || n . originalPath || '' ) . toLowerCase ( ) . replace ( /\\/g , '/' ) ;
return filePath . startsWith ( '.trash/' ) || filePath . includes ( '/.trash/' ) ;
} ) ;
} else {
list = list . filter ( n = > {
const originalPath = ( n . originalPath || '' ) . toLowerCase ( ) . replace ( /^\/+|\/+$/g , '' ) ;
return originalPath === folder || originalPath . startsWith ( folder + '/' ) ;
} ) ;
}
}
if ( tag ) {
list = list . filter ( n = > Array . isArray ( n . tags ) && n . tags . some ( t = > ( t || '' ) . toLowerCase ( ) === tag ) ) ;
}
// Apply Quick Link filter
if ( quickLink ) {
list = list . filter ( n = > {
const frontmatter = n . frontmatter || { } ;
return frontmatter [ quickLink ] === true ;
} ) ;
}
// Apply query if present
if ( q ) {
list = list . filter ( n = > {
const title = ( n . title || '' ) . toLowerCase ( ) ;
const filePath = ( n . filePath || '' ) . toLowerCase ( ) ;
return title . includes ( q ) || filePath . includes ( q ) ;
} ) ;
}
// Sort by most recent first (same as notes-list component)
const parseDate = ( s? : string ) = > ( s ? Date . parse ( s ) : 0 ) || 0 ;
const score = ( n : Note ) = > n . mtime || parseDate ( n . updatedAt ) || parseDate ( n . createdAt ) || 0 ;
return [ . . . list ] . sort ( ( a , b ) = > ( score ( b ) - score ( a ) ) ) ;
}
onQueryChange ( query : string ) {
this . listQuery = query ;
this . autoSelectFirstNote ( ) ;
}
toggleNoteFullScreen ( ) : void {
this . noteFullScreen = ! this . noteFullScreen ;
document . body . classList . toggle ( 'note-fullscreen-active' , this . noteFullScreen ) ;
}
@HostListener ( 'document:keydown.escape' )
handleEscape ( ) : void {
if ( ! this . noteFullScreen ) return ;
this . noteFullScreen = false ;
document . body . classList . remove ( 'note-fullscreen-active' ) ;
}
nextTab() {
const order : Array < 'sidebar' | 'list' | 'page' | 'toc' > = [ 'sidebar' , 'list' , 'page' , 'toc' ] ;
const idx = order . indexOf ( this . mobileNav . activeTab ( ) ) ;
@@ -340,6 +432,7 @@ export class AppShellNimbusLayoutComponent {
onFolderSelected ( path : string ) {
this . folderFilter = path || null ;
this . tagFilter = null ; // clear tag when focusing folder
this . autoSelectFirstNote ( ) ;
if ( this . responsive . isMobile ( ) || this . responsive . isTablet ( ) ) {
this . mobileNav . setActiveTab ( 'list' ) ;
}
@@ -347,6 +440,8 @@ export class AppShellNimbusLayoutComponent {
onFolderSelectedFromDrawer ( path : string ) {
this . folderFilter = path || null ;
this . tagFilter = null ; // clear tag when focusing folder
this . autoSelectFirstNote ( ) ;
this . mobileNav . setActiveTab ( 'list' ) ;
this . mobileNav . sidebarOpen . set ( false ) ;
}
@@ -433,6 +528,8 @@ export class AppShellNimbusLayoutComponent {
}
this . scheduleCloseFlyout ( 150 ) ;
}
// Auto-select first note after filter changes
this . autoSelectFirstNote ( ) ;
}
onTagSelected ( tagName : string ) {
@@ -443,6 +540,8 @@ export class AppShellNimbusLayoutComponent {
// Clear other filters and search to focus on tag results
this . quickLinkFilter = null ;
this . listQuery = '' ;
// Auto-select first note after filter changes
this . autoSelectFirstNote ( ) ;
// Ensure the list is visible: exit fullscreen if active
if ( this . noteFullScreen ) {
this . noteFullScreen = false ;