diff --git a/CHANGELOG.md b/CHANGELOG.md index 7552718..73c8356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.1.4 - 2026-07-12 + +- Attached the study margin and footnote area to the Bible page surface. +- Added translation, book, chapter, and verse selectors above the page. +- Added a compare workspace placeholder for side-by-side translations once a second translation is loaded. +- Kept notes as a distinct workspace panel beside the page. + ## 0.1.3 - 2026-07-12 - Phase 1 page-reader prototype: replaced the sidebar-first app with a Bible-page study desk. diff --git a/package-lock.json b/package-lock.json index 07fa1ea..65f53f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-study", - "version": "0.1.3", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-study", - "version": "0.1.3", + "version": "0.1.4", "dependencies": { "@tauri-apps/api": "^2.11.1", "react": "^19.2.7", diff --git a/package.json b/package.json index 498afd6..e9f6b7c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libre-study", "private": true, - "version": "0.1.3", + "version": "0.1.4", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a874e2c..741069a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.3" +version = "0.1.4" dependencies = [ "log", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3bdd8c5..e25f27d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.3" +version = "0.1.4" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 77c6990..d334f4b 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.3", + "version": "0.1.4", "identifier": "org.librestudy.desktop", "build": { "frontendDist": "../dist", diff --git a/src/App.css b/src/App.css index 9db7a71..2bfda4c 100644 --- a/src/App.css +++ b/src/App.css @@ -39,7 +39,7 @@ .reader { display: grid; - grid-template-rows: auto minmax(0, 1fr) auto; + grid-template-rows: auto minmax(0, 1fr); gap: 16px; min-width: 0; height: 100vh; @@ -49,8 +49,8 @@ .top-bar { display: grid; - grid-template-columns: minmax(260px, 1fr) minmax(320px, 520px); - gap: 18px; + grid-template-columns: minmax(260px, 0.8fr) minmax(420px, 1.1fr) minmax(300px, 0.65fr); + gap: 14px; align-items: end; } @@ -83,6 +83,7 @@ } input, +select, textarea { width: 100%; min-width: 0; @@ -116,35 +117,49 @@ button { button:focus-visible, input:focus-visible, +select:focus-visible, textarea:focus-visible { outline: 2px solid #5d7f71; outline-offset: 2px; } +.page-controls { + display: grid; + grid-template-columns: minmax(130px, 1.1fr) minmax(130px, 1.3fr) minmax(86px, 0.7fr) minmax(78px, 0.6fr); + gap: 8px; + align-items: end; +} + +.page-controls label { + display: grid; + gap: 4px; + color: #657061; + font-size: 11px; + font-weight: 800; + letter-spacing: 0; + text-transform: uppercase; +} + .study-desk { display: grid; - grid-template-columns: minmax(520px, 1.3fr) minmax(220px, 0.38fr) minmax(320px, 0.7fr); + grid-template-columns: minmax(720px, 1fr); gap: 16px; min-height: 0; } -.study-desk.page { - grid-template-columns: minmax(680px, 980px); +.study-desk.page, +.study-desk.study, +.study-desk.compare { + grid-template-columns: minmax(680px, 1160px); justify-content: center; } -.study-desk.study { - grid-template-columns: minmax(560px, 1fr) minmax(260px, 360px); -} - .study-desk.notes { - grid-template-columns: minmax(520px, 1.3fr) minmax(220px, 0.38fr) minmax(320px, 0.7fr); + grid-template-columns: minmax(760px, 1.42fr) minmax(320px, 0.58fr); } -.bible-page, -.study-margin, +.page-system, .notes-workspace, -.footnote-strip, .status-panel { border: 1px solid #d4cdbd; border-radius: 8px; @@ -152,6 +167,23 @@ textarea:focus-visible { box-shadow: 0 12px 30px rgba(44, 48, 42, 0.08); } +.page-system { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) auto; + min-height: 0; + overflow: hidden; +} + +.page-system.study, +.page-system.notes { + grid-template-columns: minmax(0, 1fr) minmax(250px, 330px); +} + +.page-system.compare-system { + display: block; +} + .bible-page { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; @@ -222,6 +254,11 @@ textarea:focus-visible { padding: 16px; } +.study-margin { + border-left: 1px solid #e4dece; + background: #fbf8ef; +} + .panel-header { display: flex; justify-content: space-between; @@ -347,9 +384,12 @@ textarea:focus-visible { .footnote-strip { display: flex; + grid-column: 1 / -1; gap: 12px; align-items: center; min-height: 46px; + border-top: 1px solid #e4dece; + background: #fbf8ef; } .footnote-strip span { @@ -361,11 +401,57 @@ textarea:focus-visible { color: #b42318; } +.compare-pages { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + min-height: 0; +} + +.compare-pages .bible-page + .bible-page { + border-left: 1px solid #e4dece; +} + +.compare-placeholder { + align-content: start; + gap: 24px; + background: #fbf8ef; +} + +.compare-placeholder h2 { + margin: 0 0 10px; + color: #252821; + font-size: 22px; + letter-spacing: 0; +} + +.compare-placeholder p { + max-width: 36rem; + margin: 0; + color: #657061; + font-size: 15px; + line-height: 1.55; +} + @media (max-width: 1200px) { - .study-desk { + .study-desk, + .study-desk.notes, + .study-desk.study, + .study-desk.compare { grid-template-columns: minmax(0, 1fr); } + .page-system.study, + .page-system.notes, + .compare-pages { + grid-template-columns: minmax(0, 1fr); + } + + .study-margin, + .compare-pages .bible-page + .bible-page { + border-top: 1px solid #e4dece; + border-left: 0; + } + .reader { height: auto; min-height: 100vh; @@ -390,6 +476,10 @@ textarea:focus-visible { grid-template-columns: 1fr; } + .page-controls { + grid-template-columns: 1fr 1fr; + } + .page-columns { column-count: 1; } diff --git a/src/App.tsx b/src/App.tsx index 7d476aa..fab2b17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,14 +4,17 @@ import type { Verse } from './types' import './App.css' const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7'] +const pageSize = 34 +type ViewMode = 'page' | 'study' | 'notes' | 'compare' function reference(verse: Verse) { return `${verse.book} ${verse.chapter}:${verse.verse}` } function App() { - const [viewMode, setViewMode] = useState<'page' | 'study' | 'notes'>('notes') + const [viewMode, setViewMode] = useState('notes') const [query, setQuery] = useState('') + const [libraryVerses, setLibraryVerses] = useState([]) const [verses, setVerses] = useState([]) const [selectedId, setSelectedId] = useState(null) const [selectedStrong, setSelectedStrong] = useState(null) @@ -23,6 +26,7 @@ function App() { async function loadLibrary() { setError(null) const state = await getLibraryState() + setLibraryVerses(state.verses) setVerses(state.verses) setSelectedId((current) => current ?? state.verses[0]?.id ?? null) } @@ -43,6 +47,9 @@ function App() { const color = tagColors[Math.abs(label.length) % tagColors.length] const updated = await addTagToVerse(selectedId, label, color) + setLibraryVerses((current) => + current.map((verse) => (verse.id === updated.id ? updated : verse)), + ) setVerses((current) => current.map((verse) => (verse.id === updated.id ? updated : verse)), ) @@ -56,8 +63,11 @@ function App() { }, []) const selectedVerse = useMemo( - () => verses.find((verse) => verse.id === selectedId) ?? verses[0], - [selectedId, verses], + () => + libraryVerses.find((verse) => verse.id === selectedId) ?? + verses.find((verse) => verse.id === selectedId) ?? + verses[0], + [libraryVerses, selectedId, verses], ) const selectedStrongEntry = useMemo(() => { if (!selectedVerse?.strongs.length) return null @@ -68,13 +78,37 @@ function App() { }, [selectedStrong, selectedVerse]) const pageVerses = useMemo(() => { if (!selectedVerse) return [] - const selectedIndex = verses.findIndex((verse) => verse.id === selectedVerse.id) + const source = libraryVerses.length ? libraryVerses : verses + const selectedIndex = source.findIndex((verse) => verse.id === selectedVerse.id) const start = Math.max(0, selectedIndex) - return verses.slice(start, start + 34) - }, [selectedVerse, verses]) + return source.slice(start, start + pageSize) + }, [libraryVerses, selectedVerse, verses]) const pageRange = pageVerses.length ? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}` : 'No page loaded' + const books = useMemo( + () => Array.from(new Set(libraryVerses.map((verse) => verse.book))), + [libraryVerses], + ) + const chapters = useMemo(() => { + if (!selectedVerse) return [] + return Array.from( + new Set( + libraryVerses + .filter((verse) => verse.book === selectedVerse.book) + .map((verse) => verse.chapter), + ), + ) + }, [libraryVerses, selectedVerse]) + const verseNumbers = useMemo(() => { + if (!selectedVerse) return [] + return libraryVerses + .filter( + (verse) => + verse.book === selectedVerse.book && verse.chapter === selectedVerse.chapter, + ) + .map((verse) => verse.verse) + }, [libraryVerses, selectedVerse]) function searchStrong(strong: string) { setQuery(strong) @@ -82,6 +116,107 @@ function App() { runSearch(strong).catch((reason) => setError(String(reason))) } + function jumpTo(book: string, chapter: number, verse: number) { + const next = + libraryVerses.find( + (entry) => + entry.book === book && entry.chapter === chapter && entry.verse === verse, + ) ?? + libraryVerses.find((entry) => entry.book === book && entry.chapter === chapter) ?? + libraryVerses.find((entry) => entry.book === book) + + if (!next) return + setQuery('') + setSelectedStrong(null) + setVerses(libraryVerses) + setSelectedId(next.id) + } + + function shiftPage(direction: -1 | 1) { + if (!selectedVerse) return + const source = libraryVerses.length ? libraryVerses : verses + const index = source.findIndex((verse) => verse.id === selectedVerse.id) + setSelectedId( + source[Math.min(source.length - 1, Math.max(0, index + direction * pageSize))] + ?.id ?? selectedVerse.id, + ) + } + + function renderBiblePage(label = 'KJV') { + if (!selectedVerse) return null + return ( +
+
+ {label} + {reference(selectedVerse)} + {verses.length} verses loaded +
+
+ {pageVerses.map((verse) => ( +

setSelectedId(verse.id)} + > + {verse.verse} + {verse.text} +

+ ))} +
+
+ + {pageRange} + +
+
+ ) + } + + function renderStudyMargin() { + if (!selectedVerse) return null + return ( + + ) + } + return (
@@ -103,6 +239,61 @@ function App() { King James Version

{pageRange}

+
+ + + + +
{ @@ -121,84 +312,35 @@ function App() {
-
-
- KJV - {reference(selectedVerse)} - {verses.length} verses loaded -
-
- {pageVerses.map((verse) => ( -

setSelectedId(verse.id)} - > - {verse.verse} - {verse.text} -

- ))} + {viewMode === 'compare' ? ( +
+
+ {renderBiblePage('KJV')} +
+
+ Second Translation + Not loaded +
+
+

Add another translation to compare side by side.

+

+ This pane is reserved for the next LibreBible translation package. + It will use the same book, chapter, and verse position as the KJV page. +

+
+
+
-
- - {pageRange} - -
-
- - {viewMode !== 'page' ? ( - - ) : null} + )} {viewMode === 'notes' ? ( ) : null}
- -
- Footnotes - Dynamic translation notes and cross references will appear here. -
) : (
No verses matched that search.