@moontra/moonui-pro 2.12.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.
@@ -65,119 +65,23 @@ import {
65
65
  } from 'lucide-react'
66
66
  import { cn } from '../../lib/utils'
67
67
  import { useSubscription } from '../../hooks/use-subscription'
68
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog'
69
+ import { ColorPicker } from '../ui/color-picker'
70
+ import { CardDetailModal } from './card-detail-modal'
71
+ import { AddCardModal } from './add-card-modal'
72
+ import { format } from 'date-fns'
73
+ import type {
74
+ KanbanAssignee,
75
+ KanbanLabel,
76
+ KanbanChecklist,
77
+ KanbanActivity,
78
+ KanbanCard,
79
+ KanbanColumn,
80
+ KanbanFilter,
81
+ KanbanProps
82
+ } from './types'
83
+ import { useToast } from '../../hooks/use-toast'
68
84
 
69
- // Enhanced types
70
- interface KanbanAssignee {
71
- id: string
72
- name: string
73
- avatar?: string
74
- email?: string
75
- }
76
-
77
- interface KanbanLabel {
78
- id: string
79
- name: string
80
- color: string
81
- }
82
-
83
- interface KanbanChecklist {
84
- id: string
85
- title: string
86
- items: {
87
- id: string
88
- text: string
89
- completed: boolean
90
- }[]
91
- }
92
-
93
- interface KanbanActivity {
94
- id: string
95
- user: KanbanAssignee
96
- action: string
97
- timestamp: Date
98
- details?: string
99
- }
100
-
101
- interface KanbanCard {
102
- id: string
103
- title: string
104
- description?: string
105
- coverImage?: string
106
- assignees?: KanbanAssignee[]
107
- dueDate?: Date
108
- startDate?: Date
109
- priority?: 'low' | 'medium' | 'high' | 'urgent'
110
- tags?: string[]
111
- labels?: KanbanLabel[]
112
- attachments?: {
113
- id: string
114
- name: string
115
- type: string
116
- url: string
117
- size: number
118
- }[]
119
- comments?: number
120
- completed?: boolean
121
- progress?: number
122
- checklist?: KanbanChecklist
123
- activities?: KanbanActivity[]
124
- customFields?: Record<string, any>
125
- position: number
126
- }
127
-
128
- interface KanbanColumn {
129
- id: string
130
- title: string
131
- color?: string
132
- cards: KanbanCard[]
133
- limit?: number
134
- collapsed?: boolean
135
- locked?: boolean
136
- template?: 'todo' | 'inProgress' | 'done' | 'custom'
137
- }
138
-
139
- interface KanbanFilter {
140
- id: string
141
- name: string
142
- query: string
143
- assignees?: string[]
144
- labels?: string[]
145
- priority?: string[]
146
- tags?: string[]
147
- dueDate?: {
148
- from?: Date
149
- to?: Date
150
- }
151
- }
152
-
153
- interface KanbanProps {
154
- columns: KanbanColumn[]
155
- onCardMove?: (cardId: string, fromColumn: string, toColumn: string, newPosition: number) => void
156
- onCardClick?: (card: KanbanCard) => void
157
- onCardEdit?: (card: KanbanCard) => void
158
- onCardDelete?: (card: KanbanCard) => void
159
- onCardUpdate?: (card: KanbanCard) => void
160
- onAddCard?: (columnId: string, card?: Partial<KanbanCard>) => void
161
- onAddColumn?: (column?: Partial<KanbanColumn>) => void
162
- onColumnUpdate?: (column: KanbanColumn) => void
163
- onColumnDelete?: (columnId: string) => void
164
- onBulkAction?: (action: string, cardIds: string[]) => void
165
- onExport?: (format: 'json' | 'csv') => void
166
- className?: string
167
- showAddColumn?: boolean
168
- showCardDetails?: boolean
169
- showFilters?: boolean
170
- showSearch?: boolean
171
- enableKeyboardShortcuts?: boolean
172
- cardTemplates?: Partial<KanbanCard>[]
173
- columnTemplates?: Partial<KanbanColumn>[]
174
- filters?: KanbanFilter[]
175
- defaultFilter?: string
176
- loading?: boolean
177
- disabled?: boolean
178
- labels?: KanbanLabel[]
179
- users?: KanbanAssignee[]
180
- }
181
85
 
182
86
  // Constants
