From f2b99cec3c1ae7d076d0893b746994ad2f685005 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 15 Jul 2026 18:51:35 -0500 Subject: [PATCH] Implement Bible desk study layout --- CHANGELOG.md | 2 + ROADMAP.md | 4 + docs/product-tracks.md | 7 +- docs/project-state-2026-07-15.md | 24 +- 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 | 312 ++++++++++- src/App.tsx | 925 +++++++++++++++++++++---------- src/sessionPortability.ts | 31 +- src/workspace.ts | 27 +- 13 files changed, 1034 insertions(+), 310 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d62aed0..77bb2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Completed Phase 9 Bible desk layout by replacing the 50/50 reader/resource split with a resizable Bible page, verse-focused margin, and tabbed study stack. +- Added verse-anchored margin notes with markers, translation/context anchors, visible verse markers, and export metadata in JSON, Markdown, HTML, and CSV. - Changed the HotView HMR host from loopback-only `127.0.0.1` to the detected Tailscale IPv4 address, with a `VITE_HMR_HOST` override for other private intranet addresses. - Updated conventions and project-state notes before the planned consolidation phase. - Completed Phase 8 consolidation by reorganizing the roadmap around durable product tracks and documenting app architecture boundaries. diff --git a/ROADMAP.md b/ROADMAP.md index 4e315fa..439bf26 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -80,6 +80,10 @@ Use these tracks for future planning. Pick one active implementation phase at a Goal: make the reader, study margin, notes, search, and resource layout feel like a serious Bible study workspace. +Current milestone: + +- Phase 9: Bible desk layout and verse-anchored notes. Complete. + Near candidates: - Improve search result navigation and saved searches. diff --git a/docs/product-tracks.md b/docs/product-tracks.md index 5a72277..c7ec82f 100644 --- a/docs/product-tracks.md +++ b/docs/product-tracks.md @@ -9,10 +9,11 @@ Purpose: make the app feel like a serious Bible study workspace. Current foundation: - Page-first reader. -- Study margin. +- Bible desk layout with a resizable Bible page, verse-focused margin, and tabbed study stack. - Session-aware resource toggles. - Strong's links and definitions. -- Notes drawer. +- Verse-anchored margin notes with markers and export metadata. +- Notebook book in the study stack. - Timeline. - Search. - Maps and audio controls where resources exist. @@ -21,7 +22,7 @@ Good next milestones: - Cross-reference UX after a LibreBible package exists. - Better search result navigation and saved searches. -- Highlights and richer note clipping. +- Highlights, richer note clipping, and image/link notebook blocks. - Sermon/study document workflows. - Responsive layout and accessibility pass. diff --git a/docs/project-state-2026-07-15.md b/docs/project-state-2026-07-15.md index d85c706..63fd19d 100644 --- a/docs/project-state-2026-07-15.md +++ b/docs/project-state-2026-07-15.md @@ -78,18 +78,7 @@ Missing or immature areas include: ## Recommended Next Step -The next phase should be consolidation, not another feature sprint. - -Consolidation should turn the completed Phase 1-7 foundation into durable product tracks: - -- Study experience. -- Resource ecosystem. -- Persistence and sync. -- Platform adapters. -- Public responsibility. -- Collaboration. - -The goal is to preserve the platform-agnostic architecture before adding more features. +After Phase 9, the next work should stay inside one product track at a time. The strongest near candidate is another Study Experience milestone focused on search result navigation, saved searches, richer clipping, and notebook document blocks. Cross-references should wait for a LibreBible cross-reference package. ## Phase 8 Result @@ -97,3 +86,14 @@ Phase 8 completed the consolidation pass. The roadmap now keeps completed founda - [Product Tracks](product-tracks.md) - [App Architecture Boundaries](app-architecture-boundaries.md) + +## Phase 9 Result + +Phase 9 started the Study Experience track by changing the workspace into a Bible desk: + +- The default desktop layout is now Bible page, verse-focused margin, and study stack. +- The Bible and margin columns resize, and the study stack can be pinned or collapsed into a right-edge tab bar. +- Long-form resources moved into stack books: Notebook, Dictionary, Lexicon, Atlas, Commentary, and Search. +- The middle margin now focuses on the hovered or clicked verse and keeps compact verse material there. +- Margin notes can be written directly against a verse, carry markers like `!` and `?`, and preserve book/chapter/verse plus translation context for export. +- Session exports now include margin-note anchor metadata in JSON, Markdown, HTML, and CSV. diff --git a/package-lock.json b/package-lock.json index 5c6b089..00bc349 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libre-study", - "version": "0.1.35", + "version": "0.1.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libre-study", - "version": "0.1.35", + "version": "0.1.36", "dependencies": { "@tauri-apps/api": "^2.11.1", "jszip": "^3.10.1", diff --git a/package.json b/package.json index 738a268..3b84067 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libre-study", "private": true, - "version": "0.1.35", + "version": "0.1.36", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fc8ab4f..b845dfc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "app" -version = "0.1.35" +version = "0.1.36" dependencies = [ "log", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index da2874a..884a22b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "app" -version = "0.1.35" +version = "0.1.36" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f67e016..e2b3ef0 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.35", + "version": "0.1.36", "identifier": "org.librestudy.desktop", "build": { "frontendDist": "../dist", diff --git a/src/App.css b/src/App.css index 2409924..49130f2 100644 --- a/src/App.css +++ b/src/App.css @@ -378,7 +378,7 @@ textarea:focus-visible { .study-desk.study, .study-desk.notes, .study-desk.compare { - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + grid-template-columns: minmax(0, 40fr) 6px minmax(0, 20fr) 6px minmax(0, 40fr); } .split-resizer { @@ -519,9 +519,10 @@ textarea:focus-visible { } .page-verse { + position: relative; break-inside: avoid; margin: 0 0 9px; - padding: 3px 4px; + padding: 3px 4px 3px 16px; color: #1f211e; font-size: 18px; line-height: 1.58; @@ -533,6 +534,41 @@ textarea:focus-visible { background: #f4ecd8; } +.page-verse.previewed { + border-radius: 5px; + background: #f8f2e4; +} + +.page-verse.has-margin-note::before, +.page-verse.range-anchored::before { + position: absolute; + top: 5px; + bottom: 5px; + left: 2px; + width: 7px; + border-top: 1px solid rgba(47, 111, 94, 0.45); + border-bottom: 1px solid rgba(47, 111, 94, 0.45); + border-left: 2px solid rgba(47, 111, 94, 0.48); + border-radius: 7px 0 0 7px; + content: ''; +} + +.verse-note-markers { + display: inline-grid; + place-items: center; + min-width: 16px; + min-height: 16px; + margin-right: 5px; + border: 1px solid rgba(47, 111, 94, 0.32); + border-radius: 50%; + background: rgba(47, 111, 94, 0.1); + color: #225446; + font-family: ui-sans-serif, system-ui, sans-serif; + font-size: 10px; + font-weight: 900; + vertical-align: middle; +} + .page-verse sup { margin-right: 5px; color: #8a6d2e; @@ -622,7 +658,7 @@ textarea:focus-visible { .margin-resource-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1fr); gap: 8px; align-items: start; } @@ -826,10 +862,63 @@ textarea:focus-visible { .commentary-card, .dictionary-card, .map-card, -.audio-card { +.audio-card, +.margin-note-card, +.compact-commentary-card, +.margin-stack-links { grid-column: 1 / -1; } +.margin-note-card { + background: #fffdf8; +} + +.marker-row, +.margin-stack-links div { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.marker-row button { + display: grid; + place-items: center; + width: 28px; + height: 28px; + padding: 0; + border-color: #d8cfbf; + background: #fbf8ef; + color: #225446; + font-size: 15px; + font-weight: 900; +} + +.marker-row button.active { + border-color: #2f6f5e; + background: #2f6f5e; + color: #fffef8; +} + +.margin-note-input { + min-height: 94px; + resize: vertical; + font-size: 13px; +} + +.compact-commentary-card p { + margin: 0; + color: #3f443d; + font-size: 12px; + line-height: 1.42; +} + +.compact-commentary-card button, +.margin-stack-links button { + justify-self: start; + border-color: #d8cfbf; + background: #fbf8ef; +} + .commentary-list, .dictionary-list { display: grid; @@ -1083,6 +1172,221 @@ textarea:focus-visible { gap: 14px; } +.study-stack { + display: grid; + min-width: 0; + min-height: 0; +} + +.study-stack.pinned { + grid-template-columns: 42px minmax(0, 1fr); +} + +.study-stack.unpinned { + position: relative; + z-index: 18; + width: 44px; +} + +.study-stack.unpinned .study-stack-panel { + position: fixed; + top: 0; + right: 44px; + width: min(560px, calc(100vw - 104px)); + height: 100vh; + box-shadow: -18px 0 42px rgba(17, 20, 17, 0.24); +} + +.study-stack-tabs { + display: grid; + grid-auto-rows: minmax(42px, auto); + gap: 0; + align-content: start; + min-height: 0; + border: 1px solid #d1cab9; + border-radius: 8px 0 0 8px; + background: #1f231f; + overflow: hidden; +} + +.study-stack-tabs button { + width: 100%; + min-height: 42px; + padding: 5px 3px; + border: 0; + border-bottom: 1px solid #3c4239; + border-radius: 0; + background: #252b25; + color: #f6f3eb; + font-size: 10px; + line-height: 1.05; + writing-mode: vertical-rl; + text-orientation: mixed; +} + +.study-stack-tabs button.active { + background: #d6a84f; + color: #141510; +} + +.study-stack-tabs .pin-stack { + background: #2f6f5e; + color: #fffef8; + font-weight: 900; +} + +.study-stack-panel { + min-width: 0; + min-height: 0; + overflow: auto; + border: 1px solid #d4cdbd; + border-left: 0; + border-radius: 0 8px 8px 0; + background: #fffef8; + box-shadow: 0 12px 30px rgba(44, 48, 42, 0.08); +} + +.stack-book { + display: grid; + gap: 14px; + align-content: start; + min-height: 100%; + padding: 18px; + box-sizing: border-box; +} + +.stack-book > header { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: start; + padding-bottom: 12px; + border-bottom: 1px solid #e4e0d6; +} + +.stack-book h2, +.stack-book h3, +.stack-book h4 { + margin: 0; + letter-spacing: 0; +} + +.stack-book > header h2 { + color: #202124; + font-size: 24px; + line-height: 1.1; +} + +.stack-book > header span:last-child { + color: #657061; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.notebook-book textarea { + min-height: 360px; + resize: vertical; + font-size: 14px; +} + +.article-book article { + display: grid; + gap: 10px; + padding-bottom: 18px; + border-bottom: 1px solid #eee7d8; +} + +.article-book h3 { + color: #202124; + font-size: 24px; + line-height: 1.15; +} + +.article-book h4 { + color: #657061; + font-size: 12px; + font-weight: 900; + text-transform: uppercase; +} + +.article-book p { + margin: 0; + color: #3f443d; + font-size: 15px; + line-height: 1.58; +} + +.article-kicker, +.article-pronunciation { + color: #657061; + font-size: 13px; + font-weight: 800; +} + +.wiki-article-list { + display: grid; + gap: 20px; +} + +.wiki-article-list article section { + display: grid; + gap: 6px; +} + +.stack-token-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 8px; +} + +.stack-token-grid button { + display: grid; + gap: 3px; + justify-items: start; + border-color: #ded6c7; + background: #fbf8ef; + text-align: left; +} + +.stack-token-grid button.active { + border-color: #2f6f5e; + background: #eef6f1; +} + +.stack-token-grid span { + color: #657061; + font-weight: 600; +} + +.search-result-list { + display: grid; + gap: 8px; + overflow: auto; +} + +.search-result-list button { + display: grid; + gap: 4px; + justify-items: start; + padding: 10px; + border-color: #ded6c7; + background: #fbf8ef; + color: #202124; + text-align: left; +} + +.search-result-list button.active { + border-color: #2f6f5e; + background: #eef6f1; +} + +.search-result-list span { + color: #3f443d; + font-weight: 500; + line-height: 1.4; +} + .notes-drawer { position: fixed; top: 0; diff --git a/src/App.tsx b/src/App.tsx index f18b510..4d39a5b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,10 +44,13 @@ import type { import type { StrongDisplayMode, HostedUser, + SessionNote, + StudyBook, StudySession, StudySettings, ThemeMode, TimelineEntry, + VerseAnchor, ViewMode, } from './workspace' import './App.css' @@ -56,11 +59,30 @@ const tagColors = ['#2f6f5e', '#735c0f', '#8a3ffc', '#b42318', '#245ba7'] const pageSize = 34 const nextcloudSettingsKey = 'libre-study.nextcloud-export.v1' type PageMotion = 'idle' | 'turn-forward' | 'turn-back' +type DeskResizeTarget = 'bible-margin' | 'margin-stack' function reference(verse: Verse) { return `${verse.book} ${verse.chapter}:${verse.verse}` } +function verseAnchor(verse: Verse, translationId: string, endVerse = verse.verse): VerseAnchor { + const startVerse = Math.min(verse.verse, endVerse) + const finalVerse = Math.max(verse.verse, endVerse) + const referenceText = + startVerse === finalVerse + ? `${verse.book} ${verse.chapter}:${startVerse}` + : `${verse.book} ${verse.chapter}:${startVerse}-${finalVerse}` + + return { + book: verse.book, + chapter: verse.chapter, + startVerse, + endVerse: finalVerse, + reference: referenceText, + translationId, + } +} + function replaceChangedVerses(verses: Verse[], hydrate: (verse: Verse) => Verse) { let changed = false const next = verses.map((verse) => { @@ -226,6 +248,7 @@ function App() { const [verses, setVerses] = useState([]) const [translations, setTranslations] = useState([]) const [selectedId, setSelectedId] = useState(null) + const [hoveredId, setHoveredId] = useState(null) const [pageStartId, setPageStartId] = useState(null) const [selectedStrong, setSelectedStrong] = useState(null) const [strongDisplay, setStrongDisplay] = useState('highlight') @@ -266,6 +289,10 @@ function App() { const [showCommentaryPanel, setShowCommentaryPanel] = useState(true) const [showDictionaryPanel, setShowDictionaryPanel] = useState(true) const [splitPercent, setSplitPercent] = useState(50) + const [bibleColumnPercent, setBibleColumnPercent] = useState(40) + const [marginColumnPercent, setMarginColumnPercent] = useState(20) + const [studyStackPinned, setStudyStackPinned] = useState(true) + const [activeStudyBook, setActiveStudyBook] = useState('notebook') const [commentaryEntries, setCommentaryEntries] = useState([]) const [dictionaryEntries, setDictionaryEntries] = useState([]) const [mapContext, setMapContext] = useState({ @@ -321,6 +348,10 @@ function App() { setShowDictionaryPanel(settings.showDictionaryPanel ?? true) setSplitPercent(settings.splitPercent ?? 50) setNotesPinned(settings.notesPinned ?? false) + setBibleColumnPercent(settings.bibleColumnPercent ?? 40) + setMarginColumnPercent(settings.marginColumnPercent ?? 20) + setStudyStackPinned(settings.studyStackPinned ?? true) + setActiveStudyBook(settings.activeStudyBook ?? 'notebook') setThemeMode(settings.themeMode ?? 'light') } @@ -470,9 +501,15 @@ function App() { showDictionaryPanel, splitPercent, notesPinned, + bibleColumnPercent, + marginColumnPercent, + studyStackPinned, + activeStudyBook, themeMode, }), [ + activeStudyBook, + bibleColumnPercent, commentaryCollapsed, definitionCollapsed, dictionaryCollapsed, @@ -481,6 +518,7 @@ function App() { lemmaFontSize, linkedCollapsed, linkedFontSize, + marginColumnPercent, notesPinned, originalCollapsed, showCommentaryPanel, @@ -489,6 +527,7 @@ function App() { showLinkedLine, showOriginalLine, splitPercent, + studyStackPinned, studyNotesCollapsed, themeMode, ], @@ -600,10 +639,12 @@ function App() { const selectedVerse = useMemo( () => + verses.find((verse) => verse.id === hoveredId) ?? + libraryVerses.find((verse) => verse.id === hoveredId) ?? verses.find((verse) => verse.id === selectedId) ?? libraryVerses.find((verse) => verse.id === selectedId) ?? verses[0], - [libraryVerses, selectedId, verses], + [hoveredId, libraryVerses, selectedId, verses], ) const selectedStrongEntry = useMemo(() => { if (!selectedVerse?.strongs.length) return null @@ -841,6 +882,32 @@ function App() { : 'No page loaded' const selectedFootnotes = selectedVerse?.footnotes ?? [] const selectedStudyNotes = selectedVerse?.studyNotes ?? [] + const selectedAnchor = selectedVerse && activeSession + ? verseAnchor(selectedVerse, activeSession.baseTranslation) + : null + const selectedMarginNote = useMemo(() => { + if (!activeSession || !selectedAnchor) return null + return activeSession.notes.find( + (note) => + note.kind === 'margin' && + note.anchor?.book === selectedAnchor.book && + note.anchor.chapter === selectedAnchor.chapter && + note.anchor.startVerse === selectedAnchor.startVerse && + note.anchor.endVerse === selectedAnchor.endVerse, + ) ?? null + }, [activeSession, selectedAnchor]) + const marginNotesByVerse = useMemo(() => { + const notes = new Map() + if (!activeSession) return notes + for (const note of activeSession.notes) { + if (note.kind !== 'margin' || !note.anchor) continue + for (let verse = note.anchor.startVerse; verse <= note.anchor.endVerse; verse += 1) { + const key = `${note.anchor.book}:${note.anchor.chapter}:${verse}` + notes.set(key, [...(notes.get(key) ?? []), note]) + } + } + return notes + }, [activeSession]) const pageFootnotes = useMemo( () => pageVerses.flatMap((verse) => verse.footnotes ?? []), [pageVerses], @@ -882,6 +949,7 @@ function App() { function searchStrong(strong: string) { setQuery(strong) setSelectedStrong(strong) + setActiveStudyBook('lexicon') recordTimeline({ type: 'strong', label: `Strong's ${strong}`, @@ -1010,6 +1078,7 @@ function App() { function selectStrong(verse: Verse, strong: string) { setSelectedId(verse.id) setSelectedStrong(strong) + setActiveStudyBook('lexicon') recordTimeline( { type: 'strong', @@ -1043,21 +1112,42 @@ function App() { recordDisplaySetting(`Theme: ${nextThemeMode}`) } - function beginSplitResize(event: ReactMouseEvent) { + function toggleStudyStackPinned(nextPinned = !studyStackPinned) { + setStudyStackPinned(nextPinned) + setNotesDrawerOpen(nextPinned) + recordDisplaySetting(nextPinned ? 'Pinned study stack' : 'Unpinned study stack') + } + + function beginDeskResize(event: ReactMouseEvent, target: DeskResizeTarget) { 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 pointer = ((moveEvent.clientX - rect.left) / rect.width) * 100 + + if (target === 'bible-margin') { + const combined = bibleColumnPercent + marginColumnPercent + const nextBible = Math.min(combined - 14, Math.max(28, pointer)) + setBibleColumnPercent(nextBible) + setMarginColumnPercent(Math.max(14, combined - nextBible)) + return + } + + const nextMargin = Math.min( + 34, + Math.max(14, pointer - bibleColumnPercent), + ) + const nextStack = 100 - bibleColumnPercent - nextMargin + if (nextStack < 24) return + setMarginColumnPercent(nextMargin) } const stop = () => { window.removeEventListener('mousemove', resize) window.removeEventListener('mouseup', stop) - recordDisplaySetting('Adjusted Bible and study margin split') + recordDisplaySetting('Adjusted Bible desk columns') } window.addEventListener('mousemove', resize) @@ -1084,11 +1174,81 @@ function App() { }) } + function updateMarginNote(value: string) { + if (!activeSession || !selectedAnchor) return + const now = new Date().toISOString() + const existing = selectedMarginNote + if (existing && !value.trim() && !(existing.markers?.length)) { + patchActiveSession({ + notes: activeSession.notes.filter((note) => note.id !== existing.id), + }) + return + } + + const nextNote: SessionNote = { + id: existing?.id ?? timelineId(), + kind: 'margin', + body: value, + createdAt: existing?.createdAt ?? now, + updatedAt: now, + reference: selectedAnchor.reference, + translationId: activeSession.baseTranslation, + anchor: selectedAnchor, + markers: existing?.markers ?? [], + } + + patchActiveSession({ + notes: [ + nextNote, + ...activeSession.notes.filter((note) => note.id !== nextNote.id), + ], + }) + } + + function toggleMarginMarker(marker: string) { + if (!activeSession || !selectedAnchor) return + const now = new Date().toISOString() + const existing = selectedMarginNote + const markers = new Set(existing?.markers ?? []) + if (markers.has(marker)) { + markers.delete(marker) + } else { + markers.add(marker) + } + + if (existing && markers.size === 0 && !existing.body.trim()) { + patchActiveSession({ + notes: activeSession.notes.filter((note) => note.id !== existing.id), + }) + return + } + + const nextNote: SessionNote = { + id: existing?.id ?? timelineId(), + kind: 'margin', + body: existing?.body ?? '', + createdAt: existing?.createdAt ?? now, + updatedAt: now, + reference: selectedAnchor.reference, + translationId: activeSession.baseTranslation, + anchor: selectedAnchor, + markers: [...markers], + } + + patchActiveSession({ + notes: [ + nextNote, + ...activeSession.notes.filter((note) => note.id !== nextNote.id), + ], + }) + } + function appendNoteClip(value: string) { const clip = value.trim() if (!clip) return const nextValue = noteDraft.trim() ? `${noteDraft.trim()}\n\n${clip}` : clip updateNoteDraft(nextValue) + setActiveStudyBook('notebook') } function startTextDrag(event: DragEvent, value: string) { @@ -1097,9 +1257,10 @@ function App() { } function scheduleNotesDragOpen() { - if (notesPinned || notesDragOpenTimeout.current) return + if (studyStackPinned || notesDragOpenTimeout.current) return notesDragOpenTimeout.current = window.setTimeout(() => { setNotesDrawerOpen(true) + setActiveStudyBook('notebook') notesDragOpenTimeout.current = null }, 550) } @@ -1606,141 +1767,6 @@ function App() { ) } - function renderCommentarySection() { - if (!showCommentaryPanel) return null - - if (activeSession?.commentaryEnabled === false) { - return ( -
-
- - Off -
-

