Kanban
GPT 5.6 Luna Pro · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 24,075 bytes · SHA-256 4a67c226c054
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Flowboard · Kanban</title>
<style>
:root{--bg:#f5f6f8;--panel:#fff;--ink:#18202d;--muted:#778196;--line:#e4e8ef;--blue:#5865f2;--shadow:0 8px 24px #18202d0c;--radius:14px}
*{box-sizing:border-box}body{margin:0;background:var(--bg);font:14px Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);overflow:hidden}button,input,textarea{font:inherit}button{cursor:pointer;border:0;background:none;color:inherit}.app{height:100vh;display:flex;flex-direction:column}
.topbar{height:68px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;padding:0 26px;gap:24px;flex:none;position:relative;z-index:4}.brand{font-size:18px;font-weight:800;letter-spacing:-.5px;display:flex;align-items:center;gap:10px}.brand-mark{width:28px;height:28px;border-radius:9px;background:linear-gradient(135deg,#6974ff,#a479ec);display:grid;place-items:center;color:white;font-weight:900}.workspace{color:var(--muted);font-size:13px;padding-left:18px;border-left:1px solid var(--line)}.top-actions{margin-left:auto;display:flex;align-items:center;gap:10px}.search{width:210px;border:1px solid var(--line);border-radius:9px;height:36px;padding:0 12px 0 34px;background:#fafbfc url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="%23778196" stroke-width="2"><circle cx="7" cy="7" r="5"/><path d="m11 11 4 4"/></svg>') 10px center no-repeat;outline:none}.search:focus{border-color:#9ca4ff;box-shadow:0 0 0 3px #5865f218}.primary{background:var(--blue);color:#fff;padding:9px 14px;border-radius:8px;font-weight:700;box-shadow:0 3px 8px #5865f238}.primary:hover{background:#4955e5}.icon-btn{width:32px;height:32px;border-radius:8px;color:var(--muted);font-size:18px}.icon-btn:hover{background:#f0f2f6;color:var(--ink)}
.toolbar{height:61px;display:flex;align-items:center;padding:0 26px;gap:12px;flex:none}.toolbar h1{font-size:21px;margin:0;letter-spacing:-.5px}.subtle{color:var(--muted);font-size:12px}.filter-wrap{margin-left:auto;display:flex;align-items:center;gap:7px}.filter-label{color:var(--muted);font-size:12px;font-weight:700}.chip{border-radius:20px;padding:6px 10px;font-size:12px;font-weight:700;border:1px solid transparent;transition:.15s;white-space:nowrap}.chip.off{background:#fff;border-color:var(--line);color:var(--muted)}.chip.on{box-shadow:0 0 0 2px #5865f230}.chip:hover{transform:translateY(-1px)}
.board-shell{flex:1;min-height:0;overflow-x:auto;overflow-y:hidden;padding:0 26px 24px}.board{display:flex;align-items:stretch;gap:16px;height:100%;min-width:max-content}.column{width:286px;min-width:286px;height:100%;display:flex;flex-direction:column;background:#edf0f4;border-radius:13px;border:1px solid transparent;transition:border-color .15s,background .15s}.column.over{border-color:#9aa3ff;background:#e7e9ff}.column.wip-over{border-color:#ef6b70;background:#fff0f0}.col-head{height:55px;display:flex;align-items:center;padding:0 12px 0 16px;gap:8px;flex:none;cursor:grab}.col-head:active{cursor:grabbing}.col-title{font-size:13px;font-weight:800;letter-spacing:.1px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.count{font-size:11px;color:#8b95a7;background:#dfe3e9;border-radius:10px;padding:2px 7px;font-weight:700}.wip{font-size:10px;color:#d45b62;font-weight:700}.col-menu{margin-left:auto;opacity:.5;font-size:18px}.col-menu:hover{opacity:1}.col-body{overflow-y:auto;flex:1;padding:2px 10px 12px;scrollbar-width:thin}.col-body::-webkit-scrollbar{width:7px}.col-body::-webkit-scrollbar-thumb{background:#c7ccd5;border-radius:10px}.card{background:#fff;border:1px solid #e5e8ee;border-radius:10px;padding:13px 13px 11px;margin-bottom:9px;box-shadow:0 2px 4px #18202d0a;transition:transform .15s,box-shadow .15s;user-select:none;position:relative}.card:hover{transform:translateY(-2px);box-shadow:0 7px 16px #18202d12}.card.drag-source{visibility:hidden}.card-title{font-size:13px;line-height:1.35;font-weight:700;cursor:text}.card-desc{font-size:12px;color:var(--muted);margin-top:7px;line-height:1.4;white-space:pre-wrap}.labels{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}.label{font-size:10px;font-weight:800;padding:3px 7px;border-radius:5px}.due{display:inline-flex;margin-top:10px;align-items:center;gap:4px;font-size:11px;color:#7c8594}.due.late{color:#d7545b}.card-actions{display:none;position:absolute;right:7px;top:7px}.card:hover .card-actions{display:block}.mini{background:#fff;border-radius:6px;color:#8690a1;padding:2px 5px}.placeholder{height:50px;border:2px dashed #8790ff;border-radius:9px;background:#dfe2ff;margin-bottom:9px;transition:height .12s}.empty{height:86px;border:1.5px dashed #c4cad5;border-radius:10px;display:grid;place-items:center;color:#9aa3b2;font-size:12px;font-weight:600;margin-bottom:9px}.drag-ghost{position:fixed;z-index:20;pointer-events:none;width:276px;transform:rotate(2deg);box-shadow:0 18px 35px #18202d2b!important;opacity:.96}.col-drag-ghost{position:fixed;z-index:22;pointer-events:none;width:286px;opacity:.88;transform:rotate(1deg)}.col-placeholder{height:100%;width:286px;min-width:286px;border:2px dashed #a4abff;border-radius:13px;background:#e8eaff}
.edit-input{width:100%;border:1px solid #8c95ff;border-radius:5px;padding:3px 5px;outline:none;font-weight:700}.modal-back{position:fixed;inset:0;background:#18202d55;z-index:40;display:grid;place-items:center}.modal{background:#fff;border-radius:14px;width:390px;padding:22px;box-shadow:0 20px 60px #18202d35}.modal h2{font-size:17px;margin:0 0 18px}.form-row{margin:12px 0}.form-row label{display:block;font-size:11px;font-weight:800;color:var(--muted);margin-bottom:6px}.form-row input,.form-row textarea,.form-row select{width:100%;border:1px solid var(--line);border-radius:7px;padding:9px;outline:none}.form-row textarea{height:80px;resize:vertical}.modal-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.secondary{background:#f0f2f5;padding:9px 13px;border-radius:8px;font-weight:700}.danger{color:#d05259}.menu{position:fixed;background:#fff;border:1px solid var(--line);box-shadow:0 10px 30px #18202d1c;border-radius:9px;z-index:30;padding:5px;min-width:160px}.menu button{display:block;text-align:left;width:100%;padding:8px 10px;border-radius:5px;font-size:12px}.menu button:hover{background:#f1f3f7}.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);background:#202938;color:#fff;padding:11px 14px;border-radius:9px;box-shadow:0 8px 20px #0003;z-index:60;opacity:0;transition:.2s;display:flex;gap:16px;align-items:center}.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}.toast button{color:#aeb6ff;font-weight:800}.settle{animation:settle .25s}@keyframes settle{50%{transform:scale(1.025)}100%{transform:scale(1)}}
@media(max-width:700px){.topbar{padding:0 14px;gap:12px}.workspace,.search{display:none}.toolbar{padding:0 14px}.filter-wrap{overflow-x:auto}.board-shell{padding-left:14px;padding-right:14px}.column{width:285px}}
</style></head>
<body><div class="app"><header class="topbar"><div class="brand"><span class="brand-mark">✓</span>Flowboard</div><div class="workspace">Product Launch · Private</div><div class="top-actions"><input id="search" class="search" placeholder="Search cards…"><button class="icon-btn" id="clearFilters" title="Clear filters">⌕</button><button class="primary" id="addCard">+ Add card</button></div></header><div class="toolbar"><div><h1>Project board</h1><span class="subtle" id="summary"></span></div><div class="filter-wrap"><span class="filter-label">Labels</span><div id="filters"></div><button class="icon-btn" id="manageLabels" title="Manage labels">⚙</button><button class="primary" id="addColumn">+ Column</button></div></div><main class="board-shell" id="boardShell"><div class="board" id="board"></div></main></div><div id="overlay"></div><div class="toast" id="toast"></div>
<script>
/* ===================== TUNABLE CONSTANTS ===================== */
const CFG={storage:'flowboard-v1',dragThreshold:5,edge:58,scrollSpeed:12,colors:{blue:'#dce4ff',purple:'#eadcff',green:'#d9f4e8',orange:'#ffe6c7',pink:'#ffdbe5',teal:'#d5f2f0'},palette:[['blue','Blue'],['purple','Purple'],['green','Green'],['orange','Orange'],['pink','Pink'],['teal','Teal']]};
/* ===================== SERIALIZABLE MODEL ===================== */
const seed={columns:[{id:'c1',title:'Backlog',wip:null,cards:[{id:'a',title:'Define launch narrative',desc:'Align the story and success metrics.',labels:['purple'],due:''},{id:'b',title:'Competitive landscape',desc:'Capture key differentiators.',labels:['blue'],due:''}]},{id:'c2',title:'To Do',wip:null,cards:[{id:'c',title:'Design onboarding flow',desc:'Map the first-time user experience.',labels:['pink'],due:'2025-04-18'},{id:'d',title:'Set up analytics events',desc:'Instrument activation and retention.',labels:['green'],due:''}]},{id:'c3',title:'In Progress',wip:4,cards:[{id:'e',title:'Build dashboard shell',desc:'Responsive layout and navigation.',labels:['teal'],due:''}]},{id:'c4',title:'Review',wip:null,cards:[{id:'f',title:'QA release candidate',desc:'Run the final quality checklist.',labels:['orange'],due:'2025-04-12'}]},{id:'c5',title:'Done',wip:null,cards:[{id:'g',title:'Kickoff with the team',desc:'Project brief and roles are confirmed.',labels:['green'],due:''}]}],labels:CFG.palette.map(x=>({id:x[0],name:x[1]})),filter:[],search:''};
let model=load(); let drag=null; let menuEl=null; const $=s=>document.querySelector(s); const uid=p=>p+Math.random().toString(36).slice(2,8);
function load(){try{return JSON.parse(localStorage.getItem(CFG.storage))||structuredClone(seed)}catch(e){return structuredClone(seed)}}
function save(){localStorage.setItem(CFG.storage,JSON.stringify(model))} function allCards(){return model.columns.flatMap(c=>c.cards)}
function visible(card){return(!model.filter.length||card.labels.some(x=>model.filter.includes(x)))&&(!model.search||card.title.toLowerCase().includes(model.search.toLowerCase()))}
/* ===================== RENDERING ===================== */
function render(){const board=$('#board'); board.innerHTML=''; let total=0,shown=0;model.columns.forEach(col=>{total+=col.cards.length;shown+=col.cards.filter(visible).length;const el=document.createElement('section');el.className='column'+(col.wip&&col.cards.length>col.wip?' wip-over':'');el.dataset.cid=col.id;el.innerHTML=`<div class="col-head" data-col-drag="${col.id}"><span class="col-title">${esc(col.title)}</span><span class="count">${col.cards.length}</span>${col.wip?`<span class="wip">WIP ${col.wip}</span>`:''}<button class="col-menu icon-btn" data-menu="${col.id}">⋯</button></div><div class="col-body" data-body="${col.id}"></div>`;const body=el.querySelector('.col-body');let cards=col.cards.filter(visible);cards.forEach(card=>body.append(cardEl(card,col.id)));if(!cards.length)body.insertAdjacentHTML('beforeend','<div class="empty">Drop cards here</div>');board.append(el)});$('#summary').textContent=` · ${shown} of ${total} cards`;renderFilters();bind();}
function cardEl(card,cid){const el=document.createElement('article');el.className='card';el.dataset.card=card.id;let labs=card.labels.map(id=>{let l=model.labels.find(x=>x.id===id)||{name:id};return `<span class="label" style="background:${CFG.colors[id]||'#eee'}">${esc(l.name)}</span>`}).join('');let due=card.due?`<div class="due ${card.due<new Date().toISOString().slice(0,10)?'late':''}">◷ ${card.due}</div>`:'';el.innerHTML=`<div class="card-title">${esc(card.title)}</div>${card.desc?`<div class="card-desc">${esc(card.desc)}</div>`:''}${labs?`<div class="labels">${labs}</div>`:''}${due}<div class="card-actions"><button class="mini" data-edit="${card.id}">✎</button><button class="mini" data-delete-card="${card.id}">×</button></div>`;return el}
function renderFilters(){const f=$('#filters');f.innerHTML=model.labels.map(l=>`<button class="chip ${model.filter.includes(l.id)?'on':'off'}" data-filter="${l.id}" style="${model.filter.includes(l.id)?`background:${CFG.colors[l.id]};color:#38404d`:''}">${esc(l.name)}</button>`).join('')||'<span class="subtle">No labels</span>'}
function bind(){document.querySelectorAll('[data-filter]').forEach(b=>b.onclick=()=>{let id=b.dataset.filter;model.filter=model.filter.includes(id)?model.filter.filter(x=>x!==id):[...model.filter,id];save();render()});document.querySelectorAll('[data-menu]').forEach(b=>b.onclick=e=>{e.stopPropagation();openMenu(b.dataset.menu,b)});document.querySelectorAll('[data-delete-card]').forEach(b=>b.onclick=e=>{e.stopPropagation();deleteCard(b.dataset.deleteCard)});document.querySelectorAll('[data-edit]').forEach(b=>b.onclick=e=>{e.stopPropagation();openCard(b.dataset.edit)});document.querySelectorAll('.card-title').forEach(t=>t.onclick=e=>inlineTitle(t,e));document.querySelectorAll('[data-card]').forEach(e=>e.onpointerdown=cardDown);document.querySelectorAll('[data-col-drag]').forEach(e=>e.onpointerdown=colDown)}
function esc(x){return String(x||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
/* ===================== CRUD + MODALS ===================== */
function modal(html,submit){$('#overlay').innerHTML=`<div class="modal-back"><div class="modal">${html}</div></div>`;const back=$('.modal-back');back.onclick=e=>{if(e.target===back)closeModal()};$('#modalCancel').onclick=closeModal;const form=$('#modalForm');if(form)form.onsubmit=e=>{e.preventDefault();submit(new FormData(e.target));closeModal()}}
function closeModal(){$('#overlay').innerHTML=''}
function openCard(id){let card=allCards().find(x=>x.id===id)||{title:'',desc:'',due:'',labels:[]};modal(`<h2>${id?'Edit':'Add'} card</h2><form id="modalForm"><div class="form-row"><label>Title</label><input name="title" required value="${esc(card.title)}"></div><div class="form-row"><label>Description</label><textarea name="desc">${esc(card.desc)}</textarea></div><div class="form-row"><label>Labels <span class="subtle">(Ctrl/Cmd-click for multiple)</span></label><select name="labels" multiple size="${Math.min(4,Math.max(2,model.labels.length))}">${model.labels.map(l=>`<option value="${l.id}" ${card.labels.includes(l.id)?'selected':''}>${esc(l.name)}</option>`).join('')}</select></div><div class="form-row"><label>Due date</label><input name="due" type="date" value="${card.due||''}"></div><div class="modal-foot"><button type="button" class="secondary" id="modalCancel">Cancel</button><button class="primary">Save card</button></div></form>`,d=>{let data={title:d.get('title').trim(),desc:d.get('desc').trim(),due:d.get('due'),labels:d.getAll('labels')};if(id)Object.assign(card,data);else (model.columns.find(c=>c.id===pendingColumn)||model.columns[0]).cards.push({id:uid('card-'),...data});pendingColumn=null;save();render()})}
function openColumn(id){let col=model.columns.find(x=>x.id===id)||{title:'',wip:null};modal(`<h2>${id?'Rename':'Add'} column</h2><form id="modalForm"><div class="form-row"><label>Column name</label><input name="title" required value="${esc(col.title)}"></div><div class="form-row"><label>WIP limit <span class="subtle">(optional)</span></label><input name="wip" type="number" min="1" value="${col.wip||''}"></div><div class="modal-foot"><button type="button" class="secondary" id="modalCancel">Cancel</button><button class="primary">Save</button></div></form>`,d=>{col.title=d.get('title').trim();col.wip=d.get('wip')?Number(d.get('wip')):null;if(!id){col.id=uid('col-');col.cards=[];model.columns.push(col)}save();render()})}
function deleteCard(id){let ci=model.columns.findIndex(c=>c.cards.some(x=>x.id===id));let card=model.columns[ci].cards.find(x=>x.id===id),at=model.columns[ci].cards.indexOf(card);model.columns[ci].cards.splice(at,1);save();render();toast('Card deleted',()=>{model.columns[ci].cards.splice(at,0,card);save();render()})}
function deleteColumn(id){if(model.columns.length===1)return toast('Keep at least one column');let at=model.columns.findIndex(c=>c.id===id),col=model.columns[at];model.columns.splice(at,1);save();render();toast('Column deleted',()=>{model.columns.splice(at,0,col);save();render()})}
function openMenu(id,anchor){closeMenu();let m=document.createElement('div');m.className='menu';m.innerHTML='<button data-rename>Rename / WIP limit</button><button data-add>Add card here</button><button data-del class="danger">Delete column</button>';document.body.append(m);let r=anchor.getBoundingClientRect();m.style.left=Math.min(innerWidth-175,r.right-165)+'px';m.style.top=r.bottom+4+'px';m.querySelector('[data-rename]').onclick=()=>{closeMenu();openColumn(id)};m.querySelector('[data-add]').onclick=()=>{closeMenu();openCard(null);pendingColumn=id};m.querySelector('[data-del]').onclick=()=>{closeMenu();deleteColumn(id)};menuEl=m}function closeMenu(){if(menuEl)menuEl.remove();menuEl=null}document.addEventListener('click',closeMenu);let pendingColumn=null;
function inlineTitle(node,e){if(e.target.closest('button'))return;let old=node.textContent,input=document.createElement('input');input.className='edit-input';input.value=old;node.replaceWith(input);input.focus();input.select();let done=ok=>{if(!input.isConnected)return;let c=allCards().find(x=>x.title===old);if(c&&ok&&input.value.trim()){c.title=input.value.trim();save();render()}else render()};input.onkeydown=e=>{if(e.key==='Enter')done(true);if(e.key==='Escape')done(false)};input.onblur=()=>done(true)}
function toast(msg,undo){let t=$('#toast');t.innerHTML=`<span>${msg}</span>${undo?'<button>Undo</button>':''}`;t.classList.add('show');if(undo)t.querySelector('button').onclick=()=>{undo();t.classList.remove('show')};clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove('show'),5000)}
/* ===================== POINTER DRAG ENGINE ===================== */
function cardDown(e){if(e.button!==0||e.target.closest('button,.edit-input'))return;let el=e.currentTarget,card=allCards().find(x=>x.id===el.dataset.card),col=model.columns.find(c=>c.id===el.closest('.column').dataset.cid);drag={kind:'card',id:card.id,sourceCol:col.id,sourceIndex:col.cards.indexOf(card),startX:e.clientX,startY:e.clientY,x:e.clientX,y:e.clientY,active:false,el};el.setPointerCapture(e.pointerId);el.onpointermove=move;el.onpointerup=up;el.onpointercancel=cancel}
function beginCard(){drag.active=true;drag.el.classList.add('drag-source');let g=drag.el.cloneNode(true);g.className='card drag-ghost';document.body.append(g);drag.ghost=g;drag.el.closest('.column').classList.add('over');indicator(drag.x,drag.y);}
function move(e){if(!drag)return;drag.x=e.clientX;drag.y=e.clientY;if(drag.kind==='card'){if(!drag.active&&Math.hypot(e.clientX-drag.startX,e.clientY-drag.startY)>CFG.dragThreshold)beginCard();if(drag.active){drag.ghost.style.left=e.clientX-138+'px';drag.ghost.style.top=e.clientY-22+'px';autoScroll(e);indicator(e.clientX,e.clientY)}}else moveCol(e)}
function indicator(x,y){let target=document.elementFromPoint(x,y)?.closest('.column');if(!target)return;document.querySelectorAll('.column.over').forEach(x=>x.classList.remove('over'));target.classList.add('over');let body=target.querySelector('.col-body'),cards=[...body.querySelectorAll('.card:not(.drag-source)')];let before=cards.find(c=>y<c.getBoundingClientRect().top+c.getBoundingClientRect().height/2);if(!drag.ph){drag.ph=document.createElement('div');drag.ph.className='placeholder'}if(before)body.insertBefore(drag.ph,before);else body.append(drag.ph);drag.targetCol=target.dataset.cid;drag.targetBefore=before?.dataset.card||null}
function autoScroll(e){let shell=$('#boardShell');if(e.clientX<CFG.edge)shell.scrollLeft-=CFG.scrollSpeed;else if(e.clientX>innerWidth-CFG.edge)shell.scrollLeft+=CFG.scrollSpeed;let col=document.elementFromPoint(e.clientX,e.clientY)?.closest('.column');if(col){let b=col.querySelector('.col-body'),r=b.getBoundingClientRect();if(e.clientY<r.top+CFG.edge)b.scrollTop-=CFG.scrollSpeed;else if(e.clientY>r.bottom-CFG.edge)b.scrollTop+=CFG.scrollSpeed}}
function up(e){if(!drag)return;if(drag.kind==='card'){if(!drag.active){drag=null;return}let d=drag, target=model.columns.find(c=>c.id===d.targetCol);if(!target){cancel();return}let src=model.columns.find(c=>c.id===d.sourceCol),card=src.cards.splice(d.sourceIndex,1)[0];let idx=d.targetBefore?target.cards.findIndex(c=>c.id===d.targetBefore):target.cards.length;if(src===target&&d.sourceIndex<idx)idx--;target.cards.splice(Math.max(0,idx),0,card);cleanup();save();render();setTimeout(()=>document.querySelector(`[data-card="${card.id}"]`)?.classList.add('settle'),0)}else upCol()}
function cancel(){if(!drag)return;if(drag.kind==='card'){cleanup();render()}else{if(drag.ghost)drag.ghost.remove();if(drag.ph)drag.ph.remove();drag=null;render()}}
function cleanup(){drag.ghost?.remove();drag.ph?.remove();document.querySelectorAll('.column.over').forEach(x=>x.classList.remove('over'));drag.el?.releasePointerCapture?.();drag=null}
/* columns */
function colDown(e){if(e.button!==0||e.target.closest('button'))return;let el=e.currentTarget,col=el.closest('.column');drag={kind:'col',id:col.dataset.cid,startX:e.clientX,startY:e.clientY,x:e.clientX,y:e.clientY,active:false,el:col};el.setPointerCapture(e.pointerId);el.onpointermove=move;el.onpointerup=up;el.onpointercancel=cancel}
function moveCol(e){drag.x=e.clientX;drag.y=e.clientY;if(!drag.active&&Math.hypot(e.clientX-drag.startX,e.clientY-drag.startY)>CFG.dragThreshold){drag.active=true;drag.el.style.visibility='hidden';let g=drag.el.cloneNode(true);g.className='column col-drag-ghost';document.body.append(g);drag.ghost=g;drag.ph=document.createElement('div');drag.ph.className='col-placeholder';drag.el.parentElement.insertBefore(drag.ph,drag.el)}if(drag.active){drag.ghost.style.left=e.clientX-143+'px';drag.ghost.style.top=e.clientY-25+'px';let cols=[...document.querySelectorAll('.column:not([style*="hidden"])')].filter(x=>x!==drag.el),before=cols.find(c=>e.clientX<c.getBoundingClientRect().left+c.offsetWidth/2);if(before)drag.el.parentElement.insertBefore(drag.ph,before);else drag.el.parentElement.append(drag.ph)}}
function upCol(){if(!drag.active){drag=null;return}let cols=model.columns,src=cols.findIndex(c=>c.id===drag.id),visual=[...document.querySelectorAll('.column')].filter(x=>x!==drag.el),before=visual.find(c=>drag.x<c.getBoundingClientRect().left+c.offsetWidth/2),idx=before?cols.findIndex(c=>c.id===before.dataset.cid):cols.length;let item=cols.splice(src,1)[0];if(src<idx)idx--;cols.splice(Math.max(0,idx),0,item);cancel();save();render()}
/* ===================== EVENTS ===================== */
$('#addCard').onclick=()=>openCard(null);$('#addColumn').onclick=()=>openColumn(null);$('#manageLabels').onclick=()=>labelManager();$('#search').oninput=e=>{model.search=e.target.value;save();render()};$('#clearFilters').onclick=()=>{model.search='';model.filter=[];$('#search').value='';save();render()};document.addEventListener('keydown',e=>{if(e.key==='Escape'&&drag)cancel()});
function labelManager(){modal(`<h2>Manage labels</h2>${model.labels.map(l=>`<div style="display:flex;gap:8px;align-items:center;margin:8px 0"><span class="label" style="background:${CFG.colors[l.id]}">${esc(l.name)}</span><input data-lab="${l.id}" value="${esc(l.name)}" style="flex:1;border:1px solid var(--line);padding:6px;border-radius:6px"></div>`).join('')}<div class="modal-foot"><button type="button" class="secondary" id="modalCancel">Cancel</button><button type="button" class="primary" id="saveLabels">Save labels</button></div>`,()=>{});$('#saveLabels').onclick=()=>{model.labels.forEach(l=>{let x=document.querySelector(`[data-lab="${l.id}"]`);if(x&&x.value.trim())l.name=x.value.trim()});save();closeModal();render()}}
render();save();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-5.6-luna-pro","provider":"openrouter","persona":"full","sessionId":"3bef6f6b-1d16-478d-9a4e-1f0e9adb8078","tokensIn":786002,"tokensOut":30651,"tokensTotal":816653,"turns":16,"toolCalls":15,"failedToolCalls":0,"timestamp":"2026-07-31T01:30:10.008Z"} -->