From 83603e2d974da4cc7106a4565fd43b3f38427d14 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 24 Oct 2025 13:08:13 -0400 Subject: [PATCH] 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 --- server/index-phase3-patch.mjs | 55 +++++ server/index.mjs | 6 +- src/app/features/list/notes-list.component.ts | 174 +++++++++++++-- .../list/paginated-notes-list.component.ts | 101 +++++++-- .../features/parameters/parameters.page.css | 154 +++++++++++++ .../features/parameters/parameters.page.html | 73 +++++++ .../features/parameters/parameters.page.ts | 53 ++++- .../sidebar/nimbus-sidebar.component.ts | 58 +++-- .../app-shell-nimbus.component.ts | 5 +- src/app/services/folder-filter.service.ts | 145 ++++++++++++ src/app/services/notes-list-focus.service.ts | 66 ++++++ src/app/shared/ui/badge-count.component.ts | 29 ++- .../context-menu/context-menu.component.ts | 10 +- .../file-explorer/file-explorer.component.ts | 206 +++++++++++++++++- src/styles/themes.css | 146 ++++++++++--- vault/Allo-3/test/Nouvelle note 3.md | 17 ++ vault/Allo-3/test/Nouvelle note 3.md.bak | 15 ++ vault/toto/Nouvelle note 2.md | 17 ++ vault/toto/Nouvelle note 2.md.bak | 4 +- 19 files changed, 1230 insertions(+), 104 deletions(-) create mode 100644 src/app/services/folder-filter.service.ts create mode 100644 src/app/services/notes-list-focus.service.ts create mode 100644 vault/Allo-3/test/Nouvelle note 3.md create mode 100644 vault/Allo-3/test/Nouvelle note 3.md.bak create mode 100644 vault/toto/Nouvelle note 2.md diff --git a/server/index-phase3-patch.mjs b/server/index-phase3-patch.mjs index 62851f2..90d183e 100644 --- a/server/index-phase3-patch.mjs +++ b/server/index-phase3-patch.mjs @@ -27,6 +27,8 @@ export function setupRenameFolderEndpoint(app, vaultDir, broadcastVaultEvent, me if (!oldPath || typeof oldPath !== 'string') { return res.status(400).json({ error: 'Missing or invalid oldPath' }); } + + if (!newName || typeof newName !== 'string') { 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 // ============================================================================ diff --git a/server/index.mjs b/server/index.mjs index 9d78ddb..d7f5013 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -32,7 +32,8 @@ import { setupDeferredIndexing, setupCreateNoteEndpoint, setupRenameFolderEndpoint, - setupDeleteFolderEndpoint + setupDeleteFolderEndpoint, + setupCreateFolderEndpoint } from './index-phase3-patch.mjs'; const __filename = fileURLToPath(import.meta.url); @@ -1532,6 +1533,9 @@ setupRenameFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache); // Setup delete folder endpoint (must be before catch-all) setupDeleteFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache); +// Setup create folder endpoint (must be before catch-all) +setupCreateFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache); + app.get('/', sendIndex); app.use((req, res) => { if (req.path.startsWith('/api/')) { diff --git a/src/app/features/list/notes-list.component.ts b/src/app/features/list/notes-list.component.ts index 6564c94..cb5ef0a 100644 --- a/src/app/features/list/notes-list.component.ts +++ b/src/app/features/list/notes-list.component.ts @@ -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" /> + +
+ + +
+ +

Folders listed here will be hidden from the sidebar. Paths are matched recursively.

+ + + +
+ +

Restore default folder filtering settings

+
+ + +

diff --git a/src/app/features/parameters/parameters.page.ts b/src/app/features/parameters/parameters.page.ts index 5c2134e..30a758b 100644 --- a/src/app/features/parameters/parameters.page.ts +++ b/src/app/features/parameters/parameters.page.ts @@ -1,21 +1,26 @@ import { Component, inject, signal, effect } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { ThemeService, ThemeMode, ThemeId, Language } from '../../core/services/theme.service'; import { ToastService } from '../../shared/toast/toast.service'; +import { FolderFilterService, FolderFilterConfig } from '../../services/folder-filter.service'; @Component({ standalone: true, selector: 'app-parameters', - imports: [CommonModule], + imports: [CommonModule, FormsModule], templateUrl: './parameters.page.html', styleUrls: ['./parameters.page.css'] }) export class ParametersPage { private themeService = inject(ThemeService); private toastService = inject(ToastService); + private folderFilterService = inject(FolderFilterService); // Reactive prefs prefs = signal(this.themeService.prefsValue); + folderFilterConfig = signal(this.folderFilterService.getConfig()); + newExcludedFolder = ''; modes: ThemeMode[] = ['system', 'light', 'dark']; themes: ThemeId[] = ['light', 'dark', 'obsidian', 'nord', 'notion', 'github', 'discord', 'monokai']; @@ -77,4 +82,50 @@ export class ParametersPage { }; 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'); + } } diff --git a/src/app/features/sidebar/nimbus-sidebar.component.ts b/src/app/features/sidebar/nimbus-sidebar.component.ts index e196217..b269540 100644 --- a/src/app/features/sidebar/nimbus-sidebar.component.ts +++ b/src/app/features/sidebar/nimbus-sidebar.component.ts @@ -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 { RouterModule } from '@angular/router'; import { FileExplorerComponent } from '../../../components/file-explorer/file-explorer.component'; @@ -49,10 +49,13 @@ import { VaultService } from '../../../services/vault.service';
-
@@ -61,13 +64,21 @@ import { VaultService } from '../../../services/vault.service';
- +
+ + +
-
    @@ -100,10 +114,13 @@ import { VaultService } from '../../../services/vault.service';
    -
    @@ -166,6 +183,7 @@ export class NimbusSidebarComponent { env = environment; open = { quick: true, folders: false, tags: false, trash: false, tests: false }; private vault = inject(VaultService); + @ViewChild('foldersExplorer') private foldersExplorer?: FileExplorerComponent; 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 { const next = !this.open.trash; this.open.trash = next; diff --git a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts index c30ad39..6f7a68a 100644 --- a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts +++ b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts @@ -179,6 +179,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component' [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" + [selectedId]="selectedNoteId" (openNote)="onOpenNote($event)" (queryChange)="onQueryChange($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()" @@ -257,7 +258,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
    - +
    @@ -288,7 +289,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component' @if (mobileNav.activeTab() === 'list') {
    - +
    } diff --git a/src/app/services/folder-filter.service.ts b/src/app/services/folder-filter.service.ts new file mode 100644 index 0000000..2cdc8c2 --- /dev/null +++ b/src/app/services/folder-filter.service.ts @@ -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(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): 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() }; + } +} diff --git a/src/app/services/notes-list-focus.service.ts b/src/app/services/notes-list-focus.service.ts new file mode 100644 index 0000000..36c7e2d --- /dev/null +++ b/src/app/services/notes-list-focus.service.ts @@ -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(null); + + // Signal to trigger focus reset + private resetFocusTrigger = signal(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); + } +} diff --git a/src/app/shared/ui/badge-count.component.ts b/src/app/shared/ui/badge-count.component.ts index f7b5dd4..c31228d 100644 --- a/src/app/shared/ui/badge-count.component.ts +++ b/src/app/shared/ui/badge-count.component.ts @@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common'; {{ count }} @@ -17,7 +18,7 @@ import { CommonModule } from '@angular/common'; }) export class BadgeCountComponent { @Input() count = 0; - @Input() color: 'slate'|'rose'|'amber'|'indigo'|'emerald'|'stone'|'zinc'|'green'|'purple' = 'slate'; + @Input() color: string = 'slate'; get bgClass() { return { @@ -32,4 +33,30 @@ export class BadgeCountComponent { '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; + } + + 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; + } + + get mergedStyle() { + return { ...this.bgStyle, ...this.fgStyle } as Record; + } } diff --git a/src/components/context-menu/context-menu.component.ts b/src/components/context-menu/context-menu.component.ts index 5c29399..7419530 100644 --- a/src/components/context-menu/context-menu.component.ts +++ b/src/components/context-menu/context-menu.component.ts @@ -29,7 +29,7 @@ type CtxAction = imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, styles: [` - :host { position: fixed; inset: 0; pointer-events: none; } + :host { position: fixed; inset: 0; pointer-events: none; z-index: 9999; } .ctx { pointer-events: auto; min-width: 14rem; @@ -44,6 +44,7 @@ type CtxAction = background: var(--card, #ffffff); border: 1px solid var(--border, #e5e7eb); color: var(--fg, #111827); + z-index: 10000; } .item { display: block; width: 100%; @@ -94,7 +95,7 @@ type CtxAction = template: ` - +
    this.reposition()); } if ((changes['x'] || changes['y']) && this.visible) { diff --git a/src/components/file-explorer/file-explorer.component.ts b/src/components/file-explorer/file-explorer.component.ts index b50e941..05e7985 100644 --- a/src/components/file-explorer/file-explorer.component.ts +++ b/src/components/file-explorer/file-explorer.component.ts @@ -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 { FormsModule } from '@angular/forms'; import { VaultNode, VaultFile, VaultFolder } from '../../types'; import { VaultService } from '../../services/vault.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 { ContextMenuComponent } from '../context-menu/context-menu.component'; @@ -11,7 +13,7 @@ import { ContextMenuComponent } from '../context-menu/context-menu.component'; selector: 'app-file-explorer', template: `
      - @for(node of nodes(); track node.path) { + @for(node of filteredNodes(); track node.path) {
    • @if (isFolder(node) && node.name !== '.trash') { @let folder = node; @@ -20,13 +22,14 @@ import { ContextMenuComponent } from '../context-menu/context-menu.component'; (click)="onFolderClick(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" + [ngStyle]="getFolderGradientStyle(folder.path)" > {{ folder.name }} - +
    @if (folder.isOpen) {
    @@ -107,6 +110,43 @@ import { ContextMenuComponent } from '../context-menu/context-menu.component';
+ + +
+
+

Create a new folder

+ +
+ + +
+
{{ createError() }}
+ +
+ + +
+
+
`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, BadgeCountComponent, ContextMenuComponent], @@ -131,11 +171,51 @@ export class FileExplorerComponent { renameInputValue = ''; renameTarget: VaultFolder | null = null; + // Create subfolder modal state + createModalVisible = signal(false); + createInputValue = ''; + createParentPath: string | null = null; + createError = signal(''); + // Folder colors storage private folderColors = new Map(); private vaultService = inject(VaultService); 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 { 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 | 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; + } + // ======================================== // FOLDER COLOR MANAGEMENT // ======================================== @@ -269,7 +385,7 @@ export class FileExplorerComponent { switch (action) { case 'create-subfolder': - this.createSubfolder(); + this.openCreateModal(this.ctxTarget?.path || null); break; case 'rename': this.openRenameModal(); @@ -295,17 +411,58 @@ export class FileExplorerComponent { onContextMenuColor(color: string) { if (!this.ctxTarget) return; 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 - private createSubfolder() { - if (!this.ctxTarget) return; - const name = prompt('Enter subfolder name:'); - if (!name) return; - const newPath = `${this.ctxTarget.path}/${name}`; - // TODO: Implement actual folder creation via VaultService - this.showNotification(`Creating subfolder: ${newPath}`, 'info'); + private openCreateModal(parentPath: string | null) { + this.createParentPath = parentPath ?? null; + this.createInputValue = ''; + this.createError.set(''); + this.createModalVisible.set(true); + setTimeout(() => { + 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() { @@ -438,6 +595,9 @@ export class FileExplorerComponent { this.removeFolderColorsRecursively(target.path); this.persistFolderColors(); + // Reset focus in Notes-list when folder is deleted + this.notesListFocus.resetFocus(); + this.showNotification(`Folder deleted: ${target.name}`, 'success'); this.ctxVisible.set(false); @@ -478,6 +638,28 @@ export class FileExplorerComponent { constructor() { 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() { diff --git a/src/styles/themes.css b/src/styles/themes.css index a0223b9..39be65d 100644 --- a/src/styles/themes.css +++ b/src/styles/themes.css @@ -114,12 +114,12 @@ html:not(.dark)[data-theme="light"] { --border: #e5e7eb; /* Brand colors */ - --primary: #3b82f6; - --brand: #3a68d1; - --brand-700: #2f56ab; - --brand-800: #254487; - --secondary: #8b5cf6; - --accent: #22c55e; + --primary: #666666; + --brand: #666666; + --brand-700: #555555; + --brand-800: #444444; + --secondary: #888888; + --accent: #999999; /* Status colors */ --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%); } +/* ============================================================================ + 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) ============================================================================ */ @@ -238,14 +316,14 @@ html:not(.dark)[data-theme="blue"] { /* Backgrounds */ --bg: #ffffff; - --bg-main: #f7faff; - --bg-muted: #eef2ff; - --card: #ffffff; + --bg-main: #fbfdff; /* overall app background (lightest after card) */ + --bg-muted: #f2f6ff; /* subtle blue tint for muted areas */ + --card: #ffffff; /* view note / cards: lightest */ --card-bg: #ffffff; --elevated: #ffffff; - --sidebar-bg: #f1f5ff; - --surface-1: #f1f5ff; - --surface-2: #e6edff; + --sidebar-bg: #eef4ff; /* sidebar: slightly darker */ + --surface-1: #f3f7ff; /* notes list container: intermediate */ + --surface-2: #e9f1ff; /* secondary surfaces */ /* Text */ --fg: #0f172a; @@ -254,15 +332,15 @@ html:not(.dark)[data-theme="blue"] { --muted: #64748b; /* Borders */ - --border: #dbeafe; + --border: #e3eeff; /* Brand colors */ - --primary: #2563eb; - --brand: #2563eb; - --brand-700: #1d4ed8; - --brand-800: #1e40af; - --secondary: #7c3aed; - --accent: #06b6d4; + --primary: #6BA7F7; /* softer, brighter blue */ + --brand: #6BA7F7; + --brand-700: #4A90E2; /* hover/focus */ + --brand-800: #2F6BD6; /* active */ + --secondary: #8FB7FF; /* badges/labels */ + --accent: #5FC8E8; /* info accents */ /* Status */ --success: #16a34a; @@ -271,40 +349,40 @@ html:not(.dark)[data-theme="blue"] { --info: #0ea5e9; /* UI */ - --chip-bg: #e2e8f0; - --link: #1d4ed8; - --link-hover: #1e40af; - --ring: #2563eb; + --chip-bg: #eaf2ff; + --link: #4A90E2; + --link-hover: #2F6BD6; + --ring: #6BA7F7; /* Shadows */ --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-bg: #ffffff; --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-fg: #64748b; --editor-cursor: #0f172a; /* Markdown overrides */ --md-h1: #0f172a; - --md-h2: #1d4ed8; - --md-h3: #2563eb; - --md-h4: #7c3aed; + --md-h2: #4A90E2; + --md-h3: #6BA7F7; + --md-h4: #5FC8E8; --md-h5: #475569; --md-h6: #64748b; - --md-quote-bar: #2563eb; - --md-quote-bg: color-mix(in oklab, #e6edff 92%, white 0%); - --md-table-head-bg: color-mix(in oklab, #e6edff 90%, white 0%); - --md-table-row-alt: color-mix(in oklab, #f1f5ff 96%, white 0%); + --md-quote-bar: #4A90E2; + --md-quote-bg: color-mix(in oklab, #e9f1ff 92%, white 0%); + --md-table-head-bg: color-mix(in oklab, #e9f1ff 90%, white 0%); + --md-table-row-alt: color-mix(in oklab, #f3f7ff 96%, white 0%); --md-pre-bg: #f1f5ff; --md-pre-border: #dbeafe; --md-syntax-1: #ef4444; - --md-syntax-2: #7c3aed; + --md-syntax-2: #4A90E2; --md-syntax-3: #16a34a; - --md-syntax-4: #2563eb; + --md-syntax-4: #6BA7F7; --md-syntax-5: #0f172a; } diff --git a/vault/Allo-3/test/Nouvelle note 3.md b/vault/Allo-3/test/Nouvelle note 3.md new file mode 100644 index 0000000..a0542bf --- /dev/null +++ b/vault/Allo-3/test/Nouvelle note 3.md @@ -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 +--- diff --git a/vault/Allo-3/test/Nouvelle note 3.md.bak b/vault/Allo-3/test/Nouvelle note 3.md.bak new file mode 100644 index 0000000..97579c1 --- /dev/null +++ b/vault/Allo-3/test/Nouvelle note 3.md.bak @@ -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 +--- + diff --git a/vault/toto/Nouvelle note 2.md b/vault/toto/Nouvelle note 2.md new file mode 100644 index 0000000..764a642 --- /dev/null +++ b/vault/toto/Nouvelle note 2.md @@ -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 +--- diff --git a/vault/toto/Nouvelle note 2.md.bak b/vault/toto/Nouvelle note 2.md.bak index 0f93c80..e3a723a 100644 --- a/vault/toto/Nouvelle note 2.md.bak +++ b/vault/toto/Nouvelle note 2.md.bak @@ -1,8 +1,8 @@ --- titre: "Nouvelle note 2" auteur: "Bruno Charest" -creation_date: "2025-10-24T11:57:19.077Z" -modification_date: "2025-10-24T11:57:19.077Z" +creation_date: "2025-10-24T12:24:03.706Z" +modification_date: "2025-10-24T12:24:03.706Z" status: "en-cours" publish: false favoris: false