@moontra/moonui-pro 2.20.2 → 2.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/package.json +8 -3
  2. package/plugin/index.d.ts +86 -0
  3. package/plugin/index.js +308 -0
  4. package/scripts/postinstall.js +191 -23
  5. package/src/components/advanced-chart/index.tsx +0 -1246
  6. package/src/components/advanced-forms/index.tsx +0 -585
  7. package/src/components/animated-button/index.tsx +0 -385
  8. package/src/components/calendar/event-dialog.tsx +0 -377
  9. package/src/components/calendar/index.tsx +0 -1220
  10. package/src/components/calendar-pro/index.tsx +0 -1697
  11. package/src/components/color-picker/index.tsx +0 -432
  12. package/src/components/credit-card-input/index.tsx +0 -406
  13. package/src/components/dashboard/dashboard-grid.tsx +0 -480
  14. package/src/components/dashboard/demo.tsx +0 -425
  15. package/src/components/dashboard/index.tsx +0 -1046
  16. package/src/components/dashboard/time-range-picker.tsx +0 -336
  17. package/src/components/dashboard/types.ts +0 -225
  18. package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
  19. package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
  20. package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
  21. package/src/components/dashboard/widgets/index.ts +0 -5
  22. package/src/components/dashboard/widgets/metric-card.tsx +0 -363
  23. package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
  24. package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
  25. package/src/components/data-table/data-table-column-toggle.tsx +0 -169
  26. package/src/components/data-table/data-table-export.ts +0 -156
  27. package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
  28. package/src/components/data-table/index.tsx +0 -845
  29. package/src/components/draggable-list/index.tsx +0 -100
  30. package/src/components/error-boundary/index.tsx +0 -232
  31. package/src/components/file-upload/index.tsx +0 -1660
  32. package/src/components/floating-action-button/index.tsx +0 -206
  33. package/src/components/form-wizard/form-wizard-context.tsx +0 -335
  34. package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
  35. package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
  36. package/src/components/form-wizard/form-wizard-step.tsx +0 -111
  37. package/src/components/form-wizard/index.tsx +0 -102
  38. package/src/components/form-wizard/types.ts +0 -77
  39. package/src/components/gesture-drawer/index.tsx +0 -551
  40. package/src/components/github-stars/github-api.ts +0 -426
  41. package/src/components/github-stars/hooks.ts +0 -517
  42. package/src/components/github-stars/index.tsx +0 -375
  43. package/src/components/github-stars/types.ts +0 -148
  44. package/src/components/github-stars/variants.tsx +0 -515
  45. package/src/components/health-check/index.tsx +0 -439
  46. package/src/components/hover-card-3d/index.tsx +0 -529
  47. package/src/components/index.ts +0 -130
  48. package/src/components/internal/index.ts +0 -78
  49. package/src/components/kanban/add-card-modal.tsx +0 -502
  50. package/src/components/kanban/card-detail-modal.tsx +0 -761
  51. package/src/components/kanban/index.ts +0 -13
  52. package/src/components/kanban/kanban.tsx +0 -1689
  53. package/src/components/kanban/types.ts +0 -168
  54. package/src/components/lazy-component/index.tsx +0 -823
  55. package/src/components/license-error/index.tsx +0 -31
  56. package/src/components/magnetic-button/index.tsx +0 -216
  57. package/src/components/memory-efficient-data/index.tsx +0 -1018
  58. package/src/components/moonui-quiz-form/index.tsx +0 -817
  59. package/src/components/navbar/index.tsx +0 -781
  60. package/src/components/optimized-image/index.tsx +0 -425
  61. package/src/components/performance-debugger/index.tsx +0 -613
  62. package/src/components/performance-monitor/index.tsx +0 -808
  63. package/src/components/phone-number-input/index.tsx +0 -343
  64. package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
  65. package/src/components/pinch-zoom/index.tsx +0 -566
  66. package/src/components/quiz-form/index.tsx +0 -479
  67. package/src/components/rich-text-editor/index.tsx +0 -2322
  68. package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
  69. package/src/components/rich-text-editor/slash-commands.css +0 -35
  70. package/src/components/rich-text-editor/table-styles.css +0 -65
  71. package/src/components/sidebar/index.tsx +0 -884
  72. package/src/components/spotlight-card/index.tsx +0 -191
  73. package/src/components/swipeable-card/index.tsx +0 -100
  74. package/src/components/timeline/index.tsx +0 -1183
  75. package/src/components/ui/accordion.tsx +0 -581
  76. package/src/components/ui/alert-dialog.tsx +0 -141
  77. package/src/components/ui/alert.tsx +0 -141
  78. package/src/components/ui/aspect-ratio.tsx +0 -245
  79. package/src/components/ui/avatar.tsx +0 -155
  80. package/src/components/ui/badge.tsx +0 -230
  81. package/src/components/ui/breadcrumb.tsx +0 -216
  82. package/src/components/ui/button.tsx +0 -228
  83. package/src/components/ui/calendar.tsx +0 -387
  84. package/src/components/ui/card.tsx +0 -216
  85. package/src/components/ui/checkbox.tsx +0 -259
  86. package/src/components/ui/collapsible.tsx +0 -631
  87. package/src/components/ui/color-picker.tsx +0 -97
  88. package/src/components/ui/command.tsx +0 -948
  89. package/src/components/ui/dialog.tsx +0 -752
  90. package/src/components/ui/dropdown-menu.tsx +0 -706
  91. package/src/components/ui/gesture-drawer.tsx +0 -11
  92. package/src/components/ui/hover-card.tsx +0 -29
  93. package/src/components/ui/index.ts +0 -222
  94. package/src/components/ui/input.tsx +0 -224
  95. package/src/components/ui/label.tsx +0 -29
  96. package/src/components/ui/lightbox.tsx +0 -606
  97. package/src/components/ui/magnetic-button.tsx +0 -129
  98. package/src/components/ui/media-gallery.tsx +0 -611
  99. package/src/components/ui/navigation-menu.tsx +0 -130
  100. package/src/components/ui/pagination.tsx +0 -125
  101. package/src/components/ui/popover.tsx +0 -185
  102. package/src/components/ui/progress.tsx +0 -30
  103. package/src/components/ui/radio-group.tsx +0 -257
  104. package/src/components/ui/scroll-area.tsx +0 -47
  105. package/src/components/ui/select.tsx +0 -378
  106. package/src/components/ui/separator.tsx +0 -145
  107. package/src/components/ui/sheet.tsx +0 -139
  108. package/src/components/ui/skeleton.tsx +0 -20
  109. package/src/components/ui/slider.tsx +0 -354
  110. package/src/components/ui/spotlight-card.tsx +0 -119
  111. package/src/components/ui/switch.tsx +0 -86
  112. package/src/components/ui/table.tsx +0 -331
  113. package/src/components/ui/tabs-pro.tsx +0 -542
  114. package/src/components/ui/tabs.tsx +0 -54
  115. package/src/components/ui/textarea.tsx +0 -28
  116. package/src/components/ui/toast.tsx +0 -317
  117. package/src/components/ui/toggle.tsx +0 -119
  118. package/src/components/ui/tooltip.tsx +0 -151
  119. package/src/components/virtual-list/index.tsx +0 -668
  120. package/src/hooks/use-chart.ts +0 -205
  121. package/src/hooks/use-data-table.ts +0 -182
  122. package/src/hooks/use-docs-pro-access.ts +0 -13
  123. package/src/hooks/use-license-check.ts +0 -65
  124. package/src/hooks/use-subscription.ts +0 -19
  125. package/src/hooks/use-toast.ts +0 -15
  126. package/src/index.ts +0 -22
  127. package/src/lib/ai-providers.ts +0 -377
  128. package/src/lib/component-metadata.ts +0 -18
  129. package/src/lib/micro-interactions.ts +0 -255
  130. package/src/lib/paddle.ts +0 -17
  131. package/src/lib/utils.ts +0 -6
  132. package/src/patterns/login-form/index.tsx +0 -276
  133. package/src/patterns/login-form/types.ts +0 -67
  134. package/src/setupTests.ts +0 -41
  135. package/src/styles/advanced-chart.css +0 -239
  136. package/src/styles/calendar.css +0 -35
  137. package/src/styles/design-system.css +0 -363
  138. package/src/styles/index.css +0 -681
  139. package/src/styles/tailwind.css +0 -7
  140. package/src/styles/tokens.css +0 -455
  141. package/src/types/next-auth.d.ts +0 -21
  142. package/src/use-intersection-observer.tsx +0 -154
  143. package/src/use-local-storage.tsx +0 -71
  144. package/src/use-paddle.ts +0 -138
  145. package/src/use-performance-optimizer.ts +0 -389
  146. package/src/use-pro-access.ts +0 -141
  147. package/src/use-scroll-animation.ts +0 -219
  148. package/src/use-subscription.ts +0 -37
  149. package/src/use-toast.ts +0 -32
  150. package/src/utils/chart-helpers.ts +0 -357
  151. package/src/utils/cn.ts +0 -6
  152. package/src/utils/data-processing.ts +0 -151
  153. package/src/utils/license-validator.tsx +0 -183
