(function(){
const SWIPE_THRESHOLD = 50;
const VERTICAL_LIMIT = 40;
// --- Config ---
const LS_KEY = 'AppOverlayConfig';
const defaultConfig = { showArrows: false, enableSwipe: false }; // 👈 hidden arrows, no swipe
function loadConfig(){
try { return Object.assign({}, defaultConfig, JSON.parse(localStorage.getItem(LS_KEY) || '{}')); }
catch { return { ...defaultConfig }; }
}
function saveConfig(cfg){
try { localStorage.setItem(LS_KEY, JSON.stringify(cfg)); } catch {}
}
let config = loadConfig();
// --- Global menu items ---
window.AppOverlayMenuItems = window.AppOverlayMenuItems || [];
// --- DOM creation ---
function createOverlayDOM(){
const overlay = document.createElement('div');
overlay.className = 'app-overlay';
overlay.setAttribute('role','dialog');
overlay.setAttribute('aria-modal','true');
overlay.setAttribute('aria-hidden','true');
overlay.innerHTML = `
<section class="app-dialog" aria-labelledby="appDialogTitle">
<header class="app-dialog__header" style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#2d2d2d;border-bottom:1px solid #3a3a3a;">
<div style="display:flex;align-items:center;gap:10px;">
<div class="app-dialog__menu" data-menu style="font-size:20px;cursor:pointer;color:#e0e0e0;">☰</div>
<div class="app-dialog__title" id="appDialogTitle" style="color:#e0e0e0;font-size:16px;font-weight:500;">Title</div>
</div>
<div style="display:flex;align-items:center;gap:8px;flex-shrink:0;">
<button class="app-navbtn" data-prev type="button" style="display:none;background:#3a3a3a;color:#e0e0e0;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;">←</button>
<button class="app-navbtn" data-next type="button" style="display:none;background:#3a3a3a;color:#e0e0e0;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;">→</button>
<button class="app-dialog__close" type="button" aria-label="Close overlay" style="background:#3a3a3a;color:#e0e0e0;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;">✕</button>
</div>
</header>
<div class="app-dialog__body" style="background:#1e1e1e;padding:20px;min-height:60vh;overflow:auto;color:#e0e0e0;"></div>
</section>
`;
// --- Overlay Styles (fixed for mobile)
overlay.style.cssText = `
position: fixed;
inset: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: flex-start; /* 👈 top-aligned on mobile */
background: rgba(0,0,0,0.8);
z-index: 2147483647;
overflow-y: auto; /* 👈 scroll overlay if content too tall */
padding: 20px 0;
-webkit-overflow-scrolling: touch;
`;
// --- Dialog container adjustments
const dialog = overlay.querySelector('.app-dialog');
dialog.style.cssText = `
background: #1e1e1e;
border: 1px solid #3a3a3a;
border-radius: 8px;
width: 94%;
max-width: 900px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
overflow: hidden;
position: relative;
`;
const bodyEl = overlay.querySelector('.app-dialog__body');
bodyEl.style.overflowY = 'auto';
bodyEl.style.maxHeight = 'calc(90vh - 60px)';
document.body.appendChild(overlay);
return overlay;
}
const overlay = createOverlayDOM();
const titleEl = overlay.querySelector('#appDialogTitle');
const bodyEl = overlay.querySelector('.app-dialog__body');
const closeEl = overlay.querySelector('.app-dialog__close');
const prevEl = overlay.querySelector('[data-prev]');
const nextEl = overlay.querySelector('[data-next]');
const menuEl = overlay.querySelector('[data-menu]');
let slides = [];
let slideEls = [];
let current = 0;
let opener = null;
// --- Menu toggle ---
let menuOpen = false;
let currentMenuStack = [];
menuEl.addEventListener('click', (e) => {
e.stopPropagation();
menuOpen = !menuOpen;
if (menuOpen) showMenu();
else hideMenu();
});
function showMenu(items = window.AppOverlayMenuItems, parentDropdown = null) {
const rect = menuEl.getBoundingClientRect();
let dropdown = document.createElement('div');
dropdown.className = 'app-menu-dropdown';
dropdown.style.position = 'fixed';
dropdown.style.background = '#fff';
dropdown.style.border = '1px solid #ddd';
dropdown.style.borderRadius = '4px';
dropdown.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
dropdown.style.minWidth = '180px';
dropdown.style.zIndex = '2147483647';
dropdown.style.transition = 'all 0.2s ease';
dropdown.style.overflow = 'hidden';
if (!parentDropdown) {
dropdown.style.top = (rect.bottom + 4) + 'px';
dropdown.style.left = rect.left + 'px';
currentMenuStack = [];
} else {
const r = parentDropdown.getBoundingClientRect();
dropdown.style.top = r.top + 'px';
dropdown.style.left = (r.right - 2) + 'px';
items = [{ label: "◀ Back", type: "back" }, ...items];
}
items.forEach(item => {
const btn = document.createElement('button');
btn.textContent = item.label;
btn.className = 'app-menu-item';
btn.style.display = 'flex';
btn.style.alignItems = 'center';
btn.style.justifyContent = 'space-between';
btn.style.width = '100%';
btn.style.padding = '8px 12px';
btn.style.border = 'none';
btn.style.background = 'transparent';
btn.style.cursor = 'pointer';
btn.style.fontSize = '14px';
btn.style.color = '#333';
btn.onmouseenter = () => btn.style.background = '#f0f0f0';
btn.onmouseleave = () => btn.style.background = 'transparent';
if (item.type === 'toggle') {
const toggle = document.createElement('span');
toggle.textContent = item.state ? '✅' : '⬜';
btn.appendChild(toggle);
btn.onclick = (e) => {
e.stopPropagation();
item.state = !item.state;
toggle.textContent = item.state ? '✅' : '⬜';
if (item.onToggle) item.onToggle(item.state);
hideMenu();
};
} else if (item.submenu) {
const arrow = document.createElement('span');
arrow.textContent = '▶';
btn.appendChild(arrow);
btn.onclick = (e) => {
e.stopPropagation();
currentMenuStack.push(dropdown);
dropdown.style.display = 'none';
const sub = showMenu(item.submenu, dropdown);
document.body.appendChild(sub);
};
} else if (item.type === 'back') {
btn.textContent = '◀ Back';
btn.onclick = (e) => {
e.stopPropagation();
dropdown.remove();
if (currentMenuStack.length > 0) {
const prevMenu = currentMenuStack.pop();
prevMenu.style.display = 'block';
}
};
} else {
btn.onclick = (e) => {
e.stopPropagation();
if (item.action) item.action();
hideMenu();
};
}
dropdown.appendChild(btn);
});
if (!parentDropdown) {
hideMenu();
document.body.appendChild(dropdown);
setTimeout(() => {
const closeOnOutsideClick = (e) => {
const clickedOnMenu = e.target.closest('.app-menu-dropdown') || e.target === menuEl;
if (!clickedOnMenu) {
hideMenu();
document.removeEventListener('click', closeOnOutsideClick);
}
};
document.addEventListener('click', closeOnOutsideClick);
}, 10);
}
dropdown.style.display = 'block';
return dropdown;
}
function hideMenu() {
document.querySelectorAll('.app-menu-dropdown').forEach(m => m.remove());
currentMenuStack = [];
menuOpen = false;
}
// --- Render / Update ---
function renderSlides(){
bodyEl.innerHTML = '';
slideEls = slides.map(s => {
const el = document.createElement('article');
el.className = 'app-slide';
el.innerHTML = s.html || '';
bodyEl.appendChild(el);
if (typeof s.onRender === 'function') requestAnimationFrame(() => s.onRender(el));
return el;
});
}
function update(){
titleEl.textContent = slides[current]?.title || '';
slideEls.forEach((el,i)=>el.classList.toggle('is-active', i===current));
const showArrows = config.showArrows && slides.length > 1;
prevEl.style.display = showArrows ? '' : 'none';
nextEl.style.display = showArrows ? '' : 'none';
menuEl.style.display = window.AppOverlayMenuItems.length > 0 ? 'block' : 'none';
}
function mountOnTop(){
if (overlay.parentElement !== document.body || document.body.lastElementChild !== overlay) {
document.body.appendChild(overlay);
}
}
// --- Controls ---
function open(items, startIndex=0, openerEl=null){
slides = Array.isArray(items) ? items : [];
if (slides.length === 0) return;
opener = openerEl || document.activeElement;
renderSlides();
current = Math.max(0, Math.min(startIndex, slides.length - 1));
mountOnTop();
overlay.classList.add('open');
overlay.setAttribute('aria-hidden','false');
overlay.style.display = 'flex';
update();
closeEl.focus();
}
function close(){
overlay.classList.remove('open');
overlay.setAttribute('aria-hidden','true');
overlay.style.display = 'none';
hideMenu();
if (opener && typeof opener.focus === 'function') opener.focus();
}
function next(){ if (slides.length > 1) { current = (current + 1) % slides.length; update(); } }
function prev(){ if (slides.length > 1) { current = (current - 1 + slides.length) % slides.length; update(); } }
// --- Events ---
closeEl.addEventListener('click', close);
nextEl.addEventListener('click', next);
prevEl.addEventListener('click', prev);
overlay.addEventListener('click', (e)=>{
if (e.target === overlay) close();
if (menuOpen && !menuEl.contains(e.target) && !e.target.closest('.app-menu-dropdown')) {
hideMenu();
}
});
window.addEventListener('keydown', (e)=>{
if (!overlay.classList.contains('open')) return;
if (e.key === 'Escape') { e.preventDefault(); if (menuOpen) hideMenu(); else close(); }
if (e.key === 'ArrowRight') { e.preventDefault(); next(); }
if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); }
});
// --- Public API ---
function configure(partial){
if (!partial || typeof partial !== 'object') return;
config = Object.assign({}, config, partial);
saveConfig(config);
update();
}
function getConfig(){ return Object.assign({}, config); }
window.AppOverlay = { open, close, next, prev, configure, getConfig };
})();