← Kanban results

Kanban

Qwen 3.6 27B MTP GGUF · typescript

Initial view of Kanban

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 77,668 bytes · SHA-256 fb3cd5ca4199
<!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;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  background: #f4f5f7;
  color: #172b4d;
  -webkit-font-smoothing: antialiased;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

/* ─── Header ──────────────────────────────────────────────────── */

#header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 24px;
  background: #0079bf;
  color: #fff;
  min-height: 48px;
  z-index: 100;
  flex-shrink: 0;
}

#header-left h1 {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.3px;
}

#header-right {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

#search-wrapper {
  position: relative;
}

#search-input {
  padding: 6px 12px 6px 30px;
  border: none;
  border-radius: 6px;
  background: rgba(255,255,255,0.2);
  color: #fff;
  font-size: 13px;
  width: 180px;
  outline: none;
  transition: all 0.15s;
}

#search-input::placeholder { color: rgba(255,255,255,0.6); }
#search-input:focus { background: rgba(255,255,255,0.3); width: 220px; }

#search-wrapper::before {
  content: '';
  position: absolute;
  left: 9px;
  top: 50%;
  transform: translateY(-50%);
  width: 13px; height: 13px;
  border: 2px solid rgba(255,255,255,0.5);
  border-radius: 50%;
  pointer-events: none;
}
#search-wrapper::after {
  content: '';
  position: absolute;
  left: 16px;
  top: 55%;
  transform: rotate(45deg);
  width: 2px; height: 6px;
  background: rgba(255,255,255,0.5);
  border-radius: 1px;
  pointer-events: none;
}

#label-filter-bar {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}

.label-filter-chip {
  display: inline-flex; align-items: center;
  padding: 3px 8px; border-radius: 12px;
  border: 2px solid transparent;
  background: rgba(255,255,255,0.2);
  color: rgba(255,255,255,0.9);
  font-size: 11px; font-weight: 600;
  cursor: pointer; transition: all 0.15s;
}
.label-filter-chip:hover { background: rgba(255,255,255,0.3); }
.label-filter-chip.active {
  background: #fff; color: #0079bf; border-color: #fff;
}

/* ─── Board ───────────────────────────────────────────────────── */

#board {
  display: flex;
  flex: 1;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 16px 24px;
  gap: 16px;
  align-items: flex-start;
}

#column-container {
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

/* ─── Column ──────────────────────────────────────────────────── */

.kanban-column {
  width: 280px; min-width: 280px;
  max-height: calc(100vh - 88px);
  display: flex; flex-direction: column;
  background: #ebecf0;
  border-radius: 12px;
  overflow: hidden;
  border: 2px solid transparent;
  transition: border-color 0.2s, background 0.2s;
}

.kanban-column.wip-exceeded {
  border-color: #eb5a46;
  background: linear-gradient(180deg, #fff5f5 0%, #ebecf0 80px);
}

.column-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 12px;
  cursor: grab; user-select: none;
  min-height: 42px;
}
.column-header:active { cursor: grabbing; }

.column-title-group {
  display: flex; align-items: center; gap: 8px;
}

.column-title {
  font-size: 13px; font-weight: 700;
  color: #172b4d; line-height: 1.2;
}

.column-count {
  background: rgba(0,0,0,0.08);
  color: #5e6c84;
  font-size: 11px; font-weight: 700;
  padding: 2px 7px;
  border-radius: 10px; line-height: 1.5;
}

.wip-badge {
  margin-left: 2px;
}
.wip-exceeded-badge {
  color: #eb5a46; font-weight: 800;
}

.column-menu-btn {
  background: transparent; border: none;
  color: #8993a4; font-size: 16px;
  cursor: pointer; padding: 4px 6px;
  border-radius: 4px; line-height: 1;
  letter-spacing: 1px;
  transition: background 0.1s, color 0.1s;
}
.column-menu-btn:hover { background: rgba(0,0,0,0.08); color: #5e6c84; }

/* ─── Card List ───────────────────────────────────────────────── */

.card-list {
  flex: 1; overflow-y: auto;
  padding: 4px 8px;
  min-height: 8px;
}
.card-list::-webkit-scrollbar { width: 6px; }
.card-list::-webkit-scrollbar-track { background: transparent; }
.card-list::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 3px; }

/* ─── Card ────────────────────────────────────────────────────── */

.kanban-card {
  background: #fff;
  border-radius: 8px;
  padding: 8px 12px;
  margin-bottom: 8px;
  cursor: grab;
  box-shadow: 0 1px 2px rgba(0,0,0,0.07);
  transition: box-shadow 0.15s, transform 0.12s, border-color 0.1s;
  position: relative;
  user-select: none;
  border: 1px solid transparent;
}
.kanban-card:hover {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  transform: translateY(-1px);
  border-color: rgba(0,0,0,0.05);
}
.kanban-card:active { cursor: grabbing; }

.card-labels {
  display: flex; flex-wrap: wrap; gap: 4px;
  margin-bottom: 6px;
}
.card-label {
  display: inline-block;
  padding: 2px 8px; border-radius: 4px;
  font-size: 11px; font-weight: 600;
  line-height: 1.6;
}

.card-title {
  font-size: 14px; line-height: 1.45;
  color: #172b4d; word-break: break-word;
}

.card-meta {
  display: flex; align-items: center; gap: 8px;
  margin-top: 6px;
}

