function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange = () => {} }) { const key = () => { const login = String(getLogin?.() || '').trim().toLowerCase(); return login ? 'stackchain.today-timer.v1.' + encodeURIComponent(login) : ''; }; const empty = () => ({ version:1, active_identity:'', entries:{}, away_at:null, pending_interruption:null, attention_interruption:null, capture_interruption:null, search_interruption:null, detour_interruption:null, timed_break:null, }); const read = () => { const ownerKey = key(); if (!ownerKey || !storage) return empty(); try { const value = JSON.parse(storage.getItem(ownerKey) || 'null'); if (value?.version !== 1 || typeof value.active_identity !== 'string' || !value.entries || typeof value.entries !== 'object') return empty(); return value; } catch (_error) { return empty(); } }; const write = state => { const ownerKey = key(); if (!ownerKey || !storage) return false; try { storage.setItem(ownerKey, JSON.stringify(state)); onChange(snapshot()); return true; } catch (_error) { return false; } }; const validPending = state => { const pending = state.pending_interruption; return pending && typeof pending.identity === 'string' && pending.identity && Number.isFinite(pending.away_ms) && pending.away_ms >= 0 ? { identity:pending.identity, away_ms:pending.away_ms } : null; }; const validAttention = state => { const pending = state.attention_interruption; return pending && typeof pending.identity === 'string' && pending.identity && typeof pending.resume === 'boolean' ? { identity:pending.identity, resume:pending.resume } : null; }; const validCapture = state => { const pending = state.capture_interruption; return pending && typeof pending.identity === 'string' && pending.identity && typeof pending.resume === 'boolean' ? { identity:pending.identity, resume:pending.resume } : null; }; const validSearch = state => { const pending = state.search_interruption; return pending && typeof pending.identity === 'string' && pending.identity && typeof pending.resume === 'boolean' ? { identity:pending.identity, resume:pending.resume } : null; }; const validDetour = state => { const pending = state.detour_interruption; return pending && typeof pending.identity === 'string' && pending.identity && typeof pending.resume === 'boolean' && ['find', 'queues', 'insights', 'device-setup', 'security-center', 'live-data-status'].includes(pending.reason) ? { identity:pending.identity, resume:pending.resume, reason:pending.reason } : null; }; const validBreak = state => { const value = state.timed_break; return value && typeof value.identity === 'string' && value.identity && Number.isFinite(value.deadline_at) && value.deadline_at >= 0 ? { identity:value.identity, deadline_at:value.deadline_at, expired:now() >= value.deadline_at } : null; }; const settle = (state, at = now()) => { const entry = state.entries[state.active_identity]; if (!entry?.running) return state; entry.elapsed_ms = Math.max(0, Number(entry.elapsed_ms) || 0) + Math.max(0, at - Number(entry.started_at ?? at)); entry.started_at = null; entry.running = false; return state; }; const snapshot = (identity = '', includeEntries = false) => { const state = read(); const sampledAt = now(); const selected = identity || state.active_identity; const entry = state.entries[selected]; const result = !selected || !entry ? { identity:selected, elapsed_ms:0, running:false } : { identity:selected, elapsed_ms:Math.max(0, Number(entry.elapsed_ms) || 0) + (entry.running ? Math.max(0, sampledAt - Number(entry.started_at ?? sampledAt)) : 0), running:Boolean(entry.running), }; const timedBreak = validBreak(state); if (timedBreak?.identity === selected) result.break_deadline_at = timedBreak.deadline_at; if (includeEntries) { result.entries = Object.entries(state.entries).slice(0, 20).map(([entryIdentity, value]) => ({ identity:entryIdentity, elapsed_ms:Math.max(0, Math.floor(Number(value.elapsed_ms) || 0) + (value.running ? Math.max(0, sampledAt - Number(value.started_at ?? sampledAt)) : 0)), })); } return result; }; return { adopt(identity, elapsedMs, running, entries = null) { if (!key() || typeof identity !== 'string' || !identity || !Number.isFinite(Number(elapsedMs)) || Number(elapsedMs) < 0) return false; const incoming = entries === null ? [{identity, elapsed_ms:elapsedMs}] : entries; if (!Array.isArray(incoming) || incoming.length > 20) return false; const seen = new Set(); const ledger = {}; for (const entry of incoming) { if (typeof entry?.identity !== 'string' || !entry.identity || entry.identity.length > 500 || seen.has(entry.identity) || !Number.isFinite(Number(entry.elapsed_ms)) || Number(entry.elapsed_ms) < 0) return false; seen.add(entry.identity); ledger[entry.identity] = { elapsed_ms:Math.floor(Number(entry.elapsed_ms)), started_at:null, running:false, }; } if (!ledger[identity] || ledger[identity].elapsed_ms !== Math.floor(Number(elapsedMs))) return false; const state = read(); state.active_identity = identity; state.entries = ledger; state.entries[identity].started_at = running ? now() : null; state.entries[identity].running = Boolean(running); state.away_at = null; state.pending_interruption = null; state.attention_interruption = null; state.search_interruption = null; state.detour_interruption = null; state.timed_break = null; return write(state); }, activate(identity) { if (!key() || typeof identity !== 'string' || !identity) return false; const state = read(); if (state.active_identity === identity && (state.entries[identity]?.running || validBreak(state)?.identity === identity)) return true; settle(state); state.active_identity = identity; const entry = state.entries[identity] || { elapsed_ms:0, started_at:null, running:false }; entry.started_at = now(); entry.running = true; state.entries[identity] = entry; state.away_at = null; state.pending_interruption = null; state.attention_interruption = null; state.search_interruption = null; state.detour_interruption = null; state.timed_break = null; return write(state); }, pause() { const state = read(); settle(state); state.away_at = null; return write(state); }, resume() { const state = read(); const entry = state.entries[state.active_identity]; if (!entry) return false; if (!entry.running) { entry.started_at = now(); entry.running = true; } state.timed_break = null; return write(state); }, startBreak(minutes) { const duration = Number(minutes); if (!Number.isInteger(duration) || duration < 1 || duration > 120) return false; const state = read(); const identity = state.active_identity; if (!identity || !state.entries[identity]) return false; settle(state); state.away_at = null; state.pending_interruption = null; state.timed_break = { identity, deadline_at:now() + duration * 60000 }; return write(state) ? validBreak(state) : false; }, restoreBreak(identity, deadlineAt) { const deadline = Number(deadlineAt); if (typeof identity !== 'string' || !identity || !Number.isFinite(deadline) || deadline <= now()) return false; const state = read(); const entry = state.entries[identity]; if (state.active_identity !== identity || !entry || entry.running || validBreak(state)) return false; state.timed_break = { identity, deadline_at:Math.floor(deadline) }; return write(state); }, breakSnapshot() { return validBreak(read()); }, resumeBreak() { const state = read(); const pending = validBreak(state); const entry = pending && state.entries[pending.identity]; if (!pending || !entry || state.active_identity !== pending.identity || entry.running) return false; state.timed_break = null; entry.started_at = now(); entry.running = true; return write(state); }, stop() { const state = read(); settle(state); state.away_at = null; state.pending_interruption = null; state.attention_interruption = null; state.search_interruption = null; state.detour_interruption = null; state.timed_break = null; return write(state); }, beginAttention() { const state = read(); const existing = validAttention(state); if (existing) return existing; const identity = state.active_identity; const entry = state.entries[identity]; if (!identity || !entry) return null; const resume = Boolean(entry.running); if (resume) settle(state); state.away_at = null; state.attention_interruption = { identity, resume }; return write(state) ? { ...state.attention_interruption } : null; }, attentionInterruption() { return validAttention(read()); }, returnFromAttention() { const state = read(); const pending = validAttention(state); const entry = pending && state.entries[pending.identity]; if (!pending || !entry) return null; state.active_identity = pending.identity; if (pending.resume && !entry.running) { entry.started_at = now(); entry.running = true; } state.attention_interruption = null; state.away_at = null; return write(state) ? { identity:pending.identity, resumed:pending.resume } : null; }, beginCapture() { const state = read(); const existing = validCapture(state); if (existing) return existing; const identity = state.active_identity; const entry = state.entries[identity]; if (!identity || !entry) return null; const resume = Boolean(entry.running); if (resume) settle(state); state.away_at = null; state.capture_interruption = { identity, resume }; return write(state) ? { ...state.capture_interruption } : null; }, captureInterruption() { return validCapture(read()); }, abandonCapture() { const state = read(); const pending = validCapture(state); if (!pending) return null; state.capture_interruption = null; state.away_at = null; return write(state) ? { identity:pending.identity, resumed:false } : null; }, returnFromCapture() { const state = read(); const pending = validCapture(state); const entry = pending && state.entries[pending.identity]; if (!pending || !entry) return null; const resumed = pending.resume && state.active_identity === pending.identity; if (resumed && !entry.running) { entry.started_at = now(); entry.running = true; } state.capture_interruption = null; state.away_at = null; return write(state) ? { identity:pending.identity, resumed } : null; }, beginSearch() { const state = read(); const existing = validSearch(state); if (existing) return existing; const identity = state.active_identity; const entry = state.entries[identity]; if (!identity || !entry?.running) return null; const resume = true; settle(state); state.away_at = null; state.search_interruption = { identity, resume }; return write(state) ? { ...state.search_interruption } : null; }, searchInterruption() { return validSearch(read()); }, abandonSearch() { const state = read(); const pending = validSearch(state); if (!pending) return null; state.search_interruption = null; state.away_at = null; return write(state) ? { identity:pending.identity, resumed:false } : null; }, returnFromSearch() { const state = read(); const pending = validSearch(state); const entry = pending && state.entries[pending.identity]; if (!pending || !entry) return null; const resumed = pending.resume && state.active_identity === pending.identity; if (resumed && !entry.running) { entry.started_at = now(); entry.running = true; } state.search_interruption = null; state.away_at = null; return write(state) ? { identity:pending.identity, resumed } : null; }, beginDetour(reason) { if (!['find', 'queues', 'insights', 'device-setup', 'security-center', 'live-data-status'].includes(reason)) return null; const state = read(); const existing = validDetour(state); if (existing) return existing; const identity = state.active_identity; const entry = state.entries[identity]; if (!identity || !entry?.running) return null; settle(state); state.away_at = null; state.detour_interruption = { identity, resume:true, reason }; return write(state) ? { ...state.detour_interruption } : null; }, detourInterruption() { return validDetour(read()); }, returnFromDetour(reason = '') { const state = read(); const pending = validDetour(state); if (reason && pending?.reason !== reason) return null; const entry = pending && state.entries[pending.identity]; if (!pending || !entry || state.active_identity !== pending.identity) return null; const resumed = pending.resume && !entry.running; if (resumed) { entry.started_at = now(); entry.running = true; } state.detour_interruption = null; state.away_at = null; return write(state) ? { identity:pending.identity, resumed, reason:pending.reason } : null; }, markAway() { const state = read(); const entry = state.entries[state.active_identity]; if (!entry?.running || validPending(state)) return false; if (Number.isFinite(state.away_at) && state.away_at >= 0) return true; state.away_at = now(); return write(state); }, reconcileInterruption(thresholdMs = 5 * 60 * 1000) { const state = read(); const existing = validPending(state); if (existing) return existing; state.pending_interruption = null; const entry = state.entries[state.active_identity]; const awayAt = Number(state.away_at); const detectedAt = now(); state.away_at = null; if (!entry?.running || !Number.isFinite(awayAt) || awayAt < 0 || detectedAt < awayAt || detectedAt - awayAt < thresholdMs) { write(state); return null; } const awayMs = detectedAt - awayAt; settle(state, detectedAt); state.pending_interruption = { identity:state.active_identity, away_ms:awayMs }; write(state); return { ...state.pending_interruption }; }, pendingInterruption() { return validPending(read()); }, resolveInterruption(decision) { if (decision !== 'count' && decision !== 'exclude') return false; const state = read(); const pending = state.pending_interruption; const entry = pending && state.entries[pending.identity]; if (!entry || !Number.isFinite(pending.away_ms) || pending.away_ms < 0) return false; if (decision === 'exclude') { entry.elapsed_ms = Math.max(0, Number(entry.elapsed_ms) - pending.away_ms); } state.active_identity = pending.identity; entry.started_at = now(); entry.running = true; state.pending_interruption = null; state.away_at = null; return write(state); }, recapEntries() { const state = read(); settle(state); write(state); return Object.entries(state.entries).map(([identity, entry]) => ({ identity, elapsed_ms:Math.max(0, Number(entry.elapsed_ms) || 0), })).filter(entry => entry.elapsed_ms > 0); }, clearRecap() { return write(empty()); }, totalElapsed() { const state = read(); return Object.entries(state.entries).reduce((total, [identity, entry]) => { const live = identity === state.active_identity && entry.running ? Math.max(0, now() - Number(entry.started_at ?? now())) : 0; return total + Math.max(0, Number(entry.elapsed_ms) || 0) + live; }, 0); }, snapshot, sessionSnapshot:(identity = '') => snapshot(identity, true), }; } function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway, getItem, onReopen, onResume, onComplete, onCapture }) { let progress = null; let runway = null; let breakView = null; if (typeof createTodayBreak === 'function') { breakView = createTodayBreak({timer, qs:selector => queryAll(selector)[0], queryAll, onChange:() => render(), onResume:onResume || onReopen}); } const captureView = createTodayCaptureInterruption({ timer, banner:queryAll('#today-capture-interruption')[0], label:queryAll('#today-capture-interruption-label')[0], getItem, onReturn:() => render(), }); const searchView = createTodaySearchInterruption({ timer, queryAll, getItemLabel:identity => getItem?.(identity)?.title, onChange:() => render(), onReturn:identity => onReopen?.(identity), }); const detourView = createTodayDetourInterruption({ timer, queryAll, getItemLabel:identity => getItem?.(identity)?.title, onChange:() => render(), onReturn:identity => onReopen?.(identity), }); queryAll('[data-mobile-today-open]').forEach(button => button.addEventListener('click', () => { const identity = timer.snapshot().identity; if (identity) onReopen?.(identity); }) ); queryAll('[data-mobile-today-toggle]').forEach(button => button.addEventListener('click', () => view.toggle()) ); queryAll('[data-mobile-today-complete]').forEach(button => button.addEventListener('click', () => { const identity = timer.snapshot().identity; if (identity) onComplete?.(identity); }) ); if (typeof document !== 'undefined') queryAll('.work-session-nav').forEach(nav => { const button = document.createElement('button'); button.type = 'button'; button.hidden = true; button.dataset.workSessionAdjustPlan = ''; button.textContent = 'Adjust remaining plan'; nav.insertBefore(button, nav.querySelector('[data-work-session-complete]')); }); if (typeof MutationObserver !== 'undefined') { const plan = queryAll('#plan-today')[0]; const sheet = queryAll('#plan-today-sheet')[0]; if (plan && sheet) { const replan = createTodayBudgetReplan({ timer, openPlan:() => plan.click() }); queryAll('[data-work-session-adjust-plan]').forEach(button => button.addEventListener('click', () => replan.open()) ); new MutationObserver(() => { if (sheet.hidden && replan.restore()) render(); }).observe(sheet, { attributes:true, attributeFilter:['hidden'] }); } } const elapsed = milliseconds => { const seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000)); const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainder = String(seconds % 60).padStart(2, '0'); return hours ? hours + ':' + String(minutes).padStart(2, '0') + ':' + remainder : minutes + ':' + remainder; }; const render = () => { const snapshot = timer.snapshot(); breakView?.render(); const active = Boolean(progress && isActive() && snapshot.identity); queryAll('[data-mobile-today-hud]').forEach(element => { element.hidden = !active; }); queryAll('[data-mobile-today-open]').forEach(element => { element.textContent = active ? String(getItem?.(snapshot.identity)?.title || 'Current Today item') : ''; }); queryAll('[data-mobile-today-complete]').forEach(button => { const itemTitle = String(getItem?.(snapshot.identity)?.title || 'current Today item'); const finalItem = Boolean(progress && progress.index >= progress.total); button.hidden = !active; button.textContent = finalItem ? 'Done & recap' : 'Done & next'; button.setAttribute('aria-label', finalItem ? 'Complete ' + itemTitle + ' and open Today recap' : 'Complete ' + itemTitle + ' and open next Today item'); }); if (typeof document !== 'undefined') document.body.classList.toggle('mobile-today-active', active); if (!progress) return; const sourceRunway = getRunway?.(snapshot) || runway; const liveRunway = sourceRunway?.future_minutes !== undefined ? (() => { const currentElapsed = Math.max(0, Math.ceil(snapshot.elapsed_ms / 60000)); const currentRemaining = sourceRunway.current_minutes === null ? null : Math.max(0, sourceRunway.current_minutes - currentElapsed); const remaining = currentRemaining === null || sourceRunway.future_minutes === null ? null : currentRemaining + sourceRunway.future_minutes; const projected = remaining === null ? null : Math.ceil(timer.totalElapsed() / 60000) + remaining; const capacityRemaining = sourceRunway.capacity_minutes === null || projected === null ? null : sourceRunway.capacity_minutes - projected; return { ...sourceRunway, remaining_minutes:remaining, over_estimate_minutes:sourceRunway.current_minutes === null ? 0 : Math.max(0, currentElapsed - sourceRunway.current_minutes), over_capacity_minutes:capacityRemaining === null ? 0 : Math.max(0, -capacityRemaining), }; })() : sourceRunway; const timing = isActive() && snapshot.identity ? ' · ' + elapsed(snapshot.elapsed_ms) + (liveRunway?.current_minutes ? ' / ' + formatEstimate(liveRunway.current_minutes) : '') : ''; const estimateRisk = liveRunway?.over_estimate_minutes ? ' · ' + formatEstimate(liveRunway.over_estimate_minutes) + ' over estimate' : ''; const capacityRisk = liveRunway?.over_capacity_minutes ? ' · Today projected ' + formatEstimate(liveRunway.over_capacity_minutes) + ' over capacity' : ''; queryAll('[data-work-session-progress]').forEach(element => { element.textContent = 'Item ' + progress.index + ' of ' + progress.total + timing + estimateRisk + capacityRisk + (!estimateRisk && !capacityRisk && liveRunway?.remaining_minutes ? ' · ' + formatEstimate(liveRunway.remaining_minutes) + ' remaining' : ''); }); queryAll('[data-work-session-adjust-plan]').forEach(button => { button.hidden = !(liveRunway?.over_estimate_minutes || liveRunway?.over_capacity_minutes); }); queryAll('[data-work-session-timer-toggle]').forEach(button => { button.hidden = !isActive(); button.textContent = snapshot.running ? 'Pause timer' : 'Resume timer'; button.setAttribute('aria-pressed', String(!snapshot.running)); }); }; const view = { open(identity, active) { const current = timer.snapshot(); if (active && current.identity !== identity) timer.activate(identity); else if (!active) timer.stop(); render(); }, reopen(identity) { onReopen?.(identity); }, search(action, ...args) { return searchView[action]?.(...args); }, beginDetour(reason) { return detourView.open(reason); }, finishDetour(reason) { return detourView.finish(reason); }, finish() { timer.stop(); progress = null; runway = null; render(); }, update(nextProgress, nextRunway) { progress = nextProgress; runway = nextRunway; render(); }, reset() { progress = null; runway = null; render(); }, toggle() { const state = timer.snapshot(); state.running ? timer.pause() : timer.resume(); render(); }, beginCapture() { const result = captureView.open(); this.restoreCapture(); render(); return result; }, restoreCapture() { const result = captureView.restore(); const button = queryAll('#save-unfiled-issue')[0]; if (button) button.textContent = result ? 'Save & return to Today' : 'Save to Drafts'; return result; }, finishCapture() { return captureView.finish(); }, transferCapture() { return captureView.transfer(); }, restore(recovery) { render(); return Promise.resolve(recovery).catch(() => false).then(() => { render(); return !!timer.breakSnapshot?.(); }); }, render, }; queryAll('#new-issue').forEach(button => button.addEventListener('click', () => { view.beginCapture(); onCapture?.(); })); if (timer.captureInterruption) view.restoreCapture(); searchView.restore(); detourView.restore(); return view; } function createTodayBudgetReplan({ timer, openPlan }) { let active = false; let resume = false; return { open() { if (active) return false; const state = timer.snapshot(); if (!state.identity) return false; resume = Boolean(state.running); if (resume && timer.pause() === false) return false; active = true; openPlan?.(state); return true; }, restore() { if (!active) return false; active = false; const shouldResume = resume; resume = false; return !shouldResume || timer.resume() !== false; }, }; } function createTodayInterruptionPrompt({ timer, sheet, description, getItemLabel, onResolved }) { const render = pending => { if (!pending) { sheet.hidden = true; return false; } const label = String(getItemLabel?.(pending.identity) || 'Current Today item'); const minutes = Math.max(1, Math.round(pending.away_ms / 60000)); description.textContent = label + ' · away for ' + minutes + ' minute' + (minutes === 1 ? '' : 's'); sheet.hidden = false; return true; }; return { background() { return timer.markAway(); }, foreground() { return render(timer.reconcileInterruption()); }, restore() { return render(timer.pendingInterruption()); }, resolve(decision) { if (!timer.resolveInterruption(decision)) return false; sheet.hidden = true; onResolved?.(); return true; }, }; } function createTodaySearchInterruption({ timer, queryAll, getItemLabel, onChange, onReturn }) { const render = (pending = timer.searchInterruption?.()) => { queryAll('[data-search-today-interruption]').forEach(element => { element.hidden = !pending; }); queryAll('[data-search-today-label]').forEach(element => { element.textContent = pending ? 'Today paused · ' + String(getItemLabel?.(pending.identity) || 'Current Today item') : ''; }); onChange?.(); return pending; }; const view = { open() { const wasRunning = timer.snapshot().running; const pending = timer.beginSearch?.(); render(pending || timer.searchInterruption?.()); if (wasRunning && !pending) { const status = queryAll('#cmd-search-action-status')[0]; if (status) status.textContent = 'Search is open, but Today timing could not be paused on this device.'; } return pending; }, restore() { return render(); }, finish() { const result = timer.returnFromSearch?.(); render(null); return result; }, transfer() { const result = timer.abandonSearch?.(); render(null); return result; }, }; queryAll('#open-palette').forEach(button => button.addEventListener('click', () => view.open())); queryAll('[data-return-from-search]').forEach(button => button.addEventListener('click', () => { const pending = timer.searchInterruption?.(); view.finish(); if (pending) onReturn?.(pending.identity); })); if (typeof MutationObserver !== 'undefined') { const overlays = [...queryAll('#cmd-palette'), ...queryAll('#search-preview')]; const observer = new MutationObserver(() => { if (overlays.every(element => !element.classList.contains('open'))) view.finish(); }); overlays.forEach(element => observer.observe(element, {attributes:true, attributeFilter:['class']})); } return view; } function createTodayCaptureInterruption({ timer, banner, label, getItem, getItemLabel, onReturn }) { banner ||= typeof document === 'undefined' ? null : document.querySelector('#today-capture-interruption'); label ||= typeof document === 'undefined' ? null : document.querySelector('#today-capture-interruption-label'); const render = pending => { if (!banner || !label) return; banner.hidden = !pending; label.textContent = pending ? 'Today paused · ' + String(getItem?.(pending.identity)?.title || getItemLabel?.(pending.identity) || 'Current Today item') : ''; }; return { open() { const pending = timer.beginCapture(); render(pending); return pending; }, restore() { const pending = timer.captureInterruption(); render(pending); return pending; }, finish() { const result = timer.returnFromCapture(); render(null); if (result) onReturn?.(result); return result; }, transfer() { const result = timer.abandonCapture(); render(null); return result; }, }; } function createTodayDetourInterruption({ timer, queryAll, getItemLabel, onChange, onReturn }) { const render = (pending = timer.detourInterruption?.()) => { queryAll('[data-today-detour]').forEach(element => { element.hidden = !pending; }); queryAll('[data-today-detour-label]').forEach(element => { element.textContent = pending ? 'Today paused · ' + String(getItemLabel?.(pending.identity) || 'Current Today item') : ''; }); onChange?.(); return pending; }; const view = { open(reason) { const pending = timer.beginDetour?.(reason); render(pending || timer.detourInterruption?.()); return pending; }, restore() { return render(); }, finish(reason) { const result = timer.returnFromDetour?.(reason); if (result || !reason) render(null); return result; }, }; queryAll('[data-return-from-detour]').forEach(button => button.addEventListener('click', () => { const pending = timer.detourInterruption?.(); view.finish(); if (pending) onReturn?.(pending.identity); })); if (typeof MutationObserver !== 'undefined') { queryAll('#find-work-sheet').forEach(sheet => new MutationObserver(() => { const pending = timer.detourInterruption?.(); if (sheet.classList.contains('open')) view.open('find'); else if (pending?.reason === 'find') view.finish(); }).observe(sheet, {attributes:true, attributeFilter:['class']})); } return view; } if (typeof module !== 'undefined' && module.exports) { createTodayTimer.createView = createTodayTimerView; createTodayTimer.createInterruptionPrompt = createTodayInterruptionPrompt; createTodayTimer.createBudgetReplan = createTodayBudgetReplan; createTodayTimer.createSearchInterruption = createTodaySearchInterruption; createTodayTimer.createCaptureInterruption = createTodayCaptureInterruption; createTodayTimer.createDetourInterruption = createTodayDetourInterruption; module.exports = createTodayTimer; }