183
87
  const PRIORITY_CONFIG = {
@@ -267,38 +171,80 @@ const useAutoScroll = () => {
267
171
  // Card component
268
172
  const KanbanCardComponent = ({
269
173
  card,
174
+ column,
270
175
  isDragging,
271
176
  onEdit,
272
177
  onDelete,
273
178
  onClick,
274
179
  showDetails,
275
- 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
276
196
  }: {
277
197
  card: KanbanCard
198
+ column: KanbanColumn
278
199
  isDragging: boolean
279
200
  onEdit?: (e: React.MouseEvent) => void
280
201
  onDelete?: (e: React.MouseEvent) => void
281
202
  onClick?: () => void
282
203
  showDetails: boolean
283
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'
284
220
  }) => {
285
221
  const [isEditingTitle, setIsEditingTitle] = useState(false)
286
222
  const [title, setTitle] = useState(card.title)
287
223
  const dragControls = useDragControls()
224
+
225
+ // Default values
226
+ const animationsEnabled = enableAnimations ?? true
227
+ const animDuration = animationDuration ?? 0.2
228
+ const variant = cardVariant ?? 'default'
288
229
 
289
230
  const completedChecklistItems = card.checklist?.items.filter(item => item.completed).length || 0
290
231
  const totalChecklistItems = card.checklist?.items.length || 0
291
232
  const checklistProgress = totalChecklistItems > 0 ? (completedChecklistItems / totalChecklistItems) * 100 : 0
292
233
 
234
+ // If custom render function is provided, use it
235
+ if (renderCard) {
236
+ return renderCard(card, column, provided || {})
237
+ }
238
+
293
239
  return (
294
240
  <motion.div
295
241
  layout
296
- initial={{ opacity: 0, y: 20 }}
242
+ initial={{ opacity: animationsEnabled ? 0 : 1, y: animationsEnabled ? 20 : 0 }}
297
243
  animate={{ opacity: 1, y: 0 }}
298
- exit={{ opacity: 0, y: -20 }}
299
- whileHover={{ scale: 1.02 }}
300
- whileDrag={{ scale: 1.05, rotate: 3 }}
301
- 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 }}
302
248
  className={cn(
303
249
  "relative group cursor-pointer select-none",
304
250
  isDragging && "z-50"
@@ -306,7 +252,11 @@ const KanbanCardComponent = ({
306
252
  >
307
253
  <Card
308
254
  className={cn(
309
- "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",
310
260
  isDragging && "shadow-2xl ring-2 ring-primary ring-offset-2",
311
261
  disabled && "cursor-not-allowed opacity-50"
312
262
  )}
@@ -319,7 +269,7 @@ const KanbanCardComponent = ({
319
269
  />
320
270
 
321
271
  {/* Cover image */}
322
- {card.coverImage && (
272
+ {cardShowCoverImage && card.coverImage && (
323
273
  <div className="relative h-32 -mx-px -mt-px rounded-t-lg overflow-hidden">
324
274
  <img
325
275
  src={card.coverImage}
@@ -332,7 +282,7 @@ const KanbanCardComponent = ({
332
282
 
333
283
  <CardContent className="p-3">
334
284
  {/* Labels */}
335
- {card.labels && card.labels.length > 0 && (
285
+ {cardShowLabels && card.labels && card.labels.length > 0 && (
336
286
  <div className="flex flex-wrap gap-1 mb-2">
337
287
  {card.labels.map((label) => (
338
288
  <div
@@ -373,27 +323,32 @@ const KanbanCardComponent = ({
373
323
  </div>
374
324
 
375
325
  {/* Quick actions */}
376
- <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
377
- <DropdownMenu>
378
- <DropdownMenuTrigger asChild>
379
- <Button
380
- variant="ghost"
381
- size="sm"
382
- className="h-6 w-6 p-0"
383
- onClick={(e) => e.stopPropagation()}
384
- >
385
- <MoreVertical className="h-3 w-3" />
386
- </Button>
387
- </DropdownMenuTrigger>
388
- <DropdownMenuContent align="end" className="w-48">
389
- <DropdownMenuItem onClick={onEdit}>
390
- <Edit className="mr-2 h-4 w-4" />
391
- Edit
392
- </DropdownMenuItem>
393
- <DropdownMenuItem>
394
- <Copy className="mr-2 h-4 w-4" />
395
- Duplicate
396
- </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>
397
352
  <DropdownMenuItem>
398
353
  <Move className="mr-2 h-4 w-4" />
399
354
  Move
@@ -410,20 +365,21 @@ const KanbanCardComponent = ({
410
365
  <Trash2 className="mr-2 h-4 w-4" />
411
366
  Delete
412
367
  </DropdownMenuItem>
413
- </DropdownMenuContent>
414
- </DropdownMenu>
415
- </div>
368
+ </DropdownMenuContent>
369
+ </DropdownMenu>
370
+ </div>
371
+ )}
416
372
  </div>
417
373
 
418
374
  {/* Description */}
419
- {card.description && (
375
+ {!cardCompactMode && card.description && (
420
376
  <p className="text-xs text-muted-foreground mb-3 line-clamp-2">
421
377
  {card.description}
422
378
  </p>
423
379
  )}
424
380
 
425
381
  {/* Progress bar */}
426
- {(card.progress !== undefined || card.checklist) && (
382
+ {cardShowProgress && (card.progress !== undefined || card.checklist) && (
427
383
  <div className="mb-3">
428
384
  <div className="flex justify-between text-xs text-muted-foreground mb-1">
429
385
  <span>Progress</span>
@@ -434,7 +390,7 @@ const KanbanCardComponent = ({
434
390
  )}
435
391
 
436
392
  {/* Tags */}
437
- {card.tags && card.tags.length > 0 && (
393
+ {!cardCompactMode && card.tags && card.tags.length > 0 && (
438
394
  <div className="flex flex-wrap gap-1 mb-3">
439
395
  {card.tags.map((tag, index) => (
440
396
  <Badge key={index} variant="secondary" className="text-xs px-1.5 py-0">
@@ -463,7 +419,7 @@ const KanbanCardComponent = ({
463
419
  isOverdue(card.dueDate) && "text-destructive"
464
420
  )}>
465
421
  <Calendar className="h-3 w-3" />
466
- <span>{formatDate(card.dueDate)}</span>
422
+ <span>{cardDateFormat ? format(card.dueDate, cardDateFormat) : formatDate(card.dueDate)}</span>
467
423
  </div>
468
424
  )}
469
425
 
@@ -494,8 +450,8 @@ const KanbanCardComponent = ({
494
450
  )}
495
451
 
496
452
  {/* Assignees */}
497
- {card.assignees && card.assignees.length > 0 && (
498
- <MoonUIAvatarGroupPro max={3} size="xs">
453
+ {cardShowAssignees && card.assignees && card.assignees.length > 0 && (
454
+ <MoonUIAvatarGroupPro max={cardMaxAssigneesToShow} size="xs">
499
455
  {card.assignees.map((assignee) => (
500
456
  <MoonUIAvatarPro key={assignee.id} className="h-5 w-5">
501
457
  <MoonUIAvatarImagePro src={assignee.avatar} />
@@ -542,7 +498,54 @@ export function Kanban({
542
498
  loading = false,
543
499
  disabled = false,
544
500
  labels = [],
545
- 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
546
549
  }: KanbanProps) {
547
550
  // Check pro access
548
551
  const { hasProAccess, isLoading } = useSubscription()
@@ -584,8 +587,21 @@ export function Kanban({
584
587
  const [draggedOverColumn, setDraggedOverColumn] = useState<string | null>(null)
585
588
  const [isCreatingColumn, setIsCreatingColumn] = useState(false)
586
589
  const [newColumnTitle, setNewColumnTitle] = useState('')
590
+
591
+ // Modal states
592
+ const [selectedCard, setSelectedCard] = useState<KanbanCard | null>(null)
593
+ const [addCardColumnId, setAddCardColumnId] = useState<string | null>(null)
594
+ const [editingColumnId, setEditingColumnId] = useState<string | null>(null)
595
+ const [editingColumnTitle, setEditingColumnTitle] = useState('')
596
+ const [wipLimitModalOpen, setWipLimitModalOpen] = useState(false)
597
+ const [wipLimitColumnId, setWipLimitColumnId] = useState<string | null>(null)
598
+ const [wipLimit, setWipLimit] = useState<number | undefined>()
599
+ const [colorPickerOpen, setColorPickerOpen] = useState(false)
600
+ const [colorPickerColumnId, setColorPickerColumnId] = useState<string | null>(null)
601
+ const [selectedColor, setSelectedColor] = useState('#6B7280')
587
602
 
588
603
  const { scrollRef, startAutoScroll, stopAutoScroll } = useAutoScroll()
604
+ const { toast } = useToast()
589
605
 
590
606
  // Filter cards based on search and filters
591
607
  const filteredColumns = useMemo(() => {
@@ -663,11 +679,22 @@ export function Kanban({
663
679
  // Drag handlers
664
680
  const handleDragStart = (card: KanbanCard, columnId: string) => {
665
681
  if (disabled) return
682
+ if (typeof dragDisabled === 'function' && dragDisabled(card)) return
683
+ if (dragDisabled === true) return
684
+
666
685
  setDraggedCard(card.id)
686
+ const column = columns.find(col => col.id === columnId)
687
+ if (column && onDragStart) {
688
+ onDragStart(card, column)
689
+ }
667
690
  }
668
691
 
669
692
  const handleDragOver = (e: React.DragEvent, columnId: string) => {
670
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
+
671
698
  e.preventDefault()
672
699
  setDraggedOverColumn(columnId)
673
700
 
@@ -688,6 +715,13 @@ export function Kanban({
688
715
  }
689
716
 
690
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
+ }
691
725
  setDraggedCard(null)
692
726
  setDraggedOverColumn(null)
693
727
  stopAutoScroll()
@@ -696,6 +730,13 @@ export function Kanban({
696
730
  const handleDrop = (e: React.DragEvent, targetColumnId: string, targetIndex: number) => {
697
731
  if (disabled || !draggedCard || !onCardMove) return
698
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
+ }
699
740
 
700
741
  // Find source column and card
701
742
  let sourceColumnId: string | null = null
@@ -729,20 +770,224 @@ export function Kanban({
729
770
  const handleColumnAction = (column: KanbanColumn, action: string) => {
730
771
  switch (action) {
731
772
  case 'rename':
732
- // Implement inline rename
773
+ setEditingColumnId(column.id)
774
+ setEditingColumnTitle(column.title)
733
775
  break
734
776
  case 'delete':
735
777
  onColumnDelete?.(column.id)
778
+ toast({
779
+ title: "Column deleted",
780
+ description: `"${column.title}" has been deleted`
781
+ })
736
782
  break
737
783
  case 'collapse':
738
- onColumnUpdate?.({ ...column, collapsed: !column.collapsed })
784
+ const updatedColumn = { ...column, collapsed: !column.collapsed }
785
+ onColumnUpdate?.(updatedColumn)
786
+ setColumns(columns.map(col => col.id === column.id ? updatedColumn : col))
739
787
  break
740
788
  case 'setLimit':
741
- // Implement WIP limit dialog
789
+ setWipLimitColumnId(column.id)
790
+ setWipLimit(column.limit)
791
+ setWipLimitModalOpen(true)
792
+ break
793
+ case 'changeColor':
794
+ setColorPickerColumnId(column.id)
795
+ setSelectedColor(column.color || '#6B7280')
796
+ setColorPickerOpen(true)
797
+ break
798
+ case 'sortByPriority':
799
+ const sortedCards = [...column.cards].sort((a, b) => {
800
+ const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 }
801
+ return (priorityOrder[b.priority || 'medium'] || 2) - (priorityOrder[a.priority || 'medium'] || 2)
802
+ })
803
+ const sortedColumn = { ...column, cards: sortedCards }
804
+ onColumnUpdate?.(sortedColumn)
805
+ setColumns(columns.map(col => col.id === column.id ? sortedColumn : col))
806
+ toast({
807
+ title: "Cards sorted",
808
+ description: "Cards sorted by priority"
809
+ })
810
+ break
811
+ case 'sortByDueDate':
812
+ const dateCards = [...column.cards].sort((a, b) => {
813
+ if (!a.dueDate && !b.dueDate) return 0
814
+ if (!a.dueDate) return 1
815
+ if (!b.dueDate) return -1
816
+ return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
817
+ })
818
+ const dateColumn = { ...column, cards: dateCards }
819
+ onColumnUpdate?.(dateColumn)
820
+ setColumns(columns.map(col => col.id === column.id ? dateColumn : col))
821
+ toast({
822
+ title: "Cards sorted",
823
+ description: "Cards sorted by due date"
824
+ })
825
+ break
826
+ case 'sortAlphabetically':
827
+ const alphaCards = [...column.cards].sort((a, b) => a.title.localeCompare(b.title))
828
+ const alphaColumn = { ...column, cards: alphaCards }
829
+ onColumnUpdate?.(alphaColumn)
830
+ setColumns(columns.map(col => col.id === column.id ? alphaColumn : col))
831
+ toast({
832
+ title: "Cards sorted",
833
+ description: "Cards sorted alphabetically"
834
+ })
742
835
  break
743
836
  }
744
837
  }
745
838
 
839
+ // Card handlers
840
+ const handleCardClick = (card: KanbanCard) => {
841
+ if (onCardClick) {
842
+ onCardClick(card)
843
+ } else {
844
+ setSelectedCard(card)
845
+ }
846
+ }
847
+
848
+ const handleCardUpdate = (updatedCard: KanbanCard) => {
849
+ onCardUpdate?.(updatedCard)
850
+ setColumns(columns.map(col => ({
851
+ ...col,
852
+ cards: col.cards.map(card => card.id === updatedCard.id ? updatedCard : card)
853
+ })))
854
+ }
855
+
856
+ const handleAddCard = (columnId: string, newCard?: Partial<KanbanCard>) => {
857
+ if (onAddCard) {
858
+ onAddCard(columnId, newCard)
859
+ } else {
860
+ setAddCardColumnId(columnId)
861
+ }
862
+ }
863
+
864
+ const handleAddNewCard = (card: Partial<KanbanCard>) => {
865
+ if (!addCardColumnId) return
866
+
867
+ const newCard: KanbanCard = {
868
+ id: Date.now().toString(),
869
+ title: card.title || 'New Card',
870
+ position: Date.now(),
871
+ ...card
872
+ }
873
+
874
+ setColumns(columns.map(col => {
875
+ if (col.id === addCardColumnId) {
876
+ return {
877
+ ...col,
878
+ cards: [...col.cards, newCard]
879
+ }
880
+ }
881
+ return col
882
+ }))
883
+
884
+ onAddCard?.(addCardColumnId, newCard)
885
+ toast({
886
+ title: "Card added",
887
+ description: `"${newCard.title}" has been added`
888
+ })
889
+ }
890
+
891
+ // Column updates
892
+ const handleColumnRename = (columnId: string) => {
893
+ const column = columns.find(col => col.id === columnId)
894
+ if (!column || !editingColumnTitle.trim()) return
895
+
896
+ const updatedColumn = { ...column, title: editingColumnTitle.trim() }
897
+ onColumnUpdate?.(updatedColumn)
898
+ setColumns(columns.map(col => col.id === columnId ? updatedColumn : col))
899
+ setEditingColumnId(null)
900
+ setEditingColumnTitle('')
901
+
902
+ toast({
903
+ title: "Column renamed",
904
+ description: `Column renamed to "${editingColumnTitle.trim()}"`
905
+ })
906
+ }
907
+
908
+ const handleWipLimitUpdate = () => {
909
+ const column = columns.find(col => col.id === wipLimitColumnId)
910
+ if (!column) return
911
+
912
+ const updatedColumn = { ...column, limit: wipLimit }
913
+ onColumnUpdate?.(updatedColumn)
914
+ setColumns(columns.map(col => col.id === wipLimitColumnId ? updatedColumn : col))
915
+ setWipLimitModalOpen(false)
916
+
917
+ toast({
918
+ title: "WIP limit updated",
919
+ description: wipLimit ? `WIP limit set to ${wipLimit}` : "WIP limit removed"
920
+ })
921
+ }
922
+
923
+ const handleColorUpdate = () => {
924
+ const column = columns.find(col => col.id === colorPickerColumnId)
925
+ if (!column) return
926
+
927
+ const updatedColumn = { ...column, color: selectedColor }
928
+ onColumnUpdate?.(updatedColumn)
929
+ setColumns(columns.map(col => col.id === colorPickerColumnId ? updatedColumn : col))
930
+ setColorPickerOpen(false)
931
+
932
+ toast({
933
+ title: "Column color updated",
934
+ description: "Column color has been changed"
935
+ })
936
+ }
937
+
938
+ // Export functionality
939
+ const handleExport = (format: 'json' | 'csv') => {
940
+ if (onExport) {
941
+ onExport(format)
942
+ } else {
943
+ if (format === 'json') {
944
+ const data = JSON.stringify(columns, null, 2)
945
+ const blob = new Blob([data], { type: 'application/json' })
946
+ const url = URL.createObjectURL(blob)
947
+ const a = document.createElement('a')
948
+ a.href = url
949
+ a.download = 'kanban-board.json'
950
+ document.body.appendChild(a)
951
+ a.click()
952
+ document.body.removeChild(a)
953
+ URL.revokeObjectURL(url)
954
+
955
+ toast({
956
+ title: "Board exported",
957
+ description: "Board exported as JSON file"
958
+ })
959
+ } else if (format === 'csv') {
960
+ let csv = 'Column,Card Title,Description,Priority,Assignees,Due Date,Tags\n'
961
+ columns.forEach(column => {
962
+ column.cards.forEach(card => {
963
+ csv += `"${column.title}",`
964
+ csv += `"${card.title}",`
965
+ csv += `"${card.description || ''}",`
966
+ csv += `"${card.priority || ''}",`
967
+ csv += `"${card.assignees?.map(a => a.name).join(', ') || ''}",`
968
+ csv += `"${card.dueDate ? new Date(card.dueDate).toLocaleDateString() : ''}",`
969
+ csv += `"${card.tags?.join(', ') || ''}"\n`
970
+ })
971
+ })
972
+
973
+ const blob = new Blob([csv], { type: 'text/csv' })
974
+ const url = URL.createObjectURL(blob)
975
+ const a = document.createElement('a')
976
+ a.href = url
977
+ a.download = 'kanban-board.csv'
978
+ document.body.appendChild(a)
979
+ a.click()
980
+ document.body.removeChild(a)
981
+ URL.revokeObjectURL(url)
982
+
983
+ toast({
984
+ title: "Board exported",
985
+ description: "Board exported as CSV file"
986
+ })
987
+ }
988
+ }
989
+ }
990
+
746
991
  // Loading state
747
992
  if (loading) {
748
993
  return (
@@ -855,8 +1100,7 @@ export function Kanban({
855
1100
  )}
856
1101
 
857
1102
  {/* Export */}
858
- {onExport && (
859
- <DropdownMenu>
1103
+ <DropdownMenu>
860
1104
  <DropdownMenuTrigger asChild>
861
1105
  <Button variant="outline" size="sm">
862
1106
  <Download className="mr-2 h-4 w-4" />
@@ -864,15 +1108,14 @@ export function Kanban({
864
1108
  </Button>
865
1109
  </DropdownMenuTrigger>
866
1110
  <DropdownMenuContent align="end">
867
- <DropdownMenuItem onClick={() => onExport('json')}>
1111
+ <DropdownMenuItem onClick={() => handleExport('json')}>
868
1112
  Export as JSON
869
1113
  </DropdownMenuItem>
870
- <DropdownMenuItem onClick={() => onExport('csv')}>
1114
+ <DropdownMenuItem onClick={() => handleExport('csv')}>
871
1115
  Export as CSV
872
1116
  </DropdownMenuItem>
873
1117
  </DropdownMenuContent>
874
1118
  </DropdownMenu>
875
- )}
876
1119
  </div>
877
1120
  </div>
878
1121
 
@@ -899,7 +1142,8 @@ export function Kanban({
899
1142
  {/* Kanban board */}
900
1143
  <div
901
1144
  ref={scrollRef}
902
- className="flex gap-6 overflow-x-auto pb-4"
1145
+ className="flex overflow-x-auto pb-4"
1146
+ style={{ gap: `${columnGap}px` }}
903
1147
  onDragOver={(e) => e.preventDefault()}
904
1148
  >
905
1149
  <AnimatePresence mode="sync">
@@ -910,14 +1154,19 @@ export function Kanban({
910
1154
  return (
911
1155
  <motion.div
912
1156
  key={column.id}
913
- layout
914
- initial={{ opacity: 0, x: -20 }}
1157
+ layout={enableAnimations}
1158
+ initial={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? -20 : 0 }}
915
1159
  animate={{ opacity: 1, x: 0 }}
916
- exit={{ opacity: 0, x: 20 }}
1160
+ exit={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? 20 : 0 }}
917
1161
  className={cn(
918
- "flex-shrink-0 w-80 transition-all duration-200",
1162
+ "flex-shrink-0 transition-all",
919
1163
  isDraggedOver && "scale-105"
920
1164
  )}
1165
+ style={{
1166
+ width: columnWidth === 'auto' ? 'auto' : (columnWidth || 320) + 'px',
1167
+ minWidth: columnWidth === 'auto' ? '300px' : undefined,
1168
+ transitionDuration: enableAnimations ? `${animationDuration}s` : '0s'
1169
+ }}
921
1170
  onDragOver={(e) => handleDragOver(e, column.id)}
922
1171
  onDragLeave={() => setDraggedOverColumn(null)}
923
1172
  onDrop={(e) => handleDrop(e, column.id, column.cards.length)}
@@ -928,114 +1177,178 @@ export function Kanban({
928
1177
  column.collapsed && "opacity-60"
929
1178
  )}>
930
1179
  <CardHeader className="pb-3">
931
- <div className="flex items-center justify-between">
932
- <div className="flex items-center gap-2">
933
- {/* Column color indicator */}
934
- {column.color && (
935
- <div
936
- className="w-3 h-3 rounded-full"
937
- style={{ backgroundColor: column.color }}
938
- />
939
- )}
940
-
941
- {/* Column title */}
942
- <CardTitle className="text-sm font-medium flex items-center gap-2">
943
- {column.title}
944
- {column.locked && <Lock className="h-3 w-3" />}
945
- </CardTitle>
946
-
947
- {/* Card count */}
948
- <Badge variant="secondary" className="text-xs">
949
- {column.cards.length}
950
- </Badge>
951
- </div>
952
-
953
- {/* Column actions */}
954
- <DropdownMenu>
955
- <DropdownMenuTrigger asChild>
956
- <Button variant="ghost" size="sm" className="h-6 w-6 p-0">
957
- <MoreHorizontal className="h-4 w-4" />
958
- </Button>
959
- </DropdownMenuTrigger>
960
- <DropdownMenuContent align="end">
961
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'rename')}>
962
- <Edit className="mr-2 h-4 w-4" />
963
- Rename
964
- </DropdownMenuItem>
965
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'collapse')}>
966
- {column.collapsed ? (
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
+ />
1192
+ )}
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
1254
+ </DropdownMenuItem>
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
+ )}
1267
+ </DropdownMenuItem>
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
1309
+ </DropdownMenuItem>
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 ? (
967
1323
  <>
968
- <Eye className="mr-2 h-4 w-4" />
969
- Expand
1324
+ <AlertCircle className="h-3 w-3" />
1325
+ Over WIP limit ({column.cards.length}/{column.limit})
970
1326
  </>
971
1327
  ) : (
972
- <>
973
- <EyeOff className="mr-2 h-4 w-4" />
974
- Collapse
975
- </>
1328
+ `WIP limit: ${column.cards.length}/${column.limit}`
976
1329
  )}
977
- </DropdownMenuItem>
978
- <DropdownMenuSeparator />
979
- <DropdownMenuSub>
980
- <DropdownMenuSubTrigger>
981
- <Settings className="mr-2 h-4 w-4" />
982
- Settings
983
- </DropdownMenuSubTrigger>
984
- <DropdownMenuSubContent>
985
- <DropdownMenuItem onClick={() => handleColumnAction(column, 'setLimit')}>
986
- <Timer className="mr-2 h-4 w-4" />
987
- Set WIP limit
988
- </DropdownMenuItem>
989
- <DropdownMenuItem>
990
- <Palette className="mr-2 h-4 w-4" />
991
- Change color
992
- </DropdownMenuItem>
993
- <DropdownMenuItem>
994
- <ArrowUpDown className="mr-2 h-4 w-4" />
995
- Sort cards
996
- </DropdownMenuItem>
997
- </DropdownMenuSubContent>
998
- </DropdownMenuSub>
999
- <DropdownMenuSeparator />
1000
- <DropdownMenuItem
1001
- onClick={() => handleColumnAction(column, 'delete')}
1002
- className="text-destructive"
1003
- >
1004
- <Trash2 className="mr-2 h-4 w-4" />
1005
- Delete column
1006
- </DropdownMenuItem>
1007
- </DropdownMenuContent>
1008
- </DropdownMenu>
1009
- </div>
1010
-
1011
- {/* WIP limit warning */}
1012
- {column.limit && (
1013
- <CardDescription className={cn(
1014
- "text-xs flex items-center gap-1 mt-1",
1015
- isOverLimit && "text-destructive"
1016
- )}>
1017
- {isOverLimit ? (
1018
- <>
1019
- <AlertCircle className="h-3 w-3" />
1020
- Over WIP limit ({column.cards.length}/{column.limit})
1021
- </>
1022
- ) : (
1023
- `WIP limit: ${column.cards.length}/${column.limit}`
1330
+ </CardDescription>
1024
1331
  )}
1025
- </CardDescription>
1332
+ </>
1026
1333
  )}
1027
1334
  </CardHeader>
1028
1335
 
1029
1336
  {!column.collapsed && (
1030
- <CardContent className="space-y-3">
1337
+ <CardContent className="space-y-3" style={{ gap: `${cardGap}px` }}>
1031
1338
  <ScrollArea className="h-[calc(100vh-300px)]">
1032
- <AnimatePresence mode="popLayout">
1033
- {column.cards
1034
- .sort((a, b) => a.position - b.position)
1035
- .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) => (
1036
1346
  <div
1037
1347
  key={card.id}
1038
- draggable={!disabled}
1348
+ draggable={
1349
+ !disabled &&
1350
+ (typeof dragDisabled === 'function' ? !dragDisabled(card) : !dragDisabled)
1351
+ }
1039
1352
  onDragStart={() => handleDragStart(card, column.id)}
1040
1353
  onDragEnd={handleDragEnd}
1041
1354
  onDrop={(e) => {
@@ -1047,6 +1360,7 @@ export function Kanban({
1047
1360
  >
1048
1361
  <KanbanCardComponent
1049
1362
  card={card}
1363
+ column={column}
1050
1364
  isDragging={draggedCard === card.id}
1051
1365
  onEdit={(e) => {
1052
1366
  e.stopPropagation()
@@ -1056,48 +1370,69 @@ export function Kanban({
1056
1370
  e.stopPropagation()
1057
1371
  onCardDelete?.(card)
1058
1372
  }}
1059
- onClick={() => onCardClick?.(card)}
1373
+ onClick={() => handleCardClick(card)}
1060
1374
  showDetails={showCardDetails}
1061
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}
1062
1390
  />
1063
1391
  </div>
1064
1392
  ))}
1065
- </AnimatePresence>
1393
+ </AnimatePresence>
1394
+ )}
1066
1395
  </ScrollArea>
1067
1396
 
1068
1397
  {/* Add card button */}
1069
1398
  {onAddCard && !column.locked && !isOverLimit && (
1070
- <DropdownMenu>
1071
- <DropdownMenuTrigger asChild>
1072
- <Button
1073
- variant="ghost"
1074
- size="sm"
1075
- className="w-full justify-start text-muted-foreground hover:text-foreground"
1076
- disabled={disabled}
1077
- >
1078
- <Plus className="h-4 w-4 mr-2" />
1079
- Add card
1080
- </Button>
1081
- </DropdownMenuTrigger>
1082
- <DropdownMenuContent align="start" className="w-48">
1083
- <DropdownMenuLabel>Card Templates</DropdownMenuLabel>
1084
- <DropdownMenuSeparator />
1085
- <DropdownMenuItem onClick={() => onAddCard(column.id)}>
1086
- <FileText className="mr-2 h-4 w-4" />
1087
- Blank card
1088
- </DropdownMenuItem>
1089
- {cardTemplates.map((template, index) => (
1090
- <DropdownMenuItem
1091
- key={index}
1092
- onClick={() => onAddCard(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}
1093
1409
  >
1094
- <Star className="mr-2 h-4 w-4" />
1095
- {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
1096
1420
  </DropdownMenuItem>
1097
- ))}
1098
- </DropdownMenuContent>
1099
- </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
+ )
1100
1433
  )}
1434
+ {/* Column footer */}
1435
+ {renderColumnFooter && renderColumnFooter(column)}
1101
1436
  </CardContent>
1102
1437
  )}
1103
1438
  </Card>
@@ -1207,8 +1542,115 @@ export function Kanban({
1207
1542
  </motion.div>
1208
1543
  )}
1209
1544
  </div>
1545
+
1546
+ {/* Modals */}
1547
+ {/* Card Detail Modal */}
1548
+ {selectedCard && (
1549
+ <CardDetailModal
1550
+ card={selectedCard}
1551
+ isOpen={!!selectedCard}
1552
+ onClose={() => setSelectedCard(null)}
1553
+ onUpdate={handleCardUpdate}
1554
+ onDelete={(card) => {
1555
+ onCardDelete?.(card)
1556
+ toast({
1557
+ title: "Card deleted",
1558
+ description: `"${card.title}" has been deleted`
1559
+ })
1560
+ }}
1561
+ availableAssignees={users}
1562
+ availableLabels={labels}
1563
+ currentColumn={columns.find(col => col.cards.some(c => c.id === selectedCard.id))?.title}
1564
+ availableColumns={columns.map(col => ({ id: col.id, title: col.title }))}
1565
+ />
1566
+ )}
1567
+
1568
+ {/* Add Card Modal */}
1569
+ {addCardColumnId && (
1570
+ <AddCardModal
1571
+ isOpen={!!addCardColumnId}
1572
+ onClose={() => setAddCardColumnId(null)}
1573
+ onAdd={handleAddNewCard}
1574
+ columnId={addCardColumnId}
1575
+ columnTitle={columns.find(col => col.id === addCardColumnId)?.title || ''}
1576
+ availableAssignees={users}
1577
+ availableLabels={labels}
1578
+ templates={cardTemplates}
1579
+ />
1580
+ )}
1581
+
1582
+ {/* WIP Limit Modal */}
1583
+ <Dialog open={wipLimitModalOpen} onOpenChange={setWipLimitModalOpen}>
1584
+ <DialogContent className="sm:max-w-md">
1585
+ <DialogHeader>
1586
+ <DialogTitle>Set WIP Limit</DialogTitle>
1587
+ </DialogHeader>
1588
+ <div className="space-y-4 py-4">
1589
+ <div className="space-y-2">
1590
+ <Label htmlFor="wip-limit">Work In Progress Limit</Label>
1591
+ <Input
1592
+ id="wip-limit"
1593
+ type="number"
1594
+ min="0"
1595
+ value={wipLimit || ''}
1596
+ onChange={(e) => setWipLimit(e.target.value ? parseInt(e.target.value) : undefined)}
1597
+ placeholder="Enter a number (leave empty to remove limit)"
1598
+ />
1599
+ <p className="text-sm text-muted-foreground">
1600
+ Set a maximum number of cards allowed in this column. Leave empty to remove the limit.
1601
+ </p>
1602
+ </div>
1603
+ </div>
1604
+ <DialogFooter>
1605
+ <Button variant="outline" onClick={() => setWipLimitModalOpen(false)}>
1606
+ Cancel
1607
+ </Button>
1608
+ <Button onClick={handleWipLimitUpdate}>
1609
+ Save
1610
+ </Button>
1611
+ </DialogFooter>
1612
+ </DialogContent>
1613
+ </Dialog>
1614
+
1615
+ {/* Color Picker Modal */}
1616
+ <Dialog open={colorPickerOpen} onOpenChange={setColorPickerOpen}>
1617
+ <DialogContent className="sm:max-w-md">
1618
+ <DialogHeader>
1619
+ <DialogTitle>Change Column Color</DialogTitle>
1620
+ </DialogHeader>
1621
+ <div className="space-y-4 py-4">
1622
+ <div className="space-y-2">
1623
+ <Label>Select a color</Label>
1624
+ <div className="grid grid-cols-8 gap-2">
1625
+ {[
1626
+ '#6B7280', '#EF4444', '#F59E0B', '#10B981',
1627
+ '#3B82F6', '#8B5CF6', '#EC4899', '#06B6D4',
1628
+ '#F43F5E', '#84CC16', '#14B8A6', '#6366F1',
1629
+ '#A855F7', '#F472B6', '#0EA5E9', '#22D3EE'
1630
+ ].map(color => (
1631
+ <button
1632
+ key={color}
1633
+ className={cn(
1634
+ "w-10 h-10 rounded-md border-2 transition-all",
1635
+ selectedColor === color ? "border-primary scale-110" : "border-transparent"
1636
+ )}
1637
+ style={{ backgroundColor: color }}
1638
+ onClick={() => setSelectedColor(color)}
1639
+ />
1640
+ ))}
1641
+ </div>
1642
+ </div>
1643
+ </div>
1644
+ <DialogFooter>
1645
+ <Button variant="outline" onClick={() => setColorPickerOpen(false)}>
1646
+ Cancel
1647
+ </Button>
1648
+ <Button onClick={handleColorUpdate}>
1649
+ Save
1650
+ </Button>
1651
+ </DialogFooter>
1652
+ </DialogContent>
1653
+ </Dialog>
1210
1654
  </div>
1211
1655
  )
1212
- }
1213
-
1214
- export default Kanban
1656
+ }