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