assets/emergence-mobile-shell.js
// Keep the working pane between two small edges; supporting chrome is on demand.
export function enhanceMobileShell() {
const root = document.documentElement;
if (!root.classList.contains('has-shell')) return;
const shell = document.querySelector('.em-shell');
const nav = shell.querySelector('.em-lens-nav');
const header = document.querySelector('header.chrome');
const mobile = matchMedia('(max-width:900px)');
const identity = shell.querySelector('.em-rail-id .em-status');
const fullIdentity = identity.textContent;
const secondary = [...nav.querySelectorAll('a')].slice(4);
let focusAfterRoute = false;
const more = document.createElement('button');
more.type = 'button';
more.className = 'em-more-toggle';
more.textContent = 'More';
more.setAttribute('aria-haspopup', 'dialog');
more.setAttribute('aria-controls', 'em-more');
more.setAttribute('aria-expanded', 'false');
nav.append(more);
const dialog = document.createElement('dialog');
dialog.id = 'em-more';
dialog.className = 'em-more';
dialog.setAttribute('aria-labelledby', 'em-more-title');
dialog.innerHTML = '<div class="em-more-heading"><h2 id="em-more-title">More</h2><button type="button" autofocus>Close</button></div>';
const destinations = document.createElement('nav');
destinations.className = 'em-more-links';
destinations.setAttribute('aria-label', 'More views');
secondary.forEach(link => destinations.append(link.cloneNode(true)));
dialog.append(destinations, shell.querySelector('.em-rail-stats').cloneNode(true));
const site = document.createElement('nav');
site.className = 'em-more-links';
site.setAttribute('aria-label', 'Site');
header.querySelectorAll('nav a[href="/"], nav a[href="/xray/"]').forEach(link => site.append(link.cloneNode(true)));
dialog.append(site);
const settings = document.createElement('div');
settings.className = 'em-more-settings';
dialog.append(settings, header.querySelector('.badge').cloneNode(true));
document.body.append(dialog);
// Move the real theme control, retaining the site's storage and accessible label.
const theme = header.querySelector('.mode-toggle');
const themeHome = document.createComment('theme control');
theme.before(themeHome);
const verified = document.createElement('a');
verified.href = '#verified';
verified.className = 'em-pocket-verified';
verified.textContent = '0 verified · inactive';
shell.querySelector('.em-rail-verified').after(verified);
function markCurrent() {
const current = shell.querySelector('[data-pane-on]')?.id;
const isSecondary = secondary.some(link => link.hash === '#' + current);
more.toggleAttribute('data-current', isSecondary);
destinations.querySelectorAll('a').forEach(link => {
if (link.hash === '#' + current) link.setAttribute('aria-current', 'page');
else link.removeAttribute('aria-current');
});
}
function measure() {
root.style.setProperty('--app-top', Math.round(header.getBoundingClientRect().height) + 'px');
root.style.setProperty('--em-dock-height', Math.ceil(nav.getBoundingClientRect().height) + 'px');
}
function sync() {
if (!mobile.matches && dialog.open) dialog.close();
root.classList.toggle('em-pocket', mobile.matches);
identity.textContent = mobile.matches ? 'Not fact-checked · AI' : fullIdentity;
if (mobile.matches) settings.append(theme);
else themeHome.after(theme);
measure();
markCurrent();
}
more.addEventListener('click', () => {
markCurrent();
dialog.showModal();
more.setAttribute('aria-expanded', 'true');
});
dialog.querySelector('button').addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => more.setAttribute('aria-expanded', 'false'));
dialog.addEventListener('click', event => {
if (event.target === dialog) {
const rect = dialog.getBoundingClientRect();
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
}
const link = event.target.closest('a[href^="#"]');
if (link) {
dialog.close();
if (link.hash === location.hash) requestAnimationFrame(focusPane);
else focusAfterRoute = true;
}
});
function focusPane() {
const heading = shell.querySelector('[data-pane-on] h2');
if (!heading) return;
heading.tabIndex = -1;
heading.focus({ preventScroll: true });
}
window.addEventListener('hashchange', () => {
if (dialog.open) dialog.close();
markCurrent();
if (focusAfterRoute) {
focusAfterRoute = false;
requestAnimationFrame(focusPane);
}
});
mobile.addEventListener('change', sync);
new ResizeObserver(measure).observe(header);
new ResizeObserver(measure).observe(nav);
sync();
}