Add session modal and book tabs

This commit is contained in:
2026-07-12 09:38:51 -05:00
parent 2731c00223
commit ea22ffd2b9
7 changed files with 704 additions and 59 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "libre-study", "name": "libre-study",
"version": "0.1.8", "version": "0.1.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "libre-study", "name": "libre-study",
"version": "0.1.8", "version": "0.1.9",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
"react": "^19.2.7", "react": "^19.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "libre-study", "name": "libre-study",
"private": true, "private": true,
"version": "0.1.8", "version": "0.1.9",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1", "dev": "vite --host 127.0.0.1",
+1 -1
View File
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]] [[package]]
name = "app" name = "app"
version = "0.1.8" version = "0.1.9"
dependencies = [ dependencies = [
"log", "log",
"rusqlite", "rusqlite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "app" name = "app"
version = "0.1.8" version = "0.1.9"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
license = "" license = ""
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Libre Study", "productName": "Libre Study",
"version": "0.1.8", "version": "0.1.9",
"identifier": "org.librestudy.desktop", "identifier": "org.librestudy.desktop",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
+278 -4
View File
@@ -6,14 +6,31 @@
color: #202124; color: #202124;
} }
.app-shell.menu-open .reader {
filter: brightness(0.72);
}
.tool-rail { .tool-rail {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
position: relative;
z-index: 20;
width: 58px;
padding: 18px 10px; padding: 18px 10px;
background: #1f231f; background: #1f231f;
border-right: 1px solid #d1cab9; border-right: 1px solid #d1cab9;
box-sizing: border-box;
transition: width 160ms ease, box-shadow 160ms ease;
overflow: hidden;
}
.tool-rail:hover,
.tool-rail:focus-within {
width: 270px;
align-items: stretch;
box-shadow: 18px 0 40px rgba(17, 20, 17, 0.26);
} }
.brand-mark { .brand-mark {
@@ -29,12 +46,74 @@
} }
.tool-rail button { .tool-rail button {
display: flex;
gap: 12px;
align-items: center;
width: 38px; width: 38px;
height: 38px; height: 38px;
padding: 0; padding: 0;
border-color: #3c4239; border-color: #3c4239;
background: #2c302a; background: #2c302a;
color: #f6f3eb; color: #f6f3eb;
white-space: nowrap;
}
.tool-rail:hover button,
.tool-rail:focus-within button {
width: 100%;
padding: 0 10px;
}
.menu-button {
flex: 0 0 auto;
justify-content: center;
}
.menu-button span {
display: block;
width: 16px;
height: 2px;
border-radius: 999px;
background: currentColor;
}
.menu-button {
flex-direction: column;
gap: 4px;
}
.rail-icon {
display: grid;
flex: 0 0 18px;
place-items: center;
font-weight: 800;
}
.rail-copy {
display: none;
min-width: 0;
text-align: left;
}
.tool-rail:hover .rail-copy,
.tool-rail:focus-within .rail-copy {
display: grid;
}
.rail-copy strong,
.rail-copy small {
overflow: hidden;
text-overflow: ellipsis;
}
.rail-copy small {
color: #b9c0b4;
font-size: 11px;
font-weight: 600;
}
.rail-bottom {
margin-top: auto;
} }
.reader { .reader {
@@ -45,6 +124,7 @@
height: 100vh; height: 100vh;
padding: 18px; padding: 18px;
box-sizing: border-box; box-sizing: border-box;
transition: filter 160ms ease;
} }
.top-bar { .top-bar {
@@ -172,7 +252,7 @@ textarea:focus-visible {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) auto; grid-template-rows: minmax(0, 1fr) auto;
min-height: 0; min-height: 0;
overflow: hidden; overflow: visible;
} }
.page-system.study, .page-system.study,
@@ -494,9 +574,10 @@ textarea:focus-visible {
grid-column: 1 / -1; grid-column: 1 / -1;
gap: 12px; gap: 12px;
align-items: flex-start; align-items: flex-start;
min-height: 46px; min-height: 50px;
border-top: 1px solid #e4dece; border-top: 1px solid #e7dfcf;
background: #fbf8ef; background: #fffef8;
box-shadow: inset 0 8px 16px rgba(44, 48, 42, 0.03);
} }
.footnote-strip strong { .footnote-strip strong {
@@ -605,6 +686,199 @@ textarea:focus-visible {
line-height: 1.45; line-height: 1.45;
} }
.book-tabs {
display: flex;
grid-column: 1 / -1;
gap: 3px;
align-items: flex-start;
padding: 0 16px 10px;
overflow-x: auto;
background: #ece9df;
}
.book-tabs button {
min-width: 44px;
height: 30px;
padding: 4px 7px;
border-color: #c8bea9;
border-top: 0;
border-radius: 0 0 7px 7px;
background: #efe4c9;
color: #4c422f;
font-size: 11px;
font-weight: 800;
box-shadow: 0 5px 10px rgba(44, 48, 42, 0.08);
}
.book-tabs button:nth-child(4n + 2) {
height: 34px;
}
.book-tabs button:nth-child(4n + 3) {
height: 27px;
}
.book-tabs button:hover,
.book-tabs button:focus-visible,
.book-tabs button.active {
background: #d6a84f;
color: #151510;
}
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 40;
display: grid;
place-items: center;
padding: 24px;
background: rgba(18, 21, 18, 0.46);
}
.modal-panel {
display: grid;
gap: 18px;
width: min(920px, 100%);
max-height: calc(100vh - 48px);
overflow: auto;
padding: 22px;
border: 1px solid #d4cdbd;
border-radius: 8px;
background: #fffef8;
box-shadow: 0 22px 60px rgba(18, 21, 18, 0.3);
}
.modal-panel > header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: start;
padding-bottom: 14px;
border-bottom: 1px solid #e7dfcf;
}
.modal-panel h2,
.modal-panel h3 {
margin: 0;
letter-spacing: 0;
}
.modal-panel h2 {
font-size: 24px;
}
.modal-panel h3 {
font-size: 16px;
}
.close-button {
background: #2c302a;
color: #fffef8;
}
.modal-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
}
.modal-section {
display: grid;
gap: 12px;
align-content: start;
padding: 16px;
border: 1px solid #e0d8c8;
border-radius: 8px;
background: #fbf8ef;
}
.modal-section label {
display: grid;
gap: 5px;
color: #657061;
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
}
.session-facts,
.about-list {
display: grid;
gap: 8px;
margin: 0;
}
.session-facts div,
.about-list div {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
gap: 12px;
}
.session-facts dt,
.about-list dt {
color: #657061;
font-size: 12px;
font-weight: 800;
}
.session-facts dd,
.about-list dd {
margin: 0;
color: #252821;
line-height: 1.45;
}
.resource-toggles {
display: grid;
gap: 8px;
}
.resource-toggles label {
display: flex;
gap: 9px;
align-items: center;
color: #252821;
font-size: 14px;
text-transform: none;
}
.resource-toggles input {
width: auto;
}
.resource-disabled {
margin: 0;
color: #657061;
font-size: 14px;
line-height: 1.5;
}
.about-modal {
width: min(760px, 100%);
}
.about-modal p {
margin: 0;
color: #3f443d;
line-height: 1.6;
}
.about-links {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.about-links a {
border-radius: 6px;
padding: 9px 11px;
background: #2f6f5e;
color: white;
font-weight: 800;
text-decoration: none;
}
.status-panel.error { .status-panel.error {
border-color: #b42318; border-color: #b42318;
color: #b42318; color: #b42318;
+404 -33
View File
@@ -25,6 +25,10 @@ type TimelineEntry = {
type StudySession = { type StudySession = {
id: string id: string
title: string title: string
baseTranslation: string
footnotesEnabled: boolean
strongsEnabled: boolean
timelineEnabled: boolean
createdAt: string createdAt: string
updatedAt: string updatedAt: string
selectedId: number | null selectedId: number | null
@@ -49,6 +53,10 @@ function createSession(title = 'Study Session'): StudySession {
return { return {
id, id,
title, title,
baseTranslation: 'kjv-eng-kjv2006',
footnotesEnabled: true,
strongsEnabled: true,
timelineEnabled: true,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
selectedId: null, selectedId: null,
@@ -68,7 +76,83 @@ function formatTimelineTime(value: string) {
return new Date(value).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) return new Date(value).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
} }
function abbreviateBook(book: string) {
const abbreviations: Record<string, string> = {
Genesis: 'Gen',
Exodus: 'Ex',
Leviticus: 'Lev',
Numbers: 'Num',
Deuteronomy: 'Deut',
Joshua: 'Josh',
Judges: 'Judg',
Ruth: 'Ruth',
'1 Samuel': '1 Sam',
'2 Samuel': '2 Sam',
'1 Kings': '1 Kgs',
'2 Kings': '2 Kgs',
'1 Chronicles': '1 Chr',
'2 Chronicles': '2 Chr',
Ezra: 'Ezra',
Nehemiah: 'Neh',
Esther: 'Est',
Job: 'Job',
Psalms: 'Ps',
Proverbs: 'Prov',
Ecclesiastes: 'Eccl',
'Song of Solomon': 'Song',
Isaiah: 'Isa',
Jeremiah: 'Jer',
Lamentations: 'Lam',
Ezekiel: 'Ezek',
Daniel: 'Dan',
Hosea: 'Hos',
Joel: 'Joel',
Amos: 'Amos',
Obadiah: 'Ob',
Jonah: 'Jon',
Micah: 'Mic',
Nahum: 'Nah',
Habakkuk: 'Hab',
Zephaniah: 'Zeph',
Haggai: 'Hag',
Zechariah: 'Zech',
Malachi: 'Mal',
Matthew: 'Matt',
Mark: 'Mark',
Luke: 'Luke',
John: 'John',
Acts: 'Acts',
Romans: 'Rom',
'1 Corinthians': '1 Cor',
'2 Corinthians': '2 Cor',
Galatians: 'Gal',
Ephesians: 'Eph',
Philippians: 'Phil',
Colossians: 'Col',
'1 Thessalonians': '1 Th',
'2 Thessalonians': '2 Th',
'1 Timothy': '1 Tim',
'2 Timothy': '2 Tim',
Titus: 'Titus',
Philemon: 'Phlm',
Hebrews: 'Heb',
James: 'Jas',
'1 Peter': '1 Pet',
'2 Peter': '2 Pet',
'1 John': '1 Jn',
'2 John': '2 Jn',
'3 John': '3 Jn',
Jude: 'Jude',
Revelation: 'Rev',
}
return abbreviations[book] ?? book.slice(0, 4)
}
function App() { function App() {
const [railOpen, setRailOpen] = useState(false)
const [sessionModalOpen, setSessionModalOpen] = useState(false)
const [aboutModalOpen, setAboutModalOpen] = useState(false)
const [viewMode, setViewMode] = useState<ViewMode>('notes') const [viewMode, setViewMode] = useState<ViewMode>('notes')
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const [libraryVerses, setLibraryVerses] = useState<Verse[]>([]) const [libraryVerses, setLibraryVerses] = useState<Verse[]>([])
@@ -81,6 +165,10 @@ function App() {
const [noteDraft, setNoteDraft] = useState('') const [noteDraft, setNoteDraft] = useState('')
const [sessions, setSessions] = useState<StudySession[]>([]) const [sessions, setSessions] = useState<StudySession[]>([])
const [activeSessionId, setActiveSessionId] = useState('') const [activeSessionId, setActiveSessionId] = useState('')
const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study')
const [newSessionFootnotes, setNewSessionFootnotes] = useState(true)
const [newSessionStrongs, setNewSessionStrongs] = useState(true)
const [newSessionTimeline, setNewSessionTimeline] = useState(true)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const restoredSessionId = useRef('') const restoredSessionId = useRef('')
@@ -125,7 +213,10 @@ function App() {
noteDraft, noteDraft,
...patch, ...patch,
updatedAt: at, updatedAt: at,
timeline: [nextEntry, ...session.timeline].slice(0, timelineLimit), timeline:
session.timelineEnabled === false
? session.timeline
: [nextEntry, ...session.timeline].slice(0, timelineLimit),
} }
: session, : session,
), ),
@@ -188,6 +279,15 @@ function App() {
nextSessions = [createSession('KJV Study Session')] nextSessions = [createSession('KJV Study Session')]
} }
nextSessions = nextSessions.map((session) => ({
...createSession(session.title),
...session,
baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006',
footnotesEnabled: session.footnotesEnabled ?? true,
strongsEnabled: session.strongsEnabled ?? true,
timelineEnabled: session.timelineEnabled ?? true,
}))
setSessions(nextSessions) setSessions(nextSessions)
setActiveSessionId( setActiveSessionId(
nextSessions.some((session) => session.id === savedActiveSessionId) nextSessions.some((session) => session.id === savedActiveSessionId)
@@ -444,9 +544,38 @@ function App() {
} }
function startNewSession() { function startNewSession() {
const nextSession = createSession(`Study Session ${sessions.length + 1}`) const nextSession = {
...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`),
footnotesEnabled: newSessionFootnotes,
strongsEnabled: newSessionStrongs,
timelineEnabled: newSessionTimeline,
}
setSessions((current) => [nextSession, ...current]) setSessions((current) => [nextSession, ...current])
setActiveSessionId(nextSession.id) setActiveSessionId(nextSession.id)
setSessionModalOpen(false)
}
function setActiveSessionOption(patch: Partial<StudySession>) {
patchActiveSession(patch)
if (patch.strongsEnabled === false) {
setStrongDisplay('off')
}
if (typeof patch.strongsEnabled === 'boolean' || typeof patch.footnotesEnabled === 'boolean') {
recordTimeline({ type: 'setting', label: 'Updated session resources' }, patch)
}
}
function exportActiveSession() {
if (!activeSession) return
const blob = new Blob([JSON.stringify(activeSession, null, 2)], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${activeSession.title.toLowerCase().replace(/[^a-z0-9]+/g, '-') || 'study-session'}.json`
link.click()
URL.revokeObjectURL(url)
} }
function restoreTimelineEntry(entry: TimelineEntry) { function restoreTimelineEntry(entry: TimelineEntry) {
@@ -467,7 +596,7 @@ function App() {
} }
function renderVerseText(verse: Verse) { function renderVerseText(verse: Verse) {
if (!verse.strongs.length || strongDisplay === 'off') return verse.text if (!activeSession?.strongsEnabled || !verse.strongs.length || strongDisplay === 'off') return verse.text
const parts: ReactNode[] = [] const parts: ReactNode[] = []
let cursor = 0 let cursor = 0
@@ -597,9 +726,13 @@ function App() {
<aside className="study-margin" aria-label="Study margin"> <aside className="study-margin" aria-label="Study margin">
<div className="panel-header"> <div className="panel-header">
<h2>Study Margin</h2> <h2>Study Margin</h2>
<span>{selectedVerse.strongs.length} links</span> <span>{activeSession?.strongsEnabled === false ? 'disabled' : `${selectedVerse.strongs.length} links`}</span>
</div> </div>
<p className="margin-reference">{reference(selectedVerse)}</p> <p className="margin-reference">{reference(selectedVerse)}</p>
{activeSession?.strongsEnabled === false ? (
<p className="resource-disabled">Strong's links are disabled for this session.</p>
) : (
<>
{renderOriginalLanguageLine()} {renderOriginalLanguageLine()}
<div className="linked-word-list" aria-label="Linked Strong's words"> <div className="linked-word-list" aria-label="Linked Strong's words">
{selectedVerse.strongs.map((entry) => ( {selectedVerse.strongs.map((entry) => (
@@ -629,11 +762,22 @@ function App() {
</button> </button>
</article> </article>
) : null} ) : null}
</>
)}
</aside> </aside>
) )
} }
function renderFootnoteStrip() { function renderFootnoteStrip() {
if (activeSession?.footnotesEnabled === false) {
return (
<section className="footnote-strip">
<strong>Footnotes</strong>
<span>Footnotes are disabled for this session.</span>
</section>
)
}
const visibleFootnotes = selectedFootnotes.length ? selectedFootnotes : pageFootnotes.slice(0, 4) const visibleFootnotes = selectedFootnotes.length ? selectedFootnotes : pageFootnotes.slice(0, 4)
return ( return (
<section className="footnote-strip"> <section className="footnote-strip">
@@ -665,14 +809,261 @@ function App() {
) )
} }
function renderBookTabs() {
if (!selectedVerse) return null
return ( return (
<main className="app-shell"> <nav className="book-tabs" aria-label="Bible book tabs">
<aside className="tool-rail" aria-label="Main tools"> {books.map((book) => (
<button
key={book}
type="button"
className={book === selectedVerse.book ? 'active' : ''}
title={book}
onClick={() => jumpTo(book, 1, 1)}
>
{abbreviateBook(book)}
</button>
))}
</nav>
)
}
function renderSessionModal() {
if (!sessionModalOpen) return null
return (
<div className="modal-backdrop" role="presentation" onMouseDown={() => setSessionModalOpen(false)}>
<section
className="modal-panel session-modal"
role="dialog"
aria-modal="true"
aria-label="Session management"
onMouseDown={(event) => event.stopPropagation()}
>
<header>
<div>
<span className="eyebrow">Session</span>
<h2>Manage Study Session</h2>
</div>
<button type="button" className="close-button" onClick={() => setSessionModalOpen(false)}>
Close
</button>
</header>
<div className="modal-grid">
<section className="modal-section">
<h3>Current Session</h3>
<label>
Active session
<select value={activeSessionId} onChange={(event) => setActiveSessionId(event.target.value)}>
{sessions.map((session) => (
<option key={session.id} value={session.id}>
{session.title}
</option>
))}
</select>
</label>
<label>
Session title
<input
value={activeSession?.title ?? ''}
onChange={(event) => renameActiveSession(event.target.value)}
/>
</label>
<dl className="session-facts">
<div>
<dt>Translation</dt>
<dd>KJV</dd>
</div>
<div>
<dt>Current page</dt>
<dd>{pageRange}</dd>
</div>
<div>
<dt>Timeline</dt>
<dd>{activeSession?.timeline.length ?? 0} events</dd>
</div>
<div>
<dt>Started</dt>
<dd>{activeSession ? new Date(activeSession.createdAt).toLocaleString() : 'Not started'}</dd>
</div>
</dl>
<div className="resource-toggles">
<label>
<input
type="checkbox"
checked={activeSession?.strongsEnabled ?? true}
onChange={(event) => setActiveSessionOption({ strongsEnabled: event.target.checked })}
/>
Strong's links
</label>
<label>
<input
type="checkbox"
checked={activeSession?.footnotesEnabled ?? true}
onChange={(event) => setActiveSessionOption({ footnotesEnabled: event.target.checked })}
/>
Footnotes
</label>
<label>
<input
type="checkbox"
checked={activeSession?.timelineEnabled ?? true}
onChange={(event) => setActiveSessionOption({ timelineEnabled: event.target.checked })}
/>
Record timeline
</label>
</div>
<button type="button" onClick={exportActiveSession}>
Export session
</button>
</section>
<section className="modal-section">
<h3>New Session</h3>
<label>
Session title
<input value={newSessionTitle} onChange={(event) => setNewSessionTitle(event.target.value)} />
</label>
<label>
Base translation
<select value="kjv-eng-kjv2006" onChange={() => null}>
<option value="kjv-eng-kjv2006">King James Version</option>
</select>
</label>
<div className="resource-toggles">
<label>
<input
type="checkbox"
checked={newSessionStrongs}
onChange={(event) => setNewSessionStrongs(event.target.checked)}
/>
Include Strong's links
</label>
<label>
<input
type="checkbox"
checked={newSessionFootnotes}
onChange={(event) => setNewSessionFootnotes(event.target.checked)}
/>
Include footnotes
</label>
<label>
<input
type="checkbox"
checked={newSessionTimeline}
onChange={(event) => setNewSessionTimeline(event.target.checked)}
/>
Record timeline
</label>
</div>
<button type="button" onClick={startNewSession}>
Create session
</button>
</section>
</div>
</section>
</div>
)
}
function renderAboutModal() {
if (!aboutModalOpen) return null
return (
<div className="modal-backdrop" role="presentation" onMouseDown={() => setAboutModalOpen(false)}>
<section
className="modal-panel about-modal"
role="dialog"
aria-modal="true"
aria-label="About Libre Study"
onMouseDown={(event) => event.stopPropagation()}
>
<header>
<div>
<span className="eyebrow">About</span>
<h2>Libre Study</h2>
</div>
<button type="button" className="close-button" onClick={() => setAboutModalOpen(false)}>
Close
</button>
</header>
<p>
Libre Study is a free Bible study application built by Christ IT. The project
started in July 2026 as an open study tool focused on readable Bible pages,
linked study data, notes, and reusable public Bible packages.
</p>
<dl className="about-list">
<div>
<dt>Project</dt>
<dd>Christ IT / Libre Study</dd>
</div>
<div>
<dt>Current Bible package</dt>
<dd>King James Version, public domain KJV text package with Strong's-linked study data.</dd>
</div>
<div>
<dt>Software stack</dt>
<dd>Tauri, TypeScript, React, SQLite, Rust, Vite.</dd>
</div>
<div>
<dt>Source data catalog</dt>
<dd>LibreBible packages are maintained separately so this app and future sites can consume the same resources.</dd>
</div>
</dl>
<div className="about-links">
<a href="https://christit.com">ChristIT.com</a>
<a href="https://git.christit.com/libre-study/libre-study">Libre Study Git</a>
<a href="https://git.christit.com/libre-study/libre-bible-data">LibreBible Data Git</a>
</div>
</section>
</div>
)
}
return (
<main className={railOpen ? 'app-shell menu-open' : 'app-shell'}>
<aside
className="tool-rail"
aria-label="Main tools"
onMouseEnter={() => setRailOpen(true)}
onMouseLeave={() => setRailOpen(false)}
onFocus={() => setRailOpen(true)}
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) setRailOpen(false)
}}
>
<button type="button" className="menu-button" title="Menu" onClick={() => setRailOpen((current) => !current)}>
<span />
<span />
<span />
</button>
<span className="brand-mark">LS</span> <span className="brand-mark">LS</span>
<button type="button" title="Bible page" onClick={() => setWorkspaceView('page')}>B</button> <button type="button" title="Bible page" onClick={() => setWorkspaceView('page')}>
<button type="button" title="Study margin" onClick={() => setWorkspaceView('study')}>M</button> <span className="rail-icon">B</span>
<button type="button" title="Notes workspace" onClick={() => setWorkspaceView('notes')}>N</button> <span className="rail-copy"><strong>Bible</strong><small>Page-only reading</small></span>
<button type="button" title="Compare translations" onClick={() => setWorkspaceView('compare')}>C</button> </button>
<button type="button" title="Study margin" onClick={() => setWorkspaceView('study')}>
<span className="rail-icon">M</span>
<span className="rail-copy"><strong>Margin</strong><small>References and linked words</small></span>
</button>
<button type="button" title="Notes workspace" onClick={() => setWorkspaceView('notes')}>
<span className="rail-icon">N</span>
<span className="rail-copy"><strong>Notes</strong><small>Study notes and timeline</small></span>
</button>
<button type="button" title="Compare translations" onClick={() => setWorkspaceView('compare')}>
<span className="rail-icon">C</span>
<span className="rail-copy"><strong>Compare</strong><small>Translation comparison</small></span>
</button>
<button type="button" title="Session management" onClick={() => setSessionModalOpen(true)}>
<span className="rail-icon">S</span>
<span className="rail-copy"><strong>Sessions</strong><small>Save, export, and configure</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>
</button>
</aside> </aside>
<section className="reader"> <section className="reader">
@@ -797,6 +1188,7 @@ function App() {
{renderBiblePage('KJV')} {renderBiblePage('KJV')}
{viewMode !== 'page' ? renderStudyMargin() : null} {viewMode !== 'page' ? renderStudyMargin() : null}
{renderFootnoteStrip()} {renderFootnoteStrip()}
{renderBookTabs()}
</div> </div>
)} )}
@@ -806,29 +1198,6 @@ function App() {
<span className="eyebrow">Notes</span> <span className="eyebrow">Notes</span>
<h2>Study Notes</h2> <h2>Study Notes</h2>
</header> </header>
<div className="session-manager" aria-label="Study session manager">
<label>
Session
<select
value={activeSessionId}
onChange={(event) => setActiveSessionId(event.target.value)}
>
{sessions.map((session) => (
<option key={session.id} value={session.id}>
{session.title}
</option>
))}
</select>
</label>
<input
value={activeSession?.title ?? ''}
onChange={(event) => renameActiveSession(event.target.value)}
placeholder="Session title"
/>
<button type="button" onClick={startNewSession}>
New
</button>
</div>
<textarea <textarea
value={noteDraft} value={noteDraft}
onChange={(event) => updateNoteDraft(event.target.value)} onChange={(event) => updateNoteDraft(event.target.value)}
@@ -889,6 +1258,8 @@ function App() {
<div className="status-panel">No verses matched that search.</div> <div className="status-panel">No verses matched that search.</div>
)} )}
</section> </section>
{renderSessionModal()}
{renderAboutModal()}
</main> </main>
) )
} }