feat: add document properties popover with frontmatter metadata
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user