General
Kanban Board code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/kanban-board.blade.php
@props([
'columns' => [],
])
@php
/*
| Kanban board with native drag-and-drop between columns (no dependencies).
| Cards can be dragged across columns; the board model updates reactively.
| Pass `columns` as:
| [
| ['key' => 'todo', 'title' => 'To do', 'accent' => 'gray', 'cards' => [
| ['title' => 'Draft spec', 'meta' => 'DZC-21', 'tag' => 'Design', 'tagVariant' => 'blue', 'assignee' => 'AM'],
| ]],
| ]
|
| Usage:
| <x-ui.kanban-board :columns="$columns" />
*/
$columnData = collect($columns)->map(function (array $col, int $ci): array {
return [
'key' => $col['key'] ?? 'col'.$ci,
'title' => $col['title'] ?? '',
'accent' => $col['accent'] ?? 'gray',
'cards' => collect($col['cards'] ?? [])->map(fn (array $card, int $i): array => [
'id' => $card['id'] ?? ('c'.$ci.'_'.$i),
'title' => $card['title'] ?? '',
'meta' => $card['meta'] ?? null,
'tag' => $card['tag'] ?? null,
'tagVariant' => $card['tagVariant'] ?? 'gray',
'assignee' => $card['assignee'] ?? null,
])->values()->all(),
];
})->values()->all();
$accentDot = [
'brand' => 'bg-brand-500',
'green' => 'bg-green-500',
'amber' => 'bg-amber-500',
'red' => 'bg-red-500',
'blue' => 'bg-accent-500',
'gray' => 'bg-gray-400',
];
@endphp
<div
x-data="{
board: @js($columnData),
accentDot: @js($accentDot),
dragId: null,
dragOver: null,
tagClass(variant) {
return ({
brand: 'bg-brand-50 text-brand-700 dark:bg-brand-950/60 dark:text-brand-300',
green: 'bg-green-50 text-green-700 dark:bg-green-950/60 dark:text-green-300',
amber: 'bg-amber-50 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300',
red: 'bg-red-50 text-red-700 dark:bg-red-950/60 dark:text-red-300',
blue: 'bg-accent-50 text-accent-700 dark:bg-accent-950/60 dark:text-accent-300',
gray: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300',
})[variant] || 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300';
},
drop(targetKey) {
if (! this.dragId) { return; }
let card = null;
this.board.forEach((col) => {
const idx = col.cards.findIndex((c) => c.id === this.dragId);
if (idx !== -1) { card = col.cards.splice(idx, 1)[0]; }
});
if (card) {
const target = this.board.find((col) => col.key === targetKey);
if (target) { target.cards.push(card); }
}
this.dragId = null;
this.dragOver = null;
},
}"
{{ $attributes->merge(['class' => 'flex gap-4 overflow-x-auto pb-2']) }}
>
<template x-for="column in board" :key="column.key">
<div
class="flex w-72 shrink-0 flex-col rounded-2xl border border-gray-200 bg-gray-50/70 dark:border-gray-800 dark:bg-gray-900/50"
:class="dragOver === column.key ? 'ring-2 ring-brand-400/60' : ''"
@dragover.prevent="dragOver = column.key"
@dragleave="dragOver === column.key && (dragOver = null)"
@drop="drop(column.key)"
>
<div class="flex items-center justify-between gap-2 px-4 py-3">
<div class="flex items-center gap-2">
<span class="h-2.5 w-2.5 rounded-full" :class="accentDot[column.accent] || 'bg-gray-400'"></span>
<span class="text-sm font-semibold text-gray-900 dark:text-white" x-text="column.title"></span>
</div>
<span class="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-gray-200 px-1.5 text-[0.6875rem] font-bold tabular-nums text-gray-600 dark:bg-gray-800 dark:text-gray-300" x-text="column.cards.length"></span>
</div>
<div class="flex min-h-24 flex-1 flex-col gap-2 px-3 pb-3">
<template x-for="card in column.cards" :key="card.id">
<div
draggable="true"
@dragstart="dragId = card.id"
@dragend="dragId = null"
class="cursor-grab rounded-xl border border-gray-200 bg-white p-3 shadow-sm transition active:cursor-grabbing hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900 dark:hover:border-gray-700"
:class="dragId === card.id ? 'opacity-50' : ''"
>
<div class="flex items-start justify-between gap-2">
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="card.title"></p>
<span
x-show="card.tag"
x-cloak
class="shrink-0 rounded-full px-2 py-0.5 text-[0.6875rem] font-semibold"
:class="tagClass(card.tagVariant)"
x-text="card.tag"
></span>
</div>
<div class="mt-3 flex items-center justify-between gap-2">
<span x-show="card.meta" x-cloak class="font-mono text-[0.6875rem] uppercase tracking-wide text-gray-400 dark:text-gray-500" x-text="card.meta"></span>
<span
x-show="card.assignee"
x-cloak
class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-brand-100 text-[0.625rem] font-bold text-brand-700 dark:bg-brand-950/60 dark:text-brand-300"
x-text="card.assignee"
></span>
</div>
</div>
</template>
<p x-show="column.cards.length === 0" x-cloak class="rounded-xl border border-dashed border-gray-200 py-6 text-center text-xs text-gray-400 dark:border-gray-800 dark:text-gray-500">
Drop cards here
</p>
</div>
</div>
</template>
</div>