Commentary is disabled for this session.

-
- ) - } - - return ( -
-
{ - setCommentaryCollapsed((current) => !current) - recordDisplaySetting('Toggled commentary accordion') - }} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault() - setCommentaryCollapsed((current) => !current) - recordDisplaySetting('Toggled commentary accordion') - } - }} - > - - {commentaryEntries.length ? 'MHCC' : marginResourcesLoading ? 'Loading' : 'None'} -
- {!commentaryCollapsed ? ( - commentaryEntries.length ? ( -
- {commentaryEntries.map((entry) => ( -
- startTextDrag(event, `${entry.reference}\n${entry.text}\n- ${entry.source}`) - } - > -

{entry.reference}

-

{entry.text}

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

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

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

Dictionary lookup is disabled for this session.

-
- ) - } - - return ( -
-
{ - setDictionaryCollapsed((current) => !current) - recordDisplaySetting('Toggled dictionary accordion') - }} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault() - setDictionaryCollapsed((current) => !current) - recordDisplaySetting('Toggled dictionary accordion') - } - }} - > - - {dictionaryEntries.length ? `${dictionaryEntries.length} terms` : marginResourcesLoading ? 'Loading' : 'None'} -
- {!dictionaryCollapsed ? ( - dictionaryEntries.length ? ( -
- {dictionaryEntries.slice(0, 6).map((entry) => ( -
- startTextDrag( - event, - `${entry.term}\n${entry.definitions - .map((definition) => `${definition.sourceName}: ${definition.text}`) - .join('\n\n')}`, - ) - } - > -

{entry.term}

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

- {definition.sourceName} - {definition.text} -

- ))} -
- ))} -
- ) : ( -

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

- ) - ) : null} -
- ) - } - function renderMapSection() { if (activeSession?.mapsEnabled === false) { return ( @@ -1961,6 +1987,393 @@ function App() { ) } + function renderMarginNoteEditor() { + if (!selectedVerse) return null + const markers = selectedMarginNote?.markers ?? [] + + return ( +
+
+ + {selectedAnchor?.reference} +
+
+ {['!', '?', '*'].map((marker) => ( + + ))} +
+