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>