refactor: redesign editor buttons and improve scrollbar theming

This commit is contained in:
Bruno Charest 2025-10-21 22:32:54 -04:00
parent 0d0607577d
commit eeb957cf17
27 changed files with 739 additions and 148 deletions

View File

@ -14,20 +14,19 @@
} }
} }
:host-context(.dark) ::ng-deep .note-content-area::-webkit-scrollbar-thumb { /* Theme-aware subtle scrollbar for note area */
background: rgba(148, 163, 184, 0.4); :host ::ng-deep .note-content-area {
scrollbar-width: thin; /* Firefox */
scrollbar-color: color-mix(in oklab, var(--scrollbar-thumb, rgba(148,163,184,0.45)) 80%, transparent) transparent; /* Firefox */
} }
:host-context(.dark) ::ng-deep .note-content-area::-webkit-scrollbar-thumb:hover { :host ::ng-deep .note-content-area::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.65); background: color-mix(in oklab, var(--scrollbar-thumb, rgba(148,163,184,0.45)) 80%, transparent);
border-radius: 9999px;
} }
:not(.dark) ::ng-deep .note-content-area::-webkit-scrollbar-thumb { :host ::ng-deep .note-content-area::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.28); background: color-mix(in oklab, var(--scrollbar-thumb, rgba(148,163,184,0.45)) 95%, transparent);
}
:not(.dark) ::ng-deep .note-content-area::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.45);
} }
:host ::ng-deep .md-tag-group { :host ::ng-deep .md-tag-group {
@ -166,30 +165,7 @@
} }
} }
/* Custom scrollbar for webkit browsers */ /* Global scrollbar rules moved to styles.css for true global scope */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
.dark ::-webkit-scrollbar-thumb {
background: #3c3d3f;
border-radius: 4px;
}
.dark ::-webkit-scrollbar-thumb:hover {
background: #5c6166;
}
/* Light mode scrollbar */
:not(.dark) ::-webkit-scrollbar-thumb {
background: #d8dbe0;
border-radius: 4px;
}
:not(.dark) ::-webkit-scrollbar-thumb:hover {
background: #b8bcc2;
}
.resize-handle { .resize-handle {
width: 8px; width: 8px;

View File

@ -555,7 +555,7 @@ export class AppComponent implements OnInit, OnDestroy {
const isDesktop = this.isDesktopView(); const isDesktop = this.isDesktopView();
if (isDesktop && !this.wasDesktop) { if (isDesktop && !this.wasDesktop) {
this.isSidebarOpen.set(true); this.isSidebarOpen.set(true);
this.isOutlineOpen.set(true); // Keep existing outline state to avoid forcing it open
} }
if (!isDesktop && this.wasDesktop) { if (!isDesktop && this.wasDesktop) {
this.isSidebarOpen.set(false); this.isSidebarOpen.set(false);

View File

@ -54,42 +54,42 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
<!-- Save Button --> <!-- Save Button -->
<button <button
type="button" type="button"
class="btn btn-solid btn-sm" class="editor-btn editor-btn--primary"
[disabled]="isSaving()" [disabled]="isSaving()"
(click)="save()" (click)="save()"
[attr.aria-label]="'Save (Ctrl+S)'"> [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"/> <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="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/> <polyline points="7 3 7 8 15 8"/>
</svg> </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"/> <circle cx="12" cy="12" r="10" stroke-dasharray="60" stroke-dashoffset="15"/>
</svg> </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> </button>
<!-- Wrap Toggle --> <!-- Wrap Toggle -->
<button <button
type="button" type="button"
class="btn btn-outline btn-sm" class="editor-btn editor-btn--neutral"
(click)="toggleWordWrap()" (click)="toggleWordWrap()"
[attr.aria-label]="'Toggle word wrap'"> [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"/> <polyline points="4 7 4 4 20 4 20 7"/>
<line x1="9" y1="20" x2="15" y2="20"/> <line x1="9" y1="20" x2="15" y2="20"/>
<line x1="12" y1="4" x2="12" y2="20"/> <line x1="12" y1="4" x2="12" y2="20"/>
</svg> </svg>
<span class="ml-1 hidden md:inline">Wrap</span> <span class="editor-btn__label hidden md:inline">Wrap</span>
</button> </button>
<!-- Undo --> <!-- Undo -->
<button <button
type="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()" (click)="undo()"
[attr.aria-label]="'Undo (Ctrl+Z)'"> [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"/> <polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/> <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg> </svg>
@ -98,10 +98,10 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
<!-- Redo --> <!-- Redo -->
<button <button
type="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()" (click)="redo()"
[attr.aria-label]="'Redo (Ctrl+Y)'"> [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"/> <polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/> <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg> </svg>
@ -110,14 +110,14 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
<!-- Close/Cancel --> <!-- Close/Cancel -->
<button <button
type="button" type="button"
class="btn btn-outline btn-sm" class="editor-btn editor-btn--neutral"
(click)="close()" (click)="close()"
[attr.aria-label]="'Close editor (Esc)'"> [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="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/> <line x1="6" y1="6" x2="18" y2="18"/>
</svg> </svg>
<span class="ml-1 hidden sm:inline">Close</span> <span class="editor-btn__label hidden sm:inline">Close</span>
</button> </button>
</div> </div>
</div> </div>
@ -171,7 +171,108 @@ import { EditorHighlightService } from '../../shared/editor/editor-highlight.ser
flex-wrap: wrap; 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 { .markdown-editor__container {
flex: 1; flex: 1;

View File

@ -30,9 +30,21 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
aria-modal="true" aria-modal="true"
(click)="$event.stopPropagation()"> (click)="$event.stopPropagation()">
<div class="flex items-center justify-center py-3 sm:py-2"> <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 class="h-1.5 w-12 rounded-full bg-muted dark:bg-surface2"></div>
</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> <div class="px-4 pb-4 overflow-y-auto max-h-[75vh] sm:max-h-[70vh]" appScrollableOverlay>
<h2 class="sr-only">Sommaire</h2> <h2 class="sr-only">Sommaire</h2>

View File

@ -168,6 +168,7 @@ export class NoteHeaderComponent implements AfterViewInit, OnDestroy {
const note = this.vaultService.getNoteById(this.noteId); const note = this.vaultService.getNoteById(this.noteId);
const props = this.frontmatterService.get(note); const props = this.frontmatterService.get(note);
compRef.instance.props = props; compRef.instance.props = props;
compRef.instance.noteId = this.noteId;
compRef.instance.requestClose.subscribe(() => this.scheduleClose()); compRef.instance.requestClose.subscribe(() => this.scheduleClose());
compRef.instance.cancelClose.subscribe(() => clearTimeout(this.closeTimer)); compRef.instance.cancelClose.subscribe(() => clearTimeout(this.closeTimer));

View File

@ -1,11 +1,12 @@
<section <section
id="note-props-popover" 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()" (mouseenter)="cancelClose.emit()"
(mouseleave)="requestClose.emit()" (mouseleave)="requestClose.emit()"
role="dialog" role="dialog"
aria-label="Propriétés du document"> 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"> <ng-container *ngIf="props as current; else emptyState">
<div class="space-y-4"> <div class="space-y-4">
@ -13,7 +14,7 @@
<section class="not-prose"> <section class="not-prose">
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5"> <dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5">
<ng-container *ngFor="let row of summaryRows"> <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> <dd class="font-medium break-words">{{ row.value }}</dd>
</ng-container> </ng-container>
</dl> </dl>
@ -22,26 +23,31 @@
<ng-container *ngIf="current.tags.length"> <ng-container *ngIf="current.tags.length">
<section class="not-prose"> <section class="not-prose">
<h4 class="text-xs uppercase tracking-wide text-muted-foreground mb-1">Tags</h4> <h4 class="text-xs uppercase tracking-wide text-muted-foreground font-semibold mb-1">Tags</h4>
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1.5">
<span *ngFor="let tag of current.tags" class="px-2 py-0.5 text-xs border rounded-full bg-muted/40">{{ tag }}</span> <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> </div>
</section> </section>
</ng-container> </ng-container>
<ng-container *ngIf="current.aliases.length"> <ng-container *ngIf="current.aliases.length">
<section class="not-prose"> <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> <p class="text-sm leading-5 text-muted-foreground">{{ current.aliases.join(' · ') }}</p>
</section> </section>
</ng-container> </ng-container>
<ng-container *ngIf="hasStates()"> <ng-container *ngIf="hasStates()">
<section class="not-prose"> <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"> <div class="flex flex-wrap gap-2">
<ng-container *ngFor="let st of stateEntries"> <ng-container *ngFor="let st of stateEntries">
<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> <app-state-chip [state]="st.key" [value]="st.value"></app-state-chip>
</button>
</ng-container> </ng-container>
</div> </div>
</section> </section>
@ -49,10 +55,10 @@
<ng-container *ngIf="hasAdditional()"> <ng-container *ngIf="hasAdditional()">
<section class="not-prose"> <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"> <dl class="space-y-2">
<ng-container *ngFor="let entry of additionalEntries"> <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> <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"> <dd class="text-sm leading-5 mt-1" [class.whitespace-pre-wrap]="entry.multiline">
{{ entry.displayValue }} {{ entry.displayValue }}

View File

@ -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 { CommonModule } from '@angular/common';
import { StateChipComponent } from '../state-chip/state-chip.component'; import { StateChipComponent } from '../state-chip/state-chip.component';
import { import {
@ -7,6 +7,8 @@ import {
NotePropertyStates, NotePropertyStates,
NotePropertySummary, NotePropertySummary,
} from '../../shared/note-properties.model'; } from '../../shared/note-properties.model';
import { VaultService } from '../../../../../services/vault.service';
import { FrontmatterPropertiesService } from '../../shared/frontmatter-properties.service';
@Component({ @Component({
selector: 'app-properties-popover', selector: 'app-properties-popover',
@ -16,9 +18,13 @@ import {
}) })
export class PropertiesPopoverComponent { export class PropertiesPopoverComponent {
@Input() props: NoteProperties | null = null; @Input() props: NoteProperties | null = null;
@Input() noteId: string | null = null;
@Output() requestClose = new EventEmitter<void>(); @Output() requestClose = new EventEmitter<void>();
@Output() cancelClose = new EventEmitter<void>(); @Output() cancelClose = new EventEmitter<void>();
private vault = inject(VaultService);
private frontmatter = inject(FrontmatterPropertiesService);
private readonly summaryConfig: Array<{ private readonly summaryConfig: Array<{
key: keyof NotePropertySummary; key: keyof NotePropertySummary;
label: string; label: string;
@ -40,6 +46,8 @@ export class PropertiesPopoverComponent {
{ key: 'archive', label: 'Archivé' }, { key: 'archive', label: 'Archivé' },
{ key: 'draft', label: 'Brouillon' }, { key: 'draft', label: 'Brouillon' },
{ key: 'private', label: 'Privé' }, { key: 'private', label: 'Privé' },
{ key: 'template', label: 'Template' },
{ key: 'task', label: 'Tâche' },
]; ];
get summaryRows(): Array<{ label: string; value: string }> { get summaryRows(): Array<{ label: string; value: string }> {
@ -69,12 +77,9 @@ export class PropertiesPopoverComponent {
return this.props?.aliases ?? []; return this.props?.aliases ?? [];
} }
get stateEntries(): Array<{ key: keyof NotePropertyStates; value: boolean | undefined }> { get stateEntries(): Array<{ key: keyof NotePropertyStates; value: boolean }> {
const states = this.props?.states; const states = this.props?.states || {};
if (!states) return []; return this.stateOrder.map(({ key }) => ({ key, value: states[key] ?? false }));
return this.stateOrder
.map(({ key }) => ({ key, value: states[key] }))
.filter(entry => entry.value !== undefined);
} }
get additionalEntries(): NotePropertyEntry[] { get additionalEntries(): NotePropertyEntry[] {
@ -97,4 +102,14 @@ export class PropertiesPopoverComponent {
hasAdditional(): boolean { hasAdditional(): boolean {
return this.additionalEntries.length > 0; 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);
}
} }

View File

@ -1,5 +1,5 @@
<span class="inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs" <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"> <span class="w-4 h-4 text-current inline-flex items-center justify-center">
<!-- Minimal Lucide-like inline SVGs to avoid extra deps --> <!-- 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> <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==='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'" 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==='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>
<span class="capitalize">{{ label }}</span> <span class="capitalize">{{ label }}</span>
</span> </span>

View File

@ -8,7 +8,7 @@ import { CommonModule } from '@angular/common';
templateUrl: './state-chip.component.html' templateUrl: './state-chip.component.html'
}) })
export class StateChipComponent { export class StateChipComponent {
@Input() state!: 'publish' | 'favoris' | 'archive' | 'draft' | 'private'; @Input() state!: 'publish' | 'favoris' | 'archive' | 'draft' | 'private' | 'template' | 'task';
@Input() value: boolean | null | undefined; @Input() value: boolean | null | undefined;
get label(): string { get label(): string {
@ -18,14 +18,38 @@ export class StateChipComponent {
case 'archive': return this.value ? 'Archivé' : 'Non archivé'; case 'archive': return this.value ? 'Archivé' : 'Non archivé';
case 'draft': return 'Brouillon'; case 'draft': return 'Brouillon';
case 'private': return 'Privé'; 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 === 'archive') return this.value ? 'archive' : 'box';
if (this.state === 'publish') return 'globe'; if (this.state === 'publish') return 'globe';
if (this.state === 'favoris') return 'heart'; if (this.state === 'favoris') return 'heart';
if (this.state === 'draft') return 'file'; if (this.state === 'draft') return 'file';
if (this.state === 'template') return 'template';
if (this.state === 'task') return 'task';
return this.value ? 'lock' : 'lock-open'; 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';
}
}
} }

View File

@ -32,15 +32,20 @@ export class FrontmatterPropertiesService {
category: this.toStr(frontmatter, consumedRawKeys, ['catégorie', 'categorie', 'category']), 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 aliases = this.toArray(frontmatter, consumedRawKeys, ['aliases', 'alias']) ?? [];
const states: NotePropertyStates = {}; const states: NotePropertyStates = {};
this.assignState(states, 'publish', frontmatter, consumedRawKeys, ['publish', 'publié']); this.assignState(states, 'publish', frontmatter, consumedRawKeys, ['publish', 'publié', 'publie', 'published']);
this.assignState(states, 'favoris', frontmatter, consumedRawKeys, ['favoris', 'favorite', 'favourite']); this.assignState(states, 'favoris', frontmatter, consumedRawKeys, ['favoris', 'favori', 'favorite', 'favourite', 'fav', 'star', 'starred']);
this.assignState(states, 'archive', frontmatter, consumedRawKeys, ['archive', 'archived']); this.assignState(states, 'archive', frontmatter, consumedRawKeys, ['archive', 'archived', 'archivé', 'archivee']);
this.assignState(states, 'draft', frontmatter, consumedRawKeys, ['draft', 'brouillon']); 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); const additional = this.buildAdditionalEntries(frontmatter, consumedRawKeys);

View File

@ -20,6 +20,8 @@ export interface NotePropertyStates {
archive?: boolean; archive?: boolean;
draft?: boolean; draft?: boolean;
private?: boolean; private?: boolean;
template?: boolean;
task?: boolean;
} }
export interface NotePropertyEntry { export interface NotePropertyEntry {

View File

@ -79,7 +79,7 @@ import { VaultService } from '../../../services/vault.service';
<div *ngIf="open.tags" class="px-2 py-2"> <div *ngIf="open.tags" class="px-2 py-2">
<ul class="space-y-0.5 text-sm"> <ul class="space-y-0.5 text-sm">
<li *ngFor="let t of tags" class="flex items-center gap-2"> <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>🏷</span>
<span class="ml-1">{{ t.name }}</span> <span class="ml-1">{{ t.name }}</span>
</button> </button>

View File

@ -142,11 +142,32 @@ import { ParametersPage } from '../../features/parameters/parameters.page';
></app-note-viewer> ></app-note-viewer>
</div> </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"> <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"> <div class="p-3 space-y-3">
<h2 class="text-sm font-semibold mb-2">Sommaire</h2> <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"> <ul class="space-y-1 text-sm text-muted dark:text-main">
<li *ngFor="let h of tableOfContents"> <li *ngFor="let h of tableOfContents" class="leading-tight">
<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> <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> </li>
</ul> </ul>
</div> </div>
@ -207,18 +228,8 @@ import { ParametersPage } from '../../features/parameters/parameters.page';
</div> </div>
} @else { } @else {
<div class="note-content-area h-full px-3 py-3 animate-fadeIn" style="overflow-y: auto !important;" appScrollableOverlay> <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) { @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 { } @else {
<div class="mt-10 text-center text-sm text-muted dark:text-muted"> <div class="mt-10 text-center text-sm text-muted dark:text-muted">
<div class="text-4xl mb-3">📄</div> <div class="text-4xl mb-3">📄</div>

View File

@ -5,6 +5,92 @@ function normalizeTag(tag: string): string {
return tag.trim().replace(/\s+/g, ' '); 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 * Déduplique les tags (case-insensitive) en préservant la première occurrence
*/ */

View File

@ -1,6 +1,6 @@
<div class="fixed inset-0 z-50 flex items-end md:items-center justify-center"> <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="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 justify-between mb-3">
<div class="flex items-center gap-2"> <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> <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>

View File

@ -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> <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> </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 <button
type="button" type="button"
class="md-tag-badge" class="md-tag-badge"

View File

@ -31,6 +31,8 @@ export class TagManagerComponent {
isEditing = signal(false); isEditing = signal(false);
readonly normalizedTags = computed(() => uniqueTags(this.tagsSignal())); 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 tagPaletteSize = 12;
private readonly tagColorCache = new Map<string, number>(); private readonly tagColorCache = new Map<string, number>();

View File

@ -51,7 +51,7 @@ export interface WikiLinkActivation {
<div class="relative p-1 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]"> <div class="relative p-1 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]">
<div class="sr-only" role="status" aria-live="polite">{{ copyStatus() }}</div> <div class="sr-only" role="status" aria-live="polite">{{ copyStatus() }}</div>
<!-- Compact Top Bar --> <!-- Compact Top Bar -->
<div class="flex items-center justify-between gap-2 pl-1 pr-2 py-1 mb-2 text-text-muted text-xs"> <div class="flex items-start justify-between gap-2 pl-1 pr-2 py-1 mb-2 text-text-muted text-xs">
<app-note-header class="flex-1 min-w-0" <app-note-header class="flex-1 min-w-0"
[fullPath]="note().filePath" [fullPath]="note().filePath"
[noteId]="note().id" [noteId]="note().id"
@ -62,7 +62,7 @@ export interface WikiLinkActivation {
(tagSelected)="tagClicked.emit($event)" (tagSelected)="tagClicked.emit($event)"
></app-note-header> ></app-note-header>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1 self-start pt-1">
<button <button
type="button" type="button"
class="note-toolbar-icon" class="note-toolbar-icon"
@ -85,6 +85,7 @@ export interface WikiLinkActivation {
{{ fullScreenActive() ? '⤢' : '⤢' }} {{ fullScreenActive() ? '⤢' : '⤢' }}
</button> </button>
<div class="hidden sm:block">
<button <button
type="button" type="button"
class="note-toolbar-icon" class="note-toolbar-icon"
@ -94,6 +95,7 @@ export interface WikiLinkActivation {
> >
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" [ngClass]="tocOpen() ? 'toc-toggle--active' : 'toc-toggle--idle'"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M13 8h5"/><path d="M13 12h5"/><path d="M13 16h5"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" [ngClass]="tocOpen() ? 'toc-toggle--active' : 'toc-toggle--idle'"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M13 8h5"/><path d="M13 12h5"/><path d="M13 16h5"/></svg>
</button> </button>
</div>
<button <button
type="button" type="button"
@ -155,7 +157,9 @@ export interface WikiLinkActivation {
</div> </div>
} }
<div class="not-prose flex items-center gap-3 text-sm text-text-muted my-4"> <div class="not-prose flex flex-col gap-2 text-sm text-text-muted my-4">
<!-- Row 1: date + author -->
<div class="flex flex-wrap items-center gap-4">
<span class="inline-flex items-center gap-1"> <span class="inline-flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -170,8 +174,12 @@ export interface WikiLinkActivation {
</svg> </svg>
{{ getAuthorFromFrontmatter() ?? note().author ?? 'Auteur inconnu' }} {{ getAuthorFromFrontmatter() ?? note().author ?? 'Auteur inconnu' }}
</span> </span>
</div>
<!-- Row 2: state icons (toggle buttons) -->
<div class="flex flex-wrap items-center gap-3">
@if (hasState('favoris')) { @if (hasState('favoris')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-muted'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" role="img" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}"> <button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-muted'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" (click)="toggleState('favoris')">
@if (state('favoris')) { @if (state('favoris')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" stroke="none"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M19 14.5c1.5-1.5 2-4 0-6s-5-2-7 1c-2-3-5-3.5-7-1s-1.5 4.5 0 6l7 6z" /> <path d="M19 14.5c1.5-1.5 2-4 0-6s-5-2-7 1c-2-3-5-3.5-7-1s-1.5 4.5 0 6l7 6z" />
@ -181,19 +189,19 @@ export interface WikiLinkActivation {
<path d="M19 14.5c1.5-1.5 2-4 0-6s-5-2-7 1c-2-3-5-3.5-7-1s-1.5 4.5 0 6l7 6z" /> <path d="M19 14.5c1.5-1.5 2-4 0-6s-5-2-7 1c-2-3-5-3.5-7-1s-1.5 4.5 0 6l7 6z" />
</svg> </svg>
} }
</span> </button>
} }
@if (hasState('publish')) { @if (hasState('publish')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('publish') ? 'text-green-500' : 'text-muted'" title="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}" role="img" aria-label="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}"> <button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('publish') ? 'text-green-500' : 'text-muted'" title="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}" aria-label="{{ state('publish') ? 'Publié sur le web' : 'Non publié' }}" (click)="toggleState('publish')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M2 12h20" /> <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" /> <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> </svg>
</span> </button>
} }
@if (hasState('draft')) { @if (hasState('draft')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('draft') ? 'text-yellow-500' : 'text-muted'" title="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}" role="img" aria-label="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}"> <button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('draft') ? 'text-yellow-500' : 'text-muted'" title="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}" aria-label="{{ state('draft') ? 'Brouillon actif' : 'Pas un brouillon' }}" (click)="toggleState('draft')">
@if (state('draft')) { @if (state('draft')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/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="M3 3h18v4H3z" /> <path d="M3 3h18v4H3z" />
@ -205,10 +213,10 @@ export interface WikiLinkActivation {
<rect x="3" y="4" width="18" height="16" rx="2" /> <rect x="3" y="4" width="18" height="16" rx="2" />
</svg> </svg>
} }
</span> </button>
} }
@if (hasState('template')) { @if (hasState('template')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('template') ? 'text-amber-500' : 'text-muted'" title="{{ state('template') ? 'Modèle' : 'Non modèle' }}" role="img" aria-label="{{ state('template') ? 'Modèle' : 'Non modèle' }}"> <button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('template') ? 'text-amber-500' : 'text-muted'" title="{{ state('template') ? 'Modèle' : 'Non modèle' }}" aria-label="{{ state('template') ? 'Modèle' : 'Non modèle' }}" (click)="toggleState('template')">
@if (state('template')) { @if (state('template')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="14" rx="2" /> <rect x="3" y="4" width="18" height="14" rx="2" />
@ -220,10 +228,10 @@ export interface WikiLinkActivation {
<rect x="3" y="5" width="18" height="12" rx="2" /> <rect x="3" y="5" width="18" height="12" rx="2" />
</svg> </svg>
} }
</span> </button>
} }
@if (hasState('task')) { @if (hasState('task')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('task') ? 'text-indigo-500' : 'text-muted'" title="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}" role="img" aria-label="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}"> <button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('task') ? 'text-indigo-500' : 'text-muted'" title="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}" aria-label="{{ state('task') ? 'Tâche' : 'Pas une tâche' }}" (click)="toggleState('task')">
@if (state('task')) { @if (state('task')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="16" rx="2" /> <rect x="3" y="4" width="18" height="16" rx="2" />
@ -234,10 +242,10 @@ export interface WikiLinkActivation {
<rect x="3" y="4" width="18" height="16" rx="2" /> <rect x="3" y="4" width="18" height="16" rx="2" />
</svg> </svg>
} }
</span> </button>
} }
@if (hasState('private')) { @if (hasState('private')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('private') ? 'text-purple-500' : 'text-muted'" title="{{ state('private') ? 'Privé' : 'Public' }}" role="img" aria-label="{{ state('private') ? 'Privé' : 'Public' }}"> <button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('private') ? 'text-purple-500' : 'text-muted'" title="{{ state('private') ? 'Privé' : 'Public' }}" aria-label="{{ state('private') ? 'Privé' : 'Public' }}" (click)="toggleState('private')">
@if (state('private')) { @if (state('private')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" /> <rect x="3" y="11" width="18" height="11" rx="2" />
@ -249,10 +257,10 @@ export interface WikiLinkActivation {
<path d="M7 11V7a5 5 0 0 1 9.9-1" /> <path d="M7 11V7a5 5 0 0 1 9.9-1" />
</svg> </svg>
} }
</span> </button>
} }
@if (hasState('archive')) { @if (hasState('archive')) {
<span class="inline-flex items-center gap-1 transition-colors" [ngClass]="state('archive') ? 'text-amber-600' : 'text-muted'" title="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}" role="img" aria-label="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}"> <button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('archive') ? 'text-amber-600' : 'text-muted'" title="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}" aria-label="{{ state('archive') ? 'Document archivé' : 'Document non archivé' }}" (click)="toggleState('archive')">
@if (state('archive')) { @if (state('archive')) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22,12 18,12 18,8"/> <polyline points="22,12 18,12 18,8"/>
@ -266,9 +274,10 @@ export interface WikiLinkActivation {
<line x1="10" y1="12" x2="14" y2="12"/> <line x1="10" y1="12" x2="14" y2="12"/>
</svg> </svg>
} }
</span> </button>
} }
</div> </div>
</div>
<div [innerHTML]="sanitizedHtmlContent()"></div> <div [innerHTML]="sanitizedHtmlContent()"></div>
@ -359,6 +368,23 @@ export class NoteViewerComponent implements OnDestroy {
return []; return [];
}); });
async toggleState(key: 'publish' | 'favoris' | 'archive' | 'draft' | 'private' | 'template' | 'task'): Promise<void> {
const currentNote = this.note();
if (!currentNote?.id) return;
const current = this.state(key);
const next = !current;
const ok = await this.vault.updateNoteStates(currentNote.id, key, next);
if (!ok) return;
try {
const fm = (currentNote.frontmatter ??= {} as Note['frontmatter']);
(fm as any)[key] = next;
} catch {
// ignore failures for optimistic update
}
this.vault.refresh();
}
constructor() { constructor() {
effect(() => { effect(() => {

View File

@ -179,7 +179,7 @@ interface TagGroup {
<button <button
type="button" type="button"
(click)="toggleGroup(group.label)" (click)="toggleGroup(group.label)"
class="group-header w-full flex items-center justify-between px-3 py-2 text-left hover:bg-obs-l-bg-hover dark:hover:bg-obs-d-bg-hover" class="group-header w-full flex items-center justify-between px-3 py-2 text-left rounded-lg transition-colors hover:bg-slate-500/10 dark:hover:bg-surface2/15"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<svg <svg
@ -208,7 +208,7 @@ interface TagGroup {
<button <button
type="button" type="button"
(click)="onTagClick(tag.name)" (click)="onTagClick(tag.name)"
class="tag-item w-full flex items-center justify-between px-3 py-2 text-left hover:bg-obs-l-bg-hover dark:hover:bg-obs-d-bg-hover group" class="tag-item w-full flex items-center justify-between px-3 py-2 text-left rounded-lg transition-colors hover:bg-slate-500/10 dark:hover:bg-surface2/15 group"
> >
<div class="flex items-center gap-2 flex-1 min-w-0"> <div class="flex items-center gap-2 flex-1 min-w-0">
<svg class="h-3.5 w-3.5 text-obs-l-text-muted dark:text-obs-d-text-muted flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-3.5 w-3.5 text-obs-l-text-muted dark:text-obs-d-text-muted flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">

View File

@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { Note, VaultNode, GraphData, TagInfo, VaultFolder, FileMetadata } from '../types'; import { Note, VaultNode, GraphData, TagInfo, VaultFolder, FileMetadata } from '../types';
import { VaultEventsService, VaultEventPayload } from './vault-events.service'; import { VaultEventsService, VaultEventPayload } from './vault-events.service';
import { Subscription, firstValueFrom } from 'rxjs'; import { Subscription, firstValueFrom } from 'rxjs';
import { rewriteTagsFrontmatter } from '../app/shared/markdown/markdown-frontmatter.util'; import { rewriteTagsFrontmatter, rewriteBooleanFrontmatter } from '../app/shared/markdown/markdown-frontmatter.util';
// ============================================================================ // ============================================================================
// INTERFACES // INTERFACES
@ -135,6 +135,30 @@ export class VaultService implements OnDestroy {
this.initialize(); this.initialize();
} }
async updateNoteStates(
noteId: string,
key: 'publish' | 'favoris' | 'archive' | 'draft' | 'private' | 'template' | 'task',
nextValue: boolean
): Promise<boolean> {
const note = this.getNoteById(noteId);
if (!note?.filePath) return false;
const currentRaw = note.rawContent ?? this.recomposeMarkdownFromNote(note);
const updatedRaw = rewriteBooleanFrontmatter(currentRaw, { [key]: nextValue });
if (!await this.saveMarkdown(note.filePath, updatedRaw)) return false;
const updatedFrontmatter = { ...(note.frontmatter || {}) } as any;
if (nextValue === undefined as any) {
delete updatedFrontmatter[key];
} else {
updatedFrontmatter[key] = nextValue;
}
this.updateNoteInMap(note, { rawContent: updatedRaw, frontmatter: updatedFrontmatter });
return true;
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.cleanup(); this.cleanup();
} }

View File

@ -1,3 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
@import './styles-test.css'; @import './styles-test.css';
@import './styles/themes.css'; @import './styles/themes.css';
@import './styles/markdown.css'; @import './styles/markdown.css';
@ -7,6 +8,23 @@
@import './styles/_overlay-scrollbar.css'; @import './styles/_overlay-scrollbar.css';
@import './styles/codemirror.css'; @import './styles/codemirror.css';
/* Local-only fallbacks: use installed fonts if available */
@font-face {
font-family: 'gg sans';
font-style: normal;
font-weight: 100 900;
src: local('gg sans'), local('ggsans'), local('GG Sans');
font-display: swap;
}
@font-face {
font-family: 'Whitney';
font-style: normal;
font-weight: 100 900;
src: local('Whitney'), local('Whitney Book'), local('Whitney Medium'), local('Whitney Semibold'), local('Whitney Bold');
font-display: swap;
}
/* Excalidraw CSS variables (thème sombre) */ /* Excalidraw CSS variables (thème sombre) */
/* .excalidraw { /* .excalidraw {
--color-primary: #ffffff; --color-primary: #ffffff;
@ -263,8 +281,9 @@ excalidraw-editor .excalidraw .layer-ui__wrapper {
--btn-hover-background: color-mix(in srgb, var(--bg-muted) 42%, transparent); --btn-hover-background: color-mix(in srgb, var(--bg-muted) 42%, transparent);
--btn-focus-ring: color-mix(in srgb, var(--border) 45%, transparent); --btn-focus-ring: color-mix(in srgb, var(--border) 45%, transparent);
--btn-muted-text: var(--text-muted); --btn-muted-text: var(--text-muted);
--scrollbar-thumb-color: rgba(148, 163, 184, 0.45); /* Scrollbar colors subtly follow theme brand/accent */
--scrollbar-thumb-color-active: rgba(148, 163, 184, 0.75); --scrollbar-thumb-color: color-mix(in oklab, var(--brand, var(--scrollbar-thumb, rgba(148, 163, 184, 0.45))) 22%, transparent);
--scrollbar-thumb-color-active: color-mix(in oklab, var(--brand, var(--scrollbar-thumb, rgba(148, 163, 184, 0.45))) 36%, transparent);
/* Theme accent + CodeMirror highlight tokens */ /* Theme accent + CodeMirror highlight tokens */
--color-accent: var(--primary, #3b82f6); --color-accent: var(--primary, #3b82f6);
@ -300,8 +319,9 @@ excalidraw-editor .excalidraw .layer-ui__wrapper {
--btn-hover-background: color-mix(in srgb, var(--bg-muted) 36%, transparent); --btn-hover-background: color-mix(in srgb, var(--bg-muted) 36%, transparent);
--btn-focus-ring: color-mix(in srgb, var(--border) 55%, transparent); --btn-focus-ring: color-mix(in srgb, var(--border) 55%, transparent);
--btn-muted-text: var(--text-muted); --btn-muted-text: var(--text-muted);
--scrollbar-thumb-color: rgba(148, 163, 184, 0.35); /* Dark-mode: slightly stronger mix for visibility on darker bg */
--scrollbar-thumb-color-active: rgba(226, 232, 240, 0.72); --scrollbar-thumb-color: color-mix(in oklab, var(--brand, var(--scrollbar-thumb, rgba(148, 163, 184, 0.35))) 28%, transparent);
--scrollbar-thumb-color-active: color-mix(in oklab, var(--brand, var(--scrollbar-thumb, rgba(148, 163, 184, 0.35))) 42%, transparent);
/* Dark theme adjustment */ /* Dark theme adjustment */
--cm-hl-bg: color-mix(in srgb, var(--color-accent) 22%, transparent); --cm-hl-bg: color-mix(in srgb, var(--color-accent) 22%, transparent);
@ -317,6 +337,21 @@ excalidraw-editor .excalidraw .layer-ui__wrapper {
} }
} }
/* Global native scrollbar fallback (for any container not using appScrollableOverlay) */
html, body {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-color);
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-color-active);
}
@layer components { @layer components {
/* CodeMirror markdown highlight class - theme driven */ /* CodeMirror markdown highlight class - theme driven */
.cm-md-highlight { .cm-md-highlight {

View File

@ -5,10 +5,20 @@
/* ---------- Thèmes & variables ---------- */ /* ---------- Thèmes & variables ---------- */
:root { :root {
/* Couleurs (clair) */ /* Couleurs (clair) — alignées sur les tokens de thème */
--ovsb-track-bg: rgba(15, 23, 42, 0.12); /* Track très discret */
--ovsb-thumb-bg: rgba(100, 116, 139, 0.55); --ovsb-track-bg: color-mix(in oklab, var(--border, rgba(15,23,42,0.25)) 16%, transparent);
--ovsb-thumb-bg-active: rgba(59, 130, 246, 0.75); /* Pouce (thumb) subtil basé sur --scrollbar-thumb, avec légère teinte brand */
--ovsb-thumb-bg: color-mix(
in oklab,
var(--scrollbar-thumb, rgba(100,116,139,0.55)) 80%,
color-mix(in oklab, var(--brand, #64748b) 18%, transparent) 20%
);
--ovsb-thumb-bg-active: color-mix(
in oklab,
var(--scrollbar-thumb, rgba(100,116,139,0.55)) 65%,
color-mix(in oklab, var(--brand-700, var(--brand, #64748b)) 40%, transparent) 35%
);
/* Géométrie & transitions */ /* Géométrie & transitions */
--ovsb-trans: 240ms ease; --ovsb-trans: 240ms ease;
@ -27,9 +37,18 @@
.dark, .dark,
[data-theme="dark"] { [data-theme="dark"] {
--ovsb-track-bg: rgba(148, 163, 184, 0.28); /* Légèrement plus visible sur fond sombre mais toujours discret */
--ovsb-thumb-bg: rgba(226, 232, 240, 0.72); --ovsb-track-bg: color-mix(in oklab, var(--border, rgba(148,163,184,0.35)) 22%, transparent);
--ovsb-thumb-bg-active: rgba(241, 245, 249, 0.92); --ovsb-thumb-bg: color-mix(
in oklab,
var(--scrollbar-thumb, rgba(226,232,240,0.62)) 85%,
color-mix(in oklab, var(--brand-700, var(--brand, #94a3b8)) 24%, transparent) 15%
);
--ovsb-thumb-bg-active: color-mix(
in oklab,
var(--scrollbar-thumb, rgba(226,232,240,0.72)) 70%,
color-mix(in oklab, var(--brand-700, var(--brand, #94a3b8)) 45%, transparent) 30%
);
} }
/* Reduced motion */ /* Reduced motion */

View File

@ -11,6 +11,9 @@
--transition-fast: 120ms ease; --transition-fast: 120ms ease;
--transition-slow: 200ms ease; --transition-slow: 200ms ease;
/* UI Font (fallback default) */
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, Arial, "Helvetica Neue", Helvetica, sans-serif;
/* Focus */ /* Focus */
--focus-ring-size: 2px; --focus-ring-size: 2px;
--focus-ring-offset: 2px; --focus-ring-offset: 2px;
@ -87,6 +90,9 @@ html.dark[data-theme] {
html:not(.dark)[data-theme="light"] { html:not(.dark)[data-theme="light"] {
color-scheme: light; color-scheme: light;
/* Fonts — Pure White */
--font-ui: Arial, "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
/* Backgrounds */ /* Backgrounds */
--bg: #ffffff; --bg: #ffffff;
--bg-main: #f7f7f7; --bg-main: #f7f7f7;
@ -153,6 +159,9 @@ html:not(.dark)[data-theme="light"] {
html.dark[data-theme="dark"] { html.dark[data-theme="dark"] {
color-scheme: dark; color-scheme: dark;
/* Fonts — default dark */
--font-ui: "Segoe UI", Roboto, Arial, sans-serif;
/* Backgrounds */ /* Backgrounds */
--bg: #0b1220; --bg: #0b1220;
--bg-main: #111827; --bg-main: #111827;
@ -218,12 +227,177 @@ html.dark[data-theme="dark"] {
--md-quote-bg: color-mix(in oklab, var(--surface-2) 88%, black 0%); --md-quote-bg: color-mix(in oklab, var(--surface-2) 88%, black 0%);
} }
/* ============================================================================
THÈME BLUE - LIGHT
============================================================================ */
html:not(.dark)[data-theme="blue"] {
color-scheme: light;
/* Fonts — Blue */
--font-ui: "Segoe UI", Roboto, Arial, sans-serif;
/* Backgrounds */
--bg: #ffffff;
--bg-main: #f7faff;
--bg-muted: #eef2ff;
--card: #ffffff;
--card-bg: #ffffff;
--elevated: #ffffff;
--sidebar-bg: #f1f5ff;
--surface-1: #f1f5ff;
--surface-2: #e6edff;
/* Text */
--fg: #0f172a;
--text-main: #0f172a;
--text-muted: #475569;
--muted: #64748b;
/* Borders */
--border: #dbeafe;
/* Brand colors */
--primary: #2563eb;
--brand: #2563eb;
--brand-700: #1d4ed8;
--brand-800: #1e40af;
--secondary: #7c3aed;
--accent: #06b6d4;
/* Status */
--success: #16a34a;
--warning: #f59e0b;
--danger: #ef4444;
--info: #0ea5e9;
/* UI */
--chip-bg: #e2e8f0;
--link: #1d4ed8;
--link-hover: #1e40af;
--ring: #2563eb;
/* Shadows */
--shadow-color: rgba(15, 23, 42, 0.08);
--scrollbar-thumb: rgba(59, 130, 246, 0.35);
/* Editor */
--editor-bg: #ffffff;
--editor-fg: #0f172a;
--editor-selection: rgba(37, 99, 235, 0.2);
--editor-gutter-bg: #f1f5ff;
--editor-gutter-fg: #64748b;
--editor-cursor: #0f172a;
/* Markdown overrides */
--md-h1: #0f172a;
--md-h2: #1d4ed8;
--md-h3: #2563eb;
--md-h4: #7c3aed;
--md-h5: #475569;
--md-h6: #64748b;
--md-quote-bar: #2563eb;
--md-quote-bg: color-mix(in oklab, #e6edff 92%, white 0%);
--md-table-head-bg: color-mix(in oklab, #e6edff 90%, white 0%);
--md-table-row-alt: color-mix(in oklab, #f1f5ff 96%, white 0%);
--md-pre-bg: #f1f5ff;
--md-pre-border: #dbeafe;
--md-syntax-1: #ef4444;
--md-syntax-2: #7c3aed;
--md-syntax-3: #16a34a;
--md-syntax-4: #2563eb;
--md-syntax-5: #0f172a;
}
/* ============================================================================
THÈME BLUE - DARK
============================================================================ */
html.dark[data-theme="blue"] {
color-scheme: dark;
/* Fonts — Blue */
--font-ui: "Segoe UI", Roboto, Arial, sans-serif;
/* Backgrounds */
--bg: #0b1020;
--bg-main: #0b1020;
--bg-muted: #0f1a2e;
--card: #0f1a2e;
--card-bg: #0f1a2e;
--elevated: #112340;
--sidebar-bg: #0b1020;
--surface-1: #0b1020;
--surface-2: #0f1a2e;
/* Text */
--fg: #dbeafe;
--text-main: #dbeafe;
--text-muted: #93c5fd;
--muted: #93c5fd;
/* Borders */
--border: #1e3a8a;
/* Brand colors */
--primary: #3b82f6;
--brand: #3b82f6;
--brand-700: #1d4ed8;
--brand-800: #1e40af;
--secondary: #a78bfa;
--accent: #22d3ee;
/* Status */
--success: #22c55e;
--warning: #f59e0b;
--danger: #f87171;
--info: #93c5fd;
/* UI */
--chip-bg: #0f1a2e;
--link: #93c5fd;
--link-hover: #bfdbfe;
--ring: #3b82f6;
/* Shadows */
--shadow-color: rgba(0, 0, 0, 0.5);
--scrollbar-thumb: rgba(147, 197, 253, 0.35);
/* Editor */
--editor-bg: #0f1a2e;
--editor-fg: #dbeafe;
--editor-selection: rgba(59, 130, 246, 0.25);
--editor-gutter-bg: #0f1a2e;
--editor-gutter-fg: #93c5fd;
--editor-cursor: #dbeafe;
/* Markdown overrides */
--md-h1: #dbeafe;
--md-h2: #93c5fd;
--md-h3: #60a5fa;
--md-h4: #a78bfa;
--md-h5: #93c5fd;
--md-h6: #6ea8ff;
--md-quote-bar: #3b82f6;
--md-quote-bg: color-mix(in oklab, #0f1a2e 92%, black 0%);
--md-table-head-bg: color-mix(in oklab, #0f1a2e 90%, black 0%);
--md-table-row-alt: color-mix(in oklab, #0b1020 85%, black 0%);
--md-pre-bg: #0f1a2e;
--md-pre-border: #1e3a8a;
--md-syntax-1: #ff7b72;
--md-syntax-2: #d2a8ff;
--md-syntax-3: #a5d6ff;
--md-syntax-4: #79c0ff;
--md-syntax-5: #93c5fd;
}
/* ============================================================================ /* ============================================================================
THÈME OBSIDIAN - LIGHT THÈME OBSIDIAN - LIGHT
============================================================================ */ ============================================================================ */
html:not(.dark)[data-theme="obsidian"] { html:not(.dark)[data-theme="obsidian"] {
color-scheme: light; color-scheme: light;
/* Fonts — Obsidian */
--font-ui: Inter, "Segoe UI", Arial, Helvetica, sans-serif;
--bg: #fbfbfb; --bg: #fbfbfb;
--bg-main: #fafaf8; --bg-main: #fafaf8;
--bg-muted: #f5f3ef; --bg-muted: #f5f3ef;
@ -291,6 +465,9 @@ html:not(.dark)[data-theme="obsidian"] {
html.dark[data-theme="obsidian"] { html.dark[data-theme="obsidian"] {
color-scheme: dark; color-scheme: dark;
/* Fonts — Obsidian */
--font-ui: Inter, "Segoe UI", Arial, Helvetica, sans-serif;
--bg: #1b1b1b; --bg: #1b1b1b;
--bg-main: #1e1e1e; --bg-main: #1e1e1e;
--bg-muted: #252525; --bg-muted: #252525;
@ -360,6 +537,10 @@ html.dark[data-theme="obsidian"] {
html:not(.dark)[data-theme="nord"] { html:not(.dark)[data-theme="nord"] {
color-scheme: light; color-scheme: light;
/* Fonts — Nord */
--font-ui: "Fira Code", "JetBrains Mono", Consolas, ui-monospace, monospace;
--cm-font-family: "Fira Code", "JetBrains Mono", Consolas, ui-monospace, monospace;
--bg: #eceff4; --bg: #eceff4;
--bg-main: #eceff4; --bg-main: #eceff4;
--bg-muted: #e5e9f0; --bg-muted: #e5e9f0;
@ -422,6 +603,10 @@ html:not(.dark)[data-theme="nord"] {
html.dark[data-theme="nord"] { html.dark[data-theme="nord"] {
color-scheme: dark; color-scheme: dark;
/* Fonts — Nord */
--font-ui: "Fira Code", "JetBrains Mono", Consolas, ui-monospace, monospace;
--cm-font-family: "Fira Code", "JetBrains Mono", Consolas, ui-monospace, monospace;
--bg: #2e3440; --bg: #2e3440;
--bg-main: #2e3440; --bg-main: #2e3440;
--bg-muted: #3b4252; --bg-muted: #3b4252;
@ -491,6 +676,9 @@ html.dark[data-theme="nord"] {
html:not(.dark)[data-theme="notion"] { html:not(.dark)[data-theme="notion"] {
color-scheme: light; color-scheme: light;
/* Fonts — Notion */
--font-ui: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
--bg: #ffffff; --bg: #ffffff;
--bg-main: #ffffff; --bg-main: #ffffff;
--bg-muted: #f7f7f5; --bg-muted: #f7f7f5;
@ -560,6 +748,9 @@ html:not(.dark)[data-theme="notion"] {
html.dark[data-theme="notion"] { html.dark[data-theme="notion"] {
color-scheme: dark; color-scheme: dark;
/* Fonts — Notion */
--font-ui: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
--bg: #171717; --bg: #171717;
--bg-main: #171717; --bg-main: #171717;
--bg-muted: #1b1b1b; --bg-muted: #1b1b1b;
@ -629,6 +820,9 @@ html.dark[data-theme="notion"] {
html:not(.dark)[data-theme="github"] { html:not(.dark)[data-theme="github"] {
color-scheme: light; color-scheme: light;
/* Fonts — GitHub */
--font-ui: "Segoe UI", Arial, -apple-system, system-ui, sans-serif;
--bg: #ffffff; --bg: #ffffff;
--bg-main: #ffffff; --bg-main: #ffffff;
--bg-muted: #f6f8fa; --bg-muted: #f6f8fa;
@ -698,6 +892,9 @@ html:not(.dark)[data-theme="github"] {
html.dark[data-theme="github"] { html.dark[data-theme="github"] {
color-scheme: dark; color-scheme: dark;
/* Fonts — GitHub */
--font-ui: "Segoe UI", Arial, -apple-system, system-ui, sans-serif;
--bg: #0d1117; --bg: #0d1117;
--bg-main: #0d1117; --bg-main: #0d1117;
--bg-muted: #161b22; --bg-muted: #161b22;
@ -767,6 +964,9 @@ html.dark[data-theme="github"] {
html:not(.dark)[data-theme="discord"] { html:not(.dark)[data-theme="discord"] {
color-scheme: light; color-scheme: light;
/* Fonts — Discord */
--font-ui: "gg sans", Whitney, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
--bg: #f6f7f9; --bg: #f6f7f9;
--bg-main: #f6f7f9; --bg-main: #f6f7f9;
--bg-muted: #eef0f5; --bg-muted: #eef0f5;
@ -836,6 +1036,9 @@ html:not(.dark)[data-theme="discord"] {
html.dark[data-theme="discord"] { html.dark[data-theme="discord"] {
color-scheme: dark; color-scheme: dark;
/* Fonts — Discord */
--font-ui: "gg sans", Whitney, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
--bg: #2b2d31; --bg: #2b2d31;
--bg-main: #2b2d31; --bg-main: #2b2d31;
--bg-muted: #232428; --bg-muted: #232428;
@ -905,6 +1108,10 @@ html.dark[data-theme="discord"] {
html:not(.dark)[data-theme="monokai"] { html:not(.dark)[data-theme="monokai"] {
color-scheme: light; color-scheme: light;
/* Fonts — Monokai */
--font-ui: Menlo, Monaco, Consolas, "Fira Code", ui-monospace, monospace;
--cm-font-family: Menlo, Monaco, Consolas, "Fira Code", ui-monospace, monospace;
--bg: #fbfbf7; --bg: #fbfbf7;
--bg-main: #fbfbf7; --bg-main: #fbfbf7;
--bg-muted: #f2f2ea; --bg-muted: #f2f2ea;
@ -974,6 +1181,10 @@ html:not(.dark)[data-theme="monokai"] {
html.dark[data-theme="monokai"] { html.dark[data-theme="monokai"] {
color-scheme: dark; color-scheme: dark;
/* Fonts — Monokai */
--font-ui: Menlo, Monaco, Consolas, "Fira Code", ui-monospace, monospace;
--cm-font-family: Menlo, Monaco, Consolas, "Fira Code", ui-monospace, monospace;
--bg: #272822; --bg: #272822;
--bg-main: #272822; --bg-main: #272822;
--bg-muted: #23241f; --bg-muted: #23241f;
@ -1043,5 +1254,6 @@ html.dark[data-theme="monokai"] {
body { body {
background-color: var(--bg-main); background-color: var(--bg-main);
color: var(--text-main); color: var(--text-main);
font-family: var(--font-ui, system-ui, -apple-system, "Segoe UI", Roboto, Arial, "Helvetica Neue", Helvetica, sans-serif);
transition: background-color var(--transition-base), color var(--transition-base); transition: background-color var(--transition-base), color var(--transition-base);
} }

View File

@ -10,10 +10,10 @@ tags:
- tag3 - tag3
aliases: [] aliases: []
status: en-cours status: en-cours
publish: false publish: true
favoris: false favoris: true
template: false template: false
task: false task: true
archive: false archive: false
draft: false draft: false
private: false private: false

View File

@ -10,14 +10,17 @@ tags:
- tag3 - tag3
aliases: [] aliases: []
status: en-cours status: en-cours
publish: false publish: true
favoris: false favoris: true
template: false template: false
task: false task: true
archive: false archive: false
draft: false draft: false
private: false private: false
--- ---
# Archived Note # Archived Note
#bruno
This note was archived and moved to trash. This note was archived and moved to trash.

View File

@ -17,6 +17,9 @@ archive: true
draft: true draft: true
private: true private: true
--- ---
Allo ceci est un tests
toto
# Test 1 Markdown # Test 1 Markdown
## Titres ## Titres

View File

@ -10,13 +10,16 @@ aliases:
- nouveau - nouveau
status: en-cours status: en-cours
publish: true publish: true
favoris: true favoris: false
template: true template: true
task: true task: true
archive: true archive: true
draft: true draft: true
private: true private: true
--- ---
Allo ceci est un tests
toto
# Test 1 Markdown # Test 1 Markdown
## Titres ## Titres