diff --git a/src/renderer/src/ListWindowSpacer.tsx b/src/renderer/src/ListWindowSpacer.tsx new file mode 100644 index 00000000..928ca938 --- /dev/null +++ b/src/renderer/src/ListWindowSpacer.tsx @@ -0,0 +1,16 @@ +/** + * Stand-ins for rows a windowed list did not mount (see use-list-window.ts). + * The height is computed geometry, so it is the one inline style these lists + * carry; everything else stays in classes. + */ +export function ListWindowTableSpacer({ height, columns, className }: { + height: number + columns: number + className?: string +}): React.JSX.Element { + return ( + + + + ) +} diff --git a/src/renderer/src/MagicNotesWorkspace.tsx b/src/renderer/src/MagicNotesWorkspace.tsx index 88f90d0c..9804cfc6 100644 --- a/src/renderer/src/MagicNotesWorkspace.tsx +++ b/src/renderer/src/MagicNotesWorkspace.tsx @@ -71,6 +71,7 @@ import type { MagicCanvasContentHandle } from './MagicCanvasContent' import { MagicCanvasThumbnail } from './MagicCanvasThumbnail' import { MarkdownRenderer } from './MarkdownRenderer' import { activateModalFocus, trapTabFocus } from './dialog-focus' +import { useGridListWindow } from './use-grid-columns' import type { AppNotificationInput } from './notifications' import { EmptyState, @@ -460,6 +461,10 @@ export function MagicNotesWorkspace({ const [libraryView, setLibraryView] = useState('notes') const [detailView, setDetailView] = useState<'notes'>() const overviewFocusRef = useRef('') + // The note whose card takes focus back on returning to the overview; the + // windowed note list keeps that card mounted. + const [overviewReturnNoteId, setOverviewReturnNoteId] = useState('') + const noteGridRef = useRef(null) const [todoFilter, setTodoFilter] = useState('active') const [loadedCommentMode, setCommentMode] = useState('immediate') @@ -1202,12 +1207,16 @@ export function MagicNotesWorkspace({ } if (target.kind === 'create-note') { overviewFocusRef.current = 'magic-note-new' + setOverviewReturnNoteId('') void createNote(target.title, true) .then(() => focusSwitchTarget(target)) return } setDeletingNote(false) - if (libraryView === 'notes') overviewFocusRef.current = `magic-note-select-${target.noteId}` + if (libraryView === 'notes') { + overviewFocusRef.current = `magic-note-select-${target.noteId}` + setOverviewReturnNoteId(target.noteId) + } setDetailView('notes') focusSwitchTarget(target) void loadDetail(target.noteId, target.entryId) @@ -1753,6 +1762,24 @@ export function MagicNotesWorkspace({ // Overview shows every note full width; the split list appears only beside an open note. const notesExpanded = isNarrowWorkspace ? !detailView : notesPaneOpen || !detailView const notesSplit = libraryView === 'notes' && Boolean(detailView) && !isNarrowWorkspace && notesPaneOpen + const visibleNoteIdsKey = visibleNotes.map((note) => note.id).join('\n') + const visibleNoteIds = useMemo( + () => (visibleNoteIdsKey ? visibleNoteIdsKey.split('\n') : []), + [visibleNoteIdsKey] + ) + // P5: thousands of notes mount only the card rows near the viewport. + const noteWindow = useGridListWindow({ + itemIds: visibleNoteIds, + gridRef: noteGridRef, + scope: `${search}\n${activeTagFilter.join('\n')}`, + // Selection, the open menu and the overview return target stay mounted. + keepIds: [selectedNoteId, noteActionsId, overviewReturnNoteId], + enabled: libraryView === 'notes' && loadStatus !== 'loading', + active: libraryView === 'notes' && notesExpanded, + layoutKey: detailView ? 'list' : 'cards', + estimatedRowHeight: detailView ? 56 : 176, + rowAttribute: 'data-magic-note-window-row' + }) const todoPaneWidthLimits = { minimum: 240, maximum: Math.max(240, todoLayoutWidth - 301) } const displayedTodoWidth = clampMagicNotesPaneWidth(todoPaneWidth, todoPaneWidthLimits) const indexExpanded = isNarrowLayout ? narrowIndexOpen : indexPaneOpen @@ -2182,6 +2209,61 @@ export function MagicNotesWorkspace({ } } + const renderNoteCard = (note: MagicNoteSummary, index: number): React.JSX.Element => ( +
+ + +
+ ) + return (
)} -
+
{loadStatus === 'loading' ? (

{t('status.loadingNotes')} @@ -2400,60 +2490,13 @@ export function MagicNotesWorkspace({ )} ) : ( - visibleNotes.map((note) => ( -

- - + noteWindow.windowed ? noteWindow.segments.map((segment) => segment.kind === 'spacer' ? ( +