K
KoreUI

KoreKanban

Kanban

Clase abstracta Livewire para un tablero Kanban con estado y persistencia. Se extiende como KoreDataTable: implementas columns(), cards() y persistMove(), y el board se encarga del arrastre, el layout y la reconciliación.

Esta es la forma con estado. Si el board no necesita vivir en su propia página —por ejemplo, dentro de un modal o de otro componente Livewire ya existente—, monta el componente anónimo directamente: ve a Kanban — Primeros pasos.

Uso

Extiende KoreKanban, define tres métodos y ya tienes un board completo.

app/Livewire/TasksBoard.php
<?php

namespace App\Livewire;

use App\Models\Task;
use KoreUi\Kanban\KoreKanban;

class TasksBoard extends KoreKanban
{
    public function columns(): array
    {
        return [
            ['id' => 'todo', 'label' => 'Por hacer'],
            ['id' => 'doing', 'label' => 'En curso', 'color' => 'warning'],
            ['id' => 'done', 'label' => 'Hecho', 'color' => 'success'],
        ];
    }

    public function cards(): array
    {
        return Task::orderBy('position')->get()
            ->map(fn ($t) => [
                'id' => $t->id,
                'column' => $t->status,
                'title' => $t->title,
                'description' => $t->description,
            ])
            ->all();
    }

    protected function persistMove(string|int $cardId, string|int $toColumn, int $position): void
    {
        Task::whereKey($cardId)->update([
            'status' => $toColumn,
            'position' => $position,
        ]);
    }
}
<?php

namespace App\Livewire;

use App\Models\Task;
use KoreUi\Kanban\KoreKanban;

class TasksBoard extends KoreKanban
{
    public function columns(): array
    {
        return [
            ['id' => 'todo', 'label' => 'Por hacer'],
            ['id' => 'doing', 'label' => 'En curso', 'color' => 'warning'],
            ['id' => 'done', 'label' => 'Hecho', 'color' => 'success'],
        ];
    }

    public function cards(): array
    {
        return Task::orderBy('position')->get()
            ->map(fn ($t) => [
                'id' => $t->id,
                'column' => $t->status,
                'title' => $t->title,
                'description' => $t->description,
            ])
            ->all();
    }

    protected function persistMove(string|int $cardId, string|int $toColumn, int $position): void
    {
        Task::whereKey($cardId)->update([
            'status' => $toColumn,
            'position' => $position,
        ]);
    }
}
resources/views/tasks/index.blade.php
<livewire:tasks-board />
<livewire:tasks-board />

Métodos a implementar

Prop Tipo Default Descripción
columns() array Columnas del board: [[id, label, color?], ...] (obligatorio)
cards() array Tarjetas: [[id, column, title, ...], ...] (obligatorio)
persistMove() void Persiste una tarjeta soltada: columna y posición destino (protected, obligatorio)

El orden de los parámetros cambia. El handler público que llama el drag-and-drop es moveCard($cardId, $position, $toColumn), pero el método que tú implementas es persistMove($cardId, $toColumn, $position) —columna y posición van al revés—. La clase base hace la traducción por ti; solo importa si alguna vez sobreescribes moveCard() directamente.

Cómo funciona

Tres piezas ya resueltas: el render, el drop y el refresco.

  • render() pinta la vista kore::kanban.index, que por dentro reutiliza el componente anónimo kore::kanban con las columnas y tarjetas que le devuelven tus métodos. No necesitas escribir ningún Blade.
  • En cada drop, el board invoca moveCard($cardId, $position, $toColumn) —ya implementado en la clase base—, que delega en tu persistMove().
  • Tras persistir, Livewire re-renderiza y cards() vuelve a leer el estado actualizado, así que el orden final que ve el usuario es siempre el que devuelve tu fuente de datos, no el DOM que quedó tras arrastrar.

Personalizar la tarjeta

Dos caminos: publicar la vista del card, o montar tú mismo el componente anónimo.

La clase base no expone una prop de slot por tarjeta —no hay dónde inyectar Blade a mitad del render() heredado—. Si necesitas contenido por tarjeta distinto del title/description/badge de serie, tienes dos opciones: publicar y sobrescribir la vista kore::components.kanban.card para todo el proyecto, o dejar de extender KoreKanban y montar kore::kanban tú mismo desde un componente Livewire normal, donde sí controlas el Blade y puedes usar el slot de kore::kanban.card por tarjeta —es exactamente lo que hace la demo de esta página—.

Demo de referencia

Visualmente es el mismo board: seis tareas, cuatro columnas, arrastre libre.

Esta demo en particular usa el componente anónimo directamente (para no acoplar la documentación al render() interno de la clase base), pero el resultado interactivo —arrastre dentro y entre columnas, wire:key por tarjeta, reconciliación tras el drop— es idéntico al de un board que extienda KoreKanban.

Backlog
2

Investigar competidores

Analizar 3 productos del mercado

Research

Definir métricas

KPIs del trimestre

Por hacer
2

Diseñar el esquema de datos

Backend

Wireframes del panel

En curso
1

Migrar autenticación

A Livewire 4

En curso
Hecho
1

Configurar CI

Listo

Configuración y notas

Personaliza los defaults en config/kore-ui.php, sección kanban.

config/kore-ui.php
'kanban' => [
    'group' => 'kanban',       // grupo x-sort compartido entre columnas
    'animation' => 150,         // ms de la animación de arrastre
    'column_width' => '18rem',  // ancho de cada columna
],
'kanban' => [
    'group' => 'kanban',       // grupo x-sort compartido entre columnas
    'animation' => 150,         // ms de la animación de arrastre
    'column_width' => '18rem',  // ancho de cada columna
],
  • El arrastre entre columnas usa un grupo x-sort compartido; si montas dos boards en la misma página con el mismo group, sus tarjetas se pueden mezclar entre sí.
  • El ancho de columna es global vía column_width —el componente anónimo no expone una prop para cambiarlo por instancia—.
  • Cuida las colisiones de posición bajo concurrencia. Si position tiene un índice único en base de datos, dos usuarios arrastrando a la vez pueden chocar: aplica la reordenación dentro de una transacción en tu persistMove().
Cargando