feat: enhance notes list UI with improved visual hierarchy
- Added new folder creation endpoint with support for path or parent/name parameters - Updated notes list styling with consistent row cards and active state indicators - Improved theme-aware color variables for better light/dark mode contrast - Added visual depth with subtle gradient overlays and active item highlighting - Implemented consistent styling between virtual and standard note list views - Enhanced new note button styling for better visibility
This commit is contained in:
parent
08a2d05dad
commit
83603e2d97
@ -27,6 +27,8 @@ export function setupRenameFolderEndpoint(app, vaultDir, broadcastVaultEvent, me
|
|||||||
if (!oldPath || typeof oldPath !== 'string') {
|
if (!oldPath || typeof oldPath !== 'string') {
|
||||||
return res.status(400).json({ error: 'Missing or invalid oldPath' });
|
return res.status(400).json({ error: 'Missing or invalid oldPath' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!newName || typeof newName !== 'string') {
|
if (!newName || typeof newName !== 'string') {
|
||||||
return res.status(400).json({ error: 'Missing or invalid newName' });
|
return res.status(400).json({ error: 'Missing or invalid newName' });
|
||||||
}
|
}
|
||||||
@ -202,6 +204,59 @@ export function setupDeleteFolderEndpoint(app, vaultDir, broadcastVaultEvent, me
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ENDPOINT 7: /api/folders (POST) - Create a folder (supports { path } or { parentPath, newFolderName })
|
||||||
|
// ============================================================================
|
||||||
|
export function setupCreateFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache) {
|
||||||
|
app.post('/api/folders', express.json(), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const body = req.body || {};
|
||||||
|
let rel = '';
|
||||||
|
if (typeof body.path === 'string' && body.path.trim()) {
|
||||||
|
rel = body.path.trim();
|
||||||
|
} else if (typeof body.parentPath === 'string' && typeof body.newFolderName === 'string') {
|
||||||
|
const parent = body.parentPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||||
|
const name = body.newFolderName.trim();
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'New folder name cannot be empty' });
|
||||||
|
}
|
||||||
|
rel = parent ? `${parent}/${name}` : name;
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: 'Missing path or (parentPath, newFolderName)' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedRel = String(rel).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||||
|
if (!sanitizedRel) {
|
||||||
|
return res.status(400).json({ error: 'Invalid folder path' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const abs = path.join(vaultDir, sanitizedRel);
|
||||||
|
const vaultAbs = path.resolve(vaultDir);
|
||||||
|
const absResolved = path.resolve(abs);
|
||||||
|
if (!absResolved.startsWith(vaultAbs)) {
|
||||||
|
return res.status(400).json({ error: 'Path escapes vault root' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.promises.mkdir(absResolved, { recursive: true });
|
||||||
|
} catch (mkErr) {
|
||||||
|
console.error('[POST /api/folders] mkdir failed:', mkErr);
|
||||||
|
return res.status(500).json({ error: 'Failed to create folder' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadataCache) metadataCache.clear();
|
||||||
|
if (broadcastVaultEvent) {
|
||||||
|
broadcastVaultEvent({ event: 'folder-create', path: sanitizedRel, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ success: true, path: sanitizedRel });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[POST /api/folders] Unexpected error:', error);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ENDPOINT 1: /api/vault/metadata - with cache read-through and monitoring
|
// ENDPOINT 1: /api/vault/metadata - with cache read-through and monitoring
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@ -32,7 +32,8 @@ import {
|
|||||||
setupDeferredIndexing,
|
setupDeferredIndexing,
|
||||||
setupCreateNoteEndpoint,
|
setupCreateNoteEndpoint,
|
||||||
setupRenameFolderEndpoint,
|
setupRenameFolderEndpoint,
|
||||||
setupDeleteFolderEndpoint
|
setupDeleteFolderEndpoint,
|
||||||
|
setupCreateFolderEndpoint
|
||||||
} from './index-phase3-patch.mjs';
|
} from './index-phase3-patch.mjs';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@ -1532,6 +1533,9 @@ setupRenameFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
|
|||||||
// Setup delete folder endpoint (must be before catch-all)
|
// Setup delete folder endpoint (must be before catch-all)
|
||||||
setupDeleteFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
|
setupDeleteFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
|
||||||
|
|
||||||
|
// Setup create folder endpoint (must be before catch-all)
|
||||||
|
setupCreateFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
|
||||||
|
|
||||||
app.get('/', sendIndex);
|
app.get('/', sendIndex);
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
if (req.path.startsWith('/api/')) {
|
if (req.path.startsWith('/api/')) {
|
||||||
|
|||||||
@ -51,7 +51,7 @@ import { NoteCreationService } from '../../services/note-creation.service';
|
|||||||
class="flex-1 rounded border border-border dark:border-border bg-card dark:bg-main px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
|
class="flex-1 rounded border border-border dark:border-border bg-card dark:bg-main px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||||
<button type="button"
|
<button type="button"
|
||||||
(click)="openNewNoteDialog()"
|
(click)="openNewNoteDialog()"
|
||||||
class="inline-flex items-center gap-1.5 px-3 py-2 rounded bg-primary hover:bg-primary/90 text-primary-foreground text-sm font-medium transition-colors"
|
class="new-note-btn inline-flex items-center gap-1.5 px-3 py-2 rounded bg-primary hover:bg-primary/90 text-primary-foreground text-sm font-medium transition-colors"
|
||||||
title="Créer une nouvelle note">
|
title="Créer une nouvelle note">
|
||||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
<span class="hidden sm:inline">Nouvelle note</span>
|
<span class="hidden sm:inline">Nouvelle note</span>
|
||||||
@ -127,27 +127,28 @@ import { NoteCreationService } from '../../services/note-creation.service';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="divide-y divide-gray-100 dark:divide-gray-800">
|
<ul class="notes-list">
|
||||||
<li *ngFor="let n of filtered()"
|
<li *ngFor="let n of filtered()"
|
||||||
|
class="note-row cursor-pointer"
|
||||||
|
[class.active]="selectedId() === n.id"
|
||||||
[ngClass]="getListItemClasses()"
|
[ngClass]="getListItemClasses()"
|
||||||
class="hover:bg-surface1 dark:hover:bg-card cursor-pointer transition-colors"
|
|
||||||
(click)="openNote.emit(n.id)">
|
(click)="openNote.emit(n.id)">
|
||||||
<!-- Compact View -->
|
<!-- Compact View -->
|
||||||
<div *ngIf="state.viewMode() === 'compact'" class="px-3 py-1.5">
|
<div *ngIf="state.viewMode() === 'compact'" class="note-inner">
|
||||||
<div class="text-xs font-semibold truncate">{{ n.title }}</div>
|
<div class="title text-xs truncate">{{ n.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comfortable View (default) -->
|
<!-- Comfortable View (default) -->
|
||||||
<div *ngIf="state.viewMode() === 'comfortable'" class="p-3">
|
<div *ngIf="state.viewMode() === 'comfortable'" class="note-inner">
|
||||||
<div class="text-sm font-semibold truncate">{{ n.title }}</div>
|
<div class="title text-sm truncate">{{ n.title }}</div>
|
||||||
<div class="text-xs text-muted truncate">{{ n.filePath }}</div>
|
<div class="meta text-xs truncate">{{ n.filePath }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detailed View -->
|
<!-- Detailed View -->
|
||||||
<div *ngIf="state.viewMode() === 'detailed'" class="p-3 space-y-1.5">
|
<div *ngIf="state.viewMode() === 'detailed'" class="note-inner space-y-1.5">
|
||||||
<div class="text-sm font-semibold truncate">{{ n.title }}</div>
|
<div class="title text-sm truncate">{{ n.title }}</div>
|
||||||
<div class="text-xs text-muted truncate">{{ n.filePath }}</div>
|
<div class="meta text-xs truncate">{{ n.filePath }}</div>
|
||||||
<div class="text-xs text-muted/70">
|
<div class="excerpt text-xs">
|
||||||
<span *ngIf="n.frontmatter?.status">Status: {{ n.frontmatter.status }}</span>
|
<span *ngIf="n.frontmatter?.status">Status: {{ n.frontmatter.status }}</span>
|
||||||
<span *ngIf="n.mtime" class="ml-2">{{ formatDate(n.mtime) }}</span>
|
<span *ngIf="n.mtime" class="ml-2">{{ formatDate(n.mtime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -162,13 +163,65 @@ import { NoteCreationService } from '../../services/note-creation.service';
|
|||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
/* Darken the list background a bit to separate from sidebar and note view */
|
||||||
|
background: var(--list-panel-bg);
|
||||||
|
position: relative;
|
||||||
|
z-index: 0; /* allow external overlays (e.g., sidebar context menu) to appear above */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle vertical depth overlay */
|
||||||
|
:host::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
color-mix(in oklab, var(--card) 100%, transparent 0%) 0%,
|
||||||
|
color-mix(in oklab, var(--card) 96%, black 0%) 35%,
|
||||||
|
color-mix(in oklab, var(--card) 92%, black 0%) 100%
|
||||||
|
);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theming variables per color scheme */
|
||||||
|
:host {
|
||||||
|
--row-radius: 8px;
|
||||||
|
--row-pad-v: 12px;
|
||||||
|
--row-pad-h: 16px;
|
||||||
|
--row-gap: 2px;
|
||||||
|
--active-line: var(--primary, #3b82f6);
|
||||||
|
--meta-color: var(--text-muted);
|
||||||
|
--row-bg: color-mix(in oklab, var(--card) 97%, black 0%);
|
||||||
|
--row-bg-hover: color-mix(in oklab, var(--card) 100%, white 6%);
|
||||||
|
--row-shadow-active: 0 2px 10px color-mix(in oklab, var(--active-line) 18%, transparent 82%);
|
||||||
|
/* Slightly darker base for the panel for better column separation */
|
||||||
|
--list-panel-bg: color-mix(in oklab, var(--card) 92%, black 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(html.dark) {
|
||||||
|
/* In dark themes, lighten rows a bit on hover */
|
||||||
|
--row-bg: color-mix(in oklab, var(--card) 94%, white 0%);
|
||||||
|
--row-bg-hover: color-mix(in oklab, var(--card) 90%, white 10%);
|
||||||
|
/* List panel still darker than note view for separation */
|
||||||
|
--list-panel-bg: color-mix(in oklab, var(--card) 86%, black 14%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(html:not(.dark)) {
|
||||||
|
/* In light themes, darken rows subtly */
|
||||||
|
--row-bg: color-mix(in oklab, var(--card) 94%, black 6%);
|
||||||
|
--row-bg-hover: color-mix(in oklab, var(--card) 90%, black 10%);
|
||||||
|
/* Subtle tint for separation in light mode */
|
||||||
|
--list-panel-bg: color-mix(in oklab, var(--card) 96%, black 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-scroll {
|
.list-scroll {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scrollbar-gutter: stable both-edges;
|
/* Reserve scrollbar space only on the end edge to avoid large left/right gaps */
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
padding-inline: 0;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
contain: content;
|
contain: content;
|
||||||
}
|
}
|
||||||
@ -186,6 +239,98 @@ import { NoteCreationService } from '../../services/note-creation.service';
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Notes list and rows */
|
||||||
|
.notes-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0; /* no side padding so cards can be edge-to-edge */
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-row {
|
||||||
|
position: relative;
|
||||||
|
margin: var(--row-gap) 0; /* flush to left/right edges */
|
||||||
|
border-radius: var(--row-radius);
|
||||||
|
background: var(--row-bg);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-row:hover {
|
||||||
|
background: var(--row-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-row.active::before,
|
||||||
|
.note-row.active::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--active-line);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-row.active::before { top: 0; }
|
||||||
|
.note-row.active::after { bottom: 0; }
|
||||||
|
|
||||||
|
.note-row.active {
|
||||||
|
box-shadow: var(--row-shadow-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-inner {
|
||||||
|
padding: var(--row-pad-v) var(--row-pad-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--text-main, #111);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(html.dark) .title {
|
||||||
|
color: var(--text-main, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-row.active .title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
color: var(--meta-color, #6b7280);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
:host-context(html.dark) .meta {
|
||||||
|
color: var(--meta-color, #94a3b8);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excerpt {
|
||||||
|
color: var(--meta-color);
|
||||||
|
opacity: 0.75;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the gradient overlay sits beneath any external overlays */
|
||||||
|
:host::before { z-index: 0; }
|
||||||
|
|
||||||
|
/* Improve New Note button contrast across themes */
|
||||||
|
.new-note-btn {
|
||||||
|
color: var(--primary-foreground, #fff);
|
||||||
|
border: 1px solid color-mix(in oklab, var(--primary) 30%, black 70%);
|
||||||
|
text-shadow: 0 0 0 transparent;
|
||||||
|
}
|
||||||
|
:host-context(html:not(.dark)) .new-note-btn {
|
||||||
|
/* In light themes, ensure minimum contrast */
|
||||||
|
border-color: color-mix(in oklab, var(--primary) 45%, black 55%);
|
||||||
|
}
|
||||||
|
:host-context(html.dark) .new-note-btn {
|
||||||
|
/* In dark themes, keep readable text color if theme var is too dim */
|
||||||
|
color: var(--primary-foreground, #fafafa);
|
||||||
|
border-color: color-mix(in oklab, var(--primary) 35%, white 65%);
|
||||||
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class NotesListComponent {
|
export class NotesListComponent {
|
||||||
@ -194,6 +339,7 @@ export class NotesListComponent {
|
|||||||
query = input<string>('');
|
query = input<string>('');
|
||||||
tagFilter = input<string | null>(null);
|
tagFilter = input<string | null>(null);
|
||||||
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
|
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
|
||||||
|
selectedId = input<string | null>(null);
|
||||||
|
|
||||||
@Output() openNote = new EventEmitter<string>();
|
@Output() openNote = new EventEmitter<string>();
|
||||||
@Output() queryChange = new EventEmitter<string>();
|
@Output() queryChange = new EventEmitter<string>();
|
||||||
|
|||||||
@ -48,14 +48,16 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
(scrolledIndexChange)="onScroll($event)"
|
(scrolledIndexChange)="onScroll($event)"
|
||||||
appScrollableOverlay>
|
appScrollableOverlay>
|
||||||
|
|
||||||
<ul class="divide-y divide-gray-100 dark:divide-gray-800">
|
<ul class="notes-list">
|
||||||
<!-- Virtual items -->
|
<!-- Virtual items -->
|
||||||
<li *cdkVirtualFor="let note of paginatedNotes(); trackBy: trackByFn"
|
<li *cdkVirtualFor="let note of paginatedNotes(); trackBy: trackByFn"
|
||||||
class="p-3 hover:bg-surface1 dark:hover:bg-card cursor-pointer transition-colors min-h-[60px] flex flex-col justify-center"
|
class="note-row cursor-pointer"
|
||||||
[class.selected]="note.id === selectedNoteId()"
|
[class.active]="(selectedId() ?? selectedNoteId()) === note.id"
|
||||||
(click)="selectNote(note)">
|
(click)="selectNote(note)">
|
||||||
<div class="text-sm font-semibold truncate">{{ note.title }}</div>
|
<div class="note-inner">
|
||||||
<div class="text-xs text-muted truncate">{{ note.filePath }}</div>
|
<div class="title text-sm truncate">{{ note.title }}</div>
|
||||||
|
<div class="meta text-xs truncate">{{ note.filePath }}</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Loading indicator -->
|
<!-- Loading indicator -->
|
||||||
@ -85,37 +87,97 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
background: var(--list-panel-bg);
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes-list-container {
|
/* Subtle vertical depth overlay */
|
||||||
min-height: 400px;
|
:host::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
color-mix(in oklab, var(--card) 100%, transparent 0%) 0%,
|
||||||
|
color-mix(in oklab, var(--card) 96%, black 0%) 35%,
|
||||||
|
color-mix(in oklab, var(--card) 92%, black 0%) 100%
|
||||||
|
);
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-item {
|
/* Theming variables per color scheme */
|
||||||
min-height: 60px;
|
:host {
|
||||||
display: flex;
|
--row-radius: 8px;
|
||||||
flex-direction: column;
|
--row-pad-v: 12px;
|
||||||
justify-content: center;
|
--row-pad-h: 16px;
|
||||||
|
--row-gap: 2px;
|
||||||
|
--active-line: var(--primary, #3b82f6);
|
||||||
|
--meta-color: var(--text-muted);
|
||||||
|
--row-bg: color-mix(in oklab, var(--card) 97%, black 0%);
|
||||||
|
--row-bg-hover: color-mix(in oklab, var(--card) 100%, white 6%);
|
||||||
|
--row-shadow-active: 0 2px 10px color-mix(in oklab, var(--active-line) 18%, transparent 82%);
|
||||||
|
--list-panel-bg: color-mix(in oklab, var(--card) 92%, black 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-item.selected {
|
:host-context(html.dark) {
|
||||||
background-color: var(--surface1);
|
--row-bg: color-mix(in oklab, var(--card) 94%, white 0%);
|
||||||
border-left: 3px solid var(--primary);
|
--row-bg-hover: color-mix(in oklab, var(--card) 90%, white 10%);
|
||||||
|
--list-panel-bg: color-mix(in oklab, var(--card) 86%, black 14%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(html:not(.dark)) {
|
||||||
|
--row-bg: color-mix(in oklab, var(--card) 94%, black 6%);
|
||||||
|
--row-bg-hover: color-mix(in oklab, var(--card) 90%, black 10%);
|
||||||
|
--list-panel-bg: color-mix(in oklab, var(--card) 96%, black 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
cdk-virtual-scroll-viewport {
|
cdk-virtual-scroll-viewport {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
/* Notes list and rows (match non-virtual list) */
|
||||||
|
.notes-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 2px 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
.note-row {
|
||||||
margin: 0;
|
position: relative;
|
||||||
|
margin: var(--row-gap) 1px;
|
||||||
|
border-radius: var(--row-radius);
|
||||||
|
background: var(--row-bg);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
min-height: 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-row:hover { background: var(--row-bg-hover); }
|
||||||
|
|
||||||
|
.note-row.active::before,
|
||||||
|
.note-row.active::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; height: 2px;
|
||||||
|
background: var(--active-line);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.note-row.active::before { top: 0; }
|
||||||
|
.note-row.active::after { bottom: 0; }
|
||||||
|
.note-row.active { box-shadow: var(--row-shadow-active); }
|
||||||
|
|
||||||
|
.note-inner { padding: var(--row-pad-v) var(--row-pad-h); }
|
||||||
|
.title { color: var(--text-main, #111); font-weight: 500; }
|
||||||
|
:host-context(html.dark) .title { color: var(--text-main, #e5e7eb); }
|
||||||
|
.note-row.active .title { font-weight: 600; }
|
||||||
|
.meta { color: var(--meta-color, #6b7280); opacity: 0.8; }
|
||||||
|
:host-context(html.dark) .meta { color: var(--meta-color, #94a3b8); opacity: 0.9; }
|
||||||
|
.excerpt { color: var(--meta-color); opacity: 0.75; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
:host::before { z-index: 0; }
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
||||||
@ -130,6 +192,7 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
|||||||
query = input<string>('');
|
query = input<string>('');
|
||||||
tagFilter = input<string | null>(null);
|
tagFilter = input<string | null>(null);
|
||||||
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
|
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
|
||||||
|
selectedId = input<string | null>(null);
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
@Output() openNote = new EventEmitter<string>();
|
@Output() openNote = new EventEmitter<string>();
|
||||||
|
|||||||
@ -301,6 +301,160 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chips editor */
|
||||||
|
.chips-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--bg-muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-text {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-remove {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-remove:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-input-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-input {
|
||||||
|
min-width: 18rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--bg-main);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch */
|
||||||
|
.toggle-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--surface-1, #e5e7eb);
|
||||||
|
border-radius: 9999px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-input:checked + .toggle-label .toggle-slider {
|
||||||
|
background-color: var(--primary, #3b82f6);
|
||||||
|
border-color: var(--primary, #3b82f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-input:checked + .toggle-label .toggle-slider::after {
|
||||||
|
transform: translateX(1.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.dark) .toggle-slider {
|
||||||
|
background-color: var(--surface-2, #1e293b);
|
||||||
|
border-color: var(--border, #334155);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.dark) .toggle-slider::after {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset Button */
|
||||||
|
.reset-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-muted);
|
||||||
|
color: var(--text-main);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:hover {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-color: var(--primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.dark) .reset-button {
|
||||||
|
background: var(--card);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.dark) .reset-button:hover {
|
||||||
|
background: var(--surface-1);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.parameters-page {
|
.parameters-page {
|
||||||
|
|||||||
@ -112,6 +112,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Folder Filtering Section -->
|
||||||
|
<section class="parameters-section">
|
||||||
|
<h2 class="section-title">
|
||||||
|
<svg class="section-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||||
|
<circle cx="12" cy="13" r="3"/>
|
||||||
|
</svg>
|
||||||
|
Folder Filtering
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid-layout">
|
||||||
|
<!-- Hidden Folders Filter -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label">Exclude Hidden Folders</label>
|
||||||
|
<div class="toggle-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="excludeHidden"
|
||||||
|
[checked]="folderFilterConfig().excludeHiddenFolders"
|
||||||
|
(change)="toggleHiddenFolders()"
|
||||||
|
class="toggle-input" />
|
||||||
|
<label for="excludeHidden" class="toggle-label">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="setting-hint">Hide folders starting with a dot (e.g., .obsidian, .trash, .git)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic Excluded Folders -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label">Excluded folders</label>
|
||||||
|
<div class="chips-container">
|
||||||
|
<span
|
||||||
|
class="chip"
|
||||||
|
*ngFor="let p of folderFilterConfig().customExclusions"
|
||||||
|
>
|
||||||
|
<span class="chip-text">{{ p }}</span>
|
||||||
|
<button type="button" class="chip-remove" (click)="removeExcludedFolder(p)" aria-label="Remove">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<div class="chip-input-wrapper">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newExcludedFolder"
|
||||||
|
(keydown.enter)="addExcludedFolder()"
|
||||||
|
placeholder="Add folder (e.g., .obsidian, attachments, old_notes)"
|
||||||
|
class="chip-input"
|
||||||
|
/>
|
||||||
|
<button type="button" class="add-btn btn-colored-xs btn-neutral" (click)="addExcludedFolder()">+ Add folder</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="setting-hint">Folders listed here will be hidden from the sidebar. Paths are matched recursively.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset Button -->
|
||||||
|
<div class="setting-group full-width">
|
||||||
|
<button
|
||||||
|
(click)="resetFolderFilters()"
|
||||||
|
class="reset-button btn-colored-xs btn-neutral">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
|
||||||
|
<path d="M21 3v5h-5"/>
|
||||||
|
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
||||||
|
<path d="M3 21v-5h5"/>
|
||||||
|
</svg>
|
||||||
|
Reset to Defaults
|
||||||
|
</button>
|
||||||
|
<p class="setting-hint">Restore default folder filtering settings</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Vault Stats Section (Placeholder) -->
|
<!-- Vault Stats Section (Placeholder) -->
|
||||||
<section class="parameters-section">
|
<section class="parameters-section">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
|
|||||||
@ -1,21 +1,26 @@
|
|||||||
import { Component, inject, signal, effect } from '@angular/core';
|
import { Component, inject, signal, effect } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ThemeService, ThemeMode, ThemeId, Language } from '../../core/services/theme.service';
|
import { ThemeService, ThemeMode, ThemeId, Language } from '../../core/services/theme.service';
|
||||||
import { ToastService } from '../../shared/toast/toast.service';
|
import { ToastService } from '../../shared/toast/toast.service';
|
||||||
|
import { FolderFilterService, FolderFilterConfig } from '../../services/folder-filter.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: 'app-parameters',
|
selector: 'app-parameters',
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, FormsModule],
|
||||||
templateUrl: './parameters.page.html',
|
templateUrl: './parameters.page.html',
|
||||||
styleUrls: ['./parameters.page.css']
|
styleUrls: ['./parameters.page.css']
|
||||||
})
|
})
|
||||||
export class ParametersPage {
|
export class ParametersPage {
|
||||||
private themeService = inject(ThemeService);
|
private themeService = inject(ThemeService);
|
||||||
private toastService = inject(ToastService);
|
private toastService = inject(ToastService);
|
||||||
|
private folderFilterService = inject(FolderFilterService);
|
||||||
|
|
||||||
// Reactive prefs
|
// Reactive prefs
|
||||||
prefs = signal(this.themeService.prefsValue);
|
prefs = signal(this.themeService.prefsValue);
|
||||||
|
folderFilterConfig = signal<FolderFilterConfig>(this.folderFilterService.getConfig());
|
||||||
|
newExcludedFolder = '';
|
||||||
|
|
||||||
modes: ThemeMode[] = ['system', 'light', 'dark'];
|
modes: ThemeMode[] = ['system', 'light', 'dark'];
|
||||||
themes: ThemeId[] = ['light', 'dark', 'obsidian', 'nord', 'notion', 'github', 'discord', 'monokai'];
|
themes: ThemeId[] = ['light', 'dark', 'obsidian', 'nord', 'notion', 'github', 'discord', 'monokai'];
|
||||||
@ -77,4 +82,50 @@ export class ParametersPage {
|
|||||||
};
|
};
|
||||||
return previews[themeId];
|
return previews[themeId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Folder Filter Methods
|
||||||
|
toggleHiddenFolders(): void {
|
||||||
|
const config = this.folderFilterConfig();
|
||||||
|
this.folderFilterService.updateConfig({
|
||||||
|
...config,
|
||||||
|
excludeHiddenFolders: !config.excludeHiddenFolders
|
||||||
|
});
|
||||||
|
this.folderFilterConfig.set(this.folderFilterService.getConfig());
|
||||||
|
this.showToast('Hidden folders filter updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAttachments(): void {
|
||||||
|
const config = this.folderFilterConfig();
|
||||||
|
this.folderFilterService.updateConfig({
|
||||||
|
...config,
|
||||||
|
excludeAttachments: !config.excludeAttachments
|
||||||
|
});
|
||||||
|
this.folderFilterConfig.set(this.folderFilterService.getConfig());
|
||||||
|
this.showToast('Attachments filter updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic excluded folders list management
|
||||||
|
addExcludedFolder(): void {
|
||||||
|
const raw = (this.newExcludedFolder || '').trim();
|
||||||
|
if (!raw) return;
|
||||||
|
// Normalize path (remove leading/trailing slashes)
|
||||||
|
const normalized = raw.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||||
|
if (!normalized) return;
|
||||||
|
this.folderFilterService.addCustomExclusion(normalized);
|
||||||
|
this.folderFilterConfig.set(this.folderFilterService.getConfig());
|
||||||
|
this.newExcludedFolder = '';
|
||||||
|
this.showToast('Folder added to exclusions');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeExcludedFolder(path: string): void {
|
||||||
|
this.folderFilterService.removeCustomExclusion(path);
|
||||||
|
this.folderFilterConfig.set(this.folderFilterService.getConfig());
|
||||||
|
this.showToast('Folder removed from exclusions');
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFolderFilters(): void {
|
||||||
|
this.folderFilterService.resetToDefaults();
|
||||||
|
this.folderFilterConfig.set(this.folderFilterService.getConfig());
|
||||||
|
this.showToast('Folder filters reset to defaults');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
import { Component, EventEmitter, Input, Output, ViewChild, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component';
|
import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component';
|
||||||
@ -49,10 +49,13 @@ import { VaultService } from '../../../services/vault.service';
|
|||||||
|
|
||||||
<!-- Quick Links accordion -->
|
<!-- Quick Links accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
<button class="w-full flex items-center px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||||
(click)="open.quick = !open.quick">
|
(click)="open.quick = !open.quick">
|
||||||
<span class="flex items-center gap-2">⚡ <span>Quick Links</span></span>
|
<span class="flex items-center gap-2">
|
||||||
<span class="text-xs text-muted">{{ open.quick ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted">{{ open.quick ? '▾' : '▸' }}</span>
|
||||||
|
<span>⚡</span>
|
||||||
|
<span>Quick Links</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="open.quick" class="pt-1">
|
<div *ngIf="open.quick" class="pt-1">
|
||||||
<app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
|
<app-quick-links (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
|
||||||
@ -61,13 +64,21 @@ import { VaultService } from '../../../services/vault.service';
|
|||||||
|
|
||||||
<!-- Folders accordion -->
|
<!-- Folders accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
<div class="w-full flex items-center px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card">
|
||||||
(click)="toggleFoldersSection()">
|
<button class="flex items-center gap-2 flex-1 text-left" (click)="toggleFoldersSection()">
|
||||||
<span class="flex items-center gap-2">📁 <span>Folders</span></span>
|
<span class="text-xs text-muted">{{ open.folders ? '▾' : '▸' }}</span>
|
||||||
<span class="text-xs text-muted">{{ open.folders ? '▾' : '▸' }}</span>
|
<span>📁</span>
|
||||||
</button>
|
<span>Folders</span>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="open.folders" (click)="onCreateFolderAtRoot()" title="Create Folder" class="flex items-center gap-1 p-2 rounded hover:bg-surface1 dark:hover:bg-card">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -ml-1" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 010 2h-3v3a1 1 0 01-2 0v-3H6a1 1 0 010-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div *ngIf="open.folders" class="px-1 py-1">
|
<div *ngIf="open.folders" class="px-1 py-1">
|
||||||
<app-file-explorer
|
<app-file-explorer
|
||||||
|
#foldersExplorer
|
||||||
[nodes]="effectiveFileTree"
|
[nodes]="effectiveFileTree"
|
||||||
[selectedNoteId]="selectedNoteId"
|
[selectedNoteId]="selectedNoteId"
|
||||||
[foldersOnly]="true"
|
[foldersOnly]="true"
|
||||||
@ -80,10 +91,13 @@ import { VaultService } from '../../../services/vault.service';
|
|||||||
|
|
||||||
<!-- Tags accordion -->
|
<!-- Tags accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
<button class="w-full flex items-center px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||||
(click)="open.tags = !open.tags">
|
(click)="open.tags = !open.tags">
|
||||||
<span class="flex items-center gap-2">🏷️ <span>Tags</span></span>
|
<span class="flex items-center gap-2">
|
||||||
<span class="text-xs text-muted">{{ open.tags ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted">{{ open.tags ? '▾' : '▸' }}</span>
|
||||||
|
<span>🏷️</span>
|
||||||
|
<span>Tags</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="open.tags" class="px-2 py-2">
|
<div *ngIf="open.tags" class="px-2 py-2">
|
||||||
<ul class="space-y-0.5 text-sm">
|
<ul class="space-y-0.5 text-sm">
|
||||||
@ -100,10 +114,13 @@ import { VaultService } from '../../../services/vault.service';
|
|||||||
|
|
||||||
<!-- Trash accordion -->
|
<!-- Trash accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
<button class="w-full flex items-center px-3 py-2 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card"
|
||||||
(click)="toggleTrashSection()">
|
(click)="toggleTrashSection()">
|
||||||
<span class="flex items-center gap-2">🗑️ <span>Trash</span></span>
|
<span class="flex items-center gap-2">
|
||||||
<span class="text-xs text-muted">{{ open.trash ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted">{{ open.trash ? '▾' : '▸' }}</span>
|
||||||
|
<span>🗑️</span>
|
||||||
|
<span>Trash</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div *ngIf="open.trash" class="px-1 py-2">
|
<div *ngIf="open.trash" class="px-1 py-2">
|
||||||
<ng-container *ngIf="trashHasContent(); else emptyTrash">
|
<ng-container *ngIf="trashHasContent(); else emptyTrash">
|
||||||
@ -166,6 +183,7 @@ export class NimbusSidebarComponent {
|
|||||||
env = environment;
|
env = environment;
|
||||||
open = { quick: true, folders: false, tags: false, trash: false, tests: false };
|
open = { quick: true, folders: false, tags: false, trash: false, tests: false };
|
||||||
private vault = inject(VaultService);
|
private vault = inject(VaultService);
|
||||||
|
@ViewChild('foldersExplorer') private foldersExplorer?: FileExplorerComponent;
|
||||||
|
|
||||||
onQuickLink(id: string) { this.quickLinkSelected.emit(id); }
|
onQuickLink(id: string) { this.quickLinkSelected.emit(id); }
|
||||||
|
|
||||||
@ -190,6 +208,16 @@ export class NimbusSidebarComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCreateFolderAtRoot(): void {
|
||||||
|
// If not yet rendered, open the section first, then defer action
|
||||||
|
if (!this.open.folders) {
|
||||||
|
this.open.folders = true;
|
||||||
|
setTimeout(() => this.foldersExplorer?.openCreateAtRoot(), 0);
|
||||||
|
} else {
|
||||||
|
this.foldersExplorer?.openCreateAtRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggleTrashSection(): void {
|
toggleTrashSection(): void {
|
||||||
const next = !this.open.trash;
|
const next = !this.open.trash;
|
||||||
this.open.trash = next;
|
this.open.trash = next;
|
||||||
|
|||||||
@ -179,6 +179,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
|||||||
[tagFilter]="tagFilter"
|
[tagFilter]="tagFilter"
|
||||||
[quickLinkFilter]="quickLinkFilter"
|
[quickLinkFilter]="quickLinkFilter"
|
||||||
[query]="listQuery"
|
[query]="listQuery"
|
||||||
|
[selectedId]="selectedNoteId"
|
||||||
(openNote)="onOpenNote($event)"
|
(openNote)="onOpenNote($event)"
|
||||||
(queryChange)="onQueryChange($event)"
|
(queryChange)="onQueryChange($event)"
|
||||||
(clearQuickLinkFilter)="onClearQuickLinkFilter()"
|
(clearQuickLinkFilter)="onClearQuickLinkFilter()"
|
||||||
@ -257,7 +258,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
|||||||
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
|
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
|
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
|
||||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onOpenNote($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()"></app-notes-list>
|
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" [selectedId]="selectedNoteId" (queryChange)="onQueryChange($event)" (openNote)="onOpenNote($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()"></app-notes-list>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
|
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
|
||||||
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
|
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
|
||||||
@ -288,7 +289,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
|||||||
|
|
||||||
@if (mobileNav.activeTab() === 'list') {
|
@if (mobileNav.activeTab() === 'list') {
|
||||||
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
|
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
|
||||||
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onNoteSelectedMobile($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()"></app-notes-list>
|
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" [selectedId]="selectedNoteId" (queryChange)="onQueryChange($event)" (openNote)="onNoteSelectedMobile($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()"></app-notes-list>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
145
src/app/services/folder-filter.service.ts
Normal file
145
src/app/services/folder-filter.service.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { Injectable, signal, computed } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Folder Filter Service
|
||||||
|
* Manages dynamic folder filtering to exclude certain folders from display
|
||||||
|
* in the Folders section of the application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface FolderFilterConfig {
|
||||||
|
excludeHiddenFolders: boolean; // Exclude folders starting with . (e.g., .obsidian, .trash)
|
||||||
|
excludeAttachments: boolean; // Exclude 'attachments' folder
|
||||||
|
customExclusions: string[]; // Custom folder paths to exclude
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FolderFilterService {
|
||||||
|
// Default filter configuration
|
||||||
|
private readonly DEFAULT_CONFIG: FolderFilterConfig = {
|
||||||
|
excludeHiddenFolders: true,
|
||||||
|
excludeAttachments: true,
|
||||||
|
customExclusions: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reactive state
|
||||||
|
private config = signal<FolderFilterConfig>(this.loadConfig());
|
||||||
|
|
||||||
|
// Computed derived state
|
||||||
|
readonly isHiddenFoldersExcluded = computed(() => this.config().excludeHiddenFolders);
|
||||||
|
readonly isAttachmentsExcluded = computed(() => this.config().excludeAttachments);
|
||||||
|
readonly customExclusions = computed(() => this.config().customExclusions);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Initialize from localStorage
|
||||||
|
this.loadConfigFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from localStorage or use defaults
|
||||||
|
*/
|
||||||
|
private loadConfig(): FolderFilterConfig {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('folderFilterConfig');
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load folder filter config:', e);
|
||||||
|
}
|
||||||
|
return { ...this.DEFAULT_CONFIG };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from storage on initialization
|
||||||
|
*/
|
||||||
|
private loadConfigFromStorage(): void {
|
||||||
|
const stored = this.loadConfig();
|
||||||
|
this.config.set(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the filter configuration
|
||||||
|
*/
|
||||||
|
updateConfig(newConfig: Partial<FolderFilterConfig>): void {
|
||||||
|
const updated = { ...this.config(), ...newConfig };
|
||||||
|
this.config.set(updated);
|
||||||
|
this.persistConfig(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist configuration to localStorage
|
||||||
|
*/
|
||||||
|
private persistConfig(config: FolderFilterConfig): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('folderFilterConfig', JSON.stringify(config));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to persist folder filter config:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a folder should be filtered out (hidden)
|
||||||
|
*/
|
||||||
|
shouldFilterFolder(folderPath: string): boolean {
|
||||||
|
if (!folderPath) return false;
|
||||||
|
|
||||||
|
const normalizedPath = folderPath.replace(/\\/g, '/').toLowerCase();
|
||||||
|
const folderName = normalizedPath.split('/').pop() || '';
|
||||||
|
|
||||||
|
// Check hidden folders (starting with .)
|
||||||
|
if (this.config().excludeHiddenFolders && folderName.startsWith('.')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check attachments folder
|
||||||
|
if (this.config().excludeAttachments && folderName === 'attachments') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom exclusions
|
||||||
|
for (const exclusion of this.config().customExclusions) {
|
||||||
|
const normalizedExclusion = exclusion.replace(/\\/g, '/').toLowerCase();
|
||||||
|
if (normalizedPath === normalizedExclusion || normalizedPath.startsWith(normalizedExclusion + '/')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom folder exclusion
|
||||||
|
*/
|
||||||
|
addCustomExclusion(folderPath: string): void {
|
||||||
|
const exclusions = [...this.config().customExclusions];
|
||||||
|
if (!exclusions.includes(folderPath)) {
|
||||||
|
exclusions.push(folderPath);
|
||||||
|
this.updateConfig({ customExclusions: exclusions });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a custom folder exclusion
|
||||||
|
*/
|
||||||
|
removeCustomExclusion(folderPath: string): void {
|
||||||
|
const exclusions = this.config().customExclusions.filter(e => e !== folderPath);
|
||||||
|
this.updateConfig({ customExclusions: exclusions });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset to default configuration
|
||||||
|
*/
|
||||||
|
resetToDefaults(): void {
|
||||||
|
this.config.set({ ...this.DEFAULT_CONFIG });
|
||||||
|
this.persistConfig(this.DEFAULT_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current configuration
|
||||||
|
*/
|
||||||
|
getConfig(): FolderFilterConfig {
|
||||||
|
return { ...this.config() };
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/app/services/notes-list-focus.service.ts
Normal file
66
src/app/services/notes-list-focus.service.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notes List Focus Service
|
||||||
|
* Manages the focus/selection state of the Notes-list component
|
||||||
|
* Allows resetting focus when folders are deleted or other operations occur
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class NotesListFocusService {
|
||||||
|
// Track the currently selected folder path
|
||||||
|
private selectedFolderPath = signal<string | null>(null);
|
||||||
|
|
||||||
|
// Signal to trigger focus reset
|
||||||
|
private resetFocusTrigger = signal<number>(0);
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the currently selected folder path
|
||||||
|
*/
|
||||||
|
setSelectedFolder(folderPath: string | null): void {
|
||||||
|
this.selectedFolderPath.set(folderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently selected folder path
|
||||||
|
*/
|
||||||
|
getSelectedFolder(): string | null {
|
||||||
|
return this.selectedFolderPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the focus/selection
|
||||||
|
* This should be called when a folder is deleted or when we need to clear the selection
|
||||||
|
*/
|
||||||
|
resetFocus(): void {
|
||||||
|
this.selectedFolderPath.set(null);
|
||||||
|
// Increment trigger to notify subscribers
|
||||||
|
this.resetFocusTrigger.set(this.resetFocusTrigger() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the reset focus trigger signal
|
||||||
|
* Components can subscribe to this to know when to reset their focus
|
||||||
|
*/
|
||||||
|
getResetFocusTrigger() {
|
||||||
|
return this.resetFocusTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific folder is currently selected
|
||||||
|
*/
|
||||||
|
isFolderSelected(folderPath: string): boolean {
|
||||||
|
return this.selectedFolderPath() === folderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all focus state
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.selectedFolderPath.set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
<span
|
<span
|
||||||
class="inline-flex items-center justify-center w-6 h-6 rounded-full text-xs font-semibold text-white"
|
class="inline-flex items-center justify-center w-6 h-6 rounded-full text-xs font-semibold text-white"
|
||||||
[ngClass]="bgClass"
|
[ngClass]="bgClass"
|
||||||
|
[ngStyle]="mergedStyle"
|
||||||
aria-label="count"
|
aria-label="count"
|
||||||
>
|
>
|
||||||
{{ count }}
|
{{ count }}
|
||||||
@ -17,7 +18,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
})
|
})
|
||||||
export class BadgeCountComponent {
|
export class BadgeCountComponent {
|
||||||
@Input() count = 0;
|
@Input() count = 0;
|
||||||
@Input() color: 'slate'|'rose'|'amber'|'indigo'|'emerald'|'stone'|'zinc'|'green'|'purple' = 'slate';
|
@Input() color: string = 'slate';
|
||||||
|
|
||||||
get bgClass() {
|
get bgClass() {
|
||||||
return {
|
return {
|
||||||
@ -32,4 +33,30 @@ export class BadgeCountComponent {
|
|||||||
'bg-purple-500': this.color === 'purple',
|
'bg-purple-500': this.color === 'purple',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get bgStyle() {
|
||||||
|
const isPredefined = ['slate','rose','amber','indigo','emerald','stone','zinc','green','purple'].includes(this.color);
|
||||||
|
if (isPredefined) return {};
|
||||||
|
// Assume custom CSS color string (e.g., #RRGGBB, rgb(), hsl())
|
||||||
|
return { backgroundColor: this.color } as Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fgStyle() {
|
||||||
|
const isPredefined = ['slate','rose','amber','indigo','emerald','stone','zinc','green','purple'].includes(this.color);
|
||||||
|
if (isPredefined) return {};
|
||||||
|
const hexMatch = /^#([0-9a-fA-F]{6})$/.exec(this.color);
|
||||||
|
if (!hexMatch) return {};
|
||||||
|
const hex = hexMatch[1];
|
||||||
|
const r = parseInt(hex.slice(0,2), 16) / 255;
|
||||||
|
const g = parseInt(hex.slice(2,4), 16) / 255;
|
||||||
|
const b = parseInt(hex.slice(4,6), 16) / 255;
|
||||||
|
// Relative luminance approximation
|
||||||
|
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
const color = lum > 0.6 ? '#0f172a' : '#ffffff';
|
||||||
|
return { color } as Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mergedStyle() {
|
||||||
|
return { ...this.bgStyle, ...this.fgStyle } as Record<string, string>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ type CtxAction =
|
|||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
styles: [`
|
styles: [`
|
||||||
:host { position: fixed; inset: 0; pointer-events: none; }
|
:host { position: fixed; inset: 0; pointer-events: none; z-index: 9999; }
|
||||||
.ctx {
|
.ctx {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
min-width: 14rem;
|
min-width: 14rem;
|
||||||
@ -44,6 +44,7 @@ type CtxAction =
|
|||||||
background: var(--card, #ffffff);
|
background: var(--card, #ffffff);
|
||||||
border: 1px solid var(--border, #e5e7eb);
|
border: 1px solid var(--border, #e5e7eb);
|
||||||
color: var(--fg, #111827);
|
color: var(--fg, #111827);
|
||||||
|
z-index: 10000;
|
||||||
}
|
}
|
||||||
.item {
|
.item {
|
||||||
display: block; width: 100%;
|
display: block; width: 100%;
|
||||||
@ -94,7 +95,7 @@ type CtxAction =
|
|||||||
template: `
|
template: `
|
||||||
<ng-container *ngIf="visible">
|
<ng-container *ngIf="visible">
|
||||||
<!-- Backdrop pour capter les clics extérieurs -->
|
<!-- Backdrop pour capter les clics extérieurs -->
|
||||||
<div class="fixed inset-0" (click)="close()" aria-hidden="true"></div>
|
<div class="fixed inset-0" (click)="close()" aria-hidden="true" style="z-index: 9998;"></div>
|
||||||
|
|
||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
<div
|
<div
|
||||||
@ -176,7 +177,10 @@ export class ContextMenuComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes['visible'] && this.visible) {
|
if (changes['visible'] && this.visible) {
|
||||||
// Laisse le DOM peindre le menu, puis calcule les bornes
|
// Immediately set to click position to avoid flashing at 0,0
|
||||||
|
this.left = this.x;
|
||||||
|
this.top = this.y;
|
||||||
|
// Then reposition for anti-overflow
|
||||||
queueMicrotask(() => this.reposition());
|
queueMicrotask(() => this.reposition());
|
||||||
}
|
}
|
||||||
if ((changes['x'] || changes['y']) && this.visible) {
|
if ((changes['x'] || changes['y']) && this.visible) {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Component, ChangeDetectionStrategy, input, output, inject, signal } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, input, output, inject, signal, effect, computed } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { VaultNode, VaultFile, VaultFolder } from '../../types';
|
import { VaultNode, VaultFile, VaultFolder } from '../../types';
|
||||||
import { VaultService } from '../../services/vault.service';
|
import { VaultService } from '../../services/vault.service';
|
||||||
import { NoteCreationService } from '../../app/services/note-creation.service';
|
import { NoteCreationService } from '../../app/services/note-creation.service';
|
||||||
|
import { NotesListFocusService } from '../../app/services/notes-list-focus.service';
|
||||||
|
import { FolderFilterService } from '../../app/services/folder-filter.service';
|
||||||
import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
|
import { BadgeCountComponent } from '../../app/shared/ui/badge-count.component';
|
||||||
import { ContextMenuComponent } from '../context-menu/context-menu.component';
|
import { ContextMenuComponent } from '../context-menu/context-menu.component';
|
||||||
|
|
||||||
@ -11,7 +13,7 @@ import { ContextMenuComponent } from '../context-menu/context-menu.component';
|
|||||||
selector: 'app-file-explorer',
|
selector: 'app-file-explorer',
|
||||||
template: `
|
template: `
|
||||||
<ul class="space-y-0.5">
|
<ul class="space-y-0.5">
|
||||||
@for(node of nodes(); track node.path) {
|
@for(node of filteredNodes(); track node.path) {
|
||||||
<li>
|
<li>
|
||||||
@if (isFolder(node) && node.name !== '.trash') {
|
@if (isFolder(node) && node.name !== '.trash') {
|
||||||
@let folder = node;
|
@let folder = node;
|
||||||
@ -20,13 +22,14 @@ import { ContextMenuComponent } from '../context-menu/context-menu.component';
|
|||||||
(click)="onFolderClick(folder)"
|
(click)="onFolderClick(folder)"
|
||||||
(contextmenu)="openContextMenu($event, folder)"
|
(contextmenu)="openContextMenu($event, folder)"
|
||||||
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition"
|
class="flex items-center cursor-pointer px-3 py-2 rounded-lg my-0.5 text-sm hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition"
|
||||||
|
[ngStyle]="getFolderGradientStyle(folder.path)"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform flex-shrink-0" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor" [style.color]="getFolderColor(folder.path)">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 transition-transform flex-shrink-0" [class.rotate-90]="folder.isOpen" fill="none" viewBox="0 0 24 24" stroke="currentColor" [style.color]="getFolderColor(folder.path)">
|
||||||
<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>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" [style.color]="getFolderColor(folder.path)"><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"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" [style.color]="getFolderColor(folder.path)"><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"></path></svg>
|
||||||
<span class="font-semibold truncate">{{ folder.name }}</span>
|
<span class="font-semibold truncate">{{ folder.name }}</span>
|
||||||
<app-badge-count class="ml-auto" [count]="folderCount(folder.path)" color="slate"></app-badge-count>
|
<app-badge-count class="ml-auto" [count]="folderCount(folder.path)" [color]="getFolderBadgeColor(folder.path)"></app-badge-count>
|
||||||
</div>
|
</div>
|
||||||
@if (folder.isOpen) {
|
@if (folder.isOpen) {
|
||||||
<div class="pl-5">
|
<div class="pl-5">
|
||||||
@ -107,6 +110,43 @@ import { ContextMenuComponent } from '../context-menu/context-menu.component';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Subfolder Modal -->
|
||||||
|
<div *ngIf="createModalVisible()" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||||
|
<div class="bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg p-6 w-full max-w-md mx-4">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--fg)] mb-4">Create a new folder</h3>
|
||||||
|
|
||||||
|
<div class="mb-1">
|
||||||
|
<label class="block text-sm font-medium text-[var(--text-main)] mb-2">
|
||||||
|
Folder name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="createInputValue"
|
||||||
|
class="w-full px-3 py-2 border border-[var(--border)] rounded-md bg-[var(--bg)] text-[var(--fg)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||||
|
placeholder="Folder name"
|
||||||
|
(keydown.enter)="confirmCreate()"
|
||||||
|
id="createInput" #createInput
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="createError()" class="text-sm text-red-500 mt-1">{{ createError() }}</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
(click)="cancelCreate()"
|
||||||
|
class="px-4 py-2 text-[var(--text-main)] hover:bg-[var(--surface-1)] rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="confirmCreate()"
|
||||||
|
class="px-4 py-2 bg-[var(--primary)] text-white rounded-md hover:bg-[var(--brand-700)] transition-colors"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, FormsModule, BadgeCountComponent, ContextMenuComponent],
|
imports: [CommonModule, FormsModule, BadgeCountComponent, ContextMenuComponent],
|
||||||
@ -131,11 +171,51 @@ export class FileExplorerComponent {
|
|||||||
renameInputValue = '';
|
renameInputValue = '';
|
||||||
renameTarget: VaultFolder | null = null;
|
renameTarget: VaultFolder | null = null;
|
||||||
|
|
||||||
|
// Create subfolder modal state
|
||||||
|
createModalVisible = signal(false);
|
||||||
|
createInputValue = '';
|
||||||
|
createParentPath: string | null = null;
|
||||||
|
createError = signal('');
|
||||||
|
|
||||||
// Folder colors storage
|
// Folder colors storage
|
||||||
private folderColors = new Map<string, string>();
|
private folderColors = new Map<string, string>();
|
||||||
|
|
||||||
private vaultService = inject(VaultService);
|
private vaultService = inject(VaultService);
|
||||||
private noteCreation = inject(NoteCreationService);
|
private noteCreation = inject(NoteCreationService);
|
||||||
|
private notesListFocus = inject(NotesListFocusService);
|
||||||
|
private folderFilter = inject(FolderFilterService);
|
||||||
|
|
||||||
|
// Computed filtered nodes based on folder filter settings
|
||||||
|
filteredNodes = computed(() => {
|
||||||
|
const allNodes = this.nodes();
|
||||||
|
return this.filterNodes(allNodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively filter nodes based on folder filter settings
|
||||||
|
*/
|
||||||
|
private filterNodes(nodes: VaultNode[]): VaultNode[] {
|
||||||
|
return nodes.filter(node => {
|
||||||
|
if (node.type === 'folder') {
|
||||||
|
// Check if folder should be filtered out
|
||||||
|
if (this.folderFilter.shouldFilterFolder(node.path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Recursively filter children
|
||||||
|
const folder = node as VaultFolder;
|
||||||
|
if (folder.children && folder.children.length > 0) {
|
||||||
|
folder.children = this.filterNodes(folder.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).map(node => {
|
||||||
|
if (node.type === 'folder') {
|
||||||
|
const folder = node as VaultFolder;
|
||||||
|
return { ...folder };
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
folderCount(path: string): number {
|
folderCount(path: string): number {
|
||||||
const quickLink = this.quickLinkFilter();
|
const quickLink = this.quickLinkFilter();
|
||||||
@ -169,6 +249,42 @@ export class FileExplorerComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color for a folder badge, synchronized with folder color
|
||||||
|
*/
|
||||||
|
getFolderBadgeColor(folderPath: string): string {
|
||||||
|
const folderColor = this.getFolderColor(folderPath);
|
||||||
|
// If folder has a custom color, use it for the badge
|
||||||
|
if (folderColor && folderColor !== 'currentColor') {
|
||||||
|
return folderColor;
|
||||||
|
}
|
||||||
|
// Default badge color
|
||||||
|
return 'slate';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a subtle right-to-center horizontal gradient for folder rows
|
||||||
|
* using the custom folder color. Returns an inline style object or null.
|
||||||
|
*/
|
||||||
|
getFolderGradientStyle(folderPath: string): Record<string, string> | null {
|
||||||
|
const color = this.getFolderColor(folderPath);
|
||||||
|
if (!color || color === 'currentColor') return null;
|
||||||
|
// Use color with transparency; fall back directly if color is not hex
|
||||||
|
// For hex like #RRGGBB convert to rgba with ~12% -> 0% fade
|
||||||
|
const hexMatch = /^#([0-9a-fA-F]{6})$/.exec(color);
|
||||||
|
let gradientColor = color;
|
||||||
|
if (hexMatch) {
|
||||||
|
const hex = hexMatch[1];
|
||||||
|
const r = parseInt(hex.slice(0,2), 16);
|
||||||
|
const g = parseInt(hex.slice(2,4), 16);
|
||||||
|
const b = parseInt(hex.slice(4,6), 16);
|
||||||
|
gradientColor = `rgba(${r}, ${g}, ${b}, 0.14)`; // ~14% alpha
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
backgroundImage: `linear-gradient(to left, ${gradientColor} 0%, transparent 65%)`
|
||||||
|
} as Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// FOLDER COLOR MANAGEMENT
|
// FOLDER COLOR MANAGEMENT
|
||||||
// ========================================
|
// ========================================
|
||||||
@ -269,7 +385,7 @@ export class FileExplorerComponent {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'create-subfolder':
|
case 'create-subfolder':
|
||||||
this.createSubfolder();
|
this.openCreateModal(this.ctxTarget?.path || null);
|
||||||
break;
|
break;
|
||||||
case 'rename':
|
case 'rename':
|
||||||
this.openRenameModal();
|
this.openRenameModal();
|
||||||
@ -295,17 +411,58 @@ export class FileExplorerComponent {
|
|||||||
onContextMenuColor(color: string) {
|
onContextMenuColor(color: string) {
|
||||||
if (!this.ctxTarget) return;
|
if (!this.ctxTarget) return;
|
||||||
this.setFolderColor(this.ctxTarget.path, color);
|
this.setFolderColor(this.ctxTarget.path, color);
|
||||||
this.showNotification(`Folder color updated to ${color}`, 'success');
|
// Close context menu after color selection
|
||||||
|
this.ctxVisible.set(false);
|
||||||
|
this.showNotification(`Folder color updated`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action implementations
|
// Action implementations
|
||||||
private createSubfolder() {
|
private openCreateModal(parentPath: string | null) {
|
||||||
if (!this.ctxTarget) return;
|
this.createParentPath = parentPath ?? null;
|
||||||
const name = prompt('Enter subfolder name:');
|
this.createInputValue = '';
|
||||||
if (!name) return;
|
this.createError.set('');
|
||||||
const newPath = `${this.ctxTarget.path}/${name}`;
|
this.createModalVisible.set(true);
|
||||||
// TODO: Implement actual folder creation via VaultService
|
setTimeout(() => {
|
||||||
this.showNotification(`Creating subfolder: ${newPath}`, 'info');
|
try { (document.querySelector('#createInput') as HTMLInputElement)?.focus(); } catch {}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async confirmCreate() {
|
||||||
|
const name = (this.createInputValue || '').trim();
|
||||||
|
if (!name) { this.createError.set('Please enter a folder name'); return; }
|
||||||
|
|
||||||
|
const payload = this.createParentPath != null
|
||||||
|
? { parentPath: this.createParentPath, newFolderName: name }
|
||||||
|
: { path: name };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/folders', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Failed to create folder');
|
||||||
|
|
||||||
|
this.showNotification(`Folder "${name}" created successfully`, 'success');
|
||||||
|
this.cancelCreate();
|
||||||
|
this.ctxVisible.set(false);
|
||||||
|
this.vaultService.refreshFoldersTree(true);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Create folder error:', err);
|
||||||
|
this.createError.set(err?.message || 'Failed to create folder');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelCreate() {
|
||||||
|
this.createModalVisible.set(false);
|
||||||
|
this.createInputValue = '';
|
||||||
|
this.createParentPath = null;
|
||||||
|
this.createError.set('');
|
||||||
|
}
|
||||||
|
|
||||||
|
openCreateAtRoot() {
|
||||||
|
this.openCreateModal(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private openRenameModal() {
|
private openRenameModal() {
|
||||||
@ -438,6 +595,9 @@ export class FileExplorerComponent {
|
|||||||
this.removeFolderColorsRecursively(target.path);
|
this.removeFolderColorsRecursively(target.path);
|
||||||
this.persistFolderColors();
|
this.persistFolderColors();
|
||||||
|
|
||||||
|
// Reset focus in Notes-list when folder is deleted
|
||||||
|
this.notesListFocus.resetFocus();
|
||||||
|
|
||||||
this.showNotification(`Folder deleted: ${target.name}`, 'success');
|
this.showNotification(`Folder deleted: ${target.name}`, 'success');
|
||||||
this.ctxVisible.set(false);
|
this.ctxVisible.set(false);
|
||||||
|
|
||||||
@ -478,6 +638,28 @@ export class FileExplorerComponent {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loadFolderColors();
|
this.loadFolderColors();
|
||||||
|
|
||||||
|
// Close context menu when clicking outside
|
||||||
|
effect(() => {
|
||||||
|
if (this.ctxVisible()) {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
// Check if click is outside the context menu
|
||||||
|
if (!target.closest('app-context-menu') && !target.closest('[role="menu"]')) {
|
||||||
|
this.ctxVisible.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add listener on next tick to avoid immediate closure
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside, true);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleClickOutside, true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|||||||
@ -114,12 +114,12 @@ html:not(.dark)[data-theme="light"] {
|
|||||||
--border: #e5e7eb;
|
--border: #e5e7eb;
|
||||||
|
|
||||||
/* Brand colors */
|
/* Brand colors */
|
||||||
--primary: #3b82f6;
|
--primary: #666666;
|
||||||
--brand: #3a68d1;
|
--brand: #666666;
|
||||||
--brand-700: #2f56ab;
|
--brand-700: #555555;
|
||||||
--brand-800: #254487;
|
--brand-800: #444444;
|
||||||
--secondary: #8b5cf6;
|
--secondary: #888888;
|
||||||
--accent: #22c55e;
|
--accent: #999999;
|
||||||
|
|
||||||
/* Status colors */
|
/* Status colors */
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
@ -153,6 +153,84 @@ html:not(.dark)[data-theme="light"] {
|
|||||||
--md-table-row-alt: color-mix(in oklab, var(--surface-1) 96%, white 0%);
|
--md-table-row-alt: color-mix(in oklab, var(--surface-1) 96%, white 0%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
THÈME PURE WHITE - DARK (theme id is "light")
|
||||||
|
============================================================================ */
|
||||||
|
html.dark[data-theme="light"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
/* Fonts — Pure White */
|
||||||
|
--font-ui: Arial, "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
|
||||||
|
|
||||||
|
/* Backgrounds - Dark version for readability */
|
||||||
|
--bg: #0f0f0f;
|
||||||
|
--bg-main: #6d6c6c;
|
||||||
|
--bg-muted: #262626;
|
||||||
|
--card: #6d6c6c;
|
||||||
|
--card-bg: #6d6c6c;
|
||||||
|
--elevated: #262626;
|
||||||
|
--sidebar-bg: #0f0f0f;
|
||||||
|
--surface-1: #0f0f0f;
|
||||||
|
--surface-2: #6d6c6c;
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--fg: #cccccc;
|
||||||
|
--text-main: #cccccc;
|
||||||
|
--text-muted: #aaaaaa;
|
||||||
|
--muted: #b3b1b1;
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--border: #333333;
|
||||||
|
|
||||||
|
/* Brand colors - Changed to grays */
|
||||||
|
--primary: #afafaf;
|
||||||
|
--brand: #888888;
|
||||||
|
--brand-700: #666666;
|
||||||
|
--brand-800: #555555;
|
||||||
|
--secondary: #777777;
|
||||||
|
--accent: #555555;
|
||||||
|
|
||||||
|
/* Status colors */
|
||||||
|
--success: #4ade80;
|
||||||
|
--warning: #fbbf24;
|
||||||
|
--danger: #f87171;
|
||||||
|
--info: #60a5fa;
|
||||||
|
|
||||||
|
/* UI elements */
|
||||||
|
--chip-bg: #333333;
|
||||||
|
--link: #cccccc;
|
||||||
|
--link-hover: #ffffff;
|
||||||
|
--ring: #888888;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.5);
|
||||||
|
--scrollbar-thumb: rgba(136, 136, 136, 0.6);
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
--editor-bg: #1a1a1a;
|
||||||
|
--editor-fg: #ffffff;
|
||||||
|
--editor-selection: rgba(255, 255, 255, 0.2);
|
||||||
|
--editor-gutter-bg: #0f0f0f;
|
||||||
|
--editor-gutter-fg: #cccccc;
|
||||||
|
--editor-cursor: #ffffff;
|
||||||
|
|
||||||
|
/* Markdown overrides */
|
||||||
|
--md-h1: #ffffff;
|
||||||
|
--md-h2: #cccccc;
|
||||||
|
--md-h3: #aaaaaa;
|
||||||
|
--md-quote-bar: #cccccc;
|
||||||
|
--md-quote-bg: color-mix(in oklab, #1a1a1a 92%, black 0%);
|
||||||
|
--md-table-head-bg: color-mix(in oklab, #262626 90%, black 0%);
|
||||||
|
--md-table-row-alt: color-mix(in oklab, #0f0f0f 85%, black 0%);
|
||||||
|
--md-pre-bg: #0f0f0f;
|
||||||
|
--md-pre-border: #333333;
|
||||||
|
--md-syntax-1: #cccccc;
|
||||||
|
--md-syntax-2: #aaaaaa;
|
||||||
|
--md-syntax-3: #888888;
|
||||||
|
--md-syntax-4: #666666;
|
||||||
|
--md-syntax-5: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
THÈME DARK (baseline dark)
|
THÈME DARK (baseline dark)
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
@ -238,14 +316,14 @@ html:not(.dark)[data-theme="blue"] {
|
|||||||
|
|
||||||
/* Backgrounds */
|
/* Backgrounds */
|
||||||
--bg: #ffffff;
|
--bg: #ffffff;
|
||||||
--bg-main: #f7faff;
|
--bg-main: #fbfdff; /* overall app background (lightest after card) */
|
||||||
--bg-muted: #eef2ff;
|
--bg-muted: #f2f6ff; /* subtle blue tint for muted areas */
|
||||||
--card: #ffffff;
|
--card: #ffffff; /* view note / cards: lightest */
|
||||||
--card-bg: #ffffff;
|
--card-bg: #ffffff;
|
||||||
--elevated: #ffffff;
|
--elevated: #ffffff;
|
||||||
--sidebar-bg: #f1f5ff;
|
--sidebar-bg: #eef4ff; /* sidebar: slightly darker */
|
||||||
--surface-1: #f1f5ff;
|
--surface-1: #f3f7ff; /* notes list container: intermediate */
|
||||||
--surface-2: #e6edff;
|
--surface-2: #e9f1ff; /* secondary surfaces */
|
||||||
|
|
||||||
/* Text */
|
/* Text */
|
||||||
--fg: #0f172a;
|
--fg: #0f172a;
|
||||||
@ -254,15 +332,15 @@ html:not(.dark)[data-theme="blue"] {
|
|||||||
--muted: #64748b;
|
--muted: #64748b;
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--border: #dbeafe;
|
--border: #e3eeff;
|
||||||
|
|
||||||
/* Brand colors */
|
/* Brand colors */
|
||||||
--primary: #2563eb;
|
--primary: #6BA7F7; /* softer, brighter blue */
|
||||||
--brand: #2563eb;
|
--brand: #6BA7F7;
|
||||||
--brand-700: #1d4ed8;
|
--brand-700: #4A90E2; /* hover/focus */
|
||||||
--brand-800: #1e40af;
|
--brand-800: #2F6BD6; /* active */
|
||||||
--secondary: #7c3aed;
|
--secondary: #8FB7FF; /* badges/labels */
|
||||||
--accent: #06b6d4;
|
--accent: #5FC8E8; /* info accents */
|
||||||
|
|
||||||
/* Status */
|
/* Status */
|
||||||
--success: #16a34a;
|
--success: #16a34a;
|
||||||
@ -271,40 +349,40 @@ html:not(.dark)[data-theme="blue"] {
|
|||||||
--info: #0ea5e9;
|
--info: #0ea5e9;
|
||||||
|
|
||||||
/* UI */
|
/* UI */
|
||||||
--chip-bg: #e2e8f0;
|
--chip-bg: #eaf2ff;
|
||||||
--link: #1d4ed8;
|
--link: #4A90E2;
|
||||||
--link-hover: #1e40af;
|
--link-hover: #2F6BD6;
|
||||||
--ring: #2563eb;
|
--ring: #6BA7F7;
|
||||||
|
|
||||||
/* Shadows */
|
/* Shadows */
|
||||||
--shadow-color: rgba(15, 23, 42, 0.08);
|
--shadow-color: rgba(15, 23, 42, 0.08);
|
||||||
--scrollbar-thumb: rgba(59, 130, 246, 0.35);
|
--scrollbar-thumb: rgba(125, 155, 195, 0.35); /* light gray-blue */
|
||||||
|
|
||||||
/* Editor */
|
/* Editor */
|
||||||
--editor-bg: #ffffff;
|
--editor-bg: #ffffff;
|
||||||
--editor-fg: #0f172a;
|
--editor-fg: #0f172a;
|
||||||
--editor-selection: rgba(37, 99, 235, 0.2);
|
--editor-selection: color-mix(in srgb, var(--primary) 22%, transparent);
|
||||||
--editor-gutter-bg: #f1f5ff;
|
--editor-gutter-bg: #f1f5ff;
|
||||||
--editor-gutter-fg: #64748b;
|
--editor-gutter-fg: #64748b;
|
||||||
--editor-cursor: #0f172a;
|
--editor-cursor: #0f172a;
|
||||||
|
|
||||||
/* Markdown overrides */
|
/* Markdown overrides */
|
||||||
--md-h1: #0f172a;
|
--md-h1: #0f172a;
|
||||||
--md-h2: #1d4ed8;
|
--md-h2: #4A90E2;
|
||||||
--md-h3: #2563eb;
|
--md-h3: #6BA7F7;
|
||||||
--md-h4: #7c3aed;
|
--md-h4: #5FC8E8;
|
||||||
--md-h5: #475569;
|
--md-h5: #475569;
|
||||||
--md-h6: #64748b;
|
--md-h6: #64748b;
|
||||||
--md-quote-bar: #2563eb;
|
--md-quote-bar: #4A90E2;
|
||||||
--md-quote-bg: color-mix(in oklab, #e6edff 92%, white 0%);
|
--md-quote-bg: color-mix(in oklab, #e9f1ff 92%, white 0%);
|
||||||
--md-table-head-bg: color-mix(in oklab, #e6edff 90%, white 0%);
|
--md-table-head-bg: color-mix(in oklab, #e9f1ff 90%, white 0%);
|
||||||
--md-table-row-alt: color-mix(in oklab, #f1f5ff 96%, white 0%);
|
--md-table-row-alt: color-mix(in oklab, #f3f7ff 96%, white 0%);
|
||||||
--md-pre-bg: #f1f5ff;
|
--md-pre-bg: #f1f5ff;
|
||||||
--md-pre-border: #dbeafe;
|
--md-pre-border: #dbeafe;
|
||||||
--md-syntax-1: #ef4444;
|
--md-syntax-1: #ef4444;
|
||||||
--md-syntax-2: #7c3aed;
|
--md-syntax-2: #4A90E2;
|
||||||
--md-syntax-3: #16a34a;
|
--md-syntax-3: #16a34a;
|
||||||
--md-syntax-4: #2563eb;
|
--md-syntax-4: #6BA7F7;
|
||||||
--md-syntax-5: #0f172a;
|
--md-syntax-5: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
vault/Allo-3/test/Nouvelle note 3.md
Normal file
17
vault/Allo-3/test/Nouvelle note 3.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
titre: Nouvelle note 3
|
||||||
|
auteur: Bruno Charest
|
||||||
|
creation_date: 2025-10-24T15:44:07.120Z
|
||||||
|
modification_date: 2025-10-24T11:44:07-04:00
|
||||||
|
catégorie: ""
|
||||||
|
tags: []
|
||||||
|
aliases: []
|
||||||
|
status: en-cours
|
||||||
|
publish: false
|
||||||
|
favoris: false
|
||||||
|
template: false
|
||||||
|
task: false
|
||||||
|
archive: false
|
||||||
|
draft: false
|
||||||
|
private: false
|
||||||
|
---
|
||||||
15
vault/Allo-3/test/Nouvelle note 3.md.bak
Normal file
15
vault/Allo-3/test/Nouvelle note 3.md.bak
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
titre: "Nouvelle note 3"
|
||||||
|
auteur: "Bruno Charest"
|
||||||
|
creation_date: "2025-10-24T15:44:07.120Z"
|
||||||
|
modification_date: "2025-10-24T15:44:07.120Z"
|
||||||
|
status: "en-cours"
|
||||||
|
publish: false
|
||||||
|
favoris: false
|
||||||
|
template: false
|
||||||
|
task: false
|
||||||
|
archive: false
|
||||||
|
draft: false
|
||||||
|
private: false
|
||||||
|
---
|
||||||
|
|
||||||
17
vault/toto/Nouvelle note 2.md
Normal file
17
vault/toto/Nouvelle note 2.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
titre: Nouvelle note 2
|
||||||
|
auteur: Bruno Charest
|
||||||
|
creation_date: 2025-10-24T12:24:03.706Z
|
||||||
|
modification_date: 2025-10-24T08:24:04-04:00
|
||||||
|
catégorie: ""
|
||||||
|
tags: []
|
||||||
|
aliases: []
|
||||||
|
status: en-cours
|
||||||
|
publish: false
|
||||||
|
favoris: false
|
||||||
|
template: false
|
||||||
|
task: false
|
||||||
|
archive: false
|
||||||
|
draft: false
|
||||||
|
private: false
|
||||||
|
---
|
||||||
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
titre: "Nouvelle note 2"
|
titre: "Nouvelle note 2"
|
||||||
auteur: "Bruno Charest"
|
auteur: "Bruno Charest"
|
||||||
creation_date: "2025-10-24T11:57:19.077Z"
|
creation_date: "2025-10-24T12:24:03.706Z"
|
||||||
modification_date: "2025-10-24T11:57:19.077Z"
|
modification_date: "2025-10-24T12:24:03.706Z"
|
||||||
status: "en-cours"
|
status: "en-cours"
|
||||||
publish: false
|
publish: false
|
||||||
favoris: false
|
favoris: false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user