@moontra/moonui-pro 2.0.22 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/index.mjs +215 -214
  2. package/package.json +4 -2
  3. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  4. package/src/__tests__/use-local-storage.test.tsx +174 -0
  5. package/src/__tests__/use-pro-access.test.tsx +183 -0
  6. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  7. package/src/components/advanced-chart/index.tsx +412 -0
  8. package/src/components/advanced-forms/index.tsx +431 -0
  9. package/src/components/animated-button/index.tsx +202 -0
  10. package/src/components/calendar/event-dialog.tsx +372 -0
  11. package/src/components/calendar/index.tsx +557 -0
  12. package/src/components/color-picker/index.tsx +434 -0
  13. package/src/components/dashboard/index.tsx +334 -0
  14. package/src/components/data-table/data-table.test.tsx +187 -0
  15. package/src/components/data-table/index.tsx +368 -0
  16. package/src/components/draggable-list/index.tsx +100 -0
  17. package/src/components/enhanced/button.tsx +360 -0
  18. package/src/components/enhanced/card.tsx +272 -0
  19. package/src/components/enhanced/dialog.tsx +248 -0
  20. package/src/components/enhanced/index.ts +3 -0
  21. package/src/components/error-boundary/index.tsx +111 -0
  22. package/src/components/file-upload/file-upload.test.tsx +242 -0
  23. package/src/components/file-upload/index.tsx +362 -0
  24. package/src/components/floating-action-button/index.tsx +209 -0
  25. package/src/components/github-stars/index.tsx +414 -0
  26. package/src/components/health-check/index.tsx +441 -0
  27. package/src/components/hover-card-3d/index.tsx +170 -0
  28. package/src/components/index.ts +76 -0
  29. package/src/components/kanban/index.tsx +436 -0
  30. package/src/components/lazy-component/index.tsx +342 -0
  31. package/src/components/magnetic-button/index.tsx +170 -0
  32. package/src/components/memory-efficient-data/index.tsx +352 -0
  33. package/src/components/optimized-image/index.tsx +427 -0
  34. package/src/components/performance-debugger/index.tsx +591 -0
  35. package/src/components/performance-monitor/index.tsx +775 -0
  36. package/src/components/pinch-zoom/index.tsx +172 -0
  37. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  38. package/src/components/rich-text-editor/index.tsx +1537 -0
  39. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  40. package/src/components/rich-text-editor/slash-commands.css +35 -0
  41. package/src/components/rich-text-editor/table-styles.css +65 -0
  42. package/src/components/spotlight-card/index.tsx +194 -0
  43. package/src/components/swipeable-card/index.tsx +100 -0
  44. package/src/components/timeline/index.tsx +333 -0
  45. package/src/components/ui/animated-button.tsx +185 -0
  46. package/src/components/ui/avatar.tsx +135 -0
  47. package/src/components/ui/badge.tsx +225 -0
  48. package/src/components/ui/button.tsx +221 -0
  49. package/src/components/ui/card.tsx +141 -0
  50. package/src/components/ui/checkbox.tsx +256 -0
  51. package/src/components/ui/color-picker.tsx +95 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/hover-card-3d.tsx +103 -0
  55. package/src/components/ui/index.ts +33 -0
  56. package/src/components/ui/input.tsx +219 -0
  57. package/src/components/ui/label.tsx +26 -0
  58. package/src/components/ui/magnetic-button.tsx +129 -0
  59. package/src/components/ui/popover.tsx +183 -0
  60. package/src/components/ui/select.tsx +273 -0
  61. package/src/components/ui/separator.tsx +140 -0
  62. package/src/components/ui/slider.tsx +351 -0
  63. package/src/components/ui/spotlight-card.tsx +119 -0
  64. package/src/components/ui/switch.tsx +83 -0
  65. package/src/components/ui/tabs.tsx +195 -0
  66. package/src/components/ui/textarea.tsx +25 -0
  67. package/src/components/ui/toast.tsx +313 -0
  68. package/src/components/ui/tooltip.tsx +152 -0
  69. package/src/components/virtual-list/index.tsx +369 -0
  70. package/src/hooks/use-chart.ts +205 -0
  71. package/src/hooks/use-data-table.ts +182 -0
  72. package/src/hooks/use-docs-pro-access.ts +13 -0
  73. package/src/hooks/use-license-check.ts +65 -0
  74. package/src/hooks/use-subscription.ts +19 -0
  75. package/src/index.ts +14 -0
  76. package/src/lib/micro-interactions.ts +255 -0
  77. package/src/lib/utils.ts +6 -0
  78. package/src/patterns/login-form/index.tsx +276 -0
  79. package/src/patterns/login-form/types.ts +67 -0
  80. package/src/setupTests.ts +41 -0
  81. package/src/styles/design-system.css +365 -0
  82. package/src/styles/index.css +4 -0
  83. package/src/styles/tailwind.css +6 -0
  84. package/src/styles/tokens.css +453 -0
  85. package/src/types/moonui.d.ts +22 -0
  86. package/src/use-intersection-observer.tsx +154 -0
  87. package/src/use-local-storage.tsx +71 -0
  88. package/src/use-paddle.ts +138 -0
  89. package/src/use-performance-optimizer.ts +379 -0
  90. package/src/use-pro-access.ts +141 -0
  91. package/src/use-scroll-animation.ts +221 -0
  92. package/src/use-subscription.ts +37 -0
  93. package/src/use-toast.ts +32 -0
  94. package/src/utils/chart-helpers.ts +257 -0
  95. package/src/utils/cn.ts +69 -0
  96. package/src/utils/data-processing.ts +151 -0
  97. package/src/utils/license-guard.tsx +177 -0
  98. package/src/utils/license-validator.tsx +183 -0
  99. package/src/utils/package-guard.ts +60 -0
