@moontra/moonui-pro 2.20.2 → 2.20.3
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 +8 -3
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postinstall.js +176 -23
- package/src/components/advanced-chart/index.tsx +0 -1246
- package/src/components/advanced-forms/index.tsx +0 -585
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -377
- package/src/components/calendar/index.tsx +0 -1220
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -480
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -225
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
- package/src/components/dashboard/widgets/index.ts +0 -5
- package/src/components/dashboard/widgets/metric-card.tsx +0 -363
- package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/error-boundary/index.tsx +0 -232
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -335
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -77
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -517
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -515
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -529
- package/src/components/index.ts +0 -130
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1689
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -31
- package/src/components/magnetic-button/index.tsx +0 -216
- package/src/components/memory-efficient-data/index.tsx +0 -1018
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/navbar/index.tsx +0 -781
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -613
- package/src/components/performance-monitor/index.tsx +0 -808
- package/src/components/phone-number-input/index.tsx +0 -343
- package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index.tsx +0 -2322
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -884
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1183
- package/src/components/ui/accordion.tsx +0 -581
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -155
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/breadcrumb.tsx +0 -216
- package/src/components/ui/button.tsx +0 -228
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -216
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -631
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -948
- package/src/components/ui/dialog.tsx +0 -752
- package/src/components/ui/dropdown-menu.tsx +0 -706
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -222
- package/src/components/ui/input.tsx +0 -224
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -611
- package/src/components/ui/navigation-menu.tsx +0 -130
- package/src/components/ui/pagination.tsx +0 -125
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -378
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -331
- package/src/components/ui/tabs-pro.tsx +0 -542
- package/src/components/ui/tabs.tsx +0 -54
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -22
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -681
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- package/src/utils/license-validator.tsx +0 -183
|
@@ -1,1689 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
|
4
|
-
import { motion, AnimatePresence, Reorder, useDragControls } from 'framer-motion'
|
|
5
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
|
6
|
-
import { Button } from '../ui/button'
|
|
7
|
-
import { MoonUIBadgePro as Badge } from '../ui/badge'
|
|
8
|
-
import { MoonUIAvatarPro, MoonUIAvatarFallbackPro, MoonUIAvatarImagePro, AvatarGroup as MoonUIAvatarGroupPro } from '../ui/avatar'
|
|
9
|
-
import { Input } from '../ui/input'
|
|
10
|
-
import { Textarea } from '../ui/textarea'
|
|
11
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
|
|
12
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../ui/dropdown-menu'
|
|
13
|
-
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../ui/command'
|
|
14
|
-
import { ScrollArea } from '../ui/scroll-area'
|
|
15
|
-
import { Skeleton } from '../ui/skeleton'
|
|
16
|
-
import { Switch } from '../ui/switch'
|
|
17
|
-
import { Label } from '../ui/label'
|
|
18
|
-
import { Progress } from '../ui/progress'
|
|
19
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
|
|
20
|
-
import {
|
|
21
|
-
Plus,
|
|
22
|
-
MoreHorizontal,
|
|
23
|
-
User,
|
|
24
|
-
Calendar,
|
|
25
|
-
MessageCircle,
|
|
26
|
-
Paperclip,
|
|
27
|
-
Edit,
|
|
28
|
-
Trash2,
|
|
29
|
-
GripVertical,
|
|
30
|
-
Lock,
|
|
31
|
-
Sparkles,
|
|
32
|
-
Search,
|
|
33
|
-
Filter,
|
|
34
|
-
Download,
|
|
35
|
-
Upload,
|
|
36
|
-
Copy,
|
|
37
|
-
Move,
|
|
38
|
-
Archive,
|
|
39
|
-
Eye,
|
|
40
|
-
EyeOff,
|
|
41
|
-
ChevronDown,
|
|
42
|
-
ChevronRight,
|
|
43
|
-
X,
|
|
44
|
-
Check,
|
|
45
|
-
Clock,
|
|
46
|
-
AlertCircle,
|
|
47
|
-
Tag,
|
|
48
|
-
Users,
|
|
49
|
-
FileText,
|
|
50
|
-
Image,
|
|
51
|
-
Link2,
|
|
52
|
-
Activity,
|
|
53
|
-
Settings,
|
|
54
|
-
Palette,
|
|
55
|
-
Star,
|
|
56
|
-
Flag,
|
|
57
|
-
CheckSquare,
|
|
58
|
-
Square,
|
|
59
|
-
MoreVertical,
|
|
60
|
-
ArrowUpDown,
|
|
61
|
-
ArrowUp,
|
|
62
|
-
ArrowDown,
|
|
63
|
-
Zap,
|
|
64
|
-
Timer
|
|
65
|
-
} from 'lucide-react'
|
|
66
|
-
import { cn } from '../../lib/utils'
|
|
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'
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Constants
|
|
87
|
-
const PRIORITY_CONFIG = {
|
|
88
|
-
low: {
|
|
89
|
-
color: 'bg-green-100 text-green-800 border-green-200',
|
|
90
|
-
dot: 'bg-green-500',
|
|
91
|
-
icon: ArrowDown
|
|
92
|
-
},
|
|
93
|
-
medium: {
|
|
94
|
-
color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
|
95
|
-
dot: 'bg-yellow-500',
|
|
96
|
-
icon: ArrowUp
|
|
97
|
-
},
|
|
98
|
-
high: {
|
|
99
|
-
color: 'bg-orange-100 text-orange-800 border-orange-200',
|
|
100
|
-
dot: 'bg-orange-500',
|
|
101
|
-
icon: Zap
|
|
102
|
-
},
|
|
103
|
-
urgent: {
|
|
104
|
-
color: 'bg-red-100 text-red-800 border-red-200',
|
|
105
|
-
dot: 'bg-red-500',
|
|
106
|
-
icon: AlertCircle
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const COLUMN_TEMPLATES = {
|
|
111
|
-
todo: { title: 'To Do', color: '#6B7280' },
|
|
112
|
-
inProgress: { title: 'In Progress', color: '#3B82F6' },
|
|
113
|
-
done: { title: 'Done', color: '#10B981' }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Helper functions
|
|
117
|
-
const formatDate = (date: Date) => {
|
|
118
|
-
return date.toLocaleDateString('en-US', {
|
|
119
|
-
month: 'short',
|
|
120
|
-
day: 'numeric',
|
|
121
|
-
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const isOverdue = (dueDate: Date) => {
|
|
126
|
-
return dueDate < new Date()
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const getInitials = (name: string) => {
|
|
130
|
-
return name.split(' ').map(n => n[0]).join('').toUpperCase()
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const formatFileSize = (bytes: number) => {
|
|
134
|
-
if (bytes === 0) return '0 Bytes'
|
|
135
|
-
const k = 1024
|
|
136
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
137
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
138
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Custom hook for auto-scroll while dragging
|
|
142
|
-
const useAutoScroll = () => {
|
|
143
|
-
const scrollRef = useRef<HTMLDivElement>(null)
|
|
144
|
-
const scrollIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
|
145
|
-
|
|
146
|
-
const startAutoScroll = useCallback((direction: 'left' | 'right') => {
|
|
147
|
-
if (scrollIntervalRef.current) return
|
|
148
|
-
|
|
149
|
-
scrollIntervalRef.current = setInterval(() => {
|
|
150
|
-
if (scrollRef.current) {
|
|
151
|
-
const scrollAmount = direction === 'left' ? -10 : 10
|
|
152
|
-
scrollRef.current.scrollLeft += scrollAmount
|
|
153
|
-
}
|
|
154
|
-
}, 20)
|
|
155
|
-
}, [])
|
|
156
|
-
|
|
157
|
-
const stopAutoScroll = useCallback(() => {
|
|
158
|
-
if (scrollIntervalRef.current) {
|
|
159
|
-
clearInterval(scrollIntervalRef.current)
|
|
160
|
-
scrollIntervalRef.current = null
|
|
161
|
-
}
|
|
162
|
-
}, [])
|
|
163
|
-
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
return () => stopAutoScroll()
|
|
166
|
-
}, [stopAutoScroll])
|
|
167
|
-
|
|
168
|
-
return { scrollRef, startAutoScroll, stopAutoScroll }
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Card component
|
|
172
|
-
const KanbanCardComponent = ({
|
|
173
|
-
card,
|
|
174
|
-
column,
|
|
175
|
-
isDragging,
|
|
176
|
-
onEdit,
|
|
177
|
-
onDelete,
|
|
178
|
-
onClick,
|
|
179
|
-
showDetails,
|
|
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
|
|
196
|
-
}: {
|
|
197
|
-
card: KanbanCard
|
|
198
|
-
column: KanbanColumn
|
|
199
|
-
isDragging: boolean
|
|
200
|
-
onEdit?: (e: React.MouseEvent) => void
|
|
201
|
-
onDelete?: (e: React.MouseEvent) => void
|
|
202
|
-
onClick?: () => void
|
|
203
|
-
showDetails: boolean
|
|
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'
|
|
220
|
-
}) => {
|
|
221
|
-
const [isEditingTitle, setIsEditingTitle] = useState(false)
|
|
222
|
-
const [title, setTitle] = useState(card.title)
|
|
223
|
-
const dragControls = useDragControls()
|
|
224
|
-
|
|
225
|
-
// Default values
|
|
226
|
-
const animationsEnabled = enableAnimations ?? true
|
|
227
|
-
const animDuration = animationDuration ?? 0.2
|
|
228
|
-
const variant = cardVariant ?? 'default'
|
|
229
|
-
|
|
230
|
-
const completedChecklistItems = card.checklist?.items.filter(item => item.completed).length || 0
|
|
231
|
-
const totalChecklistItems = card.checklist?.items.length || 0
|
|
232
|
-
const checklistProgress = totalChecklistItems > 0 ? (completedChecklistItems / totalChecklistItems) * 100 : 0
|
|
233
|
-
|
|
234
|
-
// If custom render function is provided, use it
|
|
235
|
-
if (renderCard) {
|
|
236
|
-
return renderCard(card, column, provided || {})
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return (
|
|
240
|
-
<motion.div
|
|
241
|
-
layout
|
|
242
|
-
initial={{ opacity: animationsEnabled ? 0 : 1, y: animationsEnabled ? 20 : 0 }}
|
|
243
|
-
animate={{ opacity: 1, y: 0 }}
|
|
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 }}
|
|
248
|
-
className={cn(
|
|
249
|
-
"relative group cursor-pointer select-none",
|
|
250
|
-
isDragging && "z-50"
|
|
251
|
-
)}
|
|
252
|
-
>
|
|
253
|
-
<Card
|
|
254
|
-
className={cn(
|
|
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",
|
|
260
|
-
isDragging && "shadow-2xl ring-2 ring-primary ring-offset-2",
|
|
261
|
-
disabled && "cursor-not-allowed opacity-50"
|
|
262
|
-
)}
|
|
263
|
-
onClick={onClick}
|
|
264
|
-
>
|
|
265
|
-
{/* Drag handle */}
|
|
266
|
-
<div
|
|
267
|
-
className="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-primary/20 to-primary/10 opacity-0 group-hover:opacity-100 transition-opacity cursor-move"
|
|
268
|
-
onPointerDown={(e) => dragControls.start(e)}
|
|
269
|
-
/>
|
|
270
|
-
|
|
271
|
-
{/* Cover image */}
|
|
272
|
-
{cardShowCoverImage && card.coverImage && (
|
|
273
|
-
<div className="relative h-32 -mx-px -mt-px rounded-t-lg overflow-hidden">
|
|
274
|
-
<img
|
|
275
|
-
src={card.coverImage}
|
|
276
|
-
alt=""
|
|
277
|
-
className="w-full h-full object-cover"
|
|
278
|
-
/>
|
|
279
|
-
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
|
|
283
|
-
<CardContent className="p-3">
|
|
284
|
-
{/* Labels */}
|
|
285
|
-
{cardShowLabels && card.labels && card.labels.length > 0 && (
|
|
286
|
-
<div className="flex flex-wrap gap-1 mb-2">
|
|
287
|
-
{card.labels.map((label) => (
|
|
288
|
-
<div
|
|
289
|
-
key={label.id}
|
|
290
|
-
className="h-2 w-12 rounded-full"
|
|
291
|
-
style={{ backgroundColor: label.color }}
|
|
292
|
-
title={label.name}
|
|
293
|
-
/>
|
|
294
|
-
))}
|
|
295
|
-
</div>
|
|
296
|
-
)}
|
|
297
|
-
|
|
298
|
-
{/* Title and actions */}
|
|
299
|
-
<div className="flex items-start justify-between gap-2 mb-2">
|
|
300
|
-
<div className="flex-1">
|
|
301
|
-
{isEditingTitle ? (
|
|
302
|
-
<Input
|
|
303
|
-
value={title}
|
|
304
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
305
|
-
onBlur={() => setIsEditingTitle(false)}
|
|
306
|
-
onKeyDown={(e) => {
|
|
307
|
-
if (e.key === 'Enter') {
|
|
308
|
-
setIsEditingTitle(false)
|
|
309
|
-
// Call update handler
|
|
310
|
-
}
|
|
311
|
-
if (e.key === 'Escape') {
|
|
312
|
-
setTitle(card.title)
|
|
313
|
-
setIsEditingTitle(false)
|
|
314
|
-
}
|
|
315
|
-
}}
|
|
316
|
-
className="h-6 px-1 py-0 text-sm font-medium"
|
|
317
|
-
autoFocus
|
|
318
|
-
onClick={(e) => e.stopPropagation()}
|
|
319
|
-
/>
|
|
320
|
-
) : (
|
|
321
|
-
<h4 className="font-medium text-sm line-clamp-2">{card.title}</h4>
|
|
322
|
-
)}
|
|
323
|
-
</div>
|
|
324
|
-
|
|
325
|
-
{/* Quick actions */}
|
|
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>
|
|
352
|
-
<DropdownMenuItem>
|
|
353
|
-
<Move className="mr-2 h-4 w-4" />
|
|
354
|
-
Move
|
|
355
|
-
</DropdownMenuItem>
|
|
356
|
-
<DropdownMenuItem>
|
|
357
|
-
<Archive className="mr-2 h-4 w-4" />
|
|
358
|
-
Archive
|
|
359
|
-
</DropdownMenuItem>
|
|
360
|
-
<DropdownMenuSeparator />
|
|
361
|
-
<DropdownMenuItem
|
|
362
|
-
onClick={onDelete}
|
|
363
|
-
className="text-destructive"
|
|
364
|
-
>
|
|
365
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
366
|
-
Delete
|
|
367
|
-
</DropdownMenuItem>
|
|
368
|
-
</DropdownMenuContent>
|
|
369
|
-
</DropdownMenu>
|
|
370
|
-
</div>
|
|
371
|
-
)}
|
|
372
|
-
</div>
|
|
373
|
-
|
|
374
|
-
{/* Description */}
|
|
375
|
-
{!cardCompactMode && card.description && (
|
|
376
|
-
<p className="text-xs text-muted-foreground mb-3 line-clamp-2">
|
|
377
|
-
{card.description}
|
|
378
|
-
</p>
|
|
379
|
-
)}
|
|
380
|
-
|
|
381
|
-
{/* Progress bar */}
|
|
382
|
-
{cardShowProgress && (card.progress !== undefined || card.checklist) && (
|
|
383
|
-
<div className="mb-3 px-2">
|
|
384
|
-
<div className="flex justify-between text-xs text-muted-foreground mb-1">
|
|
385
|
-
<span>Progress</span>
|
|
386
|
-
<span>{Math.round(card.progress || checklistProgress)}%</span>
|
|
387
|
-
</div>
|
|
388
|
-
<div className="flex items-center">
|
|
389
|
-
<Progress value={card.progress || checklistProgress} className="h-1.5 w-full" />
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
)}
|
|
393
|
-
|
|
394
|
-
{/* Tags */}
|
|
395
|
-
{!cardCompactMode && card.tags && card.tags.length > 0 && (
|
|
396
|
-
<div className="flex flex-wrap gap-1 mb-3">
|
|
397
|
-
{card.tags.map((tag, index) => (
|
|
398
|
-
<Badge key={index} variant="secondary" className="text-xs px-1.5 py-0">
|
|
399
|
-
{tag}
|
|
400
|
-
</Badge>
|
|
401
|
-
))}
|
|
402
|
-
</div>
|
|
403
|
-
)}
|
|
404
|
-
|
|
405
|
-
{/* Card details */}
|
|
406
|
-
{showDetails && (
|
|
407
|
-
<div className="flex items-center justify-between text-xs">
|
|
408
|
-
<div className="flex items-center gap-2">
|
|
409
|
-
{/* Priority */}
|
|
410
|
-
{card.priority && (
|
|
411
|
-
<div className="flex items-center gap-1">
|
|
412
|
-
<div className={cn("w-2 h-2 rounded-full", PRIORITY_CONFIG[card.priority].dot)} />
|
|
413
|
-
<span className="capitalize">{card.priority}</span>
|
|
414
|
-
</div>
|
|
415
|
-
)}
|
|
416
|
-
|
|
417
|
-
{/* Due date */}
|
|
418
|
-
{card.dueDate && (
|
|
419
|
-
<div className={cn(
|
|
420
|
-
"flex items-center gap-1",
|
|
421
|
-
isOverdue(card.dueDate) && "text-destructive"
|
|
422
|
-
)}>
|
|
423
|
-
<Calendar className="h-3 w-3" />
|
|
424
|
-
<span>{cardDateFormat ? format(card.dueDate, cardDateFormat) : formatDate(card.dueDate)}</span>
|
|
425
|
-
</div>
|
|
426
|
-
)}
|
|
427
|
-
|
|
428
|
-
{/* Checklist */}
|
|
429
|
-
{card.checklist && (
|
|
430
|
-
<div className="flex items-center gap-1">
|
|
431
|
-
<CheckSquare className="h-3 w-3" />
|
|
432
|
-
<span>{completedChecklistItems}/{totalChecklistItems}</span>
|
|
433
|
-
</div>
|
|
434
|
-
)}
|
|
435
|
-
</div>
|
|
436
|
-
|
|
437
|
-
<div className="flex items-center gap-2">
|
|
438
|
-
{/* Comments */}
|
|
439
|
-
{card.comments && card.comments > 0 && (
|
|
440
|
-
<div className="flex items-center gap-1">
|
|
441
|
-
<MessageCircle className="h-3 w-3" />
|
|
442
|
-
<span>{card.comments}</span>
|
|
443
|
-
</div>
|
|
444
|
-
)}
|
|
445
|
-
|
|
446
|
-
{/* Attachments */}
|
|
447
|
-
{card.attachments && card.attachments.length > 0 && (
|
|
448
|
-
<div className="flex items-center gap-1">
|
|
449
|
-
<Paperclip className="h-3 w-3" />
|
|
450
|
-
<span>{card.attachments.length}</span>
|
|
451
|
-
</div>
|
|
452
|
-
)}
|
|
453
|
-
|
|
454
|
-
{/* Assignees */}
|
|
455
|
-
{cardShowAssignees && card.assignees && card.assignees.length > 0 && (
|
|
456
|
-
<MoonUIAvatarGroupPro max={cardMaxAssigneesToShow} size="xs">
|
|
457
|
-
{card.assignees.map((assignee) => (
|
|
458
|
-
<MoonUIAvatarPro key={assignee.id} className="h-5 w-5">
|
|
459
|
-
<MoonUIAvatarImagePro src={assignee.avatar} />
|
|
460
|
-
<MoonUIAvatarFallbackPro className="text-xs">
|
|
461
|
-
{getInitials(assignee.name)}
|
|
462
|
-
</MoonUIAvatarFallbackPro>
|
|
463
|
-
</MoonUIAvatarPro>
|
|
464
|
-
))}
|
|
465
|
-
</MoonUIAvatarGroupPro>
|
|
466
|
-
)}
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
)}
|
|
470
|
-
</CardContent>
|
|
471
|
-
</Card>
|
|
472
|
-
</motion.div>
|
|
473
|
-
)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Main Kanban component
|
|
477
|
-
export function Kanban({
|
|
478
|
-
columns: initialColumns,
|
|
479
|
-
onCardMove,
|
|
480
|
-
onCardClick,
|
|
481
|
-
onCardEdit,
|
|
482
|
-
onCardDelete,
|
|
483
|
-
onCardUpdate,
|
|
484
|
-
onAddCard,
|
|
485
|
-
onAddColumn,
|
|
486
|
-
onColumnUpdate,
|
|
487
|
-
onColumnDelete,
|
|
488
|
-
onBulkAction,
|
|
489
|
-
onExport,
|
|
490
|
-
className,
|
|
491
|
-
showAddColumn = true,
|
|
492
|
-
showCardDetails = true,
|
|
493
|
-
showFilters = true,
|
|
494
|
-
showSearch = true,
|
|
495
|
-
enableKeyboardShortcuts = true,
|
|
496
|
-
cardTemplates = [],
|
|
497
|
-
columnTemplates = [],
|
|
498
|
-
filters = [],
|
|
499
|
-
defaultFilter,
|
|
500
|
-
loading = false,
|
|
501
|
-
disabled = false,
|
|
502
|
-
labels = [],
|
|
503
|
-
users = [],
|
|
504
|
-
// Card Render Customization
|
|
505
|
-
renderCard,
|
|
506
|
-
renderCardPreview,
|
|
507
|
-
renderCardBadge,
|
|
508
|
-
renderCardActions,
|
|
509
|
-
cardCompactMode = false,
|
|
510
|
-
cardShowCoverImage = true,
|
|
511
|
-
cardShowAssignees = true,
|
|
512
|
-
cardShowLabels = true,
|
|
513
|
-
cardShowProgress = true,
|
|
514
|
-
cardDateFormat,
|
|
515
|
-
cardMaxAssigneesToShow = 3,
|
|
516
|
-
// Add Card Customization
|
|
517
|
-
renderAddCardButton,
|
|
518
|
-
renderAddCardForm,
|
|
519
|
-
addCardButtonText,
|
|
520
|
-
addCardPosition = 'bottom',
|
|
521
|
-
allowQuickAdd = true,
|
|
522
|
-
quickAddFields = ['title'],
|
|
523
|
-
validateCard,
|
|
524
|
-
onBeforeCardAdd,
|
|
525
|
-
// Column Customization
|
|
526
|
-
renderColumnHeader,
|
|
527
|
-
renderColumnFooter,
|
|
528
|
-
renderEmptyColumn,
|
|
529
|
-
columnMenuActions,
|
|
530
|
-
allowColumnReorder = true,
|
|
531
|
-
columnColorOptions,
|
|
532
|
-
columnDefaultColor = '#6B7280',
|
|
533
|
-
// Drag & Drop Enhancement
|
|
534
|
-
dragDisabled = false,
|
|
535
|
-
dropDisabled = false,
|
|
536
|
-
dragPreview = 'card',
|
|
537
|
-
renderDragPreview,
|
|
538
|
-
canDrop,
|
|
539
|
-
onDragStart,
|
|
540
|
-
onDragEnd,
|
|
541
|
-
// UI/UX Customization
|
|
542
|
-
theme = 'default',
|
|
543
|
-
cardVariant = 'default',
|
|
544
|
-
enableAnimations = true,
|
|
545
|
-
animationDuration = 0.2,
|
|
546
|
-
columnWidth,
|
|
547
|
-
columnGap = 24,
|
|
548
|
-
cardGap = 12,
|
|
549
|
-
showTooltips = true,
|
|
550
|
-
tooltipDelay = 500
|
|
551
|
-
}: KanbanProps) {
|
|
552
|
-
// Check pro access
|
|
553
|
-
const { hasProAccess, isLoading } = useSubscription()
|
|
554
|
-
|
|
555
|
-
if (!isLoading && !hasProAccess) {
|
|
556
|
-
return (
|
|
557
|
-
<Card className={cn("w-full", className)}>
|
|
558
|
-
<CardContent className="py-12 text-center">
|
|
559
|
-
<div className="max-w-md mx-auto space-y-4">
|
|
560
|
-
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
561
|
-
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
562
|
-
</div>
|
|
563
|
-
<div>
|
|
564
|
-
<h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
|
|
565
|
-
<p className="text-muted-foreground text-sm mb-4">
|
|
566
|
-
Kanban Board is available exclusively to MoonUI Pro subscribers.
|
|
567
|
-
</p>
|
|
568
|
-
<div className="flex gap-3 justify-center">
|
|
569
|
-
<a href="/pricing">
|
|
570
|
-
<Button size="sm">
|
|
571
|
-
<Sparkles className="mr-2 h-4 w-4" />
|
|
572
|
-
Upgrade to Pro
|
|
573
|
-
</Button>
|
|
574
|
-
</a>
|
|
575
|
-
</div>
|
|
576
|
-
</div>
|
|
577
|
-
</div>
|
|
578
|
-
</CardContent>
|
|
579
|
-
</Card>
|
|
580
|
-
)
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// State
|
|
584
|
-
const [columns, setColumns] = useState(initialColumns)
|
|
585
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
586
|
-
const [activeFilter, setActiveFilter] = useState(defaultFilter)
|
|
587
|
-
const [selectedCards, setSelectedCards] = useState<string[]>([])
|
|
588
|
-
const [draggedCard, setDraggedCard] = useState<string | null>(null)
|
|
589
|
-
const [draggedOverColumn, setDraggedOverColumn] = useState<string | null>(null)
|
|
590
|
-
const [isCreatingColumn, setIsCreatingColumn] = useState(false)
|
|
591
|
-
const [newColumnTitle, setNewColumnTitle] = useState('')
|
|
592
|
-
|
|
593
|
-
// Modal states
|
|
594
|
-
const [selectedCard, setSelectedCard] = useState<KanbanCard | null>(null)
|
|
595
|
-
const [addCardColumnId, setAddCardColumnId] = useState<string | null>(null)
|
|
596
|
-
const [editingColumnId, setEditingColumnId] = useState<string | null>(null)
|
|
597
|
-
const [editingColumnTitle, setEditingColumnTitle] = useState('')
|
|
598
|
-
const [wipLimitModalOpen, setWipLimitModalOpen] = useState(false)
|
|
599
|
-
const [wipLimitColumnId, setWipLimitColumnId] = useState<string | null>(null)
|
|
600
|
-
const [wipLimit, setWipLimit] = useState<number | undefined>()
|
|
601
|
-
const [colorPickerOpen, setColorPickerOpen] = useState(false)
|
|
602
|
-
const [colorPickerColumnId, setColorPickerColumnId] = useState<string | null>(null)
|
|
603
|
-
const [selectedColor, setSelectedColor] = useState('#6B7280')
|
|
604
|
-
|
|
605
|
-
const { scrollRef, startAutoScroll, stopAutoScroll } = useAutoScroll()
|
|
606
|
-
const { toast } = useToast()
|
|
607
|
-
|
|
608
|
-
// Update state when props change
|
|
609
|
-
useEffect(() => {
|
|
610
|
-
setColumns(initialColumns)
|
|
611
|
-
}, [initialColumns])
|
|
612
|
-
|
|
613
|
-
// Filter cards based on search and filters
|
|
614
|
-
const filteredColumns = useMemo(() => {
|
|
615
|
-
if (!searchQuery && !activeFilter) return columns
|
|
616
|
-
|
|
617
|
-
return columns.map(column => ({
|
|
618
|
-
...column,
|
|
619
|
-
cards: column.cards.filter(card => {
|
|
620
|
-
// Search filter
|
|
621
|
-
if (searchQuery) {
|
|
622
|
-
const query = searchQuery.toLowerCase()
|
|
623
|
-
const matchesSearch =
|
|
624
|
-
card.title.toLowerCase().includes(query) ||
|
|
625
|
-
card.description?.toLowerCase().includes(query) ||
|
|
626
|
-
card.tags?.some(tag => tag.toLowerCase().includes(query)) ||
|
|
627
|
-
card.assignees?.some(a => a.name.toLowerCase().includes(query))
|
|
628
|
-
|
|
629
|
-
if (!matchesSearch) return false
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Active filter
|
|
633
|
-
if (activeFilter) {
|
|
634
|
-
const filter = filters.find(f => f.id === activeFilter)
|
|
635
|
-
if (filter) {
|
|
636
|
-
// Apply filter logic here
|
|
637
|
-
// This is a simplified example
|
|
638
|
-
if (filter.assignees?.length && !card.assignees?.some(a => filter.assignees!.includes(a.id))) {
|
|
639
|
-
return false
|
|
640
|
-
}
|
|
641
|
-
if (filter.priority?.length && !filter.priority.includes(card.priority || '')) {
|
|
642
|
-
return false
|
|
643
|
-
}
|
|
644
|
-
if (filter.labels?.length && !card.labels?.some(l => filter.labels!.includes(l.id))) {
|
|
645
|
-
return false
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
return true
|
|
651
|
-
})
|
|
652
|
-
}))
|
|
653
|
-
}, [columns, searchQuery, activeFilter, filters])
|
|
654
|
-
|
|
655
|
-
// Keyboard shortcuts
|
|
656
|
-
useEffect(() => {
|
|
657
|
-
if (!enableKeyboardShortcuts) return
|
|
658
|
-
|
|
659
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
660
|
-
// Cmd/Ctrl + F: Focus search
|
|
661
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
|
|
662
|
-
e.preventDefault()
|
|
663
|
-
document.getElementById('kanban-search')?.focus()
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Cmd/Ctrl + N: Add new card
|
|
667
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
|
|
668
|
-
e.preventDefault()
|
|
669
|
-
// Add to first column by default
|
|
670
|
-
const firstColumn = columns[0]
|
|
671
|
-
if (firstColumn && onAddCard) {
|
|
672
|
-
onAddCard(firstColumn.id)
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Escape: Clear selection
|
|
677
|
-
if (e.key === 'Escape') {
|
|
678
|
-
setSelectedCards([])
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
window.addEventListener('keydown', handleKeyDown)
|
|
683
|
-
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
684
|
-
}, [enableKeyboardShortcuts, columns, onAddCard])
|
|
685
|
-
|
|
686
|
-
// Drag handlers
|
|
687
|
-
const handleDragStart = (card: KanbanCard, columnId: string) => {
|
|
688
|
-
if (disabled) return
|
|
689
|
-
if (typeof dragDisabled === 'function' && dragDisabled(card)) return
|
|
690
|
-
if (dragDisabled === true) return
|
|
691
|
-
|
|
692
|
-
setDraggedCard(card.id)
|
|
693
|
-
const column = columns.find(col => col.id === columnId)
|
|
694
|
-
if (column && onDragStart) {
|
|
695
|
-
onDragStart(card, column)
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const handleDragOver = (e: React.DragEvent, columnId: string) => {
|
|
700
|
-
if (disabled) return
|
|
701
|
-
const column = columns.find(col => col.id === columnId)
|
|
702
|
-
if (column && typeof dropDisabled === 'function' && dropDisabled(column)) return
|
|
703
|
-
if (dropDisabled === true) return
|
|
704
|
-
|
|
705
|
-
e.preventDefault()
|
|
706
|
-
setDraggedOverColumn(columnId)
|
|
707
|
-
|
|
708
|
-
// Auto-scroll logic
|
|
709
|
-
const container = scrollRef.current
|
|
710
|
-
if (!container) return
|
|
711
|
-
|
|
712
|
-
const rect = container.getBoundingClientRect()
|
|
713
|
-
const x = e.clientX
|
|
714
|
-
|
|
715
|
-
if (x < rect.left + 100) {
|
|
716
|
-
startAutoScroll('left')
|
|
717
|
-
} else if (x > rect.right - 100) {
|
|
718
|
-
startAutoScroll('right')
|
|
719
|
-
} else {
|
|
720
|
-
stopAutoScroll()
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const handleDragEnd = () => {
|
|
725
|
-
if (draggedCard) {
|
|
726
|
-
const card = columns.flatMap(col => col.cards).find(c => c.id === draggedCard)
|
|
727
|
-
const column = columns.find(col => col.cards.some(c => c.id === draggedCard))
|
|
728
|
-
if (card && column && onDragEnd) {
|
|
729
|
-
onDragEnd(card, column)
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
setDraggedCard(null)
|
|
733
|
-
setDraggedOverColumn(null)
|
|
734
|
-
stopAutoScroll()
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const handleDrop = (e: React.DragEvent, targetColumnId: string, targetIndex: number) => {
|
|
738
|
-
if (disabled || !draggedCard) return
|
|
739
|
-
e.preventDefault()
|
|
740
|
-
|
|
741
|
-
const targetColumn = columns.find(col => col.id === targetColumnId)
|
|
742
|
-
const draggedCardObj = columns.flatMap(col => col.cards).find(card => card.id === draggedCard)
|
|
743
|
-
|
|
744
|
-
if (targetColumn && draggedCardObj && canDrop && !canDrop(draggedCardObj, targetColumn, targetIndex)) {
|
|
745
|
-
return
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Find source column and card
|
|
749
|
-
let sourceColumnId: string | null = null
|
|
750
|
-
let sourceCard: KanbanCard | null = null
|
|
751
|
-
|
|
752
|
-
for (const column of columns) {
|
|
753
|
-
const card = column.cards.find(c => c.id === draggedCard)
|
|
754
|
-
if (card) {
|
|
755
|
-
sourceColumnId = column.id
|
|
756
|
-
sourceCard = card
|
|
757
|
-
break
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (sourceColumnId && sourceCard) {
|
|
762
|
-
// Update local state immediately for better UX
|
|
763
|
-
const newColumns = columns.map(col => {
|
|
764
|
-
if (col.id === sourceColumnId) {
|
|
765
|
-
return {
|
|
766
|
-
...col,
|
|
767
|
-
cards: col.cards.filter(c => c.id !== draggedCard)
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (col.id === targetColumnId) {
|
|
771
|
-
const newCards = [...col.cards]
|
|
772
|
-
newCards.splice(targetIndex, 0, sourceCard!)
|
|
773
|
-
return {
|
|
774
|
-
...col,
|
|
775
|
-
cards: newCards
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
return col
|
|
779
|
-
})
|
|
780
|
-
setColumns(newColumns)
|
|
781
|
-
|
|
782
|
-
// Call the callback if provided
|
|
783
|
-
if (onCardMove) {
|
|
784
|
-
onCardMove(draggedCard, sourceColumnId, targetColumnId, targetIndex)
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
handleDragEnd()
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Bulk actions
|
|
792
|
-
const handleBulkAction = (action: string) => {
|
|
793
|
-
if (onBulkAction && selectedCards.length > 0) {
|
|
794
|
-
onBulkAction(action, selectedCards)
|
|
795
|
-
setSelectedCards([])
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Column actions
|
|
800
|
-
const handleColumnAction = (column: KanbanColumn, action: string) => {
|
|
801
|
-
switch (action) {
|
|
802
|
-
case 'rename':
|
|
803
|
-
setEditingColumnId(column.id)
|
|
804
|
-
setEditingColumnTitle(column.title)
|
|
805
|
-
break
|
|
806
|
-
case 'delete':
|
|
807
|
-
onColumnDelete?.(column.id)
|
|
808
|
-
toast({
|
|
809
|
-
title: "Column deleted",
|
|
810
|
-
description: `"${column.title}" has been deleted`
|
|
811
|
-
})
|
|
812
|
-
break
|
|
813
|
-
case 'collapse':
|
|
814
|
-
const updatedColumn = { ...column, collapsed: !column.collapsed }
|
|
815
|
-
onColumnUpdate?.(updatedColumn)
|
|
816
|
-
setColumns(columns.map(col => col.id === column.id ? updatedColumn : col))
|
|
817
|
-
break
|
|
818
|
-
case 'setLimit':
|
|
819
|
-
setWipLimitColumnId(column.id)
|
|
820
|
-
setWipLimit(column.limit)
|
|
821
|
-
setWipLimitModalOpen(true)
|
|
822
|
-
break
|
|
823
|
-
case 'changeColor':
|
|
824
|
-
setColorPickerColumnId(column.id)
|
|
825
|
-
setSelectedColor(column.color || '#6B7280')
|
|
826
|
-
setColorPickerOpen(true)
|
|
827
|
-
break
|
|
828
|
-
case 'sortByPriority':
|
|
829
|
-
const sortedCards = [...column.cards].sort((a, b) => {
|
|
830
|
-
const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 }
|
|
831
|
-
return (priorityOrder[b.priority || 'medium'] || 2) - (priorityOrder[a.priority || 'medium'] || 2)
|
|
832
|
-
})
|
|
833
|
-
const sortedColumn = { ...column, cards: sortedCards }
|
|
834
|
-
onColumnUpdate?.(sortedColumn)
|
|
835
|
-
setColumns(columns.map(col => col.id === column.id ? sortedColumn : col))
|
|
836
|
-
toast({
|
|
837
|
-
title: "Cards sorted",
|
|
838
|
-
description: "Cards sorted by priority"
|
|
839
|
-
})
|
|
840
|
-
break
|
|
841
|
-
case 'sortByDueDate':
|
|
842
|
-
const dateCards = [...column.cards].sort((a, b) => {
|
|
843
|
-
if (!a.dueDate && !b.dueDate) return 0
|
|
844
|
-
if (!a.dueDate) return 1
|
|
845
|
-
if (!b.dueDate) return -1
|
|
846
|
-
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
|
|
847
|
-
})
|
|
848
|
-
const dateColumn = { ...column, cards: dateCards }
|
|
849
|
-
onColumnUpdate?.(dateColumn)
|
|
850
|
-
setColumns(columns.map(col => col.id === column.id ? dateColumn : col))
|
|
851
|
-
toast({
|
|
852
|
-
title: "Cards sorted",
|
|
853
|
-
description: "Cards sorted by due date"
|
|
854
|
-
})
|
|
855
|
-
break
|
|
856
|
-
case 'sortAlphabetically':
|
|
857
|
-
const alphaCards = [...column.cards].sort((a, b) => a.title.localeCompare(b.title))
|
|
858
|
-
const alphaColumn = { ...column, cards: alphaCards }
|
|
859
|
-
onColumnUpdate?.(alphaColumn)
|
|
860
|
-
setColumns(columns.map(col => col.id === column.id ? alphaColumn : col))
|
|
861
|
-
toast({
|
|
862
|
-
title: "Cards sorted",
|
|
863
|
-
description: "Cards sorted alphabetically"
|
|
864
|
-
})
|
|
865
|
-
break
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// Card handlers
|
|
870
|
-
const handleCardClick = (card: KanbanCard) => {
|
|
871
|
-
setSelectedCard(card)
|
|
872
|
-
if (onCardClick) {
|
|
873
|
-
onCardClick(card)
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const handleCardUpdate = (updatedCard: KanbanCard) => {
|
|
878
|
-
onCardUpdate?.(updatedCard)
|
|
879
|
-
setColumns(columns.map(col => ({
|
|
880
|
-
...col,
|
|
881
|
-
cards: col.cards.map(card => card.id === updatedCard.id ? updatedCard : card)
|
|
882
|
-
})))
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const handleAddCard = (columnId: string, newCard?: Partial<KanbanCard>) => {
|
|
886
|
-
setAddCardColumnId(columnId)
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const handleAddNewCard = (card: Partial<KanbanCard>) => {
|
|
890
|
-
if (!addCardColumnId) return
|
|
891
|
-
|
|
892
|
-
const newCard: KanbanCard = {
|
|
893
|
-
id: Date.now().toString(),
|
|
894
|
-
title: card.title || 'New Card',
|
|
895
|
-
position: Date.now(),
|
|
896
|
-
...card
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Update local state
|
|
900
|
-
setColumns(columns.map(col => {
|
|
901
|
-
if (col.id === addCardColumnId) {
|
|
902
|
-
return {
|
|
903
|
-
...col,
|
|
904
|
-
cards: [...col.cards, newCard]
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
return col
|
|
908
|
-
}))
|
|
909
|
-
|
|
910
|
-
// Call the callback if provided
|
|
911
|
-
if (onAddCard) {
|
|
912
|
-
onAddCard(addCardColumnId, newCard)
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
toast({
|
|
916
|
-
title: "Card added",
|
|
917
|
-
description: `"${newCard.title}" has been added`
|
|
918
|
-
})
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Column updates
|
|
922
|
-
const handleColumnRename = (columnId: string) => {
|
|
923
|
-
const column = columns.find(col => col.id === columnId)
|
|
924
|
-
if (!column || !editingColumnTitle.trim()) return
|
|
925
|
-
|
|
926
|
-
const updatedColumn = { ...column, title: editingColumnTitle.trim() }
|
|
927
|
-
onColumnUpdate?.(updatedColumn)
|
|
928
|
-
setColumns(columns.map(col => col.id === columnId ? updatedColumn : col))
|
|
929
|
-
setEditingColumnId(null)
|
|
930
|
-
setEditingColumnTitle('')
|
|
931
|
-
|
|
932
|
-
toast({
|
|
933
|
-
title: "Column renamed",
|
|
934
|
-
description: `Column renamed to "${editingColumnTitle.trim()}"`
|
|
935
|
-
})
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const handleWipLimitUpdate = () => {
|
|
939
|
-
const column = columns.find(col => col.id === wipLimitColumnId)
|
|
940
|
-
if (!column) return
|
|
941
|
-
|
|
942
|
-
const updatedColumn = { ...column, limit: wipLimit }
|
|
943
|
-
onColumnUpdate?.(updatedColumn)
|
|
944
|
-
setColumns(columns.map(col => col.id === wipLimitColumnId ? updatedColumn : col))
|
|
945
|
-
setWipLimitModalOpen(false)
|
|
946
|
-
|
|
947
|
-
toast({
|
|
948
|
-
title: "WIP limit updated",
|
|
949
|
-
description: wipLimit ? `WIP limit set to ${wipLimit}` : "WIP limit removed"
|
|
950
|
-
})
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
const handleColorUpdate = () => {
|
|
954
|
-
const column = columns.find(col => col.id === colorPickerColumnId)
|
|
955
|
-
if (!column) return
|
|
956
|
-
|
|
957
|
-
const updatedColumn = { ...column, color: selectedColor }
|
|
958
|
-
onColumnUpdate?.(updatedColumn)
|
|
959
|
-
setColumns(columns.map(col => col.id === colorPickerColumnId ? updatedColumn : col))
|
|
960
|
-
setColorPickerOpen(false)
|
|
961
|
-
|
|
962
|
-
toast({
|
|
963
|
-
title: "Column color updated",
|
|
964
|
-
description: "Column color has been changed"
|
|
965
|
-
})
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Export functionality
|
|
969
|
-
const handleExport = (format: 'json' | 'csv') => {
|
|
970
|
-
if (onExport) {
|
|
971
|
-
onExport(format)
|
|
972
|
-
} else {
|
|
973
|
-
if (format === 'json') {
|
|
974
|
-
const data = JSON.stringify(columns, null, 2)
|
|
975
|
-
const blob = new Blob([data], { type: 'application/json' })
|
|
976
|
-
const url = URL.createObjectURL(blob)
|
|
977
|
-
const a = document.createElement('a')
|
|
978
|
-
a.href = url
|
|
979
|
-
a.download = 'kanban-board.json'
|
|
980
|
-
document.body.appendChild(a)
|
|
981
|
-
a.click()
|
|
982
|
-
document.body.removeChild(a)
|
|
983
|
-
URL.revokeObjectURL(url)
|
|
984
|
-
|
|
985
|
-
toast({
|
|
986
|
-
title: "Board exported",
|
|
987
|
-
description: "Board exported as JSON file"
|
|
988
|
-
})
|
|
989
|
-
} else if (format === 'csv') {
|
|
990
|
-
let csv = 'Column,Card Title,Description,Priority,Assignees,Due Date,Tags\n'
|
|
991
|
-
columns.forEach(column => {
|
|
992
|
-
column.cards.forEach(card => {
|
|
993
|
-
csv += `"${column.title}",`
|
|
994
|
-
csv += `"${card.title}",`
|
|
995
|
-
csv += `"${card.description || ''}",`
|
|
996
|
-
csv += `"${card.priority || ''}",`
|
|
997
|
-
csv += `"${card.assignees?.map(a => a.name).join(', ') || ''}",`
|
|
998
|
-
csv += `"${card.dueDate ? new Date(card.dueDate).toLocaleDateString() : ''}",`
|
|
999
|
-
csv += `"${card.tags?.join(', ') || ''}"\n`
|
|
1000
|
-
})
|
|
1001
|
-
})
|
|
1002
|
-
|
|
1003
|
-
const blob = new Blob([csv], { type: 'text/csv' })
|
|
1004
|
-
const url = URL.createObjectURL(blob)
|
|
1005
|
-
const a = document.createElement('a')
|
|
1006
|
-
a.href = url
|
|
1007
|
-
a.download = 'kanban-board.csv'
|
|
1008
|
-
document.body.appendChild(a)
|
|
1009
|
-
a.click()
|
|
1010
|
-
document.body.removeChild(a)
|
|
1011
|
-
URL.revokeObjectURL(url)
|
|
1012
|
-
|
|
1013
|
-
toast({
|
|
1014
|
-
title: "Board exported",
|
|
1015
|
-
description: "Board exported as CSV file"
|
|
1016
|
-
})
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// Loading state
|
|
1022
|
-
if (loading) {
|
|
1023
|
-
return (
|
|
1024
|
-
<div className={cn("w-full", className)}>
|
|
1025
|
-
<div className="flex gap-6 overflow-x-auto pb-4">
|
|
1026
|
-
{[1, 2, 3].map((i) => (
|
|
1027
|
-
<div key={i} className="flex-shrink-0 w-80">
|
|
1028
|
-
<Card>
|
|
1029
|
-
<CardHeader>
|
|
1030
|
-
<Skeleton className="h-4 w-24" />
|
|
1031
|
-
</CardHeader>
|
|
1032
|
-
<CardContent className="space-y-3">
|
|
1033
|
-
{[1, 2, 3].map((j) => (
|
|
1034
|
-
<Skeleton key={j} className="h-24 w-full" />
|
|
1035
|
-
))}
|
|
1036
|
-
</CardContent>
|
|
1037
|
-
</Card>
|
|
1038
|
-
</div>
|
|
1039
|
-
))}
|
|
1040
|
-
</div>
|
|
1041
|
-
</div>
|
|
1042
|
-
)
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
return (
|
|
1046
|
-
<div className={cn("w-full", className)}>
|
|
1047
|
-
{/* Header with search and filters */}
|
|
1048
|
-
{(showSearch || showFilters) && (
|
|
1049
|
-
<div className="mb-6 p-4 bg-muted/30 rounded-lg space-y-4">
|
|
1050
|
-
<div className="flex items-center justify-between gap-4">
|
|
1051
|
-
{/* Search */}
|
|
1052
|
-
{showSearch && (
|
|
1053
|
-
<div className="relative flex-1 max-w-md">
|
|
1054
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
1055
|
-
<Input
|
|
1056
|
-
id="kanban-search"
|
|
1057
|
-
placeholder="Search cards..."
|
|
1058
|
-
value={searchQuery}
|
|
1059
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1060
|
-
className="pl-9 bg-background"
|
|
1061
|
-
/>
|
|
1062
|
-
</div>
|
|
1063
|
-
)}
|
|
1064
|
-
|
|
1065
|
-
{/* Actions */}
|
|
1066
|
-
<div className="flex items-center gap-2 flex-shrink-0">
|
|
1067
|
-
{/* Filters */}
|
|
1068
|
-
{showFilters && filters.length > 0 && (
|
|
1069
|
-
<DropdownMenu>
|
|
1070
|
-
<DropdownMenuTrigger asChild>
|
|
1071
|
-
<Button variant="outline" size="sm">
|
|
1072
|
-
<Filter className="mr-2 h-4 w-4" />
|
|
1073
|
-
Filter
|
|
1074
|
-
{activeFilter && (
|
|
1075
|
-
<Badge variant="secondary" className="ml-2">
|
|
1076
|
-
{filters.find(f => f.id === activeFilter)?.name}
|
|
1077
|
-
</Badge>
|
|
1078
|
-
)}
|
|
1079
|
-
</Button>
|
|
1080
|
-
</DropdownMenuTrigger>
|
|
1081
|
-
<DropdownMenuContent align="end" className="w-48">
|
|
1082
|
-
<DropdownMenuLabel>Quick Filters</DropdownMenuLabel>
|
|
1083
|
-
<DropdownMenuSeparator />
|
|
1084
|
-
<DropdownMenuItem onClick={() => setActiveFilter(undefined)}>
|
|
1085
|
-
<X className="mr-2 h-4 w-4" />
|
|
1086
|
-
Clear filter
|
|
1087
|
-
</DropdownMenuItem>
|
|
1088
|
-
{filters.map((filter) => (
|
|
1089
|
-
<DropdownMenuItem
|
|
1090
|
-
key={filter.id}
|
|
1091
|
-
onClick={() => setActiveFilter(filter.id)}
|
|
1092
|
-
>
|
|
1093
|
-
<Check
|
|
1094
|
-
className={cn(
|
|
1095
|
-
"mr-2 h-4 w-4",
|
|
1096
|
-
activeFilter === filter.id ? "opacity-100" : "opacity-0"
|
|
1097
|
-
)}
|
|
1098
|
-
/>
|
|
1099
|
-
{filter.name}
|
|
1100
|
-
</DropdownMenuItem>
|
|
1101
|
-
))}
|
|
1102
|
-
</DropdownMenuContent>
|
|
1103
|
-
</DropdownMenu>
|
|
1104
|
-
)}
|
|
1105
|
-
|
|
1106
|
-
{/* Bulk actions */}
|
|
1107
|
-
{selectedCards.length > 0 && (
|
|
1108
|
-
<DropdownMenu>
|
|
1109
|
-
<DropdownMenuTrigger asChild>
|
|
1110
|
-
<Button variant="outline" size="sm">
|
|
1111
|
-
<span className="mr-2">{selectedCards.length} selected</span>
|
|
1112
|
-
<ChevronDown className="h-4 w-4" />
|
|
1113
|
-
</Button>
|
|
1114
|
-
</DropdownMenuTrigger>
|
|
1115
|
-
<DropdownMenuContent align="end">
|
|
1116
|
-
<DropdownMenuItem onClick={() => handleBulkAction('move')}>
|
|
1117
|
-
<Move className="mr-2 h-4 w-4" />
|
|
1118
|
-
Move cards
|
|
1119
|
-
</DropdownMenuItem>
|
|
1120
|
-
<DropdownMenuItem onClick={() => handleBulkAction('archive')}>
|
|
1121
|
-
<Archive className="mr-2 h-4 w-4" />
|
|
1122
|
-
Archive cards
|
|
1123
|
-
</DropdownMenuItem>
|
|
1124
|
-
<DropdownMenuItem onClick={() => handleBulkAction('delete')}>
|
|
1125
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
1126
|
-
Delete cards
|
|
1127
|
-
</DropdownMenuItem>
|
|
1128
|
-
</DropdownMenuContent>
|
|
1129
|
-
</DropdownMenu>
|
|
1130
|
-
)}
|
|
1131
|
-
|
|
1132
|
-
{/* Export */}
|
|
1133
|
-
<DropdownMenu>
|
|
1134
|
-
<DropdownMenuTrigger asChild>
|
|
1135
|
-
<Button variant="outline" size="sm">
|
|
1136
|
-
<Download className="mr-2 h-4 w-4" />
|
|
1137
|
-
Export
|
|
1138
|
-
</Button>
|
|
1139
|
-
</DropdownMenuTrigger>
|
|
1140
|
-
<DropdownMenuContent align="end">
|
|
1141
|
-
<DropdownMenuItem onClick={() => handleExport('json')}>
|
|
1142
|
-
Export as JSON
|
|
1143
|
-
</DropdownMenuItem>
|
|
1144
|
-
<DropdownMenuItem onClick={() => handleExport('csv')}>
|
|
1145
|
-
Export as CSV
|
|
1146
|
-
</DropdownMenuItem>
|
|
1147
|
-
</DropdownMenuContent>
|
|
1148
|
-
</DropdownMenu>
|
|
1149
|
-
</div>
|
|
1150
|
-
</div>
|
|
1151
|
-
|
|
1152
|
-
{/* Active filters display */}
|
|
1153
|
-
{activeFilter && (
|
|
1154
|
-
<div className="flex items-center gap-2">
|
|
1155
|
-
<span className="text-sm text-muted-foreground">Active filters:</span>
|
|
1156
|
-
<Badge variant="secondary">
|
|
1157
|
-
{filters.find(f => f.id === activeFilter)?.name}
|
|
1158
|
-
<Button
|
|
1159
|
-
variant="ghost"
|
|
1160
|
-
size="sm"
|
|
1161
|
-
className="ml-1 h-auto p-0"
|
|
1162
|
-
onClick={() => setActiveFilter(undefined)}
|
|
1163
|
-
>
|
|
1164
|
-
<X className="h-3 w-3" />
|
|
1165
|
-
</Button>
|
|
1166
|
-
</Badge>
|
|
1167
|
-
</div>
|
|
1168
|
-
)}
|
|
1169
|
-
</div>
|
|
1170
|
-
)}
|
|
1171
|
-
|
|
1172
|
-
{/* Kanban board */}
|
|
1173
|
-
<div className="w-full overflow-x-auto py-6">
|
|
1174
|
-
<div
|
|
1175
|
-
ref={scrollRef}
|
|
1176
|
-
className="flex min-w-fit mx-auto px-4"
|
|
1177
|
-
style={{ gap: `${columnGap}px` }}
|
|
1178
|
-
onDragOver={(e) => e.preventDefault()}
|
|
1179
|
-
>
|
|
1180
|
-
<AnimatePresence mode="sync">
|
|
1181
|
-
{filteredColumns.map((column) => {
|
|
1182
|
-
const isOverLimit = column.limit && column.cards.length >= column.limit
|
|
1183
|
-
const isDraggedOver = draggedOverColumn === column.id
|
|
1184
|
-
|
|
1185
|
-
return (
|
|
1186
|
-
<motion.div
|
|
1187
|
-
key={column.id}
|
|
1188
|
-
layout={enableAnimations}
|
|
1189
|
-
initial={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? -20 : 0 }}
|
|
1190
|
-
animate={{ opacity: 1, x: 0 }}
|
|
1191
|
-
exit={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? 20 : 0 }}
|
|
1192
|
-
className={cn(
|
|
1193
|
-
"flex-shrink-0 transition-all",
|
|
1194
|
-
isDraggedOver && "scale-105"
|
|
1195
|
-
)}
|
|
1196
|
-
style={{
|
|
1197
|
-
width: columnWidth === 'auto' ? 'auto' : (columnWidth || 360) + 'px',
|
|
1198
|
-
minWidth: columnWidth === 'auto' ? '320px' : undefined,
|
|
1199
|
-
flexShrink: 0,
|
|
1200
|
-
transitionDuration: enableAnimations ? `${animationDuration}s` : '0s'
|
|
1201
|
-
}}
|
|
1202
|
-
onDragOver={(e) => handleDragOver(e, column.id)}
|
|
1203
|
-
onDragLeave={() => setDraggedOverColumn(null)}
|
|
1204
|
-
onDrop={(e) => handleDrop(e, column.id, column.cards.length)}
|
|
1205
|
-
>
|
|
1206
|
-
<Card className={cn(
|
|
1207
|
-
"h-full transition-all duration-200",
|
|
1208
|
-
isDraggedOver && "ring-2 ring-primary ring-offset-2 bg-primary/5",
|
|
1209
|
-
column.collapsed && "opacity-60"
|
|
1210
|
-
)}>
|
|
1211
|
-
<CardHeader className="pb-3">
|
|
1212
|
-
{renderColumnHeader ? (
|
|
1213
|
-
renderColumnHeader(column)
|
|
1214
|
-
) : (
|
|
1215
|
-
<>
|
|
1216
|
-
<div className="flex items-center justify-between">
|
|
1217
|
-
<div className="flex items-center gap-2">
|
|
1218
|
-
{/* Column color indicator */}
|
|
1219
|
-
{column.color && (
|
|
1220
|
-
<div
|
|
1221
|
-
className="w-3 h-3 rounded-full"
|
|
1222
|
-
style={{ backgroundColor: column.color }}
|
|
1223
|
-
/>
|
|
1224
|
-
)}
|
|
1225
|
-
|
|
1226
|
-
{/* Column title */}
|
|
1227
|
-
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
1228
|
-
{editingColumnId === column.id ? (
|
|
1229
|
-
<Input
|
|
1230
|
-
value={editingColumnTitle}
|
|
1231
|
-
onChange={(e) => setEditingColumnTitle(e.target.value)}
|
|
1232
|
-
onBlur={() => handleColumnRename(column.id)}
|
|
1233
|
-
onKeyDown={(e) => {
|
|
1234
|
-
if (e.key === 'Enter') {
|
|
1235
|
-
handleColumnRename(column.id)
|
|
1236
|
-
}
|
|
1237
|
-
if (e.key === 'Escape') {
|
|
1238
|
-
setEditingColumnId(null)
|
|
1239
|
-
setEditingColumnTitle('')
|
|
1240
|
-
}
|
|
1241
|
-
}}
|
|
1242
|
-
className="h-6 w-32 text-sm"
|
|
1243
|
-
autoFocus
|
|
1244
|
-
onClick={(e) => e.stopPropagation()}
|
|
1245
|
-
/>
|
|
1246
|
-
) : (
|
|
1247
|
-
<>
|
|
1248
|
-
{column.title}
|
|
1249
|
-
{column.locked && <Lock className="h-3 w-3" />}
|
|
1250
|
-
</>
|
|
1251
|
-
)}
|
|
1252
|
-
</CardTitle>
|
|
1253
|
-
|
|
1254
|
-
{/* Card count */}
|
|
1255
|
-
<Badge variant="secondary" className="text-xs">
|
|
1256
|
-
{column.cards.length}
|
|
1257
|
-
</Badge>
|
|
1258
|
-
</div>
|
|
1259
|
-
|
|
1260
|
-
{/* Column actions */}
|
|
1261
|
-
<DropdownMenu>
|
|
1262
|
-
<DropdownMenuTrigger asChild>
|
|
1263
|
-
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
|
1264
|
-
<MoreHorizontal className="h-4 w-4" />
|
|
1265
|
-
</Button>
|
|
1266
|
-
</DropdownMenuTrigger>
|
|
1267
|
-
<DropdownMenuContent align="end">
|
|
1268
|
-
{columnMenuActions ? (
|
|
1269
|
-
columnMenuActions.map((action, index) => {
|
|
1270
|
-
if (action.visible && !action.visible(column)) return null
|
|
1271
|
-
return (
|
|
1272
|
-
<DropdownMenuItem
|
|
1273
|
-
key={index}
|
|
1274
|
-
onClick={() => action.action(column)}
|
|
1275
|
-
>
|
|
1276
|
-
{action.icon || null}
|
|
1277
|
-
{action.label}
|
|
1278
|
-
</DropdownMenuItem>
|
|
1279
|
-
)
|
|
1280
|
-
})
|
|
1281
|
-
) : (
|
|
1282
|
-
<>
|
|
1283
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'rename')}>
|
|
1284
|
-
<Edit className="mr-2 h-4 w-4" />
|
|
1285
|
-
Rename
|
|
1286
|
-
</DropdownMenuItem>
|
|
1287
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'collapse')}>
|
|
1288
|
-
{column.collapsed ? (
|
|
1289
|
-
<>
|
|
1290
|
-
<Eye className="mr-2 h-4 w-4" />
|
|
1291
|
-
Expand
|
|
1292
|
-
</>
|
|
1293
|
-
) : (
|
|
1294
|
-
<>
|
|
1295
|
-
<EyeOff className="mr-2 h-4 w-4" />
|
|
1296
|
-
Collapse
|
|
1297
|
-
</>
|
|
1298
|
-
)}
|
|
1299
|
-
</DropdownMenuItem>
|
|
1300
|
-
<DropdownMenuSeparator />
|
|
1301
|
-
<DropdownMenuSub>
|
|
1302
|
-
<DropdownMenuSubTrigger>
|
|
1303
|
-
<Settings className="mr-2 h-4 w-4" />
|
|
1304
|
-
Settings
|
|
1305
|
-
</DropdownMenuSubTrigger>
|
|
1306
|
-
<DropdownMenuSubContent>
|
|
1307
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'setLimit')}>
|
|
1308
|
-
<Timer className="mr-2 h-4 w-4" />
|
|
1309
|
-
Set WIP limit
|
|
1310
|
-
</DropdownMenuItem>
|
|
1311
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'changeColor')}>
|
|
1312
|
-
<Palette className="mr-2 h-4 w-4" />
|
|
1313
|
-
Change color
|
|
1314
|
-
</DropdownMenuItem>
|
|
1315
|
-
<DropdownMenuSub>
|
|
1316
|
-
<DropdownMenuSubTrigger>
|
|
1317
|
-
<ArrowUpDown className="mr-2 h-4 w-4" />
|
|
1318
|
-
Sort cards
|
|
1319
|
-
</DropdownMenuSubTrigger>
|
|
1320
|
-
<DropdownMenuSubContent>
|
|
1321
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByPriority')}>
|
|
1322
|
-
By Priority
|
|
1323
|
-
</DropdownMenuItem>
|
|
1324
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByDueDate')}>
|
|
1325
|
-
By Due Date
|
|
1326
|
-
</DropdownMenuItem>
|
|
1327
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortAlphabetically')}>
|
|
1328
|
-
Alphabetically
|
|
1329
|
-
</DropdownMenuItem>
|
|
1330
|
-
</DropdownMenuSubContent>
|
|
1331
|
-
</DropdownMenuSub>
|
|
1332
|
-
</DropdownMenuSubContent>
|
|
1333
|
-
</DropdownMenuSub>
|
|
1334
|
-
<DropdownMenuSeparator />
|
|
1335
|
-
<DropdownMenuItem
|
|
1336
|
-
onClick={() => handleColumnAction(column, 'delete')}
|
|
1337
|
-
className="text-destructive"
|
|
1338
|
-
>
|
|
1339
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
1340
|
-
Delete column
|
|
1341
|
-
</DropdownMenuItem>
|
|
1342
|
-
</>
|
|
1343
|
-
)}
|
|
1344
|
-
</DropdownMenuContent>
|
|
1345
|
-
</DropdownMenu>
|
|
1346
|
-
</div>
|
|
1347
|
-
|
|
1348
|
-
{/* WIP limit warning */}
|
|
1349
|
-
{column.limit && (
|
|
1350
|
-
<CardDescription className={cn(
|
|
1351
|
-
"text-xs flex items-center gap-1 mt-1",
|
|
1352
|
-
isOverLimit && "text-destructive"
|
|
1353
|
-
)}>
|
|
1354
|
-
{isOverLimit ? (
|
|
1355
|
-
<>
|
|
1356
|
-
<AlertCircle className="h-3 w-3" />
|
|
1357
|
-
Over WIP limit ({column.cards.length}/{column.limit})
|
|
1358
|
-
</>
|
|
1359
|
-
) : (
|
|
1360
|
-
`WIP limit: ${column.cards.length}/${column.limit}`
|
|
1361
|
-
)}
|
|
1362
|
-
</CardDescription>
|
|
1363
|
-
)}
|
|
1364
|
-
</>
|
|
1365
|
-
)}
|
|
1366
|
-
</CardHeader>
|
|
1367
|
-
|
|
1368
|
-
{!column.collapsed && (
|
|
1369
|
-
<CardContent className="space-y-3" style={{ gap: `${cardGap}px` }}>
|
|
1370
|
-
<ScrollArea className="h-[calc(100vh-300px)]">
|
|
1371
|
-
{column.cards.length === 0 && renderEmptyColumn ? (
|
|
1372
|
-
renderEmptyColumn(column)
|
|
1373
|
-
) : (
|
|
1374
|
-
<AnimatePresence mode={enableAnimations ? "popLayout" : undefined}>
|
|
1375
|
-
{column.cards
|
|
1376
|
-
.sort((a, b) => a.position - b.position)
|
|
1377
|
-
.map((card, index) => (
|
|
1378
|
-
<div
|
|
1379
|
-
key={card.id}
|
|
1380
|
-
draggable={
|
|
1381
|
-
!disabled &&
|
|
1382
|
-
(typeof dragDisabled === 'function' ? !dragDisabled(card) : !dragDisabled)
|
|
1383
|
-
}
|
|
1384
|
-
onDragStart={() => handleDragStart(card, column.id)}
|
|
1385
|
-
onDragEnd={handleDragEnd}
|
|
1386
|
-
onDrop={(e) => {
|
|
1387
|
-
e.preventDefault()
|
|
1388
|
-
e.stopPropagation()
|
|
1389
|
-
handleDrop(e, column.id, index)
|
|
1390
|
-
}}
|
|
1391
|
-
className="mb-3"
|
|
1392
|
-
>
|
|
1393
|
-
<KanbanCardComponent
|
|
1394
|
-
card={card}
|
|
1395
|
-
column={column}
|
|
1396
|
-
isDragging={draggedCard === card.id}
|
|
1397
|
-
onEdit={(e) => {
|
|
1398
|
-
e.stopPropagation()
|
|
1399
|
-
onCardEdit?.(card)
|
|
1400
|
-
}}
|
|
1401
|
-
onDelete={(e) => {
|
|
1402
|
-
e.stopPropagation()
|
|
1403
|
-
onCardDelete?.(card)
|
|
1404
|
-
}}
|
|
1405
|
-
onClick={() => handleCardClick(card)}
|
|
1406
|
-
showDetails={showCardDetails}
|
|
1407
|
-
disabled={disabled}
|
|
1408
|
-
renderCard={renderCard}
|
|
1409
|
-
renderCardPreview={renderCardPreview}
|
|
1410
|
-
renderCardBadge={renderCardBadge}
|
|
1411
|
-
renderCardActions={renderCardActions}
|
|
1412
|
-
cardCompactMode={cardCompactMode}
|
|
1413
|
-
cardShowCoverImage={cardShowCoverImage}
|
|
1414
|
-
cardShowAssignees={cardShowAssignees}
|
|
1415
|
-
cardShowLabels={cardShowLabels}
|
|
1416
|
-
cardShowProgress={cardShowProgress}
|
|
1417
|
-
cardDateFormat={cardDateFormat}
|
|
1418
|
-
cardMaxAssigneesToShow={cardMaxAssigneesToShow}
|
|
1419
|
-
enableAnimations={enableAnimations}
|
|
1420
|
-
animationDuration={animationDuration}
|
|
1421
|
-
cardVariant={cardVariant}
|
|
1422
|
-
/>
|
|
1423
|
-
</div>
|
|
1424
|
-
))}
|
|
1425
|
-
</AnimatePresence>
|
|
1426
|
-
)}
|
|
1427
|
-
</ScrollArea>
|
|
1428
|
-
|
|
1429
|
-
{/* Add card button */}
|
|
1430
|
-
{onAddCard && !column.locked && !isOverLimit && (
|
|
1431
|
-
renderAddCardButton ? (
|
|
1432
|
-
renderAddCardButton(column.id)
|
|
1433
|
-
) : (
|
|
1434
|
-
<DropdownMenu>
|
|
1435
|
-
<DropdownMenuTrigger asChild>
|
|
1436
|
-
<Button
|
|
1437
|
-
variant="ghost"
|
|
1438
|
-
size="sm"
|
|
1439
|
-
className="w-full justify-start text-muted-foreground hover:text-foreground"
|
|
1440
|
-
disabled={disabled}
|
|
1441
|
-
>
|
|
1442
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
1443
|
-
{typeof addCardButtonText === 'function' ? addCardButtonText(column.id) : (addCardButtonText || 'Add card')}
|
|
1444
|
-
</Button>
|
|
1445
|
-
</DropdownMenuTrigger>
|
|
1446
|
-
<DropdownMenuContent align="start" className="w-48">
|
|
1447
|
-
<DropdownMenuLabel>Card Templates</DropdownMenuLabel>
|
|
1448
|
-
<DropdownMenuSeparator />
|
|
1449
|
-
<DropdownMenuItem onClick={() => handleAddCard(column.id)}>
|
|
1450
|
-
<FileText className="mr-2 h-4 w-4" />
|
|
1451
|
-
Blank card
|
|
1452
|
-
</DropdownMenuItem>
|
|
1453
|
-
{cardTemplates.map((template, index) => (
|
|
1454
|
-
<DropdownMenuItem
|
|
1455
|
-
key={index}
|
|
1456
|
-
onClick={() => handleAddCard(column.id, template)}
|
|
1457
|
-
>
|
|
1458
|
-
<Star className="mr-2 h-4 w-4" />
|
|
1459
|
-
{template.title || `Template ${index + 1}`}
|
|
1460
|
-
</DropdownMenuItem>
|
|
1461
|
-
))}
|
|
1462
|
-
</DropdownMenuContent>
|
|
1463
|
-
</DropdownMenu>
|
|
1464
|
-
)
|
|
1465
|
-
)}
|
|
1466
|
-
{/* Column footer */}
|
|
1467
|
-
{renderColumnFooter && renderColumnFooter(column)}
|
|
1468
|
-
</CardContent>
|
|
1469
|
-
)}
|
|
1470
|
-
</Card>
|
|
1471
|
-
</motion.div>
|
|
1472
|
-
)
|
|
1473
|
-
})}
|
|
1474
|
-
</AnimatePresence>
|
|
1475
|
-
|
|
1476
|
-
{/* Add column */}
|
|
1477
|
-
{showAddColumn && onAddColumn && (
|
|
1478
|
-
<motion.div
|
|
1479
|
-
initial={{ opacity: 0 }}
|
|
1480
|
-
animate={{ opacity: 1 }}
|
|
1481
|
-
className="flex-shrink-0 w-80"
|
|
1482
|
-
>
|
|
1483
|
-
{isCreatingColumn ? (
|
|
1484
|
-
<Card>
|
|
1485
|
-
<CardHeader>
|
|
1486
|
-
<Input
|
|
1487
|
-
placeholder="Enter column title..."
|
|
1488
|
-
value={newColumnTitle}
|
|
1489
|
-
onChange={(e) => setNewColumnTitle(e.target.value)}
|
|
1490
|
-
onKeyDown={(e) => {
|
|
1491
|
-
if (e.key === 'Enter' && newColumnTitle) {
|
|
1492
|
-
onAddColumn({ title: newColumnTitle })
|
|
1493
|
-
setNewColumnTitle('')
|
|
1494
|
-
setIsCreatingColumn(false)
|
|
1495
|
-
}
|
|
1496
|
-
if (e.key === 'Escape') {
|
|
1497
|
-
setNewColumnTitle('')
|
|
1498
|
-
setIsCreatingColumn(false)
|
|
1499
|
-
}
|
|
1500
|
-
}}
|
|
1501
|
-
autoFocus
|
|
1502
|
-
/>
|
|
1503
|
-
</CardHeader>
|
|
1504
|
-
<CardContent>
|
|
1505
|
-
<div className="flex gap-2">
|
|
1506
|
-
<Button
|
|
1507
|
-
size="sm"
|
|
1508
|
-
onClick={() => {
|
|
1509
|
-
if (newColumnTitle) {
|
|
1510
|
-
onAddColumn({ title: newColumnTitle })
|
|
1511
|
-
setNewColumnTitle('')
|
|
1512
|
-
setIsCreatingColumn(false)
|
|
1513
|
-
}
|
|
1514
|
-
}}
|
|
1515
|
-
>
|
|
1516
|
-
Add column
|
|
1517
|
-
</Button>
|
|
1518
|
-
<Button
|
|
1519
|
-
size="sm"
|
|
1520
|
-
variant="ghost"
|
|
1521
|
-
onClick={() => {
|
|
1522
|
-
setNewColumnTitle('')
|
|
1523
|
-
setIsCreatingColumn(false)
|
|
1524
|
-
}}
|
|
1525
|
-
>
|
|
1526
|
-
Cancel
|
|
1527
|
-
</Button>
|
|
1528
|
-
</div>
|
|
1529
|
-
</CardContent>
|
|
1530
|
-
</Card>
|
|
1531
|
-
) : (
|
|
1532
|
-
<DropdownMenu>
|
|
1533
|
-
<DropdownMenuTrigger asChild>
|
|
1534
|
-
<Button
|
|
1535
|
-
variant="outline"
|
|
1536
|
-
className="w-full h-full min-h-[200px] border-dashed justify-center items-center"
|
|
1537
|
-
disabled={disabled}
|
|
1538
|
-
>
|
|
1539
|
-
<Plus className="h-6 w-6 mr-2" />
|
|
1540
|
-
Add column
|
|
1541
|
-
</Button>
|
|
1542
|
-
</DropdownMenuTrigger>
|
|
1543
|
-
<DropdownMenuContent align="start" className="w-48">
|
|
1544
|
-
<DropdownMenuLabel>Column Templates</DropdownMenuLabel>
|
|
1545
|
-
<DropdownMenuSeparator />
|
|
1546
|
-
<DropdownMenuItem onClick={() => setIsCreatingColumn(true)}>
|
|
1547
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
1548
|
-
Blank column
|
|
1549
|
-
</DropdownMenuItem>
|
|
1550
|
-
<DropdownMenuItem onClick={() => onAddColumn(COLUMN_TEMPLATES.todo)}>
|
|
1551
|
-
<Square className="mr-2 h-4 w-4" />
|
|
1552
|
-
To Do
|
|
1553
|
-
</DropdownMenuItem>
|
|
1554
|
-
<DropdownMenuItem onClick={() => onAddColumn(COLUMN_TEMPLATES.inProgress)}>
|
|
1555
|
-
<Clock className="mr-2 h-4 w-4" />
|
|
1556
|
-
In Progress
|
|
1557
|
-
</DropdownMenuItem>
|
|
1558
|
-
<DropdownMenuItem onClick={() => onAddColumn(COLUMN_TEMPLATES.done)}>
|
|
1559
|
-
<CheckSquare className="mr-2 h-4 w-4" />
|
|
1560
|
-
Done
|
|
1561
|
-
</DropdownMenuItem>
|
|
1562
|
-
{columnTemplates.map((template, index) => (
|
|
1563
|
-
<DropdownMenuItem
|
|
1564
|
-
key={index}
|
|
1565
|
-
onClick={() => onAddColumn(template)}
|
|
1566
|
-
>
|
|
1567
|
-
<Star className="mr-2 h-4 w-4" />
|
|
1568
|
-
{template.title || `Template ${index + 1}`}
|
|
1569
|
-
</DropdownMenuItem>
|
|
1570
|
-
))}
|
|
1571
|
-
</DropdownMenuContent>
|
|
1572
|
-
</DropdownMenu>
|
|
1573
|
-
)}
|
|
1574
|
-
</motion.div>
|
|
1575
|
-
)}
|
|
1576
|
-
</div>
|
|
1577
|
-
</div>
|
|
1578
|
-
|
|
1579
|
-
{/* Modals */}
|
|
1580
|
-
{/* Card Detail Modal */}
|
|
1581
|
-
{selectedCard && (
|
|
1582
|
-
<CardDetailModal
|
|
1583
|
-
card={selectedCard}
|
|
1584
|
-
isOpen={!!selectedCard}
|
|
1585
|
-
onClose={() => setSelectedCard(null)}
|
|
1586
|
-
onUpdate={handleCardUpdate}
|
|
1587
|
-
onDelete={(card) => {
|
|
1588
|
-
onCardDelete?.(card)
|
|
1589
|
-
toast({
|
|
1590
|
-
title: "Card deleted",
|
|
1591
|
-
description: `"${card.title}" has been deleted`
|
|
1592
|
-
})
|
|
1593
|
-
}}
|
|
1594
|
-
availableAssignees={users}
|
|
1595
|
-
availableLabels={labels}
|
|
1596
|
-
currentColumn={columns.find(col => col.cards.some(c => c.id === selectedCard.id))?.title}
|
|
1597
|
-
availableColumns={columns.map(col => ({ id: col.id, title: col.title }))}
|
|
1598
|
-
/>
|
|
1599
|
-
)}
|
|
1600
|
-
|
|
1601
|
-
{/* Add Card Modal */}
|
|
1602
|
-
{addCardColumnId && (
|
|
1603
|
-
<AddCardModal
|
|
1604
|
-
isOpen={!!addCardColumnId}
|
|
1605
|
-
onClose={() => setAddCardColumnId(null)}
|
|
1606
|
-
onAdd={handleAddNewCard}
|
|
1607
|
-
columnId={addCardColumnId}
|
|
1608
|
-
columnTitle={columns.find(col => col.id === addCardColumnId)?.title || ''}
|
|
1609
|
-
availableAssignees={users}
|
|
1610
|
-
availableLabels={labels}
|
|
1611
|
-
templates={cardTemplates}
|
|
1612
|
-
/>
|
|
1613
|
-
)}
|
|
1614
|
-
|
|
1615
|
-
{/* WIP Limit Modal */}
|
|
1616
|
-
<Dialog open={wipLimitModalOpen} onOpenChange={setWipLimitModalOpen}>
|
|
1617
|
-
<DialogContent className="sm:max-w-md">
|
|
1618
|
-
<DialogHeader>
|
|
1619
|
-
<DialogTitle>Set WIP Limit</DialogTitle>
|
|
1620
|
-
</DialogHeader>
|
|
1621
|
-
<div className="space-y-4 py-4">
|
|
1622
|
-
<div className="space-y-2">
|
|
1623
|
-
<Label htmlFor="wip-limit">Work In Progress Limit</Label>
|
|
1624
|
-
<Input
|
|
1625
|
-
id="wip-limit"
|
|
1626
|
-
type="number"
|
|
1627
|
-
min="0"
|
|
1628
|
-
value={wipLimit || ''}
|
|
1629
|
-
onChange={(e) => setWipLimit(e.target.value ? parseInt(e.target.value) : undefined)}
|
|
1630
|
-
placeholder="Enter a number (leave empty to remove limit)"
|
|
1631
|
-
/>
|
|
1632
|
-
<p className="text-sm text-muted-foreground">
|
|
1633
|
-
Set a maximum number of cards allowed in this column. Leave empty to remove the limit.
|
|
1634
|
-
</p>
|
|
1635
|
-
</div>
|
|
1636
|
-
</div>
|
|
1637
|
-
<DialogFooter>
|
|
1638
|
-
<Button variant="outline" onClick={() => setWipLimitModalOpen(false)}>
|
|
1639
|
-
Cancel
|
|
1640
|
-
</Button>
|
|
1641
|
-
<Button onClick={handleWipLimitUpdate}>
|
|
1642
|
-
Save
|
|
1643
|
-
</Button>
|
|
1644
|
-
</DialogFooter>
|
|
1645
|
-
</DialogContent>
|
|
1646
|
-
</Dialog>
|
|
1647
|
-
|
|
1648
|
-
{/* Color Picker Modal */}
|
|
1649
|
-
<Dialog open={colorPickerOpen} onOpenChange={setColorPickerOpen}>
|
|
1650
|
-
<DialogContent className="sm:max-w-md">
|
|
1651
|
-
<DialogHeader>
|
|
1652
|
-
<DialogTitle>Change Column Color</DialogTitle>
|
|
1653
|
-
</DialogHeader>
|
|
1654
|
-
<div className="space-y-4 py-4">
|
|
1655
|
-
<div className="space-y-2">
|
|
1656
|
-
<Label>Select a color</Label>
|
|
1657
|
-
<div className="grid grid-cols-8 gap-2">
|
|
1658
|
-
{[
|
|
1659
|
-
'#6B7280', '#EF4444', '#F59E0B', '#10B981',
|
|
1660
|
-
'#3B82F6', '#8B5CF6', '#EC4899', '#06B6D4',
|
|
1661
|
-
'#F43F5E', '#84CC16', '#14B8A6', '#6366F1',
|
|
1662
|
-
'#A855F7', '#F472B6', '#0EA5E9', '#22D3EE'
|
|
1663
|
-
].map(color => (
|
|
1664
|
-
<button
|
|
1665
|
-
key={color}
|
|
1666
|
-
className={cn(
|
|
1667
|
-
"w-10 h-10 rounded-md border-2 transition-all",
|
|
1668
|
-
selectedColor === color ? "border-primary scale-110" : "border-transparent"
|
|
1669
|
-
)}
|
|
1670
|
-
style={{ backgroundColor: color }}
|
|
1671
|
-
onClick={() => setSelectedColor(color)}
|
|
1672
|
-
/>
|
|
1673
|
-
))}
|
|
1674
|
-
</div>
|
|
1675
|
-
</div>
|
|
1676
|
-
</div>
|
|
1677
|
-
<DialogFooter>
|
|
1678
|
-
<Button variant="outline" onClick={() => setColorPickerOpen(false)}>
|
|
1679
|
-
Cancel
|
|
1680
|
-
</Button>
|
|
1681
|
-
<Button onClick={handleColorUpdate}>
|
|
1682
|
-
Save
|
|
1683
|
-
</Button>
|
|
1684
|
-
</DialogFooter>
|
|
1685
|
-
</DialogContent>
|
|
1686
|
-
</Dialog>
|
|
1687
|
-
</div>
|
|
1688
|
-
)
|
|
1689
|
-
}
|