Gemini 4 Argon
by Googlereasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 53 s
- Cost
- $0.128
- Output tokens
- 12,655
- Spent thinking
- 0 tokens
- Speed
- 74 tok/s
- Console errors
- 0
- First token after
- 2.1 s
- First visible text after
- 2.1 s
- Prompt tokens
- 548
- Code size
- 35.8 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-4-argon- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build an interactive Kanban board in vanilla HTML, CSS and JavaScript (no framework, no library). Requirements: - Three columns: "To do", "In progress", "Done", each showing its card count. - Pre-populate the board with 6 realistic cards spread across the columns so the first screenshot looks alive. - Each card has a title and a priority (low / medium / high) shown as a colored badge. - Add a card through a small modal form (title required), and delete a card with a button on it. - Drag and drop cards between columns with the mouse, with a clear visual drop indicator. - A search field that filters cards live by title. - A light / dark theme toggle. - State lives in memory only (no storage APIs).
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1232 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">
<title>Kanban Board</title>
<style>
:root[data-theme="light"] {
--bg-body: #f4f6f9;
--bg-header: #ffffff;
--bg-column: #ebecf0;
--bg-card: #ffffff;
--bg-card-hover: #f9fafc;
--bg-input: #f1f3f6;
--text-primary: #172b4d;
--text-secondary: #5e6c84;
--border-color: #dfe1e6;
--shadow-card: 0 2px 4px rgba(9, 30, 66, 0.08), 0 0 1px rgba(9, 30, 66, 0.12);
--shadow-card-hover: 0 6px 14px rgba(9, 30, 66, 0.14), 0 0 1px rgba(9, 30, 66, 0.16);
--shadow-modal: 0 20px 40px rgba(9, 30, 66, 0.25);
--accent: #4f46e5;
--accent-hover: #4338ca;
--accent-soft: rgba(79, 70, 229, 0.12);
--badge-high-bg: #fee2e2;
--badge-high-text: #dc2626;
--badge-medium-bg: #fef3c7;
--badge-medium-text: #d97706;
--badge-low-bg: #dcfce7;
--badge-low-text: #16a34a;
--danger-hover: #fee2e2;
--danger-text: #dc2626;
}
:root[data-theme="dark"] {
--bg-body: #0f1117;
--bg-header: #161923;
--bg-column: #161923;
--bg-card: #1f2433;
--bg-card-hover: #252b3d;
--bg-input: #0f1117;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--border-color: #2a3143;
--shadow-card: 0 2px 6px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.05);
--shadow-card-hover: 0 8px 18px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(99, 102, 241, 0.4);
--shadow-modal: 0 24px 48px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(255, 255, 255, 0.08);
--accent: #6366f1;
--accent-hover: #4f46e5;
--accent-soft: rgba(99, 102, 241, 0.16);
--badge-high-bg: rgba(239, 68, 68, 0.16);
--badge-high-text: #f87171;
--badge-medium-bg: rgba(245, 158, 11, 0.16);
--badge-medium-text: #fbbf24;
--badge-low-bg: rgba(34, 197, 94, 0.16);
--badge-low-text: #4ade80;
--danger-hover: rgba(239, 68, 68, 0.18);
--danger-text: #f87171;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, Helvetica, Arial, sans-serif;
background-color: var(--bg-body);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
transition: background-color 0.2s ease, color 0.2s ease;
}
/* Header */
.topbar {
background-color: var(--bg-header);
border-bottom: 1px solid var(--border-color);
padding: 0.85rem 1.75rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
position: sticky;
top: 0;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.brand-logo {
width: 34px;
height: 34px;
border-radius: 9px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
display: grid;
place-items: center;
color: #fff;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35);
flex-shrink: 0;
}
.brand-title {
font-size: 1.15rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.brand-subtitle {
font-size: 0.78rem;
color: var(--text-secondary);
font-weight: 500;
}
.controls {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
justify-content: flex-end;
max-width: 640px;
}
.search-wrapper {
position: relative;
flex: 1;
max-width: 300px;
min-width: 150px;
}
.search-wrapper svg {
position: absolute;
left: 11px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
pointer-events: none;
}
.search-input {
width: 100%;
padding: 0.55rem 2rem 0.55rem 2.25rem;
border-radius: 8px;
border: 1px solid var(--border-color);
background-color: var(--bg-input);
color: var(--text-primary);
font-size: 0.875rem;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.search-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.clear-search {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 2px;
border-radius: 4px;
display: none;
align-items: center;
justify-content: center;
}
.clear-search.visible {
display: flex;
}
.clear-search:hover {
color: var(--text-primary);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
padding: 0.55rem 0.95rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.15s ease;
white-space: nowrap;
}
.btn-primary {
background-color: var(--accent);
color: #ffffff;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
background-color: var(--accent-hover);
transform: translateY(-1px);
}
.btn-icon {
padding: 0.52rem;
background-color: var(--bg-input);
border-color: var(--border-color);
color: var(--text-primary);
}
.btn-icon:hover {
border-color: var(--accent);
color: var(--accent);
}
/* Board Layout */
.board-container {
flex: 1;
padding: 1.5rem 1.75rem;
max-width: 1440px;
width: 100%;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1.35rem;
align-items: start;
}
.column {
background-color: var(--bg-column);
border: 1px solid var(--border-color);
border-radius: 12px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 115px);
min-height: 460px;
transition: border-color 0.15s ease, background-color 0.2s ease, box-shadow 0.15s ease;
}
.column.drag-over {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.column-header {
padding: 1rem 1rem 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
}
.column-title-group {
display: flex;
align-items: center;
gap: 0.55rem;
}
.status-dot {
width: 9px;
height: 9px;
border-radius: 50%;
}
.status-dot.todo { background-color: #6366f1; box-shadow: 0 0 8px rgba(99, 102, 241, 0.6); }
.status-dot.inprogress { background-color: #f59e0b; box-shadow: 0 0 8px rgba(245, 158, 11, 0.6); }
.status-dot.done { background-color: #10b981; box-shadow: 0 0 8px rgba(16, 185, 129, 0.6); }
.column-title {
font-size: 0.92rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.column-count {
font-size: 0.75rem;
font-weight: 700;
padding: 0.12rem 0.5rem;
border-radius: 999px;
background-color: var(--bg-card);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.column-quick-add {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
border-radius: 6px;
display: grid;
place-items: center;
transition: background-color 0.15s, color 0.15s;
}
.column-quick-add:hover {
background-color: var(--accent-soft);
color: var(--accent);
}
.card-list {
padding: 0.85rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
overflow-y: auto;
flex: 1;
min-height: 200px;
}
/* Cards */
.card {
background-color: var(--bg-card);
border-radius: 10px;
padding: 0.9rem 0.95rem;
box-shadow: var(--shadow-card);
cursor: grab;
user-select: none;
display: flex;
flex-direction: column;
gap: 0.65rem;
position: relative;
transition: transform 0.14s ease, box-shadow 0.14s ease, background-color 0.15s ease, opacity 0.15s ease;
border-left: 3px solid transparent;
}
.card[data-priority="high"] { border-left-color: #ef4444; }
.card[data-priority="medium"] { border-left-color: #f59e0b; }
.card[data-priority="low"] { border-left-color: #22c55e; }
.card:hover {
background-color: var(--bg-card-hover);
box-shadow: var(--shadow-card-hover);
transform: translateY(-2px);
}
.card:active {
cursor: grabbing;
}
.card.dragging {
opacity: 0.35;
transform: scale(0.98);
}
.card-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.badge {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.2rem 0.55rem;
border-radius: 6px;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.badge::before {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background-color: currentColor;
}
.badge.high {
background-color: var(--badge-high-bg);
color: var(--badge-high-text);
}
.badge.medium {
background-color: var(--badge-medium-bg);
color: var(--badge-medium-text);
}
.badge.low {
background-color: var(--badge-low-bg);
color: var(--badge-low-text);
}
.card-delete {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
width: 26px;
height: 26px;
border-radius: 6px;
display: grid;
place-items: center;
opacity: 0.65;
transition: opacity 0.15s, background-color 0.15s, color 0.15s;
}
.card:hover .card-delete {
opacity: 1;
}
.card-delete:hover {
background-color: var(--danger-hover);
color: var(--danger-text);
}
.card-title {
font-size: 0.9rem;
font-weight: 600;
line-height: 1.45;
color: var(--text-primary);
word-break: break-word;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.73rem;
color: var(--text-secondary);
padding-top: 0.25rem;
}
.card-id {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 600;
opacity: 0.85;
}
.drag-grip {
display: inline-flex;
align-items: center;
gap: 2px;
opacity: 0.5;
}
/* Drop Indicator */
.drop-indicator {
height: 68px;
border-radius: 10px;
border: 2px dashed var(--accent);
background-color: var(--accent-soft);
pointer-events: none;
transition: all 0.12s ease;
flex-shrink: 0;
}
.empty-state {
border: 1px dashed var(--border-color);
border-radius: 10px;
padding: 1.75rem 1rem;
text-align: center;
color: var(--text-secondary);
font-size: 0.82rem;
margin: auto 0;
pointer-events: none;
}
/* Modal */
.modal-backdrop {
position: fixed;
inset: 0;
background-color: rgba(8, 11, 18, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.modal-backdrop.open {
opacity: 1;
pointer-events: auto;
}
.modal {
background-color: var(--bg-header);
border: 1px solid var(--border-color);
border-radius: 14px;
width: 100%;
max-width: 430px;
padding: 1.4rem;
box-shadow: var(--shadow-modal);
transform: translateY(10px) scale(0.98);
transition: transform 0.18s ease;
}
.modal-backdrop.open .modal {
transform: translateY(0) scale(1);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.15rem;
}
.modal-title {
font-size: 1.05rem;
font-weight: 700;
}
.modal-close {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 4px;
border-radius: 6px;
display: grid;
place-items: center;
}
.modal-close:hover {
background-color: var(--bg-input);
color: var(--text-primary);
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 0.4rem;
}
.form-input, .form-select {
width: 100%;
padding: 0.65rem 0.8rem;
border-radius: 8px;
border: 1px solid var(--border-color);
background-color: var(--bg-input);
color: var(--text-primary);
font-size: 0.9rem;
font-family: inherit;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.form-input:focus, .form-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.form-input.invalid {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18);
}
.error-msg {
font-size: 0.76rem;
color: #ef4444;
margin-top: 0.35rem;
display: none;
}
.error-msg.visible {
display: block;
}
.priority-selector {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.priority-option {
position: relative;
}
.priority-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.priority-pill {
display: flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
padding: 0.55rem 0.5rem;
border-radius: 8px;
border: 1px solid var(--border-color);
background-color: var(--bg-input);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
text-transform: capitalize;
}
.priority-option input:checked + .priority-pill.low {
border-color: #22c55e;
background-color: var(--badge-low-bg);
color: var(--badge-low-text);
}
.priority-option input:checked + .priority-pill.medium {
border-color: #f59e0b;
background-color: var(--badge-medium-bg);
color: var(--badge-medium-text);
}
.priority-option input:checked + .priority-pill.high {
border-color: #ef4444;
background-color: var(--badge-high-bg);
color: var(--badge-high-text);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
margin-top: 1.35rem;
}
.btn-secondary {
background-color: var(--bg-input);
border-color: var(--border-color);
color: var(--text-primary);
}
.btn-secondary:hover {
background-color: var(--bg-card-hover);
}
mark.highlight {
background-color: rgba(245, 158, 11, 0.35);
color: inherit;
border-radius: 3px;
padding: 0 2px;
}
@media (max-width: 900px) {
.board-container {
grid-template-columns: 1fr;
padding: 1rem;
}
.column {
max-height: none;
min-height: 220px;
}
.topbar {
padding: 0.85rem 1rem;
}
}
@media (max-width: 540px) {
.controls {
width: 100%;
max-width: none;
justify-content: space-between;
}
.search-wrapper {
max-width: none;
}
.brand-subtitle {
display: none;
}
}
</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="brand-logo" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="9" rx="1"></rect>
<rect x="14" y="3" width="7" height="5" rx="1"></rect>
<rect x="14" y="12" width="7" height="9" rx="1"></rect>
<rect x="3" y="16" width="7" height="5" rx="1"></rect>
</svg>
</div>
<div>
<div class="brand-title">FlowBoard</div>
<div class="brand-subtitle">Q3 Product Sprint • Drag cards to reorder</div>
</div>
</div>
<div class="controls">
<div class="search-wrapper">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" id="searchInput" class="search-input" placeholder="Search cards by title..." aria-label="Search cards by title">
<button type="button" id="clearSearchBtn" class="clear-search" title="Clear search" aria-label="Clear search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<button type="button" id="openModalBtn" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
<span>New Card</span>
</button>
<button type="button" id="themeToggleBtn" class="btn btn-icon" title="Toggle light/dark theme" aria-label="Toggle light/dark theme">
<svg id="themeIconSun" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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>
<svg id="themeIconMoon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</div>
</header>
<main class="board-container" id="board">
<!-- To do Column -->
<section class="column" data-column="todo">
<div class="column-header">
<div class="column-title-group">
<span class="status-dot todo"></span>
<h2 class="column-title">To do</h2>
<span class="column-count" id="count-todo">0</span>
</div>
<button type="button" class="column-quick-add" data-add-col="todo" title="Add card to To do" aria-label="Add card to To do">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="card-list" id="list-todo" data-column="todo"></div>
</section>
<!-- In progress Column -->
<section class="column" data-column="inprogress">
<div class="column-header">
<div class="column-title-group">
<span class="status-dot inprogress"></span>
<h2 class="column-title">In progress</h2>
<span class="column-count" id="count-inprogress">0</span>
</div>
<button type="button" class="column-quick-add" data-add-col="inprogress" title="Add card to In progress" aria-label="Add card to In progress">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="card-list" id="list-inprogress" data-column="inprogress"></div>
</section>
<!-- Done Column -->
<section class="column" data-column="done">
<div class="column-header">
<div class="column-title-group">
<span class="status-dot done"></span>
<h2 class="column-title">Done</h2>
<span class="column-count" id="count-done">0</span>
</div>
<button type="button" class="column-quick-add" data-add-col="done" title="Add card to Done" aria-label="Add card to Done">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="card-list" id="list-done" data-column="done"></div>
</section>
</main>
<!-- Add Card Modal -->
<div class="modal-backdrop" id="modalBackdrop" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title" id="modalTitle">Create New Card</h3>
<button type="button" class="modal-close" id="closeModalBtn" aria-label="Close modal">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<form id="cardForm" novalidate>
<div class="form-group">
<label class="form-label" for="cardTitleInput">Card Title *</label>
<input type="text" id="cardTitleInput" class="form-input" placeholder="e.g., Implement OAuth2 PKCE flow" maxlength="140" required>
<div class="error-msg" id="titleError">Please enter a card title.</div>
</div>
<div class="form-group">
<label class="form-label">Priority</label>
<div class="priority-selector">
<label class="priority-option">
<input type="radio" name="priority" value="low">
<span class="priority-pill low">Low</span>
</label>
<label class="priority-option">
<input type="radio" name="priority" value="medium" checked>
<span class="priority-pill medium">Medium</span>
</label>
<label class="priority-option">
<input type="radio" name="priority" value="high">
<span class="priority-pill high">High</span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label" for="cardColumnSelect">Column</label>
<select id="cardColumnSelect" class="form-select">
<option value="todo">To do</option>
<option value="inprogress">In progress</option>
<option value="done">Done</option>
</select>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" id="cancelModalBtn">Cancel</button>
<button type="submit" class="btn btn-primary">Add Card</button>
</div>
</form>
</div>
</div>
<script>
(() => {
// In-memory state pre-populated with 6 realistic cards across the 3 columns
const state = {
theme: 'dark',
searchQuery: '',
nextNumber: 107,
cards: [
{
id: 'FLW-101',
title: 'Implement rate limiting middleware for public REST API endpoints',
priority: 'high',
column: 'todo'
},
{
id: 'FLW-102',
title: 'Design onboarding checklist empty-state illustrations',
priority: 'medium',
column: 'todo'
},
{
id: 'FLW-103',
title: 'Migrate billing webhook handlers to idempotent queue workers',
priority: 'high',
column: 'inprogress'
},
{
id: 'FLW-104',
title: 'Add keyboard navigation & ARIA live announcements to command palette',
priority: 'low',
column: 'inprogress'
},
{
id: 'FLW-105',
title: 'Configure automated PostgreSQL read-replica failover alerts',
priority: 'medium',
column: 'done'
},
{
id: 'FLW-106',
title: 'Audit bundle size and tree-shake legacy chart dependencies',
priority: 'low',
column: 'done'
}
]
};
const columns = ['todo', 'inprogress', 'done'];
// DOM Elements
const searchInput = document.getElementById('searchInput');
const clearSearchBtn = document.getElementById('clearSearchBtn');
const themeToggleBtn = document.getElementById('themeToggleBtn');
const themeIconSun = document.getElementById('themeIconSun');
const themeIconMoon = document.getElementById('themeIconMoon');
const modalBackdrop = document.getElementById('modalBackdrop');
const openModalBtn = document.getElementById('openModalBtn');
const closeModalBtn = document.getElementById('closeModalBtn');
const cancelModalBtn = document.getElementById('cancelModalBtn');
const cardForm = document.getElementById('cardForm');
const cardTitleInput = document.getElementById('cardTitleInput');
const cardColumnSelect = document.getElementById('cardColumnSelect');
const titleError = document.getElementById('titleError');
// Drag and drop state
let draggedCardId = null;
const dropIndicator = document.createElement('div');
dropIndicator.className = 'drop-indicator';
function escapeHTML(str) {
return str.replace(/[&<>"']/g, ch => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[ch]));
}
function highlightMatch(title, query) {
if (!query) return escapeHTML(title);
const escapedTitle = escapeHTML(title);
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return escapedTitle.replace(regex, '<mark class="highlight">$1</mark>');
}
function renderBoard() {
const query = state.searchQuery.trim().toLowerCase();
columns.forEach(col => {
const listEl = document.getElementById(`list-${col}`);
const countEl = document.getElementById(`count-${col}`);
listEl.innerHTML = '';
const colCards = state.cards.filter(card => {
if (card.column !== col) return false;
if (query && !card.title.toLowerCase().includes(query)) return false;
return true;
});
countEl.textContent = colCards.length;
if (colCards.length === 0) {
const empty = document.createElement('div');
empty.className = 'empty-state';
empty.textContent = query ? 'No matching cards' : 'Drop cards here or click + to add';
listEl.appendChild(empty);
return;
}
colCards.forEach(card => {
const cardEl = document.createElement('article');
cardEl.className = 'card';
cardEl.draggable = true;
cardEl.dataset.id = card.id;
cardEl.dataset.priority = card.priority;
cardEl.innerHTML = `
<div class="card-top">
<span class="badge ${card.priority}">${card.priority}</span>
<button type="button" class="card-delete" data-delete-id="${card.id}" title="Delete card" aria-label="Delete card">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path>
<path d="M10 11v6"></path>
<path d="M14 11v6"></path>
</svg>
</button>
</div>
<div class="card-title">${highlightMatch(card.title, state.searchQuery.trim())}</div>
<div class="card-footer">
<span class="card-id">${card.id}</span>
<span class="drag-grip" aria-hidden="true">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
<circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/>
<circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/>
<circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/>
</svg>
</span>
</div>
`;
// Drag listeners on card
cardEl.addEventListener('dragstart', handleDragStart);
cardEl.addEventListener('dragend', handleDragEnd);
// Delete button listener
const deleteBtn = cardEl.querySelector('.card-delete');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCard(card.id);
});
listEl.appendChild(cardEl);
});
});
}
function deleteCard(id) {
const idx = state.cards.findIndex(c => c.id === id);
if (idx !== -1) {
state.cards.splice(idx, 1);
renderBoard();
}
}
// Drag and Drop Handlers
function handleDragStart(e) {
const cardEl = e.currentTarget;
draggedCardId = cardEl.dataset.id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedCardId);
// Set indicator height to match the dragged card
dropIndicator.style.height = `${cardEl.offsetHeight}px`;
requestAnimationFrame(() => {
cardEl.classList.add('dragging');
});
}
function handleDragEnd(e) {
e.currentTarget.classList.remove('dragging');
draggedCardId = null;
if (dropIndicator.parentNode) {
dropIndicator.parentNode.removeChild(dropIndicator);
}
document.querySelectorAll('.column.drag-over').forEach(col => {
col.classList.remove('drag-over');
});
}
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 };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
document.querySelectorAll('.column').forEach(columnEl => {
const listEl = columnEl.querySelector('.card-list');
columnEl.addEventListener('dragover', e => {
e.preventDefault();
if (!draggedCardId) return;
e.dataTransfer.dropEffect = 'move';
columnEl.classList.add('drag-over');
const emptyEl = listEl.querySelector('.empty-state');
if (emptyEl) emptyEl.style.display = 'none';
const afterElement = getDragAfterElement(listEl, e.clientY);
if (afterElement == null) {
listEl.appendChild(dropIndicator);
} else {
listEl.insertBefore(dropIndicator, afterElement);
}
});
columnEl.addEventListener('dragleave', e => {
if (!columnEl.contains(e.relatedTarget)) {
columnEl.classList.remove('drag-over');
if (dropIndicator.parentNode === listEl) {
listEl.removeChild(dropIndicator);
}
const emptyEl = listEl.querySelector('.empty-state');
if (emptyEl) emptyEl.style.display = '';
}
});
columnEl.addEventListener('drop', e => {
e.preventDefault();
columnEl.classList.remove('drag-over');
const targetColumn = columnEl.dataset.column;
const cardIndex = state.cards.findIndex(c => c.id === draggedCardId);
if (cardIndex === -1) return;
const [movedCard] = state.cards.splice(cardIndex, 1);
movedCard.column = targetColumn;
const afterElement = getDragAfterElement(listEl, e.clientY);
if (dropIndicator.parentNode) {
dropIndicator.parentNode.removeChild(dropIndicator);
}
if (afterElement && afterElement.dataset.id) {
const targetIdx = state.cards.findIndex(c => c.id === afterElement.dataset.id);
if (targetIdx !== -1) {
state.cards.splice(targetIdx, 0, movedCard);
} else {
state.cards.push(movedCard);
}
} else {
state.cards.push(movedCard);
}
draggedCardId = null;
renderBoard();
});
});
// Modal Controls
function openModal(defaultColumn = 'todo') {
cardForm.reset();
cardColumnSelect.value = defaultColumn;
cardTitleInput.classList.remove('invalid');
titleError.classList.remove('visible');
modalBackdrop.classList.add('open');
setTimeout(() => cardTitleInput.focus(), 60);
}
function closeModal() {
modalBackdrop.classList.remove('open');
}
openModalBtn.addEventListener('click', () => openModal('todo'));
closeModalBtn.addEventListener('click', closeModal);
cancelModalBtn.addEventListener('click', closeModal);
document.querySelectorAll('[data-add-col]').forEach(btn => {
btn.addEventListener('click', () => openModal(btn.dataset.addCol));
});
modalBackdrop.addEventListener('click', e => {
if (e.target === modalBackdrop) closeModal();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && modalBackdrop.classList.contains('open')) {
closeModal();
}
});
cardTitleInput.addEventListener('input', () => {
if (cardTitleInput.value.trim()) {
cardTitleInput.classList.remove('invalid');
titleError.classList.remove('visible');
}
});
cardForm.addEventListener('submit', e => {
e.preventDefault();
const title = cardTitleInput.value.trim();
if (!title) {
cardTitleInput.classList.add('invalid');
titleError.classList.add('visible');
cardTitleInput.focus();
return;
}
const priorityData = new FormData(cardForm).get('priority') || 'medium';
const column = cardColumnSelect.value || 'todo';
const newCard = {
id: `FLW-${state.nextNumber++}`,
title,
priority: priorityData,
column
};
state.cards.unshift(newCard);
closeModal();
renderBoard();
});
// Search Filtering
searchInput.addEventListener('input', e => {
state.searchQuery = e.target.value;
clearSearchBtn.classList.toggle('visible', state.searchQuery.length > 0);
renderBoard();
});
clearSearchBtn.addEventListener('click', () => {
state.searchQuery = '';
searchInput.value = '';
clearSearchBtn.classList.remove('visible');
searchInput.focus();
renderBoard();
});
// Theme Toggle
function updateThemeUI() {
document.documentElement.setAttribute('data-theme', state.theme);
if (state.theme === 'dark') {
themeIconSun.style.display = 'block';
themeIconMoon.style.display = 'none';
} else {
themeIconSun.style.display = 'none';
themeIconMoon.style.display = 'block';
}
}
themeToggleBtn.addEventListener('click', () => {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
updateThemeUI();
});
// Initial render
updateThemeUI();
renderBoard();
})();
</script>
</body>
</html>