← Kanban results

Kanban

GPT 5.6 terra · typescript

Initial view of Kanban

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 20,217 bytes · SHA-256 e716aed1fed2
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flowboard — Kanban Project Board</title>
<style>
/* ── css: board ── */
*{box-sizing:border-box}body{margin:0;background:#f5f7fb;color:#253047;font:14px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}.app-shell{height:100vh;display:flex;flex-direction:column;overflow:hidden}.topbar{height:70px;padding:14px 24px;background:rgba(255,255,255,.92);border-bottom:1px solid #e7eaf1;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:10}.brand{font-weight:800;font-size:20px;letter-spacing:-.5px;color:#26324e}.brand i{color:#7157e9;font-style:normal}.search{width:260px;border:1px solid #dde2ed;border-radius:9px;padding:9px 11px;background:#fafbfe;outline:none}.search:focus{border-color:#8a7af0;box-shadow:0 0 0 3px #eeeaff}.newcol{margin-left:auto}.button{border:0;background:#725be8;color:#fff;border-radius:8px;padding:9px 13px;font-weight:650;cursor:pointer}.button:hover{background:#6048d7}.filterbar{height:48px;padding:8px 24px;display:flex;align-items:center;gap:8px;background:#fff;border-bottom:1px solid #e7eaf1}.filterbar span{color:#778198;font-size:12px;font-weight:700;margin-right:3px}.filter{border:1px solid #dfe4ef;background:#fff;border-radius:20px;padding:5px 10px;cursor:pointer;color:#59627a;font-size:12px}.filter.active{border-color:var(--c);background:color-mix(in srgb,var(--c) 14%,white);color:#303a50}.board{flex:1;display:flex;gap:16px;overflow-x:auto;overflow-y:hidden;padding:20px 24px 28px;align-items:stretch}.column{width:290px;min-width:290px;display:flex;flex-direction:column;max-height:100%;background:#edf0f6;border:1px solid #e3e7ef;border-radius:13px;overflow:hidden}.column.overlimit{border-color:#ec7777;box-shadow:0 0 0 1px #f5a2a2;background:#fff5f5}.colhead{padding:14px 14px 11px;display:flex;align-items:center;gap:8px;cursor:grab;user-select:none}.coltitle{font-weight:750;letter-spacing:-.1px;flex:1}.count{font-size:12px;background:#dce1eb;color:#5f6880;padding:2px 7px;border-radius:12px}.overlimit .count{background:#ffd6d6;color:#b33e3e}.dots{border:0;background:transparent;color:#778198;font-size:19px;cursor:pointer;padding:0 3px}.cards{overflow-y:auto;padding:0 10px 12px;min-height:76px;scrollbar-width:thin}.card{background:#fff;border:1px solid #e5e8f0;border-radius:10px;margin:0 0 9px;padding:11px;box-shadow:0 2px 5px rgba(39,49,74,.05);cursor:grab;transition:transform .16s,box-shadow .16s;user-select:none}.card:hover{transform:translateY(-2px);box-shadow:0 7px 15px rgba(39,49,74,.11)}.card.drag-source{opacity:.25}.card-title{font-weight:650;outline:none;word-break:break-word}.card-desc{color:#7a8499;font-size:12px;margin-top:5px;white-space:pre-wrap}.meta{display:flex;gap:5px;align-items:center;margin-top:10px;flex-wrap:wrap}.chip{font-size:10px;font-weight:750;border-radius:5px;padding:3px 6px;color:#394055}.due{font-size:11px;color:#8a6270;margin-left:auto}.placeholder{height:74px;border:2px dashed #9d90ee;border-radius:10px;background:#f4f1ff;margin:0 0 9px}.empty{border:2px dashed #cbd2e2;border-radius:9px;color:#98a1b3;text-align:center;padding:18px 5px;margin:0 0 8px;font-size:12px}.addcard{border:0;background:transparent;color:#7662dc;text-align:left;padding:8px 4px;font-weight:650;cursor:pointer}.dragging{position:fixed!important;z-index:1000;margin:0!important;pointer-events:none;transform:rotate(2deg);box-shadow:0 18px 30px rgba(35,38,66,.22)!important;width:268px}.col-drag{opacity:.45}.menu{position:fixed;z-index:30;background:#fff;border:1px solid #e1e5ee;border-radius:9px;padding:6px;box-shadow:0 8px 24px #26324e24}.menu button{display:block;width:100%;text-align:left;border:0;background:#fff;padding:8px 10px;border-radius:6px;cursor:pointer;color:#3e4961}.menu button:hover{background:#f2f3f8}.menu input{width:70px;margin:5px 8px;padding:5px;border:1px solid #dce1eb;border-radius:5px}.modal{position:fixed;inset:0;background:#20284255;display:grid;place-items:center;z-index:50}.dialog{width:400px;background:#fff;border-radius:13px;padding:20px;box-shadow:0 16px 50px #1f294655}.dialog h3{margin:0 0 13px}.dialog input,.dialog textarea{width:100%;border:1px solid #dce2ee;border-radius:7px;padding:9px;margin:5px 0 10px;font:inherit}.dialog textarea{height:78px;resize:vertical}.dialog .actions{display:flex;justify-content:flex-end;gap:8px}.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#26324e;color:white;padding:10px 14px;border-radius:9px;z-index:70;box-shadow:0 8px 24px #26324e55}.toast button{color:#d8d0ff;background:none;border:0;font-weight:800;margin-left:10px;cursor:pointer}@media(max-width:600px){.topbar{padding:12px}.search{width:145px}.filterbar{padding:8px 12px}.board{padding:14px 12px}.column{min-width:275px}}
</style>
</head>
<body>
<div id="app" class="app-shell"></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 });
// Flowboard — single source-of-truth Kanban application
const CONFIG = {
    storageKey: 'flowboard-model-v1', dragThreshold: 5, edge: 52, scrollSpeed: 18,
    labels: [
        { id: 'pink', name: 'Design', color: '#ffd5e2' }, { id: 'orange', name: 'Research', color: '#ffe0ba' },
        { id: 'yellow', name: 'Priority', color: '#fff0aa' }, { id: 'green', name: 'Marketing', color: '#ccefdc' },
        { id: 'blue', name: 'Engineering', color: '#cfe7ff' }, { id: 'purple', name: 'Planning', color: '#e2d8ff' }
    ]
};
const id = () => Math.random().toString(36).slice(2, 10);
const initial = () => ({ columns: [
        { id: id(), title: 'Backlog', wip: null, cards: [{ id: id(), title: 'Map onboarding journey', description: 'Outline the first-run experience and key moments.', labels: ['pink'], due: 'May 18' }, { id: id(), title: 'Customer interview notes', description: 'Synthesize the latest discovery calls.', labels: ['orange'], due: '' }] },
        { id: id(), title: 'To Do', wip: 4, cards: [{ id: id(), title: 'Write launch brief', description: 'Define story, audience, and channels.', labels: ['green', 'yellow'], due: 'May 22' }, { id: id(), title: 'Component audit', description: 'Inventory visual debt before the next release.', labels: ['pink'], due: '' }] },
        { id: id(), title: 'In Progress', wip: 3, cards: [{ id: id(), title: 'Build usage dashboard', description: 'Usage trends and retention cohorts.', labels: ['blue'], due: 'May 20' }] },
        { id: id(), title: 'Review', wip: null, cards: [{ id: id(), title: 'Mobile navigation', description: 'Ready for interaction review.', labels: ['pink'], due: 'May 16' }] },
        { id: id(), title: 'Done', wip: null, cards: [{ id: id(), title: 'Q2 planning', description: 'Roadmap approved by the team.', labels: ['purple'], due: '' }] }
    ], filters: [], search: '' });
