@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.
Files changed (153) hide show
  1. package/package.json +8 -3
  2. package/plugin/index.d.ts +86 -0
  3. package/plugin/index.js +308 -0
  4. package/scripts/postinstall.js +176 -23
  5. package/src/components/advanced-chart/index.tsx +0 -1246
  6. package/src/components/advanced-forms/index.tsx +0 -585
  7. package/src/components/animated-button/index.tsx +0 -385
  8. package/src/components/calendar/event-dialog.tsx +0 -377
  9. package/src/components/calendar/index.tsx +0 -1220
  10. package/src/components/calendar-pro/index.tsx +0 -1697
  11. package/src/components/color-picker/index.tsx +0 -432
  12. package/src/components/credit-card-input/index.tsx +0 -406
  13. package/src/components/dashboard/dashboard-grid.tsx +0 -480
  14. package/src/components/dashboard/demo.tsx +0 -425
  15. package/src/components/dashboard/index.tsx +0 -1046
  16. package/src/components/dashboard/time-range-picker.tsx +0 -336
  17. package/src/components/dashboard/types.ts +0 -225
  18. package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
  19. package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
  20. package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
  21. package/src/components/dashboard/widgets/index.ts +0 -5
  22. package/src/components/dashboard/widgets/metric-card.tsx +0 -363
  23. package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
  24. package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
  25. package/src/components/data-table/data-table-column-toggle.tsx +0 -169
  26. package/src/components/data-table/data-table-export.ts +0 -156
  27. package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
  28. package/src/components/data-table/index.tsx +0 -845
  29. package/src/components/draggable-list/index.tsx +0 -100
  30. package/src/components/error-boundary/index.tsx +0 -232
  31. package/src/components/file-upload/index.tsx +0 -1660
  32. package/src/components/floating-action-button/index.tsx +0 -206
  33. package/src/components/form-wizard/form-wizard-context.tsx +0 -335
  34. package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
  35. package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
  36. package/src/components/form-wizard/form-wizard-step.tsx +0 -111
  37. package/src/components/form-wizard/index.tsx +0 -102
  38. package/src/components/form-wizard/types.ts +0 -77
  39. package/src/components/gesture-drawer/index.tsx +0 -551
  40. package/src/components/github-stars/github-api.ts +0 -426
  41. package/src/components/github-stars/hooks.ts +0 -517
  42. package/src/components/github-stars/index.tsx +0 -375
  43. package/src/components/github-stars/types.ts +0 -148
  44. package/src/components/github-stars/variants.tsx +0 -515
  45. package/src/components/health-check/index.tsx +0 -439
  46. package/src/components/hover-card-3d/index.tsx +0 -529
  47. package/src/components/index.ts +0 -130
  48. package/src/components/internal/index.ts +0 -78
  49. package/src/components/kanban/add-card-modal.tsx +0 -502
  50. package/src/components/kanban/card-detail-modal.tsx +0 -761
  51. package/src/components/kanban/index.ts +0 -13
  52. package/src/components/kanban/kanban.tsx +0 -1689
  53. package/src/components/kanban/types.ts +0 -168
  54. package/src/components/lazy-component/index.tsx +0 -823
  55. package/src/components/license-error/index.tsx +0 -31
  56. package/src/components/magnetic-button/index.tsx +0 -216
  57. package/src/components/memory-efficient-data/index.tsx +0 -1018
  58. package/src/components/moonui-quiz-form/index.tsx +0 -817
  59. package/src/components/navbar/index.tsx +0 -781
  60. package/src/components/optimized-image/index.tsx +0 -425
  61. package/src/components/performance-debugger/index.tsx +0 -613
  62. package/src/components/performance-monitor/index.tsx +0 -808
  63. package/src/components/phone-number-input/index.tsx +0 -343
  64. package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
  65. package/src/components/pinch-zoom/index.tsx +0 -566
  66. package/src/components/quiz-form/index.tsx +0 -479
  67. package/src/components/rich-text-editor/index.tsx +0 -2322
  68. package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
  69. package/src/components/rich-text-editor/slash-commands.css +0 -35
  70. package/src/components/rich-text-editor/table-styles.css +0 -65
  71. package/src/components/sidebar/index.tsx +0 -884
  72. package/src/components/spotlight-card/index.tsx +0 -191
  73. package/src/components/swipeable-card/index.tsx +0 -100
  74. package/src/components/timeline/index.tsx +0 -1183
  75. package/src/components/ui/accordion.tsx +0 -581
  76. package/src/components/ui/alert-dialog.tsx +0 -141
  77. package/src/components/ui/alert.tsx +0 -141
  78. package/src/components/ui/aspect-ratio.tsx +0 -245
  79. package/src/components/ui/avatar.tsx +0 -155
  80. package/src/components/ui/badge.tsx +0 -230
  81. package/src/components/ui/breadcrumb.tsx +0 -216
  82. package/src/components/ui/button.tsx +0 -228
  83. package/src/components/ui/calendar.tsx +0 -387
  84. package/src/components/ui/card.tsx +0 -216
  85. package/src/components/ui/checkbox.tsx +0 -259
  86. package/src/components/ui/collapsible.tsx +0 -631
  87. package/src/components/ui/color-picker.tsx +0 -97
  88. package/src/components/ui/command.tsx +0 -948
  89. package/src/components/ui/dialog.tsx +0 -752
  90. package/src/components/ui/dropdown-menu.tsx +0 -706
  91. package/src/components/ui/gesture-drawer.tsx +0 -11
  92. package/src/components/ui/hover-card.tsx +0 -29
  93. package/src/components/ui/index.ts +0 -222
  94. package/src/components/ui/input.tsx +0 -224
  95. package/src/components/ui/label.tsx +0 -29
  96. package/src/components/ui/lightbox.tsx +0 -606
  97. package/src/components/ui/magnetic-button.tsx +0 -129
  98. package/src/components/ui/media-gallery.tsx +0 -611
  99. package/src/components/ui/navigation-menu.tsx +0 -130
  100. package/src/components/ui/pagination.tsx +0 -125
  101. package/src/components/ui/popover.tsx +0 -185
  102. package/src/components/ui/progress.tsx +0 -30
  103. package/src/components/ui/radio-group.tsx +0 -257
  104. package/src/components/ui/scroll-area.tsx +0 -47
  105. package/src/components/ui/select.tsx +0 -378
  106. package/src/components/ui/separator.tsx +0 -145
  107. package/src/components/ui/sheet.tsx +0 -139
  108. package/src/components/ui/skeleton.tsx +0 -20
  109. package/src/components/ui/slider.tsx +0 -354
  110. package/src/components/ui/spotlight-card.tsx +0 -119
  111. package/src/components/ui/switch.tsx +0 -86
  112. package/src/components/ui/table.tsx +0 -331
  113. package/src/components/ui/tabs-pro.tsx +0 -542
  114. package/src/components/ui/tabs.tsx +0 -54
  115. package/src/components/ui/textarea.tsx +0 -28
  116. package/src/components/ui/toast.tsx +0 -317
  117. package/src/components/ui/toggle.tsx +0 -119
  118. package/src/components/ui/tooltip.tsx +0 -151
  119. package/src/components/virtual-list/index.tsx +0 -668
  120. package/src/hooks/use-chart.ts +0 -205
  121. package/src/hooks/use-data-table.ts +0 -182
  122. package/src/hooks/use-docs-pro-access.ts +0 -13
  123. package/src/hooks/use-license-check.ts +0 -65
  124. package/src/hooks/use-subscription.ts +0 -19
  125. package/src/hooks/use-toast.ts +0 -15
  126. package/src/index.ts +0 -22
  127. package/src/lib/ai-providers.ts +0 -377
  128. package/src/lib/component-metadata.ts +0 -18
  129. package/src/lib/micro-interactions.ts +0 -255
  130. package/src/lib/paddle.ts +0 -17
  131. package/src/lib/utils.ts +0 -6
  132. package/src/patterns/login-form/index.tsx +0 -276
  133. package/src/patterns/login-form/types.ts +0 -67
  134. package/src/setupTests.ts +0 -41
  135. package/src/styles/advanced-chart.css +0 -239
  136. package/src/styles/calendar.css +0 -35
  137. package/src/styles/design-system.css +0 -363
  138. package/src/styles/index.css +0 -681
  139. package/src/styles/tailwind.css +0 -7
  140. package/src/styles/tokens.css +0 -455
  141. package/src/types/next-auth.d.ts +0 -21
  142. package/src/use-intersection-observer.tsx +0 -154
  143. package/src/use-local-storage.tsx +0 -71
  144. package/src/use-paddle.ts +0 -138
  145. package/src/use-performance-optimizer.ts +0 -389
  146. package/src/use-pro-access.ts +0 -141
  147. package/src/use-scroll-animation.ts +0 -219
  148. package/src/use-subscription.ts +0 -37
  149. package/src/use-toast.ts +0 -32
  150. package/src/utils/chart-helpers.ts +0 -357
  151. package/src/utils/cn.ts +0 -6
  152. package/src/utils/data-processing.ts +0 -151
  153. 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
- }