feat: redesign UI with simplified header and enhanced mobile navigation
This commit is contained in:
parent
e2775a3d43
commit
8cccf83f9a
@ -110,7 +110,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM5.5 21a6.5 6.5 0 0113 0" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM5.5 21a6.5 6.5 0 0113 0" /></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-sm font-semibold tracking-wide text-obs-l-text-main dark:text-obs-d-text-main">{{ vaultName() }}</span>
|
<span class="text-sm font-semibold tracking-wide text-obs-l-text-main dark:text-obs-d-text-main">{{ vaultName() }} - ObsiViewer</span>
|
||||||
<div class="mt-1 flex items-center gap-2 text-xs uppercase text-obs-l-text-muted dark:text-obs-d-text-muted">
|
<div class="mt-1 flex items-center gap-2 text-xs uppercase text-obs-l-text-muted dark:text-obs-d-text-muted">
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-obs-l-bg-main/70 px-2 py-0.5 text-[0.65rem] font-semibold tracking-widest text-obs-l-text-main/80 dark:bg-obs-d-bg-main/60 dark:text-obs-d-text-main/80">{{ activeView() | titlecase }}</span>
|
<span class="inline-flex items-center gap-1 rounded-full bg-obs-l-bg-main/70 px-2 py-0.5 text-[0.65rem] font-semibold tracking-widest text-obs-l-text-main/80 dark:bg-obs-d-bg-main/60 dark:text-obs-d-text-main/80">{{ activeView() | titlecase }}</span>
|
||||||
<span class="hidden text-[0.65rem] tracking-widest text-obs-l-text-muted/80 dark:text-obs-d-text-muted/70 sm:inline">Vue active</span>
|
<span class="hidden text-[0.65rem] tracking-widest text-obs-l-text-muted/80 dark:text-obs-d-text-muted/70 sm:inline">Vue active</span>
|
||||||
@ -365,83 +365,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="flex min-w-0 flex-col bg-obs-l-bg-main pb-16 dark:bg-obs-d-bg-main lg:flex-1 lg:min-h-0 lg:overflow-hidden lg:pb-0">
|
<section class="flex min-w-0 flex-col bg-obs-l-bg-main pb-16 dark:bg-obs-d-bg-main lg:flex-1 lg:min-h-0 lg:overflow-hidden lg:pb-0">
|
||||||
<header class="flex flex-col gap-4 border-b border-obs-l-border/60 bg-obs-l-bg-main/95 px-4 py-3 backdrop-blur-xs dark:border-obs-d-border dark:bg-obs-d-bg-main/95 lg:px-6">
|
|
||||||
<div class="flex items-start justify-between gap-4">
|
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
|
||||||
<button
|
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary lg:hidden"
|
|
||||||
(click)="toggleSidebar()"
|
|
||||||
[attr.aria-expanded]="isSidebarOpen()"
|
|
||||||
aria-label="Basculer le menu"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="hidden rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary lg:inline-flex"
|
|
||||||
(click)="toggleSidebar()"
|
|
||||||
[attr.aria-expanded]="isSidebarOpen()"
|
|
||||||
aria-label="Afficher ou masquer la barre latérale gauche"
|
|
||||||
>
|
|
||||||
@if (isSidebarOpen()) {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>
|
|
||||||
} @else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m14 9 3 3-3 3"/></svg>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
<div class="min-w-0 flex flex-col gap-1">
|
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
|
||||||
<span class="inline-flex items-center rounded-lg border border-obs-l-border bg-obs-l-bg-secondary/60 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-obs-l-text-muted dark:border-obs-d-border dark:bg-obs-d-bg-secondary/60 dark:text-obs-d-text-muted">{{ vaultName() }}</span>
|
|
||||||
<h1 class="text-lg font-semibold leading-tight text-obs-l-text-main dark:text-obs-d-text-main">ObsiWatcher</h1>
|
|
||||||
</div>
|
|
||||||
@if (selectedNoteBreadcrumb().length > 0) {
|
|
||||||
<p class="truncate text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">{{ selectedNoteBreadcrumb().join(' / ') }}</p>
|
|
||||||
} @else {
|
|
||||||
<p class="truncate text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">Aucune note sélectionnée</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="hidden items-center gap-2 lg:flex">
|
|
||||||
<button
|
|
||||||
(click)="toggleTheme()"
|
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
|
||||||
aria-label="Basculer le thème"
|
|
||||||
>
|
|
||||||
@if (isDarkMode()) {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
|
||||||
} @else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between lg:gap-6">
|
|
||||||
<div class="flex items-center gap-2 lg:hidden">
|
|
||||||
<button
|
|
||||||
(click)="toggleTheme()"
|
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
|
||||||
aria-label="Basculer le thème"
|
|
||||||
>
|
|
||||||
@if (isDarkMode()) {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
|
||||||
} @else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="relative w-full lg:flex-1 lg:max-w-none lg:min-w-0">
|
|
||||||
<app-search-input-with-assistant
|
|
||||||
[value]="sidebarSearchTerm()"
|
|
||||||
(valueChange)="updateSearchTerm($event, true)"
|
|
||||||
(submit)="onSearchSubmit($event)"
|
|
||||||
[placeholder]="'Rechercher dans la voûte...'"
|
|
||||||
[context]="'vault-sidebar'"
|
|
||||||
[showSearchIcon]="true"
|
|
||||||
[showExamples]="false"
|
|
||||||
[inputClass]="'w-full rounded-full border border-border bg-bg-muted/70 py-2.5 pr-4 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring transition-all dark:border-gray-600 dark:bg-gray-800/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-blue-500'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="min-h-0 px-4 py-6 note-content-area lg:flex-1 lg:overflow-y-auto lg:px-8">
|
<div class="min-h-0 px-4 py-6 note-content-area lg:flex-1 lg:overflow-y-auto lg:px-8">
|
||||||
@if (selectedNote(); as note) {
|
@if (selectedNote(); as note) {
|
||||||
<app-note-viewer
|
<app-note-viewer
|
||||||
|
|||||||
@ -1170,11 +1170,18 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
scrollToHeading(id: string): void {
|
scrollToHeading(id: string): void {
|
||||||
const contentArea = (this.elementRef.nativeElement as HTMLElement).querySelector('.note-content-area');
|
const contentArea = (this.elementRef.nativeElement as HTMLElement).querySelector('.note-content-area');
|
||||||
if (!contentArea) {
|
if (!contentArea) {
|
||||||
|
console.warn('scrollToHeading: .note-content-area not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const element = contentArea.querySelector(`#${id}`);
|
// Try multiple selector strategies for robustness
|
||||||
|
let element = contentArea.querySelector(`#${CSS.escape(id)}`);
|
||||||
|
if (!element) {
|
||||||
|
element = contentArea.querySelector(`[id="${id}"]`);
|
||||||
|
}
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} else {
|
||||||
|
console.warn(`scrollToHeading: element with id "${id}" not found`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,22 +7,38 @@ import { MobileNavService } from '../../shared/services/mobile-nav.service';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
template: `
|
template: `
|
||||||
<nav class="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 h-16 flex justify-around items-center z-50">
|
<nav class="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 h-16 flex justify-around items-center z-50 safe-area-inset-bottom">
|
||||||
<button *ngFor="let tab of tabs"
|
<button *ngFor="let tab of tabs"
|
||||||
(click)="setActiveTab(tab.id)"
|
(click)="setActiveTab(tab.id)"
|
||||||
class="flex-1 flex flex-col items-center justify-center gap-1 text-xs py-2 px-1 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="relative flex-1 flex flex-col items-center justify-center gap-1 text-xs py-2 px-1 text-gray-600 dark:text-gray-400 transition-all duration-200 active:scale-95 transform"
|
||||||
[class.text-nimbus-500]="mobileNav.activeTab() === tab.id">
|
[class.text-nimbus-500]="mobileNav.activeTab() === tab.id"
|
||||||
<span class="text-lg">{{ tab.icon }}</span>
|
[class.font-semibold]="mobileNav.activeTab() === tab.id">
|
||||||
|
<!-- Active indicator -->
|
||||||
|
<div
|
||||||
|
class="absolute top-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-nimbus-500 rounded-full transition-all duration-300"
|
||||||
|
[class.opacity-100]="mobileNav.activeTab() === tab.id"
|
||||||
|
[class.opacity-0]="mobileNav.activeTab() !== tab.id">
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-lg transition-transform duration-200"
|
||||||
|
[class.scale-110]="mobileNav.activeTab() === tab.id">
|
||||||
|
{{ tab.icon }}
|
||||||
|
</span>
|
||||||
<span class="truncate">{{ tab.label }}</span>
|
<span class="truncate">{{ tab.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
`
|
`,
|
||||||
|
styles: [`
|
||||||
|
.safe-area-inset-bottom {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
`]
|
||||||
})
|
})
|
||||||
export class AppBottomNavigationComponent {
|
export class AppBottomNavigationComponent {
|
||||||
mobileNav = inject(MobileNavService);
|
mobileNav = inject(MobileNavService);
|
||||||
|
|
||||||
tabs = [
|
tabs = [
|
||||||
{ id: 'sidebar', icon: '📁', label: 'Dossiers' },
|
{ id: 'sidebar', icon: '🏠', label: 'Maison' },
|
||||||
{ id: 'list', icon: '🔍', label: 'Liste' },
|
{ id: 'list', icon: '🔍', label: 'Liste' },
|
||||||
{ id: 'page', icon: '📄', label: 'Page' },
|
{ id: 'page', icon: '📄', label: 'Page' },
|
||||||
{ id: 'toc', icon: '📋', label: 'Sommaire' }
|
{ id: 'toc', icon: '📋', label: 'Sommaire' }
|
||||||
|
|||||||
@ -1,33 +1,93 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, OnInit, Output, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-toc-overlay',
|
selector: 'app-toc-overlay',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [CommonModule, ScrollableOverlayDirective],
|
||||||
template: `
|
template: `
|
||||||
<div class="fixed inset-0 z-50 bg-black/50" (click)="close.emit()"></div>
|
<!-- Backdrop -->
|
||||||
<div class="fixed inset-x-0 bottom-0 z-50 max-h-[70vh] rounded-t-2xl bg-white dark:bg-gray-900 shadow-2xl border-t border-gray-200 dark:border-gray-800">
|
<div
|
||||||
<div class="p-4 flex items-center justify-between border-b border-gray-200 dark:border-gray-800">
|
class="fixed inset-0 z-[80] bg-black/50 transition-opacity duration-300"
|
||||||
<h2 class="text-base font-semibold">Sommaire</h2>
|
[class.opacity-0]="!isVisible"
|
||||||
<button (click)="close.emit()" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">✕</button>
|
[class.opacity-100]="isVisible"
|
||||||
|
[style.pointer-events]="isVisible ? 'auto' : 'none'"
|
||||||
|
(pointerdown)="onBackdropPointerDown($event)"
|
||||||
|
aria-hidden="true">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom sheet -->
|
||||||
|
<div
|
||||||
|
class="fixed inset-x-0 bottom-0 z-[81] max-h-[85vh] rounded-t-2xl bg-white dark:bg-slate-900 shadow-2xl
|
||||||
|
transition-transform duration-300 ease-out
|
||||||
|
sm:left-auto sm:right-4 sm:bottom-4 sm:top-auto sm:w-[420px] sm:rounded-2xl sm:max-h-[80vh]"
|
||||||
|
[class.translate-y-full]="!isVisible"
|
||||||
|
[class.translate-y-0]="isVisible"
|
||||||
|
[style.pointer-events]="isVisible ? 'auto' : 'none'"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
(click)="$event.stopPropagation()">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center py-3 sm:py-2">
|
||||||
|
<div class="h-1.5 w-12 rounded-full bg-slate-300 dark:bg-slate-700"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-3 overflow-y-auto">
|
|
||||||
<ul class="space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
<div class="px-4 pb-4 overflow-y-auto max-h-[75vh] sm:max-h-[70vh]" appScrollableOverlay>
|
||||||
<li *ngFor="let h of headings">
|
<h2 class="sr-only">Sommaire</h2>
|
||||||
<a (click)="onGo(h.id)" class="block px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer" [style.paddingLeft.rem]="(h.level - 1) * 0.75">{{ h.text }}</a>
|
<ul class="space-y-1 text-sm text-slate-800 dark:text-slate-200">
|
||||||
|
<li *ngFor="let h of headings; let i = index">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="onGo(h.id)"
|
||||||
|
class="w-full text-left block px-3 py-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition active:scale-[0.98]"
|
||||||
|
[style.paddingLeft.rem]="(h.level - 1) * 0.75 + 0.75"
|
||||||
|
[attr.data-index]="i">
|
||||||
|
<span class="text-slate-400 dark:text-slate-500 mr-2 text-xs">{{ getLevelIndicator(h.level) }}</span>
|
||||||
|
<span>{{ h.text }}</span>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`,
|
||||||
})
|
})
|
||||||
export class AppTocOverlayComponent {
|
export class AppTocOverlayComponent implements OnInit {
|
||||||
@Input() headings: Array<{ level: number; text: string; id: string }> = [];
|
@Input() headings: Array<{ level: number; text: string; id: string }> = [];
|
||||||
@Output() go = new EventEmitter<string>();
|
@Output() go = new EventEmitter<string>();
|
||||||
@Output() close = new EventEmitter<void>();
|
@Output() close = new EventEmitter<void>();
|
||||||
|
|
||||||
onGo(id: string) {
|
isVisible = false;
|
||||||
|
private ignoreBackdropUntil = 0;
|
||||||
|
|
||||||
|
constructor(private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
this.ignoreBackdropUntil = now + 500; // couvre les devices + lents
|
||||||
|
|
||||||
|
// Déclenche l’animation d’entrée puis force le rafraîchissement (OnPush)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isVisible = true;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackdropPointerDown(ev: PointerEvent): void {
|
||||||
|
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||||
|
if (now < this.ignoreBackdropUntil) {
|
||||||
|
ev.stopPropagation(); // ignore le premier tap fantôme
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.close.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
onGo(id: string): void {
|
||||||
this.go.emit(id);
|
this.go.emit(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLevelIndicator(level: number): string {
|
||||||
|
return '•'.repeat(Math.min(level, 3));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,33 +2,132 @@ import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { MobileNavService } from '../../shared/services/mobile-nav.service';
|
import { MobileNavService } from '../../shared/services/mobile-nav.service';
|
||||||
import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component';
|
import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component';
|
||||||
import type { VaultNode } from '../../../types';
|
import { QuickLinksComponent } from '../quick-links/quick-links.component';
|
||||||
|
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
|
||||||
|
import type { VaultNode, TagInfo } from '../../../types';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sidebar-drawer',
|
selector: 'app-sidebar-drawer',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FileExplorerComponent],
|
imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective],
|
||||||
template: `
|
template: `
|
||||||
<aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-white dark:bg-gray-900 shadow-lg z-40 transform transition-transform duration-300 ease-in-out"
|
<aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-white dark:bg-gray-900 shadow-2xl z-40 transform transition-all duration-300 ease-out flex flex-col"
|
||||||
[class.-translate-x-full]="!mobileNav.sidebarOpen()">
|
[class.-translate-x-full]="!mobileNav.sidebarOpen()"
|
||||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
|
[class.translate-x-0]="mobileNav.sidebarOpen()">
|
||||||
<h2 class="text-lg font-semibold">Navigation</h2>
|
<!-- Header -->
|
||||||
<button (click)="mobileNav.toggleSidebar()" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">✕</button>
|
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800 bg-gradient-to-r from-nimbus-50 to-transparent dark:from-nimbus-900/20">
|
||||||
|
<h2 class="text-lg font-semibold truncate">{{ vaultName || 'ObsiViewer' }}</h2>
|
||||||
|
<button
|
||||||
|
(click)="mobileNav.toggleSidebar()"
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all active:scale-95 transform flex-shrink-0">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-y-auto p-2">
|
|
||||||
<app-file-explorer [nodes]="nodes" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolder($event)" (fileSelected)="onSelect($event)"></app-file-explorer>
|
<!-- Scrollable Content -->
|
||||||
|
<div class="flex-1 overflow-y-auto min-h-0" appScrollableOverlay>
|
||||||
|
<!-- Section Tests (dev-only) -->
|
||||||
|
<section *ngIf="env.features.showTestSection" class="border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||||
|
(click)="open.tests = !open.tests">
|
||||||
|
<span>Section Tests</span>
|
||||||
|
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.tests">{{ open.tests ? '▾' : '▸' }}</span>
|
||||||
|
</button>
|
||||||
|
<div *ngIf="open.tests" class="px-3 py-2">
|
||||||
|
<button
|
||||||
|
(click)="onMarkdownPlaygroundClick()"
|
||||||
|
class="w-full text-left block text-sm px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-all active:scale-[0.98] transform">
|
||||||
|
🧪 Markdown Playground
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Quick Links accordion -->
|
||||||
|
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||||
|
(click)="open.quick = !open.quick">
|
||||||
|
<span>Quick Links</span>
|
||||||
|
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.quick">{{ open.quick ? '▾' : '▸' }}</span>
|
||||||
|
</button>
|
||||||
|
<div *ngIf="open.quick" class="pt-1">
|
||||||
|
<app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Folders accordion -->
|
||||||
|
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||||
|
(click)="open.folders = !open.folders">
|
||||||
|
<span>Folders</span>
|
||||||
|
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span>
|
||||||
|
</button>
|
||||||
|
<div *ngIf="open.folders" class="px-1 py-1">
|
||||||
|
<app-file-explorer [nodes]="nodes" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolder($event)" (fileSelected)="onSelect($event)"></app-file-explorer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tags accordion -->
|
||||||
|
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||||
|
(click)="open.tags = !open.tags">
|
||||||
|
<span>Tags</span>
|
||||||
|
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.tags">{{ open.tags ? '▾' : '▸' }}</span>
|
||||||
|
</button>
|
||||||
|
<div *ngIf="open.tags" class="px-2 py-2">
|
||||||
|
<ul class="space-y-1 text-sm">
|
||||||
|
<li *ngFor="let t of tags" class="flex items-center gap-2">
|
||||||
|
<button (click)="onTagSelected(t.name)" class="flex-1 text-left px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-all active:scale-[0.98] transform truncate">
|
||||||
|
<span>🏷️</span>
|
||||||
|
<span class="ml-1">{{ t.name }}</span>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-gray-500 font-medium min-w-[2rem] text-right">{{ t.count }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Trash accordion -->
|
||||||
|
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||||
|
(click)="open.trash = !open.trash">
|
||||||
|
<span>Trash</span>
|
||||||
|
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
|
||||||
|
</button>
|
||||||
|
<div *ngIf="open.trash" class="px-3 py-3 text-sm text-gray-500 dark:text-gray-400">Empty</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="h-14 border-t border-gray-200 dark:border-gray-800 flex items-center justify-between px-4 text-xs text-gray-500 bg-gray-50 dark:bg-gray-900/50">
|
||||||
|
<span>ObsiViewer</span>
|
||||||
|
<span class="text-[10px] opacity-60">v1.0</span>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div *ngIf="mobileNav.sidebarOpen()" (click)="mobileNav.toggleSidebar()" class="fixed inset-0 bg-black/50 z-30"></div>
|
<div
|
||||||
|
*ngIf="mobileNav.sidebarOpen()"
|
||||||
|
(click)="mobileNav.toggleSidebar()"
|
||||||
|
class="fixed inset-0 bg-black/50 z-30 transition-opacity duration-300 backdrop-blur-sm"
|
||||||
|
[class.opacity-0]="!mobileNav.sidebarOpen()"
|
||||||
|
[class.opacity-100]="mobileNav.sidebarOpen()">
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
export class AppSidebarDrawerComponent {
|
export class AppSidebarDrawerComponent {
|
||||||
mobileNav = inject(MobileNavService);
|
mobileNav = inject(MobileNavService);
|
||||||
|
env = environment;
|
||||||
|
|
||||||
@Input() nodes: VaultNode[] = [];
|
@Input() nodes: VaultNode[] = [];
|
||||||
@Input() selectedNoteId: string | null = null;
|
@Input() selectedNoteId: string | null = null;
|
||||||
|
@Input() vaultName = '';
|
||||||
|
@Input() tags: TagInfo[] = [];
|
||||||
@Output() noteSelected = new EventEmitter<string>();
|
@Output() noteSelected = new EventEmitter<string>();
|
||||||
@Output() folderSelected = new EventEmitter<string>();
|
@Output() folderSelected = new EventEmitter<string>();
|
||||||
|
@Output() tagSelected = new EventEmitter<string>();
|
||||||
|
@Output() quickLinkSelected = new EventEmitter<string>();
|
||||||
|
@Output() markdownPlaygroundSelected = new EventEmitter<void>();
|
||||||
|
|
||||||
|
open = { quick: true, folders: true, tags: false, trash: false, tests: true };
|
||||||
|
|
||||||
onSelect(id: string) {
|
onSelect(id: string) {
|
||||||
this.noteSelected.emit(id);
|
this.noteSelected.emit(id);
|
||||||
@ -40,4 +139,19 @@ export class AppSidebarDrawerComponent {
|
|||||||
this.folderSelected.emit(path);
|
this.folderSelected.emit(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onQuickLink(id: string) {
|
||||||
|
this.quickLinkSelected.emit(id);
|
||||||
|
this.mobileNav.sidebarOpen.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagSelected(tagName: string) {
|
||||||
|
this.tagSelected.emit(tagName);
|
||||||
|
this.mobileNav.sidebarOpen.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMarkdownPlaygroundClick(): void {
|
||||||
|
this.markdownPlaygroundSelected.emit();
|
||||||
|
this.mobileNav.sidebarOpen.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { environment } from '../../../environments/environment';
|
|||||||
<div class="h-full flex flex-col overflow-hidden select-none">
|
<div class="h-full flex flex-col overflow-hidden select-none">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="h-12 flex items-center justify-between px-3 border-b border-gray-200 dark:border-gray-800">
|
<div class="h-12 flex items-center justify-between px-3 border-b border-gray-200 dark:border-gray-800">
|
||||||
<div class="text-sm font-semibold truncate">{{ vaultName }}</div>
|
<div class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</div>
|
||||||
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" title="Hide Sidebar">⟨⟨</button>
|
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" title="Hide Sidebar">⟨⟨</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -23,25 +23,6 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
|||||||
imports: [CommonModule, FileExplorerComponent, NoteViewerComponent, AppBottomNavigationComponent, AppSidebarDrawerComponent, AppTocOverlayComponent, SwipeNavDirective, NotesListComponent, NimbusSidebarComponent, QuickLinksComponent, ScrollableOverlayDirective, MarkdownPlaygroundComponent],
|
imports: [CommonModule, FileExplorerComponent, NoteViewerComponent, AppBottomNavigationComponent, AppSidebarDrawerComponent, AppTocOverlayComponent, SwipeNavDirective, NotesListComponent, NimbusSidebarComponent, QuickLinksComponent, ScrollableOverlayDirective, MarkdownPlaygroundComponent],
|
||||||
template: `
|
template: `
|
||||||
<div class="relative h-screen flex flex-col bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
<div class="relative h-screen flex flex-col bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||||
<!-- Header (desktop/tablet), compact on mobile) -->
|
|
||||||
<header *ngIf="!noteFullScreen" class="flex items-center justify-between px-4 border-b border-gray-200 dark:border-gray-800 bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm" [class.h-14]="responsive.isDesktop() || responsive.isTablet()" [class.h-12]="responsive.isMobile()">
|
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
|
||||||
<button *ngIf="responsive.isMobile()" (click)="mobileNav.toggleSidebar()" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">☰</button>
|
|
||||||
<span class="inline-flex items-center rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300 truncate">{{ vaultName }}</span>
|
|
||||||
<h1 class="hidden sm:block text-base font-semibold truncate">ObsiViewer · Nimbus</h1>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button class="hidden lg:inline-flex p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (click)="toggleSidebarRequest.emit()" [attr.aria-expanded]="isSidebarOpen" title="Basculer la barre latérale gauche">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 15-3-3 3-3"/></svg>
|
|
||||||
</button>
|
|
||||||
<button class="hidden xl:inline-flex p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" (click)="toggleOutlineRequest.emit()" [attr.aria-expanded]="isOutlineOpen" title="Basculer la barre latérale droite">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg>
|
|
||||||
</button>
|
|
||||||
<button (click)="ui.toggleUIMode()" class="px-2 py-1 rounded text-xs font-semibold border border-gray-300 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800">
|
|
||||||
{{ ui.isNimbusMode() ? '🔧 Legacy' : '✨ Nimbus' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Fullscreen overlay for note -->
|
<!-- Fullscreen overlay for note -->
|
||||||
<div *ngIf="noteFullScreen && selectedNote && activeView !== 'markdown-playground'" class="absolute inset-0 z-50 flex flex-col bg-white dark:bg-gray-900">
|
<div *ngIf="noteFullScreen && selectedNote && activeView !== 'markdown-playground'" class="absolute inset-0 z-50 flex flex-col bg-white dark:bg-gray-900">
|
||||||
@ -182,7 +163,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
|||||||
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
|
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
|
||||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
|
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="mobileNav.activeTab() !== 'page'" class="h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
|
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
|
||||||
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
|
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -190,29 +171,48 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
|||||||
|
|
||||||
<!-- Mobile: bottom nav + drawer + swipe -->
|
<!-- Mobile: bottom nav + drawer + swipe -->
|
||||||
<div *ngIf="responsive.isMobile() && !noteFullScreen" class="flex-1 relative overflow-hidden" appSwipeNav (swipeLeft)="nextTab()" (swipeRight)="prevTab()">
|
<div *ngIf="responsive.isMobile() && !noteFullScreen" class="flex-1 relative overflow-hidden" appSwipeNav (swipeLeft)="nextTab()" (swipeRight)="prevTab()">
|
||||||
<app-sidebar-drawer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" (noteSelected)="onNoteSelectedMobile($event)" (folderSelected)="onFolderSelectedFromDrawer($event)"></app-sidebar-drawer>
|
<app-sidebar-drawer
|
||||||
|
[nodes]="effectiveFileTree"
|
||||||
|
[selectedNoteId]="selectedNoteId"
|
||||||
|
[vaultName]="vaultName"
|
||||||
|
[tags]="tags"
|
||||||
|
(noteSelected)="onNoteSelectedMobile($event)"
|
||||||
|
(folderSelected)="onFolderSelectedFromDrawer($event)"
|
||||||
|
(tagSelected)="onTagSelected($event)"
|
||||||
|
(quickLinkSelected)="onQuickLink($event)"
|
||||||
|
(markdownPlaygroundSelected)="onMarkdownPlaygroundSelected()"
|
||||||
|
></app-sidebar-drawer>
|
||||||
|
|
||||||
@if (mobileNav.activeTab() === 'list') {
|
@if (mobileNav.activeTab() === 'list') {
|
||||||
<div class="h-full flex flex-col overflow-hidden">
|
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
|
||||||
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
|
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (mobileNav.activeTab() === 'page') {
|
@if (mobileNav.activeTab() === 'page') {
|
||||||
<div class="h-full overflow-y-auto px-3 py-3" appScrollableOverlay>
|
<div class="note-content-area h-full px-3 py-3 animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-3 sticky top-0 bg-white dark:bg-gray-900 py-2 -mt-2 z-10">
|
||||||
<h2 class="text-base font-semibold truncate">{{ selectedNote?.title || 'Aucune page' }}</h2>
|
<h2 class="text-base font-semibold truncate">{{ selectedNote?.title || 'Aucune page' }}</h2>
|
||||||
<button *ngIf="tableOfContents.length > 0" (click)="mobileNav.toggleToc()" class="p-2 rounded hover:bg-gray-100 dark:hover-bg-gray-800">📋</button>
|
<button
|
||||||
|
*ngIf="tableOfContents.length > 0"
|
||||||
|
(pointerdown)="$event.stopPropagation(); mobileNav.toggleToc()"
|
||||||
|
(click)="$event.preventDefault()"
|
||||||
|
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-all active:scale-95 transform flex-shrink-0">
|
||||||
|
📋
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@if (selectedNote) {
|
@if (selectedNote) {
|
||||||
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" (fullScreenRequested)="toggleNoteFullScreen()"></app-note-viewer>
|
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" (fullScreenRequested)="toggleNoteFullScreen()"></app-note-viewer>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="mt-10 text-center text-sm text-gray-500 dark:text-gray-400">Aucune page sélectionnée pour le moment.</div>
|
<div class="mt-10 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<div class="text-4xl mb-3">📄</div>
|
||||||
|
<p>Aucune page sélectionnée pour le moment.</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<app-toc-overlay *ngIf="mobileNav.tocOpen()" [headings]="tableOfContents" (go)="navigateHeading.emit($event); mobileNav.toggleToc()" (close)="mobileNav.toggleToc()"></app-toc-overlay>
|
<app-toc-overlay *ngIf="mobileNav.tocOpen()" [headings]="tableOfContents" (go)="onTocNavigate($event)" (close)="mobileNav.toggleToc()"></app-toc-overlay>
|
||||||
|
|
||||||
<app-bottom-navigation></app-bottom-navigation>
|
<app-bottom-navigation></app-bottom-navigation>
|
||||||
</div>
|
</div>
|
||||||
@ -360,4 +360,19 @@ export class AppShellNimbusLayoutComponent {
|
|||||||
onMarkdownPlaygroundSelected(): void {
|
onMarkdownPlaygroundSelected(): void {
|
||||||
this.markdownPlaygroundSelected.emit();
|
this.markdownPlaygroundSelected.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTocNavigate(headingId: string): void {
|
||||||
|
// Ensure the page view is visible so the scroll container exists
|
||||||
|
this.mobileNav.setActiveTab('page');
|
||||||
|
|
||||||
|
// Close the TOC overlay immediately
|
||||||
|
if (this.mobileNav.tocOpen()) {
|
||||||
|
this.mobileNav.toggleToc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for DOM to update before scrolling
|
||||||
|
setTimeout(() => {
|
||||||
|
this.navigateHeading.emit(headingId);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export class MobileNavService {
|
|||||||
try {
|
try {
|
||||||
const t = localStorage.getItem('obsiviewer-mobile-tab');
|
const t = localStorage.getItem('obsiviewer-mobile-tab');
|
||||||
if (t === 'sidebar' || t === 'list' || t === 'page' || t === 'toc') {
|
if (t === 'sidebar' || t === 'list' || t === 'page' || t === 'toc') {
|
||||||
this.activeTab.set(t);
|
this.setActiveTab(t);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
@ -43,6 +43,15 @@ export class MobileNavService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleToc() {
|
toggleToc() {
|
||||||
this.tocOpen.update(v => !v);
|
const opening = !this.tocOpen();
|
||||||
|
|
||||||
|
if (opening) {
|
||||||
|
this.tocOpen.set(true);
|
||||||
|
this.sidebarOpen.set(false);
|
||||||
|
// Keep current activeTab (typically 'page') so content remains visible behind overlay
|
||||||
|
} else {
|
||||||
|
this.tocOpen.set(false);
|
||||||
|
// Do not force switch back to 'page'; preserve user's current tab
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -494,3 +494,21 @@ excalidraw-editor .excalidraw .layer-ui__wrapper {
|
|||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* Mobile animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fadeIn {
|
||||||
|
animation: fadeIn 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user