Compact reader controls and margin layout
This commit is contained in:
+264
-35
@@ -6,7 +6,7 @@ import {
|
||||
getLibraryState,
|
||||
searchVerses,
|
||||
} from './studyClient'
|
||||
import type { DragEvent, ReactNode } from 'react'
|
||||
import type { DragEvent, MouseEvent as ReactMouseEvent, ReactNode } from 'react'
|
||||
import type { CommentaryEntry, DictionaryEntry, Verse } from './types'
|
||||
import './App.css'
|
||||
|
||||
@@ -219,6 +219,7 @@ function App() {
|
||||
const [timelineModalOpen, setTimelineModalOpen] = useState(false)
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false)
|
||||
const [notesDrawerOpen, setNotesDrawerOpen] = useState(false)
|
||||
const [notesPinned, setNotesPinned] = useState(false)
|
||||
const [pageMotion, setPageMotion] = useState<PageMotion>('idle')
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('notes')
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -245,6 +246,12 @@ function App() {
|
||||
const [definitionCollapsed, setDefinitionCollapsed] = useState(false)
|
||||
const [commentaryCollapsed, setCommentaryCollapsed] = useState(false)
|
||||
const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false)
|
||||
const [showOriginalLine, setShowOriginalLine] = useState(true)
|
||||
const [showLinkedLine, setShowLinkedLine] = useState(true)
|
||||
const [showDefinitionPanel, setShowDefinitionPanel] = useState(true)
|
||||
const [showCommentaryPanel, setShowCommentaryPanel] = useState(true)
|
||||
const [showDictionaryPanel, setShowDictionaryPanel] = useState(true)
|
||||
const [splitPercent, setSplitPercent] = useState(50)
|
||||
const [commentaryEntries, setCommentaryEntries] = useState<CommentaryEntry[]>([])
|
||||
const [dictionaryEntries, setDictionaryEntries] = useState<DictionaryEntry[]>([])
|
||||
const [marginResourcesLoading, setMarginResourcesLoading] = useState(false)
|
||||
@@ -252,6 +259,8 @@ function App() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const restoredSessionId = useRef('')
|
||||
const pageMotionTimeout = useRef<number | null>(null)
|
||||
const notesDragOpenTimeout = useRef<number | null>(null)
|
||||
const readerRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
async function loadLibrary() {
|
||||
setError(null)
|
||||
@@ -389,12 +398,30 @@ function App() {
|
||||
originalCollapsed: boolean
|
||||
linkedCollapsed: boolean
|
||||
definitionCollapsed: boolean
|
||||
commentaryCollapsed: boolean
|
||||
dictionaryCollapsed: boolean
|
||||
showOriginalLine: boolean
|
||||
showLinkedLine: boolean
|
||||
showDefinitionPanel: boolean
|
||||
showCommentaryPanel: boolean
|
||||
showDictionaryPanel: boolean
|
||||
splitPercent: number
|
||||
notesPinned: boolean
|
||||
}>
|
||||
setLemmaFontSize(parsed.lemmaFontSize ?? 17)
|
||||
setLinkedFontSize(parsed.linkedFontSize ?? 15)
|
||||
setOriginalCollapsed(parsed.originalCollapsed ?? false)
|
||||
setLinkedCollapsed(parsed.linkedCollapsed ?? false)
|
||||
setDefinitionCollapsed(parsed.definitionCollapsed ?? false)
|
||||
setCommentaryCollapsed(parsed.commentaryCollapsed ?? false)
|
||||
setDictionaryCollapsed(parsed.dictionaryCollapsed ?? false)
|
||||
setShowOriginalLine(parsed.showOriginalLine ?? true)
|
||||
setShowLinkedLine(parsed.showLinkedLine ?? true)
|
||||
setShowDefinitionPanel(parsed.showDefinitionPanel ?? true)
|
||||
setShowCommentaryPanel(parsed.showCommentaryPanel ?? true)
|
||||
setShowDictionaryPanel(parsed.showDictionaryPanel ?? true)
|
||||
setSplitPercent(parsed.splitPercent ?? 50)
|
||||
setNotesPinned(parsed.notesPinned ?? false)
|
||||
} catch {
|
||||
localStorage.removeItem(studySettingsStorageKey)
|
||||
}
|
||||
@@ -457,9 +484,33 @@ function App() {
|
||||
originalCollapsed,
|
||||
linkedCollapsed,
|
||||
definitionCollapsed,
|
||||
commentaryCollapsed,
|
||||
dictionaryCollapsed,
|
||||
showOriginalLine,
|
||||
showLinkedLine,
|
||||
showDefinitionPanel,
|
||||
showCommentaryPanel,
|
||||
showDictionaryPanel,
|
||||
splitPercent,
|
||||
notesPinned,
|
||||
}),
|
||||
)
|
||||
}, [definitionCollapsed, lemmaFontSize, linkedCollapsed, linkedFontSize, originalCollapsed])
|
||||
}, [
|
||||
commentaryCollapsed,
|
||||
definitionCollapsed,
|
||||
dictionaryCollapsed,
|
||||
lemmaFontSize,
|
||||
linkedCollapsed,
|
||||
linkedFontSize,
|
||||
notesPinned,
|
||||
originalCollapsed,
|
||||
showCommentaryPanel,
|
||||
showDefinitionPanel,
|
||||
showDictionaryPanel,
|
||||
showLinkedLine,
|
||||
showOriginalLine,
|
||||
splitPercent,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (restoredSessionId.current === activeSessionId) return
|
||||
@@ -724,6 +775,34 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function recordDisplaySetting(label: string) {
|
||||
recordTimeline({
|
||||
type: 'setting',
|
||||
label,
|
||||
})
|
||||
}
|
||||
|
||||
function beginSplitResize(event: ReactMouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault()
|
||||
const reader = readerRef.current
|
||||
if (!reader) return
|
||||
|
||||
const resize = (moveEvent: MouseEvent) => {
|
||||
const rect = reader.getBoundingClientRect()
|
||||
const next = ((moveEvent.clientX - rect.left) / rect.width) * 100
|
||||
setSplitPercent(Math.min(68, Math.max(36, next)))
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
window.removeEventListener('mousemove', resize)
|
||||
window.removeEventListener('mouseup', stop)
|
||||
recordDisplaySetting('Adjusted Bible and study margin split')
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', resize)
|
||||
window.addEventListener('mouseup', stop, { once: true })
|
||||
}
|
||||
|
||||
function updateNoteDraft(value: string) {
|
||||
setNoteDraft(value)
|
||||
patchActiveSession({ noteDraft: value })
|
||||
@@ -739,7 +818,20 @@ function App() {
|
||||
function startTextDrag(event: DragEvent<HTMLElement>, value: string) {
|
||||
event.dataTransfer.setData('text/plain', value)
|
||||
event.dataTransfer.effectAllowed = 'copy'
|
||||
setNotesDrawerOpen(true)
|
||||
}
|
||||
|
||||
function scheduleNotesDragOpen() {
|
||||
if (notesPinned || notesDragOpenTimeout.current) return
|
||||
notesDragOpenTimeout.current = window.setTimeout(() => {
|
||||
setNotesDrawerOpen(true)
|
||||
notesDragOpenTimeout.current = null
|
||||
}, 550)
|
||||
}
|
||||
|
||||
function clearNotesDragOpen() {
|
||||
if (!notesDragOpenTimeout.current) return
|
||||
window.clearTimeout(notesDragOpenTimeout.current)
|
||||
notesDragOpenTimeout.current = null
|
||||
}
|
||||
|
||||
function renameActiveSession(title: string) {
|
||||
@@ -881,7 +973,7 @@ function App() {
|
||||
}
|
||||
|
||||
function renderOriginalLanguageLine() {
|
||||
if (!selectedVerse?.strongs.length) return null
|
||||
if (!showOriginalLine || !selectedVerse?.strongs.length) return null
|
||||
const isHebrew = selectedVerse.strongs.some((entry) => entry.strong.startsWith('H'))
|
||||
const direction = isHebrew ? 'rtl' : 'ltr'
|
||||
|
||||
@@ -891,11 +983,15 @@ function App() {
|
||||
className="margin-section-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setOriginalCollapsed((current) => !current)}
|
||||
onClick={() => {
|
||||
setOriginalCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled original language accordion')
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setOriginalCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled original language accordion')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -943,7 +1039,7 @@ function App() {
|
||||
}
|
||||
|
||||
function renderLinkedEnglishLine() {
|
||||
if (!selectedVerse?.strongs.length) return null
|
||||
if (!showLinkedLine || !selectedVerse?.strongs.length) return null
|
||||
|
||||
return (
|
||||
<section className="english-strong-line" aria-label="English Strong's line">
|
||||
@@ -951,11 +1047,15 @@ function App() {
|
||||
className="margin-section-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setLinkedCollapsed((current) => !current)}
|
||||
onClick={() => {
|
||||
setLinkedCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled English linked text accordion')
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setLinkedCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled English linked text accordion')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1003,6 +1103,8 @@ function App() {
|
||||
}
|
||||
|
||||
function renderCommentarySection() {
|
||||
if (!showCommentaryPanel) return null
|
||||
|
||||
if (activeSession?.commentaryEnabled === false) {
|
||||
return (
|
||||
<article className="resource-card">
|
||||
@@ -1021,11 +1123,15 @@ function App() {
|
||||
className="margin-section-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setCommentaryCollapsed((current) => !current)}
|
||||
onClick={() => {
|
||||
setCommentaryCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled commentary accordion')
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setCommentaryCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled commentary accordion')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1060,6 +1166,8 @@ function App() {
|
||||
}
|
||||
|
||||
function renderDictionarySection() {
|
||||
if (!showDictionaryPanel) return null
|
||||
|
||||
if (activeSession?.dictionaryEnabled === false) {
|
||||
return (
|
||||
<article className="resource-card">
|
||||
@@ -1078,11 +1186,15 @@ function App() {
|
||||
className="margin-section-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setDictionaryCollapsed((current) => !current)}
|
||||
onClick={() => {
|
||||
setDictionaryCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled dictionary accordion')
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setDictionaryCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled dictionary accordion')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1158,7 +1270,21 @@ function App() {
|
||||
<button type="button" onClick={() => shiftPage(-1)}>
|
||||
Previous page
|
||||
</button>
|
||||
<span>King James Version - {pageRange}</span>
|
||||
<div className="page-position">
|
||||
<span>{pageRange}</span>
|
||||
<div className="chapter-tabs" aria-label="Chapter selector">
|
||||
{chapters.map((chapter) => (
|
||||
<button
|
||||
key={chapter}
|
||||
type="button"
|
||||
className={chapter === selectedVerse?.chapter ? 'active' : ''}
|
||||
onClick={() => selectedVerse && jumpTo(selectedVerse.book, chapter, 1)}
|
||||
>
|
||||
{chapter}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick={() => shiftPage(1)}>
|
||||
Next page
|
||||
</button>
|
||||
@@ -1182,20 +1308,24 @@ function App() {
|
||||
{activeSession?.strongsEnabled === false ? (
|
||||
<p className="resource-disabled">Strong's links are disabled for this session.</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="margin-resource-grid">
|
||||
{renderOriginalLanguageLine()}
|
||||
{renderLinkedEnglishLine()}
|
||||
{selectedStrongEntry ? (
|
||||
{showDefinitionPanel && selectedStrongEntry ? (
|
||||
<article className="strong-focus" aria-label="Strong's definition">
|
||||
<header
|
||||
className="margin-section-header"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setDefinitionCollapsed((current) => !current)}
|
||||
onClick={() => {
|
||||
setDefinitionCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled Strong definition accordion')
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
setDefinitionCollapsed((current) => !current)
|
||||
recordDisplaySetting('Toggled Strong definition accordion')
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1223,10 +1353,12 @@ function App() {
|
||||
) : null}
|
||||
</article>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{renderCommentarySection()}
|
||||
{renderDictionarySection()}
|
||||
<div className="margin-resource-grid">
|
||||
{renderCommentarySection()}
|
||||
{renderDictionarySection()}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -1617,8 +1749,15 @@ function App() {
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!originalCollapsed}
|
||||
onChange={(event) => setOriginalCollapsed(!event.target.checked)}
|
||||
checked={showOriginalLine}
|
||||
onChange={(event) => {
|
||||
setShowOriginalLine(event.target.checked)
|
||||
recordDisplaySetting(
|
||||
event.target.checked
|
||||
? 'Show original language line'
|
||||
: 'Hide original language line',
|
||||
)
|
||||
}}
|
||||
/>
|
||||
Show original language line
|
||||
</label>
|
||||
@@ -1640,8 +1779,13 @@ function App() {
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!linkedCollapsed}
|
||||
onChange={(event) => setLinkedCollapsed(!event.target.checked)}
|
||||
checked={showLinkedLine}
|
||||
onChange={(event) => {
|
||||
setShowLinkedLine(event.target.checked)
|
||||
recordDisplaySetting(
|
||||
event.target.checked ? 'Show English linked text' : 'Hide English linked text',
|
||||
)
|
||||
}}
|
||||
/>
|
||||
Show English linked text
|
||||
</label>
|
||||
@@ -1652,12 +1796,47 @@ function App() {
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!definitionCollapsed}
|
||||
onChange={(event) => setDefinitionCollapsed(!event.target.checked)}
|
||||
checked={showDefinitionPanel}
|
||||
onChange={(event) => {
|
||||
setShowDefinitionPanel(event.target.checked)
|
||||
recordDisplaySetting(
|
||||
event.target.checked ? 'Show Strong definition' : 'Hide Strong definition',
|
||||
)
|
||||
}}
|
||||
/>
|
||||
Show expanded definition
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="modal-section">
|
||||
<h3>Reference Panels</h3>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showCommentaryPanel}
|
||||
onChange={(event) => {
|
||||
setShowCommentaryPanel(event.target.checked)
|
||||
recordDisplaySetting(
|
||||
event.target.checked ? 'Show commentary panel' : 'Hide commentary panel',
|
||||
)
|
||||
}}
|
||||
/>
|
||||
Show commentary
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showDictionaryPanel}
|
||||
onChange={(event) => {
|
||||
setShowDictionaryPanel(event.target.checked)
|
||||
recordDisplaySetting(
|
||||
event.target.checked ? 'Show dictionary panel' : 'Hide dictionary panel',
|
||||
)
|
||||
}}
|
||||
/>
|
||||
Show dictionary
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1669,32 +1848,58 @@ function App() {
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={notesDrawerOpen ? 'notes-drawer open' : 'notes-drawer'}
|
||||
className={[
|
||||
'notes-drawer',
|
||||
notesDrawerOpen || notesPinned ? 'open' : '',
|
||||
notesPinned ? 'pinned' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
aria-label="Notes drawer"
|
||||
onMouseEnter={() => setNotesDrawerOpen(true)}
|
||||
onMouseLeave={() => setNotesDrawerOpen(false)}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault()
|
||||
setNotesDrawerOpen(true)
|
||||
scheduleNotesDragOpen()
|
||||
}}
|
||||
onDragLeave={clearNotesDragOpen}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault()
|
||||
clearNotesDragOpen()
|
||||
appendNoteClip(event.dataTransfer.getData('text/plain'))
|
||||
setNotesDrawerOpen(true)
|
||||
}}
|
||||
>
|
||||
<div className="notes-drawer-grip" aria-hidden="true">
|
||||
<button
|
||||
type="button"
|
||||
className="notes-drawer-grip"
|
||||
aria-label={notesDrawerOpen || notesPinned ? 'Close notes drawer' : 'Open notes drawer'}
|
||||
onClick={() => {
|
||||
if (notesPinned) return
|
||||
setNotesDrawerOpen((current) => !current)
|
||||
}}
|
||||
>
|
||||
<span>N</span>
|
||||
<span>O</span>
|
||||
<span>T</span>
|
||||
<span>E</span>
|
||||
<span>S</span>
|
||||
</div>
|
||||
</button>
|
||||
<div className="notes-workspace">
|
||||
<header>
|
||||
<span className="eyebrow">Notes</span>
|
||||
<h2>Study Notes</h2>
|
||||
<div>
|
||||
<span className="eyebrow">Notes</span>
|
||||
<h2>Study Notes</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="pin-notes"
|
||||
onClick={() => {
|
||||
const nextPinned = !notesPinned
|
||||
setNotesPinned(nextPinned)
|
||||
setNotesDrawerOpen(nextPinned)
|
||||
recordDisplaySetting(nextPinned ? 'Pinned notes drawer' : 'Unpinned notes drawer')
|
||||
}}
|
||||
>
|
||||
{notesPinned ? 'Unpin' : 'Pin'}
|
||||
</button>
|
||||
</header>
|
||||
<textarea
|
||||
value={noteDraft}
|
||||
@@ -1751,8 +1956,16 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
const splitTemplate = `${splitPercent}fr 6px ${100 - splitPercent}fr`
|
||||
|
||||
return (
|
||||
<main className={railOpen ? 'app-shell menu-open' : 'app-shell'}>
|
||||
<main
|
||||
className={[
|
||||
'app-shell',
|
||||
railOpen ? 'menu-open' : '',
|
||||
notesPinned ? 'notes-pinned' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<aside
|
||||
className="tool-rail"
|
||||
aria-label="Main tools"
|
||||
@@ -1792,14 +2005,17 @@ function App() {
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<section className="reader">
|
||||
<section className="reader" ref={readerRef}>
|
||||
{loading ? (
|
||||
<div className="status-panel">Loading local library...</div>
|
||||
) : error ? (
|
||||
<div className="status-panel error">{error}</div>
|
||||
) : selectedVerse ? (
|
||||
<>
|
||||
<header className="top-bar">
|
||||
<header
|
||||
className="top-bar"
|
||||
style={{ gridTemplateColumns: splitTemplate }}
|
||||
>
|
||||
<div className="bible-top-tools">
|
||||
<div className="page-controls" aria-label="Page selectors">
|
||||
<label>
|
||||
@@ -1885,12 +2101,18 @@ function App() {
|
||||
<button type="submit">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="split-spacer" aria-hidden="true" />
|
||||
<div className="study-top-tools" aria-label="Study margin tools">
|
||||
<span>Study margin tools</span>
|
||||
<button type="button" onClick={() => setSettingsModalOpen(true)}>
|
||||
Margin settings
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className={`study-desk ${viewMode}`}>
|
||||
<div
|
||||
className={`study-desk ${viewMode}`}
|
||||
style={{ gridTemplateColumns: splitTemplate }}
|
||||
>
|
||||
{viewMode === 'compare' ? (
|
||||
<div className="page-system compare-system" aria-label="Compare translations">
|
||||
<div className="compare-pages">
|
||||
@@ -1918,6 +2140,13 @@ function App() {
|
||||
{renderPageTurner()}
|
||||
{renderBookTabs()}
|
||||
</div>
|
||||
<div
|
||||
className="split-resizer"
|
||||
role="separator"
|
||||
aria-label="Resize Bible and study margin"
|
||||
aria-orientation="vertical"
|
||||
onMouseDown={beginSplitResize}
|
||||
/>
|
||||
{renderStudyMargin()}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user