@moontra/moonui-pro 2.0.22 → 2.0.23

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 (96) hide show
  1. package/package.json +2 -1
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  6. package/src/components/advanced-chart/index.tsx +412 -0
  7. package/src/components/advanced-forms/index.tsx +431 -0
  8. package/src/components/animated-button/index.tsx +202 -0
  9. package/src/components/calendar/event-dialog.tsx +372 -0
  10. package/src/components/calendar/index.tsx +531 -0
  11. package/src/components/color-picker/index.tsx +434 -0
  12. package/src/components/dashboard/index.tsx +334 -0
  13. package/src/components/data-table/data-table.test.tsx +187 -0
  14. package/src/components/data-table/index.tsx +368 -0
  15. package/src/components/draggable-list/index.tsx +100 -0
  16. package/src/components/enhanced/button.tsx +360 -0
  17. package/src/components/enhanced/card.tsx +272 -0
  18. package/src/components/enhanced/dialog.tsx +248 -0
  19. package/src/components/enhanced/index.ts +3 -0
  20. package/src/components/error-boundary/index.tsx +111 -0
  21. package/src/components/file-upload/file-upload.test.tsx +242 -0
  22. package/src/components/file-upload/index.tsx +362 -0
  23. package/src/components/floating-action-button/index.tsx +209 -0
  24. package/src/components/github-stars/index.tsx +414 -0
  25. package/src/components/health-check/index.tsx +441 -0
  26. package/src/components/hover-card-3d/index.tsx +170 -0
  27. package/src/components/index.ts +76 -0
  28. package/src/components/kanban/index.tsx +436 -0
  29. package/src/components/lazy-component/index.tsx +342 -0
  30. package/src/components/magnetic-button/index.tsx +170 -0
  31. package/src/components/memory-efficient-data/index.tsx +352 -0
  32. package/src/components/optimized-image/index.tsx +427 -0
  33. package/src/components/performance-debugger/index.tsx +591 -0
  34. package/src/components/performance-monitor/index.tsx +775 -0
  35. package/src/components/pinch-zoom/index.tsx +172 -0
  36. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  37. package/src/components/rich-text-editor/index.tsx +1537 -0
  38. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  39. package/src/components/rich-text-editor/slash-commands.css +35 -0
  40. package/src/components/rich-text-editor/table-styles.css +65 -0
  41. package/src/components/spotlight-card/index.tsx +194 -0
  42. package/src/components/swipeable-card/index.tsx +100 -0
  43. package/src/components/timeline/index.tsx +333 -0
  44. package/src/components/ui/animated-button.tsx +185 -0
  45. package/src/components/ui/avatar.tsx +135 -0
  46. package/src/components/ui/badge.tsx +225 -0
  47. package/src/components/ui/button.tsx +221 -0
  48. package/src/components/ui/card.tsx +141 -0
  49. package/src/components/ui/checkbox.tsx +256 -0
  50. package/src/components/ui/color-picker.tsx +95 -0
  51. package/src/components/ui/dialog.tsx +332 -0
  52. package/src/components/ui/dropdown-menu.tsx +200 -0
  53. package/src/components/ui/hover-card-3d.tsx +103 -0
  54. package/src/components/ui/index.ts +33 -0
  55. package/src/components/ui/input.tsx +219 -0
  56. package/src/components/ui/label.tsx +26 -0
  57. package/src/components/ui/magnetic-button.tsx +129 -0
  58. package/src/components/ui/popover.tsx +183 -0
  59. package/src/components/ui/select.tsx +273 -0
  60. package/src/components/ui/separator.tsx +140 -0
  61. package/src/components/ui/slider.tsx +351 -0
  62. package/src/components/ui/spotlight-card.tsx +119 -0
  63. package/src/components/ui/switch.tsx +83 -0
  64. package/src/components/ui/tabs.tsx +195 -0
  65. package/src/components/ui/textarea.tsx +25 -0
  66. package/src/components/ui/toast.tsx +313 -0
  67. package/src/components/ui/tooltip.tsx +152 -0
  68. package/src/components/virtual-list/index.tsx +369 -0
  69. package/src/hooks/use-chart.ts +205 -0
  70. package/src/hooks/use-data-table.ts +182 -0
  71. package/src/hooks/use-docs-pro-access.ts +13 -0
  72. package/src/hooks/use-license-check.ts +65 -0
  73. package/src/hooks/use-subscription.ts +19 -0
  74. package/src/index.ts +11 -0
  75. package/src/lib/micro-interactions.ts +255 -0
  76. package/src/lib/utils.ts +6 -0
  77. package/src/patterns/login-form/index.tsx +276 -0
  78. package/src/patterns/login-form/types.ts +67 -0
  79. package/src/setupTests.ts +41 -0
  80. package/src/styles/design-system.css +365 -0
  81. package/src/styles/index.css +4 -0
  82. package/src/styles/tailwind.css +6 -0
  83. package/src/styles/tokens.css +453 -0
  84. package/src/types/moonui.d.ts +22 -0
  85. package/src/use-intersection-observer.tsx +154 -0
  86. package/src/use-local-storage.tsx +71 -0
  87. package/src/use-paddle.ts +138 -0
  88. package/src/use-performance-optimizer.ts +379 -0
  89. package/src/use-pro-access.ts +141 -0
  90. package/src/use-scroll-animation.ts +221 -0
  91. package/src/use-subscription.ts +37 -0
  92. package/src/use-toast.ts +32 -0
  93. package/src/utils/chart-helpers.ts +257 -0
  94. package/src/utils/cn.ts +69 -0
  95. package/src/utils/data-processing.ts +151 -0
  96. package/src/utils/license-validator.tsx +183 -0
