fix(header): two-row layout with scrollable tabs below 1100px

This commit is contained in:
Lucas Kalil 2026-06-15 15:27:10 -03:00
parent 6e33142c96
commit ad6d7ea616
4 changed files with 95 additions and 4 deletions

View file

@ -129,6 +129,7 @@ button {
display: flex;
align-items: center;
gap: 0.55rem;
flex-shrink: 0;
color: var(--text-primary);
text-decoration: none;
font-size: 1.05rem;
@ -159,6 +160,22 @@ button {
display: none;
}
/* edge fades — applied by JS only on a side that has more tabs to scroll to */
.tabs.fade-right {
-webkit-mask-image: linear-gradient(to left, transparent 0, #000 28px);
mask-image: linear-gradient(to left, transparent 0, #000 28px);
}
.tabs.fade-left {
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 28px);
mask-image: linear-gradient(to right, transparent 0, #000 28px);
}
.tabs.fade-left.fade-right {
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
mask-image: linear-gradient(to right, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
}
.tab-btn {
padding: 0.5rem 0.95rem;
border-radius: 999px;
@ -183,9 +200,13 @@ button {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.control-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.8rem;
font-size: 0.8rem;
white-space: nowrap;
@ -195,6 +216,10 @@ button {
transition: color 0.2s, border-color 0.2s;
}
.time-icon {
line-height: 1;
}
.control-btn:hover {
color: var(--text-primary);
border-color: var(--accent-gold);
@ -1048,8 +1073,22 @@ dialog.match-modal::backdrop {
}
}
/* tablet 7681439px: single-row header with centered (reduced) menu */
@media (min-width: 768px) {
/* very narrow: collapse the time toggle to its icon so logo + controls stay on
one row and the tab strip gets its own full-width scrollable row below */
@media (max-width: 420px) {
.time-label {
display: none;
}
.control-btn {
padding: 0.35rem 0.55rem;
}
}
/* single-row header only once logo + 6 tabs + controls genuinely fit (~950px of
content + container padding); below this the tabs keep their own scroll row,
so the controls never wrap into a broken second line. */
@media (min-width: 1100px) {
.tabs {
flex: 0 1 auto;
order: 0;

View file

@ -73,6 +73,7 @@ function activateTab(id, { updateHash = true } = {}) {
}
setPref('lastTab', tab);
if (updateHash) history.replaceState(null, '', `#${tab}`);
scrollActiveTabIntoView(true);
}
// programmatic navigation for cross-view links (e.g. stadium → its matches)
@ -101,7 +102,46 @@ function initTabs() {
});
window.addEventListener('hashchange', () =>
activateTab(location.hash.slice(1), { updateHash: false }));
// edge fades + keep the active tab visible while the nav scrolls horizontally
// (below the 1100px single-row breakpoint the tab strip is a scroll container)
const tabsEl = document.querySelector('.tabs');
tabsEl.addEventListener('scroll', updateTabFades, { passive: true });
let resizeRaf = 0;
window.addEventListener('resize', () => {
cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(() => { scrollActiveTabIntoView(false); updateTabFades(); });
});
// language toggle changes label widths → re-measure overflow and recenter
document.addEventListener('langchange', () => { scrollActiveTabIntoView(false); updateTabFades(); });
activateTab(location.hash.slice(1) || getPrefs().lastTab || 'home');
updateTabFades();
}
// Toggle edge-fade masks on the tab strip: a fade only shows on a side that has
// more tabs to scroll toward, so the cut-off tab no longer looks like a bug.
function updateTabFades() {
const tabs = document.querySelector('.tabs');
if (!tabs) return;
const overflowing = tabs.scrollWidth - tabs.clientWidth > 1;
const atStart = tabs.scrollLeft <= 1;
const atEnd = tabs.scrollLeft >= tabs.scrollWidth - tabs.clientWidth - 1;
tabs.classList.toggle('fade-left', overflowing && !atStart);
tabs.classList.toggle('fade-right', overflowing && !atEnd);
}
// Horizontally scroll the active tab to the center of the strip (no page jump).
function scrollActiveTabIntoView(smooth) {
const tabs = document.querySelector('.tabs');
if (!tabs) return;
const active = tabs.querySelector('.tab-btn.active');
if (!active || tabs.scrollWidth <= tabs.clientWidth) { updateTabFades(); return; }
const tabsRect = tabs.getBoundingClientRect();
const aRect = active.getBoundingClientRect();
const target = tabs.scrollLeft + (aRect.left - tabsRect.left) - (tabs.clientWidth - aRect.width) / 2;
tabs.scrollTo({ left: Math.max(0, target), behavior: smooth ? 'smooth' : 'auto' });
requestAnimationFrame(updateTabFades);
}
// ---------------------------------------------------------------- hero
@ -367,7 +407,10 @@ function initFavorites() {
function syncTimeToggle() {
const btn = document.getElementById('time-toggle');
const mode = getPrefs().timeMode ?? 'local';
btn.textContent = `🕐 ${t(mode === 'local' ? 'time.local' : 'time.stadium')}`;
// icon + label split so the label can collapse on narrow screens (the
// accessible name comes from data-i18n-aria, so hiding the text is a11y-safe).
btn.innerHTML = `<span class="time-icon" aria-hidden="true">🕐</span>` +
`<span class="time-label">${t(mode === 'local' ? 'time.local' : 'time.stadium')}</span>`;
btn.setAttribute('aria-pressed', String(mode === 'stadium'));
}