Stabilize Bible pages and add Strong's markers

This commit is contained in:
2026-07-12 08:45:33 -05:00
parent e0b5ee9e58
commit c0774c8e52
9 changed files with 163 additions and 24 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.1.5 - 2026-07-12
- Fixed page selection so clicking a verse highlights it in place instead of rebuilding the page around it.
- Added session-stable page anchors for the loaded translation.
- Added inline Strong's highlights and code badges that connect Bible text to the study margin.
## 0.1.4 - 2026-07-12
- Attached the study margin and footnote area to the Bible page surface.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "libre-study",
"version": "0.1.4",
"version": "0.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "libre-study",
"version": "0.1.4",
"version": "0.1.5",
"dependencies": {
"@tauri-apps/api": "^2.11.1",
"react": "^19.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "libre-study",
"private": true,
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
+1 -1
View File
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "app"
version = "0.1.4"
version = "0.1.5"
dependencies = [
"log",
"rusqlite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "app"
version = "0.1.4"
version = "0.1.5"
description = "A Tauri App"
authors = ["you"]
license = ""
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Libre Study",
"version": "0.1.4",
"version": "0.1.5",
"identifier": "org.librestudy.desktop",
"build": {
"frontendDist": "../dist",
+39
View File
@@ -247,6 +247,45 @@ textarea:focus-visible {
font-weight: 800;
}
.strong-token {
display: inline;
position: relative;
border-radius: 3px;
box-shadow: inset 0 -0.45em 0 rgba(214, 168, 79, 0.22);
cursor: pointer;
}
.strong-token:hover,
.strong-token:focus-visible {
box-shadow: inset 0 -1.1em 0 rgba(214, 168, 79, 0.34);
outline: none;
}
.strong-token.active {
box-shadow: inset 0 -1.1em 0 rgba(47, 111, 94, 0.3);
}
.strong-code {
display: inline-block;
margin: 0 2px 0 3px;
transform: translateY(-0.12em);
border: 1px solid rgba(138, 109, 46, 0.34);
border-radius: 4px;
padding: 0 3px;
background: #fff8e6;
color: #735c0f;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 9px;
font-weight: 800;
line-height: 1.35;
}
.strong-token.active .strong-code {
border-color: rgba(47, 111, 94, 0.42);
background: #e7f0eb;
color: #225446;
}
.study-margin,
.notes-workspace {
min-height: 0;
+106 -11
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { addTagToVerse, getLibraryState, searchVerses } from './studyClient'
import type { ReactNode } from 'react'
import type { Verse } from './types'
import './App.css'
@@ -17,6 +18,7 @@ function App() {
const [libraryVerses, setLibraryVerses] = useState<Verse[]>([])
const [verses, setVerses] = useState<Verse[]>([])
const [selectedId, setSelectedId] = useState<number | null>(null)
const [pageStartId, setPageStartId] = useState<number | null>(null)
const [selectedStrong, setSelectedStrong] = useState<string | null>(null)
const [tagInput, setTagInput] = useState('')
const [noteDraft, setNoteDraft] = useState('')
@@ -28,7 +30,9 @@ function App() {
const state = await getLibraryState()
setLibraryVerses(state.verses)
setVerses(state.verses)
setSelectedId((current) => current ?? state.verses[0]?.id ?? null)
const firstId = state.verses[0]?.id ?? null
setSelectedId((current) => current ?? firstId)
setPageStartId((current) => current ?? firstId)
}
async function runSearch(nextQuery = query) {
@@ -37,7 +41,9 @@ function App() {
setSelectedStrong(/^[gh]\d+$/i.test(trimmed) ? trimmed.toUpperCase() : null)
const results = await searchVerses(nextQuery)
setVerses(results)
setSelectedId(results[0]?.id ?? null)
const firstResult = results[0]
setSelectedId(firstResult?.id ?? null)
setPageStartId(firstResult ? findPageStartId(firstResult) : null)
}
async function addTag() {
@@ -76,13 +82,24 @@ function App() {
selectedVerse.strongs[0]
)
}, [selectedStrong, selectedVerse])
const pageAnchors = useMemo(() => {
const source = libraryVerses.length ? libraryVerses : verses
const anchors: Verse[] = []
for (let index = 0; index < source.length; index += pageSize) {
anchors.push(source[index])
}
return anchors
}, [libraryVerses, verses])
const pageVerses = useMemo(() => {
if (!selectedVerse) return []
const source = libraryVerses.length ? libraryVerses : verses
const pageStartIndex = source.findIndex((verse) => verse.id === pageStartId)
const selectedIndex = source.findIndex((verse) => verse.id === selectedVerse.id)
const start = Math.max(0, selectedIndex)
const start = Math.max(0, pageStartIndex >= 0 ? pageStartIndex : selectedIndex)
return source.slice(start, start + pageSize)
}, [libraryVerses, selectedVerse, verses])
}, [libraryVerses, pageStartId, selectedVerse, verses])
const pageRange = pageVerses.length
? `${reference(pageVerses[0])} - ${reference(pageVerses[pageVerses.length - 1])}`
: 'No page loaded'
@@ -130,16 +147,94 @@ function App() {
setSelectedStrong(null)
setVerses(libraryVerses)
setSelectedId(next.id)
setPageStartId(findPageStartId(next))
}
function shiftPage(direction: -1 | 1) {
if (!selectedVerse) return
const currentStartIndex = pageAnchors.findIndex((verse) => verse.id === pageStartId)
const selectedPageStartId = findPageStartId(selectedVerse)
const selectedIndex = pageAnchors.findIndex((verse) => verse.id === selectedPageStartId)
const index = currentStartIndex >= 0 ? currentStartIndex : selectedIndex
const next =
pageAnchors[Math.min(pageAnchors.length - 1, Math.max(0, index + direction))]
if (!next) return
setPageStartId(next.id)
setSelectedId(next.id)
setSelectedStrong(null)
}
function findPageStartId(verse: Verse) {
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,
)
const index = source.findIndex((entry) => entry.id === verse.id)
if (index < 0) return pageStartId ?? verse.id
const pageIndex = Math.floor(index / pageSize) * pageSize
return source[pageIndex]?.id ?? verse.id
}
function selectVerse(verse: Verse) {
setSelectedId(verse.id)
setSelectedStrong(null)
}
function selectStrong(verse: Verse, strong: string) {
setSelectedId(verse.id)
setSelectedStrong(strong)
}
function renderVerseText(verse: Verse) {
if (!verse.strongs.length) return verse.text
const parts: ReactNode[] = []
let cursor = 0
const lowerText = verse.text.toLowerCase()
const sortedLinks = [...verse.strongs].sort((left, right) => left.position - right.position)
sortedLinks.forEach((entry) => {
const surface = entry.surface.trim()
if (!surface) return
const index = lowerText.indexOf(surface.toLowerCase(), cursor)
if (index < 0) return
if (index > cursor) {
parts.push(verse.text.slice(cursor, index))
}
const linkedText = verse.text.slice(index, index + surface.length)
const isActive = entry.strong === selectedStrong
parts.push(
<span
key={`${verse.id}-${entry.strong}-${entry.position}`}
className={isActive ? 'strong-token active' : 'strong-token'}
role="button"
tabIndex={0}
title={`${entry.surface}: ${entry.strong}`}
onClick={(event) => {
event.stopPropagation()
selectStrong(verse, entry.strong)
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
selectStrong(verse, entry.strong)
}
}}
>
<span className="strong-surface">{linkedText}</span>
{(verse.id === selectedId || isActive) ? (
<small className="strong-code">{entry.strong}</small>
) : null}
</span>,
)
cursor = index + surface.length
})
if (cursor < verse.text.length) {
parts.push(verse.text.slice(cursor))
}
return parts.length ? parts : verse.text
}
function renderBiblePage(label = 'KJV') {
@@ -156,10 +251,10 @@ function App() {
<p
key={verse.id}
className={verse.id === selectedVerse.id ? 'page-verse selected' : 'page-verse'}
onClick={() => setSelectedId(verse.id)}
onClick={() => selectVerse(verse)}
>
<sup>{verse.verse}</sup>
{verse.text}
{renderVerseText(verse)}
</p>
))}
</div>
+6 -7
View File
@@ -48,7 +48,6 @@ type BrowserStrongData = {
let browserLibraryCache: Promise<Verse[]> | null = null
let browserStrongDataCache: Promise<BrowserStrongData> | null = null
let browserStrongData: BrowserStrongData | null = null
const browserVerseSourceIds = new Map<number, string>()
const fallbackBrowserVerses: Verse[] = [
@@ -192,11 +191,14 @@ async function loadBrowserVerses() {
})
const verses = await browserLibraryCache
void loadBrowserStrongData()
const strongData = await loadBrowserStrongData().catch((error) => {
console.warn('Unable to load browser Strong data.', error)
return null
})
const tagsByVerse = readBrowserTags()
return verses.map((verse) => ({
...verse,
strongs: browserStrongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
strongs: strongData?.linksByVerse.get(browserVerseKey(verse)) ?? verse.strongs,
tags: tagsByVerse[verse.id] ?? [],
}))
}
@@ -292,10 +294,7 @@ async function loadLibreBibleDevVerses(): Promise<Verse[]> {
}
async function loadBrowserStrongData(): Promise<BrowserStrongData> {
browserStrongDataCache ??= loadLibreBibleDevStrongData().then((data) => {
browserStrongData = data
return data
})
browserStrongDataCache ??= loadLibreBibleDevStrongData()
return browserStrongDataCache
}