@moontra/moonui-pro 2.12.0 → 2.13.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.
@@ -0,0 +1,13 @@
1
+ export { Kanban } from './kanban'
2
+ export { CardDetailModal } from './card-detail-modal'
3
+ export { AddCardModal } from './add-card-modal'
4
+ export type {
5
+ KanbanCard,
6
+ KanbanColumn,
7
+ KanbanProps,
8
+ KanbanAssignee,
9
+ KanbanLabel,
10
+ KanbanFilter,
11
+ KanbanActivity,
12
+ KanbanChecklist
13
+ } from './types'
@@ -65,119 +65,22 @@ 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 type {
73
+ KanbanAssignee,
74
+ KanbanLabel,
75
+ KanbanChecklist,
76
+ KanbanActivity,
77
+ KanbanCard,
78
+ KanbanColumn,
79
+ KanbanFilter,
80
+ KanbanProps
81
+ } from './types'
82
+ import { useToast } from '../../hooks/use-toast'
68
83
 
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
84
 
182
85
  // Constants
183
86
  const PRIORITY_CONFIG = {
@@ -584,8 +487,21 @@ export function Kanban({
584
487
  const [draggedOverColumn, setDraggedOverColumn] = useState<string | null>(null)
585
488
  const [isCreatingColumn, setIsCreatingColumn] = useState(false)
586
489
  const [newColumnTitle, setNewColumnTitle] = useState('')
490
+
491
+ // Modal states
492
+ const [selectedCard, setSelectedCard] = useState<KanbanCard | null>(null)
493
+ const [addCardColumnId, setAddCardColumnId] = useState<string | null>(null)
494
+ const [editingColumnId, setEditingColumnId] = useState<string | null>(null)
495
+ const [editingColumnTitle, setEditingColumnTitle] = useState('')
496
+ const [wipLimitModalOpen, setWipLimitModalOpen] = useState(false)
497
+ const [wipLimitColumnId, setWipLimitColumnId] = useState<string | null>(null)
498
+ const [wipLimit, setWipLimit] = useState<number | undefined>()
499
+ const [colorPickerOpen, setColorPickerOpen] = useState(false)
500
+ const [colorPickerColumnId, setColorPickerColumnId] = useState<string | null>(null)
501
+ const [selectedColor, setSelectedColor] = useState('#6B7280')
587
502
 
588
503
  const { scrollRef, startAutoScroll, stopAutoScroll } = useAutoScroll()
504
+ const { toast } = useToast()
589
505
 
590
506
  // Filter cards based on search and filters
591
507
  const filteredColumns = useMemo(() => {
@@ -729,17 +645,221 @@ export function Kanban({
729
645
  const handleColumnAction = (column: KanbanColumn, action: string) => {
730
646
  switch (action) {
731
647
  case 'rename':
732
- // Implement inline rename
648
+ setEditingColumnId(column.id)
649
+ setEditingColumnTitle(column.title)
733
650
  break
734
651
  case 'delete':
735
652
  onColumnDelete?.(column.id)
653
+ toast({
654
+ title: "Column deleted",
655
+ description: `"${column.title}" has been deleted`
656
+ })
736
657
  break
737
658
  case 'collapse':
738
- onColumnUpdate?.({ ...column, collapsed: !column.collapsed })
659
+ const updatedColumn = { ...column, collapsed: !column.collapsed }
660
+ onColumnUpdate?.(updatedColumn)
661
+ setColumns(columns.map(col => col.id === column.id ? updatedColumn : col))
739
662
  break
740
663
  case 'setLimit':
741
- // Implement WIP limit dialog
664
+ setWipLimitColumnId(column.id)
665
+ setWipLimit(column.limit)
666
+ setWipLimitModalOpen(true)
667
+ break
668
+ case 'changeColor':
669
+ setColorPickerColumnId(column.id)
670
+ setSelectedColor(column.color || '#6B7280')
671
+ setColorPickerOpen(true)
742
672
  break
673
+ case 'sortByPriority':
674
+ const sortedCards = [...column.cards].sort((a, b) => {
675
+ const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 }
676
+ return (priorityOrder[b.priority || 'medium'] || 2) - (priorityOrder[a.priority || 'medium'] || 2)
677
+ })
678
+ const sortedColumn = { ...column, cards: sortedCards }
679
+ onColumnUpdate?.(sortedColumn)
680
+ setColumns(columns.map(col => col.id === column.id ? sortedColumn : col))
681
+ toast({
682
+ title: "Cards sorted",
683
+ description: "Cards sorted by priority"
684
+ })
685
+ break
686
+ case 'sortByDueDate':
687
+ const dateCards = [...column.cards].sort((a, b) => {
688
+ if (!a.dueDate && !b.dueDate) return 0
689
+ if (!a.dueDate) return 1
690
+ if (!b.dueDate) return -1
691
+ return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
692
+ })
693
+ const dateColumn = { ...column, cards: dateCards }
694
+ onColumnUpdate?.(dateColumn)
695
+ setColumns(columns.map(col => col.id === column.id ? dateColumn : col))
696
+ toast({
697
+ title: "Cards sorted",
698
+ description: "Cards sorted by due date"
699
+ })
700
+ break
701
+ case 'sortAlphabetically':
702
+ const alphaCards = [...column.cards].sort((a, b) => a.title.localeCompare(b.title))
703
+ const alphaColumn = { ...column, cards: alphaCards }
704
+ onColumnUpdate?.(alphaColumn)
705
+ setColumns(columns.map(col => col.id === column.id ? alphaColumn : col))
706
+ toast({
707
+ title: "Cards sorted",
708
+ description: "Cards sorted alphabetically"
709
+ })
710
+ break
711
+ }
712
+ }
713
+
714
+ // Card handlers
715
+ const handleCardClick = (card: KanbanCard) => {
716
+ if (onCardClick) {
717
+ onCardClick(card)
718
+ } else {
719
+ setSelectedCard(card)
720
+ }
721
+ }
722
+
723
+ const handleCardUpdate = (updatedCard: KanbanCard) => {
724
+ onCardUpdate?.(updatedCard)
725
+ setColumns(columns.map(col => ({
726
+ ...col,
727
+ cards: col.cards.map(card => card.id === updatedCard.id ? updatedCard : card)
728
+ })))
729
+ }
730
+
731
+ const handleAddCard = (columnId: string, newCard?: Partial<KanbanCard>) => {
732
+ if (onAddCard) {
733
+ onAddCard(columnId, newCard)
734
+ } else {
735
+ setAddCardColumnId(columnId)
736
+ }
737
+ }
738
+
739
+ const handleAddNewCard = (card: Partial<KanbanCard>) => {
740
+ if (!addCardColumnId) return
741
+
742
+ const newCard: KanbanCard = {
743
+ id: Date.now().toString(),
744
+ title: card.title || 'New Card',
745
+ position: Date.now(),
746
+ ...card
747
+ }
748
+
749
+ setColumns(columns.map(col => {
750
+ if (col.id === addCardColumnId) {
751
+ return {
752
+ ...col,
753
+ cards: [...col.cards, newCard]
754
+ }
755
+ }
756
+ return col
757
+ }))
758
+
759
+ onAddCard?.(addCardColumnId, newCard)
760
+ toast({
761
+ title: "Card added",
762
+ description: `"${newCard.title}" has been added`
763
+ })
764
+ }
765
+
766
+ // Column updates
767
+ const handleColumnRename = (columnId: string) => {
768
+ const column = columns.find(col => col.id === columnId)
769
+ if (!column || !editingColumnTitle.trim()) return
770
+
771
+ const updatedColumn = { ...column, title: editingColumnTitle.trim() }
772
+ onColumnUpdate?.(updatedColumn)
773
+ setColumns(columns.map(col => col.id === columnId ? updatedColumn : col))
774
+ setEditingColumnId(null)
775
+ setEditingColumnTitle('')
776
+
777
+ toast({
778
+ title: "Column renamed",
779
+ description: `Column renamed to "${editingColumnTitle.trim()}"`
780
+ })
781
+ }
782
+
783
+ const handleWipLimitUpdate = () => {
784
+ const column = columns.find(col => col.id === wipLimitColumnId)
785
+ if (!column) return
786
+
787
+ const updatedColumn = { ...column, limit: wipLimit }
788
+ onColumnUpdate?.(updatedColumn)
789
+ setColumns(columns.map(col => col.id === wipLimitColumnId ? updatedColumn : col))
790
+ setWipLimitModalOpen(false)
791
+
792
+ toast({
793
+ title: "WIP limit updated",
794
+ description: wipLimit ? `WIP limit set to ${wipLimit}` : "WIP limit removed"
795
+ })
796
+ }
797
+
798
+ const handleColorUpdate = () => {
799
+ const column = columns.find(col => col.id === colorPickerColumnId)
800
+ if (!column) return
801
+
802
+ const updatedColumn = { ...column, color: selectedColor }
803
+ onColumnUpdate?.(updatedColumn)
804
+ setColumns(columns.map(col => col.id === colorPickerColumnId ? updatedColumn : col))
805
+ setColorPickerOpen(false)
806
+
807
+ toast({
808
+ title: "Column color updated",
809
+ description: "Column color has been changed"
810
+ })
811
+ }
812
+
813
+ // Export functionality
814
+ const handleExport = (format: 'json' | 'csv') => {
815
+ if (onExport) {
816
+ onExport(format)
817
+ } else {
818
+ if (format === 'json') {
819
+ const data = JSON.stringify(columns, null, 2)
820
+ const blob = new Blob([data], { type: 'application/json' })
821
+ const url = URL.createObjectURL(blob)
822
+ const a = document.createElement('a')
823
+ a.href = url
824
+ a.download = 'kanban-board.json'
825
+ document.body.appendChild(a)
826
+ a.click()
827
+ document.body.removeChild(a)
828
+ URL.revokeObjectURL(url)
829
+
830
+ toast({
831
+ title: "Board exported",
832
+ description: "Board exported as JSON file"
833
+ })
834
+ } else if (format === 'csv') {
835
+ let csv = 'Column,Card Title,Description,Priority,Assignees,Due Date,Tags\n'
836
+ columns.forEach(column => {
837
+ column.cards.forEach(card => {
838
+ csv += `"${column.title}",`
839
+ csv += `"${card.title}",`
840
+ csv += `"${card.description || ''}",`
841
+ csv += `"${card.priority || ''}",`
842
+ csv += `"${card.assignees?.map(a => a.name).join(', ') || ''}",`
843
+ csv += `"${card.dueDate ? new Date(card.dueDate).toLocaleDateString() : ''}",`
844
+ csv += `"${card.tags?.join(', ') || ''}"\n`
845
+ })
846
+ })
847
+
848
+ const blob = new Blob([csv], { type: 'text/csv' })
849
+ const url = URL.createObjectURL(blob)
850
+ const a = document.createElement('a')
851
+ a.href = url
852
+ a.download = 'kanban-board.csv'
853
+ document.body.appendChild(a)
854
+ a.click()
855
+ document.body.removeChild(a)
856
+ URL.revokeObjectURL(url)
857
+
858
+ toast({
859
+ title: "Board exported",
860
+ description: "Board exported as CSV file"
861
+ })
862
+ }
743
863
  }
744
864
  }
745
865
 
@@ -855,8 +975,7 @@ export function Kanban({
855
975
  )}
856
976
 
857
977
  {/* Export */}
858
- {onExport && (
859
- <DropdownMenu>
978
+ <DropdownMenu>
860
979
  <DropdownMenuTrigger asChild>
861
980
  <Button variant="outline" size="sm">
862
981
  <Download className="mr-2 h-4 w-4" />
@@ -864,15 +983,14 @@ export function Kanban({
864
983
  </Button>
865
984
  </DropdownMenuTrigger>
866
985
  <DropdownMenuContent align="end">
867
- <DropdownMenuItem onClick={() => onExport('json')}>
986
+ <DropdownMenuItem onClick={() => handleExport('json')}>
868
987
  Export as JSON
869
988
  </DropdownMenuItem>
870
- <DropdownMenuItem onClick={() => onExport('csv')}>
989
+ <DropdownMenuItem onClick={() => handleExport('csv')}>
871
990
  Export as CSV
872
991
  </DropdownMenuItem>
873
992
  </DropdownMenuContent>
874
993
  </DropdownMenu>
875
- )}
876
994
  </div>
877
995
  </div>
878
996
 
@@ -940,8 +1058,30 @@ export function Kanban({
940
1058
 
941
1059
  {/* Column title */}
942
1060
  <CardTitle className="text-sm font-medium flex items-center gap-2">
943
- {column.title}
944
- {column.locked && <Lock className="h-3 w-3" />}
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
+ )}
945
1085
  </CardTitle>
946
1086
 
947
1087
  {/* Card count */}
@@ -986,14 +1126,27 @@ export function Kanban({
986
1126
  <Timer className="mr-2 h-4 w-4" />
987
1127
  Set WIP limit
988
1128
  </DropdownMenuItem>
989
- <DropdownMenuItem>
1129
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'changeColor')}>
990
1130
  <Palette className="mr-2 h-4 w-4" />
991
1131
  Change color
992
1132
  </DropdownMenuItem>
993
- <DropdownMenuItem>
994
- <ArrowUpDown className="mr-2 h-4 w-4" />
995
- Sort cards
996
- </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
1141
+ </DropdownMenuItem>
1142
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByDueDate')}>
1143
+ By Due Date
1144
+ </DropdownMenuItem>
1145
+ <DropdownMenuItem onClick={() => handleColumnAction(column, 'sortAlphabetically')}>
1146
+ Alphabetically
1147
+ </DropdownMenuItem>
1148
+ </DropdownMenuSubContent>
1149
+ </DropdownMenuSub>
997
1150
  </DropdownMenuSubContent>
998
1151
  </DropdownMenuSub>
999
1152
  <DropdownMenuSeparator />
@@ -1056,7 +1209,7 @@ export function Kanban({
1056
1209
  e.stopPropagation()
1057
1210
  onCardDelete?.(card)
1058
1211
  }}
1059
- onClick={() => onCardClick?.(card)}
1212
+ onClick={() => handleCardClick(card)}
1060
1213
  showDetails={showCardDetails}
1061
1214
  disabled={disabled}
1062
1215
  />
@@ -1082,14 +1235,14 @@ export function Kanban({
1082
1235
  <DropdownMenuContent align="start" className="w-48">
1083
1236
  <DropdownMenuLabel>Card Templates</DropdownMenuLabel>
1084
1237
  <DropdownMenuSeparator />
1085
- <DropdownMenuItem onClick={() => onAddCard(column.id)}>
1238
+ <DropdownMenuItem onClick={() => handleAddCard(column.id)}>
1086
1239
  <FileText className="mr-2 h-4 w-4" />
1087
1240
  Blank card
1088
1241
  </DropdownMenuItem>
1089
1242
  {cardTemplates.map((template, index) => (
1090
1243
  <DropdownMenuItem
1091
1244
  key={index}
1092
- onClick={() => onAddCard(column.id, template)}
1245
+ onClick={() => handleAddCard(column.id, template)}
1093
1246
  >
1094
1247
  <Star className="mr-2 h-4 w-4" />
1095
1248
  {template.title || `Template ${index + 1}`}
@@ -1207,8 +1360,115 @@ export function Kanban({
1207
1360
  </motion.div>
1208
1361
  )}
1209
1362
  </div>
1363
+
1364
+ {/* Modals */}
1365
+ {/* Card Detail Modal */}
1366
+ {selectedCard && (
1367
+ <CardDetailModal
1368
+ card={selectedCard}
1369
+ isOpen={!!selectedCard}
1370
+ onClose={() => setSelectedCard(null)}
1371
+ onUpdate={handleCardUpdate}
1372
+ onDelete={(card) => {
1373
+ onCardDelete?.(card)
1374
+ toast({
1375
+ title: "Card deleted",
1376
+ description: `"${card.title}" has been deleted`
1377
+ })
1378
+ }}
1379
+ availableAssignees={users}
1380
+ availableLabels={labels}
1381
+ currentColumn={columns.find(col => col.cards.some(c => c.id === selectedCard.id))?.title}
1382
+ availableColumns={columns.map(col => ({ id: col.id, title: col.title }))}
1383
+ />
1384
+ )}
1385
+
1386
+ {/* Add Card Modal */}
1387
+ {addCardColumnId && (
1388
+ <AddCardModal
1389
+ isOpen={!!addCardColumnId}
1390
+ onClose={() => setAddCardColumnId(null)}
1391
+ onAdd={handleAddNewCard}
1392
+ columnId={addCardColumnId}
1393
+ columnTitle={columns.find(col => col.id === addCardColumnId)?.title || ''}
1394
+ availableAssignees={users}
1395
+ availableLabels={labels}
1396
+ templates={cardTemplates}
1397
+ />
1398
+ )}
1399
+
1400
+ {/* WIP Limit Modal */}
1401
+ <Dialog open={wipLimitModalOpen} onOpenChange={setWipLimitModalOpen}>
1402
+ <DialogContent className="sm:max-w-md">
1403
+ <DialogHeader>
1404
+ <DialogTitle>Set WIP Limit</DialogTitle>
1405
+ </DialogHeader>
1406
+ <div className="space-y-4 py-4">
1407
+ <div className="space-y-2">
1408
+ <Label htmlFor="wip-limit">Work In Progress Limit</Label>
1409
+ <Input
1410
+ id="wip-limit"
1411
+ type="number"
1412
+ min="0"
1413
+ value={wipLimit || ''}
1414
+ onChange={(e) => setWipLimit(e.target.value ? parseInt(e.target.value) : undefined)}
1415
+ placeholder="Enter a number (leave empty to remove limit)"
1416
+ />
1417
+ <p className="text-sm text-muted-foreground">
1418
+ Set a maximum number of cards allowed in this column. Leave empty to remove the limit.
1419
+ </p>
1420
+ </div>
1421
+ </div>
1422
+ <DialogFooter>
1423
+ <Button variant="outline" onClick={() => setWipLimitModalOpen(false)}>
1424
+ Cancel
1425
+ </Button>
1426
+ <Button onClick={handleWipLimitUpdate}>
1427
+ Save
1428
+ </Button>
1429
+ </DialogFooter>
1430
+ </DialogContent>
1431
+ </Dialog>
1432
+
1433
+ {/* Color Picker Modal */}
1434
+ <Dialog open={colorPickerOpen} onOpenChange={setColorPickerOpen}>
1435
+ <DialogContent className="sm:max-w-md">
1436
+ <DialogHeader>
1437
+ <DialogTitle>Change Column Color</DialogTitle>
1438
+ </DialogHeader>
1439
+ <div className="space-y-4 py-4">
1440
+ <div className="space-y-2">
1441
+ <Label>Select a color</Label>
1442
+ <div className="grid grid-cols-8 gap-2">
1443
+ {[
1444
+ '#6B7280', '#EF4444', '#F59E0B', '#10B981',
1445
+ '#3B82F6', '#8B5CF6', '#EC4899', '#06B6D4',
1446
+ '#F43F5E', '#84CC16', '#14B8A6', '#6366F1',
1447
+ '#A855F7', '#F472B6', '#0EA5E9', '#22D3EE'
1448
+ ].map(color => (
1449
+ <button
1450
+ key={color}
1451
+ className={cn(
1452
+ "w-10 h-10 rounded-md border-2 transition-all",
1453
+ selectedColor === color ? "border-primary scale-110" : "border-transparent"
1454
+ )}
1455
+ style={{ backgroundColor: color }}
1456
+ onClick={() => setSelectedColor(color)}
1457
+ />
1458
+ ))}
1459
+ </div>
1460
+ </div>
1461
+ </div>
1462
+ <DialogFooter>
1463
+ <Button variant="outline" onClick={() => setColorPickerOpen(false)}>
1464
+ Cancel
1465
+ </Button>
1466
+ <Button onClick={handleColorUpdate}>
1467
+ Save
1468
+ </Button>
1469
+ </DialogFooter>
1470
+ </DialogContent>
1471
+ </Dialog>
1210
1472
  </div>
1211
1473
  )
1212
- }
1213
-
1214
- export default Kanban
1474
+ }