refactor: redesign editor buttons and improve scrollbar theming
This commit is contained in:
@@ -54,42 +54,42 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
|
||||
<!-- Save Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-solid btn-sm"
|
||||
class="editor-btn editor-btn--primary"
|
||||
[disabled]="isSaving()"
|
||||
(click)="save()"
|
||||
[attr.aria-label]="'Save (Ctrl+S)'">
|
||||
<svg *ngIf="!isSaving()" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg *ngIf="!isSaving()" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="editor-btn__icon">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||||
<polyline points="7 3 7 8 15 8"/>
|
||||
</svg>
|
||||
<svg *ngIf="isSaving()" class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg *ngIf="isSaving()" class="animate-spin editor-btn__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" stroke-dasharray="60" stroke-dashoffset="15"/>
|
||||
</svg>
|
||||
<span class="ml-1 hidden sm:inline">{{ isSaving() ? 'Saving...' : 'Save' }}</span>
|
||||
<span class="editor-btn__label hidden sm:inline">{{ isSaving() ? 'Saving...' : 'Save' }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Wrap Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="editor-btn editor-btn--neutral"
|
||||
(click)="toggleWordWrap()"
|
||||
[attr.aria-label]="'Toggle word wrap'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="editor-btn__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 7 4 4 20 4 20 7"/>
|
||||
<line x1="9" y1="20" x2="15" y2="20"/>
|
||||
<line x1="12" y1="4" x2="12" y2="20"/>
|
||||
</svg>
|
||||
<span class="ml-1 hidden md:inline">Wrap</span>
|
||||
<span class="editor-btn__label hidden md:inline">Wrap</span>
|
||||
</button>
|
||||
|
||||
<!-- Undo -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm hidden sm:flex"
|
||||
class="editor-btn editor-btn--ghost editor-btn--icon hidden sm:inline-flex"
|
||||
(click)="undo()"
|
||||
[attr.aria-label]="'Undo (Ctrl+Z)'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="editor-btn__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"/>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
||||
</svg>
|
||||
@@ -98,10 +98,10 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
|
||||
<!-- Redo -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm hidden sm:flex"
|
||||
class="editor-btn editor-btn--ghost editor-btn--icon hidden sm:inline-flex"
|
||||
(click)="redo()"
|
||||
[attr.aria-label]="'Redo (Ctrl+Y)'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="editor-btn__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||
</svg>
|
||||
@@ -110,14 +110,14 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
|
||||
<!-- Close/Cancel -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="editor-btn editor-btn--neutral"
|
||||
(click)="close()"
|
||||
[attr.aria-label]="'Close editor (Esc)'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="editor-btn__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
<span class="ml-1 hidden sm:inline">Close</span>
|
||||
<span class="editor-btn__label hidden sm:inline">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +171,108 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
.editor-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 6px 14px -12px color-mix(in oklab, var(--brand) 70%, transparent);
|
||||
}
|
||||
|
||||
.editor-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--brand) 35%, transparent);
|
||||
}
|
||||
|
||||
.editor-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.editor-btn__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.editor-btn__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-btn--icon {
|
||||
padding: 0.4rem;
|
||||
border-radius: 9999px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.editor-btn--primary {
|
||||
color: #0f172a;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand) 55%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-700) 45%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 40%, transparent);
|
||||
}
|
||||
|
||||
.editor-btn--primary:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 22px -12px color-mix(in oklab, var(--brand) 80%, transparent);
|
||||
}
|
||||
|
||||
.editor-btn--neutral {
|
||||
color: inherit;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--surface-1, #e5e7eb) 65%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface-2, #d1d5db) 55%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #cbd5e1) 55%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.editor-btn--neutral:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 18px -14px color-mix(in oklab, var(--border, #cbd5e1) 65%, transparent);
|
||||
}
|
||||
|
||||
.editor-btn--ghost {
|
||||
color: var(--brand-500, var(--btn-bg));
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.editor-btn--ghost:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--brand) 10%, transparent);
|
||||
}
|
||||
|
||||
:host-context(.dark) .editor-btn--primary {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand-700) 55%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-900, #1e1b4b) 55%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 45%, transparent);
|
||||
}
|
||||
|
||||
:host-context(.dark) .editor-btn--neutral {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--card, #111827) 65%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface2, #0b1220) 55%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #334155) 60%, transparent);
|
||||
}
|
||||
|
||||
:host-context(.dark) .editor-btn--ghost {
|
||||
color: color-mix(in oklab, var(--brand-200, #c7d2fe) 75%, #93c5fd 25%);
|
||||
}
|
||||
|
||||
.editor-btn--ghost.editor-btn--icon:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.markdown-editor__container {
|
||||
flex: 1;
|
||||
|
||||
@@ -30,8 +30,20 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
|
||||
aria-modal="true"
|
||||
(click)="$event.stopPropagation()">
|
||||
|
||||
<div class="flex items-center justify-center py-3 sm:py-2">
|
||||
<div class="h-1.5 w-12 rounded-full bg-muted dark:bg-surface2"></div>
|
||||
<div class="flex items-center justify-between px-4 pt-3 pb-2 sm:pt-3 sm:pb-2">
|
||||
<div class="flex-1 flex justify-center">
|
||||
<div class="h-1.5 w-12 rounded-full bg-muted dark:bg-surface2"></div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-3 inline-flex items-center justify-center w-9 h-9 rounded-full bg-transparent hover:bg-surface1 dark:hover:bg-card transition"
|
||||
aria-label="Fermer le sommaire"
|
||||
(click)="close.emit()">
|
||||
<svg class="w-4 h-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="px-4 pb-4 overflow-y-auto max-h-[75vh] sm:max-h-[70vh]" appScrollableOverlay>
|
||||
|
||||
@@ -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));
|
||||
|
||||
+17
-11
@@ -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 }}
|
||||
|
||||
+22
-7
@@ -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 {
|
||||
|
||||
@@ -79,7 +79,7 @@ import { VaultService } from '../../../services/vault.service';
|
||||
<div *ngIf="open.tags" class="px-2 py-2">
|
||||
<ul class="space-y-0.5 text-sm">
|
||||
<li *ngFor="let t of tags" class="flex items-center gap-2">
|
||||
<button (click)="tagSelected.emit(t.name)" class="flex-1 text-left px-2 py-1 rounded hover:bg-surface1 dark:hover:bg-card truncate">
|
||||
<button (click)="tagSelected.emit(t.name)" class="flex-1 text-left px-2.5 py-1.5 rounded-lg transition-colors hover:bg-slate-500/10 dark:hover:bg-surface2/15 truncate">
|
||||
<span>🏷️</span>
|
||||
<span class="ml-1">{{ t.name }}</span>
|
||||
</button>
|
||||
|
||||
@@ -142,11 +142,32 @@ import { ParametersPage } from '../../features/parameters/parameters.page';
|
||||
></app-note-viewer>
|
||||
</div>
|
||||
<aside class="hidden xl:block border-l border-border dark:border-gray-800 overflow-y-auto transition-all duration-300 ease-in-out" appScrollableOverlay [style.width.px]="isOutlineOpen ? rightSidebarWidth : 0" [class.opacity-0]="!isOutlineOpen" [class.pointer-events-none]="!isOutlineOpen">
|
||||
<div class="p-3">
|
||||
<h2 class="text-sm font-semibold mb-2">Sommaire</h2>
|
||||
<div class="p-3 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold">Sommaire</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-surface1 dark:hover:bg-card transition"
|
||||
aria-label="Fermer le sommaire"
|
||||
(click)="toggleOutlineRequest.emit()">
|
||||
<svg class="w-4 h-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>
|
||||
<ul class="space-y-1 text-sm text-muted dark:text-main">
|
||||
<li *ngFor="let h of tableOfContents">
|
||||
<a class="block truncate hover:text-main dark:hover:text-white cursor-pointer" (click)="navigateHeading.emit(h.id)" [style.paddingLeft.rem]="(h.level - 1) * 0.75">{{ h.text }}</a>
|
||||
<li *ngFor="let h of tableOfContents" class="leading-tight">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left block px-3 py-2 rounded-lg transition-colors hover:bg-slate-500/10 dark:hover:bg-surface2/15 focus:outline-none"
|
||||
(click)="navigateHeading.emit(h.id)"
|
||||
[style.paddingLeft.rem]="(h.level - 1) * 0.75"
|
||||
>
|
||||
<span class="block truncate text-muted dark:text-main hover:text-main dark:hover:text-white">
|
||||
{{ h.text }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -207,18 +228,8 @@ import { ParametersPage } from '../../features/parameters/parameters.page';
|
||||
</div>
|
||||
} @else {
|
||||
<div class="note-content-area h-full px-3 py-3 animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay>
|
||||
<div class="flex items-center justify-between mb-3 sticky top-0 bg-card dark:bg-main py-2 -mt-2 z-10">
|
||||
<h2 class="text-base font-semibold truncate">{{ selectedNote?.title || 'Aucune page' }}</h2>
|
||||
<button
|
||||
*ngIf="tableOfContents.length > 0"
|
||||
(pointerdown)="$event.stopPropagation(); mobileNav.toggleToc()"
|
||||
(click)="$event.preventDefault()"
|
||||
class="p-2 rounded-lg hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-all active:scale-95 transform flex-shrink-0">
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
@if (selectedNote) {
|
||||
<app-note-viewer [note]="selectedNote || null" [noteHtmlContent]="renderedNoteContent" [allNotes]="vault.allNotes()" (noteLinkClicked)="noteSelected.emit($event)" (tagClicked)="tagClicked.emit($event)" (wikiLinkActivated)="wikiLinkActivated.emit($event)" (fullScreenRequested)="toggleNoteFullScreen()" (parametersRequested)="onParametersOpen()"></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)" (fullScreenRequested)="toggleNoteFullScreen()" (parametersRequested)="onParametersOpen()"></app-note-viewer>
|
||||
} @else {
|
||||
<div class="mt-10 text-center text-sm text-muted dark:text-muted">
|
||||
<div class="text-4xl mb-3">📄</div>
|
||||
|
||||
@@ -5,6 +5,92 @@ function normalizeTag(tag: string): string {
|
||||
return tag.trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Réécrit des clés booléennes dans le front-matter YAML.
|
||||
* - Crée la section --- ... --- si absente
|
||||
* - Met à jour/insère les clés fournies (value true/false)
|
||||
* - Supprime la clé si la valeur est undefined
|
||||
* - Préserve les autres propriétés et le corps
|
||||
*/
|
||||
export function rewriteBooleanFrontmatter(
|
||||
rawMarkdown: string,
|
||||
updates: Record<string, boolean | undefined>
|
||||
): string {
|
||||
const content = rawMarkdown.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
|
||||
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)/);
|
||||
|
||||
const normalizeKey = (k: string) => k.trim();
|
||||
const toYamlBool = (v: boolean) => (v ? 'true' : 'false');
|
||||
|
||||
const applyUpdatesToText = (fmText: string): string => {
|
||||
const lines = fmText.split('\n');
|
||||
const kept: string[] = [];
|
||||
const updatedKeys = new Set<string>(Object.keys(updates).map(normalizeKey));
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
kept.push(line);
|
||||
continue;
|
||||
}
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex === -1) {
|
||||
kept.push(line);
|
||||
continue;
|
||||
}
|
||||
const key = normalizeKey(line.slice(0, colonIndex));
|
||||
if (!updatedKeys.has(key)) {
|
||||
kept.push(line);
|
||||
continue;
|
||||
}
|
||||
const next = updates[key];
|
||||
if (next === undefined) {
|
||||
// remove this key (skip)
|
||||
continue;
|
||||
}
|
||||
kept.push(`${key}: ${toYamlBool(next)}`);
|
||||
// Mark as handled
|
||||
updatedKeys.delete(key);
|
||||
}
|
||||
|
||||
// Append remaining keys that didn't exist
|
||||
for (const key of Object.keys(updates)) {
|
||||
const norm = normalizeKey(key);
|
||||
const val = updates[key];
|
||||
if (val === undefined) continue;
|
||||
// If not already written (because it existed), append
|
||||
if (!kept.some(l => normalizeKey(l.split(':')[0] || '') === norm)) {
|
||||
kept.push(`${norm}: ${toYamlBool(val)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean multiple empty lines
|
||||
let out = kept.join('\n').replace(/\n{2,}/g, '\n').trim();
|
||||
return out;
|
||||
};
|
||||
|
||||
if (!fmMatch) {
|
||||
// No frontmatter: create one only if there is at least one defined update
|
||||
const definedEntries = Object.entries(updates).filter(([, v]) => v !== undefined) as Array<[
|
||||
string,
|
||||
boolean
|
||||
]>;
|
||||
if (!definedEntries.length) return content;
|
||||
const lines = definedEntries.map(([k, v]) => `${normalizeKey(k)}: ${toYamlBool(v)}`).join('\n');
|
||||
return `---\n${lines}\n---\n${content}`;
|
||||
}
|
||||
|
||||
const fmText = fmMatch[1] || '';
|
||||
const body = fmMatch[2] || '';
|
||||
const updatedFm = applyUpdatesToText(fmText);
|
||||
if (!updatedFm.trim()) {
|
||||
// If frontmatter became empty, drop the section entirely
|
||||
return body;
|
||||
}
|
||||
return `---\n${updatedFm}\n---\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Déduplique les tags (case-insensitive) en préservant la première occurrence
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="fixed inset-0 z-50 flex items-end md:items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40"></div>
|
||||
<div class="relative rounded-2xl shadow-xl border border-border dark:border-border bg-card dark:bg-main p-4 md:p-5 w-[min(880px,96vw)] md:max-h-[85vh] overflow-auto">
|
||||
<div class="relative rounded-2xl shadow-xl border border-border dark:border-border bg-card dark:bg-main p-4 md:p-5 w-[min(880px,96vw)] h-[92vh] md:h-auto md:max-h-[85vh] overflow-auto pb-24 md:pb-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-muted" 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>
|
||||
|
||||
@@ -9,7 +9,30 @@
|
||||
<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">
|
||||
<!-- Mobile: show first two + '+N' button -->
|
||||
<div class="flex sm:hidden flex-wrap items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="md-tag-badge"
|
||||
*ngFor="let tag of firstTwo()"
|
||||
[ngClass]="tagColorClass(tag)"
|
||||
(click)="onChipClick(tag)"
|
||||
[attr.data-tag]="tag"
|
||||
[title]="'Voir les notes #'+tag">
|
||||
{{ tag }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="remainingCount() > 0"
|
||||
type="button"
|
||||
class="btn-colored-xs btn-neutral"
|
||||
(click)="toggleEditor()"
|
||||
[title]="'Voir tous les tags'">
|
||||
+{{ remainingCount() }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Desktop: show full list -->
|
||||
<div class="hidden sm:flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="md-tag-badge"
|
||||
|
||||
@@ -31,6 +31,8 @@ export class TagManagerComponent {
|
||||
|
||||
isEditing = signal(false);
|
||||
readonly normalizedTags = computed(() => uniqueTags(this.tagsSignal()));
|
||||
readonly firstTwo = computed(() => this.normalizedTags().slice(0, 2));
|
||||
readonly remainingCount = computed(() => Math.max(0, this.normalizedTags().length - 2));
|
||||
|
||||
private readonly tagPaletteSize = 12;
|
||||
private readonly tagColorCache = new Map<string, number>();
|
||||
|
||||
Reference in New Issue
Block a user