.card-due-date {
  font-size: 12px; color: #5e6c84;
  display: flex; align-items: center; gap: 3px;
}
.card-due-date::before {
  content: '';
  display: inline-block;
  width: 12px; height: 12px;
  border: 1.5px solid currentColor;
  border-radius: 50%;
}
.due-overdue { color: #eb5a46 !important; font-weight: 600; }
.due-today { color: #f2d600 !important; font-weight: 600; }

/* ─── Drag placeholder & ghost ────────────────────────────────── */

.drag-ghost {
  box-shadow: 0 8px 24px rgba(0,0,0,0.2) !important;
  transform: rotate(-2deg) scale(1.03) !important;
  opacity: 0.95 !important;
  transition: none !important;
}

.drag-placeholder {
  border: 2px dashed #0079bf !important;
  border-radius: 8px !important;
  background: rgba(0,121,191,0.06) !important;
  transition: height 0.1s ease !important;
  pointer-events: none !important;
}
.drag-placeholder:hover {
  box-shadow: none !important;
  transform: none !important;
}

/* ─── Empty Column ────────────────────────────────────────────── */

.empty-column-zone {
  display: flex; align-items: center; justify-content: center;
  padding: 24px 16px;
  border: 2px dashed #c4c9cc;
  border-radius: 8px;
  color: #8993a4;
  font-size: 13px; text-align: center;
  min-height: 60px;
  margin: 4px;
  transition: border-color 0.15s, background 0.15s;
}

/* ─── Add Card ────────────────────────────────────────────────── */

.add-card-btn {
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: none;
  color: #5e6c84;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  border-radius: 0 0 12px 12px;
  transition: background 0.1s, color 0.1s;
}
.add-card-btn:hover {
  background: rgba(0,0,0,0.05);
  color: #172b4d;
}

.add-card-form { padding: 4px 8px 8px; }

.add-card-textarea {
  width: 100%; min-height: 40px;
  border: 2px solid #0079bf;
  border-radius: 8px; padding: 8px 12px;
  font-size: 14px; resize: none;
  font-family: inherit; outline: none;
  background: #fff;
}

.add-card-confirm-btn {
  background: #0079bf; color: #fff;
  border: none; border-radius: 4px;
  padding: 6px 16px; font-size: 14px;
  cursor: pointer; font-weight: 500;
  transition: background 0.1s;
}
.add-card-confirm-btn:hover { background: #026aa7; }

.cancel-btn {
  background: none; border: none;
  font-size: 20px; color: #8993a4;
  cursor: pointer; width: 28px; height: 28px;
  border-radius: 4px; transition: background 0.1s;
}
.cancel-btn:hover { background: rgba(0,0,0,0.05); }

/* ─── Add Column ──────────────────────────────────────────────── */

.add-column {
  display: flex; align-items: center;
  padding: 0 4px;
  flex-shrink: 0;
}

.add-column-btn {
  padding: 10px 16px;
  background: rgba(255,255,255,0.6);
  border: none; border-radius: 12px;
  color: #5e6c84;
  font-size: 14px; font-weight: 600;
  cursor: pointer; white-space: nowrap;
  transition: all 0.15s;
}
.add-column-btn:hover {
  background: rgba(255,255,255,0.9);
  color: #172b4d;
}

.add-column-input {
  padding: 8px 12px;
  border: 2px solid #0079bf;
  border-radius: 12px;
  font-size: 14px; font-weight: 600;
  color: #172b4d; background: #fff;
  outline: none; width: 200px;
  font-family: inherit;
  box-shadow: 0 0 0 4px rgba(0,121,191,0.1);
}

.add-column-cancel {
  margin-left: 8px;
  background: none; border: none;
  font-size: 22px; color: #8993a4;
  cursor: pointer; padding: 4px 8px;
}

/* ─── Context Menu ────────────────────────────────────────────── */

.context-menu {
  position: fixed; z-index: 10000;
  background: #fff; border-radius: 8px;
  box-shadow: 0 4px 24px rgba(0,0,0,0.18);
  padding: 6px; min-width: 180px;
}

.context-menu-item {
  display: block; width: 100%; text-align: left;
  padding: 7px 12px;
  background: transparent; border: none;
  border-radius: 4px;
  font-size: 13px; color: #172b4d;
  cursor: pointer; transition: background 0.1s;
  font-family: inherit;
}
.context-menu-item:hover { background: #ebecf0; }
.context-menu-item.active { background: #e6f3ff; color: #0079bf; }
.context-menu-item.danger { color: #eb5a46; }
.context-menu-item.danger:hover { background: #ffebe7; }

.context-menu-label {
  padding: 6px 12px 2px;
  font-size: 10px; font-weight: 700;
  text-transform: uppercase;
  color: #8993a4; letter-spacing: 0.5px;
}

.context-menu-section {
  border-top: 1px solid #ddd;
  padding-top: 2px; margin-top: 4px;
}

/* ─── Column Edit Input ───────────────────────────────────────── */

.column-edit-input {
  background: #fff; border: 2px solid #0079bf;
  border-radius: 4px; padding: 2px 6px;
  font-size: 13px; font-weight: 700;
  width: 100%; color: #172b4d; outline: none;
  font-family: inherit;
}

/* ─── Toasts ──────────────────────────────────────────────────── */

#toast-container {
  position: fixed; bottom: 20px; left: 50%;
  transform: translateX(-50%);
  z-index: 10001;
  display: flex; flex-direction: column;
  align-items: center; gap: 8px;
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  background: #172b4d; color: #fff;
  padding: 10px 16px; border-radius: 8px;
  display: flex; align-items: center;
  justify-content: space-between;
  gap: 16px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  font-size: 13px; font-weight: 500;
  animation: toast-in 0.25s ease;
  max-width: 380px;
}

.toast button {
  background: transparent;
  border: none; cursor: pointer;
  font-family: inherit;
}

.toast .toast-undo {
  color: #4da3ff; font-weight: 700;
  padding: 4px 10px; border-radius: 4px;
  transition: background 0.1s;
}
.toast .toast-undo:hover { background: rgba(255,255,255,0.1); }

.toast .toast-dismiss {
  color: rgba(255,255,255,0.5);
  font-size: 16px; padding: 2px 6px;
  transition: color 0.1s;
}
.toast .toast-dismiss:hover { color: #fff; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ─── Scrollbars ──────────────────────────────────────────────── */

#board::-webkit-scrollbar { height: 8px; }
#board::-webkit-scrollbar-track { background: rgba(0,0,0,0.03); }
#board::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 4px; }

/* ─── Settle Animation ────────────────────────────────────────── */

@keyframes card-settle {
  0% { transform: scale(1.04); }
  100% { transform: scale(1); }
}

.kanban-card.just-dropped {
  animation: card-settle 0.15s ease-out;
}
</style>
</head>
<body>
<div id="app">
  <div id="header">
    <div id="header-left">
      <h1 id="board-title">Kanban Board</h1>
    </div>
    <div id="header-right">
      <div id="search-wrapper">
        <input type="text" id="search-input" placeholder="Search cards...">
      </div>
      <div id="label-filter-bar"></div>
    </div>
  </div>
  <div id="board">
    <div id="column-container"></div>
    <div class="add-column">
      <button class="add-column-btn">+ Add another column</button>
    </div>
  </div>
  <div id="toast-container"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./render":"src/render.ts","./dragdrop":"src/dragdrop.ts","./toasts":"src/toasts.ts","./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts"},"src/render.ts":{"./state":"src/state.ts","./dragdrop":"src/dragdrop.ts","./types":"src/types.ts"},"src/dragdrop.ts":{"./state":"src/state.ts","./types":"src/types.ts"},"src/toasts.ts":{"./state":"src/state.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";
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const render_1 = require("./render");
const toasts_1 = require("./toasts");
// ─── Boot ────────────────────────────────────────────────────────
function start() {
    // Init state from storage
    (0, state_1.initState)(true);
    // Load filter state
    loadFilterState();
    // Get root element
    const root = document.getElementById('app');
    // Init renders
    (0, render_1.initRender)(root);
    (0, toasts_1.initToasts)(root);
    // Wire search
    const searchInput = document.getElementById('search-input');
    searchInput.addEventListener('input', () => {
        const fs = (0, state_1.getFilterState)();
        (0, state_1.setFilterState)({ ...fs, search: searchInput.value });
    });
    // Wire label filters
    const labelBar = document.getElementById('label-filter-bar');
    labelBar.addEventListener('click', (e) => {
        const chip = e.target?.closest('.label-filter-chip');
        if (!chip)
            return;
        const labelName = chip.dataset.labelName;
        if (!labelName)
            return;
        const fs = (0, state_1.getFilterState)();
        const idx = fs.labelFilters.indexOf(labelName);
        if (idx >= 0) {
            (0, state_1.setFilterState)({ ...fs, labelFilters: fs.labelFilters.filter(n => n !== labelName) });
        }
        else {
            (0, state_1.setFilterState)({ ...fs, labelFilters: [...fs.labelFilters, labelName] });
        }
        updateLabelFilterUI();
    });
    updateLabelFilterUI();
    // Wire add column button
    wireAddColumn(root);
    // Initial render
    (0, render_1.renderBoard)();
    (0, toasts_1.renderToasts)();
    // Subscribe to state changes
    (0, state_1.subscribe)(() => {
        (0, render_1.renderBoard)();
        (0, toasts_1.renderToasts)();
        updateLabelFilterUI();
    });
}
// ─── Label Filter UI ──────────────────────────────────────────────
function updateLabelFilterUI() {
    const labelBar = document.getElementById('label-filter-bar');
    const chips = labelBar.querySelectorAll('.label-filter-chip');
    const fs = (0, state_1.getFilterState)();
    chips.forEach(chip => {
        const name = chip.dataset.labelName;
        if (name && fs.labelFilters.includes(name)) {
            chip.classList.add('active');
        }
        else {
            chip.classList.remove('active');
        }
    });
}
// ─── Add Column ───────────────────────────────────────────────────
function wireAddColumn(root) {
    const addColEl = root.querySelector('.add-column');
    if (!addColEl)
        return;
    addColEl.innerHTML = '';
    const btn = document.createElement('button');
    btn.className = 'add-column-btn';
    btn.textContent = '+ Add another column';
    btn.addEventListener('click', showAddColumnForm);
    addColEl.appendChild(btn);
}
function showAddColumnForm() {
    const addColEl = document.querySelector('.add-column');
    if (!addColEl)
        return;
    addColEl.innerHTML = '';
    addColEl.classList.add('active');
    const input = document.createElement('input');
    input.type = 'text';
    input.placeholder = 'Enter column title...';
    input.className = 'add-column-input';
    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = '\u00d7';
    cancelBtn.className = 'add-column-cancel';
    addColEl.appendChild(input);
    addColEl.appendChild(cancelBtn);
    input.focus();
    const commit = () => {
        const title = input.value.trim();
        if (title) {
            (0, state_1.addColumn)(title);
        }
        wireAddColumn(document.getElementById('app'));
    };
    input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            commit();
        }
        if (e.key === 'Escape') {
            e.preventDefault();
            wireAddColumn(document.getElementById('app'));
        }
    });
    cancelBtn.addEventListener('click', () => wireAddColumn(document.getElementById('app')));
}
// ─── Helpers ──────────────────────────────────────────────────────
function loadFilterState() {
    try {
        const saved = localStorage.getItem('kanban-board-state-filters');
        if (saved) {
            const fs = JSON.parse(saved);
            (0, state_1.setFilterState)(fs);
            const searchInput = document.getElementById('search-input');
            if (searchInput)
                searchInput.value = fs.search || '';
        }
    }
    catch { /* ignore */ }
}
// ─── Entry ────────────────────────────────────────────────────────
start();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Tunable Constants ─────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.ANIM_SETTLE_MS = exports.AUTO_SCROLL_SPEED = exports.AUTO_SCROLL_MARGIN = exports.DRAG_START_THRESHOLD = exports.TOAST_UNDO_DURATION = exports.TOAST_DURATION = exports.STORAGE_KEY = exports.DEFAULT_COLUMNS = exports.LABEL_PALETTE = exports.COLORS = void 0;
exports.COLORS = {
    bg: '#f4f5f7',
    boardBg: '#f4f5f7',
    columnBg: '#ebecf0',
    columnHeaderBg: '#ebecf0',
    cardBg: '#ffffff',
    cardHoverBg: '#fafbfc',
    cardShadow: '0 1px 2px rgba(0,0,0,0.1)',
    cardShadowHover: '0 2px 8px rgba(0,0,0,0.12)',
    cardShadowDrag: '0 8px 24px rgba(0,0,0,0.2)',
    textPrimary: '#172b4d',
    textSecondary: '#5e6c84',
    textTertiary: '#8993a4',
    border: '#ddd',
    accent: '#0079bf',
    accentLight: '#e6f3ff',
    danger: '#eb5a46',
    dangerLight: '#ffebe7',
    placeholderBg: 'rgba(0,121,191,0.1)',
    wipExceeded: 'rgba(235,90,70,0.15)',
    wipExceededBorder: '#eb5a46',
    emptyColumn: '#e4e5e7',
    dropIndicator: '#0079bf',
    overlay: 'rgba(0,0,0,0.4)',
    toastBg: '#172b4d',
    toastText: '#ffffff',
};
exports.LABEL_PALETTE = [
    { name: 'Bug', color: '#eb5a46' },
    { name: 'Feature', color: '#61bd4f' },
    { name: 'Improvement', color: '#f2d600' },
    { name: 'Design', color: '#c377e0' },
    { name: 'Docs', color: '#00c2e0' },
    { name: 'Urgent', color: '#ff9f1a' },
];
exports.DEFAULT_COLUMNS = [
    { title: 'Backlog' },
    { title: 'To Do' },
    { title: 'In Progress' },
    { title: 'Review' },
    { title: 'Done' },
];
exports.STORAGE_KEY = 'kanban-board-state';
exports.TOAST_DURATION = 3000;
exports.TOAST_UNDO_DURATION = 8000;
exports.DRAG_START_THRESHOLD = 5; // px before drag begins
exports.AUTO_SCROLL_MARGIN = 60; // px from edge to start scrolling
exports.AUTO_SCROLL_SPEED = 12; // px per frame
exports.ANIM_SETTLE_MS = 150; // card settle animation duration
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initState = initState;
exports.saveModel = saveModel;
exports.saveFilterState = saveFilterState;
exports.loadFilterState = loadFilterState;
exports.getModel = getModel;
exports.getFilterState = getFilterState;
exports.setFilterState = setFilterState;
exports.setModel = setModel;
exports.getColumn = getColumn;
exports.getCard = getCard;
exports.getCardInColumn = getCardInColumn;
exports.addCard = addCard;
exports.deleteCard = deleteCard;
exports.updateCard = updateCard;
exports.addColumn = addColumn;
exports.renameColumn = renameColumn;
exports.deleteColumn = deleteColumn;
exports.updateColumnWip = updateColumnWip;
exports.moveCard = moveCard;
exports.reorderColumn = reorderColumn;
exports.getToasts = getToasts;
exports.dismissToast = dismissToast;
exports.showToast = showToast;
exports.getUndoStack = getUndoStack;
exports.subscribe = subscribe;
exports.uid = uid;
exports.cloneModel = cloneModel;
const types_1 = require("./types");
// ─── State ─────────────────────────────────────────────────────────
let model;
let filterState = { search: '', labelFilters: [] };
let undoStack = [];
let toasts = [];
const listeners = new Set();
// ─── Init ──────────────────────────────────────────────────────────
function initState(fromStorage) {
    filterState = { search: '', labelFilters: [] }; // always reset
    if (fromStorage) {
        const saved = localStorage.getItem(types_1.STORAGE_KEY);
        if (saved) {
            try {
                model = JSON.parse(saved);
                if (isValidModel(model))
                    return model;
            }
            catch { /* fall through */ }
        }
    }
    model = createEmptyBoard();
    return model;
}
function isValidModel(m) {
    return m && Array.isArray(m.columnIds) &&
        m.columns && typeof m.columns === 'object' &&
        m.cards && typeof m.cards === 'object';
}
function createEmptyBoard() {
    const columns = {};
    const columnIds = [];
    const cards = {};
    // Create columns
    const colIds = [];
    for (const def of types_1.DEFAULT_COLUMNS) {
        const id = uid();
        colIds.push(id);
        columns[id] = { id, title: def.title, cardIds: [], wipLimit: null };
        columnIds.push(id);
    }
    // Seed sample cards
    const sampleCards = [
        { title: 'Design login page', description: 'Create mockups for the new login flow', labels: [{ name: 'Design', color: '#c377e0' }], dueDate: '' },
        { title: 'Fix auth bug', description: 'Users can\'t reset password', labels: [{ name: 'Bug', color: '#eb5a46' }, { name: 'Urgent', color: '#ff9f1a' }], dueDate: new Date(Date.now() + 86400000).toISOString() },
        { title: 'Write API docs', description: '', labels: [{ name: 'Docs', color: '#00c2e0' }], dueDate: '' },
        { title: 'Implement dark mode', description: 'Add theme toggle in settings', labels: [{ name: 'Feature', color: '#61bd4f' }], dueDate: '' },
        { title: 'Performance audit', description: 'Lighthouse score below 80', labels: [{ name: 'Improvement', color: '#f2d600' }], dueDate: '' },
        { title: 'Add user avatars', description: 'Profile pictures upload & display', labels: [{ name: 'Feature', color: '#61bd4f' }], dueDate: '' },
        { title: 'Unit tests for utils', description: 'Cover helper functions', labels: [], dueDate: '' },
        { title: 'Dashboard analytics', description: 'Add charts for user activity', labels: [{ name: 'Feature', color: '#61bd4f' }, { name: 'Design', color: '#c377e0' }], dueDate: '' },
    ];
    const cardOrder = [
        // Backlog (col 0) — 2 cards
        [sampleCards[3], sampleCards[4]],
        // To Do (col 1) — 3 cards
        [sampleCards[0], sampleCards[2], sampleCards[5]],
        // In Progress (col 2) — 1 card
        [sampleCards[1]],
        // Review (col 3) — 0 cards
        [],
        // Done (col 4) — 2 cards
        [sampleCards[6], sampleCards[7]],
    ];
    for (let ci = 0; ci < colIds.length; ci++) {
        const col = columns[colIds[ci]];
        for (const sc of cardOrder[ci]) {
            const id = uid();
            const card = { ...sc, id, createdAt: Date.now() };
            cards[id] = card;
            col.cardIds.push(id);
        }
    }
    return { columnIds, columns, cards };
}
// ─── Persistence ───────────────────────────────────────────────────
function saveModel() {
    localStorage.setItem(types_1.STORAGE_KEY, JSON.stringify(model));
}
function saveFilterState() {
    const key = types_1.STORAGE_KEY + '-filters';
    localStorage.setItem(key, JSON.stringify(filterState));
}
function loadFilterState() {
    const key = types_1.STORAGE_KEY + '-filters';
    const saved = localStorage.getItem(key);
    if (saved) {
        try {
            filterState = JSON.parse(saved);
        }
        catch {
            filterState = { search: '', labelFilters: [] };
        }
    }
}
// ─── Accessors ─────────────────────────────────────────────────────
function getModel() {
    return model;
}
function getFilterState() {
    return filterState || { search: '', labelFilters: [] };
}
function setFilterState(next) {
    filterState = { search: next.search || '', labelFilters: next.labelFilters || [] };
    saveFilterState();
    notify();
}
function setModel(next) {
    model = next;
    saveModel();
    notify();
}
function getColumn(columnId) {
    return model.columns[columnId];
}
function getCard(cardId) {
    return model.cards[cardId];
}
function getCardInColumn(cardId, columnId) {
    const col = model.columns[columnId];
    return col ? col.cardIds.indexOf(cardId) : -1;
}
// ─── Card CRUD ─────────────────────────────────────────────────────
function addCard(columnId, card) {
    const id = uid();
    const full = { ...card, id, createdAt: Date.now() };
    model.cards[id] = full;
    model.columns[columnId].cardIds.push(id);
    saveModel();
    notify();
    return id;
}
function deleteCard(cardId) {
    const card = model.cards[cardId];
    if (!card)
        return;
    let colId = '';
    let idx = -1;
    for (const cid of model.columnIds) {
        idx = model.columns[cid].cardIds.indexOf(cardId);
        if (idx >= 0) {
            colId = cid;
            break;
        }
    }
    const snapshot = cloneModel(model);
    const colTitle = model.columns[colId]?.title ?? '';
    const cardTitle = card.title;
    model.cards = { ...model.cards };
    delete model.cards[cardId];
    model.columns[colId].cardIds = model.columns[colId].cardIds.filter(id => id !== cardId);
    pushUndo({
        label: `Deleted "${cardTitle}"`,
        undo: () => {
            model = snapshot;
            saveModel();
            notify();
        },
        redo: () => {
            model.cards = { ...model.cards };
            delete model.cards[cardId];
            for (const cid of model.columnIds) {
                model.columns[cid].cardIds = model.columns[cid].cardIds.filter(id => id !== cardId);
            }
            saveModel();
            notify();
        },
    });
}
function updateCard(cardId, updates) {
    const card = model.cards[cardId];
    if (!card)
        return;
    model.cards[cardId] = { ...card, ...updates };
    saveModel();
    notify();
}
// ─── Column CRUD ───────────────────────────────────────────────────
function addColumn(title) {
    const id = uid();
    const col = { id, title, cardIds: [], wipLimit: null };
    model.columns[id] = col;
    model.columnIds = [...model.columnIds, id];
    saveModel();
    notify();
    return id;
}
function renameColumn(columnId, title) {
    const col = model.columns[columnId];
    if (!col)
        return;
    col.title = title;
    saveModel();
    notify();
}
function deleteColumn(columnId) {
    if (model.columnIds.length <= 1)
        return;
    const col = model.columns[columnId];
    if (!col)
        return;
    const cardIds = [...col.cardIds];
    const colTitle = col.title;
    const snapshot = cloneModel(model);
    model.columnIds = model.columnIds.filter(id => id !== columnId);
    const newCols = { ...model.columns };
    delete newCols[columnId];
    model.columns = newCols;
    // Remove cards
    const newCards = { ...model.cards };
    for (const cid of cardIds)
        delete newCards[cid];
    model.cards = newCards;
    pushUndo({
        label: `Deleted column "${colTitle}"`,
        undo: () => {
            model = snapshot;
            saveModel();
            notify();
        },
        redo: () => {
            model.columnIds = model.columnIds.filter(id => id !== columnId);
            const nc = { ...model.columns };
            delete nc[columnId];
            model.columns = nc;
            const ncs = { ...model.cards };
            for (const cid of cardIds)
                delete ncs[cid];
            model.cards = ncs;
            saveModel();
            notify();
        },
    });
}
function updateColumnWip(columnId, wipLimit) {
    const col = model.columns[columnId];
    if (!col)
        return;
    col.wipLimit = wipLimit;
    saveModel();
    notify();
}
// ─── Card Move (for drag & drop) ──────────────────────────────────
function moveCard(cardId, sourceColId, sourceIdx, destColId, destIdx) {
    const srcCol = model.columns[sourceColId];
    const dstCol = model.columns[destColId];
    if (!srcCol || !dstCol)
        return;
    // Remove from source
    srcCol.cardIds = srcCol.cardIds.filter(id => id !== cardId);
    // Insert at destination (clamp to valid range)
    const clampedIdx = Math.min(destIdx, dstCol.cardIds.length);
    dstCol.cardIds.splice(clampedIdx, 0, cardId);
    saveModel();
    notify();
}
// ─── Column Reorder ───────────────────────────────────────────────
function reorderColumn(columnId, newIdx) {
    const oldIdx = model.columnIds.indexOf(columnId);
    if (oldIdx < 0 || oldIdx === newIdx)
        return;
    const ids = [...model.columnIds];
    const [col] = ids.splice(oldIdx, 1);
    const clamped = Math.min(newIdx, ids.length);
    ids.splice(clamped, 0, col);
    model.columnIds = ids;
    saveModel();
    notify();
}
// ─── Undo / Toasts ────────────────────────────────────────────────
function pushUndo(action) {
    const a = { id: uid(), label: action.label, undo: action.undo, redo: action.redo, timestamp: Date.now() };
    undoStack.push(a);
    showToast(`${action.label}`, true, action.undo);
}
function getToasts() {
    return toasts;
}
function dismissToast(id) {
    toasts = toasts.filter(t => t.id !== id);
    notify();
}
function showToast(message, undoable = false, undoFn) {
    const t = { id: uid(), message, undoable, undoFn };
    toasts.push(t);
    notify();
    setTimeout(() => {
        dismissToast(t.id);
    }, undoable ? types_1.TOAST_UNDO_DURATION : types_1.TOAST_UNDO_DURATION);
}
function getUndoStack() {
    return undoStack;
}
// ─── Notify ───────────────────────────────────────────────────────
function subscribe(fn) {
    listeners.add(fn);
    return () => listeners.delete(fn);
}
function notify() {
    for (const fn of listeners)
        fn();
}
// ─── Helpers ───────────────────────────────────────────────────────
let _counter = 0;
function uid() {
    return `id_${Date.now()}_${++_counter}_${Math.random().toString(36).slice(2, 7)}`;
}
function cloneModel(m) {
    return JSON.parse(JSON.stringify(m));
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRender = initRender;
exports.renderBoard = renderBoard;
const state_1 = require("./state");
const dragdrop_1 = require("./dragdrop");
const types_1 = require("./types");
// ─── DOM References ────────────────────────────────────────────────
let boardEl;
let columnContainerEl;
let searchInput;
let labelFilterBar;
// ─── Render ────────────────────────────────────────────────────────
function initRender(root) {
    boardEl = root.querySelector('#board');
    columnContainerEl = root.querySelector('#column-container');
    searchInput = root.querySelector('#search-input');
    labelFilterBar = root.querySelector('#label-filter-bar');
    // Build label filter bar
    renderLabelFilters();
}
function renderBoard() {
    const model = (0, state_1.getModel)();
    const filterState = (0, state_1.getFilterState)();
    columnContainerEl.innerHTML = '';
    for (const colId of model.columnIds) {
        const col = model.columns[colId];
        const colEl = buildColumnElement(col, model.cards, filterState);
        columnContainerEl.appendChild(colEl);
    }
    updateColumnContainerStyle();
    wireAllDrag();
}
function wireAllDrag() {
    // Wire card drag
    const cards = columnContainerEl.querySelectorAll('.kanban-card:not(.drag-ghost):not(.drag-placeholder)');
    cards.forEach(card => {
        const cardId = card.dataset.cardId;
        const colId = card.dataset.columnId;
        if (cardId && colId) {
            (0, dragdrop_1.initCardDrag)(card, cardId, colId);
        }
    });
    // Wire column drag
    const cols = columnContainerEl.querySelectorAll('.kanban-column');
    cols.forEach(col => {
        const colId = col.dataset.columnId;
        if (colId) {
            (0, dragdrop_1.initColumnDrag)(col, colId);
        }
    });
}
function updateColumnContainerStyle() {
    const model = (0, state_1.getModel)();
    const count = model.columnIds.length;
    columnContainerEl.style.minWidth = `${count * 280}px`;
}
// ─── Column Element ────────────────────────────────────────────────
function buildColumnElement(col, cards, filterState) {
    const colEl = document.createElement('div');
    colEl.className = 'kanban-column';
    colEl.dataset.columnId = col.id;
    // Check WIP
    const wipExceeded = col.wipLimit !== null && col.cardIds.length > col.wipLimit;
    if (wipExceeded)
        colEl.classList.add('wip-exceeded');
    // Header
    const header = document.createElement('div');
    header.className = 'column-header';
    const titleGroup = document.createElement('div');
    titleGroup.className = 'column-title-group';
    const title = document.createElement('span');
    title.className = 'column-title';
    title.textContent = col.title;
    const count = document.createElement('span');
    count.className = 'column-count';
    count.textContent = String(col.cardIds.length);
    // WIP indicator
    if (col.wipLimit !== null) {
        const wipBadge = document.createElement('span');
        wipBadge.className = 'wip-badge';
        wipBadge.textContent = `/${col.wipLimit}`;
        if (wipExceeded)
            wipBadge.classList.add('wip-exceeded-badge');
        count.appendChild(wipBadge);
    }
    titleGroup.appendChild(title);
    titleGroup.appendChild(count);
    // Menu button
    const menuBtn = document.createElement('button');
    menuBtn.className = 'column-menu-btn';
    menuBtn.innerHTML = '&bull;&bull;&bull;';
    menuBtn.title = 'Column options';
    header.appendChild(titleGroup);
    header.appendChild(menuBtn);
    colEl.appendChild(header);
    // Card list
    const cardList = document.createElement('div');
    cardList.className = 'card-list';
    cardList.dataset.columnId = col.id;
    const filteredCardIds = filterCards(col.cardIds, cards, filterState);
    if (col.cardIds.length === 0) {
        // Empty column drop zone
        const emptyZone = document.createElement('div');
        emptyZone.className = 'empty-column-zone';
        emptyZone.textContent = 'Drop cards here';
        cardList.appendChild(emptyZone);
    }
    for (const cardId of filteredCardIds) {
        const card = cards[cardId];
        if (!card)
            continue;
        const cardEl = buildCardElement(card, col.id);
        cardList.appendChild(cardEl);
    }
    colEl.appendChild(cardList);
    // Add card button
    const addBtn = document.createElement('button');
    addBtn.className = 'add-card-btn';
    addBtn.textContent = '+ Add a card';
    addBtn.dataset.columnId = col.id;
    colEl.appendChild(addBtn);
    // ─── Column Menu ───
    setupColumnMenu(menuBtn, col, colEl);
    // ─── Add Card ───
    addBtn.addEventListener('click', () => showAddCardForm(col.id, colEl, addBtn));
    return colEl;
}
// ─── Card Element ──────────────────────────────────────────────────
function buildCardElement(card, columnId) {
    const cardEl = document.createElement('div');
    cardEl.className = 'kanban-card';
    cardEl.dataset.cardId = card.id;
    cardEl.dataset.columnId = columnId;
    // Labels
    if (card.labels.length > 0) {
        const labelRow = document.createElement('div');
        labelRow.className = 'card-labels';
        for (const label of card.labels) {
            const chip = document.createElement('span');
            chip.className = 'card-label';
            chip.style.backgroundColor = label.color;
            chip.style.color = getContrastColor(label.color);
            chip.textContent = label.name;
            labelRow.appendChild(chip);
        }
        cardEl.appendChild(labelRow);
    }
    // Title
    const titleEl = document.createElement('div');
    titleEl.className = 'card-title';
    titleEl.textContent = card.title;
    titleEl.title = card.description || '';
    cardEl.appendChild(titleEl);
    // Meta row
    const metaRow = document.createElement('div');
    metaRow.className = 'card-meta';
    // Due date
    if (card.dueDate) {
        const dueEl = document.createElement('span');
        dueEl.className = 'card-due-date';
        const d = new Date(card.dueDate);
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        const dueDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
        const daysDiff = Math.round((dueDate.getTime() - today.getTime()) / 86400000);
        dueEl.textContent = `${d.getMonth() + 1}/${d.getDate()}`;
        if (daysDiff < 0)
            dueEl.classList.add('due-overdue');
        else if (daysDiff <= 1)
            dueEl.classList.add('due-today');
        metaRow.appendChild(dueEl);
    }
    if (metaRow.children.length > 0) {
        cardEl.appendChild(metaRow);
    }
    // Context menu on right-click
    cardEl.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        e.stopPropagation();
        showCardContextMenu(e, card);
    });
    // Double-click to inline-edit title
    cardEl.addEventListener('dblclick', (e) => {
        if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
            return;
        startInlineTitleEdit(cardEl, card);
    });
    return cardEl;
}
// ─── Add Card Form ─────────────────────────────────────────────────
function showAddCardForm(columnId, colEl, addBtn) {
    const cardList = colEl.querySelector('.card-list');
    const formEl = document.createElement('div');
    formEl.className = 'add-card-form';
    const textarea = document.createElement('textarea');
    textarea.placeholder = 'Enter a title...';
    textarea.style.cssText = `
    width: 100%; min-height: 40px; border: 2px solid ${types_1.COLORS.border};
    border-radius: 8px; padding: 8px 12px; font-size: 14px; resize: none;
    font-family: inherit; outline: none; background: #fff;
    transition: border-color 0.15s;
  `;
    const actionRow = document.createElement('div');
    actionRow.style.cssText = 'display: flex; gap: 8px; margin-top: 8px; align-items: center;';
    const addBtnInner = document.createElement('button');
    addBtnInner.className = 'add-card-confirm-btn';
    addBtnInner.textContent = 'Add card';
    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = '\u00d7';
    cancelBtn.className = 'cancel-btn';
    actionRow.appendChild(addBtnInner);
    actionRow.appendChild(cancelBtn);
    formEl.appendChild(textarea);
    formEl.appendChild(actionRow);
    addBtn.style.display = 'none';
    cardList.appendChild(formEl);
    textarea.focus();
    const removeForm = () => {
        formEl.remove();
        addBtn.style.display = '';
    };
    const add = () => {
        const title = textarea.value.trim();
        if (title) {
            (0, state_1.addCard)(columnId, { title, description: '', labels: [], dueDate: '' });
        }
        removeForm();
    };
    addBtnInner.addEventListener('click', add);
    cancelBtn.addEventListener('click', removeForm);
    textarea.addEventListener('focus', () => { textarea.style.borderColor = types_1.COLORS.accent; });
    textarea.addEventListener('blur', () => { textarea.style.borderColor = types_1.COLORS.border; });
    textarea.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            add();
        }
        if (e.key === 'Escape') {
            e.preventDefault();
            removeForm();
        }
    });
}
// ─── Column Menu ───────────────────────────────────────────────────
function setupColumnMenu(menuBtn, col, colEl) {
    menuBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        removeContextMenu();
        showColumnContextMenu(e, menuBtn, col, colEl);
    });
    // Column title double-click to rename
    const titleEl = colEl.querySelector('.column-title');
    if (titleEl) {
        titleEl.addEventListener('dblclick', (e) => {
            e.preventDefault();
            e.stopPropagation();
            startEditColumnTitle(colEl, col);
        });
    }
}
function showColumnContextMenu(e, menuBtn, col, colEl) {
    removeContextMenu();
    const menu = document.createElement('div');
    menu.className = 'context-menu';
    const rect = menuBtn.getBoundingClientRect();
    menu.style.left = `${rect.left - 160}px`;
    menu.style.top = `${rect.bottom + 4}px`;
    // Rename
    const renameBtn = document.createElement('button');
    renameBtn.className = 'context-menu-item';
    renameBtn.textContent = 'Rename column';
    renameBtn.addEventListener('click', () => {
        removeContextMenu();
        startEditColumnTitle(colEl, col);
    });
    menu.appendChild(renameBtn);
    // WIP Limit section
    const wipSection = document.createElement('div');
    wipSection.className = 'context-menu-section';
    const wipLabel = document.createElement('div');
    wipLabel.className = 'context-menu-label';
    wipLabel.textContent = 'WIP LIMIT';
    wipSection.appendChild(wipLabel);
    for (let n = 0; n <= 10; n++) {
        const optBtn = document.createElement('button');
        optBtn.className = 'context-menu-item';
        if (n === 0) {
            optBtn.textContent = 'No limit';
            if (col.wipLimit === null)
                optBtn.classList.add('active');
        }
        else {
            optBtn.textContent = String(n);
            if (col.wipLimit === n)
                optBtn.classList.add('active');
        }
        optBtn.addEventListener('click', () => {
            removeContextMenu();
            (0, state_1.updateColumnWip)(col.id, n === 0 ? null : n);
        });
        wipSection.appendChild(optBtn);
    }
    menu.appendChild(wipSection);
    // Delete
    const deleteBtn = document.createElement('button');
    deleteBtn.className = 'context-menu-item danger';
    deleteBtn.textContent = 'Delete column';
    deleteBtn.addEventListener('click', () => {
        removeContextMenu();
        (0, state_1.deleteColumn)(col.id);
    });
    menu.appendChild(deleteBtn);
    document.body.appendChild(menu);
    setTimeout(() => {
        const handler = () => { removeContextMenu(); document.removeEventListener('click', handler); };
        document.addEventListener('click', handler);
    }, 0);
}
function startEditColumnTitle(colEl, col) {
    const titleEl = colEl.querySelector('.column-title');
    if (!titleEl || titleEl.querySelector('input'))
        return;
    const input = document.createElement('input');
    input.type = 'text';
    input.value = col.title;
    input.className = 'column-edit-input';
    titleEl.replaceWith(input);
    input.focus();
    input.select();
    const commit = () => {
        const newTitle = input.value.trim();
        if (newTitle)
            (0, state_1.renameColumn)(col.id, newTitle);
    };
    input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            commit();
        }
        if (e.key === 'Escape') {
            e.preventDefault();
        }
    });
    input.addEventListener('blur', commit);
}
// ─── Card Context Menu ────────────────────────────────────────────
function showCardContextMenu(e, card) {
    removeContextMenu();
    const menu = document.createElement('div');
    menu.className = 'context-menu';
    menu.style.left = `${e.clientX}px`;
    menu.style.top = `${e.clientY}px`;
    const editBtn = document.createElement('button');
    editBtn.className = 'context-menu-item';
    editBtn.textContent = 'Edit card';
    editBtn.addEventListener('click', () => { removeContextMenu(); openCardEditModal(card); });
    menu.appendChild(editBtn);
    const deleteBtn = document.createElement('button');
    deleteBtn.className = 'context-menu-item danger';
    deleteBtn.textContent = 'Delete card';
    deleteBtn.addEventListener('click', () => { removeContextMenu(); (0, state_1.deleteCard)(card.id); });
    menu.appendChild(deleteBtn);
    document.body.appendChild(menu);
    setTimeout(() => {
        const handler = () => { removeContextMenu(); document.removeEventListener('click', handler); };
        document.addEventListener('click', handler);
    }, 0);
}
// ─── Card Edit Modal ──────────────────────────────────────────────
function openCardEditModal(card) {
    // Remove existing
    closeCardEditModal();
    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    overlay.style.cssText = `
    position: fixed; inset: 0; z-index: 20000;
    background: rgba(0,0,0,0.5);
    display: flex; align-items: center; justify-content: center;
  `;
    const modal = document.createElement('div');
    modal.className = 'card-edit-modal';
    modal.style.cssText = `
    background: #fff; border-radius: 12px;
    box-shadow: 0 8px 40px rgba(0,0,0,0.25);
    width: 480px; max-height: 80vh; overflow-y: auto;
    padding: 0;
  `;
    // Header
    const header = document.createElement('div');
    header.style.cssText = 'padding: 20px 24px 0;';
    const titleInput = document.createElement('input');
    titleInput.type = 'text';
    titleInput.value = card.title;
    titleInput.placeholder = 'Card title';
    titleInput.style.cssText = `
    width: 100%; font-size: 20px; font-weight: 700;
    border: none; outline: none; color: #172b4d;
    padding: 4px 0; font-family: inherit;
  `;
    header.appendChild(titleInput);
    modal.appendChild(header);
    // Body
    const body = document.createElement('div');
    body.style.cssText = 'padding: 16px 24px 24px;';
    // Description
    const descLabel = document.createElement('label');
    descLabel.textContent = 'Description';
    descLabel.style.cssText = 'display: block; font-size: 12px; font-weight: 600; color: #5e6c84; margin-bottom: 6px; margin-top: 16px;';
    body.appendChild(descLabel);
    const descArea = document.createElement('textarea');
    descArea.value = card.description;
    descArea.placeholder = 'Type a description...';
    descArea.style.cssText = `
    width: 100%; min-height: 80px; border: 2px solid #ddd;
    border-radius: 8px; padding: 10px 12px; font-size: 14px;
    resize: vertical; font-family: inherit; outline: none;
    transition: border-color 0.15s;
  `;
    descArea.addEventListener('focus', () => descArea.style.borderColor = '#0079bf');
    descArea.addEventListener('blur', () => descArea.style.borderColor = '#ddd');
    body.appendChild(descArea);
    // Due date
    const dueLabel = document.createElement('label');
    dueLabel.textContent = 'Due date';
    dueLabel.style.cssText = 'display: block; font-size: 12px; font-weight: 600; color: #5e6c84; margin-bottom: 6px; margin-top: 16px;';
    body.appendChild(dueLabel);
    const dueInput = document.createElement('input');
    dueInput.type = 'date';
    dueInput.value = card.dueDate ? card.dueDate.slice(0, 10) : '';
    dueInput.style.cssText = `
    width: 100%; padding: 8px 12px; border: 2px solid #ddd;
    border-radius: 8px; font-size: 14px; font-family: inherit;
    outline: none; transition: border-color 0.15s;
  `;
    dueInput.addEventListener('focus', () => dueInput.style.borderColor = '#0079bf');
    dueInput.addEventListener('blur', () => dueInput.style.borderColor = '#ddd');
    body.appendChild(dueInput);
    // Labels
    const labelsLabel = document.createElement('label');
    labelsLabel.textContent = 'Labels';
    labelsLabel.style.cssText = 'display: block; font-size: 12px; font-weight: 600; color: #5e6c84; margin-bottom: 8px; margin-top: 16px;';
    body.appendChild(labelsLabel);
    const labelsContainer = document.createElement('div');
    labelsContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 6px;';
    for (const label of types_1.LABEL_PALETTE) {
        const isSelected = card.labels.some(l => l.name === label.name);
        const chip = document.createElement('button');
        chip.className = 'modal-label-chip';
        chip.textContent = label.name;
        chip.dataset.labelName = label.name;
        chip.style.cssText = `
      padding: 4px 10px; border-radius: 6px;
      border: 2px solid ${isSelected ? label.color : '#ddd'};
      background: ${isSelected ? label.color : 'transparent'};
      color: ${isSelected ? '#fff' : '#5e6c84'};
      font-size: 12px; font-weight: 600; cursor: pointer;
      transition: all 0.1s;
    `;
        chip.addEventListener('click', () => {
            const selected = card.labels.some(l => l.name === label.name);
            if (selected) {
                chip.style.borderColor = '#ddd';
                chip.style.background = 'transparent';
                chip.style.color = '#5e6c84';
            }
            else {
                chip.style.borderColor = label.color;
                chip.style.background = label.color;
                chip.style.color = '#fff';
            }
        });
        labelsContainer.appendChild(chip);
    }
    body.appendChild(labelsContainer);
    modal.appendChild(body);
    // Footer
    const footer = document.createElement('div');
    footer.style.cssText = 'padding: 0 24px 20px; display: flex; justify-content: flex-end; gap: 8px;';
    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = 'Cancel';
    cancelBtn.style.cssText = `
    padding: 8px 16px; border: 2px solid #ddd; border-radius: 6px;
    background: #fff; color: #5e6c84; font-size: 14px; cursor: pointer;
    font-family: inherit; font-weight: 500;
  `;
    cancelBtn.addEventListener('click', closeCardEditModal);
    const saveBtn = document.createElement('button');
    saveBtn.textContent = 'Save';
    saveBtn.style.cssText = `
    padding: 8px 20px; border: none; border-radius: 6px;
    background: #0079bf; color: #fff; font-size: 14px; cursor: pointer;
    font-family: inherit; font-weight: 600;
  `;
    saveBtn.addEventListener('click', () => {
        const newLabels = [];
        labelsContainer.querySelectorAll('.modal-label-chip').forEach(chip => {
            const bg = chip.style.background;
            const name = chip.dataset.labelName;
            if (name && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)' && bg !== '') {
                const def = types_1.LABEL_PALETTE.find(l => l.name === name);
                if (def)
                    newLabels.push({ name: def.name, color: def.color });
            }
        });
        (0, state_1.updateCard)(card.id, {
            title: titleInput.value.trim() || card.title,
            description: descArea.value.trim(),
            dueDate: dueInput.value ? new Date(dueInput.value).toISOString() : '',
            labels: newLabels,
        });
        closeCardEditModal();
    });
    footer.appendChild(cancelBtn);
    footer.appendChild(saveBtn);
    modal.appendChild(footer);
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    // Click outside to close
    overlay.addEventListener('click', (e) => {
        if (e.target === overlay)
            closeCardEditModal();
    });
    // Escape to close
    const keyHandler = (e) => {
        if (e.key === 'Escape')
            closeCardEditModal();
        if (e.key === 'Enter' && document.activeElement !== descArea) {
            saveBtn.click();
        }
    };
    document.addEventListener('keydown', keyHandler);
    overlay.dataset.keyHandlerAttached = 'true';
    // Focus title
    setTimeout(() => { titleInput.focus(); titleInput.select(); }, 50);
    window._modalKeyHandler = keyHandler;
}
function closeCardEditModal() {
    const overlay = document.querySelector('.modal-overlay');
    if (overlay) {
        if (window._modalKeyHandler) {
            document.removeEventListener('keydown', window._modalKeyHandler);
        }
        overlay.remove();
    }
}
// ─── Inline Title Edit ────────────────────────────────────────────
function startInlineTitleEdit(cardEl, card) {
    const titleEl = cardEl.querySelector('.card-title');
    if (!titleEl || titleEl.querySelector('input'))
        return;
    const input = document.createElement('input');
    input.type = 'text';
    input.value = card.title;
    input.className = 'column-edit-input';
    titleEl.replaceWith(input);
    input.focus();
    input.select();
    const commit = () => {
        const newTitle = input.value.trim();
        if (newTitle && newTitle !== card.title) {
            (0, state_1.updateCard)(card.id, { title: newTitle });
        }
    };
    input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            commit();
        }
        if (e.key === 'Escape') {
            e.preventDefault();
        }
    });
    input.addEventListener('blur', commit);
}
// ─── Label Filters ─────────────────────────────────────────────────
function renderLabelFilters() {
    labelFilterBar.innerHTML = '';
    for (const label of types_1.LABEL_PALETTE) {
        const chip = document.createElement('button');
        chip.className = 'label-filter-chip';
        chip.dataset.labelName = label.name;
        chip.textContent = label.name;
        chip.title = `Filter by "${label.name}"`;
        labelFilterBar.appendChild(chip);
    }
}
// ─── Helpers ───────────────────────────────────────────────────────
function filterCards(cardIds, cards, filterState) {
    return cardIds.filter(cardId => {
        const card = cards[cardId];
        if (!card)
            return false;
        if (filterState.search) {
            if (!card.title.toLowerCase().includes(filterState.search.toLowerCase()))
                return false;
        }
        if (filterState.labelFilters.length > 0) {
            const cardLabelNames = new Set(card.labels.map(l => l.name));
            if (!filterState.labelFilters.some(fn => cardLabelNames.has(fn)))
                return false;
        }
        return true;
    });
}
function removeContextMenu() {
    document.querySelectorAll('.context-menu').forEach(el => el.remove());
}
function getContrastColor(hex) {
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
    return lum > 0.5 ? '#172b4d' : '#ffffff';
}
};

