{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "kanban-task-board",
  "title": "Kanban Task Board",
  "type": "registry:page",
  "description": "Full kanban task board surface: 4 status columns, drag-and-drop card movement with insertion indicator, board/list view toggle, search + priority + assignee filters, collapsible columns, click-to-open task detail Sheet (comments, subtasks, files), full-form Add Task Dialog with priority/assignee/due-date/tags/subtasks. Mega-block: ships 14 React files (KanbanBoard + 6 kanban-* views + 7 small badges/lists). Bind with columns/onColumnsChange; consumer owns the columns array so swapping the in-memory store for Drizzle/Postgres is a one-line change. Auto-pulls the use-kanban hook (types + configs + helpers) and kanban-data lib (seed columns).",
  "categories": [
    "productivity",
    "dashboard",
    "data"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/kanban-task-board/kanban-task-board.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport {\n  Calendar,\n  ChevronsLeft,\n  ChevronsRight,\n  LayoutGrid,\n  List,\n  MessageSquare,\n  Plus,\n  Search,\n  Trash2,\n} from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/avatar/avatar'\nimport '../../components/badge/badge'\nimport '../../components/button/button'\nimport '../../components/dialog/dialog'\nimport '../../components/input/input'\nimport '../../components/label/label'\nimport '../../components/select/select'\nimport '../../components/sheet/sheet'\nimport '../../components/textarea/textarea'\nimport type { UipInput } from '../../components/input/input'\nimport type { UipSelect } from '../../components/select/select'\n\nexport interface BoardTaskComment {\n  id: string\n  author: string\n  text: string\n  time: string\n}\n\nexport interface BoardTask {\n  id: string\n  title: string\n  description?: string\n  priority: 'low' | 'medium' | 'high' | 'urgent'\n  tags: Array<{ label: string; color: string }>\n  dueDate?: string\n  assignee: { name: string; color: string }\n  comments: BoardTaskComment[]\n}\n\nexport interface BoardColumn {\n  id: string\n  title: string\n  dotColor: string\n  tasks: BoardTask[]\n}\n\nconst PRIORITY_LABEL: Record<BoardTask['priority'], string> = {\n  low: 'Low',\n  medium: 'Medium',\n  high: 'High',\n  urgent: 'Urgent',\n}\n\nfunction priorityAccent(p: BoardTask['priority']): string {\n  switch (p) {\n    case 'low':\n      return 'bg-muted-foreground'\n    case 'medium':\n      return 'bg-info'\n    case 'high':\n      return 'bg-warning'\n    default:\n      return 'bg-destructive'\n  }\n}\n\nfunction initials(name: string): string {\n  return name\n    .split(' ')\n    .map((p) => p[0])\n    .join('')\n    .slice(0, 2)\n    .toUpperCase()\n}\n\nfunction seedBoardColumns(): BoardColumn[] {\n  return [\n    {\n      id: 'backlog',\n      title: 'Backlog',\n      dotColor: 'bg-muted-foreground/60',\n      tasks: [\n        {\n          id: 'KAN-101',\n          title: 'Cross-platform auth synchronization',\n          description: 'Implement token refresh rotation across mobile and web clients.',\n          priority: 'high',\n          tags: [{ label: 'Auth', color: 'bg-chart-1/10 text-chart-1 ring-chart-1/20' }],\n          dueDate: 'Jun 2, 2026',\n          assignee: { name: 'Alice Chen', color: 'bg-chart-1/15 text-chart-1' },\n          comments: [{ id: 'c1', author: 'Dan Ruiz', text: 'Needs a spike on refresh races first.', time: '2d ago' }],\n        },\n        {\n          id: 'KAN-102',\n          title: 'Scale multi-region edge caching',\n          priority: 'medium',\n          tags: [{ label: 'Infra', color: 'bg-info/10 text-info ring-info/20' }],\n          dueDate: 'Jul 10, 2026',\n          assignee: { name: 'Dan Ruiz', color: 'bg-info/10 text-info' },\n          comments: [],\n        },\n      ],\n    },\n    {\n      id: 'todo',\n      title: 'To Do',\n      dotColor: 'bg-info',\n      tasks: [\n        {\n          id: 'KAN-103',\n          title: 'OKLCH theme token harmonizer',\n          description: 'Calibrate contrast ratios across light and dark modes.',\n          priority: 'high',\n          tags: [{ label: 'Design', color: 'bg-warning/10 text-warning ring-warning/20' }],\n          dueDate: 'May 20, 2026',\n          assignee: { name: 'Leo Vance', color: 'bg-warning/10 text-warning' },\n          comments: [],\n        },\n      ],\n    },\n    {\n      id: 'in-progress',\n      title: 'In Progress',\n      dotColor: 'bg-warning',\n      tasks: [\n        {\n          id: 'KAN-104',\n          title: 'Real-time telemetry event streaming',\n          description: 'Wire SSE endpoints for instant live preview updates.',\n          priority: 'urgent',\n          tags: [\n            { label: 'Realtime', color: 'bg-destructive/10 text-destructive ring-destructive/20' },\n            { label: 'Backend', color: 'bg-chart-2/10 text-chart-2 ring-chart-2/20' },\n          ],\n          dueDate: 'Jun 9, 2026',\n          assignee: { name: 'Sarah Miller', color: 'bg-success/10 text-success' },\n          comments: [\n            { id: 'c1', author: 'Sarah Miller', text: 'SSE endpoint live on staging.', time: '3h ago' },\n            { id: 'c2', author: 'Leo Vance', text: 'Client reconnect looks solid.', time: '1h ago' },\n          ],\n        },\n      ],\n    },\n    {\n      id: 'done',\n      title: 'Done',\n      dotColor: 'bg-success',\n      tasks: [\n        {\n          id: 'KAN-105',\n          title: 'Sub-pixel border and shadow layering',\n          priority: 'low',\n          tags: [{ label: 'Craft', color: 'bg-muted text-muted-foreground ring-muted-foreground/20' }],\n          assignee: { name: 'Dan Ruiz', color: 'bg-info/10 text-info' },\n          comments: [],\n        },\n      ],\n    },\n  ]\n}\n\n/**\n * <uip-kanban-task-board> — the registry KanbanBoard block as a web component.\n *\n * Ports the full enterprise surface (header, toolbar with search + priority\n * + assignee filters, board/list toggle, collapsible drag-and-drop columns,\n * task detail sheet with comments, add-task dialog) with class strings kept\n * close to React's KanbanBoard / KanbanCard / KanbanToolbar.\n *\n * Deviations from React: the 14-file decomposition (KanbanCard, KanbanColumn,\n * KanbanListView, KanbanTaskSheet, KanbanAddTaskDialog, badges…) is folded\n * into this single file; subtask checklists and file attachments are\n * dropped (cards keep tags, due date, comment counts, assignee); the\n * assignee filter is a select instead of the avatar stack; card dropdown\n * menus and tooltips are dropped (click opens the sheet); `columns` is a\n * settable property with a `columns-change` event instead of the\n * use-kanban/kanban-data store. React's injected card-in keyframes are\n * copied into `static styles` (allowed exception).\n */\nexport class UipKanbanTaskBoard extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  // card-in is copied from React's injected <style> (keyframes exception).\n  static styles = [\n    tailwind,\n    css`:host { display: block; }`,\n    css`\n      .kanban-card { animation: card-in 0.25s ease-out both; }\n      @keyframes card-in {\n        from { opacity: 0; transform: translateY(6px); }\n      }\n    `,\n  ]\n\n  static properties = {\n    columns: { attribute: false },\n    heading: {},\n    description: {},\n    searchQuery: { state: true },\n    selectedPriority: { state: true },\n    selectedAssignee: { state: true },\n    viewMode: { state: true },\n    collapsed: { state: true },\n    detailOpen: { state: true },\n    detailTaskId: { state: true },\n    addOpen: { state: true },\n    addColumnId: { state: true },\n    draggedId: { state: true },\n    dragOverColumn: { state: true },\n    dropIndex: { state: true },\n    addTitle: { state: true },\n    addDescription: { state: true },\n    addPriority: { state: true },\n    addAssignee: { state: true },\n    addDueDate: { state: true },\n    commentDraft: { state: true },\n  }\n\n  columns: BoardColumn[] = []\n  /** React's `title` (`title` is taken by HTMLElement). */\n  heading = 'Kanban Board'\n  description = 'Drag tasks across columns to update their status.'\n\n  private searchQuery = ''\n  private selectedPriority: '' | BoardTask['priority'] = ''\n  private selectedAssignee = ''\n  private viewMode: 'board' | 'list' = 'board'\n  private collapsed: string[] = []\n  private detailOpen = false\n  private detailTaskId: string | null = null\n  private addOpen = false\n  private addColumnId = 'backlog'\n  private draggedId: string | null = null\n  private dragOverColumn: string | null = null\n  private dropIndex = -1\n  private addTitle = ''\n  private addDescription = ''\n  private addPriority: BoardTask['priority'] = 'medium'\n  private addAssignee = 'Alice Chen'\n  private addDueDate = ''\n  private commentDraft = ''\n  private lastDragEnd = 0\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-task-board')\n    if (this.columns.length === 0) this.columns = seedBoardColumns()\n  }\n\n  private commit(mutator: (cols: BoardColumn[]) => void) {\n    const next = this.columns.map((c) => ({ ...c, tasks: [...c.tasks] }))\n    mutator(next)\n    this.columns = next\n    this.dispatchEvent(new CustomEvent('columns-change', { detail: next, bubbles: true, composed: true }))\n  }\n\n  private get assigneeNames(): string[] {\n    const set = new Set<string>()\n    for (const c of this.columns) for (const t of c.tasks) set.add(t.assignee.name)\n    return [...set]\n  }\n\n  private get filteredColumns(): BoardColumn[] {\n    const q = this.searchQuery.toLowerCase()\n    return this.columns.map((col) => ({\n      ...col,\n      tasks: col.tasks.filter((t) => {\n        const matchesSearch = !q || t.title.toLowerCase().includes(q) || t.id.toLowerCase().includes(q)\n        const matchesPriority = !this.selectedPriority || t.priority === this.selectedPriority\n        const matchesAssignee = !this.selectedAssignee || t.assignee.name === this.selectedAssignee\n        return matchesSearch && matchesPriority && matchesAssignee\n      }),\n    }))\n  }\n\n  private get totalTasks(): number {\n    return this.columns.reduce((sum, col) => sum + col.tasks.length, 0)\n  }\n\n  private findTask(id: string): { col: BoardColumn; task: BoardTask } | null {\n    for (const col of this.columns) {\n      const task = col.tasks.find((t) => t.id === id)\n      if (task) return { col, task }\n    }\n    return null\n  }\n\n  private openDetail(task: BoardTask) {\n    this.detailTaskId = task.id\n    this.commentDraft = ''\n    this.detailOpen = true\n  }\n\n  private moveTask(taskId: string, targetColumnId: string, at?: number) {\n    this.commit((cols) => {\n      let task: BoardTask | undefined\n      for (const c of cols) {\n        const i = c.tasks.findIndex((t) => t.id === taskId)\n        if (i !== -1) {\n          task = c.tasks.splice(i, 1)[0]\n          break\n        }\n      }\n      const target = cols.find((c) => c.id === targetColumnId)\n      if (!task || !target) return\n      target.tasks.splice(at ?? target.tasks.length, 0, task)\n    })\n  }\n\n  private onDragStart(e: DragEvent, taskId: string) {\n    this.draggedId = taskId\n    if (e.dataTransfer) {\n      e.dataTransfer.effectAllowed = 'move'\n      e.dataTransfer.setData('text/plain', taskId)\n    }\n  }\n\n  private resetDrag() {\n    this.draggedId = null\n    this.dragOverColumn = null\n    this.dropIndex = -1\n    this.lastDragEnd = Date.now()\n  }\n\n  private onCardDragOver(e: DragEvent, columnId: string, taskIndex: number) {\n    e.preventDefault()\n    e.stopPropagation()\n    this.dragOverColumn = columnId\n    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()\n    this.dropIndex = e.clientY < rect.top + rect.height / 2 ? taskIndex : taskIndex + 1\n  }\n\n  private onLaneDragOver(e: DragEvent, columnId: string, taskCount: number) {\n    e.preventDefault()\n    this.dragOverColumn = columnId\n    this.dropIndex = taskCount\n  }\n\n  private onDrop(columnId: string) {\n    if (!this.draggedId) {\n      this.resetDrag()\n      return\n    }\n    this.moveTask(this.draggedId, columnId, this.dropIndex < 0 ? undefined : this.dropIndex)\n    this.resetDrag()\n  }\n\n  private addComment() {\n    const text = this.commentDraft.trim()\n    if (!text || !this.detailTaskId) return\n    const id = this.detailTaskId\n    this.commit((cols) => {\n      for (const c of cols) {\n        const t = c.tasks.find((t) => t.id === id)\n        if (t) t.comments = [...t.comments, { id: `c${Date.now()}`, author: 'Admin User', text, time: 'Just now' }]\n      }\n    })\n    this.commentDraft = ''\n  }\n\n  private createTask(e: Event) {\n    e.preventDefault()\n    if (!this.addTitle.trim()) return\n    const task: BoardTask = {\n      id: `KAN-${Math.floor(100 + Math.random() * 900)}`,\n      title: this.addTitle.trim(),\n      description: this.addDescription.trim() || undefined,\n      priority: this.addPriority,\n      tags: [],\n      dueDate: this.addDueDate || undefined,\n      assignee: { name: this.addAssignee, color: 'bg-chart-1/15 text-chart-1' },\n      comments: [],\n    }\n    const columnId = this.addColumnId\n    this.commit((cols) => {\n      cols.find((c) => c.id === columnId)?.tasks.push(task)\n    })\n    this.addTitle = ''\n    this.addDescription = ''\n    this.addOpen = false\n  }\n\n  private renderCard(task: BoardTask, isDone: boolean) {\n    return html`\n      <div\n        data-slot=\"kanban-task-board\"\n        role=\"button\"\n        tabindex=\"0\"\n        draggable=\"true\"\n        class=${cn(\n          'kanban-card group/card bg-card relative cursor-grab rounded-lg border p-3 transition-colors duration-150',\n          'hover:border-border hover:shadow-md active:scale-[0.97] active:cursor-grabbing',\n          'focus-visible:border-ring focus-visible:ring-ring/50 outline-none focus-visible:ring-[3px]',\n          isDone ? 'opacity-75 hover:opacity-100 focus-visible:opacity-100' : '',\n        )}\n        @click=${() => {\n          if (Date.now() - this.lastDragEnd < 200) return\n          this.openDetail(task)\n        }}\n        @keydown=${(e: KeyboardEvent) => {\n          if (e.key !== 'Enter' && e.key !== ' ') return\n          e.preventDefault()\n          this.openDetail(task)\n        }}\n        @dragstart=${(e: DragEvent) => this.onDragStart(e, task.id)}\n        @dragend=${this.resetDrag}\n      >\n        <div\n          class=${cn(\n            'kanban-accent absolute top-3 bottom-3 left-0 w-[1.5px] rounded-full transition-colors duration-150',\n            priorityAccent(task.priority),\n            task.priority === 'low' ? 'opacity-40' : task.priority === 'medium' ? 'opacity-60' : 'opacity-90',\n          )}\n        ></div>\n        <div class=\"mb-1 flex items-center justify-between pl-2\">\n          <span class=\"text-muted-foreground/70 font-mono text-xs\">${task.id}</span>\n          <span class=${cn('text-xs font-semibold', priorityAccent(task.priority).replace('bg-', 'text-'))}>\n            ${PRIORITY_LABEL[task.priority]}\n          </span>\n        </div>\n        <p class=${cn('mb-2 pl-2 text-sm leading-snug font-medium', isDone ? 'decoration-muted-foreground/40 line-through' : '')}>\n          ${task.title}\n        </p>\n        ${task.tags.length > 0\n          ? html`<div class=\"mb-2 flex flex-wrap gap-1 pl-2\">\n              ${task.tags.map(\n                (tag) => html`<span\n                  data-slot=\"kanban-task-board\"\n                  class=${cn('rounded-md px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset', tag.color)}\n                  >${tag.label}</span\n                >`,\n              )}\n            </div>`\n          : nothing}\n        <div class=\"flex items-center gap-2 pl-2\">\n          ${task.dueDate\n            ? html`<span class=\"text-muted-foreground bg-muted inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs\">\n                ${icon(Calendar, 'calendar', 'size-3')} ${task.dueDate}\n              </span>`\n            : nothing}\n          ${task.comments.length > 0\n            ? html`<span class=\"text-muted-foreground/70 flex items-center gap-1 text-xs\">\n                ${icon(MessageSquare, 'message-square', 'size-3')} ${task.comments.length}\n              </span>`\n            : nothing}\n          <div class=\"ml-auto\">\n            <uip-avatar class=\"size-6 shrink-0\">\n              <uip-avatar-fallback class=${cn('text-xs font-bold', task.assignee.color)}>\n                ${initials(task.assignee.name)}\n              </uip-avatar-fallback>\n            </uip-avatar>\n          </div>\n        </div>\n      </div>\n    `\n  }\n\n  render() {\n    const filtered = this.filteredColumns\n    const detail = this.detailTaskId ? this.findTask(this.detailTaskId) : null\n    return html`\n      <div data-slot=\"kanban-task-board\" class=\"flex flex-col gap-3\">\n        <div class=\"mb-3 shrink-0\">\n          <div class=\"flex items-start justify-between gap-4\">\n            <div>\n              <h2 class=\"text-foreground text-xl font-bold tracking-tight\">${this.heading}</h2>\n              <p class=\"text-muted-foreground mt-0.5 text-sm\">${this.description}</p>\n            </div>\n            <div class=\"flex shrink-0 items-center gap-2\">\n              <uip-badge variant=\"secondary\" class=\"font-mono text-xs tabular-nums\">${this.totalTasks} tasks</uip-badge>\n              <uip-button\n                size=\"sm\"\n                @click=${() => {\n                  this.addColumnId = 'backlog'\n                  this.addOpen = true\n                }}\n              >\n                ${icon(Plus, 'plus', 'size-4')} Add Task\n              </uip-button>\n            </div>\n          </div>\n        </div>\n\n        <div data-slot=\"kanban-task-board\" class=\"mb-3 flex shrink-0 flex-wrap items-center gap-2\">\n          <uip-input\n            .value=${this.searchQuery}\n            placeholder=\"Search tasks...\"\n            class=\"h-8 w-56 text-sm\"\n            @input=${(e: Event) => {\n              this.searchQuery = (e.target as UipInput).value\n            }}\n          >\n            <span slot=\"prefix\">${icon(Search, 'search', 'size-3.5')}</span>\n          </uip-input>\n          <uip-select\n            value=${this.selectedPriority}\n            class=\"h-8 w-32 text-xs\"\n            @change=${(e: Event) => {\n              this.selectedPriority = (e.target as UipSelect).value as '' | BoardTask['priority']\n            }}\n          >\n            <option value=\"\">All priorities</option>\n            <option value=\"low\">Low</option>\n            <option value=\"medium\">Medium</option>\n            <option value=\"high\">High</option>\n            <option value=\"urgent\">Urgent</option>\n          </uip-select>\n          <uip-select\n            value=${this.selectedAssignee}\n            class=\"h-8 w-36 text-xs\"\n            @change=${(e: Event) => {\n              this.selectedAssignee = (e.target as UipSelect).value\n            }}\n          >\n            <option value=\"\">All assignees</option>\n            ${this.assigneeNames.map((n) => html`<option value=${n}>${n}</option>`)}\n          </uip-select>\n          <div class=\"bg-muted ml-auto flex items-center gap-0.5 rounded-md p-0.5\">\n            <uip-button\n              variant=\"ghost\"\n              size=\"icon\"\n              class=${cn('size-7 rounded-sm p-0', this.viewMode === 'board' ? 'bg-background shadow-sm' : '')}\n              title=\"Board view\"\n              aria-label=\"Board view\"\n              @click=${() => {\n                this.viewMode = 'board'\n              }}\n            >\n              ${icon(LayoutGrid, 'layout-grid', 'size-3.5')}\n            </uip-button>\n            <uip-button\n              variant=\"ghost\"\n              size=\"icon\"\n              class=${cn('size-7 rounded-sm p-0', this.viewMode === 'list' ? 'bg-background shadow-sm' : '')}\n              title=\"List view\"\n              aria-label=\"List view\"\n              @click=${() => {\n                this.viewMode = 'list'\n              }}\n            >\n              ${icon(List, 'list', 'size-3.5')}\n            </uip-button>\n          </div>\n        </div>\n\n        ${this.viewMode === 'board'\n          ? html`\n              <div class=\"kanban-board relative flex min-h-0 flex-1 items-start gap-3 overflow-auto pb-3\">\n                ${filtered.map((column) => {\n                  const isCollapsed = this.collapsed.includes(column.id)\n                  const isOver = this.dragOverColumn === column.id\n                  return html`\n                    <div\n                      class=${cn(\n                        'bg-muted/40 border-border/80 flex shrink-0 flex-col rounded-xl border p-3 transition-colors',\n                        isCollapsed ? 'w-14' : 'w-72',\n                        isOver && 'border-primary/50',\n                      )}\n                      @dragover=${(e: DragEvent) => this.onLaneDragOver(e, column.id, column.tasks.length)}\n                      @drop=${() => this.onDrop(column.id)}\n                    >\n                      <div class=\"mb-3 flex items-center justify-between gap-2 px-1\">\n                        <div class=\"flex min-w-0 items-center gap-2\">\n                          <span class=${cn('size-2 shrink-0 rounded-full', column.dotColor)}></span>\n                          ${!isCollapsed\n                            ? html`<h3 class=\"text-foreground truncate text-sm font-semibold tracking-tight\">\n                                  ${column.title}\n                                </h3>\n                                <span\n                                  class=\"bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums\"\n                                  >${column.tasks.length}</span\n                                >`\n                            : nothing}\n                        </div>\n                        <div class=\"flex items-center gap-1\">\n                          ${!isCollapsed\n                            ? html`<uip-button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                class=\"text-muted-foreground hover:text-foreground size-7 rounded-lg\"\n                                aria-label=${`Add task to ${column.title}`}\n                                @click=${() => {\n                                  this.addColumnId = column.id\n                                  this.addOpen = true\n                                }}\n                              >\n                                ${icon(Plus, 'plus', 'size-3.5')}\n                              </uip-button>`\n                            : nothing}\n                          <uip-button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            class=\"text-muted-foreground hover:text-foreground size-7 rounded-lg\"\n                            aria-label=${isCollapsed ? `Expand ${column.title}` : `Collapse ${column.title}`}\n                            @click=${() => {\n                              this.collapsed = isCollapsed\n                                ? this.collapsed.filter((c) => c !== column.id)\n                                : [...this.collapsed, column.id]\n                            }}\n                          >\n                            ${isCollapsed ? icon(ChevronsRight, 'chevrons-right', 'size-3.5') : icon(ChevronsLeft, 'chevrons-left', 'size-3.5')}\n                          </uip-button>\n                        </div>\n                      </div>\n                      ${!isCollapsed\n                        ? html`<div class=\"kanban-lane flex min-h-[60px] flex-1 flex-col gap-2.5\">\n                            ${column.tasks.map((task, ti) => {\n                              const isTarget = isOver && this.dropIndex === ti\n                              return html`\n                                ${isTarget && this.draggedId !== task.id\n                                  ? html`<div class=\"bg-primary/20 h-1.5 w-full rounded-full\"></div>`\n                                  : nothing}\n                                <div\n                                  @dragover=${(e: DragEvent) => this.onCardDragOver(e, column.id, ti)}\n                                >\n                                  ${this.renderCard(task, column.id === 'done')}\n                                </div>\n                              `\n                            })}\n                            ${isOver && this.dropIndex >= column.tasks.length && this.draggedId\n                              ? html`<div class=\"bg-primary/20 h-1.5 w-full rounded-full\"></div>`\n                              : nothing}\n                            ${column.tasks.length === 0\n                              ? html`<div\n                                  class=\"border-border/60 text-muted-foreground/60 flex flex-1 items-center justify-center rounded-lg border border-dashed py-8 text-center text-xs\"\n                                >\n                                  Drop tasks here\n                                </div>`\n                              : nothing}\n                          </div>`\n                        : nothing}\n                    </div>\n                  `\n                })}\n              </div>\n            `\n          : html`\n              <div class=\"kanban-list flex min-h-0 flex-1 flex-col gap-2 overflow-auto pb-3\">\n                ${filtered.map(\n                  (column) => html`\n                    <div>\n                      <p class=\"text-muted-foreground px-1 py-1 text-xs font-semibold tracking-widest uppercase\">\n                        ${column.title} · ${column.tasks.length}\n                      </p>\n                      ${column.tasks.map(\n                        (task) => html`\n                          <button\n                            class=\"bg-card hover:bg-muted/40 flex w-full items-center gap-3 rounded-lg border px-3 py-2.5 text-left transition-colors\"\n                            @click=${() => this.openDetail(task)}\n                          >\n                            <span class=${cn('size-2 shrink-0 rounded-full', priorityAccent(task.priority))}></span>\n                            <span class=\"text-muted-foreground/70 shrink-0 font-mono text-xs\">${task.id}</span>\n                            <span class=\"min-w-0 flex-1 truncate text-sm font-medium\">${task.title}</span>\n                            ${task.dueDate\n                              ? html`<span class=\"text-muted-foreground hidden shrink-0 items-center gap-1 text-xs sm:inline-flex\">\n                                  ${icon(Calendar, 'calendar', 'size-3')} ${task.dueDate}\n                                </span>`\n                              : nothing}\n                            <uip-avatar class=\"size-6 shrink-0\">\n                              <uip-avatar-fallback class=${cn('text-xs font-bold', task.assignee.color)}>\n                                ${initials(task.assignee.name)}\n                              </uip-avatar-fallback>\n                            </uip-avatar>\n                            <uip-select\n                              value=${column.id}\n                              class=\"h-7 w-28 text-xs\"\n                              @click=${(e: Event) => e.stopPropagation()}\n                              @change=${(e: Event) => {\n                                this.moveTask(task.id, (e.target as UipSelect).value)\n                              }}\n                            >\n                              ${this.columns.map((c) => html`<option value=${c.id}>${c.title}</option>`)}\n                            </uip-select>\n                          </button>\n                        `,\n                      )}\n                    </div>\n                  `,\n                )}\n              </div>\n            `}\n\n        <uip-sheet\n          ?open=${this.detailOpen}\n          heading=${detail?.task.title ?? ''}\n          description=${detail ? `Task ${detail.task.id} · ${detail.col.title}` : ''}\n          @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n            this.detailOpen = e.detail.open\n          }}\n        >\n          ${detail\n            ? html`\n                <div class=\"space-y-5 py-5 text-sm\">\n                  ${detail.task.description\n                    ? html`<p class=\"text-muted-foreground text-sm\">${detail.task.description}</p>`\n                    : nothing}\n                  <div class=\"flex flex-wrap items-center gap-2\">\n                    <uip-badge variant=\"secondary\">${PRIORITY_LABEL[detail.task.priority]} priority</uip-badge>\n                    ${detail.task.dueDate\n                      ? html`<uip-badge variant=\"outline\">\n                          ${icon(Calendar, 'calendar', 'size-3')} Due ${detail.task.dueDate}\n                        </uip-badge>`\n                      : nothing}\n                    ${detail.task.tags.map(\n                      (tag) => html`<span\n                        class=${cn('rounded-md px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset', tag.color)}\n                        >${tag.label}</span\n                      >`,\n                    )}\n                  </div>\n                  <div class=\"flex items-center gap-2.5\">\n                    <uip-avatar class=\"size-7\">\n                      <uip-avatar-fallback class=${cn('text-xs font-bold', detail.task.assignee.color)}>\n                        ${initials(detail.task.assignee.name)}\n                      </uip-avatar-fallback>\n                    </uip-avatar>\n                    <span class=\"text-sm font-medium\">${detail.task.assignee.name}</span>\n                  </div>\n                  <div class=\"space-y-2\">\n                    <p class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">\n                      Comments (${detail.task.comments.length})\n                    </p>\n                    ${detail.task.comments.map(\n                      (c) => html`\n                        <div class=\"bg-muted/40 rounded-lg border px-3 py-2\">\n                          <p class=\"text-xs font-semibold\">${c.author} <span class=\"text-muted-foreground font-normal\">· ${c.time}</span></p>\n                          <p class=\"mt-0.5 text-sm\">${c.text}</p>\n                        </div>\n                      `,\n                    )}\n                    <form\n                      class=\"flex gap-2\"\n                      @submit=${(e: Event) => {\n                        e.preventDefault()\n                        this.addComment()\n                      }}\n                    >\n                      <uip-input\n                        .value=${this.commentDraft}\n                        placeholder=\"Add a comment...\"\n                        class=\"flex-1 text-sm\"\n                        @input=${(e: Event) => {\n                          this.commentDraft = (e.target as UipInput).value\n                        }}\n                      ></uip-input>\n                      <uip-button type=\"submit\" size=\"sm\" ?disabled=${!this.commentDraft.trim()}>Post</uip-button>\n                    </form>\n                  </div>\n                  <div class=\"flex items-center gap-2\">\n                    <uip-label class=\"text-xs\">Move to</uip-label>\n                    <uip-select\n                      value=${detail.col.id}\n                      class=\"h-8 w-36 text-xs\"\n                      @change=${(e: Event) => {\n                        this.moveTask(detail.task.id, (e.target as UipSelect).value)\n                      }}\n                    >\n                      ${this.columns.map((c) => html`<option value=${c.id}>${c.title}</option>`)}\n                    </uip-select>\n                    <uip-button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      class=\"text-destructive ml-auto gap-1 text-xs\"\n                      @click=${() => {\n                        const id = detail.task.id\n                        this.commit((cols) => {\n                          for (const c of cols) c.tasks = c.tasks.filter((t) => t.id !== id)\n                        })\n                        this.detailOpen = false\n                      }}\n                    >\n                      ${icon(Trash2, 'trash-2', 'size-3.5')} Delete\n                    </uip-button>\n                  </div>\n                </div>\n              `\n            : nothing}\n        </uip-sheet>\n\n        <uip-dialog\n          ?open=${this.addOpen}\n          heading=\"Add task\"\n          description=\"Create a new card in the selected column.\"\n          @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n            this.addOpen = e.detail.open\n          }}\n        >\n          <form class=\"space-y-4 py-2\" @submit=${this.createTask}>\n            <div class=\"space-y-1.5\">\n              <uip-label for=\"kanban-task-title\">Title</uip-label>\n              <uip-input\n                id=\"kanban-task-title\"\n                .value=${this.addTitle}\n                placeholder=\"e.g. Draft migration runbook\"\n                required\n                @input=${(e: Event) => {\n                  this.addTitle = (e.target as UipInput).value\n                }}\n              ></uip-input>\n            </div>\n            <div class=\"space-y-1.5\">\n              <uip-label for=\"kanban-task-desc\">Description</uip-label>\n              <uip-textarea\n                id=\"kanban-task-desc\"\n                .value=${this.addDescription}\n                placeholder=\"Optional context...\"\n                rows=\"2\"\n                @value-change=${(e: CustomEvent<{ value: string }>) => {\n                  this.addDescription = e.detail.value\n                }}\n              ></uip-textarea>\n            </div>\n            <div class=\"grid grid-cols-2 gap-3\">\n              <div class=\"space-y-1.5\">\n                <uip-label>Priority</uip-label>\n                <uip-select\n                  value=${this.addPriority}\n                  @change=${(e: Event) => {\n                    this.addPriority = (e.target as UipSelect).value as BoardTask['priority']\n                  }}\n                >\n                  <option value=\"low\">Low</option>\n                  <option value=\"medium\">Medium</option>\n                  <option value=\"high\">High</option>\n                  <option value=\"urgent\">Urgent</option>\n                </uip-select>\n              </div>\n              <div class=\"space-y-1.5\">\n                <uip-label>Column</uip-label>\n                <uip-select\n                  value=${this.addColumnId}\n                  @change=${(e: Event) => {\n                    this.addColumnId = (e.target as UipSelect).value\n                  }}\n                >\n                  ${this.columns.map((c) => html`<option value=${c.id}>${c.title}</option>`)}\n                </uip-select>\n              </div>\n            </div>\n            <div class=\"grid grid-cols-2 gap-3\">\n              <div class=\"space-y-1.5\">\n                <uip-label for=\"kanban-task-assignee\">Assignee</uip-label>\n                <uip-input\n                  id=\"kanban-task-assignee\"\n                  .value=${this.addAssignee}\n                  placeholder=\"Full name\"\n                  @input=${(e: Event) => {\n                    this.addAssignee = (e.target as UipInput).value\n                  }}\n                ></uip-input>\n              </div>\n              <div class=\"space-y-1.5\">\n                <uip-label for=\"kanban-task-due\">Due date</uip-label>\n                <uip-input\n                  id=\"kanban-task-due\"\n                  .value=${this.addDueDate}\n                  type=\"date\"\n                  @input=${(e: Event) => {\n                    this.addDueDate = (e.target as UipInput).value\n                  }}\n                ></uip-input>\n              </div>\n            </div>\n          </form>\n          <div slot=\"footer\">\n            <uip-button\n              variant=\"outline\"\n              @click=${() => {\n                this.addOpen = false\n              }}\n              >Cancel</uip-button\n            >\n            <uip-button\n              @click=${this.createTask}\n              ?disabled=${!this.addTitle.trim()}\n            >\n              ${icon(Plus, 'plus', 'size-3.5')} Create task\n            </uip-button>\n          </div>\n        </uip-dialog>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-kanban-task-board') || customElements.define('uip-kanban-task-board', UipKanbanTaskBoard)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-kanban-task-board': UipKanbanTaskBoard\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/kanban-task-board/kanban-task-board.ts"
    },
    {
      "path": "packages/elements-lit/src/blocks/kanban-task-board/simple-kanban.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { Calendar, Check, GripVertical, Plus } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/avatar/avatar'\nimport '../../components/button/button'\nimport '../../components/input/input'\n\nexport interface SimpleKanbanItem {\n  id: string\n  title: string\n  description?: string\n  date?: string\n  priority?: 'low' | 'medium' | 'high' | 'urgent'\n  tag?: string\n  tagColor?: string\n  assignee?: {\n    name: string\n    initials?: string\n    avatar?: string\n  }\n}\n\nexport interface SimpleKanbanColumn {\n  id: string\n  title: string\n  color?: string\n  dotColor?: string\n  items: SimpleKanbanItem[]\n}\n\nconst DEFAULT_DOT_COLORS: Record<string, string> = {\n  backlog: 'bg-muted-foreground/60',\n  todo: 'bg-info',\n  'in-progress': 'bg-warning',\n  review: 'bg-chart-1',\n  done: 'bg-success',\n}\n\nfunction priorityLabel(p: string): string {\n  return p.charAt(0).toUpperCase() + p.slice(1)\n}\n\nfunction priorityClass(p: string): string {\n  switch (p) {\n    case 'low':\n      return 'bg-muted text-muted-foreground'\n    case 'medium':\n      return 'bg-info/10 text-info'\n    case 'high':\n      return 'bg-warning/10 text-warning'\n    case 'urgent':\n      return 'bg-destructive/10 text-destructive font-medium'\n    default:\n      return 'bg-muted text-muted-foreground'\n  }\n}\n\n/**\n * <uip-simple-kanban> — the registry SimpleKanban block as a web component.\n *\n * Ships inside the kanban-task-board block dir (one block = one install\n * unit). Class strings are React's SimpleKanban verbatim. `columns` is a\n * settable property (defaults to the demo seed); `columns-change`\n * (columns), `card-click` ({ item, columnId }), `card-move` ({ item,\n * fromColumnId, toColumnId, newIndex }) and `add-item` ({ columnId, title })\n * CustomEvents replace the callbacks (bubbles, composed). Native HTML5\n * drag-and-drop is kept, including the drop indicator and the inline\n * quick-add row. `allow-add` hides the per-column add button.\n */\nexport class UipSimpleKanban extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    columns: { attribute: false },\n    allowAdd: { type: Boolean, attribute: 'allow-add' },\n    draggedId: { state: true },\n    dragOverColumnId: { state: true },\n    dropTargetIndex: { state: true },\n    addingColumnId: { state: true },\n    newTitle: { state: true },\n  }\n\n  columns: SimpleKanbanColumn[] = []\n  allowAdd = true\n  private draggedId: string | null = null\n  private dragOverColumnId: string | null = null\n  private dropTargetIndex = -1\n  private addingColumnId: string | null = null\n  private newTitle = ''\n  private lastDragTime = 0\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'simple-kanban')\n    if (this.columns.length === 0) this.columns = seedSimpleColumns()\n  }\n\n  private commit(mutator: (cols: SimpleKanbanColumn[]) => void) {\n    const next = this.columns.map((col) => ({ ...col, items: [...col.items] }))\n    mutator(next)\n    this.columns = next\n    this.dispatchEvent(new CustomEvent('columns-change', { detail: next, bubbles: true, composed: true }))\n  }\n\n  private handleDragStart(e: DragEvent, id: string) {\n    this.draggedId = id\n    if (e.dataTransfer) {\n      e.dataTransfer.effectAllowed = 'move'\n      e.dataTransfer.setData('text/plain', id)\n    }\n  }\n\n  private handleDragEnd() {\n    this.draggedId = null\n    this.dragOverColumnId = null\n    this.dropTargetIndex = -1\n    this.lastDragTime = Date.now()\n  }\n\n  private handleDragOverColumn(e: DragEvent, columnId: string, itemsCount: number) {\n    e.preventDefault()\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n    this.dragOverColumnId = columnId\n    this.dropTargetIndex = itemsCount\n  }\n\n  private handleCardDragOver(e: DragEvent, columnId: string, index: number) {\n    e.preventDefault()\n    e.stopPropagation()\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n    this.dragOverColumnId = columnId\n    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()\n    this.dropTargetIndex = e.clientY < rect.top + rect.height / 2 ? index : index + 1\n  }\n\n  private locate(id: string): { sourceColIdx: number; itemIdx: number; targetColIdx?: number } {\n    let sourceColIdx = -1\n    let itemIdx = -1\n    for (let c = 0; c < this.columns.length; c++) {\n      const idx = this.columns[c]!.items.findIndex((item) => item.id === id)\n      if (idx !== -1) {\n        sourceColIdx = c\n        itemIdx = idx\n        break\n      }\n    }\n    return { sourceColIdx, itemIdx }\n  }\n\n  private handleDrop(e: DragEvent, targetColumnId: string) {\n    e.preventDefault()\n    if (!this.draggedId) {\n      this.handleDragEnd()\n      return\n    }\n    const { sourceColIdx, itemIdx } = this.locate(this.draggedId)\n    const targetColIdx = this.columns.findIndex((c) => c.id === targetColumnId)\n    if (sourceColIdx === -1 || targetColIdx === -1) {\n      this.handleDragEnd()\n      return\n    }\n    const sourceCol = this.columns[sourceColIdx]!\n    const item = sourceCol.items[itemIdx]\n    if (!item) {\n      this.handleDragEnd()\n      return\n    }\n    let at = this.dropTargetIndex\n    const removed = item\n    this.commit((cols) => {\n      const src = cols[sourceColIdx]!\n      const tgt = cols[targetColIdx]!\n      src.items.splice(itemIdx, 1)\n      if (at < 0) at = tgt.items.length\n      if (sourceColIdx === targetColIdx && itemIdx < at) at--\n      tgt.items.splice(at, 0, removed)\n    })\n    this.dispatchEvent(\n      new CustomEvent('card-move', {\n        detail: { item: removed, fromColumnId: sourceCol.id, toColumnId: targetColumnId, newIndex: at },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n    this.handleDragEnd()\n  }\n\n  private handleInlineAdd(columnId: string) {\n    if (!this.newTitle.trim()) {\n      this.addingColumnId = null\n      return\n    }\n    const title = this.newTitle.trim()\n    this.dispatchEvent(new CustomEvent('add-item', { detail: { columnId, title }, bubbles: true, composed: true }))\n    const newItem: SimpleKanbanItem = { id: `item-${Date.now()}`, title }\n    this.commit((cols) => {\n      cols.find((c) => c.id === columnId)?.items.push(newItem)\n    })\n    this.newTitle = ''\n    this.addingColumnId = null\n  }\n\n  render() {\n    return html`\n      <div\n        data-slot=\"simple-kanban\"\n        class=${cn('flex min-h-[380px] w-full [scrollbar-width:thin] items-start gap-4 overflow-x-auto pb-4')}\n      >\n        ${this.columns.map((column) => {\n          const isColumnOver = this.dragOverColumnId === column.id\n          const dot = column.dotColor || column.color || DEFAULT_DOT_COLORS[column.id] || 'bg-primary'\n          return html`\n            <div\n              data-slot=\"kanban-column\"\n              class=${cn(\n                'bg-muted/40 border-border/80 flex w-72 shrink-0 flex-col rounded-xl border p-3 transition-colors',\n                isColumnOver && 'border-primary/50 bg-muted/60 ring-primary/10 ring-2',\n              )}\n              @dragover=${(e: DragEvent) => this.handleDragOverColumn(e, column.id, column.items.length)}\n              @drop=${(e: DragEvent) => this.handleDrop(e, column.id)}\n            >\n              <div class=\"mb-3 flex items-center justify-between gap-2 px-1\">\n                <div class=\"flex items-center gap-2\">\n                  <span class=${cn('size-2 rounded-full', dot)}></span>\n                  <h3 class=\"text-foreground text-sm font-semibold tracking-tight\">${column.title}</h3>\n                  <span\n                    class=\"bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums\"\n                    >${column.items.length}</span\n                  >\n                </div>\n                ${this.allowAdd\n                  ? html`<uip-button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      class=\"text-muted-foreground hover:text-foreground size-7 rounded-lg\"\n                      aria-label=${`Add item to ${column.title}`}\n                      @click=${() => {\n                        this.addingColumnId = column.id\n                        this.newTitle = ''\n                      }}\n                    >\n                      ${icon(Plus, 'plus', 'size-3.5')}\n                    </uip-button>`\n                  : ''}\n              </div>\n\n              <div class=\"flex min-h-[120px] flex-1 flex-col gap-2.5\">\n                ${this.addingColumnId === column.id\n                  ? html`\n                      <div\n                        class=\"bg-card border-border animate-in fade-in zoom-in-95 rounded-lg border p-2.5 shadow-xs duration-150\"\n                      >\n                        <input\n                          type=\"text\"\n                          autofocus\n                          placeholder=\"Item title...\"\n                          .value=${this.newTitle}\n                          @input=${(e: Event) => {\n                            this.newTitle = (e.target as HTMLInputElement).value\n                          }}\n                          @keydown=${(e: KeyboardEvent) => {\n                            if (e.key === 'Enter') this.handleInlineAdd(column.id)\n                            if (e.key === 'Escape') this.addingColumnId = null\n                          }}\n                          class=\"placeholder:text-muted-foreground/60 w-full bg-transparent text-sm font-medium outline-none\"\n                        />\n                        <div class=\"mt-2.5 flex items-center justify-end gap-1.5\">\n                          <uip-button\n                            size=\"sm\"\n                            variant=\"ghost\"\n                            class=\"h-7 px-2 text-xs\"\n                            @click=${() => {\n                              this.addingColumnId = null\n                            }}\n                            >Cancel</uip-button\n                          >\n                          <uip-button size=\"sm\" class=\"h-7 gap-1 px-2.5 text-xs\" @click=${() => this.handleInlineAdd(column.id)}>\n                            ${icon(Check, 'check', 'size-3')} Add\n                          </uip-button>\n                        </div>\n                      </div>\n                    `\n                  : ''}\n                ${column.items.map((item, index) => {\n                  const isDragging = this.draggedId === item.id\n                  const isDropTarget = isColumnOver && this.dropTargetIndex === index\n                  return html`\n                    ${isDropTarget && !isDragging\n                      ? html`<div class=\"bg-primary/20 h-1.5 w-full rounded-full\"></div>`\n                      : ''}\n                    <div\n                      role=\"button\"\n                      tabindex=\"0\"\n                      draggable=\"true\"\n                      data-slot=\"kanban-card\"\n                      @dragstart=${(e: DragEvent) => this.handleDragStart(e, item.id)}\n                      @dragend=${this.handleDragEnd}\n                      @dragover=${(e: DragEvent) => this.handleCardDragOver(e, column.id, index)}\n                      @click=${() => {\n                        if (Date.now() - this.lastDragTime < 150) return\n                        this.dispatchEvent(\n                          new CustomEvent('card-click', { detail: { item, columnId: column.id }, bubbles: true, composed: true }),\n                        )\n                      }}\n                      @keydown=${(e: KeyboardEvent) => {\n                        if (e.key === 'Enter' || e.key === ' ') {\n                          e.preventDefault()\n                          this.dispatchEvent(\n                            new CustomEvent('card-click', { detail: { item, columnId: column.id }, bubbles: true, composed: true }),\n                          )\n                        }\n                      }}\n                      class=${cn(\n                        'group bg-card text-card-foreground border-border/80 hover:border-border relative flex cursor-grab flex-col gap-2 rounded-lg border p-3 shadow-xs transition-colors hover:shadow-sm active:cursor-grabbing',\n                        isDragging && 'ring-primary/40 opacity-40 shadow-md ring-2',\n                      )}\n                    >\n                      <div class=\"flex items-start justify-between gap-2\">\n                        <p class=\"text-foreground line-clamp-2 text-sm leading-snug font-medium\">${item.title}</p>\n                        ${icon(GripVertical, 'grip-vertical', 'text-muted-foreground/30 size-3.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100')}\n                      </div>\n                      ${item.description\n                        ? html`<p class=\"text-muted-foreground line-clamp-2 text-xs leading-relaxed\">\n                            ${item.description}\n                          </p>`\n                        : ''}\n                      ${item.date || item.tag || item.priority || item.assignee\n                        ? html`\n                            <div\n                              class=\"border-border/40 mt-1 flex flex-wrap items-center justify-between gap-1.5 border-t pt-1 text-xs\"\n                            >\n                              <div class=\"flex flex-wrap items-center gap-1.5\">\n                                ${item.date\n                                  ? html`<div class=\"text-muted-foreground flex items-center gap-1 text-xs\">\n                                      ${icon(Calendar, 'calendar', 'text-muted-foreground/70 size-3')}\n                                      <span>${item.date}</span>\n                                    </div>`\n                                  : ''}\n                                ${item.tag\n                                  ? html`<span\n                                      class=${cn(\n                                        'rounded-md px-1.5 py-0.5 text-xs font-medium',\n                                        item.tagColor || 'bg-secondary text-secondary-foreground',\n                                      )}\n                                      >${item.tag}</span\n                                    >`\n                                  : ''}\n                                ${item.priority\n                                  ? html`<span\n                                      class=${cn('rounded-md px-1.5 py-0.5 text-xs', priorityClass(item.priority))}\n                                      >${priorityLabel(item.priority)}</span\n                                    >`\n                                  : ''}\n                              </div>\n                              ${item.assignee\n                                ? html`<uip-avatar class=\"border-background size-5 shrink-0 border\">\n                                    <uip-avatar-fallback class=\"text-xs font-medium\">\n                                      ${item.assignee.initials || item.assignee.name.slice(0, 2).toUpperCase()}\n                                    </uip-avatar-fallback>\n                                  </uip-avatar>`\n                                : ''}\n                            </div>\n                          `\n                        : ''}\n                    </div>\n                  `\n                })}\n                ${isColumnOver && this.dropTargetIndex >= column.items.length && this.draggedId\n                  ? html`<div class=\"bg-primary/20 h-1.5 w-full rounded-full\"></div>`\n                  : ''}\n                ${column.items.length === 0 && this.addingColumnId !== column.id\n                  ? html`<div\n                      class=\"border-border/60 text-muted-foreground/60 flex flex-1 items-center justify-center rounded-lg border border-dashed py-8 text-center text-xs\"\n                    >\n                      Drop items here\n                    </div>`\n                  : ''}\n              </div>\n            </div>\n          `\n        })}\n      </div>\n    `\n  }\n}\n\nfunction seedSimpleColumns(): SimpleKanbanColumn[] {\n  return [\n    {\n      id: 'backlog',\n      title: 'Backlog',\n      dotColor: 'bg-slate-400 dark:bg-slate-500',\n      items: [\n        { id: 'sk-1', title: 'Cross-platform auth synchronization', description: 'Implement token refresh rotation across mobile and web clients.', date: 'May 15 - Jun 2, 2026', priority: 'high', tag: 'Auth', assignee: { name: 'Alice Chen', initials: 'AC' } },\n        { id: 'sk-2', title: 'Scale multi-region edge caching', description: 'Benchmark Cloudflare worker cache hit ratios for static registry files.', date: 'Jun 22 - Jul 10, 2026', priority: 'medium', tag: 'Infra', assignee: { name: 'Dan Ruiz', initials: 'DR' } },\n        { id: 'sk-3', title: 'Extensible drag & drop plugin architecture', date: 'Jul 28 - Aug 14, 2026', priority: 'low', tag: 'Core' },\n      ],\n    },\n    {\n      id: 'in-progress',\n      title: 'In Progress',\n      dotColor: 'bg-amber-500',\n      items: [\n        { id: 'sk-4', title: 'Real-time telemetry event streaming', description: 'Wire SSE endpoints for instant live preview updates.', date: 'May 16 - Jun 9, 2026', priority: 'urgent', tag: 'Realtime', assignee: { name: 'Sarah Miller', initials: 'SM' } },\n        { id: 'sk-5', title: 'OKLCH theme token harmonizer', description: 'Calibrate contrast ratios across light and dark modes.', date: 'Apr 26 - May 20, 2026', priority: 'high', tag: 'Design', assignee: { name: 'Leo Vance', initials: 'LV' } },\n      ],\n    },\n    {\n      id: 'review',\n      title: 'In Review',\n      dotColor: 'bg-purple-500',\n      items: [\n        { id: 'sk-6', title: 'Astro island SSR hydration benchmark', description: 'Verify sub-50ms TTFB across dual-framework demo islands.', date: 'Aug 3 - Aug 18, 2026', priority: 'medium', tag: 'Performance', assignee: { name: 'Alice Chen', initials: 'AC' } },\n      ],\n    },\n    {\n      id: 'done',\n      title: 'Done',\n      dotColor: 'bg-emerald-500',\n      items: [\n        { id: 'sk-7', title: 'Sub-pixel border and shadow layering', description: 'Refined neutral borders matching Linear & Raycast standards.', date: 'Mar 10 - Apr 12, 2026', priority: 'low', tag: 'Craft', assignee: { name: 'Dan Ruiz', initials: 'DR' } },\n        { id: 'sk-8', title: 'Zero-config JSON registry endpoint schema', date: 'Feb 14 - Mar 2, 2026', priority: 'high', tag: 'Registry' },\n      ],\n    },\n  ]\n}\n\ncustomElements.get('uip-simple-kanban') || customElements.define('uip-simple-kanban', UipSimpleKanban)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-simple-kanban': UipSimpleKanban\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/kanban-task-board/simple-kanban.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/avatar.json",
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/dialog.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/label.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/select.json",
    "https://uipkge.dev/r/lit/sheet.json",
    "https://uipkge.dev/r/lit/textarea.json"
  ]
}
