# Plugin Kanban





Kanban board component with drag-and-drop powered by @dnd-kit.

## Installation [#installation]

```bash
npm install @object-ui/plugin-kanban
```

This package publishes a stylesheet. Import it after the base sheets, or the board renders unstyled — the themed utilities it uses have no other source in a published app ([#4929](https://github.com/objectstack-ai/objectui/issues/4929)):

```css
/* src/index.css */
@import "tailwindcss";
@import "@object-ui/components/style.css";
@import "@object-ui/fields/style.css";
@import "@object-ui/plugin-kanban/style.css";
```

<PluginLoader plugins="['kanban']">
  ## Interactive Examples [#interactive-examples]

  <SchemaExample id="plugin-kanban/basic-kanban-board" />

  <SchemaExample id="plugin-kanban/advanced-kanban-with-badges-and-limits" />

  ## Usage [#usage]

  ### Basic Usage [#basic-usage]

  ```tsx
  // Import once in your app entry point
  import '@object-ui/plugin-kanban'
  import type { KanbanSchema } from '@object-ui/plugin-kanban'

  // Use in schemas
  const schema: KanbanSchema = {
    type: 'kanban',
    columns: [
      {
        id: 'todo',
        title: 'To Do',
        cards: [
          { id: '1', title: 'Task 1', description: 'Do something' }
        ]
      },
      {
        id: 'done',
        title: 'Done',
        cards: []
      }
    ],
    onCardMove: (cardId, fromCol, toCol, index) => {
      console.log(`Card ${cardId} moved`)
    }
  }
  ```

  ## Features [#features]

  * **Drag and drop cards** between columns
  * **Column limits** (WIP limits)
  * **Card badges** for status/priority
  * **Keyboard navigation**
  * **Lazy-loaded** (\~100-150 KB loads only when rendered)

  ## Schema API [#schema-api]

  ```plaintext
  {
    type: 'kanban',
    columns?: KanbanColumn[],
    onCardMove?: (cardId, fromColumnId, toColumnId, newIndex) => void,
    className?: string
  }

  interface KanbanColumn {
    id: string
    title: string
    cards: KanbanCard[]
    limit?: number              // Max cards allowed
    className?: string
  }

  interface KanbanCard {
    id: string
    title: string
    description?: string
    badges?: Array<{
      label: string
      variant?: 'default' | 'secondary' | 'destructive' | 'outline'
    }>
  }
  ```

  ### Properties [#properties]

  | Property     | Type            | Description                                          |
  | ------------ | --------------- | ---------------------------------------------------- |
  | `columns`    | KanbanColumn\[] | Array of column definitions                          |
  | `limit`      | number          | Rows fetched by an object-driven board (default 100) |
  | `onCardMove` | function        | Callback when a card is moved                        |
  | `className`  | string          | Additional Tailwind CSS classes                      |

  ### Column Properties [#column-properties]

  | Property    | Type          | Description                     |
  | ----------- | ------------- | ------------------------------- |
  | `id`        | string        | Unique column identifier        |
  | `title`     | string        | Column title                    |
  | `cards`     | KanbanCard\[] | Cards in this column            |
  | `limit`     | number        | Max cards allowed (WIP limit)   |
  | `className` | string        | Additional Tailwind CSS classes |

  ### Card Properties [#card-properties]

  | Property      | Type     | Description            |
  | ------------- | -------- | ---------------------- |
  | `id`          | string   | Unique card identifier |
  | `title`       | string   | Card title             |
  | `description` | string   | Card description       |
  | `badges`      | Badge\[] | Status/priority badges |

  ### Card fields (object-driven boards) [#card-fields-object-driven-boards]

  When a board is bound to an object (an `object-kanban`, or a kanban view inside
  `ListView` / `ObjectView`) instead of being given static `columns`, the fields
  rendered on each card resolve in priority order:

  1. **View-level `cardFields`** — the fields the view configures (a kanban view's
     `columns`, forwarded as `cardFields`). An explicit choice always wins.
  2. **The object's `highlightFields`** — the object's ADR-0085 semantic role (its
     curated "most important fields"), the same list the Grid, List and Detail
     surfaces already default to. Used when the view configures no card fields, so
     a board over an object with no per-view config still surfaces meaningful
     fields. Entries that reference a field the object no longer declares are
     ignored.
  3. **A legacy semantic-field heuristic** — a best-effort guess (amount, owner,
     priority, …) used only when neither of the above is available.

  Defaulting to `highlightFields` keeps a card's contents consistent with the
  object's other views without every kanban view having to re-declare its fields.

  ### Row cap (object-driven boards) [#row-cap-object-driven-boards]

  An object-driven board fetches at most `limit` records, defaulting to **100**. A
  board renders every fetched record into a lane and offers no pagination control,
  so this is the author's window on the object rather than a page size:

  ```tsx
  import type { ObjectKanbanSchema } from '@object-ui/types'

  const board: ObjectKanbanSchema = {
    type: 'object-kanban',
    objectName: 'opportunity',
    groupBy: 'stage',
    limit: 250,          // rows fetched; omit for the default 100
  }
  ```

  The `dataSource` binding sets it too — its own `limit`, or the
  `pagination.pageSize` of the view it names (see
  [Data source](/docs/guide/data-source)).

  <Callout type="info">
    This is the **board's** `limit`, not a column's. `limit` on a *column* is that
    lane's WIP limit — the card count at which the lane warns — and has no effect
    on the query.
  </Callout>

  ## Examples [#examples]

  ### Project Task Board [#project-task-board]

  ```tsx
  import type { KanbanSchema } from '@object-ui/plugin-kanban'

  const taskBoard: KanbanSchema = {
    type: 'kanban',
    columns: [
      {
        id: 'backlog',
        title: 'Backlog',
        cards: [
          {
            id: 'task-1',
            title: 'Design new homepage',
            description: 'Create mockups for the new landing page',
            badges: [
              { label: 'Design', variant: 'default' },
              { label: 'High Priority', variant: 'destructive' }
            ]
          }
        ]
      },
      {
        id: 'in-progress',
        title: 'In Progress',
        limit: 3,  // WIP limit
        cards: [
          {
            id: 'task-2',
            title: 'Implement authentication',
            description: 'Add OAuth2 login flow',
            badges: [
              { label: 'Backend', variant: 'secondary' }
            ]
          }
        ]
      },
      {
        id: 'review',
        title: 'Code Review',
        cards: []
      },
      {
        id: 'done',
        title: 'Done',
        cards: []
      }
    ],
    onCardMove: (cardId, fromCol, toCol, index) => {
      // Update backend/state
      console.log(`Moved ${cardId} from ${fromCol} to ${toCol}`)
    }
  }
  ```

  ### Support Ticket Board [#support-ticket-board]

  ```tsx
  import type { KanbanSchema } from '@object-ui/plugin-kanban'

  const ticketBoard: KanbanSchema = {
    type: 'kanban',
    columns: [
      {
        id: 'new',
        title: 'New Tickets',
        cards: [
          {
            id: 'ticket-1',
            title: 'Login not working',
            description: 'User cannot log in with Google',
            badges: [
              { label: 'Bug', variant: 'destructive' },
              { label: 'P1', variant: 'destructive' }
            ]
          }
        ]
      },
      {
        id: 'assigned',
        title: 'Assigned',
        limit: 5,
        cards: []
      },
      {
        id: 'resolved',
        title: 'Resolved',
        cards: []
      }
    ],
    className: 'min-h-[600px]'
  }
  ```

  ### Sales Pipeline [#sales-pipeline]

  ```tsx
  import type { KanbanSchema } from '@object-ui/plugin-kanban'

  const salesPipeline: KanbanSchema = {
    type: 'kanban',
    columns: [
      {
        id: 'leads',
        title: 'Leads',
        cards: [
          {
            id: 'lead-1',
            title: 'Acme Corp',
            description: '$50,000 - Enterprise plan',
            badges: [
              { label: 'Hot Lead', variant: 'destructive' }
            ]
          }
        ]
      },
      {
        id: 'qualified',
        title: 'Qualified',
        cards: []
      },
      {
        id: 'proposal',
        title: 'Proposal Sent',
        cards: []
      },
      {
        id: 'won',
        title: 'Won',
        cards: []
      }
    ]
  }
  ```

  ## Card Badges [#card-badges]

  Use badges to show card status, priority, or categories:

  ```tsx
  import type { KanbanCard } from '@object-ui/plugin-kanban'

  const card: KanbanCard = {
    id: 'task-1',
    title: 'Important Task',
    badges: [
      { label: 'Frontend', variant: 'default' },
      { label: 'Urgent', variant: 'destructive' },
      { label: 'Reviewed', variant: 'secondary' },
      { label: 'Blocked', variant: 'outline' }
    ]
  }
  ```

  ### Badge Variants [#badge-variants]

  * `default` - Blue badge
  * `secondary` - Gray badge
  * `destructive` - Red badge
  * `outline` - Outlined badge

  ## Column Limits (WIP Limits) [#column-limits-wip-limits]

  Set maximum cards per column to enforce work-in-progress limits:

  ```tsx
  import type { KanbanColumn } from '@object-ui/plugin-kanban'

  const column: KanbanColumn = {
    id: 'in-progress',
    title: 'In Progress',
    limit: 3,  // Max 3 cards
    cards: [
      { id: 'task-2', title: 'Implement authentication' }
    ]
  }
  ```

  When the limit is reached, the column shows visual feedback.

  ## Event Handling [#event-handling]

  Handle card movements to update your backend or state:

  ```tsx
  import { useState } from 'react'
  import type { KanbanColumn, KanbanSchema } from '@object-ui/plugin-kanban'

  declare function updateCardColumn(cardId: string, toColumnId: string, newIndex: number): Promise<void>
  declare function moveCard(columns: KanbanColumn[], cardId: string, toColumnId: string, newIndex: number): KanbanColumn[]

  export function useBoard(initialColumns: KanbanColumn[]) {
    const [columns, setColumns] = useState<KanbanColumn[]>(initialColumns)

    const schema: KanbanSchema = {
      type: 'kanban',
      columns,
      // `onCardMove` returns void, so the callback cannot be awaited by the board:
      // start the write and update local state without blocking the drop.
      onCardMove: (cardId, fromColumnId, toColumnId, newIndex) => {
        // Update database
        void updateCardColumn(cardId, toColumnId, newIndex)

        // Update local state
        setColumns((prev) => moveCard(prev, cardId, toColumnId, newIndex))
      },
    }

    return schema
  }
  ```

  ## Bundle Size [#bundle-size]

  The plugin uses lazy loading to optimize bundle size:

  * **Initial load**: \~0.2 KB (entry point)
  * **Lazy chunk**: \~100-150 KB (loaded when kanban is rendered)
  * **Includes @dnd-kit** for drag-and-drop functionality

  ## Accessibility [#accessibility]

  The kanban board includes:

  * **Keyboard navigation** - Move cards with keyboard
  * **Screen reader support** - ARIA labels for all interactions
  * **Focus management** - Clear focus indicators

  ## TypeScript Support [#typescript-support]

  ```plaintext
  import type { KanbanSchema, KanbanCard, KanbanColumn } from '@object-ui/plugin-kanban'

  const column: KanbanColumn = {
    id: 'todo',
    title: 'To Do',
    cards: [],
    limit: 5
  }

  const kanbanSchema: KanbanSchema = {
    type: 'kanban',
    columns: [column]
  }
  ```

  ## Related Documentation [#related-documentation]

  * [Plugin System Overview](/docs/guide/plugins)
  * [Lazy-Loaded Plugins Architecture](/docs/guide/plugins#lazy-loading-architecture)
  * [Package README](https://github.com/objectstack-ai/objectui/tree/main/packages/plugin-kanban)
</PluginLoader>
