(function(root){ 'use strict'; function escapeText(value){ return String(value||'').replace(/\\/g,'\\\\').replace(/\r?\n/g,'\\n').replace(/,/g,'\\,').replace(/;/g,'\\;'); } function compact(value){return String(value||'').replace(/[-:]/g,'');} function addMinutes(value,minutes){ const [hours,mins]=String(value).split(':').map(Number),total=hours*60+mins+Number(minutes); return String(Math.floor(total/60)%24).padStart(2,'0')+':'+String(total%60).padStart(2,'0'); } function stableId(value){return String(value||'work').replace(/[^a-z0-9]+/gi,'-').replace(/^-|-$/g,'').toLowerCase();} function foldLine(line){ const encoder=new TextEncoder(),chunks=[];let chunk='',bytes=0,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 buildWeekBlocks(plan,startTimes,getItem,selected){ const blocks=[]; (plan?.days||[]).slice().sort((a,b)=>a.plan_date.localeCompare(b.plan_date)).forEach(day=>{ let cursor=startTimes?.[day.plan_date]||'09:00'; (day.ids||[]).forEach(id=>{ if(selected&& !selected.has(id))return; const minutes=Number(day.estimates?.[id]),item=getItem?.(id); if(!item||!Number.isFinite(minutes)||minutes<=0)return; const end=addMinutes(cursor,minutes); blocks.push({...item,id,plan_date:day.plan_date,start_time:cursor,end_time:end,minutes}); cursor=end; }); }); return blocks; } function serializeWeekCalendar(blocks,{timezone='UTC',revision=0,generatedAt}={}){ const stamp=generatedAt||new Date().toISOString().replace(/[-:]/g,'').replace(/\.\d{3}/,''); const lines=['BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//Stackchain//Week Ahead//EN','CALSCALE:GREGORIAN','METHOD:PUBLISH','X-WR-CALNAME:Stackchain Week Ahead',`X-WR-TIMEZONE:${timezone}`,'BEGIN:VTIMEZONE',`TZID:${timezone}`,'END:VTIMEZONE']; (blocks||[]).forEach(block=>{ const day=compact(block.plan_date),reference=block.repository+(block.number!=null?' #'+block.number:''); lines.push('BEGIN:VEVENT',`UID:week-${revision}-${day}-${stableId(block.id)}@stackchain`,`DTSTAMP:${stamp}`, `DTSTART;TZID=${timezone}:${day}T${compact(block.start_time)}00`,`DTEND;TZID=${timezone}:${day}T${compact(block.end_time)}00`, `SUMMARY:${escapeText(block.title||'Untitled work')}`,`DESCRIPTION:${escapeText(reference+' · Stackchain Week Ahead')}`, `URL:${String(block.url||'')}`,'TRANSP:OPAQUE','END:VEVENT'); }); lines.push('END:VCALENDAR');return lines.map(foldLine).join('\r\n')+'\r\n'; } async function deliverWeekCalendar({text,filename,navigator,document,urlApi,FileCtor}){ const file=new FileCtor([text],filename,{type:'text/calendar;charset=utf-8'}); const payload={files:[file],title:'Stackchain Week Ahead',text:'Timed Week Ahead calendar blocks'}; if(typeof navigator?.share==='function'&&typeof navigator?.canShare==='function'&&navigator.canShare(payload)){ await navigator.share(payload);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 mountWeekCalendarHandoff({qs,getItem,escapeHtml,escapeAttribute,onDone,windowObject=root,navigatorObject=root.navigator, documentObject=root.document,urlApi=root.URL,FileCtor=root.File}={}){ const handoff=qs('#week-calendar-handoff'),review=qs('#week-review'),daysRoot=qs('#week-calendar-days'); let plan=null; const selected=()=>new Set(Array.from(daysRoot.querySelectorAll('[data-week-calendar-item]')).filter(input=>input.checked).map(input=>input.value)); const starts=()=>Object.fromEntries(Array.from(daysRoot.querySelectorAll('[data-week-calendar-start]')).map(input=>[input.dataset.weekCalendarStart,input.value])); const blocks=()=>buildWeekBlocks(plan,starts(),getItem,selected()); function update(){ const current=blocks(),byId=new Map(current.map(block=>[block.id,block])); daysRoot.querySelectorAll('[data-week-calendar-preview]').forEach(node=>{ const block=byId.get(node.dataset.weekCalendarPreview); node.textContent=block?block.start_time+'–'+block.end_time+' · '+block.minutes+' min':'Excluded from calendar'; }); qs('#share-week-calendar').disabled=!current.length; qs('#week-calendar-status').textContent=current.length+' calendar block'+(current.length===1?'':'s')+' selected.'; } function close({back=false}={}){ handoff.hidden=true; if(back){review.hidden=false;qs('#confirm-week-plan').focus?.();} } function open(value){ plan=value;review.hidden=true;handoff.hidden=false; const labels=new Map((value.days||[]).map(day=>[day.plan_date,new Intl.DateTimeFormat('en',{weekday:'short',month:'short',day:'numeric',timeZone:'UTC'}).format(new Date(day.plan_date+'T12:00:00Z'))])); daysRoot.innerHTML=(value.days||[]).filter(day=>day.ids?.length).map(day=>'

'+escapeHtml(labels.get(day.plan_date)||day.plan_date)+ '

'+ day.ids.map(id=>{const item=getItem(id),reference=item?(item.repository+' #'+item.number):String(id).slice(0,96); return '';}).join('')+'
').join(''); daysRoot.querySelectorAll('input').forEach(input=>input.addEventListener('change',update)); update();qs('#back-to-week-review-from-calendar').focus?.();return true; } qs('#back-to-week-review-from-calendar').addEventListener('click',()=>close({back:true})); qs('#share-week-calendar').addEventListener('click',async()=>{ const button=qs('#share-week-calendar'),current=blocks();if(!current.length)return; button.disabled=true;qs('#week-calendar-status').textContent='Preparing calendar blocks…'; const day=new Date().toISOString().slice(0,10); try{ const text=serializeWeekCalendar(current,{timezone:plan.timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,revision:plan.revision}); const result=await deliverWeekCalendar({text,filename:'stackchain-week-ahead-'+day+'.ics',navigator:navigatorObject, document:documentObject,urlApi,FileCtor}); close();onDone?.(result); }catch(error){ qs('#week-calendar-status').textContent=error?.name==='AbortError'?'Share cancelled. Your Week Ahead is still ready.':'Calendar export failed. Retry without leaving Week Ahead.'; button.disabled=false; } }); return {open,close,blocks}; } const api={buildWeekBlocks,deliverWeekCalendar,escapeText,foldLine,mountWeekCalendarHandoff,serializeWeekCalendar}; if(typeof module!=='undefined'&&module.exports)module.exports=api;else root.StackchainWeekCalendar=api; })(typeof window!=='undefined'?window:globalThis);