/* SECTION 2/3: CSS */
#sggs-reader-widget.sggs {
–bg: #ffffff;
–panel: #f6f7f9;
–panel2: #fafbfc;
–text: #111827;
–muted: #6b7280;
–border: #e5e7eb;
–shadow: 0 8px 24px rgba(0,0,0,0.10);
–btn: #111827;
–btnText: #ffffff;
–focus: 0 0 0 3px rgba(59,130,246,0.35);
–danger: #b91c1c;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, “Noto Sans”, sans-serif;
}
#sggs-reader-widget.sggs[data-theme=”dark”] {
–bg: #0b1220;
–panel: #0f1a2e;
–panel2: #13213a;
–text: #e5e7eb;
–muted: #9ca3af;
–border: #24324b;
–shadow: 0 10px 28px rgba(0,0,0,0.35);
–btn: #e5e7eb;
–btnText: #0b1220;
–danger: #fca5a5;
}
@media (prefers-color-scheme: dark) {
#sggs-reader-widget.sggs[data-theme=”auto”] {
–bg: #0b1220;
–panel: #0f1a2e;
–panel2: #13213a;
–text: #e5e7eb;
–muted: #9ca3af;
–border: #24324b;
–shadow: 0 10px 28px rgba(0,0,0,0.35);
–btn: #e5e7eb;
–btnText: #0b1220;
–danger: #fca5a5;
}
}
#sggs-reader-widget .sggs-shell {
background: var(–bg);
color: var(–text);
border: 1px solid var(–border);
border-radius: 16px;
box-shadow: var(–shadow);
overflow: hidden;
max-width: 1120px;
margin: 14px auto;
}
#sggs-reader-widget .sggs-header {
display: flex;
gap: 12px;
justify-content: space-between;
align-items: center;
padding: 16px 16px 12px;
border-bottom: 1px solid var(–border);
background: linear-gradient(0deg, transparent, rgba(0,0,0,0.02));
}
#sggs-reader-widget .sggs-h1 {
font-weight: 700;
font-size: 19px;
line-height: 1.2;
}
#sggs-reader-widget .sggs-sub {
font-size: 12px;
color: var(–muted);
margin-top: 3px;
}
#sggs-reader-widget .sggs-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
#sggs-reader-widget .sggs-btn,
#sggs-reader-widget .sggs-iconbtn,
#sggs-reader-widget .sggs-result {
border: 1px solid var(–border);
background: var(–panel);
color: var(–text);
border-radius: 10px;
padding: 10px 12px;
font-size: 14px;
cursor: pointer;
min-height: 44px;
}
#sggs-reader-widget .sggs-btn-primary {
background: var(–btn);
color: var(–btnText);
border-color: transparent;
}
#sggs-reader-widget .sggs-btn:focus,
#sggs-reader-widget .sggs-iconbtn:focus,
#sggs-reader-widget .sggs-input:focus,
#sggs-reader-widget .sggs-result:focus,
#sggs-reader-widget select:focus {
outline: none;
box-shadow: var(–focus);
}
#sggs-reader-widget .sggs-iconbtn {
width: 44px;
height: 44px;
font-size: 22px;
display: grid;
place-items: center;
padding: 0;
}
#sggs-reader-widget .sggs-label {
font-size: 12px;
color: var(–muted);
display: block;
margin-bottom: 6px;
}
#sggs-reader-widget .sggs-input {
width: 100%;
min-height: 44px;
border: 1px solid var(–border);
background: var(–bg);
color: var(–text);
border-radius: 10px;
padding: 10px 11px;
font-size: 14px;
box-sizing: border-box;
}
#sggs-reader-widget .sggs-navrow {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
padding: 12px;
background: var(–panel);
border-bottom: 1px solid var(–border);
}
@media (min-width: 760px) {
#sggs-reader-widget .sggs-navrow {
grid-template-columns: 1fr 1fr;
}
}
#sggs-reader-widget .sggs-navitem {
background: var(–bg);
border: 1px solid var(–border);
border-radius: 12px;
padding: 10px;
}
#sggs-reader-widget .sggs-panel {
display: none;
padding: 12px;
border-bottom: 1px solid var(–border);
background: var(–panel2);
}
#sggs-reader-widget .sggs-panel.is-active {
display: block;
}
#sggs-reader-widget .sggs-reader {
padding: 12px;
}
#sggs-reader-widget .sggs-readerbar {
display: grid;
grid-template-columns: 44px minmax(0, 1fr) 44px;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
#sggs-reader-widget .sggs-loc {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 8px;
align-items: center;
}
#sggs-reader-widget .sggs-meta {
grid-column: 1 / -1;
font-size: 12px;
color: var(–muted);
padding-left: 2px;
margin-top: 6px;
}
#sggs-reader-widget .sggs-toggles {
display: flex;
gap: 12px;
flex-wrap: wrap;
padding: 10px;
background: var(–panel);
border: 1px solid var(–border);
border-radius: 12px;
margin-bottom: 10px;
}
#sggs-reader-widget .sggs-check {
font-size: 13px;
color: var(–text);
display: inline-flex;
gap: 8px;
align-items: center;
}
#sggs-reader-widget .sggs-status {
font-size: 13px;
color: var(–muted);
min-height: 18px;
margin: 6px 2px 10px;
}
#sggs-reader-widget .sggs-status.is-error {
color: var(–danger);
}
#sggs-reader-widget .sggs-content {
border: 1px solid var(–border);
background: var(–bg);
border-radius: 14px;
padding: 12px;
min-height: 300px;
max-height: 68vh;
overflow: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
#sggs-reader-widget .sggs-readerfooter {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
#sggs-reader-widget .sggs-reading-header {
position: sticky;
top: -12px;
z-index: 2;
background: var(–bg);
padding: 4px 2px 12px;
border-bottom: 1px solid var(–border);
margin-bottom: 10px;
}
#sggs-reader-widget .sggs-reading-title {
font-size: 18px;
font-weight: 700;
}
#sggs-reader-widget .sggs-reading-meta {
font-size: 12px;
color: var(–muted);
margin-top: 4px;
line-height: 1.45;
}
#sggs-reader-widget .sggs-line {
padding: 12px 10px 14px;
border-bottom: 1px solid var(–border);
}
#sggs-reader-widget .sggs-line:last-child {
border-bottom: none;
}
#sggs-reader-widget .sggs-linehead {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
#sggs-reader-widget .sggs-line-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
@media (min-width: 880px) {
#sggs-reader-widget .sggs-line-grid.dual {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
}
#sggs-reader-widget .sggs-col {
min-width: 0;
}
#sggs-reader-widget .sggs-col-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
color: var(–muted);
margin-bottom: 6px;
}
#sggs-reader-widget .sggs-gur,
#sggs-reader-widget .sggs-eng,
#sggs-reader-widget .sggs-trn,
#sggs-reader-widget .sggs-result-gur,
#sggs-reader-widget .sggs-result-eng {
white-space: pre-line;
word-break: break-word;
}
#sggs-reader-widget .sggs-gur {
font-size: 21px;
line-height: 1.7;
}
#sggs-reader-widget .sggs-eng {
font-size: 15px;
line-height: 1.6;
}
#sggs-reader-widget .sggs-trn {
font-size: 13px;
color: var(–muted);
line-height: 1.55;
margin-top: 10px;
}
#sggs-reader-widget .sggs-minirow {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-top: 8px;
}
#sggs-reader-widget .sggs-pill {
display: inline-flex;
gap: 8px;
align-items: center;
background: var(–panel);
border: 1px solid var(–border);
border-radius: 999px;
padding: 6px 10px;
font-size: 12px;
color: var(–muted);
}
#sggs-reader-widget .sggs-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
@media (min-width: 760px) {
#sggs-reader-widget .sggs-grid.cols-2 {
grid-template-columns: 1fr 1fr;
}
}
#sggs-reader-widget .sggs-card {
border: 1px solid var(–border);
background: var(–bg);
border-radius: 12px;
padding: 12px;
}
#sggs-reader-widget .sggs-card h3 {
margin: 0 0 6px 0;
font-size: 14px;
}
#sggs-reader-widget .sggs-card p {
margin: 0;
font-size: 12px;
color: var(–muted);
line-height: 1.45;
}
#sggs-reader-widget .sggs-note {
margin-top: 10px;
padding: 10px 12px;
border: 1px solid var(–border);
border-radius: 12px;
background: var(–panel);
color: var(–muted);
font-size: 12px;
line-height: 1.5;
}
#sggs-reader-widget .sggs-results {
display: grid;
gap: 8px;
}
#sggs-reader-widget .sggs-result {
text-align: left;
width: 100%;
background: var(–bg);
}
#sggs-reader-widget .sggs-result-title {
font-size: 12px;
color: var(–muted);
margin-bottom: 6px;
}
#sggs-reader-widget .sggs-result-gur {
font-size: 18px;
line-height: 1.55;
margin-bottom: 5px;
}
#sggs-reader-widget .sggs-result-eng {
font-size: 14px;
line-height: 1.45;
}
#sggs-reader-widget .sggs-bookmark {
display: flex;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
padding: 10px 0;
border-bottom: 1px solid var(–border);
}
#sggs-reader-widget .sggs-bookmark:last-child {
border-bottom: none;
}
#sggs-reader-widget .sggs-bookmark-title {
font-weight: 700;
font-size: 14px;
}
#sggs-reader-widget .sggs-bookmark-meta {
font-size: 12px;
color: var(–muted);
margin-top: 4px;
}
#sggs-reader-widget .sggs-empty {
border: 1px dashed var(–border);
border-radius: 12px;
padding: 14px;
color: var(–muted);
font-size: 13px;
background: var(–panel);
}
#sggs-reader-widget .sggs-footer {
padding: 12px;
border-top: 1px solid var(–border);
font-size: 12px;
color: var(–muted);
background: var(–panel);
text-align: center;
}
@media (max-width: 640px) {
#sggs-reader-widget .sggs-header {
align-items: flex-start;
flex-direction: column;
}
#sggs-reader-widget .sggs-readerbar {
grid-template-columns: 40px 1fr 40px;
}
#sggs-reader-widget .sggs-loc {
grid-template-columns: 1fr;
}
#sggs-reader-widget .sggs-reading-header {
position: static;
top: auto;
}
#sggs-reader-widget .sggs-content {
min-height: 260px;
max-height: 60vh;
padding: 10px;
}
#sggs-reader-widget .sggs-gur {
font-size: 19px;
line-height: 1.65;
}
#sggs-reader-widget .sggs-eng {
font-size: 14px;
line-height: 1.55;
}
#sggs-reader-widget .sggs-line {
padding: 10px 8px 12px;
}
}
/* SECTION 3/3: JAVASCRIPT */
(function () {
‘use strict’;
var API_BASE = ‘
https://api.sikher.com’ ;;
var DEFAULT_TRANSLATION = 13;
var DEFAULT_TRANSLITERATION = 69;
var MAX_PAGE = 1430;
var IS_MOBILE = window.matchMedia(‘(max-width: 767px)’).matches;
var SECTION_BATCH_SIZE = IS_MOBILE ? 3 : 8;
var INITIAL_RENDER_COUNT = IS_MOBILE ? 35 : 140;
var RENDER_INCREMENT = IS_MOBILE ? 25 : 120;
var STORAGE_KEYS = {
theme: ‘sggs_theme_v7’,
bookmarks: ‘sggs_bookmarks_v7’,
lastState: ‘sggs_last_state_v7’
};
var NAMED_READINGS = [
{ key: ‘japji-sahib’, label: ‘Japji Sahib’, startPage: 1, endPage: 8, note: ‘Opening bani.’, patterns: [‘Jap Ji Sahib’, ‘Japji’] },
{ key: ‘so-dar’, label: ‘So Dar’, startPage: 8, endPage: 12, note: ‘Evening bani reading.’, patterns: [‘So Dar’] },
{ key: ‘so-purakh’, label: ‘So Purakh’, startPage: 10, endPage: 12, note: ‘Evening bani reading.’, patterns: [‘So Purakh’, ‘So Purkh’] },
{ key: ‘kirtan-sohila’, label: ‘Kirtan Sohila’, startPage: 12, endPage: 13, note: ‘Night prayer reading.’, patterns: [‘Sohila’, ‘Kirtan Sohila’] }
];
var COMPLETE_SECTIONS = [
{ key: ‘pre-raga-section’, label: ‘Pre-Raag Section’, startPage: 1, endPage: 13, note: ‘Opening section before the main raag arrangement.’ }
];
var RAAG_SECTIONS = [
{ key: ‘sri-raga’, label: ‘Sri Raag’, startPage: 14, endPage: 93 },
{ key: ‘majh’, label: ‘Majh’, startPage: 94, endPage: 150 },
{ key: ‘gauri’, label: ‘Gauri’, startPage: 151, endPage: 346 },
{ key: ‘asa’, label: ‘Asa’, startPage: 347, endPage: 488 },
{ key: ‘gujari’, label: ‘Gujari’, startPage: 489, endPage: 526 },
{ key: ‘devgandhari’, label: ‘Devgandhari’, startPage: 527, endPage: 536 },
{ key: ‘bihagara’, label: ‘Bihagara’, startPage: 537, endPage: 556 },
{ key: ‘wadhans’, label: ‘Wadhans’, startPage: 557, endPage: 594 },
{ key: ‘sorath’, label: ‘Sorath’, startPage: 595, endPage: 659 },
{ key: ‘dhanasari’, label: ‘Dhanasari’, startPage: 660, endPage: 695 },
{ key: ‘jaitsari’, label: ‘Jaitsari’, startPage: 696, endPage: 710 },
{ key: ‘todi’, label: ‘Todi’, startPage: 711, endPage: 718 },
{ key: ‘bairari’, label: ‘Bairari’, startPage: 719, endPage: 720 },
{ key: ’tilang’, label: ‘Tilang’, startPage: 721, endPage: 727 },
{ key: ‘suhi’, label: ‘Suhi’, startPage: 728, endPage: 794 },
{ key: ‘bilaval’, label: ‘Bilaval’, startPage: 795, endPage: 858 },
{ key: ‘gaund’, label: ‘Gaund’, startPage: 859, endPage: 875 },
{ key: ‘ramkali’, label: ‘Ramkali’, startPage: 876, endPage: 974 },
{ key: ‘nat-narayan’, label: ‘Nat Narayan’, startPage: 975, endPage: 983 },
{ key: ‘mali-gaura’, label: ‘Mali Gaura’, startPage: 984, endPage: 988 },
{ key: ‘maru’, label: ‘Maru’, startPage: 989, endPage: 1106 },
{ key: ‘tukhari’, label: ‘Tukhari’, startPage: 1107, endPage: 1117 },
{ key: ‘kedara’, label: ‘Kedara’, startPage: 1118, endPage: 1124 },
{ key: ‘bhairon’, label: ‘Bhairon’, startPage: 1125, endPage: 1167 },
{ key: ‘basant’, label: ‘Basant’, startPage: 1168, endPage: 1196 },
{ key: ‘sarang’, label: ‘Sarang’, startPage: 1197, endPage: 1253 },
{ key: ‘malar’, label: ‘Malar’, startPage: 1254, endPage: 1293 },
{ key: ‘kanara’, label: ‘Kanara’, startPage: 1294, endPage: 1318 },
{ key: ‘kalyan’, label: ‘Kalyan’, startPage: 1319, endPage: 1326 },
{ key: ‘prabhati’, label: ‘Prabhati’, startPage: 1327, endPage: 1351 },
{ key: ‘jaijaiwanti’, label: ‘Jaijaiwanti’, startPage: 1352, endPage: 1353, patterns: [‘Jaijaiwanti’, ‘Jaijavanti’] }
];
var POST_RAAG_SECTIONS = [
{ key: ‘saloks-in-sahaskrit’, label: ‘Saloks in Sahaskrit’, startPage: 1353, endPage: 1360, patterns: [‘Sahaskrit’, ‘Sahaskriti’] },
{ key: ‘gatha’, label: ‘Gatha’, startPage: 1360, endPage: 1361, patterns: [‘Gatha’] },
{ key: ‘phunahe’, label: ‘Phunahe’, startPage: 1361, endPage: 1363, patterns: [‘Phunahe’, ‘Funhe’] },
{ key: ‘chaubole’, label: ‘Chaubole’, startPage: 1363, endPage: 1364, patterns: [‘Chaubole’] },
{ key: ‘saloks-of-kabir’, label: ‘Saloks of Kabir’, startPage: 1364, endPage: 1377, patterns: [‘Kabir’] },
{ key: ‘saloks-of-farid’, label: ‘Saloks of Sheikh Farid’, startPage: 1377, endPage: 1384, patterns: [‘Farid’] },
{ key: ‘swayyas’, label: ‘Swayyas’, startPage: 1385, endPage: 1409, patterns: [‘Swayiye’, ‘Swayyas’, ‘Swaiyyas’] },
{ key: ‘salok-varaan-te-wadhik’, label: ‘Salok Vaaran te Wadhik’, startPage: 1410, endPage: 1429, patterns: [‘Vaaran’, ‘Wadhik’] },
{ key: ‘mundavani’, label: ‘Mundavani’, startPage: 1429, endPage: 1429, patterns: [‘Mundavani’] },
{ key: ‘rag-mala’, label: ‘Rag Mala’, startPage: 1429, endPage: 1430, patterns: [‘Rag Mala’, ‘Ragmala’] }
];
var ALL_SECTION_ITEMS = NAMED_READINGS.concat(COMPLETE_SECTIONS, RAAG_SECTIONS, POST_RAAG_SECTIONS);
var SECTION_MAP = {};
ALL_SECTION_ITEMS.forEach(function (item) { SECTION_MAP[item.key] = item; });
function start() {
var widget = document.getElementById(‘sggs-reader-widget’);
if (!widget || widget.dataset.bound === ‘1’) return;
widget.dataset.bound = ‘1’;
var state = {
theme: widget.getAttribute(‘data-theme’) || ‘auto’,
currentType: ‘page’,
currentId: 1,
currentLabel: ‘Ang 1’,
currentMeta: ”,
currentMode: ‘direct’,
currentSectionKey: ”,
allLines: [],
renderCount: 0,
sectionProgress: null,
bookmarks: []
};
var els = {
navSelect: byId(‘sggsNavSelect’),
jumpSelect: byId(‘sggsJumpSelect’),
themeBtn: byId(‘sggsThemeBtn’),
addBookmarkBtn: byId(‘sggsAddBookmarkBtn’),
fullSectionSelect: byId(‘sggsFullSectionSelect’),
sectionNote: byId(‘sggsSectionNote’),
readType: byId(‘sggsReadType’),
readId: byId(‘sggsReadId’),
loadReadingBtn: byId(‘sggsLoadReadingBtn’),
randomBtn: byId(‘sggsRandomBtn’),
prevBtn: byId(‘sggsPrevBtn’),
nextBtn: byId(‘sggsNextBtn’),
idInput: byId(‘sggsIdInput’),
goBtn: byId(‘sggsGoBtn’),
searchInput: byId(‘sggsSearchInput’),
searchMode: byId(‘sggsSearchMode’),
searchBtn: byId(‘sggsSearchBtn’),
searchResults: byId(‘sggsSearchResults’),
bookmarksList: byId(‘sggsBookmarksList’),
showEnglish: byId(‘sggsShowEnglish’),
showGurmukhi: byId(‘sggsShowGurmukhi’),
showTranslit: byId(‘sggsShowTranslit’),
status: byId(‘sggsStatus’),
meta: byId(‘sggsMeta’),
content: byId(‘sggsContent’),
readerFooter: byId(‘sggsReaderFooter’)
};
loadStorage();
applyTheme(state.theme);
fillQuickJump();
fillSectionMenu();
bindEvents();
renderBookmarks();
syncInputs();
if (state.currentMode === ‘section’ && state.currentSectionKey && SECTION_MAP[state.currentSectionKey]) {
beginSectionLoad(SECTION_MAP[state.currentSectionKey]);
} else {
loadReading(state.currentType, state.currentId);
}
function byId(id) {
return document.getElementById(id);
}
function bindEvents() {
if (els.navSelect) {
els.navSelect.addEventListener(‘change’, function () {
showPanel(els.navSelect.value);
});
}
if (els.jumpSelect) {
els.jumpSelect.addEventListener(‘change’, function () {
var raw = els.jumpSelect.value || ”;
if (!raw) return;
try {
var parsed = JSON.parse(raw);
if (parsed.mode === ‘section’ && parsed.sectionKey) {
beginSectionLoad(SECTION_MAP[parsed.sectionKey]);
} else if (parsed.action === ‘random’) {
loadRandom(parsed.type);
} else {
loadReading(parsed.type || ‘page’, parsed.id || 1, parsed.label || null);
}
els.jumpSelect.value = ”;
} catch (err) {
setStatus(‘Quick jump option could not be read.’, true);
}
});
}
if (els.themeBtn) {
els.themeBtn.addEventListener(‘click’, function () {
var order = [‘auto’, ‘dark’, ‘light’];
var next = order[(order.indexOf(state.theme) + 1) % order.length];
applyTheme(next);
saveStorage();
});
}
if (els.addBookmarkBtn) {
els.addBookmarkBtn.addEventListener(‘click’, addBookmark);
}
if (els.fullSectionSelect) {
els.fullSectionSelect.addEventListener(‘change’, function () {
var key = els.fullSectionSelect.value || ”;
if (!key || !SECTION_MAP[key]) return;
beginSectionLoad(SECTION_MAP[key]);
showPanel(‘browse’);
});
}
if (els.readType) {
els.readType.addEventListener(‘change’, function () {
syncInputsFromBrowse();
});
}
if (els.loadReadingBtn) {
els.loadReadingBtn.addEventListener(‘click’, function () {
loadReading(els.readType.value, els.readId.value);
});
}
if (els.randomBtn) {
els.randomBtn.addEventListener(‘click’, function () {
var t = els.readType.value === ‘hymn’ ? 2 : 1;
loadRandom(t);
});
}
if (els.searchBtn) {
els.searchBtn.addEventListener(‘click’, performSearch);
}
if (els.searchInput) {
els.searchInput.addEventListener(‘keydown’, function (ev) {
if (ev.key === ‘Enter’) performSearch();
});
}
if (els.prevBtn) {
els.prevBtn.addEventListener(‘click’, function () {
loadReading(‘page’, Math.max(1, Number(state.currentId || 1) – 1));
});
}
if (els.nextBtn) {
els.nextBtn.addEventListener(‘click’, function () {
loadReading(‘page’, Math.min(MAX_PAGE, Number(state.currentId || 1) + 1));
});
}
if (els.goBtn) {
els.goBtn.addEventListener(‘click’, function () {
loadReading(‘page’, els.idInput.value);
});
}
if (els.readerFooter) {
els.readerFooter.addEventListener(‘click’, function (ev) {
var btn = ev.target.closest(‘button’);
if (!btn) return;
if (btn.id === ‘sggsShowMoreBtn’) {
state.renderCount = Math.min(state.allLines.length, state.renderCount + RENDER_INCREMENT);
renderReading();
}
if (btn.id === ‘sggsLoadNextSectionBtn’) {
loadNextSectionChunk();
}
if (btn.id === ‘sggsBackToTopBtn’) {
els.content.scrollTop = 0;
}
});
}
[els.showEnglish, els.showGurmukhi, els.showTranslit].forEach(function (el) {
if (!el) return;
el.addEventListener(‘change’, renderReading);
});
}
function showPanel(name) {
[‘start’, ‘browse’, ‘search’, ‘bookmarks’].forEach(function (key) {
var panel = byId(‘sggs-tab-‘ + key);
if (panel) panel.classList.toggle(‘is-active’, key === name);
});
}
function fillQuickJump() {
if (!els.jumpSelect) return;
els.jumpSelect.innerHTML = [
‘Choose…’,
optionValue({ mode: ‘section’, sectionKey: ‘japji-sahib’ }, ‘Japji Sahib — Full Reading’),
optionValue({ mode: ‘section’, sectionKey: ‘so-dar’ }, ‘So Dar — Full Reading’),
optionValue({ mode: ‘section’, sectionKey: ‘so-purakh’ }, ‘So Purakh — Full Reading’),
optionValue({ mode: ‘section’, sectionKey: ‘kirtan-sohila’ }, ‘Kirtan Sohila — Full Reading’),
optionValue({ mode: ‘section’, sectionKey: ‘pre-raga-section’ }, ‘Pre-Raag Section — Full Reading’),
optionValue({ mode: ‘section’, sectionKey: ‘sri-raga’ }, ‘Sri Raag — Full Reading’),
optionValue({ mode: ‘section’, sectionKey: ‘prabhati’ }, ‘Prabhati — Full Reading’),
optionValue({ mode: ‘section’, sectionKey: ‘rag-mala’ }, ‘Rag Mala — Full Reading’),
optionValue({ action: ‘random’, type: 1 }, ‘Random Ang’),
optionValue({ action: ‘random’, type: 2 }, ‘Random Hymn’)
].join(”);
}
function optionValue(data, label) {
return ” + escapeHtml(label) + ”;
}
function fillSectionMenu() {
if (!els.fullSectionSelect) return;
var html = [‘Choose a named section…’];
html.push(”);
NAMED_READINGS.forEach(function (item) {
html.push(” + escapeHtml(item.label + ‘ — Angs ‘ + item.startPage + ‘–’ + item.endPage) + ”);
});
html.push(”);
html.push(”);
COMPLETE_SECTIONS.forEach(function (item) {
html.push(” + escapeHtml(item.label + ‘ — Angs ‘ + item.startPage + ‘–’ + item.endPage) + ”);
});
html.push(”);
html.push(”);
RAAG_SECTIONS.forEach(function (item) {
html.push(” + escapeHtml(item.label + ‘ — Angs ‘ + item.startPage + ‘–’ + item.endPage) + ”);
});
html.push(”);
html.push(”);
POST_RAAG_SECTIONS.forEach(function (item) {
html.push(” + escapeHtml(item.label + ‘ — Angs ‘ + item.startPage + ‘–’ + item.endPage) + ”);
});
html.push(”);
els.fullSectionSelect.innerHTML = html.join(”);
}
function setSectionNote(text) {
if (els.sectionNote) els.sectionNote.textContent = text || ”;
}
function syncInputs() {
if (els.readType) els.readType.value = state.currentType;
if (els.readId) els.readId.value = state.currentId;
if (els.idInput) els.idInput.value = state.currentId;
updateReaderLabel();
}
function syncInputsFromBrowse() {
var type = els.readType ? els.readType.value : ‘page’;
var rawId = els.readId ? els.readId.value : 1;
rawId = type === ‘page’ ? clampInt(rawId, 1, MAX_PAGE) : Math.max(1, parseInt(rawId, 10) || 1);
if (els.readId) els.readId.value = rawId;
if (els.idInput) els.idInput.value = rawId;
setMeta(formatTypeLabel(type) + ‘ ‘ + rawId);
}
function updateReaderLabel() {
var labelEl = widget.querySelector(‘label[for=”sggsIdInput”]’);
if (labelEl) labelEl.textContent = ‘Ang / Page ID’;
}
function formatTypeLabel(type) {
if (type === ‘page’) return ‘Ang / Page’;
if (type === ‘hymn’) return ‘Hymn’;
return ‘Line’;
}
function formatReadingTitle(type, id) {
if (type === ‘page’) return ‘Ang ‘ + id;
if (type === ‘hymn’) return ‘Hymn ‘ + id;
return ‘Line ‘ + id;
}
function applyTheme(theme) {
state.theme = theme || ‘auto’;
widget.setAttribute(‘data-theme’, state.theme);
if (els.themeBtn) els.themeBtn.textContent = ‘Theme: ‘ + capitalize(state.theme);
}
function loadStorage() {
try {
var savedTheme = localStorage.getItem(STORAGE_KEYS.theme);
var savedBookmarks = JSON.parse(localStorage.getItem(STORAGE_KEYS.bookmarks) || ‘[]’);
var savedState = JSON.parse(localStorage.getItem(STORAGE_KEYS.lastState) || ‘{}’);
if (savedTheme) state.theme = savedTheme;
if (Array.isArray(savedBookmarks)) state.bookmarks = savedBookmarks;
if (savedState && typeof savedState === ‘object’) {
state.currentType = savedState.currentType || state.currentType;
state.currentId = clampForType(savedState.currentType || state.currentType, savedState.currentId || state.currentId);
state.currentMode = savedState.currentMode || state.currentMode;
state.currentSectionKey = savedState.currentSectionKey || ”;
}
} catch (err) {}
}
function saveStorage() {
try {
localStorage.setItem(STORAGE_KEYS.theme, state.theme);
localStorage.setItem(STORAGE_KEYS.bookmarks, JSON.stringify(state.bookmarks));
localStorage.setItem(STORAGE_KEYS.lastState, JSON.stringify({
currentType: state.currentType,
currentId: state.currentId,
currentMode: state.currentMode,
currentSectionKey: state.currentSectionKey
}));
} catch (err) {}
}
function setStatus(text, isError) {
if (!els.status) return;
els.status.textContent = text || ”;
els.status.classList.toggle(‘is-error’, !!isError);
}
function setMeta(text) {
if (els.meta) els.meta.textContent = text || ”;
}
function fetchJson(path) {
return fetch(API_BASE + path, {
method: ‘GET’,
headers: { ‘Accept’: ‘application/json’ },
credentials: ‘omit’
}).then(function (res) {
if (!res.ok) throw new Error(‘HTTP ‘ + res.status);
return res.json();
});
}
function normalizeReadType(type) {
return type === ‘hymn’ || type === ‘line’ ? type : ‘page’;
}
function clampForType(type, value) {
var n = parseInt(value, 10);
if (Number.isNaN(n) || n MAX_PAGE) n = MAX_PAGE;
return n;
}
function clampInt(value, min, max) {
var n = parseInt(value, 10);
if (Number.isNaN(n)) n = min;
if (n max) n = max;
return n;
}
function normalizeName(str) {
return String(str || ”).toLowerCase().replace(/raag/g, ”).replace(/[^a-z0-9]+/g, ”);
}
function capitalize(str) {
str = String(str || ”);
return str.charAt(0).toUpperCase() + str.slice(1);
}
function escapeHtml(str) {
return String(str == null ? ” : str)
.replace(/&/g, ‘&’)
.replace(//g, ‘>’)
.replace(/”/g, ‘"’)
.replace(/’/g, ‘'’);
}
function normalizePayload(payload) {
var rows = Array.isArray(payload) ? payload : (payload && typeof payload === ‘object’ ? [payload] : []);
return rows.map(normalizeLine).filter(function (item) {
return item.gurmukhi || item.english || item.translit;
});
}
function normalizeLine(item) {
item = item || {};
return {
id: item.id || ”,
gurmukhi: item.text || ”,
english: item.translation && item.translation.text ? item.translation.text : ”,
translit: item.transliteration && item.transliteration.text ? item.transliteration.text : ”,
page: item.page || ”,
line: item.line || ”,
hymn: item.hymn || ”,
section: item.section || ”,
melodyName: item.melody && item.melody.melody ? item.melody.melody : ”,
authorName: item.author && item.author.author ? item.author.author : ”
};
}
function dedupeLines(items) {
var seen = Object.create(null);
return items.filter(function (item) {
var key = [item.id || ”, item.page || ”, item.line || ”, item.hymn || ”, item.gurmukhi || ”].join(‘|’);
if (seen[key]) return false;
seen[key] = true;
return true;
});
}
function filterBySection(items, patterns) {
if (!patterns || !patterns.length) return items;
var normalized = patterns.map(normalizeName);
var filtered = items.filter(function (item) {
var sectionName = normalizeName(item.section || ”);
return normalized.some(function (pattern) {
return sectionName.indexOf(pattern) !== -1;
});
});
return filtered.length ? filtered : items;
}
function buildMeta() {
var bits = [];
var first = state.allLines[0] || {};
var last = state.allLines[state.allLines.length – 1] || {};
if (state.currentMode === ‘section’ && state.sectionProgress) {
bits.push(‘Angs ‘ + state.sectionProgress.startPage + ‘–’ + state.sectionProgress.endPage);
bits.push(‘Loaded through Ang ‘ + state.sectionProgress.loadedThrough);
if (state.sectionProgress.done) bits.push(‘Complete section’);
} else {
bits.push(formatReadingTitle(state.currentType, state.currentId));
}
bits.push(state.allLines.length + ‘ line’ + (state.allLines.length === 1 ? ” : ‘s’));
if (first.section) bits.push(first.section);
if (first.melodyName) bits.push(first.melodyName);
if (first.authorName) bits.push(first.authorName);
if (first.page && last.page && first.page !== last.page) bits.push(‘Visible range spans Angs ‘ + first.page + ‘–’ + last.page);
return bits.join(‘ • ‘);
}
function renderReaderFooter() {
if (!els.readerFooter) return;
var html = [];
if (state.allLines.length > state.renderCount) {
html.push(‘
Load more lines ‘);
}
if (state.currentMode === ‘section’ && state.sectionProgress && !state.sectionProgress.done) {
html.push(‘
Load next pages ‘);
}
if (state.allLines.length) {
html.push(‘
Back to top ‘);
}
els.readerFooter.innerHTML = html.join(”);
}
function renderReading() {
state.currentMeta = buildMeta();
setMeta(state.currentMeta || ”);
if (!els.content) return;
if (!state.allLines.length) {
if (state.currentMode === ‘section’ && state.sectionProgress) {
els.content.innerHTML = ‘
Loading section…
‘;
} else {
els.content.innerHTML = ‘
No content returned yet.
‘;
}
renderReaderFooter();
return;
}
var visibleLines = state.allLines.slice(0, state.renderCount);
var showEnglish = !!(els.showEnglish && els.showEnglish.checked);
var showGurmukhi = !!(els.showGurmukhi && els.showGurmukhi.checked);
var showTranslit = !!(els.showTranslit && els.showTranslit.checked);
var html = [
‘‘
];
visibleLines.forEach(function (line, idx) {
var dual = showGurmukhi && showEnglish ? ‘ dual’ : ”;
html.push(‘
‘);
html.push(‘‘);
html.push(‘Ang ‘ + escapeHtml(String(line.page || ‘?’)) + ‘ ‘);
if (line.hymn) html.push(‘Hymn ‘ + escapeHtml(String(line.hymn)) + ‘ ‘);
if (line.line) html.push(‘Line ‘ + escapeHtml(String(line.line)) + ‘ ‘);
if (line.section) html.push(‘‘ + escapeHtml(line.section) + ‘ ‘);
if (line.melodyName) html.push(‘‘ + escapeHtml(line.melodyName) + ‘ ‘);
if (line.authorName) html.push(‘‘ + escapeHtml(line.authorName) + ‘ ‘);
html.push(‘
‘);
if (!showGurmukhi && !showEnglish && !showTranslit) {
if (idx === 0) html.push(‘Turn on at least one display option above.
‘);
html.push(‘ ‘);
return;
}
html.push(‘
‘);
if (showGurmukhi) {
html.push(
‘
‘ +
‘
Gurmukhi
‘ +
‘
‘ + escapeHtml(line.gurmukhi) + ‘
‘ +
‘
‘
);
}
if (showEnglish) {
html.push(
‘
‘ +
‘
English
‘ +
‘
‘ + escapeHtml(line.english || ‘—’) + ‘
‘ +
‘
‘
);
}
html.push(‘
‘);
if (showTranslit && line.translit) {
html.push(
‘
‘ +
‘
Transliteration
‘ +
‘
‘ + escapeHtml(line.translit) + ‘
‘ +
‘
‘
);
}
html.push(‘‘);
});
els.content.innerHTML = html.join(”);
renderReaderFooter();
}
function applyDirectReading(type, id, items, label) {
state.currentType = type;
state.currentId = id;
state.currentLabel = label;
state.currentMode = ‘direct’;
state.currentSectionKey = ”;
state.sectionProgress = null;
state.allLines = dedupeLines(items);
state.renderCount = state.allLines.length;
if (els.fullSectionSelect) els.fullSectionSelect.value = ”;
setSectionNote(‘Choose a section to load its full text into the reader window.’);
syncInputs();
saveStorage();
renderReading();
setStatus(‘Loaded ‘ + label + ‘.’);
}
function buildReadingPath(type, id) {
if (type === ‘page’) return ‘/page/’ + id + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION;
if (type === ‘hymn’) return ‘/hymn/’ + id + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION;
return ‘/’ + id + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION;
}
function loadReading(type, id, customLabel) {
type = normalizeReadType(type);
id = clampForType(type, id);
setStatus(‘Loading ‘ + (customLabel || formatReadingTitle(type, id)) + ‘…’);
return fetchJson(buildReadingPath(type, id)).then(function (payload) {
var items = dedupeLines(normalizePayload(payload));
if (!items.length) throw new Error(‘Empty reading’);
applyDirectReading(type, id, items, customLabel || formatReadingTitle(type, id));
}).catch(function () {
state.allLines = [];
state.currentType = type;
state.currentId = id;
state.currentMode = ‘direct’;
state.currentSectionKey = ”;
state.sectionProgress = null;
syncInputs();
renderReading();
setStatus(‘Could not load ‘ + formatReadingTitle(type, id) + ‘.’, true);
});
}
function beginSectionLoad(section) {
if (!section) return;
state.currentType = ‘page’;
state.currentId = section.startPage;
state.currentLabel = section.label;
state.currentMode = ‘section’;
state.currentSectionKey = section.key;
state.allLines = [];
state.renderCount = 0;
state.sectionProgress = {
key: section.key,
startPage: section.startPage,
endPage: section.endPage,
nextPage: section.startPage,
loadedThrough: section.startPage – 1,
done: false,
patterns: section.patterns || []
};
if (els.fullSectionSelect) els.fullSectionSelect.value = section.key;
setSectionNote(section.note || (‘Loading ‘ + section.label + ‘ as a section reading.’));
syncInputs();
renderReading();
saveStorage();
loadNextSectionChunk();
}
function loadNextSectionChunk() {
if (!state.sectionProgress || state.sectionProgress.done) return Promise.resolve();
var startPage = state.sectionProgress.nextPage;
var endPage = Math.min(state.sectionProgress.endPage, startPage + SECTION_BATCH_SIZE – 1);
var requests = [];
setStatus(‘Loading ‘ + state.currentLabel + ‘… Angs ‘ + startPage + ‘–’ + endPage);
for (var p = startPage; p state.sectionProgress.endPage;
if (oldRender === 0) {
state.renderCount = Math.min(state.allLines.length, INITIAL_RENDER_COUNT);
} else if (oldRender >= oldLength) {
state.renderCount = Math.min(state.allLines.length, oldRender + newItems.length);
} else {
state.renderCount = oldRender;
}
saveStorage();
renderReading();
if (state.sectionProgress.done) {
setStatus(‘Loaded complete section: ‘ + state.currentLabel + ‘.’);
} else {
setStatus(‘Loaded through Ang ‘ + endPage + ‘.’);
}
}).catch(function () {
setStatus(‘Could not continue loading this section.’, true);
});
}
function performSearch() {
var query = (els.searchInput && els.searchInput.value || ”).trim();
var mode = els.searchMode ? els.searchMode.value : ‘2’;
if (!query) {
if (els.searchResults) els.searchResults.innerHTML = ‘
Enter a search term.
‘;
return;
}
setStatus(‘Searching GurbaniDB…’);
fetchJson(buildSearchPath(query, mode)).then(function (payload) {
var items = dedupeLines(normalizePayload(payload));
renderSearchResults(items, query);
setStatus(‘Search complete.’);
showPanel(‘search’);
}).catch(function () {
if (els.searchResults) els.searchResults.innerHTML = ‘
Search failed for this query.
‘;
setStatus(‘Search failed.’, true);
});
}
function buildSearchPath(query, mode) {
var q = encodeURIComponent(query);
if (mode === ‘0’) return ‘/search/’ + q + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION + ‘/0’;
if (mode === ‘1’) return ‘/search/1/’ + q + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION + ‘/0’;
if (mode === ‘2’) return ‘/search/2/’ + q + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION + ‘/0’;
if (mode === ‘3’) return ‘/search/3/’ + q + ‘/’ + DEFAULT_TRANSLATION + ‘/0’;
if (mode === ‘4’) return ‘/search/4/’ + q + ‘/’ + DEFAULT_TRANSLITERATION + ‘/0’;
return ‘/search/2/’ + q + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION + ‘/0’;
}
function renderSearchResults(items, query) {
if (!els.searchResults) return;
if (!items.length) {
els.searchResults.innerHTML = ‘
No results found for “’ + escapeHtml(query) + ‘”.
‘;
return;
}
els.searchResults.innerHTML = items.slice(0, 100).map(function (line, index) {
return [
‘
‘,
‘Ang ‘ + escapeHtml(String(line.page || ‘?’)) + ‘ • Hymn ‘ + escapeHtml(String(line.hymn || ‘?’)) + ‘ • Line ‘ + escapeHtml(String(line.line || ‘?’)) + ‘
‘,
line.gurmukhi ? ‘‘ + escapeHtml(line.gurmukhi) + ‘
‘ : ”,
line.english ? ‘‘ + escapeHtml(line.english) + ‘
‘ : ”,
‘ ‘
].join(”);
}).join(”);
Array.prototype.forEach.call(els.searchResults.querySelectorAll(‘[data-result-index]’), function (btn) {
btn.addEventListener(‘click’, function () {
var line = items[Number(btn.getAttribute(‘data-result-index’))];
if (!line) return;
if (line.hymn) loadReading(‘hymn’, line.hymn);
else if (line.page) loadReading(‘page’, line.page);
else if (line.id) loadReading(‘line’, line.id);
});
});
}
function loadRandom(type) {
setStatus(type === 2 ? ‘Loading random hymn…’ : ‘Loading random ang…’);
return fetchJson(‘/random/’ + type + ‘/’ + DEFAULT_TRANSLATION + ‘/’ + DEFAULT_TRANSLITERATION).then(function (payload) {
var items = dedupeLines(normalizePayload(payload));
if (!items.length) throw new Error(‘No random reading returned’);
if (type === 2) {
var hymnId = items[0].hymn || 1;
applyDirectReading(‘hymn’, hymnId, items, ‘Random Hymn ‘ + hymnId);
} else {
var pageId = items[0].page || 1;
applyDirectReading(‘page’, pageId, items, ‘Random Ang ‘ + pageId);
}
}).catch(function () {
setStatus(‘Random reading failed.’, true);
});
}
function addBookmark() {
if (!state.allLines.length) {
setStatus(‘Load a reading before adding a bookmark.’, true);
return;
}
var bookmark = {
id: Date.now(),
mode: state.currentMode,
type: state.currentType,
readingId: state.currentId,
label: state.currentLabel,
meta: state.currentMeta,
sectionKey: state.currentSectionKey
};
state.bookmarks.unshift(bookmark);
state.bookmarks = state.bookmarks.slice(0, 50);
saveStorage();
renderBookmarks();
setStatus(‘Bookmark added.’);
}
function renderBookmarks() {
if (!els.bookmarksList) return;
if (!state.bookmarks.length) {
els.bookmarksList.innerHTML = ‘
No bookmarks yet.
‘;
return;
}
els.bookmarksList.innerHTML = state.bookmarks.map(function (item) {
return [
‘
‘,
‘
‘,
‘
‘ + escapeHtml(item.label || ‘Bookmark’) + ‘
‘,
item.meta ? ‘
‘ + escapeHtml(item.meta) + ‘
‘ : ”,
‘
‘,
‘
‘,
‘Open ‘,
‘Remove ‘,
‘
‘,
‘
‘
].join(”);
}).join(”);
Array.prototype.forEach.call(els.bookmarksList.querySelectorAll(‘[data-open-bookmark]’), function (btn) {
btn.addEventListener(‘click’, function () {
var id = Number(btn.getAttribute(‘data-open-bookmark’));
var bookmark = state.bookmarks.find(function (b) { return b.id === id; });
if (!bookmark) return;
if (bookmark.mode === ‘section’ && bookmark.sectionKey && SECTION_MAP[bookmark.sectionKey]) {
beginSectionLoad(SECTION_MAP[bookmark.sectionKey]);
} else {
loadReading(bookmark.type, bookmark.readingId, bookmark.label);
}
showPanel(‘bookmarks’);
});
});
Array.prototype.forEach.call(els.bookmarksList.querySelectorAll(‘[data-remove-bookmark]’), function (btn) {
btn.addEventListener(‘click’, function () {
var id = Number(btn.getAttribute(‘data-remove-bookmark’));
state.bookmarks = state.bookmarks.filter(function (b) { return b.id !== id; });
saveStorage();
renderBookmarks();
});
});
}
}
if (document.readyState === ‘loading’) {
document.addEventListener(‘DOMContentLoaded’, start);
} else {
start();
}
})();