Ctrl K
ComunidadGuías…

mockups/project_board.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!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, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;'); }
  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>