fix: keep the notes list accessible in narrow workspaces
Default the notes list to 200px with a 160px minimum and hide it automatically only below 600px. Keep the detail toolbar toggle available to open a drawer, with dismissal and note-switching support. Restore reading positions after rich text content effects populate the record. Include the existing document-table and file-tree scroll anchoring styles in this file-level commit. Validation: 143 related tests, Electron layout regression, typecheck and changed-file lint passed; repository lint is blocked by unrelated App.tsx changes. Release note: The notes list takes less space and remains accessible through a drawer in narrow windows. Existing saved list widths are preserved.
This commit is contained in:
@@ -610,13 +610,13 @@ describe('MagicNotesWorkspace overview navigation', () => {
|
||||
const view = render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
await openNote()
|
||||
const separator = screen.getByRole('separator', { name: '调整笔记列表宽度' })
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '280')
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '200')
|
||||
fireEvent.keyDown(separator, { key: 'ArrowRight' })
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '296')
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '216')
|
||||
fireEvent.keyDown(separator, { key: 'End' })
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '420')
|
||||
fireEvent.keyDown(separator, { key: 'Home' })
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '240')
|
||||
expect(separator).toHaveAttribute('aria-valuenow', '160')
|
||||
fireEvent.click(screen.getByRole('button', { name: '收起笔记列表' }))
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: '笔记记录' })).toBeVisible()
|
||||
@@ -626,7 +626,41 @@ describe('MagicNotesWorkspace overview navigation', () => {
|
||||
await openNote()
|
||||
expect(screen.getByRole('button', { name: '展开笔记列表' })).toHaveAttribute('aria-expanded', 'false')
|
||||
fireEvent.click(screen.getByRole('button', { name: '展开笔记列表' }))
|
||||
expect(screen.getByRole('separator', { name: '调整笔记列表宽度' })).toHaveAttribute('aria-valuenow', '240')
|
||||
expect(screen.getByRole('separator', { name: '调整笔记列表宽度' })).toHaveAttribute('aria-valuenow', '160')
|
||||
})
|
||||
|
||||
it.each([600, 700])('keeps the notes list beside detail at %ipx', async (width) => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(width)
|
||||
render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
await openNote()
|
||||
expect(screen.getByRole('region', { name: '笔记列表' })).toBeVisible()
|
||||
expect(screen.getByRole('separator', { name: '调整笔记列表宽度' })).toBeVisible()
|
||||
})
|
||||
|
||||
it('opens the notes drawer below 600px and dismisses or switches notes', async () => {
|
||||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(599)
|
||||
render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||
await openNote()
|
||||
expect(screen.queryByRole('region', { name: '笔记列表' })).not.toBeInTheDocument()
|
||||
const toggle = screen.getByRole('button', { name: '展开笔记列表' })
|
||||
fireEvent.click(toggle)
|
||||
const sidebar = screen.getByRole('region', { name: '笔记列表' })
|
||||
expect(sidebar).toBeVisible()
|
||||
expect(screen.getByRole('searchbox')).toHaveFocus()
|
||||
expect(screen.queryByRole('separator', { name: '调整笔记列表宽度' })).not.toBeInTheDocument()
|
||||
fireEvent.keyDown(screen.getByRole('searchbox'), { key: 'Escape' })
|
||||
expect(toggle).toHaveFocus()
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false')
|
||||
fireEvent.click(toggle)
|
||||
fireEvent.click(document.querySelector('.magic-notes-list-backdrop')!)
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false')
|
||||
fireEvent.click(toggle)
|
||||
fireEvent.click(within(sidebar).getByRole('button', { name: '收起笔记列表' }))
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false')
|
||||
fireEvent.click(toggle)
|
||||
await openNote(second.title)
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.getByLabelText('笔记标题')).toHaveValue(second.title)
|
||||
})
|
||||
|
||||
it('pins and deletes through the persistent sidebar while detail is open', async () => {
|
||||
|
||||
@@ -134,7 +134,7 @@ type MagicNotesLayoutPreferences = {
|
||||
function loadMagicNotesLayoutPreferences(): MagicNotesLayoutPreferences {
|
||||
const defaults = {
|
||||
notesPaneOpen: true,
|
||||
notesPaneWidth: 280,
|
||||
notesPaneWidth: 200,
|
||||
recordIndexOpen: false,
|
||||
indexPaneWidth: defaultIndexPaneWidth,
|
||||
aiPanePinned: false,
|
||||
@@ -152,7 +152,7 @@ function loadMagicNotesLayoutPreferences(): MagicNotesLayoutPreferences {
|
||||
return {
|
||||
notesPaneOpen: parsed.notesPaneOpen !== false,
|
||||
notesPaneWidth: typeof parsed.notesPaneWidth === 'number' && Number.isFinite(parsed.notesPaneWidth)
|
||||
? Math.min(420, Math.max(240, parsed.notesPaneWidth)) : defaults.notesPaneWidth,
|
||||
? Math.min(420, Math.max(160, parsed.notesPaneWidth)) : defaults.notesPaneWidth,
|
||||
todoPaneWidth: typeof parsed.todoPaneWidth === 'number' && Number.isFinite(parsed.todoPaneWidth)
|
||||
? Math.max(240, parsed.todoPaneWidth) : defaults.todoPaneWidth,
|
||||
// Older layouts stored always-open panes; the compact defaults ignore those flags.
|
||||
@@ -521,6 +521,7 @@ export function MagicNotesWorkspace({
|
||||
const [indexPaneWidth, setIndexPaneWidth] = useState(initialLayoutPreferences.indexPaneWidth)
|
||||
const [todoPaneWidth, setTodoPaneWidth] = useState(initialLayoutPreferences.todoPaneWidth)
|
||||
const [notesPaneOpen, setNotesPaneOpen] = useState(initialLayoutPreferences.notesPaneOpen)
|
||||
const [narrowNotesOpen, setNarrowNotesOpen] = useState(false)
|
||||
const [notesPaneWidth, setNotesPaneWidth] = useState(initialLayoutPreferences.notesPaneWidth)
|
||||
const [workspaceWidth, setWorkspaceWidth] = useState(window.innerWidth)
|
||||
const [todoLayoutWidth, setTodoLayoutWidth] = useState(window.innerWidth)
|
||||
@@ -696,19 +697,23 @@ export function MagicNotesWorkspace({
|
||||
useEffect(() => {
|
||||
const workspace = workspaceRef.current
|
||||
if (!workspace) return
|
||||
const update = (): void => setWorkspaceWidth(workspace.getBoundingClientRect().width || window.innerWidth)
|
||||
const update = (): void => {
|
||||
const width = workspace.getBoundingClientRect().width || window.innerWidth
|
||||
setWorkspaceWidth(width)
|
||||
if (width >= 600) setNarrowNotesOpen(false)
|
||||
}
|
||||
update()
|
||||
const observer = new ResizeObserver(update)
|
||||
observer.observe(workspace)
|
||||
return () => observer.disconnect()
|
||||
}, [loadStatus])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
const stream = streamRef.current
|
||||
if (!stream || !detail?.id) return
|
||||
const position = noteScrollPositions.current.get(detail.id) ?? 0
|
||||
if (requestedEntryIdRef.current) return
|
||||
// Read-only rich text mounts in an effect; restore after it has laid out.
|
||||
// Run after the read-only rich text effects have populated their content.
|
||||
const frame = requestAnimationFrame(() => { stream.scrollTop = position })
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [detail?.id, detailView])
|
||||
@@ -1168,6 +1173,7 @@ export function MagicNotesWorkspace({
|
||||
return
|
||||
}
|
||||
if (target.kind === 'overview') {
|
||||
setNarrowNotesOpen(false)
|
||||
discardComposerDraft()
|
||||
discardEditingDraft()
|
||||
detailRequestRef.current += 1
|
||||
@@ -1183,6 +1189,7 @@ export function MagicNotesWorkspace({
|
||||
return
|
||||
}
|
||||
if (target.kind === 'library-view') {
|
||||
setNarrowNotesOpen(false)
|
||||
if (target.value === 'todos') {
|
||||
discardComposerDraft()
|
||||
discardEditingDraft()
|
||||
@@ -1218,6 +1225,7 @@ export function MagicNotesWorkspace({
|
||||
setOverviewReturnNoteId(target.noteId)
|
||||
}
|
||||
setDetailView('notes')
|
||||
setNarrowNotesOpen(false)
|
||||
focusSwitchTarget(target)
|
||||
void loadDetail(target.noteId, target.entryId)
|
||||
},
|
||||
@@ -1754,14 +1762,22 @@ export function MagicNotesWorkspace({
|
||||
// Detail tiers: >800 three columns; 601-800 editor + AI with a record drawer; <=600 AI stacks below.
|
||||
const isNarrowLayout = magicNotesLayoutWidth <= 800
|
||||
const isStackedLayout = magicNotesLayoutWidth <= 600
|
||||
const isNarrowWorkspace = workspaceWidth <= 900
|
||||
const isNarrowWorkspace = workspaceWidth < 600
|
||||
const paneResizeDisabled = (pane: 'ai' | 'index' | 'todo' | 'notes'): boolean =>
|
||||
pane === 'notes' ? isNarrowWorkspace : pane === 'todo' ? todoLayoutWidth <= 700 : pane === 'ai' ? isStackedLayout : isNarrowLayout
|
||||
const notesPaneWidthLimits = { minimum: 240, maximum: Math.min(420, Math.max(240, workspaceWidth - minimumMagicNotesEditorWidth - magicNotesResizeHandleWidth)) }
|
||||
const notesPaneWidthLimits = { minimum: 160, maximum: Math.min(420, Math.max(160, workspaceWidth - minimumMagicNotesEditorWidth - magicNotesResizeHandleWidth)) }
|
||||
const displayedNotesWidth = clampMagicNotesPaneWidth(notesPaneWidth, notesPaneWidthLimits)
|
||||
// Overview shows every note full width; the split list appears only beside an open note.
|
||||
const notesExpanded = isNarrowWorkspace ? !detailView : notesPaneOpen || !detailView
|
||||
const notesDrawerOpen = libraryView === 'notes' && Boolean(detailView) && isNarrowWorkspace && narrowNotesOpen
|
||||
const notesExpanded = isNarrowWorkspace ? !detailView || notesDrawerOpen : notesPaneOpen || !detailView
|
||||
const notesSplit = libraryView === 'notes' && Boolean(detailView) && !isNarrowWorkspace && notesPaneOpen
|
||||
const closeNotesDrawer = (): void => {
|
||||
setNarrowNotesOpen(false)
|
||||
document.getElementById('magic-notes-list-toggle')?.focus({ preventScroll: true })
|
||||
}
|
||||
useEffect(() => {
|
||||
if (notesDrawerOpen) document.querySelector<HTMLInputElement>('#magic-library-panel-notes input[type="search"]')?.focus({ preventScroll: true })
|
||||
}, [notesDrawerOpen])
|
||||
const visibleNoteIdsKey = visibleNotes.map((note) => note.id).join('\n')
|
||||
const visibleNoteIds = useMemo(
|
||||
() => (visibleNoteIdsKey ? visibleNoteIdsKey.split('\n') : []),
|
||||
@@ -2216,12 +2232,16 @@ export function MagicNotesWorkspace({
|
||||
className="magic-note-list-item"
|
||||
aria-current={selectedNoteId === note.id ? 'true' : undefined}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
selectedNoteId !== note.id && requestDraftSwitch({
|
||||
onClick={() => {
|
||||
if (selectedNoteId === note.id) {
|
||||
if (notesDrawerOpen) closeNotesDrawer()
|
||||
return
|
||||
}
|
||||
void requestDraftSwitch({
|
||||
kind: 'note',
|
||||
noteId: note.id
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="magic-note-list-item__title">
|
||||
{note.pinned && (
|
||||
@@ -2359,9 +2379,10 @@ export function MagicNotesWorkspace({
|
||||
<div
|
||||
ref={workspaceRef}
|
||||
aria-busy={Boolean(busy)}
|
||||
className={`magic-notes-workspace${notesSplit ? ' magic-notes-workspace--notes' : ''}${libraryView === 'notes' && !detailView ? ' magic-notes-workspace--overview' : ''}${notesExpanded ? '' : ' magic-notes-workspace--list-hidden'}${isNarrowWorkspace ? ' magic-notes-workspace--narrow' : ''}${resizingPane === 'notes' ? ' magic-notes-layout--resizing' : ''}`}
|
||||
className={`magic-notes-workspace${notesSplit || notesDrawerOpen ? ' magic-notes-workspace--notes' : ''}${notesDrawerOpen ? ' magic-notes-workspace--drawer' : ''}${libraryView === 'notes' && !detailView ? ' magic-notes-workspace--overview' : ''}${notesExpanded ? '' : ' magic-notes-workspace--list-hidden'}${isNarrowWorkspace ? ' magic-notes-workspace--narrow' : ''}${resizingPane === 'notes' ? ' magic-notes-layout--resizing' : ''}`}
|
||||
style={{ '--magic-notes-notes-width': `${displayedNotesWidth}px` } as React.CSSProperties}
|
||||
>
|
||||
{notesDrawerOpen && <div className="magic-notes-list-backdrop" aria-hidden="true" onClick={closeNotesDrawer} />}
|
||||
<section
|
||||
aria-label={t(
|
||||
libraryView === 'notes'
|
||||
@@ -2370,6 +2391,12 @@ export function MagicNotesWorkspace({
|
||||
)}
|
||||
className={`magic-notes-overview${libraryView === 'todos' ? ' magic-notes-overview--todos' : ''}`}
|
||||
hidden={libraryView === 'notes' ? !notesExpanded : Boolean(detailView)}
|
||||
onKeyDown={(event) => {
|
||||
if (notesDrawerOpen && event.key === 'Escape') {
|
||||
event.stopPropagation()
|
||||
closeNotesDrawer()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{libraryView === 'notes' ? (
|
||||
<div
|
||||
@@ -2379,6 +2406,7 @@ export function MagicNotesWorkspace({
|
||||
<div className="magic-notes-pane-heading">
|
||||
<strong>{t('notes.heading')}</strong>
|
||||
<span>{notes.length}</span>
|
||||
{notesDrawerOpen && <button type="button" className="icon-button" aria-label={t('actions.hideNotes')} onClick={closeNotesDrawer}><PanelLeftClose aria-hidden="true" size={16} /></button>}
|
||||
<MagicNoteTagManagerButton
|
||||
compact={Boolean(detailView)}
|
||||
disabled={Boolean(busy)}
|
||||
@@ -2744,11 +2772,11 @@ export function MagicNotesWorkspace({
|
||||
}}
|
||||
>
|
||||
<div className="magic-note-detail-toolbar" role="toolbar" aria-label={t('notes.toolbarLabel')}>
|
||||
{libraryView === 'notes' && !isNarrowWorkspace && (
|
||||
<button type="button" className="icon-button" aria-controls="magic-library-panel-notes"
|
||||
{libraryView === 'notes' && (
|
||||
<button id="magic-notes-list-toggle" type="button" className="icon-button" aria-controls="magic-library-panel-notes"
|
||||
aria-expanded={notesExpanded} aria-label={t(notesExpanded ? 'actions.hideNotes' : 'actions.showNotes')}
|
||||
title={t(notesExpanded ? 'actions.hideNotes' : 'actions.showNotes')}
|
||||
onClick={() => setNotesPaneOpen((current) => !current)}>
|
||||
onClick={() => isNarrowWorkspace ? setNarrowNotesOpen((current) => !current) : setNotesPaneOpen((current) => !current)}>
|
||||
{notesExpanded ? <PanelLeftClose aria-hidden="true" size={16} /> : <PanelLeftOpen aria-hidden="true" size={16} />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1283,6 +1283,7 @@
|
||||
}
|
||||
|
||||
.magic-notes-workspace {
|
||||
position: relative;
|
||||
display: grid;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -1291,7 +1292,7 @@
|
||||
}
|
||||
|
||||
.magic-notes-workspace--notes {
|
||||
grid-template-columns: var(--magic-notes-notes-width, 280px) 9px minmax(0, 1fr);
|
||||
grid-template-columns: var(--magic-notes-notes-width, 200px) 9px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.magic-notes-workspace--list-hidden,
|
||||
@@ -1327,6 +1328,24 @@
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.magic-notes-list-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 4;
|
||||
background: var(--overlay-backdrop);
|
||||
}
|
||||
|
||||
.magic-notes-workspace--drawer > .magic-notes-overview {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
z-index: 5;
|
||||
width: min(var(--magic-notes-notes-width, 200px), calc(100% - 32px));
|
||||
padding: var(--space-2);
|
||||
background: var(--surface-raised);
|
||||
border-right: 1px solid var(--border-default);
|
||||
box-shadow: var(--shadow-dialog);
|
||||
}
|
||||
|
||||
.magic-notes-workspace--notes .magic-notes-pane-heading {
|
||||
min-height: var(--magic-notes-heading-height);
|
||||
}
|
||||
@@ -15302,6 +15321,14 @@ textarea.field-control {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Windowed document table (P5): the table keeps the reader's row in place by
|
||||
correcting the page's scrollTop; native scroll anchoring on the page could
|
||||
pick a spacer and fight the correction. */
|
||||
.workspace-panel-scroll:has(.knowledge-documents__table-scroll--windowed),
|
||||
.knowledge-documents__table-scroll--windowed {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
/* Windowed document table (P5): spacer rows stand in for unmounted rows. */
|
||||
.knowledge-documents__table-scroll tbody tr.knowledge-documents__spacer {
|
||||
border: 0;
|
||||
@@ -19431,6 +19458,11 @@ textarea.field-control {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
/* A windowed tree keeps the reader's row in place itself; native scroll
|
||||
anchoring could pick a spacer and fight the correction. */
|
||||
.workspace-files__files-view--windowed {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
.workspace-git__changes {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
Reference in New Issue
Block a user