// Tab navigation + overflow ("More" dropdown), ported from mnw.js:85-275. function debounce(fn: () => void, ms: number): () => void { let timer: ReturnType; return () => { clearTimeout(timer); timer = setTimeout(fn, ms); }; } const tabOverflow = (() => { let containers: HTMLElement[] = []; function updateHighlight(tabsEl: Element): void { const moreWrap = tabsEl.querySelector('.tab-more-wrap'); if (!moreWrap) return; const moreBtn = moreWrap.querySelector('.tab-more-btn'); const menu = moreWrap.querySelector('.tab-overflow-menu'); if (!moreBtn || !menu) return; const hasActive = menu.querySelector('.tab.is-selected'); moreBtn.classList.toggle('is-selected', !!hasActive); } function reflow(tabsEl: HTMLElement): void { const moreWrap = tabsEl.querySelector('.tab-more-wrap'); if (!moreWrap) return; const menu = moreWrap.querySelector('.tab-overflow-menu'); if (!menu) return; // Move all tabs back from the menu into the row (before moreWrap). Array.from(menu.children).forEach((t) => tabsEl.insertBefore(t, moreWrap)); moreWrap.style.display = 'none'; const tabs = Array.from(tabsEl.querySelectorAll(':scope > .tab')); if (tabs.length === 0) return; const available = tabsEl.clientWidth; let totalWidth = 0; tabs.forEach((t) => { totalWidth += t.offsetWidth; }); if (totalWidth <= available) return; const moreBtnWidth = 90; let used = 0; let cutoff = tabs.length; for (let i = 0; i < tabs.length; i++) { used += tabs[i].offsetWidth; if (used + moreBtnWidth > available) { cutoff = i; break; } } if (cutoff < 1) cutoff = 1; for (let j = cutoff; j < tabs.length; j++) menu.appendChild(tabs[j]); moreWrap.style.display = ''; updateHighlight(tabsEl); } function setup(tabsEl: HTMLElement): void { if (tabsEl.dataset.overflowInit) return; tabsEl.dataset.overflowInit = '1'; const moreWrap = document.createElement('div'); moreWrap.className = 'tab-more-wrap'; moreWrap.style.display = 'none'; const moreBtn = document.createElement('button'); moreBtn.className = 'tab tab-more-btn'; moreBtn.type = 'button'; moreBtn.textContent = 'More'; moreBtn.setAttribute('aria-haspopup', 'true'); moreBtn.setAttribute('aria-expanded', 'false'); moreBtn.addEventListener('click', (e) => { e.stopPropagation(); const m = moreWrap.querySelector('.tab-overflow-menu'); if (!m) return; const open = m.style.display === 'block'; m.style.display = open ? 'none' : 'block'; moreBtn.setAttribute('aria-expanded', open ? 'false' : 'true'); }); const menu = document.createElement('div'); menu.className = 'tab-overflow-menu'; menu.style.display = 'none'; moreWrap.appendChild(moreBtn); moreWrap.appendChild(menu); const spinner = tabsEl.querySelector('.htmx-indicator'); if (spinner) tabsEl.insertBefore(moreWrap, spinner); else tabsEl.appendChild(moreWrap); reflow(tabsEl); } function reflowAll(): void { containers.forEach(reflow); } function init(): void { containers = Array.from(document.querySelectorAll('.tabs[role="tablist"]')); containers.forEach(setup); window.addEventListener('resize', debounce(reflowAll, 150)); } return { init, updateHighlight }; })(); /** Activate a tab button: toggle selected state, wire aria, sync the hash, and * update the overflow highlight. Ported from mnw.js:85. */ export function setActiveTab(btn: HTMLElement): void { const container = btn.closest('.tabs'); if (!container) return; container.querySelectorAll('.tab').forEach((tab) => { tab.classList.remove('is-selected'); tab.setAttribute('aria-selected', 'false'); }); btn.classList.add('is-selected'); btn.setAttribute('aria-selected', 'true'); const panel = document.getElementById('tab-content'); if (panel) panel.setAttribute('aria-labelledby', btn.id); if (btn.id) history.replaceState(null, '', '#' + btn.id); const menu = btn.closest('.tab-overflow-menu'); if (menu) menu.style.display = 'none'; tabOverflow.updateHighlight(container); } /** One-time tab wiring: hover preload, overflow init, hash restore, outside- * click close, cart-count badge. Ported from mnw.js:227. */ export function initTabs(): void { document.querySelectorAll('.tab').forEach((btn) => { btn.addEventListener('mouseenter', function (this: HTMLElement) { if (this.dataset.preloaded) return; const url = this.getAttribute('hx-get'); if (!url) return; this.dataset.preloaded = '1'; fetch(url, { headers: { 'HX-Request': 'true' } }).catch(() => {}); }); }); tabOverflow.init(); const hash = location.hash.replace('#', ''); if (hash) { const tab = document.getElementById(hash); if (tab && tab.classList.contains('tab')) tab.click(); } document.addEventListener('click', () => { document.querySelectorAll('.tab-overflow-menu').forEach((m) => { m.style.display = 'none'; }); document.querySelectorAll('.tab-more-btn').forEach((b) => b.setAttribute('aria-expanded', 'false')); }); const cartLink = document.getElementById('nav-cart-link'); if (cartLink) { fetch('/api/cart/count', { credentials: 'same-origin' }) .then((r) => (r.ok ? r.json() : null)) .then((data: { count?: number } | null) => { if (data && typeof data.count === 'number' && data.count > 0) { cartLink.classList.remove('hidden'); const badge = document.getElementById('cart-badge'); if (badge) badge.textContent = ' (' + data.count + ')'; } }) .catch(() => {}); } }