@object-ui/plugin-kanban 0.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-kanban",
3
- "version": "0.5.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Kanban board plugin for Object UI, powered by dnd-kit",
@@ -27,25 +27,25 @@
27
27
  "@dnd-kit/core": "^6.3.1",
28
28
  "@dnd-kit/sortable": "^10.0.0",
29
29
  "@dnd-kit/utilities": "^3.2.2",
30
- "@tanstack/react-virtual": "^3.10.8",
30
+ "@tanstack/react-virtual": "^3.13.18",
31
31
  "lucide-react": "^0.563.0",
32
- "@object-ui/components": "0.5.0",
33
- "@object-ui/core": "0.5.0",
34
- "@object-ui/react": "0.5.0",
35
- "@object-ui/types": "0.5.0"
32
+ "@object-ui/components": "3.0.0",
33
+ "@object-ui/core": "3.0.0",
34
+ "@object-ui/react": "3.0.0",
35
+ "@object-ui/types": "3.0.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "react": "^18.0.0 || ^19.0.0",
39
39
  "react-dom": "^18.0.0 || ^19.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/react": "^19.2.10",
43
- "@types/react-dom": "^19.2.3",
44
- "@vitejs/plugin-react": "^5.1.3",
42
+ "@types/react": "19.2.13",
43
+ "@types/react-dom": "19.2.3",
44
+ "@vitejs/plugin-react": "^5.1.4",
45
45
  "typescript": "^5.9.3",
46
46
  "vite": "^7.3.1",
47
47
  "vite-plugin-dts": "^4.5.4",
48
- "@object-ui/data-objectstack": "0.5.0"
48
+ "@object-ui/data-objectstack": "3.0.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build",
@@ -13,6 +13,7 @@ import {
13
13
  DragOverlay,
14
14
  DragStartEvent,
15
15
  PointerSensor,
16
+ TouchSensor,
16
17
  useSensor,
17
18
  useSensors,
18
19
  closestCorners,
@@ -25,6 +26,7 @@ import {
25
26
  } from "@dnd-kit/sortable"
26
27
  import { CSS } from "@dnd-kit/utilities"
27
28
  import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, ScrollArea } from "@object-ui/components"
29
+ import { useHasDndProvider, useDnd } from "@object-ui/react"
28
30
 
29
31
  // Utility function to merge class names (inline to avoid external dependency)
30
32
  const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
