1// Continuous, bounded windows let the year dock jump without loading intervening photos. 2let cleanups=[]; 3export function stopFeeds(){for(const fn of cleanups)fn();cleanups=[];} 4export function bindContinuousFeed({root,items,renderItem,batch=18,start=batch,startIndex=0,onAppend,statusText,yearOf,language='ko'}){ 5 if(!root)return; 6 const list=root.querySelector('[data-feed-list]'),sentinel=root.querySelector('[data-feed-sentinel]'),status=root.querySelector('[data-feed-status]'); 7 if(!list||!sentinel)return; 8 let first=Math.max(0,startIndex),count=Math.min(Math.max(start,first+batch),items.length),active=true,pending=false,frame=0,lastY=scrollY; 9 const year=item=>{const value=yearOf?.(item);return /^\d{4}$/.test(String(value))?String(value):'';}; 10 const years=yearOf?[...new Set(items.map(year).filter(Boolean))]:[]; 11 const indices=new Map(years.map(y=>[y,items.findIndex(item=>year(item)===y)])); 12 const previous=document.createElement('button');previous.type='button';previous.className='feed-previous';previous.textContent=language==='ko'?'â ë ìµê·¼ ê²ì물':'â More recent posts';previous.hidden=!first;list.before(previous); 13 let rail,buttons=[],currentYear=0,scrollFocus=0,hoverYear=null,focusYear=null,pointerX=null,pointerY=null; 14 if(years.length){ 15 rail=document.createElement('aside');rail.className='year-rail';rail.setAttribute('aria-label',language==='ko'?'ê²ì ì°ë íì':'Browse by publication year');rail.dataset.preserveText=''; 16 rail.innerHTML=years.map(y=>`<button type="button" class="year-dock-label" data-rail-year="${y}" aria-label="${language==='ko'?y+'ë ê²ì물': 'Posts from '+y}" tabindex="-1"><span>${y}</span></button>`).join(''); 17 root.append(rail);buttons=[...rail.querySelectorAll('button')]; 18 } 19 const drawDock=()=>{ 20 if(!rail)return; 21 const compact=innerWidth<=700,header=document.querySelector('.site-header')?.getBoundingClientRect().bottom||100; 22 const available=Math.max(120,innerHeight-Math.max(100,header)-36),peak=compact?4:14; 23 const step=Math.min(compact?24:22,Math.max(12,(available-peak*3)/years.length)); 24 const lens=hoverYear??focusYear??scrollFocus; 25 const weights=years.map((_,i)=>Math.exp(-Math.pow((i-lens)/1.25,2))); 26 const heights=weights.map(w=>step+peak*w),height=heights.reduce((a,b)=>a+b,0); 27 rail.style.height=`${height}px`;rail.style.top=`${Math.max(header+18,(innerHeight-height)/2)}px`; 28 let offset=0; 29 buttons.forEach((button,i)=>{ 30 button.style.height=`${heights[i]}px`;button.style.transform=`translateY(${offset}px)`; 31 button.style.setProperty('--year-scale',String(1+(compact ? 0.2 : 1.4)*weights[i])); 32 button.style.setProperty('--year-opacity',String(.42+.58*weights[i])); 33 offset+=heights[i]; 34 }); 35 }; 36 const setYear=(y,proximity=0)=>{if(!rail||!y)return;const n=years.indexOf(y);if(n<0)return;currentYear=n;scrollFocus=n+proximity;buttons.forEach((button,i)=>{button.tabIndex=i===(focusYear??n)?0:-1;if(i===n)button.setAttribute('aria-current','date');else button.removeAttribute('aria-current');});drawDock();}; 37 const mark=()=>{[...list.children].forEach((el,n)=>{el.dataset.feedIndex=String(first+n);if(yearOf)el.dataset.feedYear=year(items[first+n]);});list.dataset.loaded=String(count);list.dataset.start=String(first);}; 38 const update=()=>{mark();if(status)status.textContent=statusText(count-first,items.length);sentinel.hidden=count>=items.length;previous.hidden=first===0;}; 39 const paint=()=>{list.innerHTML=items.slice(first,count).map(renderItem).join('');update();onAppend?.(list);}; 40 const append=()=>{if(!active||pending||count>=items.length)return;pending=true;requestAnimationFrame(()=>{if(!active)return;list.setAttribute('aria-busy','true');const next=Math.min(count+batch,items.length);list.insertAdjacentHTML('beforeend',items.slice(count,next).map(renderItem).join(''));count=next;pending=false;list.setAttribute('aria-busy','false');update();onAppend?.(list);});}; 41 const prepend=()=>{if(!active||pending||!first)return;pending=true;const anchor=list.firstElementChild,top=anchor?.getBoundingClientRect().top||0;const next=Math.max(0,first-batch);list.insertAdjacentHTML('afterbegin',items.slice(next,first).map(renderItem).join(''));first=next;update();onAppend?.(list);if(anchor)scrollBy(0,anchor.getBoundingClientRect().top-top);lastY=scrollY;pending=false;}; 42 const jump=n=>{const y=years[n],index=indices.get(y);if(index==null)return;first=index;count=Math.min(index+batch*2,items.length);paint();setYear(y);const header=document.querySelector('.site-header')?.getBoundingClientRect().height||100;scrollTo({top:scrollY+list.getBoundingClientRect().top-header-26,behavior:'instant'});lastY=scrollY;};
43 previous.addEventListener('click',prepend); 44 if(rail){ 45 buttons.forEach((button,i)=>{ 46 button.addEventListener('click',()=>jump(i)); 47 button.addEventListener('focus',()=>{focusYear=button.matches(':focus-visible')?i:null;drawDock();}); 48 button.addEventListener('keydown',e=>{let n=i;if(e.key==='ArrowDown'||e.key==='ArrowRight')n=Math.min(i+1,years.length-1);else if(e.key==='ArrowUp'||e.key==='ArrowLeft')n=Math.max(i-1,0);else if(e.key==='Home')n=0;else if(e.key==='End')n=years.length-1;else return;e.preventDefault();buttons[n].focus();jump(n);}); 49 }); 50 // Only real pointer movement moves the lens; expanding labels must not retrigger it. 51 rail.addEventListener('pointermove',e=>{if(e.pointerType!=='mouse'||(e.clientX===pointerX&&e.clientY===pointerY))return;pointerX=e.clientX;pointerY=e.clientY;const i=buttons.indexOf(e.target.closest('button'));if(i>=0&&i!==hoverYear){hoverYear=i;drawDock();}}); 52 rail.addEventListener('pointerleave',()=>{hoverYear=null;pointerX=null;pointerY=null;drawDock();}); 53 rail.addEventListener('focusout',e=>{if(!rail.contains(e.relatedTarget)){focusYear=null;setYear(years[currentYear],scrollFocus-currentYear);}}); 54 setYear(year(items[first])); 55 } 56 const measure=()=>{frame=0;if(!active)return;const direction=scrollY-lastY;lastY=scrollY;if(sentinel.getBoundingClientRect().top<innerHeight+700)append();if(direction<0&&first&&list.getBoundingClientRect().top>120)prepend();if(rail){const bounds=root.getBoundingClientRect(),shown=bounds.top<innerHeight*.7&&bounds.bottom>200;rail.classList.toggle('is-visible',shown);rail.inert=!shown;const anchor=(document.querySelector('.site-header')?.getBoundingClientRect().bottom||100)+60;const cards=[...list.children],visible=cards.find(el=>el.getBoundingClientRect().bottom>anchor);if(visible?.dataset.feedYear){const next=cards.find(el=>Number(el.dataset.feedIndex)>Number(visible.dataset.feedIndex)&&el.dataset.feedYear&&el.dataset.feedYear!==visible.dataset.feedYear);const reach=Math.max(1,innerHeight*.75-anchor);const proximity=next?Math.max(0,Math.min(.85,(innerHeight*.75-next.getBoundingClientRect().top)/reach)):0;setYear(visible.dataset.feedYear,proximity);}}}; 57 const check=()=>{if(!frame)frame=requestAnimationFrame(measure);}; 58 let observer;if('IntersectionObserver'in window){observer=new IntersectionObserver(entries=>{if(entries.some(e=>e.isIntersecting))append();},{rootMargin:'700px 0px'});observer.observe(sentinel);} 59 const focusLoad=e=>{if([...list.children].slice(-3).some(el=>el.contains(e.target)))append();}; 60 window.addEventListener('scroll',check,{passive:true});window.addEventListener('resize',check);list.addEventListener('focusin',focusLoad); 61 if(first)paint();else update();requestAnimationFrame(measure); 62 cleanups.push(()=>{active=false;observer?.disconnect();cancelAnimationFrame(frame);window.removeEventListener('scroll',check);window.removeEventListener('resize',check);list.removeEventListener('focusin',focusLoad);rail?.remove();previous.remove();}); 63} 64export const feedTail=label=>`<div class="feed-tail"><div data-feed-sentinel aria-hidden="true"><span></span><span></span><span></span></div><p data-feed-status role="status" aria-live="polite">${label}</p><a class="feed-top" href="#page-top" data-scroll-top aria-label="Back to top / 맨 ìë¡">â</a></div>`;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.