(function (root) { 'use strict'; function escapeText(value) { return String(value || '') .replace(/\\/g, '\\\\') .replace(/\r?\n/g, '\\n') .replace(/,/g, '\\,') .replace(/;/g, '\\;'); } function calendarDay(value) { const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})/); return match ? match.slice(1).join('') : ''; } function nextDay(day) { const date = new Date(Date.UTC( Number(day.slice(0, 4)), Number(day.slice(4, 6)) - 1, Number(day.slice(6, 8)) + 1 )); return date.toISOString().slice(0, 10).replace(/-/g, ''); } function uid(item) { const repository = String(item.repository || '').replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase(); return `issue-${Number(item.number)}@${repository}`; } function foldLine(line) { const encoder = new TextEncoder(); const chunks = []; let chunk = ''; let bytes = 0; let limit = 75; for (const character of String(line)) { const width = encoder.encode(character).length; if (chunk && bytes + width > limit) { chunks.push(chunk); chunk = character; bytes = width; limit = 74; } else { chunk += character; bytes += width; } } chunks.push(chunk); return chunks.join('\r\n '); } function serializeAgendaCalendar(items, { generatedOn } = {}) { const stampDay = String(generatedOn || new Date().toISOString().slice(0, 10).replace(/-/g, '')); const lines = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Stackchain//Agenda Snapshot//EN', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'X-WR-CALNAME:Stackchain Agenda']; (items || []).forEach(item => { const day = calendarDay(item.due_date); if (!day || !Number.isInteger(Number(item.number)) || !item.repository) return; lines.push( 'BEGIN:VEVENT', `UID:${uid(item)}`, `DTSTAMP:${stampDay}T000000Z`, `DTSTART;VALUE=DATE:${day}`, `DTEND;VALUE=DATE:${nextDay(day)}`, `SUMMARY:${escapeText(item.title)}`, `DESCRIPTION:${escapeText(`${item.repository}#${item.number} · Stackchain Agenda snapshot`)}`, `URL:${String(item.url || '')}`, 'TRANSP:TRANSPARENT', 'END:VEVENT', ); }); lines.push('END:VCALENDAR'); return lines.map(foldLine).join('\r\n') + '\r\n'; } async function deliverCalendarSnapshot({ text, filename, navigator, document, urlApi, FileCtor, }) { const file = new FileCtor([text], filename, { type: 'text/calendar;charset=utf-8' }); const sharePayload = { files: [file], title: 'Stackchain Agenda', text: 'Agenda calendar snapshot' }; if (typeof navigator?.share === 'function' && typeof navigator?.canShare === 'function' && navigator.canShare(sharePayload)) { await navigator.share(sharePayload); return 'shared'; } const href = urlApi.createObjectURL(file); try { const anchor = document.createElement('a'); anchor.href = href; anchor.download = filename; anchor.click(); } finally { urlApi.revokeObjectURL(href); } return 'downloaded'; } function mountAgendaCalendarExport({ qs, getItems, escapeHtml, onDone, windowObject = root, navigatorObject = root.navigator, documentObject = root.document, urlApi = root.URL, FileCtor = root.File, }) { const sheet = qs('#agenda-export-sheet'); let items = []; let scrollY = 0; let trigger = null; const selectedItems = () => Array.from(qs('#agenda-export-items').querySelectorAll('input[type="checkbox"]')) .filter(checkbox => checkbox.checked) .map(checkbox => items[Number(checkbox.value)]) .filter(Boolean); const updateSelection = () => { const selected = selectedItems(); qs('#share-agenda-export').disabled = selected.length === 0; qs('#agenda-export-status').textContent = selected.length + ' of ' + items.length + (items.length === 1 ? ' deadline selected.' : ' deadlines selected.'); }; const close = () => { sheet.close(); windowObject.scrollTo({ top:scrollY, behavior:'instant' }); trigger?.focus(); }; qs('#open-agenda-export').addEventListener('click', event => { items = getItems(); scrollY = windowObject.scrollY; trigger = event.currentTarget; qs('#agenda-export-items').innerHTML = items.map((item, index) => '' ).join(''); qs('#agenda-export-items').querySelectorAll('input[type="checkbox"]').forEach(checkbox => checkbox.addEventListener('change', updateSelection) ); updateSelection(); sheet.showModal(); qs('#cancel-agenda-export').focus(); }); qs('#cancel-agenda-export').addEventListener('click', close); sheet.addEventListener('cancel', event => { event.preventDefault(); close(); }); qs('#share-agenda-export').addEventListener('click', async () => { const selected = selectedItems(); if (!selected.length) return; const button = qs('#share-agenda-export'); button.disabled = true; qs('#agenda-export-status').textContent = 'Preparing calendar snapshot…'; const day = new Date().toISOString().slice(0, 10); try { const text = serializeAgendaCalendar(selected, { generatedOn:day.replace(/-/g, '') }); const result = await deliverCalendarSnapshot({ text, filename:'stackchain-agenda-' + day + '.ics', navigator:navigatorObject, document:documentObject, urlApi, FileCtor, }); onDone(result); close(); } catch (error) { qs('#agenda-export-status').textContent = error?.name === 'AbortError' ? 'Share cancelled. Nothing was exported.' : 'Calendar export failed. Retry without leaving Agenda.'; button.disabled = false; } }); } const api = { calendarDay, deliverCalendarSnapshot, escapeText, foldLine, mountAgendaCalendarExport, serializeAgendaCalendar }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.StackchainAgendaCalendar = api; })(typeof window !== 'undefined' ? window : globalThis);