Compact reader controls and margin layout

This commit is contained in:
2026-07-12 10:44:01 -05:00
parent 162fb06870
commit 123cb8dd98
7 changed files with 425 additions and 100 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "libre-study", "name": "libre-study",
"version": "0.1.16", "version": "0.1.17",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "libre-study", "name": "libre-study",
"version": "0.1.16", "version": "0.1.17",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.1", "@tauri-apps/api": "^2.11.1",
"react": "^19.2.7", "react": "^19.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "libre-study", "name": "libre-study",
"private": true, "private": true,
"version": "0.1.16", "version": "0.1.17",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1", "dev": "vite --host 127.0.0.1",
+1 -1
View File
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]] [[package]]
name = "app" name = "app"
version = "0.1.16" version = "0.1.17"
dependencies = [ dependencies = [
"log", "log",
"rusqlite", "rusqlite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "app" name = "app"
version = "0.1.16" version = "0.1.17"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
license = "" license = ""
+1 -1
View File
@@ -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.16", "version": "0.1.17",
"identifier": "org.librestudy.desktop", "identifier": "org.librestudy.desktop",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
+155 -59
View File
@@ -11,6 +11,10 @@
filter: blur(1.5px) brightness(0.62) saturate(0.72); filter: blur(1.5px) brightness(0.62) saturate(0.72);
} }
.app-shell.notes-pinned .reader {
padding-right: min(500px, 30vw);
}
.tool-rail { .tool-rail {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -155,14 +159,13 @@
gap: 10px; gap: 10px;
min-width: 0; min-width: 0;
height: 100vh; height: 100vh;
padding: 18px 42px 18px 18px; padding: 10px 48px 12px 12px;
box-sizing: border-box; box-sizing: border-box;
transition: filter 160ms ease; transition: filter 160ms ease;
} }
.top-bar { .top-bar {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 6px; gap: 6px;
align-items: stretch; align-items: stretch;
min-width: 0; min-width: 0;
@@ -170,15 +173,18 @@
.bible-top-tools { .bible-top-tools {
display: grid; display: grid;
gap: 8px; grid-template-columns: minmax(0, 1.42fr) minmax(160px, 1fr);
gap: 6px;
align-items: end;
min-width: 0; min-width: 0;
} }
.study-top-tools { .study-top-tools {
display: flex; display: flex;
align-items: end; align-items: center;
justify-content: flex-start;
min-width: 0; min-width: 0;
padding: 0 12px 3px; padding: 4px 8px;
border: 1px solid #d4cdbd; border: 1px solid #d4cdbd;
border-radius: 8px; border-radius: 8px;
background: #fbf8ef; background: #fbf8ef;
@@ -188,6 +194,18 @@
text-transform: uppercase; text-transform: uppercase;
} }
.study-top-tools button {
min-height: 28px;
padding: 5px 9px;
background: #2f6f5e;
color: #fffef8;
font-size: 12px;
}
.split-spacer {
min-width: 0;
}
.study-margin h2, .study-margin h2,
.notes-workspace h2, .notes-workspace h2,
.strong-focus h4 { .strong-focus h4 {
@@ -206,7 +224,7 @@
.search-box { .search-box {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
gap: 8px; gap: 5px;
} }
input, input,
@@ -216,11 +234,11 @@ textarea {
min-width: 0; min-width: 0;
border: 1px solid #bdb8aa; border: 1px solid #bdb8aa;
border-radius: 6px; border-radius: 6px;
padding: 10px 12px; padding: 5px 8px;
background: #fffef9; background: #fffef9;
color: #202124; color: #202124;
font: inherit; font: inherit;
font-size: 14px; font-size: 12px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -233,11 +251,11 @@ textarea {
button { button {
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 6px; border-radius: 6px;
padding: 10px 12px; padding: 6px 9px;
background: #d6a84f; background: #d6a84f;
color: #141510; color: #141510;
font: inherit; font: inherit;
font-size: 14px; font-size: 12px;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
} }
@@ -252,17 +270,17 @@ textarea:focus-visible {
.page-controls { .page-controls {
display: grid; display: grid;
grid-template-columns: minmax(92px, 1fr) minmax(118px, 1.24fr) minmax(72px, 0.74fr) minmax(68px, 0.66fr) minmax(96px, 0.9fr); grid-template-columns: minmax(62px, 0.8fr) minmax(92px, 1.25fr) minmax(54px, 0.55fr) minmax(50px, 0.5fr) minmax(80px, 0.82fr);
gap: 8px; gap: 5px;
align-items: end; align-items: end;
max-width: none; max-width: none;
} }
.page-controls label { .page-controls label {
display: grid; display: grid;
gap: 4px; gap: 2px;
color: #657061; color: #657061;
font-size: 11px; font-size: 9px;
font-weight: 800; font-weight: 800;
letter-spacing: 0; letter-spacing: 0;
text-transform: uppercase; text-transform: uppercase;
@@ -270,8 +288,7 @@ textarea:focus-visible {
.study-desk { .study-desk {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0;
gap: 6px;
min-height: 0; min-height: 0;
} }
@@ -282,6 +299,19 @@ textarea:focus-visible {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
} }
.split-resizer {
width: 6px;
min-width: 6px;
cursor: col-resize;
background: linear-gradient(90deg, transparent, #cfc6b4 45%, #cfc6b4 55%, transparent);
}
.split-resizer:hover,
.split-resizer:focus-visible {
background: #d6a84f;
outline: none;
}
.page-system, .page-system,
.notes-workspace, .notes-workspace,
.status-panel { .status-panel {
@@ -310,7 +340,7 @@ textarea:focus-visible {
grid-column: 1; grid-column: 1;
grid-template-rows: auto minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
min-height: 0; min-height: 0;
padding: 26px 30px; padding: 22px 26px;
} }
.bible-page header, .bible-page header,
@@ -327,7 +357,9 @@ textarea:focus-visible {
.page-turner { .page-turner {
grid-column: 1; grid-column: 1;
padding: 14px 30px; display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
padding: 8px 26px 6px;
border-top: 1px solid #e8e2d4; border-top: 1px solid #e8e2d4;
background: #fffef8; background: #fffef8;
text-transform: none; text-transform: none;
@@ -335,10 +367,50 @@ textarea:focus-visible {
.page-turner span { .page-turner span {
color: #6b675d; color: #6b675d;
font-size: 13px; font-size: 12px;
font-weight: 700; font-weight: 700;
} }
.page-turner button {
align-self: start;
min-height: 26px;
padding: 4px 9px;
}
.page-position {
display: grid;
gap: 4px;
justify-items: center;
min-width: 0;
}
.chapter-tabs {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 3px;
max-height: 38px;
overflow: hidden;
}
.chapter-tabs button {
min-width: 22px;
min-height: 20px;
padding: 2px 5px;
border-color: #cfc4ad;
background: #f2ead8;
color: #4c422f;
font-size: 10px;
line-height: 1;
}
.chapter-tabs button.active,
.chapter-tabs button:hover,
.chapter-tabs button:focus-visible {
background: #d6a84f;
color: #151510;
}
.page-columns { .page-columns {
column-count: 2; column-count: 2;
column-gap: 34px; column-gap: 34px;
@@ -419,7 +491,7 @@ textarea:focus-visible {
.notes-workspace { .notes-workspace {
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
padding: 16px; padding: 10px;
} }
.study-margin { .study-margin {
@@ -435,13 +507,13 @@ textarea:focus-visible {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
padding-bottom: 12px; padding-bottom: 8px;
border-bottom: 1px solid #e4e0d6; border-bottom: 1px solid #e4e0d6;
} }
.panel-header h2, .panel-header h2,
.notes-workspace h2 { .notes-workspace h2 {
font-size: 18px; font-size: 16px;
} }
.panel-header span, .panel-header span,
@@ -453,16 +525,23 @@ textarea:focus-visible {
} }
.margin-reference { .margin-reference {
margin: 12px 0; margin: 8px 0;
font-weight: 800; font-weight: 800;
} }
.margin-resource-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
align-items: start;
}
.original-language-verse { .original-language-verse {
display: grid; display: grid;
gap: 8px; gap: 5px;
min-width: 0; min-width: 0;
margin: 0 0 14px; margin: 0;
padding: 12px; padding: 8px;
overflow: hidden; overflow: hidden;
border: 1px solid #dfd6c2; border: 1px solid #dfd6c2;
border-radius: 6px; border-radius: 6px;
@@ -471,11 +550,11 @@ textarea:focus-visible {
.margin-section-header { .margin-section-header {
display: flex; display: flex;
gap: 8px; gap: 6px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin: -6px -6px 2px; margin: -5px -5px 1px;
padding: 7px 8px; padding: 4px 6px;
border-radius: 5px; border-radius: 5px;
background: #f5efe1; background: #f5efe1;
cursor: pointer; cursor: pointer;
@@ -492,7 +571,7 @@ textarea:focus-visible {
border: 0; border: 0;
background: transparent; background: transparent;
color: #657061; color: #657061;
font-size: 11px; font-size: 10px;
font-weight: 800; font-weight: 800;
letter-spacing: 0; letter-spacing: 0;
text-transform: uppercase; text-transform: uppercase;
@@ -505,16 +584,10 @@ textarea:focus-visible {
cursor: pointer; cursor: pointer;
} }
.collapse-button::before {
margin-right: 5px;
color: #735c0f;
content: 'v';
}
.mini-settings { .mini-settings {
width: auto; width: auto;
height: auto; height: auto;
padding: 3px 6px; padding: 2px 5px;
border: 1px solid #d8cfbf; border: 1px solid #d8cfbf;
border-radius: 4px; border-radius: 4px;
background: #fbf8ef; background: #fbf8ef;
@@ -541,7 +614,7 @@ textarea:focus-visible {
color: #1f211e; color: #1f211e;
font-family: Georgia, 'Times New Roman', serif; font-family: Georgia, 'Times New Roman', serif;
font-size: 20px; font-size: 20px;
line-height: 1.7; line-height: 1.45;
white-space: normal; white-space: normal;
word-break: normal; word-break: normal;
} }
@@ -573,9 +646,9 @@ textarea:focus-visible {
.english-strong-line { .english-strong-line {
display: grid; display: grid;
gap: 8px; gap: 5px;
margin: 0 0 14px; margin: 0;
padding: 12px; padding: 8px;
border: 1px solid #dfd6c2; border: 1px solid #dfd6c2;
border-radius: 6px; border-radius: 6px;
background: #fffdf6; background: #fffdf6;
@@ -584,7 +657,7 @@ textarea:focus-visible {
.linked-line-text { .linked-line-text {
margin: 0; margin: 0;
color: #202124; color: #202124;
line-height: 1.65; line-height: 1.45;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
@@ -609,8 +682,8 @@ textarea:focus-visible {
.strong-focus { .strong-focus {
display: grid; display: grid;
gap: 9px; gap: 6px;
padding: 16px; padding: 8px;
border: 1px solid #b9cabf; border: 1px solid #b9cabf;
border-radius: 8px; border-radius: 8px;
background: #eef6f1; background: #eef6f1;
@@ -632,14 +705,14 @@ textarea:focus-visible {
.strong-focus h4 { .strong-focus h4 {
color: #1f211e; color: #1f211e;
font-size: 22px; font-size: 18px;
} }
.strong-focus p { .strong-focus p {
margin: 0; margin: 0;
color: #3f443d; color: #3f443d;
font-size: 14px; font-size: 12px;
line-height: 1.5; line-height: 1.38;
} }
.strong-focus button { .strong-focus button {
@@ -651,31 +724,36 @@ textarea:focus-visible {
.resource-card { .resource-card {
display: grid; display: grid;
gap: 9px; gap: 6px;
margin-top: 14px; margin-top: 0;
padding: 12px; padding: 8px;
border: 1px solid #dfd6c2; border: 1px solid #dfd6c2;
border-radius: 6px; border-radius: 6px;
background: #fffdf6; background: #fffdf6;
} }
.commentary-card,
.dictionary-card {
grid-column: 1 / -1;
}
.commentary-list, .commentary-list,
.dictionary-list { .dictionary-list {
display: grid; display: grid;
gap: 12px; gap: 9px;
} }
.commentary-list section, .commentary-list section,
.dictionary-list section { .dictionary-list section {
display: grid; display: grid;
gap: 5px; gap: 4px;
} }
.commentary-list h3, .commentary-list h3,
.dictionary-list h3 { .dictionary-list h3 {
margin: 0; margin: 0;
color: #252821; color: #252821;
font-size: 14px; font-size: 13px;
letter-spacing: 0; letter-spacing: 0;
} }
@@ -683,8 +761,8 @@ textarea:focus-visible {
.dictionary-list p { .dictionary-list p {
margin: 0; margin: 0;
color: #3f443d; color: #3f443d;
font-size: 13px; font-size: 12px;
line-height: 1.48; line-height: 1.38;
} }
.commentary-list small { .commentary-list small {
@@ -713,15 +791,15 @@ textarea:focus-visible {
right: 0; right: 0;
z-index: 28; z-index: 28;
display: grid; display: grid;
grid-template-columns: 30px minmax(360px, 30vw); grid-template-columns: 42px minmax(380px, 30vw);
width: min(540px, calc(100vw - 92px)); width: min(560px, calc(100vw - 92px));
height: 100vh; height: 100vh;
transform: translateX(calc(100% - 30px)); transform: translateX(calc(100% - 42px));
transition: transform 190ms ease, box-shadow 190ms ease; transition: transform 190ms ease, box-shadow 190ms ease;
} }
.notes-drawer.open, .notes-drawer.open,
.notes-drawer:hover, .notes-drawer.pinned,
.notes-drawer:focus-within { .notes-drawer:focus-within {
transform: translateX(0); transform: translateX(0);
box-shadow: -18px 0 42px rgba(17, 20, 17, 0.24); box-shadow: -18px 0 42px rgba(17, 20, 17, 0.24);
@@ -740,6 +818,24 @@ textarea:focus-visible {
font-weight: 900; font-weight: 900;
letter-spacing: 0; letter-spacing: 0;
cursor: pointer; cursor: pointer;
width: 42px;
height: 100%;
padding: 0;
border-top: 0;
border-bottom: 0;
border-radius: 0;
}
.notes-workspace > header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 10px;
}
.pin-notes {
background: #2f6f5e;
color: #fffef8;
} }
.notes-drawer .notes-workspace { .notes-drawer .notes-workspace {
+264 -35
View File
@@ -6,7 +6,7 @@ import {
getLibraryState, getLibraryState,
searchVerses, searchVerses,
} from './studyClient' } from './studyClient'
import type { DragEvent, ReactNode } from 'react' import type { DragEvent, MouseEvent as ReactMouseEvent, ReactNode } from 'react'
import type { CommentaryEntry, DictionaryEntry, Verse } from './types' import type { CommentaryEntry, DictionaryEntry, Verse } from './types'
import './App.css' import './App.css'
@@ -219,6 +219,7 @@ function App() {
const [timelineModalOpen, setTimelineModalOpen] = useState(false) const [timelineModalOpen, setTimelineModalOpen] = useState(false)
const [settingsModalOpen, setSettingsModalOpen] = useState(false) const [settingsModalOpen, setSettingsModalOpen] = useState(false)
const [notesDrawerOpen, setNotesDrawerOpen] = useState(false) const [notesDrawerOpen, setNotesDrawerOpen] = useState(false)
const [notesPinned, setNotesPinned] = useState(false)
const [pageMotion, setPageMotion] = useState<PageMotion>('idle') const [pageMotion, setPageMotion] = useState<PageMotion>('idle')
const [viewMode, setViewMode] = useState<ViewMode>('notes') const [viewMode, setViewMode] = useState<ViewMode>('notes')
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
@@ -245,6 +246,12 @@ function App() {
const [definitionCollapsed, setDefinitionCollapsed] = useState(false) const [definitionCollapsed, setDefinitionCollapsed] = useState(false)
const [commentaryCollapsed, setCommentaryCollapsed] = useState(false) const [commentaryCollapsed, setCommentaryCollapsed] = useState(false)
const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false) const [dictionaryCollapsed, setDictionaryCollapsed] = useState(false)
const [showOriginalLine, setShowOriginalLine] = useState(true)
const [showLinkedLine, setShowLinkedLine] = useState(true)
const [showDefinitionPanel, setShowDefinitionPanel] = useState(true)
const [showCommentaryPanel, setShowCommentaryPanel] = useState(true)
const [showDictionaryPanel, setShowDictionaryPanel] = useState(true)
const [splitPercent, setSplitPercent] = useState(50)
const [commentaryEntries, setCommentaryEntries] = useState<CommentaryEntry[]>([]) const [commentaryEntries, setCommentaryEntries] = useState<CommentaryEntry[]>([])
const [dictionaryEntries, setDictionaryEntries] = useState<DictionaryEntry[]>([]) const [dictionaryEntries, setDictionaryEntries] = useState<DictionaryEntry[]>([])
const [marginResourcesLoading, setMarginResourcesLoading] = useState(false) const [marginResourcesLoading, setMarginResourcesLoading] = useState(false)
@@ -252,6 +259,8 @@ function App() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const restoredSessionId = useRef('') const restoredSessionId = useRef('')
const pageMotionTimeout = useRef<number | null>(null) const pageMotionTimeout = useRef<number | null>(null)
const notesDragOpenTimeout = useRef<number | null>(null)
const readerRef = useRef<HTMLElement | null>(null)
async function loadLibrary() { async function loadLibrary() {
setError(null) setError(null)
@@ -389,12 +398,30 @@ function App() {
originalCollapsed: boolean originalCollapsed: boolean
linkedCollapsed: boolean linkedCollapsed: boolean
definitionCollapsed: boolean definitionCollapsed: boolean
commentaryCollapsed: boolean
dictionaryCollapsed: boolean
showOriginalLine: boolean
showLinkedLine: boolean
showDefinitionPanel: boolean
showCommentaryPanel: boolean
showDictionaryPanel: boolean
splitPercent: number
notesPinned: boolean
}> }>
setLemmaFontSize(parsed.lemmaFontSize ?? 17) setLemmaFontSize(parsed.lemmaFontSize ?? 17)
setLinkedFontSize(parsed.linkedFontSize ?? 15) setLinkedFontSize(parsed.linkedFontSize ?? 15)
setOriginalCollapsed(parsed.originalCollapsed ?? false) setOriginalCollapsed(parsed.originalCollapsed ?? false)
setLinkedCollapsed(parsed.linkedCollapsed ?? false) setLinkedCollapsed(parsed.linkedCollapsed ?? false)
setDefinitionCollapsed(parsed.definitionCollapsed ?? false) setDefinitionCollapsed(parsed.definitionCollapsed ?? false)
setCommentaryCollapsed(parsed.commentaryCollapsed ?? false)
setDictionaryCollapsed(parsed.dictionaryCollapsed ?? false)
setShowOriginalLine(parsed.showOriginalLine ?? true)
setShowLinkedLine(parsed.showLinkedLine ?? true)
setShowDefinitionPanel(parsed.showDefinitionPanel ?? true)
setShowCommentaryPanel(parsed.showCommentaryPanel ?? true)
setShowDictionaryPanel(parsed.showDictionaryPanel ?? true)
setSplitPercent(parsed.splitPercent ?? 50)
setNotesPinned(parsed.notesPinned ?? false)
} catch { } catch {
localStorage.removeItem(studySettingsStorageKey) localStorage.removeItem(studySettingsStorageKey)
} }
@@ -457,9 +484,33 @@ function App() {
originalCollapsed, originalCollapsed,
linkedCollapsed, linkedCollapsed,
definitionCollapsed, definitionCollapsed,
commentaryCollapsed,
dictionaryCollapsed,
showOriginalLine,
showLinkedLine,
showDefinitionPanel,
showCommentaryPanel,
showDictionaryPanel,
splitPercent,
notesPinned,
}), }),
) )
}, [definitionCollapsed, lemmaFontSize, linkedCollapsed, linkedFontSize, originalCollapsed]) }, [
commentaryCollapsed,
definitionCollapsed,
dictionaryCollapsed,
lemmaFontSize,
linkedCollapsed,
linkedFontSize,
notesPinned,
originalCollapsed,
showCommentaryPanel,
showDefinitionPanel,
showDictionaryPanel,
showLinkedLine,
showOriginalLine,
splitPercent,
])
useEffect(() => { useEffect(() => {
if (restoredSessionId.current === activeSessionId) return if (restoredSessionId.current === activeSessionId) return
@@ -724,6 +775,34 @@ function App() {
) )
} }
function recordDisplaySetting(label: string) {
recordTimeline({
type: 'setting',
label,
})
}
function beginSplitResize(event: ReactMouseEvent<HTMLDivElement>) {
event.preventDefault()
const reader = readerRef.current
if (!reader) return
const resize = (moveEvent: MouseEvent) => {
const rect = reader.getBoundingClientRect()
const next = ((moveEvent.clientX - rect.left) / rect.width) * 100
setSplitPercent(Math.min(68, Math.max(36, next)))
}
const stop = () => {
window.removeEventListener('mousemove', resize)
window.removeEventListener('mouseup', stop)
recordDisplaySetting('Adjusted Bible and study margin split')
}
window.addEventListener('mousemove', resize)
window.addEventListener('mouseup', stop, { once: true })
}
function updateNoteDraft(value: string) { function updateNoteDraft(value: string) {
setNoteDraft(value) setNoteDraft(value)
patchActiveSession({ noteDraft: value }) patchActiveSession({ noteDraft: value })
@@ -739,7 +818,20 @@ function App() {
function startTextDrag(event: DragEvent<HTMLElement>, value: string) { function startTextDrag(event: DragEvent<HTMLElement>, value: string) {
event.dataTransfer.setData('text/plain', value) event.dataTransfer.setData('text/plain', value)
event.dataTransfer.effectAllowed = 'copy' event.dataTransfer.effectAllowed = 'copy'
setNotesDrawerOpen(true) }
function scheduleNotesDragOpen() {
if (notesPinned || notesDragOpenTimeout.current) return
notesDragOpenTimeout.current = window.setTimeout(() => {
setNotesDrawerOpen(true)
notesDragOpenTimeout.current = null
}, 550)
}
function clearNotesDragOpen() {
if (!notesDragOpenTimeout.current) return
window.clearTimeout(notesDragOpenTimeout.current)
notesDragOpenTimeout.current = null
} }
function renameActiveSession(title: string) { function renameActiveSession(title: string) {
@@ -881,7 +973,7 @@ function App() {
} }
function renderOriginalLanguageLine() { function renderOriginalLanguageLine() {
if (!selectedVerse?.strongs.length) return null if (!showOriginalLine || !selectedVerse?.strongs.length) return null
const isHebrew = selectedVerse.strongs.some((entry) => entry.strong.startsWith('H')) const isHebrew = selectedVerse.strongs.some((entry) => entry.strong.startsWith('H'))
const direction = isHebrew ? 'rtl' : 'ltr' const direction = isHebrew ? 'rtl' : 'ltr'
@@ -891,11 +983,15 @@ function App() {
className="margin-section-header" className="margin-section-header"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setOriginalCollapsed((current) => !current)} onClick={() => {
setOriginalCollapsed((current) => !current)
recordDisplaySetting('Toggled original language accordion')
}}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault() event.preventDefault()
setOriginalCollapsed((current) => !current) setOriginalCollapsed((current) => !current)
recordDisplaySetting('Toggled original language accordion')
} }
}} }}
> >
@@ -943,7 +1039,7 @@ function App() {
} }
function renderLinkedEnglishLine() { function renderLinkedEnglishLine() {
if (!selectedVerse?.strongs.length) return null if (!showLinkedLine || !selectedVerse?.strongs.length) return null
return ( return (
<section className="english-strong-line" aria-label="English Strong's line"> <section className="english-strong-line" aria-label="English Strong's line">
@@ -951,11 +1047,15 @@ function App() {
className="margin-section-header" className="margin-section-header"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setLinkedCollapsed((current) => !current)} onClick={() => {
setLinkedCollapsed((current) => !current)
recordDisplaySetting('Toggled English linked text accordion')
}}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault() event.preventDefault()
setLinkedCollapsed((current) => !current) setLinkedCollapsed((current) => !current)
recordDisplaySetting('Toggled English linked text accordion')
} }
}} }}
> >
@@ -1003,6 +1103,8 @@ function App() {
} }
function renderCommentarySection() { function renderCommentarySection() {
if (!showCommentaryPanel) return null
if (activeSession?.commentaryEnabled === false) { if (activeSession?.commentaryEnabled === false) {
return ( return (
<article className="resource-card"> <article className="resource-card">
@@ -1021,11 +1123,15 @@ function App() {
className="margin-section-header" className="margin-section-header"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setCommentaryCollapsed((current) => !current)} onClick={() => {
setCommentaryCollapsed((current) => !current)
recordDisplaySetting('Toggled commentary accordion')
}}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault() event.preventDefault()
setCommentaryCollapsed((current) => !current) setCommentaryCollapsed((current) => !current)
recordDisplaySetting('Toggled commentary accordion')
} }
}} }}
> >
@@ -1060,6 +1166,8 @@ function App() {
} }
function renderDictionarySection() { function renderDictionarySection() {
if (!showDictionaryPanel) return null
if (activeSession?.dictionaryEnabled === false) { if (activeSession?.dictionaryEnabled === false) {
return ( return (
<article className="resource-card"> <article className="resource-card">
@@ -1078,11 +1186,15 @@ function App() {
className="margin-section-header" className="margin-section-header"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setDictionaryCollapsed((current) => !current)} onClick={() => {
setDictionaryCollapsed((current) => !current)
recordDisplaySetting('Toggled dictionary accordion')
}}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault() event.preventDefault()
setDictionaryCollapsed((current) => !current) setDictionaryCollapsed((current) => !current)
recordDisplaySetting('Toggled dictionary accordion')
} }
}} }}
> >
@@ -1158,7 +1270,21 @@ function App() {
<button type="button" onClick={() => shiftPage(-1)}> <button type="button" onClick={() => shiftPage(-1)}>
Previous page Previous page
</button> </button>
<span>King James Version - {pageRange}</span> <div className="page-position">
<span>{pageRange}</span>
<div className="chapter-tabs" aria-label="Chapter selector">
{chapters.map((chapter) => (
<button
key={chapter}
type="button"
className={chapter === selectedVerse?.chapter ? 'active' : ''}
onClick={() => selectedVerse && jumpTo(selectedVerse.book, chapter, 1)}
>
{chapter}
</button>
))}
</div>
</div>
<button type="button" onClick={() => shiftPage(1)}> <button type="button" onClick={() => shiftPage(1)}>
Next page Next page
</button> </button>
@@ -1182,20 +1308,24 @@ function App() {
{activeSession?.strongsEnabled === false ? ( {activeSession?.strongsEnabled === false ? (
<p className="resource-disabled">Strong's links are disabled for this session.</p> <p className="resource-disabled">Strong's links are disabled for this session.</p>
) : ( ) : (
<> <div className="margin-resource-grid">
{renderOriginalLanguageLine()} {renderOriginalLanguageLine()}
{renderLinkedEnglishLine()} {renderLinkedEnglishLine()}
{selectedStrongEntry ? ( {showDefinitionPanel && selectedStrongEntry ? (
<article className="strong-focus" aria-label="Strong's definition"> <article className="strong-focus" aria-label="Strong's definition">
<header <header
className="margin-section-header" className="margin-section-header"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setDefinitionCollapsed((current) => !current)} onClick={() => {
setDefinitionCollapsed((current) => !current)
recordDisplaySetting('Toggled Strong definition accordion')
}}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault() event.preventDefault()
setDefinitionCollapsed((current) => !current) setDefinitionCollapsed((current) => !current)
recordDisplaySetting('Toggled Strong definition accordion')
} }
}} }}
> >
@@ -1223,10 +1353,12 @@ function App() {
) : null} ) : null}
</article> </article>
) : null} ) : null}
</> </div>
)} )}
{renderCommentarySection()} <div className="margin-resource-grid">
{renderDictionarySection()} {renderCommentarySection()}
{renderDictionarySection()}
</div>
</aside> </aside>
) )
} }
@@ -1617,8 +1749,15 @@ function App() {
<label className="checkbox-row"> <label className="checkbox-row">
<input <input
type="checkbox" type="checkbox"
checked={!originalCollapsed} checked={showOriginalLine}
onChange={(event) => setOriginalCollapsed(!event.target.checked)} onChange={(event) => {
setShowOriginalLine(event.target.checked)
recordDisplaySetting(
event.target.checked
? 'Show original language line'
: 'Hide original language line',
)
}}
/> />
Show original language line Show original language line
</label> </label>
@@ -1640,8 +1779,13 @@ function App() {
<label className="checkbox-row"> <label className="checkbox-row">
<input <input
type="checkbox" type="checkbox"
checked={!linkedCollapsed} checked={showLinkedLine}
onChange={(event) => setLinkedCollapsed(!event.target.checked)} onChange={(event) => {
setShowLinkedLine(event.target.checked)
recordDisplaySetting(
event.target.checked ? 'Show English linked text' : 'Hide English linked text',
)
}}
/> />
Show English linked text Show English linked text
</label> </label>
@@ -1652,12 +1796,47 @@ function App() {
<label className="checkbox-row"> <label className="checkbox-row">
<input <input
type="checkbox" type="checkbox"
checked={!definitionCollapsed} checked={showDefinitionPanel}
onChange={(event) => setDefinitionCollapsed(!event.target.checked)} onChange={(event) => {
setShowDefinitionPanel(event.target.checked)
recordDisplaySetting(
event.target.checked ? 'Show Strong definition' : 'Hide Strong definition',
)
}}
/> />
Show expanded definition Show expanded definition
</label> </label>
</section> </section>
<section className="modal-section">
<h3>Reference Panels</h3>
<label className="checkbox-row">
<input
type="checkbox"
checked={showCommentaryPanel}
onChange={(event) => {
setShowCommentaryPanel(event.target.checked)
recordDisplaySetting(
event.target.checked ? 'Show commentary panel' : 'Hide commentary panel',
)
}}
/>
Show commentary
</label>
<label className="checkbox-row">
<input
type="checkbox"
checked={showDictionaryPanel}
onChange={(event) => {
setShowDictionaryPanel(event.target.checked)
recordDisplaySetting(
event.target.checked ? 'Show dictionary panel' : 'Hide dictionary panel',
)
}}
/>
Show dictionary
</label>
</section>
</div> </div>
</section> </section>
</div> </div>
@@ -1669,32 +1848,58 @@ function App() {
return ( return (
<aside <aside
className={notesDrawerOpen ? 'notes-drawer open' : 'notes-drawer'} className={[
'notes-drawer',
notesDrawerOpen || notesPinned ? 'open' : '',
notesPinned ? 'pinned' : '',
].filter(Boolean).join(' ')}
aria-label="Notes drawer" aria-label="Notes drawer"
onMouseEnter={() => setNotesDrawerOpen(true)}
onMouseLeave={() => setNotesDrawerOpen(false)}
onDragEnter={(event) => { onDragEnter={(event) => {
event.preventDefault() event.preventDefault()
setNotesDrawerOpen(true) scheduleNotesDragOpen()
}} }}
onDragLeave={clearNotesDragOpen}
onDragOver={(event) => event.preventDefault()} onDragOver={(event) => event.preventDefault()}
onDrop={(event) => { onDrop={(event) => {
event.preventDefault() event.preventDefault()
clearNotesDragOpen()
appendNoteClip(event.dataTransfer.getData('text/plain')) appendNoteClip(event.dataTransfer.getData('text/plain'))
setNotesDrawerOpen(true) setNotesDrawerOpen(true)
}} }}
> >
<div className="notes-drawer-grip" aria-hidden="true"> <button
type="button"
className="notes-drawer-grip"
aria-label={notesDrawerOpen || notesPinned ? 'Close notes drawer' : 'Open notes drawer'}
onClick={() => {
if (notesPinned) return
setNotesDrawerOpen((current) => !current)
}}
>
<span>N</span> <span>N</span>
<span>O</span> <span>O</span>
<span>T</span> <span>T</span>
<span>E</span> <span>E</span>
<span>S</span> <span>S</span>
</div> </button>
<div className="notes-workspace"> <div className="notes-workspace">
<header> <header>
<span className="eyebrow">Notes</span> <div>
<h2>Study Notes</h2> <span className="eyebrow">Notes</span>
<h2>Study Notes</h2>
</div>
<button
type="button"
className="pin-notes"
onClick={() => {
const nextPinned = !notesPinned
setNotesPinned(nextPinned)
setNotesDrawerOpen(nextPinned)
recordDisplaySetting(nextPinned ? 'Pinned notes drawer' : 'Unpinned notes drawer')
}}
>
{notesPinned ? 'Unpin' : 'Pin'}
</button>
</header> </header>
<textarea <textarea
value={noteDraft} value={noteDraft}
@@ -1751,8 +1956,16 @@ function App() {
) )
} }
const splitTemplate = `${splitPercent}fr 6px ${100 - splitPercent}fr`
return ( return (
<main className={railOpen ? 'app-shell menu-open' : 'app-shell'}> <main
className={[
'app-shell',
railOpen ? 'menu-open' : '',
notesPinned ? 'notes-pinned' : '',
].filter(Boolean).join(' ')}
>
<aside <aside
className="tool-rail" className="tool-rail"
aria-label="Main tools" aria-label="Main tools"
@@ -1792,14 +2005,17 @@ function App() {
</button> </button>
</aside> </aside>
<section className="reader"> <section className="reader" ref={readerRef}>
{loading ? ( {loading ? (
<div className="status-panel">Loading local library...</div> <div className="status-panel">Loading local library...</div>
) : error ? ( ) : error ? (
<div className="status-panel error">{error}</div> <div className="status-panel error">{error}</div>
) : selectedVerse ? ( ) : selectedVerse ? (
<> <>
<header className="top-bar"> <header
className="top-bar"
style={{ gridTemplateColumns: splitTemplate }}
>
<div className="bible-top-tools"> <div className="bible-top-tools">
<div className="page-controls" aria-label="Page selectors"> <div className="page-controls" aria-label="Page selectors">
<label> <label>
@@ -1885,12 +2101,18 @@ function App() {
<button type="submit">Go</button> <button type="submit">Go</button>
</form> </form>
</div> </div>
<div className="split-spacer" aria-hidden="true" />
<div className="study-top-tools" aria-label="Study margin tools"> <div className="study-top-tools" aria-label="Study margin tools">
<span>Study margin tools</span> <button type="button" onClick={() => setSettingsModalOpen(true)}>
Margin settings
</button>
</div> </div>
</header> </header>
<div className={`study-desk ${viewMode}`}> <div
className={`study-desk ${viewMode}`}
style={{ gridTemplateColumns: splitTemplate }}
>
{viewMode === 'compare' ? ( {viewMode === 'compare' ? (
<div className="page-system compare-system" aria-label="Compare translations"> <div className="page-system compare-system" aria-label="Compare translations">
<div className="compare-pages"> <div className="compare-pages">
@@ -1918,6 +2140,13 @@ function App() {
{renderPageTurner()} {renderPageTurner()}
{renderBookTabs()} {renderBookTabs()}
</div> </div>
<div
className="split-resizer"
role="separator"
aria-label="Resize Bible and study margin"
aria-orientation="vertical"
onMouseDown={beginSplitResize}
/>
{renderStudyMargin()} {renderStudyMargin()}
</> </>
)} )}