/** * List page tab bar — horizontal scroll behavior. * Keeps tabs on a single line, scrolls the active tab into view on mount, * and toggles has-overflow/at-start/at-end classes that drive the CSS * gradient fade masks. */ import { resolveHost } from '../core/app-dom.js'; const BOUND_ATTR = 'data-app-list-tabs-bound'; const EDGE_TOLERANCE = 1; const updateOverflow = (el) => { const hasOverflow = el.scrollWidth - el.clientWidth > EDGE_TOLERANCE; el.classList.toggle('has-overflow', hasOverflow); if (!hasOverflow) { el.classList.remove('at-start', 'at-end'); return; } const atStart = el.scrollLeft <= EDGE_TOLERANCE; const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - EDGE_TOLERANCE; el.classList.toggle('at-start', atStart); el.classList.toggle('at-end', atEnd); }; const scrollActiveIntoView = (el) => { const active = el.querySelector('.is-active'); if (!active) { return; } const activeCenter = active.offsetLeft + active.offsetWidth / 2; const target = activeCenter - el.clientWidth / 2; const max = el.scrollWidth - el.clientWidth; el.scrollLeft = Math.max(0, Math.min(target, max)); }; export function initListTabs(root = document, options = {}) { const selector = String(options.selector || '.app-list-tabs').trim() || '.app-list-tabs'; const host = resolveHost(root); const cleanupFns = []; const bound = []; const tabs = Array.from(host.querySelectorAll(selector)); tabs.forEach((el) => { if (el.getAttribute(BOUND_ATTR) === '1') { return; } el.setAttribute(BOUND_ATTR, '1'); bound.push(el); // Initial scroll-into-view uses instant positioning (no smooth behavior // before the strip has finished layout — smooth kicks in on user scroll). const previousBehavior = el.style.scrollBehavior; el.style.scrollBehavior = 'auto'; scrollActiveIntoView(el); el.style.scrollBehavior = previousBehavior; updateOverflow(el); const onScroll = () => updateOverflow(el); el.addEventListener('scroll', onScroll, { passive: true }); cleanupFns.push(() => el.removeEventListener('scroll', onScroll)); if (typeof ResizeObserver !== 'undefined') { const resizeObserver = new ResizeObserver(() => updateOverflow(el)); resizeObserver.observe(el); cleanupFns.push(() => resizeObserver.disconnect()); } else { const onResize = () => updateOverflow(el); window.addEventListener('resize', onResize, { passive: true }); cleanupFns.push(() => window.removeEventListener('resize', onResize)); } }); const destroy = () => { cleanupFns.forEach((fn) => { try { fn(); } catch { /* ignore */ } }); cleanupFns.length = 0; bound.forEach((el) => { el.removeAttribute(BOUND_ATTR); el.classList.remove('has-overflow', 'at-start', 'at-end'); }); bound.length = 0; }; return { destroy }; }