From ea22ffd2b9446afb78c8c9a285702bf9d92e1e85 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Jul 2026 09:38:51 -0500 Subject: [PATCH] Add session modal and book tabs --- package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/App.css | 282 ++++++++++++++++++++++- src/App.tsx | 469 ++++++++++++++++++++++++++++++++++---- 7 files changed, 704 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index dbbeeee..03af4a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-study", - "version": "0.1.8", + "version": "0.1.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-study", - "version": "0.1.8", + "version": "0.1.9", "dependencies": { "@tauri-apps/api": "^2.11.1", "react": "^19.2.7", diff --git a/package.json b/package.json index 8925fb1..2b3fa14 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libre-study", "private": true, - "version": "0.1.8", + "version": "0.1.9", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index af277ea..5c4f06d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.8" +version = "0.1.9" dependencies = [ "log", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5ebbf46..ce568a8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.8" +version = "0.1.9" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bac8030..e3d3758 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Libre Study", - "version": "0.1.8", + "version": "0.1.9", "identifier": "org.librestudy.desktop", "build": { "frontendDist": "../dist", diff --git a/src/App.css b/src/App.css index e987b4c..cfc609d 100644 --- a/src/App.css +++ b/src/App.css @@ -6,14 +6,31 @@ color: #202124; } +.app-shell.menu-open .reader { + filter: brightness(0.72); +} + .tool-rail { display: flex; flex-direction: column; gap: 12px; align-items: center; + position: relative; + z-index: 20; + width: 58px; padding: 18px 10px; background: #1f231f; 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 { @@ -29,12 +46,74 @@ } .tool-rail button { + display: flex; + gap: 12px; + align-items: center; width: 38px; height: 38px; padding: 0; border-color: #3c4239; background: #2c302a; 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 { @@ -45,6 +124,7 @@ height: 100vh; padding: 18px; box-sizing: border-box; + transition: filter 160ms ease; } .top-bar { @@ -172,7 +252,7 @@ textarea:focus-visible { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; min-height: 0; - overflow: hidden; + overflow: visible; } .page-system.study, @@ -494,9 +574,10 @@ textarea:focus-visible { grid-column: 1 / -1; gap: 12px; align-items: flex-start; - min-height: 46px; - border-top: 1px solid #e4dece; - background: #fbf8ef; + min-height: 50px; + border-top: 1px solid #e7dfcf; + background: #fffef8; + box-shadow: inset 0 8px 16px rgba(44, 48, 42, 0.03); } .footnote-strip strong { @@ -605,6 +686,199 @@ textarea:focus-visible { 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 { border-color: #b42318; color: #b42318; diff --git a/src/App.tsx b/src/App.tsx index 0a200a1..0f08da3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,10 @@ type TimelineEntry = { type StudySession = { id: string title: string + baseTranslation: string + footnotesEnabled: boolean + strongsEnabled: boolean + timelineEnabled: boolean createdAt: string updatedAt: string selectedId: number | null @@ -49,6 +53,10 @@ function createSession(title = 'Study Session'): StudySession { return { id, title, + baseTranslation: 'kjv-eng-kjv2006', + footnotesEnabled: true, + strongsEnabled: true, + timelineEnabled: true, createdAt: now, updatedAt: now, selectedId: null, @@ -68,7 +76,83 @@ function formatTimelineTime(value: string) { return new Date(value).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) } +function abbreviateBook(book: string) { + const abbreviations: Record = { + 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() { + const [railOpen, setRailOpen] = useState(false) + const [sessionModalOpen, setSessionModalOpen] = useState(false) + const [aboutModalOpen, setAboutModalOpen] = useState(false) const [viewMode, setViewMode] = useState('notes') const [query, setQuery] = useState('') const [libraryVerses, setLibraryVerses] = useState([]) @@ -81,6 +165,10 @@ function App() { const [noteDraft, setNoteDraft] = useState('') const [sessions, setSessions] = 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 [error, setError] = useState(null) const restoredSessionId = useRef('') @@ -125,7 +213,10 @@ function App() { noteDraft, ...patch, updatedAt: at, - timeline: [nextEntry, ...session.timeline].slice(0, timelineLimit), + timeline: + session.timelineEnabled === false + ? session.timeline + : [nextEntry, ...session.timeline].slice(0, timelineLimit), } : session, ), @@ -188,6 +279,15 @@ function App() { 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) setActiveSessionId( nextSessions.some((session) => session.id === savedActiveSessionId) @@ -444,9 +544,38 @@ function App() { } 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]) setActiveSessionId(nextSession.id) + setSessionModalOpen(false) + } + + function setActiveSessionOption(patch: Partial) { + 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) { @@ -467,7 +596,7 @@ function App() { } 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[] = [] let cursor = 0 @@ -597,11 +726,15 @@ function App() { ) } function renderFootnoteStrip() { + if (activeSession?.footnotesEnabled === false) { + return ( +
+ Footnotes + Footnotes are disabled for this session. +
+ ) + } + const visibleFootnotes = selectedFootnotes.length ? selectedFootnotes : pageFootnotes.slice(0, 4) return (
@@ -665,14 +809,261 @@ function App() { ) } + function renderBookTabs() { + if (!selectedVerse) return null + + return ( + + ) + } + + function renderSessionModal() { + if (!sessionModalOpen) return null + + return ( +
setSessionModalOpen(false)}> +
event.stopPropagation()} + > +
+
+ Session +

Manage Study Session

+
+ +
+ +
+
+

Current Session

+ + +
+
+
Translation
+
KJV
+
+
+
Current page
+
{pageRange}
+
+
+
Timeline
+
{activeSession?.timeline.length ?? 0} events
+
+
+
Started
+
{activeSession ? new Date(activeSession.createdAt).toLocaleString() : 'Not started'}
+
+
+
+ + + +
+ +
+ +
+

New Session

+ + +
+ + + +
+ +
+
+
+
+ ) + } + + function renderAboutModal() { + if (!aboutModalOpen) return null + + return ( +
setAboutModalOpen(false)}> +
event.stopPropagation()} + > +
+
+ About +

Libre Study

+
+ +
+

+ 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. +

+
+
+
Project
+
Christ IT / Libre Study
+
+
+
Current Bible package
+
King James Version, public domain KJV text package with Strong's-linked study data.
+
+
+
Software stack
+
Tauri, TypeScript, React, SQLite, Rust, Vite.
+
+
+
Source data catalog
+
LibreBible packages are maintained separately so this app and future sites can consume the same resources.
+
+
+ +
+
+ ) + } + return ( -
-