@moontra/moonui-pro 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +51 -1
- package/dist/index.mjs +186 -52
- package/package.json +1 -1
- package/src/components/kanban/kanban.tsx +387 -205
- 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()
|
|
@@ -579,11 +679,22 @@ export function Kanban({
|
|
|
579
679
|
// Drag handlers
|
|
580
680
|
const handleDragStart = (card: KanbanCard, columnId: string) => {
|
|
581
681
|
if (disabled) return
|
|
682
|
+
if (typeof dragDisabled === 'function' && dragDisabled(card)) return
|
|
683
|
+
if (dragDisabled === true) return
|
|
684
|
+
|
|
582
685
|
setDraggedCard(card.id)
|
|
686
|
+
const column = columns.find(col => col.id === columnId)
|
|
687
|
+
if (column && onDragStart) {
|
|
688
|
+
onDragStart(card, column)
|
|
689
|
+
}
|
|
583
690
|
}
|
|
584
691
|
|
|
585
692
|
const handleDragOver = (e: React.DragEvent, columnId: string) => {
|
|
586
693
|
if (disabled) return
|
|
694
|
+
const column = columns.find(col => col.id === columnId)
|
|
695
|
+
if (column && typeof dropDisabled === 'function' && dropDisabled(column)) return
|
|
696
|
+
if (dropDisabled === true) return
|
|
697
|
+
|
|
587
698
|
e.preventDefault()
|
|
588
699
|
setDraggedOverColumn(columnId)
|
|
589
700
|
|
|
@@ -604,6 +715,13 @@ export function Kanban({
|
|
|
604
715
|
}
|
|
605
716
|
|
|
606
717
|
const handleDragEnd = () => {
|
|
718
|
+
if (draggedCard) {
|
|
719
|
+
const card = columns.flatMap(col => col.cards).find(c => c.id === draggedCard)
|
|
720
|
+
const column = columns.find(col => col.cards.some(c => c.id === draggedCard))
|
|
721
|
+
if (card && column && onDragEnd) {
|
|
722
|
+
onDragEnd(card, column)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
607
725
|
setDraggedCard(null)
|
|
608
726
|
setDraggedOverColumn(null)
|
|
609
727
|
stopAutoScroll()
|
|
@@ -612,6 +730,13 @@ export function Kanban({
|
|
|
612
730
|
const handleDrop = (e: React.DragEvent, targetColumnId: string, targetIndex: number) => {
|
|
613
731
|
if (disabled || !draggedCard || !onCardMove) return
|
|
614
732
|
e.preventDefault()
|
|
733
|
+
|
|
734
|
+
const targetColumn = columns.find(col => col.id === targetColumnId)
|
|
735
|
+
const draggedCardObj = columns.flatMap(col => col.cards).find(card => card.id === draggedCard)
|
|
736
|
+
|
|
737
|
+
if (targetColumn && draggedCardObj && canDrop && !canDrop(draggedCardObj, targetColumn, targetIndex)) {
|
|
738
|
+
return
|
|
739
|
+
}
|
|
615
740
|
|
|
616
741
|
// Find source column and card
|
|
617
742
|
let sourceColumnId: string | null = null
|
|
@@ -1017,7 +1142,8 @@ export function Kanban({
|
|
|
1017
1142
|
{/* Kanban board */}
|
|
1018
1143
|
<div
|
|
1019
1144
|
ref={scrollRef}
|
|
1020
|
-
className="flex
|
|
1145
|
+
className="flex overflow-x-auto pb-4"
|
|
1146
|
+
style={{ gap: `${columnGap}px` }}
|
|
1021
1147
|
onDragOver={(e) => e.preventDefault()}
|
|
1022
1148
|
>
|
|
1023
1149
|
<AnimatePresence mode="sync">
|
|
@@ -1028,14 +1154,19 @@ export function Kanban({
|
|
|
1028
1154
|
return (
|
|
1029
1155
|
<motion.div
|
|
1030
1156
|
key={column.id}
|
|
1031
|
-
layout
|
|
1032
|
-
initial={{ opacity: 0, x: -20 }}
|
|
1157
|
+
layout={enableAnimations}
|
|
1158
|
+
initial={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? -20 : 0 }}
|
|
1033
1159
|
animate={{ opacity: 1, x: 0 }}
|
|
1034
|
-
exit={{ opacity: 0, x: 20 }}
|
|
1160
|
+
exit={{ opacity: enableAnimations ? 0 : 1, x: enableAnimations ? 20 : 0 }}
|
|
1035
1161
|
className={cn(
|
|
1036
|
-
"flex-shrink-0
|
|
1162
|
+
"flex-shrink-0 transition-all",
|
|
1037
1163
|
isDraggedOver && "scale-105"
|
|
1038
1164
|
)}
|
|
1165
|
+
style={{
|
|
1166
|
+
width: columnWidth === 'auto' ? 'auto' : (columnWidth || 320) + 'px',
|
|
1167
|
+
minWidth: columnWidth === 'auto' ? '300px' : undefined,
|
|
1168
|
+
transitionDuration: enableAnimations ? `${animationDuration}s` : '0s'
|
|
1169
|
+
}}
|
|
1039
1170
|
onDragOver={(e) => handleDragOver(e, column.id)}
|
|
1040
1171
|
onDragLeave={() => setDraggedOverColumn(null)}
|
|
1041
1172
|
onDrop={(e) => handleDrop(e, column.id, column.cards.length)}
|
|
@@ -1046,149 +1177,178 @@ export function Kanban({
|
|
|
1046
1177
|
column.collapsed && "opacity-60"
|
|
1047
1178
|
)}>
|
|
1048
1179
|
<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
|
-
</>
|
|
1180
|
+
{renderColumnHeader ? (
|
|
1181
|
+
renderColumnHeader(column)
|
|
1182
|
+
) : (
|
|
1183
|
+
<>
|
|
1184
|
+
<div className="flex items-center justify-between">
|
|
1185
|
+
<div className="flex items-center gap-2">
|
|
1186
|
+
{/* Column color indicator */}
|
|
1187
|
+
{column.color && (
|
|
1188
|
+
<div
|
|
1189
|
+
className="w-3 h-3 rounded-full"
|
|
1190
|
+
style={{ backgroundColor: column.color }}
|
|
1191
|
+
/>
|
|
1116
1192
|
)}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1193
|
+
|
|
1194
|
+
{/* Column title */}
|
|
1195
|
+
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
1196
|
+
{editingColumnId === column.id ? (
|
|
1197
|
+
<Input
|
|
1198
|
+
value={editingColumnTitle}
|
|
1199
|
+
onChange={(e) => setEditingColumnTitle(e.target.value)}
|
|
1200
|
+
onBlur={() => handleColumnRename(column.id)}
|
|
1201
|
+
onKeyDown={(e) => {
|
|
1202
|
+
if (e.key === 'Enter') {
|
|
1203
|
+
handleColumnRename(column.id)
|
|
1204
|
+
}
|
|
1205
|
+
if (e.key === 'Escape') {
|
|
1206
|
+
setEditingColumnId(null)
|
|
1207
|
+
setEditingColumnTitle('')
|
|
1208
|
+
}
|
|
1209
|
+
}}
|
|
1210
|
+
className="h-6 w-32 text-sm"
|
|
1211
|
+
autoFocus
|
|
1212
|
+
onClick={(e) => e.stopPropagation()}
|
|
1213
|
+
/>
|
|
1214
|
+
) : (
|
|
1215
|
+
<>
|
|
1216
|
+
{column.title}
|
|
1217
|
+
{column.locked && <Lock className="h-3 w-3" />}
|
|
1218
|
+
</>
|
|
1219
|
+
)}
|
|
1220
|
+
</CardTitle>
|
|
1221
|
+
|
|
1222
|
+
{/* Card count */}
|
|
1223
|
+
<Badge variant="secondary" className="text-xs">
|
|
1224
|
+
{column.cards.length}
|
|
1225
|
+
</Badge>
|
|
1226
|
+
</div>
|
|
1227
|
+
|
|
1228
|
+
{/* Column actions */}
|
|
1229
|
+
<DropdownMenu>
|
|
1230
|
+
<DropdownMenuTrigger asChild>
|
|
1231
|
+
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
|
1232
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
1233
|
+
</Button>
|
|
1234
|
+
</DropdownMenuTrigger>
|
|
1235
|
+
<DropdownMenuContent align="end">
|
|
1236
|
+
{columnMenuActions ? (
|
|
1237
|
+
columnMenuActions.map((action, index) => {
|
|
1238
|
+
if (action.visible && !action.visible(column)) return null
|
|
1239
|
+
return (
|
|
1240
|
+
<DropdownMenuItem
|
|
1241
|
+
key={index}
|
|
1242
|
+
onClick={() => action.action(column)}
|
|
1243
|
+
>
|
|
1244
|
+
{action.icon || null}
|
|
1245
|
+
{action.label}
|
|
1246
|
+
</DropdownMenuItem>
|
|
1247
|
+
)
|
|
1248
|
+
})
|
|
1249
|
+
) : (
|
|
1250
|
+
<>
|
|
1251
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'rename')}>
|
|
1252
|
+
<Edit className="mr-2 h-4 w-4" />
|
|
1253
|
+
Rename
|
|
1141
1254
|
</DropdownMenuItem>
|
|
1142
|
-
<DropdownMenuItem onClick={() => handleColumnAction(column, '
|
|
1143
|
-
|
|
1255
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'collapse')}>
|
|
1256
|
+
{column.collapsed ? (
|
|
1257
|
+
<>
|
|
1258
|
+
<Eye className="mr-2 h-4 w-4" />
|
|
1259
|
+
Expand
|
|
1260
|
+
</>
|
|
1261
|
+
) : (
|
|
1262
|
+
<>
|
|
1263
|
+
<EyeOff className="mr-2 h-4 w-4" />
|
|
1264
|
+
Collapse
|
|
1265
|
+
</>
|
|
1266
|
+
)}
|
|
1144
1267
|
</DropdownMenuItem>
|
|
1145
|
-
<
|
|
1146
|
-
|
|
1268
|
+
<DropdownMenuSeparator />
|
|
1269
|
+
<DropdownMenuSub>
|
|
1270
|
+
<DropdownMenuSubTrigger>
|
|
1271
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
1272
|
+
Settings
|
|
1273
|
+
</DropdownMenuSubTrigger>
|
|
1274
|
+
<DropdownMenuSubContent>
|
|
1275
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'setLimit')}>
|
|
1276
|
+
<Timer className="mr-2 h-4 w-4" />
|
|
1277
|
+
Set WIP limit
|
|
1278
|
+
</DropdownMenuItem>
|
|
1279
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'changeColor')}>
|
|
1280
|
+
<Palette className="mr-2 h-4 w-4" />
|
|
1281
|
+
Change color
|
|
1282
|
+
</DropdownMenuItem>
|
|
1283
|
+
<DropdownMenuSub>
|
|
1284
|
+
<DropdownMenuSubTrigger>
|
|
1285
|
+
<ArrowUpDown className="mr-2 h-4 w-4" />
|
|
1286
|
+
Sort cards
|
|
1287
|
+
</DropdownMenuSubTrigger>
|
|
1288
|
+
<DropdownMenuSubContent>
|
|
1289
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByPriority')}>
|
|
1290
|
+
By Priority
|
|
1291
|
+
</DropdownMenuItem>
|
|
1292
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortByDueDate')}>
|
|
1293
|
+
By Due Date
|
|
1294
|
+
</DropdownMenuItem>
|
|
1295
|
+
<DropdownMenuItem onClick={() => handleColumnAction(column, 'sortAlphabetically')}>
|
|
1296
|
+
Alphabetically
|
|
1297
|
+
</DropdownMenuItem>
|
|
1298
|
+
</DropdownMenuSubContent>
|
|
1299
|
+
</DropdownMenuSub>
|
|
1300
|
+
</DropdownMenuSubContent>
|
|
1301
|
+
</DropdownMenuSub>
|
|
1302
|
+
<DropdownMenuSeparator />
|
|
1303
|
+
<DropdownMenuItem
|
|
1304
|
+
onClick={() => handleColumnAction(column, 'delete')}
|
|
1305
|
+
className="text-destructive"
|
|
1306
|
+
>
|
|
1307
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
1308
|
+
Delete column
|
|
1147
1309
|
</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}`
|
|
1310
|
+
</>
|
|
1311
|
+
)}
|
|
1312
|
+
</DropdownMenuContent>
|
|
1313
|
+
</DropdownMenu>
|
|
1314
|
+
</div>
|
|
1315
|
+
|
|
1316
|
+
{/* WIP limit warning */}
|
|
1317
|
+
{column.limit && (
|
|
1318
|
+
<CardDescription className={cn(
|
|
1319
|
+
"text-xs flex items-center gap-1 mt-1",
|
|
1320
|
+
isOverLimit && "text-destructive"
|
|
1321
|
+
)}>
|
|
1322
|
+
{isOverLimit ? (
|
|
1323
|
+
<>
|
|
1324
|
+
<AlertCircle className="h-3 w-3" />
|
|
1325
|
+
Over WIP limit ({column.cards.length}/{column.limit})
|
|
1326
|
+
</>
|
|
1327
|
+
) : (
|
|
1328
|
+
`WIP limit: ${column.cards.length}/${column.limit}`
|
|
1329
|
+
)}
|
|
1330
|
+
</CardDescription>
|
|
1177
1331
|
)}
|
|
1178
|
-
|
|
1332
|
+
</>
|
|
1179
1333
|
)}
|
|
1180
1334
|
</CardHeader>
|
|
1181
1335
|
|
|
1182
1336
|
{!column.collapsed && (
|
|
1183
|
-
<CardContent className="space-y-3">
|
|
1337
|
+
<CardContent className="space-y-3" style={{ gap: `${cardGap}px` }}>
|
|
1184
1338
|
<ScrollArea className="h-[calc(100vh-300px)]">
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1339
|
+
{column.cards.length === 0 && renderEmptyColumn ? (
|
|
1340
|
+
renderEmptyColumn(column)
|
|
1341
|
+
) : (
|
|
1342
|
+
<AnimatePresence mode={enableAnimations ? "popLayout" : undefined}>
|
|
1343
|
+
{column.cards
|
|
1344
|
+
.sort((a, b) => a.position - b.position)
|
|
1345
|
+
.map((card, index) => (
|
|
1189
1346
|
<div
|
|
1190
1347
|
key={card.id}
|
|
1191
|
-
draggable={
|
|
1348
|
+
draggable={
|
|
1349
|
+
!disabled &&
|
|
1350
|
+
(typeof dragDisabled === 'function' ? !dragDisabled(card) : !dragDisabled)
|
|
1351
|
+
}
|
|
1192
1352
|
onDragStart={() => handleDragStart(card, column.id)}
|
|
1193
1353
|
onDragEnd={handleDragEnd}
|
|
1194
1354
|
onDrop={(e) => {
|
|
@@ -1200,6 +1360,7 @@ export function Kanban({
|
|
|
1200
1360
|
>
|
|
1201
1361
|
<KanbanCardComponent
|
|
1202
1362
|
card={card}
|
|
1363
|
+
column={column}
|
|
1203
1364
|
isDragging={draggedCard === card.id}
|
|
1204
1365
|
onEdit={(e) => {
|
|
1205
1366
|
e.stopPropagation()
|
|
@@ -1212,45 +1373,66 @@ export function Kanban({
|
|
|
1212
1373
|
onClick={() => handleCardClick(card)}
|
|
1213
1374
|
showDetails={showCardDetails}
|
|
1214
1375
|
disabled={disabled}
|
|
1376
|
+
renderCard={renderCard}
|
|
1377
|
+
renderCardPreview={renderCardPreview}
|
|
1378
|
+
renderCardBadge={renderCardBadge}
|
|
1379
|
+
renderCardActions={renderCardActions}
|
|
1380
|
+
cardCompactMode={cardCompactMode}
|
|
1381
|
+
cardShowCoverImage={cardShowCoverImage}
|
|
1382
|
+
cardShowAssignees={cardShowAssignees}
|
|
1383
|
+
cardShowLabels={cardShowLabels}
|
|
1384
|
+
cardShowProgress={cardShowProgress}
|
|
1385
|
+
cardDateFormat={cardDateFormat}
|
|
1386
|
+
cardMaxAssigneesToShow={cardMaxAssigneesToShow}
|
|
1387
|
+
enableAnimations={enableAnimations}
|
|
1388
|
+
animationDuration={animationDuration}
|
|
1389
|
+
cardVariant={cardVariant}
|
|
1215
1390
|
/>
|
|
1216
1391
|
</div>
|
|
1217
1392
|
))}
|
|
1218
|
-
|
|
1393
|
+
</AnimatePresence>
|
|
1394
|
+
)}
|
|
1219
1395
|
</ScrollArea>
|
|
1220
1396
|
|
|
1221
1397
|
{/* Add card button */}
|
|
1222
1398
|
{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)}
|
|
1399
|
+
renderAddCardButton ? (
|
|
1400
|
+
renderAddCardButton(column.id)
|
|
1401
|
+
) : (
|
|
1402
|
+
<DropdownMenu>
|
|
1403
|
+
<DropdownMenuTrigger asChild>
|
|
1404
|
+
<Button
|
|
1405
|
+
variant="ghost"
|
|
1406
|
+
size="sm"
|
|
1407
|
+
className="w-full justify-start text-muted-foreground hover:text-foreground"
|
|
1408
|
+
disabled={disabled}
|
|
1246
1409
|
>
|
|
1247
|
-
<
|
|
1248
|
-
{
|
|
1410
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
1411
|
+
{typeof addCardButtonText === 'function' ? addCardButtonText(column.id) : (addCardButtonText || 'Add card')}
|
|
1412
|
+
</Button>
|
|
1413
|
+
</DropdownMenuTrigger>
|
|
1414
|
+
<DropdownMenuContent align="start" className="w-48">
|
|
1415
|
+
<DropdownMenuLabel>Card Templates</DropdownMenuLabel>
|
|
1416
|
+
<DropdownMenuSeparator />
|
|
1417
|
+
<DropdownMenuItem onClick={() => handleAddCard(column.id)}>
|
|
1418
|
+
<FileText className="mr-2 h-4 w-4" />
|
|
1419
|
+
Blank card
|
|
1249
1420
|
</DropdownMenuItem>
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1421
|
+
{cardTemplates.map((template, index) => (
|
|
1422
|
+
<DropdownMenuItem
|
|
1423
|
+
key={index}
|
|
1424
|
+
onClick={() => handleAddCard(column.id, template)}
|
|
1425
|
+
>
|
|
1426
|
+
<Star className="mr-2 h-4 w-4" />
|
|
1427
|
+
{template.title || `Template ${index + 1}`}
|
|
1428
|
+
</DropdownMenuItem>
|
|
1429
|
+
))}
|
|
1430
|
+
</DropdownMenuContent>
|
|
1431
|
+
</DropdownMenu>
|
|
1432
|
+
)
|
|
1253
1433
|
)}
|
|
1434
|
+
{/* Column footer */}
|
|
1435
|
+
{renderColumnFooter && renderColumnFooter(column)}
|
|
1254
1436
|
</CardContent>
|
|
1255
1437
|
)}
|
|
1256
1438
|
</Card>
|