gradient-forge 1.0.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.
- package/.eslintrc.json +3 -0
- package/.github/FUNDING.yml +2 -0
- package/README.md +140 -0
- package/app/docs/page.tsx +417 -0
- package/app/gallery/page.tsx +398 -0
- package/app/globals.css +1155 -0
- package/app/layout.tsx +36 -0
- package/app/page.tsx +600 -0
- package/app/showcase/page.tsx +730 -0
- package/app/studio/page.tsx +1310 -0
- package/cli/index.mjs +1141 -0
- package/cli/templates/theme-context.tsx +120 -0
- package/cli/templates/theme-engine.ts +237 -0
- package/cli/templates/themes.css +512 -0
- package/components/site/component-showcase.tsx +623 -0
- package/components/site/site-data.ts +103 -0
- package/components/site/site-header.tsx +270 -0
- package/components/templates/blog.tsx +198 -0
- package/components/templates/components-showcase.tsx +298 -0
- package/components/templates/dashboard.tsx +246 -0
- package/components/templates/ecommerce.tsx +199 -0
- package/components/templates/mail.tsx +275 -0
- package/components/templates/saas-landing.tsx +169 -0
- package/components/theme/studio-code-panel.tsx +485 -0
- package/components/theme/theme-context.tsx +120 -0
- package/components/theme/theme-engine.ts +237 -0
- package/components/theme/theme-exporter.tsx +369 -0
- package/components/theme/theme-panel.tsx +268 -0
- package/components/theme/token-export-utils.ts +1211 -0
- package/components/ui/animated.tsx +55 -0
- package/components/ui/avatar.tsx +38 -0
- package/components/ui/badge.tsx +32 -0
- package/components/ui/button.tsx +65 -0
- package/components/ui/card.tsx +56 -0
- package/components/ui/checkbox.tsx +19 -0
- package/components/ui/command-palette.tsx +245 -0
- package/components/ui/gsap-animated.tsx +436 -0
- package/components/ui/input.tsx +17 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/skeleton.tsx +102 -0
- package/components/ui/switch.tsx +43 -0
- package/components/ui/tabs.tsx +115 -0
- package/components/ui/toast.tsx +119 -0
- package/gradient-forge/theme-context.tsx +119 -0
- package/gradient-forge/theme-engine.ts +236 -0
- package/gradient-forge/themes.css +556 -0
- package/lib/animations.ts +50 -0
- package/lib/gsap.ts +426 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +6 -0
- package/package.json +53 -0
- package/postcss.config.mjs +5 -0
- package/tailwind.config.ts +15 -0
- package/tsconfig.json +43 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
4
|
+
import gsap from "gsap";
|
|
5
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
6
|
+
|
|
7
|
+
if (typeof window !== "undefined") {
|
|
8
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// GSAP Animated Section Component
|
|
12
|
+
interface AnimatedSectionProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
animation?: "fadeUp" | "fadeIn" | "slideLeft" | "slideRight" | "scale" | "none";
|
|
16
|
+
delay?: number;
|
|
17
|
+
duration?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AnimatedSection({
|
|
21
|
+
children,
|
|
22
|
+
className = "",
|
|
23
|
+
animation = "fadeUp",
|
|
24
|
+
delay = 0,
|
|
25
|
+
duration = 0.6,
|
|
26
|
+
}: AnimatedSectionProps) {
|
|
27
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const element = ref.current;
|
|
31
|
+
if (!element || animation === "none") return;
|
|
32
|
+
|
|
33
|
+
const animations: Record<string, gsap.TweenVars> = {
|
|
34
|
+
fadeUp: { opacity: 0, y: 30 },
|
|
35
|
+
fadeIn: { opacity: 0 },
|
|
36
|
+
slideLeft: { opacity: 0, x: -50 },
|
|
37
|
+
slideRight: { opacity: 0, x: 50 },
|
|
38
|
+
scale: { opacity: 0, scale: 0.9 },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const fromVars = animations[animation];
|
|
42
|
+
|
|
43
|
+
gsap.fromTo(
|
|
44
|
+
element,
|
|
45
|
+
fromVars,
|
|
46
|
+
{
|
|
47
|
+
opacity: 1,
|
|
48
|
+
y: 0,
|
|
49
|
+
x: 0,
|
|
50
|
+
scale: 1,
|
|
51
|
+
duration,
|
|
52
|
+
delay,
|
|
53
|
+
ease: "power3.out",
|
|
54
|
+
scrollTrigger: {
|
|
55
|
+
trigger: element,
|
|
56
|
+
start: "top 85%",
|
|
57
|
+
toggleActions: "play none none none",
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
ScrollTrigger.getAll().forEach((trigger) => {
|
|
64
|
+
if (trigger.trigger === element) {
|
|
65
|
+
trigger.kill();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}, [animation, delay, duration]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div ref={ref} className={className}>
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Stagger Container with GSAP
|
|
79
|
+
interface StaggerContainerProps {
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
className?: string;
|
|
82
|
+
stagger?: number;
|
|
83
|
+
start?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function StaggerContainer({
|
|
87
|
+
children,
|
|
88
|
+
className = "",
|
|
89
|
+
stagger = 0.1,
|
|
90
|
+
start = "top 80%",
|
|
91
|
+
}: StaggerContainerProps) {
|
|
92
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const container = containerRef.current;
|
|
96
|
+
if (!container) return;
|
|
97
|
+
|
|
98
|
+
const children = container.children;
|
|
99
|
+
if (children.length === 0) return;
|
|
100
|
+
|
|
101
|
+
gsap.fromTo(
|
|
102
|
+
children,
|
|
103
|
+
{ opacity: 0, y: 20 },
|
|
104
|
+
{
|
|
105
|
+
opacity: 1,
|
|
106
|
+
y: 0,
|
|
107
|
+
duration: 0.5,
|
|
108
|
+
stagger,
|
|
109
|
+
ease: "power3.out",
|
|
110
|
+
scrollTrigger: {
|
|
111
|
+
trigger: container,
|
|
112
|
+
start,
|
|
113
|
+
toggleActions: "play none none none",
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
ScrollTrigger.getAll().forEach((trigger) => {
|
|
120
|
+
if (trigger.trigger === container) {
|
|
121
|
+
trigger.kill();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}, [stagger, start]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div ref={containerRef} className={className}>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Magnetic Button Component
|
|
135
|
+
interface MagneticButtonProps {
|
|
136
|
+
children: ReactNode;
|
|
137
|
+
className?: string;
|
|
138
|
+
strength?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function MagneticButton({
|
|
142
|
+
children,
|
|
143
|
+
className = "",
|
|
144
|
+
strength = 0.3,
|
|
145
|
+
}: MagneticButtonProps) {
|
|
146
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const element = ref.current;
|
|
150
|
+
if (!element) return;
|
|
151
|
+
|
|
152
|
+
// Skip on mobile
|
|
153
|
+
if (window.matchMedia("(pointer: coarse)").matches) return;
|
|
154
|
+
|
|
155
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
156
|
+
const rect = element.getBoundingClientRect();
|
|
157
|
+
const x = e.clientX - rect.left - rect.width / 2;
|
|
158
|
+
const y = e.clientY - rect.top - rect.height / 2;
|
|
159
|
+
|
|
160
|
+
gsap.to(element, {
|
|
161
|
+
x: x * strength,
|
|
162
|
+
y: y * strength,
|
|
163
|
+
duration: 0.3,
|
|
164
|
+
ease: "power2.out",
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleMouseLeave = () => {
|
|
169
|
+
gsap.to(element, {
|
|
170
|
+
x: 0,
|
|
171
|
+
y: 0,
|
|
172
|
+
duration: 0.5,
|
|
173
|
+
ease: "elastic.out(1, 0.3)",
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
element.addEventListener("mousemove", handleMouseMove);
|
|
178
|
+
element.addEventListener("mouseleave", handleMouseLeave);
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
element.removeEventListener("mousemove", handleMouseMove);
|
|
182
|
+
element.removeEventListener("mouseleave", handleMouseLeave);
|
|
183
|
+
};
|
|
184
|
+
}, [strength]);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div ref={ref} className={className}>
|
|
188
|
+
{children}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Floating Animation Component
|
|
194
|
+
interface FloatingElementProps {
|
|
195
|
+
children: ReactNode;
|
|
196
|
+
className?: string;
|
|
197
|
+
duration?: number;
|
|
198
|
+
distance?: number;
|
|
199
|
+
delay?: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function FloatingElement({
|
|
203
|
+
children,
|
|
204
|
+
className = "",
|
|
205
|
+
duration = 3,
|
|
206
|
+
distance = 10,
|
|
207
|
+
delay = 0,
|
|
208
|
+
}: FloatingElementProps) {
|
|
209
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const element = ref.current;
|
|
213
|
+
if (!element) return;
|
|
214
|
+
|
|
215
|
+
gsap.to(element, {
|
|
216
|
+
y: distance,
|
|
217
|
+
duration,
|
|
218
|
+
delay,
|
|
219
|
+
ease: "power1.inOut",
|
|
220
|
+
yoyo: true,
|
|
221
|
+
repeat: -1,
|
|
222
|
+
});
|
|
223
|
+
}, [duration, distance, delay]);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div ref={ref} className={className}>
|
|
227
|
+
{children}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Text Reveal Animation
|
|
233
|
+
interface TextRevealProps {
|
|
234
|
+
text: string;
|
|
235
|
+
className?: string;
|
|
236
|
+
delay?: number;
|
|
237
|
+
stagger?: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function TextReveal({
|
|
241
|
+
text,
|
|
242
|
+
className = "",
|
|
243
|
+
delay = 0,
|
|
244
|
+
stagger = 0.03,
|
|
245
|
+
}: TextRevealProps) {
|
|
246
|
+
const containerRef = useRef<HTMLSpanElement>(null);
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
const container = containerRef.current;
|
|
250
|
+
if (!container) return;
|
|
251
|
+
|
|
252
|
+
const chars = container.querySelectorAll(".char");
|
|
253
|
+
|
|
254
|
+
gsap.fromTo(
|
|
255
|
+
chars,
|
|
256
|
+
{ opacity: 0, y: 20 },
|
|
257
|
+
{
|
|
258
|
+
opacity: 1,
|
|
259
|
+
y: 0,
|
|
260
|
+
duration: 0.4,
|
|
261
|
+
stagger,
|
|
262
|
+
delay,
|
|
263
|
+
ease: "power3.out",
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
}, [delay, stagger, text]);
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<span ref={containerRef} className={className}>
|
|
270
|
+
{text.split("").map((char, i) => (
|
|
271
|
+
<span
|
|
272
|
+
key={i}
|
|
273
|
+
className="char inline-block"
|
|
274
|
+
style={{ opacity: 0 }}
|
|
275
|
+
>
|
|
276
|
+
{char === " " ? "\u00A0" : char}
|
|
277
|
+
</span>
|
|
278
|
+
))}
|
|
279
|
+
</span>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Parallax Container
|
|
284
|
+
interface ParallaxProps {
|
|
285
|
+
children: ReactNode;
|
|
286
|
+
className?: string;
|
|
287
|
+
speed?: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function Parallax({ children, className = "", speed = 0.5 }: ParallaxProps) {
|
|
291
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const element = ref.current;
|
|
295
|
+
if (!element) return;
|
|
296
|
+
|
|
297
|
+
gsap.to(element, {
|
|
298
|
+
yPercent: speed * 100,
|
|
299
|
+
ease: "none",
|
|
300
|
+
scrollTrigger: {
|
|
301
|
+
trigger: element,
|
|
302
|
+
start: "top bottom",
|
|
303
|
+
end: "bottom top",
|
|
304
|
+
scrub: true,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return () => {
|
|
309
|
+
ScrollTrigger.getAll().forEach((trigger) => {
|
|
310
|
+
if (trigger.trigger === element) {
|
|
311
|
+
trigger.kill();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
}, [speed]);
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div ref={ref} className={className}>
|
|
319
|
+
{children}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Counter Animation
|
|
325
|
+
interface CounterProps {
|
|
326
|
+
end: number;
|
|
327
|
+
duration?: number;
|
|
328
|
+
className?: string;
|
|
329
|
+
suffix?: string;
|
|
330
|
+
prefix?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function Counter({
|
|
334
|
+
end,
|
|
335
|
+
duration = 2,
|
|
336
|
+
className = "",
|
|
337
|
+
suffix = "",
|
|
338
|
+
prefix = "",
|
|
339
|
+
}: CounterProps) {
|
|
340
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
341
|
+
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
const element = ref.current;
|
|
344
|
+
if (!element) return;
|
|
345
|
+
|
|
346
|
+
const obj = { value: 0 };
|
|
347
|
+
gsap.to(obj, {
|
|
348
|
+
value: end,
|
|
349
|
+
duration,
|
|
350
|
+
ease: "power2.out",
|
|
351
|
+
scrollTrigger: {
|
|
352
|
+
trigger: element,
|
|
353
|
+
start: "top 85%",
|
|
354
|
+
toggleActions: "play none none none",
|
|
355
|
+
},
|
|
356
|
+
onUpdate: () => {
|
|
357
|
+
element.textContent = prefix + Math.round(obj.value) + suffix;
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}, [end, duration, prefix, suffix]);
|
|
361
|
+
|
|
362
|
+
return <span ref={ref} className={className}>0</span>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Reveal on Scroll
|
|
366
|
+
interface RevealOnScrollProps {
|
|
367
|
+
children: ReactNode;
|
|
368
|
+
className?: string;
|
|
369
|
+
direction?: "up" | "down" | "left" | "right";
|
|
370
|
+
distance?: number;
|
|
371
|
+
delay?: number;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function RevealOnScroll({
|
|
375
|
+
children,
|
|
376
|
+
className = "",
|
|
377
|
+
direction = "up",
|
|
378
|
+
distance = 30,
|
|
379
|
+
delay = 0,
|
|
380
|
+
}: RevealOnScrollProps) {
|
|
381
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
const element = ref.current;
|
|
385
|
+
if (!element) return;
|
|
386
|
+
|
|
387
|
+
const fromVars: gsap.TweenVars = { opacity: 0 };
|
|
388
|
+
|
|
389
|
+
switch (direction) {
|
|
390
|
+
case "up":
|
|
391
|
+
fromVars.y = distance;
|
|
392
|
+
break;
|
|
393
|
+
case "down":
|
|
394
|
+
fromVars.y = -distance;
|
|
395
|
+
break;
|
|
396
|
+
case "left":
|
|
397
|
+
fromVars.x = distance;
|
|
398
|
+
break;
|
|
399
|
+
case "right":
|
|
400
|
+
fromVars.x = -distance;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
gsap.fromTo(
|
|
405
|
+
element,
|
|
406
|
+
fromVars,
|
|
407
|
+
{
|
|
408
|
+
opacity: 1,
|
|
409
|
+
x: 0,
|
|
410
|
+
y: 0,
|
|
411
|
+
duration: 0.6,
|
|
412
|
+
delay,
|
|
413
|
+
ease: "power3.out",
|
|
414
|
+
scrollTrigger: {
|
|
415
|
+
trigger: element,
|
|
416
|
+
start: "top 85%",
|
|
417
|
+
toggleActions: "play none none none",
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
return () => {
|
|
423
|
+
ScrollTrigger.getAll().forEach((trigger) => {
|
|
424
|
+
if (trigger.trigger === element) {
|
|
425
|
+
trigger.kill();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
}, [direction, distance, delay]);
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div ref={ref} className={className} style={{ opacity: 0 }}>
|
|
433
|
+
{children}
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export function Input({
|
|
5
|
+
className,
|
|
6
|
+
...props
|
|
7
|
+
}: React.InputHTMLAttributes<HTMLInputElement>) {
|
|
8
|
+
return (
|
|
9
|
+
<input
|
|
10
|
+
className={cn(
|
|
11
|
+
"h-11 w-full rounded-2xl border border-border/60 bg-background/40 px-4 text-sm text-foreground shadow-inner shadow-black/5 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { ChevronDown } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface SelectProps {
|
|
6
|
+
value?: string;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
onValueChange?: (value: string) => void;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SelectTriggerProps {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SelectValueProps {
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SelectContentProps {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SelectItemProps {
|
|
28
|
+
value: string;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SelectContext = React.createContext<{
|
|
34
|
+
value: string;
|
|
35
|
+
onValueChange: (value: string) => void;
|
|
36
|
+
isOpen: boolean;
|
|
37
|
+
setIsOpen: (isOpen: boolean) => void;
|
|
38
|
+
} | null>(null);
|
|
39
|
+
|
|
40
|
+
function useSelect() {
|
|
41
|
+
const context = React.useContext(SelectContext);
|
|
42
|
+
if (!context) {
|
|
43
|
+
throw new Error("Select components must be used within a Select provider");
|
|
44
|
+
}
|
|
45
|
+
return context;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Select({
|
|
49
|
+
value: controlledValue,
|
|
50
|
+
defaultValue,
|
|
51
|
+
onValueChange,
|
|
52
|
+
children,
|
|
53
|
+
className,
|
|
54
|
+
}: SelectProps) {
|
|
55
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue || "");
|
|
56
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
57
|
+
const isControlled = controlledValue !== undefined;
|
|
58
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
59
|
+
|
|
60
|
+
const handleValueChange = React.useCallback(
|
|
61
|
+
(newValue: string) => {
|
|
62
|
+
if (!isControlled) {
|
|
63
|
+
setInternalValue(newValue);
|
|
64
|
+
}
|
|
65
|
+
onValueChange?.(newValue);
|
|
66
|
+
setIsOpen(false);
|
|
67
|
+
},
|
|
68
|
+
[isControlled, onValueChange]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<SelectContext.Provider
|
|
73
|
+
value={{ value, onValueChange: handleValueChange, isOpen, setIsOpen }}
|
|
74
|
+
>
|
|
75
|
+
<div className={cn("relative", className)}>{children}</div>
|
|
76
|
+
</SelectContext.Provider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function SelectTrigger({ children, className }: SelectTriggerProps) {
|
|
81
|
+
const { isOpen, setIsOpen } = useSelect();
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
role="combobox"
|
|
87
|
+
aria-expanded={isOpen}
|
|
88
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex h-10 w-full items-center justify-between rounded-full border border-border/70 bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
<ChevronDown
|
|
96
|
+
className={cn(
|
|
97
|
+
"h-4 w-4 opacity-50 transition-transform",
|
|
98
|
+
isOpen && "rotate-180"
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
</button>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function SelectValue({ placeholder }: SelectValueProps) {
|
|
106
|
+
const { value } = useSelect();
|
|
107
|
+
const [selectedLabel, setSelectedLabel] = React.useState<string>("");
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
// Find the selected item's label from the DOM
|
|
111
|
+
const selectedItem = document.querySelector(`[data-select-value="${value}"]`);
|
|
112
|
+
if (selectedItem) {
|
|
113
|
+
setSelectedLabel(selectedItem.textContent || "");
|
|
114
|
+
} else {
|
|
115
|
+
setSelectedLabel("");
|
|
116
|
+
}
|
|
117
|
+
}, [value]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<span className={cn(!selectedLabel && "text-muted-foreground")}>
|
|
121
|
+
{selectedLabel || placeholder}
|
|
122
|
+
</span>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function SelectContent({ children, className }: SelectContentProps) {
|
|
127
|
+
const { isOpen } = useSelect();
|
|
128
|
+
|
|
129
|
+
if (!isOpen) return null;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
<div
|
|
134
|
+
className="fixed inset-0 z-40"
|
|
135
|
+
onClick={(e) => {
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
const selectContext = React.useContext(SelectContext);
|
|
138
|
+
selectContext?.setIsOpen(false);
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
<div
|
|
142
|
+
className={cn(
|
|
143
|
+
"absolute z-50 min-w-[8rem] overflow-hidden rounded-2xl border border-border/70 bg-background/95 backdrop-blur-md shadow-lg animate-in fade-in-0 zoom-in-95",
|
|
144
|
+
"top-full mt-1 left-0 right-0",
|
|
145
|
+
className
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<div className="p-1 max-h-[300px] overflow-auto">{children}</div>
|
|
149
|
+
</div>
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function SelectItem({ value, children, className }: SelectItemProps) {
|
|
155
|
+
const { value: selectedValue, onValueChange } = useSelect();
|
|
156
|
+
const isSelected = selectedValue === value;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
role="option"
|
|
162
|
+
aria-selected={isSelected}
|
|
163
|
+
data-select-value={value}
|
|
164
|
+
onClick={() => onValueChange(value)}
|
|
165
|
+
className={cn(
|
|
166
|
+
"relative flex w-full cursor-pointer select-none items-center rounded-xl py-2.5 px-3 text-sm outline-none transition-colors",
|
|
167
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
168
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
169
|
+
isSelected && "bg-accent text-accent-foreground",
|
|
170
|
+
className
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
{children}
|
|
174
|
+
</button>
|
|
175
|
+
);
|
|
176
|
+
}
|