chore: update TypeScript build info cache for Angular 20.3.3
This commit is contained in:
parent
0e604c06d0
commit
0b72a6b810
2
.angular/cache/20.3.3/app/.tsbuildinfo
vendored
2
.angular/cache/20.3.3/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
@ -20,6 +20,8 @@
|
|||||||
"tsConfig": "tsconfig.json",
|
"tsConfig": "tsconfig.json",
|
||||||
"styles": [
|
"styles": [
|
||||||
"node_modules/angular-calendar/css/angular-calendar.css",
|
"node_modules/angular-calendar/css/angular-calendar.css",
|
||||||
|
"src/styles/tokens.css",
|
||||||
|
"src/styles/components.css",
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
41
index.html
41
index.html
@ -1,35 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>ObsiWatcher - Obsidian Vault Viewer</title>
|
<title>ObsiWatcher - Obsidian Vault Viewer</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
'obs-l-bg-main': '#f7f7f7',
|
|
||||||
'obs-l-bg-secondary': '#eef0f2',
|
|
||||||
'obs-l-text-main': '#1a1a1a',
|
|
||||||
'obs-l-text-muted': '#5c6166',
|
|
||||||
'obs-l-border': '#d8dbe0',
|
|
||||||
'obs-l-accent': '#3a68d1',
|
|
||||||
'obs-d-bg-main': '#202123',
|
|
||||||
'obs-d-bg-secondary': '#2d2e30',
|
|
||||||
'obs-d-text-main': '#e3e3e3',
|
|
||||||
'obs-d-text-muted': '#9a9b9c',
|
|
||||||
'obs-d-border': '#3c3d3f',
|
|
||||||
'obs-d-accent': '#6f96e4',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"rxjs": "https://aistudiocdn.com/rxjs@^7.8.2?conditions=es2015",
|
"rxjs": "https://aistudiocdn.com/rxjs@^7.8.2?conditions=es2015",
|
||||||
"rxjs/operators": "https://aistudiocdn.com/rxjs@^7.8.2/operators?conditions=es2015",
|
"rxjs/operators": "https://aistudiocdn.com/rxjs@^7.8.2/operators?conditions=es2015",
|
||||||
@ -44,15 +20,10 @@
|
|||||||
"@angular/platform-browser": "https://next.esm.sh/@angular/platform-browser@^20.3.1?external=rxjs",
|
"@angular/platform-browser": "https://next.esm.sh/@angular/platform-browser@^20.3.1?external=rxjs",
|
||||||
"@angular/forms": "https://next.esm.sh/@angular/forms@^20.3.1?external=rxjs"
|
"@angular/forms": "https://next.esm.sh/@angular/forms@^20.3.1?external=rxjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/index.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-obs-l-bg-main dark:bg-obs-d-bg-main">
|
<body>
|
||||||
<app-root>
|
<app-root></app-root>
|
||||||
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: sans-serif; color: #5c6166;">
|
|
||||||
Loading application...
|
|
||||||
</div>
|
|
||||||
</app-root>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -194,7 +194,12 @@
|
|||||||
.resize-handle {
|
.resize-handle {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
min-width: 8px;
|
min-width: 8px;
|
||||||
background: linear-gradient(to bottom, transparent, rgba(99, 102, 241, 0.45), transparent);
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
color-mix(in srgb, var(--brand) 60%, transparent),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
transition: background 0.2s ease, width 0.2s ease, opacity 0.2s ease;
|
transition: background 0.2s ease, width 0.2s ease, opacity 0.2s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -210,7 +215,7 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background: rgba(99, 102, 241, 0.6);
|
background: color-mix(in srgb, var(--brand-700) 75%, transparent);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
@ -220,7 +225,12 @@
|
|||||||
.resize-handle:active {
|
.resize-handle:active {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
min-width: 10px;
|
min-width: 10px;
|
||||||
background: linear-gradient(to bottom, transparent, rgba(99, 102, 241, 0.6), transparent);
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
color-mix(in srgb, var(--brand-700) 70%, transparent),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle:hover::after,
|
.resize-handle:hover::after,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!-- ObsiViewer - Application optimisée pour mobile et desktop -->
|
<!-- ObsiViewer - Application optimisée pour mobile et desktop -->
|
||||||
<main class="relative flex min-h-screen flex-col bg-obs-l-bg-main text-obs-l-text-main dark:bg-obs-d-bg-main dark:text-obs-d-text-main lg:flex-row lg:h-screen lg:overflow-hidden">
|
<main class="relative flex min-h-screen flex-col bg-bg-main text-text-main lg:flex-row lg:h-screen lg:overflow-hidden">
|
||||||
@if (isRawViewOpen()) {
|
@if (isRawViewOpen()) {
|
||||||
<app-raw-view-overlay
|
<app-raw-view-overlay
|
||||||
[content]="rawNoteContent()"
|
[content]="rawNoteContent()"
|
||||||
@ -10,57 +10,52 @@
|
|||||||
></app-raw-view-overlay>
|
></app-raw-view-overlay>
|
||||||
}
|
}
|
||||||
<!-- Navigation latérale desktop -->
|
<!-- Navigation latérale desktop -->
|
||||||
<nav class="hidden w-14 flex-col items-center gap-4 border-r border-obs-l-border bg-obs-l-bg-main py-4 dark:border-obs-d-border dark:bg-obs-d-bg-main lg:flex">
|
<nav class="hidden w-14 flex-col items-center gap-4 border-r border-border bg-bg-main py-4 lg:flex">
|
||||||
<button
|
<button
|
||||||
(click)="setView('files')"
|
(click)="setView('files')"
|
||||||
class="rounded-lg p-2 transition hover:bg-obs-l-bg-secondary dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-icon"
|
||||||
[class.bg-obs-l-bg-secondary]="activeView() === 'files'"
|
[ngClass]="activeView() === 'files' ? 'btn-primary' : 'btn-ghost'"
|
||||||
[class.dark:bg-obs-d-bg-secondary]="activeView() === 'files'"
|
|
||||||
aria-label="Afficher les fichiers"
|
aria-label="Afficher les fichiers"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setView('search')"
|
(click)="setView('search')"
|
||||||
class="rounded-lg p-2 transition hover:bg-obs-l-bg-secondary dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-icon"
|
||||||
[class.bg-obs-l-bg-secondary]="activeView() === 'search'"
|
[ngClass]="activeView() === 'search' ? 'btn-primary' : 'btn-ghost'"
|
||||||
[class.dark:bg-obs-d-bg-secondary]="activeView() === 'search'"
|
|
||||||
aria-label="Ouvrir la recherche"
|
aria-label="Ouvrir la recherche"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setView('tags')"
|
(click)="setView('tags')"
|
||||||
class="rounded-lg p-2 transition hover:bg-obs-l-bg-secondary dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-icon"
|
||||||
[class.bg-obs-l-bg-secondary]="activeView() === 'tags'"
|
[ngClass]="activeView() === 'tags' ? 'btn-primary' : 'btn-ghost'"
|
||||||
[class.dark:bg-obs-d-bg-secondary]="activeView() === 'tags'"
|
|
||||||
aria-label="Afficher les tags"
|
aria-label="Afficher les tags"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setView('graph')"
|
(click)="setView('graph')"
|
||||||
class="rounded-lg p-2 transition hover:bg-obs-l-bg-secondary dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-icon"
|
||||||
[class.bg-obs-l-bg-secondary]="activeView() === 'graph'"
|
[ngClass]="activeView() === 'graph' ? 'btn-primary' : 'btn-ghost'"
|
||||||
[class.dark:bg-obs-d-bg-secondary]="activeView() === 'graph'"
|
|
||||||
aria-label="Afficher la vue graphe"
|
aria-label="Afficher la vue graphe"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setView('calendar')"
|
(click)="setView('calendar')"
|
||||||
class="rounded-lg p-2 transition hover:bg-obs-l-bg-secondary dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-icon"
|
||||||
[class.bg-obs-l-bg-secondary]="activeView() === 'calendar'"
|
[ngClass]="activeView() === 'calendar' ? 'btn-primary' : 'btn-ghost'"
|
||||||
[class.dark:bg-obs-d-bg-secondary]="activeView() === 'calendar'"
|
|
||||||
aria-label="Afficher le calendrier"
|
aria-label="Afficher le calendrier"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-obs-l-text-muted dark:text-obs-d-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@if (isDesktop() || isSidebarOpen()) {
|
@if (isDesktop() || isSidebarOpen()) {
|
||||||
<aside
|
<aside
|
||||||
class="fixed inset-y-0 left-0 z-40 flex h-screen w-[min(320px,85vw)] -translate-x-full transform flex-col border-r border-obs-l-border bg-obs-l-bg-secondary shadow-xl transition-transform duration-200 ease-in-out dark:border-obs-d-border dark:bg-obs-d-bg-secondary lg:static lg:h-auto lg:w-auto lg:translate-x-0 lg:shadow-none"
|
class="fixed inset-y-0 left-0 z-40 flex h-screen w-[min(320px,85vw)] -translate-x-full transform flex-col border-r border-border bg-bg-muted shadow-xl transition-transform duration-200 ease-in-out lg:static lg:h-auto lg:w-auto lg:translate-x-0 lg:shadow-none"
|
||||||
[class.translate-x-0]="isSidebarOpen() || isDesktop()"
|
[class.translate-x-0]="isSidebarOpen() || isDesktop()"
|
||||||
[class.pointer-events-none]="!isSidebarOpen() && !isDesktop()"
|
[class.pointer-events-none]="!isSidebarOpen() && !isDesktop()"
|
||||||
[style.width.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
[style.width.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
||||||
@ -69,27 +64,27 @@
|
|||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label="Arborescence de la voûte"
|
aria-label="Arborescence de la voûte"
|
||||||
>
|
>
|
||||||
<div class="flex h-full flex-col overflow-hidden"
|
<div class="flex h-full flex-col overflow-hidden bg-card"
|
||||||
[style.width.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
[style.width.px]="isDesktop() ? (isSidebarOpen() ? leftSidebarWidth() : 0) : null"
|
||||||
>
|
>
|
||||||
<div class="space-y-4 border-b border-obs-l-border bg-obs-l-bg-secondary/60 px-4 py-4 dark:border-obs-d-border dark:bg-obs-d-bg-secondary/60">
|
<div class="space-y-4 border-b border-border/80 bg-bg-muted/70 px-4 py-4">
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="flex h-10 w-10 items-center justify-center rounded-2xl border border-obs-l-border/60 bg-obs-l-bg-main/70 text-obs-l-text-muted shadow-sm dark:border-obs-d-border/60 dark:bg-obs-d-bg-main/60 dark:text-obs-d-text-muted">
|
<div class="flex h-10 w-10 items-center justify-center rounded-2xl border border-border/70 bg-card/80 text-text-muted shadow-subtle">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM5.5 21a6.5 6.5 0 0113 0" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM5.5 21a6.5 6.5 0 0113 0" /></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-sm font-semibold tracking-wide text-obs-l-text-main dark:text-obs-d-text-main">{{ vaultName() }}</span>
|
<span class="text-sm font-semibold tracking-wide text-text-main">{{ vaultName() }}</span>
|
||||||
<div class="mt-1 flex items-center gap-2 text-xs uppercase text-obs-l-text-muted dark:text-obs-d-text-muted">
|
<div class="mt-1 flex items-center gap-2 text-xs uppercase text-text-muted">
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-obs-l-bg-main/70 px-2 py-0.5 text-[0.65rem] font-semibold tracking-widest text-obs-l-text-main/80 dark:bg-obs-d-bg-main/60 dark:text-obs-d-text-main/80">{{ activeView() | titlecase }}</span>
|
<span class="inline-flex items-center gap-1 rounded-full bg-card/80 px-2 py-0.5 text-[0.65rem] font-semibold tracking-widest text-text-main/80">{{ activeView() | titlecase }}</span>
|
||||||
<span class="hidden text-[0.65rem] tracking-widest text-obs-l-text-muted/80 dark:text-obs-d-text-muted/70 sm:inline">Vue active</span>
|
<span class="hidden text-[0.65rem] tracking-widest text-text-muted/80 sm:inline">Vue active</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!isDesktop()) {
|
@if (!isDesktop()) {
|
||||||
<button
|
<button
|
||||||
class="rounded-xl border border-transparent bg-obs-l-bg-main/70 p-2 text-obs-l-text-muted transition hover:border-obs-l-border/50 hover:bg-obs-l-bg-main/90 dark:bg-obs-d-bg-main/60 dark:text-obs-d-text-muted dark:hover:border-obs-d-border/60 dark:hover:bg-obs-d-bg-main/70"
|
class="btn btn-icon btn-ghost"
|
||||||
(click)="closeSidebar()"
|
(click)="closeSidebar()"
|
||||||
aria-label="Fermer le panneau latéral"
|
aria-label="Fermer le panneau latéral"
|
||||||
>
|
>
|
||||||
@ -98,12 +93,12 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-obs-l-border/60 bg-obs-l-bg-main/75 px-3 py-3 shadow-inner dark:border-obs-d-border/60 dark:bg-obs-d-bg-main/60">
|
<div class="rounded-2xl border border-border/70 bg-card/85 px-3 py-3 shadow-subtle">
|
||||||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||||
<button
|
<button
|
||||||
(click)="setView('files')"
|
(click)="setView('files')"
|
||||||
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-obs-l-text-muted transition duration-150 hover:border-obs-l-border/50 hover:bg-obs-l-bg-main/60 hover:text-obs-l-text-main dark:text-obs-d-text-muted dark:hover:border-obs-d-border/50 dark:hover:bg-obs-d-bg-main/60 dark:hover:text-obs-d-text-main"
|
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
||||||
[ngClass]="{ 'bg-obs-l-bg-main/80 text-obs-l-text-main shadow-sm dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-main': activeView() === 'files' }"
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'files' }"
|
||||||
aria-label="Afficher les fichiers"
|
aria-label="Afficher les fichiers"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
||||||
@ -111,8 +106,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setView('search')"
|
(click)="setView('search')"
|
||||||
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-obs-l-text-muted transition duration-150 hover:border-obs-l-border/50 hover:bg-obs-l-bg-main/60 hover:text-obs-l-text-main dark:text-obs-d-text-muted dark:hover:border-obs-d-border/50 dark:hover:bg-obs-d-bg-main/60 dark:hover:text-obs-d-text-main"
|
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
||||||
[ngClass]="{ 'bg-obs-l-bg-main/80 text-obs-l-text-main shadow-sm dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-main': activeView() === 'search' }"
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'search' }"
|
||||||
aria-label="Ouvrir la recherche"
|
aria-label="Ouvrir la recherche"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||||
@ -120,8 +115,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setView('tags')"
|
(click)="setView('tags')"
|
||||||
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-obs-l-text-muted transition duration-150 hover:border-obs-l-border/50 hover:bg-obs-l-bg-main/60 hover:text-obs-l-text-main dark:text-obs-d-text-muted dark:hover:border-obs-d-border/50 dark:hover:bg-obs-d-bg-main/60 dark:hover:text-obs-d-text-main"
|
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
||||||
[ngClass]="{ 'bg-obs-l-bg-main/80 text-obs-l-text-main shadow-sm dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-main': activeView() === 'tags' }"
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'tags' }"
|
||||||
aria-label="Afficher les tags"
|
aria-label="Afficher les tags"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
|
||||||
@ -129,8 +124,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setView('calendar')"
|
(click)="setView('calendar')"
|
||||||
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-obs-l-text-muted transition duration-150 hover:border-obs-l-border/50 hover:bg-obs-l-bg-main/60 hover:text-obs-l-text-main dark:text-obs-d-text-muted dark:hover:border-obs-d-border/50 dark:hover:bg-obs-d-bg-main/60 dark:hover:text-obs-d-text-main"
|
class="group flex flex-col items-center gap-1 rounded-xl border border-transparent px-2.5 py-2 text-xs font-medium text-text-muted transition duration-150 hover:border-border/60 hover:bg-bg-muted/70 hover:text-text-main"
|
||||||
[ngClass]="{ 'bg-obs-l-bg-main/80 text-obs-l-text-main shadow-sm dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-main': activeView() === 'calendar' }"
|
[ngClass]="{ 'bg-bg-muted/80 text-text-main shadow-subtle': activeView() === 'calendar' }"
|
||||||
[attr.aria-pressed]="activeView() === 'calendar'"
|
[attr.aria-pressed]="activeView() === 'calendar'"
|
||||||
aria-label="Afficher l'agenda"
|
aria-label="Afficher l'agenda"
|
||||||
>
|
>
|
||||||
@ -164,55 +159,54 @@
|
|||||||
placeholder="Rechercher dans la voûte..."
|
placeholder="Rechercher dans la voûte..."
|
||||||
[ngModel]="sidebarSearchTerm()"
|
[ngModel]="sidebarSearchTerm()"
|
||||||
(ngModelChange)="updateSearchTerm($event)"
|
(ngModelChange)="updateSearchTerm($event)"
|
||||||
class="w-full rounded-xl border border-obs-l-border/60 bg-obs-l-bg-main/80 px-3 py-2.5 text-sm text-obs-l-text-main placeholder:text-obs-l-text-muted shadow-sm focus:outline-none focus:ring-2 focus:ring-obs-l-accent dark:border-obs-d-border/60 dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-main dark:placeholder:text-obs-d-text-muted dark:focus:ring-obs-d-accent"
|
class="input"
|
||||||
aria-label="Rechercher dans la voûte"
|
aria-label="Rechercher dans la voûte"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@if (activeTagDisplay(); as tagDisplay) {
|
@if (activeTagDisplay(); as tagDisplay) {
|
||||||
<div class="flex items-center justify-between rounded-lg border border-obs-l-border/60 bg-obs-l-bg-main/75 px-3 py-2 dark:border-obs-d-border/60 dark:bg-obs-d-bg-main/65">
|
<div class="flex items-center justify-between rounded-lg border border-border bg-card px-3 py-2">
|
||||||
<div class="flex items-center gap-2 text-sm font-medium text-obs-l-text-main dark:text-obs-d-text-main">
|
<div class="flex items-center gap-2 text-sm font-medium text-text-main">
|
||||||
<span>🔖</span>
|
<span>🔖</span>
|
||||||
<span class="truncate">{{ tagDisplay }}</span>
|
<span class="truncate">{{ tagDisplay }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="text-xs text-obs-l-text-muted transition hover:text-obs-l-text-main dark:text-obs-d-text-muted dark:hover:text-obs-d-text-main" (click)="clearTagFilter()">Effacer</button>
|
<button class="btn btn-sm btn-ghost" (click)="clearTagFilter()">Effacer</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between px-2">
|
<div class="flex items-center justify-between px-2">
|
||||||
<h3 class="text-xs font-semibold uppercase tracking-wide text-obs-l-text-muted dark:text-obs-d-text-muted">Résultats</h3>
|
<h3 class="text-xs font-semibold uppercase tracking-wide text-text-muted">Résultats</h3>
|
||||||
<span class="text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">{{ searchResults().length }}</span>
|
<span class="text-xs text-text-muted">{{ searchResults().length }}</span>
|
||||||
</div>
|
</div>
|
||||||
@if (searchResults().length > 0) {
|
@if (searchResults().length > 0) {
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
@for (note of searchResults(); track note.id) {
|
@for (note of searchResults(); track note.id) {
|
||||||
<li
|
<li
|
||||||
(click)="selectNote(note.id)"
|
(click)="selectNote(note.id)"
|
||||||
class="cursor-pointer rounded-lg px-3 py-2 transition hover:bg-obs-l-bg-main/70 dark:hover:bg-obs-d-bg-main/60"
|
class="cursor-pointer rounded-lg border border-transparent bg-card px-3 py-2 transition hover:border-border hover:bg-bg-muted"
|
||||||
[class.bg-obs-l-bg-main]="selectedNoteId() === note.id"
|
[ngClass]="{ 'border-border bg-bg-muted': selectedNoteId() === note.id }"
|
||||||
[class.dark:bg-obs-d-bg-main]="selectedNoteId() === note.id"
|
|
||||||
>
|
>
|
||||||
<div class="truncate text-sm font-semibold text-obs-l-text-main dark:text-obs-d-text-main">{{ note.title }}</div>
|
<div class="truncate text-sm font-semibold text-text-main">{{ note.title }}</div>
|
||||||
<div class="truncate text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">{{ note.content.substring(0, 100) }}</div>
|
<div class="truncate text-xs text-text-muted">{{ note.content.substring(0, 100) }}</div>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
} @else {
|
} @else {
|
||||||
<p class="rounded-lg border border-dashed border-obs-l-border/60 px-3 py-3 text-sm text-obs-l-text-muted dark:border-obs-d-border/60 dark:text-obs-d-text-muted">Aucun résultat pour cette recherche.</p>
|
<p class="rounded-lg border border-dashed border-border px-3 py-3 text-sm text-text-muted">Aucun résultat pour cette recherche.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (calendarSelectionLabel() || calendarSearchState() !== 'idle' || calendarResults().length > 0 || calendarSearchError()) {
|
@if (calendarSelectionLabel() || calendarSearchState() !== 'idle' || calendarResults().length > 0 || calendarSearchError()) {
|
||||||
<div class="space-y-3 rounded-xl border border-obs-l-border/60 bg-obs-l-bg-main/75 p-3 shadow-inner dark:border-obs-d-border/60 dark:bg-obs-d-bg-main/65">
|
<div class="space-y-3 rounded-xl border border-border bg-card p-3 shadow-subtle">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-obs-l-text-main dark:text-obs-d-text-main">Résultats du calendrier</h3>
|
<h3 class="text-sm font-semibold text-text-main">Résultats du calendrier</h3>
|
||||||
@if (calendarSelectionLabel(); as selectionLabel) {
|
@if (calendarSelectionLabel(); as selectionLabel) {
|
||||||
<p class="text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">{{ selectionLabel }}</p>
|
<p class="text-xs text-text-muted">{{ selectionLabel }}</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="rounded-lg border border-transparent px-2 py-1 text-xs text-obs-l-text-muted transition hover:border-obs-l-border/60 hover:bg-obs-l-bg-main/80 hover:text-obs-l-text-main dark:text-obs-d-text-muted dark:hover:border-obs-d-border/60 dark:hover:bg-obs-d-bg-main/70 dark:hover:text-obs-d-text-main"
|
class="btn btn-sm btn-ghost"
|
||||||
(click)="clearCalendarResults()"
|
(click)="clearCalendarResults()"
|
||||||
aria-label="Effacer les résultats du calendrier"
|
aria-label="Effacer les résultats du calendrier"
|
||||||
>
|
>
|
||||||
@ -221,21 +215,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (calendarSearchState() === 'loading') {
|
@if (calendarSearchState() === 'loading') {
|
||||||
<div class="rounded-lg bg-obs-l-bg-main/80 px-3 py-2 text-xs text-obs-l-text-muted dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-muted">Recherche en cours...</div>
|
<div class="rounded-lg bg-bg-muted px-3 py-2 text-xs text-text-muted">Recherche en cours...</div>
|
||||||
} @else if (calendarSearchError(); as calError) {
|
} @else if (calendarSearchError(); as calError) {
|
||||||
<div class="rounded-lg border border-red-500/40 bg-red-500/10 px-3 py-2 text-xs text-red-500 dark:border-red-500/30 dark:bg-red-500/15 dark:text-red-400">{{ calError }}</div>
|
<div class="rounded-lg border border-red-500/40 bg-red-500/10 px-3 py-2 text-xs text-red-500 dark:border-red-500/30 dark:bg-red-500/15 dark:text-red-400">{{ calError }}</div>
|
||||||
} @else if (calendarResults().length === 0) {
|
} @else if (calendarResults().length === 0) {
|
||||||
<div class="rounded-lg bg-obs-l-bg-main/80 px-3 py-2 text-xs text-obs-l-text-muted dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-muted">Sélectionnez une date dans le calendrier pour voir les notes correspondantes.</div>
|
<div class="rounded-lg bg-bg-muted px-3 py-2 text-xs text-text-muted">Sélectionnez une date dans le calendrier pour voir les notes correspondantes.</div>
|
||||||
} @else {
|
} @else {
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
@for (file of calendarResults(); track file.id) {
|
@for (file of calendarResults(); track file.id) {
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
(click)="selectNote(file.id)"
|
(click)="selectNote(file.id)"
|
||||||
class="w-full rounded-lg border border-transparent bg-obs-l-bg-main/85 px-3 py-2 text-left transition hover:border-obs-l-border/60 hover:bg-obs-l-bg-main dark:bg-obs-d-bg-main/70 dark:hover:border-obs-d-border/60 dark:hover:bg-obs-d-bg-main"
|
class="w-full rounded-lg border border-transparent bg-card px-3 py-2 text-left transition hover:border-border hover:bg-bg-muted"
|
||||||
>
|
>
|
||||||
<div class="truncate text-sm font-semibold text-obs-l-text-main dark:text-obs-d-text-main">{{ file.title }}</div>
|
<div class="truncate text-sm font-semibold text-text-main">{{ file.title }}</div>
|
||||||
<div class="mt-1 flex flex-wrap gap-2 text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">
|
<div class="mt-1 flex flex-wrap gap-2 text-xs text-text-muted">
|
||||||
<span>Créé : {{ file.createdAt | date:'mediumDate' }}</span>
|
<span>Créé : {{ file.createdAt | date:'mediumDate' }}</span>
|
||||||
<span>Modifié : {{ file.updatedAt | date:'mediumDate' }}</span>
|
<span>Modifié : {{ file.updatedAt | date:'mediumDate' }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -250,9 +244,9 @@
|
|||||||
}
|
}
|
||||||
@case ('calendar') {
|
@case ('calendar') {
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<div class="flex items-center justify-between border-b border-obs-l-border/60 bg-obs-l-bg-main/70 px-4 py-3 dark:border-obs-d-border/60 dark:bg-obs-d-bg-main/60">
|
<div class="flex items-center justify-between border-b border-border bg-card px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-obs-l-text-main dark:text-obs-d-text-main">Vue agenda</h3>
|
<h3 class="text-sm font-semibold text-text-main">Vue agenda</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto px-3 py-4">
|
<div class="flex-1 overflow-auto px-3 py-4">
|
||||||
@ -290,12 +284,12 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="flex min-w-0 flex-col bg-obs-l-bg-main pb-16 dark:bg-obs-d-bg-main lg:flex-1 lg:min-h-0 lg:overflow-hidden lg:pb-0">
|
<section class="flex min-w-0 flex-col bg-bg-main pb-16 lg:flex-1 lg:min-h-0 lg:overflow-hidden lg:pb-0">
|
||||||
<header class="flex flex-col gap-4 border-b border-obs-l-border/60 bg-obs-l-bg-main/95 px-4 py-3 backdrop-blur-xs dark:border-obs-d-border dark:bg-obs-d-bg-main/95 lg:px-6">
|
<header class="flex flex-col gap-4 border-b border-border bg-bg-main/95 px-4 py-3 backdrop-blur-xs lg:px-6">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<button
|
<button
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary lg:hidden"
|
class="btn btn-icon btn-ghost lg:hidden"
|
||||||
(click)="toggleSidebar()"
|
(click)="toggleSidebar()"
|
||||||
[attr.aria-expanded]="isSidebarOpen()"
|
[attr.aria-expanded]="isSidebarOpen()"
|
||||||
aria-label="Basculer le menu"
|
aria-label="Basculer le menu"
|
||||||
@ -303,7 +297,7 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="hidden rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary lg:inline-flex"
|
class="hidden btn btn-icon btn-ghost lg:inline-flex"
|
||||||
(click)="toggleSidebar()"
|
(click)="toggleSidebar()"
|
||||||
[attr.aria-expanded]="isSidebarOpen()"
|
[attr.aria-expanded]="isSidebarOpen()"
|
||||||
aria-label="Afficher ou masquer la barre latérale gauche"
|
aria-label="Afficher ou masquer la barre latérale gauche"
|
||||||
@ -316,20 +310,20 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="min-w-0 flex flex-col gap-1">
|
<div class="min-w-0 flex flex-col gap-1">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<span class="inline-flex items-center rounded-lg border border-obs-l-border bg-obs-l-bg-secondary/60 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-obs-l-text-muted dark:border-obs-d-border dark:bg-obs-d-bg-secondary/60 dark:text-obs-d-text-muted">{{ vaultName() }}</span>
|
<span class="inline-flex items-center rounded-lg border border-border bg-bg-muted/70 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-text-muted">{{ vaultName() }}</span>
|
||||||
<h1 class="text-lg font-semibold leading-tight text-obs-l-text-main dark:text-obs-d-text-main">ObsiWatcher</h1>
|
<h1 class="text-lg font-semibold leading-tight text-text-main">ObsiWatcher</h1>
|
||||||
</div>
|
</div>
|
||||||
@if (selectedNoteBreadcrumb().length > 0) {
|
@if (selectedNoteBreadcrumb().length > 0) {
|
||||||
<p class="truncate text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">{{ selectedNoteBreadcrumb().join(' / ') }}</p>
|
<p class="truncate text-xs text-text-muted">{{ selectedNoteBreadcrumb().join(' / ') }}</p>
|
||||||
} @else {
|
} @else {
|
||||||
<p class="truncate text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">Aucune note sélectionnée</p>
|
<p class="truncate text-xs text-text-muted">Aucune note sélectionnée</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden items-center gap-2 lg:flex">
|
<div class="hidden items-center gap-2 lg:flex">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary disabled:opacity-40 disabled:pointer-events-none"
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
||||||
(click)="toggleRawView()"
|
(click)="toggleRawView()"
|
||||||
[disabled]="!selectedNote()"
|
[disabled]="!selectedNote()"
|
||||||
aria-label="Afficher le markdown brut"
|
aria-label="Afficher le markdown brut"
|
||||||
@ -345,7 +339,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary disabled:opacity-40 disabled:pointer-events-none"
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
||||||
(click)="downloadCurrentNote()"
|
(click)="downloadCurrentNote()"
|
||||||
[disabled]="!selectedNote()"
|
[disabled]="!selectedNote()"
|
||||||
aria-label="Télécharger le fichier markdown"
|
aria-label="Télécharger le fichier markdown"
|
||||||
@ -361,7 +355,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="toggleTheme()"
|
(click)="toggleTheme()"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-icon btn-ghost"
|
||||||
aria-label="Basculer le thème"
|
aria-label="Basculer le thème"
|
||||||
>
|
>
|
||||||
@if (isDarkMode()) {
|
@if (isDarkMode()) {
|
||||||
@ -372,7 +366,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="toggleOutline()"
|
(click)="toggleOutline()"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-icon btn-ghost"
|
||||||
[attr.aria-expanded]="isOutlineOpen()"
|
[attr.aria-expanded]="isOutlineOpen()"
|
||||||
aria-label="Basculer la barre latérale droite"
|
aria-label="Basculer la barre latérale droite"
|
||||||
>
|
>
|
||||||
@ -388,7 +382,7 @@
|
|||||||
<div class="flex items-center gap-2 lg:hidden">
|
<div class="flex items-center gap-2 lg:hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary disabled:opacity-40 disabled:pointer-events-none"
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
||||||
(click)="toggleRawView()"
|
(click)="toggleRawView()"
|
||||||
[disabled]="!selectedNote()"
|
[disabled]="!selectedNote()"
|
||||||
aria-label="Afficher le markdown brut"
|
aria-label="Afficher le markdown brut"
|
||||||
@ -404,7 +398,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary disabled:opacity-40 disabled:pointer-events-none"
|
class="btn btn-icon btn-ghost disabled:opacity-40 disabled:pointer-events-none"
|
||||||
(click)="downloadCurrentNote()"
|
(click)="downloadCurrentNote()"
|
||||||
[disabled]="!selectedNote()"
|
[disabled]="!selectedNote()"
|
||||||
aria-label="Télécharger le fichier markdown"
|
aria-label="Télécharger le fichier markdown"
|
||||||
@ -420,7 +414,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="toggleTheme()"
|
(click)="toggleTheme()"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-icon btn-ghost"
|
||||||
aria-label="Basculer le thème"
|
aria-label="Basculer le thème"
|
||||||
>
|
>
|
||||||
@if (isDarkMode()) {
|
@if (isDarkMode()) {
|
||||||
@ -431,7 +425,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="toggleOutline()"
|
(click)="toggleOutline()"
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-icon btn-ghost"
|
||||||
[attr.aria-expanded]="isOutlineOpen()"
|
[attr.aria-expanded]="isOutlineOpen()"
|
||||||
aria-label="Basculer la barre latérale droite"
|
aria-label="Basculer la barre latérale droite"
|
||||||
>
|
>
|
||||||
@ -444,18 +438,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 items-center gap-3">
|
<div class="flex flex-1 items-center gap-3">
|
||||||
<div class="relative w-full flex-1 min-w-0">
|
<div class="relative w-full flex-1 min-w-0">
|
||||||
<svg class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-obs-l-text-muted dark:text-obs-d-text-muted" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
<svg class="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-text-muted" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Rechercher dans la voûte..."
|
placeholder="Rechercher dans la voûte..."
|
||||||
[ngModel]="sidebarSearchTerm()"
|
[ngModel]="sidebarSearchTerm()"
|
||||||
(ngModelChange)="updateSearchTerm($event, true)"
|
(ngModelChange)="updateSearchTerm($event, true)"
|
||||||
class="w-full rounded-full border border-obs-l-border bg-obs-l-bg-secondary/60 pl-11 pr-4 py-2.5 text-sm text-obs-l-text-main placeholder:text-obs-l-text-muted shadow-sm focus:outline-none focus:ring-2 focus:ring-obs-l-accent dark:border-obs-d-border dark:bg-obs-d-bg-secondary/60 dark:text-obs-d-text-main dark:placeholder:text-obs-d-text-muted dark:focus:ring-obs-d-accent"
|
class="w-full rounded-full border border-border bg-bg-muted/70 pl-11 pr-4 py-2.5 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
aria-label="Rechercher dans la voûte"
|
aria-label="Rechercher dans la voûte"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="hidden rounded-full border border-obs-l-border px-3 py-2 text-xs font-semibold uppercase tracking-wide text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:border-obs-d-border dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary lg:inline-flex"
|
class="hidden rounded-full border border-border px-3 py-2 text-xs font-semibold uppercase tracking-wide text-text-muted transition hover:bg-bg-muted lg:inline-flex"
|
||||||
(click)="toggleOutline()"
|
(click)="toggleOutline()"
|
||||||
[attr.aria-pressed]="isOutlineOpen()"
|
[attr.aria-pressed]="isOutlineOpen()"
|
||||||
>
|
>
|
||||||
@ -475,7 +469,7 @@
|
|||||||
></app-note-viewer>
|
></app-note-viewer>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<p class="text-obs-l-text-muted dark:text-obs-d-text-muted">Sélectionnez une note pour commencer</p>
|
<p class="text-text-muted">Sélectionnez une note pour commencer</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -500,7 +494,7 @@
|
|||||||
|
|
||||||
@if (isDesktop() || isOutlineOpen()) {
|
@if (isDesktop() || isOutlineOpen()) {
|
||||||
<aside
|
<aside
|
||||||
class="fixed inset-x-0 bottom-0 z-40 flex max-h-[80vh] flex-col overflow-hidden border-t border-obs-l-border bg-obs-l-bg-secondary shadow-2xl transition-all duration-200 ease-out dark:border-obs-d-border dark:bg-obs-d-bg-secondary lg:static lg:max-h-none lg:border-l lg:shadow-none"
|
class="fixed inset-x-0 bottom-0 z-40 flex max-h-[80vh] flex-col overflow-hidden border-t border-border bg-card shadow-2xl transition-all duration-200 ease-out lg:static lg:max-h-none lg:border-l lg:shadow-none"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'translate-y-0 opacity-100 pointer-events-auto': isOutlineOpen() || isDesktop(),
|
'translate-y-0 opacity-100 pointer-events-auto': isOutlineOpen() || isDesktop(),
|
||||||
'translate-y-full opacity-0 pointer-events-none': !isOutlineOpen() && !isDesktop()
|
'translate-y-full opacity-0 pointer-events-none': !isOutlineOpen() && !isDesktop()
|
||||||
@ -514,37 +508,37 @@
|
|||||||
<div class="flex flex-col lg:h-full">
|
<div class="flex flex-col lg:h-full">
|
||||||
@if (!isDesktop()) {
|
@if (!isDesktop()) {
|
||||||
<div class="px-4 pt-3">
|
<div class="px-4 pt-3">
|
||||||
<div class="mx-auto mb-3 h-1.5 w-12 rounded-full bg-obs-l-border dark:bg-obs-d-border"></div>
|
<div class="mx-auto mb-3 h-1.5 w-12 rounded-full bg-border"></div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-base font-semibold text-obs-l-text-main dark:text-obs-d-text-main">Navigation</h2>
|
<h2 class="text-base font-semibold text-text-main">Navigation</h2>
|
||||||
<button
|
<button
|
||||||
class="rounded-lg p-2 text-obs-l-text-muted transition hover:bg-obs-l-bg-main/70 dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-main/60"
|
class="btn btn-icon btn-ghost"
|
||||||
(click)="closeOutlinePanel()"
|
(click)="closeOutlinePanel()"
|
||||||
aria-label="Fermer la barre latérale droite"
|
aria-label="Fermer la barre latérale droite"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs uppercase tracking-wide text-obs-l-text-muted dark:text-obs-d-text-muted">Table des matières & calendrier</p>
|
<p class="mt-1 text-xs uppercase tracking-wide text-text-muted">Table des matières & calendrier</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="hidden border-b border-obs-l-border px-4 py-3 text-sm font-semibold uppercase tracking-wide text-obs-l-text-muted dark:border-obs-d-border dark:text-obs-d-text-muted lg:block">
|
<div class="hidden border-b border-border px-4 py-3 text-sm font-semibold uppercase tracking-wide text-text-muted lg:block">
|
||||||
Outline
|
Outline
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-y-auto px-4 py-4">
|
<div class="flex-1 overflow-y-auto px-4 py-4">
|
||||||
@if (tableOfContents().length > 0) {
|
@if (tableOfContents().length > 0) {
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
@for (entry of tableOfContents(); track entry.id) {
|
@for (entry of tableOfContents(); track entry.id) {
|
||||||
<li [style.padding-left.rem]="(entry.level - 1) * 0.75" class="flex items-start gap-2 text-sm text-obs-l-text-muted dark:text-obs-d-text-muted">
|
<li [style.padding-left.rem]="(entry.level - 1) * 0.75" class="flex items-start gap-2 text-sm text-text-muted">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mt-1 h-3 w-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="mt-1 h-3 w-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" /></svg>
|
||||||
<a (click)="scrollToHeading(entry.id)" class="cursor-pointer leading-tight transition hover:text-obs-l-text-main dark:hover:text-obs-d-text-main">
|
<a (click)="scrollToHeading(entry.id)" class="cursor-pointer leading-tight text-text-muted transition hover:text-text-main">
|
||||||
{{ entry.text }}
|
{{ entry.text }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
} @else {
|
} @else {
|
||||||
<p class="text-sm italic text-obs-l-text-muted dark:text-obs-d-text-muted">Aucun titre dans cette note.</p>
|
<p class="text-sm italic text-text-muted">Aucun titre dans cette note.</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@ import { VaultService } from './services/vault.service';
|
|||||||
import { MarkdownService } from './services/markdown.service';
|
import { MarkdownService } from './services/markdown.service';
|
||||||
import { MarkdownViewerService } from './services/markdown-viewer.service';
|
import { MarkdownViewerService } from './services/markdown-viewer.service';
|
||||||
import { DownloadService } from './core/services/download.service';
|
import { DownloadService } from './core/services/download.service';
|
||||||
|
import { ThemeService } from './app/core/services/theme.service';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { FileExplorerComponent } from './components/file-explorer/file-explorer.component';
|
import { FileExplorerComponent } from './components/file-explorer/file-explorer.component';
|
||||||
@ -45,10 +46,10 @@ export class AppComponent implements OnDestroy {
|
|||||||
private markdownService = inject(MarkdownService);
|
private markdownService = inject(MarkdownService);
|
||||||
private markdownViewerService = inject(MarkdownViewerService);
|
private markdownViewerService = inject(MarkdownViewerService);
|
||||||
private downloadService = inject(DownloadService);
|
private downloadService = inject(DownloadService);
|
||||||
|
private readonly themeService = inject(ThemeService);
|
||||||
private elementRef = inject(ElementRef);
|
private elementRef = inject(ElementRef);
|
||||||
|
|
||||||
// --- State Signals ---
|
// --- State Signals ---
|
||||||
isDarkMode = signal<boolean>(true);
|
|
||||||
isSidebarOpen = signal<boolean>(true);
|
isSidebarOpen = signal<boolean>(true);
|
||||||
isOutlineOpen = signal<boolean>(true);
|
isOutlineOpen = signal<boolean>(true);
|
||||||
activeView = signal<'files' | 'graph' | 'tags' | 'search' | 'calendar'>('files');
|
activeView = signal<'files' | 'graph' | 'tags' | 'search' | 'calendar'>('files');
|
||||||
@ -88,6 +89,8 @@ export class AppComponent implements OnDestroy {
|
|||||||
private calendarSearchTriggered = false;
|
private calendarSearchTriggered = false;
|
||||||
private pendingWikiNavigation = signal<{ noteId: string; heading?: string; block?: string } | null>(null);
|
private pendingWikiNavigation = signal<{ noteId: string; heading?: string; block?: string } | null>(null);
|
||||||
|
|
||||||
|
readonly isDarkMode = this.themeService.isDark;
|
||||||
|
|
||||||
// --- Data Signals ---
|
// --- Data Signals ---
|
||||||
fileTree = this.vaultService.fileTree;
|
fileTree = this.vaultService.fileTree;
|
||||||
graphData = this.vaultService.graphData;
|
graphData = this.vaultService.graphData;
|
||||||
@ -197,6 +200,8 @@ export class AppComponent implements OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.themeService.initFromStorage();
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('resize', this.resizeHandler, { passive: true });
|
window.addEventListener('resize', this.resizeHandler, { passive: true });
|
||||||
}
|
}
|
||||||
@ -207,15 +212,6 @@ export class AppComponent implements OnDestroy {
|
|||||||
this.isOutlineOpen.set(false);
|
this.isOutlineOpen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Effect to update the DOM with the dark class
|
|
||||||
effect(() => {
|
|
||||||
if (this.isDarkMode()) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const isDesktop = this.isDesktopView();
|
const isDesktop = this.isDesktopView();
|
||||||
if (isDesktop && !this.wasDesktop) {
|
if (isDesktop && !this.wasDesktop) {
|
||||||
@ -298,7 +294,7 @@ export class AppComponent implements OnDestroy {
|
|||||||
|
|
||||||
// --- Methods ---
|
// --- Methods ---
|
||||||
toggleTheme(): void {
|
toggleTheme(): void {
|
||||||
this.isDarkMode.update(value => !value);
|
this.themeService.toggleTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidebar(): void {
|
toggleSidebar(): void {
|
||||||
|
99
src/app/core/services/theme.service.ts
Normal file
99
src/app/core/services/theme.service.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { DestroyRef, Inject, Injectable, effect, signal, computed } from '@angular/core';
|
||||||
|
|
||||||
|
export type ThemeName = 'light' | 'dark';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ThemeService {
|
||||||
|
private static readonly STORAGE_KEY = 'obsiwatcher.theme';
|
||||||
|
|
||||||
|
private readonly document = this.doc;
|
||||||
|
private readonly prefersDarkQuery = typeof window !== 'undefined'
|
||||||
|
? window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
private readonly currentTheme = signal<ThemeName>(this.detectSystemTheme());
|
||||||
|
|
||||||
|
readonly theme = computed(() => this.currentTheme());
|
||||||
|
readonly isDark = computed(() => this.currentTheme() === 'dark');
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DOCUMENT) private readonly doc: Document,
|
||||||
|
private readonly destroyRef: DestroyRef
|
||||||
|
) {
|
||||||
|
effect(() => {
|
||||||
|
const theme = this.currentTheme();
|
||||||
|
this.applyTheme(theme);
|
||||||
|
this.persist(theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.prefersDarkQuery) {
|
||||||
|
const listener = (event: MediaQueryListEvent) => {
|
||||||
|
if (!this.getStoredTheme()) {
|
||||||
|
this.currentTheme.set(event.matches ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.prefersDarkQuery.addEventListener('change', listener);
|
||||||
|
this.destroyRef.onDestroy(() => {
|
||||||
|
this.prefersDarkQuery?.removeEventListener('change', listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initFromStorage(): void {
|
||||||
|
const stored = this.getStoredTheme();
|
||||||
|
if (stored) {
|
||||||
|
this.currentTheme.set(stored);
|
||||||
|
} else {
|
||||||
|
this.currentTheme.set(this.detectSystemTheme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(theme: ThemeName): void {
|
||||||
|
this.currentTheme.set(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTheme(): void {
|
||||||
|
this.currentTheme.update(theme => (theme === 'light' ? 'dark' : 'light'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private detectSystemTheme(): ThemeName {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyTheme(theme: ThemeName): void {
|
||||||
|
const root = this.document.documentElement;
|
||||||
|
root.setAttribute('data-theme', theme);
|
||||||
|
if (theme === 'dark') {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private persist(theme: ThemeName): void {
|
||||||
|
try {
|
||||||
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(ThemeService.STORAGE_KEY, theme);
|
||||||
|
} catch {
|
||||||
|
// Ignore storage failures (private browsing, etc.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStoredTheme(): ThemeName | null {
|
||||||
|
try {
|
||||||
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const stored = window.localStorage.getItem(ThemeService.STORAGE_KEY) as ThemeName | null;
|
||||||
|
return stored === 'light' || stored === 'dark' ? stored : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
<ul class="p-2 space-y-1">
|
<ul class="space-y-1 p-2">
|
||||||
@for (node of nodes(); track node.name) {
|
@for (node of nodes(); track node.name) {
|
||||||
<li>
|
<li>
|
||||||
@if (node.type === 'folder') {
|
@if (node.type === 'folder') {
|
||||||
@let folder = node;
|
@let folder = node;
|
||||||
<div>
|
<div>
|
||||||
<button (click)="toggleFolder(folder)" class="w-full flex items-center text-left p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
|
<button (click)="toggleFolder(folder)" class="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm font-semibold text-text-main transition hover:bg-bg-muted">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="font-semibold text-sm">{{ folder.name }}</span>
|
<span class="truncate">{{ folder.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
@if (folder.isOpen) {
|
@if (folder.isOpen) {
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
@ -19,11 +19,10 @@
|
|||||||
} @else {
|
} @else {
|
||||||
@let file = node;
|
@let file = node;
|
||||||
<button (click)="onFileSelected(file.id)"
|
<button (click)="onFileSelected(file.id)"
|
||||||
class="w-full text-left p-1.5 rounded text-sm transition-colors"
|
class="flex w-full items-center gap-3 rounded-lg px-3 py-1.5 text-left text-sm transition"
|
||||||
[class]="file.id === selectedNoteId()
|
[ngClass]="file.id === selectedNoteId() ? 'bg-brand text-white shadow-subtle' : 'text-text-main hover:bg-bg-muted'">
|
||||||
? 'bg-obs-l-accent text-white dark:bg-obs-d-accent'
|
<span class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border bg-card text-xs font-semibold uppercase text-text-muted">{{ file.name | slice:0:2 }}</span>
|
||||||
: 'hover:bg-gray-200 dark:hover:bg-gray-600'">
|
<span class="truncate">{{ file.name }}</span>
|
||||||
<span class="pl-6">{{ file.name }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
|
@ -12,18 +12,18 @@ interface SimulatedNode extends GraphNode {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-graph-view',
|
selector: 'app-graph-view',
|
||||||
template: `
|
template: `
|
||||||
<div class="w-full h-full relative">
|
<div class="relative h-full w-full">
|
||||||
<svg #graphSvg class="w-full h-full">
|
<svg #graphSvg class="w-full h-full">
|
||||||
<g class="links">
|
<g class="links">
|
||||||
@for (edge of graphData().edges; track edge.source + '-' + edge.target) {
|
@for (edge of graphData().edges; track edge.source + '-' + edge.target) {
|
||||||
<path [attr.d]="getEdgePath(edge)" class="stroke-obs-l-border dark:stroke-obs-d-border" fill="none"></path>
|
<path [attr.d]="getEdgePath(edge)" class="stroke-border/80" fill="none"></path>
|
||||||
}
|
}
|
||||||
</g>
|
</g>
|
||||||
<g class="nodes">
|
<g class="nodes">
|
||||||
@for (node of simulatedNodes(); track node.id) {
|
@for (node of simulatedNodes(); track node.id) {
|
||||||
<g [attr.transform]="'translate(' + node.x + ',' + node.y + ')'" (click)="selectNode(node.id)" class="cursor-pointer group">
|
<g [attr.transform]="'translate(' + node.x + ',' + node.y + ')'" (click)="selectNode(node.id)" class="cursor-pointer group">
|
||||||
<circle r="5" class="fill-current text-obs-l-accent dark:text-obs-d-accent group-hover:r-7 transition-all"></circle>
|
<circle r="5" class="fill-accent transition-all group-hover:r-7"></circle>
|
||||||
<text y="-12" text-anchor="middle" class="text-xs fill-current text-obs-l-text-main dark:text-obs-d-text-main pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">{{ node.label }}</text>
|
<text y="-12" text-anchor="middle" class="pointer-events-none text-xs fill-text-main opacity-0 transition-opacity group-hover:opacity-100">{{ node.label }}</text>
|
||||||
</g>
|
</g>
|
||||||
}
|
}
|
||||||
</g>
|
</g>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<header class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<header class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs uppercase tracking-wide text-obs-l-text-muted dark:text-obs-d-text-muted">Calendrier</p>
|
<p class="text-xs uppercase tracking-wide text-text-muted">Calendrier</p>
|
||||||
<h2 class="text-lg font-semibold text-obs-l-text-main dark:text-obs-d-text-main">
|
<h2 class="text-lg font-semibold text-text-main">
|
||||||
{{ viewDate() | calendarDate: 'monthViewTitle' : 'fr' }}
|
{{ viewDate() | calendarDate: 'monthViewTitle' : 'fr' }}
|
||||||
</h2>
|
</h2>
|
||||||
@if (selectionLabel(); as label) {
|
@if (selectionLabel(); as label) {
|
||||||
<span class="mt-1 inline-flex items-center rounded-full bg-obs-l-bg-secondary/70 px-2 py-0.5 text-xs font-medium text-obs-l-text-muted dark:bg-obs-d-bg-secondary/60 dark:text-obs-d-text-muted">
|
<span class="mt-1 inline-flex items-center rounded-full border border-border bg-bg-muted px-2 py-0.5 text-xs font-medium text-text-muted">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@ -16,7 +16,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="shiftMonth(-1)"
|
(click)="shiftMonth(-1)"
|
||||||
aria-label="Mois précédent"
|
aria-label="Mois précédent"
|
||||||
class="h-9 w-9 rounded-full border border-obs-l-border bg-obs-l-bg-main text-lg leading-none text-obs-l-text-main transition hover:bg-obs-l-bg-secondary dark:border-obs-d-border dark:bg-obs-d-bg-main dark:text-obs-d-text-main dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-icon"
|
||||||
|
[ngClass]="'bg-card text-text-main'">
|
||||||
>
|
>
|
||||||
‹
|
‹
|
||||||
</button>
|
</button>
|
||||||
@ -24,7 +25,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="goToToday()"
|
(click)="goToToday()"
|
||||||
aria-label="Revenir à aujourd'hui"
|
aria-label="Revenir à aujourd'hui"
|
||||||
class="inline-flex items-center gap-1 rounded-full border border-obs-l-border bg-obs-l-bg-main px-3 py-1.5 text-sm font-medium text-obs-l-text-main transition hover:bg-obs-l-bg-secondary dark:border-obs-d-border dark:bg-obs-d-bg-main dark:text-obs-d-text-main dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-secondary"
|
||||||
>
|
>
|
||||||
Aujourd'hui
|
Aujourd'hui
|
||||||
</button>
|
</button>
|
||||||
@ -32,14 +33,14 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="shiftMonth(1)"
|
(click)="shiftMonth(1)"
|
||||||
aria-label="Mois suivant"
|
aria-label="Mois suivant"
|
||||||
class="h-9 w-9 rounded-full border border-obs-l-border bg-obs-l-bg-main text-lg leading-none text-obs-l-text-main transition hover:bg-obs-l-bg-secondary dark:border-obs-d-border dark:bg-obs-d-bg-main dark:text-obs-d-text-main dark:hover:bg-obs-d-bg-secondary"
|
class="btn btn-sm btn-icon bg-card text-text-main"
|
||||||
>
|
>
|
||||||
›
|
›
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-obs-l-border bg-obs-l-bg-main/80 shadow-panel backdrop-blur-xs dark:border-obs-d-border dark:bg-obs-d-bg-secondary/80">
|
<div class="rounded-2xl border border-border bg-card shadow-surface backdrop-blur-xs">
|
||||||
<mwl-calendar-month-view
|
<mwl-calendar-month-view
|
||||||
class="calendar-compact"
|
class="calendar-compact"
|
||||||
[viewDate]="viewDate()"
|
[viewDate]="viewDate()"
|
||||||
@ -51,12 +52,12 @@
|
|||||||
></mwl-calendar-month-view>
|
></mwl-calendar-month-view>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-3 text-xs text-obs-l-text-muted dark:text-obs-d-text-muted">
|
<div class="flex flex-wrap items-center justify-center gap-3 text-xs text-text-muted">
|
||||||
<span class="inline-flex items-center gap-2 rounded-full bg-obs-l-bg-secondary/60 px-3 py-1 dark:bg-obs-d-bg-secondary/60">
|
<span class="inline-flex items-center gap-2 rounded-full border border-border bg-bg-muted px-3 py-1">
|
||||||
<span class="h-2 w-2 rounded-full bg-gradient-to-r from-green-500 to-emerald-400"></span>
|
<span class="h-2 w-2 rounded-full bg-gradient-to-r from-green-500 to-emerald-400"></span>
|
||||||
Création
|
Création
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-flex items-center gap-2 rounded-full bg-obs-l-bg-secondary/60 px-3 py-1 dark:bg-obs-d-bg-secondary/60">
|
<span class="inline-flex items-center gap-2 rounded-full border border-border bg-bg-muted px-3 py-1">
|
||||||
<span class="h-2 w-2 rounded-full bg-gradient-to-r from-indigo-400 to-indigo-600"></span>
|
<span class="h-2 w-2 rounded-full bg-gradient-to-r from-indigo-400 to-indigo-600"></span>
|
||||||
Modification
|
Modification
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,42 +1,42 @@
|
|||||||
@if(note(); as note) {
|
@if(note(); as note) {
|
||||||
<div class="max-w-7xl mx-auto flex">
|
<div class="mx-auto flex max-w-7xl">
|
||||||
<div class="flex-grow min-w-0 max-w-4xl p-8 lg:p-12">
|
<div class="flex-grow min-w-0 max-w-4xl px-6 py-8 lg:px-12 lg:py-12">
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<h1 class="text-4xl font-bold text-obs-l-text-main dark:text-gray-100 mb-2">{{ note.title }}</h1>
|
<h1 class="text-4xl font-bold text-text-main mb-2">{{ note.title }}</h1>
|
||||||
<div class="text-sm text-obs-l-text-muted dark:text-obs-d-text-muted">
|
<div class="text-sm text-text-muted">
|
||||||
<span>Updated: {{ note.updatedAt | date:'medium' }}</span>
|
<span>Mise à jour : {{ note.updatedAt | date:'medium' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
@for(tag of note.tags; track tag) {
|
@for(tag of note.tags; track tag) {
|
||||||
<span class="bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300 text-xs font-medium px-2.5 py-1 rounded-full">{{ tag }}</span>
|
<span class="chip text-sm">{{ tag }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@if (getFrontmatterKeys(note.frontmatter).length > 0) {
|
@if (getFrontmatterKeys(note.frontmatter).length > 0) {
|
||||||
<div class="mb-8 p-4 border border-obs-l-border dark:border-obs-d-border rounded-lg bg-obs-l-bg-secondary dark:bg-obs-d-bg-secondary">
|
<div class="mb-8 rounded-xl border border-border bg-card p-4 shadow-subtle">
|
||||||
<h3 class="font-semibold text-obs-l-text-muted dark:text-obs-d-text-muted mb-2 text-sm">Metadata</h3>
|
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-text-muted">Métadonnées</h3>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
|
<div class="grid grid-cols-2 gap-3 text-sm md:grid-cols-4">
|
||||||
@for (key of getFrontmatterKeys(note.frontmatter); track key) {
|
@for (key of getFrontmatterKeys(note.frontmatter); track key) {
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col gap-1">
|
||||||
<span class="text-obs-l-text-muted dark:text-obs-d-text-muted capitalize">{{ key }}</span>
|
<span class="text-xs uppercase tracking-wide text-text-muted">{{ key }}</span>
|
||||||
<span class="text-obs-l-text-main dark:text-obs-d-text-main font-medium">{{ note.frontmatter[key] }}</span>
|
<span class="font-medium text-text-main">{{ note.frontmatter[key] }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<article class="prose dark:prose-invert prose-lg max-w-none prose-p:text-obs-l-text-main dark:prose-p:text-obs-d-text-main prose-headings:text-obs-l-text-main dark:prose-headings:text-white" [innerHTML]="noteHtmlContent()">
|
<article class="prose prose-lg prose-headings:text-text-main prose-p:text-text-main max-w-none dark:prose-invert" [innerHTML]="noteHtmlContent()">
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@if (note.backlinks.length > 0) {
|
@if (note.backlinks.length > 0) {
|
||||||
<footer class="mt-12 border-t border-obs-l-border dark:border-obs-d-border pt-6">
|
<footer class="mt-12 border-t border-border pt-6">
|
||||||
<h2 class="text-xl font-semibold mb-4 text-obs-l-text-main dark:text-gray-300">Backlinks</h2>
|
<h2 class="mb-4 text-xl font-semibold text-text-main">Backlinks</h2>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
@for (backlinkId of note.backlinks; track backlinkId) {
|
@for (backlinkId of note.backlinks; track backlinkId) {
|
||||||
<li>
|
<li>
|
||||||
<button (click)="noteLinkClicked.emit(backlinkId)" class="text-obs-l-accent dark:text-obs-d-accent hover:underline">
|
<button (click)="noteLinkClicked.emit(backlinkId)" class="text-sm font-medium text-accent hover:underline">
|
||||||
{{ formatBacklinkId(backlinkId) }}
|
{{ formatBacklinkId(backlinkId) }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -47,16 +47,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (tableOfContents().length > 0) {
|
@if (tableOfContents().length > 0) {
|
||||||
<aside class="w-64 flex-shrink-0 hidden lg:block">
|
<aside class="hidden w-64 flex-shrink-0 lg:block">
|
||||||
<div class="sticky top-0 h-screen overflow-y-auto pt-8 lg:pt-12">
|
<div class="sticky top-0 h-screen overflow-y-auto px-4 pt-8 lg:pt-12">
|
||||||
<nav class="pb-8">
|
<nav class="rounded-xl border border-border bg-card p-4 shadow-subtle">
|
||||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-obs-l-text-muted dark:text-obs-d-text-muted mb-4">On this page</h3>
|
<h3 class="mb-4 text-sm font-semibold uppercase tracking-wide text-text-muted">Dans cette page</h3>
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
@for(item of tableOfContents(); track item.id) {
|
@for(item of tableOfContents(); track item.id) {
|
||||||
<li>
|
<li>
|
||||||
<button (click)="scrollToHeading(item.id)"
|
<button (click)="scrollToHeading(item.id)"
|
||||||
[style.padding-left.rem]="(item.level - 1) * 1"
|
[style.padding-left.rem]="(item.level - 1) * 1"
|
||||||
class="block w-full text-left py-1 text-sm rounded text-obs-l-text-muted dark:text-obs-d-text-muted hover:text-obs-l-text-main dark:hover:text-obs-d-text-main transition-colors focus:outline-none focus:text-obs-l-text-main dark:focus:text-obs-d-text-main">
|
class="block w-full rounded-lg py-1 text-left text-sm text-text-muted transition-colors hover:text-text-main focus:outline-none focus:text-text-main">
|
||||||
{{ item.text }}
|
{{ item.text }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,8 +1,20 @@
|
|||||||
import { Component, ChangeDetectionStrategy, ElementRef, OnDestroy, afterNextRender, computed, effect, inject, input, output, signal } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
|
afterNextRender,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Note } from '../../types';
|
import { Note } from '../../types';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import mermaid, { MermaidConfig, RenderResult } from 'mermaid';
|
import mermaid from 'mermaid';
|
||||||
|
|
||||||
type MermaidLib = typeof mermaid;
|
type MermaidLib = typeof mermaid;
|
||||||
|
|
||||||
@ -10,14 +22,20 @@ type MathJaxInstance = {
|
|||||||
tex2chtml(math: string, options: { display: boolean }): HTMLElement;
|
tex2chtml(math: string, options: { display: boolean }): HTMLElement;
|
||||||
startup: {
|
startup: {
|
||||||
promise: Promise<void>;
|
promise: Promise<void>;
|
||||||
document: {
|
document: { clear(): void; updateDocument(): void };
|
||||||
clear(): void;
|
|
||||||
updateDocument(): void;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type MetadataEntryType = 'text' | 'date' | 'email' | 'url' | 'number' | 'boolean' | 'image' | 'list' | 'object';
|
type MetadataEntryType =
|
||||||
|
| 'text'
|
||||||
|
| 'date'
|
||||||
|
| 'email'
|
||||||
|
| 'url'
|
||||||
|
| 'number'
|
||||||
|
| 'boolean'
|
||||||
|
| 'image'
|
||||||
|
| 'list'
|
||||||
|
| 'object';
|
||||||
|
|
||||||
export interface WikiLinkActivation {
|
export interface WikiLinkActivation {
|
||||||
target: string;
|
target: string;
|
||||||
@ -40,12 +58,14 @@ interface MetadataEntry {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-note-viewer',
|
selector: 'app-note-viewer',
|
||||||
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="p-8 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]">
|
<div class="p-8 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]">
|
||||||
<div class="!mb-6 pb-2 border-b border-obs-l-border dark:border-obs-d-border">
|
<div class="!mb-6 pb-2 border-b border-border">
|
||||||
<h1 class="!text-4xl !font-bold !mb-3">{{ note().title }}</h1>
|
<h1 class="!text-4xl !font-bold !mb-3">{{ note().title }}</h1>
|
||||||
|
|
||||||
@if (note().tags.length > 0) {
|
@if (note().tags.length > 0) {
|
||||||
<div class="md-tag-group not-prose">
|
<div class="md-tag-group not-prose">
|
||||||
@for (tag of note().tags; track tag) {
|
@for (tag of note().tags; track tag) {
|
||||||
@ -86,35 +106,31 @@ interface MetadataEntry {
|
|||||||
[class.is-expanded]="metadataExpanded()"
|
[class.is-expanded]="metadataExpanded()"
|
||||||
[attr.id]="metadataListId()"
|
[attr.id]="metadataListId()"
|
||||||
aria-live="polite">
|
aria-live="polite">
|
||||||
|
|
||||||
@for (entry of metadataVisibleEntries(); track entry.key) {
|
@for (entry of metadataVisibleEntries(); track entry.key) {
|
||||||
<div class="metadata-panel__item" [attr.data-type]="entry.type">
|
<div class="metadata-panel__item" [attr.data-type]="entry.type">
|
||||||
<div class="metadata-panel__icon" aria-hidden="true">{{ entry.icon }}</div>
|
<div class="metadata-panel__icon" aria-hidden="true">{{ entry.icon }}</div>
|
||||||
<div class="metadata-panel__details">
|
<div class="metadata-panel__details">
|
||||||
<div class="metadata-panel__label">{{ entry.label }}</div>
|
|
||||||
@if (entry.type === 'image' && entry.imageUrl) {
|
@if (entry.type === 'image' && entry.imageUrl) {
|
||||||
<img class="metadata-panel__image" [src]="entry.imageUrl" [alt]="entry.label" loading="lazy">
|
<img class="metadata-panel__image" [src]="entry.imageUrl" [alt]="entry.label" loading="lazy">
|
||||||
} @else if ((entry.type === 'url' || entry.type === 'email') && entry.linkHref) {
|
} @else if ((entry.type === 'url' || entry.type === 'email') && entry.linkHref) {
|
||||||
<a class="metadata-panel__link" [href]="entry.linkHref" target="_blank" rel="noopener noreferrer">{{ entry.displayValue }}</a>
|
<a class="metadata-panel__link" [href]="entry.linkHref" target="_blank" rel="noopener noreferrer">{{ entry.displayValue }}</a>
|
||||||
} @else if (entry.type === 'boolean') {
|
} @else if (entry.type === 'boolean') {
|
||||||
<div class="metadata-panel__value metadata-panel__value--boolean" [class.is-true]="entry.booleanValue">
|
<span class="metadata-panel__boolean" [attr.data-value]="entry.booleanValue ? 'true' : 'false'">
|
||||||
<span class="metadata-panel__boolean-indicator" [class.is-checked]="entry.booleanValue"></span>
|
|
||||||
{{ entry.displayValue }}
|
{{ entry.displayValue }}
|
||||||
</div>
|
</span>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="metadata-panel__value">{{ entry.displayValue }}</div>
|
<span class="metadata-panel__text">{{ entry.displayValue }}</span>
|
||||||
}
|
}
|
||||||
|
<div class="metadata-panel__label">{{ entry.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (metadataEntries().length > maxMetadataPreviewItems) {
|
@if (metadataEntries().length > maxMetadataPreviewItems) {
|
||||||
<div class="metadata-panel__actions">
|
<div class="mt-3">
|
||||||
<button
|
<button type="button" class="btn btn-secondary" (click)="toggleMetadataPanel()">
|
||||||
type="button"
|
|
||||||
class="metadata-panel__toggle"
|
|
||||||
(click)="toggleMetadataPanel()"
|
|
||||||
[attr.aria-controls]="metadataListId()"
|
|
||||||
[attr.aria-expanded]="metadataExpanded()">
|
|
||||||
{{ metadataExpanded() ? translate('metadata.collapse') : translate('metadata.showAll') }}
|
{{ metadataExpanded() ? translate('metadata.collapse') : translate('metadata.showAll') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -122,18 +138,34 @@ interface MetadataEntry {
|
|||||||
</aside>
|
</aside>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="not-prose flex items-center gap-3 text-sm text-text-muted my-4">
|
||||||
|
<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">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||||
|
</svg>
|
||||||
|
{{ note().updatedAt | date:'medium' }}
|
||||||
|
</span>
|
||||||
|
<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">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 8c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM5.5 21a6.5 6.5 0 0113 0"/>
|
||||||
|
</svg>
|
||||||
|
{{ note().author ?? 'Auteur inconnu' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div [innerHTML]="sanitizedHtmlContent()"></div>
|
<div [innerHTML]="sanitizedHtmlContent()"></div>
|
||||||
|
|
||||||
@if (note().backlinks.length > 0) {
|
@if (note().backlinks.length > 0) {
|
||||||
<div class="mt-12 pt-6 border-t border-obs-l-border dark:border-obs-d-border not-prose">
|
<div class="mt-12 pt-6 border-t border-border not-prose">
|
||||||
<h2 class="text-2xl font-bold mb-4">Backlinks</h2>
|
<h2 class="text-2xl font-bold mb-4">Backlinks</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@for (backlinkId of note().backlinks; track backlinkId) {
|
@for (backlinkId of note().backlinks; track backlinkId) {
|
||||||
<li class="mb-2">
|
<li class="mb-2">
|
||||||
<a
|
<a
|
||||||
(click)="noteLinkClicked.emit(backlinkId)"
|
(click)="noteLinkClicked.emit(backlinkId)"
|
||||||
class="cursor-pointer text-obs-l-accent dark:text-obs-d-accent hover:underline"
|
class="cursor-pointer text-accent hover:underline">
|
||||||
>
|
|
||||||
{{ formatBacklinkId(backlinkId) }}
|
{{ formatBacklinkId(backlinkId) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -166,24 +198,29 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
private readonly metadataKeysToExclude = new Set(['tags', 'tag', 'keywords']);
|
private readonly metadataKeysToExclude = new Set(['tags', 'tag', 'keywords']);
|
||||||
private attachmentErrorCleanup: (() => void) | null = null;
|
private attachmentErrorCleanup: (() => void) | null = null;
|
||||||
private attachmentHandlersScheduled = false;
|
private attachmentHandlersScheduled = false;
|
||||||
|
|
||||||
readonly metadataExpanded = signal(false);
|
readonly metadataExpanded = signal(false);
|
||||||
readonly maxMetadataPreviewItems = 3;
|
readonly maxMetadataPreviewItems = 3;
|
||||||
|
|
||||||
sanitizedHtmlContent = computed<SafeHtml>(() =>
|
readonly sanitizedHtmlContent = computed<SafeHtml>(() =>
|
||||||
this.sanitizer.bypassSecurityTrustHtml(this.noteHtmlContent())
|
this.sanitizer.bypassSecurityTrustHtml(this.noteHtmlContent())
|
||||||
);
|
);
|
||||||
|
|
||||||
frontmatterTags = computed<string[]>(() => {
|
frontmatterTags = computed<string[]>(() => {
|
||||||
const tags = this.note().frontmatter?.tags;
|
const tags = this.note().frontmatter?.tags;
|
||||||
const headerTags = new Set((this.note().tags ?? []).map(tag => `${tag}`.trim().toLowerCase()).filter(Boolean));
|
const headerTags = new Set(
|
||||||
if (!tags) {
|
(this.note().tags ?? []).map(tag => `${tag}`.trim().toLowerCase()).filter(Boolean)
|
||||||
return [];
|
);
|
||||||
}
|
if (!tags) return [];
|
||||||
if (Array.isArray(tags)) {
|
if (Array.isArray(tags)) {
|
||||||
return Array.from(new Set(tags
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
tags
|
||||||
.map(tag => `${tag}`.trim())
|
.map(tag => `${tag}`.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter(tag => !headerTags.has(tag.toLowerCase()))));
|
.filter(tag => !headerTags.has(tag.toLowerCase()))
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (typeof tags === 'string') {
|
if (typeof tags === 'string') {
|
||||||
return tags
|
return tags
|
||||||
@ -200,23 +237,17 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
const keys = this.getFrontmatterKeys(frontmatter);
|
const keys = this.getFrontmatterKeys(frontmatter);
|
||||||
const entries: MetadataEntry[] = [];
|
const entries: MetadataEntry[] = [];
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (this.shouldSkipMetadataKey(key)) {
|
if (this.shouldSkipMetadataKey(key)) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const rawValue = frontmatter[key];
|
const rawValue = frontmatter[key];
|
||||||
const entry = this.buildMetadataEntry(key, rawValue);
|
const entry = this.buildMetadataEntry(key, rawValue);
|
||||||
if (entry) {
|
if (entry) entries.push(entry);
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
});
|
});
|
||||||
|
|
||||||
metadataVisibleEntries = computed<MetadataEntry[]>(() => {
|
metadataVisibleEntries = computed<MetadataEntry[]>(() => {
|
||||||
const entries = this.metadataEntries();
|
const entries = this.metadataEntries();
|
||||||
if (entries.length <= this.maxMetadataPreviewItems) {
|
if (entries.length <= this.maxMetadataPreviewItems) return entries;
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
return this.metadataExpanded() ? entries : entries.slice(0, this.maxMetadataPreviewItems);
|
return this.metadataExpanded() ? entries : entries.slice(0, this.maxMetadataPreviewItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,9 +268,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
const hostElement = this.elementRef.nativeElement as HTMLElement;
|
const hostElement = this.elementRef.nativeElement as HTMLElement;
|
||||||
hostElement.addEventListener('click', (event: MouseEvent) => {
|
hostElement.addEventListener('click', (event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement | null;
|
const target = event.target as HTMLElement | null;
|
||||||
if (!target) {
|
if (!target) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageBadge = target.closest('button.code-block__language-badge') as HTMLButtonElement | null;
|
const languageBadge = target.closest('button.code-block__language-badge') as HTMLButtonElement | null;
|
||||||
if (languageBadge) {
|
if (languageBadge) {
|
||||||
@ -252,9 +281,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
if (inlineTagButton) {
|
if (inlineTagButton) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const tagValue = inlineTagButton.dataset.tag;
|
const tagValue = inlineTagButton.dataset.tag;
|
||||||
if (tagValue) {
|
if (tagValue) this.tagClicked.emit(tagValue);
|
||||||
this.tagClicked.emit(tagValue);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,11 +290,8 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const noteId = anchor.getAttribute('data-note-id');
|
const noteId = anchor.getAttribute('data-note-id');
|
||||||
const noteTitle = anchor.getAttribute('data-note-title');
|
const noteTitle = anchor.getAttribute('data-note-title');
|
||||||
if (noteId) {
|
if (noteId) this.noteLinkClicked.emit(noteId);
|
||||||
this.noteLinkClicked.emit(noteId);
|
else if (noteTitle) this.noteLinkClicked.emit(noteTitle.toLowerCase().replace(/\s+/g, '-'));
|
||||||
} else if (noteTitle) {
|
|
||||||
this.noteLinkClicked.emit(noteTitle.toLowerCase().replace(/\s+/g, '-'));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +307,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
heading: headingSlug || undefined,
|
heading: headingSlug || undefined,
|
||||||
headingText: headingText || undefined,
|
headingText: headingText || undefined,
|
||||||
block: blockRef || undefined,
|
block: blockRef || undefined,
|
||||||
alias: wikiAnchor.textContent?.trim() ?? targetValue
|
alias: wikiAnchor.textContent?.trim() ?? targetValue,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -307,9 +331,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private scheduleMathRender(): void {
|
private scheduleMathRender(): void {
|
||||||
if (this.mathRenderScheduled) {
|
if (this.mathRenderScheduled) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.mathRenderScheduled = true;
|
this.mathRenderScheduled = true;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
this.mathRenderScheduled = false;
|
this.mathRenderScheduled = false;
|
||||||
@ -321,50 +343,30 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
return Object.keys(frontmatter).filter(key => key !== 'tags' && key !== 'aliases' && key !== 'mtime');
|
return Object.keys(frontmatter).filter(key => key !== 'tags' && key !== 'aliases' && key !== 'mtime');
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFrontmatterValue(value: any): string {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.join(', ');
|
|
||||||
}
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
return `${value}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatBacklinkId(id: string): string {
|
formatBacklinkId(id: string): string {
|
||||||
return id.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
|
return id.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
tagColorClass(tag: string): string {
|
tagColorClass(tag: string): string {
|
||||||
if (!tag) {
|
if (!tag) return '';
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const normalized = `${tag}`.toLowerCase();
|
const normalized = `${tag}`.toLowerCase();
|
||||||
if (this.tagColorCache.has(normalized)) {
|
if (this.tagColorCache.has(normalized)) return `md-tag-color-${this.tagColorCache.get(normalized)}`;
|
||||||
return `md-tag-color-${this.tagColorCache.get(normalized)}`;
|
|
||||||
}
|
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < normalized.length; i++) {
|
for (let i = 0; i < normalized.length; i++) hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0;
|
||||||
hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0;
|
|
||||||
}
|
|
||||||
const colorIndex = hash % this.tagPaletteSize;
|
const colorIndex = hash % this.tagPaletteSize;
|
||||||
this.tagColorCache.set(normalized, colorIndex);
|
this.tagColorCache.set(normalized, colorIndex);
|
||||||
return `md-tag-color-${colorIndex}`;
|
return `md-tag-color-${colorIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupMermaidObservation(): void {
|
private setupMermaidObservation(): void {
|
||||||
if (typeof MutationObserver === 'undefined') {
|
if (typeof MutationObserver === 'undefined') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.mermaidObserver?.disconnect();
|
this.mermaidObserver?.disconnect();
|
||||||
this.mermaidObserver = new MutationObserver(() => this.scheduleMermaidRender());
|
this.mermaidObserver = new MutationObserver(() => this.scheduleMermaidRender());
|
||||||
this.mermaidObserver.observe(this.elementRef.nativeElement as HTMLElement, { childList: true, subtree: true });
|
this.mermaidObserver.observe(this.elementRef.nativeElement as HTMLElement, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleMermaidRender(): void {
|
private scheduleMermaidRender(): void {
|
||||||
if (this.mermaidRenderScheduled) {
|
if (this.mermaidRenderScheduled) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.mermaidRenderScheduled = true;
|
this.mermaidRenderScheduled = true;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
this.mermaidRenderScheduled = false;
|
this.mermaidRenderScheduled = false;
|
||||||
@ -373,10 +375,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private scheduleAttachmentHandlers(): void {
|
private scheduleAttachmentHandlers(): void {
|
||||||
if (this.attachmentHandlersScheduled) {
|
if (this.attachmentHandlersScheduled) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.attachmentHandlersScheduled = true;
|
this.attachmentHandlersScheduled = true;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
this.attachmentHandlersScheduled = false;
|
this.attachmentHandlersScheduled = false;
|
||||||
@ -390,10 +389,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
|
|
||||||
const host = this.elementRef.nativeElement as HTMLElement;
|
const host = this.elementRef.nativeElement as HTMLElement;
|
||||||
const images = Array.from(host.querySelectorAll<HTMLImageElement>('img.md-attachment-image'));
|
const images = Array.from(host.querySelectorAll<HTMLImageElement>('img.md-attachment-image'));
|
||||||
|
if (!images.length) return;
|
||||||
if (!images.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupCallbacks: Array<() => void> = [];
|
const cleanupCallbacks: Array<() => void> = [];
|
||||||
const noteId = this.note()?.id ?? 'unknown-note';
|
const noteId = this.note()?.id ?? 'unknown-note';
|
||||||
@ -401,9 +397,10 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
for (const image of images) {
|
for (const image of images) {
|
||||||
const handleError = () => {
|
const handleError = () => {
|
||||||
image.removeEventListener('error', handleError);
|
image.removeEventListener('error', handleError);
|
||||||
|
|
||||||
const attachmentName = image.dataset.attachmentName?.trim() || image.alt || 'attachment';
|
const attachmentName = image.dataset.attachmentName?.trim() || image.alt || 'attachment';
|
||||||
const fallbackMarkup = image.dataset.errorMessage || `<div class=\"missing-attachment text-center text-sm text-red-500 dark:text-red-400\">‼️Attachement ${attachmentName} introuvable</div>`;
|
const fallbackMarkup =
|
||||||
|
image.dataset.errorMessage ||
|
||||||
|
`<div class="missing-attachment text-center text-sm text-red-500 dark:text-red-400">‼️Attachement ${attachmentName} introuvable</div>`;
|
||||||
|
|
||||||
console.warn('[ObsiViewer] Attachment missing', {
|
console.warn('[ObsiViewer] Attachment missing', {
|
||||||
noteId,
|
noteId,
|
||||||
@ -436,24 +433,14 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ensureMermaid(): Promise<MermaidLib> {
|
private ensureMermaid(): Promise<MermaidLib> {
|
||||||
if (this.mermaidLib) {
|
if (this.mermaidLib) return Promise.resolve(this.mermaidLib);
|
||||||
return Promise.resolve(this.mermaidLib);
|
if (this.mermaidLoader) return this.mermaidLoader;
|
||||||
}
|
if (typeof window === 'undefined') return Promise.reject(new Error('Mermaid is only available in the browser environment.'));
|
||||||
if (this.mermaidLoader) {
|
|
||||||
return this.mermaidLoader;
|
|
||||||
}
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return Promise.reject(new Error('Mermaid is only available in the browser environment.'));
|
|
||||||
}
|
|
||||||
this.mermaidLoader = import('mermaid')
|
this.mermaidLoader = import('mermaid')
|
||||||
.then(module => {
|
.then(module => {
|
||||||
const mermaidLib = (module.default ?? module) as MermaidLib;
|
const mermaidLib = (module.default ?? module) as MermaidLib;
|
||||||
const prefersDark = document.documentElement.classList.contains('dark');
|
const prefersDark = document.documentElement.classList.contains('dark');
|
||||||
mermaidLib.initialize({
|
mermaidLib.initialize({ startOnLoad: false, securityLevel: 'loose', theme: prefersDark ? 'dark' : 'default' });
|
||||||
startOnLoad: false,
|
|
||||||
securityLevel: 'loose',
|
|
||||||
theme: prefersDark ? 'dark' : 'default',
|
|
||||||
});
|
|
||||||
this.mermaidLib = mermaidLib;
|
this.mermaidLib = mermaidLib;
|
||||||
return mermaidLib;
|
return mermaidLib;
|
||||||
})
|
})
|
||||||
@ -465,28 +452,21 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderMermaidDiagrams(): void {
|
private renderMermaidDiagrams(): void {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hostElement = this.elementRef.nativeElement as HTMLElement;
|
const hostElement = this.elementRef.nativeElement as HTMLElement;
|
||||||
const diagrams = hostElement.querySelectorAll<HTMLElement>('.mermaid-diagram[data-mermaid-code]');
|
const diagrams = hostElement.querySelectorAll<HTMLElement>('.mermaid-diagram[data-mermaid-code]');
|
||||||
if (!diagrams.length) {
|
if (!diagrams.length) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ensureMermaid()
|
this.ensureMermaid()
|
||||||
.then(mermaidLib => {
|
.then(mermaidLib => {
|
||||||
diagrams.forEach((element, index) => {
|
diagrams.forEach((element, index) => {
|
||||||
if (element.dataset.mermaidRendered === 'true') {
|
if (element.dataset.mermaidRendered === 'true') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const encoded = element.dataset.mermaidCode ?? element.getAttribute('data-mermaid-code');
|
const encoded = element.dataset.mermaidCode ?? element.getAttribute('data-mermaid-code');
|
||||||
if (!encoded) {
|
if (!encoded) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const definition = this.decodeMermaidSource(encoded);
|
const definition = this.decodeMermaidSource(encoded);
|
||||||
const diagramId = `mermaid-${Date.now()}-${index}-${Math.random().toString(36).slice(2)}`;
|
const diagramId = `mermaid-${Date.now()}-${index}-${Math.random().toString(36).slice(2)}`;
|
||||||
mermaidLib.render(diagramId, definition)
|
mermaidLib
|
||||||
|
.render(diagramId, definition)
|
||||||
.then(({ svg, bindFunctions }) => {
|
.then(({ svg, bindFunctions }) => {
|
||||||
element.innerHTML = svg;
|
element.innerHTML = svg;
|
||||||
element.setAttribute('data-mermaid-rendered', 'true');
|
element.setAttribute('data-mermaid-rendered', 'true');
|
||||||
@ -498,30 +478,22 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => console.error('Mermaid initialization error:', error));
|
||||||
console.error('Mermaid initialization error:', error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async renderMathExpressions(): Promise<void> {
|
private async renderMathExpressions(): Promise<void> {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostElement = this.elementRef.nativeElement as HTMLElement;
|
const hostElement = this.elementRef.nativeElement as HTMLElement;
|
||||||
const mathElements = Array.from(hostElement.querySelectorAll<HTMLElement>('.md-math-inline, .md-math-block'));
|
const mathElements = Array.from(hostElement.querySelectorAll<HTMLElement>('.md-math-inline, .md-math-block'));
|
||||||
const pending = mathElements.filter(element => element.dataset.math && element.dataset.mathProcessed !== 'true');
|
const pending = mathElements.filter(el => el.dataset.math && el.dataset.mathProcessed !== 'true');
|
||||||
if (!pending.length) {
|
if (!pending.length) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mathJax = await this.ensureMathJax();
|
const mathJax = await this.ensureMathJax();
|
||||||
for (const element of pending) {
|
for (const element of pending) {
|
||||||
const expression = element.dataset.math ?? '';
|
const expression = element.dataset.math ?? '';
|
||||||
if (!expression.trim()) {
|
if (!expression.trim()) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const display = element.classList.contains('md-math-block');
|
const display = element.classList.contains('md-math-block');
|
||||||
try {
|
try {
|
||||||
const rendered = mathJax.tex2chtml(expression, { display });
|
const rendered = mathJax.tex2chtml(expression, { display });
|
||||||
@ -549,16 +521,11 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private escapeHtmlForMermaid(value: string): string {
|
private escapeHtmlForMermaid(value: string): string {
|
||||||
return value
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureMathJax(): Promise<MathJaxInstance> {
|
private ensureMathJax(): Promise<MathJaxInstance> {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') return Promise.reject(new Error('MathJax requires a browser environment.'));
|
||||||
return Promise.reject(new Error('MathJax requires a browser environment.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalWithMathJax = window as unknown as {
|
const globalWithMathJax = window as unknown as {
|
||||||
MathJax?: MathJaxInstance & { startup: { promise: Promise<void> } };
|
MathJax?: MathJaxInstance & { startup: { promise: Promise<void> } };
|
||||||
@ -566,19 +533,21 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (globalWithMathJax.MathJax) {
|
if (globalWithMathJax.MathJax) {
|
||||||
return globalWithMathJax.MathJax.startup.promise.then(() => globalWithMathJax.MathJax as MathJaxInstance);
|
return globalWithMathJax.MathJax.startup.promise.then(
|
||||||
|
() => globalWithMathJax.MathJax as MathJaxInstance
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.mathJaxLoader) {
|
if (this.mathJaxLoader) return this.mathJaxLoader;
|
||||||
return this.mathJaxLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mathJaxLoader = new Promise<MathJaxInstance>((resolve, reject) => {
|
this.mathJaxLoader = new Promise<MathJaxInstance>((resolve, reject) => {
|
||||||
if (globalWithMathJax._mathJaxLoading) {
|
if (globalWithMathJax._mathJaxLoading) {
|
||||||
const interval = window.setInterval(() => {
|
const interval = window.setInterval(() => {
|
||||||
if (globalWithMathJax.MathJax) {
|
if (globalWithMathJax.MathJax) {
|
||||||
window.clearInterval(interval);
|
window.clearInterval(interval);
|
||||||
globalWithMathJax.MathJax.startup.promise.then(() => resolve(globalWithMathJax.MathJax as MathJaxInstance));
|
globalWithMathJax.MathJax.startup.promise.then(
|
||||||
|
() => resolve(globalWithMathJax.MathJax as MathJaxInstance)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 30);
|
}, 30);
|
||||||
return;
|
return;
|
||||||
@ -586,24 +555,22 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
|
|
||||||
globalWithMathJax._mathJaxLoading = true;
|
globalWithMathJax._mathJaxLoading = true;
|
||||||
(window as any).MathJax = {
|
(window as any).MathJax = {
|
||||||
startup: {
|
startup: { typeset: false },
|
||||||
typeset: false
|
|
||||||
},
|
|
||||||
tex: {
|
tex: {
|
||||||
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
||||||
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
||||||
processEscapes: true
|
processEscapes: true,
|
||||||
},
|
},
|
||||||
options: {
|
options: { skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre'] },
|
||||||
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const existing = document.getElementById('mathjax-loader') as HTMLScriptElement | null;
|
const existing = document.getElementById('mathjax-loader') as HTMLScriptElement | null;
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.addEventListener('load', () => {
|
existing.addEventListener('load', () => {
|
||||||
if (globalWithMathJax.MathJax) {
|
if (globalWithMathJax.MathJax) {
|
||||||
globalWithMathJax.MathJax.startup.promise.then(() => resolve(globalWithMathJax.MathJax as MathJaxInstance));
|
globalWithMathJax.MathJax.startup.promise.then(
|
||||||
|
() => resolve(globalWithMathJax.MathJax as MathJaxInstance)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
existing.addEventListener('error', () => reject(new Error('Failed to load MathJax script.')), { once: true });
|
existing.addEventListener('error', () => reject(new Error('Failed to load MathJax script.')), { once: true });
|
||||||
@ -633,38 +600,24 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const currentScroll = window.scrollY;
|
const currentScroll = window.scrollY;
|
||||||
this.metadataExpanded.update(expanded => !expanded);
|
this.metadataExpanded.update(expanded => !expanded);
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => window.scrollTo({ top: currentScroll, behavior: 'auto' }));
|
||||||
window.scrollTo({ top: currentScroll, behavior: 'auto' });
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.metadataExpanded.update(expanded => !expanded);
|
this.metadataExpanded.update(expanded => !expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildMetadataEntry(key: string, rawValue: unknown): MetadataEntry | null {
|
private buildMetadataEntry(key: string, rawValue: unknown): MetadataEntry | null {
|
||||||
if (rawValue === undefined || rawValue === null) {
|
if (rawValue === undefined || rawValue === null) return null;
|
||||||
return null;
|
if (typeof rawValue === 'string' && rawValue.trim().length === 0) return null;
|
||||||
}
|
|
||||||
if (typeof rawValue === 'string' && rawValue.trim().length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedKey = key.toLowerCase();
|
const normalizedKey = key.toLowerCase();
|
||||||
const label = this.formatMetadataLabel(key);
|
const label = this.formatMetadataLabel(key);
|
||||||
const baseString = this.coerceToString(rawValue);
|
const baseString = this.coerceToString(rawValue);
|
||||||
const entry: MetadataEntry = {
|
const entry: MetadataEntry = { key, label, icon: '📝', type: 'text', displayValue: baseString };
|
||||||
key,
|
|
||||||
label,
|
|
||||||
icon: '📝',
|
|
||||||
type: 'text',
|
|
||||||
displayValue: baseString,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Array.isArray(rawValue)) {
|
if (Array.isArray(rawValue)) {
|
||||||
const parts = rawValue.map(value => this.coerceToString(value)).filter(Boolean);
|
const parts = rawValue.map(v => this.coerceToString(v)).filter(Boolean);
|
||||||
if (!parts.length) {
|
if (!parts.length) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
entry.type = 'list';
|
entry.type = 'list';
|
||||||
entry.icon = '🧾';
|
entry.icon = '🧾';
|
||||||
entry.displayValue = parts.join(', ');
|
entry.displayValue = parts.join(', ');
|
||||||
@ -672,9 +625,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rawValue instanceof Date) {
|
if (rawValue instanceof Date) {
|
||||||
if (Number.isNaN(rawValue.getTime())) {
|
if (Number.isNaN(rawValue.getTime())) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
entry.type = 'date';
|
entry.type = 'date';
|
||||||
entry.icon = '📅';
|
entry.icon = '📅';
|
||||||
entry.displayValue = this.dateFormatter.format(rawValue);
|
entry.displayValue = this.dateFormatter.format(rawValue);
|
||||||
@ -758,22 +709,14 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^(\w)/, (_, first) => first.toUpperCase())
|
.replace(/^(\w)/, (_, first) => first.toUpperCase())
|
||||||
.replace(/\b(\w)/g, match => match.toUpperCase());
|
.replace(/\b(\w)/g, m => m.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
private coerceToString(value: unknown): string {
|
private coerceToString(value: unknown): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) return '';
|
||||||
return '';
|
if (typeof value === 'string') return value.trim();
|
||||||
}
|
if (typeof value === 'number' || typeof value === 'boolean') return `${value}`;
|
||||||
if (typeof value === 'string') {
|
if (value instanceof Date) return this.dateFormatter.format(value);
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
||||||
return `${value}`;
|
|
||||||
}
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return this.dateFormatter.format(value);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
} catch {
|
} catch {
|
||||||
@ -782,22 +725,14 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private looksLikeEmail(value: string): boolean {
|
private looksLikeEmail(value: string): boolean {
|
||||||
if (!value) {
|
if (!value) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private looksLikeUrl(value: string): boolean {
|
private looksLikeUrl(value: string): boolean {
|
||||||
if (!value) {
|
if (!value) return false;
|
||||||
return false;
|
if (/^https?:\/\//i.test(value)) return true;
|
||||||
}
|
if (value.startsWith('www.')) return true;
|
||||||
if (/^https?:\/\//i.test(value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (value.startsWith('www.')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
new URL(value);
|
new URL(value);
|
||||||
return true;
|
return true;
|
||||||
@ -807,9 +742,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ensureUrlProtocol(value: string): string | null {
|
private ensureUrlProtocol(value: string): string | null {
|
||||||
if (!value) {
|
if (!value) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const url = value.startsWith('http') ? new URL(value) : new URL(`https://${value}`);
|
const url = value.startsWith('http') ? new URL(value) : new URL(`https://${value}`);
|
||||||
return url.toString();
|
return url.toString();
|
||||||
@ -819,37 +752,23 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private looksLikeImageUrl(value: string): boolean {
|
private looksLikeImageUrl(value: string): boolean {
|
||||||
if (!value) {
|
if (!value) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return /(\.(png|jpe?g|gif|svg|webp|avif|heic|heif)$)/i.test(value.split('?')[0]);
|
return /(\.(png|jpe?g|gif|svg|webp|avif|heic|heif)$)/i.test(value.split('?')[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryParseDate(value: unknown): Date | null {
|
private tryParseDate(value: unknown): Date | null {
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
||||||
return Number.isNaN(value.getTime()) ? null : value;
|
if (typeof value !== 'string') return null;
|
||||||
}
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const parsed = Date.parse(trimmed);
|
const parsed = Date.parse(trimmed);
|
||||||
if (Number.isNaN(parsed)) {
|
if (Number.isNaN(parsed)) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(parsed);
|
return new Date(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isBooleanLike(value: unknown): boolean {
|
private isBooleanLike(value: unknown): boolean {
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') return true;
|
||||||
return true;
|
if (typeof value === 'number') return value === 0 || value === 1;
|
||||||
}
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return value === 0 || value === 1;
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
return ['true', 'false', 'oui', 'non', 'yes', 'no', '1', '0'].includes(normalized);
|
return ['true', 'false', 'oui', 'non', 'yes', 'no', '1', '0'].includes(normalized);
|
||||||
@ -871,20 +790,12 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private slugify(value: string): string {
|
private slugify(value: string): string {
|
||||||
return value
|
return value.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-');
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.replace(/[^a-z0-9\s-]/g, '')
|
|
||||||
.replace(/\s+/g, '-');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private toBoolean(value: unknown): boolean {
|
private toBoolean(value: unknown): boolean {
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') return value;
|
||||||
return value;
|
if (typeof value === 'number') return value !== 0;
|
||||||
}
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return value !== 0;
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
return normalized === 'true' || normalized === 'oui' || normalized === 'yes' || normalized === '1';
|
return normalized === 'true' || normalized === 'oui' || normalized === 'yes' || normalized === '1';
|
||||||
@ -894,18 +805,14 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
|
|
||||||
private handleCodeCopy(button: HTMLButtonElement): void {
|
private handleCodeCopy(button: HTMLButtonElement): void {
|
||||||
const codeId = button.dataset.codeId;
|
const codeId = button.dataset.codeId;
|
||||||
if (!codeId) {
|
if (!codeId) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const block = (this.elementRef.nativeElement as HTMLElement).querySelector<HTMLElement>(
|
const block = (this.elementRef.nativeElement as HTMLElement).querySelector<HTMLElement>(
|
||||||
`.code-block[data-code-id="${codeId}"]`
|
`.code-block[data-code-id="${codeId}"]`
|
||||||
);
|
);
|
||||||
const codeElement = block?.querySelector<HTMLElement>('code[data-raw-code]');
|
const codeElement = block?.querySelector<HTMLElement>('code[data-raw-code]');
|
||||||
const rawEncoded = codeElement?.getAttribute('data-raw-code');
|
const rawEncoded = codeElement?.getAttribute('data-raw-code');
|
||||||
if (!block || !codeElement || rawEncoded == null) {
|
if (!block || !codeElement || rawEncoded == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let decoded = '';
|
let decoded = '';
|
||||||
try {
|
try {
|
||||||
@ -914,7 +821,8 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
decoded = rawEncoded;
|
decoded = rawEncoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyPromise = typeof navigator !== 'undefined' && navigator.clipboard?.writeText
|
const copyPromise =
|
||||||
|
typeof navigator !== 'undefined' && navigator.clipboard?.writeText
|
||||||
? navigator.clipboard.writeText(decoded)
|
? navigator.clipboard.writeText(decoded)
|
||||||
: this.fallbackCopy(decoded);
|
: this.fallbackCopy(decoded);
|
||||||
|
|
||||||
@ -936,11 +844,8 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
textarea.select();
|
textarea.select();
|
||||||
const successful = document.execCommand('copy');
|
const successful = document.execCommand('copy');
|
||||||
document.body.removeChild(textarea);
|
document.body.removeChild(textarea);
|
||||||
if (successful) {
|
if (successful) resolve();
|
||||||
resolve();
|
else reject(new Error('Copy command failed'));
|
||||||
} else {
|
|
||||||
reject(new Error('Copy command failed'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error as Error);
|
reject(error as Error);
|
||||||
}
|
}
|
||||||
@ -949,18 +854,14 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
|
|
||||||
private showCopyFeedback(block: HTMLElement, message: string): void {
|
private showCopyFeedback(block: HTMLElement, message: string): void {
|
||||||
const feedback = block.querySelector<HTMLElement>('.code-block__copy-feedback');
|
const feedback = block.querySelector<HTMLElement>('.code-block__copy-feedback');
|
||||||
if (!feedback) {
|
if (!feedback) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
feedback.textContent = message;
|
feedback.textContent = message;
|
||||||
feedback.hidden = false;
|
feedback.hidden = false;
|
||||||
block.classList.add('copied');
|
block.classList.add('copied');
|
||||||
|
|
||||||
const existingTimer = this.copyFeedbackTimers.get(block);
|
const existingTimer = this.copyFeedbackTimers.get(block);
|
||||||
if (existingTimer) {
|
if (existingTimer) window.clearTimeout(existingTimer);
|
||||||
window.clearTimeout(existingTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = window.setTimeout(() => {
|
const timeout = window.setTimeout(() => {
|
||||||
feedback.hidden = true;
|
feedback.hidden = true;
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
<div class="p-4">
|
|
||||||
<h3 class="text-sm font-semibold text-obs-l-text-muted dark:text-obs-d-text-muted mb-3">All Tags</h3>
|
|
||||||
@if (tags().length > 0) {
|
|
||||||
<ul class="space-y-1">
|
|
||||||
@for (tag of tags(); track tag.name) {
|
|
||||||
<li>
|
|
||||||
<button (click)="tagSelected.emit(tag.name)" class="w-full flex justify-between items-center text-left p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-600 text-sm">
|
|
||||||
<span>{{ tag.name }}</span>
|
|
||||||
<span class="text-xs bg-gray-200 dark:bg-gray-700 rounded-full px-2 py-0.5">{{ tag.count }}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
} @else {
|
|
||||||
<p class="text-sm text-obs-l-text-muted dark:text-obs-d-text-muted italic">No tags found in vault.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
@ -21,8 +21,8 @@ interface TagSection {
|
|||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
:host {
|
||||||
--tv-scroll-thumb-light: rgba(22, 28, 35, 0.35);
|
--tv-scroll-thumb-light: color-mix(in srgb, var(--text-main) 35%, transparent);
|
||||||
--tv-scroll-thumb-dark: rgba(156, 163, 175, 0.35);
|
--tv-scroll-thumb-dark: color-mix(in srgb, var(--text-muted) 55%, transparent);
|
||||||
--tv-scroll-track: transparent;
|
--tv-scroll-track: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,11 +57,11 @@ interface TagSection {
|
|||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
<div class="flex h-full flex-col gap-4 p-3">
|
<div class="flex h-full flex-col gap-4 p-3 bg-card">
|
||||||
<div class="rounded-lg border border-obs-l-border/60 bg-obs-l-bg-secondary/70 px-3 py-2 shadow-sm dark:border-obs-d-border/60 dark:bg-obs-d-bg-secondary/70">
|
<div class="rounded-xl border border-border bg-card px-3 py-2 shadow-subtle">
|
||||||
<label class="sr-only" for="tag-search">Rechercher des tags</label>
|
<label class="sr-only" for="tag-search">Rechercher des tags</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 text-text-muted">
|
||||||
<svg class="h-4 w-4 text-obs-l-text-muted dark:text-obs-d-text-muted" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-5.2-5.2m0 0A7 7 0 105.8 5.8a7 7 0 0010 10z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-5.2-5.2m0 0A7 7 0 105.8 5.8a7 7 0 0010 10z" />
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
@ -70,13 +70,13 @@ interface TagSection {
|
|||||||
[value]="searchTerm()"
|
[value]="searchTerm()"
|
||||||
(input)="onSearchChange($event.target?.value ?? '')"
|
(input)="onSearchChange($event.target?.value ?? '')"
|
||||||
placeholder="Rechercher un tag..."
|
placeholder="Rechercher un tag..."
|
||||||
class="w-full bg-transparent text-sm text-obs-l-text-main outline-none placeholder:text-obs-l-text-muted dark:text-obs-d-text-main dark:placeholder:text-obs-d-text-muted"
|
class="w-full bg-transparent text-sm text-text-main placeholder:text-text-muted focus:outline-none"
|
||||||
/>
|
/>
|
||||||
@if (searchTerm(); as value) {
|
@if (searchTerm(); as value) {
|
||||||
@if (value.length > 0) {
|
@if (value.length > 0) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-xs text-obs-l-text-muted transition hover:text-obs-l-text-main dark:text-obs-d-text-muted dark:hover:text-obs-d-text-main"
|
class="btn btn-sm btn-ghost px-2 py-1 text-xs"
|
||||||
aria-label="Effacer la recherche"
|
aria-label="Effacer la recherche"
|
||||||
(click)="clearSearch()"
|
(click)="clearSearch()"
|
||||||
>
|
>
|
||||||
@ -94,10 +94,10 @@ interface TagSection {
|
|||||||
@for (section of displayedSections(); track section.letter) {
|
@for (section of displayedSections(); track section.letter) {
|
||||||
<section class="space-y-2">
|
<section class="space-y-2">
|
||||||
<header class="flex items-center justify-between">
|
<header class="flex items-center justify-between">
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-wide text-obs-l-text-muted dark:text-obs-d-text-muted">
|
<h2 class="text-xs font-semibold uppercase tracking-wide text-text-muted">
|
||||||
{{ section.letter }}
|
{{ section.letter }}
|
||||||
</h2>
|
</h2>
|
||||||
<span class="text-[0.65rem] text-obs-l-text-muted/70 dark:text-obs-d-text-muted/70">
|
<span class="text-[0.65rem] text-text-muted/70">
|
||||||
{{ section.tags.length }} tag(s)
|
{{ section.tags.length }} tag(s)
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
@ -105,14 +105,14 @@ interface TagSection {
|
|||||||
@for (tag of section.tags; track tag.name) {
|
@for (tag of section.tags; track tag.name) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-full border border-obs-l-border/60 bg-obs-l-bg-main/80 px-3 py-1 text-xs font-medium text-obs-l-text-muted transition hover:bg-obs-l-bg-main dark:border-obs-d-border/60 dark:bg-obs-d-bg-main/70 dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-main"
|
class="chip justify-between px-3 py-1.5 text-sm"
|
||||||
(click)="tagSelected.emit(tag.name)"
|
(click)="tagSelected.emit(tag.name)"
|
||||||
>
|
>
|
||||||
<span class="text-sm">🔖</span>
|
<span class="flex items-center gap-2 text-text-main">
|
||||||
|
<span aria-hidden="true">🔖</span>
|
||||||
<span class="truncate">{{ tag.name }}</span>
|
<span class="truncate">{{ tag.name }}</span>
|
||||||
<span class="rounded-full bg-obs-l-bg-secondary/80 px-2 py-0.5 text-[0.65rem] text-obs-l-text-muted dark:bg-obs-d-bg-secondary/80 dark:text-obs-d-text-muted">
|
|
||||||
{{ tag.count }}
|
|
||||||
</span>
|
</span>
|
||||||
|
<span class="badge text-text-muted">{{ tag.count }}</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -120,23 +120,20 @@ interface TagSection {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="flex h-full items-center justify-center rounded-lg border border-dashed border-obs-l-border/60 bg-obs-l-bg-secondary/50 px-4 py-6 text-center text-sm text-obs-l-text-muted dark:border-obs-d-border/60 dark:bg-obs-d-bg-secondary/50 dark:text-obs-d-text-muted">
|
<div class="flex h-full items-center justify-center rounded-lg border border-dashed border-border bg-bg-muted px-4 py-6 text-center text-sm text-text-muted">
|
||||||
Aucun tag ne correspond à votre recherche.
|
Aucun tag ne correspond à votre recherche.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="custom-scrollbar sticky top-0 flex h-full w-12 flex-col items-center justify-start gap-2 self-start rounded-lg border border-obs-l-border/60 bg-obs-l-bg-secondary/70 px-2 py-3 text-[0.7rem] font-medium text-obs-l-text-muted shadow-sm dark:border-obs-d-border/60 dark:bg-obs-d-bg-secondary/70 dark:text-obs-d-text-muted overflow-y-auto">
|
<nav class="custom-scrollbar sticky top-0 flex h-full w-12 flex-col items-center justify-start gap-2 self-start rounded-lg border border-border bg-card px-2 py-3 text-[0.7rem] font-medium text-text-muted shadow-subtle overflow-y-auto">
|
||||||
<span class="text-[0.6rem] uppercase tracking-wide text-obs-l-text-muted/70 dark:text-obs-d-text-muted/70">A–Z</span>
|
<span class="text-[0.6rem] uppercase tracking-wide text-text-muted/70">A–Z</span>
|
||||||
<div class="grid grid-cols-1 gap-1">
|
<div class="grid grid-cols-1 gap-1">
|
||||||
@for (letter of availableLetters(); track letter) {
|
@for (letter of availableLetters(); track letter) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex h-7 w-7 items-center justify-center rounded-full text-[0.7rem] transition"
|
class="flex h-9 w-9 items-center justify-center rounded-full border border-transparent text-[0.7rem] transition"
|
||||||
[class.bg-obs-l-bg-main]="activeLetter() === letter"
|
[ngClass]="activeLetter() === letter ? 'border-border bg-bg-muted text-text-main font-semibold' : 'text-text-muted hover:border-border hover:bg-bg-muted'"
|
||||||
[class.dark:bg-obs-d-bg-main]="activeLetter() === letter"
|
|
||||||
[class.text-obs-l-text-main]="activeLetter() === letter"
|
|
||||||
[class.dark:text-obs-d-text-main]="activeLetter() === letter"
|
|
||||||
[attr.aria-pressed]="activeLetter() === letter"
|
[attr.aria-pressed]="activeLetter() === letter"
|
||||||
(click)="onLetterClick(letter)"
|
(click)="onLetterClick(letter)"
|
||||||
>
|
>
|
||||||
|
@ -85,7 +85,7 @@ export class MarkdownService {
|
|||||||
const src = `/api/attachments/resolve?name=${encodeURIComponent(filename)}¬e=${encodeURIComponent(notePath)}&base=${encodeURIComponent(attachmentsBase)}`;
|
const src = `/api/attachments/resolve?name=${encodeURIComponent(filename)}¬e=${encodeURIComponent(notePath)}&base=${encodeURIComponent(attachmentsBase)}`;
|
||||||
return `<figure class="my-4 md-attachment-figure">
|
return `<figure class="my-4 md-attachment-figure">
|
||||||
<img src="${src}" alt="${safeAlt}" loading="lazy" class="rounded-lg max-w-full h-auto mx-auto md-attachment-image" data-attachment-name="${safeAlt}" data-error-message="<div class='missing-attachment text-center text-sm text-red-500 dark:text-red-400'>Attachement ${this.escapeHtml(filename).replace(/'/g, ''')} introuvable</div>">
|
<img src="${src}" alt="${safeAlt}" loading="lazy" class="rounded-lg max-w-full h-auto mx-auto md-attachment-image" data-attachment-name="${safeAlt}" data-error-message="<div class='missing-attachment text-center text-sm text-red-500 dark:text-red-400'>Attachement ${this.escapeHtml(filename).replace(/'/g, ''')} introuvable</div>">
|
||||||
<figcaption class="text-center text-sm text-obs-l-text-muted dark:text-obs-d-text-muted">${safeAlt}</figcaption>
|
<figcaption class="text-center text-sm text-text-muted">${safeAlt}</figcaption>
|
||||||
</figure>`;
|
</figure>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ export class MarkdownService {
|
|||||||
const list = tokens as unknown as MarkdownItToken[];
|
const list = tokens as unknown as MarkdownItToken[];
|
||||||
const token = list[idx];
|
const token = list[idx];
|
||||||
const level = Number.parseInt(token.tag.replace('h', ''), 10);
|
const level = Number.parseInt(token.tag.replace('h', ''), 10);
|
||||||
const headingClass = `md-heading md-heading-${level} text-obs-l-text-main dark:text-white font-bold mt-6 mb-3 pb-1 border-b border-obs-l-border dark:border-obs-d-border`;
|
const headingClass = `md-heading md-heading-${level} text-text-main font-bold mt-6 mb-3 pb-1 border-b border-border`;
|
||||||
token.attrJoin('class', headingClass);
|
token.attrJoin('class', headingClass);
|
||||||
return self.renderToken(tokens, idx, options);
|
return self.renderToken(tokens, idx, options);
|
||||||
};
|
};
|
||||||
@ -254,7 +254,7 @@ export class MarkdownService {
|
|||||||
const titleAttr = title ? ` title="${safeTitle}"` : '';
|
const titleAttr = title ? ` title="${safeTitle}"` : '';
|
||||||
const captionSource = title && title.trim() ? title : alt;
|
const captionSource = title && title.trim() ? title : alt;
|
||||||
const safeCaption = captionSource ? this.escapeHtml(captionSource) : '';
|
const safeCaption = captionSource ? this.escapeHtml(captionSource) : '';
|
||||||
const figcaption = safeCaption ? `<figcaption class="text-center text-sm text-obs-l-text-muted dark:text-obs-d-text-muted">${safeCaption}</figcaption>` : '';
|
const figcaption = safeCaption ? `<figcaption class="text-center text-sm text-text-muted">${safeCaption}</figcaption>` : '';
|
||||||
|
|
||||||
return `<figure class="my-4 md-attachment-figure">
|
return `<figure class="my-4 md-attachment-figure">
|
||||||
<img src="${safeUrl}" alt="${safeAlt}"${titleAttr} loading="lazy" class="rounded-lg max-w-full h-auto mx-auto">
|
<img src="${safeUrl}" alt="${safeAlt}"${titleAttr} loading="lazy" class="rounded-lg max-w-full h-auto mx-auto">
|
||||||
@ -262,7 +262,7 @@ export class MarkdownService {
|
|||||||
</figure>`;
|
</figure>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
md.renderer.rules.footnote_block_open = () => '<section class="md-footnotes text-sm text-obs-l-text-muted dark:text-obs-d-text-muted mt-12"><header class="font-semibold uppercase tracking-wide mb-2">Notes</header><ol class="space-y-2">';
|
md.renderer.rules.footnote_block_open = () => '<section class="md-footnotes text-sm text-text-muted mt-12"><header class="font-semibold uppercase tracking-wide mb-2">Notes</header><ol class="space-y-2">';
|
||||||
md.renderer.rules.footnote_block_close = () => '</ol></section>';
|
md.renderer.rules.footnote_block_close = () => '</ol></section>';
|
||||||
|
|
||||||
const defaultFootnoteOpen = md.renderer.rules.footnote_open ?? ((tokens, idx, options, renderEnv, self) => self.renderToken(tokens, idx, options));
|
const defaultFootnoteOpen = md.renderer.rules.footnote_open ?? ((tokens, idx, options, renderEnv, self) => self.renderToken(tokens, idx, options));
|
||||||
@ -278,7 +278,7 @@ export class MarkdownService {
|
|||||||
return `<sup class="md-footnote-ref"><a href="#${this.escapeHtml(id)}" class="md-external-link" rel="footnote">${token.content}</a></sup>`;
|
return `<sup class="md-footnote-ref"><a href="#${this.escapeHtml(id)}" class="md-external-link" rel="footnote">${token.content}</a></sup>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
md.renderer.rules.hr = () => '<hr class="my-6 border-obs-l-border dark:border-obs-d-border">';
|
md.renderer.rules.hr = () => '<hr class="my-6 border-border">';
|
||||||
|
|
||||||
return md;
|
return md;
|
||||||
}
|
}
|
||||||
@ -493,7 +493,7 @@ export class MarkdownService {
|
|||||||
const itemRegex = /<li class="task-list-item([^"]*)">([\s\S]*?)<\/li>/g;
|
const itemRegex = /<li class="task-list-item([^"]*)">([\s\S]*?)<\/li>/g;
|
||||||
return html.replace(itemRegex, (_match, extraClasses, inner) => {
|
return html.replace(itemRegex, (_match, extraClasses, inner) => {
|
||||||
const isChecked = /<input[^>]*\schecked[^>]*>/i.test(inner);
|
const isChecked = /<input[^>]*\schecked[^>]*>/i.test(inner);
|
||||||
const classes = ['md-task-item', 'text-obs-l-text-main', 'dark:text-obs-d-text-main'];
|
const classes = ['md-task-item', 'text-text-main'];
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
classes.push('md-task-item--done');
|
classes.push('md-task-item--done');
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-footnotes {
|
.md-footnotes {
|
||||||
border-top: 1px solid var(--footnotes-border, rgba(148, 163, 184, 0.4));
|
border-top: 1px solid color-mix(in srgb, var(--brand) 35%, rgba(148, 163, 184, 0.25));
|
||||||
padding-top: 1.5rem;
|
padding-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
.md-footnote-item {
|
.md-footnote-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: var(--footnotes-text, #475569);
|
color: var(--footnotes-text, color-mix(in srgb, var(--brand-800) 35%, #475569));
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-footnote-item p {
|
.md-footnote-item p {
|
||||||
|
189
src/styles/components.css
Normal file
189
src/styles/components.css
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.bg-app {
|
||||||
|
background-color: var(--bg-main);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-surface {
|
||||||
|
background-color: var(--card);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-surface-elevated {
|
||||||
|
background-color: var(--elevated);
|
||||||
|
color: var(--text-main);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-app {
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-app {
|
||||||
|
--tw-ring-color: var(--ring);
|
||||||
|
--tw-ring-offset-color: var(--bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center gap-2 font-medium transition-colors duration-150 ease-in-out focus-visible:outline-none focus-visible:ring-2;
|
||||||
|
min-height: 3.5rem;
|
||||||
|
min-width: 3rem;
|
||||||
|
padding-inline: 1.25rem;
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
--tw-ring-color: var(--ring);
|
||||||
|
--tw-ring-offset-width: var(--focus-ring-offset);
|
||||||
|
--tw-ring-offset-color: var(--bg-main);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
min-height: 3rem;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
min-height: 4rem;
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply text-white;
|
||||||
|
background-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--brand-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
background-color: var(--brand-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
color: var(--text-main);
|
||||||
|
background-color: var(--bg-muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--bg-muted) 85%, var(--brand) 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:active {
|
||||||
|
background-color: color-mix(in srgb, var(--bg-muted) 75%, var(--brand-700) 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
color: var(--text-main);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--bg-muted) 65%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:active {
|
||||||
|
background-color: color-mix(in srgb, var(--bg-muted) 80%, var(--brand) 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-destructive {
|
||||||
|
@apply text-white;
|
||||||
|
background-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-destructive:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--danger) 90%, var(--danger) 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-destructive:active {
|
||||||
|
background-color: color-mix(in srgb, var(--danger) 85%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled,
|
||||||
|
.btn[aria-disabled="true"] {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
min-width: 3rem;
|
||||||
|
padding-inline: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
@apply inline-flex items-center gap-2 rounded-full border border-app px-3 py-1 text-sm font-medium;
|
||||||
|
min-height: 3rem;
|
||||||
|
background-color: color-mix(in srgb, var(--bg-muted) 88%, transparent);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-selected {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background-color: color-mix(in srgb, var(--accent) 25%, transparent);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold uppercase tracking-wide;
|
||||||
|
background-color: color-mix(in srgb, var(--bg-muted) 90%, transparent);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background-color: var(--card);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background-color: var(--card);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.textarea,
|
||||||
|
.select {
|
||||||
|
@apply w-full rounded-xl border border-app bg-card px-4 py-2.5 text-sm text-text-main shadow-sm placeholder:text-muted focus-visible:outline-none focus-visible:ring-2;
|
||||||
|
--tw-ring-color: var(--ring);
|
||||||
|
--tw-ring-offset-width: var(--focus-ring-offset);
|
||||||
|
--tw-ring-offset-color: var(--bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:disabled,
|
||||||
|
.textarea:disabled,
|
||||||
|
.select:disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbd {
|
||||||
|
@apply inline-flex items-center justify-center rounded-md border border-app px-2 py-1 text-[0.75rem] font-medium uppercase tracking-widest;
|
||||||
|
background-color: color-mix(in srgb, var(--bg-muted) 75%, transparent);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-subtle {
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-surface {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-elevated {
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
}
|
72
src/styles/tokens.css
Normal file
72
src/styles/tokens.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
:root {
|
||||||
|
--font-sans: "Inter", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "Fira Code", "SFMono-Regular", "Consolas", "Liberation Mono", "Menlo", monospace;
|
||||||
|
--radius-sm: 0.5rem;
|
||||||
|
--radius-md: 0.75rem;
|
||||||
|
--radius-lg: 1rem;
|
||||||
|
--radius-xl: 1.25rem;
|
||||||
|
--radius-full: 9999px;
|
||||||
|
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||||
|
--shadow-md: 0 8px 24px rgba(15, 23, 42, 0.12);
|
||||||
|
--shadow-lg: 0 20px 45px rgba(15, 23, 42, 0.16);
|
||||||
|
--focus-ring-size: 2px;
|
||||||
|
--focus-ring-offset: 2px;
|
||||||
|
--focus-ring-style: 0 0 0 var(--focus-ring-size) var(--ring);
|
||||||
|
--transition-base: 150ms ease;
|
||||||
|
--transition-fast: 120ms ease;
|
||||||
|
--transition-slow: 200ms ease;
|
||||||
|
--container-max: 80rem;
|
||||||
|
--container-gutter: clamp(1.5rem, 4vw, 2.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg-main: #f7f7f7;
|
||||||
|
--bg-muted: #eef0f2;
|
||||||
|
--text-main: #111827;
|
||||||
|
--text-muted: #6b7280;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--brand: #3a68d1;
|
||||||
|
--brand-700: #2f56ab;
|
||||||
|
--brand-800: #254487;
|
||||||
|
--accent: #14b8a6;
|
||||||
|
--success: #22c55e;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--info: #0ea5e9;
|
||||||
|
--card: #ffffff;
|
||||||
|
--elevated: #ffffff;
|
||||||
|
--ring: #3a68d1;
|
||||||
|
--shadow-color: rgba(15, 23, 42, 0.08);
|
||||||
|
--scrollbar-thumb: rgba(148, 163, 184, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg-main: #111827;
|
||||||
|
--bg-muted: #1f2937;
|
||||||
|
--text-main: #e5e7eb;
|
||||||
|
--text-muted: #9ca3af;
|
||||||
|
--border: #374151;
|
||||||
|
--brand: #6f96e4;
|
||||||
|
--brand-700: #5678b9;
|
||||||
|
--brand-800: #415a8c;
|
||||||
|
--accent: #2dd4bf;
|
||||||
|
--success: #22c55e;
|
||||||
|
--warning: #fbbf24;
|
||||||
|
--danger: #f87171;
|
||||||
|
--info: #38bdf8;
|
||||||
|
--card: #0f172a;
|
||||||
|
--elevated: #111827;
|
||||||
|
--ring: #6f96e4;
|
||||||
|
--shadow-color: rgba(15, 23, 42, 0.5);
|
||||||
|
--scrollbar-thumb: rgba(75, 85, 99, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] body {
|
||||||
|
background-color: var(--bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] body {
|
||||||
|
background-color: var(--bg-main);
|
||||||
|
}
|
@ -14,6 +14,7 @@ export interface Note {
|
|||||||
originalPath: string;
|
originalPath: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
|
author?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultFile {
|
export interface VaultFile {
|
||||||
|
@ -1,40 +1,65 @@
|
|||||||
|
const plugin = require('tailwindcss/plugin');
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./index.html",
|
'./index.html',
|
||||||
"./src/**/*.{html,ts}"
|
'./src/**/*.{html,ts,css}'
|
||||||
],
|
],
|
||||||
darkMode: "class",
|
darkMode: ['class', '[data-theme="dark"]'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
'obs-l-bg-main': '#f8fafc',
|
'bg-main': 'var(--bg-main)',
|
||||||
'obs-d-bg-main': '#202123',
|
'bg-muted': 'var(--bg-muted)',
|
||||||
'obs-l-bg-secondary': '#eef2ff',
|
card: 'var(--card)',
|
||||||
'obs-d-bg-secondary': '#2b2d46',
|
elevated: 'var(--elevated)',
|
||||||
'obs-l-text-main': '#0f172a',
|
'text-main': 'var(--text-main)',
|
||||||
'obs-d-text-main': '#e2e8f0',
|
'text-muted': 'var(--text-muted)',
|
||||||
'obs-l-text-muted': '#475569',
|
border: 'var(--border)',
|
||||||
'obs-d-text-muted': '#94a3b8',
|
brand: 'var(--brand)',
|
||||||
'obs-l-border': '#cbd5f5',
|
'brand-700': 'var(--brand-700)',
|
||||||
'obs-d-border': '#3f455a',
|
'brand-800': 'var(--brand-800)',
|
||||||
'obs-l-accent': '#6366f1',
|
accent: 'var(--accent)',
|
||||||
'obs-d-accent': '#22d3ee'
|
success: 'var(--success)',
|
||||||
|
warning: 'var(--warning)',
|
||||||
|
danger: 'var(--danger)',
|
||||||
|
info: 'var(--info)',
|
||||||
|
ring: 'var(--ring)'
|
||||||
},
|
},
|
||||||
fontFamily: {
|
ringColor: {
|
||||||
sans: ['Inter', 'Segoe UI', 'system-ui', 'sans-serif'],
|
DEFAULT: 'var(--ring)'
|
||||||
mono: ['JetBrains Mono', 'Fira Code', 'SFMono-Regular', 'Consolas', 'monospace']
|
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
'panel': '0 20px 45px -20px rgba(15, 23, 42, 0.35)'
|
surface: '0 1px 2px 0 var(--shadow-color)',
|
||||||
|
'surface-md': '0 12px 32px -16px var(--shadow-color)',
|
||||||
|
focus: '0 0 0 var(--focus-ring-size) var(--ring)'
|
||||||
},
|
},
|
||||||
backdropBlur: {
|
borderRadius: {
|
||||||
xs: '2px'
|
sm: 'var(--radius-sm)',
|
||||||
|
DEFAULT: 'var(--radius-md)',
|
||||||
|
md: 'var(--radius-md)',
|
||||||
|
lg: 'var(--radius-lg)',
|
||||||
|
xl: 'var(--radius-xl)',
|
||||||
|
'2xl': 'calc(var(--radius-xl) + 0.5rem)',
|
||||||
|
full: 'var(--radius-full)'
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['var(--font-mono)', 'ui-monospace', 'SFMono-Regular', 'monospace']
|
||||||
|
},
|
||||||
|
transitionTimingFunction: {
|
||||||
|
base: 'var(--transition-base)',
|
||||||
|
fast: 'var(--transition-fast)',
|
||||||
|
slow: 'var(--transition-slow)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/forms'),
|
require('@tailwindcss/forms'),
|
||||||
require('@tailwindcss/typography')
|
require('@tailwindcss/typography'),
|
||||||
|
plugin(({ addVariant }) => {
|
||||||
|
addVariant('theme-dark', ':is([data-theme="dark"] &)');
|
||||||
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user