@@ -0,0 +1,369 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
4
+ import { cn } from '../../lib/utils'
5
+
6
+ // Virtual List Props
7
+ export interface VirtualListProps<T = any> {
8
+ items: T[]
9
+ height: number
10
+ itemHeight?: number
11
+ estimatedItemHeight?: number
12
+ variableHeight?: boolean
13
+ overscan?: number
14
+ renderItem: (item: T, index: number) => React.ReactNode
15
+ onScroll?: (scrollTop: number) => void
16
+ className?: string
17
+ }
18
+
19
+ // Item bilgilerini saklamak için
20
+ interface ItemInfo {
21
+ index: number
22
+ height: number
23
+ top: number
24
+ }
25
+
26
+ // Performans optimize edilmiş Virtual List Component
27
+ export function VirtualList<T = any>({
28
+ items,
29
+ height,
30
+ itemHeight = 50,
31
+ estimatedItemHeight = 50,
32
+ variableHeight = false,
33
+ overscan = 5,
34
+ renderItem,
35
+ onScroll,
36
+ className
37
+ }: VirtualListProps<T>) {
38
+ // State yönetimi
39
+ const [scrollTop, setScrollTop] = useState(0)
40
+ const [isScrolling, setIsScrolling] = useState(false)
41
+ const [itemHeights, setItemHeights] = useState<Map<number, number>>(new Map())
42
+
43
+ // Refs
44
+ const containerRef = useRef<HTMLDivElement>(null)
45
+ const scrollElementRef = useRef<HTMLDivElement>(null)
46
+ const scrollTimeoutRef = useRef<NodeJS.Timeout>()
47
+ const isScrollingTimeoutRef = useRef<NodeJS.Timeout>()
48
+
49
+ // Item pozisyonlarını hesapla
50
+ const itemPositions = useMemo(() => {
51
+ const positions: ItemInfo[] = []
52
+ let currentTop = 0
53
+
54
+ for (let i = 0; i < items.length; i++) {
55
+ const currentHeight = variableHeight
56
+ ? (itemHeights.get(i) || estimatedItemHeight)
57
+ : itemHeight
58
+
59
+ positions.push({
60
+ index: i,
61
+ height: currentHeight,
62
+ top: currentTop
63
+ })
64
+
65
+ currentTop += currentHeight
66
+ }
67
+
68
+ return positions
69
+ }, [items.length, itemHeight, estimatedItemHeight, variableHeight, itemHeights])
70
+
71
+ // Toplam içerik yüksekliği
72
+ const totalHeight = useMemo(() => {
73
+ return itemPositions.length > 0
74
+ ? itemPositions[itemPositions.length - 1].top + itemPositions[itemPositions.length - 1].height
75
+ : 0
76
+ }, [itemPositions])
77
+
78
+ // Görünür aralığı hesapla
79
+ const visibleRange = useMemo(() => {
80
+ const start = itemPositions.findIndex(item => item.top + item.height >= scrollTop)
81
+ const end = itemPositions.findIndex(item => item.top > scrollTop + height)
82
+
83
+ const actualStart = Math.max(0, start - overscan)
84
+ const actualEnd = end === -1
85
+ ? Math.min(items.length - 1, start + Math.ceil(height / itemHeight) + overscan)
86
+ : Math.min(items.length - 1, end + overscan)
87
+
88
+ return {
89
+ start: actualStart,
90
+ end: actualEnd
91
+ }
92
+ }, [scrollTop, height, itemPositions, overscan, items.length, itemHeight])
93
+
94
+ // Görünür itemlar
95
+ const visibleItems = useMemo(() => {
96
+ const result: Array<{ item: T; index: number; style: React.CSSProperties }> = []
97
+
98
+ for (let i = visibleRange.start; i <= visibleRange.end; i++) {
99
+ if (i >= 0 && i < items.length && itemPositions[i]) {
100
+ const position = itemPositions[i]
101
+ result.push({
102
+ item: items[i],
103
+ index: i,
104
+ style: {
105
+ position: 'absolute',
106
+ top: position.top,
107
+ left: 0,
108
+ right: 0,
109
+ height: variableHeight ? 'auto' : position.height,
110
+ minHeight: variableHeight ? position.height : undefined
111
+ }
112
+ })
113
+ }
114
+ }
115
+
116
+ return result
117
+ }, [items, visibleRange, itemPositions, variableHeight])
118
+
119
+ // Scroll handler
120
+ const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
121
+ const scrollTop = e.currentTarget.scrollTop
122
+ setScrollTop(scrollTop)
123
+ setIsScrolling(true)
124
+
125
+ // Scroll callback
126
+ if (onScroll) {
127
+ onScroll(scrollTop)
128
+ }
129
+
130
+ // Scroll timeout'ını temizle ve yenisini ayarla
131
+ if (scrollTimeoutRef.current) {
132
+ clearTimeout(scrollTimeoutRef.current)
133
+ }
134
+
135
+ if (isScrollingTimeoutRef.current) {
136
+ clearTimeout(isScrollingTimeoutRef.current)
137
+ }
138
+
139
+ // 150ms sonra scrolling'i false yap (performans için)
140
+ isScrollingTimeoutRef.current = setTimeout(() => {
141
+ setIsScrolling(false)
142
+ }, 150)
143
+ }, [onScroll])
144
+
145
+ // Variable height için resize observer
146
+ useEffect(() => {
147
+ if (!variableHeight || !containerRef.current) return
148
+
149
+ const resizeObserver = new ResizeObserver((entries) => {
150
+ const newHeights = new Map(itemHeights)
151
+ let hasChanges = false
152
+
153
+ entries.forEach((entry) => {
154
+ const element = entry.target as HTMLElement
155
+ const indexAttr = element.getAttribute('data-index')
156
+
157
+ if (indexAttr) {
158
+ const index = parseInt(indexAttr, 10)
159
+ const newHeight = entry.contentRect.height
160
+
161
+ if (newHeights.get(index) !== newHeight) {
162
+ newHeights.set(index, newHeight)
163
+ hasChanges = true
164
+ }
165
+ }
166
+ })
167
+
168
+ if (hasChanges) {
169
+ setItemHeights(newHeights)
170
+ }
171
+ })
172
+
173
+ // Mevcut tüm item elementlerini observe et
174
+ const itemElements = containerRef.current.querySelectorAll('[data-index]')
175
+ itemElements.forEach(el => resizeObserver.observe(el))
176
+
177
+ return () => resizeObserver.disconnect()
178
+ }, [variableHeight, itemHeights, visibleItems])
179
+
180
+ // Keyboard navigation desteği
181
+ useEffect(() => {
182
+ const handleKeyDown = (e: KeyboardEvent) => {
183
+ if (!containerRef.current) return
184
+
185
+ switch (e.key) {
186
+ case 'ArrowUp':
187
+ e.preventDefault()
188
+ scrollElementRef.current?.scrollBy({ top: -itemHeight, behavior: 'smooth' })
189
+ break
190
+ case 'ArrowDown':
191
+ e.preventDefault()
192
+ scrollElementRef.current?.scrollBy({ top: itemHeight, behavior: 'smooth' })
193
+ break
194
+ case 'PageUp':
195
+ e.preventDefault()
196
+ scrollElementRef.current?.scrollBy({ top: -height, behavior: 'smooth' })
197
+ break
198
+ case 'PageDown':
199
+ e.preventDefault()
200
+ scrollElementRef.current?.scrollBy({ top: height, behavior: 'smooth' })
201
+ break
202
+ case 'Home':
203
+ e.preventDefault()
204
+ scrollElementRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
205
+ break
206
+ case 'End':
207
+ e.preventDefault()
208
+ scrollElementRef.current?.scrollTo({ top: totalHeight, behavior: 'smooth' })
209
+ break
210
+ }
211
+ }
212
+
213
+ const container = containerRef.current
214
+ if (container) {
215
+ container.addEventListener('keydown', handleKeyDown)
216
+ return () => container.removeEventListener('keydown', handleKeyDown)
217
+ }
218
+ }, [itemHeight, height, totalHeight])
219
+
220
+ return (
221
+ <div
222
+ ref={containerRef}
223
+ className={cn(
224
+ "relative overflow-hidden border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-ring",
225
+ className
226
+ )}
227
+ style={{ height }}
228
+ tabIndex={0}
229
+ role="listbox"
230
+ aria-label={`Virtual list with ${items.length} items`}
231
+ >
232
+ <div
233
+ ref={scrollElementRef}
234
+ className="h-full overflow-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent"
235
+ onScroll={handleScroll}
236
+ >
237
+ <div
238
+ style={{
239
+ height: totalHeight,
240
+ position: 'relative'
241
+ }}
242
+ >
243
+ {visibleItems.map(({ item, index, style }) => (
244
+ <div
245
+ key={index}
246
+ data-index={index}
247
+ style={style}
248
+ role="option"
249
+ aria-selected="false"
250
+ className="outline-none"
251
+ >
252
+ {renderItem(item, index)}
253
+ </div>
254
+ ))}
255
+ </div>
256
+ </div>
257
+
258
+ {/* Loading indicator for scrolling */}
259
+ {isScrolling && (
260
+ <div className="absolute top-2 right-2 bg-muted/80 text-muted-foreground px-2 py-1 rounded text-xs">
261
+ Scrolling...
262
+ </div>
263
+ )}
264
+
265
+ {/* Scroll position indicator */}
266
+ {items.length > 100 && (
267
+ <div className="absolute bottom-2 right-2 bg-muted/80 text-muted-foreground px-2 py-1 rounded text-xs">
268
+ {Math.round((scrollTop / Math.max(totalHeight - height, 1)) * 100)}%
269
+ </div>
270
+ )}
271
+ </div>
272
+ )
273
+ }
274
+
275
+ // Utility hook for virtual list state management
276
+ export function useVirtualList<T>(
277
+ items: T[],
278
+ initialConfig: Partial<VirtualListProps<T>> = {}
279
+ ) {
280
+ const [config, setConfig] = useState(initialConfig)
281
+ const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set())
282
+
283
+ const updateConfig = useCallback((newConfig: Partial<VirtualListProps<T>>) => {
284
+ setConfig(prev => ({ ...prev, ...newConfig }))
285
+ }, [])
286
+
287
+ const selectItem = useCallback((index: number) => {
288
+ setSelectedItems(prev => new Set([...prev, index]))
289
+ }, [])
290
+
291
+ const deselectItem = useCallback((index: number) => {
292
+ setSelectedItems(prev => {
293
+ const newSet = new Set(prev)
294
+ newSet.delete(index)
295
+ return newSet
296
+ })
297
+ }, [])
298
+
299
+ const clearSelection = useCallback(() => {
300
+ setSelectedItems(new Set())
301
+ }, [])
302
+
303
+ return {
304
+ config,
305
+ updateConfig,
306
+ selectedItems,
307
+ selectItem,
308
+ deselectItem,
309
+ clearSelection
310
+ }
311
+ }
312
+
313
+ // Enhanced Virtual List with selection support
314
+ export interface SelectableVirtualListProps<T = any> extends VirtualListProps<T> {
315
+ selectable?: boolean
316
+ multiSelect?: boolean
317
+ selectedItems?: Set<number>
318
+ onSelectionChange?: (selectedItems: Set<number>) => void
319
+ }
320
+
321
+ export function SelectableVirtualList<T = any>({
322
+ selectable = false,
323
+ multiSelect = false,
324
+ selectedItems = new Set(),
325
+ onSelectionChange,
326
+ renderItem: originalRenderItem,
327
+ ...props
328
+ }: SelectableVirtualListProps<T>) {
329
+ const handleItemClick = useCallback((index: number) => {
330
+ if (!selectable || !onSelectionChange) return
331
+
332
+ const newSelection = new Set(selectedItems)
333
+
334
+ if (multiSelect) {
335
+ if (newSelection.has(index)) {
336
+ newSelection.delete(index)
337
+ } else {
338
+ newSelection.add(index)
339
+ }
340
+ } else {
341
+ newSelection.clear()
342
+ newSelection.add(index)
343
+ }
344
+
345
+ onSelectionChange(newSelection)
346
+ }, [selectable, multiSelect, selectedItems, onSelectionChange])
347
+
348
+ const renderItem = useCallback((item: T, index: number) => {
349
+ const isSelected = selectedItems.has(index)
350
+
351
+ return (
352
+ <div
353
+ className={cn(
354
+ "transition-colors",
355
+ selectable && "cursor-pointer hover:bg-muted/50",
356
+ isSelected && "bg-primary/10 border-l-4 border-primary"
357
+ )}
358
+ onClick={() => selectable && handleItemClick(index)}
359
+ >
360
+ {originalRenderItem(item, index)}
361
+ </div>
362
+ )
363
+ }, [originalRenderItem, selectable, selectedItems, handleItemClick])
364
+
365
+ return <VirtualList {...props} renderItem={renderItem} />
366
+ }
367
+
368
+ // Export types
369
+ export type { VirtualListProps, SelectableVirtualListProps }
@@ -0,0 +1,205 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { ChartDataPoint, ChartSeries } from '../components/advanced-chart'
5
+
6
+ export interface UseChartOptions {
7
+ data: ChartDataPoint[]
8
+ series: ChartSeries[]
9
+ realtime?: boolean
10
+ refreshInterval?: number
11
+ maxDataPoints?: number
12
+ }
13
+
14
+ export interface UseChartReturn {
15
+ data: ChartDataPoint[]
16
+ series: ChartSeries[]
17
+ isLoading: boolean
18
+ error: string | null
19
+ addDataPoint: (point: ChartDataPoint) => void
20
+ updateSeries: (seriesUpdate: Partial<ChartSeries>[]) => void
21
+ toggleSeries: (dataKey: string) => void
22
+ resetData: () => void
23
+ exportData: (format: 'csv' | 'json') => void
24
+ getStats: () => {
25
+ total: number
26
+ average: number
27
+ min: number
28
+ max: number
29
+ trend: 'up' | 'down' | 'neutral'
30
+ }
31
+ }
32
+
33
+ export function useChart({
34
+ data: initialData,
35
+ series: initialSeries,
36
+ realtime = false,
37
+ refreshInterval = 5000,
38
+ maxDataPoints = 100,
39
+ }: UseChartOptions): UseChartReturn {
40
+ const [data, setData] = React.useState<ChartDataPoint[]>(initialData)
41
+ const [series, setSeries] = React.useState<ChartSeries[]>(initialSeries)
42
+ const [isLoading, setIsLoading] = React.useState(false)
43
+ const [error, setError] = React.useState<string | null>(null)
44
+
45
+ // Realtime data updates
46
+ React.useEffect(() => {
47
+ if (!realtime) return
48
+
49
+ const interval = setInterval(() => {
50
+ // This would typically fetch new data from an API
51
+ // For demo purposes, we'll simulate data updates
52
+ setData(prevData => {
53
+ const newData = [...prevData]
54
+
55
+ // Keep only the last maxDataPoints
56
+ if (newData.length >= maxDataPoints) {
57
+ newData.shift()
58
+ }
59
+
60
+ // Add simulated new data point
61
+ const lastPoint = newData[newData.length - 1]
62
+ if (lastPoint) {
63
+ const newPoint: ChartDataPoint = { ...lastPoint }
64
+
65
+ // Simulate data changes for each series
66
+ series.forEach(s => {
67
+ if (typeof lastPoint[s.dataKey] === 'number') {
68
+ const currentValue = lastPoint[s.dataKey] as number
69
+ const change = (Math.random() - 0.5) * currentValue * 0.1
70
+ newPoint[s.dataKey] = Math.max(0, currentValue + change)
71
+ }
72
+ })
73
+
74
+ // Update timestamp if exists
75
+ if ('timestamp' in newPoint) {
76
+ newPoint.timestamp = Date.now()
77
+ }
78
+
79
+ newData.push(newPoint)
80
+ }
81
+
82
+ return newData
83
+ })
84
+ }, refreshInterval)
85
+
86
+ return () => clearInterval(interval)
87
+ }, [realtime, refreshInterval, maxDataPoints, series])
88
+
89
+ const addDataPoint = React.useCallback((point: ChartDataPoint) => {
90
+ setData(prevData => {
91
+ const newData = [...prevData, point]
92
+
93
+ // Keep only the last maxDataPoints
94
+ if (newData.length > maxDataPoints) {
95
+ return newData.slice(-maxDataPoints)
96
+ }
97
+
98
+ return newData
99
+ })
100
+ }, [maxDataPoints])
101
+
102
+ const updateSeries = React.useCallback((seriesUpdate: Partial<ChartSeries>[]) => {
103
+ setSeries(prevSeries => {
104
+ return prevSeries.map(s => {
105
+ const update = seriesUpdate.find(u => u.dataKey === s.dataKey)
106
+ return update ? { ...s, ...update } : s
107
+ })
108
+ })
109
+ }, [])
110
+
111
+ const toggleSeries = React.useCallback((dataKey: string) => {
112
+ setSeries(prevSeries => {
113
+ return prevSeries.map(s =>
114
+ s.dataKey === dataKey ? { ...s, hide: !s.hide } : s
115
+ )
116
+ })
117
+ }, [])
118
+
119
+ const resetData = React.useCallback(() => {
120
+ setData(initialData)
121
+ setSeries(initialSeries)
122
+ setError(null)
123
+ }, [initialData, initialSeries])
124
+
125
+ const exportData = React.useCallback((format: 'csv' | 'json') => {
126
+ try {
127
+ if (format === 'csv') {
128
+ const headers = Object.keys(data[0] || {})
129
+ const csvContent = [
130
+ headers.join(','),
131
+ ...data.map(row =>
132
+ headers.map(header => {
133
+ const value = row[header]
134
+ if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
135
+ return `"${value.replace(/"/g, '""')}"`
136
+ }
137
+ return value
138
+ }).join(',')
139
+ )
140
+ ].join('\n')
141
+
142
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
143
+ const link = document.createElement('a')
144
+ link.href = URL.createObjectURL(blob)
145
+ link.download = 'chart-data.csv'
146
+ link.click()
147
+ } else if (format === 'json') {
148
+ const jsonContent = JSON.stringify({ data, series }, null, 2)
149
+ const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' })
150
+ const link = document.createElement('a')
151
+ link.href = URL.createObjectURL(blob)
152
+ link.download = 'chart-data.json'
153
+ link.click()
154
+ }
155
+ } catch (err) {
156
+ setError('Failed to export data')
157
+ }
158
+ }, [data, series])
159
+
160
+ const getStats = React.useCallback(() => {
161
+ if (!data.length || !series.length) {
162
+ return { total: 0, average: 0, min: 0, max: 0, trend: 'neutral' as const }
163
+ }
164
+
165
+ const firstSeries = series[0]
166
+ const values = data
167
+ .map(d => Number(d[firstSeries.dataKey]))
168
+ .filter(v => !isNaN(v))
169
+
170
+ if (!values.length) {
171
+ return { total: 0, average: 0, min: 0, max: 0, trend: 'neutral' as const }
172
+ }
173
+
174
+ const total = values.reduce((sum, val) => sum + val, 0)
175
+ const average = total / values.length
176
+ const min = Math.min(...values)
177
+ const max = Math.max(...values)
178
+
179
+ // Calculate trend
180
+ let trend: 'up' | 'down' | 'neutral' = 'neutral'
181
+ if (values.length >= 2) {
182
+ const first = values[0]
183
+ const last = values[values.length - 1]
184
+ const change = ((last - first) / first) * 100
185
+
186
+ if (change > 5) trend = 'up'
187
+ else if (change < -5) trend = 'down'
188
+ }
189
+
190
+ return { total, average, min, max, trend }
191
+ }, [data, series])
192
+
193
+ return {
194
+ data,
195
+ series,
196
+ isLoading,
197
+ error,
198
+ addDataPoint,
199
+ updateSeries,
200
+ toggleSeries,
201
+ resetData,
202
+ exportData,
203
+ getStats,
204
+ }
205
+ }