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:
@@ -1,5 +1,20 @@
|
|||||||
# Magic Notes Progress
|
# Magic Notes Progress
|
||||||
|
|
||||||
|
## 2026-10-03: Compact Notes List And Narrow Drawer
|
||||||
|
|
||||||
|
The notes list now defaults to 200px with a 160px minimum. At workspace widths
|
||||||
|
of 600px or more it can remain beside detail; below 600px the toolbar toggle
|
||||||
|
opens the mounted list as an overlay drawer. Escape, outside click, the close
|
||||||
|
button and note selection dismiss it. Existing desktop width preferences remain
|
||||||
|
intact. Scroll restoration now runs after read-only content effects populate the
|
||||||
|
record, fixing a failure exposed by the Electron layout regression.
|
||||||
|
|
||||||
|
Validation: 143 workspace and list-window tests passed, along with the real
|
||||||
|
Electron layout test covering native resizing, drawer geometry, Escape focus
|
||||||
|
return and note switching. Typecheck and lint on changed code passed. Repository
|
||||||
|
lint was blocked by concurrent App.tsx changes (max-lines error and a missing
|
||||||
|
conversationStore hook dependency warning). No provider calls were made.
|
||||||
|
|
||||||
## 2026-10-01: Header New Note Action
|
## 2026-10-01: Header New Note Action
|
||||||
|
|
||||||
New note is now one primary page-header button that always follows the view
|
New note is now one primary page-header button that always follows the view
|
||||||
|
|||||||
@@ -21,13 +21,17 @@ stream events, error causes and nested release/dispose cleanup.
|
|||||||
## Continuous Record Workspace
|
## Continuous Record Workspace
|
||||||
|
|
||||||
The outer workspace separates the persistent notes list from the detail container.
|
The outer workspace separates the persistent notes list from the detail container.
|
||||||
The list defaults to 280px, resizes within 240-420px and stores `notesPaneWidth`
|
The list defaults to 200px, resizes within 160-420px and stores `notesPaneWidth`
|
||||||
and `notesPaneOpen` in `goodbuddy.magic-notes-layout.v1`. At workspace widths up to
|
and `notesPaneOpen` in `goodbuddy.magic-notes-layout.v1`. Existing saved widths are
|
||||||
900px, selection replaces the visible list with detail while retaining the mounted
|
preserved. Below 600px, selection replaces the visible list with detail while
|
||||||
list. The right-hand `magic-notes-detail` query container and its ResizeObserver
|
retaining the mounted list. The detail toolbar keeps its list toggle, opening an
|
||||||
|
overlay drawer without shrinking detail. Selecting a note, clicking outside,
|
||||||
|
using the drawer close button or pressing Escape closes the drawer; dismissal
|
||||||
|
returns focus to the toggle. Drawer state is transient and separate from the
|
||||||
|
desktop open preference. The right-hand `magic-notes-detail` query container and its ResizeObserver
|
||||||
measure the detail width for two breakpoints: at 800px or less the record index
|
measure the detail width for two breakpoints: at 800px or less the record index
|
||||||
becomes a drawer while AI stays beside the editor; at 600px or less AI stacks
|
becomes a drawer while AI stays beside the editor; at 600px or less AI stacks
|
||||||
below the stream. The 900px list/detail switch is driven by the same measured
|
below the stream. The 600px list/detail switch is driven by the same measured
|
||||||
workspace width in both CSS class and React state, so the two cannot disagree.
|
workspace width in both CSS class and React state, so the two cannot disagree.
|
||||||
Per-note stream scroll positions live in a workspace-local map; explicit entry
|
Per-note stream scroll positions live in a workspace-local map; explicit entry
|
||||||
navigation takes precedence over reading-position restoration.
|
navigation takes precedence over reading-position restoration.
|
||||||
@@ -405,7 +409,9 @@ The overview's `libraryView` and the optional note-only `detailView` are indepen
|
|||||||
opening a todo's source note does not lose the originating task view.
|
opening a todo's source note does not lose the originating task view.
|
||||||
The notes list remains visible beside detail on desktop unless collapsed. On
|
The notes list remains visible beside detail on desktop unless collapsed. On
|
||||||
narrow screens, or when opening a source note from To-dos, the originating list
|
narrow screens, or when opening a source note from To-dos, the originating list
|
||||||
remains mounted but hidden, retaining scroll position and filters. Returning restores the initiating control's focus and
|
remains mounted but hidden, retaining scroll position and filters. The narrow
|
||||||
|
notes view can reopen that list through the detail toolbar drawer toggle.
|
||||||
|
Returning restores the initiating control's focus and
|
||||||
invalidates outstanding detail requests. Initial loading fetches summaries and
|
invalidates outstanding detail requests. Initial loading fetches summaries and
|
||||||
todos without selecting the first note or task. Navigation away from unsaved content or
|
todos without selecting the first note or task. Navigation away from unsaved content or
|
||||||
an unsaved title uses the existing draft confirmation; active writes finish
|
an unsaved title uses the existing draft confirmation; active writes finish
|
||||||
|
|||||||
@@ -610,13 +610,13 @@ describe('MagicNotesWorkspace overview navigation', () => {
|
|||||||
const view = render(<MagicNotesWorkspace onNotify={onNotify} />)
|
const view = render(<MagicNotesWorkspace onNotify={onNotify} />)
|
||||||
await openNote()
|
await openNote()
|
||||||
const separator = screen.getByRole('separator', { name: '调整笔记列表宽度' })
|
const separator = screen.getByRole('separator', { name: '调整笔记列表宽度' })
|
||||||
expect(separator).toHaveAttribute('aria-valuenow', '280')
|
expect(separator).toHaveAttribute('aria-valuenow', '200')
|
||||||
fireEvent.keyDown(separator, { key: 'ArrowRight' })
|
fireEvent.keyDown(separator, { key: 'ArrowRight' })
|
||||||
expect(separator).toHaveAttribute('aria-valuenow', '296')
|
expect(separator).toHaveAttribute('aria-valuenow', '216')
|
||||||
fireEvent.keyDown(separator, { key: 'End' })
|
fireEvent.keyDown(separator, { key: 'End' })
|
||||||
expect(separator).toHaveAttribute('aria-valuenow', '420')
|
expect(separator).toHaveAttribute('aria-valuenow', '420')
|
||||||
fireEvent.keyDown(separator, { key: 'Home' })
|
fireEvent.keyDown(separator, { key: 'Home' })
|
||||||
expect(separator).toHaveAttribute('aria-valuenow', '240')
|
expect(separator).toHaveAttribute('aria-valuenow', '160')
|
||||||
fireEvent.click(screen.getByRole('button', { name: '收起笔记列表' }))
|
fireEvent.click(screen.getByRole('button', { name: '收起笔记列表' }))
|
||||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument()
|
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument()
|
||||||
expect(screen.getByRole('region', { name: '笔记记录' })).toBeVisible()
|
expect(screen.getByRole('region', { name: '笔记记录' })).toBeVisible()
|
||||||
@@ -626,7 +626,41 @@ describe('MagicNotesWorkspace overview navigation', () => {
|
|||||||
await openNote()
|
await openNote()
|
||||||
expect(screen.getByRole('button', { name: '展开笔记列表' })).toHaveAttribute('aria-expanded', 'false')
|
expect(screen.getByRole('button', { name: '展开笔记列表' })).toHaveAttribute('aria-expanded', 'false')
|
||||||
fireEvent.click(screen.getByRole('button', { name: '展开笔记列表' }))
|
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 () => {
|
it('pins and deletes through the persistent sidebar while detail is open', async () => {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ type MagicNotesLayoutPreferences = {
|
|||||||
function loadMagicNotesLayoutPreferences(): MagicNotesLayoutPreferences {
|
function loadMagicNotesLayoutPreferences(): MagicNotesLayoutPreferences {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
notesPaneOpen: true,
|
notesPaneOpen: true,
|
||||||
notesPaneWidth: 280,
|
notesPaneWidth: 200,
|
||||||
recordIndexOpen: false,
|
recordIndexOpen: false,
|
||||||
indexPaneWidth: defaultIndexPaneWidth,
|
indexPaneWidth: defaultIndexPaneWidth,
|
||||||
aiPanePinned: false,
|
aiPanePinned: false,
|
||||||
@@ -152,7 +152,7 @@ function loadMagicNotesLayoutPreferences(): MagicNotesLayoutPreferences {
|
|||||||
return {
|
return {
|
||||||
notesPaneOpen: parsed.notesPaneOpen !== false,
|
notesPaneOpen: parsed.notesPaneOpen !== false,
|
||||||
notesPaneWidth: typeof parsed.notesPaneWidth === 'number' && Number.isFinite(parsed.notesPaneWidth)
|
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)
|
todoPaneWidth: typeof parsed.todoPaneWidth === 'number' && Number.isFinite(parsed.todoPaneWidth)
|
||||||
? Math.max(240, parsed.todoPaneWidth) : defaults.todoPaneWidth,
|
? Math.max(240, parsed.todoPaneWidth) : defaults.todoPaneWidth,
|
||||||
// Older layouts stored always-open panes; the compact defaults ignore those flags.
|
// 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 [indexPaneWidth, setIndexPaneWidth] = useState(initialLayoutPreferences.indexPaneWidth)
|
||||||
const [todoPaneWidth, setTodoPaneWidth] = useState(initialLayoutPreferences.todoPaneWidth)
|
const [todoPaneWidth, setTodoPaneWidth] = useState(initialLayoutPreferences.todoPaneWidth)
|
||||||
const [notesPaneOpen, setNotesPaneOpen] = useState(initialLayoutPreferences.notesPaneOpen)
|
const [notesPaneOpen, setNotesPaneOpen] = useState(initialLayoutPreferences.notesPaneOpen)
|
||||||
|
const [narrowNotesOpen, setNarrowNotesOpen] = useState(false)
|
||||||
const [notesPaneWidth, setNotesPaneWidth] = useState(initialLayoutPreferences.notesPaneWidth)
|
const [notesPaneWidth, setNotesPaneWidth] = useState(initialLayoutPreferences.notesPaneWidth)
|
||||||
const [workspaceWidth, setWorkspaceWidth] = useState(window.innerWidth)
|
const [workspaceWidth, setWorkspaceWidth] = useState(window.innerWidth)
|
||||||
const [todoLayoutWidth, setTodoLayoutWidth] = useState(window.innerWidth)
|
const [todoLayoutWidth, setTodoLayoutWidth] = useState(window.innerWidth)
|
||||||
@@ -696,19 +697,23 @@ export function MagicNotesWorkspace({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const workspace = workspaceRef.current
|
const workspace = workspaceRef.current
|
||||||
if (!workspace) return
|
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()
|
update()
|
||||||
const observer = new ResizeObserver(update)
|
const observer = new ResizeObserver(update)
|
||||||
observer.observe(workspace)
|
observer.observe(workspace)
|
||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
}, [loadStatus])
|
}, [loadStatus])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
const stream = streamRef.current
|
const stream = streamRef.current
|
||||||
if (!stream || !detail?.id) return
|
if (!stream || !detail?.id) return
|
||||||
const position = noteScrollPositions.current.get(detail.id) ?? 0
|
const position = noteScrollPositions.current.get(detail.id) ?? 0
|
||||||
if (requestedEntryIdRef.current) return
|
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 })
|
const frame = requestAnimationFrame(() => { stream.scrollTop = position })
|
||||||
return () => cancelAnimationFrame(frame)
|
return () => cancelAnimationFrame(frame)
|
||||||
}, [detail?.id, detailView])
|
}, [detail?.id, detailView])
|
||||||
@@ -1168,6 +1173,7 @@ export function MagicNotesWorkspace({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (target.kind === 'overview') {
|
if (target.kind === 'overview') {
|
||||||
|
setNarrowNotesOpen(false)
|
||||||
discardComposerDraft()
|
discardComposerDraft()
|
||||||
discardEditingDraft()
|
discardEditingDraft()
|
||||||
detailRequestRef.current += 1
|
detailRequestRef.current += 1
|
||||||
@@ -1183,6 +1189,7 @@ export function MagicNotesWorkspace({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (target.kind === 'library-view') {
|
if (target.kind === 'library-view') {
|
||||||
|
setNarrowNotesOpen(false)
|
||||||
if (target.value === 'todos') {
|
if (target.value === 'todos') {
|
||||||
discardComposerDraft()
|
discardComposerDraft()
|
||||||
discardEditingDraft()
|
discardEditingDraft()
|
||||||
@@ -1218,6 +1225,7 @@ export function MagicNotesWorkspace({
|
|||||||
setOverviewReturnNoteId(target.noteId)
|
setOverviewReturnNoteId(target.noteId)
|
||||||
}
|
}
|
||||||
setDetailView('notes')
|
setDetailView('notes')
|
||||||
|
setNarrowNotesOpen(false)
|
||||||
focusSwitchTarget(target)
|
focusSwitchTarget(target)
|
||||||
void loadDetail(target.noteId, target.entryId)
|
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.
|
// Detail tiers: >800 three columns; 601-800 editor + AI with a record drawer; <=600 AI stacks below.
|
||||||
const isNarrowLayout = magicNotesLayoutWidth <= 800
|
const isNarrowLayout = magicNotesLayoutWidth <= 800
|
||||||
const isStackedLayout = magicNotesLayoutWidth <= 600
|
const isStackedLayout = magicNotesLayoutWidth <= 600
|
||||||
const isNarrowWorkspace = workspaceWidth <= 900
|
const isNarrowWorkspace = workspaceWidth < 600
|
||||||
const paneResizeDisabled = (pane: 'ai' | 'index' | 'todo' | 'notes'): boolean =>
|
const paneResizeDisabled = (pane: 'ai' | 'index' | 'todo' | 'notes'): boolean =>
|
||||||
pane === 'notes' ? isNarrowWorkspace : pane === 'todo' ? todoLayoutWidth <= 700 : pane === 'ai' ? isStackedLayout : isNarrowLayout
|
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)
|
const displayedNotesWidth = clampMagicNotesPaneWidth(notesPaneWidth, notesPaneWidthLimits)
|
||||||
// Overview shows every note full width; the split list appears only beside an open note.
|
// 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 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 visibleNoteIdsKey = visibleNotes.map((note) => note.id).join('\n')
|
||||||
const visibleNoteIds = useMemo(
|
const visibleNoteIds = useMemo(
|
||||||
() => (visibleNoteIdsKey ? visibleNoteIdsKey.split('\n') : []),
|
() => (visibleNoteIdsKey ? visibleNoteIdsKey.split('\n') : []),
|
||||||
@@ -2216,12 +2232,16 @@ export function MagicNotesWorkspace({
|
|||||||
className="magic-note-list-item"
|
className="magic-note-list-item"
|
||||||
aria-current={selectedNoteId === note.id ? 'true' : undefined}
|
aria-current={selectedNoteId === note.id ? 'true' : undefined}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
selectedNoteId !== note.id && requestDraftSwitch({
|
if (selectedNoteId === note.id) {
|
||||||
|
if (notesDrawerOpen) closeNotesDrawer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void requestDraftSwitch({
|
||||||
kind: 'note',
|
kind: 'note',
|
||||||
noteId: note.id
|
noteId: note.id
|
||||||
})
|
})
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<span className="magic-note-list-item__title">
|
<span className="magic-note-list-item__title">
|
||||||
{note.pinned && (
|
{note.pinned && (
|
||||||
@@ -2359,9 +2379,10 @@ export function MagicNotesWorkspace({
|
|||||||
<div
|
<div
|
||||||
ref={workspaceRef}
|
ref={workspaceRef}
|
||||||
aria-busy={Boolean(busy)}
|
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}
|
style={{ '--magic-notes-notes-width': `${displayedNotesWidth}px` } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
|
{notesDrawerOpen && <div className="magic-notes-list-backdrop" aria-hidden="true" onClick={closeNotesDrawer} />}
|
||||||
<section
|
<section
|
||||||
aria-label={t(
|
aria-label={t(
|
||||||
libraryView === 'notes'
|
libraryView === 'notes'
|
||||||
@@ -2370,6 +2391,12 @@ export function MagicNotesWorkspace({
|
|||||||
)}
|
)}
|
||||||
className={`magic-notes-overview${libraryView === 'todos' ? ' magic-notes-overview--todos' : ''}`}
|
className={`magic-notes-overview${libraryView === 'todos' ? ' magic-notes-overview--todos' : ''}`}
|
||||||
hidden={libraryView === 'notes' ? !notesExpanded : Boolean(detailView)}
|
hidden={libraryView === 'notes' ? !notesExpanded : Boolean(detailView)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (notesDrawerOpen && event.key === 'Escape') {
|
||||||
|
event.stopPropagation()
|
||||||
|
closeNotesDrawer()
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{libraryView === 'notes' ? (
|
{libraryView === 'notes' ? (
|
||||||
<div
|
<div
|
||||||
@@ -2379,6 +2406,7 @@ export function MagicNotesWorkspace({
|
|||||||
<div className="magic-notes-pane-heading">
|
<div className="magic-notes-pane-heading">
|
||||||
<strong>{t('notes.heading')}</strong>
|
<strong>{t('notes.heading')}</strong>
|
||||||
<span>{notes.length}</span>
|
<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
|
<MagicNoteTagManagerButton
|
||||||
compact={Boolean(detailView)}
|
compact={Boolean(detailView)}
|
||||||
disabled={Boolean(busy)}
|
disabled={Boolean(busy)}
|
||||||
@@ -2744,11 +2772,11 @@ export function MagicNotesWorkspace({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="magic-note-detail-toolbar" role="toolbar" aria-label={t('notes.toolbarLabel')}>
|
<div className="magic-note-detail-toolbar" role="toolbar" aria-label={t('notes.toolbarLabel')}>
|
||||||
{libraryView === 'notes' && !isNarrowWorkspace && (
|
{libraryView === 'notes' && (
|
||||||
<button type="button" className="icon-button" aria-controls="magic-library-panel-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')}
|
aria-expanded={notesExpanded} aria-label={t(notesExpanded ? 'actions.hideNotes' : 'actions.showNotes')}
|
||||||
title={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} />}
|
{notesExpanded ? <PanelLeftClose aria-hidden="true" size={16} /> : <PanelLeftOpen aria-hidden="true" size={16} />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1283,6 +1283,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.magic-notes-workspace {
|
.magic-notes-workspace {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1291,7 +1292,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.magic-notes-workspace--notes {
|
.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,
|
.magic-notes-workspace--list-hidden,
|
||||||
@@ -1327,6 +1328,24 @@
|
|||||||
gap: var(--space-2);
|
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 {
|
.magic-notes-workspace--notes .magic-notes-pane-heading {
|
||||||
min-height: var(--magic-notes-heading-height);
|
min-height: var(--magic-notes-heading-height);
|
||||||
}
|
}
|
||||||
@@ -15302,6 +15321,14 @@ textarea.field-control {
|
|||||||
overflow-wrap: anywhere;
|
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. */
|
/* Windowed document table (P5): spacer rows stand in for unmounted rows. */
|
||||||
.knowledge-documents__table-scroll tbody tr.knowledge-documents__spacer {
|
.knowledge-documents__table-scroll tbody tr.knowledge-documents__spacer {
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -19431,6 +19458,11 @@ textarea.field-control {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
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 {
|
.workspace-git__changes {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ it('resizes record columns with native Electron input and preserves desktop widt
|
|||||||
const toolbarBox = await rect('.magic-note-detail-toolbar'), streamBox = await rect('.magic-notes-stream-pane');
|
const toolbarBox = await rect('.magic-note-detail-toolbar'), streamBox = await rect('.magic-notes-stream-pane');
|
||||||
const streamClient = await js('document.querySelector(".magic-notes-stream-pane").clientWidth');
|
const streamClient = await js('document.querySelector(".magic-notes-stream-pane").clientWidth');
|
||||||
assert(Math.abs(toolbarBox.x - streamBox.x) <= 1 && Math.abs(toolbarBox.width - streamClient) <= 1, 'Toolbar spans the stream: ' + JSON.stringify({ toolbarBox, streamBox, streamClient }));
|
assert(Math.abs(toolbarBox.x - streamBox.x) <= 1 && Math.abs(toolbarBox.width - streamClient) <= 1, 'Toolbar spans the stream: ' + JSON.stringify({ toolbarBox, streamBox, streamClient }));
|
||||||
assert.equal((await rect('.magic-notes-overview')).width, 280);
|
assert.equal((await rect('.magic-notes-overview')).width, 200);
|
||||||
assert(await js('document.querySelector(".magic-notes-overview").checkVisibility()'));
|
assert(await js('document.querySelector(".magic-notes-overview").checkVisibility()'));
|
||||||
await js('document.querySelector(".magic-notes-card-grid").scrollTop = 300; document.querySelector(".magic-notes-stream-pane").scrollTop = 420');
|
await js('document.querySelector(".magic-notes-card-grid").scrollTop = 300; document.querySelector(".magic-notes-stream-pane").scrollTop = 420');
|
||||||
await wait('document.querySelector(".magic-notes-stream-pane").scrollTop === 420');
|
await wait('document.querySelector(".magic-notes-stream-pane").scrollTop === 420');
|
||||||
@@ -108,7 +108,7 @@ it('resizes record columns with native Electron input and preserves desktop widt
|
|||||||
assert.equal(await js('document.querySelector("#magic-note-select-layout-note").getAttribute("aria-current")'), 'true');
|
assert.equal(await js('document.querySelector("#magic-note-select-layout-note").getAttribute("aria-current")'), 'true');
|
||||||
await drag('.magic-notes-notes-resize-handle', 40);
|
await drag('.magic-notes-notes-resize-handle', 40);
|
||||||
const notesWidth = (await rect('.magic-notes-overview')).width;
|
const notesWidth = (await rect('.magic-notes-overview')).width;
|
||||||
assert(notesWidth >= 320 && notesWidth <= 325);
|
assert(notesWidth >= 240 && notesWidth <= 245);
|
||||||
await key('.magic-notes-notes-resize-handle', 'Left');
|
await key('.magic-notes-notes-resize-handle', 'Left');
|
||||||
assert.equal((await rect('.magic-notes-overview')).width, notesWidth - 16);
|
assert.equal((await rect('.magic-notes-overview')).width, notesWidth - 16);
|
||||||
await click('button[aria-controls="magic-library-panel-notes"]');
|
await click('button[aria-controls="magic-library-panel-notes"]');
|
||||||
@@ -117,7 +117,7 @@ it('resizes record columns with native Electron input and preserves desktop widt
|
|||||||
assert.equal((await rect('.magic-notes-overview')).width, notesWidth - 16);
|
assert.equal((await rect('.magic-notes-overview')).width, notesWidth - 16);
|
||||||
assert.equal(await js('document.querySelector(".magic-notes-card-grid").scrollTop'), 300);
|
assert.equal(await js('document.querySelector(".magic-notes-card-grid").scrollTop'), 300);
|
||||||
await key('.magic-notes-notes-resize-handle', 'Home');
|
await key('.magic-notes-notes-resize-handle', 'Home');
|
||||||
assert.equal((await rect('.magic-notes-overview')).width, 240);
|
assert.equal((await rect('.magic-notes-overview')).width, 160);
|
||||||
await js('document.querySelector(".magic-notes-stream-pane").scrollTop = 0');
|
await js('document.querySelector(".magic-notes-stream-pane").scrollTop = 0');
|
||||||
const editor = '.magic-note-composer .magic-note-editor__content .ql-editor';
|
const editor = '.magic-note-composer .magic-note-editor__content .ql-editor';
|
||||||
const saveButton = '.magic-note-composer > footer > .primary-button';
|
const saveButton = '.magic-note-composer > footer > .primary-button';
|
||||||
@@ -184,10 +184,10 @@ it('resizes record columns with native Electron input and preserves desktop widt
|
|||||||
const withoutAi = (await rect(index)).width; assert(withoutAi < 250 && withoutAi > 230);
|
const withoutAi = (await rect(index)).width; assert(withoutAi < 250 && withoutAi > 230);
|
||||||
await click('button[aria-controls="magic-notes-ai-pane"]'); assert.equal((await rect(index)).width, withoutAi);
|
await click('button[aria-controls="magic-notes-ai-pane"]'); assert.equal((await rect(index)).width, withoutAi);
|
||||||
await win.loadURL(${JSON.stringify(server.resolvedUrls!.local[0] + 'layout.html')}); await open(); assert.equal((await rect(index)).width, withoutAi);
|
await win.loadURL(${JSON.stringify(server.resolvedUrls!.local[0] + 'layout.html')}); await open(); assert.equal((await rect(index)).width, withoutAi);
|
||||||
assert.equal((await rect('.magic-notes-overview')).width, 240);
|
assert.equal((await rect('.magic-notes-overview')).width, 160);
|
||||||
await js('document.querySelector(".magic-notes-card-grid").scrollTop = 300');
|
await js('document.querySelector(".magic-notes-card-grid").scrollTop = 300');
|
||||||
win.setContentSize(1000, 720); await settle(); await settle();
|
win.setContentSize(960, 720); await settle(); await settle();
|
||||||
assert.equal((await rect('.magic-notes-overview')).width, 240);
|
assert.equal((await rect('.magic-notes-overview')).width, 160);
|
||||||
assert.equal((await rect(index)).width, 0, 'Record drawer follows detail width, not the outer page');
|
assert.equal((await rect(index)).width, 0, 'Record drawer follows detail width, not the outer page');
|
||||||
const assertSideBySide = async () => {
|
const assertSideBySide = async () => {
|
||||||
const s = await rect(stream), ai = await rect('.magic-notes-ai-pane'), list = await rect('.magic-notes-overview');
|
const s = await rect(stream), ai = await rect('.magic-notes-ai-pane'), list = await rect('.magic-notes-overview');
|
||||||
@@ -198,12 +198,14 @@ it('resizes record columns with native Electron input and preserves desktop widt
|
|||||||
await assertSideBySide();
|
await assertSideBySide();
|
||||||
win.setContentSize(930, 720); await settle(); await settle();
|
win.setContentSize(930, 720); await settle(); await settle();
|
||||||
await assertSideBySide();
|
await assertSideBySide();
|
||||||
win.setContentSize(1000, 720); await settle(); await settle();
|
win.setContentSize(960, 720); await settle(); await settle();
|
||||||
assert(await js('document.documentElement.scrollWidth <= innerWidth'));
|
assert(await js('document.documentElement.scrollWidth <= innerWidth'));
|
||||||
win.setContentSize(820, 720); await settle(); await settle();
|
win.setContentSize(820, 720); await settle(); await settle();
|
||||||
assert((await rect(stream)).width >= 299);
|
assert((await rect(stream)).width >= 299);
|
||||||
const constrained = { index: (await rect(index)).width, editor: (await rect(stream)).width, ai: (await rect('.magic-notes-ai-pane')).width };
|
const constrained = { index: (await rect(index)).width, editor: (await rect(stream)).width, ai: (await rect('.magic-notes-ai-pane')).width };
|
||||||
win.setContentSize(720, 640); await settle(); await settle();
|
win.setContentSize(720, 640); await settle(); await settle();
|
||||||
|
assert.equal((await rect('.magic-notes-overview')).width, 160);
|
||||||
|
await click('#magic-notes-list-toggle');
|
||||||
assert.equal((await rect('.magic-notes-overview')).width, 0);
|
assert.equal((await rect('.magic-notes-overview')).width, 0);
|
||||||
await click('#magic-notes-back');
|
await click('#magic-notes-back');
|
||||||
assert(await js('document.querySelector(".magic-notes-overview").checkVisibility()'));
|
assert(await js('document.querySelector(".magic-notes-overview").checkVisibility()'));
|
||||||
@@ -224,10 +226,28 @@ it('resizes record columns with native Electron input and preserves desktop widt
|
|||||||
assert(Math.abs(narrowFooter.x + narrowFooter.width - footerPaddingRight - narrowSave.x - narrowSave.width) <= 1, 'Narrow Save button stays right-aligned within footer padding');
|
assert(Math.abs(narrowFooter.x + narrowFooter.width - footerPaddingRight - narrowSave.x - narrowSave.width) <= 1, 'Narrow Save button stays right-aligned within footer padding');
|
||||||
await screenshot('magic-notes-narrow');
|
await screenshot('magic-notes-narrow');
|
||||||
win.setContentSize(560, 640); await settle(); await settle();
|
win.setContentSize(560, 640); await settle(); await settle();
|
||||||
|
assert(await js('document.querySelector("#magic-notes-list-toggle").checkVisibility()'));
|
||||||
|
const detailBeforeDrawer = await rect('.magic-notes-detail-container');
|
||||||
|
await click('#magic-notes-list-toggle');
|
||||||
|
assert(await js('document.querySelector(".magic-notes-workspace--drawer") !== null'));
|
||||||
|
assert.equal((await rect('.magic-notes-overview')).width, 160);
|
||||||
|
assert.deepEqual(await rect('.magic-notes-detail-container'), detailBeforeDrawer);
|
||||||
|
assert(await js('document.querySelector(".magic-notes-overview").scrollWidth <= document.querySelector(".magic-notes-overview").clientWidth'));
|
||||||
|
await key('#magic-library-panel-notes input', 'Escape');
|
||||||
|
assert.equal((await rect('.magic-notes-overview')).width, 0);
|
||||||
|
assert(await js('document.activeElement.id === "magic-notes-list-toggle"'));
|
||||||
|
await click('#magic-notes-list-toggle');
|
||||||
|
await click('#magic-note-select-other-note-3');
|
||||||
|
await wait('document.querySelector(".magic-note-detail-title").value === "Other note 3"');
|
||||||
|
assert.equal((await rect('.magic-notes-overview')).width, 0);
|
||||||
|
await click('#magic-notes-list-toggle');
|
||||||
|
await click('#magic-note-select-layout-note');
|
||||||
|
await wait('document.querySelector(".magic-note-detail-title").value === "Layout note"');
|
||||||
assert((await rect('.magic-notes-ai-pane')).y >= (await rect(stream)).y + (await rect(stream)).height, 'AI pane stacks below the stream on very narrow widths');
|
assert((await rect('.magic-notes-ai-pane')).y >= (await rect(stream)).y + (await rect(stream)).height, 'AI pane stacks below the stream on very narrow widths');
|
||||||
assert.equal(await js('getComputedStyle(document.querySelector(".magic-notes-ai-resize-handle")).display'), 'none');
|
assert.equal(await js('getComputedStyle(document.querySelector(".magic-notes-ai-resize-handle")).display'), 'none');
|
||||||
win.setContentSize(1280, 800); await settle(); await settle();
|
win.setContentSize(1280, 800); await settle(); await settle();
|
||||||
assert.equal((await rect('.magic-notes-overview')).width, 240);
|
await click('#magic-notes-list-toggle');
|
||||||
|
assert.equal((await rect('.magic-notes-overview')).width, 160);
|
||||||
assert.equal((await rect(index)).width, withoutAi);
|
assert.equal((await rect(index)).width, withoutAi);
|
||||||
await key('.magic-note-record', 'Tab');
|
await key('.magic-note-record', 'Tab');
|
||||||
await js('document.querySelector(".magic-note-record").focus()');
|
await js('document.querySelector(".magic-note-record").focus()');
|
||||||
|
|||||||
Reference in New Issue
Block a user