feat: add syntax highlighting and code block styling with theme support

This commit is contained in:
2025-10-21 08:34:46 -04:00
parent 062d743481
commit d788c9d267
75 changed files with 4783 additions and 830 deletions
@@ -1,13 +1,13 @@
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" (click)="onBackdropClick($event)">
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full p-6" (click)="$event.stopPropagation()">
<div class="bg-card dark:bg-main rounded-lg shadow-xl max-w-2xl w-full p-6" (click)="$event.stopPropagation()">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">
<h2 class="text-xl font-semibold text-main dark:text-gray-100">
{{ isEditMode() ? 'Edit bookmark' : 'Add bookmark' }}
</h2>
<button
(click)="onCancel()"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
class="text-muted hover:text-muted dark:hover:text-gray-300 transition-colors"
aria-label="Close">
<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="M6 18L18 6M6 6l12 12" />
@@ -19,41 +19,41 @@
<div class="space-y-4">
<!-- Path -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label class="block text-sm font-medium text-main dark:text-main mb-2">
Path
</label>
<input
type="text"
[value]="path()"
(input)="onPathChange($any($event.target).value)"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="w-full px-3 py-2 border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Tests/Allo note.md"
/>
</div>
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label class="block text-sm font-medium text-main dark:text-main mb-2">
Title
</label>
<input
type="text"
[value]="title()"
(input)="onTitleChange($any($event.target).value)"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="w-full px-3 py-2 border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Display name (optional)"
/>
</div>
<!-- Bookmark group -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label class="block text-sm font-medium text-main dark:text-main mb-2">
Bookmark group
</label>
<select
[value]="selectedGroupCtime() || ''"
(change)="onGroupChange($any($event.target).value)"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
class="w-full px-3 py-2 border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Root (no group)</option>
@for (group of groups(); track group.ctime) {
<option [value]="group.ctime">{{ group.title }}</option>
@@ -79,7 +79,7 @@
<div class="flex gap-3">
<button
(click)="onCancel()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
class="px-4 py-2 text-sm font-medium text-main dark:text-main bg-surface1 dark:bg-surface2 hover:bg-surface2 dark:hover:bg-gray-600 rounded-md transition-colors">
Cancel
</button>
<button
@@ -1,11 +1,11 @@
<div class="bookmark-node">
<div
class="relative flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md cursor-pointer transition-colors"
class="relative flex items-center gap-2 px-3 py-2 hover:bg-surface1 dark:hover:bg-card rounded-md cursor-pointer transition-colors"
[style.padding-left]="indentStyle"
(click)="onClick($event)"
(contextmenu)="onContextMenu($event)"
[class.bg-gray-50]="isGroup"
[class.dark:bg-gray-800/50]="isGroup">
[class.bg-surface1]="isGroup"
[class.dark:bg-card/50]="isGroup">
<!-- Icon -->
<span class="text-base select-none" [class.cursor-pointer]="isGroup">
@@ -14,7 +14,7 @@
<!-- Text -->
<span
class="flex-1 text-sm text-gray-900 dark:text-gray-100 truncate"
class="flex-1 text-sm text-main dark:text-gray-100 truncate"
[title]="displayText">
{{ displayText }}
</span>
@@ -22,13 +22,13 @@
@if (isGroup) {
<div class="flex items-center gap-1">
<button
class="p-1 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors"
class="p-1 text-muted hover:text-blue-600 dark:text-muted dark:hover:text-blue-400 transition-colors"
title="Ajouter un favori dans ce groupe"
(click)="onAddBookmark($event)">
+
</button>
<button
class="p-1 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 transition-colors"
class="p-1 text-muted hover:text-red-600 dark:text-muted dark:hover:text-red-400 transition-colors"
title="Supprimer ce groupe"
(click)="onDelete($event)">
🗑
@@ -38,14 +38,14 @@
<!-- Badge for group count -->
@if (isGroup && hasChildren) {
<span class="text-xs text-gray-500 dark:text-gray-400 bg-gray-200 dark:bg-gray-700 px-2 py-0.5 rounded-full">
<span class="text-xs text-muted dark:text-muted bg-surface2 dark:bg-surface2 px-2 py-0.5 rounded-full">
{{ children.length }}
</span>
}
<!-- Context Menu Button -->
<button
class="opacity-0 hover:opacity-100 focus:opacity-100 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded transition-opacity"
class="opacity-0 hover:opacity-100 focus:opacity-100 p-1 text-muted hover:text-main dark:text-muted dark:hover:text-main rounded transition-opacity"
(click)="onContextMenu($event); $event.stopPropagation()"
title="More options">
⋮
@@ -54,20 +54,20 @@
<!-- Context Menu -->
@if (showMenu()) {
<div
class="absolute right-0 top-full mt-1 z-10 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg"
class="absolute right-0 top-full mt-1 z-10 w-48 bg-card dark:bg-card border border-border dark:border-border rounded-md shadow-lg"
(click)="$event.stopPropagation()">
@if (isGroup) {
<button
class="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
class="w-full px-4 py-2 text-left text-sm text-main dark:text-main hover:bg-surface1 dark:hover:bg-surface2 transition-colors"
(click)="onAddBookmark()">
Add Bookmark Here
</button>
<button
class="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
class="w-full px-4 py-2 text-left text-sm text-main dark:text-main hover:bg-surface1 dark:hover:bg-surface2 transition-colors"
(click)="onAddGroup()">
Add Subgroup
</button>
<hr class="border-gray-200 dark:border-gray-700" />
<hr class="border-border dark:border-border" />
<button
class="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
(click)="onDelete()">
@@ -75,11 +75,11 @@
</button>
} @else {
<button
class="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
class="w-full px-4 py-2 text-left text-sm text-main dark:text-main hover:bg-surface1 dark:hover:bg-surface2 transition-colors"
(click)="onEdit()">
Edit
</button>
<hr class="border-gray-200 dark:border-gray-700" />
<hr class="border-border dark:border-border" />
<button
class="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
(click)="onDelete()">
@@ -93,7 +93,7 @@
<!-- Drop list for this group (always present for drag & drop to work) -->
@if (isGroup) {
<div
class="ml-6 border-l border-gray-200 dark:border-gray-700 pl-2 min-h-[40px] transition-colors"
class="ml-6 border-l border-border dark:border-border pl-2 min-h-[40px] transition-colors"
[class.border-blue-500]="isDraggingOver()"
[class.dark:border-blue-400]="isDraggingOver()"
[class.bg-blue-500/5]="isDraggingOver()"
@@ -128,12 +128,12 @@
<!-- Drop zone (always visible for groups) -->
<div class="min-h-[20px] flex items-center justify-center">
@if (isExpanded() && children.length === 0) {
<div class="py-2 px-3 text-xs text-gray-400 dark:text-gray-500 italic">
<div class="py-2 px-3 text-xs text-muted dark:text-muted italic">
Drop items here
</div>
}
@if (!isExpanded()) {
<div class="py-1 px-2 text-xs text-gray-400 dark:text-gray-500 italic opacity-50">
<div class="py-1 px-2 text-xs text-muted dark:text-muted italic opacity-50">
Drop here ({{ children.length }} items)
</div>
}
@@ -1,12 +1,12 @@
<div class="bookmarks-panel flex flex-col h-full bg-white dark:bg-gray-900">
<div class="bookmarks-panel flex flex-col h-full bg-card dark:bg-main">
<!-- Header -->
<div class="bookmarks-header flex-shrink-0 p-4 border-b border-gray-200 dark:border-gray-700">
<div class="bookmarks-header flex-shrink-0 p-4 border-b border-border dark:border-border">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Bookmarks</h2>
<h2 class="text-lg font-semibold text-main dark:text-gray-100">Bookmarks</h2>
<div class="flex items-center gap-2">
<button
type="button"
class="btn-standard-sm"
class="btn btn-outline btn-sm"
(click)="createGroup()"
title="Créer un groupe">
+ Group
@@ -24,13 +24,14 @@
[value]="searchTerm()"
(input)="onSearchChange($any($event.target).value)"
placeholder="Search bookmarks..."
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="w-full px-3 py-2 text-sm border border-border dark:border-border rounded-md bg-card dark:bg-card text-main dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
@if (searchTerm()) {
<button
(click)="onSearchChange('')"
class="btn-standard-icon absolute right-2 top-1/2 -translate-y-1/2"
title="Clear search">
class="btn btn-ghost btn-sm absolute right-2 top-1/2 -translate-y-1/2"
title="Clear search"
aria-label="Clear search">
✕
</button>
}
@@ -41,7 +42,7 @@
<!-- Body -->
<div class="bookmarks-body flex-1 overflow-y-auto p-4">
@if (loading()) {
<div class="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
<div class="flex items-center justify-center py-8 text-muted dark:text-muted">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
} @else if (error()) {
@@ -51,14 +52,14 @@
<span class="text-sm text-red-700 dark:text-red-300 flex-1">{{ error() }}</span>
<button
(click)="clearError()"
class="btn-standard-icon !text-red-600 dark:!text-red-400 hover:!text-red-800 dark:hover:!text-red-200"
title="Dismiss">
class="btn btn-ghost btn-sm !text-red-600 dark:!text-red-400 hover:!text-red-800 dark:hover:!text-red-200"
title="Dismiss" aria-label="Dismiss">
✕
</button>
</div>
</div>
} @else if (isEmpty()) {
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
<div class="text-center py-8 text-muted dark:text-muted">
<p class="mb-4">No bookmarks yet</p>
<p class="text-sm">Use the bookmark icon in the note toolbar to add one.</p>
</div>
@@ -115,7 +116,7 @@
class="mb-1" />
}
@if (displayItems().length === 0) {
<p class="text-sm text-gray-400 dark:text-gray-500 py-4 text-center">No bookmarks to display.</p>
<p class="text-sm text-muted dark:text-muted py-4 text-center">No bookmarks to display.</p>
}
</div>
}
@@ -124,20 +125,20 @@
<!-- Conflict Modal -->
@if (conflictInfo()) {
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Conflict Detected</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
<div class="bg-card dark:bg-card rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<h3 class="text-lg font-semibold text-main dark:text-gray-100 mb-3">Conflict Detected</h3>
<p class="text-sm text-muted dark:text-muted mb-4">
The bookmarks file has been modified externally. What would you like to do?
</p>
<div class="flex gap-3">
<button
(click)="resolveConflictReload()"
class="btn-standard-primary flex-1">
class="btn btn-solid btn-md flex-1">
Reload from file
</button>
<button
(click)="resolveConflictOverwrite()"
class="btn-standard-danger flex-1">
class="btn btn-outline btn-md flex-1">
Overwrite file
</button>
</div>
@@ -15,7 +15,7 @@ import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
<div>
<div
(click)="onFolderClick(folder)"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition"
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform text-obs-l-text-muted dark:text-obs-d-text-muted" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
@@ -45,7 +45,7 @@ import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
[class.bg-obs-l-bg-main]="selectedNoteId() === file.id"
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === file.id"
[class.hover:bg-slate-500/10]="selectedNoteId() !== file.id"
[class.dark:hover:bg-slate-200/10]="selectedNoteId() !== file.id"
[class.dark:hover:bg-surface2/10]="selectedNoteId() !== file.id"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
@@ -38,12 +38,12 @@ export interface GraphOptions {
imports: [CommonModule, FormsModule, SearchInputWithAssistantComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="graph-options-panel bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 h-full overflow-y-auto">
<div class="graph-options-panel bg-card dark:bg-card border-l border-border dark:border-border h-full overflow-y-auto">
<div class="p-4 space-y-6">
<!-- Filters Section -->
<section>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2">
<h3 class="text-sm font-semibold text-main dark:text-gray-100 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
@@ -56,8 +56,8 @@ export interface GraphOptions {
type="checkbox"
[(ngModel)]="filters().showTags"
(change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Tags</span>
class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Tags</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
@@ -65,8 +65,8 @@ export interface GraphOptions {
type="checkbox"
[(ngModel)]="filters().showAttachments"
(change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Attachments</span>
class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Attachments</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
@@ -74,8 +74,8 @@ export interface GraphOptions {
type="checkbox"
[(ngModel)]="filters().existingFilesOnly"
(change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Existing files only</span>
class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Existing files only</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
@@ -83,8 +83,8 @@ export interface GraphOptions {
type="checkbox"
[(ngModel)]="filters().showOrphans"
(change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Orphans</span>
class="rounded border-border text-blue-600 focus:ring-ring dark:border-border dark:bg-surface2">
<span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Orphans</span>
</label>
</div>
@@ -96,14 +96,14 @@ export interface GraphOptions {
[placeholder]="'Search files…'"
[context]="'graph'"
[showSearchIcon]="true"
[inputClass]="'w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500'"
[inputClass]="'w-full px-3 py-2 text-sm border border-border dark:border-border rounded-md bg-card dark:bg-surface2 text-main dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-ring'"
/>
</div>
</section>
<!-- Groups Section -->
<section>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2">
<h3 class="text-sm font-semibold text-main dark:text-gray-100 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
@@ -112,7 +112,7 @@ export interface GraphOptions {
<div class="space-y-2 mb-3">
@for (group of colorGroups(); track $index) {
<div class="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="flex items-center gap-2 p-2 bg-surface1 dark:bg-surface2/50 rounded-lg">
<div
[style.background-color]="getGroupColorPreview(group)"
class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800 shadow-sm flex-shrink-0">
@@ -122,11 +122,11 @@ export interface GraphOptions {
[value]="group.query"
(input)="updateGroupQuery($index, $any($event.target).value)"
placeholder="Query (e.g., tag:#code)"
class="flex-1 px-2 py-1 text-xs bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded"
class="flex-1 px-2 py-1 text-xs bg-card dark:bg-card border border-border dark:border-border rounded"
/>
<button
(click)="removeGroup($index)"
class="p-1 text-gray-400 hover:text-red-500 transition-colors"
class="p-1 text-muted hover:text-red-500 transition-colors"
title="Remove group"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -147,7 +147,7 @@ export interface GraphOptions {
<!-- Display Section -->
<section>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3 flex items-center gap-2">
<h3 class="text-sm font-semibold text-main dark:text-gray-100 mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
@@ -161,12 +161,12 @@ export interface GraphOptions {
type="checkbox"
[(ngModel)]="display().showArrows"
(change)="emitOptionsChange()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700">
<span class="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100">Arrows</span>
class="rounded border-border text-blue-600 focus:ring-blue-500 dark:border-border dark:bg-surface2">
<span class="text-sm text-main dark:text-main group-hover:text-main dark:group-hover:text-gray-100">Arrows</span>
</label>
<div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1">
<label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Text fade threshold</span>
<span class="font-mono text-xs">{{ display().textFadeThreshold }}</span>
</label>
@@ -176,11 +176,11 @@ export interface GraphOptions {
max="100"
[(ngModel)]="display().textFadeThreshold"
(input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600">
class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div>
<div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1">
<label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Node size</span>
<span class="font-mono text-xs">{{ display().nodeSize }}</span>
</label>
@@ -190,11 +190,11 @@ export interface GraphOptions {
max="20"
[(ngModel)]="display().nodeSize"
(input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600">
class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div>
<div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1">
<label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Link thickness</span>
<span class="font-mono text-xs">{{ display().linkThickness }}</span>
</label>
@@ -204,7 +204,7 @@ export interface GraphOptions {
max="10"
[(ngModel)]="display().linkThickness"
(input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600">
class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div>
</div>
@@ -221,7 +221,7 @@ export interface GraphOptions {
<button
type="button"
(click)="forcesExpanded.set(!forcesExpanded())"
class="w-full flex items-center justify-between text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">
class="w-full flex items-center justify-between text-sm font-semibold text-main dark:text-gray-100 mb-3">
<span class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
@@ -242,7 +242,7 @@ export interface GraphOptions {
@if (forcesExpanded()) {
<div class="space-y-4">
<div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1">
<label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Charge</span>
<span class="font-mono text-xs">{{ forces().chargeStrength }}</span>
</label>
@@ -252,11 +252,11 @@ export interface GraphOptions {
max="0"
[(ngModel)]="forces().chargeStrength"
(input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600">
class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div>
<div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1">
<label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Link distance</span>
<span class="font-mono text-xs">{{ forces().linkDistance }}</span>
</label>
@@ -266,11 +266,11 @@ export interface GraphOptions {
max="500"
[(ngModel)]="forces().linkDistance"
(input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600">
class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div>
<div>
<label class="flex items-center justify-between text-sm text-gray-700 dark:text-gray-300 mb-1">
<label class="flex items-center justify-between text-sm text-main dark:text-main mb-1">
<span>Center strength</span>
<span class="font-mono text-xs">{{ forces().centerStrength.toFixed(2) }}</span>
</label>
@@ -281,7 +281,7 @@ export interface GraphOptions {
step="0.01"
[(ngModel)]="forces().centerStrength"
(input)="emitOptionsChange()"
class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600">
class="w-full h-2 bg-surface2 dark:bg-surface2 rounded-lg appearance-none cursor-pointer accent-blue-600">
</div>
</div>
}
@@ -37,7 +37,7 @@ import { createFilteredGraphData, createGroupLegend, createFocusedGraphData } fr
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col h-full w-full bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col h-full w-full bg-surface1 dark:bg-main">
<!-- Main graph area -->
<div class="flex-1 relative">
<app-graph-canvas
@@ -53,10 +53,10 @@ import { createFilteredGraphData, createGroupLegend, createFocusedGraphData } fr
<button
type="button"
(click)="toggleSettings()"
class="absolute top-5 right-5 flex items-center justify-center w-11 h-11 rounded-full bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all hover:rotate-90 z-50 border border-gray-200 dark:border-gray-700"
class="absolute top-5 right-5 flex items-center justify-center w-11 h-11 rounded-full bg-card dark:bg-card shadow-lg hover:shadow-xl transition-all hover:rotate-90 z-50 border border-border dark:border-border"
aria-label="Graph settings"
title="Graph settings">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-gray-700 dark:text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-main dark:text-main" 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" />
@@ -66,7 +66,7 @@ import { createFilteredGraphData, createGroupLegend, createFocusedGraphData } fr
<!-- Legend at bottom -->
@if (legendItems().length > 0) {
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<div class="p-4 border-t border-border dark:border-border">
<app-graph-legend
[items]="legendItems()"
(itemClicked)="onLegendItemClicked($event)">
@@ -37,7 +37,7 @@ export interface GraphDisplayOptions {
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="relative h-full w-full bg-gray-50 dark:bg-gray-900">
<div class="relative h-full w-full bg-surface1 dark:bg-main">
<svg #graphSvg class="w-full h-full">
<defs>
<marker
@@ -99,7 +99,7 @@ export interface GraphDisplayOptions {
</svg>
<!-- Info overlay -->
<div class="absolute top-4 left-4 bg-white dark:bg-gray-800 px-3 py-2 rounded-lg shadow-md text-xs text-gray-700 dark:text-gray-300">
<div class="absolute top-4 left-4 bg-card dark:bg-card px-3 py-2 rounded-lg shadow-md text-xs text-main dark:text-main">
<div><strong>Nodes:</strong> {{ simulatedNodes().length }}</div>
<div><strong>Links:</strong> {{ edges().length }}</div>
</div>
@@ -1,10 +1,11 @@
import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, effect, CUSTOM_ELEMENTS_SCHEMA, Output, EventEmitter } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges, signal, computed, inject, effect, CUSTOM_ELEMENTS_SCHEMA, Output, EventEmitter, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { MarkdownService } from '../../services/markdown.service';
import { Note } from '../../types';
import { DrawingsEditorComponent } from '../../app/features/drawings/drawings-editor.component';
import { EditorStateService } from '../../services/editor-state.service';
import { TocService } from '../../services/toc.service';
/**
* Composant réutilisable pour afficher du contenu Markdown
@@ -83,8 +84,9 @@ import { EditorStateService } from '../../services/editor-state.service';
<!-- Markdown Content -->
<div
*ngIf="!isExcalidrawFile()"
class="markdown-viewer__content prose prose-slate dark:prose-invert max-w-none"
[innerHTML]="renderedHtml()">
class="markdown-viewer__content"
#contentRef>
<div class="md-view prose themed-prose max-w-none" [innerHTML]="renderedHtml()"></div>
</div>
<!-- Error State -->
@@ -187,10 +189,12 @@ import { EditorStateService } from '../../services/editor-state.service';
}
`]
})
export class MarkdownViewerComponent implements OnChanges {
export class MarkdownViewerComponent implements OnChanges, AfterViewInit {
private markdownService = inject(MarkdownService);
private sanitizer = inject(DomSanitizer);
private editorState = inject(EditorStateService);
private toc = inject(TocService);
@ViewChild('contentRef') private contentRef?: ElementRef<HTMLElement>;
/** Contenu markdown brut à afficher */
@Input() content: string = '';
@@ -252,6 +256,21 @@ export class MarkdownViewerComponent implements OnChanges {
this.setupLazyLoading();
}
});
// After each HTML render, apply anchors and scrollspy
effect(() => {
// depend on renderedHtml() signal
void this.renderedHtml();
if (this.isExcalidrawFile()) return;
// next tick to ensure DOM updated
setTimeout(() => {
const root = this.contentRef?.nativeElement as HTMLElement | undefined;
if (!root) return;
this.applyAnchors(root);
this.toc.extract(root);
this.toc.setupScrollSpy(root, 84);
}, 0);
});
}
ngOnChanges(changes: SimpleChanges): void {
@@ -302,4 +321,37 @@ export class MarkdownViewerComponent implements OnChanges {
}
}, 0);
}
ngAfterViewInit(): void {
setTimeout(() => {
if (this.isExcalidrawFile()) return;
const root = this.contentRef?.nativeElement as HTMLElement | undefined;
if (!root) return;
this.applyAnchors(root);
this.toc.extract(root);
this.toc.setupScrollSpy(root, 84);
}, 0);
}
private applyAnchors(root: HTMLElement): void {
const headings = Array.from(root.querySelectorAll('h1, h2, h3, h4, h5, h6')) as HTMLHeadingElement[];
for (const h of headings) {
if (!h.id) {
// ensure id if missing
h.id = (h.textContent || '').trim().toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^\w\s-]/g, '')
.trim().replace(/\s+/g, '-').replace(/-+/g, '-') || 'section';
}
const exists = h.querySelector(':scope > a.heading-anchor');
if (!exists) {
const a = document.createElement('a');
a.className = 'heading-anchor';
a.href = `#${h.id}`;
a.textContent = '#';
a.setAttribute('aria-label', 'Anchor link');
h.appendChild(a);
}
}
}
}
@@ -15,23 +15,23 @@ export interface PreviewData {
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div
class="preview-card bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 p-4 max-w-md overflow-hidden"
class="preview-card bg-card dark:bg-card rounded-2xl shadow-xl border border-border dark:border-border p-4 max-w-md overflow-hidden"
(mouseenter)="mouseEnter.emit()"
(mouseleave)="mouseLeave.emit()">
<h2 class="font-bold text-lg mb-2 text-gray-900 dark:text-gray-100 truncate">
<h2 class="font-bold text-lg mb-2 text-main dark:text-gray-100 truncate">
{{ previewData().title }}
</h2>
<div
class="prose prose-sm dark:prose-invert max-w-none line-clamp-5 text-gray-700 dark:text-gray-300"
class="prose prose-sm dark:prose-invert max-w-none line-clamp-5 text-main dark:text-main"
[innerHTML]="sanitizedExcerpt()">
</div>
<div class="mt-3 flex justify-end">
<button
(click)="openNote.emit()"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors p-1 rounded hover:bg-surface1 dark:hover:bg-surface2"
aria-label="Ouvrir la note">
<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 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
@@ -32,7 +32,7 @@ import { SearchOptions } from '../../core/search/search-parser.types';
<!-- Search icon -->
<svg
*ngIf="showSearchIcon"
class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-gray-400 z-10"
class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-muted z-10"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -144,7 +144,7 @@ export class SearchBarComponent implements AfterViewInit, OnInit {
@Input() context: string = 'vault';
@Input() showExamples: boolean = true;
@Input() showSearchIcon: boolean = true;
@Input() inputClass: string = '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';
@Input() inputClass: string = '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-border dark:bg-card/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-ring';
@Input() initialQuery: string = '';
@Input() caseSensitiveDefault: boolean = false;
@Input() regexDefault: boolean = false;
@@ -31,7 +31,7 @@ import { SearchPreferencesService } from '../../core/search/search-preferences.s
<div class="relative">
<svg
*ngIf="showSearchIcon"
class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-gray-400"
class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted dark:text-muted"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -97,7 +97,7 @@ export class SearchInputWithAssistantComponent implements AfterViewInit, OnInit
@Input() value: string = '';
@Input() context: string = 'default';
@Input() showSearchIcon: boolean = true;
@Input() inputClass: string = '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';
@Input() inputClass: string = '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-border dark:bg-card/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-ring';
@Input() showExamples: boolean = true;
@Output() valueChange = new EventEmitter<string>();
@@ -39,9 +39,9 @@ import { parseSearchQuery } from '../../core/search/search-parser';
imports: [CommonModule, FormsModule, SearchBarComponent, SearchResultsComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col h-full bg-bg-primary dark:bg-gray-900">
<div class="flex flex-col h-full bg-bg-primary dark:bg-main">
<!-- Search bar -->
<div class="p-4 border-b border-border dark:border-gray-700">
<div class="p-4 border-b border-border dark:border-border">
<app-search-bar
[placeholder]="placeholder"
[context]="context"
@@ -58,10 +58,10 @@ import { parseSearchQuery } from '../../core/search/search-parser';
<!-- Search options toggles -->
@if (hasSearched() && results().length > 0) {
<div class="flex flex-col gap-3 p-4 border-b border-border dark:border-gray-700 bg-bg-muted dark:bg-gray-800">
<div class="flex flex-col gap-3 p-4 border-b border-border dark:border-border bg-bg-muted dark:bg-card">
<!-- Collapse results toggle -->
<label class="flex items-center justify-between cursor-pointer group">
<span class="text-sm text-text-main dark:text-gray-200">Collapse results</span>
<span class="text-sm text-text-main dark:text-main">Collapse results</span>
<div class="relative">
<input
type="checkbox"
@@ -69,14 +69,14 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(change)="onToggleCollapse()"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
<div class="w-11 h-6 bg-muted dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-card rounded-full transition-transform peer-checked:translate-x-5"></div>
</div>
</label>
<!-- Show more context toggle -->
<label class="flex items-center justify-between cursor-pointer group">
<span class="text-sm text-text-main dark:text-gray-200">Show more context</span>
<span class="text-sm text-text-main dark:text-main">Show more context</span>
<div class="relative">
<input
type="checkbox"
@@ -84,14 +84,14 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(change)="onToggleContext()"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
<div class="w-11 h-6 bg-muted dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-card rounded-full transition-transform peer-checked:translate-x-5"></div>
</div>
</label>
<!-- Explain search terms toggle -->
<label class="flex items-center justify-between cursor-pointer group">
<span class="text-sm text-text-main dark:text-gray-200">Explain search terms</span>
<span class="text-sm text-text-main dark:text-main">Explain search terms</span>
<div class="relative">
<input
type="checkbox"
@@ -99,19 +99,19 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(change)="onToggleExplain()"
class="sr-only peer"
/>
<div class="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
<div class="w-11 h-6 bg-muted dark:bg-gray-600 rounded-full peer peer-checked:bg-blue-500 dark:peer-checked:bg-blue-600 transition-colors"></div>
<div class="absolute left-1 top-1 w-4 h-4 bg-card rounded-full transition-transform peer-checked:translate-x-5"></div>
</div>
</label>
</div>
}
<!-- Explain search terms panel -->
@if (explainSearchTerms && currentQuery().trim()) {
<div class="p-4 border-b border-border dark:border-gray-700 bg-bg-primary/60 dark:bg-gray-900/60">
<div class="p-4 border-b border-border dark:border-border bg-bg-primary/60 dark:bg-main/60">
<h4 class="text-xs font-semibold uppercase tracking-wide text-text-muted mb-2">Match all of:</h4>
<ul class="ml-1 space-y-1 text-xs">
@for (line of explanationLines(); track $index) {
<li class="text-text-muted dark:text-gray-400">{{ line }}</li>
<li class="text-text-muted dark:text-muted">{{ line }}</li>
}
</ul>
</div>
@@ -123,11 +123,11 @@ import { parseSearchQuery } from '../../core/search/search-parser';
<div class="flex items-center justify-center h-full">
<div class="flex flex-col items-center gap-3">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-accent dark:border-blue-400"></div>
<p class="text-sm text-text-muted dark:text-gray-400">Searching...</p>
<p class="text-sm text-text-muted dark:text-muted">Searching...</p>
</div>
</div>
} @else if (hasSearched() && results().length === 0) {
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-gray-400 p-8">
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-muted p-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@@ -144,7 +144,7 @@ import { parseSearchQuery } from '../../core/search/search-parser';
(noteOpen)="onNoteOpen($event)"
/>
} @else {
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-gray-400 p-8">
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-muted p-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" 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>
@@ -35,17 +35,17 @@ type NavigationItem =
@if (isOpen()) {
<div
#popover
class="absolute top-full left-0 right-0 mt-1 bg-bg-primary dark:bg-gray-800 border border-border dark:border-gray-700 rounded-xl shadow-2xl z-50 overflow-hidden max-h-[360px]"
class="absolute top-full left-0 right-0 mt-1 bg-bg-primary dark:bg-card border border-border dark:border-border rounded-xl shadow-2xl z-50 overflow-hidden max-h-[360px]"
style="z-index: 9999;"
(click)="$event.stopPropagation()"
>
<!-- Search Options -->
<div class="p-3 border-b border-border dark:border-gray-700">
<div class="p-3 border-b border-border dark:border-border">
<div class="flex items-center justify-between mb-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-text-main dark:text-gray-100">Search options</h4>
<button
(click)="showHelp = !showHelp"
class="p-1 rounded-full hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors"
class="p-1 rounded-full hover:bg-bg-muted dark:hover:bg-surface2 transition-colors"
title="Show help"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -55,15 +55,15 @@ type NavigationItem =
</div>
@if (showHelp) {
<div class="mb-2 p-2 bg-bg-muted dark:bg-gray-900 rounded-lg text-[11px] space-y-1.5">
<div class="mb-2 p-2 bg-bg-muted dark:bg-main rounded-lg text-[11px] space-y-1.5">
<div><strong>Operators:</strong></div>
<div class="grid grid-cols-2 gap-1.5 text-text-muted dark:text-gray-400">
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">OR</code> Either term</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">-term</code> Exclude</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">"phrase"</code> Exact match</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">term*</code> Wildcard</div>
<div class="grid grid-cols-2 gap-1.5 text-text-muted dark:text-muted">
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">OR</code> Either term</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">-term</code> Exclude</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">"phrase"</code> Exact match</div>
<div><code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">term*</code> Wildcard</div>
</div>
<div><strong>Combine:</strong> <code class="px-1 py-0.5 bg-bg-primary dark:bg-gray-800 rounded">tag:#todo OR tag:#urgent</code></div>
<div><strong>Combine:</strong> <code class="px-1 py-0.5 bg-bg-primary dark:bg-card rounded">tag:#todo OR tag:#urgent</code></div>
</div>
}
@@ -71,20 +71,20 @@ type NavigationItem =
<button
*ngFor="let option of searchOptions(); let i = index"
(click)="insertOption(option.prefix)"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors group text-sm"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-surface2 transition-colors group text-sm"
[ngClass]="{
'bg-bg-muted': isSelected('option', i),
'dark:bg-gray-700': isSelected('option', i)
'dark:bg-surface2': isSelected('option', i)
}"
>
<div class="flex items-center gap-2">
<code class="text-xs font-mono text-accent dark:text-blue-400 whitespace-nowrap">{{ option.prefix }}</code>
<div class="flex-1 min-w-0 text-[13px] text-text-muted dark:text-gray-400 group-hover:text-text-main dark:group-hover:text-gray-100">
<div class="flex-1 min-w-0 text-[13px] text-text-muted dark:text-muted group-hover:text-text-main dark:group-hover:text-gray-100">
{{ option.description }}
</div>
<span
*ngIf="showExamples && option.example"
class="text-[11px] text-text-muted dark:text-gray-500 font-mono ml-2 truncate"
class="text-[11px] text-text-muted dark:text-muted font-mono ml-2 truncate"
>
{{ option.example }}
</span>
@@ -95,12 +95,12 @@ type NavigationItem =
<!-- History -->
@if (history().length > 0) {
<div class="p-3 border-b border-border dark:border-gray-700">
<div class="p-3 border-b border-border dark:border-border">
<div class="flex items-center justify-between mb-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-text-main dark:text-gray-100">History</h4>
<button
(click)="clearHistory()"
class="text-[11px] text-text-muted dark:text-gray-400 hover:text-accent dark:hover:text-blue-400 transition-colors"
class="text-[11px] text-text-muted dark:text-muted hover:text-accent dark:hover:text-blue-400 transition-colors"
>
Clear
</button>
@@ -109,13 +109,13 @@ type NavigationItem =
<button
*ngFor="let item of history(); let i = index"
(click)="selectHistoryItem(item)"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors group text-sm"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-surface2 transition-colors group text-sm"
[ngClass]="{
'bg-bg-muted': isSelected('history', i),
'dark:bg-gray-700': isSelected('history', i)
'dark:bg-surface2': isSelected('history', i)
}"
>
<span class="block truncate text-[13px] text-text-muted dark:text-gray-300 group-hover:text-text-main dark:group-hover:text-gray-100 font-mono">
<span class="block truncate text-[13px] text-text-muted dark:text-main group-hover:text-text-main dark:group-hover:text-gray-100 font-mono">
{{ item }}
</span>
</button>
@@ -133,13 +133,13 @@ type NavigationItem =
<button
*ngFor="let suggestion of suggestions(); let i = index"
(click)="insertSuggestion(suggestion)"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-gray-700 transition-colors text-sm"
class="w-full text-left px-2.5 py-1.5 rounded-lg hover:bg-bg-muted dark:hover:bg-surface2 transition-colors text-sm"
[ngClass]="{
'bg-bg-muted': isSelected('suggestion', i),
'dark:bg-gray-700': isSelected('suggestion', i)
'dark:bg-surface2': isSelected('suggestion', i)
}"
>
<span class="block truncate text-[13px] text-text-main dark:text-gray-200">{{ suggestion }}</span>
<span class="block truncate text-[13px] text-text-main dark:text-main">{{ suggestion }}</span>
</button>
</div>
</div>
@@ -45,14 +45,14 @@ type SortOption = 'relevance' | 'name' | 'modified';
imports: [CommonModule, FormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col h-full bg-bg-primary dark:bg-gray-900">
<div class="flex flex-col h-full bg-bg-primary dark:bg-main">
<!-- Results header -->
<div class="flex items-center justify-between p-4 border-b border-border dark:border-gray-700">
<div class="flex items-center justify-between p-4 border-b border-border dark:border-border">
<div class="flex items-center gap-3">
<h3 class="text-sm font-semibold text-text-main dark:text-gray-100">
{{ totalResults() }} {{ totalResults() === 1 ? 'result' : 'results' }}
@if (totalMatches() > 0) {
<span class="text-text-muted dark:text-gray-400">
<span class="text-text-muted dark:text-muted">
({{ totalMatches() }} {{ totalMatches() === 1 ? 'match' : 'matches' }})
</span>
}
@@ -61,11 +61,11 @@ type SortOption = 'relevance' | 'name' | 'modified';
<!-- Sort options -->
<div class="flex items-center gap-2">
<label class="text-xs text-text-muted dark:text-gray-400">Sort:</label>
<label class="text-xs text-text-muted dark:text-muted">Sort:</label>
<select
[(ngModel)]="sortBy"
(change)="onSortChange()"
class="text-xs px-2 py-1 rounded border border-border dark:border-gray-600 bg-bg-primary dark:bg-gray-800 text-text-main dark:text-gray-100"
class="text-xs px-2 py-1 rounded border border-border dark:border-border bg-bg-primary dark:bg-card text-text-main dark:text-gray-100"
>
<option value="relevance">Relevance</option>
<option value="name">Name</option>
@@ -77,7 +77,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
<!-- Results list -->
<div class="flex-1 overflow-y-auto">
@if (sortedGroups().length === 0) {
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-gray-400 p-8">
<div class="flex flex-col items-center justify-center h-full text-text-muted dark:text-muted p-8">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 opacity-50" 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>
@@ -86,7 +86,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
} @else {
<div class="divide-y divide-border dark:divide-gray-700">
@for (group of sortedGroups(); track group.noteId) {
<div class="hover:bg-bg-muted dark:hover:bg-gray-800 transition-colors">
<div class="hover:bg-bg-muted dark:hover:bg-card transition-colors">
<!-- File header -->
<div
class="flex items-center justify-between p-3 cursor-pointer"
@@ -96,7 +96,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
<!-- Expand/collapse icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 text-text-muted dark:text-gray-400 transition-transform"
class="h-4 w-4 text-text-muted dark:text-muted transition-transform"
[class.rotate-90]="group.isExpanded"
fill="none"
viewBox="0 0 24 24"
@@ -115,7 +115,7 @@ type SortOption = 'relevance' | 'name' | 'modified';
<div class="text-sm font-medium text-text-main dark:text-gray-100 truncate">
{{ group.fileName }}
</div>
<div class="text-xs text-text-muted dark:text-gray-400 truncate">
<div class="text-xs text-text-muted dark:text-muted truncate">
{{ group.filePath }}
</div>
</div>
@@ -143,23 +143,23 @@ type SortOption = 'relevance' | 'name' | 'modified';
<div class="px-3 pb-3 pl-11 space-y-2">
@for (match of group.matches; track $index) {
<div
class="p-2 rounded bg-bg-muted dark:bg-gray-800 hover:bg-bg-secondary dark:hover:bg-gray-700 cursor-pointer transition-colors"
class="p-2 rounded bg-bg-muted dark:bg-card hover:bg-bg-secondary dark:hover:bg-surface2 cursor-pointer transition-colors"
(click)="openNote(group.noteId, $event, match.line)"
>
<!-- Match type badge -->
<div class="flex items-center gap-2 mb-1">
<span class="text-xs px-1.5 py-0.5 rounded bg-bg-primary dark:bg-gray-900 text-text-muted dark:text-gray-400 font-mono">
<span class="text-xs px-1.5 py-0.5 rounded bg-bg-primary dark:bg-main text-text-muted dark:text-muted font-mono">
{{ match.type }}
</span>
@if (match.line) {
<span class="text-xs text-text-muted dark:text-gray-500">
<span class="text-xs text-text-muted dark:text-muted">
Line {{ match.line }}
</span>
}
</div>
<!-- Match context with highlighting -->
<div class="text-sm text-text-main dark:text-gray-200 font-mono leading-relaxed whitespace-pre-wrap">
<div class="text-sm text-text-main dark:text-main font-mono leading-relaxed whitespace-pre-wrap">
<span [innerHTML]="highlightMatch(match)"></span>
</div>
</div>
@@ -113,7 +113,7 @@ interface MetadataEntry {
[attr.title]="tocOpen() ? 'Cacher sommaire' : 'Afficher sommaire'"
[attr.aria-label]="tocOpen() ? 'Cacher sommaire' : 'Afficher sommaire'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" [ngClass]="tocOpen() ? 'text-accent' : 'text-text-muted'"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M13 8h5"/><path d="M13 12h5"/><path d="M13 16h5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" [ngClass]="tocOpen() ? 'toc-toggle--active' : 'toc-toggle--idle'"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M13 8h5"/><path d="M13 12h5"/><path d="M13 16h5"/></svg>
</button>
<button
@@ -139,6 +139,13 @@ interface MetadataEntry {
</button>
@if (menuOpen()) {
<div class="absolute right-0 mt-2 w-56 rounded-md border border-border bg-card shadow-subtle not-prose z-10">
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-muted" (click)="parametersRequested.emit(); closeMenu()">
<svg xmlns="http://www.w3.org/2000/svg" class="inline h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v6m0 6v6m-6-6h6m6 0h-6M4.93 4.93l4.24 4.24m5.66 5.66l4.24 4.24m-4.24-14.14l4.24-4.24M4.93 19.07l4.24-4.24"/>
</svg>
Parameters
</button>
<button type="button" class="block w-full text-left px-3 py-2 hover:bg-muted" (click)="legacyRequested.emit(); closeMenu()">🔧 Legacy</button>
</div>
}
@@ -224,7 +231,7 @@ interface MetadataEntry {
{{ getAuthorFromFrontmatter() ?? note().author ?? 'Auteur inconnu' }}
</span>
@if (hasState('favoris')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-gray-400'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" role="img" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}">
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-muted'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" role="img" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}">
@if (state('favoris')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M19 14.5c1.5-1.5 2-4 0-6s-5-2-7 1c-2-3-5-3.5-7-1s-1.5 4.5 0 6l7 6z" />
@@ -237,7 +244,7 @@ interface MetadataEntry {
</span>
}
@if (hasState('publish')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('publish') ? 'text-green-500' : 'text-gray-400'" title="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}" role="img" aria-label="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}">
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('publish') ? 'text-green-500' : 'text-muted'" title="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}" role="img" aria-label="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20" />
@@ -246,7 +253,7 @@ interface MetadataEntry {
</span>
}
@if (hasState('draft')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('draft') ? 'text-yellow-500' : 'text-gray-400'" title="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}" role="img" aria-label="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}">
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('draft') ? 'text-yellow-500' : 'text-muted'" title="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}" role="img" aria-label="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}">
@if (state('draft')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3h18v4H3z" />
@@ -261,7 +268,7 @@ interface MetadataEntry {
</span>
}
@if (hasState('template')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('template') ? 'text-amber-500' : 'text-gray-400'" title="{{ state('template') ? 'Modèle' : 'Non modèle' }}" role="img" aria-label="{{ state('template') ? 'Modèle' : 'Non modèle' }}">
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('template') ? 'text-amber-500' : 'text-muted'" title="{{ state('template') ? 'Modèle' : 'Non modèle' }}" role="img" aria-label="{{ state('template') ? 'Modèle' : 'Non modèle' }}">
@if (state('template')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="14" rx="2" />
@@ -276,7 +283,7 @@ interface MetadataEntry {
</span>
}
@if (hasState('task')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('task') ? 'text-indigo-500' : 'text-gray-400'" title="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}" role="img" aria-label="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}">
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('task') ? 'text-indigo-500' : 'text-muted'" title="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}" role="img" aria-label="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}">
@if (state('task')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="16" rx="2" />
@@ -290,7 +297,7 @@ interface MetadataEntry {
</span>
}
@if (hasState('private')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('private') ? 'text-purple-500' : 'text-gray-400'" title="{{ state('private') ? 'Privé' : 'Public' }}" role="img" aria-label="{{ state('private') ? 'Privé' : 'Public' }}">
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('private') ? 'text-purple-500' : 'text-muted'" title="{{ state('private') ? 'Privé' : 'Public' }}" role="img" aria-label="{{ state('private') ? 'Privé' : 'Public' }}">
@if (state('private')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" />
@@ -305,7 +312,7 @@ interface MetadataEntry {
</span>
}
@if (hasState('archive')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('archive') ? 'text-amber-600' : 'text-gray-400'" title="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}" role="img" aria-label="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}">
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('archive') ? 'text-amber-600' : 'text-muted'" title="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}" role="img" aria-label="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}">
@if (state('archive')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22,12 18,12 18,8"/>
@@ -358,6 +365,7 @@ export class NoteViewerComponent implements OnDestroy {
addTagRequested = output<void>();
fullScreenRequested = output<void>();
legacyRequested = output<void>();
parametersRequested = output<void>();
fullScreenActive = input<boolean>(false);
tocOpen = input<boolean>(false);