Add study margin display settings

This commit is contained in:
2026-07-12 10:02:18 -05:00
parent 720702105c
commit 796912db8b
7 changed files with 337 additions and 69 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "libre-study",
"version": "0.1.12",
"version": "0.1.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "libre-study",
"version": "0.1.12",
"version": "0.1.13",
"dependencies": {
"@tauri-apps/api": "^2.11.1",
"react": "^19.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "libre-study",
"private": true,
"version": "0.1.12",
"version": "0.1.13",
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
+1 -1
View File
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "app"
version = "0.1.12"
version = "0.1.13"
dependencies = [
"log",
"rusqlite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "app"
version = "0.1.12"
version = "0.1.13"
description = "A Tauri App"
authors = ["you"]
license = ""
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Libre Study",
"version": "0.1.12",
"version": "0.1.13",
"identifier": "org.librestudy.desktop",
"build": {
"frontendDist": "../dist",
+99 -18
View File
@@ -461,6 +461,44 @@ textarea:focus-visible {
background: #fffdf6;
}
.margin-section-header {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
}
.collapse-button,
.mini-settings {
border: 0;
background: transparent;
color: #657061;
font-size: 11px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
.collapse-button {
padding: 0;
text-align: left;
}
.collapse-button::before {
margin-right: 5px;
color: #735c0f;
content: 'v';
}
.mini-settings {
width: auto;
height: auto;
padding: 3px 6px;
border: 1px solid #d8cfbf;
border-radius: 4px;
background: #fbf8ef;
}
.original-language-verse h3 {
margin: 0;
color: #657061;
@@ -512,33 +550,40 @@ textarea:focus-visible {
outline: none;
}
.linked-word-list {
display: flex;
flex-wrap: wrap;
.english-strong-line {
display: grid;
gap: 8px;
margin: 0 0 14px;
padding: 12px;
border: 1px solid #dfd6c2;
border-radius: 6px;
background: #fffdf6;
}
.word-link {
display: inline-flex;
align-items: baseline;
gap: 7px;
padding: 7px 9px;
border-color: #cfc8b8;
background: #f3efe4;
.linked-line-text {
margin: 0;
color: #202124;
font-size: 13px;
line-height: 1.65;
overflow-wrap: anywhere;
}
.word-link small {
color: #657061;
font-size: 11px;
font-weight: 800;
.linked-word {
display: inline;
padding: 0 2px;
border-radius: 3px;
cursor: pointer;
overflow-wrap: anywhere;
}
.word-link.active {
border-color: #2f6f5e;
background: #e7f0eb;
.linked-word + .linked-word {
margin-inline-start: 0.35em;
}
.linked-word:hover,
.linked-word:focus-visible,
.linked-word.active {
background: rgba(214, 168, 79, 0.25);
outline: none;
}
.strong-focus {
@@ -559,6 +604,11 @@ textarea:focus-visible {
font-size: 12px;
}
.strong-focus > .margin-section-header span {
color: #657061;
font-size: 12px;
}
.strong-focus h4 {
color: #1f211e;
font-size: 22px;
@@ -970,6 +1020,37 @@ textarea:focus-visible {
width: min(1080px, 100%);
}
.settings-modal {
width: min(780px, 100%);
}
.settings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.settings-grid .modal-section:last-child {
grid-column: 1 / -1;
}
.settings-grid input[type='range'] {
padding: 0;
}
.checkbox-row {
display: flex !important;
gap: 9px !important;
align-items: center;
color: #252821 !important;
font-size: 14px !important;
text-transform: none !important;
}
.checkbox-row input {
width: auto;
}
.about-modal p {
margin: 0;
color: #3f443d;
+232 -45
View File
@@ -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>
)