kanban

Kanban board with placeholder overlay

Kanban board with placeholder overlay

Загрузка превью…

src/kanban/c-kanban-1.vue

<!--
  IconPlaceholder (иконка "grip-vertical" у ручки колонки) заменена
  инлайновым `<svg>`, тем же приёмом, что и в кейсе `kanban` (Фаза 1).
  Сетевые unsplash-аватары заменены статичным 1x1 data-URI (PIXEL).

  Доска в покое с фиксированным порядком карточек (docs/PORTING.md, kanban —
  дифу снимает только статику, drag-and-drop не проверяется).
-->
<script setup lang="ts">
import { ref } from "vue"
import { Badge } from "@/components/reui/badge"
import { Kanban, KanbanBoard, KanbanColumn, KanbanColumnContent, KanbanColumnHandle, KanbanItem, KanbanItemHandle, KanbanOverlay } from "@/components/reui/kanban"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"

const PIXEL =
  "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="

interface Task {
  id: string
  title: string
  priority: "low" | "medium" | "high"
  description?: string
  assignee?: string
  assigneeAvatar?: string
  dueDate?: string
}

const columnTitles: Record<string, string> = {
  backlog: "Backlog",
  inProgress: "In Progress",
  review: "Review",
  done: "Done",
}

function priorityVariant(priority: Task["priority"]) {
  if (priority === "high") return "destructive-light"
  if (priority === "medium") return "primary-light"
  return "warning-light"
}

const columns = ref<Record<string, Task[]>>({
  backlog: [
    {
      id: "1",
      title: "Add authentication",
      priority: "high",
      assignee: "Alex Johnson",
      assigneeAvatar: PIXEL,
      dueDate: "Jan 10, 2025",
    },
    {
      id: "2",
      title: "Create API endpoints",
      priority: "medium",
      assignee: "Sarah Chen",
      assigneeAvatar: PIXEL,
      dueDate: "Jan 15, 2025",
    },
    {
      id: "3",
      title: "Write documentation",
      priority: "low",
      assignee: "Michael Rodriguez",
      assigneeAvatar: PIXEL,
      dueDate: "Jan 20, 2025",
    },
  ],
  inProgress: [
    {
      id: "4",
      title: "Design system updates",
      priority: "high",
      assignee: "Emma Wilson",
      assigneeAvatar: PIXEL,
      dueDate: "Aug 25, 2025",
    },
    {
      id: "5",
      title: "Implement dark mode",
      priority: "medium",
      assignee: "David Kim",
      assigneeAvatar: PIXEL,
      dueDate: "Aug 25, 2025",
    },
  ],
  done: [
    {
      id: "7",
      title: "Setup project",
      priority: "high",
      assignee: "Aron Thompson",
      assigneeAvatar: PIXEL,
      dueDate: "Sep 25, 2025",
    },
    {
      id: "8",
      title: "Initial commit",
      priority: "low",
      assignee: "James Brown",
      assigneeAvatar: PIXEL,
      dueDate: "Sep 20, 2025",
    },
  ],
})
</script>

<template>
  <div>
    <Kanban v-model:value="columns" :get-item-value="(item: Task) => item.id">
      <KanbanBoard class="grid auto-rows-fr grid-cols-3">
        <KanbanColumn v-for="(tasks, columnValue) in columns" :key="columnValue" :value="columnValue">
          <Card class="mb-2.5">
            <CardHeader class="flex items-center justify-between">
              <div class="flex items-center gap-2.5">
                <span class="text-sm font-semibold">{{ columnTitles[columnValue] }}</span>
                <Badge variant="outline">{{ tasks.length }}</Badge>
              </div>
              <KanbanColumnHandle as-child>
                <Button size="icon-xs" variant="ghost">
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="24"
                    height="24"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    aria-hidden="true"
                  ><circle cx="9" cy="5" r="1" /><circle cx="9" cy="12" r="1" /><circle cx="9" cy="19" r="1" /><circle cx="15" cy="5" r="1" /><circle cx="15" cy="12" r="1" /><circle cx="15" cy="19" r="1" /></svg>
                </Button>
              </KanbanColumnHandle>
            </CardHeader>
            <CardContent>
              <KanbanColumnContent :value="columnValue" class="flex flex-col gap-2.5">
                <KanbanItem v-for="task in tasks" :key="task.id" :value="task.id">
                  <KanbanItemHandle>
                    <Card>
                      <CardContent class="space-y-2.5">
                        <div class="flex items-center justify-between gap-2">
                          <span class="line-clamp-1 text-sm font-medium">{{ task.title }}</span>
                          <Badge :variant="priorityVariant(task.priority)" class="pointer-events-none h-5 shrink-0 rounded-sm px-1.5 text-xs capitalize">
                            {{ task.priority }}
                          </Badge>
                        </div>
                        <div class="text-muted-foreground flex items-center justify-between text-xs">
                          <div v-if="task.assignee" class="flex items-center gap-1">
                            <Avatar class="size-4">
                              <AvatarImage :src="task.assigneeAvatar!" />
                              <AvatarFallback>{{ task.assignee.charAt(0) }}</AvatarFallback>
                            </Avatar>
                            <span class="line-clamp-1">{{ task.assignee }}</span>
                          </div>
                          <time v-if="task.dueDate" class="text-[10px] whitespace-nowrap tabular-nums">{{ task.dueDate }}</time>
                        </div>
                      </CardContent>
                    </Card>
                  </KanbanItemHandle>
                </KanbanItem>
              </KanbanColumnContent>
            </CardContent>
          </Card>
        </KanbanColumn>
      </KanbanBoard>
      <KanbanOverlay class="bg-muted/10 rounded-md border-2 border-dashed" />
    </Kanban>
  </div>
</template>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-kanban-1.json

Зависимости реестра

Источник: порт из ReUI (Keenthemes, MIT)