Kanban
thinkingcap qwen3.6 27b · typescript

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 52,731 bytes · SHA-256 74e6e7f6c58e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kanban Board</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
background: #f1f5f9;
color: #1e293b;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
/* ── Header bar ─────────────────────────────────────────────── */
.header-bar {
padding: 12px 24px;
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.filter-bar {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.search-wrap { position: relative; }
.search-input {
padding: 8px 14px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
width: 220px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99,102,241,.15);
}
.label-filters {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.label-chip {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
border: 2px solid transparent;
background: #f8fafc;
color: var(--chip-color);
cursor: pointer;
transition: all 0.15s;
}
.label-chip:hover { background: #e2e8f0; }
.label-chip.active {
border-color: var(--chip-color);
background: rgba(99,102,241,.08);
}
.clear-filters-btn {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
border: 1px solid #e2e8f0;
background: #fff;
cursor: pointer;
color: #64748b;
}
.clear-filters-btn:hover { background: #fee2e2; color: #ef4444; }
/* ── Board (columns container) ─────────────────────────────── */
.board {
display: flex;
gap: 16px;
padding: 20px 24px;
overflow-x: auto;
overflow-y: hidden;
flex: 1;
align-items: flex-start;
}
/* ── Column ─────────────────────────────────────────────────── */
.column {
min-width: 280px;
max-width: 320px;
width: 300px;
background: #e2e8f0;
border-radius: 12px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 90px);
transition: box-shadow 0.2s;
}
.column.wip-over-limit {
background: rgba(239,68,68,.08);
outline: 2px solid rgba(239,68,68,.3);
}
.column-header {
padding: 14px 16px 10px;
display: flex;
align-items: center;
gap: 8px;
cursor: grab;
user-select: none;
}
.column-title {
font-weight: 600;
font-size: 14px;
flex: 1;
cursor: pointer;
}
.card-count {
background: #cbd5e1;
color: #475569;
font-size: 12px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
}
.col-menu-btn {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: #94a3b8;
padding: 2px 6px;
border-radius: 4px;
}
.col-menu-btn:hover { background: rgba(0,0,0,.06); color: #475569; }
/* ── Card list (scrollable) ─────────────────────────────────── */
.card-list {
flex: 1;
overflow-y: auto;
padding: 4px 12px 8px;
}
.empty-column-hint {
border: 2px dashed #cbd5e1;
border-radius: 8px;
padding: 32px 16px;
text-align: center;
color: #94a3b8;
font-size: 13px;
}
/* ── Card ───────────────────────────────────────────────────── */
.card {
background: #ffffff;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.06);
cursor: grab;
transition: transform 0.15s, box-shadow 0.15s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,.1);
}
.card-labels { display: flex; gap: 4px; margin-bottom: 8px; flex-wrap: wrap; }
.card-label-chip {
font-size: 11px;
font-weight: 500;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
}
.card-title {
font-size: 14px;
font-weight: 500;
line-height: 1.4;
margin-bottom: 4px;
}
.card-desc {
font-size: 12px;
color: #64748b;
line-height: 1.4;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.card-due {
font-size: 12px;
color: #94a3b8;
}
.card-menu-btn {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: #cbd5e1;
padding: 2px 4px;
border-radius: 4px;
}
.card-menu-btn:hover { background: rgba(0,0,0,.06); color: #64748b; }
/* ── Add card button ────────────────────────────────────────── */
.add-card-btn {
margin: 0 12px 12px;
padding: 8px;
border: none;
background: transparent;
color: #64748b;
font-size: 13px;
cursor: pointer;
border-radius: 6px;
text-align: left;
}
.add-card-btn:hover { background: rgba(0,0,0,.05); color: #334155; }
/* ── Drag ghost & placeholder ───────────────────────────────── */
.drag-ghost {
position: fixed;
z-index: 1000;
pointer-events: none;
}
.drop-placeholder {
height: 4px;
background: rgba(99,102,241,.3);
border-radius: 4px;
margin-bottom: 8px;
}
/* ── Inline edit input ──────────────────────────────────────── */
.inline-edit-input {
font-size: inherit;
font-weight: inherit;
padding: 2px 6px;
border: 1px solid #6366f1;
border-radius: 4px;
outline: none;
width: 100%;
}
/* ── Dropdown menu ──────────────────────────────────────────── */
.dropdown-menu {
position: fixed;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,.15);
padding: 6px 0;
z-index: 999;
min-width: 180px;
}
.menu-item {
padding: 8px 16px;
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.menu-item:hover { background: #f1f5f9; }
.menu-item.danger { color: #ef4444; }
.menu-item.danger:hover { background: #fef2f2; }
.label-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
/* ── Toast notifications ────────────────────────────────────── */
.toast-container {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.toast {
background: #1e293b;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,.2);
animation: toast-in 0.3s ease-out;
}
.toast-undo-btn {
background: #fff;
color: #1e293b;
border: none;
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
font-weight: 500;
}
.toast-undo-btn:hover { background: #e2e8f0; }
@keyframes toast-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Scrollbar styling ──────────────────────────────────────── */
.card-list::-webkit-scrollbar { width: 6px; }
.card-list::-webkit-scrollbar-track { background: transparent; }
.card-list::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
.board::-webkit-scrollbar { height: 8px; }
.board::-webkit-scrollbar-track { background: transparent; }
.board::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
/* ── Add column button ──────────────────────────────────────── */
.add-column-btn {
min-width: 280px;
width: 300px;
padding: 16px;
background: rgba(255,255,255,.5);
border: 2px dashed #cbd5e1;
border-radius: 12px;
color: #94a3b8;
font-size: 14px;
cursor: pointer;
text-align: center;
}
.add-column-btn:hover { background: rgba(255,255,255,.8); border-color: #6366f1; color: #6366f1; }
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./rendering":"src/rendering.ts","./dragdrop":"src/dragdrop.ts","./state":"src/state.ts"},"src/state.ts":{"./constants":"src/constants.ts"},"src/rendering.ts":{"./constants":"src/constants.ts","./state":"src/state.ts"},"src/dragdrop.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./rendering":"src/rendering.ts"}};
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";
// ─── Entry Point ─────────────────────────────────────────────────────
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const rendering_1 = require("./rendering");
const dragdrop_1 = require("./dragdrop");
const S = __importStar(require("./state"));
// Expose render for testing
if (typeof window !== "undefined") {
window.__KANBAN_RENDER = rendering_1.render;
}
function start() {
// Initial render
(0, rendering_1.render)();
// Add column button at end of board
addColumnButton();
// Init drag & drop system
(0, dragdrop_1.initDragDrop)();
}
function addColumnButton() {
const board = document.querySelector(".board");
const btn = document.createElement("button");
btn.className = "add-column-btn";
btn.textContent = "+ Add column";
btn.addEventListener("click", () => {
S.addColumn("New Column");
(0, rendering_1.render)();
addColumnButton(); // Re-append after re-render
});
board.appendChild(btn);
}
start();
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ─── Tunable Constants ───────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.COLORS = exports.DRAG = exports.DEFAULT_COLUMNS = exports.DEFAULT_LABELS = exports.UNDO_TIMEOUT_MS = exports.STORAGE_KEY = void 0;
exports.STORAGE_KEY = "kanban-board-state";
exports.UNDO_TIMEOUT_MS = 5000;
exports.DEFAULT_LABELS = [
{ id: "l-urgent", name: "Urgent", color: "#ef4444" },
{ id: "l-feature", name: "Feature", color: "#3b82f6" },
{ id: "l-bug", name: "Bug", color: "#f97316" },
{ id: "l-enhance", name: "Enhancement", color: "#8b5cf6" },
{ id: "l-docs", name: "Docs", color: "#06b6d4" },
{ id: "l-design", name: "Design", color: "#ec4899" },
];
exports.DEFAULT_COLUMNS = [
{ title: "Backlog", wipLimit: undefined },
{ title: "To Do", wipLimit: undefined },
{ title: "In Progress", wipLimit: undefined },
{ title: "Review", wipLimit: undefined },
{ title: "Done", wipLimit: undefined },
];
exports.DRAG = {
thresholdPx: 5, // px before drag starts
placeholderHeight: 4, // gap indicator height in px
scrollMarginPx: 60, // distance from edge to trigger auto-scroll
scrollSpeed: 12, // px per frame during auto-scroll
cardTiltDeg: 3, // tilt angle while dragging
elevationShadow: "0 8px 30px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.12)",
};
exports.COLORS = {
bg: "#f1f5f9",
columnBg: "#e2e8f0",
cardBg: "#ffffff",
textPrimary: "#1e293b",
textSecondary: "#64748b",
accent: "#6366f1",
danger: "#ef4444",
wipOverLimit: "rgba(239,68,68,.15)",
placeholderBg: "rgba(99,102,241,.12)",
};
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ─── Board Model & State Management ──────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.pushUndo = pushUndo;
exports.canUndo = canUndo;
exports.setUndoCallback = setUndoCallback;
exports.performUndo = performUndo;
exports.save = save;
exports.load = load;
exports.getBoard = getBoard;
exports.setBoard = setBoard;
exports.addColumn = addColumn;
exports.renameColumn = renameColumn;
exports.deleteColumn = deleteColumn;
exports.setWipLimit = setWipLimit;
exports.addCard = addCard;
exports.updateCard = updateCard;
exports.deleteCard = deleteCard;
exports.moveCard = moveCard;
exports.reorderColumns = reorderColumns;
exports.setFilterLabels = setFilterLabels;
exports.setSearchQuery = setSearchQuery;
exports.cardMatchesFilters = cardMatchesFilters;
const constants_1 = require("./constants");
let _idCounter = Date.now();
function uid() { return `id-${_idCounter++}`; }
const undoStack = [];
let undoTimer = null;
function pushUndo(label) {
const snap = deepClone(getBoard());
undoStack.push({ snapshot: snap, label, timestamp: Date.now() });
if (undoStack.length > 20)
undoStack.shift();
}
function canUndo() {
return undoStack.length > 0 && (Date.now() - undoStack[undoStack.length - 1].timestamp < constants_1.UNDO_TIMEOUT_MS);
}
let _undoCallback = null;
function setUndoCallback(cb) { _undoCallback = cb; }
function performUndo() {
if (!canUndo())
return false;
const entry = undoStack.pop();
setBoard(entry.snapshot);
_undoCallback?.();
return true;
}
// ─── Persistence ─────────────────────────────────────────────────────
function deepClone(o) { return JSON.parse(JSON.stringify(o)); }
function save() {
try {
localStorage.setItem(constants_1.STORAGE_KEY, JSON.stringify(getBoard()));
}
catch { }
}
function load() {
try {
const raw = localStorage.getItem(constants_1.STORAGE_KEY);
if (!raw)
return null;
return JSON.parse(raw);
}
catch {
return null;
}
}
// ─── State accessors ─────────────────────────────────────────────────
let _state;
function initState() {
const saved = load();
if (saved) {
_state = saved;
}
else {
_state = createDefaultBoard();
}
}
function getBoard() { return deepClone(_state); }
function setBoard(s) { _state = s; save(); }
function createDefaultBoard() {
const cards = {};
const columns = constants_1.DEFAULT_COLUMNS.map(c => ({
id: uid(),
title: c.title,
cardIds: [],
wipLimit: c.wipLimit,
}));
// Seed some demo cards
const demoCards = [
{ colIdx: 0, title: "Research competitors", desc: "Analyze top 5 competitor products" },
{ colIdx: 1, title: "Design wireframes", desc: "Low-fidelity mockups for main flow", labels: ["l-design"] },
{ colIdx: 1, title: "Set up CI/CD pipeline", desc: "", labels: ["l-feature"] },
{ colIdx: 2, title: "Implement auth flow", desc: "OAuth + session management", labels: ["l-feature"], dueDate: getFutureDate(3) },
{ colIdx: 2, title: "Fix login redirect bug", desc: "Users redirected to 404 after SSO", labels: ["l-bug", "l-urgent"] },
{ colIdx: 3, title: "API documentation", desc: "OpenAPI spec for v2 endpoints", labels: ["l-docs"] },
{ colIdx: 4, title: "Project kickoff meeting", desc: "" },
];
demoCards.forEach(d => {
const cardId = uid();
cards[cardId] = { id: cardId, title: d.title, description: d.desc, labelIds: d.labels || [], dueDate: d.dueDate };
columns[d.colIdx].cardIds.push(cardId);
});
return { columns, cards, labels: constants_1.DEFAULT_LABELS, filterLabelIds: [], searchQuery: "" };
}
function getFutureDate(days) {
const d = new Date();
d.setDate(d.getDate() + days);
return d.toISOString().slice(0, 10);
}
// ─── Column CRUD ─────────────────────────────────────────────────────
function addColumn(title) {
pushUndo("delete column");
_state.columns.push({ id: uid(), title, cardIds: [], wipLimit: undefined });
save();
}
function renameColumn(colId, newTitle) {
const col = _state.columns.find(c => c.id === colId);
if (col) {
col.title = newTitle;
save();
}
}
function deleteColumn(colId) {
if (_state.columns.length <= 1)
return; // keep at least one
pushUndo("delete column");
const col = _state.columns.find(c => c.id === colId);
if (col) {
Object.keys(_state.cards).filter(k => col.cardIds.includes(k)).forEach(k => delete _state.cards[k]);
}
_state.columns = _state.columns.filter(c => c.id !== colId);
save();
}
function setWipLimit(colId, limit) {
const col = _state.columns.find(c => c.id === colId);
if (col) {
col.wipLimit = limit;
save();
}
}
// ─── Card CRUD ───────────────────────────────────────────────────────
function addCard(colId, title, description) {
pushUndo("delete card");
const cardId = uid();
_state.cards[cardId] = { id: cardId, title, description: description || "", labelIds: [] };
const col = _state.columns.find(c => c.id === colId);
if (col)
col.cardIds.push(cardId);
save();
}
function updateCard(cardId, updates) {
const card = _state.cards[cardId];
if (card) {
Object.assign(card, updates);
save();
}
}
function deleteCard(cardId) {
pushUndo("delete card");
delete _state.cards[cardId];
_state.columns.forEach(c => { c.cardIds = c.cardIds.filter(id => id !== cardId); });
save();
}
// ─── Drag & Drop state mutations ─────────────────────────────────────
function moveCard(cardId, toColId, toIndex) {
// Remove from source column
let srcCol;
_state.columns.forEach(c => {
const idx = c.cardIds.indexOf(cardId);
if (idx !== -1) {
c.cardIds.splice(idx, 1);
srcCol = c;
}
});
// Insert into target column at index
const tgtCol = _state.columns.find(c => c.id === toColId);
if (tgtCol) {
const insertAt = Math.min(toIndex, tgtCol.cardIds.length);
tgtCol.cardIds.splice(insertAt, 0, cardId);
}
else if (srcCol) {
// Target column was deleted? Put back at end of source.
srcCol.cardIds.push(cardId);
}
save();
}
function reorderColumns(fromIdx, toIdx) {
const col = _state.columns.splice(fromIdx, 1)[0];
if (col)
_state.columns.splice(Math.min(toIdx, _state.columns.length), 0, col);
save();
}
// ─── Filters ─────────────────────────────────────────────────────────
function setFilterLabels(ids) {
_state.filterLabelIds = ids;
save();
}
function setSearchQuery(q) {
_state.searchQuery = q;
save();
}
function cardMatchesFilters(cardId) {
const card = _state.cards[cardId];
if (!card)
return false;
// Label filter: show only cards that have ALL selected labels (if any selected)
if (_state.filterLabelIds.length > 0) {
for (const lid of _state.filterLabelIds) {
if (!card.labelIds.includes(lid))
return false;
}
}
// Search filter: title contains query (case-insensitive)
if (_state.searchQuery.trim()) {
if (!card.title.toLowerCase().includes(_state.searchQuery.trim().toLowerCase()))
return false;
}
return true;
}
// ─── Init ────────────────────────────────────────────────────────────
initState();
// Expose for testing/debugging
if (typeof window !== "undefined") {
window.__KANBAN = {
getBoard, setBoard, moveCard, reorderColumns, addColumn, deleteColumn,
addCard, deleteCard, updateCard, canUndo, performUndo, save, load,
cardMatchesFilters, setFilterLabels, setSearchQuery, renameColumn, setWipLimit, pushUndo,
};
// Expose render for testing
window.__KANBAN_RENDER = null; // will be set by main.ts
}
};
// ── module: src/rendering.ts ──
__mods["src/rendering.ts"] = function (exports, require, module) {
"use strict";
// ─── Rendering: Board → DOM ──────────────────────────────────────────
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
exports.showToast = showToast;
const constants_1 = require("./constants");
const S = __importStar(require("./state"));
const app = document.getElementById("app");
function render() {
const board = S.getBoard();
// Build the full DOM tree from state — single source of truth.
app.innerHTML = "";
// ── Header bar ────────────────────────────────────────────────────
const headerBar = el("div", "header-bar");
headerBar.appendChild(buildFilterBar(board));
app.appendChild(headerBar);
// ── Board (columns container) ─────────────────────────────────────
const boardEl = el("div", "board");
for (const col of board.columns) {
boardEl.appendChild(buildColumn(col, board.cards, board.labels));
}
app.appendChild(boardEl);
// ── Toast container ───────────────────────────────────────────────
if (!document.getElementById("toast-container")) {
app.appendChild(el("div", "toast-container", { id: "toast-container" }));
}
}
// ── Filter bar (search + label filters) ──────────────────────────────
function buildFilterBar(board) {
const bar = el("div", "filter-bar");
// Search input
const searchWrap = el("div", "search-wrap");
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Search cards…";
searchInput.className = "search-input";
searchInput.value = board.searchQuery;
searchInput.addEventListener("input", () => {
S.setSearchQuery(searchInput.value);
render();
});
searchWrap.appendChild(searchInput);
bar.appendChild(searchWrap);
// Label filter chips
const labelWrap = el("div", "label-filters");
for (const lbl of board.labels) {
const chip = document.createElement("button");
chip.className = `label-chip${board.filterLabelIds.includes(lbl.id) ? " active" : ""}`;
chip.style.setProperty("--chip-color", lbl.color);
chip.textContent = lbl.name;
chip.addEventListener("click", () => {
const ids = board.filterLabelIds.includes(lbl.id)
? board.filterLabelIds.filter(x => x !== lbl.id)
: [...board.filterLabelIds, lbl.id];
S.setFilterLabels(ids);
render();
});
labelWrap.appendChild(chip);
}
// Clear filters button (only if active)
if (board.filterLabelIds.length > 0 || board.searchQuery.trim()) {
const clearBtn = document.createElement("button");
clearBtn.className = "clear-filters-btn";
clearBtn.textContent = "✕ Clear";
clearBtn.addEventListener("click", () => {
S.setFilterLabels([]);
S.setSearchQuery("");
render();
});
labelWrap.appendChild(clearBtn);
}
bar.appendChild(labelWrap);
return bar;
}
// ── Column ───────────────────────────────────────────────────────────
function buildColumn(col, cards, labels) {
const colEl = el("div", "column");
colEl.dataset.colId = col.id;
// WIP limit highlight
if (col.wipLimit !== undefined && col.cardIds.length > col.wipLimit) {
colEl.classList.add("wip-over-limit");
}
// Header
const header = el("div", "column-header");
const titleSpan = document.createElement("span");
titleSpan.className = "column-title";
titleSpan.textContent = col.title;
titleSpan.addEventListener("dblclick", () => startEditColumnTitle(col.id, titleSpan));
header.appendChild(titleSpan);
// Card count badge
const badge = el("span", "card-count");
badge.textContent = String(col.cardIds.length);
header.appendChild(badge);
// Column menu button (⋮)
const menuBtn = document.createElement("button");
menuBtn.className = "col-menu-btn";
menuBtn.textContent = "⋮";
menuBtn.addEventListener("click", (e) => { e.stopPropagation(); showColumnMenu(col.id, menuBtn); });
header.appendChild(menuBtn);
colEl.appendChild(header);
// Card list
const cardList = el("div", "card-list");
cardList.dataset.colId = col.id;
if (col.cardIds.length === 0) {
// Empty column drop zone hint
const emptyHint = el("div", "empty-column-hint");
emptyHint.textContent = "Drop cards here";
cardList.appendChild(emptyHint);
}
else {
for (const cardId of col.cardIds) {
const card = cards[cardId];
if (!card)
continue;
if (!S.cardMatchesFilters(cardId))
continue; // filtered out — skip rendering
cardList.appendChild(buildCard(card, labels));
}
}
colEl.appendChild(cardList);
// Add card button
const addBtn = el("button", "add-card-btn");
addBtn.textContent = "+ Add card";
addBtn.addEventListener("click", () => {
S.addCard(col.id, "New Card");
render();
// Auto-focus the new card's title for inline editing
const firstTitle = cardList.querySelector(".card-title[contenteditable]");
if (!firstTitle) {
// Find the last added card and make it editable
const titles = cardList.querySelectorAll(".card-title");
if (titles.length > 0) {
const t = titles[titles.length - 1];
startEditCardTitle(t);
}
}
});
colEl.appendChild(addBtn);
return colEl;
}
// ── Card ─────────────────────────────────────────────────────────────
function buildCard(card, labels) {
const cardEl = el("div", "card");
cardEl.dataset.cardId = card.id;
// Labels row
if (card.labelIds.length > 0) {
const labelRow = el("div", "card-labels");
for (const lid of card.labelIds) {
const lbl = labels.find(l => l.id === lid);
if (!lbl)
continue;
const chip = document.createElement("span");
chip.className = "card-label-chip";
chip.style.backgroundColor = lbl.color;
chip.textContent = lbl.name;
labelRow.appendChild(chip);
}
cardEl.appendChild(labelRow);
}
// Title
const title = el("div", "card-title");
title.textContent = card.title;
title.addEventListener("dblclick", () => startEditCardTitle(title, card.id));
cardEl.appendChild(title);
// Description (truncated)
if (card.description) {
const desc = el("div", "card-desc");
desc.textContent = card.description.length > 80 ? card.description.slice(0, 80) + "…" : card.description;
cardEl.appendChild(desc);
}
// Footer: due date + delete button
const footer = el("div", "card-footer");
if (card.dueDate) {
const due = document.createElement("span");
due.className = "card-due";
due.textContent = card.dueDate;
footer.appendChild(due);
}
// Card menu button
const cMenuBtn = document.createElement("button");
cMenuBtn.className = "card-menu-btn";
cMenuBtn.textContent = "⋮";
cMenuBtn.addEventListener("click", (e) => { e.stopPropagation(); showCardMenu(card.id, cMenuBtn); });
footer.appendChild(cMenuBtn);
cardEl.appendChild(footer);
return cardEl;
}
// ── Inline editing helpers ───────────────────────────────────────────
function startEditColumnTitle(colId, titleSpan) {
const input = document.createElement("input");
input.type = "text";
input.value = titleSpan.textContent || "";
input.className = "inline-edit-input";
titleSpan.replaceWith(input);
input.focus();
input.select();
const commit = () => {
if (input.value.trim())
S.renameColumn(colId, input.value.trim());
render();
};
const cancel = () => { render(); };
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
commit();
}
if (e.key === "Escape") {
e.preventDefault();
cancel();
}
});
input.addEventListener("blur", commit);
}
function startEditCardTitle(titleEl, cardId) {
const current = titleEl.textContent || "";
const input = document.createElement("input");
input.type = "text";
input.value = current;
input.className = "inline-edit-input";
titleEl.replaceWith(input);
input.focus();
input.select();
const commit = () => {
if (cardId && input.value.trim())
S.updateCard(cardId, { title: input.value.trim() });
render();
};
const cancel = () => { render(); };
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
commit();
}
if (e.key === "Escape") {
e.preventDefault();
cancel();
}
});
input.addEventListener("blur", commit);
}
// ── Column menu ──────────────────────────────────────────────────────
function showColumnMenu(colId, btn) {
removeMenus();
const board = S.getBoard();
const col = board.columns.find(c => c.id === colId);
if (!col)
return;
const menu = el("div", "dropdown-menu");
menu.style.position = "fixed";
const rect = btn.getBoundingClientRect();
menu.style.top = (rect.bottom + 4) + "px";
menu.style.left = rect.left + "px";
// WIP limit item
const wipItem = el("div", "menu-item");
wipItem.textContent = col.wipLimit !== undefined ? `WIP Limit: ${col.wipLimit}` : "No WIP Limit";
wipItem.addEventListener("click", () => {
removeMenus();
if (col.wipLimit !== undefined)
S.setWipLimit(colId, undefined);
else
S.setWipLimit(colId, col.cardIds.length + 2);
render();
});
menu.appendChild(wipItem);
// Rename item
const renameItem = el("div", "menu-item");
renameItem.textContent = "Rename";
renameItem.addEventListener("click", () => {
removeMenus();
const titleSpan = document.querySelector(`[data-col-id="${colId}"] .column-title`);
if (titleSpan)
startEditColumnTitle(colId, titleSpan);
});
menu.appendChild(renameItem);
// Delete item
if (board.columns.length > 1) {
const delItem = el("div", "menu-item danger");
delItem.textContent = "Delete column";
delItem.addEventListener("click", () => {
removeMenus();
S.deleteColumn(colId);
render();
showToast(`Column "${col.title}" deleted`, "undo");
});
menu.appendChild(delItem);
}
document.body.appendChild(menu);
}
// ── Card menu ────────────────────────────────────────────────────────
function showCardMenu(cardId, btn) {
removeMenus();
const board = S.getBoard();
const card = board.cards[cardId];
if (!card)
return;
const menu = el("div", "dropdown-menu");
menu.style.position = "fixed";
const rect = btn.getBoundingClientRect();
menu.style.top = (rect.bottom + 4) + "px";
menu.style.left = rect.left + "px";
// Label toggles
for (const lbl of board.labels) {
const item = el("div", "menu-item");
const hasLabel = card.labelIds.includes(lbl.id);
item.innerHTML = `<span class="label-dot" style="background:${lbl.color}"></span> ${hasLabel ? "Remove" : "Add"}: ${lbl.name}`;
item.addEventListener("click", () => {
removeMenus();
const ids = hasLabel ? card.labelIds.filter(x => x !== lbl.id) : [...card.labelIds, lbl.id];
S.updateCard(cardId, { labelIds: ids });
render();
});
menu.appendChild(item);
}
// Due date toggle
const dueItem = el("div", "menu-item");
dueItem.textContent = card.dueDate ? `Due: ${card.dueDate}` : "Set due date";
dueItem.addEventListener("click", () => {
removeMenus();
if (card.dueDate)
S.updateCard(cardId, { dueDate: undefined });
else {
const d = new Date();
d.setDate(d.getDate() + 7);
S.updateCard(cardId, { dueDate: d.toISOString().slice(0, 10) });
}
render();
});
menu.appendChild(dueItem);
// Delete
const delItem = el("div", "menu-item danger");
delItem.textContent = "Delete card";
delItem.addEventListener("click", () => {
removeMenus();
S.deleteCard(cardId);
render();
showToast(`Card "${card.title}" deleted`, "undo");
});
menu.appendChild(delItem);
document.body.appendChild(menu);
}
function removeMenus() {
document.querySelectorAll(".dropdown-menu").forEach(m => m.remove());
}
// Close menus on outside click
document.addEventListener("click", (e) => {
if (!e.target.closest(".dropdown-menu") && !e.target.closest("[class$='-menu-btn']")) {
removeMenus();
}
});
// ── Toast notifications ──────────────────────────────────────────────
function showToast(message, type) {
const container = document.getElementById("toast-container");
if (!container)
return;
const toast = el("div", `toast toast-${type || "info"}`);
toast.textContent = message;
if (type === "undo") {
const undoBtn = document.createElement("button");
undoBtn.className = "toast-undo-btn";
undoBtn.textContent = "Undo";
undoBtn.addEventListener("click", () => {
S.performUndo();
render();
toast.remove();
});
toast.appendChild(undoBtn);
}
container.appendChild(toast);
setTimeout(() => { if (toast.parentNode)
toast.remove(); }, constants_1.UNDO_TIMEOUT_MS + 500);
}
// ── Utility ──────────────────────────────────────────────────────────
function el(tag, cls, attrs) {
const e = document.createElement(tag);
if (cls)
e.className = cls;
if (attrs)
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
return e;
}
};
// ── module: src/dragdrop.ts ──
__mods["src/dragdrop.ts"] = function (exports, require, module) {
"use strict";
// ─── Drag & Drop System (Pointer Events) ─────────────────────────────
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.initDragDrop = initDragDrop;
const constants_1 = require("./constants");
const S = __importStar(require("./state"));
const rendering_1 = require("./rendering");
let drag = null;
// ─── Card Drag ────────────────────────────────────────────────
function initDragDrop() {
const app = document.getElementById("app");
// pointerdown on cards — delegated from app
app.addEventListener("pointerdown", (e) => {
if (drag?.started)
return; // already dragging
const cardEl = e.target.closest(".card");
if (!cardEl || e.button !== 0)
return;
if (e.target.matches("button"))
return;
const cardId = cardEl.dataset.cardId;
const colEl = cardEl.closest(".column");
const colId = colEl.dataset.colId;
const board = S.getBoard();
const col = board.columns.find(c => c.id === colId);
const sourceIndex = col.cardIds.indexOf(cardId);
// Create ghost element (visual clone that follows cursor)
const ghost = cardEl.cloneNode(true);
ghost.classList.add("drag-ghost");
ghost.style.width = cardEl.offsetWidth + "px";
document.body.appendChild(ghost);
// Calculate offset from card top-left to pointer
const rect = cardEl.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
drag = {
cardId, sourceColId: colId, sourceIndex,
ghostEl: ghost, placeholder: null,
offsetX, offsetY, started: false,
startClientX: e.clientX,
startClientY: e.clientY,
};
// Hide original card (we'll show a placeholder instead)
cardEl.style.opacity = "0.3";
cardEl.style.transition = "opacity 0.15s";
});
// pointermove — listen on document to catch events regardless of target
document.addEventListener("pointermove", (e) => {
if (!drag)
return;
// Check threshold before starting drag
if (!drag.started) {
const dx = Math.abs(e.clientX - drag.startClientX);
const dy = Math.abs(e.clientY - drag.startClientY);
if (dx < constants_1.DRAG.thresholdPx && dy < constants_1.DRAG.thresholdPx)
return;
drag.started = true;
// Add elevation and tilt to ghost
drag.ghostEl.style.boxShadow = constants_1.DRAG.elevationShadow;
drag.ghostEl.style.transform = `rotate(${constants_1.DRAG.cardTiltDeg}deg)`;
drag.ghostEl.style.transition = "none";
// Insert placeholder at original position
const origCard = document.querySelector(`.card[data-card-id="${drag.cardId}"]`);
if (origCard) {
const ph = createPlaceholder();
origCard.parentNode?.insertBefore(ph, origCard.nextSibling);
drag.placeholder = ph;
}
// Remove empty-column hints from columns we're dragging through
document.querySelectorAll(".empty-column-hint").forEach(h => h.remove());
}
if (!drag.started)
return;
// Move ghost to follow cursor
drag.ghostEl.style.position = "fixed";
drag.ghostEl.style.left = (e.clientX - drag.offsetX) + "px";
drag.ghostEl.style.top = (e.clientY - drag.offsetY) + "px";
drag.ghostEl.style.zIndex = "1000";
// Auto-scroll
handleAutoScroll(e.clientX, e.clientY);
// Update placeholder position based on cursor location
updateDropTarget(e.clientX, e.clientY);
});
// pointerup — listen on document to catch events regardless of target
document.addEventListener("pointerup", (e) => {
if (!drag)
return;
finishDrag();
});
// Escape to cancel
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && drag?.started) {
cancelDrag();
}
});
// Column header drag for reordering
app.addEventListener("pointerdown", (e) => {
const header = e.target.closest(".column-header");
if (!header || e.button !== 0)
return;
if (e.target.matches("button"))
return;
const colEl = header.closest(".column");
const board = S.getBoard();
let fromIdx = board.columns.findIndex(c => c.id === colEl.dataset.colId);
if (fromIdx < 0)
return;
let colDragActive = false;
const startClientX = e.clientX;
const onColMove = (me) => {
if (!colDragActive && Math.abs(me.clientX - startClientX) > constants_1.DRAG.thresholdPx) {
colDragActive = true;
S.pushUndo("reorder columns");
colEl.style.opacity = "0.5";
colEl.style.transition = "none";
}
if (!colDragActive)
return;
// Find which column we're over
const cols = document.querySelectorAll(".column");
for (const c of cols) {
const rect = c.getBoundingClientRect();
if (me.clientX > rect.left && me.clientX < rect.right) {
const toIdx = board.columns.findIndex(bc => bc.id === c.dataset.colId);
if (toIdx !== fromIdx && toIdx >= 0) {
S.reorderColumns(fromIdx, toIdx);
// Re-find our column index after reorder
fromIdx = board.columns.findIndex(bc => bc.id === colEl.dataset.colId);
}
}
}
};
const onColUp = () => {
document.removeEventListener("pointermove", onColMove);
document.removeEventListener("pointerup", onColUp);
colEl.style.opacity = "";
(0, rendering_1.render)();
};
document.addEventListener("pointermove", onColMove);
document.addEventListener("pointerup", onColUp);
});
}
// ─── Drop target calculation ────────────────────────────────
function updateDropTarget(clientX, clientY) {
if (!drag)
return;
// Find which column we're over
const cols = document.querySelectorAll(".column");
let targetColEl = null;
for (const c of cols) {
const rect = c.getBoundingClientRect();
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
targetColEl = c;
break;
}
}
if (!targetColEl)
return;
const cardList = targetColEl.querySelector(".card-list");
const visibleCards = cardList.querySelectorAll(".card:not(.drag-ghost)");
// Find the closest card to the cursor (by vertical distance)
let insertIndex = visibleCards.length;
for (let i = 0; i < visibleCards.length; i++) {
const rect = visibleCards[i].getBoundingClientRect();
const midY = rect.top + rect.height / 2;
if (clientY < midY) {
insertIndex = i;
break;
}
}
// Move placeholder to the correct position
if (!drag.placeholder)
return;
if (insertIndex === 0 && visibleCards.length > 0) {
cardList.insertBefore(drag.placeholder, visibleCards[0]);
}
else if (insertIndex >= visibleCards.length) {
cardList.appendChild(drag.placeholder);
}
else {
cardList.insertBefore(drag.placeholder, visibleCards[insertIndex]);
}
// Remove empty column hint if present
const emptyHint = targetColEl.querySelector(".empty-column-hint");
if (emptyHint)
emptyHint.remove();
}
function createPlaceholder() {
const ph = document.createElement("div");
ph.className = "drop-placeholder";
ph.style.height = constants_1.DRAG.placeholderHeight + "px";
ph.style.backgroundColor = "rgba(99,102,241,0.3)";
ph.style.borderRadius = "4px";
ph.style.marginBottom = "8px";
ph.style.transition = "none";
return ph;
}
// ─── Auto-scroll during drag ────────────────────────────────
function handleAutoScroll(clientX, clientY) {
const margin = constants_1.DRAG.scrollMarginPx;
let scrollX = 0, scrollY = 0;
// Horizontal board scroll
if (clientX < margin)
scrollX = -constants_1.DRAG.scrollSpeed;
else if (clientX > window.innerWidth - margin)
scrollX = constants_1.DRAG.scrollSpeed;
// Vertical column scroll — find the column under cursor
const cols = document.querySelectorAll(".column");
for (const c of cols) {
const rect = c.getBoundingClientRect();
if (clientX >= rect.left && clientX <= rect.right) {
const cardList = c.querySelector(".card-list");
const listRect = cardList.getBoundingClientRect();
if (clientY < listRect.top + margin)
scrollY = -constants_1.DRAG.scrollSpeed;
else if (clientY > listRect.bottom - margin)
scrollY = constants_1.DRAG.scrollSpeed;
break;
}
}
if (scrollX || scrollY) {
window.scrollBy(scrollX, scrollY);
const board = document.querySelector(".board");
if (board && scrollX) {
board.scrollLeft += scrollX;
}
}
}
// ─── Finish / Cancel drag ──────────────────────────────────
function finishDrag() {
if (!drag)
return;
const saved = { ...drag }; // snapshot before cleanup
drag = null; // clear immediately to prevent re-entry
let targetColId = saved.sourceColId;
let targetIndex = saved.sourceIndex;
if (saved.placeholder) {
const parentList = saved.placeholder.parentNode;
if (parentList && parentList.classList.contains("card-list")) {
targetColId = parentList.dataset.colId || saved.sourceColId;
// Index among visible cards at placeholder position
const siblings = Array.from(parentList.querySelectorAll(".card:not(.drag-ghost)"));
targetIndex = siblings.indexOf(saved.placeholder);
if (targetIndex < 0)
targetIndex = siblings.length;
}
// Remove placeholder from DOM
saved.placeholder.remove();
}
// Remove ghost from DOM
saved.ghostEl.remove();
// Move card in state
S.moveCard(saved.cardId, targetColId, targetIndex);
(0, rendering_1.render)();
}
function cancelDrag() {
if (!drag)
return;
const saved = { ...drag };
drag = null;
// Remove ghost and placeholder
saved.ghostEl.remove();
if (saved.placeholder)
saved.placeholder.remove();
(0, rendering_1.render)();
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"39eb2864-a6c7-4cf0-a677-c8927eced7e2","tokensIn":2377655,"tokensOut":43523,"tokensTotal":2421178,"cost":0,"turns":65,"toolCalls":65,"failedToolCalls":2,"timestamp":"2026-07-31T21:58:11.519Z"} -->