@moontra/moonui-pro 2.13.0 → 2.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +51 -1
- package/dist/index.mjs +217 -63
- package/package.json +1 -1
- package/src/components/kanban/kanban.tsx +425 -215
- package/src/components/kanban/types.ts +57 -0
|
@@ -69,6 +69,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
|
|
|
69
69
|
import { ColorPicker } from '../ui/color-picker'
|
|
70
70
|
import { CardDetailModal } from './card-detail-modal'
|
|
71
71
|
import { AddCardModal } from './add-card-modal'
|
|
72
|
+
import { format } from 'date-fns'
|
|
72
73
|
import type {
|
|
73
74
|
KanbanAssignee,
|
|
74
75
|
KanbanLabel,
|
|
@@ -170,38 +171,80 @@ const useAutoScroll = () => {
|
|
|
170
171
|
// Card component
|
|
171
172
|
const KanbanCardComponent = ({
|
|
172
173
|
card,
|
|
174
|
+
column,
|
|
173
175
|
isDragging,
|
|
174
176
|
onEdit,
|
|
175
177
|
onDelete,
|
|
176
178
|
onClick,
|
|
177
179
|
showDetails,
|
|
178
|
-
disabled
|
|
180
|
+
disabled,
|
|
181
|
+
renderCard,
|
|
182
|
+
renderCardPreview,
|
|
183
|
+
renderCardBadge,
|
|
184
|
+
renderCardActions,
|
|
185
|
+
cardCompactMode,
|
|
186
|
+
cardShowCoverImage,
|
|
187
|
+
cardShowAssignees,
|
|
188
|
+
cardShowLabels,
|
|
189
|
+
cardShowProgress,
|
|
190
|
+
cardDateFormat,
|
|
191
|
+
cardMaxAssigneesToShow,
|
|
192
|
+
provided,
|
|
193
|
+
enableAnimations,
|
|
194
|
+
animationDuration,
|
|
195
|
+
cardVariant
|
|
179
196
|
}: {
|
|
180
197
|
card: KanbanCard
|
|
198
|
+
column: KanbanColumn
|
|
181
199
|
isDragging: boolean
|
|
182
200
|
onEdit?: (e: React.MouseEvent) => void
|
|
183
201
|
onDelete?: (e: React.MouseEvent) => void
|
|
184
202
|
onClick?: () => void
|
|
185
203
|
showDetails: boolean
|
|
186
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'
|
|
187
220
|
}) => {
|
|
188
221
|
const [isEditingTitle, setIsEditingTitle] = useState(false)
|
|
189
222
|
const [title, setTitle] = useState(card.title)
|
|
190
223
|
const dragControls = useDragControls()
|
|
224
|
+
|
|
225
|
+
// Default values
|
|
226
|
+
const animationsEnabled = enableAnimations ?? true
|
|
227
|
+
const animDuration = animationDuration ?? 0.2
|
|
228
|
+
const variant = cardVariant ?? 'default'
|
|
191
229
|
|
|
192
230
|
const completedChecklistItems = card.checklist?.items.filter(item => item.completed).length || 0
|
|
193
231
|
const totalChecklistItems = card.checklist?.items.length || 0
|
|
194
232
|
const checklistProgress = totalChecklistItems > 0 ? (completedChecklistItems / totalChecklistItems) * 100 : 0
|
|
195
233
|
|
|
234
|
+
// If custom render function is provided, use it
|
|
235
|
+
if (renderCard) {
|
|
236
|
+
return renderCard(card, column, provided || {})
|
|
237
|
+
}
|
|
238
|
+
|
|
196
239
|
return (
|
|
197
240
|
<motion.div
|
|
198
241
|
layout
|
|
199
|
-
initial={{ opacity: 0, y: 20 }}
|
|
242
|
+
initial={{ opacity: animationsEnabled ? 0 : 1, y: animationsEnabled ? 20 : 0 }}
|
|
200
243
|
animate={{ opacity: 1, y: 0 }}
|
|
201
|
-
exit={{ opacity: 0, y: -20 }}
|
|
202
|
-
whileHover={{ scale: 1.02 }}
|
|
203
|
-
whileDrag={{ scale: 1.05, rotate: 3 }}
|
|
204
|
-
transition={{ duration: 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 }}
|
|
205
248
|
className={cn(
|
|
206
249
|
"relative group cursor-pointer select-none",
|
|
207
250
|
isDragging && "z-50"
|
|
@@ -209,7 +252,11 @@ const KanbanCardComponent = ({
|
|
|
209
252
|
>
|
|
210
253
|
<Card
|
|
211
254
|
className={cn(
|
|
212
|
-
"border
|
|
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",
|
|
213
260
|
isDragging && "shadow-2xl ring-2 ring-primary ring-offset-2",
|
|
214
261
|
disabled && "cursor-not-allowed opacity-50"
|
|
215
262
|
)}
|
|
@@ -222,7 +269,7 @@ const KanbanCardComponent = ({
|
|
|
222
269
|
/>
|
|
223
270
|
|
|
224
271
|
{/* Cover image */}
|
|
225
|
-
{card.coverImage && (
|
|
272
|
+
{cardShowCoverImage && card.coverImage && (
|
|
226
273
|
<div className="relative h-32 -mx-px -mt-px rounded-t-lg overflow-hidden">
|
|
227
274
|
<img
|
|
228
275
|
src={card.coverImage}
|
|
@@ -235,7 +282,7 @@ const KanbanCardComponent = ({
|
|
|
235
282
|
|
|
236
283
|
<CardContent className="p-3">
|
|
237
284
|
{/* Labels */}
|
|
238
|
-
{card.labels && card.labels.length > 0 && (
|
|
285
|
+
{cardShowLabels && card.labels && card.labels.length > 0 && (
|
|
239
286
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
240
287
|
{card.labels.map((label) => (
|
|
241
288
|
<div
|
|
@@ -276,27 +323,32 @@ const KanbanCardComponent = ({
|
|
|
276
323
|
</div>
|
|
277
324
|
|
|
278
325
|
{/* Quick actions */}
|
|
279
|
-
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
</
|
|
296
|
-
<
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
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>
|
|
300
352
|
<DropdownMenuItem>
|
|
301
353
|
<Move className="mr-2 h-4 w-4" />
|
|
302
354
|
Move
|
|
@@ -313,20 +365,21 @@ const KanbanCardComponent = ({
|
|
|
313
365
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
314
366
|
Delete
|
|
315
367
|
</DropdownMenuItem>
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
368
|
+
</DropdownMenuContent>
|
|
369
|
+
</DropdownMenu>
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
319
372
|
</div>
|
|
320
373
|
|
|
321
374
|
{/* Description */}
|
|
322
|
-
{card.description && (
|
|
375
|
+
{!cardCompactMode && card.description && (
|
|
323
376
|
<p className="text-xs text-muted-foreground mb-3 line-clamp-2">
|
|
324
377
|
{card.description}
|
|
325
378
|
</p>
|
|
326
379
|
)}
|
|
327
380
|
|
|
328
381
|
{/* Progress bar */}
|
|
329
|
-
{(card.progress !== undefined || card.checklist) && (
|
|
382
|
+
{cardShowProgress && (card.progress !== undefined || card.checklist) && (
|
|
330
383
|
<div className="mb-3">
|
|
331
384
|
<div className="flex justify-between text-xs text-muted-foreground mb-1">
|
|
332
385
|
<span>Progress</span>
|
|
@@ -337,7 +390,7 @@ const KanbanCardComponent = ({
|
|
|
337
390
|
)}
|
|
338
391
|
|
|
339
392
|
{/* Tags */}
|
|
340
|
-
{card.tags && card.tags.length > 0 && (
|
|
393
|
+
{!cardCompactMode && card.tags && card.tags.length > 0 && (
|
|
341
394
|
<div className="flex flex-wrap gap-1 mb-3">
|
|
342
395
|
{card.tags.map((tag, index) => (
|
|
343
396
|
<Badge key={index} variant="secondary" className="text-xs px-1.5 py-0">
|
|
@@ -366,7 +419,7 @@ const KanbanCardComponent = ({
|
|
|
366
419
|
isOverdue(card.dueDate) && "text-destructive"
|
|
367
420
|
)}>
|
|
368
421
|
<Calendar className="h-3 w-3" />
|
|
369
|
-
<span>{formatDate(card.dueDate)}</span>
|
|
422
|
+
<span>{cardDateFormat ? format(card.dueDate, cardDateFormat) : formatDate(card.dueDate)}</span>
|
|
370
423
|
</div>
|
|
371
424
|
)}
|
|
372
425
|
|
|
@@ -397,8 +450,8 @@ const KanbanCardComponent = ({
|
|
|
397
450
|
)}
|
|
398
451
|
|
|
399
452
|
{/* Assignees */}
|
|
400
|
-
{card.assignees && card.assignees.length > 0 && (
|
|
401
|
-
<MoonUIAvatarGroupPro max={
|
|
453
|
+
{cardShowAssignees && card.assignees && card.assignees.length > 0 && (
|
|
454
|
+
<MoonUIAvatarGroupPro max={cardMaxAssigneesToShow} size="xs">
|
|
402
455
|
{card.assignees.map((assignee) => (
|
|
403
456
|
<MoonUIAvatarPro key={assignee.id} className="h-5 w-5">
|
|
404
457
|
<MoonUIAvatarImagePro src={assignee.avatar} />
|
|
@@ -445,7 +498,54 @@ export function Kanban({
|
|
|
445
498
|
loading = false,
|
|
446
499
|
disabled = false,
|
|
447
500
|
labels = [],
|
|
448
|
-
users = []
|
|
501
|
+
users = [],
|
|
502
|
+
// Card Render Customization
|
|
503
|
+
renderCard,
|
|
504
|
+
renderCardPreview,
|
|
505
|
+
renderCardBadge,
|
|
506
|
+
renderCardActions,
|
|
507
|
+
cardCompactMode = false,
|
|
508
|
+
cardShowCoverImage = true,
|
|
509
|
+
cardShowAssignees = true,
|
|
510
|
+
cardShowLabels = true,
|
|
511
|
+
cardShowProgress = true,
|
|
512
|
+
cardDateFormat,
|
|
513
|
+
cardMaxAssigneesToShow = 3,
|
|
514
|
+
// Add Card Customization
|
|
515
|
+
renderAddCardButton,
|
|
516
|
+
renderAddCardForm,
|
|
517
|
+
addCardButtonText,
|
|
518
|
+
addCardPosition = 'bottom',
|
|
519
|
+
allowQuickAdd = true,
|
|
520
|
+
quickAddFields = ['title'],
|
|
521
|
+
validateCard,
|
|
522
|
+
onBeforeCardAdd,
|
|
523
|
+
// Column Customization
|
|
524
|
+
renderColumnHeader,
|
|
525
|
+
renderColumnFooter,
|
|
526
|
+
renderEmptyColumn,
|
|
527
|
+
columnMenuActions,
|
|
528
|
+
allowColumnReorder = true,
|
|
529
|
+
columnColorOptions,
|
|
530
|
+
columnDefaultColor = '#6B7280',
|
|
531
|
+
// Drag & Drop Enhancement
|
|
532
|
+
dragDisabled = false,
|
|
533
|
+
dropDisabled = false,
|
|
534
|
+
dragPreview = 'card',
|
|
535
|
+
renderDragPreview,
|
|
536
|
+
canDrop,
|
|
537
|
+
onDragStart,
|
|
538
|
+
onDragEnd,
|
|
539
|
+
// UI/UX Customization
|
|
540
|
+
theme = 'default',
|
|
541
|
+
cardVariant = 'default',
|
|
542
|
+
enableAnimations = true,
|
|
543
|
+
animationDuration = 0.2,
|
|
544
|
+
columnWidth,
|
|
545
|
+
columnGap = 24,
|
|
546
|
+
cardGap = 12,
|
|
547
|
+
showTooltips = true,
|
|
548
|
+
tooltipDelay = 500
|
|
449
549
|
}: KanbanProps) {
|
|
450
550
|
// Check pro access
|
|
451
551
|
const { hasProAccess, isLoading } = useSubscription()
|
|
@@ -503,6 +603,11 @@ export function Kanban({
|
|
|
503
603
|
const { scrollRef, startAutoScroll, stopAutoScroll } = useAutoScroll()
|
|
504
604
|
const { toast } = useToast()
|
|
505
605
|
|
|
606
|
+
// Update state when props change
|
|
607
|
+
useEffect(() => {
|
|
608
|
+
setColumns(initialColumns)
|
|
609
|
+
}, [initialColumns])
|
|
610
|
+
|
|
506
611
|
// Filter cards based on search and filters
|
|
507
612
|
const filteredColumns = useMemo(() => {
|
|
508
613
|
if (!searchQuery && !activeFilter) return columns
|
|
@@ -579,11 +684,22 @@ export function Kanban({
|
|
|
579
684
|
// Drag handlers
|
|
580
685
|
const handleDragStart = (card: KanbanCard, columnId: string) => {
|
|
581
686
|
if (disabled) return
|
|
687
|
+
if (typeof dragDisabled === 'function' && dragDisabled(card)) return
|
|
688
|
+
if (dragDisabled === true) return
|
|
689
|
+
|
|
582
690
|
setDraggedCard(card.id)
|
|
691
|
+
const column = columns.find(col => col.id === columnId)
|
|
692
|
+
if (column && onDragStart) {
|
|
693
|
+
onDragStart(card, column)
|
|
694
|
+
}
|
|
583
695
|
}
|
|
584
696
|
|
|
585
697
|
const handleDragOver = (e: React.DragEvent, columnId: string) => {
|
|
586
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
|
+
|
|
587
703
|
e.preventDefault()
|
|
588
704
|
setDraggedOverColumn(columnId)
|
|
589
705
|
|
|
@@ -604,14 +720,28 @@ export function Kanban({
|
|
|
604
720
|
}
|
|
605
721
|
|
|
606
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
|
+
}
|
|
607
730
|
setDraggedCard(null)
|
|
608
731
|
setDraggedOverColumn(null)
|
|
609
732
|
stopAutoScroll()
|
|
610
733
|
}
|
|
611
734
|
|
|
612
735
|
const handleDrop = (e: React.DragEvent, targetColumnId: string, targetIndex: number) => {
|
|
613
|
-
if (disabled || !draggedCard
|
|
736
|
+
if (disabled || !draggedCard) return
|
|
614
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
|
+
}
|
|
615
745
|
|
|
616
746
|
// Find source column and card
|
|
617
747
|
let sourceColumnId: string | null = null
|
|
@@ -627,7 +757,30 @@ export function Kanban({
|
|
|
627
757
|
}
|
|
628
758
|
|
|
629
759
|
if (sourceColumnId && sourceCard) {
|
|
630
|
-
|
|
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
|
+
}
|
|
631
784
|
}
|
|
632
785
|
|
|
633
786
|
handleDragEnd()
|
|
@@ -713,10 +866,9 @@ export function Kanban({
|
|
|
713
866
|
|
|
714
867
|
// Card handlers
|
|
715
868
|
const handleCardClick = (card: KanbanCard) => {
|
|
869
|
+
setSelectedCard(card)
|
|
716
870
|
if (onCardClick) {
|
|
717
871
|
onCardClick(card)
|
|
718
|
-
} else {
|
|
719
|
-
setSelectedCard(card)
|
|
720
872
|
}
|
|
721
873
|
}
|
|
722
874
|
|
|
@@ -729,11 +881,7 @@ export function Kanban({
|
|
|
729
881
|
}
|
|
730
882
|
|
|
731
883
|
const handleAddCard = (columnId: string, newCard?: Partial<KanbanCard>) => {
|
|
732
|
-
|
|
733
|
-
onAddCard(columnId, newCard)
|
|
734
|
-
} else {
|
|
735
|
-
setAddCardColumnId(columnId)
|
|
736
|
-
}
|
|
884
|
+
setAddCardColumnId(columnId)
|
|
737
885
|
}
|
|
738
886
|
|
|
739
887
|
const handleAddNewCard = (card: Partial<KanbanCard>) => {
|
|
@@ -746,6 +894,7 @@ export function Kanban({
|
|
|
746
894
|
...card
|
|
747
895
|
}
|
|
748
896
|
|
|
897
|
+
// Update local state
|
|
749
898
|
setColumns(columns.map(col => {
|
|
750
899
|
if (col.id === addCardColumnId) {
|
|
751
900
|
return {
|
|
@@ -756,7 +905,11 @@ export function Kanban({
|
|
|
756
905
|
return col
|
|
757
906
|
}))
|
|
758
907
|
|
|
759
|
-
|
|
908
|
+
// Call the callback if provided
|
|
909
|
+
if (onAddCard) {
|
|
910
|
+
onAddCard(addCardColumnId, newCard)
|
|
911
|
+
}
|
|
912
|
+
|
|
760
913
|
toast({
|
|
761
914
|
title: "Card added",
|
|
762
915
|
description: `"${newCard.title}" has been added`
|
|
@@ -1017,7 +1170,8 @@ export function Kanban({
|
|
|
1017
1170
|
{/* Kanban board */}
|
|
1018
1171
|
<div
|
|
1019
1172
|
ref={scrollRef}
|
|
1020
|
-
className="flex
|
|
1173
|
+
className="flex overflow-x-auto pb-4"
|
|
1174
|
+
style={{ gap: `${columnGap}px` }}
|
|
1021
1175
|
onDragOver={(e) => e.preventDefault()}
|
|
1022
1176
|
>
|
|
1023
1177
|
<AnimatePresence mode="sync">
|
|
@@ -1028,14 +1182,19 @@ export function Kanban({
|
|
|
1028
1182
|
return (
|
|
1029
1183
|
<motion.div
|
|
1030
1184
|
key={column.id}
|
|
1031
|
-
layout
|
|
1032
|
-
initial={{ opacity: 0, x: -20 }}
|
|
1185
|
+
layout={enableAnimations}
|
|
1186
|
+
initial={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? -20 : 0 }}
|
|
1033
1187
|
animate={{ opacity: 1, x: 0 }}
|
|
1034
|
-
exit={{ opacity: 0, x: 20 }}
|
|
1188
|
+
exit={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? 20 : 0 }}
|
|
1035
1189
|
className={cn(
|
|
1036
|
-
"flex-shrink-0
|
|
1190
|
+
"flex-shrink-0 transition-all",
|
|
1037
1191
|
isDraggedOver && "scale-105"
|
|
1038
1192
|
)}
|
|
1193
|
+
style={{
|
|
1194
|
+
width: columnWidth === 'auto' ? 'auto' : (columnWidth || 320) + 'px',
|
|
1195
|
+
minWidth: columnWidth === 'auto' ? '300px' : undefined,
|
|
1196
|
+
transitionDuration: enableAnimations ? `${animationDuration}s` : '0s'
|
|
1197
|
+
}}
|
|
1039
1198
|
onDragOver={(e) => handleDragOver(e, column.id)}
|
|
1040
1199
|
onDragLeave={() => setDraggedOverColumn(null)}
|
|
1041
1200
|
onDrop={(e) => handleDrop(e, column.id, column.cards.length)}
|
|
@@ -1046,149 +1205,178 @@ export function Kanban({
|
|
|
1046
1205
|
column.collapsed && "opacity-60"
|
|
1047
1206
|
)}>
|
|
1048
1207
|
<CardHeader className="pb-3">
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
{editingColumnId === column.id ? (
|
|
1062
|
-
<Input
|
|
1063
|
-
value={editingColumnTitle}
|
|
1064
|
-
onChange={(e) => setEditingColumnTitle(e.target.value)}
|
|
1065
|
-
onBlur={() => handleColumnRename(column.id)}
|
|
1066
|
-
onKeyDown={(e) => {
|
|
1067
|
-
if (e.key === 'Enter') {
|
|
1068
|
-
handleColumnRename(column.id)
|
|
1069
|
-
}
|
|
1070
|
-
if (e.key === 'Escape') {
|
|
1071
|
-
setEditingColumnId(null)
|
|
1072
|
-
setEditingColumnTitle('')
|
|
1073
|
-
}
|
|
1074
|
-
}}
|
|
1075
|
-
className="h-6 w-32 text-sm"
|
|
1076
|
-
autoFocus
|
|
1077
|
-
onClick={(e) => e.stopPropagation()}
|
|
1078
|
-
/>
|
|
1079
|
-
) : (
|
|
1080
|
-
<>
|
|
1081
|
-
{column.title}
|
|
1082
|
-
{column.locked && <Lock className="h-3 w-3" />}
|
|
1083
|
-
</>
|
|
1084
|
-
)}
|
|
1085
|
-
</CardTitle>
|
|
1086
|
-
|
|
1087
|
-
{/* Card count */}
|
|
1088
|
-
<Badge variant="secondary" className="text-xs">
|
|
1089
|
-
{column.cards.length}
|
|
1090
|
-
</Badge>
|
|
1091
|
-
</div>
|
|
1092
|
-
|
|
1093
|
-
{/* Column actions */}
|
|
1094
|
-
<DropdownMenu>
|
|
1095
|
-
<DropdownMenuTrigger asChild>
|
|
1096
|
-
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
|
1097
|
-
<MoreHorizontal className="h-4 w-4" />
|
|
1098
|
-
</Button>
|
|
1099
|
-
</DropdownMenuTrigger>
|
|
1100
|
-
<DropdownMenuContent align="end">
|
|
1101
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'rename')}>
|
|
1102
|
-
<Edit className="mr-2 h-4 w-4" />
|
|
1103
|
-
Rename
|
|
1104
|
-
</DropdownMenuItem>
|
|
1105
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, 'collapse')}>
|
|
1106
|
-
{column.collapsed ? (
|
|
1107
|
-
<>
|
|
1108
|
-
<Eye className="mr-2 h-4 w-4" />
|
|
1109
|
-
Expand
|
|
1110
|
-
</>
|
|
1111
|
-
) : (
|
|
1112
|
-
<>
|
|
1113
|
-
<EyeOff className="mr-2 h-4 w-4" />
|
|
1114
|
-
Collapse
|
|
1115
|
-
</>
|
|
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
|
+
/>
|
|
1116
1220
|
)}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
|
1141
1282
|
</DropdownMenuItem>
|
|
1142
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, '
|
|
1143
|
-
|
|
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
|
+
)}
|
|
1144
1295
|
</DropdownMenuItem>
|
|
1145
|
-
<
|
|
1146
|
-
|
|
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
|
|
1147
1337
|
</DropdownMenuItem>
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
</
|
|
1151
|
-
</
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
)}>
|
|
1170
|
-
{isOverLimit ? (
|
|
1171
|
-
<>
|
|
1172
|
-
<AlertCircle className="h-3 w-3" />
|
|
1173
|
-
Over WIP limit ({column.cards.length}/{column.limit})
|
|
1174
|
-
</>
|
|
1175
|
-
) : (
|
|
1176
|
-
`WIP limit: ${column.cards.length}/${column.limit}`
|
|
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>
|
|
1177
1359
|
)}
|
|
1178
|
-
|
|
1360
|
+
</>
|
|
1179
1361
|
)}
|
|
1180
1362
|
</CardHeader>
|
|
1181
1363
|
|
|
1182
1364
|
{!column.collapsed && (
|
|
1183
|
-
<CardContent className="space-y-3">
|
|
1365
|
+
<CardContent className="space-y-3" style={{ gap: `${cardGap}px` }}>
|
|
1184
1366
|
<ScrollArea className="h-[calc(100vh-300px)]">
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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) => (
|
|
1189
1374
|
<div
|
|
1190
1375
|
key={card.id}
|
|
1191
|
-
draggable={
|
|
1376
|
+
draggable={
|
|
1377
|
+
!disabled &&
|
|
1378
|
+
(typeof dragDisabled === 'function' ? !dragDisabled(card) : !dragDisabled)
|
|
1379
|
+
}
|
|
1192
1380
|
onDragStart={() => handleDragStart(card, column.id)}
|
|
1193
1381
|
onDragEnd={handleDragEnd}
|
|
1194
1382
|
onDrop={(e) => {
|
|
@@ -1200,6 +1388,7 @@ export function Kanban({
|
|
|
1200
1388
|
>
|
|
1201
1389
|
<KanbanCardComponent
|
|
1202
1390
|
card={card}
|
|
1391
|
+
column={column}
|
|
1203
1392
|
isDragging={draggedCard === card.id}
|
|
1204
1393
|
onEdit={(e) => {
|
|
1205
1394
|
e.stopPropagation()
|
|
@@ -1212,45 +1401,66 @@ export function Kanban({
|
|
|
1212
1401
|
onClick={() => handleCardClick(card)}
|
|
1213
1402
|
showDetails={showCardDetails}
|
|
1214
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}
|
|
1215
1418
|
/>
|
|
1216
1419
|
</div>
|
|
1217
1420
|
))}
|
|
1218
|
-
|
|
1421
|
+
</AnimatePresence>
|
|
1422
|
+
)}
|
|
1219
1423
|
</ScrollArea>
|
|
1220
1424
|
|
|
1221
1425
|
{/* Add card button */}
|
|
1222
1426
|
{onAddCard && !column.locked && !isOverLimit && (
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
</Button>
|
|
1234
|
-
</DropdownMenuTrigger>
|
|
1235
|
-
<DropdownMenuContent align="start" className="w-48">
|
|
1236
|
-
<DropdownMenuLabel>Card Templates</DropdownMenuLabel>
|
|
1237
|
-
<DropdownMenuSeparator />
|
|
1238
|
-
<DropdownMenuItem onClick={() => handleAddCard(column.id)}>
|
|
1239
|
-
<FileText className="mr-2 h-4 w-4" />
|
|
1240
|
-
Blank card
|
|
1241
|
-
</DropdownMenuItem>
|
|
1242
|
-
{cardTemplates.map((template, index) => (
|
|
1243
|
-
<DropdownMenuItem
|
|
1244
|
-
key={index}
|
|
1245
|
-
onClick={() => handleAddCard(column.id, template)}
|
|
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}
|
|
1246
1437
|
>
|
|
1247
|
-
<
|
|
1248
|
-
{
|
|
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
|
|
1249
1448
|
</DropdownMenuItem>
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
+
)
|
|
1253
1461
|
)}
|
|
1462
|
+
{/* Column footer */}
|
|
1463
|
+
{renderColumnFooter && renderColumnFooter(column)}
|
|
1254
1464
|
</CardContent>
|
|
1255
1465
|
)}
|
|
1256
1466
|
</Card>
|