diff --git a/src/App.flow.test.jsx b/src/App.flow.test.jsx index aaa89fc..c3822e2 100644 --- a/src/App.flow.test.jsx +++ b/src/App.flow.test.jsx @@ -115,7 +115,7 @@ describe('Search palette (⌘K)', () => { await user.type(await paletteBox(), 'abounds'); const hit = await screen.findByRole('option', { name: /Titus 1:1/ }); await user.click(hit); - expect(await screen.findByText(/Your notes/i)).toBeInTheDocument(); + expect(await screen.findByRole('textbox', { name: 'Observe' })).toBeInTheDocument(); }); test('Escape closes it', async () => { diff --git a/src/components/study/PopOut.jsx b/src/components/study/PopOut.jsx new file mode 100644 index 0000000..77e4aad --- /dev/null +++ b/src/components/study/PopOut.jsx @@ -0,0 +1,21 @@ +// A section of the Study page lifted into a large window over the page, so there's room to read commentary or +// write notes without the column's cramped width. Esc, the backdrop or Close puts it back. +import { useEffect, useRef } from 'react'; + +export default function PopOut({ title, onClose, children }) { + const panel = useRef(null); + useEffect(() => { panel.current?.focus(); }, []); + return ( +
Observe first
@@ -68,8 +71,8 @@ export function useResourceTab() { const [tab, setTabState] = useState(() => { try { const saved = localStorage.getItem('studyResourceTab'); - return RESOURCE_TABS.some((t) => t.id === saved) ? saved : 'commentary'; - } catch { return 'commentary'; } + return saved === 'notes' || RESOURCE_TABS.some((t) => t.id === saved) ? saved : 'notes'; + } catch { return 'notes'; } }); const setTab = (id) => { setTabState(id); diff --git a/src/pages/StudyPage.jsx b/src/pages/StudyPage.jsx index f47609f..cfb9317 100644 --- a/src/pages/StudyPage.jsx +++ b/src/pages/StudyPage.jsx @@ -1,6 +1,6 @@ -// The Study stage: the chunk outline on the left, the passage and your -// Observe / Interpret / Apply notes in the middle, and commentary, word study, -// cross-references and people & places on the right — all for the selected chunk. +// The Study stage: the passage in the middle, with the chunk list as a card that slides over it from the left edge, +// and one adjustable column on the right where Notes (Observe / Interpret / Apply) and the resources (commentary, word +// study, cross-references, people & places) are tabs — all for the selected chunk. import { useEffect, useRef, useState } from 'react'; import { useApp } from '../context/AppContext.js'; import { useCrossRefs } from '../hooks/useCrossRefs.js'; @@ -12,6 +12,7 @@ import StudioShell from '../components/study/StudioShell.jsx'; import OutlineRail from '../components/study/OutlineRail.jsx'; import ScripturePane from '../components/study/ScripturePane.jsx'; import NotesPane from '../components/study/NotesPane.jsx'; +import PopOut from '../components/study/PopOut.jsx'; import { commentAuthor } from '../utils/chunkComments.js'; import ResourcePane, { useResourceTab } from '../components/study/ResourcePane.jsx'; import CompareSheet from '../components/study/CompareSheet.jsx'; @@ -43,30 +44,16 @@ export default function StudyPage() { // The right column's width can be dragged (remembered on this device) const gridRef = useRef(null); const rightPanelRef = useRef(null); - const rightPanel = useResizableWidth({ storageKey: 'study-right-width', containerRef: gridRef }); + const rightPanel = useResizableWidth({ storageKey: 'study-right-width', containerRef: gridRef, maxAbsolute: 960, reserve: 480 }); const [tab, setTab] = useResourceTab(); const [openWordId, setOpenWordId] = useState(null); const [comparing, setComparing] = useState(false); - // Either half of the right column can be folded away so the other fills it (remembered on this device). - const [folded, setFolded] = useState(() => { - try { const f = JSON.parse(localStorage.getItem('study-folded')); return { notes: !!f?.notes, resources: !!f?.resources && !f?.notes }; } catch { return { notes: false, resources: false }; } - }); + const [popped, setPopped] = useState(false); // the notes-and-resources column lifted into a large window const [focusRequest, setFocusRequest] = useState(null); - const showHalf = (which) => setFolded((cur) => { - if (!cur[which]) return cur; - const next = { ...cur, [which]: false }; - try { localStorage.setItem('study-folded', JSON.stringify(next)); } catch { /* storage unavailable */ } - return next; - }); - const toggleFold = (which) => setFolded((cur) => { - const other = which === 'notes' ? 'resources' : 'notes'; - // folding the last open half opens the other one instead, so the column is never empty - const next = { ...cur, [which]: !cur[which] }; - if (next[which] && next[other]) next[other] = false; - try { localStorage.setItem('study-folded', JSON.stringify(next)); } catch { /* storage unavailable */ } - return next; - }); + // The chunk list is a card that slides over the passage (remembered on this device). + const [chunksOpen, setChunksOpenState] = useState(() => { try { return localStorage.getItem('study-chunks-open') === 'on'; } catch { return false; } }); + const setChunksOpen = (open) => { setChunksOpenState(open); try { localStorage.setItem('study-chunks-open', open ? 'on' : 'off'); } catch { /* storage unavailable */ } }; useEffect(() => { setOpenWordId(null); }, [selectedChunk?.id]); const resources = useStudyResources({ @@ -77,9 +64,10 @@ export default function StudyPage() { const crossRefs = useCrossRefs({ project, allChunks, selectedChunk, updateChunk, notify }); const wordStudy = useWordStudy({ project, allChunks, updateProject, notify }); - const select = (id) => updateProject((current) => ({ ...current, selectedChunkId: id })); - const goPrev = () => { if (globalIndex > 0) select(allChunks[globalIndex - 1].id); }; - const goNext = () => { if (globalIndex >= 0 && globalIndex < allChunks.length - 1) select(allChunks[globalIndex + 1].id); }; + const select = (id) => { updateProject((current) => ({ ...current, selectedChunkId: id })); setChunksOpen(false); }; + const step = (to) => updateProject((current) => ({ ...current, selectedChunkId: to })); // prev/next keep the card as it is + const goPrev = () => { if (globalIndex > 0) step(allChunks[globalIndex - 1].id); }; + const goNext = () => { if (globalIndex >= 0 && globalIndex < allChunks.length - 1) step(allChunks[globalIndex + 1].id); }; // Arrow keys move between chunks; Escape closes the word picker useEffect(() => { @@ -87,15 +75,17 @@ export default function StudyPage() { const tag = event.target?.tagName; const editable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || event.target?.isContentEditable; if (event.key === 'Escape') { + if (popped && !wordStudy.suggestModal) { setPopped(false); return; } + if (chunksOpen && !wordStudy.suggestModal && !editable) { setChunksOpen(false); return; } if (wordStudy.suggestModal) { wordStudy.setSuggestModal(null); wordStudy.setSuggestSelection(new Set()); } else if (editable && !comparing) event.target.blur(); // leave a note box and go back to keyboard moves return; } - if (editable || wordStudy.suggestModal || comparing || event.metaKey || event.ctrlKey || event.altKey) return; + if (editable || popped || wordStudy.suggestModal || comparing || event.metaKey || event.ctrlKey || event.altKey) return; if (event.key === 'ArrowLeft') { event.preventDefault(); goPrev(); } else if (event.key === 'ArrowRight') { event.preventDefault(); goNext(); } - else if (NOTE_KEYS[event.key]) { event.preventDefault(); showHalf('notes'); setFocusRequest({ field: NOTE_KEYS[event.key], n: Date.now() }); } - else if (TAB_KEYS[event.key.toLowerCase()]) { event.preventDefault(); showHalf('resources'); setTab(TAB_KEYS[event.key.toLowerCase()]); } + else if (NOTE_KEYS[event.key]) { event.preventDefault(); setTab('notes'); setFocusRequest({ field: NOTE_KEYS[event.key], n: Date.now() }); } + else if (TAB_KEYS[event.key.toLowerCase()]) { event.preventDefault(); setTab(TAB_KEYS[event.key.toLowerCase()]); } else if (event.key.toLowerCase() === 'v') { event.preventDefault(); setComparing(true); } }; window.addEventListener('keydown', onKey); @@ -113,15 +103,40 @@ export default function StudyPage() { if (id) { setTab('words'); setOpenWordId(id); } }; + const notesPane = selectedChunk && chapter && ( +