Add session modal and book tabs
This commit is contained in:
Generated
+2
-2
@@ -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
@@ -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",
|
||||||
|
|||||||
Generated
+1
-1
@@ -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,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,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
@@ -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
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user