GPT-6.1 Terra
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 23 s
- Cost
- $0.065
- Output tokens
- 12,899
- Spent thinking
- 2,224 tokens
- Speed
- 96.7 tok/s
- Console errors
- 0
- First token after
- 9.5 s
- First visible text after
- 40.0 s
- Prompt tokens
- 493
- Code size
- 33.0 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6.1-terra- Reasoning sent
{"effort":"high"}- Max output
- 128,000 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 · 552 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tandem — Project board</title>
<style>
:root {
color-scheme: light;
--bg: #f7f8f6;
--surface: #ffffff;
--column: #eef0ed;
--text: #232a27;
--muted: #89918a;
--subtle: #aab1aa;
--border: #e8ebe6;
--accent: #e77155;
--accent-soft: #fdf0eb;
--shadow: 0 2px 8px rgba(38, 48, 39, .035), 0 1px 2px rgba(38, 48, 39, .03);
--input: #fff;
}
body[data-theme="dark"] {
color-scheme: dark;
--bg: #191e1c;
--surface: #242b27;
--column: #202723;
--text: #edf1eb;
--muted: #a2ada3;
--subtle: #7e8980;
--border: #354039;
--accent: #f28d73;
--accent-soft: #49332e;
--shadow: 0 2px 8px rgba(0, 0, 0, .15);
--input: #2a322d;
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; min-height: 100%; }
body {
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px;
-webkit-font-smoothing: antialiased;
}
button, input, select { font: inherit; }
button { cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible, .task-card:focus-visible {
outline: 3px solid rgba(231, 113, 85, .4);
outline-offset: 2px;
}
svg { display: block; flex: none; }
.app { min-height: 100vh; display: flex; }
.sidebar {
width: 226px; flex: 0 0 226px; background: var(--surface);
border-right: 1px solid var(--border); padding: 27px 17px 18px;
display: flex; flex-direction: column;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 0 7px; font-size: 21px; font-weight: 800; letter-spacing: -.9px; }
.brand-mark {
width: 32px; height: 32px; border-radius: 10px; background: #e97a5e;
display: grid; place-items: center; color: #fff;
}
.workspace {
margin: 39px 2px 22px; border: 1px solid var(--border); border-radius: 10px;
padding: 10px; display: flex; align-items: center; gap: 9px;
}
.workspace-icon {
width: 32px; height: 32px; border-radius: 8px; background: #e6eee4;
color: #608568; display: grid; place-items: center; font-weight: 800; font-size: 13px;
}
.workspace-info { min-width: 0; flex: 1; }
.workspace-info strong { display: block; font-size: 12px; white-space: nowrap; }
.workspace-info span { display: block; margin-top: 2px; font-size: 10px; color: var(--muted); }
.section-label { color: var(--subtle); font-size: 10px; letter-spacing: 1.1px; font-weight: 750; margin: 17px 10px 10px; }
.nav-item {
height: 39px; display: flex; align-items: center; gap: 11px; padding: 0 11px;
border-radius: 8px; color: var(--muted); font-size: 13px; font-weight: 550;
}
.nav-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 700; }
.nav-item .nav-end { margin-left: auto; color: var(--subtle); font-size: 11px; }
.project-dot { width: 8px; height: 8px; border-radius: 3px; flex: none; margin: 0 4px; }
.sidebar-bottom { margin-top: auto; padding: 20px 9px 0; }
.sidebar-note { border-radius: 10px; background: var(--bg); padding: 14px; }
.sidebar-note .note-icon { color: var(--accent); margin-bottom: 10px; }
.sidebar-note strong { display: block; font-size: 12px; }
.sidebar-note p { color: var(--muted); font-size: 11px; line-height: 1.5; margin: 5px 0 0; }
.main { flex: 1; min-width: 0; }
.topbar {
height: 65px; border-bottom: 1px solid var(--border); background: var(--surface);
display: flex; align-items: center; justify-content: space-between; padding: 0 39px;
}
.crumbs { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 12px; }
.crumbs strong { color: var(--text); font-weight: 650; }
.top-actions { display: flex; align-items: center; gap: 17px; }
.icon-button {
border: 0; background: transparent; color: var(--muted); padding: 7px;
display: inline-grid; place-items: center; border-radius: 7px;
}
.icon-button:hover { background: var(--column); color: var(--text); }
.top-divider { width: 1px; height: 25px; background: var(--border); }
.user { display: flex; align-items: center; gap: 9px; font-size: 12px; font-weight: 650; }
.avatar {
border-radius: 50%; width: 27px; height: 27px; display: inline-grid; place-items: center;
font-size: 9px; font-weight: 800; color: #805543; background: #f1d6c5; border: 2px solid var(--surface);
}
.user .avatar { width: 32px; height: 32px; font-size: 10px; border: 0; }
.content { max-width: 1500px; margin: 0 auto; padding: 33px 39px 40px; }
.eyebrow { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 10px; letter-spacing: 1.15px; font-weight: 750; text-transform: uppercase; }
.eyebrow .tiny-dot { width: 4px; height: 4px; border-radius: 50%; background: #c8cec7; }
.hero { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin: 10px 0 25px; }
.title-line { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
h1 { font-size: 29px; letter-spacing: -1.15px; line-height: 1.2; margin: 0; font-weight: 750; }
.status-pill { display: inline-flex; align-items: center; gap: 6px; background: #e7f2e6; color: #59855d; border-radius: 20px; padding: 5px 9px; font-size: 10px; font-weight: 700; }
.status-pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #74ae78; }
.hero p { color: var(--muted); margin: 8px 0 0; font-size: 13px; }
.primary-button {
background: #e67458; color: white; border: 1px solid #e67458; border-radius: 9px;
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
padding: 11px 15px; font-size: 12px; font-weight: 700; white-space: nowrap;
box-shadow: 0 3px 8px rgba(202, 101, 75, .14);
}
.primary-button:hover { background: #d9654a; }
.overview {
background: var(--surface); border: 1px solid var(--border); border-radius: 11px;
padding: 14px 18px; display: flex; align-items: center; gap: 22px; box-shadow: var(--shadow);
}
.overview-label { display: flex; align-items: center; gap: 10px; min-width: 156px; }
.overview-icon { width: 34px; height: 34px; border-radius: 9px; background: #fdf0eb; color: #e2765a; display: grid; place-items: center; }
.overview-label strong { display: block; font-size: 12px; }
.overview-label span { display: block; color: var(--muted); font-size: 11px; margin-top: 3px; }
.overview-divider { height: 31px; width: 1px; background: var(--border); }
.progress-area { flex: 1; max-width: 350px; }
.progress-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; font-size: 11px; font-weight: 650; }
.progress-top span:last-child { color: var(--muted); font-weight: 500; }
.progress-track { background: var(--column); height: 5px; border-radius: 10px; margin-top: 8px; overflow: hidden; }
.progress-fill { height: 100%; border-radius: inherit; background: #e98267; transition: width .25s; }
.overview-team { margin-left: auto; display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 11px; }
.avatar-stack { display: flex; padding-left: 5px; }
.avatar-stack .avatar { margin-left: -5px; }
.avatar.a2 { background: #dce8d4; color: #5b7a5c; }
.avatar.a3 { background: #e3ddf1; color: #716092; }
.board-toolbar { display: flex; align-items: center; gap: 15px; margin: 29px 0 16px; }
.board-heading { display: flex; align-items: baseline; gap: 9px; margin-right: auto; }
.board-heading h2 { font-size: 16px; margin: 0; letter-spacing: -.3px; }
.board-heading span { color: var(--muted); font-size: 11px; }
.search {
width: 230px; height: 37px; background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; color: var(--muted); display: flex; align-items: center; gap: 8px; padding: 0 11px;
}
.search:focus-within { border-color: #e6a08e; }
.search input { background: transparent; border: 0; outline: 0; color: var(--text); width: 100%; min-width: 0; font-size: 12px; }
.search input::placeholder { color: var(--subtle); }
.board-view { display: flex; gap: 7px; align-items: center; color: var(--text); font-size: 11px; font-weight: 650; padding: 10px 12px; border: 1px solid var(--border); background: var(--surface); border-radius: 8px; white-space: nowrap; }
.board { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 15px; align-items: stretch; }
.column { background: var(--column); border-radius: 12px; padding: 14px 12px 12px; min-width: 0; min-height: 400px; transition: background .15s, box-shadow .15s; }
.column.is-over { box-shadow: inset 0 0 0 2px rgba(231, 113, 85, .45); }
.column-header { display: flex; align-items: center; gap: 8px; padding: 1px 2px 13px; }
.column-dot { width: 9px; height: 9px; border-radius: 50%; }
.column-title { font-size: 13px; font-weight: 750; }
.count { font-size: 10px; font-weight: 700; color: var(--muted); background: var(--surface); border-radius: 5px; padding: 3px 6px; }
.column-add { margin-left: auto; padding: 4px; color: var(--muted); }
.cards-list { display: flex; flex-direction: column; gap: 10px; min-height: 45px; }
.task-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 9px;
padding: 13px 14px 12px; box-shadow: var(--shadow); cursor: grab; min-width: 0;
}
.task-card:active { cursor: grabbing; }
.task-card.dragging { opacity: .35; }
.card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.category { color: #83918a; font-size: 9px; text-transform: uppercase; letter-spacing: .75px; font-weight: 750; }
.delete-card { color: var(--subtle); padding: 3px; margin: -3px; }
.delete-card:hover { color: #db705d; background: var(--accent-soft); }
.task-card h3 { margin: 10px 0 5px; font-size: 13px; line-height: 1.35; letter-spacing: -.15px; font-weight: 700; overflow-wrap: anywhere; }
.description { color: var(--muted); font-size: 11px; line-height: 1.45; margin: 0; min-height: 16px; }
.card-bottom { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 12px; display: flex; align-items: center; gap: 8px; }
.priority { display: inline-flex; align-items: center; gap: 5px; font-size: 10px; font-weight: 700; padding: 5px 7px; border-radius: 5px; white-space: nowrap; }
.priority::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.priority.high { color: #cf705c; background: #fceeea; }
.priority.medium { color: #b58a48; background: #fbf3e3; }
.priority.low { color: #6c9673; background: #eaf4e9; }
.date { margin-left: auto; display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--muted); white-space: nowrap; }
.card-avatar { width: 23px; height: 23px; font-size: 8px; border: 0; }
.add-column {
width: 100%; border: 0; background: transparent; color: var(--muted); display: flex;
align-items: center; gap: 7px; border-radius: 7px; padding: 11px 5px 3px; font-size: 11px; font-weight: 600; text-align: left;
}
.add-column:hover { color: var(--accent); }
.drop-indicator { height: 6px; background: var(--accent); border-radius: 8px; flex: none; box-shadow: 0 0 0 3px var(--accent-soft); }
.empty-message { padding: 18px 8px; text-align: center; font-size: 11px; line-height: 1.5; color: var(--muted); }
.board-hint { color: var(--subtle); font-size: 11px; margin: 14px 2px 0; display: flex; align-items: center; gap: 6px; }
.modal-backdrop { position: fixed; inset: 0; background: rgba(24, 31, 26, .55); display: grid; place-items: center; z-index: 10; padding: 18px; }
.modal { width: min(100%, 440px); background: var(--surface); border: 1px solid var(--border); border-radius: 15px; box-shadow: 0 20px 65px rgba(0,0,0,.2); padding: 24px; }
.modal-header { display: flex; justify-content: space-between; gap: 15px; align-items: start; }
.modal-header h2 { margin: 0; font-size: 19px; letter-spacing: -.5px; }
.modal-header p { color: var(--muted); font-size: 12px; margin: 6px 0 0; }
.form-field { display: block; margin-top: 20px; }
.form-field > span { display: block; font-size: 12px; font-weight: 700; margin-bottom: 8px; }
.required { color: var(--accent); }
.form-field input, .form-field select {
width: 100%; height: 41px; border: 1px solid var(--border); border-radius: 8px;
background: var(--input); color: var(--text); padding: 0 11px; font-size: 13px;
}
.form-field input::placeholder { color: var(--subtle); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 9px; margin-top: 25px; }
.secondary-button { border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: 8px; padding: 10px 14px; font-size: 12px; font-weight: 650; }
.secondary-button:hover { background: var(--column); }
@media (max-width: 1000px) {
.sidebar { width: 68px; flex-basis: 68px; padding: 25px 10px; align-items: center; }
.brand { padding: 0; }
.brand-name, .workspace, .section-label, .nav-item span, .nav-item .nav-end, .sidebar-bottom { display: none; }
.nav { margin-top: 42px; width: 100%; }
.nav-item { justify-content: center; padding: 0; margin-bottom: 4px; }
.projects-nav { margin-top: 25px; }
.project-dot { margin: 0; }
.topbar { padding: 0 25px; }
.content { padding-left: 25px; padding-right: 25px; }
.overview-team { display: none; }
}
@media (max-width: 700px) {
.sidebar { display: none; }
.topbar { height: 57px; padding: 0 18px; }
.user-name, .top-divider { display: none; }
.top-actions { gap: 8px; }
.content { padding: 25px 16px 35px; }
h1 { font-size: 26px; }
.hero { align-items: flex-start; flex-direction: column; margin-bottom: 20px; }
.hero .primary-button { width: 100%; }
.overview { gap: 12px; padding: 12px; }
.overview-label { min-width: 0; }
.overview-label strong { font-size: 11px; }
.overview-label span { font-size: 10px; }
.overview-icon { width: 30px; height: 30px; }
.progress-top { font-size: 10px; }
.board-toolbar { flex-wrap: wrap; gap: 10px; margin-top: 25px; }
.board-heading { width: 100%; }
.search { flex: 1; width: auto; }
.board { grid-template-columns: 1fr; gap: 12px; }
.column { min-height: 0; }
.board-hint { display: none; }
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar" aria-label="Workspace sidebar">
<div class="brand">
<div class="brand-mark">
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 6.5h8.5v4H5zm0 7h14v4H5z" fill="currentColor" opacity=".95"/><path d="M16.5 6.5H19v4h-2.5z" fill="currentColor" opacity=".65"/></svg>
</div>
<span class="brand-name">tandem<span style="color:#e67458">.</span></span>
</div>
<div class="workspace">
<div class="workspace-icon">S</div>
<div class="workspace-info"><strong>Studio space</strong><span>Free workspace</span></div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" stroke-width="1.8" aria-hidden="true"><path d="m8 10 4 4 4-4"/></svg>
</div>
<div class="nav">
<div class="section-label">WORKSPACE</div>
<div class="nav-item">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg><span>Overview</span>
</div>
<div class="nav-item active">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="4" width="5" height="16" rx="1.3"/><rect x="10" y="4" width="5" height="10" rx="1.3"/><rect x="17" y="4" width="4" height="13" rx="1.3"/></svg><span>Board</span><span class="nav-end">●</span>
</div>
<div class="nav-item">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M7 3v4m10-4v4M3 10h18"/></svg><span>Calendar</span>
</div>
<div class="nav-item">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="9" cy="8" r="3"/><path d="M3 20v-1a6 6 0 0 1 12 0v1M16 5a3 3 0 0 1 0 6m1 3a5 5 0 0 1 4 5v1"/></svg><span>People</span>
</div>
<div class="projects-nav">
<div class="section-label" style="margin-top:30px">PROJECTS</div>
<div class="nav-item" style="color:var(--text)"><i class="project-dot" style="background:#e9876c"></i><span>Product launch</span></div>
<div class="nav-item"><i class="project-dot" style="background:#9bb7a0"></i><span>Website refresh</span></div>
</div>
</div>
<div class="sidebar-bottom">
<div class="sidebar-note">
<div class="note-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><path d="m12 3 1.7 5.3L19 10l-5.3 1.7L12 17l-1.7-5.3L5 10l5.3-1.7L12 3Zm6 11 .8 2.2L21 17l-2.2.8L18 20l-.8-2.2L15 17l2.2-.8L18 14Z"/></svg></div>
<strong>Making good things</strong>
<p>Great work starts with a little organization.</p>
</div>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span>Workspace</span><span style="color:var(--subtle)">/</span><strong>Product launch</strong></div>
<div class="top-actions">
<button class="icon-button" id="themeToggle" type="button" aria-label="Switch to dark theme" title="Switch theme"></button>
<div class="top-divider"></div>
<div class="user"><span class="avatar">AM</span><span class="user-name">Alex Morgan</span></div>
</div>
</header>
<div class="content">
<div class="eyebrow"><span>Projects</span><span class="tiny-dot"></span><span>Product launch</span></div>
<section class="hero">
<div>
<div class="title-line"><h1>Product launch</h1><span class="status-pill">On track</span></div>
<p>Good ideas happen when we work together.</p>
</div>
<button class="primary-button open-modal" type="button" data-column="todo">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
Add task
</button>
</section>
<section class="overview" aria-label="Project progress">
<div class="overview-label">
<div class="overview-icon"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M4 19V5m0 14h16M8 15l3-4 3 2 5-6"/></svg></div>
<div><strong>Project progress</strong><span>Keep up the great work</span></div>
</div>
<div class="overview-divider"></div>
<div class="progress-area">
<div class="progress-top"><span id="progressPercent">33% complete</span><span id="progressText">2 of 6 tasks</span></div>
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
</div>
<div class="overview-team"><span>Team members</span><div class="avatar-stack"><span class="avatar">AM</span><span class="avatar a2">KL</span><span class="avatar a3">MR</span></div></div>
</section>
<div class="board-toolbar">
<div class="board-heading"><h2>Board</h2><span id="totalCount">6 tasks</span></div>
<label class="search">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m16 16 4 4"/></svg>
<input id="search" type="search" placeholder="Search tasks..." aria-label="Search tasks">
</label>
<div class="board-view"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3" y="4" width="5" height="16" rx="1"/><rect x="10" y="4" width="5" height="10" rx="1"/><rect x="17" y="4" width="4" height="13" rx="1"/></svg> Board view</div>
</div>
<div class="board" id="board" aria-label="Kanban board"></div>
<p class="board-hint"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><path d="M7 8V5a2 2 0 0 1 4 0v5-3a2 2 0 1 1 4 0v3-1a2 2 0 1 1 4 0v5c0 4-3 7-7 7h-1c-2 0-4-1-5-3l-2-4a2 2 0 0 1 3-2l1 1V8Z"/></svg> Drag and drop tasks to move them between columns</p>
</div>
</main>
</div>
<div class="modal-backdrop" id="modalBackdrop" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDescription">
<div class="modal-header">
<div><h2 id="modalTitle">Create a task</h2><p id="modalDescription">Add something new to your board.</p></div>
<button class="icon-button" id="closeModal" type="button" aria-label="Close dialog"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</div>
<form id="taskForm">
<label class="form-field"><span>Task title <b class="required">*</b></span><input id="taskTitle" name="title" type="text" placeholder="What needs to be done?" maxlength="100" required></label>
<div class="form-row">
<label class="form-field"><span>Priority</span><select id="taskPriority" name="priority"><option value="low">Low</option><option value="medium" selected>Medium</option><option value="high">High</option></select></label>
<label class="form-field"><span>Column</span><select id="taskColumn" name="column"><option value="todo">To do</option><option value="progress">In progress</option><option value="done">Done</option></select></label>
</div>
<div class="modal-actions"><button class="secondary-button" id="cancelModal" type="button">Cancel</button><button class="primary-button" type="submit">Create task</button></div>
</form>
</section>
</div>
<script>
(function () {
const columns = [
{ id: 'todo', title: 'To do', color: '#e79577' },
{ id: 'progress', title: 'In progress', color: '#d3ac6b' },
{ id: 'done', title: 'Done', color: '#86ae91' }
];
const tasks = {
todo: [
{ id: '1', title: 'Write launch announcement', description: 'Draft the story for our community.', priority: 'high', category: 'Content', date: 'Jun 18', initials: 'KL', avatar: 'a2' },
{ id: '2', title: 'Prepare press kit', description: 'Collect brand assets and product shots.', priority: 'medium', category: 'Marketing', date: 'Jun 21', initials: 'MR', avatar: 'a3' }
],
progress: [
{ id: '3', title: 'Design landing page', description: 'Bring the new product story to life.', priority: 'high', category: 'Design', date: 'Jun 16', initials: 'AM', avatar: '' },
{ id: '4', title: 'Plan social campaign', description: 'Map out posts for launch week.', priority: 'medium', category: 'Marketing', date: 'Jun 19', initials: 'KL', avatar: 'a2' }
],
done: [
{ id: '5', title: 'Define project brief', description: 'Align on goals, scope, and timeline.', priority: 'low', category: 'Planning', date: 'Jun 10', initials: 'AM', avatar: '' },
{ id: '6', title: 'Finalize visual identity', description: 'Refine the colors, type, and logo.', priority: 'medium', category: 'Design', date: 'Jun 12', initials: 'MR', avatar: 'a3' }
]
};
let nextId = 7;
let draggedId = null;
let lastFocused = null;
const board = document.getElementById('board');
const search = document.getElementById('search');
const modal = document.getElementById('modalBackdrop');
const form = document.getElementById('taskForm');
const titleInput = document.getElementById('taskTitle');
const columnInput = document.getElementById('taskColumn');
const themeToggle = document.getElementById('themeToggle');
const placeholder = document.createElement('div');
placeholder.className = 'drop-indicator';
placeholder.setAttribute('aria-hidden', 'true');
const plusIcon = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>';
const trashIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M4 7h16M10 11v6m4-6v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg>';
const calendarIcon = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M7 3v4m10-4v4M3 10h18"/></svg>';
function escapeHTML(value) {
return String(value).replace(/[&<>"']/g, function (char) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char];
});
}
function cardHTML(task) {
const title = escapeHTML(task.title);
return '<article class="task-card" draggable="true" data-id="' + task.id + '" aria-label="' + title + ', ' + task.priority + ' priority">' +
'<div class="card-top"><span class="category">' + escapeHTML(task.category) + '</span><button class="icon-button delete-card" type="button" data-delete="' + task.id + '" aria-label="Delete ' + title + '" title="Delete task">' + trashIcon + '</button></div>' +
'<h3>' + title + '</h3><p class="description">' + escapeHTML(task.description) + '</p>' +
'<div class="card-bottom"><span class="priority ' + task.priority + '">' + task.priority.charAt(0).toUpperCase() + task.priority.slice(1) + '</span>' +
'<span class="date">' + calendarIcon + escapeHTML(task.date) + '</span><span class="avatar card-avatar ' + task.avatar + '" title="Assigned to ' + escapeHTML(task.initials) + '">' + escapeHTML(task.initials) + '</span></div></article>';
}
function render() {
const query = search.value.trim().toLowerCase();
board.innerHTML = columns.map(function (column) {
const filtered = tasks[column.id].filter(function (task) { return task.title.toLowerCase().includes(query); });
const empty = query ? 'No matching tasks' : 'No tasks here yet';
return '<section class="column" data-column="' + column.id + '" aria-label="' + column.title + '">' +
'<div class="column-header"><span class="column-dot" style="background:' + column.color + '"></span><span class="column-title">' + column.title + '</span><span class="count">' + filtered.length + '</span>' +
'<button class="icon-button column-add open-modal" data-column="' + column.id + '" type="button" aria-label="Add task to ' + column.title + '">' + plusIcon + '</button></div>' +
'<div class="cards-list">' + (filtered.length ? filtered.map(cardHTML).join('') : '<div class="empty-message">' + empty + '</div>') + '</div>' +
'<button class="add-column open-modal" data-column="' + column.id + '" type="button">' + plusIcon + ' Add task</button></section>';
}).join('');
const total = columns.reduce(function (sum, column) { return sum + tasks[column.id].length; }, 0);
const done = tasks.done.length;
const percent = total ? Math.round(done / total * 100) : 0;
document.getElementById('totalCount').textContent = total + (total === 1 ? ' task' : ' tasks');
document.getElementById('progressPercent').textContent = percent + '% complete';
document.getElementById('progressText').textContent = done + ' of ' + total + ' tasks';
document.getElementById('progressFill').style.width = percent + '%';
}
function openModal(column) {
lastFocused = document.activeElement;
form.reset();
columnInput.value = column || 'todo';
modal.hidden = false;
setTimeout(function () { titleInput.focus(); }, 0);
}
function closeModal() {
modal.hidden = true;
if (lastFocused && document.contains(lastFocused)) lastFocused.focus();
}
document.addEventListener('click', function (event) {
const open = event.target.closest('.open-modal');
if (open) { openModal(open.dataset.column); return; }
const remove = event.target.closest('[data-delete]');
if (remove) {
const id = remove.dataset.delete;
columns.forEach(function (column) {
const index = tasks[column.id].findIndex(function (task) { return task.id === id; });
if (index !== -1) tasks[column.id].splice(index, 1);
});
render();
}
});
document.getElementById('closeModal').addEventListener('click', closeModal);
document.getElementById('cancelModal').addEventListener('click', closeModal);
modal.addEventListener('click', function (event) { if (event.target === modal) closeModal(); });
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && !modal.hidden) closeModal();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
const title = titleInput.value.trim();
if (!title) {
titleInput.setCustomValidity('Please enter a task title.');
titleInput.reportValidity();
return;
}
titleInput.setCustomValidity('');
tasks[columnInput.value].push({
id: String(nextId++), title: title, description: 'Ready for the next step.',
priority: document.getElementById('taskPriority').value, category: 'New task',
date: 'No date', initials: 'AM', avatar: ''
});
search.value = '';
closeModal();
render();
});
titleInput.addEventListener('input', function () { titleInput.setCustomValidity(''); });
search.addEventListener('input', render);
board.addEventListener('dragstart', function (event) {
const card = event.target.closest('.task-card');
if (!card) return;
draggedId = card.dataset.id;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', draggedId);
requestAnimationFrame(function () { card.classList.add('dragging'); });
});
board.addEventListener('dragover', function (event) {
if (!draggedId) return;
const column = event.target.closest('.column');
if (!column) return;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
board.querySelectorAll('.column.is-over').forEach(function (el) { if (el !== column) el.classList.remove('is-over'); });
column.classList.add('is-over');
const list = column.querySelector('.cards-list');
const cards = Array.from(list.querySelectorAll('.task-card:not(.dragging)')).filter(function (card) { return card.dataset.id !== draggedId; });
const before = cards.find(function (card) {
const rect = card.getBoundingClientRect();
return event.clientY < rect.top + rect.height / 2;
});
list.insertBefore(placeholder, before || null);
});
board.addEventListener('drop', function (event) {
const column = event.target.closest('.column');
if (!column || !draggedId) return;
event.preventDefault();
const targetId = column.dataset.column;
const list = column.querySelector('.cards-list');
if (placeholder.parentElement !== list) list.appendChild(placeholder);
let next = placeholder.nextElementSibling;
while (next && (!next.classList.contains('task-card') || next.dataset.id === draggedId)) next = next.nextElementSibling;
const nextIdInList = next ? next.dataset.id : null;
let moved = null;
columns.forEach(function (item) {
const index = tasks[item.id].findIndex(function (task) { return task.id === draggedId; });
if (index !== -1) moved = tasks[item.id].splice(index, 1)[0];
});
if (moved) {
const target = tasks[targetId];
const index = nextIdInList ? target.findIndex(function (task) { return task.id === nextIdInList; }) : -1;
target.splice(index < 0 ? target.length : index, 0, moved);
}
draggedId = null;
placeholder.remove();
render();
});
board.addEventListener('dragend', function () {
draggedId = null;
placeholder.remove();
board.querySelectorAll('.dragging').forEach(function (el) { el.classList.remove('dragging'); });
board.querySelectorAll('.is-over').forEach(function (el) { el.classList.remove('is-over'); });
});
function updateThemeIcon() {
const dark = document.body.dataset.theme === 'dark';
themeToggle.setAttribute('aria-label', dark ? 'Switch to light theme' : 'Switch to dark theme');
themeToggle.innerHTML = dark
? '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>'
: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><path d="M20.4 14.5A8.5 8.5 0 0 1 9.5 3.6 8.5 8.5 0 1 0 20.4 14.5Z"/></svg>';
}
themeToggle.addEventListener('click', function () {
document.body.dataset.theme = document.body.dataset.theme === 'dark' ? 'light' : 'dark';
updateThemeIcon();
});
updateThemeIcon();
render();
})();
</script>
</body>
</html>Reasoning trace (2,224 tokens)
**Figuring out drag-and-drop logic** I need to remove the placeholder and determine the new position based on the number of cards before it, excluding the dragged element. I’ll update both source and target arrays, focusing on the data model as array objects with the correct properties. When dropping, I'll find the source column and index, splice the source, and handle the target insertion based on visible tasks while preserving the hidden state. So, there’s a lot to manage here!**Designing a task board layout** I’m thinking about how to design a task board with specific features. Each column will have a small colored dot to represent status, using different colors like coral, purple, and green for clarity. I want to ensure the background isn't harsh, avoiding large shadows. Including controls for filtering and displaying search counts is essential, plus a button to "Add task." I also need to keep track of totals, progress, and stats effectively in memory.**Updating UI Tasks** I'm working on updating the user interface, ensuring consistency even without cards or search. I need to add an extra board toolbar and implement light/dark themes using CSS variables. I'll ensure the theme switch functions properly, and code needs to be mobile-friendly too. I'll hide the sidebar on screens under 800px and adjust padding for various layouts. I have to make sure there are no accidental JavaScript syntax errors, especially with user-safe task creation. Let's get coding!**Implementing Task Features** I'm focusing on the priority settings using CSS labels for high, medium, and low priorities, and I'll add counts and generic descriptions for added tasks. For dates, I'll use "No due date" and tags will say "New task." I'm planning to use dynamic top overviews with delegated event handlers for interactions. I need to implement modal fields with native validation and consider autofocus to enhance usability. Also, I'll set up drag-and-drop initialization with proper event listeners on containers, ensuring smooth interactions for users.**Handling Drag-and-Drop Events** I need to make sure to use `e.preventDefault()` only when there’s a `dragId` present, and I should set the drop effect accordingly. It’s crucial to check that the placeholder is correctly placed in the list during the drop event. If not, the drop process should end. I’ll compute the next card after the placeholder while ensuring I skip the dragged node. I have to carefully manage element references to avoid errors when removing and inserting tasks in the right position.