- Added clickable app title links in header and sidebar that reset view to all markdown files - Fixed quicklink filtering to use correct frontmatter boolean keys (favoris, publish, draft, etc.) - Updated sidebar behavior to auto-open Quick Links section when viewing markdown files - Added filter clearing when switching between views to prevent conflicting filters - Modified URL state handling to preserve kind parameter when navigating - Enforced markdown-only
683 lines
38 KiB
HTML
683 lines
38 KiB
HTML
<!-- ObsiViewer - Application optimisée pour mobile et desktop -->
|
|
<app-toast-container></app-toast-container>
|
|
@if (uiMode.isNimbusMode()) {
|
|
<app-shell-nimbus-layout
|
|
[vaultName]="vaultName()"
|
|
[effectiveFileTree]="effectiveFileTree()"
|
|
[selectedNoteId]="selectedNoteId()"
|
|
[selectedNote]="selectedNote()"
|
|
[renderedNoteContent]="renderedNoteContent()"
|
|
[tableOfContents]="tableOfContents()"
|
|
[isSidebarOpen]="isSidebarOpen()"
|
|
[isOutlineOpen]="isOutlineOpen()"
|
|
[leftSidebarWidth]="leftSidebarWidth()"
|
|
[rightSidebarWidth]="rightSidebarWidth()"
|
|
[centerPanelWidth]="centerPanelWidth()"
|
|
[searchTerm]="sidebarSearchTerm()"
|
|
[tags]="allTags()"
|
|
[activeView]="activeView()"
|
|
(noteSelected)="selectNote($event)"
|
|
(tagClicked)="handleTagClick($event)"
|
|
(wikiLinkActivated)="handleWikiLink($event)"
|
|
(leftResizeStart)="startLeftResize($event)"
|
|
(rightResizeStart)="startRightResize($event)"
|
|
(centerResizeStart)="startCenterResize($event)"
|
|
(toggleSidebarRequest)="toggleSidebar()"
|
|
(toggleOutlineRequest)="toggleOutline()"
|
|
(navigateHeading)="scrollToHeading($event)"
|
|
(searchTermChange)="onSidebarSearchTermChange($event)"
|
|
(searchOptionsChange)="onHeaderSearchOptionsChange($event)"
|
|
(markdownPlaygroundSelected)="setView('markdown-playground')"
|
|
(parametersOpened)="setView('parameters')"
|
|
(testsPanelRequested)="setView('tests-panel')"
|
|
(testsExcalidrawRequested)="setView('tests-excalidraw')"
|
|
(helpPageRequested)="openHelpPage()"
|
|
(noteCreated)="onNoteCreated($event)"
|
|
(noteCreatedAndSelected)="onNoteCreatedAndSelected($event)"
|
|
></app-shell-nimbus-layout>
|
|
} @else {
|
|
<main class="relative flex min-h-screen flex-col bg-bg-main text-text-main lg:flex-row lg:h-screen lg:overflow-hidden">
|
|
@if (isRawViewOpen()) {
|
|
<app-raw-view-overlay
|
|
[content]="rawNoteContent()"
|
|
[filename]="rawNoteFilename()"
|
|
[wrap]="isRawViewWrapped()"
|
|
(close)="closeRawView()"
|
|
(toggleWrap)="toggleRawWrap()"
|
|
></app-raw-view-overlay>
|
|
}
|
|
|
|
@if (showAddBookmarkModal()) {
|
|
<app-add-bookmark-modal
|
|
[notePath]="selectedNote()?.filePath || ''"
|
|
[noteTitle]="selectedNote()?.title || ''"
|
|
(close)="closeBookmarkModal()"
|
|
(save)="onBookmarkSave($event)"
|
|
(delete)="onBookmarkDelete($event)"
|
|
></app-add-bookmark-modal>
|
|
}
|
|
<!-- Navigation latérale desktop -->
|
|
<nav class="hidden w-14 flex-col items-center gap-4 border-r border-border bg-bg-main py-4 lg:flex">
|
|
<button
|
|
(click)="setView('files')"
|
|
class="btn btn-sm btn-icon"
|
|
[ngClass]="activeView() === 'files' ? 'btn-primary' : 'btn-ghost'"
|
|
aria-label="Afficher les fichiers"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('search')"
|
|
class="btn btn-sm btn-icon"
|
|
[ngClass]="activeView() === 'search' ? 'btn-primary' : 'btn-ghost'"
|
|
aria-label="Ouvrir la recherche"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('tags')"
|
|
class="btn btn-sm btn-icon"
|
|
[ngClass]="activeView() === 'tags' ? 'btn-primary' : 'btn-ghost'"
|
|
aria-label="Afficher les tags"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('graph')"
|
|
class="btn btn-sm btn-icon"
|
|
[ngClass]="activeView() === 'graph' ? 'btn-primary' : 'btn-ghost'"
|
|
aria-label="Afficher la vue graphe"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-6 w-6 text-text-muted">
|
|
<path d="M5 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4z" />
|
|
<path d="M12 3a2 2 0 1 1 0 4 2 2 0 0 1 0-4z" />
|
|
<path d="M19 16a2 2 0 1 1 0 4 2 2 0 0 1 0-4z" />
|
|
<path d="M5.88 9.32l4.24 2.46" />
|
|
<path d="M11.3 5.7l5.4 8.6" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('calendar')"
|
|
class="btn btn-sm btn-icon"
|
|
[ngClass]="activeView() === 'calendar' ? 'btn-primary' : 'btn-ghost'"
|
|
aria-label="Afficher le calendrier"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('bookmarks')"
|
|
class="btn btn-sm btn-icon"
|
|
[ngClass]="activeView() === 'bookmarks' ? 'btn-primary' : 'btn-ghost'"
|
|
aria-label="Afficher les favoris"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /></svg>
|
|
</button>
|
|
</nav>
|
|
|
|
@if (isDesktop() || isSidebarOpen()) {
|
|
<aside
|
|
class="fixed inset-y-0 left-0 z-40 flex h-screen w-[min(320px,85vw)] -translate-x-full transform flex-col border-r border-border bg-bg-muted shadow-xl transition-transform duration-200 ease-in-out lg:static lg:h-auto lg:w-auto lg:translate-x-0 lg:shadow-none"
|
|
[class.translate-x-0]="isSidebarOpen() || isDesktop()"
|
|
[class.pointer-events-none]="!isSidebarOpen() && !isDesktop()"
|
|
[style.width.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
|
[style.minWidth.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
|
[style.maxWidth.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
|
role="navigation"
|
|
aria-label="Arborescence de la voûte"
|
|
>
|
|
<div class="flex h-full flex-col overflow-hidden bg-card"
|
|
[style.width.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
|
>
|
|
<div class="space-y-4 border-b border-border/80 bg-bg-muted/70 px-4 py-4">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-2xl border border-border/70 bg-card/80 text-text-muted shadow-subtle">
|
|
<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>
|
|
<span class="text-sm font-semibold tracking-wide text-text-main">{{ vaultName() }}</span>
|
|
<div class="mt-1 flex items-center gap-2 text-xs uppercase text-text-muted">
|
|
<span class="inline-flex items-center gap-1 rounded-full bg-card/80 px-2 py-0.5 text-[0.65rem] font-semibold tracking-widest text-text-main/80">{{ activeView() | titlecase }}</span>
|
|
<span class="hidden text-[0.65rem] tracking-widest text-text-muted/80 sm:inline">Vue active</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!isDesktop()) {
|
|
<button
|
|
class="btn btn-icon btn-ghost"
|
|
(click)="closeSidebar()"
|
|
aria-label="Fermer le panneau latéral"
|
|
>
|
|
<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="M6 18L18 6M6 6l12 12" /></svg>
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
<div class="rounded-2xl border border-border/70 bg-card/85 px-3 py-3 shadow-subtle">
|
|
<div class="grid grid-cols-3 gap-2 sm:grid-cols-5">
|
|
<button
|
|
(click)="setView('files')"
|
|
class="group flex items-center justify-center rounded-xl border border-transparent px-2.5 py-2 text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'files' }"
|
|
aria-label="Afficher les fichiers" title="Fichiers"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('search')"
|
|
class="group flex items-center justify-center rounded-xl border border-transparent px-2.5 py-2 text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'search' }"
|
|
aria-label="Ouvrir la recherche" title="Recherche"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('tags')"
|
|
class="group flex items-center justify-center rounded-xl border border-transparent px-2.5 py-2 text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'tags' }"
|
|
aria-label="Afficher les tags" title="Tags"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('graph')"
|
|
class="group flex items-center justify-center rounded-xl border border-transparent px-2.5 py-2 text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'graph' }"
|
|
aria-label="Afficher la vue graphe" title="Graph View"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('calendar')"
|
|
class="group flex items-center justify-center rounded-xl border border-transparent px-2.5 py-2 text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'calendar' }"
|
|
[attr.aria-pressed]="activeView() === 'calendar'"
|
|
aria-label="Afficher l'agenda" title="Agenda"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
</button>
|
|
<button
|
|
(click)="setView('bookmarks')"
|
|
class="group flex items-center justify-center rounded-xl border border-transparent px-2.5 py-2 text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'bookmarks' }"
|
|
[attr.aria-pressed]="activeView() === 'bookmarks'"
|
|
aria-label="Afficher les favoris" title="Favoris"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 overflow-y-auto">
|
|
@switch (activeView()) {
|
|
@case ('files') {
|
|
<div class="px-2 pt-2 pb-2">
|
|
<input
|
|
type="search"
|
|
[value]="fileExplorerFilter()"
|
|
(input)="fileExplorerFilter.set(($any($event.target).value || '').toString())"
|
|
placeholder="Filtrer fichiers et dossiers..."
|
|
class="w-full rounded-full border border-border bg-card px-3 py-1.5 text-sm text-text-main placeholder:text-text-muted"
|
|
aria-label="Filtrer arborescence"
|
|
/>
|
|
</div>
|
|
<app-file-explorer
|
|
[nodes]="filteredExplorerTree()"
|
|
[selectedNoteId]="selectedNoteId()"
|
|
(fileSelected)="onFileNodeSelected($event)"
|
|
></app-file-explorer>
|
|
}
|
|
@case ('tags') {
|
|
<app-tags-view [tags]="allTags()" (tagSelected)="handleTagClick($event)"></app-tags-view>
|
|
}
|
|
|
|
@case ('search') {
|
|
<div class="h-full p-2">
|
|
<app-search-panel
|
|
[placeholder]="'Rechercher dans la voûte...'"
|
|
[context]="'vault-sidebar'"
|
|
[initialQuery]="sidebarSearchTerm()"
|
|
(noteOpen)="selectNote($event.noteId)"
|
|
(searchTermChange)="onSidebarSearchTermChange($event)"
|
|
(optionsChange)="onSidebarOptionsChange($event)"
|
|
></app-search-panel>
|
|
</div>
|
|
}
|
|
@case ('calendar') {
|
|
<div class="flex h-full flex-col">
|
|
<div class="flex items-center justify-between border-b border-border bg-card px-4 py-3">
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-text-main">Vue agenda</h3>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 overflow-auto px-3 py-4">
|
|
<app-markdown-calendar
|
|
(fileSelected)="selectNote($event)"
|
|
(searchResultsChange)="onCalendarResultsChange($event)"
|
|
(searchStateChange)="onCalendarSearchStateChange($event)"
|
|
(searchErrorChange)="onCalendarSearchErrorChange($event)"
|
|
(selectionSummaryChange)="onCalendarSelectionSummaryChange($event)"
|
|
(requestSearchPanel)="onCalendarRequestSearchPanel()"
|
|
></app-markdown-calendar>
|
|
</div>
|
|
</div>
|
|
}
|
|
@case ('bookmarks') {
|
|
<div class="flex h-full flex-col">
|
|
<app-bookmarks-panel (bookmarkClick)="onBookmarkNavigate($event)"></app-bookmarks-panel>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
}
|
|
|
|
@if (!isDesktop() && isSidebarOpen()) {
|
|
<div
|
|
class="fixed inset-0 z-30 bg-black/40 transition-opacity duration-200"
|
|
(click)="closeSidebar()"
|
|
></div>
|
|
}
|
|
|
|
<div class="hidden lg:flex">
|
|
<div
|
|
class="resize-handle h-full"
|
|
role="separator"
|
|
aria-orientation="vertical"
|
|
aria-label="Redimensionner la barre latérale gauche"
|
|
(pointerdown)="startLeftResize($event)"
|
|
></div>
|
|
</div>
|
|
|
|
<section class="flex min-w-0 flex-col bg-bg-main pb-16 lg:flex-1 lg:min-h-0 lg:overflow-hidden lg:pb-0">
|
|
<header class="flex flex-col gap-4 border-b border-border bg-bg-main/95 px-4 py-3 backdrop-blur-xs lg:px-6">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<button
|
|
class="btn btn-icon btn-ghost 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 btn btn-icon btn-ghost 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-border bg-bg-muted/70 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-text-muted">{{ vaultName() }}</span>
|
|
<h1 class="text-lg font-semibold leading-tight text-text-main">
|
|
<a href="/" (click)="onAppTitleClick($event)" class="hover:underline">ObsiWatcher</a>
|
|
</h1>
|
|
</div>
|
|
@if (selectedNoteBreadcrumb().length > 0) {
|
|
<p class="truncate text-xs text-text-muted">{{ selectedNoteBreadcrumb().join(' / ') }}</p>
|
|
} @else {
|
|
<p class="truncate text-xs text-text-muted">Aucune note sélectionnée</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="hidden items-center gap-2 lg:flex">
|
|
<button
|
|
type="button"
|
|
class="btn btn-icon btn-ghost"
|
|
(click)="createNewDrawing()"
|
|
aria-label="Nouveau dessin Excalidraw"
|
|
title="Nouveau dessin"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
|
<path d="M12 5v14" />
|
|
<path d="M5 12h14" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
|
(click)="toggleRawView()"
|
|
[disabled]="!selectedNote()"
|
|
aria-label="Afficher le markdown brut"
|
|
aria-keyshortcuts="Alt+R"
|
|
title="Vue brute (Alt+R)"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
|
<path d="M8.5 8 5 12l3.5 4" />
|
|
<path d="M15.5 8 19 12l-3.5 4" />
|
|
<path d="M12.5 6 11 18" />
|
|
</svg>
|
|
<span class="sr-only">Alt+R</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
|
(click)="downloadCurrentNote()"
|
|
[disabled]="!selectedNote()"
|
|
aria-label="Télécharger le fichier markdown"
|
|
aria-keyshortcuts="Alt+D"
|
|
title="Télécharger (Alt+D)"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
|
<path d="M12 3v12" />
|
|
<path d="M7 11l5 5 5-5" />
|
|
<path d="M5 19h14" />
|
|
</svg>
|
|
<span class="sr-only">Alt+D</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
|
(click)="toggleBookmarkModal()"
|
|
[disabled]="!selectedNote()"
|
|
aria-label="Ajouter aux favoris"
|
|
title="Ajouter aux favoris"
|
|
>
|
|
@if (isCurrentNoteBookmarked()) {
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5 text-blue-600 dark:text-blue-400">
|
|
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
|
</svg>
|
|
} @else {
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
|
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
|
</svg>
|
|
}
|
|
</button>
|
|
<button
|
|
(click)="toggleTheme()"
|
|
class="btn btn-icon btn-ghost"
|
|
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>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost rounded px-2 py-1 text-xs font-semibold"
|
|
(click)="toggleUIMode()"
|
|
[attr.aria-label]="'Basculer vers ' + (uiMode.isNimbusMode() ? 'ancienne' : 'nouvelle') + ' interface'"
|
|
title="Basculer d'interface"
|
|
>
|
|
@if (uiMode.isNimbusMode()) { 🔧 Legacy } @else { ✨ Nimbus }
|
|
</button>
|
|
<button
|
|
(click)="toggleOutline()"
|
|
class="btn btn-icon btn-ghost"
|
|
[attr.aria-expanded]="isOutlineOpen()"
|
|
aria-label="Basculer la barre latérale droite"
|
|
>
|
|
@if (isOutlineOpen()) {
|
|
<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="M15 3v18"/><path d="m8 9 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="M15 3v18"/><path d="m10 15-3-3 3-3"/></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
|
|
type="button"
|
|
class="btn btn-icon btn-ghost"
|
|
(click)="createNewDrawing()"
|
|
aria-label="Nouveau dessin Excalidraw"
|
|
title="Nouveau dessin"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
|
<path d="M12 5v14" />
|
|
<path d="M5 12h14" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
|
(click)="toggleRawView()"
|
|
[disabled]="!selectedNote()"
|
|
aria-label="Afficher le markdown brut"
|
|
aria-keyshortcuts="Alt+R"
|
|
title="Vue brute (Alt+R)"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
|
<path d="M8.5 8 5 12l3.5 4" />
|
|
<path d="M15.5 8 19 12l-3.5 4" />
|
|
<path d="M12.5 6 11 18" />
|
|
</svg>
|
|
<span class="sr-only">Raccourci Alt+R</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
|
(click)="downloadCurrentNote()"
|
|
[disabled]="!selectedNote()"
|
|
aria-label="Télécharger le fichier markdown"
|
|
aria-keyshortcuts="Alt+D"
|
|
title="Télécharger (Alt+D)"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
|
<path d="M12 3v12" />
|
|
<path d="M7 11l5 5 5-5" />
|
|
<path d="M5 19h14" />
|
|
</svg>
|
|
<span class="sr-only">Raccourci Alt+D</span>
|
|
</button>
|
|
<button
|
|
(click)="toggleTheme()"
|
|
class="btn btn-icon btn-ghost"
|
|
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>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost rounded px-2 py-1 text-xs font-semibold"
|
|
(click)="toggleUIMode()"
|
|
[attr.aria-label]="'Basculer vers ' + (uiMode.isNimbusMode() ? 'ancienne' : 'nouvelle') + ' interface'"
|
|
title="Basculer d'interface"
|
|
>
|
|
@if (uiMode.isNimbusMode()) { 🔧 Legacy } @else { ✨ Nimbus }
|
|
</button>
|
|
<button
|
|
(click)="toggleOutline()"
|
|
class="btn btn-icon btn-ghost"
|
|
[attr.aria-expanded]="isOutlineOpen()"
|
|
aria-label="Basculer la barre latérale droite"
|
|
>
|
|
@if (isOutlineOpen()) {
|
|
<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="M15 3v18"/><path d="m8 9 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="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg>
|
|
}
|
|
</button>
|
|
</div>
|
|
<div class="flex flex-1 items-center gap-3 lg:hidden">
|
|
<div class="relative w-full flex-1 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-header'"
|
|
[showSearchIcon]="true"
|
|
[inputClass]="'w-full rounded-full border border-border bg-bg-muted/70 pl-11 pr-4 py-2.5 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring'"
|
|
(optionsChange)="onHeaderSearchOptionsChange($event)"
|
|
/>
|
|
</div>
|
|
<button
|
|
class="rounded-full border border-border px-3 py-2 text-xs font-semibold uppercase tracking-wide text-text-muted transition hover:bg-bg-muted"
|
|
(click)="toggleOutline()"
|
|
[attr.aria-pressed]="isOutlineOpen()"
|
|
>
|
|
Outline
|
|
</button>
|
|
</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">
|
|
@if (activeView() === 'graph') {
|
|
<div class="h-[calc(100vh-180px)] lg:h-[calc(100vh-140px)] rounded-xl border border-border bg-card p-2">
|
|
<app-graph-view-container-v2
|
|
[centerNoteId]="selectedNoteId()"
|
|
(nodeSelected)="selectNoteFromGraph($event)">
|
|
</app-graph-view-container-v2>
|
|
</div>
|
|
} @else if (activeView() === 'markdown-playground') {
|
|
<div class="h-[calc(100vh-180px)] lg:h-[calc(100vh-140px)]">
|
|
<app-markdown-playground></app-markdown-playground>
|
|
</div>
|
|
} @else if (activeView() === 'parameters') {
|
|
<app-parameters></app-parameters>
|
|
} @else if (activeView() === 'tests-panel') {
|
|
<app-tests-panel></app-tests-panel>
|
|
} @else if (activeView() === 'tests-excalidraw') {
|
|
<app-test-excalidraw-page></app-test-excalidraw-page>
|
|
} @else {
|
|
@if (activeView() === 'drawings') {
|
|
@if (currentDrawingPath()) {
|
|
<app-drawings-editor [path]="currentDrawingPath()!"></app-drawings-editor>
|
|
} @else {
|
|
<div class="flex h-full items-center justify-center">
|
|
<p class="text-text-muted">Aucun dessin sélectionné</p>
|
|
</div>
|
|
}
|
|
} @else {
|
|
@if (selectedNote(); as note) {
|
|
<app-note-viewer
|
|
[note]="note"
|
|
[noteHtmlContent]="renderedNoteContent()"
|
|
[allNotes]="vaultService.allNotes()"
|
|
(noteLinkClicked)="selectNote($event)"
|
|
(tagClicked)="handleTagClick($event)"
|
|
(wikiLinkActivated)="handleWikiLink($event)"
|
|
></app-note-viewer>
|
|
} @else {
|
|
<div class="flex h-full items-center justify-center">
|
|
<p class="text-text-muted">Sélectionnez une note pour commencer</p>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
<div class="hidden lg:flex">
|
|
<div
|
|
class="resize-handle h-full"
|
|
role="separator"
|
|
aria-orientation="vertical"
|
|
aria-label="Redimensionner la barre latérale droite"
|
|
(pointerdown)="startRightResize($event)"
|
|
></div>
|
|
</div>
|
|
|
|
@if (!isDesktop() && isOutlineOpen()) {
|
|
<div
|
|
class="fixed inset-0 z-30 bg-black/40 transition-opacity duration-200"
|
|
(click)="closeOutlinePanel()"
|
|
></div>
|
|
}
|
|
|
|
@if (isDesktop() || isOutlineOpen()) {
|
|
<aside
|
|
class="fixed inset-x-0 bottom-0 z-40 flex max-h-[80vh] flex-col overflow-hidden border-t border-border bg-card shadow-2xl transition-all duration-200 ease-out lg:static lg:max-h-none lg:border-l lg:shadow-none"
|
|
[ngClass]="{
|
|
'translate-y-0 opacity-100 pointer-events-auto': isOutlineOpen() || isDesktop(),
|
|
'translate-y-full opacity-0 pointer-events-none': !isOutlineOpen() && !isDesktop()
|
|
}"
|
|
[style.width.px]="isDesktop() ? (isOutlineOpen() ? rightSidebarWidth() : 0) : null"
|
|
[style.minWidth.px]="isDesktop() ? (isOutlineOpen() ? rightSidebarWidth() : 0) : null"
|
|
[style.maxWidth.px]="isDesktop() ? (isOutlineOpen() ? rightSidebarWidth() : 0) : null"
|
|
role="complementary"
|
|
aria-label="Table des matières et calendrier"
|
|
>
|
|
<div class="flex flex-col lg:h-full">
|
|
@if (!isDesktop()) {
|
|
<div class="px-4 pt-3">
|
|
<div class="mx-auto mb-3 h-1.5 w-12 rounded-full bg-border"></div>
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-base font-semibold text-text-main">Navigation</h2>
|
|
<button
|
|
class="btn btn-icon btn-ghost"
|
|
(click)="closeOutlinePanel()"
|
|
aria-label="Fermer la barre latérale droite"
|
|
>
|
|
<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="M6 18L18 6M6 6l12 12" /></svg>
|
|
</button>
|
|
</div>
|
|
<p class="mt-1 text-xs uppercase tracking-wide text-text-muted">Table des matières & calendrier</p>
|
|
</div>
|
|
}
|
|
<div class="border-b border-border px-3 py-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
(click)="setOutlineTab('outline')"
|
|
(keydown.enter)="setOutlineTab('outline')"
|
|
(keydown.space)="setOutlineTab('outline'); $event.preventDefault()"
|
|
[class.opacity-100]="outlineTab() === 'outline'"
|
|
[class.opacity-60]="outlineTab() !== 'outline'"
|
|
class="rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
aria-label="Afficher Outline"
|
|
[attr.aria-pressed]="outlineTab() === 'outline'"
|
|
title="Outline">
|
|
<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="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" /></svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="setOutlineTab('settings')"
|
|
(keydown.enter)="setOutlineTab('settings')"
|
|
(keydown.space)="setOutlineTab('settings'); $event.preventDefault()"
|
|
[class.opacity-100]="outlineTab() === 'settings'"
|
|
[class.opacity-60]="outlineTab() !== 'settings'"
|
|
class="rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
aria-label="Ouvrir les réglages du graphe"
|
|
[attr.aria-pressed]="outlineTab() === 'settings'"
|
|
title="Graph-View Settings">
|
|
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
</button>
|
|
</div>
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-text-muted">{{ outlineTab() | titlecase }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 overflow-y-auto px-4 py-4">
|
|
@if (outlineTab() === 'outline') {
|
|
@if (tableOfContents().length > 0) {
|
|
<ul class="space-y-2">
|
|
@for (entry of tableOfContents(); track entry.id) {
|
|
<li [style.padding-left.rem]="(entry.level - 1) * 0.75" class="flex items-start gap-2 text-sm text-text-muted">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="mt-1 h-3 w-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" /></svg>
|
|
<a (click)="scrollToHeading(entry.id)" class="cursor-pointer leading-tight text-text-muted transition hover:text-text-main">
|
|
{{ entry.text }}
|
|
</a>
|
|
</li>
|
|
}
|
|
</ul>
|
|
} @else {
|
|
<p class="text-sm italic text-text-muted">Aucun titre dans cette note.</p>
|
|
}
|
|
} @else {
|
|
<app-graph-inline-settings></app-graph-inline-settings>
|
|
}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
}
|
|
</main>
|
|
}
|
|
|