apex-ui-kit 2.0.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +74 -53
- package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +5 -3
- package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +14 -2
- package/dist/ApexUI-Kit/templates/CardStack/CardStack.jsx +56 -28
- package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +3 -3
- package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +1 -1
- package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +116 -98
- package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +69 -37
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +2 -2
- package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +14 -13
- package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +59 -42
- package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +17 -2
- package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +18 -3
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +15 -1
- package/dist/ApexUI-Kit/templates/MaskView/MaskView.jsx +345 -0
- package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +34 -45
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +272 -101
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +8 -2
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +13 -11
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +9 -9
- package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +41 -38
- package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +7 -7
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +4 -3
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +13 -2
- package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +11 -8
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +40 -69
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +1 -1
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +25 -7
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +71 -52
- package/dist/cli/index.js +24 -13
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +0 -151
|
@@ -7,7 +7,7 @@ const LinkedInIcon = () => (<svg width="24" height="24" fill="none" stroke="curr
|
|
|
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
9
|
const defaultProfile = {
|
|
10
|
-
avatarUrl: "/assets/spiderman.png",
|
|
10
|
+
avatarUrl: "https://ik.imagekit.io/ny6yinyut/apexui/assets/spiderman.png?tr=f-auto,q-80,w-480",
|
|
11
11
|
name: "Spider Man",
|
|
12
12
|
username: "spider_man",
|
|
13
13
|
dob: "10-08-2001",
|
|
@@ -44,7 +44,7 @@ const ParallaxProfileCard = ({ profile }) => {
|
|
|
44
44
|
gsap.to(card, { duration: 1, rotationX: 0, rotationY: 0, scale: 1, ease: "elastic.out(1, 0.3)" });
|
|
45
45
|
};
|
|
46
46
|
gsap.set(card, { transformPerspective: 1000 });
|
|
47
|
-
card.addEventListener("mousemove", handleMouseMove);
|
|
47
|
+
card.addEventListener("mousemove", handleMouseMove, { passive: true });
|
|
48
48
|
card.addEventListener("mouseleave", handleMouseLeave);
|
|
49
49
|
return () => {
|
|
50
50
|
card.removeEventListener("mousemove", handleMouseMove);
|
|
@@ -52,51 +52,54 @@ const ParallaxProfileCard = ({ profile }) => {
|
|
|
52
52
|
};
|
|
53
53
|
}, []);
|
|
54
54
|
|
|
55
|
+
const iconBtn = "flex h-9 w-9 items-center justify-center rounded-full border border-cyan-400/20 bg-cyan-400/5 text-cyan-300 transition-colors duration-200 hover:border-cyan-300/60 hover:text-white [&_svg]:h-[18px] [&_svg]:w-[18px]";
|
|
56
|
+
|
|
55
57
|
return (
|
|
56
58
|
<div
|
|
57
59
|
ref={cardRef}
|
|
58
|
-
className="relative w-[300px]
|
|
60
|
+
className="relative w-[300px] max-w-full bg-[#1D2B3A] rounded-3xl p-6 border border-cyan-400/30 shadow-2xl shadow-cyan-900/40"
|
|
59
61
|
style={{ transformStyle: "preserve-3d" }}
|
|
60
62
|
>
|
|
61
|
-
<div className="absolute inset-0 bg-gradient-to-br from-[#1D2B3A] to-[#
|
|
62
|
-
<div className="absolute
|
|
63
|
+
<div className="absolute inset-0 bg-gradient-to-br from-[#1D2B3A] to-[#0b1220] rounded-3xl"></div>
|
|
64
|
+
<div className="absolute inset-x-0 top-0 h-28 rounded-t-3xl" style={{ background: "radial-gradient(120% 100% at 50% 0%, rgba(34, 211, 238, 0.22), transparent 70%)", transform: "translateZ(20px)" }}></div>
|
|
63
65
|
|
|
64
|
-
<div className="relative z-10 flex flex-col items-center
|
|
65
|
-
{/*
|
|
66
|
-
<div className="
|
|
67
|
-
<img src={avatarUrl} alt="Profile" className="w-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
<div className="relative z-10 flex flex-col items-center text-center" style={{ transform: "translateZ(50px)" }}>
|
|
67
|
+
{/* Avatar with status dot */}
|
|
68
|
+
<div className="relative">
|
|
69
|
+
<img src={avatarUrl} alt="Profile" className="w-24 h-24 rounded-full border-[3px] border-cyan-400 shadow-lg shadow-cyan-500/30 object-cover" onError={(e) => { e.target.onerror = null; e.target.src = "https://placehold.co/128x128/1D2B3A/A5F3FC?text=ERR"; }} />
|
|
70
|
+
{status && <span className="absolute bottom-1 right-1 h-4 w-4 rounded-full border-[3px] border-[#1D2B3A] bg-green-500" />}
|
|
71
|
+
</div>
|
|
72
|
+
<h2 className="mt-3 text-2xl font-bold text-cyan-300 tracking-wide">{name}</h2>
|
|
73
|
+
<p className="text-sm text-slate-400">{title}</p>
|
|
74
|
+
<div className="mt-3 flex flex-wrap items-center justify-center gap-1.5">
|
|
75
|
+
<span className="text-[11px] font-semibold text-slate-300 bg-slate-700/70 px-2.5 py-1 rounded-full">@{username}</span>
|
|
76
|
+
<span className="text-[11px] font-medium text-cyan-300 bg-slate-700/70 px-2.5 py-1 rounded-full">DOB: {dob}</span>
|
|
73
77
|
</div>
|
|
74
78
|
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<div className="flex items-center justify-center gap-2 mt-2">
|
|
80
|
-
<span className="relative flex h-3 w-3"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span></span>
|
|
81
|
-
<span className="text-green-300 text-sm">{status}</span>
|
|
82
|
-
</div>
|
|
83
|
-
)}
|
|
84
|
-
<div className="flex justify-center gap-4 my-4">
|
|
85
|
-
{socialLinks?.github && (<a href={socialLinks.github} target="_blank" rel="noopener noreferrer" title="GitHub" className="text-cyan-400 hover:text-white transition-colors duration-200"><GitHubIcon /></a>)}
|
|
86
|
-
{socialLinks?.linkedin && (<a href={socialLinks.linkedin} target="_blank" rel="noopener noreferrer" title="LinkedIn" className="text-cyan-400 hover:text-white transition-colors duration-200"><LinkedInIcon /></a>)}
|
|
87
|
-
{socialLinks?.twitter && (<a href={socialLinks.twitter} target="_blank" rel="noopener noreferrer" title="Twitter" className="text-cyan-400 hover:text-white transition-colors duration-200"><TwitterIcon /></a>)}
|
|
88
|
-
{socialLinks?.instagram && (<a href={socialLinks.instagram} target="_blank" rel="noopener noreferrer" title="Instagram" className="text-cyan-400 hover:text-white transition-colors duration-200"><InstagramIcon /></a>)}
|
|
79
|
+
{status && (
|
|
80
|
+
<div className="mt-4 inline-flex items-center gap-2 rounded-full border border-green-400/20 bg-green-400/5 px-3 py-1">
|
|
81
|
+
<span className="relative flex h-2 w-2"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span></span>
|
|
82
|
+
<span className="text-green-300 text-xs font-medium">{status}</span>
|
|
89
83
|
</div>
|
|
84
|
+
)}
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
<div className="my-5 h-px w-full bg-gradient-to-r from-transparent via-cyan-400/25 to-transparent" />
|
|
87
|
+
|
|
88
|
+
<div className="flex justify-center gap-2.5">
|
|
89
|
+
{socialLinks?.github && (<a href={socialLinks.github} target="_blank" rel="noopener noreferrer" title="GitHub" className={iconBtn}><GitHubIcon /></a>)}
|
|
90
|
+
{socialLinks?.linkedin && (<a href={socialLinks.linkedin} target="_blank" rel="noopener noreferrer" title="LinkedIn" className={iconBtn}><LinkedInIcon /></a>)}
|
|
91
|
+
{socialLinks?.twitter && (<a href={socialLinks.twitter} target="_blank" rel="noopener noreferrer" title="Twitter" className={iconBtn}><TwitterIcon /></a>)}
|
|
92
|
+
{socialLinks?.instagram && (<a href={socialLinks.instagram} target="_blank" rel="noopener noreferrer" title="Instagram" className={iconBtn}><InstagramIcon /></a>)}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Buttons Section */}
|
|
96
|
+
<div className="mt-5 flex w-full gap-3">
|
|
97
|
+
<button onClick={onHireClick} className="flex-1 whitespace-nowrap bg-cyan-500 text-slate-900 text-sm font-bold py-2.5 px-3 rounded-xl hover:bg-cyan-400 transition-colors duration-300">
|
|
98
|
+
Hire Me
|
|
99
|
+
</button>
|
|
100
|
+
<button onClick={onCvClick} className="flex-1 whitespace-nowrap bg-slate-700/80 text-cyan-300 text-sm font-bold py-2.5 px-3 rounded-xl hover:bg-slate-600 transition-colors duration-300">
|
|
101
|
+
Download CV
|
|
102
|
+
</button>
|
|
100
103
|
</div>
|
|
101
104
|
</div>
|
|
102
105
|
</div>
|
|
@@ -106,7 +109,7 @@ const ParallaxProfileCard = ({ profile }) => {
|
|
|
106
109
|
const ParallaxProfile = ({ profiles = [defaultProfile], className = '' }) => {
|
|
107
110
|
const safe = Array.isArray(profiles) && profiles.length > 0 ? profiles : [defaultProfile];
|
|
108
111
|
return (
|
|
109
|
-
<div className={`flex flex-wrap gap-6 justify-center ${className}`}>
|
|
112
|
+
<div className={`flex w-full flex-wrap gap-6 justify-center ${className}`}>
|
|
110
113
|
{safe.map((p, i) => (
|
|
111
114
|
<ParallaxProfileCard key={p.id || p.username || i} profile={p} />
|
|
112
115
|
))}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
const defaultPeople = [
|
|
4
|
-
{ id: 1, name: "Captain America", job: "Leader of the Avengers", img: "/assets/captainamerica.png" },
|
|
5
|
-
{ id: 2, name: "Doctor Strange", job: "Sorcerer Supreme", img: "/assets/doctorStrange.png" },
|
|
6
|
-
{ id: 3, name: "Iron Man", job: "Leader Of Stark Industries", img: "/assets/ironman.png" },
|
|
7
|
-
{ id: 4, name: "HULK", job: "Scientist", img: "/assets/hulk.png" },
|
|
8
|
-
{ id: 5, name: "Spider-Man", job: "Friendly Neighborhood Spider-Man", img: "/assets/spiderman.png" },
|
|
9
|
-
{ id: 6, name: "Thanos", job: "The Mad Titan", img: "/assets/thanos.png" },
|
|
4
|
+
{ id: 1, name: "Captain America", job: "Leader of the Avengers", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/captainamerica.png?tr=f-auto,q-80,w-480" },
|
|
5
|
+
{ id: 2, name: "Doctor Strange", job: "Sorcerer Supreme", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/doctorStrange.png?tr=f-auto,q-80,w-480" },
|
|
6
|
+
{ id: 3, name: "Iron Man", job: "Leader Of Stark Industries", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/ironman.png?tr=f-auto,q-80,w-480" },
|
|
7
|
+
{ id: 4, name: "HULK", job: "Scientist", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/hulk.png?tr=f-auto,q-80,w-480" },
|
|
8
|
+
{ id: 5, name: "Spider-Man", job: "Friendly Neighborhood Spider-Man", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/spiderman.png?tr=f-auto,q-80,w-480" },
|
|
9
|
+
{ id: 6, name: "Thanos", job: "The Mad Titan", img: "https://ik.imagekit.io/ny6yinyut/apexui/assets/thanos.png?tr=f-auto,q-80,w-480" },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
const PointerFollower = ({
|
|
@@ -53,7 +53,7 @@ const PointerFollower = ({
|
|
|
53
53
|
setHovered(false);
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
container.addEventListener("pointermove", handleMove);
|
|
56
|
+
container.addEventListener("pointermove", handleMove, { passive: true });
|
|
57
57
|
container.addEventListener("pointerleave", handleLeave);
|
|
58
58
|
|
|
59
59
|
return () => {
|
|
@@ -123,10 +123,11 @@ const SmartGridCard = ({
|
|
|
123
123
|
<div
|
|
124
124
|
className={`grid gap-2 justify-center p-4 w-full ${screenWidth >= 1024 ? 'grid-cols-3' : screenWidth >= 768 ? 'grid-cols-2' : 'grid-cols-1'}`}
|
|
125
125
|
style={screenWidth >= 1024 ? {
|
|
126
|
-
|
|
126
|
+
// minmax(0, …): columns share the container's width instead of overflowing it.
|
|
127
|
+
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
|
127
128
|
gridTemplateRows: `repeat(2, ${CARD_HEIGHT}px)`
|
|
128
129
|
} : screenWidth >= 768 ? {
|
|
129
|
-
gridTemplateColumns: "repeat(2, minmax(
|
|
130
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
130
131
|
gridTemplateRows: `repeat(3, ${CARD_HEIGHT}px)`
|
|
131
132
|
} : {}}
|
|
132
133
|
>
|
|
@@ -164,7 +165,7 @@ const SmartGridCard = ({
|
|
|
164
165
|
</div>
|
|
165
166
|
<button
|
|
166
167
|
type="button"
|
|
167
|
-
className="mt-auto relative overflow-hidden px-
|
|
168
|
+
className="mt-auto relative overflow-hidden whitespace-nowrap px-4 py-3 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-xs md:text-base"
|
|
168
169
|
>
|
|
169
170
|
<span className="relative z-10 flex items-center gap-2">
|
|
170
171
|
{card.buttonText}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from "react";
|
|
2
|
-
import { Renderer, Program, Mesh, Triangle } from "
|
|
2
|
+
import { Renderer, Program, Mesh, Triangle } from "ogl";
|
|
3
3
|
|
|
4
4
|
const hexToRgb = (hex) => {
|
|
5
5
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
@@ -158,7 +158,7 @@ export const SmokeBackground = ({
|
|
|
158
158
|
mouseNeedsUpdate = true;
|
|
159
159
|
};
|
|
160
160
|
if (mouseInteractive) {
|
|
161
|
-
mountNode.addEventListener("mousemove", handleMouseMove);
|
|
161
|
+
mountNode.addEventListener("mousemove", handleMouseMove, { passive: true });
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
const setSize = () => {
|
|
@@ -178,8 +178,12 @@ export const SmokeBackground = ({
|
|
|
178
178
|
setSize();
|
|
179
179
|
|
|
180
180
|
let raf = 0;
|
|
181
|
+
// Only render while the background is on screen.
|
|
182
|
+
let visible = true;
|
|
181
183
|
const t0 = performance.now();
|
|
182
184
|
const loop = (t) => {
|
|
185
|
+
raf = 0;
|
|
186
|
+
if (!visible) return;
|
|
183
187
|
let timeValue = (t - t0) * 0.001;
|
|
184
188
|
|
|
185
189
|
if (direction === "funny") {
|
|
@@ -199,9 +203,15 @@ export const SmokeBackground = ({
|
|
|
199
203
|
raf = requestAnimationFrame(loop);
|
|
200
204
|
};
|
|
201
205
|
raf = requestAnimationFrame(loop);
|
|
206
|
+
const visibilityObserver = new IntersectionObserver(([entry]) => {
|
|
207
|
+
visible = entry.isIntersecting;
|
|
208
|
+
if (visible && !raf) raf = requestAnimationFrame(loop);
|
|
209
|
+
});
|
|
210
|
+
visibilityObserver.observe(mountNode);
|
|
202
211
|
|
|
203
212
|
return () => {
|
|
204
213
|
cancelAnimationFrame(raf);
|
|
214
|
+
visibilityObserver.disconnect();
|
|
205
215
|
ro.disconnect();
|
|
206
216
|
if (mouseInteractive && mountNode) {
|
|
207
217
|
mountNode.removeEventListener("mousemove", handleMouseMove);
|
|
@@ -209,6 +219,7 @@ export const SmokeBackground = ({
|
|
|
209
219
|
try {
|
|
210
220
|
mountNode.removeChild(canvas);
|
|
211
221
|
} catch (error) { console.error("Error removing canvas:", error); }
|
|
222
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
212
223
|
};
|
|
213
224
|
}, [color, speed, direction, scale, opacity, mouseInteractive, className]);
|
|
214
225
|
|
|
@@ -24,12 +24,12 @@ const Testimonials = ({ testimonials }) => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<div className="w-full max-w-4xl mx-auto bg-slate-950 rounded-2xl p-8 md:p-12 border border-slate-800 grid md:grid-cols-2 gap-12 items-center overflow-hidden">
|
|
27
|
+
<div className="w-full max-w-4xl mx-auto bg-slate-950 rounded-2xl p-6 sm:p-8 md:p-12 border border-slate-800 grid md:grid-cols-2 gap-8 md:gap-12 items-center overflow-hidden">
|
|
28
28
|
|
|
29
29
|
{/* Left Part: Text Content and Navigation */}
|
|
30
30
|
<div className="flex flex-col justify-center h-full text-center md:text-left order-2 md:order-1">
|
|
31
|
-
{/* Title and
|
|
32
|
-
<div className="relative h-
|
|
31
|
+
{/* Title, subtitle and quote with animation; the min height keeps the buttons from jumping. */}
|
|
32
|
+
<div className="relative min-h-[13rem] sm:min-h-[12rem]">
|
|
33
33
|
<AnimatePresence initial={false} mode="wait">
|
|
34
34
|
<motion.div
|
|
35
35
|
key={activeIndex}
|
|
@@ -38,11 +38,11 @@ const Testimonials = ({ testimonials }) => {
|
|
|
38
38
|
animate="center"
|
|
39
39
|
exit="exit"
|
|
40
40
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
|
41
|
-
className="
|
|
41
|
+
className="w-full"
|
|
42
42
|
>
|
|
43
|
-
<h2 className="text-3xl font-bold text-white tracking-tight">{testimonials[activeIndex].author}</h2>
|
|
43
|
+
<h2 className="text-2xl sm:text-3xl font-bold text-white tracking-tight">{testimonials[activeIndex].author}</h2>
|
|
44
44
|
<p className="mt-2 text-lime-400 font-medium">{testimonials[activeIndex].title}</p>
|
|
45
|
-
<p className="mt-4 text-slate-400 text-sm leading-relaxed
|
|
45
|
+
<p className="mt-4 text-slate-400 text-sm leading-relaxed">"{testimonials[activeIndex].text}"</p>
|
|
46
46
|
</motion.div>
|
|
47
47
|
</AnimatePresence>
|
|
48
48
|
</div>
|
|
@@ -71,7 +71,9 @@ const Testimonials = ({ testimonials }) => {
|
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
{/* Right Part: Stacked Images */}
|
|
74
|
-
<div className="
|
|
74
|
+
<div className="flex w-full items-center justify-center order-1 md:order-2">
|
|
75
|
+
{/* Sized to the card plus the fan, so the stack stays centred and inside the panel. */}
|
|
76
|
+
<div className="relative h-[calc(16rem+30px)] w-[calc(12rem+60px)] sm:h-[calc(20rem+30px)] sm:w-[calc(16rem+60px)]">
|
|
75
77
|
{testimonials.map((testimonial, index) => {
|
|
76
78
|
// Determine the position of the card in the stack
|
|
77
79
|
const position = (index - activeIndex + testimonials.length) % testimonials.length;
|
|
@@ -82,7 +84,7 @@ const Testimonials = ({ testimonials }) => {
|
|
|
82
84
|
return (
|
|
83
85
|
<motion.div
|
|
84
86
|
key={testimonial.id || index}
|
|
85
|
-
className="absolute w-64 h-80 bg-slate-800 rounded-xl shadow-2xl overflow-hidden border-2 border-slate-700"
|
|
87
|
+
className="absolute w-48 h-64 sm:w-64 sm:h-80 bg-slate-800 rounded-xl shadow-2xl overflow-hidden border-2 border-slate-700"
|
|
86
88
|
initial={false}
|
|
87
89
|
animate={{
|
|
88
90
|
scale: 1 - position * 0.08,
|
|
@@ -107,6 +109,7 @@ const Testimonials = ({ testimonials }) => {
|
|
|
107
109
|
</motion.div>
|
|
108
110
|
);
|
|
109
111
|
})}
|
|
112
|
+
</div>
|
|
110
113
|
</div>
|
|
111
114
|
</div>
|
|
112
115
|
);
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
// config/animations.js
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// Soft-edged circle that grows from `at` until it covers the screen. It animates
|
|
4
|
+
// a radial-gradient mask through a registered property, which stays smooth;
|
|
5
|
+
// scaling a blurred SVG mask to screen size re-rasterises every frame and flickers.
|
|
6
|
+
const circleReveal = (at) => `
|
|
7
|
+
@property --vt-r { syntax: '<length>'; inherits: false; initial-value: 0px; }
|
|
8
|
+
::view-transition-new(root) {
|
|
9
|
+
-webkit-mask-image: radial-gradient(circle at ${at}, #000 var(--vt-r), transparent calc(var(--vt-r) + 12vmax));
|
|
10
|
+
mask-image: radial-gradient(circle at ${at}, #000 var(--vt-r), transparent calc(var(--vt-r) + 12vmax));
|
|
11
|
+
animation: vt-circle var(--vt-duration) var(--vt-ease);
|
|
12
|
+
}
|
|
13
|
+
::view-transition-old(root) { animation: none; }
|
|
14
|
+
@keyframes vt-circle {
|
|
15
|
+
from { --vt-r: -12vmax; }
|
|
16
|
+
to { --vt-r: 150vmax; }
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
// Holds the last frame until the transition ends (no flash of the old theme)
|
|
21
|
+
// and keeps the browser's default plus-lighter blending out of the reveal.
|
|
22
|
+
const footer = `
|
|
23
|
+
::view-transition-old(root), ::view-transition-new(root) {
|
|
24
|
+
animation-fill-mode: both;
|
|
25
|
+
mix-blend-mode: normal;
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
5
28
|
|
|
6
29
|
export function buildAnimationCSS(animation, opts = {}) {
|
|
30
|
+
return presetCSS(animation, opts) + footer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function presetCSS(animation, opts = {}) {
|
|
7
34
|
const duration = opts.duration || "1.5s";
|
|
8
35
|
const ease = opts.ease || "var(--vt-ease)";
|
|
9
36
|
|
|
37
|
+
// `var(--vt-ease)` would refer to itself here, so only set a real easing.
|
|
10
38
|
const header = `
|
|
11
|
-
:root { --vt-duration: ${duration}
|
|
39
|
+
:root { --vt-duration: ${duration};${ease === "var(--vt-ease)" ? "" : ` --vt-ease: ${ease};`} }
|
|
12
40
|
::view-transition-group(root){ animation-timing-function: var(--vt-ease); }
|
|
13
41
|
`;
|
|
14
42
|
|
|
@@ -37,72 +65,15 @@ export function buildAnimationCSS(animation, opts = {}) {
|
|
|
37
65
|
const preset = typeof animation === "string" ? animation : animation?.type;
|
|
38
66
|
|
|
39
67
|
switch (preset) {
|
|
40
|
-
case "circle":
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
return header + `
|
|
45
|
-
::view-transition-new(root) {
|
|
46
|
-
mask: url('data:image/svg+xml,${circleSVG}') center / 0 no-repeat;
|
|
47
|
-
-webkit-mask: url('data:image/svg+xml,${circleSVG}') center / 0 no-repeat;
|
|
48
|
-
animation: vt-scale var(--vt-duration);
|
|
49
|
-
}
|
|
50
|
-
::view-transition-old(root), .dark::view-transition-old(root) {
|
|
51
|
-
animation: vt-scale var(--vt-duration);
|
|
52
|
-
}
|
|
53
|
-
@keyframes vt-scale {
|
|
54
|
-
to { mask-size: 200vmax; -webkit-mask-size: 200vmax; }
|
|
55
|
-
}
|
|
56
|
-
`;
|
|
57
|
-
}
|
|
68
|
+
case "circle":
|
|
69
|
+
// Grows from the clicked button (ThemeToggle sets --cx / --cy).
|
|
70
|
+
return header + circleReveal("var(--cx, 50%) var(--cy, 50%)");
|
|
58
71
|
|
|
59
|
-
case "circle-left":
|
|
60
|
-
|
|
61
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="b"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="0" cy="0" r="18" fill="white" filter="url(%23b)"/></svg>`
|
|
62
|
-
);
|
|
63
|
-
return header + `
|
|
64
|
-
::view-transition-new(root) {
|
|
65
|
-
mask: url('data:image/svg+xml,${svg}') top left / 0 no-repeat;
|
|
66
|
-
-webkit-mask: url('data:image/svg+xml,${svg}') top left / 0 no-repeat;
|
|
67
|
-
transform-origin: top left;
|
|
68
|
-
animation: vt-scale-tl var(--vt-duration);
|
|
69
|
-
}
|
|
70
|
-
::view-transition-old(root), .dark::view-transition-old(root) {
|
|
71
|
-
transform-origin: top left;
|
|
72
|
-
animation: vt-scale-tl var(--vt-duration);
|
|
73
|
-
}
|
|
74
|
-
@keyframes vt-scale-tl { to { mask-size: 350vmax; -webkit-mask-size: 350vmax; } }
|
|
75
|
-
`;
|
|
76
|
-
}
|
|
77
|
-
case "circle-right": {
|
|
78
|
-
const svg = esc(
|
|
79
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
80
|
-
<defs><filter id="b"><feGaussianBlur stdDeviation="2"/></filter></defs>
|
|
81
|
-
<circle cx="40" cy="0" r="18" fill="white" filter="url(%23b)"/>
|
|
82
|
-
</svg>`
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
return header + `
|
|
86
|
-
::view-transition-new(root) {
|
|
87
|
-
mask: url('data:image/svg+xml,${svg}') top right / 0 no-repeat;
|
|
88
|
-
-webkit-mask: url('data:image/svg+xml,${svg}') top right / 0 no-repeat;
|
|
89
|
-
transform-origin: top right;
|
|
90
|
-
animation: vt-scale-tr 0.9s;
|
|
91
|
-
}
|
|
72
|
+
case "circle-left":
|
|
73
|
+
return header + circleReveal("0 0");
|
|
92
74
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
animation: vt-scale-tr 0.9s;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@keyframes vt-scale-tr {
|
|
99
|
-
to {
|
|
100
|
-
mask-size: 350vmax;
|
|
101
|
-
-webkit-mask-size: 350vmax;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
`;
|
|
105
|
-
}
|
|
75
|
+
case "circle-right":
|
|
76
|
+
return header + circleReveal("100% 0");
|
|
106
77
|
case "split-reveal-horizontal": {
|
|
107
78
|
return header + `
|
|
108
79
|
::view-transition-new(root) {
|
|
@@ -241,7 +212,7 @@ export function buildAnimationCSS(animation, opts = {}) {
|
|
|
241
212
|
}
|
|
242
213
|
`;
|
|
243
214
|
}
|
|
244
|
-
|
|
215
|
+
// Add more animation
|
|
245
216
|
|
|
246
217
|
|
|
247
218
|
|
|
@@ -249,4 +220,4 @@ export function buildAnimationCSS(animation, opts = {}) {
|
|
|
249
220
|
default:
|
|
250
221
|
return header; // no-op if unknown
|
|
251
222
|
}
|
|
252
|
-
}
|
|
223
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useId } from "react";
|
|
2
|
+
import { flushSync } from "react-dom";
|
|
2
3
|
import { FiSun, FiMoon } from "react-icons/fi";
|
|
3
4
|
import { buildAnimationCSS } from "./ThemeAnimation.js";
|
|
4
5
|
|
|
6
|
+
// Swaps the theme-* class on <html>.
|
|
7
|
+
function applyThemeClass(theme) {
|
|
8
|
+
const html = document.documentElement;
|
|
9
|
+
[...html.classList].forEach((c) => c.startsWith("theme-") && html.classList.remove(c));
|
|
10
|
+
html.classList.add(`theme-${theme}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export default function ThemeToggle({
|
|
6
14
|
animation = "circle",
|
|
7
15
|
duration = "1.5s",
|
|
@@ -29,12 +37,19 @@ export default function ThemeToggle({
|
|
|
29
37
|
// ------------------------------
|
|
30
38
|
useEffect(() => {
|
|
31
39
|
const rawCSS = buildAnimationCSS(animation, { duration, ease });
|
|
40
|
+
const suffix = uid.replace(/[^\w-]/g, "");
|
|
32
41
|
|
|
33
|
-
// ✅ Scope selectors with html[vt-owner]
|
|
42
|
+
// ✅ Scope selectors with html[vt-owner]; :root variables and keyframe names
|
|
43
|
+
// too, so several toggles on one page don't override each other's settings.
|
|
34
44
|
const scopedCSS = rawCSS
|
|
45
|
+
.replace(/(?<![-\w])vt-(?=[a-z])/g, `vt-${suffix}-`)
|
|
46
|
+
.replace(/:root\b/g, `html[vt-owner="${uid}"]`)
|
|
35
47
|
.replaceAll("::view-transition-new", `html[vt-owner="${uid}"]::view-transition-new`)
|
|
36
48
|
.replaceAll("::view-transition-old", `html[vt-owner="${uid}"]::view-transition-old`)
|
|
37
|
-
.replaceAll("::view-transition-group", `html[vt-owner="${uid}"]::view-transition-group`)
|
|
49
|
+
.replaceAll("::view-transition-group", `html[vt-owner="${uid}"]::view-transition-group`)
|
|
50
|
+
// No CSS transitions while the view transition runs, otherwise elements are
|
|
51
|
+
// captured mid-fade in the old colours and snap to the new ones afterwards.
|
|
52
|
+
+ `html[vt-owner="${uid}"] *, html[vt-owner="${uid}"] *::before, html[vt-owner="${uid}"] *::after { transition: none !important; }`;
|
|
38
53
|
|
|
39
54
|
let styleEl = document.getElementById(`vt-style-${uid}`);
|
|
40
55
|
if (!styleEl) {
|
|
@@ -49,9 +64,7 @@ export default function ThemeToggle({
|
|
|
49
64
|
// Apply theme globally + notify
|
|
50
65
|
// ------------------------------
|
|
51
66
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
html.classList.forEach((c) => c.startsWith("theme-") && html.classList.remove(c));
|
|
54
|
-
html.classList.add(`theme-${theme}`);
|
|
67
|
+
applyThemeClass(theme);
|
|
55
68
|
|
|
56
69
|
try {
|
|
57
70
|
localStorage.setItem("apexui-theme", theme);
|
|
@@ -88,7 +101,12 @@ export default function ThemeToggle({
|
|
|
88
101
|
// ✅ Use html instead of body
|
|
89
102
|
document.documentElement.setAttribute("vt-owner", uid);
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
// Apply the theme synchronously: the browser takes the transition's "new"
|
|
105
|
+
// snapshot as soon as this returns, before a normal React update would land.
|
|
106
|
+
const run = () => {
|
|
107
|
+
applyThemeClass(nextTheme);
|
|
108
|
+
flushSync(() => setTheme(nextTheme));
|
|
109
|
+
};
|
|
92
110
|
|
|
93
111
|
if (document.startViewTransition) {
|
|
94
112
|
const vt = document.startViewTransition(run);
|
|
@@ -115,4 +133,4 @@ export default function ThemeToggle({
|
|
|
115
133
|
{isLight ? <FiMoon /> : <FiSun />}
|
|
116
134
|
</button>
|
|
117
135
|
);
|
|
118
|
-
}
|
|
136
|
+
}
|