let model;
try {
    model = JSON.parse(localStorage.getItem(CONFIG.storageKey) || '');
    if (!model.columns?.length)
        throw 0;
}
catch {
    model = initial();
}
const save = () => localStorage.setItem(CONFIG.storageKey, JSON.stringify(model));
const app = document.querySelector('#app');
let undoAction = null, toastTimer = 0;
function esc(s) { return s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[c])); }
function visible(card) { return (!model.search || card.title.toLowerCase().includes(model.search.toLowerCase())) && (!model.filters.length || model.filters.every(x => card.labels.includes(x))); }
function render() {
    app.innerHTML = `<header class="topbar"><div class="brand"><i>Flow</i>board</div><input class="search" placeholder="Search cards…" value="${esc(model.search)}" aria-label="Search cards"><button class="button newcol">+ Add column</button></header><nav class="filterbar"><span>FILTER LABELS</span>${CONFIG.labels.map(l => `<button class="filter ${model.filters.includes(l.id) ? 'active' : ''}" style="--c:${l.color}" data-label="${l.id}">${l.name}</button>`).join('')}<button class="filter" id="clearFilters">Clear</button></nav><main class="board" id="board">${model.columns.map(columnHTML).join('')}</main>`;
    bind();
}
function columnHTML(c) { const over = c.wip !== null && c.cards.length > c.wip; const shown = c.cards.filter(visible); return `<section class="column ${over ? 'overlimit' : ''}" data-col="${c.id}"><div class="colhead" data-colhead="${c.id}"><div class="coltitle" title="Double click to rename">${esc(c.title)}</div><span class="count">${c.cards.length}${c.wip !== null ? ` / ${c.wip}` : ''}</span><button class="dots" data-menu="${c.id}" aria-label="Column menu">•••</button></div><div class="cards" data-cards="${c.id}">${shown.map(cardHTML).join('')}${shown.length ? '' : '<div class="empty">Drop cards here</div>'}</div><button class="addcard" data-add="${c.id}">+ Add card</button></section>`; }
function cardHTML(c) { return `<article class="card" data-card="${c.id}"><div class="card-title" data-title="${c.id}">${esc(c.title)}</div>${c.description ? `<div class="card-desc">${esc(c.description)}</div>` : ''}<div class="meta">${c.labels.map(x => { const l = CONFIG.labels.find(z => z.id === x); return `<span class="chip" style="background:${l.color}">${l.name}</span>`; }).join('')}${c.due ? `<span class="due">◷ ${esc(c.due)}</span>` : ''}</div></article>`; }
function findCard(cid) { for (const col of model.columns) {
    const i = col.cards.findIndex(c => c.id === cid);
    if (i >= 0)
        return { col, index: i, card: col.cards[i] };
} return null; }
function bind() {
    document.querySelector('.search').addEventListener('input', e => { model.search = e.target.value; save(); render(); });
    document.querySelector('.newcol').addEventListener('click', () => { const title = prompt('Column name', 'New column'); if (title?.trim()) {
        model.columns.push({ id: id(), title: title.trim(), cards: [], wip: null });
        save();
        render();
    } });
    document.querySelectorAll('[data-label]').forEach(b => b.onclick = () => { const x = b.dataset.label; model.filters = model.filters.includes(x) ? model.filters.filter(y => y !== x) : [...model.filters, x]; save(); render(); });
    document.querySelector('#clearFilters').addEventListener('click', () => { model.filters = []; model.search = ''; save(); render(); });
    document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => cardDialog(b.dataset.add));
    document.querySelectorAll('.card').forEach(el => { el.addEventListener('pointerdown', startCard); el.addEventListener('dblclick', () => { const f = findCard(el.dataset.card); if (f)
        cardDialog(f.col.id, f.card); }); });
    document.querySelectorAll('[data-colhead]').forEach(el => { el.addEventListener('pointerdown', startColumn); el.addEventListener('dblclick', () => renameColumn(el.dataset.colhead)); });
    document.querySelectorAll('[data-title]').forEach(el => el.addEventListener('click', () => inlineTitle(el)));
    document.querySelectorAll('[data-menu]').forEach(el => el.onclick = e => { e.stopPropagation(); columnMenu(el.dataset.menu, el.getBoundingClientRect()); });
}
function inlineTitle(el) { const found = findCard(el.dataset.title); if (!found)
    return; const old = found.card.title; el.contentEditable = 'true'; el.focus(); const range = document.createRange(); range.selectNodeContents(el); range.collapse(false); getSelection()?.removeAllRanges(); getSelection()?.addRange(range); const finish = (ok) => { el.contentEditable = 'false'; if (ok) {
    found.card.title = (el.textContent || '').trim() || old;
    save();
} render(); }; el.onkeydown = e => { if (e.key === 'Enter') {
    e.preventDefault();
    finish(true);
} if (e.key === 'Escape') {
    e.preventDefault();
    finish(false);
} }; el.onblur = () => finish(true); }
function renameColumn(cid) { const col = model.columns.find(x => x.id === cid); const n = prompt('Rename column', col.title); if (n?.trim()) {
    col.title = n.trim();
    save();
    render();
} }
function columnMenu(cid, r) { document.querySelector('.menu')?.remove(); const menu = document.createElement('div'); menu.className = 'menu'; menu.style.left = r.left + 'px'; menu.style.top = (r.bottom + 4) + 'px'; menu.innerHTML = `<button data-ren>Rename column</button><label>WIP limit <input type="number" min="1" value="${model.columns.find(c => c.id === cid).wip ?? ''}" placeholder="none"></label><button data-del>Delete column</button>`; document.body.append(menu); menu.querySelector('[data-ren]').addEventListener('click', () => { menu.remove(); renameColumn(cid); }); menu.querySelector('input').addEventListener('change', e => { const v = e.target.value; model.columns.find(c => c.id === cid).wip = v ? +v : null; save(); render(); }); menu.querySelector('[data-del]').addEventListener('click', () => { if (model.columns.length < 2)
    return alert('Keep at least one column.'); const idx = model.columns.findIndex(c => c.id === cid), removed = model.columns.splice(idx, 1)[0]; save(); render(); showUndo('Column deleted', () => { model.columns.splice(idx, 0, removed); save(); render(); }); }); setTimeout(() => document.addEventListener('pointerdown', () => menu.remove(), { once: true }), 0); }
