MiMo-V2.6-Pro
by Xiaomireasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 31 s
- Cost
- $0.012
- Output tokens
- 14,569
- Spent thinking
- 3,344 tokens
- Speed
- 161.1 tok/s
- Console errors
- 0
- First token after
- 635 ms
- First visible text after
- 37.4 s
- Prompt tokens
- 496
- Code size
- 38.0 KB
- Finish reason
- stop
- Served by
- DeepInfra
- Model id
xiaomi/mimo-v2.6-pro- Reasoning sent
model default- Max output
- 131,072 tokens
- Generated
- Nov 13, 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 · 1402 lines
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Flowboard — Kanban Board</title>
<style>
:root {
--bg: #0b1020;
--bg-glow-1: rgba(110, 95, 255, 0.18);
--bg-glow-2: rgba(37, 211, 167, 0.11);
--surface: rgba(19, 26, 45, 0.82);
--surface-solid: #141c30;
--surface-2: rgba(30, 40, 65, 0.72);
--card: rgba(27, 36, 59, 0.95);
--card-hover: rgba(36, 47, 76, 0.98);
--border: rgba(159, 178, 221, 0.14);
--border-strong: rgba(159, 178, 221, 0.24);
--text: #edf2ff;
--muted: #94a1bb;
--muted-2: #6f7c96;
--accent: #8b7cff;
--accent-strong: #6e5df5;
--accent-soft: rgba(139, 124, 255, 0.16);
--green: #31d6a4;
--green-soft: rgba(49, 214, 164, 0.15);
--yellow: #f5c451;
--yellow-soft: rgba(245, 196, 81, 0.15);
--red: #ff7189;
--red-soft: rgba(255, 113, 137, 0.15);
--shadow: 0 20px 55px rgba(2, 5, 18, 0.38);
--shadow-soft: 0 8px 24px rgba(2, 5, 18, 0.22);
--radius-lg: 22px;
--radius-md: 15px;
--radius-sm: 10px;
}
[data-theme="light"] {
--bg: #edf1f8;
--bg-glow-1: rgba(110, 95, 255, 0.14);
--bg-glow-2: rgba(37, 211, 167, 0.12);
--surface: rgba(255, 255, 255, 0.82);
--surface-solid: #ffffff;
--surface-2: rgba(245, 247, 252, 0.9);
--card: rgba(255, 255, 255, 0.96);
--card-hover: #ffffff;
--border: rgba(46, 63, 100, 0.12);
--border-strong: rgba(46, 63, 100, 0.22);
--text: #172238;
--muted: #66738d;
--muted-2: #8c98ad;
--accent: #6557e8;
--accent-strong: #5546dc;
--accent-soft: rgba(101, 87, 232, 0.12);
--green: #159b73;
--green-soft: rgba(21, 155, 115, 0.12);
--yellow: #b47a08;
--yellow-soft: rgba(222, 161, 28, 0.15);
--red: #d84c68;
--red-soft: rgba(216, 76, 104, 0.12);
--shadow: 0 22px 55px rgba(54, 70, 111, 0.15);
--shadow-soft: 0 8px 22px rgba(54, 70, 111, 0.1);
}
* {
box-sizing: border-box;
}
html {
min-height: 100%;
background: var(--bg);
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
overflow-x: hidden;
color: var(--text);
background:
radial-gradient(circle at 8% 0%, var(--bg-glow-1), transparent 31rem),
radial-gradient(circle at 92% 10%, var(--bg-glow-2), transparent 28rem),
var(--bg);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
transition: background 240ms ease, color 240ms ease;
}
button,
input,
select {
font: inherit;
}
button {
cursor: pointer;
}
.app-shell {
width: min(1480px, 100%);
margin: 0 auto;
padding: 28px clamp(16px, 3.5vw, 52px) 38px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-bottom: 34px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.logo {
display: grid;
place-items: center;
width: 48px;
height: 48px;
flex: 0 0 48px;
border: 1px solid rgba(160, 146, 255, 0.38);
border-radius: 15px;
color: #fff;
background: linear-gradient(145deg, #8c7dff, #5b49df);
box-shadow: 0 12px 26px rgba(92, 73, 223, 0.32);
}
.logo svg {
width: 25px;
height: 25px;
}
.brand-copy {
min-width: 0;
}
.brand-copy h1 {
margin: 0;
font-size: clamp(1.25rem, 2.2vw, 1.65rem);
letter-spacing: -0.035em;
line-height: 1.1;
}
.brand-copy p {
margin: 5px 0 0;
color: var(--muted);
font-size: 0.82rem;
letter-spacing: 0.01em;
}
.toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
min-width: 0;
}
.search-box {
position: relative;
display: flex;
align-items: center;
width: min(285px, 31vw);
min-width: 180px;
}
.search-box > svg {
position: absolute;
left: 13px;
width: 17px;
height: 17px;
color: var(--muted);
pointer-events: none;
}
.search-box input {
width: 100%;
height: 43px;
padding: 0 36px 0 39px;
border: 1px solid var(--border);
border-radius: 12px;
outline: none;
color: var(--text);
background: var(--surface);
transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease;
}
.search-box input::placeholder {
color: var(--muted-2);
}
.search-box input:focus {
border-color: rgba(139, 124, 255, 0.7);
box-shadow: 0 0 0 4px var(--accent-soft);
}
.clear-search {
position: absolute;
right: 7px;
display: none;
width: 28px;
height: 28px;
padding: 0;
border: 0;
border-radius: 8px;
color: var(--muted);
background: transparent;
}
.clear-search:hover {
color: var(--text);
background: var(--accent-soft);
}
.clear-search.visible {
display: grid;
place-items: center;
}
.icon-button,
.primary-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 43px;
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text);
background: var(--surface);
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
}
.icon-button {
padding: 0 13px;
white-space: nowrap;
}
.icon-button:hover,
.primary-button:hover {
transform: translateY(-1px);
border-color: var(--border-strong);
box-shadow: var(--shadow-soft);
}
.icon-button svg {
width: 17px;
height: 17px;
}
.primary-button {
padding: 0 17px;
border-color: transparent;
color: #fff;
font-weight: 700;
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
box-shadow: 0 10px 22px rgba(94, 76, 226, 0.25);
}
.primary-button:hover {
border-color: transparent;
box-shadow: 0 14px 28px rgba(94, 76, 226, 0.38);
}
.primary-button svg {
width: 17px;
height: 17px;
}
.board-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
margin-bottom: 17px;
}
.board-heading h2 {
margin: 0;
font-size: clamp(1.15rem, 2.5vw, 1.55rem);
letter-spacing: -0.035em;
}
.board-heading p {
margin: 7px 0 0;
color: var(--muted);
font-size: 0.87rem;
}
.board-status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted);
background: var(--surface);
font-size: 0.78rem;
white-space: nowrap;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 0 4px var(--green-soft);
}
.board {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: start;
gap: 18px;
}
.column {
min-width: 0;
padding: 15px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--surface);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(14px);
}
.column[data-column="todo"] {
--column-accent: var(--red);
--column-soft: var(--red-soft);
}
.column[data-column="doing"] {
--column-accent: var(--yellow);
--column-soft: var(--yellow-soft);
}
.column[data-column="done"] {
--column-accent: var(--green);
--column-soft: var(--green-soft);
}
.column-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 3px 3px 15px;
}
.column-title {
display: flex;
align-items: center;
gap: 9px;
min-width: 0;
}
.column-title h3 {
overflow: hidden;
margin: 0;
font-size: 0.96rem;
letter-spacing: -0.015em;
text-overflow: ellipsis;
white-space: nowrap;
}
.column-marker {
width: 9px;
height: 9px;
flex: 0 0 9px;
border-radius: 50%;
background: var(--column-accent);
box-shadow: 0 0 0 4px var(--column-soft);
}
.count {
display: inline-grid;
place-items: center;
min-width: 28px;
height: 25px;
padding: 0 8px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted);
background: var(--surface-2);
font-size: 0.76rem;
font-weight: 700;
}
.cards {
display: flex;
flex-direction: column;
gap: 11px;
min-height: 218px;
padding: 2px;
border-radius: 14px;
transition: background 180ms ease, box-shadow 180ms ease;
}
.cards.drag-over {
background: var(--column-soft);
box-shadow: inset 0 0 0 1px var(--column-accent);
}
.card {
position: relative;
padding: 14px 14px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--card);
box-shadow: 0 8px 18px rgba(4, 8, 22, 0.12);
cursor: grab;
user-select: none;
transition: transform 170ms ease, border-color 170ms ease, background 170ms ease, box-shadow 170ms ease, opacity 170ms ease;
}
[data-theme="light"] .card {
box-shadow: 0 7px 18px rgba(64, 80, 120, 0.08);
}
.card:hover {
transform: translateY(-2px);
border-color: var(--border-strong);
background: var(--card-hover);
box-shadow: 0 13px 25px rgba(4, 8, 22, 0.2);
}
.card:active {
cursor: grabbing;
}
.card.is-dragging {
opacity: 0.32;
transform: rotate(1deg) scale(0.99);
}
.card-top,
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.priority {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 9px;
border-radius: 999px;
font-size: 0.69rem;
font-weight: 800;
letter-spacing: 0.045em;
text-transform: uppercase;
}
.priority-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.priority.low {
color: var(--green);
background: var(--green-soft);
}
.priority.medium {
color: var(--yellow);
background: var(--yellow-soft);
}
.priority.high {
color: var(--red);
background: var(--red-soft);
}
.delete-card {
display: grid;
place-items: center;
width: 27px;
height: 27px;
padding: 0;
border: 1px solid transparent;
border-radius: 8px;
color: var(--muted-2);
background: transparent;
transition: color 160ms ease, background 160ms ease, border-color 160ms ease;
}
.delete-card:hover {
border-color: var(--red-soft);
color: var(--red);
background: var(--red-soft);
}
.delete-card svg {
width: 15px;
height: 15px;
}
.card h4 {
margin: 13px 0 15px;
color: var(--text);
font-size: 0.96rem;
font-weight: 650;
letter-spacing: -0.012em;
line-height: 1.42;
}
.card-footer {
color: var(--muted-2);
font-size: 0.71rem;
}
.drag-label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.drag-label svg {
width: 13px;
height: 13px;
}
.card-id {
font-variant-numeric: tabular-nums;
letter-spacing: 0.05em;
}
.drop-indicator {
height: 0;
margin: -2px 0;
border: 1px dashed transparent;
border-radius: 11px;
opacity: 0;
pointer-events: none;
transition: height 140ms ease, opacity 140ms ease, background 140ms ease;
}
.drop-indicator.visible {
height: 38px;
border-color: var(--column-accent);
background: var(--column-soft);
opacity: 1;
}
.empty-state {
display: grid;
place-items: center;
min-height: 124px;
padding: 16px;
border: 1px dashed var(--border-strong);
border-radius: var(--radius-md);
color: var(--muted-2);
text-align: center;
font-size: 0.82rem;
line-height: 1.5;
}
.empty-state strong {
display: block;
margin-bottom: 3px;
color: var(--muted);
font-size: 0.86rem;
}
.quick-add {
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
width: 100%;
height: 38px;
margin-top: 11px;
border: 1px dashed var(--border-strong);
border-radius: 11px;
color: var(--muted);
background: transparent;
font-size: 0.79rem;
font-weight: 650;
transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
}
.quick-add:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-soft);
}
.quick-add svg {
width: 15px;
height: 15px;
}
.modal-backdrop {
position: fixed;
z-index: 20;
inset: 0;
display: grid;
place-items: center;
padding: 18px;
background: rgba(5, 9, 22, 0.58);
opacity: 0;
visibility: hidden;
transition: opacity 180ms ease, visibility 180ms ease;
backdrop-filter: blur(8px);
}
.modal-backdrop.open {
opacity: 1;
visibility: visible;
}
.modal {
width: min(440px, 100%);
padding: 23px;
border: 1px solid var(--border-strong);
border-radius: 20px;
background: var(--surface-solid);
box-shadow: var(--shadow);
transform: translateY(12px) scale(0.98);
transition: transform 180ms ease;
}
.modal-backdrop.open .modal {
transform: translateY(0) scale(1);
}
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 21px;
}
.modal-header h2 {
margin: 0;
font-size: 1.18rem;
letter-spacing: -0.025em;
}
.modal-header p {
margin: 6px 0 0;
color: var(--muted);
font-size: 0.82rem;
}
.close-modal {
display: grid;
place-items: center;
width: 31px;
height: 31px;
padding: 0;
border: 1px solid var(--border);
border-radius: 9px;
color: var(--muted);
background: transparent;
}
.close-modal:hover {
color: var(--text);
background: var(--surface-2);
}
.close-modal svg {
width: 16px;
height: 16px;
}
.form-field {
display: block;
margin-bottom: 15px;
}
.form-field > span {
display: block;
margin-bottom: 7px;
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
}
.form-field input,
.form-field select {
width: 100%;
height: 43px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 11px;
outline: none;
color: var(--text);
background: var(--surface-2);
}
.form-field input:focus,
.form-field select:focus {
border-color: rgba(139, 124, 255, 0.7);
box-shadow: 0 0 0 4px var(--accent-soft);
}
.form-field input.invalid {
border-color: var(--red);
box-shadow: 0 0 0 4px var(--red-soft);
}
.form-error {
min-height: 17px;
margin: -5px 0 10px;
color: var(--red);
font-size: 0.75rem;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 9px;
margin-top: 7px;
}
.secondary-button {
height: 41px;
padding: 0 15px;
border: 1px solid var(--border);
border-radius: 11px;
color: var(--text);
background: transparent;
font-weight: 650;
}
.secondary-button:hover {
background: var(--surface-2);
border-color: var(--border-strong);
}
.modal-actions .primary-button {
height: 41px;
padding: 0 17px;
}
@media (max-width: 1060px) {
.topbar {
align-items: flex-start;
flex-direction: column;
gap: 18px;
}
.toolbar {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
.search-box {
width: min(360px, 100%);
flex: 1 1 230px;
}
}
@media (max-width: 860px) {
.board {
grid-template-columns: 1fr;
}
.cards {
min-height: 126px;
}
}
@media (max-width: 520px) {
.app-shell {
padding: 19px 13px 28px;
}
.topbar {
margin-bottom: 25px;
}
.logo {
width: 43px;
height: 43px;
flex-basis: 43px;
border-radius: 13px;
}
.brand-copy p {
font-size: 0.76rem;
}
.toolbar {
gap: 8px;
}
.search-box {
flex-basis: 100%;
width: 100%;
}
.icon-button,
.primary-button {
flex: 1 1 auto;
}
.board-heading {
align-items: flex-start;
flex-direction: column;
gap: 12px;
}
.board-status {
align-self: stretch;
justify-content: center;
}
.column {
padding: 12px;
border-radius: 17px;
}
.modal {
padding: 19px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<div class="app-shell">
<header class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="3.5" y="3.5" width="6.2" height="13" rx="1.7"></rect>
<rect x="14.3" y="3.5" width="6.2" height="8.2" rx="1.7"></rect>
<path d="M3.5 20.5h6.2M14.3 20.5h6.2"></path>
</svg>
</div>
<div class="brand-copy">
<h1>Flowboard</h1>
<p>Product sprint · keep the momentum visible</p>
</div>
</div>
<div class="toolbar">
<label class="search-box" aria-label="Search cards by title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="6.7"></circle>
<path d="m16.2 16.2 4.1 4.1"></path>
</svg>
<input id="search-input" type="search" placeholder="Search cards..." autocomplete="off" />
<button id="clear-search" class="clear-search" type="button" aria-label="Clear search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="m7 7 10 10M17 7 7 17"></path>
</svg>
</button>
</label>
<button id="theme-toggle" class="icon-button" type="button" aria-label="Switch to light theme">
<svg id="theme-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2.5v2M12 19.5v2M21.5 12h-2M4.5 12h-2M18.7 5.3l-1.4 1.4M6.7 17.3l-1.4 1.4M18.7 18.7l-1.4-1.4M6.7 6.7 5.3 5.3"></path>
</svg>
<span id="theme-label">Light</span>
</button>
<button id="new-card-button" class="primary-button" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round">
<path d="M12 5v14M5 12h14"></path>
</svg>
<span>New card</span>
</button>
</div>
</header>
<section class="board-heading" aria-label="Board overview">
<div>
<h2>Current sprint</h2>
<p>Drag cards between columns to keep your workflow up to date.</p>
</div>
<div class="board-status" aria-live="polite">
<span class="status-dot"></span>
<span id="board-status-text">Showing 6 of 6 cards</span>
</div>
</section>
<main class="board" id="board">
<section class="column" data-column="todo" aria-labelledby="todo-title">
<header class="column-header">
<div class="column-title">
<span class="column-marker"></span>
<h3 id="todo-title">To do</h3>
</div>
<span class="count" data-count="todo">0</span>
</header>
<div class="cards" data-list="todo" aria-label="To do cards"></div>
<button class="quick-add" type="button" data-add-to="todo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M12 5v14M5 12h14"></path>
</svg>
Add card
</button>
</section>
<section class="column" data-column="doing" aria-labelledby="doing-title">
<header class="column-header">
<div class="column-title">
<span class="column-marker"></span>
<h3 id="doing-title">In progress</h3>
</div>
<span class="count" data-count="doing">0</span>
</header>
<div class="cards" data-list="doing" aria-label="In progress cards"></div>
<button class="quick-add" type="button" data-add-to="doing">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M12 5v14M5 12h14"></path>
</svg>
Add card
</button>
</section>
<section class="column" data-column="done" aria-labelledby="done-title">
<header class="column-header">
<div class="column-title">
<span class="column-marker"></span>
<h3 id="done-title">Done</h3>
</div>
<span class="count" data-count="done">0</span>
</header>
<div class="cards" data-list="done" aria-label="Done cards"></div>
<button class="quick-add" type="button" data-add-to="done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M12 5v14M5 12h14"></path>
</svg>
Add card
</button>
</section>
</main>
</div>
<div id="modal-backdrop" class="modal-backdrop" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-header">
<div>
<h2 id="modal-title">Create a new card</h2>
<p>Capture the next actionable piece of work.</p>
</div>
<button id="close-modal" class="close-modal" type="button" aria-label="Close dialog">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="m7 7 10 10M17 7 7 17"></path>
</svg>
</button>
</div>
<form id="card-form" novalidate>
<label class="form-field">
<span>Title</span>
<input id="card-title" name="title" type="text" maxlength="110" placeholder="e.g. Prepare launch checklist" required />
</label>
<div id="form-error" class="form-error" role="alert"></div>
<label class="form-field">
<span>Priority</span>
<select id="card-priority" 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="card-column" name="column">
<option value="todo">To do</option>
<option value="doing">In progress</option>
<option value="done">Done</option>
</select>
</label>
<div class="modal-actions">
<button id="cancel-modal" class="secondary-button" type="button">Cancel</button>
<button class="primary-button" type="submit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round">
<path d="M12 5v14M5 12h14"></path>
</svg>
Add card
</button>
</div>
</form>
</div>
</div>
<script>
(() => {
"use strict";
const state = {
theme: "dark",
query: "",
nextId: 1042,
draggedId: null,
draggedElement: null,
columns: {
todo: [
{ id: 1041, title: "Write onboarding email sequence", priority: "high", column: "todo" },
{ id: 1040, title: "Audit checkout analytics events", priority: "medium", column: "todo" }
],
doing: [
{ id: 1039, title: "Design empty states for dashboard", priority: "high", column: "doing" },
{ id: 1038, title: "Refactor invoice PDF export", priority: "medium", column: "doing" }
],
done: [
{ id: 1037, title: "Ship password reset flow", priority: "low", column: "done" },
{ id: 1036, title: "Update pricing page copy", priority: "low", column: "done" }
]
}
};
const board = document.getElementById("board");
const searchInput = document.getElementById("search-input");
const clearSearchButton = document.getElementById("clear-search");
const boardStatusText = document.getElementById("board-status-text");
const themeToggle = document.getElementById("theme-toggle");
const themeLabel = document.getElementById("theme-label");
const themeIcon = document.getElementById("theme-icon");
const modalBackdrop = document.getElementById("modal-backdrop");
const cardForm = document.getElementById("card-form");
const cardTitle = document.getElementById("card-title");
const cardPriority = document.getElementById("card-priority");
const cardColumn = document.getElementById("card-column");
const formError = document.getElementById("form-error");
const columnLabels = {
todo: "To do",
doing: "In progress",
done: "Done"
};
const priorityLabels = {
low: "Low",
medium: "Medium",
high: "High"
};
function allCards() {
return [...state.columns.todo, ...state.columns.doing, ...state.columns.done];
}
function findCard(id) {
return allCards().find((card) => card.id === Number(id));
}
function isVisible(card) {
return card.title.toLowerCase().includes(state.query);
}
function createEmptyState(isFiltered) {
const empty = document.createElement("div");
empty.className = "empty-state";
const strong = document.createElement("strong");
strong.textContent = isFiltered ? "No matching cards" : "Nothing here yet";
const text = document.createElement("span");
text.textContent = isFiltered
? "Try a different search term."
: "Drag a card here or add a new one.";
empty.append(strong, text);
return empty;
}
function createCardElement(card) {
const article = document.createElement("article");
article.className = "card";
article.dataset.id = String(card.id);
article.draggable = !state.query;
article.setAttribute("aria-label", `${card.title}, ${priorityLabels[card.priority]} priority`);
const top = document.createElement("div");
top.className = "card-top";
const priority = document.createElement("span");
priority.className = `priority ${card.priority}`;
const priorityDot = document.createElement("span");
priorityDot.className = "priority-dot";
const priorityText = document.createElement("span");
priorityText.textContent = priorityLabels[card.priority];
priority.append(priorityDot, priorityText);
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-card";
deleteButton.dataset.deleteId = String(card.id);
deleteButton.setAttribute("aria-label", `Delete ${card.title}`);
deleteButton.title = "Delete card";
deleteButton.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M5 7h14M10 11v6M14 11v6M8 7l.7-2.2h6.6L16 7M7 7l.7 13h8.6L17 7"></path>
</svg>
`;
top.append(priority, deleteButton);
const title = document.createElement("h4");
title.textContent = card.title;
const footer = document.createElement("div");
footer.className = "card-footer";
const dragLabel = document.createElement("span");
dragLabel.className = "drag-label";
dragLabel.innerHTML = `
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<circle cx="8" cy="6" r="1.5"></circle>
<circle cx="16" cy="6" r="1.5"></circle>
<circle cx="8" cy="12" r="1.5"></circle>
<circle cx="16" cy="12" r="1.5"></circle>
<circle cx="8" cy="18" r="1.5"></circle>
<circle cx="16" cy="18" r="1.5"></circle>
</svg>
<span>${state.query ? "Search active" : "Drag to move"}</span>
`;
const id = document.createElement("span");
id.className = "card-id";
id.textContent = `#${card.id}`;
footer.append(dragLabel, id);
article.append(top, title, footer);
return article;
}
function render() {
let visibleTotal = 0;
let totalCount = 0;
Object.entries(state.columns).forEach(([columnKey, cards]) => {
const list = document.querySelector(`[data-list="${columnKey}"]`);
const count = document.querySelector(`[data-count="${columnKey}"]`);
const indicator = document.createElement("div");
indicator.className = "drop-indicator";
list.replaceChildren(indicator);
const visibleCards = cards.filter(isVisible);
visibleTotal += visibleCards.length;
totalCount += cards.length;
count.textContent = String(visibleCards.length);
if (visibleCards.length === 0) {
list.append(createEmptyState(Boolean(state.query)));
return;
}
visibleCards.forEach((card) => {
list.append(createCardElement(card));
});
});
boardStatusText.textContent = state.query
? `Showing ${visibleTotal} of ${totalCount} cards`
: `Showing ${totalCount} of ${totalCount} cards`;
clearSearchButton.classList.toggle("visible", Boolean(state.query));
}
function setTheme(theme) {
state.theme = theme;
document.documentElement.dataset.theme = theme;
const isDark = theme === "dark";
themeLabel.textContent = isDark ? "Light" : "Dark";
themeToggle.setAttribute("aria-label", isDark ? "Switch to light theme" : "Switch to dark theme");
themeIcon.innerHTML = isDark
? `<circle cx="12" cy="12" r="4"></circle><path d="M12 2.5v2M12 19.5v2M21.5 12h-2M4.5 12h-2M18.7 5.3l-1.4 1.4M6.7 17.3l-1.4 1.4M18.7 18.7l-1.4-1.4M6.7 6.7 5.3 5.3"></path>`
: `<path d="M20.5 14.7A8.6 8.6 0 0 1 9.3 3.5 8.7 8.7 0 1 0 20.5 14.7Z"></path>`;
}
function openModal(column = "todo") {
formError.textContent = "";
cardTitle.value = "";
cardPriority.value = "medium";
cardColumn.value = column;
cardTitle.classList.remove("invalid");
modalBackdrop.classList.add("open");
modalBackdrop.setAttribute("aria-hidden", "false");
window.setTimeout(() => cardTitle.focus(), 80);
}
function closeModal() {
modalBackdrop.classList.remove("open");
modalBackdrop.setAttribute("aria-hidden", "true");
}
function clearDragState() {
if (state.draggedElement) {
state.draggedElement.classList.remove("is-dragging");
}
state.draggedId = null;
state.draggedElement = null;
document.querySelectorAll(".cards").forEach((list) => {
list.classList.remove("drag-over");
const indicator = list.querySelector(".drop-indicator");
if (indicator) indicator.classList.remove("visible");
});
}
function moveIndicator(list, clientY) {
const indicator = list.querySelector(".drop-indicator");
const cards = [...list.querySelectorAll(".card:not(.is-dragging)")];
let target = null;
for (const card of cards) {
const rect = card.getBoundingClientRect();
if (clientY < rect.top + rect.height / 2) {
target = card;
break;
}
}
if (target) {
list.insertBefore(indicator, target);
} else {
list.appendChild(indicator);
}
indicator.classList.add("visible");
}
board.addEventListener("click", (event) => {
const deleteButton = event.target.closest(".delete-card");
if (deleteButton) {
const id = Number(deleteButton.dataset.deleteId);
Object.keys(state.columns).forEach((columnKey) => {
state.columns[columnKey] = state.columns[columnKey].filter((card) => card.id !== id);
});
render();
return;
}
const addButton = event.target.closest("[data-add-to]");
if (addButton) {
openModal(addButton.dataset.addTo);
}
});
board.addEventListener("dragstart", (event) => {
const card = event.target.closest(".card");
if (!card || state.query) {
event.preventDefault();
return;
}
state.draggedId = Number(card.dataset.id);
state.draggedElement = card;
card.classList.add("is-dragging");
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", card.dataset.id);
});
board.addEventListener("dragover", (event) => {
const list = event.target.closest(".cards");
if (!list || !state.draggedElement) return;
event.preventDefault();
event.dataTransfer.dropEffect = "move";
document.querySelectorAll(".cards").forEach((otherList) => {
if (otherList !== list) {
otherList.classList.remove("drag-over");
const otherIndicator = otherList.querySelector(".drop-indicator");
if (otherIndicator) otherIndicator.classList.remove("visible");
}
});
list.classList.add("drag-over");
moveIndicator(list, event.clientY);
});
board.addEventListener("drop", (event) => {
const list = event.target.closest(".cards");
if (!list || !state.draggedElement) return;
event.preventDefault();
const card = findCard(state.draggedId);
if (!card) {
clearDragState();
return;
}
const targetColumn = list.dataset.list;
const indicator = list.querySelector(".drop-indicator");
let targetIndex = 0;
for (const child of list.children) {
if (child === indicator) break;
if (child.classList.contains("card") && child !== state.draggedElement) {
targetIndex += 1;
}
}
const sourceCards = state.columns[card.column];
const sourceIndex = sourceCards.findIndex((item) => item.id === card.id);
if (sourceIndex !== -1) sourceCards.splice(sourceIndex, 1);
card.column = targetColumn;
state.columns[targetColumn].splice(targetIndex, 0, card);
clearDragState();
render();
});
board.addEventListener("dragend", clearDragState);
searchInput.addEventListener("input", (event) => {
state.query = event.target.value.trim().toLowerCase();
render();
});
clearSearchButton.addEventListener("click", () => {
searchInput.value = "";
state.query = "";
render();
searchInput.focus();
});
themeToggle.addEventListener("click", () => {
setTheme(state.theme === "dark" ? "light" : "dark");
});
document.getElementById("new-card-button").addEventListener("click", () => openModal("todo"));
document.getElementById("close-modal").addEventListener("click", closeModal);
document.getElementById("cancel-modal").addEventListener("click", closeModal);
modalBackdrop.addEventListener("click", (event) => {
if (event.target === modalBackdrop) closeModal();
});
cardTitle.addEventListener("input", () => {
if (cardTitle.value.trim()) {
cardTitle.classList.remove("invalid");
formError.textContent = "";
}
});
cardForm.addEventListener("submit", (event) => {
event.preventDefault();
const title = cardTitle.value.trim();
if (!title) {
cardTitle.classList.add("invalid");
formError.textContent = "Please enter a title for the card.";
cardTitle.focus();
return;
}
const newCard = {
id: state.nextId++,
title,
priority: cardPriority.value,
column: cardColumn.value
};
state.columns[newCard.column].push(newCard);
closeModal();
render();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && modalBackdrop.classList.contains("open")) {
closeModal();
}
});
setTheme("dark");
render();
})();
</script>
</body>
</html>Reasoning trace (3,344 tokens)
We need output only code. Need build polished interactive kanban. Need self-contained. Must consider drag drop mouse with drop indicator. Also keyboard maybe not required. Need first screenshot alive. Modal, delete, search, theme toggle, responsive.
Let me craft HTML with CSS, JS. No external fonts (no network). Use system fonts. Nice design.
Features:
- header: brand icon, title "Kanban", subtitle; search input with clear; theme toggle; add card button.
- Board 3 columns, each with header, count pill, cards container with drop indicator element.
- 6 cards realistic: To do: "Write onboarding email sequence" high, "Audit checkout analytics events" medium; In progress: "Design empty states for dashboard" high, "Refactor invoice PDF export" medium; Done: "Ship password reset flow" low... plus "Update pricing page copy" low. Maybe 2 each.
- Each card: priority badge, title, maybe footer with id / handle? Requirement only title + priority. Add subtle meta like card ID and drag grip. Keep it clean.
- Add modal: overlay dialog with title input, priority select/segmented, cancel, add. Enter submits. Escape closes. Focus trap-ish.
- Delete button on card with aria-label.
- Drag: HTML5 draggable. dragstart sets dragging card, dataTransfer, adds .dragging, creates drag image maybe. dragover on column/card computes insertion position and shows indicator line; drop moves card. Clear on dragleave/drop/dragend.
- Live search filters cards by title (and update counts to visible counts, with empty state message "No cards match"). Show "N cards" in header maybe.
- Theme toggle persists in memory only (no localStorage), initial dark? A polished dark default with bright accent looks striking; toggle to light. Use data-theme attribute on body/html.
- Responsive: grid columns repeat(3,1fr) -> 1 column below 860, board horizontally... Requirement no horizontal scrollbar, so stack columns.
Implement carefully with vanilla JS and CSS. Need final answer as code only. Need ensure drag & drop with indicator. HTML5 DnD:
- cards draggable=true.
- dragstart: currentDrag = card; setTimeout add class.
- containers dragover preventDefault; compute closest card under pointer and insert indicator before/after. Use indicator element per column or single moved element. Better each column has <div class="drop-indicator">. On dragover, show in appropriate position: determine after element via elementFromPoint / closest .card and midpoint. If no target, append to list end. Hide other indicators.
- drop: preventDefault, move card element into position based on indicator (insertBefore). Then update state ordering from DOM; re-render? If re-render loses drag but fine after drop. Better maintain state array and reorder. Simpler: manipulate DOM, then syncStateFromDOM() to rebuild arrays. But data model with objects and render. Implement model: state.columns = {todo:[card...], doing:[], done:[]}. Render full board on changes. For drag drop, on drop compute target column & index, update model and render. Indicator determines index. During dragover don't rerender.
Implement:
```
const state = { theme:'dark', query:'', columns: {...}, cardsById: Map? }
```
Cards objects with id, title, priority, column. arrays.
render():
for each column: list.innerHTML=''; for card matching query create element.
Count: column header count should show filtered count? Requirement each showing card count. Could show visible count and total? Use count element textContent = visible.length maybe; when filtering, count reflects visible. Maybe label "2 cards" hidden. Fine. Show count as `2`. Include empty states per column when no visible cards ("No cards match" or "Drop cards here").
When query filters, cards hidden (not in DOM) — drag drop while filtering complicated. Could disable dragging when query active? Better allow, but index computed among visible; model index mapping. Simpler: when query non-empty, set draggable false on cards and show hint "Clear search to reorder". That is graceful. Requirement drag drop between columns with mouse works generally. Do this.
Drag start only if !state.query.
Dragover: `const list = e.currentTarget` (column body). Prevent default. Determine target card among `.card:not(.dragging)`. But if query empty all visible. Compute afterElement:
```
const cards = [...list.querySelectorAll('.card:not(.dragging)')];
let after = null;
for (const c of cards) { const box=c.getBoundingClientRect(); if (e.clientY < box.top + box.height/2) { after=c; break; } }
const indicator = list.querySelector('.drop-indicator');
if (after) list.insertBefore(indicator, after) else list.appendChild(indicator);
indicator.classList.add('visible');
```
Need indicator in DOM initially as first child. list structure: `div.cards` contains indicator and cards. On render, clear and append indicator + cards.
On drop:
```
const indicator = list.querySelector('.drop-indicator');
const columnKey = list.closest('.column').dataset.column;
const visibleCards = [...list.querySelectorAll('.card:not(.dragging)')]; // but indicator moved
const domCards = [...list.querySelectorAll('.card')]; includes dragging card? In DOM dragging card remains in old position. We want index among cards in target list excluding dragged. Compute target index from indicator: number of .card before indicator excluding dragged element. Then remove dragged from old model array and splice into target at index. render().
```
Careful indicator position with dragged card still in old list; target list may be same list. Use `const beforeCards = []` iterate list.children, count cards that aren't dragged until indicator. That gives insertion index in the resulting array after removal? If same list and dragged card is before indicator, need adjust. Compute target array = column array minus dragged; then index = count of non-dragged cards before indicator in DOM. But DOM includes dragged in old position; skip it. Works.
drop on empty list area: indicator placed end.
Need dragover on column? list only; but drop indicator visible. Column height. Make `.cards` min-height.
Dragend cleanup.
Also allow touch? Requirement mouse; fine. Maybe pointer fallback not needed.
Modal:
- `dialog`? Use custom div overlay to avoid quirks. `#modal` class hidden, open with class show, aria-hidden.
- form submit: title trim required; if empty show error and input aria-invalid. priority select.
- Add to `state.columns.todo`? Could add to "To do" by default. Maybe modal includes "Add to" select column. Requirement add card through small modal form (title required). Include column select — nice. Keep small. On add: push to selected column at beginning? append. Render, close, maybe flash card.
- open on button, close overlay click, Escape.
- Keyboard: Enter submit, Escape close. Since board not keyboard game.
Theme toggle: button icon sun/moon and text maybe. `document.documentElement.dataset.theme`. Use CSS variables.
Need no console errors. Avoid `structuredClone` etc.
Visual design:
CSS variables dark:
--bg #0d1117-ish; --surface #151b23; --surface-2 #1c2430; --border rgba(255,255,255,.09); --text #e7edf4; --muted #8b98a8; --accent #6ee7b7 or #7c5cff. Use purple/indigo accent with lime? Let's design dark navy with accent #7c8cff and colored priority badges.
Light theme: bg #eef2f7, surface #fff, text #17202c.
Header:
```
<header class="topbar">
<div class="brand"><div class="logo"> ... svg</div><div><h1>Flowboard</h1><p>Product sprint · 6 cards</p></div></div>
<div class="controls">
<label class=search> svg input button clear</label>
<button theme>...</button>
<button class=primary>+ New card</button>
</div>
</header>
```
Make topbar responsive: flex wrap, search width 280. On 375px, brand and controls stack, search full width.
Board section with heading maybe "Sprint board" and legend. Avoid too much. Add `main` with `board-toolbar` line: "Drag cards between columns to update status" and filter status "Showing 6 of 6 cards". That is visible and useful.
Columns:
```
<section class="column" data-column="todo">
<header class=column-header>
<div class=column-title><span class=dot ...></span><h2>To do</h2></div>
<span class=count>2</span>
</header>
<div class=cards id=todo-cards>...</div>
<button class=quick-add data-column=...>+ Add card</button> maybe adds modal with column preset. Nice.
</section>
```
Card:
```
<article class=card draggable=true data-id>
<div class=card-top><span class=priority high><span dot></span>High</span><button class=delete ...>×</button></div>
<h3>...</h3>
<div class=card-foot><span class=drag-handle>⠿ Move</span><span class=id>#1042</span></div>
</article>
```
Requirement each card title and priority. Extras fine.
Maybe add card created date? Not needed.
Empty state inside list if no cards.
Drag indicator: `.drop-indicator { height:0; border-top: 2px dashed accent; margin: 2px 0; opacity:0; } .visible {opacity:1; height: ...}` Actually when indicator hidden, keep height 0. On visible set `height:34px`? A drop indicator line with gap. Use `.drop-indicator.visible { height: 44px; opacity:1; }` and background subtle, but drop gap large. Better `.drop-indicator.visible{height:34px; opacity:1; border:1px dashed; border-radius:10px; background:...}`. Clear visual.
Need state updates and counts.
Deleting: event delegation on cards list click `.delete-card`, find id, remove from model, render. Add `data-id` and use helper.
Search input listener state.query = value.toLowerCase(); render(); toggle clear button.
Use requestAnimation? Not needed.
Potential issue `render()` rebuilds cards and drag element references. Fine.
Drag handle not necessary since entire card draggable.
Accessibility: aria-labels, roles, `aria-live` on status line.
Let's write complete code. Must output only fenced html.
Let's ensure CSS no horizontal scrollbar: `*{box-sizing:border-box}` `body{overflow-x:hidden}`. Board grid `grid-template-columns: repeat(3,minmax(0,1fr))` and at max-width 900 -> 1 column? At tablet 2? Requirement three columns; keep 3 until 860, then 1 column maybe. Could do `@media (max-width: 980px) { grid-template-columns:1fr; }` but board gets long; screenshot 1280 fine. Maybe 2 columns at 900-1100? Requirement three columns ideally always; keep 3 until 860, then 1 column. Fine.
Modal styling.
Include `prefers-reduced-motion`.
Need maybe `data-theme="dark"` on html initially set by JS? Put `<html lang="en" data-theme="dark">`. Theme toggle flips.
Drop on column body: dragover listener attached to `.cards`. If dragging over gap between cards but within cards works. Also attach to `.column`? Cards container has padding; column header outside won't accept. Fine.
`dragover` on cards list: if state.query return. `e.preventDefault()`, `e.dataTransfer.dropEffect='move'`. For empty list, indicator appended.
`drop` handler on cards list. Determine `const dragged = state.draggedEl`; if not return. Then:
```
const columnKey = list.closest('.column').dataset.column;
const indicator = list.querySelector('.drop-indicator');
let idx = 0;
for (const child of list.children) {
if (child === indicator) break;
if (child.classList.contains('card') && child !== dragged) idx++;
}
const card = findCard(state.draggedId);
const sourceArr = state.columns[card.column];
sourceArr.splice(sourceArr.indexOf(card),1);
if (card.column !== columnKey) card.column = columnKey;
state.columns[columnKey].splice(idx,0,card);
```
Issue: when same column and dragged card before indicator, index computed among non-dragged cards before indicator = correct insertion index in array after removal? Example [A,B,C], drag A, indicator after B => children A(dragged skip), B count 1, indicator -> idx 1. Remove A => [B,C], insert at 1 => [B,A,C] correct. Drag C, indicator before B => idx 0 => remove C [A,B], insert 0 => [C,A,B] correct. Good.
But when dropping into target list, dragged DOM element is not moved before render; indicator placed relative to cards ignoring dragged. fine.
Need dragstart: `e.dataTransfer.setData('text/plain', id)` and effectAllowed. `state.draggedEl = cardEl; state.draggedId=id`. Add class after timeout? class immediately might affect getBoundingClientRect slightly (transform). Use `requestAnimationFrame(() => cardEl.classList.add('is-dragging'))`, but then dragover queries exclude `.is-dragging`; at time dragover maybe class added. Safer class immediately `cardEl.classList.add('is-dragging')`; style opacity .35, transform rotate(1deg) might slightly shift. Use opacity only. `e.dataTransfer.setDragImage(cardEl, 20,20)` maybe.
Also on `dragstart` if state.query nonempty preventDefault.
Search hides cards; if drag disabled and cursor default.
Delete event listener attached to board via `document.getElementById('board').addEventListener('click',...)`; if click target closest `.delete-card`, get id.
Quick add buttons: `data-column` open modal with column preselected. Modal form has select id `card-column`. `openModal(columnKey='todo')` sets.
Card count should include cards per column total or visible? Let's display `count.textContent = visible.length` and maybe when filtering `count.dataset.filtered`; Also a small line "2 of 6 cards". Fine.
Rendering cards with `cardEl.innerHTML` using textContent for title to avoid injection. Build via createElement and textContent.
Let's write.
Some detail: `escape` handling modal and maybe search clear.
Use SVG inline icons.
Theme toggle text label `Light`/`Dark`? Button with icon and `.theme-label`. On dark, show "Light mode"? Actually button action label. Use `<span class="theme-label">Light</span>` meaning switch to light. Update based on current theme. Fine.
`<meta name="color-scheme" content="dark light">`.
Make body background with radial gradients.
Let's produce final code only.
One concern: "The iframe only receives keyboard events after user clicks it: if demo uses keyboard, display a clear 'Click or press a key to start' message." This is a web app not game, keyboard not necessary. Modal Enter works after click. No need.
Potential issue user wants title required: HTML `required` plus custom validation.
Card IDs increment.
Now produce code.