refactor: redesign editor buttons and improve scrollbar theming

This commit is contained in:
2025-10-21 22:32:54 -04:00
parent 0d0607577d
commit eeb957cf17
27 changed files with 739 additions and 148 deletions
@@ -168,6 +168,7 @@ export class NoteHeaderComponent implements AfterViewInit, OnDestroy {
const note = this.vaultService.getNoteById(this.noteId);
const props = this.frontmatterService.get(note);
compRef.instance.props = props;
compRef.instance.noteId = this.noteId;
compRef.instance.requestClose.subscribe(() => this.scheduleClose());
compRef.instance.cancelClose.subscribe(() => clearTimeout(this.closeTimer));
@@ -1,11 +1,12 @@
<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"
class="max-w-[420px] w-[min(92vw,420px)] p-5 text-[0.9375rem] leading-6 bg-card text-popover-foreground border border-border rounded-2xl shadow-2xl"
(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>
<h3 class="font-semibold text-xl mb-3">Propriétés du document</h3>
<div class="border-b border-border mb-4"></div>
<ng-container *ngIf="props as current; else emptyState">
<div class="space-y-4">
@@ -13,7 +14,7 @@
<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>
<dt class="text-muted-foreground whitespace-nowrap font-semibold">{{ row.label }}</dt>
<dd class="font-medium break-words">{{ row.value }}</dd>
</ng-container>
</dl>
@@ -22,26 +23,31 @@
<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>
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-semibold mb-1">Tags</h4>
<div class="flex flex-wrap gap-1.5">
<span *ngFor="let tag of current.tags" class="px-2.5 py-0.5 text-xs font-medium border border-border rounded-full bg-muted/70">{{ 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>
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-semibold 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>
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-semibold 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>
<button type="button"
class="focus:outline-none"
(click)="toggleState(st)"
title="Basculer l'état">
<app-state-chip [state]="st.key" [value]="st.value"></app-state-chip>
</button>
</ng-container>
</div>
</section>
@@ -49,10 +55,10 @@
<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>
<h4 class="text-xs uppercase tracking-wide text-muted-foreground font-semibold 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">
<div class="rounded-lg border border-border bg-card 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 }}
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StateChipComponent } from '../state-chip/state-chip.component';
import {
@@ -7,6 +7,8 @@ import {
NotePropertyStates,
NotePropertySummary,
} from '../../shared/note-properties.model';
import { VaultService } from '../../../../../services/vault.service';
import { FrontmatterPropertiesService } from '../../shared/frontmatter-properties.service';
@Component({
selector: 'app-properties-popover',
@@ -16,9 +18,13 @@ import {
})
export class PropertiesPopoverComponent {
@Input() props: NoteProperties | null = null;
@Input() noteId: string | null = null;
@Output() requestClose = new EventEmitter<void>();
@Output() cancelClose = new EventEmitter<void>();
private vault = inject(VaultService);
private frontmatter = inject(FrontmatterPropertiesService);
private readonly summaryConfig: Array<{
key: keyof NotePropertySummary;
label: string;
@@ -40,6 +46,8 @@ export class PropertiesPopoverComponent {
{ key: 'archive', label: 'Archivé' },
{ key: 'draft', label: 'Brouillon' },
{ key: 'private', label: 'Privé' },
{ key: 'template', label: 'Template' },
{ key: 'task', label: 'Tâche' },
];
get summaryRows(): Array<{ label: string; value: string }> {
@@ -69,12 +77,9 @@ export class PropertiesPopoverComponent {
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 stateEntries(): Array<{ key: keyof NotePropertyStates; value: boolean }> {
const states = this.props?.states || {};
return this.stateOrder.map(({ key }) => ({ key, value: states[key] ?? false }));
}
get additionalEntries(): NotePropertyEntry[] {
@@ -97,4 +102,14 @@ export class PropertiesPopoverComponent {
hasAdditional(): boolean {
return this.additionalEntries.length > 0;
}
async toggleState(entry: { key: keyof NotePropertyStates; value: boolean }): Promise<void> {
if (!this.noteId) return;
const next = !entry.value;
const ok = await this.vault.updateNoteStates(this.noteId, entry.key, next);
if (!ok) return;
// Refresh props from updated note
const note = this.vault.getNoteById(this.noteId);
this.props = this.frontmatter.get(note);
}
}
@@ -1,5 +1,5 @@
<span class="inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs"
[class.opacity-60]="value === false">
[ngClass]="chipClass">
<span class="w-4 h-4 text-current inline-flex items-center justify-center">
<!-- Minimal Lucide-like inline SVGs to avoid extra deps -->
<svg *ngIf="icon==='globe'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
@@ -9,6 +9,8 @@
<svg *ngIf="icon==='file'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/></svg>
<svg *ngIf="icon==='lock'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
<svg *ngIf="icon==='lock-open'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>
<svg *ngIf="icon==='template'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="6" rx="2"/><rect x="3" y="11" width="18" height="10" rx="2"/></svg>
<svg *ngIf="icon==='task'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="6" width="16" height="12" rx="2"/><path *ngIf="value" d="M8 12l3 3 5-5"/></svg>
</span>
<span class="capitalize">{{ label }}</span>
</span>
@@ -8,7 +8,7 @@ import { CommonModule } from '@angular/common';
templateUrl: './state-chip.component.html'
})
export class StateChipComponent {
@Input() state!: 'publish' | 'favoris' | 'archive' | 'draft' | 'private';
@Input() state!: 'publish' | 'favoris' | 'archive' | 'draft' | 'private' | 'template' | 'task';
@Input() value: boolean | null | undefined;
get label(): string {
@@ -18,14 +18,38 @@ export class StateChipComponent {
case 'archive': return this.value ? 'Archivé' : 'Non archivé';
case 'draft': return 'Brouillon';
case 'private': return 'Privé';
case 'template': return 'Template';
case 'task': return this.value ? 'Tâche' : 'Tâche';
}
}
get icon(): 'archive' | 'box' | 'globe' | 'heart' | 'file' | 'lock' | 'lock-open' {
get icon(): 'archive' | 'box' | 'globe' | 'heart' | 'file' | 'lock' | 'lock-open' | 'template' | 'task' {
if (this.state === 'archive') return this.value ? 'archive' : 'box';
if (this.state === 'publish') return 'globe';
if (this.state === 'favoris') return 'heart';
if (this.state === 'draft') return 'file';
if (this.state === 'template') return 'template';
if (this.state === 'task') return 'task';
return this.value ? 'lock' : 'lock-open';
}
get chipClass(): string {
if (this.value === false) return 'text-muted-foreground border-border bg-transparent';
switch (this.state) {
case 'publish':
return 'text-emerald-400 border-emerald-600/40 bg-emerald-500/10';
case 'favoris':
return 'text-rose-400 border-rose-600/40 bg-rose-500/10';
case 'archive':
return 'text-amber-400 border-amber-600/40 bg-amber-500/10';
case 'draft':
return 'text-sky-400 border-sky-600/40 bg-sky-500/10';
case 'private':
return 'text-violet-400 border-violet-600/40 bg-violet-500/10';
case 'template':
return 'text-amber-400 border-amber-600/40 bg-amber-500/10';
case 'task':
return 'text-sky-400 border-sky-600/40 bg-sky-500/10';
}
}
}
@@ -32,15 +32,20 @@ export class FrontmatterPropertiesService {
category: this.toStr(frontmatter, consumedRawKeys, ['catégorie', 'categorie', 'category']),
};
const tags = this.toArray(frontmatter, consumedRawKeys, ['tags', 'tag']) ?? [];
// Merge tags from frontmatter and from the parsed note content to ensure completeness
const fmTags = this.toArray(frontmatter, consumedRawKeys, ['tags', 'tag']) ?? [];
const noteTags = Array.isArray(note.tags) ? note.tags : [];
const tags = Array.from(new Set([...(noteTags ?? []), ...fmTags]));
const aliases = this.toArray(frontmatter, consumedRawKeys, ['aliases', 'alias']) ?? [];
const states: NotePropertyStates = {};
this.assignState(states, 'publish', frontmatter, consumedRawKeys, ['publish', 'publié']);
this.assignState(states, 'favoris', frontmatter, consumedRawKeys, ['favoris', 'favorite', 'favourite']);
this.assignState(states, 'archive', frontmatter, consumedRawKeys, ['archive', 'archived']);
this.assignState(states, 'publish', frontmatter, consumedRawKeys, ['publish', 'publié', 'publie', 'published']);
this.assignState(states, 'favoris', frontmatter, consumedRawKeys, ['favoris', 'favori', 'favorite', 'favourite', 'fav', 'star', 'starred']);
this.assignState(states, 'archive', frontmatter, consumedRawKeys, ['archive', 'archived', 'archivé', 'archivee']);
this.assignState(states, 'draft', frontmatter, consumedRawKeys, ['draft', 'brouillon']);
this.assignState(states, 'private', frontmatter, consumedRawKeys, ['private', 'privé', 'prive']);
this.assignState(states, 'private', frontmatter, consumedRawKeys, ['private', 'privé', 'prive', 'privée', 'privee']);
this.assignState(states, 'template', frontmatter, consumedRawKeys, ['template', 'modèle', 'modele']);
this.assignState(states, 'task', frontmatter, consumedRawKeys, ['task', 'tâche', 'tache']);
const additional = this.buildAdditionalEntries(frontmatter, consumedRawKeys);
@@ -20,6 +20,8 @@ export interface NotePropertyStates {
archive?: boolean;
draft?: boolean;
private?: boolean;
template?: boolean;
task?: boolean;
}
export interface NotePropertyEntry {