function cardDialog(colId, card) { const overlay = document.createElement('div'); overlay.className = 'modal'; overlay.innerHTML = `<form class="dialog"><h3>${card ? 'Edit card' : 'New card'}</h3><input name="title" required placeholder="Card title" value="${esc(card?.title || '')}"><textarea name="desc" placeholder="Description">${esc(card?.description || '')}</textarea><input name="due" placeholder="Due date (e.g. May 24)" value="${esc(card?.due || '')}"><div class="labels">${CONFIG.labels.map(l => `<label><input type="checkbox" value="${l.id}" ${card?.labels.includes(l.id) ? 'checked' : ''}> ${l.name}</label>`).join(' ')}</div><div class="actions"><button type="button" class="cancel">Cancel</button><button class="button">Save</button></div></form>`; document.body.append(overlay); const form = overlay.querySelector('form'); overlay.querySelector('.cancel').addEventListener('click', () => overlay.remove()); form.addEventListener('submit', e => { e.preventDefault(); const data = new FormData(form); const value = { id: card?.id || id(), title: String(data.get('title')).trim(), description: String(data.get('desc')).trim(), due: String(data.get('due')).trim(), labels: data.getAll('labels') }; if (card)
    Object.assign(card, value);
else
    model.columns.find(c => c.id === colId).cards.push(value); save(); overlay.remove(); render(); }); }
