feat: add fullscreen mode and toolbar to note viewer
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||
import { Component, EventEmitter, HostListener, Input, Output, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiModeService } from '../../shared/services/ui-mode.service';
|
||||
import { ResponsiveService } from '../../shared/services/responsive.service';
|
||||
@@ -22,9 +22,9 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
standalone: true,
|
||||
imports: [CommonModule, FileExplorerComponent, NoteViewerComponent, AppBottomNavigationComponent, AppSidebarDrawerComponent, AppTocOverlayComponent, SwipeNavDirective, NotesListComponent, NimbusSidebarComponent, QuickLinksComponent, ScrollableOverlayDirective, MarkdownPlaygroundComponent],
|
||||
template: `
|
||||
<div class="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 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()">
|
||||
<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>
|
||||
@@ -43,8 +43,28 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 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 class="note-content-area flex-1 overflow-y-auto px-4 py-4 lg:px-12" 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)="toggleOutlineRequest.emit()"
|
||||
(directoryClicked)="onFolderSelected($event)"
|
||||
[tocOpen]="isOutlineOpen"
|
||||
></app-note-viewer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop 3-column layout -->
|
||||
<div *ngIf="responsive.isDesktop()" class="flex-1 flex overflow-hidden relative">
|
||||
<div *ngIf="responsive.isDesktop() && !noteFullScreen" class="flex-1 flex overflow-hidden relative">
|
||||
<!-- Left: full sidebar or collapsed rail -->
|
||||
<ng-container *ngIf="isSidebarOpen; else collapsedRail">
|
||||
<aside class="flex flex-col border-r border-gray-200 dark:border-gray-800 min-h-0" [style.width.px]="leftSidebarWidth">
|
||||
@@ -127,9 +147,15 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
(noteLinkClicked)="noteSelected.emit($event)"
|
||||
(tagClicked)="tagClicked.emit($event)"
|
||||
(wikiLinkActivated)="wikiLinkActivated.emit($event)"
|
||||
[fullScreenActive]="noteFullScreen"
|
||||
(fullScreenRequested)="toggleNoteFullScreen()"
|
||||
(legacyRequested)="ui.toggleUIMode()"
|
||||
(showToc)="toggleOutlineRequest.emit()"
|
||||
(directoryClicked)="onFolderSelected($event)"
|
||||
[tocOpen]="isOutlineOpen"
|
||||
></app-note-viewer>
|
||||
</div>
|
||||
<aside class="hidden xl:block border-l border-gray-200 dark:border-gray-800 overflow-y-auto" appScrollableOverlay [style.width.px]="isOutlineOpen ? rightSidebarWidth : 0" [class.opacity-0]="!isOutlineOpen" [class.pointer-events-none]="!isOutlineOpen">
|
||||
<aside class="hidden xl:block border-l border-gray-200 dark:border-gray-800 overflow-y-auto transition-all duration-300 ease-in-out" appScrollableOverlay [style.width.px]="isOutlineOpen ? rightSidebarWidth : 0" [class.opacity-0]="!isOutlineOpen" [class.pointer-events-none]="!isOutlineOpen">
|
||||
<div class="p-3">
|
||||
<h2 class="text-sm font-semibold mb-2">Sommaire</h2>
|
||||
<ul class="space-y-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
@@ -143,7 +169,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
</div>
|
||||
|
||||
<!-- Tablet: simple tabbed areas -->
|
||||
<div *ngIf="responsive.isTablet()" class="flex-1 flex flex-col overflow-hidden">
|
||||
<div *ngIf="responsive.isTablet() && !noteFullScreen" class="flex-1 flex flex-col overflow-hidden">
|
||||
<div class="h-12 border-b border-gray-200 dark:border-gray-800 flex items-center">
|
||||
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'sidebar'" (click)="mobileNav.setActiveTab('sidebar')">Sidebar</button>
|
||||
<button class="flex-1 h-full text-sm" [class.text-nimbus-500]="mobileNav.activeTab() === 'list'" (click)="mobileNav.setActiveTab('list')">Liste</button>
|
||||
@@ -157,13 +183,13 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
|
||||
</div>
|
||||
<div [hidden]="mobileNav.activeTab() !== 'page'" class="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)"></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>
|
||||
|
||||
<!-- Mobile: bottom nav + drawer + swipe -->
|
||||
<div *ngIf="responsive.isMobile()" 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>
|
||||
|
||||
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full flex flex-col overflow-hidden">
|
||||
@@ -175,7 +201,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
|
||||
<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>
|
||||
</div>
|
||||
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)"></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>
|
||||
</div>
|
||||
|
||||
<app-toc-overlay *ngIf="mobileNav.tocOpen()" [headings]="tableOfContents" (go)="navigateHeading.emit($event); mobileNav.toggleToc()" (close)="mobileNav.toggleToc()"></app-toc-overlay>
|
||||
@@ -191,6 +217,8 @@ export class AppShellNimbusLayoutComponent {
|
||||
responsive = inject(ResponsiveService);
|
||||
mobileNav = inject(MobileNavService);
|
||||
|
||||
noteFullScreen = false;
|
||||
|
||||
@Input() vaultName = '';
|
||||
@Input() effectiveFileTree: VaultNode[] = [];
|
||||
@Input() selectedNoteId: string | null = '';
|
||||
@@ -198,7 +226,7 @@ export class AppShellNimbusLayoutComponent {
|
||||
@Input() renderedNoteContent = '';
|
||||
@Input() tableOfContents: Array<{ level: number; text: string; id: string }> = [];
|
||||
@Input() isSidebarOpen = true;
|
||||
@Input() isOutlineOpen = true;
|
||||
@Input() isOutlineOpen = false;
|
||||
@Input() leftSidebarWidth = 288;
|
||||
@Input() rightSidebarWidth = 288;
|
||||
@Input() searchTerm = '';
|
||||
@@ -225,6 +253,18 @@ export class AppShellNimbusLayoutComponent {
|
||||
private flyoutCloseTimer: any = null;
|
||||
tagFilter: string | null = null;
|
||||
|
||||
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());
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ClipboardService {
|
||||
async write(text: string): Promise<boolean> {
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text ?? '');
|
||||
return true;
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text ?? '';
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
ta.style.pointerEvents = 'none';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
return ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export type TriBool = 'absent' | 'true' | 'false';
|
||||
|
||||
export interface HasFrontmatterLike {
|
||||
frontmatter?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function trilean(meta: Record<string, unknown> | undefined | null, key: string): TriBool {
|
||||
const m = meta ?? {};
|
||||
if (!(key in m)) return 'absent';
|
||||
return Boolean((m as any)[key]) ? 'true' : 'false';
|
||||
}
|
||||
Reference in New Issue
Block a user