@moontra/moonui-pro 2.11.4 → 2.13.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 +80 -11
- package/dist/index.mjs +2314 -376
- package/package.json +1 -1
- package/src/components/file-upload/index.tsx +95 -40
- package/src/components/index.ts +2 -1
- package/src/components/kanban/add-card-modal.tsx +502 -0
- package/src/components/kanban/card-detail-modal.tsx +769 -0
- package/src/components/kanban/index.ts +13 -0
- package/src/components/kanban/kanban.tsx +1474 -0
- package/src/components/kanban/types.ts +111 -0
- package/src/components/ui/avatar.tsx +34 -19
- package/src/hooks/use-toast.ts +15 -0
- package/src/components/kanban/index.tsx +0 -434
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface KanbanAssignee {
|
|
2
|
+
id: string
|
|
3
|
+
name: string
|
|
4
|
+
avatar?: string
|
|
5
|
+
email?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface KanbanLabel {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
color: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface KanbanChecklist {
|
|
15
|
+
id: string
|
|
16
|
+
title: string
|
|
17
|
+
items: {
|
|
18
|
+
id: string
|
|
19
|
+
text: string
|
|
20
|
+
completed: boolean
|
|
21
|
+
}[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface KanbanActivity {
|
|
25
|
+
id: string
|
|
26
|
+
user: KanbanAssignee
|
|
27
|
+
action: string
|
|
28
|
+
timestamp: Date
|
|
29
|
+
details?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface KanbanCard {
|
|
33
|
+
id: string
|
|
34
|
+
title: string
|
|
35
|
+
description?: string
|
|
36
|
+
coverImage?: string
|
|
37
|
+
assignees?: KanbanAssignee[]
|
|
38
|
+
dueDate?: Date
|
|
39
|
+
startDate?: Date
|
|
40
|
+
priority?: 'low' | 'medium' | 'high' | 'urgent'
|
|
41
|
+
tags?: string[]
|
|
42
|
+
labels?: KanbanLabel[]
|
|
43
|
+
attachments?: {
|
|
44
|
+
id: string
|
|
45
|
+
name: string
|
|
46
|
+
type: string
|
|
47
|
+
url: string
|
|
48
|
+
size: number
|
|
49
|
+
}[]
|
|
50
|
+
comments?: number
|
|
51
|
+
completed?: boolean
|
|
52
|
+
progress?: number
|
|
53
|
+
checklist?: KanbanChecklist
|
|
54
|
+
activities?: KanbanActivity[]
|
|
55
|
+
customFields?: Record<string, any>
|
|
56
|
+
position: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface KanbanColumn {
|
|
60
|
+
id: string
|
|
61
|
+
title: string
|
|
62
|
+
color?: string
|
|
63
|
+
cards: KanbanCard[]
|
|
64
|
+
limit?: number
|
|
65
|
+
collapsed?: boolean
|
|
66
|
+
locked?: boolean
|
|
67
|
+
template?: 'todo' | 'inProgress' | 'done' | 'custom'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface KanbanFilter {
|
|
71
|
+
id: string
|
|
72
|
+
name: string
|
|
73
|
+
query: string
|
|
74
|
+
assignees?: string[]
|
|
75
|
+
labels?: string[]
|
|
76
|
+
priority?: string[]
|
|
77
|
+
tags?: string[]
|
|
78
|
+
dueDate?: {
|
|
79
|
+
from?: Date
|
|
80
|
+
to?: Date
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface KanbanProps {
|
|
85
|
+
columns: KanbanColumn[]
|
|
86
|
+
onCardMove?: (cardId: string, fromColumn: string, toColumn: string, newPosition: number) => void
|
|
87
|
+
onCardClick?: (card: KanbanCard) => void
|
|
88
|
+
onCardEdit?: (card: KanbanCard) => void
|
|
89
|
+
onCardDelete?: (card: KanbanCard) => void
|
|
90
|
+
onCardUpdate?: (card: KanbanCard) => void
|
|
91
|
+
onAddCard?: (columnId: string, card?: Partial<KanbanCard>) => void
|
|
92
|
+
onAddColumn?: (column?: Partial<KanbanColumn>) => void
|
|
93
|
+
onColumnUpdate?: (column: KanbanColumn) => void
|
|
94
|
+
onColumnDelete?: (columnId: string) => void
|
|
95
|
+
onBulkAction?: (action: string, cardIds: string[]) => void
|
|
96
|
+
onExport?: (format: 'json' | 'csv') => void
|
|
97
|
+
className?: string
|
|
98
|
+
showAddColumn?: boolean
|
|
99
|
+
showCardDetails?: boolean
|
|
100
|
+
showFilters?: boolean
|
|
101
|
+
showSearch?: boolean
|
|
102
|
+
enableKeyboardShortcuts?: boolean
|
|
103
|
+
cardTemplates?: Partial<KanbanCard>[]
|
|
104
|
+
columnTemplates?: Partial<KanbanColumn>[]
|
|
105
|
+
filters?: KanbanFilter[]
|
|
106
|
+
defaultFilter?: string
|
|
107
|
+
loading?: boolean
|
|
108
|
+
disabled?: boolean
|
|
109
|
+
labels?: KanbanLabel[]
|
|
110
|
+
users?: KanbanAssignee[]
|
|
111
|
+
}
|
|
@@ -83,16 +83,29 @@ const MoonUIAvatarFallbackPro = React.forwardRef<
|
|
|
83
83
|
MoonUIAvatarFallbackPro.displayName = AvatarPrimitive.Fallback.displayName;
|
|
84
84
|
|
|
85
85
|
// Avatar Group Component for displaying multiple avatars
|
|
86
|
-
interface
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
interface MoonUIAvatarGroupProProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
87
|
+
max?: number;
|
|
88
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
89
|
+
children?: React.ReactNode;
|
|
89
90
|
overlapOffset?: number;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
const MoonUIAvatarGroupPro = React.forwardRef<HTMLDivElement,
|
|
93
|
-
({ className,
|
|
94
|
-
const
|
|
95
|
-
const
|
|
93
|
+
const MoonUIAvatarGroupPro = React.forwardRef<HTMLDivElement, MoonUIAvatarGroupProProps>(
|
|
94
|
+
({ className, max = 3, size = "md", children, overlapOffset, ...props }, ref) => {
|
|
95
|
+
const childrenArray = React.Children.toArray(children);
|
|
96
|
+
const visibleChildren = max ? childrenArray.slice(0, max) : childrenArray;
|
|
97
|
+
const remainingCount = max ? Math.max(0, childrenArray.length - max) : 0;
|
|
98
|
+
|
|
99
|
+
// Calculate overlap offset based on size
|
|
100
|
+
const defaultOffsets = {
|
|
101
|
+
xs: -4,
|
|
102
|
+
sm: -6,
|
|
103
|
+
md: -8,
|
|
104
|
+
lg: -10,
|
|
105
|
+
xl: -12,
|
|
106
|
+
"2xl": -16,
|
|
107
|
+
};
|
|
108
|
+
const finalOffset = overlapOffset ?? defaultOffsets[size as keyof typeof defaultOffsets] ?? -8;
|
|
96
109
|
|
|
97
110
|
return (
|
|
98
111
|
<div
|
|
@@ -101,25 +114,27 @@ const MoonUIAvatarGroupPro = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
|
101
114
|
{...props}
|
|
102
115
|
>
|
|
103
116
|
<div className="flex">
|
|
104
|
-
{
|
|
117
|
+
{visibleChildren.map((child, index) => (
|
|
105
118
|
<div
|
|
106
119
|
key={index}
|
|
107
|
-
className="relative"
|
|
120
|
+
className="relative ring-2 ring-background rounded-full"
|
|
108
121
|
style={{
|
|
109
|
-
marginLeft: index === 0 ? 0 : `${
|
|
110
|
-
zIndex:
|
|
122
|
+
marginLeft: index === 0 ? 0 : `${finalOffset}px`,
|
|
123
|
+
zIndex: visibleChildren.length - index
|
|
111
124
|
}}
|
|
112
125
|
>
|
|
113
|
-
{
|
|
126
|
+
{React.isValidElement(child) && child.type === MoonUIAvatarPro
|
|
127
|
+
? React.cloneElement(child as React.ReactElement<any>, { size })
|
|
128
|
+
: child}
|
|
114
129
|
</div>
|
|
115
130
|
))}
|
|
116
131
|
{remainingCount > 0 && (
|
|
117
132
|
<div
|
|
118
|
-
className="relative z-0"
|
|
119
|
-
style={{ marginLeft: `${
|
|
133
|
+
className="relative z-0 ring-2 ring-background rounded-full"
|
|
134
|
+
style={{ marginLeft: `${finalOffset}px` }}
|
|
120
135
|
>
|
|
121
|
-
<MoonUIAvatarPro
|
|
122
|
-
<MoonUIAvatarFallbackPro>
|
|
136
|
+
<MoonUIAvatarPro size={size} className="bg-muted">
|
|
137
|
+
<MoonUIAvatarFallbackPro className="text-xs font-medium">
|
|
123
138
|
+{remainingCount}
|
|
124
139
|
</MoonUIAvatarFallbackPro>
|
|
125
140
|
</MoonUIAvatarPro>
|
|
@@ -130,9 +145,9 @@ const MoonUIAvatarGroupPro = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
|
130
145
|
);
|
|
131
146
|
}
|
|
132
147
|
);
|
|
133
|
-
MoonUIAvatarGroupPro.displayName = "
|
|
148
|
+
MoonUIAvatarGroupPro.displayName = "MoonUIAvatarGroupPro";
|
|
134
149
|
|
|
135
|
-
export { MoonUIAvatarPro, MoonUIAvatarImagePro, MoonUIAvatarFallbackPro, MoonUIAvatarGroupPro
|
|
150
|
+
export { MoonUIAvatarPro, MoonUIAvatarImagePro, MoonUIAvatarFallbackPro, MoonUIAvatarGroupPro };
|
|
136
151
|
|
|
137
152
|
// Backward compatibility exports
|
|
138
|
-
export { MoonUIAvatarPro as Avatar, MoonUIAvatarImagePro as AvatarImage, MoonUIAvatarFallbackPro as AvatarFallback };
|
|
153
|
+
export { MoonUIAvatarPro as Avatar, MoonUIAvatarImagePro as AvatarImage, MoonUIAvatarFallbackPro as AvatarFallback, MoonUIAvatarGroupPro as AvatarGroup };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface ToastOptions {
|
|
2
|
+
title: string
|
|
3
|
+
description?: string
|
|
4
|
+
variant?: 'default' | 'destructive'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function useToast() {
|
|
8
|
+
const toast = (options: ToastOptions) => {
|
|
9
|
+
// Simple console implementation for now
|
|
10
|
+
// In production, this would integrate with a proper toast system
|
|
11
|
+
console.log(`[Toast] ${options.title}${options.description ? ': ' + options.description : ''}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return { toast }
|
|
15
|
+
}
|
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
|
5
|
-
import { Button } from '../ui/button'
|
|
6
|
-
import { MoonUIBadgePro as Badge } from '../ui/badge'
|
|
7
|
-
import { MoonUIAvatarPro, MoonUIAvatarFallbackPro, MoonUIAvatarImagePro } from '../ui/avatar'
|
|
8
|
-
import {
|
|
9
|
-
Plus,
|
|
10
|
-
MoreHorizontal,
|
|
11
|
-
User,
|
|
12
|
-
Calendar,
|
|
13
|
-
MessageCircle,
|
|
14
|
-
Paperclip,
|
|
15
|
-
Edit,
|
|
16
|
-
Trash2,
|
|
17
|
-
GripVertical,
|
|
18
|
-
Lock,
|
|
19
|
-
Sparkles
|
|
20
|
-
} from 'lucide-react'
|
|
21
|
-
import { cn } from '../../lib/utils'
|
|
22
|
-
// Note: DocsProAccess should be handled by consuming application
|
|
23
|
-
import { useSubscription } from '../../hooks/use-subscription'
|
|
24
|
-
|
|
25
|
-
interface KanbanCard {
|
|
26
|
-
id: string
|
|
27
|
-
title: string
|
|
28
|
-
description?: string
|
|
29
|
-
assignee?: {
|
|
30
|
-
name: string
|
|
31
|
-
avatar?: string
|
|
32
|
-
email?: string
|
|
33
|
-
}
|
|
34
|
-
dueDate?: Date
|
|
35
|
-
priority?: 'low' | 'medium' | 'high' | 'urgent'
|
|
36
|
-
tags?: string[]
|
|
37
|
-
attachments?: number
|
|
38
|
-
comments?: number
|
|
39
|
-
completed?: boolean
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface KanbanColumn {
|
|
43
|
-
id: string
|
|
44
|
-
title: string
|
|
45
|
-
color?: string
|
|
46
|
-
cards: KanbanCard[]
|
|
47
|
-
limit?: number
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface KanbanProps {
|
|
51
|
-
columns: KanbanColumn[]
|
|
52
|
-
onCardMove?: (cardId: string, fromColumn: string, toColumn: string, newIndex: number) => void
|
|
53
|
-
onCardClick?: (card: KanbanCard) => void
|
|
54
|
-
onCardEdit?: (card: KanbanCard) => void
|
|
55
|
-
onCardDelete?: (card: KanbanCard) => void
|
|
56
|
-
onAddCard?: (columnId: string) => void
|
|
57
|
-
onAddColumn?: () => void
|
|
58
|
-
className?: string
|
|
59
|
-
showAddColumn?: boolean
|
|
60
|
-
showCardDetails?: boolean
|
|
61
|
-
disabled?: boolean
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const PRIORITY_COLORS = {
|
|
65
|
-
low: 'bg-green-100 text-green-800 border-green-200',
|
|
66
|
-
medium: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
|
67
|
-
high: 'bg-orange-100 text-orange-800 border-orange-200',
|
|
68
|
-
urgent: 'bg-red-100 text-red-800 border-red-200'
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const PRIORITY_DOTS = {
|
|
72
|
-
low: 'bg-green-500',
|
|
73
|
-
medium: 'bg-yellow-500',
|
|
74
|
-
high: 'bg-orange-500',
|
|
75
|
-
urgent: 'bg-red-500'
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function Kanban({
|
|
79
|
-
columns,
|
|
80
|
-
onCardMove,
|
|
81
|
-
onCardClick,
|
|
82
|
-
onCardEdit,
|
|
83
|
-
onCardDelete,
|
|
84
|
-
onAddCard,
|
|
85
|
-
onAddColumn,
|
|
86
|
-
className,
|
|
87
|
-
showAddColumn = true,
|
|
88
|
-
showCardDetails = true,
|
|
89
|
-
disabled = false
|
|
90
|
-
}: KanbanProps) {
|
|
91
|
-
// Check if we're in docs mode or have pro access
|
|
92
|
-
const { hasProAccess, isLoading } = useSubscription()
|
|
93
|
-
|
|
94
|
-
// In docs mode, always show the component
|
|
95
|
-
|
|
96
|
-
// If not in docs mode and no pro access, show upgrade prompt
|
|
97
|
-
if (!isLoading && !hasProAccess) {
|
|
98
|
-
return (
|
|
99
|
-
<Card className={cn("w-full", className)}>
|
|
100
|
-
<CardContent className="py-12 text-center">
|
|
101
|
-
<div className="max-w-md mx-auto space-y-4">
|
|
102
|
-
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
103
|
-
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
104
|
-
</div>
|
|
105
|
-
<div>
|
|
106
|
-
<h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
|
|
107
|
-
<p className="text-muted-foreground text-sm mb-4">
|
|
108
|
-
Kanban Board is available exclusively to MoonUI Pro subscribers.
|
|
109
|
-
</p>
|
|
110
|
-
<div className="flex gap-3 justify-center">
|
|
111
|
-
<a href="/pricing">
|
|
112
|
-
<Button size="sm">
|
|
113
|
-
<Sparkles className="mr-2 h-4 w-4" />
|
|
114
|
-
Upgrade to Pro
|
|
115
|
-
</Button>
|
|
116
|
-
</a>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
</CardContent>
|
|
121
|
-
</Card>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const [draggedCard, setDraggedCard] = React.useState<string | null>(null)
|
|
126
|
-
const [draggedOverColumn, setDraggedOverColumn] = React.useState<string | null>(null)
|
|
127
|
-
const [draggedFromColumn, setDraggedFromColumn] = React.useState<string | null>(null)
|
|
128
|
-
|
|
129
|
-
const handleDragStart = (e: React.DragEvent, cardId: string) => {
|
|
130
|
-
if (disabled) return
|
|
131
|
-
|
|
132
|
-
// Find which column this card belongs to
|
|
133
|
-
const sourceColumn = columns.find(col =>
|
|
134
|
-
col.cards.some(card => card.id === cardId)
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
setDraggedCard(cardId)
|
|
138
|
-
setDraggedFromColumn(sourceColumn?.id || null)
|
|
139
|
-
e.dataTransfer.effectAllowed = 'move'
|
|
140
|
-
e.dataTransfer.setData('text/plain', cardId)
|
|
141
|
-
|
|
142
|
-
// Add visual feedback
|
|
143
|
-
e.currentTarget.classList.add('opacity-50')
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const handleDragEnd = (e: React.DragEvent) => {
|
|
147
|
-
if (disabled) return
|
|
148
|
-
|
|
149
|
-
// Reset all states
|
|
150
|
-
setDraggedCard(null)
|
|
151
|
-
setDraggedOverColumn(null)
|
|
152
|
-
setDraggedFromColumn(null)
|
|
153
|
-
|
|
154
|
-
// Remove visual feedback
|
|
155
|
-
e.currentTarget.classList.remove('opacity-50')
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const handleDragOver = (e: React.DragEvent, columnId: string) => {
|
|
159
|
-
if (disabled) return
|
|
160
|
-
e.preventDefault()
|
|
161
|
-
e.dataTransfer.dropEffect = 'move'
|
|
162
|
-
setDraggedOverColumn(columnId)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const handleDragEnter = (e: React.DragEvent, columnId: string) => {
|
|
166
|
-
if (disabled) return
|
|
167
|
-
e.preventDefault()
|
|
168
|
-
setDraggedOverColumn(columnId)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const handleDragLeave = (e: React.DragEvent, columnId: string) => {
|
|
172
|
-
if (disabled) return
|
|
173
|
-
e.preventDefault()
|
|
174
|
-
|
|
175
|
-
// Only clear if we're leaving the column entirely
|
|
176
|
-
const rect = e.currentTarget.getBoundingClientRect()
|
|
177
|
-
const isLeavingColumn = (
|
|
178
|
-
e.clientX < rect.left ||
|
|
179
|
-
e.clientX > rect.right ||
|
|
180
|
-
e.clientY < rect.top ||
|
|
181
|
-
e.clientY > rect.bottom
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
if (isLeavingColumn) {
|
|
185
|
-
setDraggedOverColumn(null)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const handleDrop = (e: React.DragEvent, columnId: string) => {
|
|
190
|
-
if (disabled) return
|
|
191
|
-
e.preventDefault()
|
|
192
|
-
|
|
193
|
-
const cardId = e.dataTransfer.getData('text/plain') || draggedCard
|
|
194
|
-
|
|
195
|
-
if (cardId && onCardMove && draggedFromColumn && draggedFromColumn !== columnId) {
|
|
196
|
-
const targetColumn = columns.find(col => col.id === columnId)
|
|
197
|
-
const newIndex = targetColumn?.cards.length || 0
|
|
198
|
-
onCardMove(cardId, draggedFromColumn, columnId, newIndex)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Reset all states
|
|
202
|
-
setDraggedCard(null)
|
|
203
|
-
setDraggedOverColumn(null)
|
|
204
|
-
setDraggedFromColumn(null)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const handleCardClick = (card: KanbanCard) => {
|
|
208
|
-
if (disabled) return
|
|
209
|
-
onCardClick?.(card)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const handleCardEdit = (card: KanbanCard, e: React.MouseEvent) => {
|
|
213
|
-
if (disabled) return
|
|
214
|
-
e.stopPropagation()
|
|
215
|
-
onCardEdit?.(card)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const handleCardDelete = (card: KanbanCard, e: React.MouseEvent) => {
|
|
219
|
-
if (disabled) return
|
|
220
|
-
e.stopPropagation()
|
|
221
|
-
onCardDelete?.(card)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const formatDate = (date: Date) => {
|
|
225
|
-
return date.toLocaleDateString('en-US', {
|
|
226
|
-
month: 'short',
|
|
227
|
-
day: 'numeric'
|
|
228
|
-
})
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const isOverdue = (dueDate: Date) => {
|
|
232
|
-
return dueDate < new Date()
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const getInitials = (name: string) => {
|
|
236
|
-
return name.split(' ').map(n => n[0]).join('').toUpperCase()
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return (
|
|
240
|
-
<div className={cn("w-full", className)}>
|
|
241
|
-
<div className="flex gap-6 overflow-x-auto pb-4">
|
|
242
|
-
{columns.map((column) => {
|
|
243
|
-
const isOverLimit = column.limit && column.cards.length > column.limit
|
|
244
|
-
const isDraggedOver = draggedOverColumn === column.id
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<div
|
|
248
|
-
key={column.id}
|
|
249
|
-
className={cn(
|
|
250
|
-
"flex-shrink-0 w-80 transition-colors duration-200",
|
|
251
|
-
isDraggedOver && "bg-primary/5 rounded-lg border-2 border-primary/20"
|
|
252
|
-
)}
|
|
253
|
-
onDragOver={(e) => handleDragOver(e, column.id)}
|
|
254
|
-
onDragEnter={(e) => handleDragEnter(e, column.id)}
|
|
255
|
-
onDragLeave={(e) => handleDragLeave(e, column.id)}
|
|
256
|
-
onDrop={(e) => handleDrop(e, column.id)}
|
|
257
|
-
>
|
|
258
|
-
<Card className="h-full">
|
|
259
|
-
<CardHeader className="pb-3">
|
|
260
|
-
<div className="flex items-center justify-between">
|
|
261
|
-
<div className="flex items-center gap-2">
|
|
262
|
-
{column.color && (
|
|
263
|
-
<div
|
|
264
|
-
className="w-3 h-3 rounded-full"
|
|
265
|
-
style={{ backgroundColor: column.color }}
|
|
266
|
-
/>
|
|
267
|
-
)}
|
|
268
|
-
<CardTitle className="text-sm font-medium">
|
|
269
|
-
{column.title}
|
|
270
|
-
</CardTitle>
|
|
271
|
-
<Badge variant="secondary" className="text-xs">
|
|
272
|
-
{column.cards.length}
|
|
273
|
-
</Badge>
|
|
274
|
-
</div>
|
|
275
|
-
<Button variant="ghost" size="sm">
|
|
276
|
-
<MoreHorizontal className="h-4 w-4" />
|
|
277
|
-
</Button>
|
|
278
|
-
</div>
|
|
279
|
-
{column.limit && (
|
|
280
|
-
<CardDescription className={cn(
|
|
281
|
-
"text-xs",
|
|
282
|
-
isOverLimit && "text-destructive"
|
|
283
|
-
)}>
|
|
284
|
-
{isOverLimit ? 'Over limit' : `${column.cards.length}/${column.limit} cards`}
|
|
285
|
-
</CardDescription>
|
|
286
|
-
)}
|
|
287
|
-
</CardHeader>
|
|
288
|
-
|
|
289
|
-
<CardContent className="space-y-3">
|
|
290
|
-
{/* Cards */}
|
|
291
|
-
{column.cards.map((card) => (
|
|
292
|
-
<div
|
|
293
|
-
key={card.id}
|
|
294
|
-
draggable={!disabled}
|
|
295
|
-
onDragStart={(e) => handleDragStart(e, card.id)}
|
|
296
|
-
onDragEnd={handleDragEnd}
|
|
297
|
-
onClick={() => handleCardClick(card)}
|
|
298
|
-
className={cn(
|
|
299
|
-
"p-3 bg-background border rounded-lg cursor-pointer hover:shadow-md transition-all duration-200",
|
|
300
|
-
"group relative select-none",
|
|
301
|
-
draggedCard === card.id && "opacity-50 scale-95",
|
|
302
|
-
disabled && "cursor-not-allowed"
|
|
303
|
-
)}
|
|
304
|
-
>
|
|
305
|
-
<div className="flex items-start justify-between gap-2">
|
|
306
|
-
<div className="flex-1">
|
|
307
|
-
<h4 className="font-medium text-sm mb-1">{card.title}</h4>
|
|
308
|
-
{card.description && (
|
|
309
|
-
<p className="text-xs text-muted-foreground mb-2 line-clamp-2">
|
|
310
|
-
{card.description}
|
|
311
|
-
</p>
|
|
312
|
-
)}
|
|
313
|
-
</div>
|
|
314
|
-
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
315
|
-
<Button
|
|
316
|
-
variant="ghost"
|
|
317
|
-
size="sm"
|
|
318
|
-
className="h-6 w-6 p-0"
|
|
319
|
-
onClick={(e) => handleCardEdit(card, e)}
|
|
320
|
-
>
|
|
321
|
-
<Edit className="h-3 w-3" />
|
|
322
|
-
</Button>
|
|
323
|
-
<Button
|
|
324
|
-
variant="ghost"
|
|
325
|
-
size="sm"
|
|
326
|
-
className="h-6 w-6 p-0"
|
|
327
|
-
onClick={(e) => handleCardDelete(card, e)}
|
|
328
|
-
>
|
|
329
|
-
<Trash2 className="h-3 w-3" />
|
|
330
|
-
</Button>
|
|
331
|
-
<div className="cursor-move">
|
|
332
|
-
<GripVertical className="h-3 w-3 text-muted-foreground" />
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
{/* Tags */}
|
|
338
|
-
{card.tags && card.tags.length > 0 && (
|
|
339
|
-
<div className="flex flex-wrap gap-1 mb-2">
|
|
340
|
-
{card.tags.map((tag, index) => (
|
|
341
|
-
<Badge key={index} variant="outline" className="text-xs px-1 py-0">
|
|
342
|
-
{tag}
|
|
343
|
-
</Badge>
|
|
344
|
-
))}
|
|
345
|
-
</div>
|
|
346
|
-
)}
|
|
347
|
-
|
|
348
|
-
{/* Card Details */}
|
|
349
|
-
{showCardDetails && (
|
|
350
|
-
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
351
|
-
<div className="flex items-center gap-2">
|
|
352
|
-
{card.priority && (
|
|
353
|
-
<div className="flex items-center gap-1">
|
|
354
|
-
<div className={cn("w-2 h-2 rounded-full", PRIORITY_DOTS[card.priority])} />
|
|
355
|
-
<span className="capitalize">{card.priority}</span>
|
|
356
|
-
</div>
|
|
357
|
-
)}
|
|
358
|
-
{card.dueDate && (
|
|
359
|
-
<div className={cn(
|
|
360
|
-
"flex items-center gap-1",
|
|
361
|
-
isOverdue(card.dueDate) && "text-destructive"
|
|
362
|
-
)}>
|
|
363
|
-
<Calendar className="h-3 w-3" />
|
|
364
|
-
<span>{formatDate(card.dueDate)}</span>
|
|
365
|
-
</div>
|
|
366
|
-
)}
|
|
367
|
-
</div>
|
|
368
|
-
|
|
369
|
-
<div className="flex items-center gap-2">
|
|
370
|
-
{card.comments && card.comments > 0 && (
|
|
371
|
-
<div className="flex items-center gap-1">
|
|
372
|
-
<MessageCircle className="h-3 w-3" />
|
|
373
|
-
<span>{card.comments}</span>
|
|
374
|
-
</div>
|
|
375
|
-
)}
|
|
376
|
-
{card.attachments && card.attachments > 0 && (
|
|
377
|
-
<div className="flex items-center gap-1">
|
|
378
|
-
<Paperclip className="h-3 w-3" />
|
|
379
|
-
<span>{card.attachments}</span>
|
|
380
|
-
</div>
|
|
381
|
-
)}
|
|
382
|
-
{card.assignee && (
|
|
383
|
-
<MoonUIAvatarPro className="h-5 w-5">
|
|
384
|
-
<MoonUIAvatarImagePro src={card.assignee.avatar} />
|
|
385
|
-
<MoonUIAvatarFallbackPro className="text-xs">
|
|
386
|
-
{getInitials(card.assignee.name)}
|
|
387
|
-
</MoonUIAvatarFallbackPro>
|
|
388
|
-
</MoonUIAvatarPro>
|
|
389
|
-
)}
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
)}
|
|
393
|
-
</div>
|
|
394
|
-
))}
|
|
395
|
-
|
|
396
|
-
{/* Add Card Button */}
|
|
397
|
-
{onAddCard && (
|
|
398
|
-
<Button
|
|
399
|
-
variant="ghost"
|
|
400
|
-
size="sm"
|
|
401
|
-
onClick={() => onAddCard(column.id)}
|
|
402
|
-
className="w-full justify-start text-muted-foreground hover:text-foreground"
|
|
403
|
-
disabled={disabled}
|
|
404
|
-
>
|
|
405
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
406
|
-
Add card
|
|
407
|
-
</Button>
|
|
408
|
-
)}
|
|
409
|
-
</CardContent>
|
|
410
|
-
</Card>
|
|
411
|
-
</div>
|
|
412
|
-
)
|
|
413
|
-
})}
|
|
414
|
-
|
|
415
|
-
{/* Add Column Button */}
|
|
416
|
-
{showAddColumn && onAddColumn && (
|
|
417
|
-
<div className="flex-shrink-0 w-80">
|
|
418
|
-
<Button
|
|
419
|
-
variant="outline"
|
|
420
|
-
onClick={onAddColumn}
|
|
421
|
-
className="w-full h-full min-h-[200px] border-dashed justify-center items-center"
|
|
422
|
-
disabled={disabled}
|
|
423
|
-
>
|
|
424
|
-
<Plus className="h-6 w-6 mr-2" />
|
|
425
|
-
Add column
|
|
426
|
-
</Button>
|
|
427
|
-
</div>
|
|
428
|
-
)}
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export default Kanban
|