> ## Documentation Index
> Fetch the complete documentation index at: https://woltz.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# DataKanbanCriteria

> A Kanban board component with drag and drop, virtualization, and Criteria integration.

<a href="https://react-rich-domain.netlify.app/#kanban" target="_blank">Live Demo</a>

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npx shadcn add "https://tarcisioandrade.github.io/rich-domain/packages/react-rich-domain/public/r/data-kanban-criteria.json"
  ```

  ```bash pnpm theme={null}
  pnpm dlx shadcn add "https://tarcisioandrade.github.io/rich-domain/packages/react-rich-domain/public/r/data-kanban-criteria.json"
  ```

  ```bash yarn theme={null}
  yarn dlx shadcn add "https://tarcisioandrade.github.io/rich-domain/packages/react-rich-domain/public/r/data-kanban-criteria.json"
  ```
</CodeGroup>

#### Features

* Drag and drop between columns with @dnd-kit
* Virtualized lists for large datasets with @tanstack/react-virtual
* Infinite scroll per column
* Horizontal scroll for unlimited columns
* Integrated filtering and search
* Optimistic updates with automatic rollback on error
* Fractional indexing for scalable ordering
* Customizable card, column, and empty state rendering
* Smart cursor styles (grab/grabbing/pointer)
* Prevent drag on specific elements with `data-no-drag`
* URL sync for filters and search

#### Basic Usage

```tsx theme={null}
import { useCriteriaKanban } from "@/hooks/use-criteria-kanban";
import {
  DataKanbanCriteria,
  KanbanCard,
  KanbanCardHeader,
  KanbanCardTitle,
  KanbanCardDescription,
  KanbanCardContent,
  KanbanCardFooter,
  KanbanCardBadge,
} from "@/components/data-kanban-criteria";
import type { KanbanColumnDefinition } from "@/types/use-criteria-kanban.type";
import type { QueryFilter } from "@/lib/filter-utils";

interface Task {
  id: string;
  title: string;
  description: string;
  status: "todo" | "doing" | "done";
  priority: "low" | "medium" | "high";
  order: string; // Fractional index for ordering
}

// Define columns with criteria filters
const columns: KanbanColumnDefinition<Task>[] = [
  {
    id: "todo",
    title: "To Do",
    criteria: (c) => c.where("status", "equals", "todo"),
    value: "todo",
  },
  {
    id: "doing",
    title: "In Progress",
    criteria: (c) => c.where("status", "equals", "doing"),
    value: "doing",
  },
  {
    id: "done",
    title: "Done",
    criteria: (c) => c.where("status", "equals", "done"),
    value: "done",
  },
];

const filterFields: QueryFilter[] = [
  {
    field: "priority",
    fieldLabel: "Priority",
    type: "string",
    multiSelect: true,
    options: [
      { label: "High", value: "high" },
      { label: "Medium", value: "medium" },
      { label: "Low", value: "low" },
    ],
  },
];

function TaskKanban() {
  const kanban = useCriteriaKanban<Task>(["tasks", "kanban"], getTasks, {
    columns,
    getItemId: (task) => task.id,
    groupField: "status",
    filterFields,
    columnPageSize: 20,
    syncWithUrl: true,
    onCardMove: async ({ cardId, toColumn, insertAfterId }) => {
      // Call your API to move the task
      await moveTask(cardId, toColumn.id, insertAfterId);
    },
    onMoveError: (error, { fromColumn }) => {
      toast.error(`Failed to move task. Reverted to ${fromColumn.title}`);
    },
  });

  return (
    <DataKanbanCriteria
      kanban={kanban}
      renderCard={(task, isDragging, isClickable) => (
        <KanbanCard id={task.id} isDragging={isDragging} isClickable={isClickable}>
          <KanbanCardHeader>
            <KanbanCardTitle>{task.title}</KanbanCardTitle>
          </KanbanCardHeader>
          <KanbanCardContent>
            <KanbanCardDescription>{task.description}</KanbanCardDescription>
          </KanbanCardContent>
          <KanbanCardFooter>
            <KanbanCardBadge>{task.priority}</KanbanCardBadge>
          </KanbanCardFooter>
        </KanbanCard>
      )}
      estimatedCardHeight={120}
    />
  );
}
```

#### Hook Options

```tsx theme={null}
interface UseCriteriaKanbanOptions<T> {
  // Required
  columns: KanbanColumnDefinition<T>[]; // Column definitions
  getItemId: (item: T) => string; // Get unique ID from item
  groupField: FieldPath<T>; // Field that determines column

