mockups/project_board.html
Rama main · réplica de lectura del repositorio original.
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Roadmap Q2 · mkappworks/cloudzilla — Cloudzilla</title>
<link rel="stylesheet" href="/assets/tailwind.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&family=Geist:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body class="min-h-screen antialiased">
<a href="#main-content" class="skip-link">Skip to main content</a>
<div id="cz-header" data-repo-tab="projects"></div>
<script src="shell.js"></script>
<main id="main-content" tabindex="-1" class="max-w-[1400px] mx-auto px-6 pt-6 pb-20">
<div class="space-y-6 pt-2" id="board-root">
<dialog id="card-dialog" class="rounded-lg border border-border bg-popover text-popover-foreground p-0 m-auto w-[960px] max-w-[calc(100vw-2rem)] backdrop:bg-black/60 backdrop:backdrop-blur-xs" aria-labelledby="card-dialog-title">
<form id="card-form" class="flex max-h-[calc(100dvh-4rem)] flex-col">
<div class="flex items-start justify-between gap-3 border-b border-border p-5">
<div class="min-w-0 flex-1">
<p id="card-column" class="mb-1 truncate font-mono text-[11px] uppercase tracking-wider text-muted-foreground"></p>
<div id="title-view" class="flex items-start gap-1">
<div class="min-w-0 pt-1">
<h2 id="card-dialog-title" class="text-base font-semibold leading-tight"><span id="card-heading" class="block break-words"></span></h2>
</div>
<button type="button" id="title-edit-btn" aria-label="Edit title" title="Edit title" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"></path></svg>
</button>
</div>
<div id="title-editor" hidden>
<div class="flex flex-col gap-2">
<label for="card-title-edit" class="sr-only">Title</label>
<input id="card-title-edit" type="text" maxlength="256" autocomplete="off" class="h-9 w-full rounded-md border border-input bg-card px-2.5 text-[15px] font-semibold focus:outline-hidden focus:ring-2 focus:ring-ring disabled:opacity-70"/>
<div class="flex items-center gap-2">
<button type="button" id="title-save" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-8 px-3 text-[13px]">Save</button>
<button type="button" id="title-cancel" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-8 px-3 text-[13px]">Cancel</button>
</div>
</div>
</div>
</div>
<button type="button" id="card-close" aria-label="Close" class="-m-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M18 6L6 18M6 6l12 12"></path></svg>
</button>
</div>
<div class="min-h-0 flex-1 overflow-y-auto p-5 text-sm">
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_17rem]">
<div class="flex min-w-0 flex-col gap-5">
<div id="create-title-field">
<label for="card-title-input" class="mb-1 block text-[12px] font-medium text-muted-foreground">Title</label>
<input id="card-title-input" type="text" maxlength="256" autocomplete="off" class="h-9 w-full rounded-md border border-input bg-card px-2.5 text-[13px] focus:outline-hidden focus:ring-2 focus:ring-ring"/>
</div>
<div>
<div class="mb-1 flex min-h-7 items-center gap-1">
<span id="card-desc-label" class="text-[12px] font-medium text-muted-foreground">Description</span>
<button type="button" id="desc-edit-btn" aria-label="Edit description" title="Edit description" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"></path></svg>
</button>
</div>
<div id="desc-view" role="region" aria-labelledby="card-desc-label">
<div id="desc-preview" class="prose prose-sm dark:prose-invert max-w-none break-words"></div>
<button type="button" id="desc-add" class="w-full rounded-md border border-dashed border-border px-3 py-4 text-left text-[13px] text-muted-foreground hover:border-ring hover:text-foreground">Add a description</button>
</div>
<div id="desc-editor" hidden class="flex flex-col gap-2">
<fieldset class="min-w-0 overflow-hidden rounded-md border border-border bg-card">
<legend class="sr-only">Description</legend>
<div data-markdown-editor data-rows="10" data-standalone="false" data-placeholder="Markdown supported. Reference #123 to link an issue or pull request."></div>
</fieldset>
<div id="desc-actions" class="flex items-center gap-2">
<button type="button" id="desc-save" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-8 px-3 text-[13px]">Save</button>
<button type="button" id="desc-cancel" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-8 px-3 text-[13px]">Cancel</button>
</div>
</div>
</div>
</div>
<div class="flex min-w-0 flex-col gap-5">
<div>
<div class="mb-1 flex min-h-7 items-center gap-2"><span id="card-assignees-label" class="text-[12px] font-medium text-muted-foreground">Assignees</span></div>
<div class="relative" data-multi="assignees">
<button type="button" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="card-assignees-label" class="flex min-h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-card px-2.5 py-1 text-left text-[13px] focus:outline-hidden focus:ring-2 focus:ring-ring">
<span class="flex min-w-0 flex-1 flex-wrap items-center gap-1" data-multi-value></span>
<svg class="shrink-0 text-muted-foreground" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><path d="M6 9l6 6 6-6"></path></svg>
</button>
<ul role="listbox" aria-multiselectable="true" hidden class="absolute left-0 right-0 top-full mt-1 z-50 max-h-60 overflow-y-auto rounded-md border border-border bg-popover text-popover-foreground shadow-md p-1" data-multi-list></ul>
</div>
</div>
<div>
<div class="mb-1 flex min-h-7 items-center gap-2"><span id="card-labels-label" class="text-[12px] font-medium text-muted-foreground">Labels</span></div>
<div class="relative" data-multi="labels">
<button type="button" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="card-labels-label" class="flex min-h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-card px-2.5 py-1 text-left text-[13px] focus:outline-hidden focus:ring-2 focus:ring-ring">
<span class="flex min-w-0 flex-1 flex-wrap items-center gap-1" data-multi-value></span>
<svg class="shrink-0 text-muted-foreground" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><path d="M6 9l6 6 6-6"></path></svg>
</button>
<ul role="listbox" aria-multiselectable="true" hidden class="absolute left-0 right-0 top-full mt-1 z-50 max-h-60 overflow-y-auto rounded-md border border-border bg-popover text-popover-foreground shadow-md p-1" data-multi-list></ul>
</div>
</div>
<div>
<div class="mb-1 flex min-h-7 items-center gap-2"><label for="card-due-input" class="text-[12px] font-medium text-muted-foreground">Due date</label></div>
<input id="card-due-input" type="date" class="flex h-9 w-full rounded-md border border-input bg-card px-2.5 py-1 text-[13px] focus:outline-hidden focus:ring-2 focus:ring-ring"/>
</div>
<div>
<div class="mb-1 flex min-h-7 items-center gap-2"><span id="card-link-label" class="text-[12px] font-medium text-muted-foreground">Linked item</span></div>
<div id="link-chip" hidden class="flex items-center gap-1 rounded-md border border-border bg-card py-1.5 pl-2.5 pr-1 text-[13px]">
<a id="link-anchor" href="#" class="flex min-w-0 flex-1 items-baseline gap-2 rounded hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"></a>
<button type="button" id="link-clear" aria-label="Remove link" class="h-7 w-7 shrink-0 rounded-md text-xs text-muted-foreground hover:bg-muted hover:text-destructive disabled:opacity-50">✕</button>
</div>
<div id="link-picker" class="flex flex-col gap-2">
<label for="card-link-input" class="sr-only">Find an issue or pull request</label>
<input id="card-link-input" type="text" placeholder="Find an issue or PR by title or #number" autocomplete="off" class="h-9 w-full rounded-md border border-input bg-card px-2.5 text-[13px] placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring disabled:opacity-70"/>
</div>
</div>
</div>
</div>
</div>
<p id="card-error" hidden class="px-5 pb-3 text-[12px] text-destructive" role="alert"></p>
<div class="flex flex-wrap items-center gap-2 border-t border-border px-5 py-4">
<div id="card-actions" class="flex w-full flex-wrap items-center gap-2">
<button type="button" id="card-delete" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-destructive/40 text-destructive bg-transparent hover:bg-destructive/10 hover:border-destructive h-9 px-4 text-sm">Delete</button>
<button type="button" id="card-convert" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-9 px-4 text-sm">Convert to issue</button>
<div id="create-actions" class="ml-auto flex items-center gap-2">
<button type="button" id="create-cancel" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-9 px-4 text-sm">Cancel</button>
<button type="submit" id="create-submit" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 text-sm disabled:opacity-50 disabled:pointer-events-none">Create card</button>
</div>
</div>
<div id="card-confirm" hidden class="flex w-full flex-wrap items-center justify-end gap-x-2 gap-y-3" role="group" aria-labelledby="card-confirm-text">
<p id="card-confirm-text" class="basis-full text-[13px] font-medium"></p>
<button type="button" id="confirm-cancel" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-9 px-4 text-sm">Cancel</button>
<button type="button" id="confirm-ok" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-4 text-sm"></button>
</div>
</div>
</form>
</dialog>
<dialog id="add-column-dialog" class="rounded-lg border border-border bg-popover text-popover-foreground p-0 w-[480px] max-w-[calc(100vw-2rem)] max-h-[calc(100dvh-2rem)] overflow-y-auto mt-[28vh] mb-auto translate-y-[min(0px,calc(100dvh-28vh-1rem-100%))] backdrop:bg-black/60 backdrop:backdrop-blur-xs open:animate-in" aria-labelledby="add-column-dialog-title">
<form id="add-column-form">
<div class="sticky top-0 z-10 bg-popover flex flex-col gap-1 p-5 border-b border-border">
<h2 id="add-column-dialog-title" class="text-base font-semibold leading-tight">Add column</h2>
<p class="text-[13px] text-muted-foreground leading-snug">Columns group the cards on this board.</p>
</div>
<div class="p-5 text-sm">
<div class="space-y-3">
<div>
<label for="add-column-name" class="block text-[12px] font-medium text-foreground mb-1">Name</label>
<input id="add-column-name" type="text" required autofocus autocomplete="off" placeholder="To do" class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"/>
</div>
</div>
</div>
<div class="sticky bottom-0 z-10 bg-popover flex items-center justify-end gap-2 p-5 border-t border-border">
<button type="button" id="add-column-cancel" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-9 px-4 text-sm">Cancel</button>
<button type="submit" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 text-sm">Add column</button>
</div>
</form>
</dialog>
<section class="flex flex-wrap items-start justify-between gap-4" aria-labelledby="project-title">
<div class="min-w-0">
<p class="font-mono text-[11px] text-muted-foreground uppercase tracking-wider mb-2">
<a href="repo.html" class="hover:underline">cloudzilla</a>
<span aria-hidden="true">·</span>
<a href="project.html" class="hover:underline">Projects</a>
<span aria-hidden="true">·</span>
Roadmap Q2
</p>
<div class="flex items-center gap-2">
<h1 id="project-title" class="text-2xl font-semibold tracking-tight">Roadmap Q2</h1>
<span id="closed-badge" hidden class="inline-flex items-center gap-1.5 h-[22px] px-2 rounded-full border text-[11px] font-medium bg-muted text-muted-foreground border-transparent">Closed</span>
</div>
<p class="mt-1 text-[13px] text-muted-foreground max-w-2xl">Phase 16–17 work — performance, search, and the new SSO settings UI.</p>
</div>
<button type="button" id="toggle-closed" class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-9 px-4 text-sm">Close project</button>
</section>
<div id="board" class="flex items-start gap-4 overflow-x-auto pb-6"></div>
</div>
</main>
<div id="cz-footer"></div>
<script src="markdown-editor.js"></script>
<script>
(function () {
var TODAY = '2026-05-13';
var PEOPLE = ['malith', 'daisy', 'yumi', 'priya'];
var LABELS = [
{ id: 1, name: 'bug', color: '#d73a4a' },
{ id: 2, name: 'performance', color: '#fbca04' },
{ id: 3, name: 'sso', color: '#5319e7' },
{ id: 4, name: 'documentation', color: '#0075ca' },
{ id: 5, name: 'ui', color: '#7057ff' }
];
var nextId = 100;
var cols = [
{ id: 1, name: 'Backlog', cards: [
{ id: 1, kind: 'issue', num: 142, title: 'Index commits table by repo_id + author_date', state: 'open', labels: [2], assignees: ['priya'] },
{ id: 2, kind: 'issue', num: 138, title: 'Profile slow query: pulls list with assignee join', state: 'open', labels: [2], assignees: [] },
{ id: 3, kind: 'note', title: 'Decide retention window for audit_log table', desc: '90 days vs 365 days. Check with **compliance** before the SSO audit export lands.', labels: [4], assignees: ['malith'], due: '2026-05-30' }
] },
{ id: 2, name: 'In progress', cards: [
{ id: 4, kind: 'pull', num: 341, title: 'tech: lift UI design system foundation across pages', state: 'open', labels: [5], assignees: ['malith'] },
{ id: 5, kind: 'issue', num: 149, title: 'Wire SAML SSO toggle in admin settings', state: 'open', labels: [3], assignees: ['daisy'], due: '2026-05-01' }
] },
{ id: 3, name: 'Review', cards: [
{ id: 6, kind: 'pull', num: 318, title: 'feat: dependency graph rendering', state: 'open', labels: [], assignees: ['yumi'] }
] },
{ id: 4, name: 'Done', cards: [
{ id: 7, kind: 'pull', num: 312, title: 'feat: phase 15.3 dependency graph data model', state: 'merged', labels: [], assignees: [] },
{ id: 8, kind: 'pull', num: 305, title: 'chore: release-please scaffolding', state: 'merged', labels: [], assignees: [] }
] }
];
function $(id) { return document.getElementById(id); }
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"'); }
function label(id) { return LABELS.filter(function (l) { return l.id === id; })[0]; }
var CHIP_STYLES = {
'#d73a4a': '--label-bg:#d73a4a;--label-fg:#ffffff;--label-border:#bd3341;--label-dark-bg:#d73a4a2e;--label-dark-border:#d73a4a66;--label-dark-fg:#e16b77',
'#fbca04': '--label-bg:#fbca04;--label-fg:#1f2328;--label-border:#ddb204;--label-dark-bg:#fbca042e;--label-dark-border:#fbca0466;--label-dark-fg:#fbca04',
'#5319e7': '--label-bg:#5319e7;--label-fg:#ffffff;--label-border:#4916cb;--label-dark-bg:#5319e72e;--label-dark-border:#5319e766;--label-dark-fg:#9875f1',
'#0075ca': '--label-bg:#0075ca;--label-fg:#ffffff;--label-border:#0067b2;--label-dark-bg:#0075ca2e;--label-dark-border:#0075ca66;--label-dark-fg:#4098d7',
'#7057ff': '--label-bg:#7057ff;--label-fg:#ffffff;--label-border:#634de0;--label-dark-bg:#7057ff2e;--label-dark-border:#7057ff66;--label-dark-fg:#9481ff'
};
function chipStyle(color) { return CHIP_STYLES[color]; }
function avatar(name, cls) {
return '<span class="inline-grid place-items-center rounded-full bg-muted border border-border font-medium text-muted-foreground ' + (cls || 'h-5 w-5 text-[10px]') + '" aria-hidden="true">' + esc(name.slice(0, 1).toUpperCase()) + '</span>';
}
function dueLabel(d) {
return new Date(d + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
var BADGE = 'inline-flex items-center gap-1.5 h-[22px] px-2 rounded-full border text-[11px] font-medium ';
function stateBadge(card) {
var s = card.state;
var cls = s === 'open' ? 'bg-success/15 text-success border-success/30'
: s === 'merged' ? 'bg-[hsl(270_70%_50%/0.15)] text-[hsl(270_70%_70%)] border-[hsl(270_70%_50%/0.30)]'
: 'bg-destructive/15 text-destructive border-destructive/30';
var text = card.kind === 'pull' ? s.charAt(0).toUpperCase() + s.slice(1) : s;
return '<span class="' + BADGE + cls + '">' + text + '</span>';
}
function kindIcon(kind) {
return kind === 'pull'
? '<svg class="mt-0.5 shrink-0 text-muted-foreground" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><circle cx="6" cy="6" r="2.5"></circle><circle cx="6" cy="18" r="2.5"></circle><circle cx="18" cy="18" r="2.5"></circle><path d="M6 8.5v7M18 15.5V10a3 3 0 0 0-3-3h-3"></path></svg>'
: '<svg class="mt-0.5 shrink-0 text-muted-foreground" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"></circle><circle cx="12" cy="12" r="1" fill="currentColor"></circle></svg>';
}
function plain(md) { return esc(md).replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/`(.+?)`/g, '<code>$1</code>'); }
function cardHTML(card) {
var note = card.kind === 'note', linked = !note && card.num;
var h = '<li class="group relative rounded-md border border-border bg-card p-3 shadow-xs hover:border-ring/60 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" data-card-id="' + card.id + '" draggable="true"' +
' tabindex="0" role="button" aria-label="Open card: ' + esc(card.title) + '">';
if (note) {
h += '<p class="text-[13px] font-medium line-clamp-3 break-words">' + esc(card.title) + '</p>';
} else {
h += '<div class="flex items-start gap-2">' + kindIcon(card.kind) +
'<p class="min-w-0 text-[13px] font-medium line-clamp-3 break-words">' + esc(card.title) + '</p></div>';
}
if (card.desc) h += '<p class="mt-1 text-[12px] text-muted-foreground line-clamp-1 break-all">' + esc(card.desc) + '</p>';
if (card.labels.length) {
h += '<div class="mt-2 flex flex-wrap gap-1">' + card.labels.map(function (id) {
var l = label(id);
return '<span class="inline-block max-w-full truncate align-middle rounded-full border px-2 py-px text-[11px] leading-4 font-medium border-(color:--label-border) bg-(color:--label-bg) text-(color:--label-fg) dark:border-(color:--label-dark-border) dark:bg-(color:--label-dark-bg) dark:text-(color:--label-dark-fg)" style="' + chipStyle(l.color) + '">' + esc(l.name) + '</span>';
}).join('') + '</div>';
}
if (card.num) {
h += '<div class="mt-2 flex items-center gap-2">' + (note ? kindIcon(card.kind) : '') +
'<span class="font-mono text-[11px] text-muted-foreground">#' + card.num + '</span>' + stateBadge(card) + '</div>';
}
if (card.due || card.assignees.length) {
var overdue = card.due && card.due < TODAY;
h += '<div class="mt-2 flex items-center justify-between gap-2">';
h += card.due
? '<span class="inline-flex items-center gap-1 text-[11px] ' + (overdue ? 'text-destructive font-medium' : 'text-muted-foreground') + '"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="16" rx="2"></rect><path d="M3 10h18M8 3v4M16 3v4"></path></svg><time datetime="' + card.due + '">' + dueLabel(card.due) + '</time>' + (overdue ? '<span class="sr-only">(overdue)</span>' : '') + '</span>'
: '<span></span>';
if (card.assignees.length) {
h += '<div class="flex -space-x-1.5">' + card.assignees.map(function (a) {
return '<span class="rounded-full ring-2 ring-card" title="' + esc(a) + '">' + avatar(a) + '</span>';
}).join('') + '</div>';
}
h += '</div>';
}
if (!note) h += '<button type="button" data-remove aria-label="Remove from board" class="absolute right-1.5 top-1.5 z-10 rounded px-1 text-xs text-muted-foreground opacity-0 hover:bg-muted hover:text-destructive focus:opacity-100 group-hover:opacity-100">✕</button>';
return h + '</li>';
}
function colHTML(col) {
return '<section class="flex-none w-72 rounded-md border border-border bg-muted/30" data-column-id="' + col.id + '" aria-labelledby="col-' + col.id + '">' +
'<header class="px-3 py-2.5 flex items-center justify-between gap-2">' +
'<h2 id="col-' + col.id + '" class="flex items-center gap-2 min-w-0 text-[13px] font-semibold"><span class="truncate">' + esc(col.name) + '</span>' +
'<span class="shrink-0 rounded-full bg-muted px-1.5 font-mono text-[11px] font-normal text-muted-foreground">' + col.cards.length + '</span></h2>' +
'<div class="relative"><button type="button" data-col-menu aria-label="Actions for column ' + esc(col.name) + '" aria-haspopup="menu" aria-expanded="false" class="rounded px-1.5 text-muted-foreground hover:bg-muted hover:text-foreground">⋯</button>' +
'<div hidden data-col-panel class="absolute right-0 top-full z-20 mt-1 w-56 rounded-md border border-border bg-popover p-1 shadow-md">' +
'<div data-col-ask role="menu"><button type="button" role="menuitem" data-col-delete class="w-full rounded px-2 py-1.5 text-left text-[13px] text-destructive hover:bg-muted">Delete column</button></div>' +
'<div hidden data-col-confirm role="group" class="flex flex-col gap-2 p-2"><p class="text-[13px] font-medium">' + delPrompt(col.cards.length) + '</p>' +
'<div class="flex justify-end gap-2"><button type="button" data-col-cancel class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors border border-input bg-transparent hover:bg-accent hover:text-accent-foreground h-8 px-3 text-[13px]">Cancel</button>' +
'<button type="button" data-col-remove class="inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors bg-destructive text-destructive-foreground hover:bg-destructive/90 h-8 px-3 text-[13px]">Delete column</button></div></div>' +
'</div></div></header>' +
'<ul class="space-y-2 px-2 pb-2 min-h-2" data-cards>' + col.cards.map(cardHTML).join('') + '</ul>' +
'<div class="px-2 pb-2"><button type="button" data-add-card class="w-full rounded-md px-2 py-1.5 text-left text-[13px] text-muted-foreground hover:bg-muted hover:text-foreground">+ Add item</button></div></section>';
}
function delPrompt(n) {
return n === 0 ? 'Delete this column?' : n === 1 ? 'Delete this column and its 1 card?' : 'Delete this column and its ' + n + ' cards?';
}
var board = $('board');
function render() {
board.innerHTML = cols.map(colHTML).join('') +
'<div class="flex-none w-72"><button type="button" id="add-column-btn" class="flex h-11 w-full items-center justify-center rounded-md border border-dashed border-border text-[13px] text-muted-foreground hover:border-ring hover:text-foreground">+ Add column</button></div>';
}
function find(cardId) {
for (var i = 0; i < cols.length; i++) for (var j = 0; j < cols[i].cards.length; j++)
if (cols[i].cards[j].id === cardId) return { col: cols[i], idx: j, card: cols[i].cards[j] };
}
function colOf(el) { var s = el.closest('[data-column-id]'); return s && cols.filter(function (c) { return c.id === +s.getAttribute('data-column-id'); })[0]; }
var dlg = $('card-dialog'), cur = null, mode = 'edit', draft = null, form = $('card-form');
var descTa = function () { return $('desc-editor').querySelector('textarea'); };
function chips(kind, selected) {
if (!selected.length) return '<span class="text-muted-foreground">' + (kind === 'assignees' ? 'Unassigned' : 'No labels') + '</span>';
return selected.map(function (v) {
if (kind === 'assignees') return '<span class="inline-flex min-w-0 items-center gap-1">' + avatar(v) + '<span class="truncate">' + esc(v) + '</span></span>';
var l = label(v);
return '<span class="inline-block max-w-full truncate align-middle rounded-full border px-2 py-px text-[11px] leading-4 font-medium border-(color:--label-border) bg-(color:--label-bg) text-(color:--label-fg) dark:border-(color:--label-dark-border) dark:bg-(color:--label-dark-bg) dark:text-(color:--label-dark-fg)" style="' + chipStyle(l.color) + '">' + esc(l.name) + '</span>';
}).join('');
}
function renderMulti(kind) {
var box = document.querySelector('[data-multi="' + kind + '"]');
var sel = draft[kind];
box.querySelector('[data-multi-value]').innerHTML = chips(kind, sel);
box.querySelector('[data-multi-list]').innerHTML = (kind === 'assignees' ? PEOPLE : LABELS.map(function (l) { return l.id; })).map(function (v) {
var on = sel.indexOf(v) !== -1;
var inner = kind === 'assignees' ? avatar(v) + '<span class="flex-1 truncate">' + esc(v) + '</span>'
: '<span class="flex-1"><span class="inline-block max-w-full truncate align-middle rounded-full border px-2 py-px text-[11px] leading-4 font-medium border-(color:--label-border) bg-(color:--label-bg) text-(color:--label-fg) dark:border-(color:--label-dark-border) dark:bg-(color:--label-dark-bg) dark:text-(color:--label-dark-fg)" style="' + chipStyle(label(v).color) + '">' + esc(label(v).name) + '</span></span>';
return '<li role="option" aria-selected="' + on + '"><button type="button" data-multi-opt="' + esc(v) + '" class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-[13px] hover:bg-muted">' + inner +
(on ? '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="shrink-0 text-success" aria-hidden="true"><polyline points="20 6 9 17 4 12"></polyline></svg>' : '') + '</button></li>';
}).join('');
}
function renderLink() {
var l = draft.num ? draft : null;
$('link-chip').hidden = !l;
$('link-picker').hidden = !!l;
if (l) {
$('link-anchor').href = l.kind === 'pull' ? 'pr.html' : 'issue_detail.html';
$('link-anchor').innerHTML = '<span class="shrink-0 font-mono text-[11px] text-muted-foreground">' + (l.kind === 'pull' ? 'PR #' : '#') + l.num + '</span><span class="min-w-0 flex-1 truncate">' + esc(l.title) + '</span><span class="shrink-0 text-[11px] text-muted-foreground">' + l.state + '</span>';
}
}
function renderDesc() {
$('desc-preview').innerHTML = draft.desc ? '<p>' + plain(draft.desc) + '</p>' : '';
$('desc-preview').hidden = !draft.desc;
$('desc-add').hidden = !!draft.desc;
}
function setEditing(field, on) {
if (field === 'title') { $('title-view').hidden = on; $('title-editor').hidden = !on; if (on) { $('card-title-edit').value = draft.title; $('card-title-edit').focus(); } }
if (field === 'desc') { $('desc-view').hidden = on; $('desc-editor').hidden = !on; $('desc-edit-btn').hidden = on; if (on) descTa().value = draft.desc || ''; }
}
function confirmAsk(kind) {
$('card-actions').hidden = true; $('card-confirm').hidden = false;
$('card-confirm-text').textContent = kind === 'convert' ? 'Create an issue from this card? The card will link to the new issue.' : 'Delete this card?';
$('confirm-ok').textContent = kind === 'convert' ? 'Create issue' : 'Delete card';
$('confirm-ok').className = $('confirm-ok').className.replace(/bg-\S+ text-\S+ hover:bg-\S+/, kind === 'convert' ? 'bg-primary text-primary-foreground hover:bg-primary/90' : 'bg-destructive text-destructive-foreground hover:bg-destructive/90');
$('confirm-ok').setAttribute('data-kind', kind);
}
function openCard(col, card) {
cur = { col: col, card: card };
mode = card ? 'edit' : 'create';
draft = card ? JSON.parse(JSON.stringify(card)) : { kind: 'note', title: '', desc: '', labels: [], assignees: [] };
$('card-column').hidden = mode !== 'edit';
$('card-column').textContent = col.name;
$('card-heading').textContent = mode === 'edit' ? draft.title : 'Add item';
$('title-edit-btn').hidden = mode !== 'edit' || !!draft.num;
$('create-title-field').hidden = mode !== 'create';
$('card-title-input').value = '';
$('desc-edit-btn').hidden = mode !== 'edit';
$('card-due-input').value = draft.due || '';
$('card-delete').hidden = $('card-convert').hidden = mode !== 'edit';
$('card-convert').hidden = mode !== 'edit' || !!draft.num;
$('create-actions').hidden = mode !== 'create';
$('card-confirm').hidden = true; $('card-actions').hidden = false;
$('card-error').hidden = true;
setEditing('title', false);
setEditing('desc', mode === 'create');
$('desc-actions').hidden = mode === 'create';
renderDesc(); renderMulti('assignees'); renderMulti('labels'); renderLink();
$('create-submit').disabled = true;
dlg.showModal();
if (mode === 'create') $('card-title-input').focus();
}
function commit() {
if (mode !== 'edit') return;
Object.keys(draft).forEach(function (k) { cur.card[k] = draft[k]; });
render();
}
board.addEventListener('click', function (e) {
var t = e.target;
var add = t.closest('[data-add-card]');
if (add) return openCard(colOf(add), null);
if (t.closest('#add-column-btn')) { $('add-column-dialog').showModal(); return; }
var rm = t.closest('[data-remove]');
if (rm) { var f = find(+rm.closest('[data-card-id]').getAttribute('data-card-id')); f.col.cards.splice(f.idx, 1); render(); return; }
var menu = t.closest('[data-col-menu]');
var panels = board.querySelectorAll('[data-col-panel]');
if (menu) {
var p = menu.parentNode.querySelector('[data-col-panel]'), was = !p.hidden;
panels.forEach(function (x) { x.hidden = true; x.querySelector('[data-col-ask]').hidden = false; x.querySelector('[data-col-confirm]').hidden = true; });
p.hidden = was; menu.setAttribute('aria-expanded', String(!was));
return;
}
var panel = t.closest('[data-col-panel]');
if (panel) {
if (t.closest('[data-col-delete]')) { panel.querySelector('[data-col-ask]').hidden = true; panel.querySelector('[data-col-confirm]').hidden = false; }
if (t.closest('[data-col-cancel]')) { panel.hidden = true; panel.querySelector('[data-col-ask]').hidden = false; panel.querySelector('[data-col-confirm]').hidden = true; }
if (t.closest('[data-col-remove]')) { cols.splice(cols.indexOf(colOf(panel)), 1); render(); }
return;
}
var li = t.closest('li[role="button"]');
if (li) { var ff = find(+li.getAttribute('data-card-id')); openCard(ff.col, ff.card); }
});
document.addEventListener('click', function (e) {
if (!e.target.closest('[data-col-panel],[data-col-menu]')) board.querySelectorAll('[data-col-panel]').forEach(function (x) { x.hidden = true; });
if (!e.target.closest('[data-multi]')) dlg.querySelectorAll('[data-multi-list]').forEach(function (x) { x.hidden = true; });
});
board.addEventListener('keydown', function (e) {
var li = e.target.closest('li[role="button"]');
if (li && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); li.click(); }
});
var dragging = null;
board.addEventListener('dragstart', function (e) { var li = e.target.closest && e.target.closest('[data-card-id]'); if (li) { dragging = +li.getAttribute('data-card-id'); e.dataTransfer.effectAllowed = 'move'; } });
board.addEventListener('dragover', function (e) { if (dragging && e.target.closest('[data-column-id]')) e.preventDefault(); });
board.addEventListener('drop', function (e) {
var target = e.target.closest('[data-column-id]');
if (!dragging || !target) return;
e.preventDefault();
var f = find(dragging), dest = colOf(target);
f.col.cards.splice(f.idx, 1); dest.cards.push(f.card);
dragging = null; render();
});
document.querySelectorAll('[data-multi]').forEach(function (box) {
var kind = box.getAttribute('data-multi');
box.querySelector('button').addEventListener('click', function () {
var list = box.querySelector('[data-multi-list]'), was = !list.hidden;
dlg.querySelectorAll('[data-multi-list]').forEach(function (x) { x.hidden = true; });
list.hidden = was;
box.querySelector('button').setAttribute('aria-expanded', String(!was));
});
box.addEventListener('click', function (e) {
var opt = e.target.closest('[data-multi-opt]');
if (!opt) return;
var v = kind === 'labels' ? +opt.getAttribute('data-multi-opt') : opt.getAttribute('data-multi-opt');
var i = draft[kind].indexOf(v);
if (i === -1) draft[kind].push(v); else draft[kind].splice(i, 1);
renderMulti(kind);
box.querySelector('[data-multi-list]').hidden = false;
commit();
});
});
$('title-edit-btn').addEventListener('click', function () { setEditing('title', true); });
$('title-cancel').addEventListener('click', function () { setEditing('title', false); });
$('card-title-edit').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); $('title-save').click(); } });
$('title-save').addEventListener('click', function () {
var v = $('card-title-edit').value.trim();
if (!v) return;
draft.title = v; $('card-heading').textContent = v; setEditing('title', false); commit();
});
function editDesc() { setEditing('desc', true); }
$('desc-edit-btn').addEventListener('click', editDesc);
$('desc-add').addEventListener('click', editDesc);
$('desc-cancel').addEventListener('click', function () { setEditing('desc', false); });
$('desc-save').addEventListener('click', function () { draft.desc = descTa().value.trim(); renderDesc(); setEditing('desc', false); commit(); });
$('card-due-input').addEventListener('change', function () { draft.due = this.value || undefined; commit(); });
$('link-clear').addEventListener('click', function () { delete draft.num; delete draft.state; draft.kind = 'note'; renderLink(); $('card-convert').hidden = mode !== 'edit'; commit(); });
$('card-link-input').addEventListener('keydown', function (e) {
if (e.key !== 'Enter') return;
e.preventDefault();
var m = this.value.match(/\d+/);
if (!m) return;
draft.kind = 'issue'; draft.num = +m[0]; draft.state = 'open';
if (!draft.title) draft.title = 'Issue #' + m[0];
this.value = ''; renderLink(); $('card-convert').hidden = true; commit();
});
$('card-title-input').addEventListener('input', function () { $('create-submit').disabled = !this.value.trim(); });
form.addEventListener('submit', function (e) {
e.preventDefault();
if (mode !== 'create') return;
draft.title = $('card-title-input').value.trim();
draft.desc = descTa().value.trim();
draft.due = $('card-due-input').value || undefined;
draft.id = ++nextId;
cur.col.cards.push(draft);
dlg.close(); render();
});
$('card-close').addEventListener('click', function () { dlg.close(); });
$('create-cancel').addEventListener('click', function () { dlg.close(); });
$('card-delete').addEventListener('click', function () { confirmAsk('delete'); });
$('card-convert').addEventListener('click', function () { confirmAsk('convert'); });
$('confirm-cancel').addEventListener('click', function () { $('card-confirm').hidden = true; $('card-actions').hidden = false; });
$('confirm-ok').addEventListener('click', function () {
if (this.getAttribute('data-kind') === 'delete') {
var f = find(cur.card.id); f.col.cards.splice(f.idx, 1);
} else {
cur.card.kind = 'issue'; cur.card.num = 350; cur.card.state = 'open';
}
dlg.close(); render();
});
$('add-column-form').addEventListener('submit', function (e) {
e.preventDefault();
var name = $('add-column-name').value.trim();
if (!name) return;
cols.push({ id: ++nextId, name: name, cards: [] });
$('add-column-name').value = '';
$('add-column-dialog').close(); render();
});
$('add-column-cancel').addEventListener('click', function () { $('add-column-dialog').close(); });
var closed = false;
$('toggle-closed').addEventListener('click', function () {
closed = !closed;
$('closed-badge').hidden = !closed;
this.textContent = closed ? 'Reopen' : 'Close project';
});
render();
})();
</script>
</body>
</html>