From 768a74c09c4791b59a4cceef09cfa19bc5baf9e1 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 12 Jul 2026 10:25:22 -0500 Subject: [PATCH] Wire dynamic commentary and dictionary margin resources --- 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 | 52 ++++++++ src/App.tsx | 267 +++++++++++++++++++++++++++++++++++++- src/studyClient.ts | 139 +++++++++++++++++++- src/types.ts | 34 +++++ 9 files changed, 493 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index de7fae4..83918c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-study", - "version": "0.1.14", + "version": "0.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-study", - "version": "0.1.14", + "version": "0.1.15", "dependencies": { "@tauri-apps/api": "^2.11.1", "react": "^19.2.7", diff --git a/package.json b/package.json index ba67df3..c8f97c6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libre-study", "private": true, - "version": "0.1.14", + "version": "0.1.15", "type": "module", "scripts": { "dev": "vite --host 127.0.0.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fa92be1..131c688 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.14" +version = "0.1.15" dependencies = [ "log", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7cc9a59..99b03dd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.14" +version = "0.1.15" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0b1e343..11bef3a 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.14", + "version": "0.1.15", "identifier": "org.librestudy.desktop", "build": { "frontendDist": "../dist", diff --git a/src/App.css b/src/App.css index 9c37e01..812e712 100644 --- a/src/App.css +++ b/src/App.css @@ -635,6 +635,58 @@ textarea:focus-visible { color: #ffffff; } +.resource-card { + display: grid; + gap: 9px; + margin-top: 14px; + padding: 12px; + border: 1px solid #dfd6c2; + border-radius: 6px; + background: #fffdf6; +} + +.commentary-list, +.dictionary-list { + display: grid; + gap: 12px; +} + +.commentary-list section, +.dictionary-list section { + display: grid; + gap: 5px; +} + +.commentary-list h3, +.dictionary-list h3 { + margin: 0; + color: #252821; + font-size: 14px; + letter-spacing: 0; +} + +.commentary-list p, +.dictionary-list p { + margin: 0; + color: #3f443d; + font-size: 13px; + line-height: 1.48; +} + +.commentary-list small { + color: #657061; + font-size: 11px; + font-weight: 800; +} + +.dictionary-list p strong { + display: block; + margin-bottom: 2px; + color: #657061; + font-size: 11px; + text-transform: uppercase; +} + .notes-workspace { display: flex; flex-direction: column; diff --git a/src/App.tsx b/src/App.tsx index 62e7159..53f5502 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,13 @@ import { useEffect, useMemo, useRef, useState } from 'react' -import { addTagToVerse, getLibraryState, searchVerses } from './studyClient' +import { + addTagToVerse, + getCommentaryForVerse, + getDictionaryEntriesForTerms, + getLibraryState, + searchVerses, +} from './studyClient' import type { ReactNode } from 'react' -import type { Verse } from './types' +import type { CommentaryEntry, DictionaryEntry, Verse } from './types' import './App.css' const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7'] @@ -30,6 +36,8 @@ type StudySession = { baseTranslation: string footnotesEnabled: boolean strongsEnabled: boolean + commentaryEnabled: boolean + dictionaryEnabled: boolean timelineEnabled: boolean createdAt: string updatedAt: string @@ -58,6 +66,8 @@ function createSession(title = 'Study Session'): StudySession { baseTranslation: 'kjv-eng-kjv2006', footnotesEnabled: true, strongsEnabled: true, + commentaryEnabled: true, + dictionaryEnabled: true, timelineEnabled: true, createdAt: now, updatedAt: now, @@ -151,6 +161,57 @@ function abbreviateBook(book: string) { return abbreviations[book] ?? book.slice(0, 4) } +function dictionaryTermsForVerse(verse: Verse) { + const stopWords = new Set([ + 'a', + 'an', + 'and', + 'are', + 'as', + 'at', + 'be', + 'but', + 'by', + 'for', + 'from', + 'he', + 'her', + 'him', + 'his', + 'i', + 'in', + 'is', + 'it', + 'of', + 'on', + 'or', + 'that', + 'the', + 'their', + 'them', + 'they', + 'this', + 'to', + 'unto', + 'was', + 'were', + 'with', + 'ye', + 'you', + ]) + const strongSurfaces = verse.strongs.map((entry) => entry.surface) + const textWords = [...verse.text.matchAll(/[A-Za-z][A-Za-z'-]*/g)].map((match) => match[0]) + const unique = new Map() + + for (const term of [...strongSurfaces, ...textWords]) { + const normalized = term.toLowerCase().replace(/[^a-z0-9\s'-]/g, '').trim() + if (!normalized || stopWords.has(normalized) || normalized.length < 3) continue + unique.set(normalized, term) + } + + return [...unique.values()].slice(0, 28) +} + function App() { const [railOpen, setRailOpen] = useState(false) const [sessionModalOpen, setSessionModalOpen] = useState(false) @@ -173,12 +234,19 @@ function App() { const [newSessionTitle, setNewSessionTitle] = useState('New KJV Study') const [newSessionFootnotes, setNewSessionFootnotes] = useState(true) const [newSessionStrongs, setNewSessionStrongs] = useState(true) + const [newSessionCommentary, setNewSessionCommentary] = useState(true) + const [newSessionDictionary, setNewSessionDictionary] = 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 [commentaryCollapsed, setCommentaryCollapsed] = useState(false) + const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false) + const [commentaryEntries, setCommentaryEntries] = useState([]) + const [dictionaryEntries, setDictionaryEntries] = useState([]) + const [marginResourcesLoading, setMarginResourcesLoading] = useState(false) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const restoredSessionId = useRef('') @@ -350,6 +418,8 @@ function App() { baseTranslation: session.baseTranslation ?? 'kjv-eng-kjv2006', footnotesEnabled: session.footnotesEnabled ?? true, strongsEnabled: session.strongsEnabled ?? true, + commentaryEnabled: session.commentaryEnabled ?? true, + dictionaryEnabled: session.dictionaryEnabled ?? true, timelineEnabled: session.timelineEnabled ?? true, })) @@ -422,6 +492,44 @@ function App() { () => sessions.find((session) => session.id === activeSessionId) ?? sessions[0], [activeSessionId, sessions], ) + + useEffect(() => { + let cancelled = false + + async function loadMarginResources() { + if (!selectedVerse) { + setCommentaryEntries([]) + setDictionaryEntries([]) + return + } + + setMarginResourcesLoading(true) + try { + const [nextCommentary, nextDictionary] = await Promise.all([ + activeSession?.commentaryEnabled === false + ? Promise.resolve([]) + : getCommentaryForVerse(selectedVerse), + activeSession?.dictionaryEnabled === false + ? Promise.resolve([]) + : getDictionaryEntriesForTerms(dictionaryTermsForVerse(selectedVerse)), + ]) + + if (cancelled) return + setCommentaryEntries(nextCommentary) + setDictionaryEntries(nextDictionary) + } catch (reason) { + if (!cancelled) setError(String(reason)) + } finally { + if (!cancelled) setMarginResourcesLoading(false) + } + } + + loadMarginResources() + return () => { + cancelled = true + } + }, [activeSession?.commentaryEnabled, activeSession?.dictionaryEnabled, selectedVerse]) + const pageAnchors = useMemo(() => { const source = libraryVerses.length ? libraryVerses : verses const anchors: Verse[] = [] @@ -629,6 +737,8 @@ function App() { ...createSession(newSessionTitle.trim() || `Study Session ${sessions.length + 1}`), footnotesEnabled: newSessionFootnotes, strongsEnabled: newSessionStrongs, + commentaryEnabled: newSessionCommentary, + dictionaryEnabled: newSessionDictionary, timelineEnabled: newSessionTimeline, } setSessions((current) => [nextSession, ...current]) @@ -641,7 +751,12 @@ function App() { if (patch.strongsEnabled === false) { setStrongDisplay('off') } - if (typeof patch.strongsEnabled === 'boolean' || typeof patch.footnotesEnabled === 'boolean') { + if ( + typeof patch.strongsEnabled === 'boolean' || + typeof patch.footnotesEnabled === 'boolean' || + typeof patch.commentaryEnabled === 'boolean' || + typeof patch.dictionaryEnabled === 'boolean' + ) { recordTimeline({ type: 'setting', label: 'Updated session resources' }, patch) } } @@ -873,6 +988,112 @@ function App() { ) } + function renderCommentarySection() { + if (activeSession?.commentaryEnabled === false) { + return ( +
+
+ + Off +
+

Commentary is disabled for this session.

+
+ ) + } + + return ( +
+
setCommentaryCollapsed((current) => !current)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + setCommentaryCollapsed((current) => !current) + } + }} + > + + {commentaryEntries.length ? 'MHCC' : marginResourcesLoading ? 'Loading' : 'None'} +
+ {!commentaryCollapsed ? ( + commentaryEntries.length ? ( +
+ {commentaryEntries.map((entry) => ( +
+

{entry.reference}

+

{entry.text}

+ {entry.source} +
+ ))} +
+ ) : ( +

+ {marginResourcesLoading ? 'Loading commentary...' : 'No commentary matched this verse.'} +

+ ) + ) : null} +
+ ) + } + + function renderDictionarySection() { + if (activeSession?.dictionaryEnabled === false) { + return ( +
+
+ + Off +
+

Dictionary lookup is disabled for this session.

+
+ ) + } + + return ( +
+
setDictionaryCollapsed((current) => !current)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + setDictionaryCollapsed((current) => !current) + } + }} + > + + {dictionaryEntries.length ? `${dictionaryEntries.length} terms` : marginResourcesLoading ? 'Loading' : 'None'} +
+ {!dictionaryCollapsed ? ( + dictionaryEntries.length ? ( +
+ {dictionaryEntries.slice(0, 6).map((entry) => ( +
+

{entry.term}

+ {entry.definitions.slice(0, 2).map((definition) => ( +

+ {definition.sourceName} + {definition.text} +

+ ))} +
+ ))} +
+ ) : ( +

+ {marginResourcesLoading ? 'Loading dictionary...' : 'No dictionary terms matched this verse.'} +

+ ) + ) : null} +
+ ) + } + function renderBiblePage(label = 'KJV') { if (!selectedVerse) return null return ( @@ -918,7 +1139,11 @@ function App() { ) } @@ -1169,6 +1396,22 @@ function App() { /> Footnotes + + + +