diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..3ec04b3
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+provinent.org
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f2aa5e1
--- /dev/null
+++ b/index.html
@@ -0,0 +1 @@
+
Provinent Scripture Study
Welcome to Provinent Scripture Study! A comprehensive Bible study companion with daily passages, notes, and powerful study tools.
Daily Passages Get a new scripture passage every day with sequential or specific book reading plans.
Highlight & Annotate Right‑click verses to highlight in colors. Take notes with full markdown support.
Original Languages Click any verse to access Strong's Concordance, Greek/Hebrew interlinear, and more.
Reference Bible Open a side‑by‑side reference panel to compare translations while you study.
Export & Import Save your highlights and notes. Import/export your data anytime.
Offline Mode Upload a PDF Bible to read and take notes while offline.
Optional: Upload a PDF to View Offline You can upload a free Berean Standard Bible PDF. This is entirely optional – you can skip this and use the online version.
2. Upload the downloaded PDF here:
Click to select your downloaded PDF
Or drag and drop here Get Started!
Prev Next Resume Reading Plan Random Passage Choose a book Choose a chapter Click any verse for further analysis • Right‑click to highlight
Attribution
Scripture quotations are provided by API from bible.helloao.org , without which this app would not have been possible. And a thank you to Berean Bible for their excellent translation work. All copyrights reserved by their respective owners. Currently selected translation: Berean Standard Bible ®
Text Markdown
B I H1 H2 H3 • List 1. List " Quote </> 🔗 Link
Daily Passage Bible Translation American Standard Version (ASV) King James Version (KJV) Geneva Bible 1599 (GNV) Berean Standard Bible (BSB) New English Translation (NET) This sets the translation for the Passage of the Day.
Reference Panel Bible Version NASB 1995 (Strictly literal) NASB 2020 (Updated literal) ASV (Rigorously literal) ESV (Elegant literal) KJV (Classic literal) GNV (Scholarly literal; Old English) NKJV (Modernized classic) BSB (Study‑focused) CSB (Optimal balance) NET (Scholarly transparent) NIV (Readable dynamic) NLT (Thought‑for‑thought) This sets the translation only for the side‑panel Reference Bible and the Bible Gateway search. Please note that the BSB translation is not supported by Bible Gateway.
Reading Plan 90‑Day Sequential Genesis Psalms Proverbs Ecclesiastes Romans Revelation Select which set of passages the “Next/Prev” buttons iterate over for your reading plan.
Restart Reading Plan This will set the plan back to the first entry. Your highlights and notes will be kept.
Offline Storage Clear Highlights Removes all verse‑highlight colors. Your notes stay intact.
Clear Cached Data Clear all cached Bible passages and resources. This will free up storage space but remove offline access to previously viewed content including any uploaded PDF.
Delete All Stored Data Warning: This will permanently delete all your highlights, notes, uploaded PDF, and settings. This action cannot be undone.
About Provinent Scripture Study Created by: Jordan DiPasquale
Version:
Cancel Save Settings
\ No newline at end of file
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..5d6f768
--- /dev/null
+++ b/main.js
@@ -0,0 +1,610 @@
+import {
+ initBookChapterControls,
+ nextPassage,
+ prevPassage,
+ randomPassage
+} from './modules/navigation.js'
+import { loadPassage, setupFootnoteHandlers } from './modules/passage.js'
+import {
+ clearSearch,
+ currentSearch,
+ handlePDFUpload,
+ navigateToSearchResult,
+ renderPage,
+ savePDFToIndexedDB,
+ searchPDF,
+ setupPDFCleanup,
+ updateCustomPdfInfo,
+ updatePDFZoom
+} from './modules/pdf.js'
+import {
+ clearCache,
+ closeSettings,
+ deleteAllData,
+ exportData,
+ importData,
+ openSettings,
+ restartReadingPlan,
+ resumeReadingPlan,
+ saveSettings
+} from './modules/settings.js'
+import {
+ updateBibleGatewayVersion,
+ loadFromCookies,
+ loadFromStorage,
+ saveToCookies,
+ saveToStorage,
+ state
+} from './modules/state.js'
+import { closeStrongsPopup } from './modules/strongs.js'
+import {
+ exportNotes,
+ initResizeHandles,
+ insertMarkdown,
+ makeToggleSticky,
+ restoreBookChapterUI,
+ restorePanelStates,
+ restoreSidebarState,
+ switchNotesView,
+ toggleNotes,
+ togglePanelCollapse,
+ toggleReferencePanel,
+ toggleSection,
+ updateMarkdownPreview,
+ updateReferencePanel
+} from './modules/ui.js'
+if (typeof pdfjsLib !== 'undefined') {
+ pdfjsLib.GlobalWorkerOptions.workerSrc =
+ 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
+}
+if (typeof marked !== 'undefined') {
+ marked.setOptions({
+ breaks: true,
+ gfm: true
+ });
+}
+function updateDateTime() {
+ const now = new Date();
+ document.getElementById('currentDate').textContent =
+ now.toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+}
+class AppError extends Error {
+ constructor(message, type, originalError) {
+ super(message);
+ this.name = 'AppError';
+ this.type = type;
+ this.originalError = originalError;
+ }
+}
+export function handleError(error, context) {
+ console.error(`Error in ${context}:`, error);
+ const userMessage = error instanceof AppError
+ ? error.message
+ : 'An unexpected error occurred';
+ showError(userMessage);
+ if (window.errorTracker) {
+ window.errorTracker.log(error, context);
+ }
+}
+function setupEventListeners() {
+ document.getElementById('getStartedBtn')
+ .addEventListener('click', completeWelcome);
+ document.getElementById('welcomePdfUploadArea')
+ .addEventListener('click', () => {
+ document.getElementById('welcomePdfUpload').click();
+ });
+ document.getElementById('welcomePdfUpload')
+ .addEventListener('change', handleWelcomePDFUpload);
+ document.querySelector('.theme-toggle')
+ .addEventListener('click', toggleTheme);
+ document.getElementById('openSettingsBtn')
+ .addEventListener('click', openSettings);
+ document.getElementById('exportDataBtn')
+ .addEventListener('click', exportData);
+ document.getElementById('importDataBtn')
+ .addEventListener('click', () => {
+ document.getElementById('importFile').click();
+ });
+ document.getElementById('importFile')
+ .addEventListener('change', importData);
+ document.querySelector('.toggle-notes')
+ .addEventListener('click', toggleNotes);
+ document.getElementById('prevPassageBtn')
+ .addEventListener('click', prevPassage);
+ document.getElementById('nextPassageBtn')
+ .addEventListener('click', nextPassage);
+ document.getElementById('resumeReadingPlanBtn')
+ .addEventListener('click', () => {
+ if (confirm('Return to the daily reading plan where you left off?')) {
+ resumeReadingPlan();
+ }
+ });
+ document.getElementById('randomPassageBtn')
+ .addEventListener('click', randomPassage);
+ document.getElementById('referencePanelToggle')
+ .addEventListener('click', toggleReferencePanel);
+ document.querySelectorAll('.sidebar-section-header')
+ .forEach(h => h.addEventListener('click', () => {
+ const sec = h.dataset.section;
+ toggleSection(sec);
+ }));
+ document.getElementById('referenceTranslation').addEventListener('change', function() {
+ const tempTranslation = this.value;
+ const oldTranslation = state.settings.referenceVersion;
+ state.settings.referenceVersion = tempTranslation;
+ updateBibleGatewayVersion();
+ state.settings.referenceVersion = oldTranslation;
+ });
+ document.addEventListener('DOMContentLoaded', makeToggleSticky);
+ document.querySelectorAll('.collapse-toggle')
+ .forEach(btn => btn.addEventListener('click', function () {
+ const panel = this.closest('[id]');
+ if (panel) togglePanelCollapse(panel.id);
+ }));
+ document.getElementById('referenceSource')
+ .addEventListener('change', updateReferencePanel);
+ document.getElementById('referenceTranslation')
+ .addEventListener('change', updateReferencePanel);
+ document.querySelector('.reference-panel-close')
+ .addEventListener('click', toggleReferencePanel);
+ document.getElementById('prevPage').addEventListener('click', async () => {
+ if (!state.pdf.doc || state.pdf.currentPage <= 1) return;
+ try {
+ if (state.pdf.renderTask) {
+ await state.pdf.renderTask.cancel();
+ state.pdf.renderTask = null;
+ }
+ state.pdf.currentPage--;
+ await renderPage(state.pdf.currentPage);
+ } catch (err) {
+ console.warn('Error navigating to previous page:', err);
+ await loadPDF();
+ }
+ });
+ document.getElementById('nextPage').addEventListener('click', async () => {
+ if (!state.pdf.doc || state.pdf.currentPage >= state.pdf.doc.numPages) return;
+ try {
+ if (state.pdf.renderTask) {
+ await state.pdf.renderTask.cancel();
+ state.pdf.renderTask = null;
+ }
+ state.pdf.currentPage++;
+ await renderPage(state.pdf.currentPage);
+ } catch (err) {
+ console.warn('Error navigating to next page:', err);
+ await loadPDF();
+ }
+ });
+ document.getElementById('pageInput').addEventListener('change', async () => {
+ if (!state.pdf.doc) {
+ document.getElementById('pageInput').value = state.pdf.currentPage;
+ return;
+ }
+ const inp = document.getElementById('pageInput');
+ let p = parseInt(inp.value, 10);
+ if (Number.isNaN(p)) {
+ inp.value = state.pdf.currentPage;
+ return;
+ }
+ p = Math.max(1, Math.min(p, state.pdf.doc.numPages));
+ try {
+ state.pdf.currentPage = p;
+ await renderPage(p);
+ } catch (err) {
+ console.warn('Error navigating to page:', err);
+ inp.value = state.pdf.currentPage;
+ await loadPDF();
+ }
+ });
+ document.getElementById('zoomIn').addEventListener('click', () => {
+ if (!state.pdf.doc) return;
+ const newZoom = Math.min(state.pdf.zoomLevel + 0.25, 3.0);
+ updatePDFZoom(newZoom);
+ });
+ document.getElementById('zoomOut').addEventListener('click', () => {
+ if (!state.pdf.doc) return;
+ const newZoom = Math.max(state.pdf.zoomLevel - 0.25, 0.5);
+ updatePDFZoom(newZoom);
+ });
+ document.getElementById('pdfSearchBtn').addEventListener('click', searchPDF);
+ document.getElementById('pdfSearchInput').addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') searchPDF();
+ });
+ document.getElementById('clearSearchBtn').addEventListener('click', clearSearch);
+ const nextSearchBtn = document.getElementById('nextSearchResult');
+ const prevSearchBtn = document.getElementById('prevSearchResult');
+ if (nextSearchBtn) {
+ nextSearchBtn.addEventListener('click', () => {
+ navigateToSearchResult(currentSearch.currentResult + 1);
+ });
+ }
+ if (prevSearchBtn) {
+ prevSearchBtn.addEventListener('click', () => {
+ navigateToSearchResult(currentSearch.currentResult - 1);
+ });
+ }
+ document.getElementById('notesInput')
+ .addEventListener('input', e => {
+ state.notes = e.target.value;
+ saveToStorage();
+ if (state.settings.notesView === 'markdown') {
+ updateMarkdownPreview();
+ }
+ });
+ document.getElementById('textViewBtn')
+ .addEventListener('click', () => switchNotesView('text'));
+ document.getElementById('markdownViewBtn')
+ .addEventListener('click', () => switchNotesView('markdown'));
+ document.querySelectorAll('.markdown-btn')
+ .forEach(btn => btn.addEventListener('click', () => {
+ const fmt = btn.dataset.format;
+ insertMarkdown(fmt);
+ }));
+ document.querySelectorAll('.notes-controls button')
+ .forEach(btn => btn.addEventListener('click', () => {
+ const fmt = btn.dataset.format;
+ exportNotes(fmt);
+ }));
+ document.querySelectorAll('.color-option')
+ .forEach(opt => opt.addEventListener('click', () => {
+ const col = opt.dataset.color;
+ applyHighlight(col);
+ }));
+ document.getElementById('removeHighlight')
+ .addEventListener('click', () => applyHighlight('none'));
+ document.addEventListener('contextmenu', e => {
+ const verse = e.target.closest('.verse');
+ if (verse) {
+ e.preventDefault();
+ showColorPicker(e, verse);
+ }
+ });
+ document.addEventListener('click', e => {
+ const picker = document.getElementById('colorPicker');
+ if (!picker.contains(e.target) && !e.target.closest('.verse')) {
+ picker.classList.remove('active');
+ }
+ });
+ document.getElementById('popupOverlay')
+ .addEventListener('click', closeStrongsPopup);
+ document.querySelector('#strongsPopup .popup-close')
+ .addEventListener('click', closeStrongsPopup);
+ document.getElementById('settingsOverlay')
+ .addEventListener('click', closeSettings);
+ document.getElementById('closeSettingsBtn')
+ .addEventListener('click', closeSettings);
+ document.getElementById('cancelSettingsBtn')
+ .addEventListener('click', closeSettings);
+ document.getElementById('saveSettingsBtn')
+ .addEventListener('click', saveSettings);
+ document.getElementById('clearHighlightsBtn')
+ .addEventListener('click', clearHighlights);
+ document.getElementById('restartReadingPlanBtn')
+ .addEventListener('click', () => {
+ restartReadingPlan();
+ });
+ document.addEventListener('contentLoaded', () => {
+ setTimeout(setupFootnoteHandlers, 50);
+ });
+ document.getElementById('clearCacheBtn')
+ .addEventListener('click', clearCache);
+ document.getElementById('deleteAllDataBtn')
+ .addEventListener('click', deleteAllData);
+ document.getElementById('settingsPdfUploadBtn')
+ .addEventListener('click', () => {
+ document.getElementById('settingsPdfUpload').click();
+ });
+ document.getElementById('settingsPdfUpload')
+ .addEventListener('change', handlePDFUpload);
+ document.querySelectorAll('.color-theme-option')
+ .forEach(opt => opt.addEventListener('click', () => {
+ const theme = opt.dataset.theme;
+ selectColorTheme(theme);
+ }));
+ document.addEventListener('keydown', e => {
+ const ta = document.getElementById('notesInput');
+ if (document.activeElement !== ta) return;
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
+ switch (e.key.toLowerCase()) {
+ case 'b':
+ e.preventDefault();
+ insertMarkdown('bold');
+ break;
+ case 'i':
+ e.preventDefault();
+ insertMarkdown('italic');
+ break;
+ }
+ }
+ });
+}
+export function arrayBufferToBase64(buf) {
+ let binary = '';
+ const bytes = new Uint8Array(buf);
+ const chunk = 0x8000;
+ for (let i = 0; i < bytes.length; i += chunk) {
+ binary += String.fromCharCode.apply(
+ null,
+ Array.from(bytes.subarray(i, i + chunk))
+ );
+ }
+ return btoa(binary);
+}
+export function base64ToArrayBuffer(b64) {
+ const bin = atob(b64);
+ const arr = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) {
+ arr[i] = bin.charCodeAt(i);
+ }
+ return arr.buffer;
+}
+export function readFileAsArrayBuffer(file) {
+ return new Promise((resolve, reject) => {
+ const r = new FileReader();
+ r.onload = e => resolve(e.target.result);
+ r.onerror = () => reject(new Error('Failed to read file'));
+ r.readAsArrayBuffer(file);
+ });
+}
+export function showLoading(flag) {
+ document.getElementById('loadingOverlay').classList.toggle('active', flag);
+}
+export function showError(msg) {
+ document.getElementById('errorContainer').innerHTML =
+ `${msg}
`;
+}
+export function clearError() {
+ document.getElementById('errorContainer').innerHTML = '';
+}
+async function registerServiceWorker() {
+ if ('serviceWorker' in navigator) {
+ try {
+ const response = await fetch('/sw.js');
+ if (!response.ok) {
+ console.error('Service worker script not found or inaccessible');
+ return null;
+ }
+ const registration = await navigator.serviceWorker.register('/sw.js', {
+ scope: '/'
+ });
+ console.log('Service Worker registered successfully:', registration);
+ return registration;
+ } catch (err) {
+ console.error('Service Worker registration failed:', err);
+ if (err.message.includes('MIME')) {
+ console.error('MIME type issue - ensure server serves sw.js as application/javascript');
+ }
+ return null;
+ }
+ } else {
+ console.log('Service workers are not supported');
+ return null;
+ }
+}
+function updateOfflineStatus(isOffline) {
+ const indicator = document.getElementById('offlineIndicator');
+ if (!indicator) {
+ const newIndicator = document.createElement('div');
+ newIndicator.id = 'offlineIndicator';
+ newIndicator.style.cssText = `
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ padding: 10px;
+ background: ${isOffline ? '#ff6b6b' : '#51cf66'};
+ color: white;
+ border-radius: 5px;
+ z-index: 10000;
+ font-size: 14px;
+ transition: all 0.3s ease;
+ `;
+ newIndicator.textContent = isOffline ? 'Offline Mode' : 'Online';
+ document.body.appendChild(newIndicator);
+ setTimeout(() => {
+ newIndicator.style.opacity = '0';
+ setTimeout(() => newIndicator.remove(), 300);
+ }, 3000);
+ } else {
+ indicator.textContent = isOffline ? 'Offline Mode' : 'Online';
+ indicator.style.background = isOffline ? '#ff6b6b' : '#51cf66';
+ }
+}
+const offlineStyles = `
+#offlineIndicator {
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ padding: 10px 15px;
+ background: #ff6b6b;
+ color: white;
+ border-radius: 5px;
+ z-index: 10000;
+ font-size: 14px;
+ font-weight: bold;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
+ transition: all 0.3s ease;
+}
+#offlineIndicator.online {
+ background: #51cf66;
+}
+`;
+function showColorPicker(ev, verseEl) {
+ const picker = document.getElementById('colorPicker');
+ state.currentVerse = verseEl;
+ picker.style.left = ev.pageX + 'px';
+ picker.style.top = ev.pageY + 'px';
+ picker.classList.add('active');
+}
+function applyHighlight(col) {
+ if (!state.currentVerse) return;
+ const verseRef = state.currentVerse.dataset.verse;
+ state.currentVerse.classList.remove(
+ 'highlight-yellow', 'highlight-green', 'highlight-blue',
+ 'highlight-pink', 'highlight-orange', 'highlight-purple'
+ );
+ if (col !== 'none') {
+ state.currentVerse.classList.add(`highlight-${col}`);
+ state.highlights[verseRef] = col;
+ } else {
+ delete state.highlights[verseRef];
+ }
+ saveToStorage();
+ document.getElementById('colorPicker').classList.remove('active');
+}
+function clearHighlights() {
+ if (!confirm('Delete ALL highlights?')) return;
+ state.highlights = {};
+ document.querySelectorAll('.verse')
+ .forEach(v => v.classList.remove(
+ 'highlight-yellow', 'highlight-green', 'highlight-blue',
+ 'highlight-pink', 'highlight-orange', 'highlight-purple'
+ ));
+ saveToStorage();
+}
+function toggleTheme() {
+ state.settings.theme = state.settings.theme === 'light' ? 'dark' : 'light';
+ applyTheme();
+ saveToStorage();
+ saveToCookies();
+}
+export function applyTheme() {
+ document.documentElement.setAttribute('data-theme', state.settings.theme);
+ document.getElementById('themeIcon').textContent =
+ state.settings.theme === 'light' ? '🌙' : '☀️';
+}
+export function selectColorTheme(t) {
+ state.settings.colorTheme = t;
+ applyColorTheme();
+ document.querySelectorAll('.color-theme-option')
+ .forEach(o => o.classList.remove('selected'));
+ document.querySelector(`.color-theme-option[data-theme="${t}"]`)
+ .classList.add('selected');
+}
+export function applyColorTheme() {
+ document.documentElement.setAttribute('data-color-theme',
+ state.settings.colorTheme);
+}
+async function handleWelcomePDFUpload(ev) {
+ try {
+ const file = ev.target.files[0];
+ if (!file) return;
+ if (file.size > 50 * 1024 * 1024) {
+ alert('PDF file is too large (max 50 MiB).');
+ ev.target.value = '';
+ return;
+ }
+ state.welcomePdfFile = file;
+ document.getElementById('welcomePdfUploadArea').classList.add('has-file');
+ document.getElementById('welcomeUploadText').innerHTML = `
+ ${file.name}
+ Ready to use for offline mode `;
+ } catch (err) {
+ handleError(err, 'handleWelcomePDFUpload');
+ }
+}
+async function completeWelcome() {
+ showLoading(true);
+ try {
+ if (state.welcomePdfFile) {
+ const reader = new FileReader();
+ const arrayBuffer = await new Promise((resolve, reject) => {
+ reader.onload = (e) => resolve(e.target.result);
+ reader.onerror = (e) => reject(new Error('Failed to read file'));
+ reader.readAsArrayBuffer(state.welcomePdfFile);
+ });
+ const bufferCopy = arrayBuffer.slice(0);
+ const loadingTask = pdfjsLib.getDocument({ data: bufferCopy });
+ const pdf = await loadingTask.promise;
+ const base64 = arrayBufferToBase64(arrayBuffer);
+ const pdfData = {
+ name: state.welcomePdfFile.name,
+ data: base64,
+ uploadDate: new Date().toISOString(),
+ numPages: pdf.numPages
+ };
+ await savePDFToIndexedDB(pdfData);
+ state.settings.customPdf = {
+ name: pdfData.name,
+ uploadDate: pdfData.uploadDate,
+ numPages: pdfData.numPages,
+ storedInDB: true
+ };
+ }
+ state.settings.hasSeenWelcome = true;
+ saveToStorage();
+ saveToCookies();
+ document.getElementById('welcomeScreen').classList.add('hidden');
+ await init();
+ } catch (err) {
+ handleError(err, 'completeWelcome');
+ alert('Error processing PDF: ' + err.message +
+ '. You can continue without offline mode.');
+ state.settings.hasSeenWelcome = true;
+ saveToStorage();
+ saveToCookies();
+ document.getElementById('welcomeScreen').classList.add('hidden');
+ await init();
+ } finally {
+ showLoading(false);
+ }
+}
+function attachWelcomeListeners() {
+ document.getElementById('getStartedBtn')
+ .addEventListener('click', completeWelcome);
+ document.getElementById('welcomePdfUploadArea')
+ .addEventListener('click', () => {
+ document.getElementById('welcomePdfUpload').click();
+ });
+ document.getElementById('welcomePdfUpload')
+ .addEventListener('change', handleWelcomePDFUpload);
+}
+async function init() {
+ await loadFromStorage();
+ loadFromCookies();
+ setupPDFCleanup();
+ const style = document.createElement('style');
+ style.textContent = offlineStyles;
+ document.head.appendChild(style);
+ updateOfflineStatus(!navigator.onLine);
+ window.addEventListener('online', () => updateOfflineStatus(false));
+ window.addEventListener('offline', () => updateOfflineStatus(true));
+ if (!state.settings.readingMode) state.settings.readingMode = 'readingPlan';
+ if (!state.settings.readingPlanId) state.settings.readingPlanId = 'default';
+ initBookChapterControls();
+ restoreBookChapterUI();
+ if (!state.settings.hasSeenWelcome) {
+ attachWelcomeListeners();
+ return;
+ }
+ document.getElementById('welcomeScreen').classList.add('hidden');
+ applyTheme();
+ applyColorTheme();
+ restoreSidebarState();
+ restorePanelStates();
+ updateDateTime();
+ initResizeHandles();
+ updateCustomPdfInfo();
+ switchNotesView(state.settings.notesView || 'text');
+ updateBibleGatewayVersion();
+ loadPassage();
+ setupEventListeners();
+ setInterval(updateDateTime, 1_000);
+ setTimeout(async () => {
+ try {
+ await registerServiceWorker();
+ } catch (err) {
+ handleError(err, 'init');
+ }
+ }, 1000);
+ console.log('App initialized successfully');
+}
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+} else {
+ init();
+}
\ No newline at end of file
diff --git a/modules/api.js b/modules/api.js
new file mode 100644
index 0000000..749cbfb
--- /dev/null
+++ b/modules/api.js
@@ -0,0 +1,115 @@
+import {
+ clearError,
+ handleError,
+ showError,
+ showLoading
+} from '../main.js'
+import {
+ afterContentLoad,
+ displayPassage,
+ extractVerseText
+} from './passage.js'
+import {
+ bookNameMapping,
+ state
+} from './state.js'
+const API_BASE_URL = 'https://bible.helloao.org/api';
+const translationMap = {
+ BSB: 'BSB',
+ KJV: 'eng_kjv',
+ NET: 'eng_net',
+ ASV: 'eng_asv',
+ GNV: 'eng_gnv'
+};
+export function apiTranslationCode(uiCode) {
+ return translationMap[uiCode] ?? uiCode;
+}
+export function getApiBookCode(displayName) {
+ const code = bookNameMapping[displayName];
+ if (code) return code;
+ console.warn('Missing book‑code mapping for:', displayName);
+ showError(`Cannot load “${displayName}” – unknown book code.`);
+ throw new Error('Unknown book code');
+}
+export async function fetchChapter(translation, book, chapter) {
+ if (!navigator.onLine) {
+ throw new Error('Offline mode: Cannot fetch new chapters. Using cached data if available.');
+ }
+ const trans = translation.trim();
+ const bk = book.replace(/\s+/g, '').toUpperCase();
+ const ch = Number(chapter);
+ if (!trans || !bk || Number.isNaN(ch) || ch < 1) {
+ throw new Error('Invalid parameters for Bible API request');
+ }
+ const url = `${API_BASE_URL}/${trans}/${bk}/${ch}.json`;
+ try {
+ const resp = await fetch(url, {
+ method: 'GET',
+ headers: { Accept: 'application/json' },
+ cache: 'no-store'
+ });
+ if (!resp.ok) {
+ const txt = await resp.text();
+ throw new Error(`API error ${resp.status}: ${txt}`);
+ }
+ const ct = resp.headers.get('content-type') || '';
+ if (!ct.includes('application/json')) {
+ if (ct.startsWith('<')) {
+ throw new Error('API returned HTML instead of JSON');
+ }
+ try {
+ return JSON.parse(await resp.text());
+ } catch (_) {
+ throw new Error('Unable to parse API response as JSON');
+ }
+ }
+ return resp.json();
+ } catch (err) {
+ handleError(err, 'fetchChapter');
+ }
+}
+export async function loadPassageFromAPI(passageInfo) {
+ try {
+ showLoading(true);
+ const { book, chapter, startVerse, endVerse, displayRef } = passageInfo;
+ state.currentPassageReference = displayRef;
+ const apiMap = apiTranslationCode(state.settings.bibleTranslation);
+ const apiBook = getApiBookCode(book);
+ const chapterData = await fetchChapter(apiMap, apiBook, chapter);
+ if (!chapterData || !chapterData.chapter ||
+ !Array.isArray(chapterData.chapter.content)) {
+ throw new Error('Malformed API response – missing chapter.content');
+ }
+ const chapterFootnotes = chapterData.chapter.footnotes || [];
+ const footnoteCounter = { value: 1 };
+ const verses = chapterData.chapter.content
+ .filter(v =>
+ v.type === 'verse' &&
+ v.number >= startVerse &&
+ v.number <= endVerse
+ )
+ .map(v => {
+ const verseData = extractVerseText(v.content, chapterFootnotes, footnoteCounter);
+ return {
+ number: v.number,
+ text: verseData,
+ reference: `${book} ${chapter}:${v.number}`,
+ rawContent: v.content
+ };
+ });
+ if (verses.length === 0) {
+ throw new Error('No verses found in the requested range');
+ }
+ displayPassage(verses);
+ afterContentLoad();
+ clearError();
+ if (chapterData.translation && chapterData.translation.name) {
+ document.getElementById('bibleName').textContent =
+ chapterData.translation.name;
+ }
+ } catch (err) {
+ handleError(err, 'loadPassageFromAPI');
+ } finally {
+ showLoading(false);
+ }
+}
\ No newline at end of file
diff --git a/modules/navigation.js b/modules/navigation.js
new file mode 100644
index 0000000..9199eed
--- /dev/null
+++ b/modules/navigation.js
@@ -0,0 +1,252 @@
+import {
+ apiTranslationCode,
+ fetchChapter,
+ getApiBookCode,
+ loadPassageFromAPI
+} from './api.js'
+import {
+ clearError,
+ handleError,
+ showError,
+ showLoading
+} from '../main.js'
+import {
+ displayPassage,
+ extractVerseText,
+ loadPassage
+} from './passage.js'
+import {
+ BOOK_ORDER,
+ CHAPTER_COUNTS,
+ bookNameMapping,
+ getActivePlan,
+ saveToStorage,
+ state
+} from './state.js'
+import { updateReferencePanel } from './ui.js'
+export function populateBookDropdown() {
+ const bookSel = document.getElementById('bookSelect');
+ bookSel.innerHTML = '';
+ BOOK_ORDER.forEach(book => {
+ const opt = document.createElement('option');
+ opt.value = book;
+ opt.textContent = book;
+ bookSel.appendChild(opt);
+ });
+}
+export function populateChapterDropdown(selectedBook) {
+ const chapSel = document.getElementById('chapterSelect');
+ chapSel.innerHTML = '';
+ const max = CHAPTER_COUNTS[selectedBook];
+ for (let i = 1; i <= max; i++) {
+ const opt = document.createElement('option');
+ opt.value = i;
+ opt.textContent = i;
+ chapSel.appendChild(opt);
+ }
+}
+export async function loadSelectedChapter(book = null, chapter = null) {
+ const selBook = book || document.getElementById('bookSelect').value;
+ const selChapter = chapter || document.getElementById('chapterSelect').value;
+ const apiBook = getApiBookCode(selBook);
+ try {
+ showLoading(true);
+ const apiTranslation = apiTranslationCode(state.settings.bibleTranslation);
+ const chapterData = await fetchChapter(
+ apiTranslation,
+ apiBook,
+ selChapter
+ );
+ const chapterFootnotes = chapterData.chapter.footnotes || [];
+ const footnoteCounter = { value: 1 };
+ const verses = chapterData.chapter.content
+ .filter(v => v.type === 'verse')
+ .map(v => ({
+ number: v.number,
+ text: extractVerseText(v.content, chapterFootnotes, footnoteCounter),
+ reference: `${selBook} ${selChapter}:${v.number}`
+ }));
+ document.getElementById('passageReference').textContent =
+ `${selBook} ${selChapter}`;
+ state.footnotes = {};
+ displayPassage(verses, `${selBook} ${selChapter}`);
+ clearError();
+ document.getElementById('scriptureSection').scrollTop = 0;
+ if (state.settings.readingMode === 'manual') {
+ state.settings.manualBook = selBook;
+ state.settings.manualChapter = Number(selChapter);
+ saveToStorage();
+ }
+ if (state.settings.referencePanelOpen) {
+ updateReferencePanel();
+ }
+ } catch (err) {
+ handleError(err, 'loadSelectedChapter');
+ showError(`Could not load ${selBook} ${selChapter}: ${err.message}`);
+ } finally {
+ showLoading(false);
+ }
+}
+export function initBookChapterControls() {
+ populateBookDropdown();
+ document.getElementById('bookSelect').addEventListener('change', e => {
+ const book = e.target.value;
+ state.settings.readingMode = 'manual';
+ populateChapterDropdown(book);
+ state.settings.manualBook = book;
+ state.settings.manualChapter = 1;
+ const chapterSel = document.getElementById('chapterSelect');
+ chapterSel.value = '1';
+ loadSelectedChapter(book, 1);
+ saveToStorage();
+ });
+ document.getElementById('chapterSelect').addEventListener('change', () => {
+ const book = document.getElementById('bookSelect').value;
+ const chap = Number(document.getElementById('chapterSelect').value);
+ state.settings.readingMode = 'manual';
+ state.settings.manualBook = book;
+ state.settings.manualChapter = chap;
+ loadSelectedChapter(book, chap);
+ saveToStorage();
+ });
+ populateChapterDropdown(BOOK_ORDER[0]);
+}
+export function manualPrevChapter() {
+ let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook);
+ let chap = state.settings.manualChapter;
+ if (chap > 1) {
+ state.settings.manualChapter = chap - 1;
+ } else {
+ if (bookIdx > 0) {
+ const prevBook = BOOK_ORDER[bookIdx - 1];
+ const maxCh = CHAPTER_COUNTS[prevBook];
+ state.settings.manualBook = prevBook;
+ state.settings.manualChapter = maxCh;
+ } else {
+ return;
+ }
+ }
+ state.settings.readingMode = 'manual';
+ loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter);
+ syncBookChapterSelectors();
+ saveToStorage();
+ if (state.settings.referencePanelOpen) {
+ updateReferencePanel();
+ }
+}
+export function manualNextChapter() {
+ let bookIdx = BOOK_ORDER.indexOf(state.settings.manualBook);
+ let chap = state.settings.manualChapter;
+ const maxCh = CHAPTER_COUNTS[state.settings.manualBook];
+ if (chap < maxCh) {
+ state.settings.manualChapter = chap + 1;
+ } else {
+ if (bookIdx < BOOK_ORDER.length - 1) {
+ const nextBook = BOOK_ORDER[bookIdx + 1];
+ state.settings.manualBook = nextBook;
+ state.settings.manualChapter = 1;
+ } else {
+ return;
+ }
+ }
+ state.settings.readingMode = 'manual';
+ loadSelectedChapter(state.settings.manualBook, state.settings.manualChapter);
+ syncBookChapterSelectors();
+ saveToStorage();
+ if (state.settings.referencePanelOpen) {
+ updateReferencePanel();
+ }
+}
+export function prevPassage() {
+ if (state.settings.readingMode === 'readingPlan') {
+ const len = getActivePlan().length;
+ let newIndex = (state.settings.currentPassageIndex - 1 + len) % len;
+ state.settings.currentPassageIndex = newIndex;
+ loadPassage();
+ } else {
+ manualPrevChapter();
+ }
+ document.getElementById('scriptureSection').scrollTop = 0;
+}
+export function nextPassage() {
+ if (state.settings.readingMode === 'readingPlan') {
+ const len = getActivePlan().length;
+ let newIndex = (state.settings.currentPassageIndex + 1) % len;
+ if (newIndex < 0) newIndex = len - 1;
+ state.settings.currentPassageIndex = newIndex;
+ loadPassage();
+ } else {
+ manualNextChapter();
+ }
+ document.getElementById('scriptureSection').scrollTop = 0;
+}
+export async function randomPassage() {
+ try {
+ state.settings.readingMode = 'manual';
+ const randomLoc = await getRandomBibleLocation();
+ state.settings.manualBook = randomLoc.book;
+ state.settings.manualChapter = randomLoc.chapter;
+ saveToStorage();
+ await loadPassageFromAPI(randomLoc);
+ document.getElementById('passageReference').textContent = randomLoc.displayRef;
+ state.currentPassageReference = randomLoc.displayRef;
+ syncBookChapterSelectors();
+ if (state.settings.referencePanelOpen) {
+ updateReferencePanel();
+ }
+ } catch (err) {
+ handleError(err, 'randomPassage');
+ showError('Could not load a random passage – see console for details.');
+ }
+}
+export function syncBookChapterSelectors() {
+ const bookSel = document.getElementById('bookSelect');
+ const chapterSel = document.getElementById('chapterSelect');
+ if (bookSel.value !== state.settings.manualBook) {
+ bookSel.value = state.settings.manualBook;
+ populateChapterDropdown(state.settings.manualBook);
+ }
+ const curMax = CHAPTER_COUNTS[state.settings.manualBook];
+ const curChap = state.settings.manualChapter;
+ populateChapterDropdown(state.settings.manualBook);
+ chapterSel.value = (curChap <= curMax) ? curChap : curMax;
+}
+export function syncSelectorsToReadingPlan() {
+ if (state.settings.readingMode !== 'readingPlan') return;
+ const plan = getActivePlan();
+ const passage = plan[state.settings.currentPassageIndex];
+ if (!passage || !passage.book) {
+ console.error('Invalid passage object:', passage);
+ return;
+ }
+ const bookSel = document.getElementById('bookSelect');
+ const chapterSel = document.getElementById('chapterSelect');
+ state.settings.manualBook = passage.book;
+ state.settings.manualChapter = passage.chapter;
+ if (bookSel) bookSel.value = passage.book;
+ populateChapterDropdown(passage.book);
+ if (chapterSel) chapterSel.value = passage.chapter;
+ saveToStorage();
+}
+async function getRandomBibleLocation() {
+ try {
+ const randomBook = BOOK_ORDER[Math.floor(Math.random() * BOOK_ORDER.length)];
+ const maxCh = CHAPTER_COUNTS[randomBook];
+ const randomChapter = Math.floor(Math.random() * maxCh) + 1;
+ const apiMap = apiTranslationCode(state.settings.bibleTranslation);
+ const apiBook = bookNameMapping[randomBook] ||
+ randomBook.replace(/\s+/g, '').toUpperCase();
+ const chapterData = await fetchChapter(apiMap, apiBook, randomChapter);
+ const verses = chapterData.chapter.content.filter(v => v.type === 'verse');
+ const verseCount = verses.length || 1;
+ return {
+ book: randomBook,
+ chapter: randomChapter,
+ startVerse: 1,
+ endVerse: verseCount,
+ displayRef: `${randomBook} ${randomChapter}`
+ };
+ } catch (err) {
+ handleError(err, 'getRandomBibleLocation');
+ }
+}
\ No newline at end of file
diff --git a/modules/passage.js b/modules/passage.js
new file mode 100644
index 0000000..0d35df6
--- /dev/null
+++ b/modules/passage.js
@@ -0,0 +1,274 @@
+import { loadPassageFromAPI } from './api.js'
+import { handleError } from '../main.js'
+import { syncSelectorsToReadingPlan } from './navigation.js'
+import {
+ getActivePlan,
+ getCurrentPlanLabel,
+ getTranslationShorthand,
+ saveToStorage,
+ state
+} from './state.js'
+import { showStrongsReference } from './strongs.js'
+import { updateReferencePanel } from './ui.js'
+export function displayPassage(verses) {
+ const container = document.getElementById('scriptureContent');
+ const fragment = document.createDocumentFragment();
+ state.footnotes = {};
+ const allFootnotes = [];
+ verses.forEach(v => {
+ const verseDiv = document.createElement('div');
+ verseDiv.className = 'verse';
+ verseDiv.dataset.verse = v.reference;
+ verseDiv.dataset.verseNumber = v.number;
+ let plainText = v.text.text;
+ plainText = plainText.replace(/<[^>]*>/g, '');
+ plainText = plainText.replace(/\s+/g, ' ').trim();
+ verseDiv.dataset.verseText = plainText;
+ const key = v.reference;
+ if (state.highlights[key]) {
+ verseDiv.classList.add(`highlight-${state.highlights[key]}`);
+ }
+ const numSpan = document.createElement('span');
+ numSpan.className = 'verse-number';
+ numSpan.textContent = v.number;
+ const txtSpan = document.createElement('span');
+ txtSpan.className = 'verse-text';
+ txtSpan.innerHTML = v.text.text;
+ if (v.text.footnotes && v.text.footnotes.length > 0) {
+ state.footnotes[v.reference] = v.text.footnotes;
+ allFootnotes.push(...v.text.footnotes);
+ }
+ verseDiv.appendChild(numSpan);
+ verseDiv.appendChild(txtSpan);
+ fragment.appendChild(verseDiv);
+ });
+ container.innerHTML = '';
+ container.appendChild(fragment);
+ if (allFootnotes.length > 0) {
+ const footnotesContainer = document.createElement('div');
+ footnotesContainer.className = 'footnotes-container';
+ const separator = document.createElement('hr');
+ separator.className = 'footnotes-separator';
+ const heading = document.createElement('h4');
+ heading.className = 'footnotes-heading';
+ heading.textContent = 'Footnotes';
+ const footnotesFragment = document.createDocumentFragment();
+ allFootnotes.sort((a, b) => a.number - b.number).forEach(fn => {
+ const footnoteElement = document.createElement('div');
+ footnoteElement.className = 'footnote';
+ footnoteElement.innerHTML = `
+
+
+ `;
+ footnoteElement.dataset.footnoteId = fn.index;
+ footnoteElement.dataset.footnoteNumber = fn.number;
+ footnotesFragment.appendChild(footnoteElement);
+ });
+ footnotesContainer.appendChild(footnotesFragment);
+ container.appendChild(separator);
+ container.appendChild(heading);
+ container.appendChild(footnotesContainer);
+ }
+ container.addEventListener('click', (e) => {
+ const verse = e.target.closest('.verse');
+ if (verse && !e.target.closest('.footnote-ref')) {
+ showStrongsReference(verse);
+ }
+ }, { once: false });
+ setTimeout(() => {
+ setupFootnoteHandlers();
+ }, 100);
+}
+export function setupFootnoteHandlers() {
+ const scriptureContent = document.getElementById('scriptureContent');
+ if (scriptureContent._footnoteHandler) {
+ scriptureContent.removeEventListener('click', scriptureContent._footnoteHandler);
+ }
+ const footnoteHandler = (e) => {
+ const footnoteRef = e.target.closest('[class*="footnote-ref"]');
+ const footnoteElement = e.target.closest('.footnote');
+ if (footnoteRef) {
+ e.preventDefault();
+ e.stopPropagation();
+ const footnoteId = (footnoteRef.dataset.footnoteId || '').trim();
+ const footnoteNumber = (footnoteRef.dataset.footnoteNumber || '').trim();
+ let targetFootnote = null;
+ if (footnoteId) {
+ targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-id="${footnoteId}"]`);
+ }
+ if (!targetFootnote && footnoteNumber) {
+ targetFootnote = scriptureContent.querySelector(`.footnote[data-footnote-number="${footnoteNumber}"]`);
+ }
+ if (!targetFootnote && footnoteId) {
+ const allFootnotes = scriptureContent.querySelectorAll('.footnote');
+ for (const fn of allFootnotes) {
+ const fnId = (fn.dataset.footnoteId || '').trim();
+ const fnNum = (fn.dataset.footnoteNumber || '').trim();
+ if (fnId === footnoteId) {
+ targetFootnote = fn;
+ break;
+ }
+ }
+ }
+ if (targetFootnote) {
+ targetFootnote.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ targetFootnote.style.backgroundColor = 'var(--verse-hover)';
+ setTimeout(() => {
+ targetFootnote.style.backgroundColor = '';
+ }, 1000);
+ } else {
+ console.log('No target footnote found for ref:', footnoteId, footnoteNumber);
+ }
+ }
+ if (footnoteElement) {
+ e.preventDefault();
+ e.stopPropagation();
+ const footnoteId = (footnoteElement.dataset.footnoteId || '').trim();
+ const footnoteNumber = (footnoteElement.dataset.footnoteNumber || '').trim();
+ let targetRef = null;
+ const selectors = [
+ `[class*="footnote-ref"][data-footnote-id="${footnoteId}"]`,
+ `[class*="footnote-ref"][data-footnote-number="${footnoteNumber}"]`,
+ `[class*="footnote-ref"][data-footnote-id="${footnoteNumber}"]`,
+ `[class*="footnote-ref"][data-footnote-number="${footnoteId}"]`
+ ];
+ for (const selector of selectors) {
+ targetRef = scriptureContent.querySelector(selector);
+ if (targetRef) break;
+ }
+ if (!targetRef) {
+ const allRefs = scriptureContent.querySelectorAll('[class*="footnote-ref"]');
+ for (const ref of allRefs) {
+ const refId = (ref.dataset.footnoteId || '').trim();
+ const refNum = (ref.dataset.footnoteNumber || '').trim();
+ if (refId === footnoteId || refNum === footnoteNumber ||
+ refId === footnoteNumber || refNum === footnoteId) {
+ targetRef = ref;
+ break;
+ }
+ }
+ }
+ if (targetRef) {
+ targetRef.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center'
+ });
+ targetRef.style.backgroundColor = 'var(--verse-hover)';
+ setTimeout(() => {
+ targetRef.style.backgroundColor = '';
+ }, 1000);
+ } else {
+ console.log('No target ref found for footnote:', footnoteId, footnoteNumber);
+ console.log('Available refs:', scriptureContent.querySelectorAll('[class*="footnote-ref"]'));
+ }
+ }
+ };
+ scriptureContent._footnoteHandler = footnoteHandler;
+ scriptureContent.addEventListener('click', footnoteHandler);
+}
+export function extractVerseText(content, chapterFootnotes = [], footnoteCounter) {
+ let txt = '';
+ let footnotes = [];
+ for (const item of content) {
+ if (typeof item === 'string') {
+ txt += ensureProperSpacing(item);
+ } else if (item.text) {
+ txt += ensureProperSpacing(item.text);
+ } else if (item.heading) {
+ txt += ' ' + ensureProperSpacing(item.heading) + ' ';
+ } else if (item.noteId !== undefined) {
+ const footnote = chapterFootnotes.find(fn => fn.noteId === item.noteId);
+ if (footnote) {
+ const footnoteRef = ``;
+ txt += footnoteRef;
+ footnotes.push({
+ index: footnote.noteId,
+ number: footnoteCounter.value,
+ caller: footnote.caller,
+ content: footnote.text,
+ reference: footnote.reference
+ });
+ footnoteCounter.value++;
+ }
+ } else if (item.type === 'verse') {
+ txt += ' ';
+ } else if (item.type === 'chapter') {
+ txt += ' ';
+ } else {
+ if (item.content && Array.isArray(item.content)) {
+ const nestedResult = extractVerseText(item.content, chapterFootnotes, footnoteCounter);
+ txt += nestedResult.text;
+ footnotes.push(...nestedResult.footnotes);
+ }
+ }
+ }
+ txt = txt.replace(/\s+/g, ' ').trim();
+ txt = txt.replace(/\s+([.,;:!?])/g, '$1');
+ txt = txt.replace(/([.,;:!?])(?=\w)/g, '$1 ');
+ txt = txt.replace(/\s*"\s*/g, '" ');
+ txt = txt.replace(/\s*'\s*/g, "' ");
+ return {
+ text: txt,
+ footnotes: footnotes
+ };
+}
+function ensureProperSpacing(text) {
+ if (!text) return '';
+ let cleanedText = text
+ .replace(/\s+/g, ' ')
+ .trim();
+ cleanedText = cleanedText
+ .replace(/([^'"\s])\s+([.,;:!?])/g, '$1$2')
+ .replace(/([.,;:!?])(?=[A-Za-z])/g, '$1 ')
+ .replace(/\s*"\s*/g, (match) => {
+ if (match === '"' || match === ' "') return '"';
+ return '" ';
+ })
+ .replace(/\s*'\s*/g, (match) => {
+ if (match === "'" || match === " '") return "'";
+ return "' ";
+ });
+ cleanedText = cleanedText
+ .replace(/\s+/g, ' ')
+ .replace(/([.,;:!?]) (["'])/g, '$1$2')
+ .replace(/(["']) ([.,;:!?])/g, '$1$2')
+ .trim();
+ return cleanedText;
+}
+export async function loadPassage() {
+ try {
+ const plan = getActivePlan();
+ if (state.settings.currentPassageIndex < 0 || state.settings.currentPassageIndex >= plan.length) {
+ state.settings.currentPassageIndex = 0;
+ }
+ const passage = plan[state.settings.currentPassageIndex];
+ const headerTitleEl = document.getElementById('passageHeaderTitle');
+ if (headerTitleEl) {
+ const transShorthand = getTranslationShorthand();
+ headerTitleEl.textContent = `Holy Bible: ${transShorthand}`;
+ }
+ const planLabelEl = document.getElementById('planLabel');
+ if (planLabelEl) {
+ planLabelEl.textContent = `Reading plan: ${getCurrentPlanLabel()}`;
+ }
+ document.getElementById('passageReference').textContent = passage.displayRef;
+ state.currentPassageReference = passage.displayRef;
+ await loadPassageFromAPI(passage);
+ if (state.settings.referencePanelOpen) {
+ updateReferencePanel();
+ }
+ saveToStorage();
+ if (state.settings.readingMode === 'readingPlan') {
+ syncSelectorsToReadingPlan();
+ }
+ } catch (err) {
+ handleError(err, 'loadPassage');
+ }
+}
+export function afterContentLoad() {
+ const event = new CustomEvent('contentLoaded');
+ document.dispatchEvent(event);
+}
\ No newline at end of file
diff --git a/modules/pdf.js b/modules/pdf.js
new file mode 100644
index 0000000..96e586d
--- /dev/null
+++ b/modules/pdf.js
@@ -0,0 +1,372 @@
+import {
+ arrayBufferToBase64,
+ base64ToArrayBuffer,
+ handleError,
+ readFileAsArrayBuffer,
+ showLoading
+} from '../main.js'
+import {
+ saveToStorage,
+ state
+} from './state.js'
+import { updateReferencePanel } from './ui.js'
+const DB_NAME = 'BibleStudyDB';
+const DB_VERSION = 1;
+export const STORE_NAME = 'pdfStore';
+export let currentSearch = {
+ query: '',
+ results: [],
+ currentResult: -1,
+ highlights: []
+};
+export async function openDB() {
+ try {
+ return new Promise((resolve, reject) => {
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
+ req.onerror = () => reject(req.error);
+ req.onsuccess = () => resolve(req.result);
+ req.onupgradeneeded = ev => {
+ const db = ev.target.result;
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
+ db.createObjectStore(STORE_NAME);
+ }
+ };
+ });
+ } catch (err) {
+ handleError(err, 'openDB');
+ }
+}
+export async function savePDFToIndexedDB(pdfData) {
+ try {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction([STORE_NAME], 'readwrite');
+ const store = tx.objectStore(STORE_NAME);
+ const req = store.put(pdfData, 'customPdf');
+ req.onsuccess = () => resolve();
+ req.onerror = () => reject(req.error);
+ });
+ } catch (err) {
+ handleError(err, 'savePDFToIndexedDB');
+ }
+}
+export async function loadPDFFromIndexedDB() {
+ try {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction([STORE_NAME], 'readonly');
+ const store = tx.objectStore(STORE_NAME);
+ const req = store.get('customPdf');
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
+ } catch (err) {
+ handleError(err, 'loadPDFFromIndexedDB');
+ }
+}
+export async function deletePDFFromIndexedDB() {
+ try {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction([STORE_NAME], 'readwrite');
+ const store = tx.objectStore(STORE_NAME);
+ const req = store.delete('customPdf');
+ req.onsuccess = () => resolve();
+ req.onerror = () => reject(req.error);
+ });
+ } catch (err) {
+ handleError(err, 'deletePDFFromIndexedDB');
+ }
+}
+export async function handlePDFUpload(ev) {
+ const file = ev.target.files[0];
+ if (!file) return;
+ if (file.size > 50 * 1024 * 1024) {
+ alert('PDF file is too large (max 50 MiB).');
+ ev.target.value = '';
+ return;
+ }
+ try {
+ showLoading(true);
+ const buf = await readFileAsArrayBuffer(file);
+ const bufferCopy = buf.slice(0);
+ const pdf = await pdfjsLib.getDocument({ data: bufferCopy }).promise;
+ const storageBuffer = buf.slice(0);
+ const b64 = arrayBufferToBase64(storageBuffer);
+ const pdfData = {
+ name: file.name,
+ data: b64,
+ uploadDate: new Date().toISOString(),
+ numPages: pdf.numPages
+ };
+ await savePDFToIndexedDB(pdfData);
+ state.settings.customPdf = {
+ name: pdfData.name,
+ uploadDate: pdfData.uploadDate,
+ numPages: pdfData.numPages,
+ storedInDB: true
+ };
+ saveToStorage();
+ updateCustomPdfInfo();
+ alert('PDF uploaded successfully! You can now use it in the Reference Panel.');
+ } catch (e) {
+ handleError(err, 'handlePDFUpload');
+ alert('Error uploading PDF: ' + e.message);
+ } finally {
+ showLoading(false);
+ ev.target.value = '';
+ }
+}
+export function updateCustomPdfInfo() {
+ const container = document.getElementById('customPdfInfo');
+ if (state.settings.customPdf) {
+ const date = new Date(state.settings.customPdf.uploadDate)
+ .toLocaleDateString();
+ container.innerHTML = `
+
+
+ Uploaded: ${date} • ${state.settings.customPdf.numPages} pages
+
+ `;
+ document.getElementById('removePdfBtn')
+ .addEventListener('click', removeCustomPdf);
+ } else {
+ container.innerHTML = `
+
+ No custom PDF uploaded
+ `;
+ document.getElementById('pageInput').value = 1;
+ document.getElementById('pageCount').textContent = '?';
+ const zoomDisplay = document.getElementById('zoomLevel');
+ if (zoomDisplay) {
+ zoomDisplay.textContent = '100%';
+ }
+ }
+}
+async function removeCustomPdf() {
+ try {
+ if (!confirm('Delete the uploaded PDF? This cannot be undone.')) return;
+ await deletePDFFromIndexedDB();
+ state.settings.customPdf = null;
+ state.settings.referenceSource = 'biblegateway';
+ saveToStorage();
+ updateCustomPdfInfo();
+ if (document.getElementById('referenceSource').value === 'pdf') {
+ document.getElementById('referenceSource').value = 'biblegateway';
+ updateReferencePanel();
+ }
+ } catch (err) {
+ handleError(err, 'removeCustomPdf');
+ }
+}
+export async function loadPDF() {
+ if (!state.settings.customPdf) {
+ alert('No custom PDF uploaded. Please upload one first.');
+ return;
+ }
+ try {
+ showLoading(true);
+ if (state.pdf.doc) {
+ state.pdf.doc.destroy().catch(() => {});
+ state.pdf.doc = null;
+ }
+ state.pdf.renderTask = null;
+ const pdfData = await loadPDFFromIndexedDB();
+ if (!pdfData) throw new Error('PDF not found in DB');
+ const buf = base64ToArrayBuffer(pdfData.data);
+ const loadingTask = pdfjsLib.getDocument({
+ data: buf,
+ onPassword: (updatePassword, reason) => {
+ const password = prompt(`This PDF requires a ${reason} password. Please enter the password:`);
+ if (password) {
+ updatePassword(password);
+ } else {
+ throw new Error(`Password required to open this PDF. ${reason === 1 ? 'Owner' : 'User'} password needed.`);
+ }
+ }
+ });
+ state.pdf.doc = await loadingTask.promise;
+ const savedPage = state.pdf.currentPage || 1;
+ const validPage = Math.min(savedPage, state.pdf.doc.numPages);
+ state.pdf.currentPage = validPage;
+ const savedZoom = state.pdf.zoomLevel || state.settings.pdfZoom;
+ updatePDFZoom(savedZoom);
+ document.getElementById('pageCount').textContent = state.pdf.doc.numPages;
+ document.getElementById('pageInput').max = state.pdf.doc.numPages;
+ document.getElementById('pageInput').value = validPage;
+ } catch (err) {
+ handleError(err, 'loadPDF');
+ alert('Could not load PDF: ' + err.message);
+ state.pdf.doc = null;
+ state.pdf.renderTask = null;
+ } finally {
+ showLoading(false);
+ }
+}
+export function updatePDFZoom(zoomLevel) {
+ state.settings.pdfZoom = zoomLevel;
+ state.pdf.zoomLevel = zoomLevel;
+ saveToStorage();
+ const zoomDisplay = document.getElementById('zoomLevel');
+ if (zoomDisplay) {
+ zoomDisplay.textContent = `${Math.round(zoomLevel * 100)}%`;
+ }
+ if (state.pdf.doc && state.pdf.currentPage) {
+ renderPage(state.pdf.currentPage);
+ }
+}
+export function setupPDFCleanup() {
+ window.addEventListener('beforeunload', () => {
+ if (state.pdf.renderTask) {
+ state.pdf.renderTask.cancel().catch(() => {});
+ state.pdf.renderTask = null;
+ }
+ });
+}
+export async function renderPage(pageNum) {
+ if (!state.pdf.doc) {
+ console.warn('PDF document not loaded, attempting to reload...');
+ await loadPDF();
+ return;
+ }
+ try {
+ if (state.pdf.renderTask) {
+ try {
+ await state.pdf.renderTask.cancel();
+ } catch (e) {
+ }
+ state.pdf.renderTask = null;
+ }
+ const page = await state.pdf.doc.getPage(pageNum);
+ const canvas = document.getElementById('pdfCanvas');
+ const ctx = canvas.getContext('2d', { willReadFrequently: false });
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ const viewport = page.getViewport({ scale: state.pdf.zoomLevel });
+ canvas.style.width = viewport.width + 'px';
+ canvas.style.height = viewport.height + 'px';
+ canvas.width = viewport.width * window.devicePixelRatio;
+ canvas.height = viewport.height * window.devicePixelRatio;
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+ state.pdf.renderTask = page.render({
+ canvasContext: ctx,
+ viewport: viewport
+ });
+ await state.pdf.renderTask.promise;
+ page.cleanup();
+ state.pdf.renderTask = null;
+ document.getElementById('pageInput').value = pageNum;
+ document.getElementById('prevPage').disabled = pageNum <= 1;
+ document.getElementById('nextPage').disabled = pageNum >= state.pdf.doc.numPages;
+ state.pdf.currentPage = pageNum;
+ saveToStorage();
+ } catch (err) {
+ if (err.name === 'RenderingCancelledException') {
+ console.log('Rendering cancelled normally');
+ return;
+ }
+ console.warn('Render error, reloading PDF:', err);
+ await loadPDF();
+ handleError(err, 'renderPage');
+ }
+}
+export async function searchPDF() {
+ const query = document.getElementById('pdfSearchInput').value.trim();
+ if (!query || !state.pdf.doc) return;
+ const resultsSpan = document.getElementById('pdfSearchResults');
+ const searchBtn = document.getElementById('pdfSearchBtn');
+ resultsSpan.textContent = 'Searching...';
+ searchBtn.disabled = true;
+ searchBtn.textContent = 'Searching...';
+ clearSearchHighlights();
+ currentSearch = {
+ query: query,
+ results: [],
+ currentResult: -1,
+ highlights: []
+ };
+ try {
+ for (let pageNum = 1; pageNum <= state.pdf.doc.numPages; pageNum++) {
+ const page = await state.pdf.doc.getPage(pageNum)
+ const textContent = await page.getTextContent();
+ const text = textContent.items.map(item => item.str).join(' ');
+ const regex = new RegExp(query.replace(/[.*+?^{}()|[\]\\]/g, '\\$&'), 'gi');
+ let match;
+ while ((match = regex.exec(text)) !== null) {
+ currentSearch.results.push({
+ page: pageNum,
+ index: match.index,
+ text: match[0]
+ });
+ }
+ }
+ if (currentSearch.results.length > 0) {
+ resultsSpan.textContent = `Found ${currentSearch.results.length} results`;
+ document.getElementById('clearSearchBtn').style.display = 'inline-block';
+ if (currentSearch.results.length > 1) {
+ document.getElementById('prevSearchResult').style.display = 'inline-block';
+ document.getElementById('nextSearchResult').style.display = 'inline-block';
+ }
+ navigateToSearchResult(0);
+ } else {
+ resultsSpan.textContent = 'No results found';
+ document.getElementById('clearSearchBtn').style.display = 'none';
+ document.getElementById('prevSearchResult').style.display = 'none';
+ document.getElementById('nextSearchResult').style.display = 'none';
+ }
+ } catch (err) {
+ handleError(err, 'searchPDF');
+ resultsSpan.textContent = 'Search failed';
+ document.getElementById('clearSearchBtn').style.display = 'none';
+ document.getElementById('prevSearchResult').style.display = 'none';
+ document.getElementById('nextSearchResult').style.display = 'none';
+ } finally {
+ searchBtn.disabled = false;
+ searchBtn.textContent = 'Search';
+ }
+}
+export function clearSearch() {
+ document.getElementById('pdfSearchInput').value = '';
+ document.getElementById('pdfSearchResults').textContent = '';
+ document.getElementById('clearSearchBtn').style.display = 'none';
+ document.getElementById('prevSearchResult').style.display = 'none';
+ document.getElementById('nextSearchResult').style.display = 'none';
+ clearSearchHighlights();
+ currentSearch = {
+ query: '',
+ results: [],
+ currentResult: -1,
+ highlights: []
+ };
+}
+export async function navigateToSearchResult(index) {
+ try {
+ if (!currentSearch.results.length || index < 0 || index >= currentSearch.results.length) return;
+ if (state.pdf.renderTask) {
+ await state.pdf.renderTask.cancel();
+ state.pdf.renderTask = null;
+ }
+ currentSearch.currentResult = index;
+ const result = currentSearch.results[index];
+ state.pdf.currentPage = result.page;
+ await renderPage(result.page);
+ document.getElementById('pdfSearchResults').textContent =
+ `Result ${index + 1} of ${currentSearch.results.length}`;
+ } catch (err) {
+ handleError(err, 'navigateToSearchResult');
+ }
+}
+function clearSearchHighlights() {
+ currentSearch.highlights.forEach(highlight => {
+ if (highlight.animation) {
+ currentSearch.highlights = [];
+ }
+ });
+ if (state.pdf.currentPage) {
+ renderPage(state.pdf.currentPage);
+ }
+}
\ No newline at end of file
diff --git a/modules/settings.js b/modules/settings.js
new file mode 100644
index 0000000..32d73b3
--- /dev/null
+++ b/modules/settings.js
@@ -0,0 +1,274 @@
+import {
+ applyColorTheme,
+ applyTheme,
+ clearError,
+ handleError,
+ showLoading
+} from '../main.js'
+import { loadPassage } from './passage.js'
+import {
+ deletePDFFromIndexedDB,
+ openDB,
+ STORE_NAME,
+ updateCustomPdfInfo
+} from './pdf.js'
+import {
+ APP_VERSION,
+ BOOK_ORDER,
+ readingPlan,
+ saveToCookies,
+ saveToStorage,
+ state,
+ updateBibleGatewayVersion
+} from './state.js'
+import {
+ restorePanelStates,
+ restoreSidebarState,
+ switchNotesView,
+ updateMarkdownPreview,
+ updateReferencePanel
+} from './ui.js'
+export function exportData() {
+ const payload = {
+ version: '2.0',
+ exportDate: new Date().toISOString(),
+ highlights: state.highlights,
+ notes: state.notes,
+ settings: { ...state.settings }
+ };
+ if (payload.settings.customPdf && payload.settings.customPdf.data) {
+ const { data, ...meta } = payload.settings.customPdf;
+ payload.settings.customPdf = meta;
+ }
+ const blob = new Blob([JSON.stringify(payload, null, 2)],
+ { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `bible-study-backup-${new Date().toISOString().split('T')[0]}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+export function importData(ev) {
+ const file = ev.target.files[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = async e => {
+ try {
+ const incoming = JSON.parse(e.target.result);
+ if (!incoming.settings) {
+ throw new Error('Invalid backup format');
+ }
+ if (!confirm('Import will overwrite all current data (except any uploaded PDF). Continue?')) {
+ return;
+ }
+ Object.assign(state.settings, incoming.settings);
+ if (incoming.highlights) {
+ state.highlights = incoming.highlights;
+ }
+ if (incoming.notes) {
+ state.notes = incoming.notes;
+ }
+ saveToStorage();
+ applyTheme();
+ applyColorTheme();
+ restoreSidebarState();
+ restorePanelStates();
+ updateCustomPdfInfo();
+ switchNotesView(state.settings.notesView || 'text');
+ await loadPassage();
+ document.getElementById('notesInput').value = state.notes;
+ updateMarkdownPreview();
+ alert('Backup imported successfully!');
+ } catch (err) {
+ console.error('Import error:', err);
+ alert('Failed to import backup – see console for details.');
+ }
+ };
+ reader.readAsText(file);
+ ev.target.value = '';
+}
+export function resumeReadingPlan() {
+ const curBook = state.settings.manualBook;
+ const curChapter = state.settings.manualChapter;
+ const idx = findReadingPlanIndex(curBook, curChapter);
+ if (idx !== -1) {
+ state.settings.currentPassageIndex = idx;
+ }
+ state.settings.readingMode = 'readingPlan';
+ loadPassage();
+}
+function findReadingPlanIndex(book, chapter) {
+ for (let i = 0; i < readingPlan.length; i++) {
+ const p = readingPlan[i];
+ if (p.book === book && p.chapter === chapter) {
+ return i;
+ }
+ }
+ return -1;
+}
+export function openSettings() {
+ document.getElementById('bibleTranslationSetting').value =
+ state.settings.bibleTranslation;
+ document.getElementById('referenceVersionSetting').value =
+ state.settings.referenceVersion;
+ document.getElementById('readingPlanId').value =
+ state.settings.readingPlanId || 'default';
+ document.querySelectorAll('.color-theme-option')
+ .forEach(o => o.classList.toggle('selected',
+ o.dataset.theme === state.settings.colorTheme));
+ document.getElementById('settingsModal').classList.add('active');
+ document.getElementById('settingsOverlay').classList.add('active');
+ document.getElementById('appVersion').textContent = APP_VERSION;
+}
+export function closeSettings() {
+ document.getElementById('settingsModal').classList.remove('active');
+ document.getElementById('settingsOverlay').classList.remove('active');
+}
+export async function saveSettings() {
+ try {
+ const newPlanId = document.getElementById('readingPlanId').value;
+ const newTranslation = document.getElementById('bibleTranslationSetting').value;
+ const newReferenceVersion = document.getElementById('referenceVersionSetting').value;
+ const oldPlanId = state.settings.readingPlanId;
+ state.settings.bibleTranslation = newTranslation;
+ state.settings.referenceVersion = newReferenceVersion;
+ state.settings.readingPlanId = newPlanId;
+ if (newReferenceVersion === 'BSB' && state.settings.referenceSource === 'biblegateway') {
+ state.settings.referenceSource = 'biblehub';
+ document.getElementById('referenceSource').value = 'biblehub';
+ } else if (newReferenceVersion === 'NASB1995' && state.settings.referenceSource === 'biblehub') {
+ state.settings.referenceSource = 'biblegateway';
+ document.getElementById('referenceSource').value = 'biblegateway';
+ }
+ if (oldPlanId !== newPlanId) {
+ state.settings.currentPassageIndex = 0;
+ state.settings.readingMode = 'readingPlan';
+ }
+ const selectedTheme = document.querySelector('.color-theme-option.selected');
+ if (selectedTheme) {
+ state.settings.colorTheme = selectedTheme.dataset.theme;
+ applyColorTheme();
+ }
+ document.getElementById('referenceTranslation').value = state.settings.referenceVersion;
+ updateBibleGatewayVersion();
+ saveToStorage();
+ saveToCookies();
+ closeSettings();
+ await loadPassage();
+ if (state.settings.referencePanelOpen) {
+ updateReferencePanel();
+ }
+ alert('Settings saved!' + (oldPlanId !== newPlanId ? ' Starting from beginning of new reading plan.' : ''));
+ } catch (err) {
+ handleError(err, 'saveSettings');
+ }
+}
+export function restartReadingPlan() {
+ if (confirm('Reset the reading plan to the very first passage? Highlights and notes will stay unchanged.')) {
+ state.settings.currentPassageIndex = 0;
+ state.settings.readingMode = 'readingPlan';
+ saveToStorage();
+ loadPassage();
+ alert('Reading plan restarted – you are now at the beginning.');
+ }
+}
+export async function clearCache() {
+ if (confirm('Clear all cached Bible data? This will remove offline access to previously viewed passages.')) {
+ try {
+ if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' });
+ }
+ const db = await openDB();
+ const tx = db.transaction([STORE_NAME], 'readwrite');
+ const store = tx.objectStore(STORE_NAME);
+ await store.clear();
+ alert('Cache cleared successfully');
+ } catch (err) {
+ handleError(err, 'clearCache');
+ alert('Error clearing cache: ' + err.message);
+ }
+ }
+}
+export async function deleteAllData() {
+ const confirmDelete = confirm('WARNING: This will delete ALL your data. Would you like to create a backup first?');
+ if (confirmDelete) {
+ exportData();
+ const proceed = confirm('Backup created. Proceed with deletion?');
+ if (!proceed) return;
+ }
+ try {
+ showLoading(true);
+ localStorage.removeItem('bibleStudyState');
+ document.cookie = 'bibleStudySettings=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
+ try {
+ await deletePDFFromIndexedDB();
+ } catch (e) {
+ console.warn('Could not delete PDF from IndexedDB:', e);
+ }
+ const defaultState = {
+ currentVerse: null,
+ currentVerseData: null,
+ highlights: {},
+ notes: '',
+ settings: {
+ bibleTranslation: 'BSB',
+ referenceVersion: 'NASB1995',
+ passageType: 'default',
+ readingMode: 'readingPlan',
+ manualBook: BOOK_ORDER[0],
+ manualChapter: 1,
+ lastUpdate: null,
+ currentPassageIndex: 0,
+ theme: 'light',
+ colorTheme: 'blue',
+ notesView: 'text',
+ hasSeenWelcome: true,
+ referencePanelOpen: false,
+ referenceSource: 'biblegateway',
+ collapsedSections: {},
+ collapsedPanels: {},
+ panelWidths: {
+ sidebar: 280,
+ referencePanel: 400,
+ scriptureSection: null,
+ notesSection: 400
+ },
+ customPdf: null,
+ pdfZoom: 1
+ },
+ currentPassageReference: '',
+ pdf: {
+ doc: null,
+ currentPage: 1,
+ renderTask: null,
+ zoomLevel: 1
+ },
+ welcomePdfFile: null
+ };
+ Object.assign(state, defaultState);
+ document.getElementById('notesInput').value = '';
+ updateMarkdownPreview();
+ updateCustomPdfInfo();
+ applyTheme();
+ applyColorTheme();
+ updateBibleGatewayVersion();
+ document.getElementById('pageInput').value = 1;
+ document.getElementById('pageCount').textContent = '?';
+ const zoomDisplay = document.getElementById('zoomLevel');
+ if (zoomDisplay) {
+ zoomDisplay.textContent = '100%';
+ }
+ await loadPassage();
+ clearError();
+ closeSettings();
+ alert('All data has been deleted. The app has been reset to defaults.');
+ } catch (err) {
+ handleError(err, 'deleteAllData');
+ alert('Error deleting data. See console for details.');
+ } finally {
+ showLoading(false);
+ }
+}
\ No newline at end of file
diff --git a/modules/state.js b/modules/state.js
new file mode 100644
index 0000000..847efc2
--- /dev/null
+++ b/modules/state.js
@@ -0,0 +1,399 @@
+import { handleError } from '../main.js'
+import { loadPDFFromIndexedDB } from './pdf.js'
+export const APP_VERSION = '1.0.2025.11.01';
+let saveTimeout = null;
+const SAVE_DEBOUNCE_MS = 500;
+export const BOOK_ORDER = [
+ 'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy',
+ 'Joshua', 'Judges', 'Ruth', '1 Samuel', '2 Samuel', '1 Kings', '2 Kings',
+ '1 Chronicles', '2 Chronicles', 'Ezra', 'Nehemiah', 'Esther',
+ 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon',
+ 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel',
+ 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', 'Nahum',
+ 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi',
+ 'Matthew', 'Mark', 'Luke', 'John',
+ 'Acts',
+ 'Romans', '1 Corinthians', '2 Corinthians', 'Galatians', 'Ephesians',
+ 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians',
+ '1 Timothy', '2 Timothy', 'Titus', 'Philemon',
+ 'Hebrews', 'James', '1 Peter', '2 Peter', '1 John', '2 John', '3 John', 'Jude',
+ 'Revelation'
+];
+export const CHAPTER_COUNTS = {
+ Genesis: 50, Exodus: 40, Leviticus: 27, Numbers: 36, Deuteronomy: 34,
+ Joshua: 24, Judges: 21, Ruth: 4, '1 Samuel': 31, '2 Samuel': 24,
+ '1 Kings': 22, '2 Kings': 25, '1 Chronicles': 29, '2 Chronicles': 36,
+ Ezra: 10, Nehemiah: 13, Esther: 10,
+ Job: 42, Psalms: 150, Proverbs: 31, Ecclesiastes: 12, 'Song of Solomon': 8,
+ Isaiah: 66, Jeremiah: 52, Lamentations: 5, Ezekiel: 48, Daniel: 12,
+ Hosea: 14, Joel: 3, Amos: 9, Obadiah: 1, Jonah: 4, Micah: 7,
+ Nahum: 3, Habakkuk: 3, Zephaniah: 3, Haggai: 2, Zechariah: 14, Malachi: 4,
+ Matthew: 28, Mark: 16, Luke: 24, John: 21,
+ Acts: 28,
+ Romans: 16, '1 Corinthians': 16, '2 Corinthians': 13, Galatians: 6,
+ Ephesians: 6, Philippians: 4, Colossians: 4, '1 Thessalonians': 5,
+ '2 Thessalonians': 3, '1 Timothy': 6, '2 Timothy': 4, Titus: 3, Philemon: 1,
+ Hebrews: 13, James: 5, '1 Peter': 5, '2 Peter': 3, '1 John': 5,
+ '2 John': 1, '3 John': 1, Jude: 1,
+ Revelation: 22
+};
+export const state = {
+ currentVerse: null,
+ currentVerseData: null,
+ highlights: {},
+ notes: '', // User's study notes (plain text/markdown)
+ settings: {
+ bibleTranslation: 'BSB',
+ referenceVersion: 'NASB1995',
+ footnotes: {},
+ passageType: 'default',
+ readingMode: 'readingPlan', // 'readingPlan' | 'manual'
+ manualBook: BOOK_ORDER[0],
+ manualChapter: 1,
+ lastUpdate: null,
+ currentPassageIndex: 0,
+ theme: 'light', // 'light' | 'dark'
+ colorTheme: 'blue',
+ notesView: 'text', // 'text' | 'markdown'
+ hasSeenWelcome: false,
+ referencePanelOpen: false,
+ referenceSource: 'biblegateway',// 'biblegateway' | 'biblehub' | 'pdf'
+ collapsedSections: {},
+ collapsedPanels: {},
+ panelWidths: {
+ sidebar: 280,
+ referencePanel: 400,
+ scriptureSection: null,
+ notesSection: 400
+ },
+ customPdf: null,
+ pdfZoom: 1
+ },
+ currentPassageReference: '',
+ pdf: {
+ doc: null,
+ currentPage: 1,
+ renderTask: null,
+ zoomLevel: 1
+ },
+ welcomePdfFile: null
+};
+export function formatBookNameForSource(bookName, source) {
+ const book = bookName.toLowerCase();
+ switch(source) {
+ case 'biblecom':
+ const bibleComCodes = {
+ 'genesis': 'GEN', 'exodus': 'EXO', 'leviticus': 'LEV', 'numbers': 'NUM',
+ 'deuteronomy': 'DEU', 'joshua': 'JOS', 'judges': 'JDG', 'ruth': 'RUT',
+ '1 samuel': '1SA', '2 samuel': '2SA', '1 kings': '1KI', '2 kings': '2KI',
+ '1 chronicles': '1CH', '2 chronicles': '2CH', 'ezra': 'EZR', 'nehemiah': 'NEH',
+ 'esther': 'EST', 'job': 'JOB', 'psalms': 'PSA', 'proverbs': 'PRO',
+ 'ecclesiastes': 'ECC', 'song of solomon': 'SNG', 'isaiah': 'ISA', 'jeremiah': 'JER',
+ 'lamentations': 'LAM', 'ezekiel': 'EZK', 'daniel': 'DAN', 'hosea': 'HOS',
+ 'joel': 'JOL', 'amos': 'AMO', 'obadiah': 'OBA', 'jonah': 'JON',
+ 'micah': 'MIC', 'nahum': 'NAM', 'habakkuk': 'HAB', 'zephaniah': 'ZEP',
+ 'haggai': 'HAG', 'zechariah': 'ZEC', 'malachi': 'MAL', 'matthew': 'MAT',
+ 'mark': 'MRK', 'luke': 'LUK', 'john': 'JHN', 'acts': 'ACT',
+ 'romans': 'ROM', '1 corinthians': '1CO', '2 corinthians': '2CO', 'galatians': 'GAL',
+ 'ephesians': 'EPH', 'philippians': 'PHP', 'colossians': 'COL', '1 thessalonians': '1TH',
+ '2 thessalonians': '2TH', '1 timothy': '1TI', '2 timothy': '2TI', 'titus': 'TIT',
+ 'philemon': 'PHM', 'hebrews': 'HEB', 'james': 'JAS', '1 peter': '1PE',
+ '2 peter': '2PE', '1 john': '1JN', '2 john': '2JN', '3 john': '3JN',
+ 'jude': 'JUD', 'revelation': 'REV'
+ };
+ return bibleComCodes[book] || book.substring(0, 3).toUpperCase();
+ case 'ebibleorg':
+ if (book === 'psalms') return 'PS1';
+ return book.substring(0, 3).toUpperCase() + '1';
+ default:
+ return book.replace(/\s+/g, '_');
+ }
+}
+export function saveToStorage() {
+ if (saveTimeout) {
+ clearTimeout(saveTimeout);
+ }
+ saveTimeout = setTimeout(() => {
+ try {
+ const cleanState = {
+ currentVerse: null,
+ currentVerseData: state.currentVerseData,
+ highlights: state.highlights,
+ notes: state.notes,
+ settings: { ...state.settings },
+ currentPassageReference: state.currentPassageReference,
+ pdf: {
+ currentPage: state.pdf.currentPage,
+ zoomLevel: state.pdf.zoomLevel
+ },
+ welcomePdfFile: null
+ };
+ if (cleanState.settings.customPdf && cleanState.settings.customPdf.data) {
+ const { data, ...meta } = cleanState.settings.customPdf;
+ cleanState.settings.customPdf = { ...meta, storedInDB: true };
+ }
+ localStorage.setItem('bibleStudyState', JSON.stringify(cleanState));
+ saveToCookies();
+ } catch (e) {
+ console.error('Storage error:', e);
+ }
+ }, SAVE_DEBOUNCE_MS);
+}
+export async function loadFromStorage() {
+ const raw = localStorage.getItem('bibleStudyState');
+ if (!raw) return;
+ try {
+ const parsed = JSON.parse(raw);
+ Object.assign(state, parsed);
+ if (parsed.pdf) {
+ state.pdf.currentPage = parsed.pdf.currentPage || 1;
+ state.pdf.zoomLevel = parsed.pdf.zoomLevel || state.settings.pdfZoom;
+ }
+ if (state.settings.customPdf && state.settings.customPdf.storedInDB) {
+ const pdfMeta = await loadPDFFromIndexedDB();
+ if (!pdfMeta) {
+ console.warn('PDF metadata present but DB entry missing');
+ state.settings.customPdf = null;
+ }
+ }
+ document.getElementById('notesInput').value = state.notes;
+ } catch (e) {
+ handleError(err, 'loadFromStorage');
+ }
+}
+export function saveToCookies() {
+ const exp = new Date();
+ exp.setFullYear(exp.getFullYear() + 10);
+ const cookieVal = encodeURIComponent(JSON.stringify({
+ ...state.settings,
+ customPdf: undefined
+ }));
+ document.cookie = `bibleStudySettings=${cookieVal}; expires=${exp.toUTCString()}; path=/; SameSite=Strict`;
+}
+export function loadFromCookies() {
+ const pairs = document.cookie.split(';');
+ for (let pair of pairs) {
+ const [k, v] = pair.trim().split('=');
+ if (k === 'bibleStudySettings') {
+ try {
+ const settings = JSON.parse(decodeURIComponent(v));
+ Object.assign(state.settings, settings);
+ } catch (e) {
+ console.error('Cookie parse error:', e);
+ }
+ }
+ }
+}
+export const readingPlan = [
+ { book: 'Genesis', chapter: 1, startVerse: 1, endVerse: 31, displayRef: 'Genesis 1' },
+ { book: 'Genesis', chapter: 2, startVerse: 1, endVerse: 25, displayRef: 'Genesis 2' },
+ { book: 'Genesis', chapter: 3, startVerse: 1, endVerse: 24, displayRef: 'Genesis 3' },
+ { book: 'Genesis', chapter: 6, startVerse: 5, endVerse: 22, displayRef: 'Genesis 6:5-22' },
+ { book: 'Genesis', chapter: 12, startVerse: 1, endVerse: 9, displayRef: 'Genesis 12:1-9' },
+ { book: 'Genesis', chapter: 22, startVerse: 1, endVerse: 19, displayRef: 'Genesis 22:1-19' },
+ { book: 'Exodus', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Exodus 3' },
+ { book: 'Exodus', chapter: 20, startVerse: 1, endVerse: 21, displayRef: 'Exodus 20:1-21' },
+ { book: 'Leviticus', chapter: 19, startVerse: 1, endVerse: 18, displayRef: 'Leviticus 19:1-18' },
+ { book: 'Numbers', chapter: 14, startVerse: 1, endVerse: 38, displayRef: 'Numbers 14:1-38' },
+ { book: 'Deuteronomy', chapter: 6, startVerse: 1, endVerse: 25, displayRef: 'Deuteronomy 6' },
+ { book: 'Deuteronomy', chapter: 30, startVerse: 1, endVerse: 20, displayRef: 'Deuteronomy 30' },
+ { book: 'Joshua', chapter: 1, startVerse: 1, endVerse: 9, displayRef: 'Joshua 1:1-9' },
+ { book: 'Judges', chapter: 2, startVerse: 6, endVerse: 23, displayRef: 'Judges 2:6-23' },
+ { book: 'Ruth', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Ruth 1' },
+ { book: '1 Samuel', chapter: 16, startVerse: 1, endVerse: 13, displayRef: '1 Samuel 16:1-13' },
+ { book: '2 Samuel', chapter: 7, startVerse: 1, endVerse: 29, displayRef: '2 Samuel 7' },
+ { book: '1 Kings', chapter: 18, startVerse: 1, endVerse: 46, displayRef: '1 Kings 18' },
+ { book: '2 Kings', chapter: 22, startVerse: 1, endVerse: 20, displayRef: '2 Kings 22' },
+ { book: 'Ezra', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Ezra 1' },
+ { book: 'Nehemiah', chapter: 1, startVerse: 1, endVerse: 11, displayRef: 'Nehemiah 1' },
+ { book: 'Esther', chapter: 4, startVerse: 1, endVerse: 17, displayRef: 'Esther 4' },
+ { book: 'Job', chapter: 1, startVerse: 1, endVerse: 22, displayRef: 'Job 1' },
+ { book: 'Psalms', chapter: 1, startVerse: 1, endVerse: 6, displayRef: 'Psalm 1' },
+ { book: 'Psalms', chapter: 19, startVerse: 1, endVerse: 14, displayRef: 'Psalm 19' },
+ { book: 'Psalms', chapter: 23, startVerse: 1, endVerse: 6, displayRef: 'Psalm 23' },
+ { book: 'Psalms', chapter: 51, startVerse: 1, endVerse: 19, displayRef: 'Psalm 51' },
+ { book: 'Psalms', chapter: 103, startVerse: 1, endVerse: 22, displayRef: 'Psalm 103' },
+ { book: 'Psalms', chapter: 119, startVerse: 1, endVerse: 16, displayRef: 'Psalm 119:1-16' },
+ { book: 'Proverbs', chapter: 3, startVerse: 1, endVerse: 12, displayRef: 'Proverbs 3:1-12' },
+ { book: 'Ecclesiastes', chapter: 3, startVerse: 1, endVerse: 22, displayRef: 'Ecclesiastes 3' },
+ { book: 'Song of Solomon', chapter: 2, startVerse: 1, endVerse: 17, displayRef: 'Song of Solomon 2' },
+ { book: 'Isaiah', chapter: 6, startVerse: 1, endVerse: 13, displayRef: 'Isaiah 6' },
+ { book: 'Isaiah', chapter: 9, startVerse: 1, endVerse: 7, displayRef: 'Isaiah 9:1-7' },
+ { book: 'Isaiah', chapter: 40, startVerse: 1, endVerse: 31, displayRef: 'Isaiah 40' },
+ { book: 'Isaiah', chapter: 53, startVerse: 1, endVerse: 12, displayRef: 'Isaiah 53' },
+ { book: 'Jeremiah', chapter: 29, startVerse: 1, endVerse: 14, displayRef: 'Jeremiah 29:1-14' },
+ { book: 'Lamentations', chapter: 3, startVerse: 1, endVerse: 33, displayRef: 'Lamentations 3:1-33' },
+ { book: 'Ezekiel', chapter: 36, startVerse: 22, endVerse: 38, displayRef: 'Ezekiel 36:22-38' },
+ { book: 'Daniel', chapter: 3, startVerse: 1, endVerse: 30, displayRef: 'Daniel 3' },
+ { book: 'Daniel', chapter: 6, startVerse: 1, endVerse: 28, displayRef: 'Daniel 6' },
+ { book: 'Hosea', chapter: 6, startVerse: 1, endVerse: 11, displayRef: 'Hosea 6' },
+ { book: 'Joel', chapter: 2, startVerse: 12, endVerse: 32, displayRef: 'Joel 2:12-32' },
+ { book: 'Jonah', chapter: 1, startVerse: 1, endVerse: 17, displayRef: 'Jonah 1' },
+ { book: 'Micah', chapter: 6, startVerse: 1, endVerse: 16, displayRef: 'Micah 6' },
+ { book: 'Habakkuk', chapter: 3, startVerse: 1, endVerse: 19, displayRef: 'Habakkuk 3' },
+ { book: 'Malachi', chapter: 3, startVerse: 1, endVerse: 18, displayRef: 'Malachi 3' },
+ { book: 'Matthew', chapter: 5, startVerse: 1, endVerse: 30, displayRef: 'Matthew 5:1-30' },
+ { book: 'Matthew', chapter: 6, startVerse: 1, endVerse: 34, displayRef: 'Matthew 6' },
+ { book: 'Matthew', chapter: 7, startVerse: 1, endVerse: 29, displayRef: 'Matthew 7' },
+ { book: 'Mark', chapter: 10, startVerse: 17, endVerse: 45, displayRef: 'Mark 10:17-45' },
+ { book: 'Luke', chapter: 15, startVerse: 1, endVerse: 32, displayRef: 'Luke 15' },
+ { book: 'John', chapter: 1, startVerse: 1, endVerse: 51, displayRef: 'John 1:1-51' },
+ { book: 'John', chapter: 3, startVerse: 1, endVerse: 36, displayRef: 'John 3:1-36' },
+ { book: 'John', chapter: 6, startVerse:30, endVerse: 66, displayRef: 'John 6:30-66' },
+ { book: 'John', chapter: 14, startVerse: 1, endVerse: 31, displayRef: 'John 14' },
+ { book: 'Acts', chapter: 2, startVerse: 1, endVerse: 47, displayRef: 'Acts 2' },
+ { book: 'Romans', chapter: 1, startVerse: 1, endVerse: 32, displayRef: 'Romans 1' },
+ { book: 'Romans', chapter: 3, startVerse: 1, endVerse: 31, displayRef: 'Romans 3' },
+ { book: 'Romans', chapter: 8, startVerse: 1, endVerse: 39, displayRef: 'Romans 8' },
+ { book: 'Romans', chapter: 12, startVerse: 1, endVerse: 21, displayRef: 'Romans 12' },
+ { book: '1 Corinthians', chapter: 13, startVerse: 1, endVerse: 13, displayRef: '1 Corinthians 13' },
+ { book: '2 Corinthians', chapter: 5, startVerse: 1, endVerse: 21, displayRef: '2 Corinthians 5' },
+ { book: 'Galatians', chapter: 5, startVerse: 16, endVerse: 26, displayRef: 'Galatians 5:16-26' },
+ { book: 'Ephesians', chapter: 2, startVerse: 1, endVerse: 22, displayRef: 'Ephesians 2' },
+ { book: 'Philippians', chapter: 2, startVerse: 1, endVerse: 18, displayRef: 'Philippians 2:1-18' },
+ { book: 'Colossians', chapter: 1, startVerse: 1, endVerse: 29, displayRef: 'Colossians 1' },
+ { book: '1 Thessalonians', chapter: 4, startVerse: 1, endVerse: 18, displayRef: '1 Thessalonians 4' },
+ { book: '1 Timothy', chapter: 3, startVerse: 1, endVerse: 16, displayRef: '1 Timothy 3' },
+ { book: 'Hebrews', chapter: 11, startVerse: 1, endVerse: 40, displayRef: 'Hebrews 11' },
+ { book: 'James', chapter: 1, startVerse: 1, endVerse: 27, displayRef: 'James 1' },
+ { book: '1 Peter', chapter: 1, startVerse: 1, endVerse: 25, displayRef: '1 Peter 1' },
+ { book: '1 John', chapter: 4, startVerse: 1, endVerse: 21, displayRef: '1 John 4' },
+ { book: 'Revelation', chapter: 1, startVerse: 1, endVerse: 20, displayRef: 'Revelation 1' },
+ { book: 'Revelation', chapter: 21, startVerse: 1, endVerse: 27, displayRef: 'Revelation 21' },
+ { book: 'Revelation', chapter: 22, startVerse: 1, endVerse: 21, displayRef: 'Revelation 22' }
+];
+function buildFullBookPlan(bookName) {
+ const maxChapters = CHAPTER_COUNTS[bookName];
+ if (!maxChapters) {
+ console.warn(`No chapter count for "${bookName}" – skipping plan`);
+ return [];
+ }
+ const plan = [];
+ for (let ch = 1; ch <= maxChapters; ch++) {
+ plan.push({
+ book: bookName,
+ chapter: ch,
+ startVerse: 1,
+ endVerse: 999,
+ displayRef: `${bookName} ${ch}`
+ });
+ }
+ return plan;
+}
+const READING_PLANS = {
+ default: readingPlan,
+ genesis: buildFullBookPlan('Genesis'),
+ psalms: buildFullBookPlan('Psalms'),
+ proverbs: buildFullBookPlan('Proverbs'),
+ ecclesiastes: buildFullBookPlan('Ecclesiastes'),
+ romans: buildFullBookPlan('Romans'),
+ revelation: buildFullBookPlan('Revelation')
+};
+export function getActivePlan() {
+ const id = state.settings.readingPlanId || 'default';
+ return READING_PLANS[id] || READING_PLANS['default'];
+}
+const PLAN_LABELS = {
+ default: '90‑Day Sequential',
+ genesis: 'Genesis',
+ psalms: 'Psalms',
+ proverbs: 'Proverbs',
+ ecclesiastes: 'Ecclesiastes',
+ romans: 'Romans',
+ revelation: 'Revelation'
+};
+export function getCurrentPlanLabel() {
+ const id = state.settings.readingPlanId || 'default';
+ return PLAN_LABELS[id] || id;
+}
+export const bookNameMapping = {
+ Genesis: 'GEN', Exodus: 'EXO', Leviticus: 'LEV', Numbers: 'NUM', Deuteronomy: 'DEU',
+ Joshua: 'JOS', Judges: 'JDG', Ruth: 'RUT', '1 Samuel': '1SA', '2 Samuel': '2SA',
+ '1 Kings': '1KI', '2 Kings': '2KI', '1 Chronicles': '1CH', '2 Chronicles': '2CH',
+ Ezra: 'EZR', Nehemiah: 'NEH', Esther: 'EST', Job: 'JOB', Psalms: 'PSA',
+ Proverbs: 'PRO', Ecclesiastes: 'ECC', 'Song of Solomon': 'SNG', Isaiah: 'ISA', Jeremiah: 'JER',
+ Lamentations: 'LAM', Ezekiel: 'EZK', Daniel: 'DAN', Hosea: 'HOS', Joel: 'JOL',
+ Amos: 'AMO', Obadiah: 'OBA', Jonah: 'JON', Micah: 'MIC', Nahum: 'NAM',
+ Habakkuk: 'HAB', Zephaniah: 'ZEP', Haggai: 'HAG', Zechariah: 'ZEC', Malachi: 'MAL',
+ Matthew: 'MAT', Mark: 'MRK', Luke: 'LUK', John: 'JHN', Acts: 'ACT',
+ Romans: 'ROM', '1 Corinthians': '1CO', '2 Corinthians': '2CO', Galatians: 'GAL',
+ Ephesians: 'EPH', Philippians: 'PHP', Colossians: 'COL', '1 Thessalonians': '1TH',
+ '2 Thessalonians': '2TH', '1 Timothy': '1TI', '2 Timothy': '2TI', Titus: 'TIT',
+ Philemon: 'PHM', Hebrews: 'HEB', James: 'JAS', '1 Peter': '1PE', '2 Peter': '2PE',
+ '1 John': '1JN', '2 John': '2JN', '3 John': '3JN', Jude: 'JUD', Revelation: 'REV'
+};
+export const bibleHubUrlMap = {
+ 'NASB1995': 'nasb', // Bible Hub uses 'nasb' for NASB 1995
+ 'NASB': 'nasb_', // Bible Hub uses 'nasb_' for NASB 2020
+ 'ASV': 'asv',
+ 'ESV': 'esv',
+ 'KJV': 'kjv',
+ 'NKJV': 'nkjv',
+ 'BSB': 'bsb',
+ 'CSB': 'csb',
+ 'NET': 'net',
+ 'NIV': 'niv',
+ 'NLT': 'nlt'
+};
+export const bibleComUrlMap = {
+ 'NASB1995': '100',
+ 'NASB': '2692',
+ 'ASV': '12',
+ 'ESV': '59',
+ 'KJV': '1',
+ 'GNV': '2163',
+ 'NKJV': '114',
+ 'BSB': '3034',
+ 'CSB': '1713',
+ 'NET': '107',
+ 'NIV': '111',
+ 'NLT': '116'
+};
+export const ebibleOrgUrlMap = {
+ 'NASB1995': 'local:engnasb',
+ 'ASV': 'local:eng-asv',
+ 'KJV': 'local:eng-kjv2006',
+ 'GNV': 'local:enggnv',
+ 'BSB': 'local:engbsb',
+ 'NET': 'local:engnet'
+};
+export const stepBibleUrlMap = {
+ 'NASB1995': 'NASB1995',
+ 'NASB': 'NASB2020',
+ 'ASV': 'ASV',
+ 'ESV': 'ESV',
+ 'KJV': 'KJV',
+ 'GNV': 'Gen',
+ 'BSB': 'BSB',
+ 'NET': 'NET2full',
+ 'NIV': 'NIV'
+};
+export function getTranslationShorthand() {
+ return state.settings.bibleTranslation || 'BSB';
+}
+function getBibleGatewayVersionCode(appTranslation) {
+ const versionMap = {
+ 'NASB1995': 'NASB1995',
+ 'NASB': 'NASB',
+ 'ASV': 'ASV',
+ 'ESV': 'ESV',
+ 'KJV': 'KJV',
+ 'GNV': 'GNV',
+ 'NKJV': 'NKJV',
+ 'BSB': 'BSB',
+ 'CSB': 'CSB',
+ 'NET': 'NET',
+ 'NIV': 'NIV',
+ 'NLT': 'NLT'
+ };
+ return versionMap[appTranslation] || 'NASB1995';
+}
+export function updateBibleGatewayVersion() {
+ const versionCode = getBibleGatewayVersionCode(state.settings.referenceVersion);
+ const versionInput = document.getElementById('bgVersion');
+ if (versionCode === 'BSB') {
+ versionInput.value = 'NASB1995';
+ } else {
+ versionInput.value = versionCode;
+ }
+}
\ No newline at end of file
diff --git a/modules/strongs.js b/modules/strongs.js
new file mode 100644
index 0000000..5a3d7c0
--- /dev/null
+++ b/modules/strongs.js
@@ -0,0 +1,254 @@
+import { state } from './state.js'
+import { getStepBibleUrl } from './ui.js'
+export function showStrongsReference(verseEl) {
+ const ref = verseEl.dataset.verse;
+ state.currentVerseElement = verseEl;
+ const textSpan = verseEl.querySelector('.verse-text');
+ let verseText = '';
+ if (textSpan) {
+ verseText = textSpan.innerHTML;
+ if (!verseText || verseText.trim() === '') {
+ verseText = textSpan.textContent || '';
+ if (!verseText || verseText.trim() === '') {
+ verseText = verseEl.dataset.verseText || '';
+ }
+ }
+ } else {
+ verseText = verseEl.dataset.verseText || '';
+ }
+ if (!verseText || verseText.trim() === '') {
+ verseText = 'Verse text not available ';
+ }
+ state.currentVerseData = { reference: ref, text: verseText };
+ const content = document.getElementById('strongsContent');
+ const m = ref.match(/^([\w\s]+)\s+(\d+):(\d+)/);
+ let book = '', chapter = '', verse = '';
+ if (m) {
+ book = m[1].trim().replace(/\s+/g, '_').toLowerCase();
+ chapter = m[2];
+ verse = m[3];
+ }
+ const greekUrl = `https://biblehub.com/interlinear/${book}/${chapter}-${verse}.htm`;
+ const currentTranslation = state.settings.referenceVersion;
+ const stepUrl = getStepBibleUrl(ref, currentTranslation);
+ content.innerHTML = `
+
+
+
+
+
+ ${ref}
+
+
+
+
+
+ Copy Verse
+
+
+
+ ${verseText}
+
+
+
+
+
+ These resources provide detailed word-by-word analysis.
+ Use the "Pop Out" button to open them in a new tab.
+
+
+ `;
+ populateStrongsFootnotes(ref);
+ document.getElementById('strongsPopup').classList.add('active');
+ document.getElementById('popupOverlay').classList.add('active');
+ setTimeout(() => {
+ const copyBtn = document.getElementById('copyVerseBtn');
+ if (copyBtn) {
+ copyBtn.addEventListener('click', copyVerseText);
+ }
+ const prevBtn = document.getElementById('prevVerseBtn');
+ const nextBtn = document.getElementById('nextVerseBtn');
+ if (prevBtn) {
+ prevBtn.addEventListener('click', navigateToPreviousVerse);
+ }
+ if (nextBtn) {
+ nextBtn.addEventListener('click', navigateToNextVerse);
+ }
+ document.querySelectorAll('.resource-frame-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const url = this.dataset.url;
+ const title = this.dataset.title;
+ popOutResource(url, title);
+ });
+ });
+ setupStrongsFootnoteHandlers();
+ }, 0);
+}
+function navigateToPreviousVerse() {
+ const currentVerseEl = state.currentVerseElement;
+ if (!currentVerseEl) return;
+ const allVerses = Array.from(document.querySelectorAll('.verse'));
+ const currentIndex = allVerses.indexOf(currentVerseEl);
+ if (currentIndex > 0) {
+ const prevVerseEl = allVerses[currentIndex - 1];
+ showStrongsReference(prevVerseEl);
+ }
+}
+function navigateToNextVerse() {
+ const currentVerseEl = state.currentVerseElement;
+ if (!currentVerseEl) return;
+ const allVerses = Array.from(document.querySelectorAll('.verse'));
+ const currentIndex = allVerses.indexOf(currentVerseEl);
+ if (currentIndex < allVerses.length - 1) {
+ const nextVerseEl = allVerses[currentIndex + 1];
+ showStrongsReference(nextVerseEl);
+ }
+}
+export function closeStrongsPopup() {
+ document.getElementById('strongsPopup').classList.remove('active');
+ document.getElementById('popupOverlay').classList.remove('active');
+ state.currentVerseData = null;
+}
+function copyVerseText() {
+ if (!state.currentVerseData) return;
+ const tempDiv = document.createElement('div');
+ tempDiv.innerHTML = state.currentVerseData.text;
+ let plainText = tempDiv.textContent || tempDiv.innerText || '';
+ plainText = plainText
+ .replace(/\s+/g, ' ')
+ .replace(/\s([.,;:!?])/g, '$1')
+ .replace(/([.,;:!?])(?=\w)/g, '$1 ')
+ .trim();
+ const txt = `${state.currentVerseData.reference} – ${plainText}`;
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(txt)
+ .then(() => {
+ const btn = document.getElementById('copyVerseBtn');
+ const original = btn.innerHTML;
+ btn.innerHTML = ' Copied!';
+ btn.classList.add('copied');
+ setTimeout(() => {
+ btn.innerHTML = original;
+ btn.classList.remove('copied');
+ }, 2000);
+ })
+ .catch(err => {
+ console.error('Copy failed:', err);
+ copyVerseFallback(txt);
+ });
+ } else {
+ copyVerseFallback(txt);
+ }
+}
+function populateStrongsFootnotes(verseRef) {
+ const container = document.getElementById('strongsFootnotesContainer');
+ if (!container) return;
+ container.innerHTML = '';
+ const verseFootnotes = state.footnotes[verseRef];
+ if (!verseFootnotes || verseFootnotes.length === 0) {
+ container.innerHTML = 'No footnotes available for this verse
';
+ return;
+ }
+ console.log('Found stored footnotes:', verseFootnotes);
+ container.innerHTML = `
+
+
+ `;
+ verseFootnotes.forEach(fn => {
+ const footnoteDiv = document.createElement('div');
+ footnoteDiv.className = 'footnote';
+ footnoteDiv.innerHTML = `
+
+
+ `;
+ container.appendChild(footnoteDiv);
+ });
+ container.style.display = 'block';
+}
+function setupStrongsFootnoteHandlers() {
+ document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => {
+ const newRef = ref.cloneNode(true);
+ ref.parentNode.replaceChild(newRef, ref);
+ });
+ document.querySelectorAll('#strongsFootnotesContainer .footnote-ref').forEach(ref => {
+ ref.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ const footnoteNumber = this.dataset.footnoteNumber;
+ const footnoteElement = document.querySelector(`#strongsFootnotesContainer .footnote[data-footnote-number="${footnoteNumber}"]`);
+ if (footnoteElement) {
+ footnoteElement.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest'
+ });
+ footnoteElement.style.backgroundColor = 'var(--verse-hover)';
+ setTimeout(() => {
+ footnoteElement.style.backgroundColor = '';
+ }, 2000);
+ }
+ });
+ });
+}
+function copyVerseFallback(text) {
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ document.execCommand('copy');
+ const btn = document.getElementById('copyVerseBtn');
+ const original = btn.textContent;
+ btn.textContent = '✓ Copied!';
+ btn.classList.add('copied');
+ setTimeout(() => {
+ btn.textContent = original;
+ btn.classList.remove('copied');
+ }, 2000);
+ } catch (err) {
+ console.error('Copy fallback failed:', err);
+ alert('Could not copy verse text.');
+ } finally {
+ document.body.removeChild(textarea);
+ }
+}
+function popOutResource(url, title) {
+ window.open(url, title,
+ 'width=800,height=600,menubar=no,toolbar=no,location=no');
+}
\ No newline at end of file
diff --git a/modules/ui.js b/modules/ui.js
new file mode 100644
index 0000000..1fa2216
--- /dev/null
+++ b/modules/ui.js
@@ -0,0 +1,407 @@
+import { handleError } from '../main.js'
+import {
+ loadSelectedChapter,
+ populateBookDropdown,
+ populateChapterDropdown
+} from './navigation.js'
+import { loadPDF } from './pdf.js'
+import {
+ BOOK_ORDER,
+ CHAPTER_COUNTS,
+ bibleComUrlMap,
+ bibleHubUrlMap,
+ ebibleOrgUrlMap,
+ formatBookNameForSource,
+ getActivePlan,
+ saveToStorage,
+ state,
+ stepBibleUrlMap
+} from './state.js'
+export function switchNotesView(view) {
+ state.settings.notesView = view;
+ const txtBtn = document.getElementById('textViewBtn');
+ const mdBtn = document.getElementById('markdownViewBtn');
+ const input = document.getElementById('notesInput');
+ const display = document.getElementById('notesDisplay');
+ if (view === 'text') {
+ txtBtn.classList.add('active');
+ mdBtn.classList.remove('active');
+ input.style.display = 'block';
+ display.style.display = 'none';
+ } else {
+ txtBtn.classList.remove('active');
+ mdBtn.classList.add('active');
+ input.style.display = 'none';
+ display.style.display = 'block';
+ updateMarkdownPreview();
+ }
+ saveToStorage();
+}
+export function updateMarkdownPreview() {
+ if (state.settings.notesView !== 'markdown' || typeof marked === 'undefined') return;
+ const out = document.getElementById('notesDisplay');
+ try {
+ out.innerHTML = marked.parse(state.notes);
+ } catch (e) {
+ console.error('Markdown error:', e);
+ out.innerHTML = 'Error rendering markdown
';
+ }
+}
+export function insertMarkdown(type) {
+ const ta = document.getElementById('notesInput');
+ const start = ta.selectionStart;
+ const end = ta.selectionEnd;
+ const sel = ta.value.substring(start, end);
+ let repl = '';
+ let cursorAdj = 0;
+ switch (type) {
+ case 'bold': repl = `**${sel || 'bold text'}**`; cursorAdj = sel ? 0 : -2; break;
+ case 'italic': repl = `*${sel || 'italic text'}*`; cursorAdj = sel ? 0 : -1; break;
+ case 'h1': repl = `# ${sel || 'Heading 1'}`; cursorAdj = sel ? 0 : -10; break;
+ case 'h2': repl = `## ${sel || 'Heading 2'}`; cursorAdj = sel ? 0 : -10; break;
+ case 'h3': repl = `### ${sel || 'Heading 3'}`; cursorAdj = sel ? 0 : -10; break;
+ case 'ul': repl = `- ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break;
+ case 'ol': repl = `1. ${sel || 'List item'}`; cursorAdj = sel ? 0 : -10; break;
+ case 'quote': repl = `> ${sel || 'Quote'}`; cursorAdj = sel ? 0 : -6; break;
+ case 'code': repl = `\`${sel || 'code'}\``; cursorAdj = sel ? 0 : -1; break;
+ case 'link': repl = `[${sel || 'link text'}](url)`; cursorAdj = sel ? -4 : -14; break;
+ }
+ ta.value = ta.value.slice(0, start) + repl + ta.value.slice(end);
+ const newPos = start + repl.length + cursorAdj;
+ ta.setSelectionRange(newPos, newPos);
+ ta.focus();
+ state.notes = ta.value;
+ saveToStorage();
+ updateMarkdownPreview();
+}
+export function exportNotes(ext) {
+ if (!state.notes || state.notes.trim() === '') {
+ alert('No notes to export!');
+ return;
+ }
+ const blob = new Blob([state.notes], { type: 'text/plain;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `study-notes-${new Date().toISOString().split('T')[0]}.${ext}`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+export function toggleNotes() {
+ document.getElementById('notesSection').classList.toggle('hidden');
+}
+export function togglePanelCollapse(panelId) {
+ const panel = document.getElementById(panelId);
+ const collapsed = panel.classList.contains('panel-collapsed');
+ if (collapsed) {
+ panel.classList.remove('panel-collapsed');
+ if (state.settings.panelWidths[panelId]) {
+ panel.style.width = state.settings.panelWidths[panelId] + 'px';
+ }
+ state.settings.collapsedPanels[panelId] = false;
+ } else {
+ panel.classList.add('panel-collapsed');
+ state.settings.collapsedPanels[panelId] = true;
+ }
+ saveToStorage();
+}
+export function toggleSection(sectionId) {
+ const content = document.getElementById(`content-${sectionId}`);
+ const header = document.querySelector(`[data-section="${sectionId}"]`);
+ const toggle = header.querySelector('.section-toggle');
+ const nowCollapsed = content.classList.contains('collapsed');
+ content.classList.toggle('collapsed');
+ toggle.classList.toggle('collapsed');
+ state.settings.collapsedSections[sectionId] = !nowCollapsed;
+ saveToStorage();
+}
+export function toggleReferencePanel() {
+ const panel = document.getElementById('referencePanel');
+ const nowOpen = panel.classList.contains('active');
+ if (nowOpen) {
+ panel.classList.remove('active');
+ state.settings.referencePanelOpen = false;
+ } else {
+ panel.classList.add('active');
+ state.settings.referencePanelOpen = true;
+ updateReferencePanel();
+ }
+ saveToStorage();
+}
+export async function updateReferencePanel() {
+ try {
+ const sourceSelect = document.getElementById('referenceSource');
+ const source = sourceSelect.value;
+ const iframe = document.getElementById('referenceIframe');
+ const pdfViewer = document.getElementById('pdfViewer');
+ const transSel = document.getElementById('referenceTranslation');
+ state.settings.referenceSource = source;
+ state.settings.referenceVersion = transSel.value;
+ transSel.style.display = source === 'pdf' ? 'none' : 'block';
+ filterTranslationOptions(source, transSel);
+ const actualSource = document.getElementById('referenceSource').value;
+ const translation = transSel.value;
+ let passage;
+ if (state.settings.readingMode === 'readingPlan') {
+ passage = getActivePlan()[state.settings.currentPassageIndex];
+ } else {
+ passage = {
+ book: state.settings.manualBook,
+ chapter: state.settings.manualChapter,
+ displayRef: `${state.settings.manualBook} ${state.settings.manualChapter}`
+ };
+ }
+ const bookName = passage.book.toLowerCase().replace(/\s+/g, '_');
+ const bookAbbr = bookName.substring(0, 3).toUpperCase();
+ const chapter = passage.chapter;
+ if (actualSource === 'pdf') {
+ if (!state.settings.customPdf) {
+ alert('No PDF uploaded. Please upload one in Settings first.');
+ document.getElementById('referenceSource').value = 'biblegateway';
+ transSel.style.display = 'block';
+ filterTranslationOptions('biblegateway', transSel);
+ return;
+ }
+ iframe.style.display = 'none';
+ pdfViewer.classList.add('active');
+ document.getElementById('zoomLevel').textContent =
+ Math.round(state.settings.pdfZoom * 100) + '%';
+ await loadPDF();
+ } else if (actualSource === 'biblehub') {
+ const bibleHubCode = bibleHubUrlMap[translation] || translation.toLowerCase();
+ const url = `https://biblehub.com/${bibleHubCode}/${bookName}/${chapter}.htm`;
+ iframe.src = url;
+ } else if (actualSource === 'biblecom') {
+ const bibleComCode = bibleComUrlMap[translation];
+ if (!bibleComCode) {
+ alert(`Bible.com doesn't support ${translation}. Please choose another translation.`);
+ return;
+ }
+ const formattedBook = formatBookNameForSource(passage.book, 'biblecom');
+ const urlFormats = [
+ `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}?interface=embed`,
+ `https://www.bible.com/bible/${bibleComCode}/${formattedBook}.${chapter}.${translation}`,
+ `https://www.bible.com/bible/${bibleComCode}/${chapter}.${translation}?${formattedBook}=${chapter}`
+ ];
+ let currentUrlIndex = 0;
+ function tryNextUrl() {
+ if (currentUrlIndex >= urlFormats.length) {
+ alert('Could not load Bible.com. Please try another reference source.');
+ return;
+ }
+ iframe.src = urlFormats[currentUrlIndex];
+ currentUrlIndex++;
+ }
+ iframe.onload = function() {
+ console.log('Bible.com loaded successfully with format', currentUrlIndex);
+ };
+ iframe.onerror = function() {
+ console.log('Trying next Bible.com URL format...');
+ tryNextUrl();
+ };
+ tryNextUrl();
+ } else if (actualSource === 'ebibleorg') {
+ const ebibleOrgCode = ebibleOrgUrlMap[translation];
+ if (!ebibleOrgCode) {
+ alert(`eBible.org doesn't support ${translation}. Please choose another translation.`);
+ return;
+ }
+ const bookRef = bookName === 'psalms' ? 'PS1' : `${bookAbbr}1`;
+ const url = `https://ebible.org/study/?w1=bible&t1=${encodeURIComponent(ebibleOrgCode)}&v1=${bookRef}_${chapter}`;
+ iframe.src = url;
+ } else if (actualSource === 'stepbible') {
+ const stepBibleCode = stepBibleUrlMap[translation];
+ if (!stepBibleCode) {
+ alert(`STEP Bible doesn't support ${translation}. Please choose another translation.`);
+ return;
+ }
+ const url = getStepBibleUrl(passage.displayRef, translation);
+ iframe.src = url;
+ } else {
+ const query = passage.displayRef.replace(/\s+/g, '+');
+ let version = translation;
+ if (translation === 'GNV') version = 'GNV';
+ const url = `https://www.biblegateway.com/passage/?search=${query}&version=${version}&interface=print`;
+ iframe.src = url;
+ }
+ saveToStorage();
+ } catch (err) {
+ handleError(err, 'updateReferencePanel');
+ }
+}
+function filterTranslationOptions(source, selectElement) {
+ const unsupportedTranslations = {
+ biblecom: [],
+ biblehub: ['GNV'],
+ biblegateway: ['BSB'],
+ stepbible: ['NKJV', 'CSB', 'NLT'],
+ ebibleorg: ['NASB', 'ASV', 'ESV', 'NKJV', 'CSB', 'NIV', 'NLT'],
+ pdf: ['NASB1995', 'NASB', 'ASV', 'ESV', 'KJV', 'GNV', 'NKJV', 'BSB', 'CSB', 'NET', 'NIV', 'NLT']
+ };
+ const allOptions = selectElement.querySelectorAll('option');
+ const currentValue = selectElement.value;
+ let needsNewSelection = false;
+ let needsSourceChange = false;
+ allOptions.forEach(option => {
+ const value = option.value;
+ const isUnsupported = unsupportedTranslations[source]?.includes(value);
+ if (isUnsupported) {
+ option.style.display = 'none';
+ option.disabled = true;
+ if (value === currentValue) {
+ needsNewSelection = true;
+ if (value === 'BSB' && source === 'biblegateway') {
+ needsSourceChange = true;
+ }
+ }
+ } else {
+ option.style.display = 'block';
+ option.disabled = false;
+ }
+ });
+ if (needsSourceChange) {
+ document.getElementById('referenceSource').value = 'biblehub';
+ state.settings.referenceSource = 'biblehub';
+ const sourceSelect = document.getElementById('referenceSource');
+ sourceSelect.value = 'biblehub';
+ selectElement.value = 'BSB';
+ state.settings.referenceVersion = 'BSB';
+ }
+ else if (needsNewSelection) {
+ let fallbackValue = 'NASB1995';
+ if (source === 'biblehub') {
+ fallbackValue = 'NASB';
+ }
+ selectElement.value = fallbackValue;
+ state.settings.referenceVersion = fallbackValue;
+ }
+ if (needsSourceChange || needsNewSelection) {
+ saveToStorage();
+ }
+}
+export function restoreBookChapterUI() {
+ populateBookDropdown();
+ const bookSel = document.getElementById('bookSelect');
+ const chapterSel = document.getElementById('chapterSelect');
+ const savedBook = state.settings.manualBook || BOOK_ORDER[0];
+ const bookIdx = BOOK_ORDER.indexOf(savedBook);
+ const book = bookIdx >= 0 ? BOOK_ORDER[bookIdx] : BOOK_ORDER[0];
+ populateBookDropdown();
+ bookSel.value = book;
+ populateChapterDropdown(book);
+ const savedChap = Number(state.settings.manualChapter) || 1;
+ const maxChap = CHAPTER_COUNTS[book];
+ const chapter = Math.min(savedChap, maxChap);
+ chapterSel.value = String(chapter);
+ state.settings.manualBook = book;
+ state.settings.manualChapter = chapter;
+ loadSelectedChapter(book, chapter);
+}
+export function initResizeHandles() {
+ const handles = document.querySelectorAll('.resize-handle');
+ const SPEED_FACTOR = 1.8;
+ const limits = {
+ sidebar: { min: 150, max: 600 },
+ referencePanel: { min: 250, max: 800 },
+ scriptureSection: { min: 300, max: 1200 },
+ notesSection: { min: 250, max: 800 }
+ };
+ let resizing = false,
+ startX = 0,
+ startW = 0,
+ panel = null,
+ invert = false,
+ pendingRAF = false;
+ handles.forEach(handle => {
+ handle.addEventListener('mousedown', e => {
+ resizing = true;
+ startX = e.clientX;
+ const panelId = handle.dataset.panel;
+ panel = document.getElementById(panelId);
+ startW = panel.offsetWidth;
+ invert = (panel.id === 'notesSection');
+ document.body.style.cursor = 'ew-resize';
+ document.body.style.userSelect = 'none';
+ e.preventDefault();
+ });
+ });
+ document.addEventListener('mousemove', e => {
+ if (!resizing || !panel) return;
+ let delta = (invert ? startX - e.clientX : e.clientX - startX) * SPEED_FACTOR;
+ let newW = startW + delta;
+ const { min, max } = limits[panel.id] || { min: 150, max: 1200 };
+ if (newW < min) newW = min;
+ if (newW > max) newW = max;
+ if (!pendingRAF) {
+ pendingRAF = true;
+ requestAnimationFrame(() => {
+ panel.style.width = newW + 'px';
+ state.settings.panelWidths[panel.id] = newW;
+ pendingRAF = false;
+ });
+ }
+ });
+ document.addEventListener('mouseup', () => {
+ if (resizing) {
+ resizing = false;
+ panel = null;
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ saveToStorage();
+ }
+ });
+}
+export function popOutResource(url, title) {
+ window.open(url, title,
+ 'width=800,height=600,menubar=no,toolbar=no,location=no');
+}
+export function getStepBibleUrl(reference, translation) {
+ const stepBibleCode = stepBibleUrlMap[translation] || translation;
+ return `https://www.stepbible.org/?q=version=${stepBibleCode}@reference=${encodeURIComponent(reference)}&options=HNVUG`;
+}
+export function makeToggleSticky() {
+ const sidebar = document.getElementById('sidebar');
+ const toggle = sidebar.querySelector('.collapse-toggle');
+ if (!toggle) return;
+ toggle.style.position = 'sticky';
+ toggle.style.top = '10px';
+ toggle.style.zIndex = '1000';
+ toggle.style.marginLeft = 'auto';
+ toggle.style.marginRight = '10px';
+}
+export function restoreSidebarState() {
+ Object.entries(state.settings.collapsedSections || {})
+ .forEach(([sec, collapsed]) => {
+ if (collapsed) {
+ const content = document.getElementById(`content-${sec}`);
+ const header = document.querySelector(`[data-section="${sec}"]`);
+ const toggle = header?.querySelector('.section-toggle');
+ if (content && toggle) {
+ content.classList.add('collapsed');
+ toggle.classList.add('collapsed');
+ }
+ }
+ });
+}
+export function restorePanelStates() {
+ Object.entries(state.settings.panelWidths || {})
+ .forEach(([id, w]) => {
+ const el = document.getElementById(id);
+ if (el && w) el.style.width = w + 'px';
+ });
+ Object.entries(state.settings.collapsedPanels || {})
+ .forEach(([id, collapsed]) => {
+ const el = document.getElementById(id);
+ if (el && collapsed) el.classList.add('panel-collapsed');
+ });
+ if (state.settings.referencePanelOpen) {
+ document.getElementById('referencePanel').classList.add('active');
+ document.getElementById('referenceSource').value =
+ state.settings.referenceSource || 'biblegateway';
+ document.getElementById('referenceTranslation').value =
+ state.settings.referenceVersion || 'NASB1995';
+ updateReferencePanel();
+ }
+}
\ No newline at end of file
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..8d5580b
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,363 @@
+:root{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db;--background:#ecf0f1;--card-background:#ffffff;--toolbar-background:#f8f9fa;--notes-background:#f8f9fa;--sidebar-background:#2c3e50;--header-background:#ffffff;--popup-background:#ffffff;--text-color:#2c3e50;--sidebar-text:#ecf0f1;--border-color:#bdc3c7;--verse-hover:#f0f0f0;--shadow:rgba(0,0,0,0.1);--shadow-strong:rgba(0,0,0,0.3);--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--transition-fast:150ms ease;--transition-normal:300ms ease;--transition-slow:500ms ease}
+[data-color-theme="blue"]{--primary-color:#2c3e50;--secondary-color:#34495e;--accent-color:#3498db}
+[data-color-theme="green"]{--primary-color:#1b5e20;--secondary-color:#2e7d32;--accent-color:#4caf50}
+[data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#6a1b9a;--accent-color:#9c27b0}
+[data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#c62828;--accent-color:#f44336}
+[data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#ef6c00;--accent-color:#ff9800}
+[data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00695c;--accent-color:#009688}
+[data-theme="dark"]{--background:#0d1117;--card-background:#161b22;--toolbar-background:#161b22;--notes-background:#0d1117;--sidebar-background:#0d1117;--header-background:#161b22;--popup-background:#161b22;--text-color:#e1e1e1;--sidebar-text:#c9d1d9;--border-color:#444;--verse-hover:#21262d;--shadow:rgba(0,0,0,0.4);--shadow-strong:rgba(0,0,0,0.6)}
+[data-theme="dark"][data-color-theme="blue"]{--primary-color:#1a3a52;--secondary-color:#2c5f7f;--accent-color:#5dade2}
+[data-theme="dark"][data-color-theme="green"]{--primary-color:#1b4d20;--secondary-color:#2e6d32;--accent-color:#66bb6a}
+[data-theme="dark"][data-color-theme="purple"]{--primary-color:#4a148c;--secondary-color:#7b1fa2;--accent-color:#ab47bc}
+[data-theme="dark"][data-color-theme="red"]{--primary-color:#b71c1c;--secondary-color:#d32f2f;--accent-color:#ef5350}
+[data-theme="dark"][data-color-theme="orange"]{--primary-color:#e65100;--secondary-color:#f57c00;--accent-color:#ffa726}
+[data-theme="dark"][data-color-theme="teal"]{--primary-color:#004d40;--secondary-color:#00796b;--accent-color:#26a69a}
+
+*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
+@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:0.01ms!important;animation-iteration-count:1!important;transition-duration:0.01ms!important;scroll-behavior:auto!important}
+}
+:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px}
+@media(prefers-contrast:high){:root{--border-color:#000000;--shadow:rgba(0,0,0,0.8);--shadow-strong:rgba(0,0,0,0.9)}
+.verse:hover{outline:2px solid var(--accent-color)}
+}
+
+body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background-color:var(--background);color:var(--text-color);line-height:1.6;transition:background-color 0.3s,color 0.3s}
+.container{display:flex;height:100vh;overflow:hidden}
+.panel-collapsed{min-width:50px!important;max-width:50px!important}
+.panel-collapsed>*:not(.collapse-toggle):not(.resize-handle){display:none!important}
+.collapse-toggle{will-change:transform;position:absolute;top:55%;right:5px;transform:translateY(-50%);background:var(--accent-color);color:white;border:none;width:40px;height:40px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;z-index:100;transition:transform 0.3s ease,background-color 0.3s ease;box-shadow:0 2px 8px rgba(0,0,0,0.2)}
+.collapse-toggle:hover{transform:translateY(-50%)scale(1.1);box-shadow:0 4px 12px rgba(0,0,0,0.3)}
+.collapse-toggle:focus{outline:2px solid white;outline-offset:2px}
+.panel-collapsed .collapse-toggle{right:5px}
+.collapse-toggle::before{content:'\25C0';transition:transform 0.3s ease}
+.panel-collapsed .collapse-toggle::before{content:'\25B6'}
+#scriptureSection .collapse-toggle::before{content:'\25B6'}
+#scriptureSection.panel-collapsed .collapse-toggle::before{content:'\25C0'}
+#notesSection .collapse-toggle::before{content:'\25B6'}
+#notesSection.panel-collapsed .collapse-toggle::before{content:'\25C0'}
+.scripture-section{scroll-behavior:smooth;scroll-padding-top:20px}
+.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
+.content-area{flex:1;display:flex;overflow:hidden}
+
+.welcome-screen{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,var(--primary-color),var(--accent-color));z-index:5000;display:flex;align-items:center;justify-content:center;padding:20px;overflow-y:auto}
+.welcome-screen.hidden{display:none}
+.welcome-content{background:var(--card-background);border-radius:20px;padding:40px;max-width:900px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,0.3);margin:auto;max-height:90vh;overflow-y:auto}
+.welcome-content h1{color:var(--primary-color);text-align:center;margin-bottom:15px;font-size:2.5em}
+.welcome-content>p{color:var(--text-color);text-align:center;margin-bottom:30px;font-size:1.2em;opacity:0.8}
+.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin:30px 0}
+.feature-card{padding:20px;background:var(--background);border-radius:10px;border:2px solid var(--border-color);transition:all 0.3s}
+.feature-card:hover{transform:translateY(-5px);box-shadow:0 5px 20px var(--shadow);border-color:var(--accent-color)}
+.feature-icon{font-size:2.5em;margin-bottom:10px}
+.feature-card h3{color:var(--primary-color);margin-bottom:10px}
+.feature-card p{color:var(--text-color);opacity:0.8;font-size:0.95em}
+.offline-setup{margin:30px 0;padding:25px;background:var(--background);border-radius:10px;border:2px solid var(--accent-color)}
+.offline-setup h2{color:var(--primary-color);margin-bottom:15px;display:flex;align-items:center;gap:10px}
+.pdf-upload-area{border:2px dashed var(--border-color);border-radius:8px;padding:30px;text-align:center;cursor:pointer;transition:all 0.3s;background:var(--card-background);margin:15px 0}
+.pdf-upload-area:hover{border-color:var(--accent-color);background-color:rgba(52,152,219,0.05)}
+.pdf-upload-area.has-file{border-color:var(--accent-color);background-color:rgba(52,152,219,0.1)}
+.pdf-download-options{margin-top:15px;padding:15px;background-color:var(--card-background);border-radius:5px}
+.pdf-download-options h4{margin-bottom:10px;color:var(--text-color)}
+.pdf-download-link{display:block;padding:8px 12px;margin-bottom:8px;background-color:var(--background);border:1px solid var(--border-color);border-radius:4px;color:var(--accent-color);text-decoration:none;transition:all 0.2s}
+.pdf-download-link:hover{background-color:var(--accent-color);color:white}
+.welcome-actions{display:flex;gap:15px;margin-top:30px;justify-content:center}
+.welcome-actions button{padding:15px 40px;border:none;border-radius:8px;font-size:16px;font-weight:bold;cursor:pointer;transition:all 0.3s}
+
+.sidebar{width:280px;background-color:var(--sidebar-background);color:var(--sidebar-text);padding:20px;overflow-y:auto;flex-shrink:0;border-right:1px solid var(--border-color);position:relative;min-width:50px;transition:all 0.3s ease}
+.sidebar h2{margin-bottom:20px;font-size:1.3em;border-bottom:2px solid var(--accent-color);padding-bottom:10px;color:var(--sidebar-text)}
+.sidebar-section{margin-bottom:10px}
+.sidebar-section-header{display:flex;justify-content:space-between;align-items:center;cursor:pointer;padding:10px;margin-top:15px;background-color:rgba(255,255,255,0.05);border-radius:5px;transition:background-color 0.2s}
+.sidebar-section-header:hover{background-color:rgba(255,255,255,0.1)}
+.sidebar-section-header:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.sidebar-section-header h3{margin:0;font-size:1.1em;color:var(--accent-color)}
+.section-toggle{font-size:0.9em;transition:transform 0.3s}
+.section-toggle.collapsed{transform:rotate(-90deg)}
+.sidebar-section-content{max-height:1000px;overflow:hidden;transition:max-height 0.3s ease-out,opacity 0.3s ease-out;opacity:1}
+.sidebar-section-content.collapsed{max-height:0;opacity:0}
+.sidebar-links{list-style:none;margin-top:10px}
+.sidebar-links li{margin-bottom:8px}
+.sidebar-links a{color:var(--sidebar-text);text-decoration:none;display:block;padding:8px 12px;border-radius:5px;transition:background-color 0.3s;opacity:0.9}
+.sidebar-links a:hover{background-color:var(--secondary-color);opacity:1}
+.sidebar-links a:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.reference-panel-toggle{margin-top:20px;padding:12px;background:var(--accent-color);color:white;border:none;border-radius:5px;cursor:pointer;width:100%;font-size:14px;font-weight:bold;transition:opacity 0.3s}
+.reference-panel-toggle:hover{opacity:0.8}
+.reference-panel-toggle:focus{outline:2px solid white;outline-offset:2px}
+
+.reference-panel{display:none;width:400px;background-color:var(--card-background);border-right:1px solid var(--border-color);position:relative;flex-shrink:0;min-width:50px;transition:all 0.3s ease}
+.reference-panel.active{display:flex;flex-direction:column}
+.reference-panel-header{padding:15px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
+.reference-panel-header h3{color:var(--text-color);margin:0}
+.reference-panel-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
+.reference-panel-controls select{padding:5px 10px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px}
+.reference-panel-controls select:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.reference-panel-close{background:none;border:none;color:var(--text-color);cursor:pointer;font-size:20px;padding:5px 10px;border-radius:4px;transition:background-color 0.2s;margin-left:10px}
+.reference-panel-close:hover{background-color:var(--verse-hover)}
+.reference-panel-close:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.reference-panel-content{flex:1;overflow:hidden;position:relative}
+.reference-panel-iframe{width:100%;height:100%;border:none}
+.pdf-viewer{width:100%;height:100%;display:none;flex-direction:column;background-color:var(--card-background)}
+.pdf-viewer.active{display:flex}
+.pdf-controls{padding:10px;background-color:var(--toolbar-background);border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap}
+.pdf-controls button{padding:5px 15px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px}
+.pdf-controls button:hover{opacity:0.8}
+.pdf-controls button:focus{outline:2px solid white;outline-offset:2px}
+.pdf-controls button:disabled{opacity:0.5;cursor:not-allowed}
+.pdf-controls span{color:var(--text-color);font-size:13px}
+.pdf-controls input[type="number"]{width:60px;padding:5px;border:1px solid var(--border-color);border-radius:4px;background-color:var(--card-background);color:var(--text-color);font-size:13px}
+.pdf-controls input[type="number"]:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.pdf-canvas-container{flex:1;overflow:auto;display:flex;justify-content:center;align-items:flex-start;padding:20px;background-color:var(--background)}
+#pdfCanvas{box-shadow:0 2px 10px var(--shadow)}
+
+.resize-handle{position:absolute;top:0;bottom:0;width:10px;cursor:ew-resize;background-color:transparent;transition:background-color 0.2s;z-index:10;display:flex;align-items:center;justify-content:center}
+.resize-handle::after{content:'\22EE\22EE';color:var(--border-color);font-size:18px;opacity:0;transition:opacity 0.2s}
+.resize-handle:hover{background-color:rgba(52,152,219,0.1)}
+.resize-handle:hover::after{opacity:1;color:var(--accent-color)}
+.resize-handle-right{right:0}
+.resize-handle-left{left:0}
+
+.header{background-color:var(--header-background);padding:20px 30px;box-shadow:0 2px 5px var(--shadow);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)}
+.header h1{color:var(--text-color);font-size:1.8em}
+.header-controls{display:flex;gap:10px;align-items:center}
+.theme-toggle{background:none;border:2px solid var(--border-color);color:var(--text-color);width:44px;height:44px;border-radius:50%;cursor:pointer;font-size:20px;display:flex;align-items:center;justify-content:center;transition:all 0.3s}
+.theme-toggle:hover{background-color:var(--accent-color);border-color:var(--accent-color);color:white;transform:rotate(180deg)}
+.theme-toggle:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.btn{position:relative;padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-size:14px;transition:all 0.3s;background-color:var(--accent-color);color:white}
+.btn:hover{opacity:0.8;transform:translateY(-1px)}
+.btn:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.btn:focus::after{content:'';position:absolute;top:-4px;left:-4px;right:-4px;bottom:-4px;border:2px solid var(--accent-color);border-radius:7px;pointer-events:none}
+.btn-secondary{background-color:var(--secondary-color)}
+.btn:disabled{opacity:0.5;cursor:not-allowed;transform:none}
+.btn-primary{background:var(--accent-color);color:white}
+.btn-primary:hover{opacity:0.9;transform:translateY(-2px);box-shadow:0 5px 15px var(--shadow)}
+.btn-primary:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.btn-danger{background-color:#f44336!important;color:white}
+.btn-danger:hover{background-color:#d32f2f!important;opacity:0.9}
+.toolbar{background-color:var(--toolbar-background);padding:15px 30px;border-bottom:1px solid var(--border-color);display:flex;gap:10px;align-items:center;flex-wrap:wrap}
+.toolbar button{padding:8px 15px;font-size:13px}
+.toolbar-info{margin-left:auto;color:var(--text-color);opacity:0.7;font-size:14px}
+.toolbar-divider{width:1px;height:24px;margin:0 12px;background-color:var(--border-color);align-self:center;opacity:0.6}
+
+.scripture-section{flex:1;padding:30px;overflow-y:auto;background-color:var(--card-background);margin:var(--spacing-xl);padding-inline:var(--spacing-xl);padding-block:var(--spacing-lg);border-radius:10px;box-shadow:0 2px 10px var(--shadow);border:1px solid var(--border-color);position:relative;min-width:300px;transition:all 0.3s ease;contain:layout style}
+.scripture-content{font-size:1.1em;line-height:1.8;text-rendering:optimizeLegibility;font-feature-settings:"kern" 1,"liga" 1,"calt" 1}
+.passage-header{background:linear-gradient(135deg,var(--primary-color),var(--primary-color));color:white;padding:20px;border-radius:8px;margin-bottom:20px}
+.passage-header h2{font-size:1.5em;margin-bottom:5px;color:white;border:none}
+.passage-header .date{opacity:0.9;font-size:0.9em}
+.passage-reference{font-weight:bold;font-size:1.2em;margin-bottom:15px;color:var(--accent-color)}
+.plan-label{font-size:1.1rem;font-weight:500;color:var(--accent-color);margin-top:8px;margin-bottom:12px}
+.verse{margin-bottom:8px;padding:var(--spacing-sm);border-radius:var(--radius-sm);cursor:pointer;transition:background-color var(--transition-fast);position:relative;contain:content}
+.verse:hover,.verse:active{background-color:var(--verse-hover);will-change:background-color}
+.verse:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.verse-number{font-weight:bold;color:var(--accent-color);margin-right:8px;font-size:0.85em;vertical-align:super}
+.verse-text{display:inline}
+.highlight-yellow{background-color:#fff59d;color:#000}
+.highlight-green{background-color:#a5d6a7;color:#000}
+.highlight-blue{background-color:#90caf9;color:#000}
+.highlight-pink{background-color:#f48fb1;color:#000}
+.highlight-orange{background-color:#ffcc80;color:#000}
+.highlight-purple{background-color:#ce93d8;color:#000}
+.footnote-ref{position:relative;color:var(--accent-color);font-size:0.7em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease}
+.footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)}
+.footnote-ref::before{content:'['}
+.footnote-ref::after{content:']'}
+.footnotes-container{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:6px;border-left:3px solid var(--accent-color);font-size:0.9em;overflow-y:auto;max-height:300px;position:relative;z-index:100}
+.footnotes-separator{margin:30px 0 15px 0;border:none;border-top:1px solid var(--border-color);opacity:0.5}
+.footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600}
+.footnote{cursor:pointer;transition:all 0.2s ease;padding:8px 12px;border-radius:4px;margin-bottom:8px}
+.footnote:hover{background-color:var(--verse-hover);transform:translateX(2px)}
+.footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;background-color:rgba(0,0,0,0.08);padding:2px 6px;border-radius:3px;transition:all 0.2s ease}
+.footnote:hover .footnote-number{background-color:var(--accent-color);color:white}
+.footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95}
+.footnote.highlighted{background-color:var(--verse-hover);border-left-color:var(--accent-color)}
+.strongs-footnotes-container{display:block!important;max-height:200px;overflow-y:auto;padding:2px;background-color:var(--toolbar-background)}
+.strongs-footnotes-container .footnotes-heading{color:var(--text-color);margin-bottom:15px;font-size:1.1em;font-weight:600}
+.strongs-footnotes-container .footnote{margin-bottom:12px;padding:10px;border-radius:4px;transition:all 0.3s ease}
+.strongs-footnotes-container .footnote:hover{background-color:rgba(0,0,0,0.03)}
+.strongs-footnotes-container .footnote-number{color:var(--accent-color);font-weight:bold;margin-right:8px;font-size:0.85em;background-color:rgba(0,0,0,0.08);padding:3px 6px;border-radius:3px}
+.strongs-footnotes-container .footnote-content{color:var(--text-color);font-size:0.9em;line-height:1.4;opacity:0.95}
+.strongs-footnotes-container .footnote-ref{color:var(--accent-color);font-size:0.75em;vertical-align:super;cursor:pointer;margin:0 1px;text-decoration:none;font-weight:bold;background-color:rgba(0,0,0,0.08);padding:1px 4px;border-radius:3px;line-height:1;transition:all 0.2s ease}
+.strongs-footnotes-container .footnote-ref:hover{background-color:var(--accent-color);color:white;transform:scale(1.1)}
+#strongsFootnotesContainer .footnote{margin-bottom:12px;padding:10px;background-color:var(--toolbar-background);border-radius:4px}
+#strongsFootnotesContainer .footnote-number{font-weight:bold;color:var(--accent-color);margin-right:8px}
+#strongsFootnotesContainer .footnote-content{display:inline}
+#strongsPopup .footnote{cursor:pointer;transition:all 0.2s ease}
+#strongsPopup .footnote:hover{background-color:var(--verse-hover)}
+#strongsPopup .footnote:hover .footnote-number{background-color:var(--accent-color);color:white}
+.verse-navigation{display:flex;align-items:center;gap:10px;margin-right:auto}
+.nav-btn{background:var(--button-bg);border:1px solid var(--border-color);border-radius:4px;padding:6px 10px;cursor:pointer;font-size:12px;color:var(--text-color);transition:all 0.2s ease}
+.nav-btn:hover{background:var(--button-hover-bg);border-color:var(--accent-color)}
+.nav-btn:disabled{opacity:0.5;cursor:not-allowed}
+
+.color-picker{transform:translateZ(0);display:none;position:absolute;background:var(--popup-background);border:1px solid var(--border-color);border-radius:5px;padding:10px;box-shadow:0 4px 15px var(--shadow-strong);z-index:1000}
+.color-picker.active{display:block}
+.color-options{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
+.color-option{width:40px;height:40px;border-radius:5px;cursor:pointer;border:2px solid transparent;transition:all 0.2s}
+.color-option:hover{border-color:var(--accent-color);transform:scale(1.1)}
+.color-option:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+
+.strongs-popup{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:25px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:90vw;width:90%;max-height:90vh;overflow-y:auto;border:1px solid var(--border-color)}
+.strongs-popup.active{display:block}
+.popup-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1999}
+.popup-overlay.active{display:block}
+.popup-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;border-bottom:2px solid var(--accent-color);padding-bottom:10px}
+.popup-header h2{color:var(--text-color)}
+.popup-close{cursor:pointer;font-size:24px;color:var(--text-color);background:none;border:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color 0.2s}
+.popup-close:hover{background-color:var(--verse-hover)}
+.popup-close:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.strongs-content{line-height:1.8;color:var(--text-color)}
+.verse-reference-display{font-size:1.2em;font-weight:bold;color:var(--accent-color);margin-bottom:15px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
+.verse-text-display{padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px;margin-bottom:20px;font-size:1.1em;line-height:1.6}
+.copy-verse-btn{padding:6px 12px;background-color:var(--accent-color);color:white;border:none;border-radius:4px;cursor:pointer;font-size:13px;transition:all 0.2s}
+.copy-verse-btn:hover{opacity:0.8}
+.copy-verse-btn:focus{outline:2px solid white;outline-offset:2px}
+.copy-verse-btn.copied{background-color:#4caf50}
+.embedded-resources{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:20px}
+.resource-frame{border:1px solid var(--border-color);border-radius:5px;overflow:hidden;background:var(--card-background)}
+.resource-frame-header{background:var(--toolbar-background);padding:10px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border-color)}
+.resource-frame-header h4{margin:0;font-size:0.9em;color:var(--text-color)}
+.resource-frame-actions{display:flex;gap:5px}
+.resource-frame-btn{padding:4px 8px;font-size:11px;background:var(--accent-color);color:white;border:none;border-radius:3px;cursor:pointer;transition:opacity 0.2s}
+.resource-frame-btn:hover{opacity:0.8}
+.resource-frame-btn:focus{outline:2px solid white;outline-offset:2px}
+.resource-frame iframe{width:100%;height:400px;border:none}
+.strongs-definition{margin-top:15px;padding:15px;background-color:var(--toolbar-background);border-left:4px solid var(--accent-color);border-radius:4px}
+.strongs-definition h3{color:var(--text-color);margin-bottom:10px}
+.api-attribution{margin-top:20px;padding:15px;background-color:var(--toolbar-background);border-radius:5px;border:1px solid var(--border-color);font-size:0.85em;opacity:0.8}
+.api-attribution a{color:var(--accent-color);text-decoration:none}
+.api-attribution a:hover{text-decoration:underline}
+.api-attribution a:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+
+.notes-section{width:400px;padding:20px;background-color:var(--notes-background);border-left:1px solid var(--border-color);display:flex;flex-direction:column;overflow:hidden;position:relative;min-width:50px;transition:all 0.3s ease}
+.notes-section.hidden{display:none}
+.notes-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}
+.notes-header h3{color:var(--text-color)}
+.notes-controls{display:flex;gap:5px}
+.notes-controls button{padding:5px 10px;font-size:12px}
+.notes-view-toggle{display:flex;gap:5px;padding:5px;background-color:var(--toolbar-background);border-radius:5px;margin-bottom:10px}
+.view-toggle-btn{flex:1;padding:8px;border:none;border-radius:4px;cursor:pointer;font-size:13px;background-color:transparent;color:var(--text-color);transition:all 0.2s}
+.view-toggle-btn.active{background-color:var(--accent-color);color:white}
+.view-toggle-btn:hover:not(.active){background-color:var(--verse-hover)}
+.view-toggle-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.markdown-toolbar{display:flex;gap:5px;padding:10px;background-color:var(--toolbar-background);border:1px solid var(--border-color);border-radius:5px 5px 0 0;flex-wrap:wrap}
+.markdown-btn{padding:6px 12px;background-color:var(--card-background);border:1px solid var(--border-color);border-radius:4px;cursor:pointer;font-size:13px;color:var(--text-color);transition:all 0.2s;display:flex;align-items:center;gap:5px}
+.markdown-btn:hover{background-color:var(--accent-color);color:white;border-color:var(--accent-color)}
+.markdown-btn:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.markdown-btn:active{transform:scale(0.95)}
+#notesInput,#notesDisplay{flex:1;width:100%;border:1px solid var(--border-color);border-radius:0 0 5px 5px;padding:15px;margin-bottom:10px;background-color:var(--card-background);color:var(--text-color)}
+#notesInput{font-family:'Courier New',monospace;font-size:14px;resize:none}
+#notesInput:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+#notesDisplay{overflow-y:auto;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;font-size:14px}
+#notesDisplay h1,#notesDisplay h2,#notesDisplay h3{margin-top:20px;margin-bottom:10px;color:var(--text-color)}
+#notesDisplay h1{font-size:2em;border-bottom:2px solid var(--border-color);padding-bottom:10px}
+#notesDisplay h2{font-size:1.5em}
+#notesDisplay h3{font-size:1.2em}
+#notesDisplay code{background-color:var(--toolbar-background);padding:2px 6px;border-radius:3px;font-family:'Courier New',monospace;border:1px solid var(--border-color)}
+#notesDisplay pre{background-color:var(--toolbar-background);padding:15px;border-radius:5px;overflow-x:auto;border:1px solid var(--border-color)}
+#notesDisplay pre code{background:none;border:none;padding:0}
+#notesDisplay blockquote{border-left:4px solid var(--accent-color);padding-left:15px;margin:15px 0;opacity:0.8}
+#notesDisplay ul,#notesDisplay ol{margin-left:25px;margin-bottom:15px}
+#notesDisplay a{color:var(--accent-color)}
+#notesDisplay a:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+#notesDisplay p{margin-bottom:10px}
+#notesDisplay table{border-collapse:collapse;width:100%;margin:15px 0}
+#notesDisplay table th,#notesDisplay table td{border:1px solid var(--border-color);padding:8px;text-align:left}
+#notesDisplay table th{background-color:var(--toolbar-background);font-weight:bold}
+
+.settings-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--popup-background);border-radius:10px;padding:30px;box-shadow:0 10px 40px var(--shadow-strong);z-index:2000;max-width:600px;width:90%;border:1px solid var(--border-color);max-height:80vh;overflow-y:auto}
+.settings-modal.active{display:block}
+.settings-group{margin-bottom:20px}
+.settings-group label{display:block;margin-bottom:8px;font-weight:bold;color:var(--text-color)}
+.settings-group input,.settings-group select{width:100%;padding:10px;border:1px solid var(--border-color);border-radius:5px;font-size:14px;background-color:var(--card-background);color:var(--text-color)}
+.settings-group input:focus,.settings-group select:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.settings-group small{display:block;margin-top:5px;opacity:0.7;color:var(--text-color)}
+.settings-group small a{color:var(--accent-color)}
+.settings-group small a:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.color-theme-options{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}
+.color-theme-option{padding:15px;border:2px solid var(--border-color);border-radius:8px;cursor:pointer;text-align:center;transition:all 0.3s;background:var(--card-background)}
+.color-theme-option:hover{transform:scale(1.05)}
+.color-theme-option:focus{outline:2px solid var(--accent-color);outline-offset:2px}
+.color-theme-option.selected{border-color:var(--accent-color);background-color:var(--accent-color);color:white}
+.color-theme-preview{width:100%;height:40px;border-radius:5px;margin-bottom:8px}
+.settings-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:25px}
+.settings-section{margin-bottom:25px;padding-bottom:20px;border-bottom:1px solid var(--border-color)}
+.about-content{line-height:1.6}
+.about-creator{margin-bottom:12px;font-size:.9em}
+.about-description{margin-bottom:5px;padding:5px;background-color:var(--bg-secondary);border-radius:5px}
+.about-description p{margin:0;color:var(--text-color)}
+.attribution-links{margin-top:20px;padding-top:15px;border-top:1px solid var(--border-color)}
+.attribution-links h4{margin-bottom:12px;color:var(--text-color);font-size:1em;font-weight:600}
+.attribution-link{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-bottom:8px;background-color:var(--bg-secondary);border-radius:6px;text-decoration:none;color:var(--text-color);transition:all 0.2s ease;border:1px solid transparent}
+.attribution-link:hover{background-color:var(--button-hover-bg);border-color:var(--accent-color);transform:translateY(-1px)}
+.attribution-link i{font-size:1.2em;width:20px;text-align:center}
+.attribution-link.gab{color:#4cf278}
+.attribution-link.gab:hover{background-color:rgba(151,246,202,0.1)}
+.attribution-link.lumo{color:#8c67cd}
+.attribution-link.lumo:hover{background-color:rgba(197,151,246,0.1)}
+.link-description{font-size:0.85em;opacity:0.8;margin-top:4px}
+.version-info{font-family:monospace;padding:2px 4px;background-color:var(--bg-secondary);border-radius:6px;display:inline-block}
+
+.loading-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);display:none;justify-content:center;align-items:center;z-index:3000}
+.loading-overlay.active{display:flex}
+.loading-spinner{width:60px;height:60px;border:6px solid rgba(255,255,255,0.3);border-top-color:var(--accent-color);border-radius:50%;animation:spin 1s linear infinite}
+@keyframes spin{to{transform:rotate(360deg)}
+}
+
+.error-message{background-color:#ff5252;color:white;padding:15px;border-radius:5px;margin-bottom:20px}
+
+@media(max-width:1024px){.sidebar{width:250px}
+.notes-section{width:350px}
+.reference-panel{width:350px}
+.feature-grid{grid-template-columns:1fr}
+}
+@media(max-width:768px){.container{flex-direction:column}
+.sidebar{width:100%;height:auto;max-height:200px}
+.content-area{flex-direction:column}
+.notes-section{width:100%;border-left:none;border-top:1px solid var(--border-color)}
+.reference-panel{width:100%;border-left:none;border-top:1px solid var(--border-color)}
+.footnote{padding:6px;margin-bottom:8px}
+.toolbar-info{display:none}
+.embedded-resources{grid-template-columns:1fr}
+.welcome-content h1{font-size:2em}
+.welcome-actions{flex-direction:column}
+}
+@container(max-width:768px){.feature-grid{grid-template-columns:1fr}
+.toolbar{flex-direction:column;gap:15px}
+}
+.toggle-notes{display:none}
+@media(max-width:768px){.toggle-notes{display:inline-block}
+}
+.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
+@media print{.sidebar,.header-controls,.toolbar,.notes-section,.reference-panel-toggle{display:none!important}
+.scripture-section{margin:0;box-shadow:none;border:none}
+body{background:white;color:black}
+}
+html{font-size:16px}
+@media(max-width:768px){html{font-size:14px}
+}
+@media(hover:none)and(pointer:coarse){.verse:hover{background-color:transparent}
+.sidebar-links a{padding:var(--spacing-md)var(--spacing-lg)}
+.btn{min-height:44px;min-width:44px}
+}
+@media(prefers-contrast:high){.verse-number{font-weight:900}
+.passage-header{border:2px solid var(--text-color)}
+}
+@keyframes spin{0%{transform:rotate(0deg)}
+100%{transform:rotate(360deg)}
+}
+.loading-spinner{animation:spin 1s linear infinite;transform:translateZ(0)}
+.settings-modal:focus{outline:none}
+.settings-modal *:focus-visible{outline:2px solid var(--accent-color);outline-offset:2px}
+@media print{.verse{page-break-inside:avoid}
+.passage-header{break-after:avoid}
+body{background:white!important;color:black!important}
+.verse{cursor:default!important}
+.highlight-yellow{background-color:#fff59d!important}
+}
+
+::-webkit-scrollbar{width:10px;height:10px}
+::-webkit-scrollbar-track{background:var(--background)}
+::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:5px}
+::-webkit-scrollbar-thumb:hover{background:var(--secondary-color)}
+[data-theme="dark"]::-webkit-scrollbar-thumb{background:var(--secondary-color)}
+[data-theme="dark"]::-webkit-scrollbar-thumb:hover{background:var(--accent-color)}
+*{scrollbar-width:thin;scrollbar-color:var(--border-color)var(--background)}
+[data-theme="dark"]*{scrollbar-color:var(--secondary-color)var(--background)}
+
diff --git a/sw.js b/sw.js
new file mode 100644
index 0000000..67c6375
--- /dev/null
+++ b/sw.js
@@ -0,0 +1,249 @@
+import { APP_VERSION } from "./modules/state";
+const CACHE_VERSION = {
+ static: 'v2',
+ api: 'v1',
+ pdf: 'v1'
+};
+const CACHE_NAME = `provinent-scripture-${APP_VERSION}-${CACHE_VERSION.static}`;
+const API_CACHE_NAME = `provinent-api-cache-${CACHE_VERSION.api}`;
+const OFFLINE_PDF_CACHE = `provinent-pdf-cache-${CACHE_VERSION.pdf}`;
+const MAX_CACHE_AGE = 24 * 60 * 60 * 1000;
+const PDF_SIZE_LIMIT = 10 * 1024 * 1024;
+const PRECACHE_URLS = [
+ '/',
+ '/index.html',
+ '/api.js',
+ '/main.js',
+ '/navigation.js',
+ '/passage.js',
+ '/pdf.js',
+ '/settings.js',
+ '/state.js',
+ '/strongs.js',
+ '/ui.js',
+ '/styles.css',
+ '/manifest.json'
+];
+self.addEventListener('install', (event) => {
+ console.log('[Service Worker] Installing and pre-caching static assets');
+ event.waitUntil(
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ console.log('[Service Worker] Caching static assets:', PRECACHE_URLS);
+ return cache.addAll(PRECACHE_URLS);
+ })
+ .then(() => {
+ console.log('[Service Worker] Pre-caching complete, skipping waiting');
+ return self.skipWaiting();
+ })
+ .catch((error) => {
+ console.error('[Service Worker] Pre-caching failed:', error);
+ })
+ );
+});
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys()
+ .then((cacheNames) => {
+ return Promise.all(
+ cacheNames.map((cacheName) => {
+ if (cacheName !== CACHE_NAME &&
+ cacheName !== API_CACHE_NAME &&
+ cacheName !== OFFLINE_PDF_CACHE) {
+ console.log('[Service Worker] Deleting old cache:', cacheName);
+ return caches.delete(cacheName)
+ .catch((error) => {
+ console.warn('[Service Worker] Failed to delete cache:', cacheName, error);
+ });
+ }
+ })
+ );
+ })
+ .then(() => {
+ return Promise.all([
+ manageCacheSize(API_CACHE_NAME, 100),
+ manageCacheSize(OFFLINE_PDF_CACHE, 5)
+ ]);
+ })
+ .then(() => {
+ console.log('[Service Worker] Cache cleanup complete, claiming clients');
+ return self.clients.claim();
+ })
+ );
+});
+self.addEventListener('fetch', (event) => {
+ const url = new URL(event.request.url);
+ if (url.origin === 'https://bible.helloao.org') {
+ event.respondWith(handleApiRequest(event.request));
+ }
+ else if (url.hostname.includes('biblehub.com') ||
+ url.hostname.includes('biblegateway.com') ||
+ url.hostname.includes('bible.com')) {
+ event.respondWith(handleExternalBibleResource(event.request));
+ }
+ else if (url.pathname.endsWith('.pdf')) {
+ event.respondWith(handlePdfRequest(event.request));
+ }
+ else if (PRECACHE_URLS.includes(url.pathname) ||
+ PRECACHE_URLS.includes(url.pathname + '/')) {
+ event.respondWith(handleStaticAssetRequest(event.request));
+ }
+});
+async function handleApiRequest(request) {
+ const cache = await caches.open(API_CACHE_NAME);
+ const cachedResponse = await cache.match(request);
+ if (cachedResponse) {
+ const cacheTime = new Date(cachedResponse.headers.get('sw-cache-time'));
+ if (Date.now() - cacheTime.getTime() < MAX_CACHE_AGE) {
+ console.log('[Service Worker] Serving fresh cached API response');
+ return cachedResponse;
+ }
+ }
+ try {
+ console.log('[Service Worker] Fetching fresh API response from network');
+ const networkResponse = await fetch(request);
+ if (networkResponse.ok) {
+ const headers = new Headers(networkResponse.headers);
+ headers.set('sw-cache-time', new Date().toISOString());
+ const responseToCache = new Response(
+ await networkResponse.clone().blob(),
+ {
+ status: networkResponse.status,
+ statusText: networkResponse.statusText,
+ headers: headers
+ }
+ );
+ await cache.put(request, responseToCache);
+ console.log('[Service Worker] Cached fresh API response');
+ await manageCacheSize(API_CACHE_NAME, 100);
+ }
+ return networkResponse;
+ }
+ catch (error) {
+ console.error('[Service Worker] API request failed, returning offline response');
+ return new Response(
+ JSON.stringify({
+ error: 'Offline mode - Bible data not available',
+ message: 'Please connect to the internet to access Scripture data'
+ }),
+ {
+ status: 503,
+ headers: { 'Content-Type': 'application/json' }
+ }
+ );
+ }
+}
+async function handleExternalBibleResource(request) {
+ try {
+ console.log('[Service Worker] Fetching external Bible resource:', request.url);
+ return await fetch(request);
+ }
+ catch (error) {
+ console.warn('[Service Worker] External resource unavailable offline:', request.url);
+ return new Response(
+ `
+
+ Offline Mode
+ External Bible resources from ${new URL(request.url).hostname}
+ are not available offline.
+ Please connect to the internet to access this resource.
+
+ `,
+ {
+ headers: { 'Content-Type': 'text/html' }
+ }
+ );
+ }
+}
+async function handlePdfRequest(request) {
+ const cache = await caches.open(OFFLINE_PDF_CACHE);
+ const cachedPdf = await cache.match(request);
+ if (cachedPdf) {
+ console.log('[Service Worker] Serving cached PDF:', request.url);
+ return cachedPdf;
+ }
+ try {
+ console.log('[Service Worker] Fetching PDF from network:', request.url);
+ const response = await fetch(request);
+ if (response.status === 200) {
+ const contentLength = response.headers.get('content-length');
+ if (!contentLength || parseInt(contentLength) <= PDF_SIZE_LIMIT) {
+ await cache.put(request, response.clone());
+ console.log('[Service Worker] Cached PDF:', request.url);
+ await manageCacheSize(OFFLINE_PDF_CACHE, 5);
+ }
+ }
+ return response;
+ }
+ catch (error) {
+ console.warn('[Service Worker] PDF unavailable offline:', request.url);
+ return new Response(
+ 'PDF not available offline. Please connect to the internet to access this resource.',
+ {
+ status: 503,
+ headers: { 'Content-Type': 'text/plain' }
+ }
+ );
+ }
+}
+async function handleStaticAssetRequest(request) {
+ try {
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ console.log('[Service Worker] Serving cached static asset:', request.url);
+ return cachedResponse;
+ }
+ console.log('[Service Worker] Fetching static asset from network:', request.url);
+ return await fetch(request);
+ }
+ catch (error) {
+ console.error('[Service Worker] Static asset unavailable:', request.url);
+ if (event.request.destination === 'document') {
+ return new Response(
+ `
+
+ Offline
+ Provident Scripture Study is currently offline.
+ Some features may be limited without an internet connection.
+
+ `,
+ {
+ headers: { 'Content-Type': 'text/html' }
+ }
+ );
+ }
+ throw error;
+ }
+}
+self.addEventListener('message', (event) => {
+ switch (event.data.type) {
+ case 'CLEAR_CACHE':
+ console.log('[Service Worker] Clearing caches per client request');
+ caches.delete(CACHE_NAME).catch(console.warn);
+ caches.delete(API_CACHE_NAME).catch(console.warn);
+ break;
+ case 'CACHE_PDF':
+ console.log('[Service Worker] Caching PDF from client data:', event.data.url);
+ caches.open(OFFLINE_PDF_CACHE)
+ .then(cache => cache.put(event.data.url, new Response(event.data.data)))
+ .catch(console.error);
+ break;
+ default:
+ console.log('[Service Worker] Received unknown message:', event.data.type);
+ }
+});
+async function manageCacheSize(cacheName, maxSize = 50) {
+ try {
+ const cache = await caches.open(cacheName);
+ const requests = await cache.keys();
+ if (requests.length > maxSize) {
+ const excessCount = requests.length - maxSize;
+ const excessRequests = requests.slice(0, excessCount);
+ await Promise.all(excessRequests.map(request => cache.delete(request)));
+ console.log(`[Service Worker] Trimmed ${excessCount} entries from ${cacheName}`);
+ }
+ }
+ catch (error) {
+ console.error(`[Service Worker] Failed to manage cache size for ${cacheName}:`, error);
+ }
+}
\ No newline at end of file