  // Card move handling
  onCardMove?: (params: CardMoveParams<T>) => Promise<void>;
  onMoveError?: (error: Error, params: CardMoveParams<T>) => void;

  // Filter configuration
  filterFields?: QueryFilter[];

  // Search
  searchDebounceMs?: number; // Default: 300

  // Pagination
  columnPageSize?: number; // Default: 50

  // Drag and drop
  enableDragDrop?: boolean; // Default: true
  sensors?: SensorDescriptor<SensorOptions>[]; // Custom dnd-kit sensors

  // URL sync
  syncWithUrl?: boolean;
}
```

#### Column Definition

```tsx theme={null}
interface KanbanColumnDefinition<T> {
  id: string; // Unique identifier
  title: string; // Display title
  criteria: (base: Criteria<T>) => Criteria<T>; // Filter for this column
  value?: unknown; // Value to set on items when moved here
  color?: string; // Optional color theme
  icon?: React.ReactNode; // Optional icon
  limit?: number; // WIP limit
}
```

<Note>
  The `value` property is used when the column `id` differs from the actual value stored in your data.
  For example, if your column has `id: "in-progress"` but your database stores `status: "DOING"`,
  set `value: "DOING"`. When a card is moved to this column, the hook will use `value` for optimistic updates.
  If `value` is omitted, the column `id` is used instead.
</Note>

#### Card Move Parameters

When a card is moved, `onCardMove` receives:

```tsx theme={null}
interface CardMoveParams<T> {
  cardId: string; // ID of the moved card
  item: T; // The complete item data
  fromColumn: KanbanColumnDefinition<T>;
  toColumn: KanbanColumnDefinition<T>;
  toIndex: number; // New position index
  insertAfterId: string | null; // ID of item above, null if first
}
```

The `insertAfterId` pattern is designed for scalability:

* Frontend sends only the reference ID (not the order)
* Backend queries the real neighbors to calculate the correct order
* Works correctly even with filters applied

#### Backend Integration

Your backend should implement a move endpoint like:

```typescript theme={null}
// Backend receives insertAfterId and calculates order
async function moveTask(
  taskId: string,
  newStatus: string,
  insertAfterId: string | null
) {
  let prevOrder: string | null = null;
  let nextOrder: string | null = null;

  if (insertAfterId === null) {
    // Insert at top - find first item's order
    const firstItem = await db.task.findFirst({
      where: { status: newStatus },
      orderBy: { order: "asc" },
      select: { order: true },
    });
    nextOrder = firstItem?.order ?? null;
  } else {
    // Find the reference task's order
    const refTask = await db.task.findUnique({
      where: { id: insertAfterId },
      select: { order: true },
    });
    prevOrder = refTask!.order;

    // Find next order after reference
    const nextItem = await db.task.findFirst({
      where: { status: newStatus, order: { gt: prevOrder } },
      orderBy: { order: "asc" },
      select: { order: true },
    });
    nextOrder = nextItem?.order ?? null;
  }

  // Calculate new order between prev and next
  const newOrder = generateFractionalIndex(prevOrder, nextOrder);

  await db.task.update({
    where: { id: taskId },
    data: { status: newStatus, order: newOrder },
  });
}
```

#### Component Props

```tsx theme={null}
interface DataKanbanCriteriaProps<T> {
  kanban: UseCriteriaKanbanReturn<T>; // From useCriteriaKanban
  renderCard: (item: T, isDragging: boolean, isClickable: boolean) => React.ReactNode;
  renderColumnHeader?: (column, itemCount) => React.ReactNode;
  renderColumnFooter?: (column) => React.ReactNode;
  renderEmptyState?: (column) => React.ReactNode; // Custom empty state
  onCardClick?: (item: T) => void;

  // Layout
  toolbarLayout?: "default" | "compact" | "none";
  actionBar?: React.ReactNode;
  className?: string;
  columnsClassName?: string;
  columnClassName?: string;
  columnsContentScrollClassName?: string;

