// The section menu: horizontal tabs on desktop/tablet, a burger menu on phones // (the markup is the same, the CSS decides). Tabs unlock with progress. import { isUnlocked } from '../engine/engine.js'; import { has, t } from '../services/i18n.js'; import { $, h } from './dom.js'; import { showToast } from './toast.js'; const labelKey = (id) => (has(`theme.tab.${id}`) ? `theme.tab.${id}` : `tab.${id}`); /** * @param tabs data/tabs.json * @param onSelect (tabId) called when the visible section changes */ export function createTabs(tabs, onSelect) { const list = $('#tabs'); const menu = $('#menu'); const toggle = $('#menu-toggle'); const buttons = new Map(); let active = 'main'; let known = null; // unlocked tabs seen so far (null until the first update) const unseen = new Set(); const view = (id) => $(`[data-view="${id}"]`); function select(id) { active = id; unseen.delete(id); for (const tab of tabs) { const section = view(tab.id); if (section) section.hidden = tab.id !== id; buttons.get(tab.id)?.button.setAttribute('aria-current', tab.id === id ? 'page' : 'false'); } buttons.get(id)?.item.classList.remove('tab--new'); setMenuOpen(false); onSelect(id); } function setMenuOpen(open) { menu.toggleAttribute('data-open', open); toggle.setAttribute('aria-expanded', String(open)); } toggle.addEventListener('click', () => setMenuOpen(!menu.hasAttribute('data-open'))); /** Build the buttons (call again after a language or theme change to relabel). */ function rebuild() { list.replaceChildren(); buttons.clear(); for (const tab of tabs) { const button = h('button', { class: 'tab', type: 'button', 'aria-current': tab.id === active ? 'page' : 'false', text: t(labelKey(tab.id)), onclick: () => select(tab.id), }); const item = h('li', { class: 'tabs__item' }, button); item.hidden = known ? !known.has(tab.id) : tab.id !== 'main'; if (unseen.has(tab.id)) item.classList.add('tab--new'); list.append(item); buttons.set(tab.id, { item, button }); } } /** Call every frame: reveals newly unlocked tabs, flags the ones needing attention. */ function update(state) { const unlocked = new Set(tabs.filter((tab) => isUnlocked(state, tab.unlock)).map((tab) => tab.id)); for (const tab of tabs) { const entry = buttons.get(tab.id); const isNew = known && unlocked.has(tab.id) && !known.has(tab.id); entry.item.hidden = !unlocked.has(tab.id); if (isNew) { unseen.add(tab.id); entry.item.classList.add('tab--new'); showToast(t('toast.tabUnlocked', { name: t(labelKey(tab.id)) })); } } known = unlocked; // An opportunity is waiting on the contracts tab. buttons.get('contracts')?.item.classList.toggle('tab--alert', Boolean(state.events.current)); if (!unlocked.has(active)) select('main'); } return { rebuild, update, select, get active() { return active; } }; }