@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.
- package/package.json +8 -3
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postinstall.js +191 -23
- package/src/components/advanced-chart/index.tsx +0 -1246
- package/src/components/advanced-forms/index.tsx +0 -585
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -377
- package/src/components/calendar/index.tsx +0 -1220
- 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 -480
- 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 -225
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
- package/src/components/dashboard/widgets/index.ts +0 -5
- package/src/components/dashboard/widgets/metric-card.tsx +0 -363
- package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
- 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/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/error-boundary/index.tsx +0 -232
- 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 -335
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
- 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 -77
- 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 -517
- 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 -515
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -529
- package/src/components/index.ts +0 -130
- 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 -1689
- 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 -31
- package/src/components/magnetic-button/index.tsx +0 -216
- package/src/components/memory-efficient-data/index.tsx +0 -1018
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/navbar/index.tsx +0 -781
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -613
- package/src/components/performance-monitor/index.tsx +0 -808
- package/src/components/phone-number-input/index.tsx +0 -343
- package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
- 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.tsx +0 -2322
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
- 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 -884
- 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 -1183
- package/src/components/ui/accordion.tsx +0 -581
- 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 -155
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/breadcrumb.tsx +0 -216
- package/src/components/ui/button.tsx +0 -228
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -216
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -631
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -948
- package/src/components/ui/dialog.tsx +0 -752
- package/src/components/ui/dropdown-menu.tsx +0 -706
- 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 -222
- package/src/components/ui/input.tsx +0 -224
- 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 -611
- package/src/components/ui/navigation-menu.tsx +0 -130
- package/src/components/ui/pagination.tsx +0 -125
- 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 -378
- 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 -331
- package/src/components/ui/tabs-pro.tsx +0 -542
- package/src/components/ui/tabs.tsx +0 -54
- 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 -22
- 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 -681
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- 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,668 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
|
4
|
-
import { motion, AnimatePresence } from 'framer-motion'
|
|
5
|
-
import { cn } from '../../lib/utils'
|
|
6
|
-
|
|
7
|
-
// Virtual List Props
|
|
8
|
-
export interface VirtualListProps<T = any> {
|
|
9
|
-
items: T[]
|
|
10
|
-
height: number
|
|
11
|
-
itemHeight?: number
|
|
12
|
-
estimatedItemHeight?: number
|
|
13
|
-
variableHeight?: boolean
|
|
14
|
-
overscan?: number
|
|
15
|
-
renderItem: (item: T, index: number) => React.ReactNode
|
|
16
|
-
onScroll?: (scrollTop: number) => void
|
|
17
|
-
className?: string
|
|
18
|
-
// Infinite scrolling props
|
|
19
|
-
hasNextPage?: boolean
|
|
20
|
-
isLoadingNextPage?: boolean
|
|
21
|
-
onLoadMore?: () => void | Promise<void>
|
|
22
|
-
loadMoreThreshold?: number
|
|
23
|
-
renderLoader?: () => React.ReactNode
|
|
24
|
-
renderEndMessage?: () => React.ReactNode
|
|
25
|
-
// Animation props
|
|
26
|
-
enableAnimations?: boolean
|
|
27
|
-
itemEnterAnimation?: {
|
|
28
|
-
initial?: any
|
|
29
|
-
animate?: any
|
|
30
|
-
exit?: any
|
|
31
|
-
transition?: any
|
|
32
|
-
}
|
|
33
|
-
listAnimation?: {
|
|
34
|
-
initial?: any
|
|
35
|
-
animate?: any
|
|
36
|
-
transition?: any
|
|
37
|
-
}
|
|
38
|
-
staggerDelay?: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Item bilgilerini saklamak için
|
|
42
|
-
interface ItemInfo {
|
|
43
|
-
index: number
|
|
44
|
-
height: number
|
|
45
|
-
top: number
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Performans optimize edilmiş Virtual List Component
|
|
49
|
-
export function VirtualList<T = any>({
|
|
50
|
-
items,
|
|
51
|
-
height,
|
|
52
|
-
itemHeight = 50,
|
|
53
|
-
estimatedItemHeight = 50,
|
|
54
|
-
variableHeight = false,
|
|
55
|
-
overscan = 5,
|
|
56
|
-
renderItem,
|
|
57
|
-
onScroll,
|
|
58
|
-
className,
|
|
59
|
-
// Infinite scrolling props
|
|
60
|
-
hasNextPage = false,
|
|
61
|
-
isLoadingNextPage = false,
|
|
62
|
-
onLoadMore,
|
|
63
|
-
loadMoreThreshold = 500,
|
|
64
|
-
renderLoader,
|
|
65
|
-
renderEndMessage,
|
|
66
|
-
// Animation props
|
|
67
|
-
enableAnimations = false,
|
|
68
|
-
itemEnterAnimation = {
|
|
69
|
-
initial: { opacity: 0, y: 20 },
|
|
70
|
-
animate: { opacity: 1, y: 0 },
|
|
71
|
-
exit: { opacity: 0, y: -20 },
|
|
72
|
-
transition: { duration: 0.2 }
|
|
73
|
-
},
|
|
74
|
-
listAnimation = {
|
|
75
|
-
initial: { opacity: 0 },
|
|
76
|
-
animate: { opacity: 1 },
|
|
77
|
-
transition: { duration: 0.3 }
|
|
78
|
-
},
|
|
79
|
-
staggerDelay = 0.05
|
|
80
|
-
}: VirtualListProps<T>) {
|
|
81
|
-
// State yönetimi
|
|
82
|
-
const [scrollTop, setScrollTop] = useState(0)
|
|
83
|
-
const [isScrolling, setIsScrolling] = useState(false)
|
|
84
|
-
const [itemHeights, setItemHeights] = useState<Map<number, number>>(new Map())
|
|
85
|
-
|
|
86
|
-
// Refs
|
|
87
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
88
|
-
const scrollElementRef = useRef<HTMLDivElement>(null)
|
|
89
|
-
const scrollTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
|
90
|
-
const isScrollingTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
|
91
|
-
|
|
92
|
-
// Container dimensions tracking
|
|
93
|
-
const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 })
|
|
94
|
-
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
if (!containerRef.current) return
|
|
97
|
-
|
|
98
|
-
const resizeObserver = new ResizeObserver((entries) => {
|
|
99
|
-
const entry = entries[0]
|
|
100
|
-
if (entry) {
|
|
101
|
-
setContainerDimensions({
|
|
102
|
-
width: entry.contentRect.width,
|
|
103
|
-
height: entry.contentRect.height
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
resizeObserver.observe(containerRef.current)
|
|
109
|
-
return () => resizeObserver.disconnect()
|
|
110
|
-
}, [])
|
|
111
|
-
|
|
112
|
-
// Item pozisyonlarını hesapla
|
|
113
|
-
const itemPositions = useMemo(() => {
|
|
114
|
-
|
|
115
|
-
const positions: ItemInfo[] = []
|
|
116
|
-
let currentTop = 0
|
|
117
|
-
let currentLeft = 0
|
|
118
|
-
|
|
119
|
-
for (let i = 0; i < items.length; i++) {
|
|
120
|
-
const currentHeight = variableHeight
|
|
121
|
-
? (itemHeights.get(i) || estimatedItemHeight)
|
|
122
|
-
: itemHeight
|
|
123
|
-
|
|
124
|
-
positions.push({
|
|
125
|
-
index: i,
|
|
126
|
-
height: currentHeight,
|
|
127
|
-
top: currentTop
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
currentTop += currentHeight
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return positions
|
|
134
|
-
}, [
|
|
135
|
-
items.length,
|
|
136
|
-
itemHeight,
|
|
137
|
-
estimatedItemHeight,
|
|
138
|
-
variableHeight,
|
|
139
|
-
itemHeights
|
|
140
|
-
])
|
|
141
|
-
|
|
142
|
-
// Toplam içerik yüksekliği
|
|
143
|
-
const totalHeight = useMemo(() => {
|
|
144
|
-
let baseHeight = itemPositions.length > 0
|
|
145
|
-
? itemPositions[itemPositions.length - 1].top + itemPositions[itemPositions.length - 1].height
|
|
146
|
-
: 0
|
|
147
|
-
|
|
148
|
-
// Infinite scrolling için ek yükseklik ekle
|
|
149
|
-
if (hasNextPage && isLoadingNextPage) {
|
|
150
|
-
baseHeight += 50 // Loader yüksekliği
|
|
151
|
-
} else if (!hasNextPage && items.length > 0) {
|
|
152
|
-
baseHeight += 40 // End message yüksekliği
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return baseHeight
|
|
156
|
-
}, [itemPositions, hasNextPage, isLoadingNextPage, items.length])
|
|
157
|
-
|
|
158
|
-
// Görünür aralığı hesapla
|
|
159
|
-
const visibleRange = useMemo(() => {
|
|
160
|
-
const start = itemPositions.findIndex(item => item.top + item.height >= scrollTop)
|
|
161
|
-
const end = itemPositions.findIndex(item => item.top > scrollTop + height)
|
|
162
|
-
|
|
163
|
-
const actualStart = Math.max(0, start - overscan)
|
|
164
|
-
const actualEnd = end === -1
|
|
165
|
-
? Math.min(items.length - 1, start + Math.ceil(height / itemHeight) + overscan)
|
|
166
|
-
: Math.min(items.length - 1, end + overscan)
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
start: actualStart,
|
|
170
|
-
end: actualEnd
|
|
171
|
-
}
|
|
172
|
-
}, [scrollTop, height, itemPositions, overscan, items.length, itemHeight])
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Görünür itemlar
|
|
176
|
-
const visibleItems = useMemo(() => {
|
|
177
|
-
const result: Array<{ item: T; index: number; style: React.CSSProperties }> = []
|
|
178
|
-
|
|
179
|
-
for (let i = visibleRange.start; i <= visibleRange.end; i++) {
|
|
180
|
-
if (i >= 0 && i < items.length && itemPositions[i]) {
|
|
181
|
-
const position = itemPositions[i]
|
|
182
|
-
result.push({
|
|
183
|
-
item: items[i],
|
|
184
|
-
index: i,
|
|
185
|
-
style: {
|
|
186
|
-
position: 'absolute',
|
|
187
|
-
top: position.top,
|
|
188
|
-
left: 0,
|
|
189
|
-
right: 0,
|
|
190
|
-
height: variableHeight ? 'auto' : position.height,
|
|
191
|
-
minHeight: variableHeight ? position.height : undefined
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return result
|
|
198
|
-
}, [items, visibleRange, itemPositions, variableHeight])
|
|
199
|
-
|
|
200
|
-
// Scroll handler
|
|
201
|
-
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
202
|
-
const scrollTop = e.currentTarget.scrollTop
|
|
203
|
-
const scrollHeight = e.currentTarget.scrollHeight
|
|
204
|
-
const clientHeight = e.currentTarget.clientHeight
|
|
205
|
-
|
|
206
|
-
setScrollTop(scrollTop)
|
|
207
|
-
setIsScrolling(true)
|
|
208
|
-
|
|
209
|
-
// Scroll callback
|
|
210
|
-
if (onScroll) {
|
|
211
|
-
onScroll(scrollTop)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Infinite scrolling check
|
|
215
|
-
if (
|
|
216
|
-
hasNextPage &&
|
|
217
|
-
!isLoadingNextPage &&
|
|
218
|
-
onLoadMore &&
|
|
219
|
-
scrollHeight - (scrollTop + clientHeight) <= loadMoreThreshold
|
|
220
|
-
) {
|
|
221
|
-
onLoadMore()
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Scroll timeout'ını temizle ve yenisini ayarla
|
|
225
|
-
if (scrollTimeoutRef.current) {
|
|
226
|
-
clearTimeout(scrollTimeoutRef.current)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (isScrollingTimeoutRef.current) {
|
|
230
|
-
clearTimeout(isScrollingTimeoutRef.current)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 150ms sonra scrolling'i false yap (performans için)
|
|
234
|
-
isScrollingTimeoutRef.current = setTimeout(() => {
|
|
235
|
-
setIsScrolling(false)
|
|
236
|
-
}, 150)
|
|
237
|
-
}, [onScroll, hasNextPage, isLoadingNextPage, onLoadMore, loadMoreThreshold])
|
|
238
|
-
|
|
239
|
-
// Variable height için resize observer
|
|
240
|
-
useEffect(() => {
|
|
241
|
-
if (!variableHeight || !containerRef.current) return
|
|
242
|
-
|
|
243
|
-
const resizeObserver = new ResizeObserver((entries) => {
|
|
244
|
-
const newHeights = new Map(itemHeights)
|
|
245
|
-
let hasChanges = false
|
|
246
|
-
|
|
247
|
-
entries.forEach((entry) => {
|
|
248
|
-
const element = entry.target as HTMLElement
|
|
249
|
-
const indexAttr = element.getAttribute('data-index')
|
|
250
|
-
|
|
251
|
-
if (indexAttr) {
|
|
252
|
-
const index = parseInt(indexAttr, 10)
|
|
253
|
-
const newHeight = entry.contentRect.height
|
|
254
|
-
|
|
255
|
-
if (newHeights.get(index) !== newHeight) {
|
|
256
|
-
newHeights.set(index, newHeight)
|
|
257
|
-
hasChanges = true
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
if (hasChanges) {
|
|
263
|
-
setItemHeights(newHeights)
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
// Mevcut tüm item elementlerini observe et
|
|
268
|
-
const itemElements = containerRef.current.querySelectorAll('[data-index]')
|
|
269
|
-
itemElements.forEach(el => resizeObserver.observe(el))
|
|
270
|
-
|
|
271
|
-
return () => resizeObserver.disconnect()
|
|
272
|
-
}, [variableHeight, itemHeights, visibleItems])
|
|
273
|
-
|
|
274
|
-
// Keyboard navigation desteği
|
|
275
|
-
useEffect(() => {
|
|
276
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
277
|
-
if (!containerRef.current) return
|
|
278
|
-
|
|
279
|
-
switch (e.key) {
|
|
280
|
-
case 'ArrowUp':
|
|
281
|
-
e.preventDefault()
|
|
282
|
-
scrollElementRef.current?.scrollBy({ top: -itemHeight, behavior: 'smooth' })
|
|
283
|
-
break
|
|
284
|
-
case 'ArrowDown':
|
|
285
|
-
e.preventDefault()
|
|
286
|
-
scrollElementRef.current?.scrollBy({ top: itemHeight, behavior: 'smooth' })
|
|
287
|
-
break
|
|
288
|
-
case 'PageUp':
|
|
289
|
-
e.preventDefault()
|
|
290
|
-
scrollElementRef.current?.scrollBy({ top: -height, behavior: 'smooth' })
|
|
291
|
-
break
|
|
292
|
-
case 'PageDown':
|
|
293
|
-
e.preventDefault()
|
|
294
|
-
scrollElementRef.current?.scrollBy({ top: height, behavior: 'smooth' })
|
|
295
|
-
break
|
|
296
|
-
case 'Home':
|
|
297
|
-
e.preventDefault()
|
|
298
|
-
scrollElementRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
299
|
-
break
|
|
300
|
-
case 'End':
|
|
301
|
-
e.preventDefault()
|
|
302
|
-
scrollElementRef.current?.scrollTo({ top: totalHeight, behavior: 'smooth' })
|
|
303
|
-
break
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const container = containerRef.current
|
|
308
|
-
if (container) {
|
|
309
|
-
container.addEventListener('keydown', handleKeyDown)
|
|
310
|
-
return () => container.removeEventListener('keydown', handleKeyDown)
|
|
311
|
-
}
|
|
312
|
-
}, [itemHeight, height, totalHeight])
|
|
313
|
-
|
|
314
|
-
const ContainerComponent = enableAnimations ? motion.div : 'div'
|
|
315
|
-
const ContentComponent = enableAnimations ? motion.div : 'div'
|
|
316
|
-
const ItemComponent = enableAnimations ? motion.div : 'div'
|
|
317
|
-
|
|
318
|
-
return (
|
|
319
|
-
<ContainerComponent
|
|
320
|
-
ref={containerRef}
|
|
321
|
-
className={cn(
|
|
322
|
-
"relative overflow-hidden border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-ring",
|
|
323
|
-
className
|
|
324
|
-
)}
|
|
325
|
-
style={{ height }}
|
|
326
|
-
tabIndex={0}
|
|
327
|
-
role="listbox"
|
|
328
|
-
aria-label={`Virtual list with ${items.length} items`}
|
|
329
|
-
{...(enableAnimations && listAnimation)}
|
|
330
|
-
>
|
|
331
|
-
<div
|
|
332
|
-
ref={scrollElementRef}
|
|
333
|
-
className="h-full overflow-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent"
|
|
334
|
-
onScroll={handleScroll}
|
|
335
|
-
>
|
|
336
|
-
<ContentComponent
|
|
337
|
-
style={{
|
|
338
|
-
height: totalHeight,
|
|
339
|
-
position: 'relative'
|
|
340
|
-
}}
|
|
341
|
-
{...(enableAnimations && {
|
|
342
|
-
initial: { opacity: 0 },
|
|
343
|
-
animate: { opacity: 1 },
|
|
344
|
-
transition: { duration: 0.2 }
|
|
345
|
-
})}
|
|
346
|
-
>
|
|
347
|
-
<AnimatePresence mode="popLayout">
|
|
348
|
-
{visibleItems.map(({ item, index, style }, itemIndex) => (
|
|
349
|
-
<ItemComponent
|
|
350
|
-
key={`${index}-${JSON.stringify(item)}`}
|
|
351
|
-
data-index={index}
|
|
352
|
-
style={style}
|
|
353
|
-
role="option"
|
|
354
|
-
aria-selected="false"
|
|
355
|
-
className="outline-none"
|
|
356
|
-
{...(enableAnimations && {
|
|
357
|
-
...itemEnterAnimation,
|
|
358
|
-
transition: {
|
|
359
|
-
...itemEnterAnimation.transition,
|
|
360
|
-
delay: itemIndex * staggerDelay
|
|
361
|
-
}
|
|
362
|
-
})}
|
|
363
|
-
>
|
|
364
|
-
{renderItem(item, index)}
|
|
365
|
-
</ItemComponent>
|
|
366
|
-
))}
|
|
367
|
-
</AnimatePresence>
|
|
368
|
-
|
|
369
|
-
{/* Infinite scrolling loader */}
|
|
370
|
-
{hasNextPage && isLoadingNextPage && (
|
|
371
|
-
<motion.div
|
|
372
|
-
style={{
|
|
373
|
-
position: 'absolute',
|
|
374
|
-
top: totalHeight,
|
|
375
|
-
left: 0,
|
|
376
|
-
right: 0,
|
|
377
|
-
minHeight: 50
|
|
378
|
-
}}
|
|
379
|
-
className="flex items-center justify-center py-4"
|
|
380
|
-
initial={{ opacity: 0, y: 20 }}
|
|
381
|
-
animate={{ opacity: 1, y: 0 }}
|
|
382
|
-
exit={{ opacity: 0, y: -20 }}
|
|
383
|
-
transition={{ duration: 0.3 }}
|
|
384
|
-
>
|
|
385
|
-
{renderLoader ? renderLoader() : (
|
|
386
|
-
<div className="flex items-center gap-2 text-muted-foreground">
|
|
387
|
-
<motion.div
|
|
388
|
-
className="rounded-full h-4 w-4 border-2 border-primary border-t-transparent"
|
|
389
|
-
animate={{ rotate: 360 }}
|
|
390
|
-
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
|
391
|
-
/>
|
|
392
|
-
<span className="text-sm">Loading more...</span>
|
|
393
|
-
</div>
|
|
394
|
-
)}
|
|
395
|
-
</motion.div>
|
|
396
|
-
)}
|
|
397
|
-
|
|
398
|
-
{/* End message */}
|
|
399
|
-
{!hasNextPage && items.length > 0 && (
|
|
400
|
-
<motion.div
|
|
401
|
-
style={{
|
|
402
|
-
position: 'absolute',
|
|
403
|
-
top: totalHeight,
|
|
404
|
-
left: 0,
|
|
405
|
-
right: 0,
|
|
406
|
-
minHeight: 40
|
|
407
|
-
}}
|
|
408
|
-
className="flex items-center justify-center py-3"
|
|
409
|
-
initial={{ opacity: 0, scale: 0.9 }}
|
|
410
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
411
|
-
transition={{ duration: 0.2 }}
|
|
412
|
-
>
|
|
413
|
-
{renderEndMessage ? renderEndMessage() : (
|
|
414
|
-
<div className="text-sm text-muted-foreground">
|
|
415
|
-
No more items to load
|
|
416
|
-
</div>
|
|
417
|
-
)}
|
|
418
|
-
</motion.div>
|
|
419
|
-
)}
|
|
420
|
-
</ContentComponent>
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
{/* Loading indicator for scrolling */}
|
|
424
|
-
<AnimatePresence>
|
|
425
|
-
{isScrolling && (
|
|
426
|
-
<motion.div
|
|
427
|
-
className="absolute top-2 right-2 bg-muted/80 text-muted-foreground px-2 py-1 rounded text-xs"
|
|
428
|
-
initial={{ opacity: 0, scale: 0.8 }}
|
|
429
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
430
|
-
exit={{ opacity: 0, scale: 0.8 }}
|
|
431
|
-
transition={{ duration: 0.15 }}
|
|
432
|
-
>
|
|
433
|
-
Scrolling...
|
|
434
|
-
</motion.div>
|
|
435
|
-
)}
|
|
436
|
-
</AnimatePresence>
|
|
437
|
-
|
|
438
|
-
{/* Scroll position indicator */}
|
|
439
|
-
{items.length > 100 && (
|
|
440
|
-
<motion.div
|
|
441
|
-
className="absolute bottom-2 right-2 bg-muted/80 text-muted-foreground px-2 py-1 rounded text-xs"
|
|
442
|
-
initial={{ opacity: 0, y: 10 }}
|
|
443
|
-
animate={{ opacity: 1, y: 0 }}
|
|
444
|
-
transition={{ duration: 0.2, delay: 0.5 }}
|
|
445
|
-
>
|
|
446
|
-
{Math.round((scrollTop / Math.max(totalHeight - height, 1)) * 100)}%
|
|
447
|
-
</motion.div>
|
|
448
|
-
)}
|
|
449
|
-
|
|
450
|
-
</ContainerComponent>
|
|
451
|
-
)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
// Utility hook for virtual list state management
|
|
456
|
-
export function useVirtualList<T>(
|
|
457
|
-
items: T[],
|
|
458
|
-
initialConfig: Partial<VirtualListProps<T>> = {}
|
|
459
|
-
) {
|
|
460
|
-
const [config, setConfig] = useState(initialConfig)
|
|
461
|
-
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set())
|
|
462
|
-
const [focusedIndex, setFocusedIndex] = useState<number>(-1)
|
|
463
|
-
|
|
464
|
-
const updateConfig = useCallback((newConfig: Partial<VirtualListProps<T>>) => {
|
|
465
|
-
setConfig(prev => ({ ...prev, ...newConfig }))
|
|
466
|
-
}, [])
|
|
467
|
-
|
|
468
|
-
const selectItem = useCallback((index: number) => {
|
|
469
|
-
setSelectedItems(prev => new Set([...prev, index]))
|
|
470
|
-
}, [])
|
|
471
|
-
|
|
472
|
-
const deselectItem = useCallback((index: number) => {
|
|
473
|
-
setSelectedItems(prev => {
|
|
474
|
-
const newSet = new Set(prev)
|
|
475
|
-
newSet.delete(index)
|
|
476
|
-
return newSet
|
|
477
|
-
})
|
|
478
|
-
}, [])
|
|
479
|
-
|
|
480
|
-
const toggleItem = useCallback((index: number) => {
|
|
481
|
-
setSelectedItems(prev => {
|
|
482
|
-
const newSet = new Set(prev)
|
|
483
|
-
if (newSet.has(index)) {
|
|
484
|
-
newSet.delete(index)
|
|
485
|
-
} else {
|
|
486
|
-
newSet.add(index)
|
|
487
|
-
}
|
|
488
|
-
return newSet
|
|
489
|
-
})
|
|
490
|
-
}, [])
|
|
491
|
-
|
|
492
|
-
const selectRange = useCallback((startIndex: number, endIndex: number) => {
|
|
493
|
-
setSelectedItems(prev => {
|
|
494
|
-
const newSet = new Set(prev)
|
|
495
|
-
const start = Math.min(startIndex, endIndex)
|
|
496
|
-
const end = Math.max(startIndex, endIndex)
|
|
497
|
-
|
|
498
|
-
for (let i = start; i <= end; i++) {
|
|
499
|
-
newSet.add(i)
|
|
500
|
-
}
|
|
501
|
-
return newSet
|
|
502
|
-
})
|
|
503
|
-
}, [])
|
|
504
|
-
|
|
505
|
-
const selectAll = useCallback(() => {
|
|
506
|
-
setSelectedItems(new Set(Array.from({ length: items.length }, (_, i) => i)))
|
|
507
|
-
}, [items.length])
|
|
508
|
-
|
|
509
|
-
const clearSelection = useCallback(() => {
|
|
510
|
-
setSelectedItems(new Set())
|
|
511
|
-
}, [])
|
|
512
|
-
|
|
513
|
-
const isSelected = useCallback((index: number) => {
|
|
514
|
-
return selectedItems.has(index)
|
|
515
|
-
}, [selectedItems])
|
|
516
|
-
|
|
517
|
-
const getSelectedItems = useCallback(() => {
|
|
518
|
-
return Array.from(selectedItems).map(index => items[index]).filter(Boolean)
|
|
519
|
-
}, [selectedItems, items])
|
|
520
|
-
|
|
521
|
-
const getSelectedCount = useCallback(() => {
|
|
522
|
-
return selectedItems.size
|
|
523
|
-
}, [selectedItems])
|
|
524
|
-
|
|
525
|
-
return {
|
|
526
|
-
config,
|
|
527
|
-
updateConfig,
|
|
528
|
-
selectedItems,
|
|
529
|
-
focusedIndex,
|
|
530
|
-
setFocusedIndex,
|
|
531
|
-
selectItem,
|
|
532
|
-
deselectItem,
|
|
533
|
-
toggleItem,
|
|
534
|
-
selectRange,
|
|
535
|
-
selectAll,
|
|
536
|
-
clearSelection,
|
|
537
|
-
isSelected,
|
|
538
|
-
getSelectedItems,
|
|
539
|
-
getSelectedCount
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Enhanced Virtual List with selection support
|
|
544
|
-
export interface SelectableVirtualListProps<T = any> extends VirtualListProps<T> {
|
|
545
|
-
selectable?: boolean
|
|
546
|
-
multiSelect?: boolean
|
|
547
|
-
selectedItems?: Set<number>
|
|
548
|
-
onSelectionChange?: (selectedItems: Set<number>) => void
|
|
549
|
-
focusedIndex?: number
|
|
550
|
-
onFocusChange?: (focusedIndex: number) => void
|
|
551
|
-
selectAllEnabled?: boolean
|
|
552
|
-
onSelectAll?: () => void
|
|
553
|
-
onDeselectAll?: () => void
|
|
554
|
-
getItemId?: (item: T, index: number) => string | number
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
export function SelectableVirtualList<T = any>({
|
|
558
|
-
selectable = false,
|
|
559
|
-
multiSelect = false,
|
|
560
|
-
selectedItems = new Set(),
|
|
561
|
-
onSelectionChange,
|
|
562
|
-
focusedIndex = -1,
|
|
563
|
-
onFocusChange,
|
|
564
|
-
selectAllEnabled = false,
|
|
565
|
-
onSelectAll,
|
|
566
|
-
onDeselectAll,
|
|
567
|
-
getItemId,
|
|
568
|
-
renderItem: originalRenderItem,
|
|
569
|
-
...props
|
|
570
|
-
}: SelectableVirtualListProps<T>) {
|
|
571
|
-
const [lastSelectedIndex, setLastSelectedIndex] = useState<number>(-1)
|
|
572
|
-
|
|
573
|
-
const handleItemClick = useCallback((index: number, event?: React.MouseEvent) => {
|
|
574
|
-
if (!selectable || !onSelectionChange) return
|
|
575
|
-
|
|
576
|
-
const newSelection = new Set(selectedItems)
|
|
577
|
-
|
|
578
|
-
if (multiSelect && event?.shiftKey && lastSelectedIndex !== -1) {
|
|
579
|
-
// Shift+click için range selection
|
|
580
|
-
const start = Math.min(lastSelectedIndex, index)
|
|
581
|
-
const end = Math.max(lastSelectedIndex, index)
|
|
582
|
-
|
|
583
|
-
for (let i = start; i <= end; i++) {
|
|
584
|
-
newSelection.add(i)
|
|
585
|
-
}
|
|
586
|
-
} else if (multiSelect && (event?.ctrlKey || event?.metaKey)) {
|
|
587
|
-
// Ctrl/Cmd+click için toggle
|
|
588
|
-
if (newSelection.has(index)) {
|
|
589
|
-
newSelection.delete(index)
|
|
590
|
-
} else {
|
|
591
|
-
newSelection.add(index)
|
|
592
|
-
}
|
|
593
|
-
} else if (multiSelect) {
|
|
594
|
-
// Normal click in multi-select mode
|
|
595
|
-
if (newSelection.has(index)) {
|
|
596
|
-
newSelection.delete(index)
|
|
597
|
-
} else {
|
|
598
|
-
newSelection.add(index)
|
|
599
|
-
}
|
|
600
|
-
} else {
|
|
601
|
-
// Single select mode
|
|
602
|
-
newSelection.clear()
|
|
603
|
-
newSelection.add(index)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
setLastSelectedIndex(index)
|
|
607
|
-
onSelectionChange(newSelection)
|
|
608
|
-
onFocusChange?.(index)
|
|
609
|
-
}, [selectable, multiSelect, selectedItems, onSelectionChange, lastSelectedIndex, onFocusChange])
|
|
610
|
-
|
|
611
|
-
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
|
612
|
-
if (!selectable) return
|
|
613
|
-
|
|
614
|
-
switch (event.key) {
|
|
615
|
-
case 'Enter':
|
|
616
|
-
case ' ':
|
|
617
|
-
event.preventDefault()
|
|
618
|
-
if (focusedIndex >= 0 && focusedIndex < props.items.length) {
|
|
619
|
-
handleItemClick(focusedIndex)
|
|
620
|
-
}
|
|
621
|
-
break
|
|
622
|
-
case 'a':
|
|
623
|
-
if ((event.ctrlKey || event.metaKey) && multiSelect && selectAllEnabled) {
|
|
624
|
-
event.preventDefault()
|
|
625
|
-
if (onSelectAll) {
|
|
626
|
-
onSelectAll()
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
break
|
|
630
|
-
case 'Escape':
|
|
631
|
-
if (multiSelect && onDeselectAll) {
|
|
632
|
-
event.preventDefault()
|
|
633
|
-
onDeselectAll()
|
|
634
|
-
}
|
|
635
|
-
break
|
|
636
|
-
}
|
|
637
|
-
}, [selectable, focusedIndex, props.items.length, handleItemClick, multiSelect, selectAllEnabled, onSelectAll, onDeselectAll])
|
|
638
|
-
|
|
639
|
-
const renderItem = useCallback((item: T, index: number) => {
|
|
640
|
-
const isSelected = selectedItems.has(index)
|
|
641
|
-
const isFocused = focusedIndex === index
|
|
642
|
-
const itemId = getItemId ? getItemId(item, index) : index
|
|
643
|
-
|
|
644
|
-
return (
|
|
645
|
-
<div
|
|
646
|
-
key={itemId}
|
|
647
|
-
className={cn(
|
|
648
|
-
"transition-colors outline-none",
|
|
649
|
-
selectable && "cursor-pointer hover:bg-muted/50",
|
|
650
|
-
isSelected && "bg-primary/10 border-l-4 border-primary",
|
|
651
|
-
isFocused && "ring-2 ring-ring ring-offset-2"
|
|
652
|
-
)}
|
|
653
|
-
onClick={(e) => selectable && handleItemClick(index, e)}
|
|
654
|
-
onKeyDown={handleKeyDown}
|
|
655
|
-
tabIndex={selectable ? 0 : undefined}
|
|
656
|
-
role={selectable ? "option" : undefined}
|
|
657
|
-
aria-selected={selectable ? isSelected : undefined}
|
|
658
|
-
aria-label={selectable ? `Item ${index + 1}${isSelected ? ', selected' : ''}` : undefined}
|
|
659
|
-
>
|
|
660
|
-
{originalRenderItem(item, index)}
|
|
661
|
-
</div>
|
|
662
|
-
)
|
|
663
|
-
}, [originalRenderItem, selectable, selectedItems, focusedIndex, handleItemClick, handleKeyDown, getItemId])
|
|
664
|
-
|
|
665
|
-
return <VirtualList {...props} renderItem={renderItem} />
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Export types
|