Dots3-Note Preview (free)
by Dots StudioReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 20 s
- Cost
- $0
- Output tokens
- 12,579
- Spent thinking
- 9,614 tokens
- Speed
- 90.2 tok/s
- Console errors
- 0
- First token after
- 880 ms
- First visible text after
- 1 min 51 s
- Prompt tokens
- 492
- Code size
- 13.3 KB
- Finish reason
- stop
- Served by
- AtlasCloud
- Model id
dots-studio/dots-3-note-preview:free- Reasoning sent
model default- 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 · 521 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-color: #f0f2f5;
--text-color: #333;
--card-bg: #fff;
--column-bg: #e2e8f0;
--header-bg: #cbd5e1;
--accent: #4a90e2;
--danger: #e53e3e;
--low: #48bb78;
--medium: #ecc94b;
--high: #f56565;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dark {
--bg-color: #1a202c;
--text-color: #e2e8f0;
--card-bg: #2d3748;
--column-bg: #4a5568;
--header-bg: #718096;
--accent: #63b3ed;
--danger: #fc8181;
--low: #68d391;
--medium: #fbd38d;
--high: #fc8181;
--shadow: 0 2px 4px rgba(0,0,0,0.3);
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background: var(--bg-color);
color: var(--text-color);
transition: background 0.3s, color 0.3s;
overflow-x: hidden;
}
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 10px;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--card-bg);
box-shadow: var(--shadow);
border-radius: 8px;
margin-bottom: 1rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.top-bar h1 {
margin: 0;
font-size: 1.5rem;
}
.controls {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
#search {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
background: var(--card-bg);
color: var(--text-color);
min-width: 150px;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
#add-card {
background: var(--accent);
color: #fff;
}
#theme-toggle {
background: var(--column-bg);
color: var(--text-color);
}
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1rem;
}
@media (max-width: 768px) {
.board {
grid-template-columns: 1fr;
}
}
.column {
background: var(--column-bg);
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
min-height: 300px;
}
.column-header {
padding: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--header-bg);
color: var(--text-color);
font-weight: bold;
}
.card-count {
background: var(--accent);
color: #fff;
border-radius: 12px;
padding: 2px 8px;
font-size: 0.8rem;
}
.card-list {
padding: 0.5rem;
overflow-y: auto;
flex: 1;
}
.card {
background: var(--card-bg);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.5rem;
box-shadow: var(--shadow);
position: relative;
cursor: grab;
border-left: 4px solid var(--low);
}
.card.low { border-left-color: var(--low); }
.card.medium { border-left-color: var(--medium); }
.card.high { border-left-color: var(--high); }
.card .title {
display: block;
margin-right: 2.5rem;
word-break: break-word;
}
.priority-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.7rem;
text-transform: uppercase;
color: #fff;
}
.priority-badge.low { background: var(--low); }
.priority-badge.medium { background: var(--medium); }
.priority-badge.high { background: var(--high); }
.delete-btn {
position: absolute;
top: 0.25rem;
right: 0.25rem;
background: none;
border: none;
color: var(--danger);
cursor: pointer;
font-size: 1rem;
display: none;
line-height: 1;
padding: 0 4px;
}
.card:hover .delete-btn {
display: block;
}
.card.dragging {
opacity: 0.5;
}
.column.drag-over {
outline: 2px dashed var(--accent);
outline-offset: -4px;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--card-bg);
color: var(--text-color);
padding: 1.5rem;
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: var(--shadow);
}
.modal-content h2 {
margin-top: 0;
}
.modal-content label {
display: block;
margin-bottom: 0.75rem;
}
.modal-content input[type="text"],
.modal-content select {
width: 100%;
padding: 0.5rem;
margin-top: 0.25rem;
border: 1px solid #ccc;
border-radius: 4px;
background: var(--card-bg);
color: var(--text-color);
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
.modal-buttons button[type="submit"] {
background: var(--accent);
color: #fff;
}
.modal-buttons button[type="button"] {
background: var(--column-bg);
color: var(--text-color);
}
</style>
</head>
<body>
<div class="container">
<header class="top-bar">
<h1>Kanban Board</h1>
<div class="controls">
<input type="text" id="search" placeholder="Search cards...">
<button id="theme-toggle">Toggle Theme</button>
<button id="add-card">+ Add Card</button>
</div>
</header>
<div class="board" id="board">
<!-- Columns will be generated by JavaScript -->
</div>
</div>
<!-- Modal for adding a card -->
<div id="modal" class="modal">
<div class="modal-content">
<h2>Add New Card</h2>
<form id="card-form">
<label>
Title
<input type="text" id="title" required>
</label>
<label>
Priority
<select id="priority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</label>
<div class="modal-buttons">
<button type="submit">Add</button>
<button type="button" id="cancel">Cancel</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const board = document.getElementById('board');
const searchInput = document.getElementById('search');
const addCardBtn = document.getElementById('add-card');
const themeToggle = document.getElementById('theme-toggle');
const modal = document.getElementById('modal');
const cardForm = document.getElementById('card-form');
const cancelBtn = document.getElementById('cancel');
const titleInput = document.getElementById('title');
const prioritySelect = document.getElementById('priority');
// Pre-populated cards
let cards = [
{ id: 1, title: 'Write unit tests', priority: 'high', column: 'todo' },
{ id: 2, title: 'Design landing page', priority: 'medium', column: 'todo' },
{ id: 3, title: 'Implement login API', priority: 'high', column: 'inprogress' },
{ id: 4, title: 'Refactor user model', priority: 'low', column: 'inprogress' },
{ id: 5, title: 'Set up CI pipeline', priority: 'medium', column: 'done' },
{ id: 6, title: 'Write README', priority: 'low', column: 'done' }
];
let nextId = 7;
let currentFilter = '';
const columnConfigs = [
{ key: 'todo', title: 'To do' },
{ key: 'inprogress', title: 'In progress' },
{ key: 'done', title: 'Done' }
];
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function createCardElement(card) {
const cardEl = document.createElement('div');
cardEl.className = `card ${card.priority}`;
cardEl.dataset.id = card.id;
cardEl.draggable = true;
cardEl.innerHTML = `
<span class="title">${escapeHtml(card.title)}</span>
<span class="priority-badge ${card.priority}">${card.priority}</span>
<button class="delete-btn" title="Delete">✕</button>
`;
return cardEl;
}
function renderBoard() {
board.innerHTML = '';
const filteredCards = cards.filter(card =>
card.title.toLowerCase().includes(currentFilter.toLowerCase())
);
columnConfigs.forEach(colConfig => {
const column = document.createElement('section');
column.className = 'column';
column.dataset.column = colConfig.key;
const header = document.createElement('div');
header.className = 'column-header';
const title = document.createElement('h2');
title.textContent = colConfig.title;
const count = document.createElement('span');
count.className = 'card-count';
const totalInColumn = cards.filter(c => c.column === colConfig.key).length;
count.textContent = totalInColumn;
header.appendChild(title);
header.appendChild(count);
const cardList = document.createElement('div');
cardList.className = 'card-list';
filteredCards
.filter(c => c.column === colConfig.key)
.forEach(card => {
cardList.appendChild(createCardElement(card));
});
column.appendChild(header);
column.appendChild(cardList);
board.appendChild(column);
});
}
// Drag and drop
let draggedId = null;
board.addEventListener('dragstart', e => {
const card = e.target.closest('.card');
if (card) {
draggedId = parseInt(card.dataset.id, 10);
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedId);
}
});
board.addEventListener('dragover', e => {
const column = e.target.closest('.column');
if (column) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
document.querySelectorAll('.column').forEach(col => col.classList.remove('drag-over'));
column.classList.add('drag-over');
}
});
board.addEventListener('drop', e => {
const column = e.target.closest('.column');
if (column && draggedId) {
e.preventDefault();
const newColumn = column.dataset.column;
const card = cards.find(c => c.id === draggedId);
if (card && card.column !== newColumn) {
card.column = newColumn;
renderBoard();
}
document.querySelectorAll('.column').forEach(col => col.classList.remove('drag-over'));
draggedId = null;
}
});
board.addEventListener('dragend', e => {
const card = e.target.closest('.card');
if (card) card.classList.remove('dragging');
draggedId = null;
document.querySelectorAll('.column').forEach(col => col.classList.remove('drag-over'));
});
// Delete card
board.addEventListener('click', e => {
if (e.target.classList.contains('delete-btn')) {
const cardEl = e.target.closest('.card');
const id = parseInt(cardEl.dataset.id, 10);
cards = cards.filter(c => c.id !== id);
renderBoard();
}
});
// Search filter
searchInput.addEventListener('input', e => {
currentFilter = e.target.value;
renderBoard();
});
// Add card modal
addCardBtn.addEventListener('click', () => {
modal.style.display = 'flex';
titleInput.focus();
});
cancelBtn.addEventListener('click', () => {
modal.style.display = 'none';
cardForm.reset();
});
modal.addEventListener('click', e => {
if (e.target === modal) {
modal.style.display = 'none';
cardForm.reset();
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && modal.style.display === 'flex') {
modal.style.display = 'none';
cardForm.reset();
}
});
cardForm.addEventListener('submit', e => {
e.preventDefault();
const title = titleInput.value.trim();
if (!title) return;
const priority = prioritySelect.value;
const newCard = {
id: nextId++,
title,
priority,
column: 'todo'
};
cards.push(newCard);
renderBoard();
modal.style.display = 'none';
cardForm.reset();
});
// Theme toggle
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark');
});
// Initial render
renderBoard();
});
</script>
</body>
</html>Reasoning trace (9,614 tokens)
We need to produce a single HTML file with inline CSS and JS. Must be self-contained, no external resources, except allowed CDN? The task says no external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. But here no CDN is mentioned, so we should avoid any external requests. Use only inline CSS/JS.
We need to build interactive Kanban board. Requirements:
- Three columns: "To do", "In progress", "Done", each showing its card count.
- Pre-populate with 6 realistic cards spread across columns.
- Each card has title and priority (low/medium/high) shown as colored badge.
- Add card via modal form (title required), delete card with button.
- Drag and drop cards between columns with mouse, with clear visual drop indicator.
- Search field that filters cards live by title.
- Light/dark theme toggle.
- State lives in memory only.
We need to ensure responsive design, works in 1280x800 and down to 375px, no horizontal scrollbar.
We must output exactly one fenced code block with the complete file.
We need to implement drag and drop using HTML5 drag and drop API or custom mouse events. Probably use HTML5 DnD for simplicity, but need to show drop indicator. Could use dragstart, dragover, drop events.
We also need to update card counts in each column header.
We need to handle search filter: as user types, hide cards that don't match title (case-insensitive). Should keep cards in their columns, just hide.
We need modal for adding card: could be a simple div overlay with form, title input, maybe priority select? Requirement says title required, but could include priority? It says add a card through a small modal form (title required). It doesn't forbid extra fields, but at least title. Probably include priority selection as well, but not required. But each card has priority, so we need to set priority. Could default to medium. Could include dropdown.
But requirement: "Add a card through a small modal form (title required)" - so we need a modal with at least title field. Could also include priority. We'll include priority select.
Delete button on each card.
Theme toggle: light/dark, toggle class on body.
State in memory: we'll have an array of cards, each with id, title, priority, column (status). We'll render based on state.
We need to persist state only in memory; no storage.
We need to pre-populate with 6 realistic cards spread across columns. For example:
To do:
- Write unit tests
- Design landing page
In progress:
- Implement login API
- Refactor user model
Done:
- Set up CI pipeline
- Write README
We need to assign priorities.
We'll create cards with id, title, priority, column.
We'll implement drag and drop: each card draggable, column as drop target. On dragstart, store card id. On dragover column, prevent default, show drop indicator (maybe a line or highlight). On drop, move card to that column, update state, re-render.
We need to update counts.
Search: input event, filter cards by title.
Modal: open button, close on background click or cancel. On submit, add new card to "To do" column (or maybe to current column? Probably to "To do").
We'll implement with vanilla JS.
We need to ensure visual drop indicator: perhaps add a class to column while dragging over, or insert a placeholder element.
We can use HTML5 DnD with dragover event, and set effectAllowed, dropEffect. On dragover, we can add a class "drag-over" to column. On drop, remove class.
Alternatively, we can use a custom mouse-based drag with mousedown, mousemove, mouseup. But HTML5 DnD is simpler.
However, we need to support mobile? Not required.
We'll use HTML5 DnD.
Potential issues: drag and drop with HTML5 API can be tricky with event delegation, but we can attach events to cards and columns.
We'll assign draggable="true" to each card.
We'll attach dragstart event to card, set dataTransfer.setData("text/plain", cardId). Also maybe set effectAllowed.
We'll attach dragover event to column, prevent default, add drop indicator.
We'll attach drop event to column, retrieve card id, move card.
We'll also attach dragend to clean up.
We need to update card counts after move.
We'll re-render the board after each change? Could manipulate DOM directly, but easier to re-render entire board from state. However, re-rendering on each drop might cause flicker. But it's okay.
Alternatively, we can move the DOM element directly and update counts. That might be smoother.
But we also have search filter, which hides cards. If we re-render, we need to preserve filter.
We can keep state and then render filtered view.
We'll implement a render function that clears columns and appends cards based on state, filtered by search term.
But drag and drop with re-render might cause loss of drag state? We can re-render after drop.
Simplify: On drop, update state (change column), then re-render board. That will also update counts.
But during drag, we might want to show drop indicator without re-render. We can add a class to column while dragging over, and on drop, re-render.
We'll attach dragover to column, prevent default, add class "drag-over". On drop, remove class, update state, re-render.
On dragleave, remove class.
We'll also need to handle dragstart to store card id.
We'll use event delegation: attach dragstart to board container, but we need to know which card.
Simplify: assign each card element dataset.id.
In dragstart, set dataTransfer.setData("text/plain", card.id).
In dragover column, prevent default, add class.
In drop column, get id from dataTransfer.getData("text/plain"), find card object, change column, re-render.
But re-render will recreate cards, so drag ends.
That's fine.
We need to ensure that while dragging, the card is not hidden by search filter? The card being dragged should remain visible. But if we re-render after drop, it's fine.
During dragover, we might want to show drop indicator. We'll add a class to column.
We'll also need to prevent default on dragover for column to allow drop.
We'll also need to handle dragover on body? Not needed.
We'll implement.
Now, layout: board with three columns, each column has header with title and count, and card list.
Add card button above board.
Search field above board.
Theme toggle button.
Modal overlay.
We'll use CSS grid or flexbox for columns.
We'll make responsive: on small screens, columns stack vertically? Could be horizontal scroll? Requirement: adapt down to 375px wide, no horizontal scrollbar. So we need to make columns fit within viewport. Perhaps use flex with min-width: 0, overflow-x: auto? But no horizontal scrollbar allowed. So better to stack columns vertically on small screens, or use horizontal scroll? The requirement says no horizontal scrollbar, so we must avoid overflow-x: auto that shows scrollbar. We can make columns stack vertically using media queries.
We'll design with CSS grid: grid-template-columns: repeat(3, 1fr) on large screens. On small screens, maybe each column takes full width.
We'll set max-width: 1280px, margin: 0 auto.
We'll ensure body has no horizontal overflow.
We'll use box-sizing: border-box.
We'll implement dark theme by toggling class "dark" on body, and adjust colors via CSS variables.
We'll define CSS variables for background, text, card background, etc.
We'll set default light theme.
We'll add button to toggle theme.
Now, let's plan code structure.
HTML:
<!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>
/* CSS variables */
:root {
--bg-color: #f0f2f5;
--text-color: #333;
--card-bg: #fff;
--column-bg: #e2e8f0;
--header-bg: #cbd5e1;
--accent: #4a90e2;
--danger: #e53e3e;
--low: #48bb78;
--medium: #ecc94b;
--high: #f56565;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dark {
--bg-color: #1a202c;
--text-color: #e2e8f0;
--card-bg: #2d3748;
--column-bg: #4a5568;
--header-bg: #718096;
--accent: #63b3ed;
--danger: #fc8181;
--low: #68d391;
--medium: #fbd38d;
--high: #fc8181;
--shadow: 0 2px 4px rgba(0,0,0,0.3);
}
/* rest of styles */
</style>
</head>
<body>
<div class="container">
<header class="top-bar">
<h1>Kanban Board</h1>
<div class="controls">
<input type="text" id="search" placeholder="Search cards...">
<button id="theme-toggle">Toggle Theme</button>
<button id="add-card">+ Add Card</button>
</div>
</header>
<div class="board" id="board">
<!-- columns will be generated by JS -->
</div>
</div>
<!-- Modal -->
<div id="modal" class="modal" style="display:none;">
<div class="modal-content">
<h2>Add New Card</h2>
<form id="card-form">
<label>Title <input type="text" id="title" required></label>
<label>Priority
<select id="priority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</label>
<div class="modal-buttons">
<button type="submit">Add</button>
<button type="button" id="cancel">Cancel</button>
</div>
</form>
</div>
</div>
<script>
// JS code
</script>
</body>
</html>
We'll generate columns via JS.
We'll define state array.
We'll render columns and cards.
We'll attach events.
Now, implement.
First, define columns: To do, In progress, Done.
State: cards array.
Prepopulate:
const initialCards = [
{ id: 1, title: 'Write unit tests', priority: 'high', column: 'todo' },
{ id: 2, title: 'Design landing page', priority: 'medium', column: 'todo' },
{ id: 3, title: 'Implement login API', priority: 'high', column: 'inprogress' },
{ id: 4, title: 'Refactor user model', priority: 'low', column: 'inprogress' },
{ id: 5, title: 'Set up CI pipeline', priority: 'medium', column: 'done' },
{ id: 6, title: 'Write README', priority: 'low', column: 'done' }
];
We'll assign column values: 'todo', 'inprogress', 'done'.
We'll create column config array: [{ key: 'todo', title: 'To do' }, ...]
We'll render board: for each column, create section with header containing title and count, then list for cards.
We'll filter cards by search term.
We'll attach drag and drop events.
We'll implement modal.
We'll implement theme toggle.
Now, write JS.
We'll use document.addEventListener('DOMContentLoaded', init).
In init:
- Load initial cards into state variable.
- Render board.
- Attach event listeners: search input, add card button, modal form, theme toggle.
- Implement drag and drop using HTML5 DnD.
We'll need to re-render after drop.
But we also need to keep search filter applied after re-render.
We can store current search term.
We'll have a function renderBoard() that clears board and rebuilds based on filtered cards.
We'll attach dragstart event to each card element after rendering.
We'll use event delegation for dragstart? Could attach to board container.
Simpler: after rendering, add dragstart listener to each card.
But re-rendering will recreate cards, so we need to attach after each render.
We'll create a function attachDragEvents() that selects all cards and adds event listeners.
We'll also attach dragover/drop to columns.
We can attach dragover/drop to column elements (which are static, not re-created each render? Actually we will create column elements each render, so we need to attach after each render as well.
Alternatively, we can use event delegation on board container: listen for dragstart, dragover, drop events on board.
But dragstart originates from card, we can listen on board and check target.
We'll use board.addEventListener('dragstart', handler) etc.
But we need to ensure that cards are draggable and set draggable attribute.
We'll set draggable="true" on each card element.
We'll attach dragstart to board: e.target if matches card.
We'll attach dragover to column elements (maybe we can attach to board and check if target is column or inside column). But easier: after rendering, add listeners to each column.
But re-rendering will remove columns, so we need to reattach.
We can use delegation: board.addEventListener('dragover', handler) and check if e.target is a column (has class 'column'). Similarly for drop.
But dragover needs to be prevented on column to allow drop.
We'll attach to board.
We'll also need to add dragover listener to document to allow drop? Usually you need to prevent default on dragover for drop target.
We'll implement.
We'll also need to update drop indicator: add class 'drag-over' to column while dragging over.
We'll handle dragleave to remove.
We'll implement.
Now, code.
Let's write JS.
We'll define state variable:
let cards = [...initialCards];
let currentFilter = '';
let currentTheme = 'light';
We'll define columnConfigs.
We'll define elements.
We'll write renderBoard():
- Clear board innerHTML.
- For each column config, create column element with class 'column', dataset.column = key.
- Create header with title and count (filtered count).
- Create card list container with class 'card-list'.
- For each card in cards where card.column === key and title matches filter, create card element.
- Append to list.
- Append column to board.
After rendering, attach drag events? We'll use delegation.
We'll attach dragstart to board: board.addEventListener('dragstart', onDragStart)
Similarly dragover: board.addEventListener('dragover', onDragOver)
drop: board.addEventListener('drop', onDrop)
dragend: board.addEventListener('dragend', onDragEnd) maybe.
We'll also attach dragover to body to allow drop? Not needed.
We'll implement onDragStart(e): if e.target is card, set dataTransfer.setData('text/plain', card.id), add dragging class maybe.
We'll implement onDragOver(e): if e.target is column or inside column, prevent default, add drag-over class to column.
We'll need to find closest column: e.target.closest('.column').
If found, prevent default, add class.
We'll also remove drag-over from other columns.
We'll implement onDrop(e): get id from dataTransfer, find card, update column to target column's data-column, remove drag-over class, re-render board.
We'll implement onDragEnd(e): clean up any dragging class.
We'll also need to handle dragover on document to allow drop? Usually you need to prevent default on dragover for drop target; we already do.
But if you drop outside column, nothing happens.
Now, search: input event, update currentFilter, re-render.
Add card: open modal, on submit, create new card with id (generate unique), title, priority, column: 'todo', push to cards, re-render, close modal.
Delete card: attach event to each card's delete button. Could use delegation: board.addEventListener('click', onClick) where if target is delete button, remove card from array, re-render.
Theme toggle: toggle class 'dark' on document.body or root.
We'll use CSS variables defined on :root and .dark.
We'll set theme based on class.
We'll also store theme preference? Not needed, state in memory only.
Now, implement modal: show/hide by setting display flex.
We'll add event listeners to open modal (add card button), close modal (cancel, background click).
We'll prevent form submission default.
Now, ensure accessibility? Not required.
Now, write code.
We'll need to generate unique IDs. Could use Date.now() or simple counter.
We'll use let nextId = 7; then increment.
Now, let's write the full HTML.
We'll include CSS for layout.
We'll make board display flex, columns flex: 1, with min-width 0.
On small screens, flex-direction column.
We'll set card max-width.
We'll style cards with background, padding, border-radius, shadow.
Priority badge: small circle or text with background color.
We'll use classes low, medium, high.
We'll style delete button: absolute top right, background transparent, border none, cursor pointer, color var(--danger).
We'll make card position relative.
We'll style modal overlay fixed, background rgba(0,0,0,0.5), display flex, align center, justify center.
Modal content background, padding, border-radius.
We'll make it responsive.
Now, write code.
We'll also ensure no horizontal scrollbar: set body overflow-x: hidden; but better to set max-width: 100vw, overflow-x: hidden; but we can just ensure content fits.
We'll set box-sizing: border-box.
We'll set body margin:0.
We'll set container max-width: 1280px; margin: 0 auto; padding: 0 10px.
We'll set board display: flex; gap: 10px; overflow-x: auto; but that may show scrollbar. Instead, on large screens, use grid with 3 columns; on small screens, stack.
We'll use media query.
We'll set board display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;
At max-width 768px, grid-template-columns: 1fr; to stack.
That will avoid horizontal scrollbar.
We'll set column min-width: 0; overflow: auto? Not needed.
We'll set card list min-height: 100px; but we can let it grow.
We'll ensure each column has background var(--column-bg), border-radius, padding, etc.
We'll set column display: flex; flex-direction: column; min-height: 50vh; but we can set height: fit-content.
We'll set header display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; background: var(--header-bg); border-radius: 6px 6px 0 0; color: var(--text-color); font-weight: bold;
We'll set card list padding: 0.5rem; overflow-y: auto; max-height: 70vh; but we can let it auto.
We'll set card background var(--card-bg); color var(--text-color); border-radius: 6px; padding: 0.75rem; margin-bottom: 0.5rem; box-shadow: var(--shadow); position: relative; cursor: grab; border-left: 4px solid priority color? Could add.
We'll add priority badge: position absolute; top: 0.5rem; right: 0.5rem; padding: 2px 6px; border-radius: 10px; font-size: 0.7rem; text-transform: uppercase; color: #fff; background: var(--priority-color).
We'll set delete button: position absolute; top: 0.25rem; right: 0.25rem; background: none; border: none; color: var(--danger); cursor: pointer; font-size: 1rem; display: none; maybe show on hover.
We'll show delete button on hover via :hover .delete-btn { display: block; } or always visible? Could be always visible. We'll make it visible on hover.
We'll set card:hover .delete-btn { display: block; }
We'll also add dragging class: .dragging { opacity: 0.5; }
We'll add drop indicator: column.drag-over { outline: 2px dashed var(--accent); outline-offset: -4px; }
Now, implement JS.
We'll write code in script tag.
Let's outline:
document.addEventListener('DOMContentLoaded', () => {
const board = document.getElementById('board');
const searchInput = document.getElementById('search');
const addCardBtn = document.getElementById('add-card');
const themeToggle = document.getElementById('theme-toggle');
const modal = document.getElementById('modal');
const cardForm = document.getElementById('card-form');
const cancelBtn = document.getElementById('cancel');
const titleInput = document.getElementById('title');
const prioritySelect = document.getElementById('priority');
let cards = [
{ id: 1, title: 'Write unit tests', priority: 'high', column: 'todo' },
{ id: 2, title: 'Design landing page', priority: 'medium', column: 'todo' },
{ id: 3, title: 'Implement login API', priority: 'high', column: 'inprogress' },
{ id: 4, title: 'Refactor user model', priority: 'low', column: 'inprogress' },
{ id: 5, title: 'Set up CI pipeline', priority: 'medium', column: 'done' },
{ id: 6, title: 'Write README', priority: 'low', column: 'done' }
];
let nextId = 7;
let currentFilter = '';
const columnConfigs = [
{ key: 'todo', title: 'To do' },
{ key: 'inprogress', title: 'In progress' },
{ key: 'done', title: 'Done' }
];
function renderBoard() {
board.innerHTML = '';
const filteredCards = cards.filter(card => card.title.toLowerCase().includes(currentFilter.toLowerCase()));
columnConfigs.forEach(colConfig => {
const column = document.createElement('section');
column.className = 'column';
column.dataset.column = colConfig.key;
const header = document.createElement('div');
header.className = 'column-header';
const title = document.createElement('h2');
title.textContent = colConfig.title;
const count = document.createElement('span');
count.className = 'card-count';
const countNum = filteredCards.filter(c => c.column === colConfig.key).length;
count.textContent = countNum;
header.appendChild(title);
header.appendChild(count);
const cardList = document.createElement('div');
cardList.className = 'card-list';
filteredCards.filter(c => c.column === colConfig.key).forEach(card => {
cardList.appendChild(createCardElement(card));
});
column.appendChild(header);
column.appendChild(cardList);
board.appendChild(column);
});
}
function createCardElement(card) {
const cardEl = document.createElement('div');
cardEl.className = 'card';
cardEl.dataset.id = card.id;
cardEl.draggable = true;
cardEl.innerHTML = `
<span class="title">${escapeHtml(card.title)}</span>
<span class="priority-badge ${card.priority}">${card.priority}</span>
<button class="delete-btn" title="Delete">✕</button>
`;
return cardEl;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Drag and drop
board.addEventListener('dragstart', onDragStart);
board.addEventListener('dragover', onDragOver);
board.addEventListener('drop', onDrop);
board.addEventListener('dragend', onDragEnd);
board.addEventListener('dragleave', onDragLeave);
let draggedId = null;
function onDragStart(e) {
if (e.target.classList.contains('card')) {
draggedId = parseInt(e.target.dataset.id, 10);
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedId);
}
}
function onDragOver(e) {
const column = e.target.closest('.column');
if (column) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
// Remove drag-over from all columns
document.querySelectorAll('.column').forEach(col => col.classList.remove('drag-over'));
column.classList.add('drag-over');
}
}
function onDrop(e) {
const column = e.target.closest('.column');
if (column && draggedId) {
e.preventDefault();
const newColumn = column.dataset.column;
const card = cards.find(c => c.id === draggedId);
if (card && card.column !== newColumn) {
card.column = newColumn;
renderBoard();
}
// Clean up
document.querySelectorAll('.column').forEach(col => col.classList.remove('drag-over'));
draggedId = null;
}
}
function onDragEnd(e) {
if (e.target.classList.contains('card')) {
e.target.classList.remove('dragging');
}
draggedId = null;
document.querySelectorAll('.column').forEach(col => col.classList.remove('drag-over'));
}
function onDragLeave(e) {
// Remove drag-over from column if leaving it
const column = e.target.closest('.column');
if (column) {
// We could check relatedTarget, but simpler to remove on dragend/drop
}
}
// Delete card
board.addEventListener('click', e => {
if (e.target.classList.contains('delete-btn')) {
const cardEl = e.target.closest('.card');
const id = parseInt(cardEl.dataset.id, 10);
cards = cards.filter(c => c.id !== id);
renderBoard();
}
});
// Search
searchInput.addEventListener('input', e => {
currentFilter = e.target.value;
renderBoard();
});
// Add card
addCardBtn.addEventListener('click', () => {
modal.style.display = 'flex';
titleInput.focus();
});
cancelBtn.addEventListener('click', () => {
modal.style.display = 'none';
cardForm.reset();
});
// Close modal when clicking outside content
modal.addEventListener('click', e => {
if (e.target === modal) {
modal.style.display = 'none';
cardForm.reset();
}
});
cardForm.addEventListener('submit', e => {
e.preventDefault();
const title = titleInput.value.trim();
if (!title) return;
const priority = prioritySelect.value;
const newCard = {
id: nextId++,
title,
priority,
column: 'todo'
};
cards.push(newCard);
renderBoard();
modal.style.display = 'none';
cardForm.reset();
});
// Theme toggle
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark');
});
// Initial render
renderBoard();
});
Now, we need to ensure that drag and drop works with re-rendering. After drop, we re-render board, which removes the dragging class and resets.
But during dragover, we add drag-over class. After drop, we remove it.
Potential issue: dragover event may not fire on column if we don't prevent default on dragover for the column. We do prevent default.
But we need to ensure that dragover event is fired when dragging over column. Since we attach to board, and use closest('.column'), it should work.
But we also need to prevent default on dragover for the document to allow drop? Usually you need to prevent default on dragover for the drop target. We do that.
But if the user drags outside column, nothing happens.
Now, we need to ensure that the card being dragged is not hidden by filter. If the card is hidden by filter, the dragstart may still fire? But if card is hidden, its element still exists? In our render, we only create elements for cards that match filter. So if a card is hidden, its element is not in DOM, so cannot be dragged. That's fine.
But if user types filter that hides the card being dragged, it's not possible because the card element is not present. However, if the card is in a column but hidden because filter doesn't match, it won't be rendered. So drag cannot start. That's okay.
But after drop, we re-render with current filter, so the card will appear in new column if matches filter.
Now, we need to ensure that the drop indicator is clear. We'll use outline dashed.
We'll also add a class to column while dragging over.
Now, we need to ensure that the board layout works on small screens.
We'll add CSS:
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1rem;
}
@media (max-width: 768px) {
.board {
grid-template-columns: 1fr;
}
}
We'll set column background, padding, etc.
We'll set card list min-height: 200px; but can be auto.
We'll set column background var(--column-bg); border-radius: 8px; overflow: hidden; box-shadow: var(--shadow);
We'll set column-header padding: 0.75rem; color: var(--text-color); background: var(--header-bg); display: flex; justify-content: space-between; align-items: center;
We'll set card-count background: var(--accent); color: #fff; border-radius: 12px; padding: 2px 8px; font-size: 0.8rem;
We'll set card background var(--card-bg); border-radius: 6px; padding: 0.75rem; margin-bottom: 0.5rem; box-shadow: var(--shadow); position: relative; cursor: grab; border-left: 4px solid var(--priority-color);
But we need to set priority colors via CSS variables.
We'll define:
.card.low { border-left-color: var(--low); }
.card.medium { border-left-color: var(--medium); }
.card.high { border-left-color: var(--high); }
Priority badge: we'll style.
.priority-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.7rem;
text-transform: uppercase;
color: #fff;
background: var(--priority-color);
}
But we need to set background per priority. We can use classes:
.priority-badge.low { background: var(--low); }
.priority-badge.medium { background: var(--medium); }
.priority-badge.high { background: var(--high); }
We'll also set delete button.
.delete-btn {
position: absolute;
top: 0.25rem;
right: 0.25rem;
background: none;
border: none;
color: var(--danger);
cursor: pointer;
font-size: 1rem;
display: none;
}
.card:hover .delete-btn {
display: block;
}
We'll also style modal.
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--card-bg);
color: var(--text-color);
padding: 1.5rem;
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: var(--shadow);
}
.modal-content h2 {
margin-top: 0;
}
.modal-content label {
display: block;
margin-bottom: 0.75rem;
}
.modal-content input[type="text"],
.modal-content select {
width: 100%;
padding: 0.5rem;
margin-top: 0.25rem;
border: 1px solid #ccc;
border-radius: 4px;
background: var(--card-bg);
color: var(--text-color);
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
.modal-buttons button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.modal-buttons button[type="submit"] {
background: var(--accent);
color: #fff;
}
.modal-buttons button[type="button"] {
background: var(--column-bg);
color: var(--text-color);
}
We'll also style top bar.
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--card-bg);
box-shadow: var(--shadow);
margin-bottom: 1rem;
border-radius: 8px;
}
.top-bar h1 {
margin: 0;
font-size: 1.5rem;
}
.controls {
display: flex;
gap: 0.5rem;
}
#search {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
background: var(--card-bg);
color: var(--text-color);
}
#theme-toggle, #add-card {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
background: var(--accent);
color: #fff;
}
#theme-toggle {
background: var(--column-bg);
color: var(--text-color);
}
We'll also set overall body styles.
body {
background: var(--bg-color);
color: var(--text-color);
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 10px;
}
We'll also set scrollbar style? Not needed.
Now, ensure that drag and drop works on touch? Not required.
We'll also ensure that the card count updates after filtering. In renderBoard, we compute count based on filteredCards, which is correct.
But note: when filtering, the count should reflect only visible cards? The requirement says "each showing its card count." It might mean total cards in column, not filtered. But typical Kanban board shows total count, not filtered. However, they might want filtered count? The requirement: "Three columns: 'To do', 'In progress', 'Done', each showing its card count." It doesn't specify filtered or total. Usually it's total. But we can show total count regardless of filter. That might be more useful. However, if we filter, the count might be misleading. But we can show total count always. Let's show total count (all cards in that column). That is simpler and matches typical board.
We'll compute count from cards, not filteredCards.
So in renderBoard, for each column, count = cards.filter(c => c.column === colConfig.key).length.
We'll still filter cards for display.
Thus, we need to separate filtered cards for rendering.
We'll create filteredCards for display, but count from all cards.
We'll adjust.
In renderBoard:
const filteredCards = cards.filter(card => card.title.toLowerCase().includes(currentFilter.toLowerCase()));
Then for each column, count = cards.filter(c => c.column === colConfig.key).length.
Display count.
Then render cards from filteredCards.
That's fine.
Now, we need to ensure that drag and drop works with filtered view. If a card is hidden, you can't drag it. That's okay.
But when you drop a card into a column, it will appear if matches filter.
Now, we need to ensure that the drop indicator works when dropping on column that has no cards or filtered out. The column element exists regardless of cards, so drop target is there.
Now, we need to handle case where draggedId is set but drop target is not a column. Then nothing happens.
Now, we need to ensure that dragover event is triggered for column even if it's empty. It will be.
Now, we need to ensure that we prevent default on dragover for the board? We only prevent when column found.
But if we drag over board but not column, we don't prevent default; that's fine.
Now, we need to ensure that the dragstart event is fired on card. We set draggable true.
But we also need to set dragstart on card element; we attach to board via delegation. The event target will be the card.
We need to ensure that the event listener is attached after rendering, because board innerHTML is replaced. But we attach listeners to board element, which persists. So delegation works.
But we need to ensure that the dragstart event is triggered on the card element. Since we set draggable true, it will fire.
Now, we need to ensure that the card element is not removed during drag? It stays.
Now, we need to ensure that the dragover event fires when dragging over column. Since we attach to board, and event bubbles, it will reach board. But we need to ensure that we call e.preventDefault() in dragover to allow drop. We do.
Now, we need to ensure that drop event fires on column. It will bubble to board.
Now, we need to ensure that after drop, we re-render board. This will remove the dragging class and reset.
But re-rendering will also remove the card from old column and add to new column. That's fine.
Potential issue: during re-render, the dragend event may not fire? But we also have dragend listener. After drop, dragend will fire. In our onDragEnd, we remove dragging class. But after re-render, the card element is replaced, so the dragging class is gone anyway. But we can keep.
We'll also call onDragEnd after drop? The drop event fires before dragend. In onDrop, we re-render, which removes the old card element. Then dragend fires on the original element? The element may be removed, but the event still fires on the detached element? Might not cause issues.
We can also in onDrop, set draggedId = null after re-render.
We'll also remove dragover class.
Now, we need to ensure that the drop indicator is removed after drop or dragend.
We'll remove in both.
Now, we need to handle case where user drops outside column. Then dragend fires, we clean up.
Now, we need to ensure that the modal form works.
We'll add event listeners.
Now, we need to ensure that the search input does not trigger any other issues.
Now, we need to ensure that the theme toggle works.
We'll add class 'dark' to body.
Now, we need to ensure that the board is accessible? Not required.
Now, we need to ensure that the code runs with zero errors.
We'll test mentally.
Potential errors:
- In onDragStart, we check if e.target.classList.contains('card'). But e.target could be child element inside card (like title span). The event target is the element on which the listener is triggered. Since we attach to board, the target is the element that received the event. For dragstart, it's the element that is draggable. If we click on child, the target might be child, but the dragstart event is fired on the draggable element. Actually, the dragstart event is fired on the element that is being dragged, which is the card element. So e.target should be the card element. But if we have child elements, the event target might be the child? According to spec, dragstart is fired on the element that the user started dragging, which is the draggable element. So e.target should be the card element. However, if we attach listener to board, the event bubbles? The dragstart event does bubble? I think dragstart bubbles. So e.target will be the card element. So our check is fine.
But to be safe, we can use e.target.closest('.card').
We'll modify onDragStart:
const card = e.target.closest('.card');
if (card) {
draggedId = parseInt(card.dataset.id, 10);
card.classList.add('dragging');
...
}
Similarly for onDragEnd, we can use e.target.closest('.card').
But dragend also fires on the element.
We'll adjust.
In onDragOver, we use e.target.closest('.column').
In onDrop, similarly.
Now, we need to ensure that we prevent default on dragover only when over column. That's fine.
Now, we need to ensure that we set dropEffect to move.
Now, we need to ensure that we don't have any console errors.
Now, we need to ensure that the board is responsive.
We'll add CSS for small screens.
Now, we need to ensure that the modal is accessible.
We'll add close on Escape key.
We'll add event listener for keydown on document when modal open.
But not required.
We'll add for completeness.
Now, let's write final code.
We'll include all CSS and JS in one HTML file.
We'll ensure no external resources.
We'll output as a single fenced code block.
Now, produce final answer.