@moontra/moonui-pro 2.20.1 → 2.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +691 -261
- package/dist/index.mjs +7418 -4934
- package/package.json +11 -5
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postbuild.js +27 -0
- package/scripts/postinstall.js +176 -23
- package/src/__tests__/use-intersection-observer.test.tsx +0 -216
- package/src/__tests__/use-local-storage.test.tsx +0 -174
- package/src/__tests__/use-pro-access.test.tsx +0 -183
- package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
- package/src/components/advanced-chart/index.tsx +0 -1242
- package/src/components/advanced-forms/index.tsx +0 -426
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -372
- package/src/components/calendar/index.tsx +0 -1073
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -462
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -222
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -344
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/metric-card.tsx +0 -343
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/data-table.test.tsx +0 -187
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/enhanced/badge.tsx +0 -191
- package/src/components/enhanced/button.tsx +0 -362
- package/src/components/enhanced/card.tsx +0 -266
- package/src/components/enhanced/dialog.tsx +0 -246
- package/src/components/enhanced/index.ts +0 -4
- package/src/components/error-boundary/index.tsx +0 -109
- package/src/components/file-upload/file-upload.test.tsx +0 -243
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -307
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -298
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -76
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -516
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -513
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -530
- package/src/components/index.ts +0 -128
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1684
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -29
- package/src/components/magnetic-button/index.tsx +0 -167
- package/src/components/memory-efficient-data/index.tsx +0 -1016
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -589
- package/src/components/performance-monitor/index.tsx +0 -794
- package/src/components/phone-number-input/index.tsx +0 -338
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
- package/src/components/rich-text-editor/index.tsx +0 -2324
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -220
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -865
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1148
- package/src/components/ui/accordion.tsx +0 -73
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -153
- package/src/components/ui/badge.tsx +0 -228
- package/src/components/ui/breadcrumb.tsx +0 -214
- package/src/components/ui/button.tsx +0 -222
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -214
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -135
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -225
- package/src/components/ui/dialog.tsx +0 -334
- package/src/components/ui/dropdown-menu.tsx +0 -218
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -190
- package/src/components/ui/input.tsx +0 -222
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -612
- package/src/components/ui/pagination.tsx +0 -123
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -374
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -329
- package/src/components/ui/tabs.tsx +0 -198
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -14
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -85
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/moonui.d.ts +0 -22
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- package/src/utils/license-validator.tsx +0 -183
|
@@ -1,1148 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
|
4
|
-
import { motion, AnimatePresence, Reorder } from 'framer-motion'
|
|
5
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
|
6
|
-
import { MoonUIBadgePro as Badge } from '../ui/badge'
|
|
7
|
-
import { MoonUIAvatarPro, MoonUIAvatarFallbackPro, MoonUIAvatarImagePro } from '../ui/avatar'
|
|
8
|
-
import { Button } from '../ui/button'
|
|
9
|
-
import { Input } from '../ui/input'
|
|
10
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
|
|
11
|
-
import {
|
|
12
|
-
Select,
|
|
13
|
-
SelectContent,
|
|
14
|
-
SelectItem,
|
|
15
|
-
SelectTrigger,
|
|
16
|
-
SelectValue,
|
|
17
|
-
} from '../ui/select'
|
|
18
|
-
import {
|
|
19
|
-
DropdownMenu,
|
|
20
|
-
DropdownMenuContent,
|
|
21
|
-
DropdownMenuItem,
|
|
22
|
-
DropdownMenuLabel,
|
|
23
|
-
DropdownMenuSeparator,
|
|
24
|
-
DropdownMenuTrigger,
|
|
25
|
-
} from '../ui/dropdown-menu'
|
|
26
|
-
import {
|
|
27
|
-
Clock,
|
|
28
|
-
CheckCircle2,
|
|
29
|
-
AlertCircle,
|
|
30
|
-
XCircle,
|
|
31
|
-
Circle,
|
|
32
|
-
Calendar,
|
|
33
|
-
User,
|
|
34
|
-
MessageCircle,
|
|
35
|
-
Paperclip,
|
|
36
|
-
ExternalLink,
|
|
37
|
-
Lock,
|
|
38
|
-
Sparkles,
|
|
39
|
-
Filter,
|
|
40
|
-
Search,
|
|
41
|
-
Download,
|
|
42
|
-
Printer,
|
|
43
|
-
ChevronRight,
|
|
44
|
-
ChevronDown,
|
|
45
|
-
MoreVertical,
|
|
46
|
-
Edit,
|
|
47
|
-
Trash,
|
|
48
|
-
Copy,
|
|
49
|
-
Share,
|
|
50
|
-
Flag,
|
|
51
|
-
Zap,
|
|
52
|
-
Target,
|
|
53
|
-
TrendingUp,
|
|
54
|
-
Repeat,
|
|
55
|
-
GitBranch,
|
|
56
|
-
Layers,
|
|
57
|
-
ArrowRight,
|
|
58
|
-
ArrowLeft,
|
|
59
|
-
Maximize2,
|
|
60
|
-
Minimize2
|
|
61
|
-
} from 'lucide-react'
|
|
62
|
-
import { cn } from '../../lib/utils'
|
|
63
|
-
import { VariantProps, cva } from 'class-variance-authority'
|
|
64
|
-
|
|
65
|
-
// Enhanced Event Types
|
|
66
|
-
export type TimelineEventType = 'success' | 'warning' | 'error' | 'info' | 'pending' | 'milestone' | 'custom'
|
|
67
|
-
export type TimelineLayout = 'vertical' | 'vertical-left' | 'vertical-right' | 'horizontal' | 'alternating' | 'grouped'
|
|
68
|
-
export type TimelineTheme = 'default' | 'minimal' | 'detailed' | 'compact'
|
|
69
|
-
export type TimelineAnimation = 'fade' | 'slide' | 'scale' | 'none'
|
|
70
|
-
export type TimelineGroupBy = 'none' | 'date' | 'week' | 'month' | 'year'
|
|
71
|
-
|
|
72
|
-
// Event interfaces
|
|
73
|
-
export interface TimelineEventBase {
|
|
74
|
-
id: string
|
|
75
|
-
title: string
|
|
76
|
-
description?: string
|
|
77
|
-
date: Date
|
|
78
|
-
type: TimelineEventType
|
|
79
|
-
icon?: React.ReactNode
|
|
80
|
-
color?: string
|
|
81
|
-
metadata?: TimelineEventMetadata
|
|
82
|
-
user?: TimelineUser
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface TimelineEventMetadata {
|
|
86
|
-
location?: string
|
|
87
|
-
duration?: string
|
|
88
|
-
tags?: string[]
|
|
89
|
-
attachments?: number
|
|
90
|
-
comments?: number
|
|
91
|
-
externalLink?: string
|
|
92
|
-
priority?: 'low' | 'medium' | 'high'
|
|
93
|
-
progress?: number
|
|
94
|
-
custom?: Record<string, any>
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface TimelineUser {
|
|
98
|
-
name: string
|
|
99
|
-
avatar?: string
|
|
100
|
-
email?: string
|
|
101
|
-
role?: string
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Extended event types
|
|
105
|
-
export interface MilestoneEvent extends TimelineEventBase {
|
|
106
|
-
type: 'milestone'
|
|
107
|
-
milestone: {
|
|
108
|
-
target?: string
|
|
109
|
-
achievement?: string
|
|
110
|
-
impact?: string
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface RangeEvent extends TimelineEventBase {
|
|
115
|
-
endDate: Date
|
|
116
|
-
isActive?: boolean
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface RecurringEvent extends TimelineEventBase {
|
|
120
|
-
recurrence: {
|
|
121
|
-
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly'
|
|
122
|
-
interval?: number
|
|
123
|
-
endDate?: Date
|
|
124
|
-
occurrences?: number
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface NestedEvent extends TimelineEventBase {
|
|
129
|
-
subEvents?: TimelineEvent[]
|
|
130
|
-
expanded?: boolean
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export type TimelineEvent = TimelineEventBase | MilestoneEvent | RangeEvent | RecurringEvent | NestedEvent
|
|
134
|
-
|
|
135
|
-
// Render function types
|
|
136
|
-
export type EventRenderer = (event: TimelineEvent, index: number) => React.ReactNode
|
|
137
|
-
export type ConnectorRenderer = (fromEvent: TimelineEvent, toEvent: TimelineEvent) => React.ReactNode
|
|
138
|
-
export type IconRenderer = (event: TimelineEvent) => React.ReactNode
|
|
139
|
-
|
|
140
|
-
// Timeline Props
|
|
141
|
-
export interface TimelineProps extends VariantProps<typeof timelineVariants> {
|
|
142
|
-
events: TimelineEvent[]
|
|
143
|
-
onEventClick?: (event: TimelineEvent) => void
|
|
144
|
-
onEventEdit?: (event: TimelineEvent) => void
|
|
145
|
-
onEventDelete?: (event: TimelineEvent) => void
|
|
146
|
-
onEventsReorder?: (events: TimelineEvent[]) => void
|
|
147
|
-
className?: string
|
|
148
|
-
|
|
149
|
-
// Layout options
|
|
150
|
-
layout?: TimelineLayout
|
|
151
|
-
theme?: TimelineTheme
|
|
152
|
-
animation?: TimelineAnimation
|
|
153
|
-
groupBy?: TimelineGroupBy
|
|
154
|
-
|
|
155
|
-
// Display options
|
|
156
|
-
showUserInfo?: boolean
|
|
157
|
-
showMetadata?: boolean
|
|
158
|
-
showRelativeTime?: boolean
|
|
159
|
-
showSearch?: boolean
|
|
160
|
-
showFilter?: boolean
|
|
161
|
-
showExport?: boolean
|
|
162
|
-
|
|
163
|
-
// Customization
|
|
164
|
-
eventRenderer?: EventRenderer
|
|
165
|
-
connectorRenderer?: ConnectorRenderer
|
|
166
|
-
iconRenderer?: IconRenderer
|
|
167
|
-
colorScheme?: Record<TimelineEventType, string>
|
|
168
|
-
|
|
169
|
-
// Interactive features
|
|
170
|
-
draggable?: boolean
|
|
171
|
-
expandable?: boolean
|
|
172
|
-
editable?: boolean
|
|
173
|
-
selectable?: boolean
|
|
174
|
-
virtualScroll?: boolean
|
|
175
|
-
|
|
176
|
-
// Advanced features
|
|
177
|
-
maxEventsPerGroup?: number
|
|
178
|
-
printMode?: boolean
|
|
179
|
-
compactMode?: boolean
|
|
180
|
-
parallaxEffect?: boolean
|
|
181
|
-
gradientConnectors?: boolean
|
|
182
|
-
animatedProgress?: boolean
|
|
183
|
-
|
|
184
|
-
// Accessibility
|
|
185
|
-
ariaLabel?: string
|
|
186
|
-
keyboardNavigation?: boolean
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Timeline variants
|
|
190
|
-
const timelineVariants = cva("w-full", {
|
|
191
|
-
variants: {
|
|
192
|
-
theme: {
|
|
193
|
-
default: "",
|
|
194
|
-
minimal: "border-0 shadow-none",
|
|
195
|
-
detailed: "border-2",
|
|
196
|
-
compact: "p-0"
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
defaultVariants: {
|
|
200
|
-
theme: "default"
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// Default color schemes
|
|
205
|
-
const DEFAULT_COLORS: Record<TimelineEventType, string> = {
|
|
206
|
-
success: 'bg-green-500 border-green-500',
|
|
207
|
-
warning: 'bg-yellow-500 border-yellow-500',
|
|
208
|
-
error: 'bg-red-500 border-red-500',
|
|
209
|
-
info: 'bg-blue-500 border-blue-500',
|
|
210
|
-
pending: 'bg-muted-foreground/40 border-muted-foreground/40',
|
|
211
|
-
milestone: 'bg-purple-500 border-purple-500',
|
|
212
|
-
custom: 'bg-slate-500 border-slate-500'
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const DEFAULT_ICONS: Record<TimelineEventType, React.ReactNode> = {
|
|
216
|
-
success: <CheckCircle2 className="h-4 w-4 text-white" />,
|
|
217
|
-
warning: <AlertCircle className="h-4 w-4 text-white" />,
|
|
218
|
-
error: <XCircle className="h-4 w-4 text-white" />,
|
|
219
|
-
info: <Circle className="h-4 w-4 text-white" />,
|
|
220
|
-
pending: <Clock className="h-4 w-4 text-white" />,
|
|
221
|
-
milestone: <Flag className="h-4 w-4 text-white" />,
|
|
222
|
-
custom: <Sparkles className="h-4 w-4 text-white" />
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const TEXT_COLORS: Record<TimelineEventType, string> = {
|
|
226
|
-
success: 'text-green-700 dark:text-green-400',
|
|
227
|
-
warning: 'text-yellow-700 dark:text-yellow-400',
|
|
228
|
-
error: 'text-red-700 dark:text-red-400',
|
|
229
|
-
info: 'text-blue-700 dark:text-blue-400',
|
|
230
|
-
pending: 'text-muted-foreground',
|
|
231
|
-
milestone: 'text-purple-700 dark:text-purple-400',
|
|
232
|
-
custom: 'text-slate-700 dark:text-slate-400'
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Animation variants
|
|
236
|
-
const animationVariants = {
|
|
237
|
-
fade: {
|
|
238
|
-
initial: { opacity: 0 },
|
|
239
|
-
animate: { opacity: 1 },
|
|
240
|
-
exit: { opacity: 0 }
|
|
241
|
-
},
|
|
242
|
-
slide: {
|
|
243
|
-
initial: { opacity: 0, x: -20 },
|
|
244
|
-
animate: { opacity: 1, x: 0 },
|
|
245
|
-
exit: { opacity: 0, x: 20 }
|
|
246
|
-
},
|
|
247
|
-
scale: {
|
|
248
|
-
initial: { opacity: 0, scale: 0.8 },
|
|
249
|
-
animate: { opacity: 1, scale: 1 },
|
|
250
|
-
exit: { opacity: 0, scale: 0.8 }
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function Timeline({
|
|
255
|
-
events: initialEvents,
|
|
256
|
-
onEventClick,
|
|
257
|
-
onEventEdit,
|
|
258
|
-
onEventDelete,
|
|
259
|
-
onEventsReorder,
|
|
260
|
-
className,
|
|
261
|
-
layout = 'vertical',
|
|
262
|
-
theme = 'default',
|
|
263
|
-
animation = 'fade',
|
|
264
|
-
groupBy = 'none',
|
|
265
|
-
showUserInfo = true,
|
|
266
|
-
showMetadata = true,
|
|
267
|
-
showRelativeTime = true,
|
|
268
|
-
showSearch = true,
|
|
269
|
-
showFilter = true,
|
|
270
|
-
showExport = true,
|
|
271
|
-
eventRenderer,
|
|
272
|
-
connectorRenderer,
|
|
273
|
-
iconRenderer,
|
|
274
|
-
colorScheme = DEFAULT_COLORS,
|
|
275
|
-
draggable = false,
|
|
276
|
-
expandable = true,
|
|
277
|
-
editable = false,
|
|
278
|
-
selectable = false,
|
|
279
|
-
virtualScroll = false,
|
|
280
|
-
maxEventsPerGroup = 20,
|
|
281
|
-
printMode = false,
|
|
282
|
-
compactMode = false,
|
|
283
|
-
parallaxEffect = false,
|
|
284
|
-
gradientConnectors = false,
|
|
285
|
-
animatedProgress = false,
|
|
286
|
-
ariaLabel = "Timeline of events",
|
|
287
|
-
keyboardNavigation = true,
|
|
288
|
-
...props
|
|
289
|
-
}: TimelineProps) {
|
|
290
|
-
const [events, setEvents] = useState<TimelineEvent[]>(initialEvents)
|
|
291
|
-
const [searchQuery, setSearchQuery] = useState('')
|
|
292
|
-
const [filterType, setFilterType] = useState<TimelineEventType | 'all'>('all')
|
|
293
|
-
const [selectedEvents, setSelectedEvents] = useState<Set<string>>(new Set())
|
|
294
|
-
const [expandedEvents, setExpandedEvents] = useState<Set<string>>(new Set())
|
|
295
|
-
const [focusedEventId, setFocusedEventId] = useState<string | null>(null)
|
|
296
|
-
|
|
297
|
-
const timelineRef = useRef<HTMLDivElement>(null)
|
|
298
|
-
const eventRefs = useRef<Map<string, HTMLDivElement>>(new Map())
|
|
299
|
-
|
|
300
|
-
// Update events when prop changes
|
|
301
|
-
useEffect(() => {
|
|
302
|
-
setEvents(initialEvents)
|
|
303
|
-
}, [initialEvents])
|
|
304
|
-
|
|
305
|
-
// Keyboard navigation
|
|
306
|
-
useEffect(() => {
|
|
307
|
-
if (!keyboardNavigation) return
|
|
308
|
-
|
|
309
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
310
|
-
const currentIndex = events.findIndex(event => event.id === focusedEventId)
|
|
311
|
-
|
|
312
|
-
switch (e.key) {
|
|
313
|
-
case 'ArrowDown':
|
|
314
|
-
case 'ArrowRight':
|
|
315
|
-
e.preventDefault()
|
|
316
|
-
if (currentIndex < events.length - 1) {
|
|
317
|
-
const nextEvent = events[currentIndex + 1]
|
|
318
|
-
setFocusedEventId(nextEvent.id)
|
|
319
|
-
eventRefs.current.get(nextEvent.id)?.focus()
|
|
320
|
-
}
|
|
321
|
-
break
|
|
322
|
-
case 'ArrowUp':
|
|
323
|
-
case 'ArrowLeft':
|
|
324
|
-
e.preventDefault()
|
|
325
|
-
if (currentIndex > 0) {
|
|
326
|
-
const prevEvent = events[currentIndex - 1]
|
|
327
|
-
setFocusedEventId(prevEvent.id)
|
|
328
|
-
eventRefs.current.get(prevEvent.id)?.focus()
|
|
329
|
-
}
|
|
330
|
-
break
|
|
331
|
-
case 'Enter':
|
|
332
|
-
case ' ':
|
|
333
|
-
e.preventDefault()
|
|
334
|
-
if (focusedEventId) {
|
|
335
|
-
const event = events.find(e => e.id === focusedEventId)
|
|
336
|
-
if (event && onEventClick) {
|
|
337
|
-
onEventClick(event)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
break
|
|
341
|
-
case 'Delete':
|
|
342
|
-
if (editable && focusedEventId && onEventDelete) {
|
|
343
|
-
const event = events.find(e => e.id === focusedEventId)
|
|
344
|
-
if (event) {
|
|
345
|
-
onEventDelete(event)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
break
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
timelineRef.current?.addEventListener('keydown', handleKeyDown)
|
|
353
|
-
return () => timelineRef.current?.removeEventListener('keydown', handleKeyDown)
|
|
354
|
-
}, [keyboardNavigation, events, focusedEventId, onEventClick, onEventDelete, editable])
|
|
355
|
-
|
|
356
|
-
// Utility functions
|
|
357
|
-
const formatDate = useCallback((date: Date) => {
|
|
358
|
-
return date.toLocaleDateString('en-US', {
|
|
359
|
-
year: 'numeric',
|
|
360
|
-
month: 'long',
|
|
361
|
-
day: 'numeric'
|
|
362
|
-
})
|
|
363
|
-
}, [])
|
|
364
|
-
|
|
365
|
-
const formatTime = useCallback((date: Date) => {
|
|
366
|
-
return date.toLocaleTimeString('en-US', {
|
|
367
|
-
hour: '2-digit',
|
|
368
|
-
minute: '2-digit',
|
|
369
|
-
hour12: true
|
|
370
|
-
})
|
|
371
|
-
}, [])
|
|
372
|
-
|
|
373
|
-
const getRelativeTime = useCallback((date: Date) => {
|
|
374
|
-
const now = new Date()
|
|
375
|
-
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
|
376
|
-
|
|
377
|
-
if (diffInSeconds < 0) {
|
|
378
|
-
const futureDiff = Math.abs(diffInSeconds)
|
|
379
|
-
if (futureDiff < 3600) {
|
|
380
|
-
const minutes = Math.floor(futureDiff / 60)
|
|
381
|
-
return `In ${minutes} minute${minutes !== 1 ? 's' : ''}`
|
|
382
|
-
} else if (futureDiff < 86400) {
|
|
383
|
-
const hours = Math.floor(futureDiff / 3600)
|
|
384
|
-
return `In ${hours} hour${hours !== 1 ? 's' : ''}`
|
|
385
|
-
} else {
|
|
386
|
-
const days = Math.floor(futureDiff / 86400)
|
|
387
|
-
return `In ${days} day${days !== 1 ? 's' : ''}`
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (diffInSeconds < 60) {
|
|
392
|
-
return 'Just now'
|
|
393
|
-
} else if (diffInSeconds < 3600) {
|
|
394
|
-
const minutes = Math.floor(diffInSeconds / 60)
|
|
395
|
-
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`
|
|
396
|
-
} else if (diffInSeconds < 86400) {
|
|
397
|
-
const hours = Math.floor(diffInSeconds / 3600)
|
|
398
|
-
return `${hours} hour${hours > 1 ? 's' : ''} ago`
|
|
399
|
-
} else if (diffInSeconds < 604800) {
|
|
400
|
-
const days = Math.floor(diffInSeconds / 86400)
|
|
401
|
-
return `${days} day${days > 1 ? 's' : ''} ago`
|
|
402
|
-
} else {
|
|
403
|
-
return formatDate(date)
|
|
404
|
-
}
|
|
405
|
-
}, [formatDate])
|
|
406
|
-
|
|
407
|
-
const getInitials = useCallback((name: string) => {
|
|
408
|
-
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
|
409
|
-
}, [])
|
|
410
|
-
|
|
411
|
-
// Grouping functions
|
|
412
|
-
const groupEventsByDate = useCallback((events: TimelineEvent[]) => {
|
|
413
|
-
const groups: { [key: string]: TimelineEvent[] } = {}
|
|
414
|
-
|
|
415
|
-
events.forEach(event => {
|
|
416
|
-
let dateKey: string
|
|
417
|
-
|
|
418
|
-
switch (groupBy) {
|
|
419
|
-
case 'week':
|
|
420
|
-
const weekStart = new Date(event.date)
|
|
421
|
-
weekStart.setDate(weekStart.getDate() - weekStart.getDay())
|
|
422
|
-
dateKey = `Week of ${formatDate(weekStart)}`
|
|
423
|
-
break
|
|
424
|
-
case 'month':
|
|
425
|
-
dateKey = event.date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' })
|
|
426
|
-
break
|
|
427
|
-
case 'year':
|
|
428
|
-
dateKey = event.date.getFullYear().toString()
|
|
429
|
-
break
|
|
430
|
-
case 'date':
|
|
431
|
-
default:
|
|
432
|
-
dateKey = formatDate(event.date)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (!groups[dateKey]) {
|
|
436
|
-
groups[dateKey] = []
|
|
437
|
-
}
|
|
438
|
-
groups[dateKey].push(event)
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
return groups
|
|
442
|
-
}, [groupBy, formatDate])
|
|
443
|
-
|
|
444
|
-
// Filtering and sorting
|
|
445
|
-
const filteredAndSortedEvents = useMemo(() => {
|
|
446
|
-
let filtered = [...events]
|
|
447
|
-
|
|
448
|
-
// Apply search filter
|
|
449
|
-
if (searchQuery) {
|
|
450
|
-
filtered = filtered.filter(event =>
|
|
451
|
-
event.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
452
|
-
event.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
453
|
-
event.metadata?.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
454
|
-
)
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Apply type filter
|
|
458
|
-
if (filterType !== 'all') {
|
|
459
|
-
filtered = filtered.filter(event => event.type === filterType)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Sort by date
|
|
463
|
-
return filtered.sort((a, b) => b.date.getTime() - a.date.getTime())
|
|
464
|
-
}, [events, searchQuery, filterType])
|
|
465
|
-
|
|
466
|
-
// Event handlers
|
|
467
|
-
const handleEventClick = useCallback((event: TimelineEvent) => {
|
|
468
|
-
if (selectable) {
|
|
469
|
-
setSelectedEvents(prev => {
|
|
470
|
-
const newSet = new Set(prev)
|
|
471
|
-
if (newSet.has(event.id)) {
|
|
472
|
-
newSet.delete(event.id)
|
|
473
|
-
} else {
|
|
474
|
-
newSet.add(event.id)
|
|
475
|
-
}
|
|
476
|
-
return newSet
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (expandable && 'subEvents' in event) {
|
|
481
|
-
setExpandedEvents(prev => {
|
|
482
|
-
const newSet = new Set(prev)
|
|
483
|
-
if (newSet.has(event.id)) {
|
|
484
|
-
newSet.delete(event.id)
|
|
485
|
-
} else {
|
|
486
|
-
newSet.add(event.id)
|
|
487
|
-
}
|
|
488
|
-
return newSet
|
|
489
|
-
})
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
onEventClick?.(event)
|
|
493
|
-
}, [selectable, expandable, onEventClick])
|
|
494
|
-
|
|
495
|
-
const handleExport = useCallback(() => {
|
|
496
|
-
const dataStr = JSON.stringify(filteredAndSortedEvents, null, 2)
|
|
497
|
-
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr)
|
|
498
|
-
|
|
499
|
-
const exportFileDefaultName = `timeline-export-${new Date().toISOString()}.json`
|
|
500
|
-
|
|
501
|
-
const linkElement = document.createElement('a')
|
|
502
|
-
linkElement.setAttribute('href', dataUri)
|
|
503
|
-
linkElement.setAttribute('download', exportFileDefaultName)
|
|
504
|
-
linkElement.click()
|
|
505
|
-
}, [filteredAndSortedEvents])
|
|
506
|
-
|
|
507
|
-
const handlePrint = useCallback(() => {
|
|
508
|
-
window.print()
|
|
509
|
-
}, [])
|
|
510
|
-
|
|
511
|
-
// Render helpers
|
|
512
|
-
const renderEventIcon = useCallback((event: TimelineEvent) => {
|
|
513
|
-
if (iconRenderer) {
|
|
514
|
-
return iconRenderer(event)
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if (event.icon) {
|
|
518
|
-
return event.icon
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if ('milestone' in event) {
|
|
522
|
-
return <Target className="h-4 w-4 text-white" />
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if ('recurrence' in event) {
|
|
526
|
-
return <Repeat className="h-4 w-4 text-white" />
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if ('subEvents' in event) {
|
|
530
|
-
return <GitBranch className="h-4 w-4 text-white" />
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return DEFAULT_ICONS[event.type] || DEFAULT_ICONS.custom
|
|
534
|
-
}, [iconRenderer])
|
|
535
|
-
|
|
536
|
-
const renderConnector = useCallback((fromEvent: TimelineEvent, toEvent: TimelineEvent, isLast: boolean) => {
|
|
537
|
-
if (connectorRenderer) {
|
|
538
|
-
return connectorRenderer(fromEvent, toEvent)
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (isLast) return null
|
|
542
|
-
|
|
543
|
-
const baseClasses = cn(
|
|
544
|
-
"absolute",
|
|
545
|
-
layout === 'horizontal' ? "h-0.5 top-4" : "w-0.5 left-1/2 -ml-px transform -translate-x-1/2",
|
|
546
|
-
gradientConnectors && "bg-gradient-to-b from-border to-transparent"
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
if (layout === 'horizontal') {
|
|
550
|
-
return <div className={cn(baseClasses, "bg-border")} style={{ width: '100px' }} />
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return (
|
|
554
|
-
<div className={cn(
|
|
555
|
-
baseClasses,
|
|
556
|
-
"bg-border",
|
|
557
|
-
layout === 'alternating' && "hidden"
|
|
558
|
-
)}
|
|
559
|
-
style={{
|
|
560
|
-
height: 'calc(100% + 2rem)',
|
|
561
|
-
top: '2rem'
|
|
562
|
-
}} />
|
|
563
|
-
)
|
|
564
|
-
}, [connectorRenderer, layout, gradientConnectors])
|
|
565
|
-
|
|
566
|
-
const renderEvent = useCallback((event: TimelineEvent, index: number, isLast: boolean = false) => {
|
|
567
|
-
const eventColor = event.color || colorScheme[event.type] || DEFAULT_COLORS[event.type]
|
|
568
|
-
const textColor = TEXT_COLORS[event.type] || TEXT_COLORS.custom
|
|
569
|
-
const isSelected = selectedEvents.has(event.id)
|
|
570
|
-
const isExpanded = expandedEvents.has(event.id)
|
|
571
|
-
const isFocused = focusedEventId === event.id
|
|
572
|
-
|
|
573
|
-
// Custom renderer
|
|
574
|
-
if (eventRenderer) {
|
|
575
|
-
return eventRenderer(event, index)
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Check if event is a range event
|
|
579
|
-
const isRangeEvent = 'endDate' in event
|
|
580
|
-
const isNestedEvent = 'subEvents' in event
|
|
581
|
-
const isMilestoneEvent = 'milestone' in event
|
|
582
|
-
|
|
583
|
-
const eventContent = (
|
|
584
|
-
<div
|
|
585
|
-
ref={(el) => {
|
|
586
|
-
if (el) eventRefs.current.set(event.id, el)
|
|
587
|
-
}}
|
|
588
|
-
tabIndex={keyboardNavigation ? 0 : -1}
|
|
589
|
-
className={cn(
|
|
590
|
-
"relative flex gap-4",
|
|
591
|
-
compactMode ? "pb-2" : "pb-8",
|
|
592
|
-
layout === 'alternating' && index % 2 === 0 && "flex-row-reverse",
|
|
593
|
-
layout === 'horizontal' && "flex-col items-center",
|
|
594
|
-
"cursor-pointer rounded-lg p-3 -m-1 transition-all duration-200",
|
|
595
|
-
"hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring",
|
|
596
|
-
isSelected && "bg-muted/70",
|
|
597
|
-
isFocused && "ring-2 ring-ring",
|
|
598
|
-
printMode && "print:break-inside-avoid"
|
|
599
|
-
)}
|
|
600
|
-
onClick={() => handleEventClick(event)}
|
|
601
|
-
onFocus={() => setFocusedEventId(event.id)}
|
|
602
|
-
role="button"
|
|
603
|
-
aria-label={`Event: ${event.title}`}
|
|
604
|
-
aria-expanded={isNestedEvent ? isExpanded : undefined}
|
|
605
|
-
>
|
|
606
|
-
{/* Timeline Node */}
|
|
607
|
-
<div className={cn(
|
|
608
|
-
"flex flex-col items-center relative",
|
|
609
|
-
layout === 'horizontal' && "flex-row"
|
|
610
|
-
)}>
|
|
611
|
-
<motion.div
|
|
612
|
-
className={cn(
|
|
613
|
-
"flex items-center justify-center rounded-full border-2 bg-background z-10",
|
|
614
|
-
isMilestoneEvent ? "w-10 h-10" : "w-8 h-8",
|
|
615
|
-
eventColor,
|
|
616
|
-
isSelected && "ring-2 ring-ring ring-offset-2"
|
|
617
|
-
)}
|
|
618
|
-
whileHover={{ scale: 1.1 }}
|
|
619
|
-
whileTap={{ scale: 0.95 }}
|
|
620
|
-
>
|
|
621
|
-
{renderEventIcon(event)}
|
|
622
|
-
</motion.div>
|
|
623
|
-
|
|
624
|
-
{/* Connector */}
|
|
625
|
-
{!isLast && renderConnector(event, events[index + 1], isLast)}
|
|
626
|
-
</div>
|
|
627
|
-
|
|
628
|
-
{/* Event Content */}
|
|
629
|
-
<div className={cn(
|
|
630
|
-
"flex-1 min-w-0",
|
|
631
|
-
layout === 'alternating' && index % 2 === 0 && "text-right"
|
|
632
|
-
)}>
|
|
633
|
-
{/* Header */}
|
|
634
|
-
<div className="flex items-start justify-between gap-2 mb-2">
|
|
635
|
-
<div className="flex-1">
|
|
636
|
-
<h4 className={cn(
|
|
637
|
-
"font-semibold",
|
|
638
|
-
compactMode ? "text-sm" : "text-base",
|
|
639
|
-
isMilestoneEvent && "text-lg"
|
|
640
|
-
)}>
|
|
641
|
-
{event.title}
|
|
642
|
-
{isMilestoneEvent && (
|
|
643
|
-
<Badge variant="secondary" className="ml-2 bg-purple-500">
|
|
644
|
-
Milestone
|
|
645
|
-
</Badge>
|
|
646
|
-
)}
|
|
647
|
-
</h4>
|
|
648
|
-
|
|
649
|
-
{/* Date/Time display */}
|
|
650
|
-
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
|
|
651
|
-
<Clock className="h-3 w-3" />
|
|
652
|
-
{showRelativeTime ? (
|
|
653
|
-
<span>{getRelativeTime(event.date)}</span>
|
|
654
|
-
) : (
|
|
655
|
-
<span>{formatTime(event.date)}</span>
|
|
656
|
-
)}
|
|
657
|
-
{isRangeEvent && (
|
|
658
|
-
<>
|
|
659
|
-
<ArrowRight className="h-3 w-3" />
|
|
660
|
-
<span>{formatTime((event as RangeEvent).endDate)}</span>
|
|
661
|
-
</>
|
|
662
|
-
)}
|
|
663
|
-
</div>
|
|
664
|
-
</div>
|
|
665
|
-
|
|
666
|
-
{/* Actions */}
|
|
667
|
-
{(editable || event.metadata?.externalLink) && (
|
|
668
|
-
<DropdownMenu>
|
|
669
|
-
<DropdownMenuTrigger asChild>
|
|
670
|
-
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
671
|
-
<MoreVertical className="h-4 w-4" />
|
|
672
|
-
</Button>
|
|
673
|
-
</DropdownMenuTrigger>
|
|
674
|
-
<DropdownMenuContent align="end">
|
|
675
|
-
{editable && (
|
|
676
|
-
<>
|
|
677
|
-
<DropdownMenuItem onClick={() => onEventEdit?.(event)}>
|
|
678
|
-
<Edit className="mr-2 h-4 w-4" />
|
|
679
|
-
Edit
|
|
680
|
-
</DropdownMenuItem>
|
|
681
|
-
<DropdownMenuItem onClick={() => onEventDelete?.(event)}>
|
|
682
|
-
<Trash className="mr-2 h-4 w-4" />
|
|
683
|
-
Delete
|
|
684
|
-
</DropdownMenuItem>
|
|
685
|
-
<DropdownMenuSeparator />
|
|
686
|
-
</>
|
|
687
|
-
)}
|
|
688
|
-
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(event.title)}>
|
|
689
|
-
<Copy className="mr-2 h-4 w-4" />
|
|
690
|
-
Copy
|
|
691
|
-
</DropdownMenuItem>
|
|
692
|
-
<DropdownMenuItem>
|
|
693
|
-
<Share className="mr-2 h-4 w-4" />
|
|
694
|
-
Share
|
|
695
|
-
</DropdownMenuItem>
|
|
696
|
-
{event.metadata?.externalLink && (
|
|
697
|
-
<DropdownMenuItem onClick={() => window.open(event.metadata?.externalLink, '_blank')}>
|
|
698
|
-
<ExternalLink className="mr-2 h-4 w-4" />
|
|
699
|
-
View Details
|
|
700
|
-
</DropdownMenuItem>
|
|
701
|
-
)}
|
|
702
|
-
</DropdownMenuContent>
|
|
703
|
-
</DropdownMenu>
|
|
704
|
-
)}
|
|
705
|
-
</div>
|
|
706
|
-
|
|
707
|
-
{/* Description */}
|
|
708
|
-
{event.description && (
|
|
709
|
-
<p className={cn(
|
|
710
|
-
"text-muted-foreground mb-2",
|
|
711
|
-
compactMode ? "text-xs" : "text-sm"
|
|
712
|
-
)}>
|
|
713
|
-
{event.description}
|
|
714
|
-
</p>
|
|
715
|
-
)}
|
|
716
|
-
|
|
717
|
-
{/* Milestone details */}
|
|
718
|
-
{isMilestoneEvent && (event as MilestoneEvent).milestone && (
|
|
719
|
-
<div className="bg-purple-50 dark:bg-purple-950/20 rounded-md p-3 mb-2">
|
|
720
|
-
{(event as MilestoneEvent).milestone.target && (
|
|
721
|
-
<div className="flex items-center gap-2 text-sm">
|
|
722
|
-
<Target className="h-4 w-4 text-purple-600" />
|
|
723
|
-
<span className="font-medium">Target:</span>
|
|
724
|
-
<span>{(event as MilestoneEvent).milestone.target}</span>
|
|
725
|
-
</div>
|
|
726
|
-
)}
|
|
727
|
-
{(event as MilestoneEvent).milestone.achievement && (
|
|
728
|
-
<div className="flex items-center gap-2 text-sm mt-1">
|
|
729
|
-
<TrendingUp className="h-4 w-4 text-purple-600" />
|
|
730
|
-
<span className="font-medium">Achievement:</span>
|
|
731
|
-
<span>{(event as MilestoneEvent).milestone.achievement}</span>
|
|
732
|
-
</div>
|
|
733
|
-
)}
|
|
734
|
-
</div>
|
|
735
|
-
)}
|
|
736
|
-
|
|
737
|
-
{/* Progress bar for events with progress */}
|
|
738
|
-
{event.metadata?.progress !== undefined && (
|
|
739
|
-
<div className="mb-2">
|
|
740
|
-
<div className="flex justify-between text-xs mb-1">
|
|
741
|
-
<span>Progress</span>
|
|
742
|
-
<span>{event.metadata.progress}%</span>
|
|
743
|
-
</div>
|
|
744
|
-
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
|
|
745
|
-
<motion.div
|
|
746
|
-
className="h-full bg-primary"
|
|
747
|
-
initial={{ width: 0 }}
|
|
748
|
-
animate={{ width: `${event.metadata.progress}%` }}
|
|
749
|
-
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
750
|
-
/>
|
|
751
|
-
</div>
|
|
752
|
-
</div>
|
|
753
|
-
)}
|
|
754
|
-
|
|
755
|
-
{/* User info */}
|
|
756
|
-
{showUserInfo && event.user && (
|
|
757
|
-
<div className="flex items-center gap-2 mb-2">
|
|
758
|
-
<MoonUIAvatarPro className="h-6 w-6">
|
|
759
|
-
<MoonUIAvatarImagePro src={event.user.avatar} />
|
|
760
|
-
<MoonUIAvatarFallbackPro className="text-xs">
|
|
761
|
-
{getInitials(event.user.name)}
|
|
762
|
-
</MoonUIAvatarFallbackPro>
|
|
763
|
-
</MoonUIAvatarPro>
|
|
764
|
-
<div className="flex flex-col">
|
|
765
|
-
<span className="text-sm font-medium">{event.user.name}</span>
|
|
766
|
-
{event.user.role && (
|
|
767
|
-
<span className="text-xs text-muted-foreground">{event.user.role}</span>
|
|
768
|
-
)}
|
|
769
|
-
</div>
|
|
770
|
-
</div>
|
|
771
|
-
)}
|
|
772
|
-
|
|
773
|
-
{/* Metadata */}
|
|
774
|
-
{showMetadata && event.metadata && (
|
|
775
|
-
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
776
|
-
{event.metadata.duration && (
|
|
777
|
-
<div className="flex items-center gap-1">
|
|
778
|
-
<Clock className="h-3 w-3" />
|
|
779
|
-
<span>{event.metadata.duration}</span>
|
|
780
|
-
</div>
|
|
781
|
-
)}
|
|
782
|
-
{event.metadata.location && (
|
|
783
|
-
<div className="flex items-center gap-1">
|
|
784
|
-
<Calendar className="h-3 w-3" />
|
|
785
|
-
<span>{event.metadata.location}</span>
|
|
786
|
-
</div>
|
|
787
|
-
)}
|
|
788
|
-
{event.metadata.comments !== undefined && event.metadata.comments > 0 && (
|
|
789
|
-
<div className="flex items-center gap-1">
|
|
790
|
-
<MessageCircle className="h-3 w-3" />
|
|
791
|
-
<span>{event.metadata.comments}</span>
|
|
792
|
-
</div>
|
|
793
|
-
)}
|
|
794
|
-
{event.metadata.attachments !== undefined && event.metadata.attachments > 0 && (
|
|
795
|
-
<div className="flex items-center gap-1">
|
|
796
|
-
<Paperclip className="h-3 w-3" />
|
|
797
|
-
<span>{event.metadata.attachments}</span>
|
|
798
|
-
</div>
|
|
799
|
-
)}
|
|
800
|
-
{event.metadata.priority && (
|
|
801
|
-
<Badge
|
|
802
|
-
variant={
|
|
803
|
-
event.metadata.priority === 'high' ? 'destructive' :
|
|
804
|
-
event.metadata.priority === 'medium' ? 'outline' :
|
|
805
|
-
'secondary'
|
|
806
|
-
}
|
|
807
|
-
className="text-xs"
|
|
808
|
-
>
|
|
809
|
-
{event.metadata.priority}
|
|
810
|
-
</Badge>
|
|
811
|
-
)}
|
|
812
|
-
</div>
|
|
813
|
-
)}
|
|
814
|
-
|
|
815
|
-
{/* Tags */}
|
|
816
|
-
{event.metadata?.tags && event.metadata.tags.length > 0 && (
|
|
817
|
-
<div className="flex flex-wrap gap-1 mt-2">
|
|
818
|
-
{event.metadata.tags.map((tag, tagIndex) => (
|
|
819
|
-
<Badge key={tagIndex} variant="outline" className="text-xs">
|
|
820
|
-
{tag}
|
|
821
|
-
</Badge>
|
|
822
|
-
))}
|
|
823
|
-
</div>
|
|
824
|
-
)}
|
|
825
|
-
|
|
826
|
-
{/* Nested events */}
|
|
827
|
-
{isNestedEvent && isExpanded && (event as NestedEvent).subEvents && (
|
|
828
|
-
<div className="mt-3 ml-4 pl-4 border-l-2 border-muted">
|
|
829
|
-
{(event as NestedEvent).subEvents!.map((subEvent, subIndex) => (
|
|
830
|
-
<div key={subEvent.id} className="mb-2 last:mb-0">
|
|
831
|
-
<div className="flex items-start gap-2">
|
|
832
|
-
<div className={cn(
|
|
833
|
-
"w-2 h-2 rounded-full mt-1.5",
|
|
834
|
-
colorScheme[subEvent.type] || DEFAULT_COLORS[subEvent.type]
|
|
835
|
-
)} />
|
|
836
|
-
<div className="flex-1">
|
|
837
|
-
<h5 className="text-sm font-medium">{subEvent.title}</h5>
|
|
838
|
-
{subEvent.description && (
|
|
839
|
-
<p className="text-xs text-muted-foreground">{subEvent.description}</p>
|
|
840
|
-
)}
|
|
841
|
-
<span className="text-xs text-muted-foreground">
|
|
842
|
-
{getRelativeTime(subEvent.date)}
|
|
843
|
-
</span>
|
|
844
|
-
</div>
|
|
845
|
-
</div>
|
|
846
|
-
</div>
|
|
847
|
-
))}
|
|
848
|
-
</div>
|
|
849
|
-
)}
|
|
850
|
-
|
|
851
|
-
{/* Expand/Collapse button for nested events */}
|
|
852
|
-
{isNestedEvent && (event as NestedEvent).subEvents && (event as NestedEvent).subEvents!.length > 0 && (
|
|
853
|
-
<Button
|
|
854
|
-
variant="ghost"
|
|
855
|
-
size="sm"
|
|
856
|
-
className="mt-2 h-6 text-xs"
|
|
857
|
-
onClick={(e) => {
|
|
858
|
-
e.stopPropagation()
|
|
859
|
-
setExpandedEvents(prev => {
|
|
860
|
-
const newSet = new Set(prev)
|
|
861
|
-
if (newSet.has(event.id)) {
|
|
862
|
-
newSet.delete(event.id)
|
|
863
|
-
} else {
|
|
864
|
-
newSet.add(event.id)
|
|
865
|
-
}
|
|
866
|
-
return newSet
|
|
867
|
-
})
|
|
868
|
-
}}
|
|
869
|
-
>
|
|
870
|
-
{isExpanded ? (
|
|
871
|
-
<>
|
|
872
|
-
<ChevronDown className="mr-1 h-3 w-3" />
|
|
873
|
-
Hide {(event as NestedEvent).subEvents!.length} sub-events
|
|
874
|
-
</>
|
|
875
|
-
) : (
|
|
876
|
-
<>
|
|
877
|
-
<ChevronRight className="mr-1 h-3 w-3" />
|
|
878
|
-
Show {(event as NestedEvent).subEvents!.length} sub-events
|
|
879
|
-
</>
|
|
880
|
-
)}
|
|
881
|
-
</Button>
|
|
882
|
-
)}
|
|
883
|
-
</div>
|
|
884
|
-
</div>
|
|
885
|
-
)
|
|
886
|
-
|
|
887
|
-
if (draggable && onEventsReorder) {
|
|
888
|
-
return (
|
|
889
|
-
<Reorder.Item
|
|
890
|
-
key={event.id}
|
|
891
|
-
value={event}
|
|
892
|
-
dragListener={false}
|
|
893
|
-
dragControls={undefined}
|
|
894
|
-
>
|
|
895
|
-
{eventContent}
|
|
896
|
-
</Reorder.Item>
|
|
897
|
-
)
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const variants = animation !== 'none' ? animationVariants[animation] : undefined
|
|
901
|
-
|
|
902
|
-
return (
|
|
903
|
-
<motion.div
|
|
904
|
-
key={event.id}
|
|
905
|
-
variants={variants}
|
|
906
|
-
initial={variants?.initial}
|
|
907
|
-
animate={variants?.animate}
|
|
908
|
-
exit={variants?.exit}
|
|
909
|
-
transition={{ duration: 0.3, delay: index * 0.05 }}
|
|
910
|
-
>
|
|
911
|
-
{eventContent}
|
|
912
|
-
</motion.div>
|
|
913
|
-
)
|
|
914
|
-
}, [
|
|
915
|
-
eventRenderer,
|
|
916
|
-
colorScheme,
|
|
917
|
-
selectedEvents,
|
|
918
|
-
expandedEvents,
|
|
919
|
-
focusedEventId,
|
|
920
|
-
keyboardNavigation,
|
|
921
|
-
layout,
|
|
922
|
-
compactMode,
|
|
923
|
-
printMode,
|
|
924
|
-
handleEventClick,
|
|
925
|
-
renderEventIcon,
|
|
926
|
-
renderConnector,
|
|
927
|
-
events,
|
|
928
|
-
showRelativeTime,
|
|
929
|
-
getRelativeTime,
|
|
930
|
-
formatTime,
|
|
931
|
-
editable,
|
|
932
|
-
onEventEdit,
|
|
933
|
-
onEventDelete,
|
|
934
|
-
showUserInfo,
|
|
935
|
-
getInitials,
|
|
936
|
-
showMetadata,
|
|
937
|
-
draggable,
|
|
938
|
-
onEventsReorder,
|
|
939
|
-
animation
|
|
940
|
-
])
|
|
941
|
-
|
|
942
|
-
const renderGroupedEvents = useCallback(() => {
|
|
943
|
-
const groups = groupEventsByDate(filteredAndSortedEvents)
|
|
944
|
-
|
|
945
|
-
return Object.entries(groups).map(([dateGroup, groupEvents], groupIndex) => {
|
|
946
|
-
const displayEvents = maxEventsPerGroup
|
|
947
|
-
? groupEvents.slice(0, maxEventsPerGroup)
|
|
948
|
-
: groupEvents
|
|
949
|
-
const remainingCount = groupEvents.length - displayEvents.length
|
|
950
|
-
|
|
951
|
-
return (
|
|
952
|
-
<div key={dateGroup} className="mb-8">
|
|
953
|
-
<div className={cn(
|
|
954
|
-
"sticky top-0 z-20 bg-background/80 backdrop-blur-sm border-b p-3 mb-4 -mx-6 px-6",
|
|
955
|
-
printMode && "print:relative print:bg-transparent"
|
|
956
|
-
)}>
|
|
957
|
-
<h3 className="font-semibold text-sm text-muted-foreground flex items-center justify-between">
|
|
958
|
-
<span>{dateGroup}</span>
|
|
959
|
-
<Badge variant="secondary" className="text-xs">
|
|
960
|
-
{groupEvents.length} event{groupEvents.length !== 1 ? 's' : ''}
|
|
961
|
-
</Badge>
|
|
962
|
-
</h3>
|
|
963
|
-
</div>
|
|
964
|
-
|
|
965
|
-
<AnimatePresence mode="popLayout">
|
|
966
|
-
{displayEvents.map((event, index) =>
|
|
967
|
-
renderEvent(event, index, index === displayEvents.length - 1)
|
|
968
|
-
)}
|
|
969
|
-
</AnimatePresence>
|
|
970
|
-
|
|
971
|
-
{remainingCount > 0 && (
|
|
972
|
-
<Button
|
|
973
|
-
variant="ghost"
|
|
974
|
-
size="sm"
|
|
975
|
-
className="w-full mt-2"
|
|
976
|
-
onClick={() => {
|
|
977
|
-
// In a real implementation, this would expand the group
|
|
978
|
-
console.log(`Show ${remainingCount} more events`)
|
|
979
|
-
}}
|
|
980
|
-
>
|
|
981
|
-
Show {remainingCount} more event{remainingCount !== 1 ? 's' : ''}
|
|
982
|
-
</Button>
|
|
983
|
-
)}
|
|
984
|
-
</div>
|
|
985
|
-
)
|
|
986
|
-
})
|
|
987
|
-
}, [groupEventsByDate, filteredAndSortedEvents, maxEventsPerGroup, printMode, renderEvent])
|
|
988
|
-
|
|
989
|
-
const renderTimeline = () => {
|
|
990
|
-
const content = groupBy !== 'none'
|
|
991
|
-
? renderGroupedEvents()
|
|
992
|
-
: (
|
|
993
|
-
<AnimatePresence mode="popLayout">
|
|
994
|
-
{draggable && onEventsReorder ? (
|
|
995
|
-
<Reorder.Group
|
|
996
|
-
axis={layout === 'horizontal' ? 'x' : 'y'}
|
|
997
|
-
values={filteredAndSortedEvents}
|
|
998
|
-
onReorder={(newOrder) => {
|
|
999
|
-
setEvents(newOrder)
|
|
1000
|
-
onEventsReorder(newOrder)
|
|
1001
|
-
}}
|
|
1002
|
-
className={cn(
|
|
1003
|
-
"space-y-0",
|
|
1004
|
-
layout === 'horizontal' && "flex overflow-x-auto gap-6 pb-4"
|
|
1005
|
-
)}
|
|
1006
|
-
>
|
|
1007
|
-
{filteredAndSortedEvents.map((event, index) =>
|
|
1008
|
-
renderEvent(event, index, index === filteredAndSortedEvents.length - 1)
|
|
1009
|
-
)}
|
|
1010
|
-
</Reorder.Group>
|
|
1011
|
-
) : (
|
|
1012
|
-
<div className={cn(
|
|
1013
|
-
"space-y-0",
|
|
1014
|
-
layout === 'horizontal' && "flex overflow-x-auto gap-6 pb-4",
|
|
1015
|
-
layout === 'alternating' && "relative"
|
|
1016
|
-
)}>
|
|
1017
|
-
{filteredAndSortedEvents.map((event, index) =>
|
|
1018
|
-
renderEvent(event, index, index === filteredAndSortedEvents.length - 1)
|
|
1019
|
-
)}
|
|
1020
|
-
</div>
|
|
1021
|
-
)}
|
|
1022
|
-
</AnimatePresence>
|
|
1023
|
-
)
|
|
1024
|
-
|
|
1025
|
-
if (virtualScroll && filteredAndSortedEvents.length > 50) {
|
|
1026
|
-
// In a real implementation, use a virtual scrolling library
|
|
1027
|
-
return (
|
|
1028
|
-
<div className="h-[600px] overflow-y-auto">
|
|
1029
|
-
{content}
|
|
1030
|
-
</div>
|
|
1031
|
-
)
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
return content
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
return (
|
|
1038
|
-
<Card
|
|
1039
|
-
className={cn(
|
|
1040
|
-
timelineVariants({ theme }),
|
|
1041
|
-
"relative",
|
|
1042
|
-
printMode && "print:shadow-none print:border-0",
|
|
1043
|
-
className
|
|
1044
|
-
)}
|
|
1045
|
-
ref={timelineRef}
|
|
1046
|
-
role="region"
|
|
1047
|
-
aria-label={ariaLabel}
|
|
1048
|
-
{...props}
|
|
1049
|
-
>
|
|
1050
|
-
<CardHeader className={cn(
|
|
1051
|
-
compactMode && "pb-3",
|
|
1052
|
-
printMode && "print:pb-2"
|
|
1053
|
-
)}>
|
|
1054
|
-
<div className="flex items-center justify-between">
|
|
1055
|
-
<div>
|
|
1056
|
-
<CardTitle className="flex items-center gap-2">
|
|
1057
|
-
<Clock className="h-5 w-5" />
|
|
1058
|
-
Timeline
|
|
1059
|
-
</CardTitle>
|
|
1060
|
-
<CardDescription>
|
|
1061
|
-
{filteredAndSortedEvents.length} event{filteredAndSortedEvents.length !== 1 ? 's' : ''}
|
|
1062
|
-
{searchQuery && ` matching "${searchQuery}"`}
|
|
1063
|
-
{filterType !== 'all' && ` of type ${filterType}`}
|
|
1064
|
-
</CardDescription>
|
|
1065
|
-
</div>
|
|
1066
|
-
|
|
1067
|
-
{/* Actions */}
|
|
1068
|
-
<div className="flex items-center gap-2">
|
|
1069
|
-
{showExport && (
|
|
1070
|
-
<Button
|
|
1071
|
-
variant="ghost"
|
|
1072
|
-
size="sm"
|
|
1073
|
-
onClick={handleExport}
|
|
1074
|
-
className="print:hidden"
|
|
1075
|
-
>
|
|
1076
|
-
<Download className="h-4 w-4" />
|
|
1077
|
-
</Button>
|
|
1078
|
-
)}
|
|
1079
|
-
<Button
|
|
1080
|
-
variant="ghost"
|
|
1081
|
-
size="sm"
|
|
1082
|
-
onClick={handlePrint}
|
|
1083
|
-
className="print:hidden"
|
|
1084
|
-
>
|
|
1085
|
-
<Printer className="h-4 w-4" />
|
|
1086
|
-
</Button>
|
|
1087
|
-
</div>
|
|
1088
|
-
</div>
|
|
1089
|
-
|
|
1090
|
-
{/* Search and Filter */}
|
|
1091
|
-
{(showSearch || showFilter) && (
|
|
1092
|
-
<div className="flex gap-2 mt-4 print:hidden">
|
|
1093
|
-
{showSearch && (
|
|
1094
|
-
<div className="relative flex-1">
|
|
1095
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
1096
|
-
<Input
|
|
1097
|
-
type="search"
|
|
1098
|
-
placeholder="Search events..."
|
|
1099
|
-
value={searchQuery}
|
|
1100
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
1101
|
-
className="pl-9"
|
|
1102
|
-
/>
|
|
1103
|
-
</div>
|
|
1104
|
-
)}
|
|
1105
|
-
|
|
1106
|
-
{showFilter && (
|
|
1107
|
-
<Select value={filterType} onValueChange={(value: any) => setFilterType(value)}>
|
|
1108
|
-
<SelectTrigger className="w-[150px]">
|
|
1109
|
-
<Filter className="mr-2 h-4 w-4" />
|
|
1110
|
-
<SelectValue placeholder="Filter by type" />
|
|
1111
|
-
</SelectTrigger>
|
|
1112
|
-
<SelectContent>
|
|
1113
|
-
<SelectItem value="all">All Types</SelectItem>
|
|
1114
|
-
<SelectItem value="success">Success</SelectItem>
|
|
1115
|
-
<SelectItem value="warning">Warning</SelectItem>
|
|
1116
|
-
<SelectItem value="error">Error</SelectItem>
|
|
1117
|
-
<SelectItem value="info">Info</SelectItem>
|
|
1118
|
-
<SelectItem value="pending">Pending</SelectItem>
|
|
1119
|
-
<SelectItem value="milestone">Milestone</SelectItem>
|
|
1120
|
-
</SelectContent>
|
|
1121
|
-
</Select>
|
|
1122
|
-
)}
|
|
1123
|
-
</div>
|
|
1124
|
-
)}
|
|
1125
|
-
</CardHeader>
|
|
1126
|
-
|
|
1127
|
-
<CardContent className={cn(
|
|
1128
|
-
compactMode ? "pt-2" : "pt-6",
|
|
1129
|
-
"relative"
|
|
1130
|
-
)}>
|
|
1131
|
-
{filteredAndSortedEvents.length > 0 ? (
|
|
1132
|
-
renderTimeline()
|
|
1133
|
-
) : (
|
|
1134
|
-
<div className="text-center py-12">
|
|
1135
|
-
<Clock className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
|
1136
|
-
<p className="text-muted-foreground">
|
|
1137
|
-
{searchQuery || filterType !== 'all'
|
|
1138
|
-
? "No events match your filters"
|
|
1139
|
-
: "No events to display"}
|
|
1140
|
-
</p>
|
|
1141
|
-
</div>
|
|
1142
|
-
)}
|
|
1143
|
-
</CardContent>
|
|
1144
|
-
</Card>
|
|
1145
|
-
)
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
export default Timeline
|