  // Virtualization
  estimatedCardHeight?: number; // Default: 120
  showItemCount?: boolean; // Default: true
  showSkeleton?: boolean; // Default: true
}
```

#### Toolbar Layouts

```tsx theme={null}
// Default layout - toolbar with filters and search
<DataKanbanCriteria
  kanban={kanban}
  renderCard={renderCard}
  toolbarLayout="default"
/>

// Compact - smaller toolbar
<DataKanbanCriteria
  kanban={kanban}
  renderCard={renderCard}
  toolbarLayout="compact"
/>

// No toolbar - just the board
<DataKanbanCriteria
  kanban={kanban}
  renderCard={renderCard}
  toolbarLayout="none"
/>
```

#### Programmatic Card Move

```tsx theme={null}
const { moveCard } = useCriteriaKanban(...);

// Move card programmatically
await moveCard(
  cardId,      // Card to move
  fromColumnId,
  toColumnId,
  toIndex      // Position in target column
);
```

#### Custom Card Components

The package includes pre-built card components:

```tsx theme={null}
import {
  KanbanCard,
  KanbanCardHeader,
  KanbanCardTitle,
  KanbanCardDescription,
  KanbanCardContent,
  KanbanCardFooter,
  KanbanCardBadge,
} from "@/components/data-kanban-criteria";

<KanbanCard id={task.id} isDragging={isDragging} isClickable={isClickable}>
  <KanbanCardHeader>
    <KanbanCardTitle>{task.title}</KanbanCardTitle>
  </KanbanCardHeader>
  <KanbanCardContent>
    <KanbanCardDescription>{task.description}</KanbanCardDescription>
  </KanbanCardContent>
  <KanbanCardFooter>
    <KanbanCardBadge className="bg-red-100 text-red-700">
      {task.priority}
    </KanbanCardBadge>
  </KanbanCardFooter>
</KanbanCard>
```

#### Custom Empty State

Customize what's shown when a column has no items:

```tsx theme={null}
<DataKanbanCriteria
  kanban={kanban}
  renderCard={renderCard}
  renderEmptyState={(column) => (
    <div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
      <p className="text-sm">No tasks in {column.title}</p>
      <button className="mt-2 text-primary text-sm">
        + Add task
      </button>
    </div>
  )}
/>
```

If not provided, defaults to a simple "No items" message.

#### Preventing Drag on Specific Elements

Use the `data-no-drag` attribute on elements that should not trigger dragging:

```tsx theme={null}
<KanbanCard id={task.id} isDragging={isDragging} isClickable={isClickable}>
  <KanbanCardHeader>
    <KanbanCardTitle>{task.title}</KanbanCardTitle>
    {/* Button won't trigger drag when clicked */}
    <button data-no-drag onClick={() => deleteTask(task.id)}>
      Delete
    </button>
  </KanbanCardHeader>
</KanbanCard>
```

This is useful for interactive elements inside cards like buttons, inputs, or dropdown menus.

#### Cursor Styles

The `KanbanCard` component automatically handles cursor styles based on state:

| State                | Cursor | When                                |
| -------------------- | ------ | ----------------------------------- |
| `cursor-grab`        | ✋      | Card is draggable, no `onCardClick` |
| `cursor-pointer`     | 👆     | Card has `onCardClick` handler      |
| `cursor-grabbing`    | ✊      | Card is being dragged               |
| `cursor-not-allowed` | 🚫     | Card is disabled                    |

The `isClickable` parameter in `renderCard` determines if the card should show `cursor-pointer`:

```tsx theme={null}
<DataKanbanCriteria
  kanban={kanban}
  renderCard={(task, isDragging, isClickable) => (
    <KanbanCard
      id={task.id}
      isDragging={isDragging}
      isClickable={isClickable}  // Pass this to get correct cursor
    >
      {/* ... */}
    </KanbanCard>
  )}
  onCardClick={(task) => openTaskModal(task)}  // When set, isClickable=true
/>
```

#### Horizontal Scroll

The board automatically supports horizontal scrolling when you have many columns. Each column has a fixed width (320px) and the container scrolls horizontally when columns exceed the viewport.

This works out of the box - no configuration needed.

#### Full Height Layout

To make the kanban fill the viewport height:

```tsx theme={null}
<DataKanbanCriteria
  kanban={kanban}
  renderCard={renderCard}
  columnsContentScrollClassName="h-[calc(100vh-200px)]"
/>
```
