@moontra/moonui-pro 2.13.0 → 2.14.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.
@@ -69,6 +69,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
69
69
  import { ColorPicker } from '../ui/color-picker'
70
70
  import { CardDetailModal } from './card-detail-modal'
71
71
  import { AddCardModal } from './add-card-modal'
72
+ import { format } from 'date-fns'
72
73
  import type {
73
74
  KanbanAssignee,
74
75
  KanbanLabel,
@@ -170,38 +171,80 @@ const useAutoScroll = () => {
170
171
  // Card component
171
172
  const KanbanCardComponent = ({
172
173
  card,
174
+ column,
173
175
  isDragging,
174
176
  onEdit,
175
177
  onDelete,
176
178
  onClick,
177
179
  showDetails,
178
- disabled
180
+ disabled,
181
+ renderCard,
182
+ renderCardPreview,
183
+ renderCardBadge,
184
+ renderCardActions,
185
+ cardCompactMode,
186
+ cardShowCoverImage,
187
+ cardShowAssignees,
188
+ cardShowLabels,
189
+ cardShowProgress,
190
+ cardDateFormat,
191
+ cardMaxAssigneesToShow,
192
+ provided,
193
+ enableAnimations,
194
+ animationDuration,
195
+ cardVariant
179
196
  }: {
180
197
  card: KanbanCard
198
+ column: KanbanColumn
181
199
  isDragging: boolean
182
200
  onEdit?: (e: React.MouseEvent) => void
183
201
  onDelete?: (e: React.MouseEvent) => void
184
202
  onClick?: () => void
185
203
  showDetails: boolean
186
204
  disabled: boolean
205
+ renderCard?: (card: KanbanCard, column: KanbanColumn, provided: any) => React.ReactNode
206
+ renderCardPreview?: (card: KanbanCard) => React.ReactNode
207
+ renderCardBadge?: (card: KanbanCard) => React.ReactNode
208
+ renderCardActions?: (card: KanbanCard) => React.ReactNode
209
+ cardCompactMode?: boolean
210
+ cardShowCoverImage?: boolean
211
+ cardShowAssignees?: boolean
212
+ cardShowLabels?: boolean
213
+ cardShowProgress?: boolean
214
+ cardDateFormat?: string
215
+ cardMaxAssigneesToShow?: number
216
+ provided?: any
217
+ enableAnimations?: boolean
218
+ animationDuration?: number
219
+ cardVariant?: 'default' | 'bordered' | 'elevated' | 'flat'
187
220
  }) => {
188
221
  const [isEditingTitle, setIsEditingTitle] = useState(false)
189
222
  const [title, setTitle] = useState(card.title)
190
223
  const dragControls = useDragControls()
224
+
225
+ // Default values
226
+ const animationsEnabled = enableAnimations ?? true
227
+ const animDuration = animationDuration ?? 0.2
228
+ const variant = cardVariant ?? 'default'
191
229
 
192
230
  const completedChecklistItems = card.checklist?.items.filter(item => item.completed).length || 0
193
231
  const totalChecklistItems = card.checklist?.items.length || 0
194
232
  const checklistProgress = totalChecklistItems > 0 ? (completedChecklistItems / totalChecklistItems) * 100 : 0
195
233
 
234
+ // If custom render function is provided, use it
235
+ if (renderCard) {
236
+ return renderCard(card, column, provided || {})
237
+ }
238
+
196
239
  return (
197
240
  <motion.div
198
241
  layout
199
- initial={{ opacity: 0, y: 20 }}
242
+ initial={{ opacity: animationsEnabled ? 0 : 1, y: animationsEnabled ? 20 : 0 }}
200
243
  animate={{ opacity: 1, y: 0 }}
201
- exit={{ opacity: 0, y: -20 }}
202
- whileHover={{ scale: 1.02 }}
203
- whileDrag={{ scale: 1.05, rotate: 3 }}
204
- transition={{ duration: 0.2 }}
244
+ exit={{ opacity: animationsEnabled ? 0 : 1, y: animationsEnabled ? -20 : 0 }}
245
+ whileHover={animationsEnabled ? { scale: 1.02 } : {}}
246
+ whileDrag={animationsEnabled ? { scale: 1.05, rotate: 3 } : {}}
247
+ transition={{ duration: animationsEnabled ? animDuration : 0 }}
205
248
  className={cn(
206
249
  "relative group cursor-pointer select-none",
207
250
  isDragging && "z-50"
@@ -209,7 +252,11 @@ const KanbanCardComponent = ({
209
252
  >
210
253
  <Card
211
254
  className={cn(
212
- "border hover:shadow-md transition-all duration-200",
255
+ "border transition-all duration-200",
256
+ variant === 'bordered' && "border-2",
257
+ variant === 'elevated' && "shadow-lg hover:shadow-xl",
258
+ variant === 'flat' && "border-0 bg-muted/30",
259
+ variant === 'default' && "hover:shadow-md",
213
260
  isDragging && "shadow-2xl ring-2 ring-primary ring-offset-2",
214
261
  disabled && "cursor-not-allowed opacity-50"
215
262
  )}
@@ -222,7 +269,7 @@ const KanbanCardComponent = ({
222
269
  />
223
270
 
224
271
  {/* Cover image */}
225
- {card.coverImage && (
272
+ {cardShowCoverImage && card.coverImage && (
226
273
  <div className="relative h-32 -mx-px -mt-px rounded-t-lg overflow-hidden">
227
274
  <img
228
275
  src={card.coverImage}
@@ -235,7 +282,7 @@ const KanbanCardComponent = ({
235
282
 
236
283
  <CardContent className="p-3">
237
284
  {/* Labels */}
238
- {card.labels && card.labels.length > 0 && (
285
+ {cardShowLabels && card.labels && card.labels.length > 0 && (
239
286
  <div className="flex flex-wrap gap-1 mb-2">
240
287
  {card.labels.map((label) => (
241
288
  <div
@@ -276,27 +323,32 @@ const KanbanCardComponent = ({
276
323
  </div>
277
324
 
278
325
  {/* Quick actions */}
279
- <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
280
- <DropdownMenu>
281
- <DropdownMenuTrigger asChild>
282
- <Button
283
- variant="ghost"
284
- size="sm"
285
- className="h-6 w-6 p-0"
286
- onClick={(e) => e.stopPropagation()}
287
- >
288
- <MoreVertical className="h-3 w-3" />
289
- </Button>
290
- </DropdownMenuTrigger>
291
- <DropdownMenuContent align="end" className="w-48">
292
- <DropdownMenuItem onClick={onEdit}>
293
- <Edit className="mr-2 h-4 w-4" />
294
- Edit
295
- </DropdownMenuItem>
296
- <DropdownMenuItem>
297
- <Copy className="mr-2 h-4 w-4" />
298
- Duplicate
299
- </DropdownMenuItem>
326
+ {renderCardActions ? (
327
+ <div className="opacity-0 group-hover:opacity-100 transition-opacity">
328
+ {renderCardActions(card)}
329
+ </div>
330
+ ) : (
331
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
332
+ <DropdownMenu>
333
+ <DropdownMenuTrigger asChild>
334
+ <Button
335
+ variant="ghost"
336
+ size="sm"
337
+ className="h-6 w-6 p-0"
338
+ onClick={(e) => e.stopPropagation()}
339
+ >
340
+ <MoreVertical className="h-3 w-3" />
341
+ </Button>
342
+ </DropdownMenuTrigger>
343
+ <DropdownMenuContent align="end" className="w-48">
344
+ <DropdownMenuItem onClick={onEdit}>
345
+ <Edit className="mr-2 h-4 w-4" />
346
+ Edit
347
+ </DropdownMenuItem>
348
+ <DropdownMenuItem>
349
+ <Copy className="mr-2 h-4 w-4" />
350
+ Duplicate
351
+ </DropdownMenuItem>
300
352
  <DropdownMenuItem>
301
353
  <Move className="mr-2 h-4 w-4" />
302
354
  Move
@@ -313,20 +365,21 @@ const KanbanCardComponent = ({
313
365
  <Trash2 className="mr-2 h-4 w-4" />
314
366
  Delete
315
367
  </DropdownMenuItem>
316
- </DropdownMenuContent>
317
- </DropdownMenu>
318
- </div>
368
+ </DropdownMenuContent>
369
+ </DropdownMenu>
370
+ </div>
371
+ )}
319
372
  </div>
320
373
 
321
374
  {/* Description */}
322
- {card.description && (
375
+ {!cardCompactMode && card.description && (
323
376
  <p className="text-xs text-muted-foreground mb-3 line-clamp-2">
324
377
  {card.description}
325
378
  </p>
326
379
  )}
327
380
 
328
381
  {/* Progress bar */}
329
- {(card.progress !== undefined || card.checklist) && (
382
+ {cardShowProgress && (card.progress !== undefined || card.checklist) && (
330
383
  <div className="mb-3">
331
384
  <div className="flex justify-between text-xs text-muted-foreground mb-1">
332
385
  <span>Progress</span>
@@ -337,7 +390,7 @@ const KanbanCardComponent = ({
337
390
  )}
338
391
 
339
392
  {/* Tags */}
340
- {card.tags && card.tags.length > 0 && (
393
+ {!cardCompactMode && card.tags && card.tags.length > 0 && (
341
394
  <div className="flex flex-wrap gap-1 mb-3">
342
395
  {card.tags.map((tag, index) => (
343
396
  <Badge key={index} variant="secondary" className="text-xs px-1.5 py-0">
@@ -366,7 +419,7 @@ const KanbanCardComponent = ({
366
419
  isOverdue(card.dueDate) && "text-destructive"
367
420
  )}>
368
421
  <Calendar className="h-3 w-3" />
369
- <span>{formatDate(card.dueDate)}</span>
422
+ <span>{cardDateFormat ? format(card.dueDate, cardDateFormat) : formatDate(card.dueDate)}</span>
370
423
  </div>
371
424
  )}
372
425
 
@@ -397,8 +450,8 @@ const KanbanCardComponent = ({
397
450
  )}
398
451
 
399
452
  {/* Assignees */}
400
- {card.assignees && card.assignees.length > 0 && (
401
- <MoonUIAvatarGroupPro max={3} size="xs">
453
+ {cardShowAssignees && card.assignees && card.assignees.length > 0 && (
454
+ <MoonUIAvatarGroupPro max={cardMaxAssigneesToShow} size="xs">
402
455
  {card.assignees.map((assignee) => (
403
456
  <MoonUIAvatarPro key={assignee.id} className="h-5 w-5">
404
457
  <MoonUIAvatarImagePro src={assignee.avatar} />
@@ -445,7 +498,54 @@ export function Kanban({
445
498
  loading = false,
446
499
  disabled = false,
447
500
  labels = [],
448
- users = []
501
+ users = [],
502
+ // Card Render Customization
503
+ renderCard,
504
+ renderCardPreview,
505
+ renderCardBadge,
506
+ renderCardActions,
507
+ cardCompactMode = false,
508
+ cardShowCoverImage = true,
509
+ cardShowAssignees = true,
510
+ cardShowLabels = true,
511
+ cardShowProgress = true,
512
+ cardDateFormat,
513
+ cardMaxAssigneesToShow = 3,
514
+ // Add Card Customization
515
+ renderAddCardButton,
516
+ renderAddCardForm,
517
+ addCardButtonText,
518
+ addCardPosition = 'bottom',
519
+ allowQuickAdd = true,
520
+ quickAddFields = ['title'],
521
+ validateCard,
522
+ onBeforeCardAdd,
523
+ // Column Customization
524
+ renderColumnHeader,
525
+ renderColumnFooter,
526
+ renderEmptyColumn,
527
+ columnMenuActions,
528
+ allowColumnReorder = true,
529
+ columnColorOptions,
530
+ columnDefaultColor = '#6B7280',
531
+ // Drag & Drop Enhancement
532
+ dragDisabled = false,
533
+ dropDisabled = false,
534
+ dragPreview = 'card',
535
+ renderDragPreview,
536
+ canDrop,
537
+ onDragStart,
538
+ onDragEnd,
539
+ // UI/UX Customization
540
+ theme = 'default',
541
+ cardVariant = 'default',
542
+ enableAnimations = true,
543
+ animationDuration = 0.2,
544
+ columnWidth,
545
+ columnGap = 24,
546
+ cardGap = 12,
547
+ showTooltips = true,
548
+ tooltipDelay = 500
449
549
  }: KanbanProps) {
450
550
  // Check pro access
451
551
  const { hasProAccess, isLoading } = useSubscription()
@@ -579,11 +679,22 @@ export function Kanban({
579
679
  // Drag handlers
580
680
  const handleDragStart = (card: KanbanCard, columnId: string) => {
581
681
  if (disabled) return
682
+ if (typeof dragDisabled === 'function' && dragDisabled(card)) return
683
+ if (dragDisabled === true) return
684
+
582
685
  setDraggedCard(card.id)
686
+ const column = columns.find(col => col.id === columnId)
687
+ if (column && onDragStart) {
688
+ onDragStart(card, column)
689
+ }
583
690
  }
584
691
 
585
692
  const handleDragOver = (e: React.DragEvent, columnId: string) => {
586
693
  if (disabled) return
694
+ const column = columns.find(col => col.id === columnId)
695
+ if (column && typeof dropDisabled === 'function' && dropDisabled(column)) return
696
+ if (dropDisabled === true) return
697
+
587
698
  e.preventDefault()
588
699
  setDraggedOverColumn(columnId)
589
700
 
@@ -604,6 +715,13 @@ export function Kanban({
604
715
  }
605
716
 
606
717
  const handleDragEnd = () => {
718
+ if (draggedCard) {
719
+ const card = columns.flatMap(col => col.cards).find(c => c.id === draggedCard)
720
+ const column = columns.find(col => col.cards.some(c => c.id === draggedCard))
721
+ if (card && column && onDragEnd) {
722
+ onDragEnd(card, column)
723
+ }
724
+ }
607
725
  setDraggedCard(null)
608
726
  setDraggedOverColumn(null)
609
727
  stopAutoScroll()
@@ -612,6 +730,13 @@ export function Kanban({
612
730
  const handleDrop = (e: React.DragEvent, targetColumnId: string, targetIndex: number) => {
613
731
  if (disabled || !draggedCard || !onCardMove) return
614
732
  e.preventDefault()
733
+
734
+ const targetColumn = columns.find(col => col.id === targetColumnId)
735
+ const draggedCardObj = columns.flatMap(col => col.cards).find(card => card.id === draggedCard)
736
+
737
+ if (targetColumn && draggedCardObj && canDrop && !canDrop(draggedCardObj, targetColumn, targetIndex)) {
738
+ return
739
+ }
615
740
 
616
741
  // Find source column and card
617
742
  let sourceColumnId: string | null = null
@@ -1017,7 +1142,8 @@ export function Kanban({
1017
1142
  {/* Kanban board */}
1018
1143
  <div
1019
1144
  ref={scrollRef}
1020
- className="flex gap-6 overflow-x-auto pb-4"
1145
+ className="flex overflow-x-auto pb-4"
1146
+ style={{ gap: `${columnGap}px` }}
1021
1147
  onDragOver={(e) => e.preventDefault()}
1022
1148
  >
1023
1149
  <AnimatePresence mode="sync">
@@ -1028,14 +1154,19 @@ export function Kanban({
1028
1154
  return (
1029
1155
  <motion.div
1030
1156
  key={column.id}
1031
- layout
1032
- initial={{ opacity: 0, x: -20 }}
1157
+ layout={enableAnimations}
1158
+ initial={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? -20 : 0 }}
1033
1159
  animate={{ opacity: 1, x: 0 }}
1034
- exit={{ opacity: 0, x: 20 }}
1160
+ exit={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? 20 : 0 }}
1035
1161
  className={cn(
1036
- "flex-shrink-0 w-80 transition-all duration-200",
1162
+ "flex-shrink-0 transition-all",
1037
1163
  isDraggedOver && "scale-105"
1038
1164
  )}
1165
+ style={{
1166
+ width: columnWidth === 'auto' ? 'auto' : (columnWidth || 320) + 'px',
1167
+ minWidth: columnWidth === 'auto' ? '300px' : undefined,
1168
+ transitionDuration: enableAnimations ? `${animationDuration}s` : '0s'
1169
+ }}
1039
1170
  onDragOver={(e) => handleDragOver(e, column.id)}
1040
1171
  onDragLeave={() => setDraggedOverColumn(null)}
1041
1172
  onDrop={(e) => handleDrop(e, column.id, column.cards.length)}
@@ -1046,149 +1177,178 @@ export function Kanban({
1046
1177
  column.collapsed && "opacity-60"
1047
1178
  )}>
1048
1179
  <CardHeader className="pb-3">
1049
- <div className="flex items-center justify-between">
1050
- <div className="flex items-center gap-2">
1051
- {/* Column color indicator */}
1052
- {column.color && (
1053
- <div
1054
- className="w-3 h-3 rounded-full"
1055
- style={{ backgroundColor: column.color }}
1056
- />
1057
- )}
1058
-
1059
- {/* Column title */}
1060
- <CardTitle className="text-sm font-medium flex items-center gap-2">
1061
- {editingColumnId === column.id ? (
1062
- <Input
1063
- value={editingColumnTitle}
1064
- onChange={(e) => setEditingColumnTitle(e.target.value)}
1065
- onBlur={() => handleColumnRename(column.id)}
1066
- onKeyDown={(e) => {
1067
- if (e.key === 'Enter') {
1068
- handleColumnRename(column.id)
1069
- }
1070
- if (e.key === 'Escape') {
1071
- setEditingColumnId(null)
1072
- setEditingColumnTitle('')
1073
- }
1074
- }}
1075
- className="h-6 w-32 text-sm"
1076
- autoFocus
1077
- onClick={(e) => e.stopPropagation()}
1078
- />
1079
- ) : (
1080
- <>
1081
- {column.title}
1082
- {column.locked && <Lock className="h-3 w-3" />}
1083
- </>
1084
- )}
1085
- </CardTitle>
1086
-
1087
- {/* Card count */}
1088
- <Badge variant="secondary" className="text-xs">
1089
- {column.cards.length}
1090
- </Badge>
1091
- </div>
1092
-
1093
- {/* Column actions */}
1094
- <DropdownMenu>
1095
- <DropdownMenuTrigger asChild>
1096
- <Button variant="ghost" size="sm" className="h-6 w-6 p-0">
1097
- <MoreHorizontal className="h-4 w-4" />
1098
- </Button>
1099
- </DropdownMenuTrigger>
1100
- <DropdownMenuContent align="end">
1101
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'rename')}>
1102
- <Edit className="mr-2 h-4 w-4" />
1103
- Rename
1104
- </DropdownMenuItem>
1105
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'collapse')}>
1106
- {column.collapsed ? (
1107
- <>
1108
- <Eye className="mr-2 h-4 w-4" />
1109
- Expand
1110
- </>
1111
- ) : (
1112
- <>
1113
- <EyeOff className="mr-2 h-4 w-4" />
1114
- Collapse
1115
- </>
1180
+ {renderColumnHeader ? (
1181
+ renderColumnHeader(column)
1182
+ ) : (
1183
+ <>
1184
+ <div className="flex items-center justify-between">
1185
+ <div className="flex items-center gap-2">
1186
+ {/* Column color indicator */}
1187
+ {column.color && (
1188
+ <div
1189
+ className="w-3 h-3 rounded-full"
1190
+ style={{ backgroundColor: column.color }}
1191
+ />
1116
1192
  )}
1117
- </DropdownMenuItem>
1118
- <DropdownMenuSeparator />
1119
- <DropdownMenuSub>
1120
- <DropdownMenuSubTrigger>
1121
- <Settings className="mr-2 h-4 w-4" />
1122
- Settings
1123
- </DropdownMenuSubTrigger>
1124
- <DropdownMenuSubContent>
1125
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'setLimit')}>
1126
- <Timer className="mr-2 h-4 w-4" />
1127
- Set WIP limit
1128
- </DropdownMenuItem>
1129
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'changeColor')}>
1130
- <Palette className="mr-2 h-4 w-4" />
1131
- Change color
1132
- </DropdownMenuItem>
1133
- <DropdownMenuSub>
1134
- <DropdownMenuSubTrigger>
1135
- <ArrowUpDown className="mr-2 h-4 w-4" />
1136
- Sort cards
1137
- </DropdownMenuSubTrigger>
1138
- <DropdownMenuSubContent>
1139
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByPriority')}>
1140
- By Priority
1193
+
1194
+ {/* Column title */}
1195
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
1196
+ {editingColumnId === column.id ? (
1197
+ <Input
1198
+ value={editingColumnTitle}
1199
+ onChange={(e) => setEditingColumnTitle(e.target.value)}
1200
+ onBlur={() => handleColumnRename(column.id)}
1201
+ onKeyDown={(e) => {
1202
+ if (e.key === 'Enter') {
1203
+ handleColumnRename(column.id)
1204
+ }
1205
+ if (e.key === 'Escape') {
1206
+ setEditingColumnId(null)
1207
+ setEditingColumnTitle('')
1208
+ }
1209
+ }}
1210
+ className="h-6 w-32 text-sm"
1211
+ autoFocus
1212
+ onClick={(e) => e.stopPropagation()}
1213
+ />
1214
+ ) : (
1215
+ <>
1216
+ {column.title}
1217
+ {column.locked && <Lock className="h-3 w-3" />}
1218
+ </>
1219
+ )}
1220
+ </CardTitle>
1221
+
1222
+ {/* Card count */}
1223
+ <Badge variant="secondary" className="text-xs">
1224
+ {column.cards.length}
1225
+ </Badge>
1226
+ </div>
1227
+
1228
+ {/* Column actions */}
1229
+ <DropdownMenu>
1230
+ <DropdownMenuTrigger asChild>
1231
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0">
1232
+ <MoreHorizontal className="h-4 w-4" />
1233
+ </Button>
1234
+ </DropdownMenuTrigger>
1235
+ <DropdownMenuContent align="end">
1236
+ {columnMenuActions ? (
1237
+ columnMenuActions.map((action, index) => {
1238
+ if (action.visible && !action.visible(column)) return null
1239
+ return (
1240
+ <DropdownMenuItem
1241
+ key={index}
1242
+ onClick={() => action.action(column)}
1243
+ >
1244
+ {action.icon || null}
1245
+ {action.label}
1246
+ </DropdownMenuItem>
1247
+ )
1248
+ })
1249
+ ) : (
1250
+ <>
1251
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'rename')}>
1252
+ <Edit className="mr-2 h-4 w-4" />
1253
+ Rename
1141
1254
  </DropdownMenuItem>
1142
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByDueDate')}>
1143
- By Due Date
1255
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'collapse')}>
1256
+ {column.collapsed ? (
1257
+ <>
1258
+ <Eye className="mr-2 h-4 w-4" />
1259
+ Expand
1260
+ </>
1261
+ ) : (
1262
+ <>
1263
+ <EyeOff className="mr-2 h-4 w-4" />
1264
+ Collapse
1265
+ </>
1266
+ )}
1144
1267
  </DropdownMenuItem>
1145
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortAlphabetically')}>
1146
- Alphabetically
1268
+ <DropdownMenuSeparator />
1269
+ <DropdownMenuSub>
1270
+ <DropdownMenuSubTrigger>
1271
+ <Settings className="mr-2 h-4 w-4" />
1272
+ Settings
1273
+ </DropdownMenuSubTrigger>
1274
+ <DropdownMenuSubContent>
1275
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'setLimit')}>
1276
+ <Timer className="mr-2 h-4 w-4" />
1277
+ Set WIP limit
1278
+ </DropdownMenuItem>
1279
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'changeColor')}>
1280
+ <Palette className="mr-2 h-4 w-4" />
1281
+ Change color
1282
+ </DropdownMenuItem>
1283
+ <DropdownMenuSub>
1284
+ <DropdownMenuSubTrigger>
1285
+ <ArrowUpDown className="mr-2 h-4 w-4" />
1286
+ Sort cards
1287
+ </DropdownMenuSubTrigger>
1288
+ <DropdownMenuSubContent>
1289
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByPriority')}>
1290
+ By Priority
1291
+ </DropdownMenuItem>
1292
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByDueDate')}>
1293
+ By Due Date
1294
+ </DropdownMenuItem>
1295
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortAlphabetically')}>
1296
+ Alphabetically
1297
+ </DropdownMenuItem>
1298
+ </DropdownMenuSubContent>
1299
+ </DropdownMenuSub>
1300
+ </DropdownMenuSubContent>
1301
+ </DropdownMenuSub>
1302
+ <DropdownMenuSeparator />
1303
+ <DropdownMenuItem
1304
+ onClick={() => handleColumnAction(column, 'delete')}
1305
+ className="text-destructive"
1306
+ >
1307
+ <Trash2 className="mr-2 h-4 w-4" />
1308
+ Delete column
1147
1309
  </DropdownMenuItem>
1148
- </DropdownMenuSubContent>
1149
- </DropdownMenuSub>
1150
- </DropdownMenuSubContent>
1151
- </DropdownMenuSub>
1152
- <DropdownMenuSeparator />
1153
- <DropdownMenuItem
1154
- onClick={() => handleColumnAction(column, 'delete')}
1155
- className="text-destructive"
1156
- >
1157
- <Trash2 className="mr-2 h-4 w-4" />
1158
- Delete column
1159
- </DropdownMenuItem>
1160
- </DropdownMenuContent>
1161
- </DropdownMenu>
1162
- </div>
1163
-
1164
- {/* WIP limit warning */}
1165
- {column.limit && (
1166
- <CardDescription className={cn(
1167
- "text-xs flex items-center gap-1 mt-1",
1168
- isOverLimit && "text-destructive"
1169
- )}>
1170
- {isOverLimit ? (
1171
- <>
1172
- <AlertCircle className="h-3 w-3" />
1173
- Over WIP limit ({column.cards.length}/{column.limit})
1174
- </>
1175
- ) : (
1176
- `WIP limit: ${column.cards.length}/${column.limit}`
1310
+ </>
1311
+ )}
1312
+ </DropdownMenuContent>
1313
+ </DropdownMenu>
1314
+ </div>
1315
+
1316
+ {/* WIP limit warning */}
1317
+ {column.limit && (
1318
+ <CardDescription className={cn(
1319
+ "text-xs flex items-center gap-1 mt-1",
1320
+ isOverLimit && "text-destructive"
1321
+ )}>
1322
+ {isOverLimit ? (
1323
+ <>
1324
+ <AlertCircle className="h-3 w-3" />
1325
+ Over WIP limit ({column.cards.length}/{column.limit})
1326
+ </>
1327
+ ) : (
1328
+ `WIP limit: ${column.cards.length}/${column.limit}`
1329
+ )}
1330
+ </CardDescription>
1177
1331
  )}
1178
- </CardDescription>
1332
+ </>
1179
1333
  )}
1180
1334
  </CardHeader>
1181
1335
 
1182
1336
  {!column.collapsed && (
1183
- <CardContent className="space-y-3">
1337
+ <CardContent className="space-y-3" style={{ gap: `${cardGap}px` }}>
1184
1338
  <ScrollArea className="h-[calc(100vh-300px)]">
1185
- <AnimatePresence mode="popLayout">
1186
- {column.cards
1187
- .sort((a, b) => a.position - b.position)
1188
- .map((card, index) => (
1339
+ {column.cards.length === 0 && renderEmptyColumn ? (
1340
+ renderEmptyColumn(column)
1341
+ ) : (
1342
+ <AnimatePresence mode={enableAnimations ? "popLayout" : undefined}>
1343
+ {column.cards
1344
+ .sort((a, b) => a.position - b.position)
1345
+ .map((card, index) => (
1189
1346
  <div
1190
1347
  key={card.id}
1191
- draggable={!disabled}
1348
+ draggable={
1349
+ !disabled &&
1350
+ (typeof dragDisabled === 'function' ? !dragDisabled(card) : !dragDisabled)
1351
+ }
1192
1352
  onDragStart={() => handleDragStart(card, column.id)}
1193
1353
  onDragEnd={handleDragEnd}
1194
1354
  onDrop={(e) => {
@@ -1200,6 +1360,7 @@ export function Kanban({
1200
1360
  >
1201
1361
  <KanbanCardComponent
1202
1362
  card={card}
1363
+ column={column}
1203
1364
  isDragging={draggedCard === card.id}
1204
1365
  onEdit={(e) => {
1205
1366
  e.stopPropagation()
@@ -1212,45 +1373,66 @@ export function Kanban({
1212
1373
  onClick={() => handleCardClick(card)}
1213
1374
  showDetails={showCardDetails}
1214
1375
  disabled={disabled}
1376
+ renderCard={renderCard}
1377
+ renderCardPreview={renderCardPreview}
1378
+ renderCardBadge={renderCardBadge}
1379
+ renderCardActions={renderCardActions}
1380
+ cardCompactMode={cardCompactMode}
1381
+ cardShowCoverImage={cardShowCoverImage}
1382
+ cardShowAssignees={cardShowAssignees}
1383
+ cardShowLabels={cardShowLabels}
1384
+ cardShowProgress={cardShowProgress}
1385
+ cardDateFormat={cardDateFormat}
1386
+ cardMaxAssigneesToShow={cardMaxAssigneesToShow}
1387
+ enableAnimations={enableAnimations}
1388
+ animationDuration={animationDuration}
1389
+ cardVariant={cardVariant}
1215
1390
  />
1216
1391
  </div>
1217
1392
  ))}
1218
- </AnimatePresence>
1393
+ </AnimatePresence>
1394
+ )}
1219
1395
  </ScrollArea>
1220
1396
 
1221
1397
  {/* Add card button */}
1222
1398
  {onAddCard && !column.locked && !isOverLimit && (
1223
- <DropdownMenu>
1224
- <DropdownMenuTrigger asChild>
1225
- <Button
1226
- variant="ghost"
1227
- size="sm"
1228
- className="w-full justify-start text-muted-foreground hover:text-foreground"
1229
- disabled={disabled}
1230
- >
1231
- <Plus className="h-4 w-4 mr-2" />
1232
- Add card
1233
- </Button>
1234
- </DropdownMenuTrigger>
1235
- <DropdownMenuContent align="start" className="w-48">
1236
- <DropdownMenuLabel>Card Templates</DropdownMenuLabel>
1237
- <DropdownMenuSeparator />
1238
- <DropdownMenuItem onClick={() => handleAddCard(column.id)}>
1239
- <FileText className="mr-2 h-4 w-4" />
1240
- Blank card
1241
- </DropdownMenuItem>
1242
- {cardTemplates.map((template, index) => (
1243
- <DropdownMenuItem
1244
- key={index}
1245
- onClick={() => handleAddCard(column.id, template)}
1399
+ renderAddCardButton ? (
1400
+ renderAddCardButton(column.id)
1401
+ ) : (
1402
+ <DropdownMenu>
1403
+ <DropdownMenuTrigger asChild>
1404
+ <Button
1405
+ variant="ghost"
1406
+ size="sm"
1407
+ className="w-full justify-start text-muted-foreground hover:text-foreground"
1408
+ disabled={disabled}
1246
1409
  >
1247
- <Star className="mr-2 h-4 w-4" />
1248
- {template.title || `Template ${index + 1}`}
1410
+ <Plus className="h-4 w-4 mr-2" />
1411
+ {typeof addCardButtonText === 'function' ? addCardButtonText(column.id) : (addCardButtonText || 'Add card')}
1412
+ </Button>
1413
+ </DropdownMenuTrigger>
1414
+ <DropdownMenuContent align="start" className="w-48">
1415
+ <DropdownMenuLabel>Card Templates</DropdownMenuLabel>
1416
+ <DropdownMenuSeparator />
1417
+ <DropdownMenuItem onClick={() => handleAddCard(column.id)}>
1418
+ <FileText className="mr-2 h-4 w-4" />
1419
+ Blank card
1249
1420
  </DropdownMenuItem>
1250
- ))}
1251
- </DropdownMenuContent>
1252
- </DropdownMenu>
1421
+ {cardTemplates.map((template, index) => (
1422
+ <DropdownMenuItem
1423
+ key={index}
1424
+ onClick={() => handleAddCard(column.id, template)}
1425
+ >
1426
+ <Star className="mr-2 h-4 w-4" />
1427
+ {template.title || `Template ${index + 1}`}
1428
+ </DropdownMenuItem>
1429
+ ))}
1430
+ </DropdownMenuContent>
1431
+ </DropdownMenu>
1432
+ )
1253
1433
  )}
1434
+ {/* Column footer */}
1435
+ {renderColumnFooter && renderColumnFooter(column)}
1254
1436
  </CardContent>
1255
1437
  )}
1256
1438
  </Card>