kanban
Kanban board with dynamic overlay
Kanban board with dynamic overlay
Загрузка превью…
src/kanban/c-kanban-2.vue
<!--
IconPlaceholder (иконка "grip-vertical" у ручки колонки) заменена
инлайновым `<svg>`, тем же приёмом, что и в кейсе `kanban` (Фаза 1).
Сетевые unsplash-аватары заменены статичным 1x1 data-URI (PIXEL).
Доска в покое с фиксированным порядком карточек (docs/PORTING.md, kanban —
дифу снимает только статику, drag-and-drop не проверяется). Оверлей
(`KanbanOverlay` со скоуп-слотом `{ value, variant }`) не рендерится в
покое (`activeId` пуст до начала перетаскивания, см.
`packages/ui/src/reui/kanban/KanbanOverlay.vue`) — реализован по
оригиналу для полноты логики, но дифом не проверяется.
-->
<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-outline"
if (priority === "medium") return "primary-outline"
return "warning-outline"
}
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",
},
],
})
function findTask(id: string): Task | undefined {
return Object.values(columns.value)
.flat()
.find((task) => task.id === id)
}
</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 p-0.5">
<KanbanItem v-for="task in tasks" :key="task.id" :value="task.id">
<KanbanItemHandle>
<Card>
<CardContent class="flex flex-col gap-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-[11px] 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 #default="{ value, variant }">
<template v-if="variant === 'column'">
<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[String(value)] }}</span>
<Badge variant="outline">{{ (columns[String(value)] ?? []).length }}</Badge>
</div>
</CardHeader>
<CardContent>
<div class="flex flex-col gap-2.5">
<Card v-for="task in columns[String(value)] ?? []" :key="task.id">
<CardContent class="flex flex-col gap-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-[11px] capitalize">
{{ task.priority }}
</Badge>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</template>
<template v-else-if="findTask(String(value))">
<Card>
<CardContent class="flex flex-col gap-2.5">
<div class="flex items-center justify-between gap-2">
<span class="line-clamp-1 text-sm font-medium">{{ findTask(String(value))!.title }}</span>
<Badge :variant="priorityVariant(findTask(String(value))!.priority)" class="pointer-events-none h-5 shrink-0 rounded-sm px-1.5 text-[11px] capitalize">
{{ findTask(String(value))!.priority }}
</Badge>
</div>
</CardContent>
</Card>
</template>
</KanbanOverlay>
</Kanban>
</div>
</template>