# rn-kanban URL: /docs/native/docyrus/kanban Horizontally scrollable kanban board with generic items, position-preserving drag, column reorder, final drop zones, and rich default cards. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/rn-kanban ``` **Dependencies:** - [react-native-gesture-handler](https://www.npmjs.com/package/react-native-gesture-handler) - [react-native-reanimated](https://www.npmjs.com/package/react-native-reanimated) ## Usage ```tsx import { DocyKanban, type DocyKanbanColumn, type DocyKanbanItem } from '@/components/docyrus-native/kanban'; const columns: DocyKanbanColumn[] = [ { id: 'todo', title: 'To Do', color: '#6366f1' }, { id: 'progress', title: 'In Progress', color: '#f59e0b' }, { id: 'review', title: 'Review', color: '#8b5cf6' }, ]; const [items, setItems] = useState([ { id: 1, title: 'Setup CI', columnId: 'todo', priority: 'high', tag: 'DevOps' }, { id: 2, title: 'Auth flow', columnId: 'progress', priority: 'medium', assignee: 'John' }, ]); console.log(item.title)} onAddCard={(columnId) => console.log('add to', columnId)} /> ``` `DocyKanban` (also exported as `Kanban`) is a monolithic adaptation of the web compound primitive (`Kanban`, `KanbanBoard`, `KanbanColumn`, `KanbanColumnHandle`, `KanbanItem`, `KanbanOverlay`, `KanbanFinalZone`, `KanbanFinalColumn`, …). On React Native the compound pieces would only re-wrap the same gesture + measurement plumbing, so the web capabilities are exposed as props instead: the board is `KanbanBoard`, each column header is the `KanbanColumnHandle`, every card is a `KanbanItem`, the floating copy that follows the finger is the `KanbanOverlay`, and final zones are `KanbanFinalColumn`s. ### Drag and drop With `draggable`, **long-press a card for 200 ms** (the web touch-sensor delay) and drag it: - **Reorder inside a column.** The other cards slide out of the way as you drag. - **Move to another column.** The card lands exactly at the index shown by the gap. - **Drop on a final zone** (Done / Archive / custom) to mark it final. The board scrolls horizontally when you drag near its left or right edge. Because a card only activates after the long-press, a normal swipe still scrolls the board. With `columnDraggable` (defaults to `draggable`), **long-press a column header** and drag it horizontally to reorder the columns. The other columns animate out of the way. Moves are applied to the board optimistically. The board resyncs whenever `items`, `value`, `columns` or `columnOrder` changes. | Change | Callbacks | |--------|-----------| | Reorder inside a column | `onMove(event)` if set, otherwise `onValueChange(next)` (web semantics) · `onMoveCard` · `onItemsChange` | | Move to another column | `onValueChange(next)` + `onMove(event)` · `onMoveCard` · `onItemsChange` | | Drop on a final zone | `onValueChange(next)` (without the item) · `onFinalDrop(item, finalColumnId)` · `onMoveCard` · `onItemsChange` | | Reorder columns | `onColumnOrderChange(ids)` · `onMove(event)` if set, otherwise `onValueChange(next)` (keys in the new order) | ### Item sources - **Flat mode** (`items`): items are grouped with `getItemColumnId` (default `item.columnId`). With the default accessor, a moved item is returned with its `columnId` updated (and `done` / `archived` set for the presets). So `onItemsChange={setItems}` keeps both the column AND the position. With a custom accessor, the item is returned untouched: use `onValueChange` or `onMoveCard` to persist the move. - **Grouped mode** (`value`): the web `Record` shape. It is authoritative for both membership and order. `columns` is optional here; without it, the keys become the columns. ### Generic items Any record type works. Pass `getItemValue` (the web name) for the id and a `renderCard` for the content. This is the mode a Docyrus kanban hook feeds with raw records and `DocyrusKanbanColumnMeta` columns: ```tsx type Deal = { id: string; name: string; amount: number }; columns={[ { id: 'lead', label: 'Lead', icon: 'fal user-plus', color: '#0ea5e9' }, { id: 'won', label: 'Won', color: '#22c55e', isFinal: true }, ]} value={deals} onValueChange={next => setDeals(prev => ({ ...prev, ...next }))} getItemValue={deal => deal.id} draggable onFinalDrop={(deal, columnId) => closeDeal(deal.id, columnId)} onItemClick={id => openDeal(id)} renderCard={(deal, index, { isOverlay }) => } /> ``` ### Final zones Final zones are drop targets rendered below the board. Tap a zone to expand the items it holds. They come from three sources, and duplicate ids are ignored: 1. Columns with `isFinal: true` (the enum `is_final_option` from the web hook). 2. `finalColumns` — `{ id, label, color?, icon? }[]`. 3. The presets: `showDoneSection` (`KANBAN_DONE_COLUMN_ID` = `'__done__'`) and `showArchiveSection` (`KANBAN_ARCHIVE_COLUMN_ID` = `'__archive__'`). In flat mode, items with `done: true` / `archived: true` land in these presets. A final zone lists the items assigned to it: `value[finalId]` in grouped mode, or items whose column id is the final id in flat mode. ## API Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `columns` | `DocyKanbanColumn[]` | — | Column definitions (order + metadata). `isFinal` columns become final zones. Optional in `value` mode | | `items` | `T[]` | — | Flat item list, grouped by `getItemColumnId`. Ignored when `value` is set | | `value` | `Record` | — | Items grouped by column id (web `value`) | | `onValueChange` | `(value: Record) => void` | — | Next grouped value (board columns only) after a committed change | | `onItemsChange` | `(items: T[]) => void` | — | Every item (board + final zones) in board order after a move | | `getItemValue` | `(item: T) => string \| number` | `item.id` | Item id accessor | | `getItemColumnId` | `(item: T) => string` | `item.columnId` | Flat-mode column accessor | | `columnOrder` | `string[]` | — | Column order by id | | `onColumnOrderChange` | `(columnIds: string[]) => void` | — | New column order after a column reorder | | `variant` | `'default' \| 'outlined' \| 'elevated'` | `'default'` | Column visual style | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Column width (288 / 320 / 384) and card padding | | `showCounts` | `boolean` | `true` | Show the live item count in each column header | | `showDoneSection` | `boolean` | `false` | Add the Done preset final zone | | `showArchiveSection` | `boolean` | `false` | Add the Archive preset final zone | | `finalColumns` | `KanbanFinalColumn[]` | — | Extra final zones | | `onFinalDrop` | `(item: T, finalColumnId: string) => void` | — | A card was dropped on a final zone | | `draggable` | `boolean` | `false` | Long-press drag of cards | | `columnDraggable` | `boolean` | `draggable` | Long-press drag of column headers | | `onCardPress` | `(item: T) => void` | — | Card tap | | `onItemClick` | `(value: string \| number) => void` | — | Web-parity card tap with the item id | | `onAddCard` | `(columnId: string) => void` | — | Shows a **+ Add** button under each column | | `onMoveCard` | `(item: T, fromColumnId: string, toColumnId: string, toIndex: number, fromIndex: number) => void` | — | Called after every card move, including final drops | | `onMove` | `(event: KanbanMoveEvent) => void` | — | Web `onMove`. See [Drag and drop](#drag-and-drop) | | `onDragStart` | `(item: T, columnId: string) => void` | — | A card drag activated | | `onDragEnd` | `(item: T) => void` | — | A card drag ended (dropped or cancelled) | | `renderCard` | `(item: T, index: number, context: KanbanCardRenderContext) => ReactNode` | — | Custom card (no default surface or padding; still draggable) | | `renderColumnHeader` | `(column: DocyKanbanColumn, count: number) => ReactNode` | — | Custom column header (still the column-drag handle) | | `className` | `string` | — | Root view classes | All other `View` props (`style`, `testID`, …) are forwarded to the root view. ### DocyKanbanColumn A superset of the web hook's `DocyrusKanbanColumnMeta`. | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Column id | | `title` | `string` | Header label (legacy native name) | | `label` | `string` | Header label (web name). Wins over `title` | | `color` | `string \| null` | Accent color (header dot / final-zone tint) | | `icon` | `string \| null` | `DocyrusIcon` string shown in the header | | `imageUrl` | `string \| null` | Avatar shown in the header (user / team grouping) | | `isFinal` | `boolean` | Render as a final drop zone instead of a board column | | `count` | `number` | Informational. The header shows the live item count | ### DocyKanbanItem The default item shape, used by the built-in rich card. | Field | Type | Description | |-------|------|-------------| | `id` | `string \| number` | Unique id | | `title` | `string` | Card title | | `description` | `string` | Optional description | | `columnId` | `string` | Column id (flat mode) | | `tag` | `string` | Tag chip | | `priority` | `'low' \| 'medium' \| 'high'` | Priority label | | `assignee` | `string` | Assignee name (initials avatar) | | `dueDate` | `Date \| string` | Due-date chip (Today / Tomorrow / *N*d left / *N*d overdue) | | `done` | `boolean` | Done strike-through. Lands in the Done preset when `showDoneSection` is on | | `archived` | `boolean` | Lands in the Archive preset when `showArchiveSection` is on | ### KanbanFinalColumn | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Final zone id (passed to `onFinalDrop`) | | `label` | `string` | Zone label | | `color` | `string \| null` | Tint | | `icon` | `string \| null` | `DocyrusIcon` string | ### KanbanMoveEvent | Field | Type | Description | |-------|------|-------------| | `type` | `'item' \| 'column'` | Card move or column reorder | | `activeId` | `string \| number` | Moved item id or column id | | `activeIndex` | `number` | Index before the move | | `overIndex` | `number` | Index after the move | | `fromColumnId` | `string` | Source column | | `toColumnId` | `string` | Target column | | `item` | `T` | Moved item (`type: 'item'` only) | ### KanbanCardRenderContext | Field | Type | Description | |-------|------|-------------| | `columnId` | `string` | Column (or final zone) the card is rendered in | | `isDragging` | `boolean` | The placeholder slot of the card being dragged | | `isOverlay` | `boolean` | Rendering the floating drag overlay | ## Translations | Key | Fallback | |-----|----------| | `ui.common.today` | `Today` | | `ui.kanban.tomorrow` | `Tomorrow` | | `ui.kanban.daysLeft` | `{count}d left` | | `ui.kanban.overdue` | `{count}d overdue` | | `ui.kanban.noItems` | `No items` | | `ui.kanban.addCard` | `+ Add` | | `ui.kanban.done` | `Done` | | `ui.kanban.archive` | `Archive` | | `ui.kanban.dropToMarkFinal` | `Drop to mark final` | ## Components | Component | Description | |-----------|-------------| | `DocyKanban` | Board component | | `Kanban` | Web-parity alias of `DocyKanban` | | `KanbanCardContent` | The built-in rich card body, for reuse inside a custom `renderCard` | ## Exports | Export | Description | |--------|-------------| | `KANBAN_DONE_COLUMN_ID` | `'__done__'` | | `KANBAN_ARCHIVE_COLUMN_ID` | `'__archive__'` | | `withAlpha(color, alpha)` | Color helper (re-exported from the shared control styles) | ## Type Exports | Type | Description | |------|-------------| | `DocyKanbanProps` | Props for DocyKanban | | `DocyKanbanColumn` | Column metadata | | `KanbanColumnMeta` | Web-parity alias of `DocyKanbanColumn` | | `DocyKanbanItem` | Default item shape | | `KanbanFinalColumn` | Final zone definition | | `KanbanMoveEvent` | `onMove` payload | | `KanbanValue` | `Record` | | `KanbanId` | `string \| number` | | `KanbanCardRenderContext` | Third `renderCard` argument |