@@ -0,0 +1,334 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
5
+ import { Badge } from '../ui/badge'
6
+ import { Button } from '../ui/button'
7
+ import {
8
+ TrendingUp,
9
+ TrendingDown,
10
+ Users,
11
+ DollarSign,
12
+ Activity,
13
+ BarChart3,
14
+ Eye,
15
+ Download,
16
+ Star,
17
+ Clock,
18
+ Calendar,
19
+ ArrowUpRight,
20
+ ArrowDownRight,
21
+ Minus,
22
+ Lock,
23
+ Sparkles
24
+ } from 'lucide-react'
25
+ import { cn } from '@moontra/moonui'
26
+
27
+ export interface DashboardMetric {
28
+ id: string
29
+ title: string
30
+ value: string | number
31
+ change?: {
32
+ value: number
33
+ type: 'increase' | 'decrease' | 'neutral'
34
+ period: string
35
+ }
36
+ icon?: React.ReactNode
37
+ color?: string
38
+ description?: string
39
+ trend?: number[]
40
+ }
41
+
42
+ export interface DashboardWidget {
43
+ id: string
44
+ title: string
45
+ description?: string
46
+ content: React.ReactNode
47
+ size?: 'sm' | 'md' | 'lg' | 'xl'
48
+ loading?: boolean
49
+ error?: string
50
+ }
51
+
52
+ export interface DashboardProps {
53
+ metrics?: DashboardMetric[]
54
+ widgets?: DashboardWidget[]
55
+ onMetricClick?: (metric: DashboardMetric) => void
56
+ onWidgetAction?: (widgetId: string, action: string) => void
57
+ className?: string
58
+ showHeader?: boolean
59
+ title?: string
60
+ description?: string
61
+ refreshable?: boolean
62
+ onRefresh?: () => void
63
+ loading?: boolean
64
+ }
65
+
66
+ const METRIC_COLORS = {
67
+ primary: 'text-blue-600',
68
+ success: 'text-green-600',
69
+ warning: 'text-yellow-600',
70
+ danger: 'text-red-600',
71
+ info: 'text-purple-600'
72
+ }
73
+
74
+ const WIDGET_SIZES = {
75
+ sm: 'col-span-1',
76
+ md: 'col-span-2',
77
+ lg: 'col-span-3',
78
+ xl: 'col-span-4'
79
+ }
80
+
81
+ export function Dashboard({
82
+ metrics = [],
83
+ widgets = [],
84
+ onMetricClick,
85
+ onWidgetAction,
86
+ className,
87
+ showHeader = true,
88
+ title = 'Dashboard',
89
+ description = 'Overview of your key metrics and performance',
90
+ refreshable = true,
91
+ onRefresh,
92
+ loading = false
93
+ }: DashboardProps) {
94
+ const [refreshing, setRefreshing] = React.useState(false)
95
+
96
+ const handleRefresh = async () => {
97
+ if (onRefresh) {
98
+ setRefreshing(true)
99
+ try {
100
+ await onRefresh()
101
+ } finally {
102
+ setRefreshing(false)
103
+ }
104
+ }
105
+ }
106
+
107
+ const formatValue = (value: string | number): string => {
108
+ if (typeof value === 'number') {
109
+ if (value >= 1000000) {
110
+ return (value / 1000000).toFixed(1) + 'M'
111
+ } else if (value >= 1000) {
112
+ return (value / 1000).toFixed(1) + 'K'
113
+ }
114
+ return value.toLocaleString()
115
+ }
116
+ return value.toString()
117
+ }
118
+
119
+ const getChangeIcon = (type: 'increase' | 'decrease' | 'neutral') => {
120
+ switch (type) {
121
+ case 'increase':
122
+ return <ArrowUpRight className="h-4 w-4" />
123
+ case 'decrease':
124
+ return <ArrowDownRight className="h-4 w-4" />
125
+ case 'neutral':
126
+ return <Minus className="h-4 w-4" />
127
+ }
128
+ }
129
+
130
+ const getChangeColor = (type: 'increase' | 'decrease' | 'neutral') => {
131
+ switch (type) {
132
+ case 'increase':
133
+ return 'text-green-600'
134
+ case 'decrease':
135
+ return 'text-red-600'
136
+ case 'neutral':
137
+ return 'text-gray-600'
138
+ }
139
+ }
140
+
141
+ const renderMetricCard = (metric: DashboardMetric) => {
142
+ const colorClass = metric.color ? METRIC_COLORS[metric.color as keyof typeof METRIC_COLORS] : 'text-foreground'
143
+
144
+ return (
145
+ <Card
146
+ key={metric.id}
147
+ className={cn(
148
+ "cursor-pointer hover:shadow-md transition-shadow",
149
+ onMetricClick && "hover:bg-muted/50"
150
+ )}
151
+ onClick={() => onMetricClick?.(metric)}
152
+ >
153
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
154
+ <CardTitle className="text-sm font-medium">
155
+ {metric.title}
156
+ </CardTitle>
157
+ <div className={cn("h-4 w-4", colorClass)}>
158
+ {metric.icon}
159
+ </div>
160
+ </CardHeader>
161
+ <CardContent>
162
+ <div className="text-2xl font-bold">{formatValue(metric.value)}</div>
163
+ {metric.change && (
164
+ <div className={cn(
165
+ "flex items-center text-xs mt-1",
166
+ getChangeColor(metric.change.type)
167
+ )}>
168
+ {getChangeIcon(metric.change.type)}
169
+ <span className="ml-1">
170
+ {Math.abs(metric.change.value)}% from {metric.change.period}
171
+ </span>
172
+ </div>
173
+ )}
174
+ {metric.description && (
175
+ <p className="text-xs text-muted-foreground mt-1">
176
+ {metric.description}
177
+ </p>
178
+ )}
179
+ </CardContent>
180
+ </Card>
181
+ )
182
+ }
183
+
184
+ const renderWidget = (widget: DashboardWidget) => {
185
+ const sizeClass = WIDGET_SIZES[widget.size || 'md']
186
+
187
+ return (
188
+ <Card key={widget.id} className={cn("h-fit", sizeClass)}>
189
+ <CardHeader>
190
+ <div className="flex items-center justify-between">
191
+ <div>
192
+ <CardTitle className="text-base">{widget.title}</CardTitle>
193
+ {widget.description && (
194
+ <CardDescription className="mt-1">
195
+ {widget.description}
196
+ </CardDescription>
197
+ )}
198
+ </div>
199
+ <Button
200
+ variant="ghost"
201
+ size="sm"
202
+ onClick={() => onWidgetAction?.(widget.id, 'menu')}
203
+ >
204
+ <BarChart3 className="h-4 w-4" />
205
+ </Button>
206
+ </div>
207
+ </CardHeader>
208
+ <CardContent>
209
+ {widget.loading ? (
210
+ <div className="flex items-center justify-center h-32">
211
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
212
+ </div>
213
+ ) : widget.error ? (
214
+ <div className="flex items-center justify-center h-32 text-destructive">
215
+ <p className="text-sm">{widget.error}</p>
216
+ </div>
217
+ ) : (
218
+ widget.content
219
+ )}
220
+ </CardContent>
221
+ </Card>
222
+ )
223
+ }
224
+
225
+ const defaultMetrics: DashboardMetric[] = [
226
+ {
227
+ id: 'total-users',
228
+ title: 'Total Users',
229
+ value: 2543,
230
+ change: { value: 12, type: 'increase', period: 'last month' },
231
+ icon: <Users className="h-4 w-4" />,
232
+ color: 'primary'
233
+ },
234
+ {
235
+ id: 'revenue',
236
+ title: 'Revenue',
237
+ value: '$12,345',
238
+ change: { value: 8, type: 'increase', period: 'last month' },
239
+ icon: <DollarSign className="h-4 w-4" />,
240
+ color: 'success'
241
+ },
242
+ {
243
+ id: 'active-sessions',
244
+ title: 'Active Sessions',
245
+ value: 1234,
246
+ change: { value: 2, type: 'decrease', period: 'last hour' },
247
+ icon: <Activity className="h-4 w-4" />,
248
+ color: 'warning'
249
+ },
250
+ {
251
+ id: 'conversion-rate',
252
+ title: 'Conversion Rate',
253
+ value: '3.2%',
254
+ change: { value: 0.3, type: 'increase', period: 'last week' },
255
+ icon: <TrendingUp className="h-4 w-4" />,
256
+ color: 'info'
257
+ }
258
+ ]
259
+
260
+ const displayMetrics = metrics.length > 0 ? metrics : defaultMetrics
261
+
262
+ if (loading) {
263
+ return (
264
+ <div className={cn("w-full", className)}>
265
+ <div className="animate-pulse space-y-4">
266
+ <div className="h-8 bg-muted rounded w-1/4"></div>
267
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
268
+ {[...Array(4)].map((_, i) => (
269
+ <div key={i} className="h-32 bg-muted rounded"></div>
270
+ ))}
271
+ </div>
272
+ </div>
273
+ </div>
274
+ )
275
+ }
276
+
277
+ return (
278
+ <div className={cn("w-full space-y-6", className)}>
279
+ {/* Header */}
280
+ {showHeader && (
281
+ <div className="flex items-center justify-between">
282
+ <div>
283
+ <h1 className="text-2xl font-bold tracking-tight">{title}</h1>
284
+ <p className="text-muted-foreground">{description}</p>
285
+ </div>
286
+ {refreshable && (
287
+ <Button
288
+ variant="outline"
289
+ onClick={handleRefresh}
290
+ disabled={refreshing}
291
+ >
292
+ {refreshing ? (
293
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div>
294
+ ) : (
295
+ <Activity className="h-4 w-4 mr-2" />
296
+ )}
297
+ Refresh
298
+ </Button>
299
+ )}
300
+ </div>
301
+ )}
302
+
303
+ {/* Metrics Grid */}
304
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
305
+ {displayMetrics.map(renderMetricCard)}
306
+ </div>
307
+
308
+ {/* Widgets Grid */}
309
+ {widgets.length > 0 && (
310
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
311
+ {widgets.map(renderWidget)}
312
+ </div>
313
+ )}
314
+
315
+ {/* Quick Actions */}
316
+ <div className="flex items-center gap-2 pt-4 border-t">
317
+ <Button variant="outline" size="sm">
318
+ <Download className="h-4 w-4 mr-2" />
319
+ Export Data
320
+ </Button>
321
+ <Button variant="outline" size="sm">
322
+ <Calendar className="h-4 w-4 mr-2" />
323
+ Schedule Report
324
+ </Button>
325
+ <Button variant="outline" size="sm">
326
+ <Star className="h-4 w-4 mr-2" />
327
+ Save View
328
+ </Button>
329
+ </div>
330
+ </div>
331
+ )
332
+ }
333
+
334
+ export default Dashboard
@@ -0,0 +1,187 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import '@testing-library/jest-dom'
3
+ import { DataTable } from './index'
4
+
5
+ // Mock data for testing
6
+ const mockData = [
7
+ { id: 1, name: 'John Doe', email: 'john@example.com', status: 'active' },
8
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'inactive' },
9
+ { id: 3, name: 'Bob Johnson', email: 'bob@example.com', status: 'active' }
10
+ ]
11
+
12
+ const mockColumns = [
13
+ {
14
+ accessorKey: 'name',
15
+ header: 'Name',
16
+ },
17
+ {
18
+ accessorKey: 'email',
19
+ header: 'Email',
20
+ },
21
+ {
22
+ accessorKey: 'status',
23
+ header: 'Status',
24
+ }
25
+ ]
26
+
27
+ describe('DataTable', () => {
28
+ it('renders without crashing', () => {
29
+ render(
30
+ <DataTable
31
+ columns={mockColumns}
32
+ data={mockData}
33
+ />
34
+ )
35
+
36
+ expect(screen.getByText('Name')).toBeInTheDocument()
37
+ expect(screen.getByText('Email')).toBeInTheDocument()
38
+ expect(screen.getByText('Status')).toBeInTheDocument()
39
+ })
40
+
41
+ it('displays data correctly', () => {
42
+ render(
43
+ <DataTable
44
+ columns={mockColumns}
45
+ data={mockData}
46
+ />
47
+ )
48
+
49
+ expect(screen.getByText('John Doe')).toBeInTheDocument()
50
+ expect(screen.getByText('jane@example.com')).toBeInTheDocument()
51
+ expect(screen.getByText('active')).toBeInTheDocument()
52
+ })
53
+
54
+ it('shows search input when searchable is true', () => {
55
+ render(
56
+ <DataTable
57
+ columns={mockColumns}
58
+ data={mockData}
59
+ searchable
60
+ />
61
+ )
62
+
63
+ expect(screen.getByPlaceholderText('Search all columns...')).toBeInTheDocument()
64
+ })
65
+
66
+ it('shows filter button when filterable is true', () => {
67
+ render(
68
+ <DataTable
69
+ columns={mockColumns}
70
+ data={mockData}
71
+ filterable
72
+ />
73
+ )
74
+
75
+ expect(screen.getByText('Filters')).toBeInTheDocument()
76
+ })
77
+
78
+ it('shows export button when exportable is true', () => {
79
+ render(
80
+ <DataTable
81
+ columns={mockColumns}
82
+ data={mockData}
83
+ exportable
84
+ />
85
+ )
86
+
87
+ expect(screen.getByText('Export')).toBeInTheDocument()
88
+ })
89
+
90
+ it('handles search functionality', () => {
91
+ render(
92
+ <DataTable
93
+ columns={mockColumns}
94
+ data={mockData}
95
+ searchable
96
+ />
97
+ )
98
+
99
+ const searchInput = screen.getByPlaceholderText('Search all columns...')
100
+ fireEvent.change(searchInput, { target: { value: 'john' } })
101
+
102
+ expect(screen.getByText('John Doe')).toBeInTheDocument()
103
+ // Jane Smith should be filtered out
104
+ expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument()
105
+ })
106
+
107
+ it('handles pagination', () => {
108
+ // Create more data for pagination testing
109
+ const moreData = Array.from({ length: 20 }, (_, i) => ({
110
+ id: i + 1,
111
+ name: `User ${i + 1}`,
112
+ email: `user${i + 1}@example.com`,
113
+ status: i % 2 === 0 ? 'active' : 'inactive'
114
+ }))
115
+
116
+ render(
117
+ <DataTable
118
+ columns={mockColumns}
119
+ data={moreData}
120
+ pagination
121
+ pageSize={10}
122
+ />
123
+ )
124
+
125
+ expect(screen.getByText('Page 1 of 2')).toBeInTheDocument()
126
+ expect(screen.getByText('Rows per page')).toBeInTheDocument()
127
+ })
128
+
129
+ it('calls onRowSelect when row is selected', () => {
130
+ const mockOnRowSelect = jest.fn()
131
+
132
+ render(
133
+ <DataTable
134
+ columns={mockColumns}
135
+ data={mockData}
136
+ selectable
137
+ onRowSelect={mockOnRowSelect}
138
+ />
139
+ )
140
+
141
+ // This would require implementing selection in the component
142
+ // For now, just verify the component renders with selection enabled
143
+ expect(screen.getByRole('table')).toBeInTheDocument()
144
+ })
145
+
146
+ it('calls onExport when export button is clicked', () => {
147
+ const mockOnExport = jest.fn()
148
+
149
+ render(
150
+ <DataTable
151
+ columns={mockColumns}
152
+ data={mockData}
153
+ exportable
154
+ onExport={mockOnExport}
155
+ />
156
+ )
157
+
158
+ const exportButton = screen.getByText('Export')
159
+ fireEvent.click(exportButton)
160
+
161
+ expect(mockOnExport).toHaveBeenCalledWith(mockData)
162
+ })
163
+
164
+ it('handles empty data state', () => {
165
+ render(
166
+ <DataTable
167
+ columns={mockColumns}
168
+ data={[]}
169
+ />
170
+ )
171
+
172
+ expect(screen.getByText('No results found.')).toBeInTheDocument()
173
+ })
174
+
175
+ it('applies custom className', () => {
176
+ render(
177
+ <DataTable
178
+ columns={mockColumns}
179
+ data={mockData}
180
+ className="custom-table"
181
+ />
182
+ )
183
+
184
+ const tableContainer = screen.getByRole('table').closest('div')
185
+ expect(tableContainer).toHaveClass('custom-table')
186
+ })
187
+ })