@moontra/moonui 2.0.7 → 2.0.9

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 (94) hide show
  1. package/package.json +26 -19
  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/ui/__tests__/accordion.test.tsx +571 -0
  6. package/src/components/ui/__tests__/alert.test.tsx +484 -0
  7. package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
  8. package/src/components/ui/__tests__/avatar.test.tsx +382 -0
  9. package/src/components/ui/__tests__/badge.test.tsx +364 -0
  10. package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
  11. package/src/components/ui/__tests__/button.test.tsx +91 -0
  12. package/src/components/ui/__tests__/card.test.tsx +104 -0
  13. package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
  14. package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
  15. package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
  16. package/src/components/ui/__tests__/command.test.tsx +677 -0
  17. package/src/components/ui/__tests__/data-table.test.tsx +256 -0
  18. package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
  19. package/src/components/ui/__tests__/dialog.test.tsx +764 -0
  20. package/src/components/ui/__tests__/input.test.tsx +318 -0
  21. package/src/components/ui/__tests__/label.test.tsx +103 -0
  22. package/src/components/ui/__tests__/popover.test.tsx +637 -0
  23. package/src/components/ui/__tests__/progress.test.tsx +382 -0
  24. package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
  25. package/src/components/ui/__tests__/select.test.tsx +705 -0
  26. package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
  27. package/src/components/ui/__tests__/slider.test.tsx +493 -0
  28. package/src/components/ui/__tests__/switch.test.tsx +372 -0
  29. package/src/components/ui/__tests__/table.test.tsx +824 -0
  30. package/src/components/ui/__tests__/tabs.test.tsx +887 -0
  31. package/src/components/ui/__tests__/toast.test.tsx +458 -0
  32. package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
  33. package/src/components/ui/accordion.tsx +63 -0
  34. package/src/components/ui/alert.tsx +130 -0
  35. package/src/components/ui/aspect-ratio.tsx +72 -0
  36. package/src/components/ui/avatar.tsx +135 -0
  37. package/src/components/ui/badge.tsx +225 -0
  38. package/src/components/ui/breadcrumb.tsx +207 -0
  39. package/src/components/ui/button.stories.tsx +162 -0
  40. package/src/components/ui/button.tsx +221 -0
  41. package/src/components/ui/calendar.tsx +262 -0
  42. package/src/components/ui/card.stories.tsx +208 -0
  43. package/src/components/ui/card.tsx +141 -0
  44. package/src/components/ui/checkbox.tsx +256 -0
  45. package/src/components/ui/collapsible.tsx +129 -0
  46. package/src/components/ui/color-picker.tsx +664 -0
  47. package/src/components/ui/command.tsx +218 -0
  48. package/src/components/ui/data-table.stories.tsx +509 -0
  49. package/src/components/ui/data-table.tsx +623 -0
  50. package/src/components/ui/date-picker.stories.tsx +321 -0
  51. package/src/components/ui/date-picker.tsx +603 -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/file-upload.tsx +400 -0
  55. package/src/components/ui/github-stars.tsx +201 -0
  56. package/src/components/ui/index.ts +12 -0
  57. package/src/components/ui/input.stories.tsx +255 -0
  58. package/src/components/ui/input.tsx +219 -0
  59. package/src/components/ui/label.tsx +26 -0
  60. package/src/components/ui/locked-component.tsx +215 -0
  61. package/src/components/ui/moon-logo.tsx +97 -0
  62. package/src/components/ui/pagination.tsx +116 -0
  63. package/src/components/ui/popover.tsx +183 -0
  64. package/src/components/ui/progress.tsx +182 -0
  65. package/src/components/ui/radio-group.tsx +243 -0
  66. package/src/components/ui/select.tsx +273 -0
  67. package/src/components/ui/separator.tsx +140 -0
  68. package/src/components/ui/simple-editor.tsx +652 -0
  69. package/src/components/ui/skeleton.tsx +351 -0
  70. package/src/components/ui/slider.tsx +351 -0
  71. package/src/components/ui/switch.tsx +83 -0
  72. package/src/components/ui/table.tsx +322 -0
  73. package/src/components/ui/tabs.tsx +195 -0
  74. package/src/components/ui/textarea.tsx +46 -0
  75. package/src/components/ui/toast.tsx +313 -0
  76. package/src/components/ui/toggle.tsx +47 -0
  77. package/src/components/ui/tooltip.tsx +152 -0
  78. package/src/docs/theme-system.md +1194 -0
  79. package/src/index.tsx +39 -0
  80. package/src/lib/micro-interactions.ts +255 -0
  81. package/src/lib/theme.tsx +398 -0
  82. package/src/lib/utils.ts +69 -0
  83. package/src/styles/design-system.css +365 -0
  84. package/src/styles/index.css +4 -0
  85. package/src/styles/tailwind.css +6 -0
  86. package/src/styles/tokens.css +453 -0
  87. package/src/use-intersection-observer.tsx +154 -0
  88. package/src/use-local-storage.tsx +71 -0
  89. package/src/use-paddle.ts +138 -0
  90. package/src/use-performance-optimizer.ts +379 -0
  91. package/src/use-pro-access.ts +141 -0
  92. package/src/use-scroll-animation.ts +221 -0
  93. package/src/use-subscription.ts +37 -0
  94. package/src/use-toast.ts +32 -0