@@ -1,1183 +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-600',
207
- warning: 'bg-yellow-500 border-yellow-600',
208
- error: 'bg-red-500 border-red-600',
209
- info: 'bg-blue-500 border-blue-600',
210
- pending: 'bg-gray-400 border-gray-500',
211
- milestone: 'bg-purple-500 border-purple-600',
212
- custom: 'bg-slate-500 border-slate-600'
213
- }
214
-
215
- const DEFAULT_ICONS: Record<TimelineEventType, React.ReactNode> = {
216
- success: <CheckCircle2 className="h-5 w-5 text-white" />,
217
- warning: <AlertCircle className="h-5 w-5 text-white" />,
218
- error: <XCircle className="h-5 w-5 text-white" />,
219
- info: <Circle className="h-5 w-5 text-white" />,
220
- pending: <Clock className="h-5 w-5 text-white" />,
221
- milestone: <Flag className="h-5 w-5 text-white" />,
222
- custom: <Sparkles className="h-5 w-5 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
- if (layout === 'horizontal') {
544
- return (
545
- <div
546
- className={cn(
547
- "absolute h-1 bg-border",
548
- gradientConnectors ? "bg-gradient-to-r from-primary/60 via-primary/30 to-transparent" : "bg-border"
549
- )}
550
- style={{
551
- width: '100%',
552
- left: '100%',
553
- top: '50%',
554
- transform: 'translateY(-50%)'
555
- }}
556
- />
557
- )
558
- }
559
-
560
- const baseClasses = cn(
561
- "absolute w-1 left-1/2 -ml-0.5 transform -translate-x-1/2",
562
- gradientConnectors ? "bg-gradient-to-b from-primary/60 via-primary/30 to-transparent" : "bg-border"
563
- )
564
-
565
- return (
566
- <div className={cn(
567
- baseClasses,
568
- layout === 'alternating' && "left-1/2 -ml-0.5"
569
- )}
570
- style={{
571
- height: 'calc(100% + 3rem)',
572
- top: '2.5rem'
573
- }} />
574
- )
575
- }, [connectorRenderer, layout, gradientConnectors])
576
-
577
- const renderEvent = useCallback((event: TimelineEvent, index: number, isLast: boolean = false) => {
578
- const eventColor = event.color || colorScheme[event.type] || DEFAULT_COLORS[event.type]
579
- const textColor = TEXT_COLORS[event.type] || TEXT_COLORS.custom
580
- const isSelected = selectedEvents.has(event.id)
581
- const isExpanded = expandedEvents.has(event.id)
582
- const isFocused = focusedEventId === event.id
583
-
584
- // Custom renderer
585
- if (eventRenderer) {
586
- return eventRenderer(event, index)
587
- }
588
-
589
- // Check if event is a range event
590
- const isRangeEvent = 'endDate' in event
591
- const isNestedEvent = 'subEvents' in event
592
- const isMilestoneEvent = 'milestone' in event
593
-
594
- const eventContent = (
595
- <div
596
- ref={(el) => {
597
- if (el) eventRefs.current.set(event.id, el)
598
- }}
599
- tabIndex={keyboardNavigation ? 0 : -1}
600
- className={cn(
601
- "relative flex gap-6",
602
- compactMode ? "pb-4" : "pb-10",
603
- layout === 'alternating' && index % 2 === 0 && "flex-row-reverse",
604
- layout === 'horizontal' && "flex-col items-center pb-0 mr-8",
605
- "transition-all duration-200",
606
- isFocused && "scale-[1.02]",
607
- printMode && "print:break-inside-avoid"
608
- )}
609
- onFocus={() => setFocusedEventId(event.id)}
610
- role="button"
611
- aria-label={`Event: ${event.title}`}
612
- aria-expanded={isNestedEvent ? isExpanded : undefined}
613
- >
614
- {/* Timeline Node */}
615
- <div className={cn(
616
- "flex flex-col items-center relative",
617
- layout === 'horizontal' && "flex-row"
618
- )}>
619
- <motion.div
620
- className={cn(
621
- "flex items-center justify-center rounded-full border-4 bg-background z-10 shadow-lg relative",
622
- isMilestoneEvent ? "w-12 h-12" : "w-10 h-10",
623
- eventColor,
624
- isSelected && "ring-4 ring-ring ring-offset-2"
625
- )}
626
- whileHover={{ scale: 1.15 }}
627
- whileTap={{ scale: 0.95 }}
628
- >
629
- {renderEventIcon(event)}
630
-
631
- {/* Connector - Horizontal için node'un içinden çıkan çizgi */}
632
- {!isLast && layout === 'horizontal' && (
633
- <div
634
- className={cn(
635
- "absolute h-1",
636
- gradientConnectors ? "bg-gradient-to-r from-border via-border/60 to-border/30" : "bg-border"
637
- )}
638
- style={{
639
- width: '18.5rem', // Card genişliği (16rem) + margin (2rem) + node yarısı (0.5rem)
640
- left: isMilestoneEvent ? '3rem' : '2.5rem', // Node genişliğinin yarısı
641
- top: '50%',
642
- transform: 'translateY(-50%)',
643
- zIndex: -1
644
- }}
645
- />
646
- )}
647
- </motion.div>
648
-
649
- {/* Connector - Vertical layouts için */}
650
- {!isLast && layout !== 'horizontal' && renderConnector(event, events[index + 1], isLast)}
651
- </div>
652
-
653
- {/* Event Content */}
654
- <Card
655
- className={cn(
656
- layout === 'horizontal' ? "w-64" : "flex-1 min-w-0",
657
- "shadow-md hover:shadow-lg transition-shadow duration-200 cursor-pointer",
658
- layout === 'alternating' && index % 2 === 0 && "text-right",
659
- isSelected && "ring-2 ring-primary",
660
- "bg-card/50 backdrop-blur-sm"
661
- )}
662
- onClick={() => handleEventClick(event)}
663
- >
664
- <CardContent className="p-4">
665
- {/* Header */}
666
- <div className="flex items-start justify-between gap-2 mb-2">
667
- <div className="flex-1">
668
- <h4 className={cn(
669
- "font-semibold",
670
- compactMode ? "text-sm" : "text-base",
671
- isMilestoneEvent && "text-lg"
672
- )}>
673
- {event.title}
674
- {isMilestoneEvent && (
675
- <Badge variant="secondary" className="ml-2 bg-purple-500">
676
- Milestone
677
- </Badge>
678
- )}
679
- </h4>
680
-
681
- {/* Date/Time display */}
682
- <div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
683
- <Clock className="h-3 w-3" />
684
- {showRelativeTime ? (
685
- <span>{getRelativeTime(event.date)}</span>
686
- ) : (
687
- <span>{formatTime(event.date)}</span>
688
- )}
689
- {isRangeEvent && (
690
- <>
691
- <ArrowRight className="h-3 w-3" />
692
- <span>{formatTime((event as RangeEvent).endDate)}</span>
693
- </>
694
- )}
695
- </div>
696
- </div>
697
-
698
- {/* Actions */}
699
- {(editable || event.metadata?.externalLink) && (
700
- <DropdownMenu>
701
- <DropdownMenuTrigger asChild>
702
- <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
703
- <MoreVertical className="h-4 w-4" />
704
- </Button>
705
- </DropdownMenuTrigger>
706
- <DropdownMenuContent align="end">
707
- {editable && (
708
- <>
709
- <DropdownMenuItem onClick={() => onEventEdit?.(event)}>
710
- <Edit className="mr-2 h-4 w-4" />
711
- Edit
712
- </DropdownMenuItem>
713
- <DropdownMenuItem onClick={() => onEventDelete?.(event)}>
714
- <Trash className="mr-2 h-4 w-4" />
715
- Delete
716
- </DropdownMenuItem>
717
- <DropdownMenuSeparator />
718
- </>
719
- )}
720
- <DropdownMenuItem onClick={() => navigator.clipboard.writeText(event.title)}>
721
- <Copy className="mr-2 h-4 w-4" />
722
- Copy
723
- </DropdownMenuItem>
724
- <DropdownMenuItem>
725
- <Share className="mr-2 h-4 w-4" />
726
- Share
727
- </DropdownMenuItem>
728
- {event.metadata?.externalLink && (
729
- <DropdownMenuItem onClick={() => window.open(event.metadata?.externalLink, '_blank')}>
730
- <ExternalLink className="mr-2 h-4 w-4" />
731
- View Details
732
- </DropdownMenuItem>
733
- )}
734
- </DropdownMenuContent>
735
- </DropdownMenu>
736
- )}
737
- </div>
738
-
739
- {/* Description */}
740
- {event.description && (
741
- <p className={cn(
742
- "text-muted-foreground mb-2",
743
- compactMode ? "text-xs" : "text-sm"
744
- )}>
745
- {event.description}
746
- </p>
747
- )}
748
-
749
- {/* Milestone details */}
750
- {isMilestoneEvent && (event as MilestoneEvent).milestone && (
751
- <div className="bg-purple-50 dark:bg-purple-950/20 rounded-md p-3 mb-2">
752
- {(event as MilestoneEvent).milestone.target && (
753
- <div className="flex items-center gap-2 text-sm">
754
- <Target className="h-4 w-4 text-purple-600" />
755
- <span className="font-medium">Target:</span>
756
- <span>{(event as MilestoneEvent).milestone.target}</span>
757
- </div>
758
- )}
759
- {(event as MilestoneEvent).milestone.achievement && (
760
- <div className="flex items-center gap-2 text-sm mt-1">
761
- <TrendingUp className="h-4 w-4 text-purple-600" />
762
- <span className="font-medium">Achievement:</span>
763
- <span>{(event as MilestoneEvent).milestone.achievement}</span>
764
- </div>
765
- )}
766
- </div>
767
- )}
768
-
769
- {/* Progress bar for events with progress */}
770
- {event.metadata?.progress !== undefined && (
771
- <div className="mb-2">
772
- <div className="flex justify-between text-xs mb-1">
773
- <span>Progress</span>
774
- <span>{event.metadata.progress}%</span>
775
- </div>
776
- <div className="w-full bg-muted rounded-full h-2 overflow-hidden">
777
- <motion.div
778
- className="h-full bg-primary"
779
- initial={{ width: 0 }}
780
- animate={{ width: `${event.metadata.progress}%` }}
781
- transition={{ duration: 0.5, ease: "easeOut" }}
782
- />
783
- </div>
784
- </div>
785
- )}
786
-
787
- {/* User info */}
788
- {showUserInfo && event.user && (
789
- <div className="flex items-center gap-2 mb-2">
790
- <MoonUIAvatarPro className="h-6 w-6">
791
- <MoonUIAvatarImagePro src={event.user.avatar} />
792
- <MoonUIAvatarFallbackPro className="text-xs">
793
- {getInitials(event.user.name)}
794
- </MoonUIAvatarFallbackPro>
795
- </MoonUIAvatarPro>
796
- <div className="flex flex-col">
797
- <span className="text-sm font-medium">{event.user.name}</span>
798
- {event.user.role && (
799
- <span className="text-xs text-muted-foreground">{event.user.role}</span>
800
- )}
801
- </div>
802
- </div>
803
- )}
804
-
805
- {/* Metadata */}
806
- {showMetadata && event.metadata && (
807
- <div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
808
- {event.metadata.duration && (
809
- <div className="flex items-center gap-1">
810
- <Clock className="h-3 w-3" />
811
- <span>{event.metadata.duration}</span>
812
- </div>
813
- )}
814
- {event.metadata.location && (
815
- <div className="flex items-center gap-1">
816
- <Calendar className="h-3 w-3" />
817
- <span>{event.metadata.location}</span>
818
- </div>
819
- )}
820
- {event.metadata.comments !== undefined && event.metadata.comments > 0 && (
821
- <div className="flex items-center gap-1">
822
- <MessageCircle className="h-3 w-3" />
823
- <span>{event.metadata.comments}</span>
824
- </div>
825
- )}
826
- {event.metadata.attachments !== undefined && event.metadata.attachments > 0 && (
827
- <div className="flex items-center gap-1">
828
- <Paperclip className="h-3 w-3" />
829
- <span>{event.metadata.attachments}</span>
830
- </div>
831
- )}
832
- {event.metadata.priority && (
833
- <Badge
834
- variant={
835
- event.metadata.priority === 'high' ? 'destructive' :
836
- event.metadata.priority === 'medium' ? 'outline' :
837
- 'secondary'
838
- }
839
- className="text-xs"
840
- >
841
- {event.metadata.priority}
842
- </Badge>
843
- )}
844
- </div>
845
- )}
846
-
847
- {/* Tags */}
848
- {event.metadata?.tags && event.metadata.tags.length > 0 && (
849
- <div className="flex flex-wrap gap-1 mt-2">
850
- {event.metadata.tags.map((tag, tagIndex) => (
851
- <Badge key={tagIndex} variant="outline" className="text-xs">
852
- {tag}
853
- </Badge>
854
- ))}
855
- </div>
856
- )}
857
-
858
- {/* Nested events */}
859
- {isNestedEvent && isExpanded && (event as NestedEvent).subEvents && (
860
- <div className="mt-3 ml-4 pl-4 border-l-2 border-muted">
861
- {(event as NestedEvent).subEvents!.map((subEvent, subIndex) => (
862
- <div key={subEvent.id} className="mb-2 last:mb-0">
863
- <div className="flex items-start gap-2">
864
- <div className={cn(
865
- "w-2 h-2 rounded-full mt-1.5",
866
- colorScheme[subEvent.type] || DEFAULT_COLORS[subEvent.type]
867
- )} />
868
- <div className="flex-1">
869
- <h5 className="text-sm font-medium">{subEvent.title}</h5>
870
- {subEvent.description && (
871
- <p className="text-xs text-muted-foreground">{subEvent.description}</p>
872
- )}
873
- <span className="text-xs text-muted-foreground">
874
- {getRelativeTime(subEvent.date)}
875
- </span>
876
- </div>
877
- </div>
878
- </div>
879
- ))}
880
- </div>
881
- )}
882
-
883
- {/* Expand/Collapse button for nested events */}
884
- {isNestedEvent && (event as NestedEvent).subEvents && (event as NestedEvent).subEvents!.length > 0 && (
885
- <Button
886
- variant="ghost"
887
- size="sm"
888
- className="mt-2 h-6 text-xs"
889
- onClick={(e) => {
890
- e.stopPropagation()
891
- setExpandedEvents(prev => {
892
- const newSet = new Set(prev)
893
- if (newSet.has(event.id)) {
894
- newSet.delete(event.id)
895
- } else {
896
- newSet.add(event.id)
897
- }
898
- return newSet
899
- })
900
- }}
901
- >
902
- {isExpanded ? (
903
- <>
904
- <ChevronDown className="mr-1 h-3 w-3" />
905
- Hide {(event as NestedEvent).subEvents!.length} sub-events
906
- </>
907
- ) : (
908
- <>
909
- <ChevronRight className="mr-1 h-3 w-3" />
910
- Show {(event as NestedEvent).subEvents!.length} sub-events
911
- </>
912
- )}
913
- </Button>
914
- )}
915
- </CardContent>
916
- </Card>
917
- </div>
918
- )
919
-
920
- if (draggable && onEventsReorder) {
921
- return (
922
- <Reorder.Item
923
- key={event.id}
924
- value={event}
925
- dragListener={false}
926
- dragControls={undefined}
927
- >
928
- {eventContent}
929
- </Reorder.Item>
930
- )
931
- }
932
-
933
- const variants = animation !== 'none' ? animationVariants[animation] : undefined
934
-
935
- return (
936
- <motion.div
937
- key={event.id}
938
- variants={variants}
939
- initial={variants?.initial}
940
- animate={variants?.animate}
941
- exit={variants?.exit}
942
- transition={{ duration: 0.3, delay: index * 0.05 }}
943
- >
944
- {eventContent}
945
- </motion.div>
946
- )
947
- }, [
948
- eventRenderer,
949
- colorScheme,
950
- selectedEvents,
951
- expandedEvents,
952
- focusedEventId,
953
- keyboardNavigation,
954
- layout,
955
- compactMode,
956
- printMode,
957
- handleEventClick,
958
- renderEventIcon,
959
- renderConnector,
960
- events,
961
- showRelativeTime,
962
- getRelativeTime,
963
- formatTime,
964
- editable,
965
- onEventEdit,
966
- onEventDelete,
967
- showUserInfo,
968
- getInitials,
969
- showMetadata,
970
- draggable,
971
- onEventsReorder,
972
- animation
973
- ])
974
-
975
- const renderGroupedEvents = useCallback(() => {
976
- const groups = groupEventsByDate(filteredAndSortedEvents)
977
-
978
- return Object.entries(groups).map(([dateGroup, groupEvents], groupIndex) => {
979
- const displayEvents = maxEventsPerGroup
980
- ? groupEvents.slice(0, maxEventsPerGroup)
981
- : groupEvents
982
- const remainingCount = groupEvents.length - displayEvents.length
983
-
984
- return (
985
- <div key={dateGroup} className="mb-8">
986
- <div className={cn(
987
- "sticky top-0 z-20 bg-background/80 backdrop-blur-sm border-b p-3 mb-4 -mx-6 px-6",
988
- printMode && "print:relative print:bg-transparent"
989
- )}>
990
- <h3 className="font-semibold text-sm text-muted-foreground flex items-center justify-between">
991
- <span>{dateGroup}</span>
992
- <Badge variant="secondary" className="text-xs">
993
- {groupEvents.length} event{groupEvents.length !== 1 ? 's' : ''}
994
- </Badge>
995
- </h3>
996
- </div>
997
-
998
- <AnimatePresence mode="popLayout">
999
- {displayEvents.map((event, index) =>
1000
- renderEvent(event, index, index === displayEvents.length - 1)
1001
- )}
1002
- </AnimatePresence>
1003
-
1004
- {remainingCount > 0 && (
1005
- <Button
1006
- variant="ghost"
1007
- size="sm"
1008
- className="w-full mt-2"
1009
- onClick={() => {
1010
- // In a real implementation, this would expand the group
1011
- console.log(`Show ${remainingCount} more events`)
1012
- }}
1013
- >
1014
- Show {remainingCount} more event{remainingCount !== 1 ? 's' : ''}
1015
- </Button>
1016
- )}
1017
- </div>
1018
- )
1019
- })
1020
- }, [groupEventsByDate, filteredAndSortedEvents, maxEventsPerGroup, printMode, renderEvent])
1021
-
1022
- const renderTimeline = () => {
1023
- const content = groupBy !== 'none'
1024
- ? renderGroupedEvents()
1025
- : (
1026
- <AnimatePresence mode="popLayout">
1027
- {draggable && onEventsReorder ? (
1028
- <Reorder.Group
1029
- axis={layout === 'horizontal' ? 'x' : 'y'}
1030
- values={filteredAndSortedEvents}
1031
- onReorder={(newOrder) => {
1032
- setEvents(newOrder)
1033
- onEventsReorder(newOrder)
1034
- }}
1035
- className={cn(
1036
- "space-y-0",
1037
- layout === 'horizontal' && "flex overflow-x-auto gap-6 pb-4"
1038
- )}
1039
- >
1040
- {filteredAndSortedEvents.map((event, index) =>
1041
- renderEvent(event, index, index === filteredAndSortedEvents.length - 1)
1042
- )}
1043
- </Reorder.Group>
1044
- ) : (
1045
- <div className={cn(
1046
- "space-y-0",
1047
- layout === 'horizontal' && "flex flex-row overflow-x-auto gap-0 pb-4 items-start",
1048
- layout === 'alternating' && "relative"
1049
- )}>
1050
- {/* Center line for alternating layout */}
1051
- {layout === 'alternating' && (
1052
- <div className="absolute left-1/2 -ml-0.5 top-0 bottom-0 w-1 bg-border" />
1053
- )}
1054
- {filteredAndSortedEvents.map((event, index) =>
1055
- renderEvent(event, index, index === filteredAndSortedEvents.length - 1)
1056
- )}
1057
- </div>
1058
- )}
1059
- </AnimatePresence>
1060
- )
1061
-
1062
- if (virtualScroll && filteredAndSortedEvents.length > 50) {
1063
- // In a real implementation, use a virtual scrolling library
1064
- return (
1065
- <div className="h-[600px] overflow-y-auto">
1066
- {content}
1067
- </div>
1068
- )
1069
- }
1070
-
1071
- return content
1072
- }
1073
-
1074
- return (
1075
- <Card
1076
- className={cn(
1077
- timelineVariants({ theme }),
1078
- "relative",
1079
- printMode && "print:shadow-none print:border-0",
1080
- className
1081
- )}
1082
- ref={timelineRef}
1083
- role="region"
1084
- aria-label={ariaLabel}
1085
- {...props}
1086
- >
1087
- {(showSearch || showFilter || showExport) && (
1088
- <CardHeader className={cn(
1089
- compactMode && "pb-3",
1090
- printMode && "print:pb-2"
1091
- )}>
1092
- <div className="flex items-center justify-between">
1093
- <div>
1094
- <CardDescription>
1095
- {filteredAndSortedEvents.length} event{filteredAndSortedEvents.length !== 1 ? 's' : ''}
1096
- {searchQuery && ` matching "${searchQuery}"`}
1097
- {filterType !== 'all' && ` of type ${filterType}`}
1098
- </CardDescription>
1099
- </div>
1100
-
1101
- {/* Actions */}
1102
- <div className="flex items-center gap-2">
1103
- {showExport && (
1104
- <Button
1105
- variant="ghost"
1106
- size="sm"
1107
- onClick={handleExport}
1108
- className="print:hidden"
1109
- >
1110
- <Download className="h-4 w-4" />
1111
- </Button>
1112
- )}
1113
- <Button
1114
- variant="ghost"
1115
- size="sm"
1116
- onClick={handlePrint}
1117
- className="print:hidden"
1118
- >
1119
- <Printer className="h-4 w-4" />
1120
- </Button>
1121
- </div>
1122
- </div>
1123
-
1124
- {/* Search and Filter */}
1125
- {(showSearch || showFilter) && (
1126
- <div className="flex gap-2 mt-4 print:hidden">
1127
- {showSearch && (
1128
- <div className="relative flex-1">
1129
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
1130
- <Input
1131
- type="search"
1132
- placeholder="Search events..."
1133
- value={searchQuery}
1134
- onChange={(e) => setSearchQuery(e.target.value)}
1135
- className="pl-9"
1136
- />
1137
- </div>
1138
- )}
1139
-
1140
- {showFilter && (
1141
- <Select value={filterType} onValueChange={(value: any) => setFilterType(value)}>
1142
- <SelectTrigger className="w-[150px]">
1143
- <Filter className="mr-2 h-4 w-4" />
1144
- <SelectValue placeholder="Filter by type" />
1145
- </SelectTrigger>
1146
- <SelectContent>
1147
- <SelectItem value="all">All Types</SelectItem>
1148
- <SelectItem value="success">Success</SelectItem>
1149
- <SelectItem value="warning">Warning</SelectItem>
1150
- <SelectItem value="error">Error</SelectItem>
1151
- <SelectItem value="info">Info</SelectItem>
1152
- <SelectItem value="pending">Pending</SelectItem>
1153
- <SelectItem value="milestone">Milestone</SelectItem>
1154
- </SelectContent>
1155
- </Select>
1156
- )}
1157
- </div>
1158
- )}
1159
- </CardHeader>
1160
- )}
1161
-
1162
- <CardContent className={cn(
1163
- compactMode ? "pt-2" : "pt-6",
1164
- "relative"
1165
- )}>
1166
- {filteredAndSortedEvents.length > 0 ? (
1167
- renderTimeline()
1168
- ) : (
1169
- <div className="text-center py-12">
1170
- <Clock className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
1171
- <p className="text-muted-foreground">
1172
- {searchQuery || filterType !== 'all'
1173
- ? "No events match your filters"
1174
- : "No events to display"}
1175
- </p>
1176
- </div>
1177
- )}
1178
- </CardContent>
1179
- </Card>
1180
- )
1181
- }
1182
-
1183
- export default Timeline