(function (root, factory) { if (typeof module === 'object' && module.exports) module.exports = factory; else root.createTodayHandoff = factory; })(typeof self !== 'undefined' ? self : this, function createTodayHandoff(options) { const prefix = 'stackchain.today-handoff.v1.'; const storage = options.storage || null; const selected = new Set(); const excluded = new Set(); function key() { const login = String(options.getLogin?.() || '').trim().toLowerCase(); return login ? prefix + encodeURIComponent(login) : ''; } function pending() { const storageKey = key(); if (!storageKey || !storage) return []; try { const value = JSON.parse(storage.getItem(storageKey) || '[]'); return Array.isArray(value) ? value.filter(id => typeof id === 'string' && id) : []; } catch (_) { return []; } } function write(ids) { const storageKey = key(); if (!storageKey || !storage) return false; const unique = [...new Set(ids.filter(id => typeof id === 'string' && id))]; try { if (unique.length) storage.setItem(storageKey, JSON.stringify(unique)); else storage.removeItem(storageKey); return true; } catch (_) { return false; } } function capture(ids) { const merged = [...pending()]; (ids || []).forEach(id => { if (typeof id === 'string' && id && !merged.includes(id)) merged.push(id); if (typeof id === 'string' && id) { selected.add(id); excluded.delete(id); } }); return write(merged); } function review(items) { const byIdentity = new Map((items || options.items?.() || []).map(item => [options.identity(item), item])); return pending().flatMap(identity => { const item = byIdentity.get(identity); if (!excluded.has(identity)) selected.add(identity); return item ? [{ identity, item, selected:selected.has(identity) }] : []; }); } function choose(identity, include) { if (!pending().includes(identity)) return false; if (include) { selected.add(identity); excluded.delete(identity); } else { selected.delete(identity); excluded.add(identity); } return true; } function render(items, list, escapeHtml) { const rows = review(items); list.innerHTML = rows.map(row => { const title = String(row.item.title || row.identity).slice(0, 180); const context = String(row.item.key || row.item.repository || 'Work item').slice(0, 180); return '
' + escapeHtml(title) + '' + '' + escapeHtml(context) + '
'; }).join(''); list.querySelectorAll('[data-today-handoff]').forEach(input => input.addEventListener('change', () => choose(input.dataset.todayHandoff, input.checked))); return rows; } async function commit(items) { const rows = review(items); const remaining = pending(); let scheduled = 0; let blocked = 0; for (const row of rows) { if (!selected.has(row.identity)) continue; if (options.todayWork.contains(row.item)) { const index = remaining.indexOf(row.identity); if (index >= 0) remaining.splice(index, 1); selected.delete(row.identity); continue; } const added = options.todayWork.add(row.item); if (added !== 'added') { blocked += 1; continue; } if (!options.todaySync.enqueue('add', row.identity)) { options.todayWork.remove?.(row.item); blocked += 1; continue; } const index = remaining.indexOf(row.identity); if (index >= 0) remaining.splice(index, 1); selected.delete(row.identity); scheduled += 1; } write(remaining); if (scheduled) await options.todaySync.flush(); return { scheduled, blocked, remaining:remaining.length }; } function mount({ qs, escapeHtml, onComplete = () => {} }) { const list = qs('#today-handoff-items'); let prompted = false; function open() { if (prompted) return false; const rows = render(null, list, escapeHtml); if (!rows.length) return false; qs('#today-handoff-status').textContent = rows.length + (rows.length === 1 ? ' commitment is ready to review.' : ' commitments are ready to review.'); const dialog = qs('#today-handoff-dialog'); if (!dialog.open) dialog.showModal(); prompted = true; requestAnimationFrame(() => (list.querySelector('input') || qs('#confirm-today-handoff')).focus()); return true; } qs('#close-today-handoff').addEventListener('click', () => qs('#today-handoff-dialog').close()); qs('#confirm-today-handoff').addEventListener('click', async event => { const button = event.currentTarget; button.disabled = true; qs('#today-handoff-status').textContent = 'Saving Today’s plan…'; try { const result = await commit(); if (result.blocked) qs('#today-handoff-status').textContent = 'Today is full. ' + result.remaining + ' commitment remains safe in My Work.'; else qs('#today-handoff-dialog').close(); onComplete(result); } finally { button.disabled = false; } }); return { open, reset:() => { prompted = false; } }; } return { capture, choose, commit, mount, pending, render, review }; });