@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.
- package/package.json +26 -19
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/ui/__tests__/accordion.test.tsx +571 -0
- package/src/components/ui/__tests__/alert.test.tsx +484 -0
- package/src/components/ui/__tests__/aspect-ratio.test.tsx +492 -0
- package/src/components/ui/__tests__/avatar.test.tsx +382 -0
- package/src/components/ui/__tests__/badge.test.tsx +364 -0
- package/src/components/ui/__tests__/breadcrumb.test.tsx +687 -0
- package/src/components/ui/__tests__/button.test.tsx +91 -0
- package/src/components/ui/__tests__/card.test.tsx +104 -0
- package/src/components/ui/__tests__/checkbox.test.tsx +591 -0
- package/src/components/ui/__tests__/collapsible.test.tsx +592 -0
- package/src/components/ui/__tests__/color-picker.test.tsx +365 -0
- package/src/components/ui/__tests__/command.test.tsx +677 -0
- package/src/components/ui/__tests__/data-table.test.tsx +256 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +320 -0
- package/src/components/ui/__tests__/dialog.test.tsx +764 -0
- package/src/components/ui/__tests__/input.test.tsx +318 -0
- package/src/components/ui/__tests__/label.test.tsx +103 -0
- package/src/components/ui/__tests__/popover.test.tsx +637 -0
- package/src/components/ui/__tests__/progress.test.tsx +382 -0
- package/src/components/ui/__tests__/radio-group.test.tsx +555 -0
- package/src/components/ui/__tests__/select.test.tsx +705 -0
- package/src/components/ui/__tests__/skeleton.test.tsx +253 -0
- package/src/components/ui/__tests__/slider.test.tsx +493 -0
- package/src/components/ui/__tests__/switch.test.tsx +372 -0
- package/src/components/ui/__tests__/table.test.tsx +824 -0
- package/src/components/ui/__tests__/tabs.test.tsx +887 -0
- package/src/components/ui/__tests__/toast.test.tsx +458 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +583 -0
- package/src/components/ui/accordion.tsx +63 -0
- package/src/components/ui/alert.tsx +130 -0
- package/src/components/ui/aspect-ratio.tsx +72 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/breadcrumb.tsx +207 -0
- package/src/components/ui/button.stories.tsx +162 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/calendar.tsx +262 -0
- package/src/components/ui/card.stories.tsx +208 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/collapsible.tsx +129 -0
- package/src/components/ui/color-picker.tsx +664 -0
- package/src/components/ui/command.tsx +218 -0
- package/src/components/ui/data-table.stories.tsx +509 -0
- package/src/components/ui/data-table.tsx +623 -0
- package/src/components/ui/date-picker.stories.tsx +321 -0
- package/src/components/ui/date-picker.tsx +603 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +400 -0
- package/src/components/ui/github-stars.tsx +201 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/input.stories.tsx +255 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/locked-component.tsx +215 -0
- package/src/components/ui/moon-logo.tsx +97 -0
- package/src/components/ui/pagination.tsx +116 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/progress.tsx +182 -0
- package/src/components/ui/radio-group.tsx +243 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/simple-editor.tsx +652 -0
- package/src/components/ui/skeleton.tsx +351 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +322 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +46 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/docs/theme-system.md +1194 -0
- package/src/index.tsx +39 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/theme.tsx +398 -0
- package/src/lib/utils.ts +69 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { motion } from "framer-motion";
|
|
7
|
+
import { skeletonAnimation } from "@/lib/micro-interactions";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Skeleton Component
|
|
11
|
+
*
|
|
12
|
+
* A loading placeholder that mimics the structure of content.
|
|
13
|
+
* Fully integrated with the theme system and provides various shapes, sizes, and animations.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const skeletonVariants = cva(
|
|
17
|
+
"animate-pulse rounded-md",
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: "bg-muted",
|
|
22
|
+
primary: "bg-primary/10",
|
|
23
|
+
secondary: "bg-secondary/10",
|
|
24
|
+
accent: "bg-accent",
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
default: "",
|
|
28
|
+
sm: "scale-90",
|
|
29
|
+
lg: "scale-110",
|
|
30
|
+
},
|
|
31
|
+
shape: {
|
|
32
|
+
rect: "rounded-md",
|
|
33
|
+
circle: "rounded-full",
|
|
34
|
+
pill: "rounded-full",
|
|
35
|
+
},
|
|
36
|
+
animation: {
|
|
37
|
+
pulse: "animate-pulse",
|
|
38
|
+
wave: "overflow-hidden relative after:absolute after:inset-0 after:-translate-x-full after:animate-[shimmer_2s_infinite] after:bg-gradient-to-r after:from-transparent after:via-white/10 after:to-transparent dark:after:via-white/5",
|
|
39
|
+
none: "animate-none",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: {
|
|
43
|
+
variant: "default",
|
|
44
|
+
size: "default",
|
|
45
|
+
shape: "rect",
|
|
46
|
+
animation: "pulse",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export interface SkeletonProps
|
|
52
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
53
|
+
VariantProps<typeof skeletonVariants> {
|
|
54
|
+
/**
|
|
55
|
+
* Height of the skeleton
|
|
56
|
+
*/
|
|
57
|
+
height?: string | number;
|
|
58
|
+
/**
|
|
59
|
+
* Width of the skeleton
|
|
60
|
+
*/
|
|
61
|
+
width?: string | number;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the content is loaded
|
|
64
|
+
*/
|
|
65
|
+
isLoaded?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Content to show when loaded
|
|
68
|
+
*/
|
|
69
|
+
children?: React.ReactNode;
|
|
70
|
+
/**
|
|
71
|
+
* Fade-in animation duration in ms
|
|
72
|
+
*/
|
|
73
|
+
fadeInDuration?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Use motion animation for loading state
|
|
76
|
+
*/
|
|
77
|
+
motion?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
|
|
81
|
+
({
|
|
82
|
+
className,
|
|
83
|
+
variant,
|
|
84
|
+
size,
|
|
85
|
+
shape,
|
|
86
|
+
animation,
|
|
87
|
+
height,
|
|
88
|
+
width,
|
|
89
|
+
isLoaded = false,
|
|
90
|
+
children,
|
|
91
|
+
fadeInDuration = 400,
|
|
92
|
+
motion: useMotion = false,
|
|
93
|
+
style,
|
|
94
|
+
...props
|
|
95
|
+
}, ref) => {
|
|
96
|
+
// Show content when loaded
|
|
97
|
+
if (isLoaded && children) {
|
|
98
|
+
if (useMotion) {
|
|
99
|
+
return (
|
|
100
|
+
<motion.div
|
|
101
|
+
ref={ref}
|
|
102
|
+
initial={{ opacity: 0 }}
|
|
103
|
+
animate={{ opacity: 1 }}
|
|
104
|
+
transition={{ duration: fadeInDuration / 1000 }}
|
|
105
|
+
className={className}
|
|
106
|
+
style={{
|
|
107
|
+
height,
|
|
108
|
+
width,
|
|
109
|
+
...style,
|
|
110
|
+
}}
|
|
111
|
+
{...props}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</motion.div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
ref={ref}
|
|
121
|
+
className={cn("animate-fade-in", className)}
|
|
122
|
+
style={{
|
|
123
|
+
animationDuration: `${fadeInDuration}ms`,
|
|
124
|
+
height,
|
|
125
|
+
width,
|
|
126
|
+
...style,
|
|
127
|
+
}}
|
|
128
|
+
{...props}
|
|
129
|
+
>
|
|
130
|
+
{children}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const SkeletonElement = useMotion ? motion.div : "div";
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<SkeletonElement
|
|
139
|
+
ref={ref}
|
|
140
|
+
className={cn(
|
|
141
|
+
skeletonVariants({ variant, size, shape, animation }),
|
|
142
|
+
"relative isolate",
|
|
143
|
+
className
|
|
144
|
+
)}
|
|
145
|
+
style={{
|
|
146
|
+
height,
|
|
147
|
+
width,
|
|
148
|
+
...style,
|
|
149
|
+
}}
|
|
150
|
+
{...(useMotion ? skeletonAnimation : {})}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
Skeleton.displayName = "Skeleton";
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Text Skeleton Component
|
|
160
|
+
*
|
|
161
|
+
* Creates skeleton loading states for text content
|
|
162
|
+
*/
|
|
163
|
+
export interface SkeletonTextProps extends Omit<SkeletonProps, "children" | "isLoaded"> {
|
|
164
|
+
/**
|
|
165
|
+
* Kaç satır gösterileceği
|
|
166
|
+
*/
|
|
167
|
+
lines?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Satır yüksekliği
|
|
170
|
+
*/
|
|
171
|
+
lineHeight?: string | number;
|
|
172
|
+
/**
|
|
173
|
+
* Satırlar arası boşluk
|
|
174
|
+
*/
|
|
175
|
+
spacing?: string | number;
|
|
176
|
+
/**
|
|
177
|
+
* Son satırın genişlik yüzdesi (0-100)
|
|
178
|
+
*/
|
|
179
|
+
lastLineWidth?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Rastgele satır genişlikleri
|
|
182
|
+
*/
|
|
183
|
+
randomWidths?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
|
|
187
|
+
({
|
|
188
|
+
className,
|
|
189
|
+
lines = 3,
|
|
190
|
+
lineHeight = "0.85rem",
|
|
191
|
+
spacing = "0.5rem",
|
|
192
|
+
lastLineWidth = 80,
|
|
193
|
+
randomWidths = false,
|
|
194
|
+
variant = "default",
|
|
195
|
+
animation = "pulse",
|
|
196
|
+
...props
|
|
197
|
+
}, ref) => {
|
|
198
|
+
return (
|
|
199
|
+
<div
|
|
200
|
+
ref={ref}
|
|
201
|
+
className={cn("flex flex-col", className)}
|
|
202
|
+
style={{ gap: spacing }}
|
|
203
|
+
{...props}
|
|
204
|
+
>
|
|
205
|
+
{Array.from({ length: lines }).map((_, i) => {
|
|
206
|
+
const isLastLine = i === lines - 1;
|
|
207
|
+
// Use deterministic widths based on line index for SSR compatibility
|
|
208
|
+
const widthPercentage = isLastLine
|
|
209
|
+
? lastLineWidth
|
|
210
|
+
: randomWidths
|
|
211
|
+
? 85 + (i % 3) * 5 // Creates a pattern: 85%, 90%, 95%, 85%, ...
|
|
212
|
+
: 100;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<Skeleton
|
|
216
|
+
key={i}
|
|
217
|
+
variant={variant}
|
|
218
|
+
animation={animation}
|
|
219
|
+
className="w-full"
|
|
220
|
+
style={{
|
|
221
|
+
height: lineHeight,
|
|
222
|
+
width: `${widthPercentage}%`,
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
SkeletonText.displayName = "SkeletonText";
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Avatar Skeleton Component
|
|
235
|
+
*/
|
|
236
|
+
export interface SkeletonAvatarProps extends Omit<SkeletonProps, "shape" | "size"> {
|
|
237
|
+
/**
|
|
238
|
+
* Avatar boyutu (piksel)
|
|
239
|
+
*/
|
|
240
|
+
size?: string | number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const SkeletonAvatar = React.forwardRef<HTMLDivElement, SkeletonAvatarProps>(
|
|
244
|
+
({
|
|
245
|
+
className,
|
|
246
|
+
size = "2.5rem",
|
|
247
|
+
variant = "default",
|
|
248
|
+
animation = "pulse",
|
|
249
|
+
...props
|
|
250
|
+
}, ref) => {
|
|
251
|
+
return (
|
|
252
|
+
<Skeleton
|
|
253
|
+
ref={ref}
|
|
254
|
+
variant={variant}
|
|
255
|
+
animation={animation}
|
|
256
|
+
shape="circle"
|
|
257
|
+
className={cn("shrink-0", className)}
|
|
258
|
+
style={{
|
|
259
|
+
height: size,
|
|
260
|
+
width: size,
|
|
261
|
+
}}
|
|
262
|
+
{...props}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
SkeletonAvatar.displayName = "SkeletonAvatar";
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Card Skeleton Component
|
|
271
|
+
*/
|
|
272
|
+
export interface SkeletonCardProps extends Omit<SkeletonProps, "children" | "isLoaded"> {
|
|
273
|
+
/**
|
|
274
|
+
* Kart başlığının gösterilip gösterilmeyeceği
|
|
275
|
+
*/
|
|
276
|
+
showHeader?: boolean;
|
|
277
|
+
/**
|
|
278
|
+
* İçerik satır sayısı
|
|
279
|
+
*/
|
|
280
|
+
contentLines?: number;
|
|
281
|
+
/**
|
|
282
|
+
* Alt bilgi gösterilip gösterilmeyeceği
|
|
283
|
+
*/
|
|
284
|
+
showFooter?: boolean;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export const SkeletonCard = React.forwardRef<HTMLDivElement, SkeletonCardProps>(
|
|
288
|
+
({
|
|
289
|
+
className,
|
|
290
|
+
showHeader = true,
|
|
291
|
+
contentLines = 3,
|
|
292
|
+
showFooter = true,
|
|
293
|
+
variant = "default",
|
|
294
|
+
animation = "pulse",
|
|
295
|
+
...props
|
|
296
|
+
}, ref) => {
|
|
297
|
+
return (
|
|
298
|
+
<div
|
|
299
|
+
ref={ref}
|
|
300
|
+
className={cn(
|
|
301
|
+
"overflow-hidden rounded-md border border-border bg-background p-4",
|
|
302
|
+
className
|
|
303
|
+
)}
|
|
304
|
+
{...props}
|
|
305
|
+
>
|
|
306
|
+
{showHeader && (
|
|
307
|
+
<div className="mb-4 flex items-center gap-4">
|
|
308
|
+
<SkeletonAvatar size="2.5rem" variant={variant} animation={animation} />
|
|
309
|
+
<div className="flex-1">
|
|
310
|
+
<Skeleton
|
|
311
|
+
variant={variant}
|
|
312
|
+
animation={animation}
|
|
313
|
+
className="mb-2 h-4 w-1/3"
|
|
314
|
+
/>
|
|
315
|
+
<Skeleton
|
|
316
|
+
variant={variant}
|
|
317
|
+
animation={animation}
|
|
318
|
+
className="h-3 w-1/4"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
|
|
324
|
+
<SkeletonText
|
|
325
|
+
lines={contentLines}
|
|
326
|
+
variant={variant}
|
|
327
|
+
animation={animation}
|
|
328
|
+
className="mb-4"
|
|
329
|
+
/>
|
|
330
|
+
|
|
331
|
+
{showFooter && (
|
|
332
|
+
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border">
|
|
333
|
+
<Skeleton
|
|
334
|
+
variant={variant}
|
|
335
|
+
animation={animation}
|
|
336
|
+
className="h-4 w-1/4"
|
|
337
|
+
/>
|
|
338
|
+
<Skeleton
|
|
339
|
+
variant={variant}
|
|
340
|
+
animation={animation}
|
|
341
|
+
className="h-4 w-1/5"
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
SkeletonCard.displayName = "SkeletonCard";
|
|
350
|
+
|
|
351
|
+
export { skeletonVariants };
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Slider Component
|
|
10
|
+
*
|
|
11
|
+
* Accessible, customizable slider component fully integrated with the theme system.
|
|
12
|
+
* Used for value ranges like volume, brightness, price ranges.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const sliderVariants = cva(
|
|
16
|
+
"relative flex w-full touch-none select-none items-center",
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
sm: "h-5",
|
|
21
|
+
default: "h-6",
|
|
22
|
+
md: "h-8",
|
|
23
|
+
lg: "h-10",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
size: "default",
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const sliderTrackVariants = cva(
|
|
33
|
+
"relative h-1.5 w-full grow overflow-hidden rounded-full",
|
|
34
|
+
{
|
|
35
|
+
variants: {
|
|
36
|
+
variant: {
|
|
37
|
+
default: "bg-muted",
|
|
38
|
+
primary: "bg-primary/20",
|
|
39
|
+
secondary: "bg-secondary/20",
|
|
40
|
+
accent: "bg-accent/20",
|
|
41
|
+
success: "bg-success/20",
|
|
42
|
+
warning: "bg-warning/20",
|
|
43
|
+
error: "bg-error/20",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
defaultVariants: {
|
|
47
|
+
variant: "default",
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const sliderRangeVariants = cva(
|
|
53
|
+
"absolute h-full",
|
|
54
|
+
{
|
|
55
|
+
variants: {
|
|
56
|
+
variant: {
|
|
57
|
+
default: "bg-foreground",
|
|
58
|
+
primary: "bg-primary",
|
|
59
|
+
secondary: "bg-secondary",
|
|
60
|
+
accent: "bg-accent",
|
|
61
|
+
success: "bg-success",
|
|
62
|
+
warning: "bg-warning",
|
|
63
|
+
error: "bg-error",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
defaultVariants: {
|
|
67
|
+
variant: "primary",
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const sliderThumbVariants = cva(
|
|
73
|
+
"block rounded-full border-2 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
74
|
+
{
|
|
75
|
+
variants: {
|
|
76
|
+
variant: {
|
|
77
|
+
default: "border-foreground bg-background",
|
|
78
|
+
primary: "border-primary bg-background",
|
|
79
|
+
secondary: "border-secondary bg-background",
|
|
80
|
+
accent: "border-accent bg-background",
|
|
81
|
+
success: "border-success bg-background",
|
|
82
|
+
warning: "border-warning bg-background",
|
|
83
|
+
error: "border-error bg-background",
|
|
84
|
+
},
|
|
85
|
+
size: {
|
|
86
|
+
sm: "h-3 w-3",
|
|
87
|
+
default: "h-4 w-4",
|
|
88
|
+
md: "h-5 w-5",
|
|
89
|
+
lg: "h-6 w-6",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
defaultVariants: {
|
|
93
|
+
variant: "primary",
|
|
94
|
+
size: "default",
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// Custom type definition for component properties
|
|
100
|
+
type SliderBaseProps = {
|
|
101
|
+
/**
|
|
102
|
+
* Track variant
|
|
103
|
+
*/
|
|
104
|
+
trackVariant?: VariantProps<typeof sliderTrackVariants>["variant"];
|
|
105
|
+
/**
|
|
106
|
+
* Range variant
|
|
107
|
+
*/
|
|
108
|
+
rangeVariant?: VariantProps<typeof sliderRangeVariants>["variant"];
|
|
109
|
+
/**
|
|
110
|
+
* Thumb variant
|
|
111
|
+
*/
|
|
112
|
+
thumbVariant?: VariantProps<typeof sliderThumbVariants>["variant"];
|
|
113
|
+
/**
|
|
114
|
+
* Thumb size
|
|
115
|
+
*/
|
|
116
|
+
thumbSize?: VariantProps<typeof sliderThumbVariants>["size"];
|
|
117
|
+
/**
|
|
118
|
+
* Show value label
|
|
119
|
+
*/
|
|
120
|
+
showValueLabel?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Value label format function
|
|
123
|
+
*/
|
|
124
|
+
valueLabelFormat?: (value: number) => string;
|
|
125
|
+
/**
|
|
126
|
+
* Value label class name
|
|
127
|
+
*/
|
|
128
|
+
valueLabelClassName?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Slider value
|
|
131
|
+
*/
|
|
132
|
+
value?: number[];
|
|
133
|
+
/**
|
|
134
|
+
* Default value
|
|
135
|
+
*/
|
|
136
|
+
defaultValue?: number[];
|
|
137
|
+
/**
|
|
138
|
+
* Function called when value changes
|
|
139
|
+
*/
|
|
140
|
+
onValueChange?: (value: number[]) => void;
|
|
141
|
+
/**
|
|
142
|
+
* Minimum value
|
|
143
|
+
*/
|
|
144
|
+
min?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Maximum value
|
|
147
|
+
*/
|
|
148
|
+
max?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Step value
|
|
151
|
+
*/
|
|
152
|
+
step?: number;
|
|
153
|
+
/**
|
|
154
|
+
* Slider size
|
|
155
|
+
*/
|
|
156
|
+
size?: VariantProps<typeof sliderVariants>["size"];
|
|
157
|
+
/**
|
|
158
|
+
* Disabled state
|
|
159
|
+
*/
|
|
160
|
+
disabled?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Merge HTML properties without defaultValue conflicts
|
|
164
|
+
type SliderProps = SliderBaseProps & Omit<React.HTMLAttributes<HTMLDivElement>, 'defaultValue'>
|
|
165
|
+
|
|
166
|
+
const Slider = React.forwardRef<
|
|
167
|
+
HTMLDivElement,
|
|
168
|
+
SliderProps
|
|
169
|
+
>(({
|
|
170
|
+
className,
|
|
171
|
+
size,
|
|
172
|
+
trackVariant,
|
|
173
|
+
rangeVariant,
|
|
174
|
+
thumbVariant,
|
|
175
|
+
thumbSize,
|
|
176
|
+
showValueLabel = false,
|
|
177
|
+
valueLabelFormat,
|
|
178
|
+
valueLabelClassName,
|
|
179
|
+
value,
|
|
180
|
+
defaultValue = [0],
|
|
181
|
+
onValueChange,
|
|
182
|
+
disabled,
|
|
183
|
+
...props
|
|
184
|
+
}, ref) => {
|
|
185
|
+
// Value management
|
|
186
|
+
const [sliderValue, setSliderValue] = React.useState<number[]>(
|
|
187
|
+
value as number[] || defaultValue as number[] || [0]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
React.useEffect(() => {
|
|
191
|
+
if (value !== undefined) {
|
|
192
|
+
setSliderValue(value as number[]);
|
|
193
|
+
}
|
|
194
|
+
}, [value]);
|
|
195
|
+
|
|
196
|
+
// Format value label with proper precision handling
|
|
197
|
+
const formatValue = (val: number) => {
|
|
198
|
+
if (valueLabelFormat) {
|
|
199
|
+
return valueLabelFormat(val);
|
|
200
|
+
}
|
|
201
|
+
// Round to avoid floating point precision issues
|
|
202
|
+
const roundedValue = Math.round(val * 100) / 100;
|
|
203
|
+
return `${roundedValue}`;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Calculate percentage for thumb position
|
|
207
|
+
const calculateThumbPercent = (value: number, min: number, max: number) => {
|
|
208
|
+
return ((value - min) / (max - min)) * 100;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Handle mouse/touch events
|
|
212
|
+
const trackRef = React.useRef<HTMLDivElement>(null);
|
|
213
|
+
|
|
214
|
+
const min = props.min || 0;
|
|
215
|
+
const max = props.max || 100;
|
|
216
|
+
const step = props.step || 1;
|
|
217
|
+
|
|
218
|
+
// Handle value change with proper precision
|
|
219
|
+
const handleValueChange = (newValues: number[]) => {
|
|
220
|
+
// Round values to avoid floating point precision issues
|
|
221
|
+
const roundedValues = newValues.map(val => {
|
|
222
|
+
if (step < 1) {
|
|
223
|
+
// For decimal steps, round to appropriate decimal places
|
|
224
|
+
const decimals = step.toString().split('.')[1]?.length || 0;
|
|
225
|
+
return Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
226
|
+
}
|
|
227
|
+
return Math.round(val);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
setSliderValue(roundedValues);
|
|
231
|
+
if (onValueChange) {
|
|
232
|
+
onValueChange(roundedValues);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Handle track click
|
|
237
|
+
const handleTrackClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
238
|
+
if (disabled) return;
|
|
239
|
+
|
|
240
|
+
const track = trackRef.current;
|
|
241
|
+
if (!track) return;
|
|
242
|
+
|
|
243
|
+
const rect = track.getBoundingClientRect();
|
|
244
|
+
const percent = (event.clientX - rect.left) / rect.width;
|
|
245
|
+
const rawValue = min + percent * (max - min);
|
|
246
|
+
const steppedValue = Math.round(rawValue / step) * step;
|
|
247
|
+
const boundedValue = Math.max(min, Math.min(max, steppedValue));
|
|
248
|
+
|
|
249
|
+
const newValues = [...sliderValue];
|
|
250
|
+
// Just update the first thumb for simplicity
|
|
251
|
+
newValues[0] = boundedValue;
|
|
252
|
+
handleValueChange(newValues);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const handleThumbMouseDown = (index: number) => (event: React.MouseEvent) => {
|
|
256
|
+
if (disabled) return;
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
|
|
259
|
+
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
260
|
+
const track = trackRef.current;
|
|
261
|
+
if (!track) return;
|
|
262
|
+
|
|
263
|
+
const rect = track.getBoundingClientRect();
|
|
264
|
+
const percent = (moveEvent.clientX - rect.left) / rect.width;
|
|
265
|
+
const rawValue = min + percent * (max - min);
|
|
266
|
+
const steppedValue = Math.round(rawValue / step) * step;
|
|
267
|
+
const boundedValue = Math.max(min, Math.min(max, steppedValue));
|
|
268
|
+
|
|
269
|
+
const newValues = [...sliderValue];
|
|
270
|
+
newValues[index] = boundedValue;
|
|
271
|
+
handleValueChange(newValues);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const handleMouseUp = () => {
|
|
275
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
276
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
280
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div className="w-full" ref={ref} {...props}>
|
|
285
|
+
{showValueLabel && (
|
|
286
|
+
<div className={cn(
|
|
287
|
+
"flex justify-end mb-1 text-sm text-muted-foreground",
|
|
288
|
+
valueLabelClassName
|
|
289
|
+
)}>
|
|
290
|
+
{sliderValue.length === 1 ? (
|
|
291
|
+
<span>{formatValue(sliderValue[0])}</span>
|
|
292
|
+
) : (
|
|
293
|
+
<span>{formatValue(sliderValue[0])} - {formatValue(sliderValue[sliderValue.length - 1])}</span>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
<div
|
|
299
|
+
className={cn(sliderVariants({ size }), className)}
|
|
300
|
+
data-disabled={disabled ? true : undefined}
|
|
301
|
+
>
|
|
302
|
+
{/* Track */}
|
|
303
|
+
<div
|
|
304
|
+
ref={trackRef}
|
|
305
|
+
className={cn(sliderTrackVariants({ variant: trackVariant || thumbVariant || "default" }))}
|
|
306
|
+
onClick={handleTrackClick}
|
|
307
|
+
>
|
|
308
|
+
{/* Range */}
|
|
309
|
+
<div
|
|
310
|
+
className={cn(sliderRangeVariants({ variant: rangeVariant || thumbVariant || "primary" }))}
|
|
311
|
+
style={{
|
|
312
|
+
left: '0%',
|
|
313
|
+
width: `${calculateThumbPercent(sliderValue[0], min, max)}%`,
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Thumbs */}
|
|
319
|
+
{sliderValue.map((value, i) => (
|
|
320
|
+
<div
|
|
321
|
+
key={i}
|
|
322
|
+
className={cn(sliderThumbVariants({
|
|
323
|
+
variant: thumbVariant || "primary",
|
|
324
|
+
size: thumbSize
|
|
325
|
+
}))}
|
|
326
|
+
style={{
|
|
327
|
+
left: `${calculateThumbPercent(value, min, max)}%`,
|
|
328
|
+
transform: 'translateX(-50%)',
|
|
329
|
+
position: 'absolute',
|
|
330
|
+
top: '50%',
|
|
331
|
+
marginTop: thumbSize === "sm" ? '-6px' : thumbSize === "lg" ? '-12px' : '-8px',
|
|
332
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
333
|
+
}}
|
|
334
|
+
onMouseDown={handleThumbMouseDown(i)}
|
|
335
|
+
aria-label={`Thumb ${i + 1}`}
|
|
336
|
+
tabIndex={disabled ? -1 : 0}
|
|
337
|
+
role="slider"
|
|
338
|
+
aria-valuemin={min}
|
|
339
|
+
aria-valuemax={max}
|
|
340
|
+
aria-valuenow={value}
|
|
341
|
+
aria-disabled={disabled}
|
|
342
|
+
/>
|
|
343
|
+
))}
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
Slider.displayName = "Slider"
|
|
350
|
+
|
|
351
|
+
export { Slider }
|