feat: add document properties popover with frontmatter metadata

This commit is contained in:
2025-10-21 11:41:39 -04:00
parent d788c9d267
commit 6e50acbd22
17 changed files with 853 additions and 339 deletions
@@ -0,0 +1,71 @@
<section
id="note-props-popover"
class="max-w-[420px] w-[min(92vw,420px)] p-4 text-sm leading-5 bg-popover text-popover-foreground border border-border rounded-2xl shadow-xl backdrop-blur"
(mouseenter)="cancelClose.emit()"
(mouseleave)="requestClose.emit()"
role="dialog"
aria-label="Propriétés du document">
<h3 class="font-semibold mb-3">Propriétés du document</h3>
<ng-container *ngIf="props as current; else emptyState">
<div class="space-y-4">
<ng-container *ngIf="hasSummary">
<section class="not-prose">
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5">
<ng-container *ngFor="let row of summaryRows">
<dt class="text-muted-foreground whitespace-nowrap">{{ row.label }}</dt>
<dd class="font-medium break-words">{{ row.value }}</dd>
</ng-container>
</dl>
</section>
</ng-container>
<ng-container *ngIf="current.tags.length">
<section class="not-prose">
<h4 class="text-xs uppercase tracking-wide text-muted-foreground mb-1">Tags</h4>
<div class="flex flex-wrap gap-1">
<span *ngFor="let tag of current.tags" class="px-2 py-0.5 text-xs border rounded-full bg-muted/40">{{ tag }}</span>
</div>
</section>
</ng-container>
<ng-container *ngIf="current.aliases.length">
<section class="not-prose">
<h4 class="text-xs uppercase tracking-wide text-muted-foreground mb-1">Aliases</h4>
<p class="text-sm leading-5 text-muted-foreground">{{ current.aliases.join(' · ') }}</p>
</section>
</ng-container>
<ng-container *ngIf="hasStates()">
<section class="not-prose">
<h4 class="text-xs uppercase tracking-wide text-muted-foreground mb-1">États</h4>
<div class="flex flex-wrap gap-2">
<ng-container *ngFor="let st of stateEntries">
<app-state-chip [state]="st.key" [value]="st.value"></app-state-chip>
</ng-container>
</div>
</section>
</ng-container>
<ng-container *ngIf="hasAdditional()">
<section class="not-prose">
<h4 class="text-xs uppercase tracking-wide text-muted-foreground mb-2">Autres propriétés</h4>
<dl class="space-y-2">
<ng-container *ngFor="let entry of additionalEntries">
<div class="rounded-lg border border-border/60 bg-card/60 px-3 py-2">
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wide">{{ entry.label }}</dt>
<dd class="text-sm leading-5 mt-1" [class.whitespace-pre-wrap]="entry.multiline">
{{ entry.displayValue }}
</dd>
</div>
</ng-container>
</dl>
</section>
</ng-container>
</div>
</ng-container>
<ng-template #emptyState>
<p class="text-muted-foreground">Aucune propriété détectée.</p>
</ng-template>
</section>
@@ -0,0 +1,100 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StateChipComponent } from '../state-chip/state-chip.component';
import {
NoteProperties,
NotePropertyEntry,
NotePropertyStates,
NotePropertySummary,
} from '../../shared/note-properties.model';
@Component({
selector: 'app-properties-popover',
standalone: true,
imports: [CommonModule, StateChipComponent],
templateUrl: './properties-popover.component.html'
})
export class PropertiesPopoverComponent {
@Input() props: NoteProperties | null = null;
@Output() requestClose = new EventEmitter<void>();
@Output() cancelClose = new EventEmitter<void>();
private readonly summaryConfig: Array<{
key: keyof NotePropertySummary;
label: string;
type: 'text' | 'date';
}> = [
{ key: 'title', label: 'Titre', type: 'text' },
{ key: 'author', label: 'Auteur', type: 'text' },
{ key: 'creationDate', label: 'Créé le', type: 'date' },
{ key: 'modificationDate', label: 'Modifié le', type: 'date' },
{ key: 'category', label: 'Catégorie', type: 'text' },
];
private readonly stateOrder: Array<{
key: keyof NotePropertyStates;
label: string;
}> = [
{ key: 'publish', label: 'Publié' },
{ key: 'favoris', label: 'Favori' },
{ key: 'archive', label: 'Archivé' },
{ key: 'draft', label: 'Brouillon' },
{ key: 'private', label: 'Privé' },
];
get summaryRows(): Array<{ label: string; value: string }> {
const summary = this.props?.summary;
if (!summary) return [];
const rows: Array<{ label: string; value: string }> = [];
for (const item of this.summaryConfig) {
const rawValue = summary[item.key];
if (!rawValue) continue;
rows.push({
label: item.label,
value: item.type === 'date' ? this.formatDate(rawValue) : rawValue,
});
}
return rows;
}
get hasSummary(): boolean {
return this.summaryRows.length > 0;
}
get tags(): string[] {
return this.props?.tags ?? [];
}
get aliases(): string[] {
return this.props?.aliases ?? [];
}
get stateEntries(): Array<{ key: keyof NotePropertyStates; value: boolean | undefined }> {
const states = this.props?.states;
if (!states) return [];
return this.stateOrder
.map(({ key }) => ({ key, value: states[key] }))
.filter(entry => entry.value !== undefined);
}
get additionalEntries(): NotePropertyEntry[] {
return this.props?.additional ?? [];
}
formatDate(value?: string | null): string {
if (!value) return '';
const date = new Date(value);
if (isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium', timeStyle: 'short'
}).format(date);
}
hasStates(): boolean {
return this.stateEntries.length > 0;
}
hasAdditional(): boolean {
return this.additionalEntries.length > 0;
}
}