feat: implement new tag management system with filtering and modern UI overlay

This commit is contained in:
Bruno Charest 2025-10-20 11:07:07 -04:00
parent 168fcaf049
commit 3c716fab58
28 changed files with 596 additions and 27 deletions

View File

@ -65,6 +65,38 @@ ObsiViewer est une application web **Angular 20** moderne et performante qui per
- **Keyboard shortcuts** : Raccourcis clavier (Alt+R, Alt+D)
- **Animations fluides** : 60fps avec optimisations performance
---
## 🧭 Migration UI — Tags (nouveau gestionnaire)
Depuis cette version, la gestion des tags a été refondue pour une UX claire et performante.
- **Lecture**: un bouton icône Tag situé à gauche des chips ouvre/ferme l'éditeur. Cliquer un chip applique un filtre sur `NotesList`.
- **Édition**: l'éditeur est un overlay moderne avec dé-dupe forte, suggestions et raccourcis (Enter/Tab/Backspace). Aucune fermeture par clic extérieur ou touche ESC.
### Intégration
- Composant lecture/commande: `src/app/shared/tags/tag-manager/tag-manager.component.ts`
- Overlay édition: `src/app/shared/tags/tag-editor-overlay/`
- Store de filtre: `src/app/core/stores/tag-filter.store.ts`
- Utilitaires: `src/app/shared/tags/tag-utils.ts`
### API composants
- `TagManagerComponent`
- `@Input() tags: string[]`
- `@Input() noteId: string`
- `@Output() tagSelected(tag: string)` (lecture)
- `@Output() editingChanged(isEditing: boolean)`
- `@Output() saved(tags: string[])` (après Enregistrer)
### Comportements clés
- Lédition ne se déclenche QUE via licône Tag.
- En lecture, cliquer un chip: met à jour `TagFilterStore` et filtre la liste des notes. Un badge “Filtre: #tag” apparaît avec action “Effacer le filtre”.
- Sauvegarde des tags via `VaultService.updateNoteTags(noteId, tags)` qui réécrit proprement le frontmatter `tags:`.
### Tests
- `tag-utils.spec.ts` couvre `normalizeTag` et `uniqueTags`.
- `tag-manager.component.spec.ts` vérifie lémission de `tagSelected`.
### ✏️ Dessins Excalidraw
- **Éditeur intégré** : Ouvrez et modifiez des fichiers `.excalidraw` directement dans l'app
- **Création rapide** : Bouton "Nouveau dessin" dans l'en-tête (icône +)

View File

@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class TagFilterStore {
private _tag = new BehaviorSubject<string | null>(null);
readonly selectedTag$: Observable<string | null> = this._tag.asObservable();
set(tag: string | null): void {
this._tag.next(tag);
}
get(): string | null {
return this._tag.getValue();
}
}

View File

