Muse Ember
by MetaReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 19.9 s
- Cost
- $0.0054
- Output tokens
- 10,665
- Spent thinking
- 2,541 tokens
- Speed
- 585.6 tok/s
- Console errors
- 0
- First token after
- 1.7 s
- First visible text after
- 8.2 s
- Prompt tokens
- 511
- Code size
- 27.1 KB
- Finish reason
- stop
- Served by
- Meta
- Model id
meta/muse-ember- Reasoning sent
{"effort":"high"}- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build an interactive Kanban board in vanilla HTML, CSS and JavaScript (no framework, no library). Requirements: - Three columns: "To do", "In progress", "Done", each showing its card count. - Pre-populate the board with 6 realistic cards spread across the columns so the first screenshot looks alive. - Each card has a title and a priority (low / medium / high) shown as a colored badge. - Add a card through a small modal form (title required), and delete a card with a button on it. - Drag and drop cards between columns with the mouse, with a clear visual drop indicator. - A search field that filters cards live by title. - A light / dark theme toggle. - State lives in memory only (no storage APIs).
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 527 lines
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlowBoard — Kanban</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#f3f4f6; --surface:#ffffff; --surface2:#eef0f3; --text:#161c2d; --muted:#6b7280;
--border:#e2e5ea; --border2:#d1d5db; --accent:#4f46e5; --accent2:#4338ca; --accentSoft:#eef2ff;
--shadow:0 1px 2px rgba(0,0,0,.06),0 4px 16px rgba(0,0,0,.06);
--shadowLg:0 20px 50px rgba(0,0,0,.18);
--input:#ffffff;
}
[data-theme="dark"]{
--bg:#0f1115; --surface:#1a1d24; --surface2:#14161c; --text:#eef2f7; --muted:#9aa3b2;
--border:#2a2e38; --border2:#353b48; --accent:#7c86ff; --accent2:#8b96ff; --accentSoft:#23263f;
--shadow:0 1px 2px rgba(0,0,0,.4),0 4px 16px rgba(0,0,0,.3); --shadowLg:0 20px 50px rgba(0,0,0,.6);
--input:#15171d;
}
html,body{height:100%}
body{font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
input,select,textarea{font-family:inherit;color:var(--text)}
/* Header */
header{background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:30}
.header-inner{max-width:1240px;margin:0 auto;padding:14px 20px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.logo{display:flex;align-items:center;gap:11px;min-width:0}
.logo-mark{width:38px;height:38px;border-radius:10px;background:var(--accent);color:#fff;display:grid;place-items:center;font-weight:800;font-size:19px;flex:0 0 auto;box-shadow:0 4px 12px rgba(79,70,229,.35)}
.logo h1{font-size:19px;letter-spacing:-.02em;line-height:1.1;white-space:nowrap}
.logo p{font-size:12px;color:var(--muted);margin-top:2px;white-space:nowrap}
.search-wrap{margin-left:auto;position:relative;flex:1 1 280px;max-width:380px}
.search-wrap span{position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:15px;color:var(--muted);pointer-events:none}
.search-wrap input{width:100%;height:40px;border:1.5px solid var(--border);background:var(--input);border-radius:999px;padding:0 38px 0 38px;font-size:14px;outline:none;transition:.2s}
.search-wrap input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accentSoft)}
.search-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:26px;height:26px;border-radius:50%;display:none;place-items:center;color:var(--muted);font-size:16px}
.search-clear:hover{background:var(--surface2)}
.btn-primary{background:var(--accent);color:#fff;height:40px;padding:0 18px;border-radius:999px;font-weight:700;font-size:14px;display:inline-flex;align-items:center;gap:7px;box-shadow:0 4px 12px rgba(79,70,229,.3);transition:.2s;white-space:nowrap}
.btn-primary:hover{background:var(--accent2);transform:translateY(-1px)}
.theme-btn{width:40px;height:40px;border-radius:50%;border:1.5px solid var(--border);background:var(--surface);display:grid;place-items:center;font-size:17px;transition:.2s;flex:0 0 auto}
.theme-btn:hover{border-color:var(--border2);transform:scale(1.05)}
/* Progress strip */
.strip{max-width:1240px;margin:0 auto;padding:18px 20px 0;width:100%}
.strip-inner{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:14px 18px;display:flex;align-items:center;gap:18px;flex-wrap:wrap;box-shadow:var(--shadow)}
.strip-inner .label{font-size:13px;color:var(--muted);font-weight:600;letter-spacing:.02em;text-transform:uppercase}
.strip-inner .big{font-size:15px;font-weight:800}
.bar{flex:1 1 180px;height:8px;background:var(--surface2);border-radius:999px;overflow:hidden;min-width:140px}
.bar-fill{height:100%;background:#16a34a;border-radius:999px;transition:width .5s ease;width:0%}
.hint{font-size:13px;color:var(--muted)}
.hint b{color:var(--text);font-weight:700}
/* Board */
.board{max-width:1240px;margin:0 auto;padding:18px 20px 28px;display:grid;grid-template-columns:repeat(3,1fr);gap:18px;width:100%;flex:1;align-items:start}
.col{background:var(--surface2);border:1px solid var(--border);border-radius:18px;display:flex;flex-direction:column;max-height:calc(100vh - 210px);min-height:340px;transition:.2s}
.col.drag-over{border:2px dashed var(--accent);background:var(--accentSoft)}
[data-theme="dark"] .col.drag-over{background:#1d2033}
.col-head{padding:16px 16px 12px;display:flex;align-items:center;gap:9px;border-bottom:1px solid var(--border);flex:0 0 auto}
.dot{width:10px;height:10px;border-radius:50%;flex:0 0 auto}
.dot-todo{background:#f59e0b}.dot-prog{background:#3b82f6}.dot-done{background:#22c55e}
.col-head h2{font-size:15px;font-weight:800;letter-spacing:-.01em}
.count{background:var(--surface);border:1px solid var(--border);color:var(--muted);font-size:12px;font-weight:800;min-width:26px;height:24px;padding:0 7px;border-radius:999px;display:inline-grid;place-items:center}
.add-mini{margin-left:auto;width:30px;height:30px;border-radius:50%;background:var(--surface);border:1px solid var(--border);font-size:18px;line-height:1;display:grid;place-items:center;transition:.2s}
.add-mini:hover{background:var(--accent);color:#fff;border-color:var(--accent);transform:scale(1.08)}
.col-body{padding:12px;display:flex;flex-direction:column;gap:11px;overflow-y:auto;flex:1;min-height:120px}
.col-body::-webkit-scrollbar{width:6px}
.col-body::-webkit-scrollbar-thumb{background:var(--border2);border-radius:999px}
.card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:14px 14px 12px;box-shadow:var(--shadow);cursor:grab;position:relative;transition:transform .15s,box-shadow .15s,opacity .15s;animation:fadeUp .35s both}
.card:hover{transform:translateY(-2px);box-shadow:0 8px 22px rgba(0,0,0,.10)}
.card:active{cursor:grabbing}
.card.dragging{opacity:.38;transform:rotate(2deg) scale(.98);box-shadow:var(--shadowLg)}
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.card-top{display:flex;align-items:flex-start;gap:8px}
.card h3{font-size:14.5px;line-height:1.35;font-weight:750;letter-spacing:-.01em;flex:1;padding-right:22px}
.del{position:absolute;top:9px;right:9px;width:26px;height:26px;border-radius:7px;display:grid;place-items:center;color:var(--muted);font-size:15px;opacity:.55;transition:.15s}
.del:hover{opacity:1;background:#fee2e2;color:#dc2626}
[data-theme="dark"] .del:hover{background:#3a1f24}
.card p{font-size:13px;line-height:1.5;color:var(--muted);margin-top:6px}
.card-foot{display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap}
.badge{font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;padding:4px 8px;border-radius:999px;display:inline-flex;align-items:center;gap:5px}
.badge::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
.p-low{background:#dcfce7;color:#15803d}
.p-medium{background:#fef3c7;color:#b45309}
.p-high{background:#fee2e2;color:#b91c1c}
[data-theme="dark"] .p-low{background:rgba(34,197,94,.18)}[data-theme="dark"] .p-medium{background:rgba(245,158,11,.18)}[data-theme="dark"] .p-high{background:rgba(239,68,68,.18)}
.meta{margin-left:auto;display:flex;align-items:center;gap:7px}
.due{font-size:12px;color:var(--muted);font-weight:600}
.avatar{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;font-size:11px;font-weight:800;color:#fff;flex:0 0 auto}
.drop-placeholder{height:6px;border-radius:999px;background:var(--accent);box-shadow:0 0 0 3px var(--accentSoft);flex:0 0 auto;margin:2px 0}
.empty{border:1.5px dashed var(--border2);border-radius:14px;padding:22px 14px;text-align:center;color:var(--muted);font-size:13px;line-height:1.5;background:transparent}
.empty .e-icon{font-size:22px;display:block;margin-bottom:6px}
.empty button{color:var(--accent);font-weight:800;font-size:13px;margin-top:6px}
.empty button:hover{text-decoration:underline}
/* Modal */
.overlay{position:fixed;inset:0;background:rgba(10,10,20,.55);backdrop-filter:blur(3px);display:none;place-items:center;z-index:50;padding:16px}
.overlay.open{display:grid}
.modal{background:var(--surface);width:100%;max-width:460px;border-radius:20px;padding:24px;box-shadow:var(--shadowLg);animation:pop .22s both;border:1px solid var(--border)}
@keyframes pop{from{opacity:0;transform:scale(.96) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}
.modal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
.modal-head h2{font-size:18px;letter-spacing:-.02em}
.close-x{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-size:18px;color:var(--muted)}
.close-x:hover{background:var(--surface2)}
.modal-sub{font-size:13px;color:var(--muted);margin-bottom:18px}
label.f{display:block;font-size:13px;font-weight:700;margin:14px 0 6px}
label.f span{color:#dc2626}
.field{width:100%;border:1.5px solid var(--border);background:var(--input);border-radius:11px;padding:11px 13px;font-size:14px;outline:none;transition:.2s}
.field:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accentSoft)}
.field.error{border-color:#dc2626;box-shadow:0 0 0 3px rgba(220,38,38,.15)}
textarea.field{resize:vertical;min-height:68px}
.err-msg{display:none;color:#dc2626;font-size:12px;font-weight:600;margin-top:5px}
.err-msg.show{display:block}
.seg{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.seg label{margin:0}
.seg input{display:none}
.seg .pill{text-align:center;padding:9px 4px;border-radius:999px;border:1.5px solid var(--border);font-size:13px;font-weight:800;cursor:pointer;transition:.15s;color:var(--muted)}
.seg input:checked + .pill{color:#fff}
.seg .pill.low-c{border-color:#86efac}.seg input:checked + .pill.low-c{background:#16a34a;border-color:#16a34a}
.seg .pill.med-c{border-color:#fcd34d}.seg input:checked + .pill.med-c{background:#d97706;border-color:#d97706;color:#fff}
.seg .pill.high-c{border-color:#fca5a5}.seg input:checked + .pill.high-c{background:#dc2626;border-color:#dc2626}
.modal-actions{display:flex;gap:10px;margin-top:22px}
.btn-cancel{flex:1;height:42px;border-radius:999px;border:1.5px solid var(--border);font-weight:700;font-size:14px}
.btn-cancel:hover{background:var(--surface2)}
.btn-save{flex:1.4;height:42px;border-radius:999px;background:var(--accent);color:#fff;font-weight:800;font-size:14px}
.btn-save:hover{background:var(--accent2)}
/* Toast */
.toast{position:fixed;bottom:20px;left:50%;transform:translate(-50%,80px);background:#161c2d;color:#fff;padding:11px 18px;border-radius:999px;font-size:13px;font-weight:700;box-shadow:var(--shadowLg);z-index:60;transition:.35s cubic-bezier(.2,.9,.3,1.2);display:flex;gap:8px;align-items:center;max-width:calc(100vw - 32px)}
.toast.show{transform:translate(-50%,0)}
footer{max-width:1240px;margin:0 auto;padding:0 20px 20px;width:100%;font-size:12px;color:var(--muted);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
@media(max-width:900px){
.board{grid-template-columns:1fr}
.col{max-height:none;min-height:0}
.col-body{overflow:visible}
.logo p{display:none}
}
@media(max-width:520px){
.header-inner{padding:12px 14px;gap:10px}
.search-wrap{order:3;flex-basis:100%;max-width:none}
.strip,.board{padding-left:14px;padding-right:14px}
.btn-primary{margin-left:auto}
.hint{display:none}
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="logo">
<div class="logo-mark">◧</div>
<div>
<h1>FlowBoard</h1>
<p>Sprint 12 · Product Squad</p>
</div>
</div>
<div class="search-wrap">
<span>⌕</span>
<input id="search" type="search" placeholder="Search cards by title…" aria-label="Search cards">
<button class="search-clear" id="clearSearch" aria-label="Clear search">×</button>
</div>
<button class="theme-btn" id="themeBtn" aria-label="Toggle theme" title="Toggle theme">🌙</button>
<button class="btn-primary" id="openModal">+ Add card</button>
</div>
</header>
<div class="strip">
<div class="strip-inner">
<div><div class="label">Progress</div><div class="big" id="progressText">0 of 0 done</div></div>
<div class="bar"><div class="bar-fill" id="barFill"></div></div>
<div class="hint"><b>Drag</b> cards between columns · <b>Search</b> filters live · <b>×</b> deletes</div>
</div>
</div>
<main class="board" id="board">
<section class="col" data-col="todo">
<div class="col-head"><span class="dot dot-todo"></span><h2>To do</h2><span class="count" id="count-todo">0</span><button class="add-mini" data-add="todo" aria-label="Add to To do">+</button></div>
<div class="col-body" data-body="todo"></div>
</section>
<section class="col" data-col="progress">
<div class="col-head"><span class="dot dot-prog"></span><h2>In progress</h2><span class="count" id="count-progress">0</span><button class="add-mini" data-add="progress" aria-label="Add to In progress">+</button></div>
<div class="col-body" data-body="progress"></div>
</section>
<section class="col" data-col="done">
<div class="col-head"><span class="dot dot-done"></span><h2>Done</h2><span class="count" id="count-done">0</span><button class="add-mini" data-add="done" aria-label="Add to Done">+</button></div>
<div class="col-body" data-body="done"></div>
</section>
</main>
<footer>
<span>Tip: drag a card to the blue line to place it exactly there.</span>
<span id="totalTxt">6 cards total</span>
</footer>
<!-- Modal -->
<div class="overlay" id="overlay" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<form class="modal" id="cardForm" novalidate>
<div class="modal-head"><h2 id="modalTitle">Add a new card</h2><button type="button" class="close-x" id="closeModal" aria-label="Close">×</button></div>
<p class="modal-sub">Fill in the details — title is required.</p>
<label class="f" for="fTitle">Card title <span>*</span></label>
<input class="field" id="fTitle" type="text" placeholder="e.g. Fix login redirect loop" maxlength="90" autocomplete="off">
<div class="err-msg" id="errMsg">Please enter a title to continue.</div>
<label class="f" for="fDesc">Description <span style="color:var(--muted);font-weight:500">(optional)</span></label>
<textarea class="field" id="fDesc" placeholder="Add a short note, acceptance criteria, or link…"></textarea>
<label class="f">Priority</label>
<div class="seg" id="prioritySeg">
<label><input type="radio" name="priority" value="low"><span class="pill low-c">● Low</span></label>
<label><input type="radio" name="priority" value="medium" checked><span class="pill med-c">● Medium</span></label>
<label><input type="radio" name="priority" value="high"><span class="pill high-c">● High</span></label>
</div>
<label class="f" for="fCol">Column</label>
<select class="field" id="fCol">
<option value="todo">To do</option>
<option value="progress">In progress</option>
<option value="done">Done</option>
</select>
<div class="modal-actions">
<button type="button" class="btn-cancel" id="cancelBtn">Cancel</button>
<button type="submit" class="btn-save">Add card →</button>
</div>
</form>
</div>
<div class="toast" id="toast">✓ Done</div>
<script>
(function(){
"use strict";
// ---------- State (in memory only) ----------
let nextId = 7;
const people = {
"AR": "#4f46e5", "JM": "#0e7c66", "SK": "#b45309",
"LT": "#be185d", "NP": "#0369a1", "MD": "#15803d"
};
let cards = [
{id:1, title:"Redesign onboarding wizard", desc:"Simplify 4 steps into 2. Test variants with 5 new users this week.", priority:"high", column:"todo", who:"AR", due:"Jun 18"},
{id:2, title:"Write release notes for v2.4", desc:"Summarize billing, search and dark-mode improvements for the changelog.", priority:"low", column:"todo", who:"JM", due:"Jun 20"},
{id:3, title:"Build search autocomplete", desc:"Debounced API calls, keyboard navigation and highlighting matches.", priority:"medium", column:"progress", who:"SK", due:"Jun 16"},
{id:4, title:"Migrate auth to OAuth2", desc:"Google + GitHub providers, silent session refresh and token rotation.", priority:"high", column:"progress", who:"LT", due:"Jun 17"},
{id:5, title:"Fix mobile nav overflow", desc:"Hamburger menu now closes on route change and respects safe areas.", priority:"medium", column:"done", who:"NP", due:"Jun 12"},
{id:6, title:"Ship dark mode for settings", desc:"Persist preference per user and respect the system setting by default.", priority:"low", column:"done", who:"MD", due:"Jun 10"}
];
const bodies = {
todo: document.querySelector('[data-body="todo"]'),
progress: document.querySelector('[data-body="progress"]'),
done: document.querySelector('[data-body="done"]')
};
const searchInput = document.getElementById('search');
const clearBtn = document.getElementById('clearSearch');
const toast = document.getElementById('toast');
let query = "";
let toastTimer = null;
const AVATAR_COLORS = Object.values(people);
function avatarColor(who){ return people[who] || AVATAR_COLORS[Math.abs(who.charCodeAt(0)+who.charCodeAt(1)) % AVATAR_COLORS.length]; }
function showToast(msg){
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(()=>toast.classList.remove('show'), 2400);
}
// ---------- Render ----------
function matches(c){ return c.title.toLowerCase().includes(query.toLowerCase().trim()); }
function render(){
["todo","progress","done"].forEach(col=>{
const body = bodies[col];
body.innerHTML = "";
const list = cards.filter(c=>c.column===col && matches(c));
document.getElementById('count-'+col).textContent = list.length;
if(list.length===0){
const empty = document.createElement('div');
empty.className = 'empty';
if(query.trim()){
empty.innerHTML = '<span class="e-icon">🔍</span>No cards match “'+escapeHtml(query)+'”<br><button type="button" data-clear="1">Clear search</button>';
} else {
empty.innerHTML = '<span class="e-icon">✨</span>Nothing here yet.<br><button type="button" data-addempty="'+col+'">+ Add the first card</button>';
}
body.appendChild(empty);
return;
}
list.forEach((c,idx)=>{
const el = document.createElement('article');
el.className = 'card';
el.draggable = true;
el.dataset.id = c.id;
el.style.animationDelay = (idx*0.05)+'s';
el.innerHTML =
'<button class="del" data-del="'+c.id+'" aria-label="Delete card" title="Delete card">×</button>'+
'<div class="card-top"><h3>'+escapeHtml(c.title)+'</h3></div>'+
(c.desc ? '<p>'+escapeHtml(c.desc)+'</p>' : '')+
'<div class="card-foot">'+
'<span class="badge p-'+c.priority+'">'+c.priority+'</span>'+
'<span class="meta"><span class="due">📅 '+escapeHtml(c.due||'Soon')+'</span><span class="avatar" style="background:'+avatarColor(c.who)+'">'+escapeHtml(c.who)+'</span></span>'+
'</div>';
attachDrag(el, c.id);
body.appendChild(el);
});
});
const total = cards.length;
const doneCount = cards.filter(c=>c.column==='done').length;
const pct = total ? Math.round(doneCount/total*100) : 0;
document.getElementById('progressText').textContent = doneCount + ' of ' + total + ' done · ' + pct + '%';
document.getElementById('barFill').style.width = pct + '%';
document.getElementById('totalTxt').textContent = total + ' card' + (total===1?'':'s') + ' total';
}
function escapeHtml(s){
return String(s).replace(/[&<>"']/g, m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
}
// Delegated clicks: delete, add-empty, clear search
document.getElementById('board').addEventListener('click', e=>{
const del = e.target.closest('[data-del]');
if(del){
const id = +del.dataset.del;
const card = cards.find(c=>c.id===id);
cards = cards.filter(c=>c.id!==id);
render();
showToast('🗑 Deleted “'+(card?card.title:'card')+'”');
return;
}
const addEmpty = e.target.closest('[data-addempty]');
if(addEmpty){ openModal(addEmpty.dataset.addempty); return; }
const clr = e.target.closest('[data-clear]');
if(clr){ searchInput.value=""; query=""; clearBtn.style.display="none"; render(); searchInput.focus(); }
const addMini = e.target.closest('[data-add]');
if(addMini){ openModal(addMini.dataset.add); }
});
// ---------- Search ----------
searchInput.addEventListener('input', ()=>{
query = searchInput.value;
clearBtn.style.display = query ? "grid" : "none";
render();
});
clearBtn.addEventListener('click', ()=>{
searchInput.value=""; query=""; clearBtn.style.display="none"; render(); searchInput.focus();
});
// ---------- Theme ----------
const themeBtn = document.getElementById('themeBtn');
function syncThemeIcon(){
themeBtn.textContent = document.documentElement.dataset.theme === 'dark' ? '☀️' : '🌙';
}
themeBtn.addEventListener('click', ()=>{
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
syncThemeIcon();
showToast(next==='dark' ? '🌙 Dark theme on' : '☀️ Light theme on');
});
syncThemeIcon();
// ---------- Modal ----------
const overlay = document.getElementById('overlay');
const form = document.getElementById('cardForm');
const fTitle = document.getElementById('fTitle');
const fDesc = document.getElementById('fDesc');
const fCol = document.getElementById('fCol');
const errMsg = document.getElementById('errMsg');
function openModal(col){
if(col) fCol.value = col;
overlay.classList.add('open');
errMsg.classList.remove('show');
fTitle.classList.remove('error');
setTimeout(()=>fTitle.focus(), 50);
}
function closeModal(){ overlay.classList.remove('open'); }
document.getElementById('openModal').addEventListener('click', ()=>openModal('todo'));
document.getElementById('closeModal').addEventListener('click', closeModal);
document.getElementById('cancelBtn').addEventListener('click', closeModal);
overlay.addEventListener('click', e=>{ if(e.target===overlay) closeModal(); });
document.addEventListener('keydown', e=>{ if(e.key==='Escape') closeModal(); });
fTitle.addEventListener('input', ()=>{ fTitle.classList.remove('error'); errMsg.classList.remove('show'); });
form.addEventListener('submit', e=>{
e.preventDefault();
const title = fTitle.value.trim();
if(!title){
fTitle.classList.add('error');
errMsg.classList.add('show');
fTitle.focus();
return;
}
const priority = form.querySelector('input[name="priority"]:checked').value;
const whoKeys = Object.keys(people);
const who = whoKeys[Math.floor(Math.random()*whoKeys.length)];
const du = new Date(); du.setDate(du.getDate()+3);
const due = du.toLocaleDateString('en-US',{month:'short', day:'numeric'});
cards.push({
id: nextId++,
title: title,
desc: fDesc.value.trim(),
priority: priority,
column: fCol.value,
who: who,
due: due
});
// If a search is active and the new card doesn't match, clear it so the card is visible
if(!title.toLowerCase().includes(query.toLowerCase().trim()) && query.trim()){
searchInput.value=""; query=""; clearBtn.style.display="none";
}
render();
form.reset();
form.querySelector('input[name="priority"][value="medium"]').checked = true;
closeModal();
showToast('✓ Added “'+title+'”');
});
// ---------- Drag & Drop with drop indicator ----------
let dragId = null;
let dropInfo = null; // {column, index}
function getDragAfterElement(container, y){
const cardsEls = [...container.querySelectorAll('.card:not(.dragging)')];
return cardsEls.reduce((closest, child)=>{
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height/2;
if(offset < 0 && offset > closest.offset){
return {offset: offset, element: child};
}
return closest;
}, {offset: -Infinity, element: null}).element;
}
function attachDrag(el, id){
el.addEventListener('dragstart', e=>{
dragId = id;
el.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
try{ e.dataTransfer.setData('text/plain', String(id)); }catch(_){}
});
el.addEventListener('dragend', ()=>{
el.classList.remove('dragging');
dragId = null; dropInfo = null;
document.querySelectorAll('.drop-placeholder').forEach(p=>p.remove());
document.querySelectorAll('.col').forEach(c=>c.classList.remove('drag-over'));
});
}
Object.entries(bodies).forEach(([col, body])=>{
body.addEventListener('dragover', e=>{
if(dragId===null) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const after = getDragAfterElement(body, e.clientY);
const existing = body.querySelector('.drop-placeholder');
if(existing) existing.remove();
const ph = document.createElement('div');
ph.className = 'drop-placeholder';
if(after===null) body.appendChild(ph); else body.insertBefore(ph, after);
// index = number of cards before placeholder in this column
let idx = 0;
for(const child of body.children){
if(child===ph) break;
if(child.classList && child.classList.contains('card')) idx++;
}
dropInfo = {column: col, index: idx};
document.querySelectorAll('.col').forEach(c=>c.classList.toggle('drag-over', c.dataset.col===col));
});
body.addEventListener('dragleave', e=>{
if(!body.contains(e.relatedTarget)){
body.querySelector('.drop-placeholder')?.remove();
body.closest('.col').classList.remove('drag-over');
}
});
body.addEventListener('drop', e=>{
e.preventDefault();
if(dragId===null) return;
const targetCol = dropInfo ? dropInfo.column : col;
let targetIdx = dropInfo ? dropInfo.index : 999;
const fromIdx = cards.findIndex(c=>c.id===dragId);
if(fromIdx===-1) return;
const fromCol = cards[fromIdx].column;
const [moved] = cards.splice(fromIdx,1);
if(fromCol===targetCol && fromIdx < targetIdx) targetIdx--;
// Find flat-array insertion point for target column & index
const colPositions = [];
cards.forEach((c,i)=>{ if(c.column===targetCol) colPositions.push(i); });
moved.column = targetCol;
let insertAt;
if(colPositions.length===0) insertAt = cards.length;
else if(targetIdx >= colPositions.length) insertAt = colPositions[colPositions.length-1]+1;
else insertAt = colPositions[targetIdx];
cards.splice(insertAt, 0, moved);
dragId = null; dropInfo = null;
document.querySelectorAll('.drop-placeholder').forEach(p=>p.remove());
document.querySelectorAll('.col').forEach(c=>c.classList.remove('drag-over'));
render();
showToast('Moved “'+moved.title+'” → '+({todo:'To do',progress:'In progress',done:'Done'}[targetCol]));
});
});
render();
})();
</script>
</body>
</html>Reasoning trace (2,541 tokens)
Designing a single-file vanilla HTML/CSS/JS Kanban board with drag-and-drop, search, theme toggle, and responsive layout.Extending the design with drag indicators, search filtering, modal form, theme variables, and empty-state handling.Implementing drag-and-drop with placeholder indicators and live state re-rendering.Extending drag handling with placeholder insertion and index adjustment for same-column moves.