// ── module: src/dragdrop.ts ──
__mods["src/dragdrop.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initCardDrag = initCardDrag;
exports.initColumnDrag = initColumnDrag;
exports.subscribe = subscribe;
exports.isDragging = isDragging;
exports.getDragInfo = getDragInfo;
const state_1 = require("./state");
const types_1 = require("./types");
let info = { active: false, kind: 'card', dragOffsetX: 0, dragOffsetY: 0 };
let ghostEl = null;
let placeholderEl = null;
let colGhostEl = null;
let dragSourceEl = null; // the original card/column element
const listeners = new Set();
// ─── Card Drag ─────────────────────────────────────────────────────
function initCardDrag(cardEl, cardId, columnId) {
    // Prevent duplicate handlers by marking
    if (cardEl.dataset.dragInitialized === 'true')
        return;
    cardEl.dataset.dragInitialized = 'true';
    cardEl.addEventListener('mousedown', (e) => {
        if (e.button !== 0)
            return;
        if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ||
            e.target instanceof HTMLButtonElement || e.target instanceof HTMLSelectElement)
            return;
        if (e.detail >= 2)
            return; // ignore double-click
        const cardRect = cardEl.getBoundingClientRect();
        const model = (0, state_1.getModel)();
        const colData = model.columns[columnId];
        const sourceIdx = colData ? colData.cardIds.indexOf(cardId) : -1;
        if (sourceIdx < 0)
            return;
        e.preventDefault();
        const startX = e.clientX;
        const startY = e.clientY;
        let started = false;
        const preMove = (ev) => {
            if (started)
                return;
            const dx = Math.abs(ev.clientX - startX);
            const dy = Math.abs(ev.clientY - startY);
            if (dx < types_1.DRAG_START_THRESHOLD && dy < types_1.DRAG_START_THRESHOLD)
                return;
            started = true;
            info = {
                active: true, kind: 'card',
                cardId, sourceColumnId: columnId, sourceCardIndex: sourceIdx,
                originalModel: JSON.parse(JSON.stringify(model)),
                dragOffsetX: ev.clientX - cardRect.left,
                dragOffsetY: ev.clientY - cardRect.top,
                targetColumnId: columnId,
                targetCardIndex: sourceIdx,
                sourceCardEl: cardEl,
            };
            dragSourceEl = cardEl;
            makeCardGhost(cardEl, ev.clientX, ev.clientY);
            // Show placeholder at original position
            showPlaceholderAt(cardEl);
            document.addEventListener('mousemove', activeMove);
            document.addEventListener('mouseup', activeUp);
            document.addEventListener('keydown', activeKey);
            notify();
        };
        const preUp = () => {
            document.removeEventListener('mousemove', preMove);
            document.removeEventListener('mouseup', preUp);
        };
        document.addEventListener('mousemove', preMove);
        document.addEventListener('mouseup', preUp);
    });
}
function makeCardGhost(cardEl, x, y) {
    const d = info;
    // Clone without inner edit inputs
    const clone = cardEl.cloneNode(true);
    const editInput = clone.querySelector('input, textarea');
    if (editInput)
        editInput.remove();
    ghostEl = clone;
    ghostEl.className = 'kanban-card drag-ghost';
    ghostEl.style.cssText = '';
    Object.assign(ghostEl.style, {
        position: 'fixed',
        zIndex: '9999',
        pointerEvents: 'none',
        width: `${cardEl.offsetWidth}px`,
        left: `${x - d.dragOffsetX}px`,
        top: `${y - d.dragOffsetY}px`,
        boxShadow: types_1.COLORS.cardShadowDrag,
        transform: 'rotate(-2deg) scale(1.03)',
        opacity: '0.95',
        transition: 'none',
        margin: '0',
        background: '#fff',
        borderRadius: '8px',
    });
    document.body.appendChild(ghostEl);
}
function showPlaceholderAt(cardEl) {
    if (placeholderEl) {
        placeholderEl.remove();
        placeholderEl = null;
    }
    placeholderEl = document.createElement('div');
    placeholderEl.className = 'kanban-card drag-placeholder';
    placeholderEl.style.cssText = `
    height: ${cardEl.offsetHeight}px;
    border: 2px dashed ${types_1.COLORS.dropIndicator};
    border-radius: 8px;
    background: ${types_1.COLORS.placeholderBg};
    margin-bottom: 8px;
    pointer-events: none;
    transition: height 0.1s ease;
  `;
    const parent = cardEl.parentElement;
    if (parent) {
        parent.insertBefore(placeholderEl, cardEl);
    }
}
// ─── Active Drag ───────────────────────────────────────────────────
let rafId = 0;
let pendingMove = null;
function activeMove(e) {
    const d = info;
    if (!d.active || d.kind !== 'card')
        return;
    // Move ghost synchronously for immediate visual feedback
    if (ghostEl) {
        ghostEl.style.left = `${e.clientX - d.dragOffsetX}px`;
        ghostEl.style.top = `${e.clientY - d.dragOffsetY}px`;
    }
    // Hit test synchronously — works with both real dragging and programmatic dispatch
    hitTestCardDrop(e.clientX, e.clientY);
    // Auto-scroll via rAF (continuous action)
    pendingMove = { x: e.clientX, y: e.clientY };
    if (!rafId)
        rafId = requestAnimationFrame(autoScrollFrame);
}
function autoScrollFrame() {
    rafId = 0;
    if (pendingMove) {
        autoScroll(pendingMove.x, pendingMove.y);
        // Continue if still dragging
        const d = info;
        if (d.active && d.kind === 'card') {
            rafId = requestAnimationFrame(autoScrollFrame);
        }
    }
}
function activeUp(e) {
    const d = info;
    if (!d.active || d.kind !== 'card')
        return;
    document.removeEventListener('mousemove', activeMove);
    document.removeEventListener('mouseup', activeUp);
    document.removeEventListener('keydown', activeKey);
    // Execute move
    if (d.targetColumnId) {
        if (d.targetColumnId !== d.sourceColumnId || d.targetCardIndex !== d.sourceCardIndex) {
            (0, state_1.moveCard)(d.cardId, d.sourceColumnId, d.sourceCardIndex, d.targetColumnId, d.targetCardIndex);
        }
        // If same position, just do nothing — model is unchanged
    }
    clearDrag();
}
function activeKey(e) {
    if (e.key === 'Escape')
        cancelCardDrag();
}
function cancelCardDrag() {
    document.removeEventListener('mousemove', activeMove);
    document.removeEventListener('mouseup', activeUp);
    document.removeEventListener('keydown', activeKey);
    // Model was never changed, so no rollback needed
    clearDrag();
}
function clearDrag() {
    if (ghostEl) {
        ghostEl.remove();
        ghostEl = null;
    }
    if (placeholderEl) {
        placeholderEl.remove();
        placeholderEl = null;
    }
    if (colGhostEl) {
        colGhostEl.remove();
        colGhostEl = null;
    }
    dragSourceEl = null;
    info = { active: false, kind: 'card', dragOffsetX: 0, dragOffsetY: 0 };
    notify();
}
// ─── Auto-scroll ───────────────────────────────────────────────────
function autoScroll(x, y) {
    const winW = window.innerWidth;
    if (x < types_1.AUTO_SCROLL_MARGIN)
        window.scrollBy(-types_1.AUTO_SCROLL_SPEED, 0);
    else if (x > winW - types_1.AUTO_SCROLL_MARGIN)
        window.scrollBy(types_1.AUTO_SCROLL_SPEED, 0);
    // Vertical scroll within the target column
    const col = findColumnAt(x, y);
    if (col) {
        const cardList = col.querySelector('.card-list');
        if (cardList) {
            const rect = cardList.getBoundingClientRect();
            if (y < rect.top + types_1.AUTO_SCROLL_MARGIN && cardList.scrollTop > 0) {
                cardList.scrollTop -= types_1.AUTO_SCROLL_SPEED;
            }
            else if (y > rect.bottom - types_1.AUTO_SCROLL_MARGIN) {
                cardList.scrollTop += types_1.AUTO_SCROLL_SPEED;
            }
        }
    }
}
// ─── Hit Testing ───────────────────────────────────────────────────
function findColumnAt(x, y) {
    if (ghostEl)
        ghostEl.style.display = 'none';
    if (placeholderEl)
        placeholderEl.style.display = 'none';
    const el = document.elementFromPoint(x, y);
    if (ghostEl)
        ghostEl.style.display = '';
    if (placeholderEl)
        placeholderEl.style.display = '';
    if (!el)
        return null;
    return el.closest('.kanban-column') ?? null;
}
function hitTestCardDrop(x, y) {
    const d = info;
    if (!d.active || d.kind !== 'card')
        return;
    // Remove old placeholder
    if (placeholderEl) {
        placeholderEl.remove();
        placeholderEl = null;
    }
    const col = findColumnAt(x, y);
    if (!col)
        return;
    const colId = col.dataset.columnId;
    if (!colId)
        return;
    const cardList = col.querySelector('.card-list');
    if (!cardList)
        return;
    // Get visible card elements (not ghost, not placeholder, not form, not hidden by filter)
    const allCardEls = Array.from(cardList.querySelectorAll('.kanban-card:not(.drag-ghost):not(.drag-placeholder)'));
    const model = (0, state_1.getModel)();
    const colData = model.columns[colId];
    if (!colData)
        return;
    // Only cards that exist in this column's model
    const validCards = allCardEls.filter(el => {
        const cid = el.dataset.cardId;
        return cid && colData.cardIds.includes(cid);
    });
    let insertIdx = validCards.length; // default: append at end
    for (let i = 0; i < validCards.length; i++) {
        const rect = validCards[i].getBoundingClientRect();
        const midY = rect.top + rect.height / 2;
        if (y < midY) {
            insertIdx = i;
            break;
        }
    }
    // Hide empty zone
    const emptyZone = cardList.querySelector('.empty-column-zone');
    if (emptyZone)
        emptyZone.style.display = 'none';
    // Create and insert placeholder
    const ph = document.createElement('div');
    ph.className = 'kanban-card drag-placeholder';
    ph.style.cssText = `
    height: 44px; border: 2px dashed ${types_1.COLORS.dropIndicator};
    border-radius: 8px; background: ${types_1.COLORS.placeholderBg};
    margin-bottom: 8px; pointer-events: none;
  `;
    if (insertIdx < validCards.length) {
        cardList.insertBefore(ph, validCards[insertIdx]);
    }
    else {
        cardList.appendChild(ph);
    }
    placeholderEl = ph;
    d.targetColumnId = colId;
    d.targetCardIndex = insertIdx;
}
// ─── Column Drag ───────────────────────────────────────────────────
function initColumnDrag(colEl, columnId) {
    if (colEl.dataset.colDragInitialized === 'true')
        return;
    colEl.dataset.colDragInitialized = 'true';
    const header = colEl.querySelector('.column-header');
    if (!header)
        return;
    header.style.cursor = 'grab';
    header.addEventListener('mousedown', (e) => {
        if (e.button !== 0)
            return;
        if (e.target instanceof HTMLButtonElement)
            return;
        if (e.detail >= 2)
            return;
        e.preventDefault();
        const startX = e.clientX;
        const startY = e.clientY;
        let started = false;
        const model = (0, state_1.getModel)();
        const sourceIdx = model.columnIds.indexOf(columnId);
        if (sourceIdx < 0)
            return;
        const preMove = (ev) => {
            if (started)
                return;
            const dx = Math.abs(ev.clientX - startX);
            const dy = Math.abs(ev.clientY - startY);
            if (dx < types_1.DRAG_START_THRESHOLD && dy < types_1.DRAG_START_THRESHOLD)
                return;
            started = true;
            info = { active: true, kind: 'column', columnId, targetIndex: sourceIdx };
            makeColumnGhost(colEl, ev.clientX, ev.clientY);
            document.addEventListener('mousemove', colDragMove);
            document.addEventListener('mouseup', colDragUp);
            document.addEventListener('keydown', colDragKey);
            notify();
        };
        const preUp = () => {
            document.removeEventListener('mousemove', preMove);
            document.removeEventListener('mouseup', preUp);
        };
        document.addEventListener('mousemove', preMove);
        document.addEventListener('mouseup', preUp);
    });
}
function makeColumnGhost(colEl, x, y) {
    const header = colEl.querySelector('.column-header');
    if (!header)
        return;
    colGhostEl = header.cloneNode(true);
    colGhostEl.style.cssText = `
    position: fixed; z-index: 10000; pointer-events: none;
    width: ${colEl.offsetWidth}px; left: ${x - 40}px; top: ${y - 10}px;
    opacity: 0.85; transform: rotate(-3deg); padding: 8px 12px;
    background: #fff; border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.2);
  `;
    document.body.appendChild(colGhostEl);
}
function colDragMove(e) {
    const d = info;
    if (!d.active || d.kind !== 'column')
        return;
    if (colGhostEl) {
        colGhostEl.style.left = `${e.clientX - 40}px`;
        colGhostEl.style.top = `${e.clientY - 10}px`;
    }
    const cols = Array.from(document.querySelectorAll('.kanban-column'));
    if (colGhostEl)
        colGhostEl.style.display = 'none';
    let insertIdx = cols.length;
    for (let i = 0; i < cols.length; i++) {
        const rect = cols[i].getBoundingClientRect();
        const midX = rect.left + rect.width / 2;
        if (e.clientX < midX) {
            insertIdx = i;
            break;
        }
    }
    if (colGhostEl)
        colGhostEl.style.display = '';
    d.targetIndex = insertIdx;
}
function colDragUp(e) {
    const d = info;
    if (!d.active || d.kind !== 'column')
        return;
    document.removeEventListener('mousemove', colDragMove);
    document.removeEventListener('mouseup', colDragUp);
    document.removeEventListener('keydown', colDragKey);
    const model = (0, state_1.getModel)();
    const origIdx = model.columnIds.indexOf(d.columnId);
    if (origIdx >= 0 && d.targetIndex !== origIdx) {
        (0, state_1.reorderColumn)(d.columnId, d.targetIndex);
    }
    clearDrag();
}
function colDragKey(e) {
    if (e.key === 'Escape') {
        document.removeEventListener('mousemove', colDragMove);
        document.removeEventListener('mouseup', colDragUp);
        document.removeEventListener('keydown', colDragKey);
        clearDrag();
    }
}
// ─── Subscribe ─────────────────────────────────────────────────────
function subscribe(fn) {
    listeners.add(fn);
    return () => listeners.delete(fn);
}
function notify() {
    for (const fn of listeners)
        fn();
}
function isDragging() {
    return info.active;
}
function getDragInfo() {
    return info;
}
};

