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
|
@@ -1,114 +1,285 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
import { ChevronDown } from 'lucide-react';
|
|
4
|
-
const DropdownPanel = ({ item, isActive }) => {
|
|
5
|
-
const panelRef = useRef(null);
|
|
1
|
+
import React, { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion';
|
|
3
|
+
import { ArrowRight, ArrowUpRight, ChevronDown } from 'lucide-react';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
5
|
+
const ease = [0.22, 1, 0.36, 1];
|
|
6
|
+
const CLOSE_DELAY = 140;
|
|
7
|
+
|
|
8
|
+
/* "services": two columns of feature tiles with a monogram (or a custom icon). */
|
|
9
|
+
const Services = ({ items }) => (
|
|
10
|
+
<ul className="grid w-[min(34rem,calc(100vw-2rem))] grid-cols-1 gap-1 p-2 sm:grid-cols-2">
|
|
11
|
+
{items.map((sub, i) => (
|
|
12
|
+
<li key={i}>
|
|
13
|
+
<a href={sub.href} className="group flex items-start gap-3 rounded-xl p-3 outline-none transition-colors hover:bg-white/[0.05] focus-visible:bg-white/[0.07]">
|
|
14
|
+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-lime-400/20 bg-lime-400/10 text-sm font-semibold text-lime-300 transition-colors group-hover:border-lime-400/40 group-hover:bg-lime-400/15">
|
|
15
|
+
{sub.icon ? <sub.icon size={16} /> : sub.label.charAt(0)}
|
|
16
|
+
</span>
|
|
17
|
+
<span className="min-w-0">
|
|
18
|
+
<span className="flex items-center gap-1 text-sm font-medium text-zinc-100">
|
|
19
|
+
{sub.label}
|
|
20
|
+
<ArrowRight size={13} className="-translate-x-1 text-lime-300 opacity-0 transition-all duration-200 group-hover:translate-x-0 group-hover:opacity-100" />
|
|
21
|
+
</span>
|
|
22
|
+
{sub.subtitle && <span className="mt-0.5 block text-[0.8rem] leading-snug text-zinc-500">{sub.subtitle}</span>}
|
|
23
|
+
</span>
|
|
24
|
+
</a>
|
|
25
|
+
</li>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/* "products": image cards, four across on wide screens. */
|
|
31
|
+
const Products = ({ items }) => (
|
|
32
|
+
<ul className="grid w-[min(40rem,calc(100vw-2rem))] grid-cols-2 gap-2 p-2 sm:grid-cols-4">
|
|
33
|
+
{items.map((sub, i) => (
|
|
34
|
+
<li key={i}>
|
|
35
|
+
<a href={sub.href} className="group block rounded-xl p-1.5 outline-none transition-colors hover:bg-white/[0.05] focus-visible:bg-white/[0.07]">
|
|
36
|
+
<span className="block aspect-[4/3] overflow-hidden sm:aspect-square rounded-lg bg-zinc-900 ring-1 ring-white/10">
|
|
37
|
+
<img src={sub.image} alt="" decoding="async" className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110" />
|
|
38
|
+
</span>
|
|
39
|
+
<span className="mt-2 block truncate px-0.5 text-sm font-medium text-zinc-100">{sub.label}</span>
|
|
40
|
+
{sub.subtitle && <span className="block truncate px-0.5 text-xs text-zinc-500">{sub.subtitle}</span>}
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
);
|
|
27
46
|
|
|
47
|
+
/* "links": a titled list; switches to two columns when it gets long. */
|
|
48
|
+
const Links = ({ title, items }) => {
|
|
49
|
+
const wide = items.length > 5;
|
|
28
50
|
return (
|
|
29
|
-
<div
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{subItem.label}
|
|
42
|
-
</a>
|
|
43
|
-
</li>
|
|
44
|
-
))}
|
|
45
|
-
</ul>
|
|
46
|
-
)}
|
|
47
|
-
{/* Products Dropdown */}
|
|
48
|
-
{item.type === "products" && (
|
|
49
|
-
<div className=' border border-gray-700/40 bg-black/10 rounded-2xl p-5'>
|
|
50
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 z-50">
|
|
51
|
-
{item.submenu.map((subItem, i) => (
|
|
52
|
-
<a href={subItem.href} key={i} className={`flex items-center gap-4 bg-slate-800/50 p-3 rounded-lg hover:bg-slate-800 transition-all group w-80`}>
|
|
53
|
-
<div className="overflow-hidden rounded-md">
|
|
54
|
-
<img src={subItem.image} alt={subItem.label} className="w-20 h-20 object-cover transition-transform duration-300 group-hover:scale-110" />
|
|
55
|
-
</div>
|
|
56
|
-
<div className="text-left">
|
|
57
|
-
<h3 className="text-gray-200 font-medium text-sm">{subItem.label}</h3>
|
|
58
|
-
<p className="text-gray-500 text-xs">{subItem.subtitle}</p>
|
|
59
|
-
</div>
|
|
60
|
-
</a>
|
|
61
|
-
))}
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
{/* Services Dropdown */}
|
|
66
|
-
{item.type === "services" && (
|
|
67
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
68
|
-
{item.submenu.map((subItem, i) => (
|
|
69
|
-
<a href={subItem.href} key={i} className="group block bg-slate-800/50 p-3 rounded-lg hover:bg-slate-800 transition-all w-64">
|
|
70
|
-
<h3 className="text-lime-400 font-medium transition-colors group-hover:text-lime-300">{subItem.label}</h3>
|
|
71
|
-
<p className="text-gray-500 text-sm">{subItem.subtitle}</p>
|
|
72
|
-
</a>
|
|
73
|
-
))}
|
|
74
|
-
</div>
|
|
75
|
-
)}
|
|
76
|
-
</div>
|
|
51
|
+
<div className={`p-2 ${wide ? 'w-[min(26rem,calc(100vw-2rem))]' : 'w-[min(15rem,calc(100vw-2rem))]'}`}>
|
|
52
|
+
<p className="px-3 pb-1 pt-2 text-[0.68rem] font-medium uppercase tracking-[0.14em] text-zinc-500">{title}</p>
|
|
53
|
+
<ul className={wide ? 'grid grid-cols-2' : ''}>
|
|
54
|
+
{items.map((sub, i) => (
|
|
55
|
+
<li key={i}>
|
|
56
|
+
<a href={sub.href} className="group flex items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm text-zinc-400 outline-none transition-colors hover:bg-white/[0.05] hover:text-zinc-100 focus-visible:bg-white/[0.07] focus-visible:text-zinc-100">
|
|
57
|
+
{sub.label}
|
|
58
|
+
<ArrowUpRight size={14} className="text-lime-300 opacity-0 transition-opacity group-hover:opacity-100" />
|
|
59
|
+
</a>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
77
63
|
</div>
|
|
78
64
|
);
|
|
79
65
|
};
|
|
80
66
|
|
|
81
|
-
const
|
|
82
|
-
|
|
67
|
+
const PanelContent = ({ item }) => {
|
|
68
|
+
if (item.type === 'products') return <Products items={item.submenu} />;
|
|
69
|
+
if (item.type === 'links') return <Links title={item.label} items={item.submenu} />;
|
|
70
|
+
return <Services items={item.submenu} />;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const NavDropDown = ({ navData = [], className = '' }) => {
|
|
74
|
+
const uid = useId();
|
|
75
|
+
const rootRef = useRef(null);
|
|
76
|
+
const barRef = useRef(null);
|
|
77
|
+
const triggerRefs = useRef([]);
|
|
78
|
+
const contentRef = useRef(null);
|
|
79
|
+
const closeTimer = useRef(0);
|
|
80
|
+
const pointerType = useRef('');
|
|
81
|
+
|
|
82
|
+
const [open, setOpen] = useState(null);
|
|
83
|
+
const [hovered, setHovered] = useState(null);
|
|
84
|
+
// +1 / -1: which side the next panel's content slides in from.
|
|
85
|
+
const [dir, setDir] = useState(0);
|
|
86
|
+
const [size, setSize] = useState({ w: 0, h: 0 });
|
|
87
|
+
const [caretX, setCaretX] = useState(0);
|
|
88
|
+
|
|
89
|
+
const show = useCallback((index) => {
|
|
90
|
+
clearTimeout(closeTimer.current);
|
|
91
|
+
setOpen((prev) => {
|
|
92
|
+
if (prev !== null && prev !== index) setDir(index > prev ? 1 : -1);
|
|
93
|
+
else if (prev === null) setDir(0);
|
|
94
|
+
return index;
|
|
95
|
+
});
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const hide = useCallback(() => {
|
|
99
|
+
clearTimeout(closeTimer.current);
|
|
100
|
+
setOpen(null);
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
// A short delay lets the pointer travel from a trigger into the panel.
|
|
104
|
+
const hideSoon = () => {
|
|
105
|
+
clearTimeout(closeTimer.current);
|
|
106
|
+
closeTimer.current = setTimeout(hide, CLOSE_DELAY);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Size the panel to its content and point the caret at the open trigger.
|
|
110
|
+
const measure = useCallback(() => {
|
|
111
|
+
const content = contentRef.current;
|
|
112
|
+
const bar = barRef.current;
|
|
113
|
+
const trigger = triggerRefs.current[open];
|
|
114
|
+
if (!content || !bar || !trigger) return;
|
|
115
|
+
const w = content.offsetWidth;
|
|
116
|
+
setSize({ w, h: content.offsetHeight });
|
|
117
|
+
const barBox = bar.getBoundingClientRect();
|
|
118
|
+
const t = trigger.getBoundingClientRect();
|
|
119
|
+
const panelLeft = (barBox.width - w) / 2;
|
|
120
|
+
setCaretX(Math.min(Math.max(t.left - barBox.left + t.width / 2 - panelLeft, 20), w - 20));
|
|
121
|
+
}, [open]);
|
|
122
|
+
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
if (open !== null) measure();
|
|
125
|
+
}, [open, measure]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (open === null) return undefined;
|
|
129
|
+
const onKey = (e) => {
|
|
130
|
+
if (e.key !== 'Escape') return;
|
|
131
|
+
triggerRefs.current[open]?.focus();
|
|
132
|
+
hide();
|
|
133
|
+
};
|
|
134
|
+
const onPointer = (e) => {
|
|
135
|
+
if (!rootRef.current?.contains(e.target)) hide();
|
|
136
|
+
};
|
|
137
|
+
window.addEventListener('keydown', onKey);
|
|
138
|
+
window.addEventListener('pointerdown', onPointer);
|
|
139
|
+
window.addEventListener('resize', measure);
|
|
140
|
+
return () => {
|
|
141
|
+
window.removeEventListener('keydown', onKey);
|
|
142
|
+
window.removeEventListener('pointerdown', onPointer);
|
|
143
|
+
window.removeEventListener('resize', measure);
|
|
144
|
+
};
|
|
145
|
+
}, [open, hide, measure]);
|
|
146
|
+
|
|
147
|
+
useEffect(() => () => clearTimeout(closeTimer.current), []);
|
|
148
|
+
|
|
149
|
+
// Warm the cache for submenu images so a panel never opens with empty tiles.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const id = setTimeout(() => {
|
|
152
|
+
navData.forEach((item) => item.submenu?.forEach((sub) => {
|
|
153
|
+
if (sub.image) new Image().src = sub.image;
|
|
154
|
+
}));
|
|
155
|
+
}, 300);
|
|
156
|
+
return () => clearTimeout(id);
|
|
157
|
+
}, [navData]);
|
|
158
|
+
|
|
159
|
+
const onTriggerKey = (e, index) => {
|
|
160
|
+
if (e.key !== 'ArrowDown') return;
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
show(index);
|
|
163
|
+
// Wait for the panel to render, then move focus into it.
|
|
164
|
+
requestAnimationFrame(() => contentRef.current?.querySelector('a')?.focus());
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const active = open !== null ? navData[open] : null;
|
|
168
|
+
const panelId = `${uid}-panel`;
|
|
83
169
|
|
|
84
170
|
return (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
171
|
+
<MotionConfig reducedMotion="user">
|
|
172
|
+
<div
|
|
173
|
+
ref={rootRef}
|
|
174
|
+
className={`relative inline-block max-w-full ${className}`}
|
|
175
|
+
style={{ zIndex: 4000 }}
|
|
176
|
+
onPointerLeave={(e) => e.pointerType === 'mouse' && hideSoon()}
|
|
177
|
+
onPointerEnter={() => clearTimeout(closeTimer.current)}
|
|
178
|
+
onBlur={(e) => !rootRef.current?.contains(e.relatedTarget) && hide()}
|
|
179
|
+
>
|
|
180
|
+
<nav aria-label="Main" ref={barRef} className="max-w-full overflow-x-auto rounded-full border border-white/10 bg-zinc-950/85 p-1 [scrollbar-width:none] sm:p-1.5 shadow-[0_10px_40px_-12px_rgba(0,0,0,0.7)] backdrop-blur-xl">
|
|
181
|
+
<LayoutGroup id={uid}>
|
|
182
|
+
<ul className="flex items-center gap-0.5" onPointerLeave={() => setHovered(null)}>
|
|
183
|
+
{navData.map((item, index) => {
|
|
184
|
+
const hasMenu = Boolean(item.submenu?.length);
|
|
185
|
+
const isOpen = open === index;
|
|
186
|
+
const lit = hovered === index || (hovered === null && isOpen);
|
|
187
|
+
const base = `relative flex items-center gap-0.5 whitespace-nowrap rounded-full px-2 py-1.5 text-[0.78rem] font-medium sm:gap-1 sm:px-3.5 sm:py-2 sm:text-sm outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-lime-400/60 ${isOpen ? 'text-lime-300' : 'text-zinc-300 hover:text-white'}`;
|
|
188
|
+
const highlight = lit && (
|
|
189
|
+
<motion.span layoutId="hover" className="absolute inset-0 -z-10 rounded-full bg-white/[0.07]" transition={{ type: 'spring', stiffness: 500, damping: 40 }} />
|
|
190
|
+
);
|
|
191
|
+
return (
|
|
192
|
+
<li key={index} className="relative isolate" onPointerEnter={() => setHovered(index)}>
|
|
193
|
+
{hasMenu ? (
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
ref={(el) => (triggerRefs.current[index] = el)}
|
|
197
|
+
className={base}
|
|
198
|
+
aria-expanded={isOpen}
|
|
199
|
+
aria-controls={isOpen ? panelId : undefined}
|
|
200
|
+
onPointerEnter={(e) => e.pointerType === 'mouse' && show(index)}
|
|
201
|
+
onPointerDown={(e) => (pointerType.current = e.pointerType)}
|
|
202
|
+
onClick={() => {
|
|
203
|
+
// Hover already opened it for a mouse; touch and keyboard toggle.
|
|
204
|
+
if (pointerType.current === 'mouse' || !isOpen) show(index);
|
|
205
|
+
else hide();
|
|
206
|
+
pointerType.current = '';
|
|
207
|
+
}}
|
|
208
|
+
onKeyDown={(e) => onTriggerKey(e, index)}
|
|
209
|
+
>
|
|
210
|
+
{highlight}
|
|
211
|
+
{item.label}
|
|
212
|
+
<ChevronDown size={15} className={`transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
|
|
213
|
+
</button>
|
|
214
|
+
) : (
|
|
215
|
+
<a href={item.href || '#'} className={base} onPointerEnter={(e) => e.pointerType === 'mouse' && hideSoon()}>
|
|
216
|
+
{highlight}
|
|
217
|
+
{item.label}
|
|
218
|
+
</a>
|
|
219
|
+
)}
|
|
220
|
+
</li>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
223
|
+
</ul>
|
|
224
|
+
</LayoutGroup>
|
|
225
|
+
</nav>
|
|
226
|
+
|
|
227
|
+
{/* One shared panel that resizes and slides its content as you move between menus. */}
|
|
228
|
+
<div className="pointer-events-none absolute left-1/2 top-full flex -translate-x-1/2 justify-center pt-3">
|
|
229
|
+
<AnimatePresence>
|
|
230
|
+
{active && (
|
|
231
|
+
<motion.div
|
|
232
|
+
id={panelId}
|
|
233
|
+
className="pointer-events-auto relative origin-top"
|
|
234
|
+
initial={{ opacity: 0, y: -8, scale: 0.97 }}
|
|
235
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
236
|
+
exit={{ opacity: 0, y: -6, scale: 0.98, transition: { duration: 0.15 } }}
|
|
237
|
+
transition={{ duration: 0.25, ease }}
|
|
99
238
|
>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
239
|
+
<motion.span
|
|
240
|
+
aria-hidden
|
|
241
|
+
className="absolute -top-[5px] z-10 h-2.5 w-2.5 -translate-x-1/2 rotate-45 rounded-[2px] border-l border-t border-white/10 bg-zinc-950"
|
|
242
|
+
initial={false}
|
|
243
|
+
animate={{ left: caretX }}
|
|
244
|
+
transition={{ duration: 0.3, ease }}
|
|
245
|
+
/>
|
|
246
|
+
<motion.div
|
|
247
|
+
className="relative overflow-hidden rounded-2xl border border-white/10 bg-zinc-950/95 shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] backdrop-blur-xl"
|
|
248
|
+
initial={false}
|
|
249
|
+
animate={{ width: size.w || 'auto', height: size.h || 'auto' }}
|
|
250
|
+
transition={{ duration: 0.3, ease }}
|
|
251
|
+
>
|
|
252
|
+
<span aria-hidden className="pointer-events-none absolute inset-x-10 top-0 z-10 h-px bg-gradient-to-r from-transparent via-lime-400/50 to-transparent" />
|
|
253
|
+
<AnimatePresence initial={false} custom={dir}>
|
|
254
|
+
<motion.div
|
|
255
|
+
key={open}
|
|
256
|
+
// The leaving copy unmounts later; ignore its null so the ref keeps the new content.
|
|
257
|
+
ref={(el) => {
|
|
258
|
+
if (el) contentRef.current = el;
|
|
259
|
+
}}
|
|
260
|
+
custom={dir}
|
|
261
|
+
className="absolute left-0 top-0"
|
|
262
|
+
variants={{
|
|
263
|
+
enter: (d) => ({ opacity: 0, x: d * 32 }),
|
|
264
|
+
center: { opacity: 1, x: 0 },
|
|
265
|
+
leave: (d) => ({ opacity: 0, x: d * -32 }),
|
|
266
|
+
}}
|
|
267
|
+
initial="enter"
|
|
268
|
+
animate="center"
|
|
269
|
+
exit="leave"
|
|
270
|
+
transition={{ duration: 0.25, ease }}
|
|
271
|
+
>
|
|
272
|
+
<PanelContent item={active} />
|
|
273
|
+
</motion.div>
|
|
274
|
+
</AnimatePresence>
|
|
275
|
+
</motion.div>
|
|
276
|
+
</motion.div>
|
|
277
|
+
)}
|
|
278
|
+
</AnimatePresence>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</MotionConfig>
|
|
111
282
|
);
|
|
112
283
|
};
|
|
113
284
|
|
|
114
|
-
export default NavDropDown;
|
|
285
|
+
export default NavDropDown;
|
|
@@ -121,7 +121,13 @@ const ResponsiveNavMenu = (props) => {
|
|
|
121
121
|
</div>
|
|
122
122
|
|
|
123
123
|
<div className="md:hidden flex w-full justify-end">
|
|
124
|
-
|
|
124
|
+
{/* Same backdrop as the desktop bar, so the icon stays visible on light pages too. */}
|
|
125
|
+
<button
|
|
126
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
127
|
+
aria-label={isOpen ? "Close menu" : "Open menu"}
|
|
128
|
+
className="rounded-full border border-white/10 p-2.5 text-white shadow-lg focus:outline-none"
|
|
129
|
+
style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: props.backgroundColor ?? 'rgba(17, 24, 39, 0.7)' }}
|
|
130
|
+
>
|
|
125
131
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
126
132
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16m-7 6h7"} />
|
|
127
133
|
</svg>
|
|
@@ -132,7 +138,7 @@ const ResponsiveNavMenu = (props) => {
|
|
|
132
138
|
<div className="md:hidden absolute top-20 left-4 right-4 rounded-xl shadow-lg border border-white/10"
|
|
133
139
|
style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: 'rgba(17, 24, 39, 0.7)' }}>
|
|
134
140
|
<div className="flex flex-col items-center gap-2 p-4">
|
|
135
|
-
{navItems.map((label
|
|
141
|
+
{navItems.map((label) => (
|
|
136
142
|
<button
|
|
137
143
|
key={label}
|
|
138
144
|
onClick={() => {
|
|
@@ -14,6 +14,7 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
|
|
|
14
14
|
const linkRefs = useRef([]);
|
|
15
15
|
|
|
16
16
|
useEffect(() => {
|
|
17
|
+
const cleanups = [];
|
|
17
18
|
cardData.forEach((_, idx) => {
|
|
18
19
|
const cardElement = cardRefs.current[idx];
|
|
19
20
|
const titleElement = titleRefs.current[idx];
|
|
@@ -40,11 +41,11 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
|
|
|
40
41
|
ease: "power3.out",
|
|
41
42
|
});
|
|
42
43
|
|
|
43
|
-
gsap.to(titleElement, { duration: 0.7, x: rotateY * 0.8, y: -rotateX * 0.8 });
|
|
44
|
-
gsap.to(imageElement, { duration: 0.8, x: rotateY * 1.5, y: -rotateX * 1.5 });
|
|
45
|
-
gsap.to(descElement, { duration: 0.9, x: rotateY * 1.2, y: -rotateX * 1.2 });
|
|
46
|
-
gsap.to(buttonElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
|
|
47
|
-
gsap.to(linkElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
|
|
44
|
+
if (titleElement) gsap.to(titleElement, { duration: 0.7, x: rotateY * 0.8, y: -rotateX * 0.8 });
|
|
45
|
+
if (imageElement) gsap.to(imageElement, { duration: 0.8, x: rotateY * 1.5, y: -rotateX * 1.5 });
|
|
46
|
+
if (descElement) gsap.to(descElement, { duration: 0.9, x: rotateY * 1.2, y: -rotateX * 1.2 });
|
|
47
|
+
if (buttonElement) gsap.to(buttonElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
|
|
48
|
+
if (linkElement) gsap.to(linkElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
const handleMouseLeave = () => {
|
|
@@ -56,7 +57,7 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
|
|
|
56
57
|
scale: 1,
|
|
57
58
|
ease: "elastic.out(1, 0.5)",
|
|
58
59
|
});
|
|
59
|
-
gsap.to([titleElement, imageElement, descElement, buttonElement, linkElement], {
|
|
60
|
+
gsap.to([titleElement, imageElement, descElement, buttonElement, linkElement].filter(Boolean), {
|
|
60
61
|
duration: 1.2,
|
|
61
62
|
x: 0,
|
|
62
63
|
y: 0,
|
|
@@ -65,18 +66,19 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
|
|
|
65
66
|
}
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
cardElement.addEventListener("mousemove", handleMouseMove);
|
|
69
|
+
cardElement.addEventListener("mousemove", handleMouseMove, { passive: true });
|
|
69
70
|
cardElement.addEventListener("mouseleave", handleMouseLeave);
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
cleanups.push(() => {
|
|
72
73
|
cardElement.removeEventListener("mousemove", handleMouseMove);
|
|
73
74
|
cardElement.removeEventListener("mouseleave", handleMouseLeave);
|
|
74
|
-
};
|
|
75
|
+
});
|
|
75
76
|
});
|
|
77
|
+
return () => cleanups.forEach((fn) => fn());
|
|
76
78
|
}, [cardData, tiltEnable, scale]);
|
|
77
79
|
|
|
78
80
|
return (
|
|
79
|
-
<div className="flex flex-wrap gap-8 justify-center">
|
|
81
|
+
<div className="flex w-full flex-wrap gap-8 justify-center">
|
|
80
82
|
{cardData.map((card, idx) => (
|
|
81
83
|
<div
|
|
82
84
|
key={idx}
|
|
@@ -84,7 +86,7 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
|
|
|
84
86
|
className="w-full max-w-md rounded-2xl"
|
|
85
87
|
style={{ transformStyle: 'preserve-3d', perspective: `${perspective}px` }}
|
|
86
88
|
>
|
|
87
|
-
<div className="relative
|
|
89
|
+
<div className="relative w-full md:h-full bg-stone-950 rounded-2xl p-6 sm:p-8 border border-lime-500/20 shadow-2 shadow-lime-900/40 overflow-hidden"
|
|
88
90
|
style={{ transformStyle: 'preserve-3d' }}
|
|
89
91
|
>
|
|
90
92
|
{glareEnable && (
|
|
@@ -47,10 +47,10 @@ const CarouselCard = ({ slide, isCenter, isScrolling }) => {
|
|
|
47
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
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" />
|
|
49
49
|
</div>
|
|
50
|
-
<div className="relative flex flex-col justify-end h-full text-white p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
|
|
51
|
-
<h2 className="text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
|
|
50
|
+
<div className="relative flex flex-col justify-end h-full text-white p-5 sm:p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
|
|
51
|
+
<h2 className="text-2xl sm:text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
|
|
52
52
|
<p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
|
|
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>
|
|
53
|
+
<p className="text-slate-300 mt-3 sm:mt-4 text-xs sm:text-sm leading-relaxed line-clamp-3 sm:line-clamp-none group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
|
|
54
54
|
</div>
|
|
55
55
|
</motion.div>
|
|
56
56
|
);
|
|
@@ -109,7 +109,7 @@ const ParallaxCarousel = ({ slides }) => {
|
|
|
109
109
|
return (
|
|
110
110
|
<div className="relative w-full flex flex-col items-center px-2">
|
|
111
111
|
<div
|
|
112
|
-
className="relative w-full h-[
|
|
112
|
+
className="relative w-full h-[380px] sm:h-[480px]"
|
|
113
113
|
style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
|
|
114
114
|
>
|
|
115
115
|
{slides.map((slide, index) => {
|
|
@@ -125,7 +125,7 @@ const ParallaxCarousel = ({ slides }) => {
|
|
|
125
125
|
return (
|
|
126
126
|
<motion.div
|
|
127
127
|
key={index}
|
|
128
|
-
className={`absolute
|
|
128
|
+
className={`absolute left-1/2 top-1/2 aspect-[25/26] w-[min(80%,25rem)] sm:w-[min(88%,25rem)] -translate-x-1/2 -translate-y-1/2 ${positionOffset === 0 ? '' : 'pointer-events-none'}`}
|
|
129
129
|
initial={false}
|
|
130
130
|
style={{ transformStyle: 'preserve-3d', willChange: 'transform, opacity' }}
|
|
131
131
|
animate={{
|
|
@@ -141,11 +141,11 @@ const ParallaxCarousel = ({ slides }) => {
|
|
|
141
141
|
})}
|
|
142
142
|
</div>
|
|
143
143
|
|
|
144
|
-
<motion.button onClick={handlePrev} className="absolute
|
|
145
|
-
<ArrowLeft size={32} />
|
|
144
|
+
<motion.button onClick={handlePrev} className="absolute left-0 top-1/2 -translate-y-1/2 z-30 p-1.5 sm:p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
|
|
145
|
+
<ArrowLeft size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
|
|
146
146
|
</motion.button>
|
|
147
|
-
<motion.button onClick={handleNext} className="absolute
|
|
148
|
-
<ArrowRight size={32} />
|
|
147
|
+
<motion.button onClick={handleNext} className="absolute right-0 top-1/2 -translate-y-1/2 z-30 p-1.5 sm:p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
|
|
148
|
+
<ArrowRight size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
|
|
149
149
|
</motion.button>
|
|
150
150
|
</div>
|
|
151
151
|
);
|