Kanban
GPT 5.6 Luna Pro · typescript

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 21,647 bytes · SHA-256 c1d139a95299
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flowboard — Project Board</title>
<style>
/* ── css: base ── */
:root{--ink:#17212f;--muted:#718096;--line:#e7ebf0;--canvas:#f5f7fa;--card:#fff;--accent:#5b61e8;--shadow:0 8px 24px rgba(35,48,75,.08);font-family:'DM Sans',system-ui,sans-serif;color:var(--ink)}*{box-sizing:border-box}body{margin:0;background:var(--canvas);overflow:hidden} .topbar{height:68px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 30px}.brand{display:flex;align-items:center;gap:11px}.brandMark{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,#656af0,#4b45c5);color:white;display:grid;place-items:center;font-family:system-ui;font-weight:700}.brand strong{display:block;font-weight:700;font-size:17px;letter-spacing:-.3px}.brand span{font-size:11px;color:var(--muted)}.topActions,.tools{display:flex;align-items:center;gap:10px}.btn{border:0;border-radius:8px;padding:8px 12px;cursor:pointer;font-weight:600;color:var(--ink);background:#fff}.btn:hover{background:#f1f3f8}.btn.ghost{border:1px solid var(--line);font-size:13px}.avatar{width:30px;height:30px;border-radius:50%;background:#ffc9a8;color:#7e4930;display:grid;place-items:center;font-size:11px;font-weight:700}.toolbar{height:70px;padding:0 30px;display:flex;align-items:center;justify-content:space-between}.crumb{display:flex;align-items:center;gap:11px;font-size:13px;color:var(--muted)}.crumb strong{color:var(--ink)}.search{height:35px;width:210px;display:flex;align-items:center;gap:7px;background:white;border:1px solid var(--line);border-radius:8px;padding:0 10px;color:#9aa5b5}.search input{border:0;outline:0;width:100%;font-size:12px;color:var(--ink)}.filters{display:flex;gap:5px}.filterChip{border:1px solid var(--line);background:#fff;border-radius:15px;padding:6px 9px;font-size:11px;cursor:pointer;color:#617085}.filterChip.active{color:#fff;border-color:transparent}.board{height:calc(100vh - 138px);display:flex;gap:18px;padding:0 30px 30px;overflow-x:auto;overflow-y:hidden;align-items:stretch}.column{flex:0 0 286px;display:flex;flex-direction:column;min-height:0}.colHead{height:52px;display:flex;align-items:center;justify-content:space-between;padding:0 5px 0 7px;cursor:grab}.colTitle{display:flex;align-items:center;gap:8px;font-weight:700;font-size:13px}.colTitle h2{font-size:13px;margin:0;font-weight:600}.count{font-size:11px;color:#8d99a9;background:#e9edf3;padding:2px 7px;border-radius:12px}.colMenu{border:0;background:transparent;color:#97a1af;font-size:20px;cursor:pointer;border-radius:6px}.colMenu:hover{background:#e9edf3;color:var(--ink)}.colBody{min-height:120px;overflow-y:auto;padding:4px 4px 20px 2px;border-radius:10px;transition:background .15s}.colBody.over{background:#e9ebff}.wip{box-shadow:inset 0 3px #e47777}.card{background:var(--card);border:1px solid #edf0f4;border-radius:10px;padding:14px 14px 12px;margin:0 0 10px;box-shadow:0 2px 5px rgba(37,50,74,.04);cursor:grab;user-select:none;transition:box-shadow .15s,transform .15s}.card:hover{transform:translateY(-2px);box-shadow:var(--shadow)}.card.dragging{opacity:.28}.card.settle{animation:settle .35s ease}.cardTitle{font-weight:600;font-size:13px;line-height:1.3;margin-bottom:7px}.cardDesc{color:#7b8797;font-size:11px;line-height:1.45;margin-bottom:10px}.labels{display:flex;gap:5px;flex-wrap:wrap}.label{font-size:10px;font-weight:600;border-radius:4px;padding:3px 6px}.meta{display:flex;justify-content:space-between;margin-top:11px;color:#98a3b2;font-size:10px}.dropIndicator{height:8px;border-radius:5px;background:#6268eb;margin:1px 2px 9px;box-shadow:0 0 0 3px #dfe1ff}.empty{height:100px;border:1.5px dashed #cbd3df;border-radius:9px;display:grid;place-items:center;color:#9ba6b5;font-size:11px;margin:0 2px}.empty.over{border-color:#6268eb;color:#6268eb;background:#f0f1ff}.editInput{width:100%;border:1px solid #777bea;border-radius:5px;outline:0;padding:3px;font-size:13px}.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(80px);background:#202938;color:#fff;padding:11px 15px;border-radius:8px;font-size:12px;transition:.25s;z-index:20;box-shadow:var(--shadow)}.toast.show{transform:translateX(-50%) translateY(0)}.toast button{border:0;background:transparent;color:#aeb3ff;font-weight:700;margin-left:12px;cursor:pointer}.modalLayer{position:fixed;inset:0;background:rgba(24,32,48,.25);display:grid;place-items:center;z-index:15}.hidden{display:none}.modal{background:#fff;border-radius:12px;padding:20px;width:340px;box-shadow:0 18px 50px #17212f30}.modal h3{margin:0 0 15px;font-weight:700}.modal input{width:100%;padding:10px;border:1px solid var(--line);border-radius:7px;outline:none;margin-bottom:12px}.modalActions{display:flex;justify-content:flex-end;gap:8px}.primary{background:var(--accent);color:#fff}.context{position:absolute;z-index:10;background:#fff;border:1px solid var(--line);border-radius:8px;box-shadow:var(--shadow);padding:5px;min-width:150px}.context button{display:block;width:100%;border:0;background:transparent;padding:8px;text-align:left;font-size:12px;border-radius:5px;cursor:pointer}.context button:hover{background:#f1f3f8}.danger{color:#d85c63!important}@keyframes settle{0%{transform:scale(1.03)}100%{transform:scale(1)}}@media(max-width:700px){.topbar{padding:0 16px}.toolbar{padding:0 16px}.crumb{display:none}.board{padding-left:16px}.topActions .ghost{display:none}}
/* ── css: drag ── */
.dragGhost{position:fixed;z-index:30;pointer-events:none;width:286px;transform:rotate(2deg);box-shadow:0 18px 36px rgba(35,48,75,.2);opacity:.96}.column.colDragging{opacity:.55}.column.dropCol{outline:2px solid #bfc3ff;border-radius:12px}
/* ── css: labels ── */
.filterChip .dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px}.labelRow{display:flex;align-items:center;gap:8px;margin:8px 0;font-size:12px}.labelRow input{width:auto;margin:0}.danger{color:#d85c63!important}
/* ── css: cardActions ── */
.card{position:relative}.cardDelete{position:absolute;right:8px;top:8px;border:0;background:transparent;color:#aab3c0;font-size:17px;line-height:1;cursor:pointer;opacity:0;transition:.15s}.card:hover .cardDelete{opacity:1}.cardDelete:hover{color:#d85c63}
</style>
</head>
<body>
<div id="appShell"><header class="topbar"><div class="brand"><div class="brandMark">F</div><div><strong>Flowboard</strong><span>Project workspace</span></div></div><div class="topActions"><button id="addColumnBtn" class="btn ghost">+ Column</button><button id="labelBtn" class="btn ghost">◉ Labels</button><div class="avatar">JD</div></div></header><section class="toolbar"><div class="crumb"><span>My workspace</span><b>/</b><strong>Website redesign</strong></div><div class="tools"><div class="search"><span>⌕</span><input id="searchInput" placeholder="Search cards..."></div><div id="filterBar" class="filters"></div></div></section><main id="board" class="board"></main></div><div id="toast" class="toast"></div><div id="modalLayer" class="modalLayer hidden"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const PALETTE = [{ id: 'violet', name: 'Design', color: '#8b7cf6' }, { id: 'blue', name: 'Development', color: '#5b9bea' }, { id: 'mint', name: 'Research', color: '#55b99b' }, { id: 'orange', name: 'Priority', color: '#ee9b61' }, { id: 'pink', name: 'Marketing', color: '#e47b9b' }, { id: 'yellow', name: 'Feedback', color: '#d3a936' }];
const KEY = 'flowboard-v1';
const uid = () => Math.random().toString(36).slice(2, 9);
const seedCards = (items) => { const out = {}; const ids = items.map(([title, desc, labels]) => { const id = uid(); out[id] = { id, title, desc, labels }; return id; }); return { out, ids }; };
function initial() { const a = seedCards([['Audit current site', 'Review analytics, navigation, and the content hierarchy.', ['violet', 'mint']], ['Collect inspiration', 'Save references for the new visual direction.', ['violet']], ['Stakeholder interviews', 'Schedule sessions with the core team.', ['mint']]]); const b = seedCards([['Define success metrics', 'Align on what a successful launch looks like.', ['orange']], ['Create sitemap', 'Map the new information architecture.', ['blue']]]); const c = seedCards([['Homepage concepts', 'Explore three bold directions for the homepage.', ['violet', 'orange']], ['Design system tokens', 'Document color, type, and spacing decisions.', ['blue']], ['Mobile experience', 'Bring the responsive behavior to life.', ['blue', 'pink']]]); const d = seedCards([['Usability testing', 'Test the prototype with five customers.', ['mint', 'yellow']], ['Polish empty states', 'Add guidance wherever the interface is quiet.', ['pink']]]); const e = seedCards([['Launch checklist', 'Final QA, docs, and handoff.', ['orange']], ['Celebrate the launch', 'Share the work with the team.', ['pink']]]); return { cards: { ...a.out, ...b.out, ...c.out, ...d.out, ...e.out }, columns: [{ id: 'backlog', title: 'Backlog', cardIds: a.ids }, { id: 'todo', title: 'To Do', cardIds: b.ids }, { id: 'progress', title: 'In Progress', cardIds: c.ids }, { id: 'review', title: 'Review', cardIds: d.ids }, { id: 'done', title: 'Done', cardIds: e.ids }], labels: PALETTE.map(x => ({ ...x })), filters: [], search: '' }; }
let model;
try {
model = JSON.parse(localStorage.getItem(KEY) || 'null') || initial();
}
catch {
model = initial();
}
let undoModel = null;
let drag = null;
const $ = (s) => document.querySelector(s);
const esc = (s) => s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
function save() { localStorage.setItem(KEY, JSON.stringify(model)); window.flowboardModel = model; }
function visible(c) { return (!model.search || c.title.toLowerCase().includes(model.search.toLowerCase())) && (!model.filters.length || model.filters.some(x => c.labels.includes(x))); }
function render() {
save();
const board = $('#board');
board.innerHTML = '';
const filter = $('#filterBar');
filter.innerHTML = model.labels.map(l => `<button class="filterChip ${model.filters.includes(l.id) ? 'active' : ''}" data-filter="${l.id}" style="${model.filters.includes(l.id) ? `background:${l.color}` : ''}"><span class="dot" style="background:${l.color}"></span>${esc(l.name)}</button>`).join('');
model.columns.forEach(col => { const over = col.wip && col.cardIds.length > col.wip; const el = document.createElement('section'); el.className = 'column'; el.dataset.col = col.id; el.innerHTML = `<div class="colHead" data-col-drag="${col.id}"><div class="colTitle"><h2>${esc(col.title)}</h2><span class="count">${col.cardIds.length}</span>${col.wip ? `<span class="count">WIP ${col.wip}</span>` : ''}</div><button class="colMenu" data-menu="${col.id}">•••</button></div><div class="colBody ${over ? 'wip' : ''}" data-body="${col.id}"></div>`; board.append(el); const body = el.querySelector('.colBody'); const ids = col.cardIds.filter(id => visible(model.cards[id])); if (!ids.length)
body.innerHTML = '<div class="empty">Drop cards here</div>'; ids.forEach(id => body.append(cardEl(model.cards[id]))); });
}
function cardEl(c) { const el = document.createElement('article'); el.className = 'card'; el.dataset.card = c.id; el.innerHTML = `<button class="cardDelete" data-delete-card="${c.id}">×</button><div class="cardTitle">${esc(c.title)}</div>${c.desc ? `<div class="cardDesc">${esc(c.desc)}</div>` : ''}<div class="labels">${c.labels.map(id => { const l = model.labels.find(x => x.id === id); return `<span class="label" style="color:${l.color};background:${l.color}20">${esc(l.name)}</span>`; }).join('')}</div>${c.due ? `<div class="meta">Due ${esc(c.due)}</div>` : ''}`; return el; }
function toast(msg, undo) { const t = $('#toast'); t.innerHTML = esc(msg) + (undo ? '<button id="undo">Undo</button>' : ''); t.classList.add('show'); if (undo) {
$('#undo').onclick = () => { undo(); t.classList.remove('show'); };
} setTimeout(() => t.classList.remove('show'), 3500); }
function modal(title, fields, done) { const layer = $('#modalLayer'); layer.classList.remove('hidden'); layer.innerHTML = `<div class="modal"><h3>${title}</h3>${fields.map((x, i) => `<input id="m${i}" placeholder="${x}" />`).join('')}<div class="modalActions"><button class="btn" id="cancelM">Cancel</button><button class="btn primary" id="okM">Save</button></div></div>`; $('#cancelM').onclick = () => layer.classList.add('hidden'); $('#okM').onclick = () => { done(fields.map((_, i) => $(`#m${i}`).value)); layer.classList.add('hidden'); }; $(`#m0`).focus(); }
function editTitle(el, c) { const input = document.createElement('input'); input.className = 'editInput'; input.value = c.title; el.replaceWith(input); input.focus(); const finish = (commit) => { if (commit && input.value.trim()) {
c.title = input.value.trim();
render();
}
else
render(); }; input.onkeydown = e => { if (e.key === 'Enter')
finish(true); if (e.key === 'Escape')
finish(false); }; input.onblur = () => finish(true); }
function beginCard(e, el) { if (e.button !== 0)
return; const id = el.dataset.card; let col = model.columns.find(c => c.cardIds.includes(id)); drag = { kind: 'card', id, fromCol: col.id, fromIndex: col.cardIds.indexOf(id), startX: e.clientX, startY: e.clientY }; el.setPointerCapture(e.pointerId); el.classList.add('dragging'); }
function beginColumn(e, el) { if (e.button !== 0)
return; drag = { kind: 'column', id: el.dataset.col, startX: e.clientX, startY: e.clientY }; el.setPointerCapture(e.pointerId); }
function updateCard(e) {
if (!drag || drag.kind !== 'card')
return;
const board = $('#board');
if (e.clientX < 55)
board.scrollLeft -= 12;
else if (e.clientX > innerWidth - 55)
board.scrollLeft += 12;
const el = document.querySelector(`[data-card="${drag.id}"]`);
if (!drag.ghost && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 5)
return;
if (!drag.ghost) {
drag.ghost = el.cloneNode(true);
drag.ghost.className = 'card dragGhost';
document.body.append(drag.ghost);
el.classList.add('dragging');
}
drag.ghost.style.left = e.clientX - 143 + 'px';
drag.ghost.style.top = e.clientY - 35 + 'px';
const under = document.elementFromPoint(e.clientX, e.clientY);
const body = under?.closest('[data-body]');
if (!body)
return;
const colId = body.dataset.body;
const cards = [...body.querySelectorAll('.card:not(.dragging)')];
let idx = cards.length;
for (let i = 0; i < cards.length; i++) {
if (e.clientY < cards[i].getBoundingClientRect().top + cards[i].offsetHeight / 2) {
idx = i;
break;
}
}
;
const targetModel = model.columns.find(c => c.id === colId);
const visibleIds = targetModel.cardIds.filter(id => visible(model.cards[id]));
const beforeId = visibleIds[idx];
let modelAt = beforeId ? targetModel.cardIds.indexOf(beforeId) : targetModel.cardIds.length;
if (!beforeId && visibleIds.length) {
const last = visibleIds[visibleIds.length - 1];
modelAt = targetModel.cardIds.indexOf(last) + 1;
}
if (!beforeId && visibleIds.length === 0)
modelAt = targetModel.cardIds.length;
idx = modelAt;
if (drag.indicator) {
drag.indicator.remove();
}
drag.indicator = document.createElement('div');
drag.indicator.className = 'dropIndicator';
if (idx < cards.length)
body.insertBefore(drag.indicator, cards[idx]);
else
body.append(drag.indicator);
drag.targetCol = colId;
drag.targetIndex = idx;
body.classList.add('over');
}
function endCard(cancel = false) { if (!drag || drag.kind !== 'card')
return; const d = drag; document.querySelectorAll('.colBody').forEach(x => x.classList.remove('over')); if (d.ghost)
d.ghost.remove(); if (d.indicator)
d.indicator.remove(); if (!cancel && d.targetCol) {
const from = model.columns.find(c => c.id === d.fromCol);
const pos = from.cardIds.indexOf(d.id);
if (pos >= 0)
from.cardIds.splice(pos, 1);
const to = model.columns.find(c => c.id === d.targetCol);
let at = d.targetIndex ?? to.cardIds.length;
if (from.id === to.id && pos < at)
at--;
to.cardIds.splice(Math.max(0, at), 0, d.id);
} drag = null; render(); }
function endColumn(x, y) { const d = drag; if (!d || d.kind !== 'column')
return; const target = document.elementFromPoint(x, y)?.closest('.column'); if (target && target.dataset.col !== d.id) {
const a = model.columns.findIndex(c => c.id === d.id), b = model.columns.findIndex(c => c.id === target.dataset.col);
const [c] = model.columns.splice(a, 1);
model.columns.splice(b, 0, c);
} drag = null; render(); }
function context(colId, x, y) { document.querySelector('.context')?.remove(); const m = document.createElement('div'); m.className = 'context'; m.style.left = x + 'px'; m.style.top = y + 'px'; m.innerHTML = '<button data-act="add">+ Add card</button><button data-act="rename">Rename column</button><button data-act="wip">Set WIP limit</button><button data-act="delete" class="danger">Delete column</button>'; document.body.append(m); m.onclick = e => { const act = e.target.dataset.act; m.remove(); const col = model.columns.find(c => c.id === colId); if (act === 'add')
modal('Add card', ['Card title', 'Description'], v => { if (!v[0].trim())
return; const id = uid(); model.cards[id] = { id, title: v[0], desc: v[1], labels: [] }; col.cardIds.push(id); render(); }); if (act === 'rename')
modal('Rename column', ['Column name'], v => { if (v[0].trim()) {
col.title = v[0].trim();
render();
} }); if (act === 'wip')
modal('WIP limit', ['Number, or leave blank'], v => { col.wip = v[0] ? Math.max(1, +v[0]) : undefined; render(); }); if (act === 'delete' && model.columns.length > 1) {
undoModel = JSON.parse(JSON.stringify(model));
model.columns = model.columns.filter(c => c.id !== colId);
toast('Column deleted', () => { if (undoModel) {
model = undoModel;
undoModel = null;
render();
} });
render();
} }; }
function wire() { const board = $('#board'); board.onpointerdown = e => { const t = e.target; const card = t.closest('.card'); const head = t.closest('.colHead'); if (card) {
if (t.closest('.cardDelete'))
return;
beginCard(e, card);
return;
} if (head && !t.closest('.colMenu'))
beginColumn(e, head); }; board.onpointermove = e => { if (drag?.kind === 'card')
updateCard(e);
else if (drag?.kind === 'column' && drag && Math.abs(e.clientX - drag.startX) > 5) {
const c = document.querySelector(`[data-col="${drag.id}"]`);
c?.classList.add('colDragging');
} }; board.onpointerup = e => { if (drag?.kind === 'card')
endCard();
else
endColumn(e.clientX, e.clientY); }; board.onpointercancel = () => endCard(true); document.onkeydown = e => { if (e.key === 'Escape' && drag)
endCard(true); }; board.onclick = e => { const t = e.target; const menu = t.closest('[data-menu]'); if (menu) {
e.stopPropagation();
context(menu.dataset.menu, e.clientX, e.clientY);
return;
} const del = t.closest('[data-delete-card]'); if (del) {
const id = del.dataset.deleteCard;
const col = model.columns.find(c => c.cardIds.includes(id));
undoModel = JSON.parse(JSON.stringify(model));
col.cardIds = col.cardIds.filter(x => x !== id);
delete model.cards[id];
toast('Card deleted', () => { if (undoModel) {
model = undoModel;
undoModel = null;
render();
} });
render();
return;
} const title = t.closest('.cardTitle'); if (title && !drag) {
const c = model.cards[title.closest('.card').dataset.card];
editTitle(title, c);
} }; $('#searchInput').oninput = e => { model.search = e.target.value; render(); }; $('#filterBar').onclick = e => { const id = e.target.closest('[data-filter]')?.getAttribute('data-filter'); if (id) {
model.filters = model.filters.includes(id) ? model.filters.filter(x => x !== id) : [...model.filters, id];
render();
} }; $('#addColumnBtn').onclick = () => modal('Add column', ['Column name'], v => { if (v[0].trim()) {
model.columns.push({ id: uid(), title: v[0].trim(), cardIds: [] });
render();
} }); $('#labelBtn').onclick = () => toast('Choose a label chip to filter the board'); }
wire();
render();
window.flowboard = { getModel: () => JSON.parse(JSON.stringify(model)), render, beginCard, endCard };
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-luna-pro","provider":"openrouter","persona":"typescript","sessionId":"8df2f879-fa85-4076-8b98-6d4a55216ac7","tokensIn":617749,"tokensOut":22556,"tokensTotal":640305,"turns":16,"toolCalls":28,"failedToolCalls":2,"timestamp":"2026-07-31T03:07:34.507Z"} -->