// ── module: src/toasts.ts ──
__mods["src/toasts.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initToasts = initToasts;
exports.renderToasts = renderToasts;
const state_1 = require("./state");
let container;
function initToasts(root) {
    container = root.querySelector('#toast-container');
}
function renderToasts() {
    container.innerHTML = '';
    const toasts = (0, state_1.getToasts)();
    for (const t of toasts) {
        const el = document.createElement('div');
        el.className = 'toast';
        const msg = document.createElement('span');
        msg.textContent = t.message;
        const btnRow = document.createElement('div');
        btnRow.style.cssText = 'display: flex; gap: 8px;';
        if (t.undoable && t.undoFn) {
            const undoBtn = document.createElement('button');
            undoBtn.className = 'toast-undo';
            undoBtn.textContent = 'Undo';
            undoBtn.addEventListener('click', () => {
                t.undoFn();
                (0, state_1.dismissToast)(t.id);
                renderToasts();
            });
            btnRow.appendChild(undoBtn);
        }
        const dismissBtn = document.createElement('button');
        dismissBtn.className = 'toast-dismiss';
        dismissBtn.textContent = '✕';
        dismissBtn.addEventListener('click', () => { (0, state_1.dismissToast)(t.id); renderToasts(); });
        btnRow.appendChild(dismissBtn);
        el.appendChild(msg);
        el.appendChild(btnRow);
        container.appendChild(el);
    }
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"88a524ae-fdbd-412f-a8d4-c91a0f4efefc","tokensIn":8139175,"tokensOut":88667,"tokensTotal":8227842,"cost":0,"turns":129,"toolCalls":130,"failedToolCalls":1,"timestamp":"2026-08-02T10:15:58.004Z"} -->