feat: add Meilisearch backend integration with Docker Compose setup and Excalidraw support

This commit is contained in:
2025-10-14 10:41:15 -04:00
parent b359e8ab8e
commit 32a9998b40
71 changed files with 11544 additions and 435 deletions
+69 -106
View File
@@ -175,108 +175,36 @@
<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]="filteredFileTree()"
[nodes]="filteredExplorerTree()"
[selectedNoteId]="selectedNoteId()"
(fileSelected)="selectNote($event)"
(fileSelected)="onFileNodeSelected($event)"
></app-file-explorer>
}
@case ('tags') {
<app-tags-view [tags]="filteredTags()" (tagSelected)="handleTagClick($event)"></app-tags-view>
<app-tags-view [tags]="allTags()" (tagSelected)="handleTagClick($event)"></app-tags-view>
}
@case ('search') {
<div class="space-y-4 p-3">
<div class="flex flex-col gap-3">
<div class="relative">
<app-search-input-with-assistant
[value]="sidebarSearchTerm()"
(valueChange)="updateSearchTerm($event)"
(submit)="onSearchSubmit($event)"
[placeholder]="'Rechercher dans la voûte...'"
[context]="'vault-sidebar'"
[showSearchIcon]="true"
[showExamples]="false"
[inputClass]="'w-full rounded-full border border-border bg-bg-muted/70 py-2.5 pr-4 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring transition-all dark:border-gray-600 dark:bg-gray-800/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-blue-500'"
/>
</div>
@if (activeTagDisplay(); as tagDisplay) {
<div class="flex items-center justify-between rounded-lg border border-border bg-card px-3 py-2">
<div class="flex items-center gap-2 text-sm font-small text-text-main">
<span>🔖</span>
<span class="truncate">{{ tagDisplay }}</span>
</div>
<button class="btn btn-sm btn-ghost" (click)="clearTagFilter()">Effacer</button>
</div>
}
</div>
<div class="space-y-3">
<div class="flex items-center justify-between px-2">
<h3 class="text-xs font-semibold uppercase tracking-wide text-text-muted">Résultats</h3>
<span class="text-xs text-text-muted">{{ searchResults().length }}</span>
</div>
@if (searchResults().length > 0) {
<ul class="space-y-1">
@for (note of searchResults(); track note.id) {
<li
(click)="selectNote(note.id)"
class="cursor-pointer rounded-lg border border-transparent bg-card px-3 py-2 transition hover:border-border hover:bg-bg-muted"
[ngClass]="{ 'border-border bg-bg-muted': selectedNoteId() === note.id }"
>
<div class="truncate text-sm font-semibold text-text-main">{{ note.title }}</div>
<div class="truncate text-xs text-text-muted">{{ note.content.substring(0, 100) }}</div>
</li>
}
</ul>
} @else {
<p class="rounded-lg border border-dashed border-border px-3 py-3 text-sm text-text-muted">Aucun résultat pour cette recherche.</p>
}
</div>
@if (calendarSelectionLabel() || calendarSearchState() !== 'idle' || calendarResults().length > 0 || calendarSearchError()) {
<div class="space-y-3 rounded-xl border border-border bg-card p-3 shadow-subtle">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-semibold text-text-main">Résultats du calendrier</h3>
@if (calendarSelectionLabel(); as selectionLabel) {
<p class="text-xs text-text-muted">{{ selectionLabel }}</p>
}
</div>
<button
class="btn btn-sm btn-ghost"
(click)="clearCalendarResults()"
aria-label="Effacer les résultats du calendrier"
>
Effacer
</button>
</div>
@if (calendarSearchState() === 'loading') {
<div class="rounded-lg bg-bg-muted px-3 py-2 text-xs text-text-muted">Recherche en cours...</div>
} @else if (calendarSearchError(); as calError) {
<div class="rounded-lg border border-red-500/40 bg-red-500/10 px-3 py-2 text-xs text-red-500 dark:border-red-500/30 dark:bg-red-500/15 dark:text-red-400">{{ calError }}</div>
} @else if (calendarResults().length === 0) {
<div class="rounded-lg bg-bg-muted px-3 py-2 text-xs text-text-muted">Sélectionnez une date dans le calendrier pour voir les notes correspondantes.</div>
} @else {
<ul class="space-y-2">
@for (file of calendarResults(); track file.id) {
<li>
<button
(click)="selectNote(file.id)"
class="w-full rounded-lg border border-transparent bg-card px-3 py-2 text-left transition hover:border-border hover:bg-bg-muted"
>
<div class="truncate text-sm font-semibold text-text-main">{{ file.title }}</div>
<div class="mt-1 flex flex-wrap gap-2 text-xs text-text-muted">
<span>Créé&nbsp;: {{ file.createdAt | date:'mediumDate' }}</span>
<span>Modifié&nbsp;: {{ file.updatedAt | date:'mediumDate' }}</span>
</div>
</button>
</li>
}
</ul>
}
</div>
}
<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') {
@@ -363,6 +291,18 @@
</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"
@@ -440,6 +380,18 @@
</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"
@@ -506,6 +458,7 @@
[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
@@ -527,19 +480,29 @@
</app-graph-view-container-v2>
</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>
@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 {
<div class="flex h-full items-center justify-center">
<p class="text-text-muted">Sélectionnez une note pour commencer</p>
</div>
@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>