(function (root, factory) { const api = factory(); if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.liveDataStatus = api; })(typeof globalThis !== 'undefined' ? globalThis : this, function () { const feeds = [ ['context', 'Work'], ['notifications', 'Updates'], ['events', 'Activity'], ]; function boundedSeconds(value) { const seconds = Number(value); return Number.isFinite(seconds) && seconds >= 0 ? Math.min(Math.round(seconds), 86400) : null; } function describe(freshness = {}) { const sections = freshness.sections || {}; const hasSectionData = feeds.some(([key]) => Object.prototype.hasOwnProperty.call(sections, key)); const described = feeds.map(([key, label]) => { const section = sections[key] || {}; const state = section.revalidating ? 'refreshing' : (section.stale || section.degraded ? 'delayed' : 'live'); return { key, label, state, ageSeconds: boundedSeconds(section.age_seconds) }; }); const delayed = described.filter(feed => feed.state === 'delayed'); const refreshing = described.filter(feed => feed.state === 'refreshing'); let summary = hasSectionData ? 'Live' : 'Live data unavailable'; if (delayed.length === 1) summary = delayed[0].label + ' delayed'; else if (delayed.length > 1) summary = delayed.length + ' data feeds delayed'; else if (refreshing.length === 1) summary = refreshing[0].label + ' refreshing'; else if (refreshing.length > 1) summary = 'Refreshing live data'; const retryValues = described.map(feed => { const section = sections[feed.key] || {}; return boundedSeconds(section.retry_in_seconds); }).filter(value => value !== null && value > 0); const aggregateRetry = boundedSeconds(freshness.retry_in_seconds); if (aggregateRetry !== null && aggregateRetry > 0) retryValues.push(aggregateRetry); return { summary, feeds: described, nextRetrySeconds: retryValues.length ? Math.min(...retryValues) : null, }; } function createRefreshController({ button, output, refresh, onState = () => {} }) { let pending = null; function run() { if (pending) return pending; button.disabled = true; output.textContent = 'Refreshing live data…'; onState('refreshing'); let request; try { request = refresh(); } catch (error) { request = Promise.reject(error); } pending = Promise.resolve(request).then(result => { if (!result) throw new Error('Live data remains unavailable.'); output.textContent = 'Live data refreshed.'; onState('success'); return result; }).catch(error => { output.textContent = error && error.message ? error.message : 'Live data refresh failed.'; onState('error'); return null; }).finally(() => { button.disabled = false; pending = null; }); return pending; } return { run }; } function createSheetController(options) { let ownsDetour = false; let backgroundState = null; function focusableControls() { return [...options.sheet.querySelectorAll('button:not([disabled]), [tabindex]:not([tabindex="-1"])')] .filter(control => !control.hidden); } function containBackground() { if (backgroundState) return; backgroundState = new Map((options.backgroundElements || []).map(element => [element, element.inert])); backgroundState.forEach((_wasInert, element) => { element.inert = true; }); } function releaseBackground() { if (!backgroundState) return; backgroundState.forEach((wasInert, element) => { element.inert = wasInert; }); backgroundState = null; } function finishClose() { if (options.sheet.hidden) return; options.sheet.hidden = true; options.trigger.setAttribute('aria-expanded', 'false'); if (options.pausedStatus) options.pausedStatus.hidden = true; releaseBackground(); if (ownsDetour) options.timerView?.finishDetour?.(); ownsDetour = false; options.trigger.focus?.(); } function close() { if (options.history?.state?.liveDataStatus) { options.history.back(); return; } finishClose(); } function open() { if (!options.sheet.hidden) return; ownsDetour = options.timerView?.beginDetour?.('live-data-status')?.reason === 'live-data-status'; if (options.pausedStatus) options.pausedStatus.hidden = !ownsDetour; if (options.returnButton) options.returnButton.hidden = !ownsDetour; containBackground(); options.sheet.hidden = false; options.trigger.setAttribute('aria-expanded', 'true'); if (options.history && !options.history.state?.liveDataStatus) { options.history.pushState({ ...options.history.state, liveDataStatus:true }, ''); } options.closeButton.focus(); } function start() { options.trigger.addEventListener('click', open); options.closeButton.addEventListener('click', close); options.returnButton?.addEventListener('click', close); options.sheet.addEventListener('click', event => { if (event.target === options.sheet) close(); }); options.escapeTarget?.addEventListener('keydown', event => { if (options.sheet.hidden) return; if (event.key === 'Escape') { event.preventDefault?.(); close(); return; } if (event.key !== 'Tab') return; const controls = focusableControls(); if (!controls.length) return; const first = controls[0]; const last = controls[controls.length - 1]; if (event.shiftKey && event.target === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && event.target === last) { event.preventDefault(); first.focus(); } }); options.historyTarget?.addEventListener('popstate', event => { if (!options.sheet.hidden && !event.state?.liveDataStatus) finishClose(); }); } return { start, open, close }; } function mount(options) { const document = options.document; const qs = selector => document.querySelector(selector); const trigger = qs('#open-live-data-status'); trigger.addEventListener('click', options.onOpen); return createSheetController({ sheet:qs('#live-data-status-sheet'), trigger, closeButton:qs('#close-live-data-status'), returnButton:qs('#return-from-live-data-status'), pausedStatus:qs('#live-data-status-today-paused'), timerView:options.timerView, history:options.window.history, historyTarget:options.window, escapeTarget:document, backgroundElements:[qs('header'), qs('main'), qs('#mobile-task-dock')].filter(Boolean), }); } return { describe, createRefreshController, createSheetController, mount }; });