Add study margin display settings
This commit is contained in:
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
Generated
+1
-1
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rusqlite",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
|
||||
@@ -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
@@ -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
@@ -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