@@ -0,0 +1,400 @@
1
+ "use client"
2
+
3
+ import React, { useCallback, useState, useId } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { Button } from './button'
6
+ import { Progress } from './progress'
7
+ import { Badge } from './badge'
8
+ import { cn } from '../../lib/utils'
9
+ import {
10
+ Upload,
11
+ X,
12
+ File,
13
+ Image as ImageIcon,
14
+ Video,
15
+ Music,
16
+ FileText,
17
+ CheckCircle,
18
+ AlertCircle,
19
+ Loader2
20
+ } from 'lucide-react'
21
+
22
+ export interface FileUploadProps {
23
+ accept?: string
24
+ multiple?: boolean
25
+ maxSize?: number // in bytes
26
+ maxFiles?: number
27
+ disabled?: boolean
28
+ className?: string
29
+ onUpload?: (files: File[]) => Promise<void>
30
+ onRemove?: (fileId: string) => void
31
+ children?: React.ReactNode
32
+ }
33
+
34
+ interface UploadedFile {
35
+ id: string
36
+ file: File
37
+ progress: number
38
+ status: 'uploading' | 'success' | 'error'
39
+ preview?: string
40
+ }
41
+
42
+ const getFileIcon = (type: string) => {
43
+ if (type.startsWith('image/')) return <ImageIcon className="h-4 w-4" />
44
+ if (type.startsWith('video/')) return <Video className="h-4 w-4" />
45
+ if (type.startsWith('audio/')) return <Music className="h-4 w-4" />
46
+ if (type.includes('text') || type.includes('document')) return <FileText className="h-4 w-4" />
47
+ return <File className="h-4 w-4" />
48
+ }
49
+
50
+ const formatFileSize = (bytes: number): string => {
51
+ if (bytes === 0) return '0 Bytes'
52
+ const k = 1024
53
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
54
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
55
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
56
+ }
57
+
58
+ const FileItem = ({
59
+ uploadedFile,
60
+ onRemove
61
+ }: {
62
+ uploadedFile: UploadedFile
63
+ onRemove: (id: string) => void
64
+ }) => {
65
+ const { id, file, progress, status, preview } = uploadedFile
66
+
67
+ return (
68
+ <motion.div
69
+ initial={{ opacity: 0, y: 10 }}
70
+ animate={{ opacity: 1, y: 0 }}
71
+ exit={{ opacity: 0, y: -10 }}
72
+ className="flex items-center space-x-3 p-3 border rounded-lg bg-background"
73
+ >
74
+ {/* File Preview/Icon */}
75
+ <div className="flex-shrink-0">
76
+ {preview ? (
77
+ <img
78
+ src={preview}
79
+ alt={file.name}
80
+ className="h-10 w-10 object-cover rounded"
81
+ />
82
+ ) : (
83
+ <div className="h-10 w-10 rounded bg-muted flex items-center justify-center">
84
+ {getFileIcon(file.type)}
85
+ </div>
86
+ )}
87
+ </div>
88
+
89
+ {/* File Info */}
90
+ <div className="flex-1 min-w-0">
91
+ <div className="flex items-center justify-between">
92
+ <p className="text-sm font-medium truncate">{file.name}</p>
93
+ <Button
94
+ variant="ghost"
95
+ size="sm"
96
+ className="h-6 w-6 p-0"
97
+ onClick={() => onRemove(id)}
98
+ >
99
+ <X className="h-3 w-3" />
100
+ </Button>
101
+ </div>
102
+
103
+ <div className="flex items-center space-x-2 mt-1">
104
+ <p className="text-xs text-muted-foreground">{formatFileSize(file.size)}</p>
105
+
106
+ {/* Status Badge */}
107
+ {status === 'uploading' && (
108
+ <Badge variant="secondary" className="h-5">
109
+ <Loader2 className="h-3 w-3 mr-1 animate-spin" />
110
+ Uploading
111
+ </Badge>
112
+ )}
113
+ {status === 'success' && (
114
+ <Badge variant="default" className="h-5 bg-green-500">
115
+ <CheckCircle className="h-3 w-3 mr-1" />
116
+ Done
117
+ </Badge>
118
+ )}
119
+ {status === 'error' && (
120
+ <Badge variant="destructive" className="h-5">
121
+ <AlertCircle className="h-3 w-3 mr-1" />
122
+ Error
123
+ </Badge>
124
+ )}
125
+ </div>
126
+
127
+ {/* Progress Bar */}
128
+ {status === 'uploading' && (
129
+ <div className="mt-2">
130
+ <Progress value={progress} className="h-1" />
131
+ </div>
132
+ )}
133
+ </div>
134
+ </motion.div>
135
+ )
136
+ }
137
+
138
+ export const FileUpload = React.forwardRef<HTMLDivElement, FileUploadProps>(
139
+ ({
140
+ accept = "*",
141
+ multiple = true,
142
+ maxSize = 10 * 1024 * 1024, // 10MB
143
+ maxFiles = 5,
144
+ disabled = false,
145
+ className,
146
+ onUpload,
147
+ onRemove,
148
+ children,
149
+ ...props
150
+ }, ref) => {
151
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
152
+ const [isDragOver, setIsDragOver] = useState(false)
153
+ const [error, setError] = useState<string | null>(null)
154
+
155
+ const validateFile = (file: File): string | null => {
156
+ if (file.size > maxSize) {
157
+ return `File size exceeds ${formatFileSize(maxSize)}`
158
+ }
159
+
160
+ if (accept !== "*" && !accept.split(',').some(type => {
161
+ const cleanType = type.trim()
162
+ if (cleanType.includes('*')) {
163
+ return file.type.startsWith(cleanType.replace('*', ''))
164
+ }
165
+ return file.type === cleanType
166
+ })) {
167
+ return `File type not supported`
168
+ }
169
+
170
+ return null
171
+ }
172
+
173
+ const processFiles = useCallback(async (files: FileList | File[]) => {
174
+ const fileArray = Array.from(files)
175
+ setError(null)
176
+
177
+ // Check total file count
178
+ if (uploadedFiles.length + fileArray.length > maxFiles) {
179
+ setError(`Maximum ${maxFiles} files allowed`)
180
+ return
181
+ }
182
+
183
+ const validFiles: File[] = []
184
+ const errors: string[] = []
185
+
186
+ // Validate each file
187
+ fileArray.forEach(file => {
188
+ const validationError = validateFile(file)
189
+ if (validationError) {
190
+ errors.push(`${file.name}: ${validationError}`)
191
+ } else {
192
+ validFiles.push(file)
193
+ }
194
+ })
195
+
196
+ if (errors.length > 0) {
197
+ setError(errors.join(', '))
198
+ return
199
+ }
200
+
201
+ // Create uploaded file entries
202
+ const newUploadedFiles: UploadedFile[] = validFiles.map((file, index) => ({
203
+ id: `${Date.now()}-${index}-${file.name.replace(/[^a-zA-Z0-9]/g, '')}`,
204
+ file,
205
+ progress: 0,
206
+ status: 'uploading' as const,
207
+ preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined
208
+ }))
209
+
210
+ setUploadedFiles(prev => [...prev, ...newUploadedFiles])
211
+
212
+ // Simulate upload progress
213
+ for (const uploadedFile of newUploadedFiles) {
214
+ let progress = 0
215
+ const interval = setInterval(() => {
216
+ progress += 15 + (uploadedFile.file.size % 10) // Deterministic progress based on file size
217
+ if (progress >= 100) {
218
+ progress = 100
219
+ clearInterval(interval)
220
+
221
+ setUploadedFiles(prev =>
222
+ prev.map(f =>
223
+ f.id === uploadedFile.id
224
+ ? { ...f, progress: 100, status: 'success' as const }
225
+ : f
226
+ )
227
+ )
228
+ } else {
229
+ setUploadedFiles(prev =>
230
+ prev.map(f =>
231
+ f.id === uploadedFile.id
232
+ ? { ...f, progress }
233
+ : f
234
+ )
235
+ )
236
+ }
237
+ }, 200)
238
+ }
239
+
240
+ // Call onUpload if provided
241
+ if (onUpload) {
242
+ try {
243
+ await onUpload(validFiles)
244
+ } catch (error) {
245
+ console.error('Upload failed:', error)
246
+ setUploadedFiles(prev =>
247
+ prev.map(f =>
248
+ newUploadedFiles.some(nf => nf.id === f.id)
249
+ ? { ...f, status: 'error' as const }
250
+ : f
251
+ )
252
+ )
253
+ }
254
+ }
255
+ }, [uploadedFiles.length, maxFiles, maxSize, accept, onUpload])
256
+
257
+ const handleDrop = useCallback((e: React.DragEvent) => {
258
+ e.preventDefault()
259
+ setIsDragOver(false)
260
+
261
+ if (disabled) return
262
+
263
+ const files = e.dataTransfer.files
264
+ if (files.length > 0) {
265
+ processFiles(files)
266
+ }
267
+ }, [processFiles, disabled])
268
+
269
+ const handleDragOver = useCallback((e: React.DragEvent) => {
270
+ e.preventDefault()
271
+ if (!disabled) {
272
+ setIsDragOver(true)
273
+ }
274
+ }, [disabled])
275
+
276
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
277
+ e.preventDefault()
278
+ setIsDragOver(false)
279
+ }, [])
280
+
281
+ const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
282
+ const files = e.target.files
283
+ if (files && files.length > 0) {
284
+ processFiles(files)
285
+ }
286
+ // Reset input value
287
+ e.target.value = ''
288
+ }, [processFiles])
289
+
290
+ const removeFile = useCallback((fileId: string) => {
291
+ setUploadedFiles(prev => {
292
+ const fileToRemove = prev.find(f => f.id === fileId)
293
+ if (fileToRemove?.preview) {
294
+ URL.revokeObjectURL(fileToRemove.preview)
295
+ }
296
+ return prev.filter(f => f.id !== fileId)
297
+ })
298
+
299
+ if (onRemove) {
300
+ onRemove(fileId)
301
+ }
302
+ }, [onRemove])
303
+
304
+ return (
305
+ <div ref={ref} className={cn("space-y-4", className)} {...props}>
306
+ {/* Upload Area */}
307
+ <motion.div
308
+ className={cn(
309
+ "relative border-2 border-dashed rounded-lg p-8 text-center transition-colors",
310
+ isDragOver && "border-primary bg-primary/5",
311
+ disabled && "opacity-50 cursor-not-allowed",
312
+ !disabled && "hover:border-primary/50 cursor-pointer"
313
+ )}
314
+ onDrop={handleDrop}
315
+ onDragOver={handleDragOver}
316
+ onDragLeave={handleDragLeave}
317
+ animate={{
318
+ scale: isDragOver ? 1.02 : 1,
319
+ borderColor: isDragOver ? "hsl(var(--primary))" : "hsl(var(--border))"
320
+ }}
321
+ >
322
+ <input
323
+ type="file"
324
+ accept={accept}
325
+ multiple={multiple}
326
+ disabled={disabled}
327
+ onChange={handleFileSelect}
328
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
329
+ />
330
+
331
+ {children || (
332
+ <div className="space-y-4">
333
+ <div className="mx-auto h-12 w-12 text-muted-foreground">
334
+ <Upload className="h-full w-full" />
335
+ </div>
336
+ <div>
337
+ <p className="text-lg font-medium">
338
+ {isDragOver ? 'Drop files here' : 'Upload files'}
339
+ </p>
340
+ <p className="text-sm text-muted-foreground">
341
+ Drag & drop files here, or click to select
342
+ </p>
343
+ <p className="text-xs text-muted-foreground mt-2">
344
+ Max {maxFiles} files, {formatFileSize(maxSize)} each
345
+ </p>
346
+ </div>
347
+ <Button variant="outline" disabled={disabled}>
348
+ Select Files
349
+ </Button>
350
+ </div>
351
+ )}
352
+ </motion.div>
353
+
354
+ {/* Error Message */}
355
+ <AnimatePresence>
356
+ {error && (
357
+ <motion.div
358
+ initial={{ opacity: 0, y: -10 }}
359
+ animate={{ opacity: 1, y: 0 }}
360
+ exit={{ opacity: 0, y: -10 }}
361
+ className="flex items-center space-x-2 text-sm text-destructive"
362
+ >
363
+ <AlertCircle className="h-4 w-4" />
364
+ <span>{error}</span>
365
+ </motion.div>
366
+ )}
367
+ </AnimatePresence>
368
+
369
+ {/* Uploaded Files */}
370
+ <AnimatePresence>
371
+ {uploadedFiles.length > 0 && (
372
+ <motion.div
373
+ initial={{ opacity: 0, height: 0 }}
374
+ animate={{ opacity: 1, height: 'auto' }}
375
+ exit={{ opacity: 0, height: 0 }}
376
+ className="space-y-2"
377
+ >
378
+ <h4 className="text-sm font-medium">
379
+ Uploaded Files ({uploadedFiles.length})
380
+ </h4>
381
+ <div className="space-y-2">
382
+ <AnimatePresence>
383
+ {uploadedFiles.map(uploadedFile => (
384
+ <FileItem
385
+ key={uploadedFile.id}
386
+ uploadedFile={uploadedFile}
387
+ onRemove={removeFile}
388
+ />
389
+ ))}
390
+ </AnimatePresence>
391
+ </div>
392
+ </motion.div>
393
+ )}
394
+ </AnimatePresence>
395
+ </div>
396
+ )
397
+ }
398
+ )
399
+
400
+ FileUpload.displayName = "FileUpload"
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Star, GitFork, Eye, ExternalLink } from 'lucide-react';
5
+ import { Button } from './button';
6
+ import { cn } from '../../lib/utils';
7
+
8
+ interface GitHubStarsProps {
9
+ className?: string;
10
+ size?: 'sm' | 'md' | 'lg';
11
+ showForks?: boolean;
12
+ showWatchers?: boolean;
13
+ repoUrl?: string;
14
+ }
15
+
16
+ interface GitHubStats {
17
+ stars: number;
18
+ forks: number;
19
+ watchers: number;
20
+ isLive: boolean;
21
+ }
22
+
23
+ export function GitHubStars({
24
+ className,
25
+ size = 'md',
26
+ showForks = true,
27
+ showWatchers = false,
28
+ repoUrl = 'https://github.com/moonui/moonui'
29
+ }: GitHubStarsProps) {
30
+ const [stats, setStats] = useState<GitHubStats | null>(null);
31
+ const [isLoading, setIsLoading] = useState(true);
32
+
33
+ useEffect(() => {
34
+ const fetchStats = async () => {
35
+ try {
36
+ const response = await fetch('/api/github-stars');
37
+ const data = await response.json();
38
+ setStats(data);
39
+ } catch (error) {
40
+ console.error('Failed to fetch GitHub stats:', error);
41
+ // Set fallback data
42
+ setStats({
43
+ stars: 1240,
44
+ forks: 89,
45
+ watchers: 156,
46
+ isLive: false
47
+ });
48
+ } finally {
49
+ setIsLoading(false);
50
+ }
51
+ };
52
+
53
+ fetchStats();
54
+ }, []);
55
+
56
+ const formatNumber = (num: number) => {
57
+ if (num >= 1000) {
58
+ return (num / 1000).toFixed(1) + 'k';
59
+ }
60
+ return num.toString();
61
+ };
62
+
63
+ const sizeClasses = {
64
+ sm: 'text-sm gap-2',
65
+ md: 'text-base gap-3',
66
+ lg: 'text-lg gap-4'
67
+ };
68
+
69
+ const iconSizes = {
70
+ sm: 14,
71
+ md: 16,
72
+ lg: 20
73
+ };
74
+
75
+ if (isLoading) {
76
+ return (
77
+ <div className={cn("flex items-center animate-pulse", sizeClasses[size], className)}>
78
+ <div className="flex items-center gap-1">
79
+ <div className="w-4 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
80
+ <div className="w-8 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
81
+ </div>
82
+ {showForks && (
83
+ <div className="flex items-center gap-1">
84
+ <div className="w-4 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
85
+ <div className="w-6 h-4 bg-gray-300 dark:bg-gray-700 rounded"></div>
86
+ </div>
87
+ )}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ if (!stats) {
93
+ return null;
94
+ }
95
+
96
+ return (
97
+ <div className={cn("flex items-center", sizeClasses[size], className)}>
98
+ <a
99
+ href={repoUrl}
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ className="flex items-center gap-1 text-gray-700 dark:text-gray-300 hover:text-primary transition-colors"
103
+ >
104
+ <Star className="fill-current text-yellow-500" size={iconSizes[size]} />
105
+ <span className="font-medium">{formatNumber(stats.stars)}</span>
106
+ </a>
107
+
108
+ {showForks && (
109
+ <a
110
+ href={`${repoUrl}/network/members`}
111
+ target="_blank"
112
+ rel="noopener noreferrer"
113
+ className="flex items-center gap-1 text-gray-700 dark:text-gray-300 hover:text-primary transition-colors"
114
+ >
115
+ <GitFork size={iconSizes[size]} />
116
+ <span className="font-medium">{formatNumber(stats.forks)}</span>
117
+ </a>
118
+ )}
119
+
120
+ {showWatchers && (
121
+ <a
122
+ href={`${repoUrl}/watchers`}
123
+ target="_blank"
124
+ rel="noopener noreferrer"
125
+ className="flex items-center gap-1 text-gray-700 dark:text-gray-300 hover:text-primary transition-colors"
126
+ >
127
+ <Eye size={iconSizes[size]} />
128
+ <span className="font-medium">{formatNumber(stats.watchers)}</span>
129
+ </a>
130
+ )}
131
+
132
+ {!stats.isLive && (
133
+ <span className="text-xs text-gray-500 dark:text-gray-500 ml-2">
134
+ (demo)
135
+ </span>
136
+ )}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ export function GitHubStarButton({
142
+ className,
143
+ size = 'md',
144
+ repoUrl = 'https://github.com/moonui/moonui'
145
+ }: {
146
+ className?: string;
147
+ size?: 'sm' | 'md' | 'lg';
148
+ repoUrl?: string;
149
+ }) {
150
+ const [stars, setStars] = useState<number | null>(null);
151
+ const [isLoading, setIsLoading] = useState(true);
152
+
153
+ useEffect(() => {
154
+ const fetchStats = async () => {
155
+ try {
156
+ const response = await fetch('/api/github-stars');
157
+ const data = await response.json();
158
+ setStars(data.stars);
159
+ } catch (error) {
160
+ console.error('Failed to fetch GitHub stats:', error);
161
+ setStars(1240);
162
+ } finally {
163
+ setIsLoading(false);
164
+ }
165
+ };
166
+
167
+ fetchStats();
168
+ }, []);
169
+
170
+ const formatNumber = (num: number) => {
171
+ if (num >= 1000) {
172
+ return (num / 1000).toFixed(1) + 'k';
173
+ }
174
+ return num.toString();
175
+ };
176
+
177
+ return (
178
+ <Button
179
+ variant="outline"
180
+ size={size}
181
+ className={cn("group", className)}
182
+ asChild
183
+ >
184
+ <a
185
+ href={repoUrl}
186
+ target="_blank"
187
+ rel="noopener noreferrer"
188
+ className="flex items-center gap-2"
189
+ >
190
+ <Star className="fill-current text-yellow-500 group-hover:scale-110 transition-transform" size={16} />
191
+ <span>Star on GitHub</span>
192
+ {!isLoading && stars && (
193
+ <span className="px-2 py-0.5 bg-gray-100 dark:bg-gray-800 rounded-full text-xs font-medium">
194
+ {formatNumber(stars)}
195
+ </span>
196
+ )}
197
+ <ExternalLink size={14} className="opacity-50 group-hover:opacity-100 transition-opacity" />
198
+ </a>
199
+ </Button>
200
+ );
201
+ }
@@ -0,0 +1,12 @@
1
+ // FREE UI Components - Source of Truth: @moontra/moonui NPM package
2
+ // Ana proje artık NPM paketlerini kullanır (wrapper pattern)
3
+
4
+ // All FREE components from @moontra/moonui package
5
+ export * from "@moontra/moonui"
6
+
7
+ // Local development-only components (not in package)
8
+ export * from "./locked-component"
9
+ export * from "./moon-logo"
10
+
11
+ // Pro Components - Available via @moontra/moonui-pro package
12
+ // Import Pro components via: import { Component } from "@/components/pro"