@@ -48,10 +50,11 @@ export interface KanbanColumn {
48
50
  export interface KanbanBoardProps {
49
51
  columns: KanbanColumn[]
50
52
  onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void
53
+ onCardClick?: (card: KanbanCard) => void
51
54
  className?: string
52
55
  }
53
56
 
54
- function SortableCard({ card }: { card: KanbanCard }) {
57
+ function SortableCard({ card, onCardClick }: { card: KanbanCard; onCardClick?: (card: KanbanCard) => void }) {
55
58
  const {
56
59
  attributes,
57
60
  listeners,
@@ -68,18 +71,20 @@ function SortableCard({ card }: { card: KanbanCard }) {
68
71
  }
69
72
 
70
73
  return (
71
- <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
72
- <Card className="mb-2 cursor-grab active:cursor-grabbing border-border bg-card/60 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 group">
73
- <CardHeader className="p-4">
74
- <CardTitle className="text-sm font-medium font-mono tracking-tight text-foreground group-hover:text-primary transition-colors">{card.title}</CardTitle>
74
+ <div ref={setNodeRef} style={style} {...attributes} {...listeners} role="listitem" aria-label={card.title}
75
+ onClick={() => onCardClick?.(card)}
76
+ >
77
+ <Card className="mb-2 cursor-grab active:cursor-grabbing border-border bg-card/60 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 group touch-manipulation">
78
+ <CardHeader className="p-2 sm:p-4">
79
+ <CardTitle className="text-xs sm:text-sm font-medium font-mono tracking-tight text-foreground group-hover:text-primary transition-colors">{card.title}</CardTitle>
75
80
  {card.description && (
76
- <CardDescription className="text-xs text-muted-foreground font-mono">
81
+ <CardDescription className="text-xs text-muted-foreground font-mono line-clamp-2 sm:line-clamp-none">
77
82
  {card.description}
78
83
  </CardDescription>
79
84
  )}
80
85
  </CardHeader>
81
86
  {card.badges && card.badges.length > 0 && (
82
- <CardContent className="p-4 pt-0">
87
+ <CardContent className="p-2 sm:p-4 pt-0">
83
88
  <div className="flex flex-wrap gap-1">
84
89
  {card.badges.map((badge, index) => (
85
90
  <Badge key={index} variant={badge.variant || "default"} className="text-xs">
@@ -97,9 +102,11 @@ function SortableCard({ card }: { card: KanbanCard }) {
97
102
  function KanbanColumn({
98
103
  column,
99
104
  cards,
105
+ onCardClick,
100
106
  }: {
101
107
  column: KanbanColumn
102
108
  cards: KanbanCard[]
109
+ onCardClick?: (card: KanbanCard) => void
103
110
  }) {
104
111
  const safeCards = cards || [];
105
112
  const { setNodeRef } = useSortable({
@@ -114,16 +121,18 @@ function KanbanColumn({
114
121
  return (
115
122
  <div
116
123
  ref={setNodeRef}
124
+ role="group"
125
+ aria-label={column.title}
117
126
  className={cn(
118
- "flex flex-col w-80 flex-shrink-0 rounded-lg border border-border bg-card/20 backdrop-blur-sm shadow-xl",
127
+ "flex flex-col w-[85vw] sm:w-80 flex-shrink-0 rounded-lg border border-border bg-card/20 backdrop-blur-sm shadow-xl snap-start",
119
128
  column.className
120
129
  )}
121
130
  >
122
- <div className="p-4 border-b border-border/50 bg-muted/20">
131
+ <div className="p-3 sm:p-4 border-b border-border/50 bg-muted/20">
123
132
  <div className="flex items-center justify-between">
124
- <h3 className="font-mono text-sm font-semibold tracking-wider text-primary/90 uppercase">{column.title}</h3>
133
+ <h3 id={`kanban-col-${column.id}`} className="font-mono text-xs sm:text-sm font-semibold tracking-wider text-primary/90 uppercase truncate">{column.title}</h3>
125
134
  <div className="flex items-center gap-2">
126
- <span className="font-mono text-xs text-muted-foreground">
135
+ <span className="font-mono text-xs text-muted-foreground" aria-label={`${safeCards.length} cards${column.limit ? ` of ${column.limit} maximum` : ''}`}>
127
136
  {safeCards.length}
128
137
  {column.limit && ` / ${column.limit}`}
129
138
  </span>
@@ -140,9 +149,9 @@ function KanbanColumn({
140
149
  items={safeCards.map((c) => c.id)}
141
150
  strategy={verticalListSortingStrategy}
142
151
  >
143
- <div className="space-y-2">
152
+ <div className="space-y-2" role="list" aria-label={`${column.title} cards`}>
144
153
  {safeCards.map((card) => (
145
- <SortableCard key={card.id} card={card} />
154
+ <SortableCard key={card.id} card={card} onCardClick={onCardClick} />
146
155
  ))}
147
156
  </div>
148
157
  </SortableContext>
@@ -151,7 +160,27 @@ function KanbanColumn({
151
160
  )
152
161
  }
153
162
 
154
- export default function KanbanBoard({ columns, onCardMove, className }: KanbanBoardProps) {
163
+ /** Bridge wrapper that reads the ObjectUI DnD context and injects it into KanbanBoardInner. */
164
+ function DndBridge({ children }: { children: (dnd: ReturnType<typeof useDnd>) => React.ReactNode }) {
165
+ const dnd = useDnd()
166
+ return <>{children(dnd)}</>
167
+ }
168
+
169
+ export default function KanbanBoard({ columns, onCardMove, onCardClick, className }: KanbanBoardProps) {
170
+ const hasDnd = useHasDndProvider()
171
+
172
+ if (hasDnd) {
173
+ return (
174
+ <DndBridge>
175
+ {(dnd) => <KanbanBoardInner columns={columns} onCardMove={onCardMove} onCardClick={onCardClick} className={className} dnd={dnd} />}
176
+ </DndBridge>
177
+ )
178
+ }
179
+
180
+ return <KanbanBoardInner columns={columns} onCardMove={onCardMove} onCardClick={onCardClick} className={className} dnd={null} />
181
+ }
182
+
183
+ function KanbanBoardInner({ columns, onCardMove, onCardClick, className, dnd }: KanbanBoardProps & { dnd: ReturnType<typeof useDnd> | null }) {
155
184
  const [activeCard, setActiveCard] = React.useState<KanbanCard | null>(null)
156
185
 
157
186
  // Ensure we always have valid columns with cards array
@@ -171,7 +200,13 @@ export default function KanbanBoard({ columns, onCardMove, className }: KanbanBo
171
200
  const sensors = useSensors(
172
201
  useSensor(PointerSensor, {
173
202
  activationConstraint: {
174
- distance: 8,
203
+ distance: 5,
204
+ },
205
+ }),
206
+ useSensor(TouchSensor, {
207
+ activationConstraint: {
208
+ delay: 200,
209
+ tolerance: 5,
175
210
  },
176
211
  })
177
212
  )
@@ -180,23 +215,40 @@ export default function KanbanBoard({ columns, onCardMove, className }: KanbanBo
180
215
  const { active } = event
181
216
  const card = findCard(active.id as string)
182
217
  setActiveCard(card)
218
+
219
+ // Bridge to ObjectUI spec DnD system
220
+ if (dnd && card) {
221
+ const column = findColumnByCardId(card.id)
222
+ if (column) {
223
+ dnd.startDrag({ id: card.id, type: 'kanban-card', data: card, sourceId: column.id })
224
+ }
225
+ }
183
226
  }
184
227
 
185
228
  const handleDragEnd = (event: DragEndEvent) => {
186
229
  const { active, over } = event
187
230
  setActiveCard(null)
188
231
 
189
- if (!over) return
232
+ if (!over) {
233
+ if (dnd) dnd.endDrag()
234
+ return
235
+ }
190
236
 
191
237
  const activeId = active.id as string
192
238
  const overId = over.id as string
193
239
 
194
- if (activeId === overId) return
240
+ if (activeId === overId) {
241
+ if (dnd) dnd.endDrag()
242
+ return
243
+ }
195
244
 
196
245
  const activeColumn = findColumnByCardId(activeId)
197
246
  const overColumn = findColumnByCardId(overId) || findColumnById(overId)
198
247
 
199
- if (!activeColumn || !overColumn) return
248
+ if (!activeColumn || !overColumn) {
249
+ if (dnd) dnd.endDrag()
250
+ return
251
+ }
200
252
 
201
253
  if (activeColumn.id === overColumn.id) {
202
254
  // Same column reordering
@@ -241,6 +293,9 @@ export default function KanbanBoard({ columns, onCardMove, className }: KanbanBo
241
293
  onCardMove(activeId, activeColumn.id, overColumn.id, overIndex)
242
294
  }
243
295
  }
296
+
297
+ // Bridge to ObjectUI spec DnD system
298
+ if (dnd) dnd.endDrag(overColumn.id)
244
299
  }
245
300
 
246
301
  const findCard = React.useCallback(
@@ -275,17 +330,24 @@ export default function KanbanBoard({ columns, onCardMove, className }: KanbanBo
275
330
  onDragStart={handleDragStart}
276
331
  onDragEnd={handleDragEnd}
277
332
  >
278
- <div className={cn("flex gap-4 overflow-x-auto p-4", className)}>
333
+ <div className="flex sm:hidden items-center justify-between px-3 pb-2 text-xs text-muted-foreground">
334
+ <span>{boardColumns.length} columns</span>
335
+ <span>← Swipe to navigate →</span>
336
+ </div>
337
+ <div className={cn("flex gap-3 sm:gap-4 overflow-x-auto snap-x snap-mandatory p-2 sm:p-4 [-webkit-overflow-scrolling:touch]", className)} role="region" aria-label="Kanban board">
279
338
  {boardColumns.map((column) => (
280
339
  <KanbanColumn
281
340
  key={column.id}
282
341
  column={column}
283
342
  cards={column.cards}
343
+ onCardClick={onCardClick}
284
344
  />
285
345
  ))}
286
346
  </div>
287
347
  <DragOverlay>
288
- {activeCard ? <SortableCard card={activeCard} /> : null}
348
+ <div aria-live="assertive" aria-label={activeCard ? `Dragging ${activeCard.title}` : undefined}>
349
+ {activeCard ? <SortableCard card={activeCard} /> : null}
350
+ </div>
289
351
  </DragOverlay>
290
352
  </DndContext>
291
353
  )
@@ -0,0 +1,168 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SchemaRenderer } from '@object-ui/react';
3
+ import type { BaseSchema } from '@object-ui/types';
4
+
5
+ const meta = {
6
+ title: 'Plugins/ObjectKanban/Edge Cases',
7
+ component: SchemaRenderer,
8
+ parameters: {
9
+ layout: 'padded',
10
+ },
11
+ tags: ['autodocs'],
12
+ argTypes: {
13
+ schema: { table: { disable: true } },
14
+ },
15
+ } satisfies Meta<any>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
21
+
22
+ // ── Empty Board ───────────────────────────────────────────────
23
+
24
+ export const EmptyBoard: Story = {
25
+ name: 'Empty Board – No Columns',
26
+ render: renderStory,
27
+ args: {
28
+ type: 'kanban',
29
+ columns: [],
30
+ className: 'w-full',
31
+ } as any,
32
+ };
33
+
34
+ // ── Columns With No Cards ─────────────────────────────────────
35
+
36
+ export const ColumnsWithNoCards: Story = {
37
+ name: 'Columns With No Cards',
38
+ render: renderStory,
39
+ args: {
40
+ type: 'kanban',
41
+ columns: [
42
+ { id: 'todo', title: 'To Do', cards: [] },
43
+ { id: 'in-progress', title: 'In Progress', cards: [] },
44
+ { id: 'done', title: 'Done', cards: [] },
45
+ ],
46
+ className: 'w-full',
47
+ } as any,
48
+ };
49
+
50
+ // ── Column At WIP Limit ───────────────────────────────────────
51
+
52
+ export const ColumnAtWipLimit: Story = {
53
+ name: 'Column At WIP Limit',
54
+ render: renderStory,
55
+ args: {
56
+ type: 'kanban',
57
+ columns: [
58
+ {
59
+ id: 'todo',
60
+ title: 'To Do',
61
+ cards: [
62
+ { id: 'c-1', title: 'Plan sprint', badges: [{ label: 'Planning', variant: 'default' }] },
63
+ ],
64
+ },
65
+ {
66
+ id: 'wip',
67
+ title: 'In Progress (At Limit)',
68
+ limit: 3,
69
+ cards: [
70
+ { id: 'c-2', title: 'Build auth module', description: 'JWT implementation', badges: [{ label: 'Feature', variant: 'default' }] },
71
+ { id: 'c-3', title: 'Write unit tests', description: 'Coverage > 80%', badges: [{ label: 'Testing', variant: 'secondary' }] },
72
+ { id: 'c-4', title: 'Deploy staging', description: 'Push to staging env', badges: [{ label: 'DevOps', variant: 'secondary' }] },
73
+ ],
74
+ },
75
+ {
76
+ id: 'done',
77
+ title: 'Done',
78
+ cards: [
79
+ { id: 'c-5', title: 'Setup repo', badges: [{ label: 'Completed', variant: 'outline' }] },
80
+ ],
81
+ },
82
+ ],
83
+ className: 'w-full',
84
+ } as any,
85
+ };
86
+
87
+ // ── Cards With Very Long Titles ───────────────────────────────
88
+
89
+ export const CardsWithLongTitles: Story = {
90
+ name: 'Cards With Very Long Titles',
91
+ render: renderStory,
92
+ args: {
93
+ type: 'kanban',
94
+ columns: [
95
+ {
96
+ id: 'backlog',
97
+ title: 'Backlog',
98
+ cards: [
99
+ {
100
+ id: 'long-1',
101
+ title: 'Investigate the root cause of the intermittent timeout errors occurring in the payment processing pipeline during peak traffic hours on weekends',
102
+ description: 'This card has an extremely long title to test text wrapping and overflow behaviour within kanban cards.',
103
+ badges: [
104
+ { label: 'Bug', variant: 'destructive' },
105
+ { label: 'P0 – Critical Production Incident', variant: 'destructive' },
106
+ ],
107
+ },
108
+ {
109
+ id: 'long-2',
110
+ title: 'Refactor the legacy monolithic authentication service into a set of microservices following domain-driven design principles and ensuring backward compatibility',
111
+ badges: [{ label: 'Tech Debt', variant: 'secondary' }],
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ id: 'in-progress',
117
+ title: 'In Progress',
118
+ cards: [
119
+ {
120
+ id: 'long-3',
121
+ title: 'A short title for contrast',
122
+ description: 'Normal-length description.',
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ className: 'w-full',
128
+ } as any,
129
+ };
130
+
131
+ // ── Many Columns (10+) ───────────────────────────────────────
132
+
133
+ export const ManyColumns: Story = {
134
+ name: 'Many Columns (10+)',
135
+ render: renderStory,
136
+ args: {
137
+ type: 'kanban',
138
+ columns: Array.from({ length: 12 }, (_, i) => ({
139
+ id: `col-${i + 1}`,
140
+ title: `Stage ${i + 1}`,
141
+ limit: i === 3 ? 2 : undefined,
142
+ cards: i % 3 === 0
143
+ ? [
144
+ {
145
+ id: `mc-${i}-1`,
146
+ title: `Task ${i * 2 + 1}`,
147
+ description: `Description for task in stage ${i + 1}`,
148
+ badges: [{ label: `S${i + 1}`, variant: 'default' as const }],
149
+ },
150
+ {
151
+ id: `mc-${i}-2`,
152
+ title: `Task ${i * 2 + 2}`,
153
+ badges: [{ label: 'Active', variant: 'secondary' as const }],
154
+ },
155
+ ]
156
+ : i % 3 === 1
157
+ ? [
158
+ {
159
+ id: `mc-${i}-1`,
160
+ title: `Task ${i * 2 + 1}`,
161
+ badges: [{ label: 'Review', variant: 'outline' as const }],
162
+ },
163
+ ]
164
+ : [],
165
+ })),
166
+ className: 'w-full',
167
+ } as any,
168
+ };
@@ -0,0 +1,152 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SchemaRenderer } from '@object-ui/react';
3
+ import type { BaseSchema } from '@object-ui/types';
4
+
5
+ const meta = {
6
+ title: 'Plugins/ObjectKanban',
7
+ component: SchemaRenderer,
8
+ parameters: {
9
+ layout: 'padded',
10
+ },
11
+ tags: ['autodocs'],
12
+ argTypes: {
13
+ schema: { table: { disable: true } },
14
+ },
15
+ } satisfies Meta<any>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
21
+
22
+ export const Default: Story = {
23
+ render: renderStory,
24
+ args: {
25
+ type: 'kanban',
26
+ columns: [
27
+ {
28
+ id: 'todo',
29
+ title: 'To Do',
30
+ cards: [
31
+ {
32
+ id: 'card-1',
33
+ title: 'Design homepage',
34
+ description: 'Create wireframes and mockups',
35
+ badges: [{ label: 'Design', variant: 'default' }],
36
+ },
37
+ {
38
+ id: 'card-2',
39
+ title: 'Setup CI pipeline',
40
+ description: 'Configure GitHub Actions',
41
+ badges: [{ label: 'DevOps', variant: 'secondary' }],
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ id: 'in-progress',
47
+ title: 'In Progress',
48
+ limit: 3,
49
+ cards: [
50
+ {
51
+ id: 'card-3',
52
+ title: 'Implement auth',
53
+ description: 'JWT-based authentication flow',
54
+ badges: [
55
+ { label: 'Feature', variant: 'default' },
56
+ { label: 'High Priority', variant: 'destructive' },
57
+ ],
58
+ },
59
+ ],
60
+ },
61
+ {
62
+ id: 'done',
63
+ title: 'Done',
64
+ cards: [
65
+ {
66
+ id: 'card-4',
67
+ title: 'Project scaffolding',
68
+ description: 'Initial project setup completed',
69
+ badges: [{ label: 'Completed', variant: 'outline' }],
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ className: 'w-full',
75
+ } as any,
76
+ };
77
+
78
+ export const SprintBoard: Story = {
79
+ render: renderStory,
80
+ args: {
81
+ type: 'kanban',
82
+ columns: [
83
+ {
84
+ id: 'backlog',
85
+ title: 'Backlog',
86
+ cards: [
87
+ { id: 's-1', title: 'Refactor API layer', description: 'Improve error handling', badges: [{ label: 'Tech Debt', variant: 'secondary' }] },
88
+ { id: 's-2', title: 'Add unit tests', description: 'Increase coverage to 80%', badges: [{ label: 'Testing', variant: 'default' }] },
89
+ ],
90
+ },
91
+ {
92
+ id: 'in-progress',
93
+ title: 'In Progress',
94
+ limit: 2,
95
+ cards: [
96
+ { id: 's-3', title: 'User dashboard', description: 'Build analytics dashboard', badges: [{ label: 'Feature', variant: 'default' }, { label: 'P1', variant: 'destructive' }] },
97
+ ],
98
+ },
99
+ {
100
+ id: 'review',
101
+ title: 'In Review',
102
+ cards: [
103
+ { id: 's-4', title: 'Search functionality', description: 'Full-text search implementation', badges: [{ label: 'Feature', variant: 'default' }] },
104
+ ],
105
+ },
106
+ {
107
+ id: 'done',
108
+ title: 'Done',
109
+ cards: [
110
+ { id: 's-5', title: 'Login page', badges: [{ label: 'Done', variant: 'outline' }] },
111
+ { id: 's-6', title: 'Database schema', badges: [{ label: 'Done', variant: 'outline' }] },
112
+ ],
113
+ },
114
+ ],
115
+ className: 'w-full',
116
+ } as any,
117
+ };
118
+
119
+ export const WithColumnLimits: Story = {
120
+ render: renderStory,
121
+ args: {
122
+ type: 'kanban',
123
+ columns: [
124
+ {
125
+ id: 'todo',
126
+ title: 'To Do',
127
+ cards: [
128
+ { id: 'l-1', title: 'Task A', badges: [{ label: 'P1', variant: 'destructive' }] },
129
+ { id: 'l-2', title: 'Task B', badges: [{ label: 'P2', variant: 'default' }] },
130
+ ],
131
+ },
132
+ {
133
+ id: 'wip',
134
+ title: 'WIP (Over Limit)',
135
+ limit: 2,
136
+ cards: [
137
+ { id: 'l-3', title: 'Task C', description: 'Almost done' },
138
+ { id: 'l-4', title: 'Task D', description: 'In review' },
139
+ { id: 'l-5', title: 'Task E', description: 'Blocked', badges: [{ label: 'Blocked', variant: 'destructive' }] },
140
+ ],
141
+ },
142
+ {
143
+ id: 'done',
144
+ title: 'Done',
145
+ cards: [
146
+ { id: 'l-6', title: 'Task F', badges: [{ label: 'Completed', variant: 'outline' }] },
147
+ ],
148
+ },
149
+ ],
150
+ className: 'w-full',
151
+ } as any,
152
+ };
@@ -8,7 +8,8 @@
8
8
 
9
9
  import React, { useEffect, useState, useMemo } from 'react';
10
10
  import type { DataSource } from '@object-ui/types';
11
- import { useDataScope } from '@object-ui/react';
11
+ import { useDataScope, useNavigationOverlay } from '@object-ui/react';
12
+ import { NavigationOverlay } from '@object-ui/components';
12
13
  import { KanbanRenderer } from './index';
13
14
  import { KanbanSchema } from './types';
14
15
 
@@ -16,12 +17,16 @@ export interface ObjectKanbanProps {
16
17
  schema: KanbanSchema;
17
18
  dataSource?: DataSource;
18
19
  className?: string; // Allow override
20
+ onRowClick?: (record: any) => void;
21
+ onCardClick?: (record: any) => void;
19
22
  }
20
23
 
21
24
  export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
22
25
  schema,
23
26
  dataSource,
24
27
  className,
28
+ onRowClick,
29
+ onCardClick,
25
30
  ...props
26
31
  }) => {
27
32
  const [fetchedData, setFetchedData] = useState<any[]>([]);
@@ -175,6 +180,12 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
175
180
  className: className || schema.className
176
181
  };
177
182
 
183
+ const navigation = useNavigationOverlay({
184
+ navigation: (schema as any).navigation,
185
+ objectName: schema.objectName,
186
+ onRowClick: onRowClick ?? onCardClick,
187
+ });
188
+
178
189
  if (error) {
179
190
  return (
180
191
  <div className="p-4 border border-destructive/50 rounded bg-destructive/10 text-destructive">
@@ -184,5 +195,35 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
184
195
  }
185
196
 
186
197
  // Pass through to the renderer
187
- return <KanbanRenderer schema={effectiveSchema} />;
198
+ const detailTitle = schema.objectName
199
+ ? `${schema.objectName.charAt(0).toUpperCase() + schema.objectName.slice(1).replace(/_/g, ' ')} Detail`
200
+ : 'Card Details';
201
+
202
+ return (
203
+ <>
204
+ <KanbanRenderer schema={{
205
+ ...effectiveSchema,
206
+ onCardClick: (card: any) => {
207
+ navigation.handleClick(card);
208
+ onCardClick?.(card);
209
+ },
210
+ }} />
211
+ {navigation.isOverlay && (
212
+ <NavigationOverlay {...navigation} title={detailTitle}>
213
+ {(record) => (
214
+ <div className="space-y-3">
215
+ {Object.entries(record).map(([key, value]) => (
216
+ <div key={key} className="flex flex-col">
217
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
218
+ {key.replace(/_/g, ' ')}
219
+ </span>
220
+ <span className="text-sm">{String(value ?? '—')}</span>
221
+ </div>
222
+ ))}
223
+ </div>
224
+ )}
225
+ </NavigationOverlay>
226
+ )}
227
+ </>
228
+ );
188
229
  }
@@ -24,6 +24,7 @@ vi.mock('@dnd-kit/core', () => ({
24
24
  DndContext: ({ children }: any) => <div data-testid="dnd-context">{children}</div>,
25
25
  DragOverlay: ({ children }: any) => <div data-testid="drag-overlay">{children}</div>,
26
26
  PointerSensor: vi.fn(),
27
+ TouchSensor: vi.fn(),
27
28
  useSensor: vi.fn(),
28
29
  useSensors: () => [],
29
30
  closestCorners: vi.fn(),