@ -1,8 +1,9 @@
import { Component, EventEmitter, Output, computed, signal, effect } from '@angular/core';
import { Component, EventEmitter, Output, computed, signal, effect, inject } from '@angular/core';
import { input } from '@angular/core';
import { CommonModule } from '@angular/common';
import type { Note } from '../../../types';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import { TagFilterStore } from '../../core/stores/tag-filter.store';
@Component({
selector: 'app-notes-list',
@ -10,7 +11,16 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
imports: [CommonModule, ScrollableOverlayDirective],
template: `
<div class="h-full flex flex-col">
<div class="p-2 border-b border-gray-200 dark:border-gray-800">
<div class="p-2 border-b border-gray-200 dark:border-gray-800 space-y-2">
<div *ngIf="activeTag() as t" class="flex items-center gap-2 text-xs">
<span class="inline-flex items-center gap-1 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 px-2 py-1">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
Filtre: #{{ t }}
</span>
<button type="button" (click)="clearTagFilter()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<input type="text"
[value]="query()"
(input)="onQuery($any($event.target).value)"
@ -55,15 +65,26 @@ export class NotesListComponent {
@Output() openNote = new EventEmitter<string>();
@Output() queryChange = new EventEmitter<string>();
private store = inject(TagFilterStore);
private q = signal('');
activeTag = signal<string | null>(null);
private syncQuery = effect(() => {
this.q.set(this.query() || '');
});
private syncTagFromStore = effect(() => {
// Prefer explicit input; otherwise, take store value
const inputTag = this.tagFilter();
if (inputTag !== null && inputTag !== undefined) {
this.activeTag.set(inputTag || null);
return;
}
this.activeTag.set(this.store.get());
});
filtered = computed(() => {
const q = (this.q() || '').toLowerCase().trim();
const folder = (this.folderFilter() || '').toLowerCase().replace(/^\/+|\/+$/g, '');
const tag = (this.tagFilter() || '').toLowerCase();
const tag = (this.activeTag() || '').toLowerCase();
const quickLink = this.quickLinkFilter();
let list = this.notes();
@ -113,4 +134,12 @@ export class NotesListComponent {
this.q.set(v);
this.queryChange.emit(v);
}
clearTagFilter(): void {
// Clear both local input state and store
this.activeTag.set(null);
if (this.tagFilter() == null) {
this.store.set(null);
}
}
}

View File

@ -19,9 +19,9 @@
</div>
</div>
<app-tags-editor class="note-header__tags"
<app-tag-manager class="note-header__tags"
[noteId]="noteId"
[tags]="tags"
(tagsUpdated)="tagsChange.emit($event)"
></app-tags-editor>
(saved)="tagsChange.emit($event)"
></app-tag-manager>
</header>

View File

@ -2,12 +2,12 @@ import { AfterViewInit, Component, ElementRef, Input, OnDestroy, Output, EventEm
import { CommonModule } from '@angular/common';
import { debounceTime, Subject } from 'rxjs';
import { splitPathKeepFilename } from '../../../../shared/utils/path';
import { TagsEditorComponent } from '../../../../shared/tags-editor/tags-editor.component';
import { TagManagerComponent } from '../../../../shared/tags/tag-manager/tag-manager.component';
@Component({
selector: 'app-note-header',
standalone: true,
imports: [CommonModule, TagsEditorComponent],
imports: [CommonModule, TagManagerComponent],
templateUrl: './note-header.component.html',
styleUrls: ['./note-header.component.scss']
})

View File

@ -32,7 +32,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
[noteHtmlContent]="renderedNoteContent"
[allNotes]="vault.allNotes()"
(noteLinkClicked)="noteSelected.emit($event)"
(tagClicked)="tagClicked.emit($event)"
(tagClicked)="onTagSelected($event)"
(wikiLinkActivated)="wikiLinkActivated.emit($event)"
[fullScreenActive]="noteFullScreen"
(fullScreenRequested)="toggleNoteFullScreen()"
@ -127,7 +127,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
[noteHtmlContent]="renderedNoteContent"
[allNotes]="vault.allNotes()"
(noteLinkClicked)="noteSelected.emit($event)"
(tagClicked)="tagClicked.emit($event)"
(tagClicked)="onTagSelected($event)"
(wikiLinkActivated)="wikiLinkActivated.emit($event)"
[fullScreenActive]="noteFullScreen"
(fullScreenRequested)="toggleNoteFullScreen()"
@ -165,7 +165,7 @@ import { MarkdownPlaygroundComponent } from '../../features/tests/markdown-playg
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
</div>
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="onTagSelected($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" [fullScreenActive]="noteFullScreen" (fullScreenRequested)="toggleNoteFullScreen()" (legacyRequested)="ui.toggleUIMode()" (showToc)="mobileNav.toggleToc()" (directoryClicked)="onFolderSelected($event)" [tocOpen]="mobileNav.tocOpen()"></app-note-viewer>
</div>
</div>
</div>
@ -398,10 +398,20 @@ export class AppShellNimbusLayoutComponent {
}
onTagSelected(tagName: string) {
const norm = (tagName || '').replace(/^#/, '').trim();
const norm = (tagName || '').replace(/^#/, '').trim().toLowerCase();
if (!norm) return;
this.tagFilter = norm;
this.folderFilter = null; // clear folder when focusing tag
// Clear other filters and search to focus on tag results
this.quickLinkFilter = null;
this.listQuery = '';
// Ensure the list is visible: exit fullscreen if active
if (this.noteFullScreen) {
this.noteFullScreen = false;
document.body.classList.remove('note-fullscreen-active');
}
// Bubble up for global handlers (keeps parity with right sidebar tags)
this.tagClicked.emit(norm);
if (!this.responsive.isDesktop()) {
this.mobileNav.setActiveTab('list');
}

View File

@ -0,0 +1,2 @@
/* Overlay styling relies on Tailwind utilities in the template. Keep minimal spacing fixes here if needed. */
:host { display: contents; }

View File

@ -0,0 +1,50 @@
<div class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/40"></div>
<div class="relative rounded-2xl shadow-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-4 md:p-5 w-[min(780px,92vw)]">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
<h3 class="text-base font-semibold">Éditer les tags</h3>
<span class="text-sm text-slate-500">({{ count() }})</span>
</div>
<button type="button" class="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10" (click)="close.emit()" aria-label="Fermer">
<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="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<div class="flex flex-col gap-3">
<div class="flex flex-wrap gap-2 rounded-xl border border-slate-200 dark:border-slate-700 p-3 min-h-[44px]">
@for (t of working(); track t) {
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium bg-slate-100 dark:bg-slate-800">
{{ t }}
<button type="button" class="w-6 h-6 inline-flex items-center justify-center rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10" (click)="removeTag(t)" aria-label="Retirer">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</span>
}
<input data-tag-input type="text" [value]="inputValue()" (input)="inputValue.set($any($event.target).value)" (keydown)="onKeydown($event)" placeholder="Ajouter un tag..." class="min-w-[200px] flex-1 bg-transparent outline-none px-2 py-1.5" />
</div>
<div class="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
<div class="max-h-64 overflow-y-auto">
@if (suggestions().length === 0) {
<div class="px-3 py-2 text-sm text-slate-500">Aucune suggestion</div>
} @else {
<ul>
@for (s of suggestions(); track s) {
<li>
<button type="button" class="w-full text-left px-3 py-2 hover:bg-slate-50 dark:hover:bg-slate-800" (click)="pickSuggestion(s)">{{ s }}</button>
</li>
}
</ul>
}
</div>
</div>
<div class="flex items-center justify-end gap-2 pt-1">
<button type="button" class="px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800" (click)="close.emit()">Annuler</button>
<button type="button" class="px-3 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-60" [disabled]="saving()" (click)="save()">Enregistrer</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,114 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, inject, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { uniqueTags, normalizeTag } from '../tag-utils';
import { VaultService } from '../../../../services/vault.service';
import { ToastService } from '../../toast/toast.service';
@Component({
selector: 'app-tag-editor-overlay',
standalone: true,
imports: [CommonModule],
templateUrl: './tag-editor-overlay.component.html',
styleUrls: ['./tag-editor-overlay.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TagEditorOverlayComponent {
private vault = inject(VaultService);
private toast = inject(ToastService);
@Input() value: string[] = [];
@Input() allTags: string[] = [];
@Input() noteId = '';
@Output() saved = new EventEmitter<string[]>();
@Output() close = new EventEmitter<void>();
inputValue = signal('');
saving = signal(false);
working = signal<string[]>([]);
count = computed(() => this.working().length);
ngOnInit() {
this.working.set(uniqueTags(this.value || []));
}
private allKnownTags = computed(() => {
const provided = this.allTags || [];
if (provided.length > 0) return provided;
try {
return this.vault.tags().map(t => t.name);
} catch {
return [];
}
});
suggestions = computed(() => {
const q = this.inputValue().trim().toLowerCase();
const source = this.allKnownTags();
const pool = q ? source.filter(t => t.toLowerCase().includes(q)) : source;
const exist = new Set(this.working().map(t => t.toLowerCase()));
return pool.filter(t => !exist.has(t.toLowerCase())).slice(0, 50);
});
onKeydown(ev: KeyboardEvent) {
if (ev.key === 'Enter') {
ev.preventDefault();
this.addFromInput();
} else if (ev.key === 'Tab') {
const s = this.suggestions();
if (s.length) {
ev.preventDefault();
this.addTag(s[0]);
}
} else if (ev.key === 'Backspace' && !this.inputValue()) {
ev.preventDefault();
const cur = this.working();
if (cur.length) this.removeTag(cur[cur.length - 1]);
}
}
addFromInput() {
const raw = this.inputValue().trim();
if (!raw) return;
this.addTag(raw);
}
addTag(raw: string) {
const t = normalizeTag(raw);
if (!t) return;
if (this.working().some(x => x.toLowerCase() === t.toLowerCase())) return;
this.working.update(arr => [...arr, t]);
this.inputValue.set('');
}
pickSuggestion(s: string) {
this.addTag(s);
}
removeTag(tag: string) {
this.working.update(arr => arr.filter(t => t.toLowerCase() !== tag.toLowerCase()));
}
async save() {
const tags = uniqueTags(this.working());
if (!this.noteId) {
this.saved.emit(tags);
this.close.emit();
return;
}
this.saving.set(true);
try {
const ok = await this.vault.updateNoteTags(this.noteId, tags);
if (ok) {
this.toast.success('✅ Tags mis à jour');
this.saved.emit(tags);
} else {
this.toast.error('❌ Échec de la mise à jour des tags');
}
} catch {
this.toast.error('❌ Échec de la mise à jour des tags');
} finally {
this.saving.set(false);
}
}
}

View File

@ -0,0 +1 @@
/* Styles are provided via Tailwind utility classes directly in templates. */

View File

@ -0,0 +1,31 @@
<div class="flex items-center gap-2">
<button
type="button"
class="inline-flex items-center justify-center w-9 h-9 md:w-9 md:h-9 sm:w-8 sm:h-8 rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10"
aria-label="Modifier les tags"
[attr.aria-pressed]="isEditing()"
(click)="toggleEditor()"
title="Modifier les tags">
<svg class="text-xl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
</button>
<div class="flex flex-wrap items-center gap-1">
<button
type="button"
class="px-3 py-1.5 rounded-full bg-slate-700/10 dark:bg-slate-300/10 text-sm font-medium hover:bg-slate-500/10 dark:hover:bg-slate-200/10"
*ngFor="let tag of normalizedTags()"
(click)="onChipClick(tag)"
[title]="'Voir les notes #'+tag">
{{ tag }}
</button>
</div>
</div>
<app-tag-editor-overlay
*ngIf="isEditing()"
[value]="normalizedTags()"
[noteId]="noteId"
[allTags]="allTags || []"
(close)="onClose()"
(saved)="onSaved($event)"
></app-tag-editor-overlay>

View File

@ -0,0 +1,37 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TagManagerComponent } from './tag-manager.component';
describe('TagManagerComponent', () => {
let component: TagManagerComponent;
let fixture: ComponentFixture<TagManagerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TagManagerComponent],
}).compileComponents();
fixture = TestBed.createComponent(TagManagerComponent);
component = fixture.componentInstance;
component.tags = ['tag1', 'tag2'];
fixture.detectChanges();
});
it('emits tagSelected when chip clicked in read mode', () => {
const spy = jasmine.createSpy('tagSelected');
component.tagSelected.subscribe(spy);
component.onChipClick('tag1');
expect(spy).toHaveBeenCalledWith('tag1');
});
it('does not emit when editing', () => {
const spy = jasmine.createSpy('tagSelected');
component.tagSelected.subscribe(spy);
component.toggleEditor();
component.onChipClick('tag1');
expect(spy).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,59 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, inject, signal, computed } from '@angular/core';
import { CommonModule, NgFor, NgIf } from '@angular/common';
import { TagEditorOverlayComponent } from '../tag-editor-overlay/tag-editor-overlay.component';
import { TagFilterStore } from '../../../core/stores/tag-filter.store';
import { uniqueTags } from '../tag-utils';
@Component({
selector: 'app-tag-manager',
standalone: true,
imports: [CommonModule, NgFor, NgIf, TagEditorOverlayComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './tag-manager.component.html',
styleUrls: ['./tag-manager.component.css']
})
export class TagManagerComponent {
private store = inject(TagFilterStore);
private readonly tagsSignal = signal<string[]>([]);
@Input() set tags(value: string[] | null | undefined) {
this.tagsSignal.set(value ? [...value] : []);
}
get tags(): string[] {
return this.tagsSignal();
}
@Input() allTags: string[] | null = null; // optional preloaded
@Input() noteId = '';
@Output() tagSelected = new EventEmitter<string>();
@Output() editingChanged = new EventEmitter<boolean>();
@Output() saved = new EventEmitter<string[]>();
isEditing = signal(false);
readonly normalizedTags = computed(() => uniqueTags(this.tagsSignal()));
toggleEditor(): void {
const next = !this.isEditing();
this.isEditing.set(next);
this.editingChanged.emit(next);
}
onChipClick(tag: string): void {
if (this.isEditing()) return; // read-mode only
this.store.set(tag);
this.tagSelected.emit(tag);
}
onClose(): void {
if (!this.isEditing()) return;
this.isEditing.set(false);
this.editingChanged.emit(false);
}
onSaved(next: string[]): void {
this.saved.emit(next);
this.tagsSignal.set(next);
this.isEditing.set(false);
this.editingChanged.emit(false);
}
}

View File

@ -0,0 +1,13 @@
import { uniqueTags, normalizeTag } from './tag-utils';
describe('tag-utils', () => {
it('normalizeTag trims, replaces spaces with underscore, strips invalids, lowercases', () => {
expect(normalizeTag(' Hello World ')).toBe('hello_world');
expect(normalizeTag('A/B C')).toBe('ab_c');
expect(normalizeTag('Déjà Vu!')).toBe('dj_vu');
});
it('uniqueTags removes duplicates case-insensitive and empties', () => {
expect(uniqueTags(['Tag', 'tag', ' other ', 'OTHER', '', ' '])).toEqual(['tag', 'other']);
});
});

View File

@ -0,0 +1,19 @@
export const normalizeTag = (s: string) =>
(s ?? '')
.trim()
.replace(/\s+/g, '_')
.replace(/[^\w-]/g, '')
.toLowerCase();
export const uniqueTags = (arr: string[]) => {
const seen = new Set<string>();
const out: string[] = [];
for (const raw of arr ?? []) {
const t = normalizeTag(String(raw ?? ''));
if (t && !seen.has(t)) {
seen.add(t);
out.push(t);
}
}
return out;
};

View File

@ -17,17 +17,19 @@ import { CommonModule } from '@angular/common';
})
export class BadgeCountComponent {
@Input() count = 0;
@Input() color: 'slate'|'rose'|'amber'|'indigo'|'emerald'|'stone'|'zinc' = 'slate';
@Input() color: 'slate'|'rose'|'amber'|'indigo'|'emerald'|'stone'|'zinc'|'green'|'purple' = 'slate';
get bgClass() {
return {
'bg-slate-600': this.color === 'slate',
'bg-rose-600': this.color === 'rose',
'bg-amber-600': this.color === 'amber',
'bg-sky-400': this.color === 'amber',
'bg-indigo-600': this.color === 'indigo',
'bg-emerald-600': this.color === 'emerald',
'bg-stone-600': this.color === 'stone',
'bg-zinc-700': this.color === 'zinc',
'bg-green-500': this.color === 'green',
'bg-purple-500': this.color === 'purple',
};
}
}

View File

@ -4,7 +4,10 @@ auteur: Bruno Charest
creation_date: 2025-10-19T11:13:12-04:00
modification_date: 2025-10-19T12:09:46-04:00
catégorie: ""
tags: []
tags:
- configuration
- bruno
- markdown
aliases: []
status: en-cours
publish: false

View File

@ -1,3 +1,23 @@
---
titre: old-note-2
auteur: Bruno Charest
creation_date: 2025-10-19T11:13:12-04:00
modification_date: 2025-10-19T12:09:46-04:00
catégorie: ""
aliases: []
status: en-cours
publish: false
favoris: false
template: false
task: false
archive: false
draft: false
private: false
tags:
- configuration
- bruno
- markdown
---
# Old Note 2
This note is in a subfolder of trash.

View File

@ -3,9 +3,13 @@ titre: Nouveau-markdown
auteur: Bruno Charest
creation_date: 2025-10-19T21:42:53-04:00
modification_date: 2025-10-19T21:43:06-04:00
catégorie: ""
tags: []
aliases: []
catégorie: markdown
tags:
- test
- Bruno
- markdown
aliases:
- nouveau
status: en-cours
publish: true
favoris: true
@ -15,3 +19,25 @@ archive: true
draft: true
private: true
---
# Nouveau-markdown
#tag1 #tag2 #tag3
## sous-titre
## sous-titre 2
## sous-titre 3
## sous-titre 4
## sous-titre 5
## sous-titre 6
## sous-titre 7
## sous-titre 8

View File

@ -0,0 +1,28 @@
---
titre: Nouveau-markdown
auteur: Bruno Charest
creation_date: 2025-10-19T21:42:53-04:00
modification_date: 2025-10-19T21:43:06-04:00
catégorie: markdown
tags:
- test
- Bruno
- markdown
aliases:
- nouveau
status: en-cours
publish: true
favoris: true
template: true
task: true
archive: true
draft: true
private: true
---
# Nouveau-markdown
#tag1 #tag2 #tag3
## sous-titre

View File

@ -4,7 +4,9 @@ auteur: Bruno Charest
creation_date: 2025-10-19T12:15:21-04:00
modification_date: 2025-10-19T12:15:21-04:00
catégorie: ""
tags: []
tags:
- home
- accueil
aliases: []
status: en-cours
publish: false

View File

@ -0,0 +1,19 @@
---
titre: test-new-file
auteur: Bruno Charest
creation_date: 2025-10-19T12:15:21-04:00
modification_date: 2025-10-19T12:15:21-04:00
catégorie: ""
aliases: []
status: en-cours
publish: false
favoris: false
template: false
task: false
archive: false
draft: false
private: false
tags:
- home
- accueil
---

View File

@ -4,7 +4,10 @@ auteur: Bruno Charest
creation_date: 2025-10-02T16:10:42-04:00
modification_date: 2025-10-19T12:09:47-04:00
catégorie: ""
tags: []
tags:
- accueil
- markdown
- bruno
aliases: []
status: en-cours
publish: false

View File

@ -1,4 +1,22 @@
---
titre: test2
auteur: Bruno Charest
creation_date: 2025-10-02T16:10:42-04:00
modification_date: 2025-10-19T12:09:47-04:00
catégorie: ""
aliases: []
status: en-cours
publish: false
favoris: true
template: false
task: false
archive: false
draft: false
private: false
tag: testTag
tags:
- accueil
- markdown
- bruno
---
Ceci est la page 1

View File

@ -5,9 +5,11 @@ creation_date: 2025-09-25T07:45:20-04:00
modification_date: 2025-10-19T12:09:47-04:00
catégorie: ""
tags:
- tag_metadata_1
- tag_metadata_2
- tag_metadata_test
- tag1
- tag2
- test
- test2
- home
aliases: []
status: en-cours
publish: false

View File

@ -1,9 +1,19 @@
---
titre: test
auteur: Bruno Charest
creation_date: 2025-09-25T07:45:20-04:00
modification_date: 2025-10-19T12:09:47-04:00
catégorie: ""
aliases: []
status: en-cours
publish: false
favoris: false
template: false
task: false
archive: false
draft: false
private: false
title: Page de test Markdown
tags:
- tag_metadata_1
- tag_metadata_2
- tag_metadata_test
created: 2025-09-25T21:20:45-04:00
modified: 2025-09-25T21:20:45-04:00
category: test
@ -14,6 +24,12 @@ number: 12345
todo: false
url: https://google.com
image: https://images.unsplash.com/photo-1675789652575-0a5d2425b6c2?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80
tags:
- tag1
- tag2
- test
- test2
- home
---
#tag1 #tag2 #test #test2

17
vault/totoTata.md Normal file
View File

@ -0,0 +1,17 @@
---
titre: totoTata
auteur: Bruno Charest
creation_date: 2025-10-20T10:11:39-04:00
modification_date: 2025-10-20T10:11:39-04:00
catégorie: ""
tags: []
aliases: []
status: en-cours
publish: false
favoris: true
template: false
task: true
archive: false
draft: false
private: false
---

0
vault/totoTata.md.bak Normal file
View File