Nemotron 3 Ultra (free)
by NVIDIAReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 44 s
- Cost
- $0
- Output tokens
- 10,453
- Spent thinking
- 268 tokens
- Speed
- 101.1 tok/s
- Console errors
- 0
- First token after
- 680 ms
- First visible text after
- 6.5 s
- Prompt tokens
- 513
- Code size
- 34.1 KB
- Finish reason
- stop
- Served by
- Nvidia
- Model id
nvidia/nemotron-3-ultra-550b-a55b:free- Reasoning sent
{"effort":"medium"}- Max output
- 32,000 tokens
- Generated
- Nov 9, 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 · 754 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Board</title>
<style>
:root {
--bg-primary: #f4f5f7;
--bg-secondary: #ffffff;
--bg-tertiary: #ebecf0;
--text-primary: #172b4d;
--text-secondary: #5e6c84;
--text-muted: #8898aa;
--border-color: #dfe1e6;
--shadow-sm: 0 1px 2px rgba(9,30,66,.15);
--shadow-md: 0 4px 8px rgba(9,30,66,.12);
--shadow-lg: 0 8px 16px rgba(9,30,66,.15);
--accent: #0052cc;
--accent-hover: #0065ff;
--accent-light: #d6e4f0;
--danger: #de350b;
--danger-hover: #ff5630;
--danger-light: #ffe9e6;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--transition: 150ms ease;
--column-width: 300px;
--header-height: 56px;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg-primary: #1d2128;
--bg-secondary: #282c34;
--bg-tertiary: #2e333a;
--text-primary: #ffffff;
--text-secondary: #b6bec8;
--text-muted: #8b919a;
--border-color: #3e4451;
--shadow-sm: 0 1px 2px rgba(0,0,0,.3);
--shadow-md: 0 4px 8px rgba(0,0,0,.4);
--shadow-lg: 0 8px 16px rgba(0,0,0,.5);
--accent: #4c9aff;
--accent-hover: #7ab3ff;
--accent-light: #1a3a5c;
--danger: #ff6b4a;
--danger-hover: #ff8b73;
--danger-light: #4a1a12;
}
}
[data-theme="light"] {
--bg-primary: #f4f5f7;
--bg-secondary: #ffffff;
--bg-tertiary: #ebecf0;
--text-primary: #172b4d;
--text-secondary: #5e6c84;
--text-muted: #8898aa;
--border-color: #dfe1e6;
--shadow-sm: 0 1px 2px rgba(9,30,66,.15);
--shadow-md: 0 4px 8px rgba(9,30,66,.12);
--shadow-lg: 0 8px 16px rgba(9,30,66,.15);
--accent: #0052cc;
--accent-hover: #0065ff;
--accent-light: #d6e4f0;
--danger: #de350b;
--danger-hover: #ff5630;
--danger-light: #ffe9e6;
}
[data-theme="dark"] {
--bg-primary: #1d2128;
--bg-secondary: #282c34;
--bg-tertiary: #2e333a;
--text-primary: #ffffff;
--text-secondary: #b6bec8;
--text-muted: #8b919a;
--border-color: #3e4451;
--shadow-sm: 0 1px 2px rgba(0,0,0,.3);
--shadow-md: 0 4px 8px rgba(0,0,0,.4);
--shadow-lg: 0 8px 16px rgba(0,0,0,.5);
--accent: #4c9aff;
--accent-hover: #7ab3ff;
--accent-light: #1a3a5c;
--danger: #ff6b4a;
--danger-hover: #ff8b73;
--danger-light: #4a1a12;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; background: var(--bg-primary); color: var(--text-primary); transition: background var(--transition), color var(--transition); }
.app { display: flex; flex-direction: column; min-height: 100vh; }
/* Header */
.header { display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: var(--header-height); background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); position: sticky; top: 0; z-index: 100; box-shadow: var(--shadow-sm); }
.header h1 { font-size: 18px; font-weight: 600; color: var(--text-primary); }
.header-actions { display: flex; align-items: center; gap: 12px; }
.search-wrapper { position: relative; }
.search-input { width: 280px; padding: 8px 12px 8px 36px; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-tertiary); color: var(--text-primary); font-size: 13px; transition: all var(--transition); }
.search-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); width: 320px; }
.search-input::placeholder { color: var(--text-muted); }
.search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-muted); pointer-events: none; }
.theme-toggle { width: 40px; height: 40px; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-tertiary); color: var(--text-primary); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--transition); }
.theme-toggle:hover { background: var(--bg-primary); border-color: var(--accent); }
.theme-toggle:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-light); }
.theme-toggle svg { width: 20px; height: 20px; }
.theme-toggle .sun-icon { display: none; }
[data-theme="light"] .theme-toggle .sun-icon { display: block; }
[data-theme="light"] .theme-toggle .moon-icon { display: none; }
.add-card-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border: none; border-radius: var(--radius-md); background: var(--accent); color: white; font-size: 13px; font-weight: 500; cursor: pointer; transition: all var(--transition); }
.add-card-btn:hover { background: var(--accent-hover); }
.add-card-btn:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-light); }
.add-card-btn svg { width: 16px; height: 16px; }
/* Main Board */
.board { flex: 1; display: flex; gap: 12px; padding: 24px; overflow-x: auto; align-items: flex-start; min-height: calc(100vh - var(--header-height)); }
.column { width: var(--column-width); min-width: var(--column-width); flex-shrink: 0; background: var(--bg-tertiary); border-radius: var(--radius-lg); display: flex; flex-direction: column; height: calc(100vh - var(--header-height) - 48px); max-height: calc(100vh - var(--header-height) - 48px); box-shadow: var(--shadow-sm); transition: background var(--transition), box-shadow var(--transition); }
.column.drag-over { background: var(--accent-light); box-shadow: 0 0 0 2px var(--accent), var(--shadow-md); }
.column-header { display: flex; align-items: center; justify-content: space-between; padding: 16px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
.column-title { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); }
.column-count { background: var(--bg-primary); color: var(--text-secondary); padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; min-width: 24px; text-align: center; }
.cards-container { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; min-height: 100px; }
.cards-container.drag-over { background: rgba(0, 82, 204, 0.05); border-radius: var(--radius-md); }
.drop-indicator { height: 2px; background: var(--accent); border-radius: 2px; margin: 4px 0; opacity: 0; transform: scaleX(0); transition: all 100ms ease; }
.drop-indicator.visible { opacity: 1; transform: scaleX(1); }
/* Cards */
.card { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 12px; box-shadow: var(--shadow-sm); cursor: grab; transition: all var(--transition); position: relative; animation: cardEnter 200ms ease; }
@keyframes cardEnter { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
.card.dragging { opacity: 0.4; cursor: grabbing; box-shadow: var(--shadow-lg); transform: rotate(2deg); z-index: 1000; }
.card:hover { box-shadow: var(--shadow-md); }
.card:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-light), var(--shadow-md); }
.card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.card-title { font-size: 14px; font-weight: 500; color: var(--text-primary); line-height: 1.4; word-break: break-word; flex: 1; }
.card-delete { width: 24px; height: 24px; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: all var(--transition); flex-shrink: 0; }
.card:hover .card-delete, .card:focus-within .card-delete { opacity: 1; }
.card-delete:hover { background: var(--danger-light); color: var(--danger); }
.card-delete:focus { outline: none; box-shadow: 0 0 0 2px var(--danger); }
.card-delete svg { width: 14px; height: 14px; }
.card-footer { display: flex; align-items: center; justify-content: space-between; }
.priority-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: var(--radius-sm); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
.priority-badge.low { background: #e3fcef; color: #006633; }
.priority-badge.medium { background: #fff4d6; color: #974d00; }
.priority-badge.high { background: #ffe9e6; color: #de350b; }
[data-theme="dark"] .priority-badge.low { background: #0d3b24; color: #57d9a3; }
[data-theme="dark"] .priority-badge.medium { background: #3d3300; color: #ffab00; }
[data-theme="dark"] .priority-badge.high { background: #3d1a12; color: #ff8b73; }
/* Modal */
.modal-overlay { position: fixed; inset: 0; background: rgba(9, 30, 66, 0.54); display: flex; align-items: center; justify-content: center; padding: 24px; z-index: 1000; opacity: 0; visibility: hidden; transition: all var(--transition); backdrop-filter: blur(2px); }
.modal-overlay.visible { opacity: 1; visibility: visible; }
.modal { background: var(--bg-secondary); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto; transform: scale(0.95) translateY(10px); transition: all var(--transition); }
.modal-overlay.visible .modal { transform: scale(1) translateY(0); }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid var(--border-color); }
.modal-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.modal-close { width: 32px; height: 32px; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--transition); }
.modal-close:hover { background: var(--bg-tertiary); color: var(--text-primary); }
.modal-close:focus { outline: none; box-shadow: 0 0 0 2px var(--accent); }
.modal-close svg { width: 18px; height: 18px; }
.modal-body { padding: 24px; }
.form-group { margin-bottom: 20px; }
.form-label { display: block; font-size: 13px; font-weight: 500; color: var(--text-primary); margin-bottom: 6px; }
.form-input, .form-select { width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-secondary); color: var(--text-primary); font-size: 14px; transition: all var(--transition); }
.form-input:focus, .form-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
.form-input::placeholder { color: var(--text-muted); }
.form-error { color: var(--danger); font-size: 12px; margin-top: 4px; display: none; }
.form-error.visible { display: block; }
.modal-footer { display: flex; justify-content: flex-end; gap: 12px; padding: 16px 24px; border-top: 1px solid var(--border-color); }
.btn { padding: 10px 20px; border-radius: var(--radius-md); font-size: 13px; font-weight: 500; cursor: pointer; transition: all var(--transition); border: 1px solid transparent; }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-light); }
.btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); border-color: var(--border-color); }
.btn-secondary:hover { background: var(--bg-primary); }
.btn-secondary:focus { outline: none; box-shadow: 0 0 0 3px var(--accent-light); }
/* Empty state */
.empty-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-muted); font-size: 13px; text-align: center; padding: 24px 12px; }
.empty-state svg { width: 48px; height: 48px; margin-bottom: 12px; opacity: 0.5; }
/* Responsive */
@media (max-width: 768px) {
.board { flex-direction: column; align-items: stretch; padding: 16px; gap: 16px; }
.column { width: 100%; min-width: 0; height: auto; max-height: none; min-height: 300px; }
.search-input { width: 180px; }
.search-input:focus { width: 220px; }
.header { padding: 0 16px; }
}
@media (max-width: 480px) {
.header { flex-wrap: wrap; height: auto; padding: 12px 16px; gap: 12px; }
.header h1 { font-size: 16px; }
.search-wrapper { order: 3; width: 100%; }
.search-input { width: 100%; }
.search-input:focus { width: 100%; }
.modal { margin: 12px; max-height: calc(100vh - 24px); }
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
</style>
</head>
<body>
<div class="app">
<header class="header" role="banner">
<h1>Kanban Board</h1>
<div class="header-actions">
<div class="search-wrapper">
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="search" class="search-input" placeholder="Search cards..." aria-label="Search cards" id="searchInput">
</div>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme" title="Toggle theme">
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
</button>
<button class="add-card-btn" id="addCardBtn" aria-haspopup="dialog" aria-label="Add new card">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
<span>Add Card</span>
</button>
</div>
</header>
<main class="board" id="board" role="main">
<section class="column" data-column="todo" aria-labelledby="todo-title">
<header class="column-header">
<h2 class="column-title" id="todo-title">To do <span class="column-count" id="todo-count">0</span></h2>
</header>
<div class="cards-container" id="todo-cards" role="list" aria-label="To do cards">
<div class="empty-state" id="todo-empty" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg>
No cards yet. Drag cards here or add new ones.
</div>
</div>
</section>
<section class="column" data-column="inprogress" aria-labelledby="inprogress-title">
<header class="column-header">
<h2 class="column-title" id="inprogress-title">In progress <span class="column-count" id="inprogress-count">0</span></h2>
</header>
<div class="cards-container" id="inprogress-cards" role="list" aria-label="In progress cards">
<div class="empty-state" id="inprogress-empty" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg>
No cards in progress.
</div>
</div>
</section>
<section class="column" data-column="done" aria-labelledby="done-title">
<header class="column-header">
<h2 class="column-title" id="done-title">Done <span class="column-count" id="done-count">0</span></h2>
</header>
<div class="cards-container" id="done-cards" role="list" aria-label="Done cards">
<div class="empty-state" id="done-empty" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><polyline points="20 6 9 17 4 12"></polyline></svg>
No completed cards yet.
</div>
</div>
</section>
</main>
</div>
<div class="modal-overlay" id="modalOverlay" role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden>
<div class="modal">
<header class="modal-header">
<h2 class="modal-title" id="modal-title">Add New Card</h2>
<button class="modal-close" id="modalClose" aria-label="Close modal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</header>
<div class="modal-body">
<form id="cardForm" novalidate>
<div class="form-group">
<label class="form-label" for="cardTitle">Title <span aria-hidden="true">*</span></label>
<input type="text" class="form-input" id="cardTitle" placeholder="Enter card title" required aria-required="true" autocomplete="off">
<div class="form-error" id="titleError" role="alert">Title is required</div>
</div>
<div class="form-group">
<label class="form-label" for="cardPriority">Priority</label>
<select class="form-select" id="cardPriority" aria-describedby="priorityHelp">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
<span id="priorityHelp" class="form-error" style="color: var(--text-muted); display: block; margin-top: 4px; font-size: 12px;">Priority helps visualize urgency</span>
</div>
</form>
</div>
<footer class="modal-footer">
<button type="button" class="btn btn-secondary" id="modalCancel">Cancel</button>
<button type="submit" form="cardForm" class="btn btn-primary" id="modalSubmit">Add Card</button>
</footer>
</div>
</div>
<script>
// State
const state = {
cards: [],
nextId: 1,
searchQuery: '',
draggedCard: null,
dragOverColumn: null,
dragOverCard: null
};
// Initial cards
const initialCards = [
{ id: 1, title: "Design new dashboard layout", priority: "high", column: "todo" },
{ id: 2, title: "Set up CI/CD pipeline", priority: "medium", column: "todo" },
{ id: 3, title: "Write unit tests for auth module", priority: "medium", column: "inprogress" },
{ id: 4, title: "Refactor legacy API endpoints", priority: "high", column: "inprogress" },
{ id: 5, title: "Update documentation for v2.0", priority: "low", column: "done" },
{ id: 6, title: "Deploy to staging environment", priority: "high", column: "done" }
];
// DOM Elements
const board = document.getElementById('board');
const searchInput = document.getElementById('searchInput');
const themeToggle = document.getElementById('themeToggle');
const addCardBtn = document.getElementById('addCardBtn');
const modalOverlay = document.getElementById('modalOverlay');
const modalClose = document.getElementById('modalClose');
const modalCancel = document.getElementById('modalCancel');
const modalSubmit = document.getElementById('modalSubmit');
const cardForm = document.getElementById('cardForm');
const cardTitleInput = document.getElementById('cardTitle');
const titleError = document.getElementById('titleError');
const columnCards = {
todo: document.getElementById('todo-cards'),
inprogress: document.getElementById('inprogress-cards'),
done: document.getElementById('done-cards')
};
const columnCounts = {
todo: document.getElementById('todo-count'),
inprogress: document.getElementById('inprogress-count'),
done: document.getElementById('done-count')
};
const emptyStates = {
todo: document.getElementById('todo-empty'),
inprogress: document.getElementById('inprogress-empty'),
done: document.getElementById('done-empty')
};
// Priority styles
const priorityLabels = { low: 'Low', medium: 'Medium', high: 'High' };
// Initialize
function init() {
// Load initial cards
initialCards.forEach(card => {
state.cards.push({ ...card, id: state.nextId++ });
});
// Set theme from system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
render();
bindEvents();
}
// Event binding
function bindEvents() {
// Search
searchInput.addEventListener('input', (e) => {
state.searchQuery = e.target.value.toLowerCase().trim();
render();
});
// Theme toggle
themeToggle.addEventListener('click', toggleTheme);
// Modal
addCardBtn.addEventListener('click', openModal);
modalClose.addEventListener('click', closeModal);
modalCancel.addEventListener('click', closeModal);
modalOverlay.addEventListener('click', (e) => {
if (e.target === modalOverlay) closeModal();
});
// Form submit
cardForm.addEventListener('submit', (e) => {
e.preventDefault();
addCard();
});
// Title validation
cardTitleInput.addEventListener('input', () => {
titleError.classList.remove('visible');
cardTitleInput.style.borderColor = '';
});
// Keyboard
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modalOverlay.hidden) closeModal();
if ((e.key === 'Enter' || e.key === ' ') && e.target === addCardBtn) {
e.preventDefault();
openModal();
}
});
// Drag and drop on columns
Object.values(columnCards).forEach(container => {
container.addEventListener('dragover', handleDragOver);
container.addEventListener('dragleave', handleDragLeave);
container.addEventListener('drop', handleDrop);
});
// Prevent default on board for drag
board.addEventListener('dragover', (e) => e.preventDefault());
}
// Theme
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
}
// Modal
function openModal() {
modalOverlay.hidden = false;
requestAnimationFrame(() => modalOverlay.classList.add('visible'));
cardTitleInput.value = '';
document.getElementById('cardPriority').value = 'medium';
titleError.classList.remove('visible');
setTimeout(() => cardTitleInput.focus(), 100);
document.body.style.overflow = 'hidden';
}
function closeModal() {
modalOverlay.classList.remove('visible');
setTimeout(() => {
modalOverlay.hidden = true;
document.body.style.overflow = '';
}, 150);
}
// Add card
function addCard() {
const title = cardTitleInput.value.trim();
if (!title) {
titleError.classList.add('visible');
cardTitleInput.style.borderColor = 'var(--danger)';
cardTitleInput.focus();
return;
}
const priority = document.getElementById('cardPriority').value;
const card = {
id: state.nextId++,
title,
priority,
column: 'todo'
};
state.cards.push(card);
closeModal();
render();
}
// Delete card
function deleteCard(id) {
state.cards = state.cards.filter(c => c.id !== id);
render();
}
// Move card
function moveCard(id, targetColumn, insertBeforeId = null) {
const cardIndex = state.cards.findIndex(c => c.id === id);
if (cardIndex === -1) return;
const card = state.cards[cardIndex];
card.column = targetColumn;
// Reorder within column
const columnCards = state.cards.filter(c => c.column === targetColumn);
if (insertBeforeId) {
const insertIndex = columnCards.findIndex(c => c.id === insertBeforeId);
if (insertIndex !== -1) {
// Remove from current position
state.cards.splice(cardIndex, 1);
// Find new index in full array
const targetIndex = state.cards.findIndex(c => c.id === insertBeforeId);
state.cards.splice(targetIndex, 0, card);
}
}
render();
}
// Render
function render() {
const columns = ['todo', 'inprogress', 'done'];
columns.forEach(col => {
const container = columnCards[col];
const countEl = columnCounts[col];
const emptyEl = emptyStates[col];
// Filter cards for this column
let cards = state.cards.filter(c => c.column === col);
// Apply search filter
if (state.searchQuery) {
cards = cards.filter(c => c.title.toLowerCase().includes(state.searchQuery));
}
// Update count
countEl.textContent = cards.length;
// Show/hide empty state
emptyEl.hidden = cards.length > 0;
// Render cards
const existingCards = container.querySelectorAll('.card');
existingCards.forEach(el => el.remove());
cards.forEach(card => {
const cardEl = createCardElement(card);
container.appendChild(cardEl);
});
});
}
// Create card element
function createCardElement(card) {
const cardEl = document.createElement('article');
cardEl.className = 'card';
cardEl.draggable = true;
cardEl.dataset.id = card.id;
cardEl.dataset.column = card.column;
cardEl.setAttribute('role', 'listitem');
cardEl.setAttribute('tabindex', '0');
cardEl.setAttribute('aria-label', `${card.title}, ${priorityLabels[card.priority]} priority`);
cardEl.innerHTML = `
<div class="card-header">
<h3 class="card-title">${escapeHtml(card.title)}</h3>
<button class="card-delete" aria-label="Delete card" data-id="${card.id}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
</button>
</div>
<div class="card-footer">
<span class="priority-badge ${card.priority}">${priorityLabels[card.priority]}</span>
</div>
`;
// Drag events
cardEl.addEventListener('dragstart', handleDragStart);
cardEl.addEventListener('dragend', handleDragEnd);
cardEl.addEventListener('dragenter', handleCardDragEnter);
cardEl.addEventListener('dragleave', handleCardDragLeave);
cardEl.addEventListener('dragover', (e) => e.preventDefault());
cardEl.addEventListener('drop', handleCardDrop);
// Delete button
const deleteBtn = cardEl.querySelector('.card-delete');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCard(card.id);
});
// Keyboard support for delete
deleteBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
deleteCard(card.id);
}
});
// Keyboard drag support (Space to pick up, arrows to move, Enter to drop)
cardEl.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
// Simple keyboard reorder: cycle columns
const columns = ['todo', 'inprogress', 'done'];
const currentIndex = columns.indexOf(card.column);
const nextIndex = (currentIndex + 1) % columns.length;
moveCard(card.id, columns[nextIndex]);
}
});
return cardEl;
}
// Drag and drop handlers
function handleDragStart(e) {
if (e.target.closest('.card-delete')) {
e.preventDefault();
return;
}
const cardEl = e.target.closest('.card');
if (!cardEl) return;
state.draggedCard = cardEl;
cardEl.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', cardEl.dataset.id);
// Create drag image
const dragImage = cardEl.cloneNode(true);
dragImage.style.width = cardEl.offsetWidth + 'px';
dragImage.style.position = 'absolute';
dragImage.style.top = '-1000px';
dragImage.classList.remove('dragging');
document.body.appendChild(dragImage);
e.dataTransfer.setDragImage(dragImage, 10, 10);
setTimeout(() => document.body.removeChild(dragImage), 0);
}
function handleDragEnd(e) {
const cardEl = e.target.closest('.card');
if (cardEl) cardEl.classList.remove('dragging');
// Clean up drop indicators
document.querySelectorAll('.drop-indicator.visible').forEach(el => el.classList.remove('visible'));
document.querySelectorAll('.column.drag-over').forEach(el => el.classList.remove('drag-over'));
document.querySelectorAll('.cards-container.drag-over').forEach(el => el.classList.remove('drag-over'));
state.draggedCard = null;
state.dragOverColumn = null;
state.dragOverCard = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const container = e.currentTarget;
const column = container.closest('.column');
// Show column drop indicator
column.classList.add('drag-over');
container.classList.add('drag-over');
// Position drop indicator between cards
const afterElement = getDragAfterElement(container, e.clientY);
const indicator = container.querySelector('.drop-indicator') || createDropIndicator(container);
if (afterElement == null) {
container.appendChild(indicator);
} else {
container.insertBefore(indicator, afterElement);
}
indicator.classList.add('visible');
state.dragOverColumn = column.dataset.column;
state.dragOverCard = afterElement?.dataset.id || null;
}
function handleDragLeave(e) {
const container = e.currentTarget;
const column = container.closest('.column');
// Only remove if actually leaving the container
if (!container.contains(e.relatedTarget)) {
column.classList.remove('drag-over');
container.classList.remove('drag-over');
const indicator = container.querySelector('.drop-indicator');
if (indicator) indicator.classList.remove('visible');
}
}
function handleDrop(e) {
e.preventDefault();
const container = e.currentTarget;
const column = container.closest('.column');
const targetColumn = column.dataset.column;
const cardId = parseInt(e.dataTransfer.getData('text/plain'), 10);
const afterElement = getDragAfterElement(container, e.clientY);
const insertBeforeId = afterElement?.dataset.id ? parseInt(afterElement.dataset.id, 10) : null;
if (cardId) {
moveCard(cardId, targetColumn, insertBeforeId);
}
// Clean up
column.classList.remove('drag-over');
container.classList.remove('drag-over');
const indicator = container.querySelector('.drop-indicator');
if (indicator) indicator.classList.remove('visible');
}
function handleCardDragEnter(e) {
if (e.target.closest('.card') === state.draggedCard) return;
const card = e.target.closest('.card');
if (card) card.classList.add('drag-over');
}
function handleCardDragLeave(e) {
const card = e.target.closest('.card');
if (card && !card.contains(e.relatedTarget)) {
card.classList.remove('drag-over');
}
}
function handleCardDrop(e) {
e.preventDefault();
const targetCard = e.target.closest('.card');
if (!targetCard || targetCard === state.draggedCard) return;
const container = targetCard.closest('.cards-container');
const column = container.closest('.column');
const targetColumn = column.dataset.column;
const insertBeforeId = parseInt(targetCard.dataset.id, 10);
const cardId = parseInt(e.dataTransfer.getData('text/plain'), 10);
if (cardId) {
moveCard(cardId, targetColumn, insertBeforeId);
}
}
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.card:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
}
return closest;
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function createDropIndicator(container) {
const indicator = document.createElement('div');
indicator.className = 'drop-indicator';
return indicator;
}
// Utility
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize
init();
</script>
</body>
</html>Reasoning trace (268 tokens)
The user wants an interactive Kanban board with specific requirements. Let me break down what's needed: 1. Three columns: To do, In progress, Done with card counts 2. 6 pre-populated realistic cards spread across columns 3. Each card has title and priority (low/medium/high) with colored badge 4. Add card via modal form (title required) 5. Delete card with button on card 6. Drag and drop between columns with visual drop indicator 7. Search field filtering cards live by title 8. Light/dark theme toggle 9. State in memory only (no storage APIs) 10. Must work in 1280x800 and adapt to 375px 11. No external dependencies 12. Must be self-contained HTML with inline CSS/JS 13. No console errors 14. Visible on first paint Let me design this carefully with good visual polish, smooth animations, and proper drag-and-drop UX. I'll use: - CSS Grid/Flexbox for layout - Native HTML5 Drag and Drop API - CSS custom properties for theming - Vanilla JS with clean event delegation - Nice visual feedback for drag operations Let me write the complete code.