(function (root, factory) { if (typeof module === 'object' && module.exports) module.exports = factory; else root.createMobileDeviceSetup = factory; })(typeof self !== 'undefined' ? self : this, function createMobileDeviceSetup(options) { const promptDismissKey = 'stackchain.device-setup-prompt-dismissed-until'; const promptDismissMs = 7 * 24 * 60 * 60 * 1000; const steps = [ ['install', options.installButton, options.installStatus, options.install, 'Install'], ['offline', options.offlineButton, options.offlineStatus, options.enableOffline, 'Enable'], ['protection', options.protectionButton, options.protectionStatus, options.protectStorage, 'Protect'], ['push', options.pushButton, options.pushStatus, options.enablePush, 'Enable'], ['deadline', options.deadlineButton, options.deadlineStatus, options.enableDeadline, 'Enable'], ]; let trigger = options.launcher; let ownsDetour = false; let backgroundInert = null; let pendingResume = true; function focusableControls() { return [...options.sheet.querySelectorAll( 'button:not([disabled]), select:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' )].filter(control => !control.hidden); } function containBackground() { if (backgroundInert) return; backgroundInert = new Map((options.backgroundElements || []).map(element => [element, element.inert])); backgroundInert.forEach((_wasInert, element) => { element.inert = true; }); } function releaseBackground() { if (!backgroundInert) return; backgroundInert.forEach((wasInert, element) => { element.inert = wasInert; }); backgroundInert = null; } function readinessCounts(readiness) { const available = steps.filter(([name]) => readiness[name].state !== 'unavailable').length; const complete = steps.filter(([name]) => readiness[name].state === 'complete').length; return {available, complete}; } function renderPrompt(readiness) { if (!options.promptCard) return; const {available, complete} = readinessCounts(readiness); let dismissed = false; try { dismissed = Number(options.promptStorage?.getItem(promptDismissKey) || 0) > (options.now?.() ?? Date.now()); } catch (_error) {} options.promptSummary.textContent = `${complete} of ${available} steps complete`; options.promptCard.hidden = dismissed || available === 0 || complete === available; } async function render() { const readiness = await options.getReadiness(); let available = 0; let complete = 0; steps.forEach(([name, button, status, _action, defaultActionLabel]) => { const step = readiness[name]; status.textContent = step.detail; button.textContent = step.actionLabel || defaultActionLabel; button.hidden = step.state === 'complete' || step.state === 'unavailable'; button.disabled = step.state === 'pending'; if (step.state !== 'unavailable') available += 1; if (step.state === 'complete') complete += 1; }); options.readyStatus.textContent = complete === available ? 'This device is ready.' : `${complete} of ${available} available steps ready.`; renderPrompt(readiness); return readiness; } async function open(event) { trigger = event?.currentTarget || options.launcher; if (options.isMobile?.()) { ownsDetour = options.timerView?.beginDetour?.('device-setup')?.reason === 'device-setup'; } await render(); containBackground(); options.sheet.hidden = false; if (options.history && !options.history.state?.deviceSetup) { options.history.pushState({...options.history.state, deviceSetup:true}, ''); } options.closeButton.focus(); } function finishClose(resume = true) { options.sheet.hidden = true; releaseBackground(); if (resume && ownsDetour) options.timerView?.finishDetour?.(); ownsDetour = false; trigger?.focus?.(); } function close(resume = true) { if (options.history?.state?.deviceSetup) { pendingResume = resume; options.history.back(); return; } finishClose(resume); } async function start() { options.launcher.addEventListener('click', open); options.promptLauncher?.addEventListener('click', open); options.promptDismiss?.addEventListener('click', () => { try { options.promptStorage?.setItem(promptDismissKey, String((options.now?.() ?? Date.now()) + promptDismissMs)); } catch (_error) {} options.promptCard.hidden = true; }); options.closeButton.addEventListener('click', close); options.returnButton?.addEventListener('click', () => close(false)); options.sheet.addEventListener('click', event => { if (event.target === options.sheet) close(); }); options.escapeTarget.addEventListener('keydown', event => { if (event.key === 'Escape' && !options.sheet.hidden) { event.preventDefault?.(); close(); } if (event.key !== 'Tab' || options.sheet.hidden) 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?.deviceSetup) { finishClose(pendingResume); pendingResume = true; } }); steps.forEach(([_name, button, _status, action]) => { button.addEventListener('click', async () => { button.disabled = true; try { await action(); } finally { await render(); } }); }); renderPrompt(await options.getReadiness()); } return {start, open, close, render}; }); if (typeof module === 'object' && module.exports) { module.exports.mount = mountMobileDeviceSetup; } else { self.createMobileDeviceSetup.mount = mountMobileDeviceSetup; } function mountMobileDeviceSetup(options) { const qs = selector => options.document.querySelector(selector); const window = options.window || options.document.defaultView; return createMobileDeviceSetup({ launcher:qs('#open-device-setup'), closeButton:qs('#close-device-setup'), sheet:qs('#device-setup-sheet'), installButton:qs('#device-setup-install'), offlineButton:qs('#device-setup-offline'), pushButton:qs('#device-setup-push'), protectionButton:qs('#device-setup-protection'), deadlineButton:qs('#device-setup-deadline'), installStatus:qs('#device-setup-install-status'), offlineStatus:qs('#device-setup-offline-status'), protectionStatus:qs('#device-setup-protection-status'), pushStatus:qs('#device-setup-push-status'), deadlineStatus:qs('#device-setup-deadline-status'), readyStatus:qs('#device-setup-ready-status'), promptCard:qs('#device-readiness-card'), promptSummary:qs('#device-readiness-summary'), promptLauncher:qs('#finish-device-setup'), promptDismiss:qs('#dismiss-device-readiness'), promptStorage:options.promptStorage, returnButton:qs('#return-from-device-setup'), timerView:options.timerView, isMobile:options.isMobile || (() => innerWidth <= 600), escapeTarget:options.document, history:window?.history, historyTarget:window, backgroundElements:[ qs('header'), qs('main'), qs('#mobile-task-dock'), ].filter(Boolean), getReadiness:() => ({ install:options.installApp.state(), offline:!options.offlineAvailable() ? {state:'unavailable', detail:'Offline saving is unavailable in this browser.'} : options.offlineEnabled() ? {state:'complete', detail:qs('#offline-work-status').textContent || 'Offline work is saved.'} : {state:'incomplete', detail:'Private My Work and Today data are not saved offline.'}, protection:options.storageProtectionReadiness(), push:options.notificationReadiness(), deadline:options.deadlineReadiness(), }), install:() => options.installApp.install(), enableOffline:options.enableOffline, protectStorage:options.protectStorage, enablePush:options.enablePush, enableDeadline:options.enableDeadline, }); }