Add study margin display settings
This commit is contained in:
+232
-45
@@ -8,6 +8,7 @@ const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7']
|
||||
const pageSize = 34
|
||||
const sessionStorageKey = 'libre-study.sessions.v1'
|
||||
const activeSessionStorageKey = 'libre-study.active-session.v1'
|
||||
const studySettingsStorageKey = 'libre-study.study-settings.v1'
|
||||
const timelineLimit = 200
|
||||
type ViewMode = 'page' | 'study' | 'notes' | 'compare'
|
||||
type StrongDisplayMode = 'off' | 'highlight' | 'numbers'
|
||||
@@ -155,6 +156,7 @@ function App() {
|
||||
const [sessionModalOpen, setSessionModalOpen] = useState(false)
|
||||
const [aboutModalOpen, setAboutModalOpen] = useState(false)
|
||||
const [timelineModalOpen, setTimelineModalOpen] = useState(false)
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false)
|
||||
const [pageMotion, setPageMotion] = useState<PageMotion>('idle')
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('notes')
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -172,6 +174,11 @@ function App() {
|
||||
const [newSessionFootnotes, setNewSessionFootnotes] = useState(true)
|
||||
const [newSessionStrongs, setNewSessionStrongs] = useState(true)
|
||||
const [newSessionTimeline, setNewSessionTimeline] = useState(true)
|
||||
const [lemmaFontSize, setLemmaFontSize] = useState(17)
|
||||
const [linkedFontSize, setLinkedFontSize] = useState(15)
|
||||
const [originalCollapsed, setOriginalCollapsed] = useState(false)
|
||||
const [linkedCollapsed, setLinkedCollapsed] = useState(false)
|
||||
const [definitionCollapsed, setDefinitionCollapsed] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const restoredSessionId = useRef('')
|
||||
@@ -300,10 +307,30 @@ function App() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const savedStudySettings = localStorage.getItem(studySettingsStorageKey)
|
||||
const savedSessions = localStorage.getItem(sessionStorageKey)
|
||||
const savedActiveSessionId = localStorage.getItem(activeSessionStorageKey)
|
||||
let nextSessions: StudySession[] = []
|
||||
|
||||
if (savedStudySettings) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedStudySettings) as Partial<{
|
||||
lemmaFontSize: number
|
||||
linkedFontSize: number
|
||||
originalCollapsed: boolean
|
||||
linkedCollapsed: boolean
|
||||
definitionCollapsed: boolean
|
||||
}>
|
||||
setLemmaFontSize(parsed.lemmaFontSize ?? 17)
|
||||
setLinkedFontSize(parsed.linkedFontSize ?? 15)
|
||||
setOriginalCollapsed(parsed.originalCollapsed ?? false)
|
||||
setLinkedCollapsed(parsed.linkedCollapsed ?? false)
|
||||
setDefinitionCollapsed(parsed.definitionCollapsed ?? false)
|
||||
} catch {
|
||||
localStorage.removeItem(studySettingsStorageKey)
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSessions) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedSessions) as StudySession[]
|
||||
@@ -350,6 +377,19 @@ function App() {
|
||||
}
|
||||
}, [activeSessionId])
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
studySettingsStorageKey,
|
||||
JSON.stringify({
|
||||
lemmaFontSize,
|
||||
linkedFontSize,
|
||||
originalCollapsed,
|
||||
linkedCollapsed,
|
||||
definitionCollapsed,
|
||||
}),
|
||||
)
|
||||
}, [definitionCollapsed, lemmaFontSize, linkedCollapsed, linkedFontSize, originalCollapsed])
|
||||
|
||||
useEffect(() => {
|
||||
if (restoredSessionId.current === activeSessionId) return
|
||||
const activeSession = sessions.find((session) => session.id === activeSessionId)
|
||||
@@ -718,27 +758,83 @@ function App() {
|
||||
|
||||
return (
|
||||
<section className="original-language-verse" aria-label="Original language lemma line">
|
||||
<h3>{isHebrew ? 'Hebrew Lemma Line' : 'Greek Lemma Line'}</h3>
|
||||
<p className="lemma-line-text" dir={direction}>
|
||||
{selectedVerse.strongs.map((entry) => (
|
||||
<span
|
||||
key={`${entry.strong}-${entry.position}-lemma`}
|
||||
className={entry.strong === selectedStrongEntry?.strong ? 'lemma-word active' : 'lemma-word'}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={`${entry.surface}: ${entry.strong}`}
|
||||
onClick={() => selectStrong(selectedVerse, entry.strong)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
selectStrong(selectedVerse, entry.strong)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{entry.lemma || entry.surface}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
<header className="margin-section-header">
|
||||
<button
|
||||
type="button"
|
||||
className="collapse-button"
|
||||
onClick={() => setOriginalCollapsed((current) => !current)}
|
||||
>
|
||||
{isHebrew ? 'Hebrew Lemma Line' : 'Greek Lemma Line'}
|
||||
</button>
|
||||
<button type="button" className="mini-settings" onClick={() => setSettingsModalOpen(true)}>
|
||||
Aa
|
||||
</button>
|
||||
</header>
|
||||
{!originalCollapsed ? (
|
||||
<p className="lemma-line-text" dir={direction} style={{ fontSize: `${lemmaFontSize}px` }}>
|
||||
{selectedVerse.strongs.map((entry) => (
|
||||
<span
|
||||
key={`${entry.strong}-${entry.position}-lemma`}
|
||||
className={entry.strong === selectedStrongEntry?.strong ? 'lemma-word active' : 'lemma-word'}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={`${entry.surface}: ${entry.strong}`}
|
||||
onClick={() => selectStrong(selectedVerse, entry.strong)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
selectStrong(selectedVerse, entry.strong)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{entry.lemma || entry.surface}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function renderLinkedEnglishLine() {
|
||||
if (!selectedVerse?.strongs.length) return null
|
||||
|
||||
return (
|
||||
<section className="english-strong-line" aria-label="English Strong's line">
|
||||
<header className="margin-section-header">
|
||||
<button
|
||||
type="button"
|
||||
className="collapse-button"
|
||||
onClick={() => setLinkedCollapsed((current) => !current)}
|
||||
>
|
||||
English Linked Text
|
||||
</button>
|
||||
<button type="button" className="mini-settings" onClick={() => setSettingsModalOpen(true)}>
|
||||
Aa
|
||||
</button>
|
||||
</header>
|
||||
{!linkedCollapsed ? (
|
||||
<p className="linked-line-text" style={{ fontSize: `${linkedFontSize}px` }}>
|
||||
{selectedVerse.strongs.map((entry) => (
|
||||
<span
|
||||
key={`${entry.strong}-${entry.position}-english`}
|
||||
className={entry.strong === selectedStrongEntry?.strong ? 'linked-word active' : 'linked-word'}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={`${entry.surface}: ${entry.strong}`}
|
||||
onClick={() => selectStrong(selectedVerse, entry.strong)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
selectStrong(selectedVerse, entry.strong)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{entry.surface}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -796,32 +892,33 @@ function App() {
|
||||
) : (
|
||||
<>
|
||||
{renderOriginalLanguageLine()}
|
||||
<div className="linked-word-list" aria-label="Linked Strong's words">
|
||||
{selectedVerse.strongs.map((entry) => (
|
||||
<button
|
||||
key={`${entry.strong}-${entry.position}`}
|
||||
className={entry.strong === selectedStrongEntry?.strong ? 'word-link active' : 'word-link'}
|
||||
type="button"
|
||||
title={`${entry.surface}: ${entry.strong}`}
|
||||
onClick={() => selectStrong(selectedVerse, entry.strong)}
|
||||
>
|
||||
<span>{entry.surface}</span>
|
||||
<small>{entry.strong}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{renderLinkedEnglishLine()}
|
||||
{selectedStrongEntry ? (
|
||||
<article className="strong-focus">
|
||||
<div>
|
||||
<strong>{selectedStrongEntry.strong}</strong>
|
||||
<article className="strong-focus" aria-label="Strong's definition">
|
||||
<header className="margin-section-header">
|
||||
<button
|
||||
type="button"
|
||||
className="collapse-button"
|
||||
onClick={() => setDefinitionCollapsed((current) => !current)}
|
||||
>
|
||||
Definition
|
||||
</button>
|
||||
<span>{selectedStrongEntry.language}</span>
|
||||
</div>
|
||||
<h4>{selectedStrongEntry.lemma}</h4>
|
||||
<p className="translit">{selectedStrongEntry.transliteration}</p>
|
||||
<p>{selectedStrongEntry.definition}</p>
|
||||
<button type="button" onClick={() => searchStrong(selectedStrongEntry.strong)}>
|
||||
Show matching verses
|
||||
</button>
|
||||
</header>
|
||||
{!definitionCollapsed ? (
|
||||
<>
|
||||
<div>
|
||||
<strong>{selectedStrongEntry.strong}</strong>
|
||||
<span>{selectedStrongEntry.language}</span>
|
||||
</div>
|
||||
<h4>{selectedStrongEntry.lemma}</h4>
|
||||
<p className="translit">{selectedStrongEntry.transliteration}</p>
|
||||
<p>{selectedStrongEntry.definition}</p>
|
||||
<button type="button" onClick={() => searchStrong(selectedStrongEntry.strong)}>
|
||||
Show matching verses
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</article>
|
||||
) : null}
|
||||
</>
|
||||
@@ -1144,6 +1241,91 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function renderSettingsModal() {
|
||||
if (!settingsModalOpen) return null
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" role="presentation" onMouseDown={() => setSettingsModalOpen(false)}>
|
||||
<section
|
||||
className="modal-panel settings-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Study display settings"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<span className="eyebrow">Settings</span>
|
||||
<h2>Study Display</h2>
|
||||
</div>
|
||||
<button type="button" className="close-button" onClick={() => setSettingsModalOpen(false)}>
|
||||
Close
|
||||
</button>
|
||||
</header>
|
||||
<div className="settings-grid">
|
||||
<section className="modal-section">
|
||||
<h3>Original Language Line</h3>
|
||||
<label>
|
||||
Text size
|
||||
<input
|
||||
type="range"
|
||||
min="13"
|
||||
max="26"
|
||||
value={lemmaFontSize}
|
||||
onChange={(event) => setLemmaFontSize(Number(event.target.value))}
|
||||
/>
|
||||
<span>{lemmaFontSize}px</span>
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!originalCollapsed}
|
||||
onChange={(event) => setOriginalCollapsed(!event.target.checked)}
|
||||
/>
|
||||
Show original language line
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="modal-section">
|
||||
<h3>English Strong's Line</h3>
|
||||
<label>
|
||||
Text size
|
||||
<input
|
||||
type="range"
|
||||
min="12"
|
||||
max="23"
|
||||
value={linkedFontSize}
|
||||
onChange={(event) => setLinkedFontSize(Number(event.target.value))}
|
||||
/>
|
||||
<span>{linkedFontSize}px</span>
|
||||
</label>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!linkedCollapsed}
|
||||
onChange={(event) => setLinkedCollapsed(!event.target.checked)}
|
||||
/>
|
||||
Show English linked text
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="modal-section">
|
||||
<h3>Definition Panel</h3>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!definitionCollapsed}
|
||||
onChange={(event) => setDefinitionCollapsed(!event.target.checked)}
|
||||
/>
|
||||
Show expanded definition
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={railOpen ? 'app-shell menu-open' : 'app-shell'}>
|
||||
<aside
|
||||
@@ -1175,6 +1357,10 @@ function App() {
|
||||
<span className="rail-icon">S</span>
|
||||
<span className="rail-copy"><strong>Sessions</strong><small>Save, export, and configure</small></span>
|
||||
</button>
|
||||
<button type="button" title="Settings" onClick={() => setSettingsModalOpen(true)}>
|
||||
<span className="rail-icon">A</span>
|
||||
<span className="rail-copy"><strong>Settings</strong><small>Study display controls</small></span>
|
||||
</button>
|
||||
<button type="button" className="rail-bottom" title="About Libre Study" onClick={() => setAboutModalOpen(true)}>
|
||||
<span className="rail-icon">?</span>
|
||||
<span className="rail-copy"><strong>About</strong><small>Project, licenses, and links</small></span>
|
||||
@@ -1368,6 +1554,7 @@ function App() {
|
||||
</section>
|
||||
{renderSessionModal()}
|
||||
{renderAboutModal()}
|
||||
{renderSettingsModal()}
|
||||
{renderTimelineModal()}
|
||||
</main>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user