feat: enhance note creation and navigation UX

- Added note creation event handlers to sync selection, scrolling and focus across components
- Improved context menu styling with better opacity and z-index handling
- Updated mobile layout to hide search input on larger screens
- Fixed focus timing issues in move-note-to-folder component
- Added URL state management for newly created notes
- Removed unused archived note files
- Added proper note ID tracking and scroll-into-view behavior
- Enhanced error
This commit is contained in:
2025-10-26 11:54:47 -04:00
parent 0ae9cae1eb
commit 917af04642
40 changed files with 658 additions and 15 deletions
+68 -3
View File
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Output, computed, signal, effect, inject, ChangeDetectionStrategy } from '@angular/core';
import { Component, EventEmitter, Output, computed, signal, effect, inject, ChangeDetectionStrategy, ViewChild, ElementRef } from '@angular/core';
import { input } from '@angular/core';
import { CommonModule } from '@angular/common';
import type { Note } from '../../../types';
@@ -9,6 +9,7 @@ import { NoteCreationService } from '../../services/note-creation.service';
import { NoteContextMenuComponent } from '../../../components/note-context-menu/note-context-menu.component';
import { WarningPanelComponent } from '../../components/warning-panel/warning-panel.component';
import { NoteContextMenuService } from '../../services/note-context-menu.service';
import { UrlStateService } from '../../services/url-state.service';
@Component({
selector: 'app-notes-list',
@@ -117,7 +118,7 @@ import { NoteContextMenuService } from '../../services/note-context-menu.service
</div>
<!-- List Container -->
<div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay>
<div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay #listContainer>
<!-- Empty state when a folder is selected but no notes match -->
<div *ngIf="(folderFilter() && filtered().length === 0)" class="h-full min-h-[240px] flex items-center justify-center text-center p-6">
<div>
@@ -133,10 +134,12 @@ import { NoteContextMenuService } from '../../services/note-context-menu.service
<ul class="notes-list">
<li *ngFor="let n of filtered()"
class="note-row cursor-pointer"
[class.active]="selectedId() === n.id"
[class.active]="(selectedId() === n.id) || (pendingSelectId() === n.id)"
[ngClass]="getListItemClasses()"
[ngStyle]="getNoteGradientStyle(n)"
[attr.data-note-path]="n.filePath"
[attr.data-note-id]="n.id"
tabindex="-1"
(click)="openNote.emit(n.id)"
(contextmenu)="openContextMenu($event, n)">
<!-- Compact View -->
@@ -373,11 +376,15 @@ export class NotesListComponent {
@Output() queryChange = new EventEmitter<string>();
@Output() clearQuickLinkFilter = new EventEmitter<void>();
@Output() noteCreated = new EventEmitter<string>();
@Output() noteCreatedAndSelected = new EventEmitter<{ id: string; filePath: string }>();
private store = inject(TagFilterStore);
readonly state = inject(NotesListStateService);
private noteCreationService = inject(NoteCreationService);
readonly contextMenuService = inject(NoteContextMenuService);
@ViewChild('listContainer') listContainer?: ElementRef<HTMLElement>;
private urlState = inject(UrlStateService);
private pendingSelectId = signal<string | null>(null);
// Delete warning modal state
deleteWarningOpen = signal<boolean>(false);
@@ -435,6 +442,45 @@ export class NotesListComponent {
}, 10);
});
private scrollToSelectedEffect = effect(() => {
const id = this.selectedId() || this.pendingSelectId();
if (!id) return;
const host = this.listContainer?.nativeElement;
if (!host) return;
const tryFocus = () => {
const el = host.querySelector(`li[data-note-id="${id}"]`) as HTMLElement | null;
if (el) {
try { el.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch {}
try { el.focus({ preventScroll: true } as any); } catch {}
return true;
}
return false;
};
// Attempt after microtask, then a few RAF retries to wait for DOM
queueMicrotask(() => {
if (tryFocus()) {
// If parent hasn't yet reflected selection, keep local pending highlight
return;
}
let attempts = 4;
const raf = () => {
if (tryFocus()) return;
if (--attempts > 0) requestAnimationFrame(raf);
};
requestAnimationFrame(raf);
});
});
private clearPendingWhenSynced = effect(() => {
const sel = this.selectedId();
const pending = this.pendingSelectId();
if (sel && pending && sel === pending) {
this.pendingSelectId.set(null);
}
});
private syncTagFromStore = effect(() => {
const inputTag = this.tagFilter();
if (inputTag !== null && inputTag !== undefined) {
@@ -603,8 +649,27 @@ export class NotesListComponent {
this.noteCreationService.createNote(fileName, folderPath)
.then(response => {
if (!response?.filePath || !response?.id) {
console.error('Invalid response from note creation:', response);
this.state.setRequestStats(false, 0);
return;
}
// 1) Set URL immediately with note + folder
this.urlState.setNote(response.filePath, folderPath).catch(err => {
console.error('Failed to update URL:', err);
});
// 2) Local optimistic selection
this.pendingSelectId.set(response.id);
// 3) Emit events for parent to handle scroll + focus
this.noteCreated.emit(response.id);
this.openNote.emit(response.id);
this.noteCreatedAndSelected.emit({
id: response.id,
filePath: response.filePath
});
})
.catch(error => {
console.error('Failed to create note:', error);
@@ -132,7 +132,7 @@ export class MoveNoteToFolderComponent {
queueMicrotask(() => {
this.computeMenuPosition();
this.focusSearchField();
setTimeout(() => this.focusSearchField(), 0);
});
}
@@ -308,6 +308,9 @@ export class MoveNoteToFolderComponent {
}
private focusSearchField(): void {
if (!this.showMenu()) {
return;
}
const el = this.searchField?.nativeElement;
if (el) {
el.focus();