{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"mini-kanban","type":"registry:component","title":"Mini Kanban","description":"A drag-and-drop board that lives inside a page, not a database.","author":"Notion Arena","categories":["planning"],"tags":["widget","pro","kanban","board","drag-drop","triage"],"meta":{"kind":"widget","tier":"pro","version":"1.1.0","updatedAt":"2026-09-22","notionPlan":"Free","emoji":"🗃️","delivery":["embed","html-block","download"],"capabilities":{"interactive":true,"offlineSafe":true,"persistsLocally":true,"writesToNotion":false,"needsNetwork":false},"embedUrl":"https://notionarena.vercel.app/embed/mini-kanban","fileUrl":"https://notionarena.vercel.app/widgets/mini-kanban.html","downloadUrl":"https://notionarena.vercel.app/api/widget/mini-kanban","height":340,"minHeight":260,"maxHeight":640,"params":[{"key":"columns","label":"Columns","type":"text","default":"Backlog,Today,Doing,Done"},{"key":"cards","label":"Seed cards (JSON)","type":"text","default":""},{"key":"wip","label":"WIP limit on 'Doing'","type":"number","default":3,"min":0,"max":20},{"key":"accent","label":"Accent","type":"color","default":"#6940a5"},{"key":"theme","label":"Theme","type":"select","default":"auto","options":[{"value":"auto","label":"Match Notion"},{"value":"light","label":"Light"},{"value":"dark","label":"Dark"}]}]},"files":[{"path":"widgets/mini-kanban.html","type":"registry:file","target":"notion/widgets/mini-kanban.html","content":"<!doctype html>\n<html lang=\"en\" data-theme=\"auto\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<title>Mini Kanban</title>\n<style>\n:root{--bg:#ffffff;--fg:#37352f;--muted:#787774;--surface:#f7f7f5;--border:#e9e9e7;--accent:#6940a5;--card:#ffffff;--warn:#e03e3e;\n--font:ui-sans-serif,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif}\nhtml[data-theme=\"dark\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#202020;--border:#2f2f2f;--card:#252525;--warn:#ff7369}\n@media (prefers-color-scheme:dark){html[data-theme=\"auto\"]{--bg:#191919;--fg:#e6e6e4;--muted:#9b9a97;--surface:#202020;--border:#2f2f2f;--card:#252525;--warn:#ff7369}}\n*{box-sizing:border-box}html,body{margin:0;height:100%}\nbody{background:var(--bg);color:var(--fg);font-family:var(--font);font-size:13px;-webkit-font-smoothing:antialiased;overflow:hidden}\n.wrap{height:100%;display:flex;flex-direction:column;gap:8px;padding:11px 13px;overflow:hidden}\n.bar{display:flex;gap:6px;align-items:center;flex-wrap:wrap}\ninput[type=text]{flex:1 1 150px;min-width:110px;font:inherit;font-size:12.5px;padding:5px 9px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--fg)}\ninput:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px color-mix(in oklab,var(--accent) 18%,transparent)}\nselect{font:inherit;font-size:11.5px;border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:8px;padding:5px 7px}\nbutton{font:inherit;font-size:11.5px;font-weight:500;border:1px solid var(--border);background:var(--surface);color:var(--muted);border-radius:8px;padding:5px 10px;cursor:pointer;white-space:nowrap}\nbutton:hover{color:var(--fg)}\n.board{flex:1;min-height:0;display:flex;gap:8px;overflow-x:auto;padding-bottom:2px}\n.col{flex:1 1 0;min-width:150px;display:flex;flex-direction:column;border:1px solid var(--border);border-radius:11px;background:var(--surface);overflow:hidden;transition:.12s}\n.col.over{border-color:var(--accent);background:color-mix(in oklab,var(--accent) 7%,var(--surface))}\n.ch{display:flex;align-items:center;gap:6px;padding:8px 9px 6px}\n.ch b{font-size:11.5px;font-weight:650;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ch .n{font-size:10px;color:var(--muted);font-variant-numeric:tabular-nums;font-weight:600;background:var(--bg);border:1px solid var(--border);border-radius:99px;padding:0 6px}\n.ch .n.over{color:var(--warn);border-color:color-mix(in oklab,var(--warn) 45%,var(--border));background:color-mix(in oklab,var(--warn) 10%,var(--bg))}\n.ch .plus{cursor:pointer;color:var(--muted);font-size:14px;line-height:1;padding:0 2px}\n.ch .plus:hover{color:var(--accent)}\n.cards{flex:1;min-height:0;overflow:auto;padding:2px 7px 8px;display:flex;flex-direction:column;gap:5px}\n.card{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:6px 8px;font-size:12px;line-height:1.35;\ncursor:grab;display:flex;gap:6px;align-items:flex-start;box-shadow:0 1px 1px rgba(15,15,15,.03)}\n.card:active{cursor:grabbing}\n.card.dragging{opacity:.4}\n.card .t{flex:1;min-width:0;word-break:break-word}\n.card .x{color:var(--muted);opacity:0;cursor:pointer;font-size:12px;line-height:1.2}\n.card:hover .x{opacity:1}\n.card .tag{display:inline-block;font-size:9.5px;font-weight:600;padding:0 5px;border-radius:4px;margin-top:4px;\nbackground:color-mix(in oklab,var(--accent) 14%,transparent);color:var(--accent)}\n.empty{color:var(--muted);font-size:11px;text-align:center;padding:10px 4px;opacity:.65}\n.foot{font-size:10.5px;color:var(--muted);display:flex;justify-content:space-between;gap:8px}\nkbd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9.5px;border:1px solid var(--border);border-radius:4px;padding:0 3px;background:var(--bg)}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n  <div class=\"bar\">\n    <input type=\"text\" id=\"add\" placeholder=\"Add a card to the first column and press Enter…\" />\n    <select id=\"target\" title=\"Which column new cards go to\"></select>\n    <button id=\"json\">Copy JSON</button>\n    <button id=\"csv\">Copy CSV</button>\n    <button id=\"reset\">Reset</button>\n  </div>\n  <div class=\"board\" id=\"board\"></div>\n  <div class=\"foot\">\n    <span>Drag cards between columns · <kbd>⌘/Ctrl</kbd> + <kbd>←</kbd><kbd>→</kbd> on a focused card</span>\n    <span id=\"counts\"></span>\n  </div>\n</div>\n<script>\n(function(){\n  var cfgEl=document.getElementById('bk-config'),base={};\n  try{base=cfgEl?JSON.parse(cfgEl.textContent||'{}'):{};}catch(e){}\n  var q=new URLSearchParams(location.search),c={};for(var k in base)c[k]=base[k];\n  q.forEach(function(v,k){c[k]=v;});\n  function num(k,d){var v=parseInt(c[k],10);return isFinite(v)?v:d;}\n\n  var ACCENT=c.accent||'#6940a5';\n  if(c.theme)document.documentElement.setAttribute('data-theme',c.theme);\n  document.documentElement.style.setProperty('--accent',ACCENT);\n\n  var COLS=(c.columns||'Backlog,Today,Doing,Done').split(',').map(function(s){return s.trim();}).filter(Boolean).slice(0,6);\n  if(!COLS.length)COLS=['Backlog','Doing','Done'];\n  var WIP=Math.max(0,num('wip',3));\n  var KEY='notion-arena.kanban.v1';\n\n  var DEFAULT={};\n  COLS.forEach(function(n){DEFAULT[n]=[];});\n  DEFAULT[COLS[0]]=[{t:'Draft the onboarding email',tag:'content'},{t:'Review pull request #482'},{t:'Book the offsite venue',tag:'ops'}];\n  if(COLS[2])DEFAULT[COLS[2]]=[{t:'Fix the embed height bug',tag:'eng'}];\n  if(COLS[3])DEFAULT[COLS[3]]=[{t:'Ship the pricing page'}];\n\n  var state=DEFAULT;\n  try{var s=JSON.parse(localStorage.getItem(KEY)||'null');if(s&&typeof s==='object')state=s;}catch(e){}\n  COLS.forEach(function(n){if(!Array.isArray(state[n]))state[n]=[];});\n  if(c.cards){\n    try{\n      var parsed=JSON.parse(decodeURIComponent(String(c.cards)));\n      if(Array.isArray(parsed)){\n        state={};COLS.forEach(function(n){state[n]=[];});\n        parsed.forEach(function(item){\n          var col=COLS.indexOf(item.column)>-1?item.column:COLS[0];\n          state[col].push({t:String(item.title||item.t||''),tag:item.tag||''});\n        });\n      }\n    }catch(e){}\n  }\n  function save(){try{localStorage.setItem(KEY,JSON.stringify(state));}catch(e){}}\n\n  var board=document.getElementById('board'),sel=document.getElementById('target'),addEl=document.getElementById('add');\n  sel.innerHTML=COLS.map(function(n,i){return '<option value=\"'+i+'\">'+n.replace(/[<>&]/g,'')+'</option>';}).join('');\n  var drag=null;\n\n  function render(){\n    board.innerHTML='';\n    COLS.forEach(function(name,ci){\n      var cards=state[name]||[];\n      var over=WIP>0&&ci===COLS.length-2&&cards.length>WIP;\n      var col=document.createElement('div');col.className='col';col.dataset.col=name;\n      col.innerHTML='<div class=\"ch\"><b>'+name.replace(/[<>&]/g,'')+'</b>'+\n        '<span class=\"n'+(over?' over':'')+'\" title=\"'+(over?'Over the WIP limit of '+WIP:'Cards in column')+'\">'+cards.length+(over?' / '+WIP:'')+'</span>'+\n        '<span class=\"plus\" title=\"Add a card here\">+</span></div>'+\n        '<div class=\"cards\">'+(cards.length?cards.map(function(cd,i){\n          return '<div class=\"card\" draggable=\"true\" tabindex=\"0\" data-i=\"'+i+'\"><span class=\"t\">'+String(cd.t||'').replace(/[<>&]/g,'')+\n            (cd.tag?'<br><span class=\"tag\">'+String(cd.tag).replace(/[<>&]/g,'')+'</span>':'')+'</span><span class=\"x\" title=\"Delete\">×</span></div>';\n        }).join(''):'<div class=\"empty\">No cards</div>')+'</div>';\n      board.appendChild(col);\n\n      col.addEventListener('dragover',function(e){e.preventDefault();col.classList.add('over');});\n      col.addEventListener('dragleave',function(){col.classList.remove('over');});\n      col.addEventListener('drop',function(e){\n        e.preventDefault();col.classList.remove('over');\n        if(!drag)return;\n        if(drag.col===name)return;\n        var moved=(state[drag.col]||[]).splice(drag.i,1)[0];\n        if(!moved)return;\n        state[name]=state[name]||[];\n        var at=drag.over!==undefined?drag.over:state[name].length;\n        state[name].splice(Math.max(0,Math.min(at,state[name].length)),0,moved);\n        drag=null;save();render();\n      });\n      col.querySelector('.plus').addEventListener('click',function(){\n        var t=prompt('New card in “'+name+'”:');\n        if(!t||!t.trim())return;\n        state[name]=state[name]||[];state[name].push({t:t.trim()});save();render();\n      });\n      var cardsEl=col.querySelector('.cards');\n      cardsEl.querySelectorAll('.card').forEach(function(node){\n        var i=parseInt(node.dataset.i,10);\n        node.addEventListener('dragstart',function(e){drag={col:name,i:i};node.classList.add('dragging');try{e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/plain',i+'');}catch(err){}});\n        node.addEventListener('dragend',function(){node.classList.remove('dragging');drag=null;});\n        node.querySelector('.x').addEventListener('click',function(e){e.stopPropagation();state[name].splice(i,1);save();render();});\n        node.addEventListener('keydown',function(e){\n          if(!(e.metaKey||e.ctrlKey))return;\n          var d=e.key==='ArrowLeft'?-1:e.key==='ArrowRight'?1:0;\n          if(!d)return;\n          e.preventDefault();\n          var ni=COLS.indexOf(name)+d;\n          if(ni<0||ni>=COLS.length)return;\n          var moved=state[name].splice(i,1)[0];\n          var to=COLS[ni];state[to]=state[to]||[];state[to].push(moved);\n          save();render();\n        });\n      });\n    });\n    var total=COLS.reduce(function(a,n){return a+(state[n]||[]).length;},0);\n    document.getElementById('counts').textContent=total+' cards · '+(WIP>0?'WIP limit '+WIP+' on “'+COLS[COLS.length-2]+'”':'no WIP limit');\n  }\n\n  addEl.addEventListener('keydown',function(e){\n    if(e.key!=='Enter')return;\n    var v=addEl.value.trim();if(!v)return;\n    var tag='';var m=v.match(/\\s#([\\w-]+)$/);\n    if(m){tag=m[1];v=v.slice(0,m.index).trim();}\n    var col=COLS[parseInt(sel.value,10)]||COLS[0];\n    state[col]=state[col]||[];state[col].push({t:v,tag:tag});\n    addEl.value='';save();render();\n  });\n  document.getElementById('reset').addEventListener('click',function(){\n    if(!confirm('Clear every card on this board?'))return;\n    state={};COLS.forEach(function(n){state[n]=[];});save();render();\n  });\n  function copy(txt,label,btn){\n    function done(t){btn.textContent=t;setTimeout(function(){btn.textContent=label;},1400);}\n    try{navigator.clipboard.writeText(txt).then(function(){done('Copied ✓');},function(){done('See console');});}catch(err){done('See console');}\n    try{console.log(txt);}catch(err){}\n  }\n  document.getElementById('json').addEventListener('click',function(e){\n    var out=[];COLS.forEach(function(n){(state[n]||[]).forEach(function(cd){out.push({title:cd.t,column:n,tag:cd.tag||null});});});\n    copy(JSON.stringify(out,null,2),'Copy JSON',e.currentTarget);\n  });\n  document.getElementById('csv').addEventListener('click',function(e){\n    var rows=['Name,Status,Tags'];\n    COLS.forEach(function(n){(state[n]||[]).forEach(function(cd){\n      rows.push('\"'+String(cd.t||'').replace(/\"/g,'\"\"')+'\",\"'+n+'\",\"'+(cd.tag||'')+'\"');\n    });});\n    copy(rows.join('\\n'),'Copy CSV',e.currentTarget);\n  });\n  render();\n})();\n</script>\n</body>\n</html>\n"}]}