feat: add syntax highlighting and code block styling with theme support
This commit is contained in:
@@ -7,10 +7,10 @@ import { MobileNavService } from '../../shared/services/mobile-nav.service';
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<nav class="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 h-16 flex justify-around items-center z-50 safe-area-inset-bottom">
|
||||
<nav class="fixed bottom-0 left-0 right-0 bg-card dark:bg-main border-t border-border dark:border-gray-800 h-16 flex justify-around items-center z-50 safe-area-inset-bottom">
|
||||
<button *ngFor="let tab of tabs"
|
||||
(click)="setActiveTab(tab.id)"
|
||||
class="relative flex-1 flex flex-col items-center justify-center gap-1 text-xs py-2 px-1 text-gray-600 dark:text-gray-400 transition-all duration-200 active:scale-95 transform"
|
||||
class="relative flex-1 flex flex-col items-center justify-center gap-1 text-xs py-2 px-1 text-muted dark:text-muted transition-all duration-200 active:scale-95 transform"
|
||||
[class.text-nimbus-500]="mobileNav.activeTab() === tab.id"
|
||||
[class.font-semibold]="mobileNav.activeTab() === tab.id">
|
||||
<!-- Active indicator -->
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
[class.text-red-500]="dirty() && !isSaving()"
|
||||
[class.text-gray-400]="!dirty() && !isSaving()"
|
||||
[class.text-muted]="!dirty() && !isSaving()"
|
||||
[class.text-yellow-500]="isSaving()"
|
||||
[class.animate-pulse]="isSaving()"
|
||||
>
|
||||
@@ -32,7 +32,7 @@
|
||||
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||||
<polyline points="7 3 7 8 15 8"></polyline>
|
||||
</svg>
|
||||
<span class="text-xs font-medium" [class.text-red-500]="dirty() && !isSaving()" [class.text-gray-400]="!dirty() && !isSaving()" [class.text-yellow-500]="isSaving()">
|
||||
<span class="text-xs font-medium" [class.text-red-500]="dirty() && !isSaving()" [class.text-muted]="!dirty() && !isSaving()" [class.text-yellow-500]="isSaving()">
|
||||
{{isSaving() ? 'Sauvegarde...' : dirty() ? 'Non sauvegardé' : 'Sauvegardé'}}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -15,13 +15,15 @@ import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection, dr
|
||||
import { EditorState, Compartment } from '@codemirror/state';
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter } from '@codemirror/language';
|
||||
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter, LanguageDescription } from '@codemirror/language';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { VaultService } from '../../../services/vault.service';
|
||||
import { EditorStateService } from '../../../services/editor-state.service';
|
||||
import { ToastService } from '../../shared/toast/toast.service';
|
||||
import { ThemeService } from '../../core/services/theme.service';
|
||||
import { EditorHighlightService } from '../../shared/editor/editor-highlight.service';
|
||||
|
||||
/**
|
||||
* Composant d'édition Markdown avec CodeMirror 6
|
||||
@@ -52,8 +54,7 @@ import { ToastService } from '../../shared/toast/toast.service';
|
||||
<!-- Save Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-editor"
|
||||
[class.btn-editor--primary]="isDirty()"
|
||||
class="btn btn-solid btn-sm"
|
||||
[disabled]="isSaving()"
|
||||
(click)="save()"
|
||||
[attr.aria-label]="'Save (Ctrl+S)'">
|
||||
@@ -71,8 +72,7 @@ import { ToastService } from '../../shared/toast/toast.service';
|
||||
<!-- Wrap Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-editor"
|
||||
[class.btn-editor--active]="wordWrap()"
|
||||
class="btn btn-outline btn-sm"
|
||||
(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">
|
||||
@@ -86,7 +86,7 @@ import { ToastService } from '../../shared/toast/toast.service';
|
||||
<!-- Undo -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-editor hidden sm:flex"
|
||||
class="btn btn-ghost btn-sm hidden sm: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">
|
||||
@@ -98,7 +98,7 @@ import { ToastService } from '../../shared/toast/toast.service';
|
||||
<!-- Redo -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-editor hidden sm:flex"
|
||||
class="btn btn-ghost btn-sm hidden sm: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">
|
||||
@@ -110,7 +110,7 @@ import { ToastService } from '../../shared/toast/toast.service';
|
||||
<!-- Close/Cancel -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-editor"
|
||||
class="btn btn-outline btn-sm"
|
||||
(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">
|
||||
@@ -171,49 +171,7 @@ import { ToastService } from '../../shared/toast/toast.service';
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-editor {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-editor:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-editor:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-editor--primary {
|
||||
background: var(--brand);
|
||||
color: white;
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.btn-editor--primary:hover:not(:disabled) {
|
||||
background: var(--brand-hover);
|
||||
border-color: var(--brand-hover);
|
||||
}
|
||||
|
||||
.btn-editor--active {
|
||||
background: var(--brand);
|
||||
color: white;
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
|
||||
.markdown-editor__container {
|
||||
flex: 1;
|
||||
@@ -289,6 +247,7 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
private vaultService = inject(VaultService);
|
||||
private editorStateService = inject(EditorStateService);
|
||||
private toastService = inject(ToastService);
|
||||
private themeService = inject(ThemeService);
|
||||
|
||||
// Signals (public pour utilisation dans le template)
|
||||
filePath = signal<string>('');
|
||||
@@ -310,13 +269,75 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
// CodeMirror
|
||||
private editorView?: EditorView;
|
||||
private wrapCompartment = new Compartment();
|
||||
private themeCompartment = new Compartment();
|
||||
private occurrencesCompartment = new Compartment();
|
||||
private highlightFacetCompartment = new Compartment();
|
||||
private autosaveTimer?: ReturnType<typeof setTimeout>;
|
||||
private themeSubscription?: any;
|
||||
|
||||
private highlightService = inject(EditorHighlightService);
|
||||
private readonly markdownCodeLanguages = [
|
||||
LanguageDescription.of({
|
||||
name: 'javascript',
|
||||
alias: ['js', 'node', 'jsx', 'mjs', 'cjs'],
|
||||
load: () => import('@codemirror/lang-javascript').then(m => m.javascript())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'typescript',
|
||||
alias: ['ts', 'tsx'],
|
||||
load: () => import('@codemirror/lang-javascript').then(m => m.javascript({ typescript: true, jsx: true }))
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'json',
|
||||
load: () => import('@codemirror/lang-json').then(m => m.json())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'html',
|
||||
alias: ['xml', 'svg'],
|
||||
load: () => import('@codemirror/lang-html').then(m => m.html())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'css',
|
||||
alias: ['scss', 'less'],
|
||||
load: () => import('@codemirror/lang-css').then(m => m.css())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'python',
|
||||
alias: ['py'],
|
||||
load: () => import('@codemirror/lang-python').then(m => m.python())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'go',
|
||||
load: () => import('@codemirror/lang-go').then(m => m.go())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'rust',
|
||||
load: () => import('@codemirror/lang-rust').then(m => m.rust())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'php',
|
||||
load: () => import('@codemirror/lang-php').then(m => m.php())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'sql',
|
||||
load: () => import('@codemirror/lang-sql').then(m => m.sql())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'java',
|
||||
load: () => import('@codemirror/lang-java').then(m => m.java())
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: 'yaml',
|
||||
alias: ['yml'],
|
||||
load: () => import('@codemirror/lang-yaml').then(m => m.yaml())
|
||||
})
|
||||
];
|
||||
|
||||
constructor() {
|
||||
// Détecter le thème
|
||||
// S'abonner aux changements de thème
|
||||
effect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const darkMode = document.documentElement.classList.contains('dark');
|
||||
const darkMode = this.themeService.isDark();
|
||||
this.isDarkTheme.set(darkMode);
|
||||
}
|
||||
});
|
||||
@@ -324,7 +345,7 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeEditor();
|
||||
this.setupThemeObserver();
|
||||
this.subscribeToThemeChanges();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -353,9 +374,12 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
autocompletion(),
|
||||
highlightSelectionMatches(),
|
||||
foldGutter(),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
markdown({ base: markdownLanguage }),
|
||||
markdown({ base: markdownLanguage, addKeymap: true, codeLanguages: this.markdownCodeLanguages }),
|
||||
...this.highlightService.extensions,
|
||||
this.occurrencesCompartment.of([]),
|
||||
this.wrapCompartment.of(this.wordWrap() ? EditorView.lineWrapping : []),
|
||||
this.highlightFacetCompartment.of([]),
|
||||
this.themeCompartment.of(this.themeService.getCodeMirrorExtensions()),
|
||||
keymap.of([
|
||||
saveCommand,
|
||||
...defaultKeymap,
|
||||
@@ -373,30 +397,6 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
if (update.selectionSet) {
|
||||
this.updateCursorPosition();
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#ffffff'
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6',
|
||||
color: this.isDarkTheme() ? '#e2e8f0' : '#1e293b'
|
||||
},
|
||||
'.cm-cursor': {
|
||||
borderLeftColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6'
|
||||
},
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: this.isDarkTheme() ? '#334155' : '#f1f5f9'
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#f8fafc',
|
||||
color: this.isDarkTheme() ? '#64748b' : '#94a3b8',
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: this.isDarkTheme() ? '#334155' : '#e2e8f0'
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -409,52 +409,21 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
this.updateCursorPosition();
|
||||
}
|
||||
|
||||
private setupThemeObserver(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const darkMode = document.documentElement.classList.contains('dark');
|
||||
this.isDarkTheme.set(darkMode);
|
||||
this.reconfigureTheme();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
private subscribeToThemeChanges(): void {
|
||||
this.themeSubscription = this.themeService.onPrefs$.subscribe(() => {
|
||||
this.updateEditorTheme();
|
||||
});
|
||||
}
|
||||
|
||||
private reconfigureTheme(): void {
|
||||
private updateEditorTheme(): void {
|
||||
if (!this.editorView) return;
|
||||
|
||||
const newTheme = EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#ffffff'
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6',
|
||||
color: this.isDarkTheme() ? '#e2e8f0' : '#1e293b'
|
||||
},
|
||||
'.cm-cursor': {
|
||||
borderLeftColor: this.isDarkTheme() ? '#60a5fa' : '#3b82f6'
|
||||
},
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: this.isDarkTheme() ? '#334155' : '#f1f5f9'
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: this.isDarkTheme() ? '#1e293b' : '#f8fafc',
|
||||
color: this.isDarkTheme() ? '#64748b' : '#94a3b8',
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: this.isDarkTheme() ? '#334155' : '#e2e8f0'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const newThemeExtensions = this.themeService.getCodeMirrorExtensions();
|
||||
this.editorView.dispatch({
|
||||
effects: []
|
||||
effects: this.themeCompartment.reconfigure(newThemeExtensions)
|
||||
});
|
||||
// trigger a light re-measure so CSS variable based highlights pick up instantly
|
||||
this.editorView.requestMeasure({ read: () => null, write: () => {} });
|
||||
}
|
||||
|
||||
private onContentChange(newContent: string): void {
|
||||
@@ -474,6 +443,28 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
this.cursorCol.set(pos - line.from + 1);
|
||||
}
|
||||
|
||||
// Public API: highlight helpers
|
||||
highlightOccurrences(pattern: string | RegExp): void {
|
||||
if (!this.editorView) return;
|
||||
this.highlightService.applyOccurrences(this.editorView, this.occurrencesCompartment, pattern);
|
||||
}
|
||||
|
||||
setHighlights(ranges: { from: number; to: number }[]): void {
|
||||
if (!this.editorView) return;
|
||||
this.highlightService.setRanges(this.editorView, ranges);
|
||||
}
|
||||
|
||||
clearHighlights(): void {
|
||||
if (!this.editorView) return;
|
||||
this.highlightService.clearRanges(this.editorView);
|
||||
}
|
||||
|
||||
setHighlightColor(cssColor: string | null): void {
|
||||
if (!this.editorView) return;
|
||||
const ext = cssColor ? [ (this.highlightService.facet() as any).of(cssColor) ] : [];
|
||||
this.editorView.dispatch({ effects: this.highlightFacetCompartment.reconfigure(ext) });
|
||||
}
|
||||
|
||||
toggleWordWrap(): void {
|
||||
this.wordWrap.update(v => !v);
|
||||
|
||||
@@ -559,7 +550,9 @@ export class MarkdownEditorComponent implements OnInit, OnDestroy {
|
||||
if (this.autosaveTimer) {
|
||||
clearTimeout(this.autosaveTimer);
|
||||
}
|
||||
|
||||
if (this.themeSubscription) {
|
||||
this.themeSubscription.unsubscribe();
|
||||
}
|
||||
if (this.editorView) {
|
||||
this.editorView.destroy();
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ import { TagFilterStore } from '../../core/stores/tag-filter.store';
|
||||
imports: [CommonModule, ScrollableOverlayDirective],
|
||||
template: `
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="p-2 border-b border-gray-200 dark:border-gray-800 space-y-2">
|
||||
<div class="p-2 border-b border-border dark:border-gray-800 space-y-2">
|
||||
<div *ngIf="activeTag() as t" class="flex items-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 px-2 py-1">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-surface1 dark:bg-card text-main dark:text-main px-2 py-1">
|
||||
<svg class="h-3.5 w-3.5" 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>
|
||||
Filtre: #{{ t }}
|
||||
</span>
|
||||
<button type="button" (click)="clearTagFilter()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
|
||||
<button type="button" (click)="clearTagFilter()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-surface2/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
|
||||
<svg class="h-3.5 w-3.5" 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>
|
||||
@@ -25,13 +25,13 @@ import { TagFilterStore } from '../../core/stores/tag-filter.store';
|
||||
[value]="query()"
|
||||
(input)="onQuery($any($event.target).value)"
|
||||
placeholder="Rechercher..."
|
||||
class="w-full rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm" />
|
||||
class="w-full rounded border border-border dark:border-border bg-card dark:bg-main px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
</div>
|
||||
<div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay>
|
||||
<ul class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<li *ngFor="let n of filtered()" class="p-3 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer" (click)="openNote.emit(n.id)">
|
||||
<li *ngFor="let n of filtered()" class="p-3 hover:bg-surface1 dark:hover:bg-card cursor-pointer" (click)="openNote.emit(n.id)">
|
||||
<div class="text-sm font-semibold truncate">{{ n.title }}</div>
|
||||
<div class="text-xs text-gray-500 truncate">{{ n.filePath }}</div>
|
||||
<div class="text-xs text-muted truncate">{{ n.filePath }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
|
||||
|
||||
<!-- Bottom sheet -->
|
||||
<div
|
||||
class="fixed inset-x-0 bottom-0 z-[81] max-h-[85vh] rounded-t-2xl bg-white dark:bg-slate-900 shadow-2xl
|
||||
class="fixed inset-x-0 bottom-0 z-[81] max-h-[85vh] rounded-t-2xl bg-card dark:bg-main shadow-2xl
|
||||
transition-transform duration-300 ease-out
|
||||
sm:left-auto sm:right-4 sm:bottom-4 sm:top-auto sm:w-[420px] sm:rounded-2xl sm:max-h-[80vh]"
|
||||
[class.translate-y-full]="!isVisible"
|
||||
@@ -31,20 +31,20 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
|
||||
(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-slate-300 dark:bg-slate-700"></div>
|
||||
<div class="h-1.5 w-12 rounded-full bg-muted dark:bg-surface2"></div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-4 overflow-y-auto max-h-[75vh] sm:max-h-[70vh]" appScrollableOverlay>
|
||||
<h2 class="sr-only">Sommaire</h2>
|
||||
<ul class="space-y-1 text-sm text-slate-800 dark:text-slate-200">
|
||||
<ul class="space-y-1 text-sm text-main dark:text-main">
|
||||
<li *ngFor="let h of headings; let i = index">
|
||||
<button
|
||||
type="button"
|
||||
(click)="onGo(h.id)"
|
||||
class="w-full text-left block px-3 py-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition active:scale-[0.98]"
|
||||
class="w-full text-left block px-3 py-2 rounded-lg hover:bg-surface1 dark:hover:bg-card transition active:scale-[0.98]"
|
||||
[style.paddingLeft.rem]="(h.level - 1) * 0.75 + 0.75"
|
||||
[attr.data-index]="i">
|
||||
<span class="text-slate-400 dark:text-slate-500 mr-2 text-xs">{{ getLevelIndicator(h.level) }}</span>
|
||||
<span class="text-muted dark:text-muted mr-2 text-xs">{{ getLevelIndicator(h.level) }}</span>
|
||||
<span>{{ h.text }}</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<header class="note-header flex flex-col gap-2 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button"
|
||||
class="flex-shrink-0 inline-flex items-center justify-center w-8 h-8 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-slate-200 transition-all duration-150 shadow-sm"
|
||||
class="flex-shrink-0 inline-flex items-center justify-center w-8 h-8 rounded-lg border border-border dark:border-border bg-card dark:bg-card text-muted dark:text-muted hover:bg-surface1 dark:hover:bg-surface2 hover:text-main dark:hover:text-main transition-all duration-150 shadow-sm"
|
||||
aria-label="Copier le chemin"
|
||||
title="Copier le chemin"
|
||||
(click)="copyRequested.emit()">
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
.parameters-page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-main);
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.parameters-container {
|
||||
max-width: 56rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.parameters-header {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.parameters-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.parameters-subtitle {
|
||||
font-size: 1rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.parameters-section {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Grid Layout */
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.setting-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setting-hint {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Button Group */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mode-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1rem;
|
||||
background: var(--bg-muted);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-main);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
min-width: 6.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mode-button:hover {
|
||||
background: var(--bg-main);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mode-button.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Theme Grid */
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-muted);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 0.625rem;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.theme-card:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px var(--shadow-color);
|
||||
}
|
||||
|
||||
.theme-card.active {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
height: 4rem;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.theme-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.theme-check {
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Stats Placeholder */
|
||||
.stats-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 3rem 1.5rem;
|
||||
background: var(--bg-muted);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.stats-description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 1.5rem 0;
|
||||
max-width: 32rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.stats-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: var(--bg-main);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.parameters-footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer-note svg {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (min-width: 640px) {
|
||||
.parameters-page {
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.parameters-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.parameters-section {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.mode-button {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<div class="parameters-page">
|
||||
<div class="parameters-container">
|
||||
<!-- Header -->
|
||||
<header class="parameters-header">
|
||||
<h1 class="parameters-title">Parameters</h1>
|
||||
<p class="parameters-subtitle">Customize your ObsiViewer experience</p>
|
||||
</header>
|
||||
|
||||
<!-- Appearance Section -->
|
||||
<section class="parameters-section">
|
||||
<h2 class="section-title">
|
||||
<svg class="section-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
Appearance
|
||||
</h2>
|
||||
|
||||
<div class="grid-layout">
|
||||
<!-- Mode Selection -->
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">Display Mode</label>
|
||||
<div class="button-group">
|
||||
<button
|
||||
*ngFor="let m of modes"
|
||||
(click)="setMode(m)"
|
||||
class="mode-button"
|
||||
[class.active]="prefs().mode === m"
|
||||
[attr.aria-label]="'Set mode to ' + m">
|
||||
<svg *ngIf="m === 'system'" class="button-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
<svg *ngIf="m === 'light'" class="button-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>
|
||||
<svg *ngIf="m === 'dark'" class="button-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
<span class="button-text">{{ m | titlecase }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="setting-hint">Choose how ObsiViewer determines light or dark appearance</p>
|
||||
</div>
|
||||
|
||||
<!-- Theme Selection -->
|
||||
<div class="setting-group full-width">
|
||||
<label class="setting-label">Color Theme</label>
|
||||
<div class="theme-grid">
|
||||
<button
|
||||
*ngFor="let t of themes"
|
||||
(click)="setTheme(t)"
|
||||
class="theme-card"
|
||||
[class.active]="prefs().theme === t"
|
||||
[attr.aria-label]="'Set theme to ' + themeLabels[t]">
|
||||
<div class="theme-preview" [style.background]="getThemePreview(t)"></div>
|
||||
<div class="theme-info">
|
||||
<span class="theme-name">{{ themeLabels[t] }}</span>
|
||||
<svg *ngIf="prefs().theme === t" class="theme-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<p class="setting-hint">Select a color scheme for the entire application including the editor</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Language Section -->
|
||||
<section class="parameters-section">
|
||||
<h2 class="section-title">
|
||||
<svg class="section-icon" width="20" height="20" 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"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<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>
|
||||
Language
|
||||
</h2>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">Interface Language</label>
|
||||
<div class="button-group">
|
||||
<button
|
||||
*ngFor="let l of languages"
|
||||
(click)="setLanguage(l)"
|
||||
class="mode-button"
|
||||
[class.active]="prefs().language === l"
|
||||
[attr.aria-label]="'Set language to ' + l">
|
||||
<span class="button-text">{{ l === 'fr' ? 'Français' : 'English' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="setting-hint">Interface language (i18n implementation coming soon)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Vault Stats Section (Placeholder) -->
|
||||
<section class="parameters-section">
|
||||
<h2 class="section-title">
|
||||
<svg class="section-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||||
</svg>
|
||||
Vault Statistics
|
||||
</h2>
|
||||
|
||||
<div class="stats-placeholder">
|
||||
<svg class="stats-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18 17V9"/>
|
||||
<path d="M13 17V5"/>
|
||||
<path d="M8 17v-3"/>
|
||||
</svg>
|
||||
<h3 class="stats-title">Vault Analytics</h3>
|
||||
<p class="stats-description">Coming soon: View detailed statistics about your vault including note count, tag usage, file sizes, and more.</p>
|
||||
<button class="stats-button" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||||
</svg>
|
||||
View Stats (Coming Soon)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="parameters-footer">
|
||||
<p class="footer-note">
|
||||
<svg width="16" height="16" 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"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>
|
||||
Changes are saved automatically and applied immediately
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component, inject, signal, effect } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ThemeService, ThemeMode, ThemeId, Language } from '../../core/services/theme.service';
|
||||
import { ToastService } from '../../shared/toast/toast.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-parameters',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './parameters.page.html',
|
||||
styleUrls: ['./parameters.page.css']
|
||||
})
|
||||
export class ParametersPage {
|
||||
private themeService = inject(ThemeService);
|
||||
private toastService = inject(ToastService);
|
||||
|
||||
// Reactive prefs
|
||||
prefs = signal(this.themeService.prefsValue);
|
||||
|
||||
modes: ThemeMode[] = ['system', 'light', 'dark'];
|
||||
themes: ThemeId[] = ['light', 'dark', 'obsidian', 'nord', 'notion', 'github', 'discord', 'monokai'];
|
||||
languages: Language[] = ['fr', 'en'];
|
||||
|
||||
// Labels pour les thèmes
|
||||
themeLabels: Record<ThemeId, string> = {
|
||||
light: 'Pure White',
|
||||
dark: 'Blue',
|
||||
obsidian: 'Obsidian',
|
||||
nord: 'Nord',
|
||||
notion: 'Notion',
|
||||
github: 'GitHub',
|
||||
discord: 'Discord',
|
||||
monokai: 'Monokai'
|
||||
};
|
||||
|
||||
constructor() {
|
||||
// S'abonner aux changements de préférences
|
||||
effect(() => {
|
||||
const subscription = this.themeService.onPrefs$.subscribe(prefs => {
|
||||
this.prefs.set(prefs);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
setMode(mode: ThemeMode): void {
|
||||
this.themeService.setMode(mode);
|
||||
this.showToast('Mode changed successfully');
|
||||
}
|
||||
|
||||
setTheme(theme: ThemeId): void {
|
||||
this.themeService.setTheme(theme);
|
||||
this.showToast('Theme changed successfully');
|
||||
}
|
||||
|
||||
setLanguage(language: Language): void {
|
||||
this.themeService.setLanguage(language);
|
||||
this.showToast('Language changed successfully');
|
||||
}
|
||||
|
||||
private showToast(message: string): void {
|
||||
this.toastService.success(message, 2000);
|
||||
}
|
||||
|
||||
getThemePreview(themeId: ThemeId): string {
|
||||
// Couleurs de preview pour chaque thème
|
||||
const previews: Record<ThemeId, string> = {
|
||||
light: 'linear-gradient(135deg, #ffffff 0%, #f7f7f7 100%)',
|
||||
dark: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
|
||||
obsidian: 'linear-gradient(135deg, #fafaf8 0%, #a89984 100%)',
|
||||
nord: 'linear-gradient(135deg, #2e3440 0%, #88c0d0 100%)',
|
||||
notion: 'linear-gradient(135deg, #ffffff 0%, #f7f7f5 100%)',
|
||||
github: 'linear-gradient(135deg, #ffffff 0%, #0969da 100%)',
|
||||
discord: 'linear-gradient(135deg, #313338 0%, #5865f2 100%)',
|
||||
monokai: 'linear-gradient(135deg, #272822 0%, #a6e22e 100%)'
|
||||
};
|
||||
return previews[themeId];
|
||||
}
|
||||
}
|
||||
@@ -22,49 +22,49 @@ interface QuickLinkCountsUi {
|
||||
<div class="p-3">
|
||||
<ul class="text-sm space-y-1">
|
||||
<li>
|
||||
<button (click)="select('all')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
|
||||
<button (click)="select('all')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||
<span class="flex items-center gap-2"><span>🗂️</span> <span>All Pages</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().all" color="slate"></app-badge-count>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
|
||||
<button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||
<span class="flex items-center gap-2"><span>❤️</span> <span>Favoris</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().favorites" color="rose"></app-badge-count>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="select('publish')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
|
||||
<button (click)="select('publish')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||
<span class="flex items-center gap-2"><span>🌐</span> <span>Publish</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().publish" color="green"></app-badge-count>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="select('drafts')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
|
||||
<button (click)="select('drafts')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||
<span class="flex items-center gap-2"><span>📝</span> <span>Draft</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().drafts" color="emerald"></app-badge-count>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="select('templates')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text left">
|
||||
<button (click)="select('templates')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text left">
|
||||
<span class="flex items-center gap-2"><span>📑</span> <span>Template</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().templates" color="amber"></app-badge-count>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="select('tasks')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
|
||||
<button (click)="select('tasks')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||
<span class="flex items-center gap-2"><span>🗒️</span> <span>Task</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().tasks" color="indigo"></app-badge-count>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="select('private')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
|
||||
<button (click)="select('private')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||
<span class="flex items-center gap-2"><span>🔒</span> <span>Private</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().private" color="purple"></app-badge-count>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="select('archive')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-slate-200/10 transition w-full text-left">
|
||||
<button (click)="select('archive')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||
<span class="flex items-center gap-2"><span>🗃️</span> <span>Archive</span></span>
|
||||
<app-badge-count class="ml-auto" [count]="counts().archive" color="stone"></app-badge-count>
|
||||
</button>
|
||||
|
||||
@@ -14,15 +14,15 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
|
||||
standalone: true,
|
||||
imports: [CommonModule, FileExplorerComponent, QuickLinksComponent, ScrollableOverlayDirective, TrashExplorerComponent],
|
||||
template: `
|
||||
<aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-white dark:bg-gray-900 shadow-2xl z-40 transform transition-all duration-300 ease-out flex flex-col"
|
||||
<aside class="fixed left-0 top-0 bottom-0 w-80 max-w-[80vw] bg-card dark:bg-main shadow-2xl z-40 transform transition-all duration-300 ease-out flex flex-col"
|
||||
[class.-translate-x-full]="!mobileNav.sidebarOpen()"
|
||||
[class.translate-x-0]="mobileNav.sidebarOpen()">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800 bg-gradient-to-r from-nimbus-50 to-transparent dark:from-nimbus-900/20">
|
||||
<div class="flex items-center justify-between p-4 border-b border-border dark:border-gray-800 bg-gradient-to-r from-nimbus-50 to-transparent dark:from-nimbus-900/20">
|
||||
<h2 class="text-lg font-semibold truncate">{{ vaultName || 'ObsiViewer' }}</h2>
|
||||
<button
|
||||
(click)="mobileNav.toggleSidebar()"
|
||||
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-all active:scale-95 transform flex-shrink-0">
|
||||
class="p-2 rounded-full hover:bg-surface1 dark:hover:bg-card transition-all active:scale-95 transform flex-shrink-0">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -30,27 +30,27 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
|
||||
<!-- Scrollable Content -->
|
||||
<div class="flex-1 overflow-y-auto min-h-0" appScrollableOverlay>
|
||||
<!-- Section Tests (dev-only) -->
|
||||
<section *ngIf="env.features.showTestSection" class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||
<section *ngIf="env.features.showTestSection" class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||
(click)="open.tests = !open.tests">
|
||||
<span>Section Tests</span>
|
||||
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.tests">{{ open.tests ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tests">{{ open.tests ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.tests" class="px-3 py-2">
|
||||
<button
|
||||
(click)="onMarkdownPlaygroundClick()"
|
||||
class="w-full text-left block text-sm px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-all active:scale-[0.98] transform">
|
||||
class="w-full text-left block text-sm px-3 py-2 rounded-lg hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 text-main dark:text-main hover:text-main dark:hover:text-gray-100 transition-all active:scale-[0.98] transform">
|
||||
🧪 Markdown Playground
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Links accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||
(click)="open.quick = !open.quick">
|
||||
<span>Quick Links</span>
|
||||
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.quick">{{ open.quick ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.quick">{{ open.quick ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.quick" class="pt-1">
|
||||
<app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
|
||||
@@ -58,11 +58,11 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
|
||||
</section>
|
||||
|
||||
<!-- Folders accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||
(click)="open.folders = !open.folders">
|
||||
<span>Folders</span>
|
||||
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.folders" class="px-1 py-1">
|
||||
<app-file-explorer [nodes]="nodes" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolder($event)" (fileSelected)="onSelect($event)"></app-file-explorer>
|
||||
@@ -70,45 +70,45 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
|
||||
</section>
|
||||
|
||||
<!-- Tags accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||
(click)="open.tags = !open.tags">
|
||||
<span>Tags</span>
|
||||
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.tags">{{ open.tags ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tags">{{ open.tags ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.tags" class="px-2 py-2">
|
||||
<ul class="space-y-1 text-sm">
|
||||
<li *ngFor="let t of tags" class="flex items-center gap-2">
|
||||
<button (click)="onTagSelected(t.name)" class="flex-1 text-left px-3 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-all active:scale-[0.98] transform truncate">
|
||||
<button (click)="onTagSelected(t.name)" class="flex-1 text-left px-3 py-2 rounded-lg hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-all active:scale-[0.98] transform truncate">
|
||||
<span>🏷️</span>
|
||||
<span class="ml-1">{{ t.name }}</span>
|
||||
</button>
|
||||
<span class="text-xs text-gray-500 font-medium min-w-[2rem] text-right">{{ t.count }}</span>
|
||||
<span class="text-xs text-muted font-medium min-w-[2rem] text-right">{{ t.count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trash accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800 active:bg-gray-200 dark:active:bg-gray-700 transition-colors"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||
(click)="open.trash = !open.trash; onFolder('.trash')">
|
||||
<span class="flex items-center gap-2">Trash</span>
|
||||
<span class="text-xs text-gray-500 transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.trash" class="px-1 py-2">
|
||||
<ng-container *ngIf="trashHasContent(); else emptyTrash">
|
||||
<app-trash-explorer (folderSelected)="onFolder($event)"></app-trash-explorer>
|
||||
</ng-container>
|
||||
<ng-template #emptyTrash>
|
||||
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div>
|
||||
<div class="px-3 py-2 text-muted text-sm">La corbeille est vide</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="h-14 border-t border-gray-200 dark:border-gray-800 flex items-center justify-between px-4 text-xs text-gray-500 bg-gray-50 dark:bg-gray-900/50">
|
||||
<div class="h-14 border-t border-border dark:border-gray-800 flex items-center justify-between px-4 text-xs text-muted bg-surface1 dark:bg-main/50">
|
||||
<span>ObsiViewer</span>
|
||||
<span class="text-[10px] opacity-60">v1.0</span>
|
||||
</div>
|
||||
|
||||
@@ -16,35 +16,35 @@ import { VaultService } from '../../../services/vault.service';
|
||||
template: `
|
||||
<div class="h-full flex flex-col overflow-hidden select-none">
|
||||
<!-- Header -->
|
||||
<div class="h-12 flex items-center justify-between px-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
|
||||
<div class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</div>
|
||||
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800" title="Hide Sidebar">⟨⟨</button>
|
||||
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" title="Hide Sidebar">⟨⟨</button>
|
||||
</div>
|
||||
|
||||
<!-- Content (scroll) -->
|
||||
<div class="flex-1 overflow-y-auto min-h-0" appScrollableOverlay>
|
||||
<!-- Section Tests (dev-only) -->
|
||||
<section *ngIf="env.features.showTestSection" class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
<section *ngIf="env.features.showTestSection" class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||
(click)="open.tests = !open.tests">
|
||||
<span>Section Tests</span>
|
||||
<span class="text-xs text-gray-500">{{ open.tests ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted">{{ open.tests ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.tests" class="px-3 py-2">
|
||||
<button
|
||||
(click)="onMarkdownPlaygroundClick()"
|
||||
class="w-full text-left block text-sm px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">
|
||||
class="w-full text-left block text-sm px-2 py-1.5 rounded hover:bg-surface1 dark:hover:bg-card text-main dark:text-main hover:text-main dark:hover:text-gray-100">
|
||||
Markdown Playground
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quick Links accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||
(click)="open.quick = !open.quick">
|
||||
<span>Quick Links</span>
|
||||
<span class="text-xs text-gray-500">{{ open.quick ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted">{{ open.quick ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.quick" class="pt-1">
|
||||
<app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
|
||||
@@ -52,11 +52,11 @@ import { VaultService } from '../../../services/vault.service';
|
||||
</section>
|
||||
|
||||
<!-- Folders accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||
(click)="toggleFoldersSection()">
|
||||
<span>Folders</span>
|
||||
<span class="text-xs text-gray-500">{{ open.folders ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted">{{ open.folders ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.folders" class="px-1 py-1">
|
||||
<app-file-explorer
|
||||
@@ -70,31 +70,31 @@ import { VaultService } from '../../../services/vault.service';
|
||||
</section>
|
||||
|
||||
<!-- Tags accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||
(click)="open.tags = !open.tags">
|
||||
<span>Tags</span>
|
||||
<span class="text-xs text-gray-500">{{ open.tags ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted">{{ open.tags ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<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-gray-100 dark:hover:bg-gray-800 truncate">
|
||||
<button (click)="tagSelected.emit(t.name)" class="flex-1 text-left px-2 py-1 rounded hover:bg-surface1 dark:hover:bg-card truncate">
|
||||
<span>🏷️</span>
|
||||
<span class="ml-1">{{ t.name }}</span>
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">{{ t.count }}</span>
|
||||
<span class="text-xs text-muted">{{ t.count }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Trash accordion -->
|
||||
<section class="border-b border-gray-200 dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
<section class="border-b border-border dark:border-gray-800">
|
||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||
(click)="toggleTrashSection()">
|
||||
<span class="flex items-center gap-2">Trash</span>
|
||||
<span class="text-xs text-gray-500">{{ open.trash ? '▾' : '▸' }}</span>
|
||||
<span class="text-xs text-muted">{{ open.trash ? '▾' : '▸' }}</span>
|
||||
</button>
|
||||
<div *ngIf="open.trash" class="px-1 py-2">
|
||||
<ng-container *ngIf="trashHasContent(); else emptyTrash">
|
||||
@@ -108,14 +108,14 @@ import { VaultService } from '../../../services/vault.service';
|
||||
</app-file-explorer>
|
||||
</ng-container>
|
||||
<ng-template #emptyTrash>
|
||||
<div class="px-3 py-2 text-slate-400 text-sm">La corbeille est vide</div>
|
||||
<div class="px-3 py-2 text-muted text-sm">La corbeille est vide</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Footer placeholder -->
|
||||
<div class="h-14 border-t border-gray-200 dark:border-gray-800 flex items-center px-3 text-xs text-gray-500">ObsiViewer</div>
|
||||
<div class="h-14 border-t border-border dark:border-gray-800 flex items-center px-3 text-xs text-muted">ObsiViewer</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
@@ -13,11 +13,11 @@ const DEFAULT_MD_PATH_ABS = '/assets/samples/markdown-playground.md';
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, HttpClientModule, MarkdownViewerComponent],
|
||||
template: `
|
||||
<div class="h-full flex flex-col bg-gray-50 dark:bg-gray-900">
|
||||
<div class="h-full flex flex-col bg-surface1 dark:bg-main">
|
||||
<!-- Header -->
|
||||
<header class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Markdown Playground</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
<header class="bg-card dark:bg-card border-b border-border dark:border-border px-6 py-4">
|
||||
<h1 class="text-2xl font-semibold text-main dark:text-gray-100">Markdown Playground</h1>
|
||||
<p class="text-sm text-muted dark:text-muted mt-1">
|
||||
Page de test interne pour valider tous les formatages Markdown supportés par ObsiViewer.
|
||||
</p>
|
||||
</header>
|
||||
@@ -25,34 +25,34 @@ const DEFAULT_MD_PATH_ABS = '/assets/samples/markdown-playground.md';
|
||||
<!-- Content -->
|
||||
<div class="flex-1 flex gap-4 p-4 overflow-hidden">
|
||||
<!-- Editor Panel -->
|
||||
<div class="flex-1 flex flex-col bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750">
|
||||
<h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Markdown Source</h2>
|
||||
<div class="flex-1 flex flex-col bg-card dark:bg-card rounded-lg border border-border dark:border-border overflow-hidden">
|
||||
<div class="px-4 py-2 border-b border-border dark:border-border bg-surface1 dark:bg-gray-750">
|
||||
<h2 class="text-sm font-semibold text-main dark:text-main">Markdown Source</h2>
|
||||
</div>
|
||||
<textarea
|
||||
[ngModel]="sample()"
|
||||
(ngModelChange)="sample.set($event)"
|
||||
class="flex-1 p-4 font-mono text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 resize-none focus:outline-none"
|
||||
class="flex-1 p-4 font-mono text-sm bg-card dark:bg-card text-main dark:text-gray-100 resize-none focus:outline-none"
|
||||
placeholder="Entrez votre Markdown ici..."
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Preview Panel -->
|
||||
<div class="flex-1 flex flex-col bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Preview</h2>
|
||||
<div class="flex-1 flex flex-col bg-card dark:bg-card rounded-lg border border-border dark:border-border overflow-hidden">
|
||||
<div class="px-4 py-2 border-b border-border dark:border-border bg-surface1 dark:bg-gray-750 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-main dark:text-main">Preview</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
(click)="toggleViewMode()"
|
||||
class="text-xs px-3 py-1 rounded bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
|
||||
class="text-xs px-3 py-1 rounded bg-surface2 dark:bg-surface2 hover:bg-muted dark:hover:bg-gray-600 text-main dark:text-main"
|
||||
title="Toggle between inline and component view"
|
||||
>
|
||||
{{ useComponentView() ? 'Inline View' : 'Component View' }}
|
||||
</button>
|
||||
<button
|
||||
(click)="resetToDefault()"
|
||||
class="text-xs px-3 py-1 rounded bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
|
||||
class="text-xs px-3 py-1 rounded bg-surface2 dark:bg-surface2 hover:bg-muted dark:hover:bg-gray-600 text-main dark:text-main"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
@@ -71,7 +71,7 @@ const DEFAULT_MD_PATH_ABS = '/assets/samples/markdown-playground.md';
|
||||
<!-- Inline View -->
|
||||
<div
|
||||
*ngIf="!useComponentView()"
|
||||
class="prose prose-slate dark:prose-invert max-w-none"
|
||||
class="md-view"
|
||||
[innerHTML]="renderedHtml()"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user