apex-ui-kit 2.0.6 → 2.0.7
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/dist/ApexUI-Kit/templates/CardStack/CardStack.jsx +101 -0
- package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +0 -161
- package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +38 -6
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +54 -61
- package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +30 -19
- package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +27 -7
- package/dist/ApexUI-Kit/templates/ScrollMarquee/CardMarquee.jsx +0 -0
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +23 -15
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +4 -0
- package/dist/cli/index.js +1 -1
- package/package.json +1 -1
- package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +0 -111
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { gsap } from "gsap";
|
|
4
|
+
import { FaExternalLinkAlt, FaGithub, FaRegImage } from "react-icons/fa";
|
|
5
|
+
const CARD_STACK_OFFSET = 32;
|
|
6
|
+
const CARD_SCALE_STEP = 0.08;
|
|
7
|
+
const AUTO_CYCLE_INTERVAL = 5000;
|
|
8
|
+
export default function CardStack({
|
|
9
|
+
cards = [],
|
|
10
|
+
autoCycle = true,
|
|
11
|
+
cycleInterval = AUTO_CYCLE_INTERVAL,
|
|
12
|
+
}) {
|
|
13
|
+
const [active, setActive] = useState(0);
|
|
14
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
15
|
+
const [isScrolling, setIsScrolling] = useState(false);
|
|
16
|
+
const timeoutRef = useRef(null);
|
|
17
|
+
const scrollTimeoutRef = useRef(null);
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!autoCycle || isHovered || isScrolling) return;
|
|
21
|
+
timeoutRef.current = setTimeout(() => {
|
|
22
|
+
setActive((prev) => (prev + 1) % cards.length);
|
|
23
|
+
}, cycleInterval);
|
|
24
|
+
return () => clearTimeout(timeoutRef.current);
|
|
25
|
+
}, [active, isHovered, isScrolling, autoCycle, cycleInterval, cards.length]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleScroll = () => {
|
|
29
|
+
setIsScrolling(true);
|
|
30
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
31
|
+
scrollTimeoutRef.current = setTimeout(() => setIsScrolling(false), 150);
|
|
32
|
+
};
|
|
33
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener('scroll', handleScroll);
|
|
36
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!containerRef.current) return;
|
|
41
|
+
const cardElements = gsap.utils.toArray(".card-item", containerRef.current);
|
|
42
|
+
const tl = gsap.timeline({
|
|
43
|
+
defaults: { duration: 0.6, ease: "elastic.out(1, 0.3)" },
|
|
44
|
+
});
|
|
45
|
+
cardElements.forEach((card, i) => {
|
|
46
|
+
const isTop = i === 0;
|
|
47
|
+
const z = cards.length - i;
|
|
48
|
+
const offset = i * CARD_STACK_OFFSET;
|
|
49
|
+
const scale = 1 - i * CARD_SCALE_STEP;
|
|
50
|
+
|
|
51
|
+
tl.to(card, {
|
|
52
|
+
y: -offset,
|
|
53
|
+
scale: scale,
|
|
54
|
+
filter: isTop ? "none" : "blur(1px)",
|
|
55
|
+
opacity: 1,
|
|
56
|
+
zIndex: z,
|
|
57
|
+
}, 0);
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
tl.kill();
|
|
61
|
+
};
|
|
62
|
+
}, [active]);
|
|
63
|
+
const getCardIndex = (i) => (active + i) % cards.length;
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
ref={containerRef}
|
|
67
|
+
className="relative flex justify-center items-center h-[340px] md:h-[400px] w-full max-w-lg mx-auto"
|
|
68
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
69
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
70
|
+
>
|
|
71
|
+
{cards.map((_, i) => {
|
|
72
|
+
if (i >= 4) return null;
|
|
73
|
+
|
|
74
|
+
const idx = getCardIndex(i);
|
|
75
|
+
const isTop = i === 0;
|
|
76
|
+
const card = cards[idx];
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
key={idx}
|
|
81
|
+
className={`card-item absolute left-0 right-0 mx-auto w-11/12 md:w-96 h-48 md:h-60 rounded-3xl shadow-xl bg-gradient-to-br ${card.color} bg-slate-950 border border-white/20 flex flex-row items-center px-3 md:px-6 py-4 cursor-pointer select-none ${isTop ? "" : "pointer-events-none"}`}
|
|
82
|
+
style={{ opacity: 0 }}
|
|
83
|
+
>
|
|
84
|
+
<div className="flex-shrink-0 flex items-center justify-center w-16 h-16 md:w-24 md:h-24 lg:w-28 lg:h-28 rounded-xl overflow-hidden bg-white/10 border border-white/20 mr-3 md:mr-6">
|
|
85
|
+
{card.image ? (<img src={card.image} alt={card.title} className="object-cover w-full h-full" />) : (<FaRegImage className="text-3xl text-white/40" />)}
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex-1 flex flex-col justify-center gap-1 md:gap-2 overflow-hidden">
|
|
88
|
+
<div className="flex items-center gap-2">
|
|
89
|
+
<h2 className="text-base md:text-xl font-bold text-white drop-shadow truncate">{card.title}</h2>
|
|
90
|
+
{card.github && (<a href={card.github} target="_blank" rel="noopener noreferrer" className="text-white/70 hover:text-white ml-1 flex-shrink-0"><FaGithub className="inline-block align-middle text-lg md:text-xl" /></a>)}
|
|
91
|
+
</div>
|
|
92
|
+
<p className="text-xs md:text-sm text-white/60 mb-1 truncate">{card.subtitle}</p>
|
|
93
|
+
<p className="text-xs md:text-base text-white/80 mb-1 line-clamp-2">{card.desc}</p>
|
|
94
|
+
{card.link && (<a href={card.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs md:text-sm px-2 py-1 rounded bg-blue-500/80 hover:bg-blue-600/90 text-white font-semibold shadow transition w-fit mt-1"><FaExternalLinkAlt className="inline-block mr-1 text-xs md:text-sm" />Visit</a>)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */
|
|
2
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { motion, useMotionValue, useTransform } from 'framer-motion';
|
|
4
|
-
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
// --- Card Component with Hover Parallax Effect ---
|
|
7
|
-
const CarouselCard = ({ slide, isCenter }) => {
|
|
8
|
-
const cardRef = useRef(null);
|
|
9
|
-
const x = useMotionValue(0);
|
|
10
|
-
const y = useMotionValue(0);
|
|
11
|
-
|
|
12
|
-
// Mouse move ke hisab se rotation calculate karta hai (effect ko strong banaya gaya hai)
|
|
13
|
-
const rotateX = useTransform(y, [-200, 200], [15, -15]);
|
|
14
|
-
const rotateY = useTransform(x, [-200, 200], [-15, 15]);
|
|
15
|
-
const handleMouseMove = (event) => {
|
|
16
|
-
if (!cardRef.current || !isCenter) return;
|
|
17
|
-
const rect = cardRef.current.getBoundingClientRect();
|
|
18
|
-
x.set(event.clientX - rect.left - rect.width / 2);
|
|
19
|
-
y.set(event.clientY - rect.top - rect.height / 2);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const handleMouseLeave = () => {
|
|
23
|
-
x.set(0);
|
|
24
|
-
y.set(0);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<motion.div
|
|
29
|
-
ref={cardRef}
|
|
30
|
-
className="w-full h-full bg-slate-900 rounded-2xl group"
|
|
31
|
-
style={{
|
|
32
|
-
transformStyle: 'preserve-3d',
|
|
33
|
-
perspective: '1500px',
|
|
34
|
-
rotateX: isCenter ? rotateX : 0,
|
|
35
|
-
rotateY: isCenter ? rotateY : 0,
|
|
36
|
-
}}
|
|
37
|
-
onMouseMove={handleMouseMove}
|
|
38
|
-
onMouseLeave={handleMouseLeave}
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
{/* Slide Content */}
|
|
42
|
-
<div className="absolute inset-0 w-full h-full"
|
|
43
|
-
style={{
|
|
44
|
-
transformStyle: 'preserve-3d',
|
|
45
|
-
perspective: '1500px',
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
<img
|
|
49
|
-
src={slide.imageUrl}
|
|
50
|
-
alt={slide.title}
|
|
51
|
-
className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl"
|
|
52
|
-
/>
|
|
53
|
-
<img
|
|
54
|
-
src={slide.pngUrl}
|
|
55
|
-
alt={slide.title}
|
|
56
|
-
className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl"
|
|
57
|
-
/>
|
|
58
|
-
</div>
|
|
59
|
-
<div
|
|
60
|
-
className="relative flex flex-col justify-end h-full text-white p-8"
|
|
61
|
-
style={{
|
|
62
|
-
transformStyle: 'preserve-3d',
|
|
63
|
-
perspective: '1500px',
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
66
|
-
<h2 className="text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
|
|
67
|
-
<p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
|
|
68
|
-
<p className="text-slate-300 mt-4 text-sm leading-relaxed group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
|
|
69
|
-
</div>
|
|
70
|
-
</motion.div>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// --- Custom Parallax Carousel Component ---
|
|
76
|
-
const ParallaxCarousel = ({ slides }) => {
|
|
77
|
-
const [activeIndex, setActiveIndex] = useState(0);
|
|
78
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
79
|
-
|
|
80
|
-
const handleNext = () => {
|
|
81
|
-
setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const handlePrev = () => {
|
|
85
|
-
setActiveIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Automatic sliding ke liye
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
// Agar hovered hai to interval start hi mat karo
|
|
91
|
-
if (isHovered) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const interval = setInterval(() => {
|
|
96
|
-
handleNext();
|
|
97
|
-
}, 4000); // Har 4 second mein slide change hoga
|
|
98
|
-
|
|
99
|
-
return () => clearInterval(interval); // Cleanup
|
|
100
|
-
}, [activeIndex, isHovered]); // isHovered state change hone par effect ko firse chalayega
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div
|
|
104
|
-
className="relative w-full flex flex-col items-center px-2"
|
|
105
|
-
onMouseEnter={() => setIsHovered(true)}
|
|
106
|
-
onMouseLeave={() => setIsHovered(false)}
|
|
107
|
-
>
|
|
108
|
-
{/* 3D Carousel Container */}
|
|
109
|
-
<div
|
|
110
|
-
className="relative w-full h-[480px] portrait:h-[480px] portrait:px-0"
|
|
111
|
-
style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
|
|
112
|
-
>
|
|
113
|
-
{slides.map((slide, index) => {
|
|
114
|
-
let positionOffset = index - activeIndex;
|
|
115
|
-
if (positionOffset > slides.length / 2) {
|
|
116
|
-
positionOffset -= slides.length;
|
|
117
|
-
} else if (positionOffset < -slides.length / 2) {
|
|
118
|
-
positionOffset += slides.length;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const isVisible = Math.abs(positionOffset) <= 2;
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<motion.div
|
|
125
|
-
key={index}
|
|
126
|
-
className={`absolute w-full h-[340px] portrait:w-full portrait:h-[480px] top-0 left-0 portrait:left-0 portrait:top-0 ${positionOffset === 0 ? '' : ' pointer-events-none'} md:w-[50%] md:h-[400px] md:top-[50px] md:left-1/2 lg:-translate-x-1/2`}
|
|
127
|
-
initial={false}
|
|
128
|
-
style={{ transformStyle: 'preserve-3d' }}
|
|
129
|
-
animate={{
|
|
130
|
-
transform: `translateX(${positionOffset * 50}%) translateZ(${-Math.abs(positionOffset) * 250}px) scale(${1 - Math.abs(positionOffset) * 0.15})`,
|
|
131
|
-
opacity: isVisible ? (1 - Math.abs(positionOffset) * 0.45) : 0,
|
|
132
|
-
zIndex: slides.length - Math.abs(positionOffset),
|
|
133
|
-
}}
|
|
134
|
-
transition={{ type: 'spring', stiffness: 290, damping: 30 }}
|
|
135
|
-
>
|
|
136
|
-
<CarouselCard slide={slide} isCenter={positionOffset === 0} />
|
|
137
|
-
</motion.div>
|
|
138
|
-
);
|
|
139
|
-
})}
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
{/* Side Navigation Buttons */}
|
|
143
|
-
<motion.button
|
|
144
|
-
onClick={handlePrev}
|
|
145
|
-
className="absolute -left-10 lg:left-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors"
|
|
146
|
-
whileTap={{ scale: 0.9 }}
|
|
147
|
-
>
|
|
148
|
-
<ArrowLeft size={32} />
|
|
149
|
-
</motion.button>
|
|
150
|
-
<motion.button
|
|
151
|
-
onClick={handleNext}
|
|
152
|
-
className="absolute -right-10 lg:right-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors"
|
|
153
|
-
whileTap={{ scale: 0.9 }}
|
|
154
|
-
>
|
|
155
|
-
<ArrowRight size={32} />
|
|
156
|
-
</motion.button>
|
|
157
|
-
</div>
|
|
158
|
-
);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
export default ParallaxCarousel;
|
|
@@ -16,10 +16,16 @@ const DragCarousel = ({ images }) => {
|
|
|
16
16
|
const dragProxyRef = useRef(null);
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
const cards = cardsRef.current;
|
|
19
|
+
// hint browser to use the GPU for transforms
|
|
20
|
+
cards.forEach((c) => {
|
|
21
|
+
if (c) c.style.willChange = 'transform, opacity';
|
|
22
|
+
});
|
|
23
|
+
|
|
19
24
|
gsap.to(cards, {
|
|
20
|
-
duration: 0.
|
|
21
|
-
ease: '
|
|
22
|
-
stagger: 0.
|
|
25
|
+
duration: 0.45,
|
|
26
|
+
ease: 'power2.out',
|
|
27
|
+
stagger: 0.03,
|
|
28
|
+
overwrite: true,
|
|
23
29
|
// Har card ke liye naya transform calculate karta hai
|
|
24
30
|
transform: (i) => {
|
|
25
31
|
let positionOffset = i - activeIndex;
|
|
@@ -31,7 +37,7 @@ const DragCarousel = ({ images }) => {
|
|
|
31
37
|
|
|
32
38
|
const translateX = positionOffset * 50;
|
|
33
39
|
const translateZ = -Math.abs(positionOffset) * 250;
|
|
34
|
-
const scale = 1 - Math.abs(positionOffset) * 0.
|
|
40
|
+
const scale = 1 - Math.abs(positionOffset) * 0.12;
|
|
35
41
|
|
|
36
42
|
return `translateX(${translateX}%) translateZ(${translateZ}px) scale(${scale})`;
|
|
37
43
|
},
|
|
@@ -53,14 +59,39 @@ const DragCarousel = ({ images }) => {
|
|
|
53
59
|
// GSAP Draggable ko setup karta hai
|
|
54
60
|
useEffect(() => {
|
|
55
61
|
// BUG FIX: Draggable ab proxy element par lagaya gaya hai
|
|
62
|
+
const cards = cardsRef.current;
|
|
63
|
+
const setters = cards.map((c) => (c ? gsap.quickSetter(c, 'css', 'transform') : null));
|
|
64
|
+
|
|
56
65
|
const draggableInstance = Draggable.create(dragProxyRef.current, {
|
|
57
66
|
type: "x",
|
|
58
67
|
trigger: carouselRef.current,
|
|
59
68
|
inertia: true,
|
|
60
69
|
cursor: 'grab',
|
|
61
70
|
activeCursor: 'grabbing',
|
|
71
|
+
onDrag: function () {
|
|
72
|
+
const w = carouselRef.current ? carouselRef.current.clientWidth : window.innerWidth;
|
|
73
|
+
const shiftPercent = (this.x / w) * 100; // percentage shift based on drag
|
|
74
|
+
|
|
75
|
+
cards.forEach((c, i) => {
|
|
76
|
+
if (!c || !setters[i]) return;
|
|
77
|
+
let positionOffset = i - activeIndex;
|
|
78
|
+
if (positionOffset > images.length / 2) positionOffset -= images.length;
|
|
79
|
+
if (positionOffset < -images.length / 2) positionOffset += images.length;
|
|
80
|
+
|
|
81
|
+
const baseX = positionOffset * 50;
|
|
82
|
+
const translateX = baseX + shiftPercent;
|
|
83
|
+
const translateZ = -Math.abs(positionOffset) * 250;
|
|
84
|
+
const scale = 1 - Math.abs(positionOffset) * 0.12;
|
|
85
|
+
|
|
86
|
+
setters[i](`translateX(${translateX}%) translateZ(${translateZ}px) scale(${scale})`);
|
|
87
|
+
c.style.opacity = Math.abs(positionOffset - (this.x / w)) > 2 ? 0 : 1;
|
|
88
|
+
c.style.zIndex = images.length - Math.abs(positionOffset);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
62
91
|
onDragEnd: function () {
|
|
63
|
-
|
|
92
|
+
// Reset proxy position
|
|
93
|
+
gsap.to(this.target, { x: 0, duration: 0.35, ease: 'power2.out' });
|
|
94
|
+
// Direction based index change
|
|
64
95
|
if (this.getDirection("start") === "left") {
|
|
65
96
|
setActiveIndex(prev => (prev + 1) % images.length);
|
|
66
97
|
} else if (this.getDirection("start") === "right") {
|
|
@@ -74,8 +105,9 @@ const DragCarousel = ({ images }) => {
|
|
|
74
105
|
if (draggableInstance && draggableInstance[0]) {
|
|
75
106
|
draggableInstance[0].kill();
|
|
76
107
|
}
|
|
108
|
+
setters.forEach((s) => s && s.kill && s.kill());
|
|
77
109
|
};
|
|
78
|
-
}, [images.length]);
|
|
110
|
+
}, [images.length, activeIndex]);
|
|
79
111
|
|
|
80
112
|
return (
|
|
81
113
|
<div
|
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
import React, { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { motion, useMotionValue, useTransform } from 'framer-motion';
|
|
4
4
|
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
// --- Card Component with Hover Parallax Effect ---
|
|
7
|
-
const CarouselCard = ({ slide, isCenter }) => {
|
|
5
|
+
const CarouselCard = ({ slide, isCenter, isScrolling }) => {
|
|
8
6
|
const cardRef = useRef(null);
|
|
9
7
|
const x = useMotionValue(0);
|
|
10
8
|
const y = useMotionValue(0);
|
|
11
9
|
|
|
12
|
-
// Mouse move ke hisab se rotation calculate karta hai (effect ko strong banaya gaya hai)
|
|
13
10
|
const rotateX = useTransform(y, [-200, 200], [15, -15]);
|
|
14
11
|
const rotateY = useTransform(x, [-200, 200], [-15, 15]);
|
|
12
|
+
|
|
15
13
|
const handleMouseMove = (event) => {
|
|
16
|
-
if (!cardRef.current || !isCenter) return;
|
|
14
|
+
if (!cardRef.current || !isCenter || isScrolling) return;
|
|
17
15
|
const rect = cardRef.current.getBoundingClientRect();
|
|
18
16
|
x.set(event.clientX - rect.left - rect.width / 2);
|
|
19
17
|
y.set(event.clientY - rect.top - rect.height / 2);
|
|
@@ -24,6 +22,13 @@ const CarouselCard = ({ slide, isCenter }) => {
|
|
|
24
22
|
y.set(0);
|
|
25
23
|
};
|
|
26
24
|
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isScrolling) {
|
|
27
|
+
x.set(0);
|
|
28
|
+
y.set(0);
|
|
29
|
+
}
|
|
30
|
+
}, [isScrolling, x, y]);
|
|
31
|
+
|
|
27
32
|
return (
|
|
28
33
|
<motion.div
|
|
29
34
|
ref={cardRef}
|
|
@@ -36,33 +41,13 @@ const CarouselCard = ({ slide, isCenter }) => {
|
|
|
36
41
|
}}
|
|
37
42
|
onMouseMove={handleMouseMove}
|
|
38
43
|
onMouseLeave={handleMouseLeave}
|
|
39
|
-
|
|
40
44
|
>
|
|
41
45
|
{/* Slide Content */}
|
|
42
|
-
<div className="absolute inset-0 w-full h-full"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
perspective: '1500px',
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
<img
|
|
49
|
-
src={slide.imageUrl}
|
|
50
|
-
alt={slide.title}
|
|
51
|
-
className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl"
|
|
52
|
-
/>
|
|
53
|
-
<img
|
|
54
|
-
src={slide.pngUrl}
|
|
55
|
-
alt={slide.title}
|
|
56
|
-
className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl"
|
|
57
|
-
/>
|
|
46
|
+
<div className="absolute inset-0 w-full h-full" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
|
|
47
|
+
<img src={slide.imageUrl} alt={slide.title} className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl" />
|
|
48
|
+
<img src={slide.pngUrl} alt={slide.title} className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl" />
|
|
58
49
|
</div>
|
|
59
|
-
<div
|
|
60
|
-
className="relative flex flex-col justify-end h-full text-white p-8"
|
|
61
|
-
style={{
|
|
62
|
-
transformStyle: 'preserve-3d',
|
|
63
|
-
perspective: '1500px',
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
50
|
+
<div className="relative flex flex-col justify-end h-full text-white p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
|
|
66
51
|
<h2 className="text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
|
|
67
52
|
<p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
|
|
68
53
|
<p className="text-slate-300 mt-4 text-sm leading-relaxed group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
|
|
@@ -75,37 +60,54 @@ const CarouselCard = ({ slide, isCenter }) => {
|
|
|
75
60
|
// --- Custom Parallax Carousel Component ---
|
|
76
61
|
const ParallaxCarousel = ({ slides }) => {
|
|
77
62
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
78
|
-
const
|
|
63
|
+
const animatingRef = useRef(false);
|
|
64
|
+
const animTimeoutRef = useRef(null);
|
|
79
65
|
|
|
66
|
+
const [isScrolling, setIsScrolling] = useState(false);
|
|
67
|
+
const scrollTimeoutRef = useRef(null);
|
|
68
|
+
|
|
69
|
+
// ✅ REMOVED: isHovered state hata diya gaya hai.
|
|
70
|
+
|
|
71
|
+
const NAV_ANIM_MS = 600;
|
|
80
72
|
const handleNext = () => {
|
|
73
|
+
if (animatingRef.current) return;
|
|
74
|
+
animatingRef.current = true;
|
|
75
|
+
clearTimeout(animTimeoutRef.current);
|
|
81
76
|
setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
|
|
77
|
+
animTimeoutRef.current = setTimeout(() => {
|
|
78
|
+
animatingRef.current = false;
|
|
79
|
+
}, NAV_ANIM_MS);
|
|
82
80
|
};
|
|
83
81
|
|
|
84
82
|
const handlePrev = () => {
|
|
83
|
+
if (animatingRef.current) return;
|
|
84
|
+
animatingRef.current = true;
|
|
85
|
+
clearTimeout(animTimeoutRef.current);
|
|
85
86
|
setActiveIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length);
|
|
87
|
+
animTimeoutRef.current = setTimeout(() => {
|
|
88
|
+
animatingRef.current = false;
|
|
89
|
+
}, NAV_ANIM_MS);
|
|
86
90
|
};
|
|
87
|
-
|
|
88
|
-
// Automatic sliding ke liye
|
|
89
91
|
useEffect(() => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const handleScroll = () => {
|
|
93
|
+
setIsScrolling(true);
|
|
94
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
95
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
96
|
+
setIsScrolling(false);
|
|
97
|
+
}, 150);
|
|
98
|
+
};
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
window.removeEventListener('scroll', handleScroll);
|
|
104
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
105
|
+
};
|
|
106
|
+
}, []);
|
|
98
107
|
|
|
99
|
-
return () => clearInterval(interval); // Cleanup
|
|
100
|
-
}, [activeIndex, isHovered]); // isHovered state change hone par effect ko firse chalayega
|
|
101
108
|
|
|
102
109
|
return (
|
|
103
|
-
<div
|
|
104
|
-
className="relative w-full flex flex-col items-center px-2"
|
|
105
|
-
onMouseEnter={() => setIsHovered(true)}
|
|
106
|
-
onMouseLeave={() => setIsHovered(false)}
|
|
107
|
-
>
|
|
108
|
-
{/* 3D Carousel Container */}
|
|
110
|
+
<div className="relative w-full flex flex-col items-center px-2">
|
|
109
111
|
<div
|
|
110
112
|
className="relative w-full h-[480px] portrait:h-[480px] portrait:px-0"
|
|
111
113
|
style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
|
|
@@ -125,33 +127,24 @@ const ParallaxCarousel = ({ slides }) => {
|
|
|
125
127
|
key={index}
|
|
126
128
|
className={`absolute w-full h-[340px] portrait:w-full portrait:h-[480px] top-0 left-0 portrait:left-0 portrait:top-0 ${positionOffset === 0 ? '' : ' pointer-events-none'} md:w-[50%] md:h-[400px] md:top-[50px] md:left-1/2 lg:-translate-x-1/2`}
|
|
127
129
|
initial={false}
|
|
128
|
-
style={{ transformStyle: 'preserve-3d' }}
|
|
130
|
+
style={{ transformStyle: 'preserve-3d', willChange: 'transform, opacity' }}
|
|
129
131
|
animate={{
|
|
130
132
|
transform: `translateX(${positionOffset * 50}%) translateZ(${-Math.abs(positionOffset) * 250}px) scale(${1 - Math.abs(positionOffset) * 0.15})`,
|
|
131
133
|
opacity: isVisible ? (1 - Math.abs(positionOffset) * 0.45) : 0,
|
|
132
134
|
zIndex: slides.length - Math.abs(positionOffset),
|
|
133
135
|
}}
|
|
134
|
-
transition={{ type: 'spring', stiffness:
|
|
136
|
+
transition={{ type: 'spring', stiffness: 160, damping: 22 }}
|
|
135
137
|
>
|
|
136
|
-
<CarouselCard slide={slide} isCenter={positionOffset === 0} />
|
|
138
|
+
<CarouselCard slide={slide} isCenter={positionOffset === 0} isScrolling={isScrolling} />
|
|
137
139
|
</motion.div>
|
|
138
140
|
);
|
|
139
141
|
})}
|
|
140
142
|
</div>
|
|
141
143
|
|
|
142
|
-
{
|
|
143
|
-
<motion.button
|
|
144
|
-
onClick={handlePrev}
|
|
145
|
-
className="absolute -left-10 lg:left-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors"
|
|
146
|
-
whileTap={{ scale: 0.9 }}
|
|
147
|
-
>
|
|
144
|
+
<motion.button onClick={handlePrev} className="absolute -left-10 lg:left-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
|
|
148
145
|
<ArrowLeft size={32} />
|
|
149
146
|
</motion.button>
|
|
150
|
-
<motion.button
|
|
151
|
-
onClick={handleNext}
|
|
152
|
-
className="absolute -right-10 lg:right-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors"
|
|
153
|
-
whileTap={{ scale: 0.9 }}
|
|
154
|
-
>
|
|
147
|
+
<motion.button onClick={handleNext} className="absolute -right-10 lg:right-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
|
|
155
148
|
<ArrowRight size={32} />
|
|
156
149
|
</motion.button>
|
|
157
150
|
</div>
|
|
@@ -6,25 +6,24 @@ const GitHubIcon = () => (<svg width="24" height="24" fill="none" stroke="curren
|
|
|
6
6
|
const LinkedInIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" /><rect x="2" y="9" width="4" height="12" /><circle cx="4" cy="4" r="2" /></svg>);
|
|
7
7
|
const TwitterIcon = () => (<BsTwitterX size={24} />);
|
|
8
8
|
const InstagramIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="5" ry="5" /><path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z" /><line x1="17.5" y1="6.5" x2="17.5" y2="6.5" /></svg>);
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
onCvClick: () => alert("CV button clicked!"),
|
|
26
|
-
};
|
|
9
|
+
const defaultProfile = {
|
|
10
|
+
avatarUrl: "/assets/spiderman.png",
|
|
11
|
+
name: "Spider Man",
|
|
12
|
+
username: "spider_man",
|
|
13
|
+
dob: "10-08-2001",
|
|
14
|
+
title: "PhotoGrapher",
|
|
15
|
+
status: "Available for Hire",
|
|
16
|
+
socialLinks: {
|
|
17
|
+
github: "https://github.com/",
|
|
18
|
+
linkedin: "https://www.linkedin.com/",
|
|
19
|
+
twitter: "https://twitter.com/",
|
|
20
|
+
instagram: "https://www.instagram.com/",
|
|
21
|
+
},
|
|
22
|
+
onHireClick: () => alert("Hire Me button clicked!"),
|
|
23
|
+
onCvClick: () => alert("CV button clicked!"),
|
|
24
|
+
};
|
|
27
25
|
|
|
26
|
+
const ParallaxProfileCard = ({ profile }) => {
|
|
28
27
|
const {
|
|
29
28
|
avatarUrl, name, username, dob, title, status, socialLinks, onHireClick, onCvClick
|
|
30
29
|
} = profile || defaultProfile;
|
|
@@ -104,4 +103,16 @@ const ParallaxProfileCard = ({ profile }) => {
|
|
|
104
103
|
);
|
|
105
104
|
};
|
|
106
105
|
|
|
107
|
-
|
|
106
|
+
const ParallaxProfile = ({ profiles = [defaultProfile], className = '' }) => {
|
|
107
|
+
const safe = Array.isArray(profiles) && profiles.length > 0 ? profiles : [defaultProfile];
|
|
108
|
+
return (
|
|
109
|
+
<div className={`flex flex-wrap gap-6 justify-center ${className}`}>
|
|
110
|
+
{safe.map((p, i) => (
|
|
111
|
+
<ParallaxProfileCard key={p.id || p.username || i} profile={p} />
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export { ParallaxProfileCard };
|
|
118
|
+
export default ParallaxProfile;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const defaultPeople = [
|
|
4
4
|
{ id: 1, name: "Captain America", job: "Leader of the Avengers", img: "/assets/captainamerica.png" },
|
|
5
5
|
{ id: 2, name: "Doctor Strange", job: "Sorcerer Supreme", img: "/assets/doctorStrange.png" },
|
|
6
6
|
{ id: 3, name: "Iron Man", job: "Leader Of Stark Industries", img: "/assets/ironman.png" },
|
|
@@ -9,7 +9,13 @@ const people = [
|
|
|
9
9
|
{ id: 6, name: "Thanos", job: "The Mad Titan", img: "/assets/thanos.png" },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
|
-
const PointerFollower = ({
|
|
12
|
+
const PointerFollower = ({
|
|
13
|
+
cursorColor = "#fff",
|
|
14
|
+
interval = 3000,
|
|
15
|
+
badgeColor = "#fff",
|
|
16
|
+
badgeTextColor = "#212121",
|
|
17
|
+
people = defaultPeople,
|
|
18
|
+
}) => {
|
|
13
19
|
const containerRef = useRef(null);
|
|
14
20
|
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
15
21
|
const [hovered, setHovered] = useState(false);
|
|
@@ -20,11 +26,14 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
|
|
|
20
26
|
if (hovered) return;
|
|
21
27
|
|
|
22
28
|
const autoInterval = setInterval(() => {
|
|
23
|
-
setActiveIndex((prev) =>
|
|
29
|
+
setActiveIndex((prev) => {
|
|
30
|
+
if (!people || people.length === 0) return 0;
|
|
31
|
+
return (prev + 1) % people.length;
|
|
32
|
+
});
|
|
24
33
|
}, interval);
|
|
25
34
|
|
|
26
35
|
return () => clearInterval(autoInterval);
|
|
27
|
-
}, [hovered, interval]);
|
|
36
|
+
}, [hovered, interval, people]);
|
|
28
37
|
|
|
29
38
|
// 🖱️ Mouse tracking
|
|
30
39
|
useEffect(() => {
|
|
@@ -53,13 +62,24 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
|
|
|
53
62
|
};
|
|
54
63
|
}, []);
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
// Guard against empty people array
|
|
66
|
+
const safePeople = Array.isArray(people) && people.length > 0 ? people : defaultPeople;
|
|
67
|
+
const person = safePeople[activeIndex % safePeople.length]; // ✅ SYNCED HERE
|
|
68
|
+
|
|
69
|
+
// If the provided people list changes, ensure activeIndex is valid
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!Array.isArray(people) || people.length === 0) {
|
|
72
|
+
setActiveIndex(0);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setActiveIndex((idx) => idx % people.length);
|
|
76
|
+
}, [people]);
|
|
57
77
|
|
|
58
78
|
return (
|
|
59
79
|
<div className="relative w-full h-fit flex items-center justify-center">
|
|
60
80
|
<div ref={containerRef} className="absolute flex items-center justify-center w-64 h-66 cursor-none">
|
|
61
81
|
{/* 🔁 Image stack with auto fade */}
|
|
62
|
-
{
|
|
82
|
+
{safePeople.map((p, i) => (
|
|
63
83
|
<img
|
|
64
84
|
key={i}
|
|
65
85
|
src={p.img}
|
|
@@ -67,7 +87,7 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
|
|
|
67
87
|
className={`absolute w-full h-full object-cover rounded-xl transition-opacity duration-1000 ease-in-out border border-white/10`}
|
|
68
88
|
style={{
|
|
69
89
|
transform: `translate(${i * -2}px, ${i * -2}px)`,
|
|
70
|
-
zIndex:
|
|
90
|
+
zIndex: safePeople.length - i,
|
|
71
91
|
opacity: activeIndex === i ? 1 : 0,
|
|
72
92
|
}}
|
|
73
93
|
/>
|
|
File without changes
|
|
@@ -87,30 +87,37 @@ void main() {
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
export const SmokeBackground = ({
|
|
90
|
-
color = "#
|
|
90
|
+
color = "#00c950",
|
|
91
91
|
speed = 1,
|
|
92
92
|
direction = "forward",
|
|
93
93
|
scale = 1,
|
|
94
94
|
opacity = 1,
|
|
95
95
|
mouseInteractive = true,
|
|
96
96
|
className = "",
|
|
97
|
+
maxWidth = 'max-w-4xl',
|
|
97
98
|
}) => {
|
|
98
99
|
const containerRef = useRef(null);
|
|
99
100
|
const mousePos = useRef({ x: 0, y: 0 });
|
|
100
101
|
|
|
101
102
|
useEffect(() => {
|
|
102
|
-
|
|
103
|
+
const mountNode = containerRef.current;
|
|
104
|
+
if (!mountNode) return;
|
|
103
105
|
|
|
104
106
|
const useCustomColor = color ? 1.0 : 0.0;
|
|
105
107
|
const customColorRgb = color ? hexToRgb(color) : [1, 1, 1];
|
|
106
108
|
|
|
107
109
|
let directionMultiplier = direction === "backward" ? -1.0 : 1.0;
|
|
108
110
|
|
|
111
|
+
// cap DPR depending on mount width to avoid heavy pixel loads on small devices
|
|
112
|
+
const mountWidth = mountNode.clientWidth || window.innerWidth;
|
|
113
|
+
const dprCap = mountWidth > 1200 ? 1.8 : (mountWidth > 768 ? 1.4 : 1.0);
|
|
114
|
+
const dpr = Math.min(window.devicePixelRatio || 1, dprCap);
|
|
115
|
+
|
|
109
116
|
const renderer = new Renderer({
|
|
110
117
|
webgl: 2,
|
|
111
118
|
alpha: true,
|
|
112
119
|
antialias: false,
|
|
113
|
-
dpr
|
|
120
|
+
dpr,
|
|
114
121
|
});
|
|
115
122
|
|
|
116
123
|
const gl = renderer.gl;
|
|
@@ -118,7 +125,7 @@ export const SmokeBackground = ({
|
|
|
118
125
|
canvas.style.display = "block";
|
|
119
126
|
canvas.style.width = "100%";
|
|
120
127
|
canvas.style.height = "100%";
|
|
121
|
-
|
|
128
|
+
mountNode.appendChild(canvas);
|
|
122
129
|
|
|
123
130
|
const geometry = new Triangle(gl);
|
|
124
131
|
|
|
@@ -145,17 +152,17 @@ export const SmokeBackground = ({
|
|
|
145
152
|
let mouseNeedsUpdate = false;
|
|
146
153
|
const handleMouseMove = (e) => {
|
|
147
154
|
if (!mouseInteractive) return;
|
|
148
|
-
const rect =
|
|
155
|
+
const rect = mountNode.getBoundingClientRect();
|
|
149
156
|
mousePos.current.x = e.clientX - rect.left;
|
|
150
157
|
mousePos.current.y = rect.height - (e.clientY - rect.top);
|
|
151
158
|
mouseNeedsUpdate = true;
|
|
152
159
|
};
|
|
153
160
|
if (mouseInteractive) {
|
|
154
|
-
|
|
161
|
+
mountNode.addEventListener("mousemove", handleMouseMove);
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
const setSize = () => {
|
|
158
|
-
const rect =
|
|
165
|
+
const rect = mountNode.getBoundingClientRect();
|
|
159
166
|
const width = Math.max(1, Math.floor(rect.width));
|
|
160
167
|
const height = Math.max(1, Math.floor(rect.height));
|
|
161
168
|
renderer.setSize(width, height);
|
|
@@ -167,7 +174,7 @@ export const SmokeBackground = ({
|
|
|
167
174
|
const ro = new ResizeObserver(() => {
|
|
168
175
|
requestAnimationFrame(setSize);
|
|
169
176
|
});
|
|
170
|
-
ro.observe(
|
|
177
|
+
ro.observe(mountNode);
|
|
171
178
|
setSize();
|
|
172
179
|
|
|
173
180
|
let raf = 0;
|
|
@@ -196,20 +203,21 @@ export const SmokeBackground = ({
|
|
|
196
203
|
return () => {
|
|
197
204
|
cancelAnimationFrame(raf);
|
|
198
205
|
ro.disconnect();
|
|
199
|
-
if (mouseInteractive &&
|
|
200
|
-
|
|
206
|
+
if (mouseInteractive && mountNode) {
|
|
207
|
+
mountNode.removeEventListener("mousemove", handleMouseMove);
|
|
201
208
|
}
|
|
202
209
|
try {
|
|
203
|
-
|
|
210
|
+
mountNode.removeChild(canvas);
|
|
204
211
|
} catch (error) { console.error("Error removing canvas:", error); }
|
|
205
212
|
};
|
|
206
213
|
}, [color, speed, direction, scale, opacity, mouseInteractive, className]);
|
|
207
214
|
|
|
208
215
|
return (
|
|
209
|
-
<div
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
<div className={`w-full ${className}`}>
|
|
217
|
+
<div className={`mx-auto w-full ${maxWidth} h-[100vh] lg:h-[90vh] overflow-hidden relative`}>
|
|
218
|
+
<div ref={containerRef} className="absolute inset-0 w-full h-full" />
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
213
221
|
);
|
|
214
222
|
};
|
|
215
223
|
|
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
--color-template-border: #fdc700;
|
|
96
96
|
--color-template-text: #fdc700;
|
|
97
97
|
--color-scan: #460809;
|
|
98
|
+
--selection-bg: #460809;
|
|
99
|
+
--selection-text: #fdc700;
|
|
98
100
|
|
|
99
101
|
/* Hero Component */
|
|
100
102
|
--right-color-bg: #e5c287;
|
|
@@ -246,6 +248,8 @@
|
|
|
246
248
|
--color-btn-hover-text: #d9f99d;
|
|
247
249
|
--color-stroke-color: #9ae600;
|
|
248
250
|
--color-scan: #9ae600;
|
|
251
|
+
--selection-bg: #9ae600;
|
|
252
|
+
--selection-text: #1e2939;
|
|
249
253
|
/* Hero Component */
|
|
250
254
|
--color-stroke-color: #9ae600;
|
|
251
255
|
--color-template: #101828;
|
package/dist/cli/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const componentMap = {
|
|
|
21
21
|
"basic-card": "Cards",
|
|
22
22
|
"edge-glow-from": "EdgeGlowFrom",
|
|
23
23
|
"glare-card": "GlareCard",
|
|
24
|
-
"
|
|
24
|
+
"card-stack": "CardStack",
|
|
25
25
|
"glass-profile": "GlassProfile",
|
|
26
26
|
"hover-text": "HoverText",
|
|
27
27
|
"hyper-card": "HyperCard",
|
package/package.json
CHANGED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */
|
|
2
|
-
import React, { useState, useEffect, useRef } from "react";
|
|
3
|
-
import { motion, AnimatePresence } from "framer-motion";
|
|
4
|
-
import { FaExternalLinkAlt, FaGithub, FaRegImage } from "react-icons/fa";
|
|
5
|
-
const CARD_STACK_OFFSET = 32;
|
|
6
|
-
const CARD_SCALE_STEP = 0.08;
|
|
7
|
-
const AUTO_CYCLE_INTERVAL = 5000;
|
|
8
|
-
export default function GlassCardStack({
|
|
9
|
-
cards = [],
|
|
10
|
-
autoCycle = true,
|
|
11
|
-
cycleInterval = AUTO_CYCLE_INTERVAL,
|
|
12
|
-
}) {
|
|
13
|
-
const [active, setActive] = useState(0);
|
|
14
|
-
const [paused, setPaused] = useState(false);
|
|
15
|
-
const timeoutRef = useRef();
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!autoCycle || paused) return;
|
|
19
|
-
timeoutRef.current = setTimeout(() => {
|
|
20
|
-
setActive((prev) => (prev + 1) % cards.length);
|
|
21
|
-
}, cycleInterval);
|
|
22
|
-
return () => clearTimeout(timeoutRef.current);
|
|
23
|
-
}, [active, paused, autoCycle, cycleInterval, cards.length]);
|
|
24
|
-
|
|
25
|
-
// For cycling animation
|
|
26
|
-
const getCardIndex = (i) => (active + i) % cards.length;
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div
|
|
30
|
-
className="relative flex justify-center items-center h-[340px] md:h-[400px] w-full max-w-lg mx-auto select-none"
|
|
31
|
-
onMouseEnter={() => setPaused(true)}
|
|
32
|
-
onMouseLeave={() => setPaused(false)}
|
|
33
|
-
>
|
|
34
|
-
{cards.map((_, i) => {
|
|
35
|
-
// Stack order: 0 is top, 1 is next, etc.
|
|
36
|
-
const idx = getCardIndex(i);
|
|
37
|
-
const isTop = i === 0;
|
|
38
|
-
const z = cards.length - i;
|
|
39
|
-
const offset = i * CARD_STACK_OFFSET;
|
|
40
|
-
const scale = 1 - i * CARD_SCALE_STEP;
|
|
41
|
-
const card = cards[idx];
|
|
42
|
-
return (
|
|
43
|
-
<AnimatePresence key={idx}>
|
|
44
|
-
<motion.div
|
|
45
|
-
key={idx}
|
|
46
|
-
initial={{
|
|
47
|
-
y: 60,
|
|
48
|
-
opacity: 0,
|
|
49
|
-
scale: scale * 0.95,
|
|
50
|
-
}}
|
|
51
|
-
animate={{
|
|
52
|
-
y: -offset,
|
|
53
|
-
opacity: 1,
|
|
54
|
-
scale,
|
|
55
|
-
zIndex: z,
|
|
56
|
-
filter: isTop ? "none" : "blur(2px)",
|
|
57
|
-
}}
|
|
58
|
-
exit={{
|
|
59
|
-
y: -100,
|
|
60
|
-
opacity: 0,
|
|
61
|
-
scale: scale * 0.95,
|
|
62
|
-
}}
|
|
63
|
-
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
|
64
|
-
className={`absolute left-0 right-0 mx-auto w-11/12 md:w-96 h-48 md:h-60 rounded-3xl shadow-xl bg-gradient-to-br ${card.color} backdrop-blur-lg border border-white/20 flex flex-row items-center px-3 md:px-6 py-4 cursor-pointer select-none ${isTop ? "" : "pointer-events-none"}`}
|
|
65
|
-
style={{ zIndex: z }}
|
|
66
|
-
>
|
|
67
|
-
{/* Left: Responsive Image */}
|
|
68
|
-
<div className="flex-shrink-0 flex items-center justify-center w-16 h-16 md:w-24 md:h-24 lg:w-28 lg:h-28 rounded-xl overflow-hidden bg-white/10 border border-white/20 mr-3 md:mr-6">
|
|
69
|
-
{card.image ? (
|
|
70
|
-
<img
|
|
71
|
-
src={card.image}
|
|
72
|
-
alt={card.title}
|
|
73
|
-
className="object-cover w-full h-full"
|
|
74
|
-
/>
|
|
75
|
-
) : (
|
|
76
|
-
<FaRegImage className="text-3xl text-white/40" />
|
|
77
|
-
)}
|
|
78
|
-
</div>
|
|
79
|
-
{/* Right: Info */}
|
|
80
|
-
<div className="flex-1 flex flex-col justify-center gap-1 md:gap-2">
|
|
81
|
-
<div className="flex items-center gap-2">
|
|
82
|
-
<h2 className="text-base md:text-xl font-bold text-white drop-shadow">
|
|
83
|
-
{card.title}
|
|
84
|
-
</h2>
|
|
85
|
-
{card.github && (
|
|
86
|
-
<a href={card.github} target="_blank" rel="noopener noreferrer" className="text-white/70 hover:text-white ml-1">
|
|
87
|
-
<FaGithub className="inline-block align-middle text-lg md:text-xl" />
|
|
88
|
-
</a>
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
<p className="text-xs md:text-sm text-white/60 mb-1">{card.subtitle}</p>
|
|
92
|
-
<p className="text-xs md:text-base text-white/80 mb-1 line-clamp-2">{card.desc}</p>
|
|
93
|
-
{card.link && (
|
|
94
|
-
<a
|
|
95
|
-
href={card.link}
|
|
96
|
-
target="_blank"
|
|
97
|
-
rel="noopener noreferrer"
|
|
98
|
-
className="inline-flex items-center gap-1 text-xs md:text-sm px-2 py-1 rounded bg-blue-500/80 hover:bg-blue-600/90 text-white font-semibold shadow transition w-fit mt-1"
|
|
99
|
-
>
|
|
100
|
-
<FaExternalLinkAlt className="inline-block mr-1 text-xs md:text-sm" />
|
|
101
|
-
Visit
|
|
102
|
-
</a>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
</motion.div>
|
|
106
|
-
</AnimatePresence>
|
|
107
|
-
);
|
|
108
|
-
})}
|
|
109
|
-
</div>
|
|
110
|
-
);
|
|
111
|
-
}
|