function showUndo(text, fn) { undoAction = fn; clearTimeout(toastTimer); document.querySelector('.toast')?.remove(); const t = document.createElement('div'); t.className = 'toast'; t.innerHTML = `${text}<button>Undo</button>`; document.body.append(t); t.querySelector('button').onclick = () => { undoAction?.(); undoAction = null; t.remove(); }; toastTimer = window.setTimeout(() => t.remove(), 5000); }
let drag = null;
function startCard(e) { if (e.target.isContentEditable || model.filters.length || model.search)
    return; const el = e.currentTarget, found = findCard(el.dataset.card); if (!found)
    return; drag = { kind: 'card', id: found.card.id, el, startX: e.clientX, startY: e.clientY, origCol: found.col.id, origIndex: found.index, active: false }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', end, { once: true }); }
function startColumn(e) { if (e.target.closest('button'))
    return; const el = e.currentTarget.closest('.column'); drag = { kind: 'column', id: el.dataset.col, el, startX: e.clientX, startY: e.clientY, active: false }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', end, { once: true }); }
function move(e) { if (!drag)
    return; if (!drag.active) {
    if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < CONFIG.dragThreshold)
        return;
    drag.active = true;
    if (drag.kind === 'card') {
        const p = document.createElement('div');
        p.className = 'placeholder';
        drag.placeholder = p;
        drag.el.after(p);
        drag.el.classList.add('drag-source', 'dragging');
        drag.el.style.left = (e.clientX - 134) + 'px';
        drag.el.style.top = (e.clientY - 24) + 'px';
    }
    else
        drag.el.classList.add('col-drag');
} if (drag.kind === 'card') {
    drag.el.style.left = (e.clientX - 134) + 'px';
    drag.el.style.top = (e.clientY - 24) + 'px';
    placeCard(e.clientX, e.clientY);
    autoScroll(e);
}
else
    placeColumn(e.clientX); }
function placeCard(x, y) { if (!drag?.placeholder)
    return; const target = document.elementsFromPoint(x, y).find(n => n.matches?.('.cards')); if (!target)
    return; const cards = [...target.querySelectorAll('.card:not(.drag-source)')]; const before = cards.find(c => y < c.getBoundingClientRect().top + c.getBoundingClientRect().height / 2); if (before)
    target.insertBefore(drag.placeholder, before);
else
    target.append(drag.placeholder); target.querySelector('.empty')?.remove(); }
function placeColumn(x) { if (!drag)
    return; const board = document.querySelector('#board'); const cols = [...board.querySelectorAll('.column:not(.col-drag)')]; const before = cols.find(c => x < c.getBoundingClientRect().left + c.offsetWidth / 2); if (before)
    board.insertBefore(drag.el, before);
else
    board.append(drag.el); }
function autoScroll(e) { const board = document.querySelector('#board'); if (e.clientX < CONFIG.edge)
    board.scrollLeft -= CONFIG.scrollSpeed;
else if (e.clientX > innerWidth - CONFIG.edge)
    board.scrollLeft += CONFIG.scrollSpeed; const target = document.elementsFromPoint(e.clientX, e.clientY).find(n => n.matches?.('.cards')); if (target) {
    const r = target.getBoundingClientRect();
    if (e.clientY < r.top + CONFIG.edge)
        target.scrollTop -= CONFIG.scrollSpeed;
    else if (e.clientY > r.bottom - CONFIG.edge)
        target.scrollTop += CONFIG.scrollSpeed;
} }
function end() { window.removeEventListener('pointermove', move); if (!drag)
    return; const d = drag; if (d.active) {
    if (d.kind === 'card' && d.placeholder) {
        const ph = d.placeholder;
        const cards = ph.parentElement;
        const col = model.columns.find(c => c.id === cards.dataset.cards);
        const index = [...cards.querySelectorAll('.card:not(.drag-source)')].filter(c => c.compareDocumentPosition(ph) & Node.DOCUMENT_POSITION_FOLLOWING).length;
        const source = model.columns.find(c => c.id === d.origCol);
        const card = source.cards.splice(d.origIndex, 1)[0];
        col.cards.splice(index, 0, card);
    }
    else if (d.kind === 'column') {
        const order = [...document.querySelectorAll('.column')].map(x => x.dataset.col);
        model.columns.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
    }
    save();
} drag = null; render(); }
document.addEventListener('keydown', e => { if (e.key === 'Escape' && drag) {
    window.removeEventListener('pointermove', move);
    drag = null;
    render();
} });
// Test-facing API, deliberately model-driven.
window.flowboard = { get: () => JSON.parse(JSON.stringify(model)), set: m => { model = m; save(); render(); } };
render();
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-terra","provider":"openrouter","persona":"typescript","sessionId":"99cd7f2f-ecc7-4e0f-bd0e-6151e50d8996","tokensIn":122185,"tokensOut":7291,"tokensTotal":129476,"turns":12,"toolCalls":19,"failedToolCalls":1,"timestamp":"2026-07-31T17:59:50.445Z"} -->