apex-ui-kit 2.12.1 → 2.12.2
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/ReadMe.md +5 -5
- package/dist/ApexUI-Kit/templates/FloatingNavbar/FloatingNavbar.jsx +36 -11
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +4 -6
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +5 -8
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +158 -153
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/ThemeSwitcher.jsx +24 -8
- package/dist/cli/index.js +0 -1
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +0 -104
package/ReadMe.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/apex-ui-kit)
|
|
5
5
|
[](./Licence.md)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**116 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
|
|
8
8
|
|
|
9
9
|
Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL where a component needs them.
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL wh
|
|
|
15
15
|
|
|
16
16
|
## Why ApexUI
|
|
17
17
|
|
|
18
|
-
- **
|
|
18
|
+
- **116 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
|
|
19
19
|
- **You own the code.** The CLI copies plain `.jsx` files into your project, no runtime package to keep in sync.
|
|
20
20
|
- **Every docs page has** a live preview, a playground to tweak props, a Desktop / Tablet / Mobile preview, the full props table and a ready prompt you can paste into ChatGPT or Claude.
|
|
21
21
|
- **Smooth on real devices.** Animations stick to transform and opacity, respect reduced motion, and work with mouse, touch and keyboard.
|
|
@@ -127,7 +127,6 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
127
127
|
| --- | --- |
|
|
128
128
|
| [Bounce Cards](https://apex-ui.in/components/bounce-cards) | `bounce-cards` |
|
|
129
129
|
| [Card Stack](https://apex-ui.in/components/card-stack) | `card-stack` |
|
|
130
|
-
| [Card Swap 3D](https://apex-ui.in/components/card-swap-3d) | `card-swap-3d` |
|
|
131
130
|
| [Cards](https://apex-ui.in/components/cards) | `basic-card`, `meteor-card` |
|
|
132
131
|
| [Distortion Image](https://apex-ui.in/components/distortion-image) | `distortion-image` |
|
|
133
132
|
| [Folder](https://apex-ui.in/components/folder) | `folder` |
|
|
@@ -261,13 +260,14 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
261
260
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
262
261
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
263
262
|
|
|
264
|
-
## What's new in 2.
|
|
263
|
+
## What's new in 2.12
|
|
265
264
|
|
|
265
|
+
- New in 2.12: components size from their own container instead of the screen (Testimonials, Animated Tabs, Gooey Nav, Nav Menu, Flip Clock, Image Accordion, Beam Bar), a smoother Floating Navbar menu, no flicker in Theme Switcher and Parallax Carousel. Smart Grid Card and Card Swap 3D were removed.
|
|
266
266
|
- New in 2.10: Fluid Cursor (a real-time WebGL fluid sim that trails the cursor) and Sketch Reveal (a photo turns into a glowing sketch and burns away into the next one as you scroll).
|
|
267
267
|
- New in 2.9: Liquid Glass (iOS style WebGL glass), 10 live page templates on the site, Arc Gallery auto scroll, Scroll Stack on the page scroll, Sunrise trees and birds.
|
|
268
268
|
- New in 2.8: Glass Lens (WebGL liquid glass), Dither Waves (WebGL 1-bit background), Infinite Canvas, Image Accordion, Orbit Icons, Command Palette, Crossed Marquee (tech-stack bands with simple-icons) and Theme Switcher (8 themes with a circle reveal).
|
|
269
269
|
- Toast Stack and Radial Menu no longer blink in production builds; Radial Menu, 3D Text Ring, Animated Tabs and Gooey Nav now fit 320px phones.
|
|
270
|
-
- 38 new components since 2.6, including WebGL and three.js (Sunrise Background, Horizon Glow, Circular Carousel, Arc Gallery, Galaxy 3D, Warp Tunnel, Wireframe Knot 3D, Marquee 3D, Distortion Image, Liquid Blob 3D, Particle Morph, Retro Grid 3D), Scroll Stack, Particle Text, Edge Blur, Magnetic Button, Radial Menu, the AI voice family (Wave, Bars, Ring, Blob), Gooey Nav (WebGL metaball pill), Coverflow 3D,
|
|
270
|
+
- 38 new components since 2.6, including WebGL and three.js (Sunrise Background, Horizon Glow, Circular Carousel, Arc Gallery, Galaxy 3D, Warp Tunnel, Wireframe Knot 3D, Marquee 3D, Distortion Image, Liquid Blob 3D, Particle Morph, Retro Grid 3D), Scroll Stack, Particle Text, Edge Blur, Magnetic Button, Radial Menu, the AI voice family (Wave, Bars, Ring, Blob), Gooey Nav (WebGL metaball pill), Coverflow 3D, 3D Text Ring, Elastic Slider, Stepper and Pixel Transition.
|
|
271
271
|
- Accordion Marquee is rebuilt: one liquid band glides between rows on springs, leans toward the pointer, and its marquee speeds up with sideways mouse moves. No GSAP needed any more.
|
|
272
272
|
- AI Model Prompt now exports `DEFAULT_LLM_MODELS` and an index file, so you can import both from the folder.
|
|
273
273
|
- Loading Screens follow the real page load by default: drop `<LoadingScreen fullScreen />` into your app and it covers the page while it loads, with a live bar and percentage, then hides itself. Pass `progress` to drive it yourself.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useId, useState } from "react";
|
|
1
|
+
import React, { useEffect, useId, useRef, useState } from "react";
|
|
2
2
|
import { AnimatePresence, motion } from "framer-motion";
|
|
3
3
|
import { ArrowUpRight } from "lucide-react";
|
|
4
4
|
|
|
@@ -18,12 +18,14 @@ const DEFAULT_LINKS = [
|
|
|
18
18
|
{ label: "Blog", href: "#blog" },
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
+
// Three lines that fold into an X. Transforms only, so it stays smooth on phones.
|
|
21
22
|
function Burger({ open }) {
|
|
23
|
+
const spring = { type: "spring", stiffness: 500, damping: 32 };
|
|
22
24
|
return (
|
|
23
25
|
<span className="relative block h-3.5 w-5" aria-hidden="true">
|
|
24
|
-
<motion.span className="absolute left-0 top-
|
|
25
|
-
<motion.span className="absolute left-0 top-
|
|
26
|
-
<motion.span className="absolute
|
|
26
|
+
<motion.span className="absolute left-0 top-[6px] h-[2px] w-full rounded-full bg-current" initial={false} animate={{ y: open ? 0 : -6, rotate: open ? 45 : 0 }} transition={spring} />
|
|
27
|
+
<motion.span className="absolute left-0 top-[6px] h-[2px] w-full rounded-full bg-current" initial={false} animate={{ opacity: open ? 0 : 1, scaleX: open ? 0.3 : 1 }} transition={{ duration: 0.15 }} />
|
|
28
|
+
<motion.span className="absolute left-0 top-[6px] h-[2px] w-full rounded-full bg-current" initial={false} animate={{ y: open ? 0 : 6, rotate: open ? -45 : 0 }} transition={spring} />
|
|
27
29
|
</span>
|
|
28
30
|
);
|
|
29
31
|
}
|
|
@@ -63,6 +65,20 @@ const FloatingNavbar = ({
|
|
|
63
65
|
return () => root.removeEventListener("scroll", onScroll);
|
|
64
66
|
}, [hideOnScroll, scrollRoot]);
|
|
65
67
|
|
|
68
|
+
// Close the menu with Escape or a tap outside it.
|
|
69
|
+
const wrap = useRef(null);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!open) return undefined;
|
|
72
|
+
const onKey = (e) => e.key === "Escape" && setOpen(false);
|
|
73
|
+
const onDown = (e) => !wrap.current?.contains(e.target) && setOpen(false);
|
|
74
|
+
document.addEventListener("keydown", onKey);
|
|
75
|
+
document.addEventListener("pointerdown", onDown);
|
|
76
|
+
return () => {
|
|
77
|
+
document.removeEventListener("keydown", onKey);
|
|
78
|
+
document.removeEventListener("pointerdown", onDown);
|
|
79
|
+
};
|
|
80
|
+
}, [open]);
|
|
81
|
+
|
|
66
82
|
const go = (link, e) => {
|
|
67
83
|
setActive(link.label);
|
|
68
84
|
setOpen(false);
|
|
@@ -70,16 +86,18 @@ const FloatingNavbar = ({
|
|
|
70
86
|
};
|
|
71
87
|
|
|
72
88
|
const surface = dark ? "border-white/10 bg-neutral-900/70 text-white shadow-[0_10px_40px_-12px_rgba(0,0,0,0.7)]" : "border-black/10 bg-white/75 text-neutral-900 shadow-[0_10px_40px_-14px_rgba(0,0,0,0.25)]";
|
|
89
|
+
// Solid: a blur nested inside the blurred bar cannot see the page behind it.
|
|
90
|
+
const menuSurface = dark ? "border-white/10 bg-neutral-900 text-white shadow-[0_18px_50px_-16px_rgba(0,0,0,0.8)]" : "border-black/10 bg-white text-neutral-900 shadow-[0_18px_50px_-18px_rgba(0,0,0,0.3)]";
|
|
73
91
|
const muted = dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900";
|
|
74
92
|
|
|
75
93
|
return (
|
|
76
|
-
<div className={`@container sticky top-3 z-50 w-full px-3 ${className}`}>
|
|
94
|
+
<div ref={wrap} className={`@container sticky top-3 z-50 w-full px-3 ${className}`}>
|
|
77
95
|
<motion.nav
|
|
78
96
|
aria-label="Main"
|
|
79
97
|
initial={false}
|
|
80
98
|
animate={{ y: hidden && !open ? "-140%" : "0%", opacity: hidden && !open ? 0 : 1 }}
|
|
81
99
|
transition={{ type: "spring", stiffness: 380, damping: 34 }}
|
|
82
|
-
className={`mx-auto max-w-4xl rounded-[1.6rem] border backdrop-blur-xl transition-[padding] duration-300 ${surface} ${compact ? "px-2.5 py-1.5" : "px-3 py-2.5"}`}
|
|
100
|
+
className={`relative mx-auto max-w-4xl rounded-[1.6rem] border backdrop-blur-xl transition-[padding] duration-300 ${surface} ${compact ? "px-2.5 py-1.5" : "px-3 py-2.5"}`}
|
|
83
101
|
>
|
|
84
102
|
<div className="flex items-center justify-between gap-3">
|
|
85
103
|
<a href="#home" onClick={(e) => go(links[0], e)} className="flex items-center gap-2 pl-1.5 text-[0.95rem] font-bold">
|
|
@@ -130,13 +148,20 @@ const FloatingNavbar = ({
|
|
|
130
148
|
</div>
|
|
131
149
|
</div>
|
|
132
150
|
|
|
133
|
-
{/* Mobile menu
|
|
151
|
+
{/* Mobile menu: its own panel under the bar that only fades and slides
|
|
152
|
+
(no height animation, so nothing re-lays out or re-blurs each frame). */}
|
|
134
153
|
<AnimatePresence initial={false}>
|
|
135
154
|
{open && (
|
|
136
|
-
<motion.div
|
|
137
|
-
|
|
155
|
+
<motion.div
|
|
156
|
+
initial={{ opacity: 0, y: -8, scale: 0.98 }}
|
|
157
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
158
|
+
exit={{ opacity: 0, y: -8, scale: 0.98 }}
|
|
159
|
+
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
|
|
160
|
+
className={`absolute inset-x-0 top-full mt-2 origin-top rounded-[1.4rem] border will-change-transform @xl:hidden ${menuSurface}`}
|
|
161
|
+
>
|
|
162
|
+
<ul className="space-y-1 p-2">
|
|
138
163
|
{links.map((link, i) => (
|
|
139
|
-
<motion.li key={link.label} initial={{ x: -
|
|
164
|
+
<motion.li key={link.label} initial={{ x: -10, opacity: 0 }} animate={{ x: 0, opacity: 1 }} transition={{ delay: 0.03 * i + 0.04, duration: 0.25 }}>
|
|
140
165
|
<a href={link.href} onClick={(e) => go(link, e)} className={`flex items-center justify-between rounded-2xl px-4 py-3 text-[0.95rem] font-medium ${active === link.label ? (dark ? "bg-white/[0.08]" : "bg-black/[0.05]") : ""}`}>
|
|
141
166
|
{link.label}
|
|
142
167
|
{active === link.label && <span className="h-1.5 w-1.5 rounded-full" style={{ background: accentColor }} />}
|
|
@@ -144,7 +169,7 @@ const FloatingNavbar = ({
|
|
|
144
169
|
</motion.li>
|
|
145
170
|
))}
|
|
146
171
|
{cta && (
|
|
147
|
-
<motion.li initial={{ y: 8, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.
|
|
172
|
+
<motion.li initial={{ y: 8, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: 0.03 * links.length + 0.06, duration: 0.25 }} className="pt-1">
|
|
148
173
|
<a href={cta.href} className="flex items-center justify-center gap-1 rounded-2xl py-3 text-[0.95rem] font-semibold text-black" style={{ background: accentColor }}>
|
|
149
174
|
{cta.label} <ArrowUpRight className="h-4 w-4" />
|
|
150
175
|
</a>
|
|
@@ -149,9 +149,8 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
|
|
|
149
149
|
const el = links.current[i];
|
|
150
150
|
const b = bar.current;
|
|
151
151
|
if (!el || !b) return null;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return { l: a.left - p.left + PAD, r: a.right - p.left + PAD, t: a.top - p.top + PAD, h: a.height };
|
|
152
|
+
// Layout offsets (not screen rects), so it stays right inside zoomed or scaled parents.
|
|
153
|
+
return { l: el.offsetLeft + PAD, r: el.offsetLeft + el.offsetWidth + PAD, t: el.offsetTop + PAD, h: el.offsetHeight };
|
|
155
154
|
};
|
|
156
155
|
|
|
157
156
|
// Set up GL + simulation once.
|
|
@@ -177,10 +176,9 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
|
|
|
177
176
|
// Hold the element itself: the ref is cleared on unmount before cleanup runs.
|
|
178
177
|
const barEl = bar.current;
|
|
179
178
|
const resize = () => {
|
|
180
|
-
const r = barEl.getBoundingClientRect();
|
|
181
179
|
s.dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
182
|
-
const w =
|
|
183
|
-
const h =
|
|
180
|
+
const w = barEl.offsetWidth + PAD * 2;
|
|
181
|
+
const h = barEl.offsetHeight + PAD * 2;
|
|
184
182
|
cv.width = Math.round(w * s.dpr);
|
|
185
183
|
cv.height = Math.round(h * s.dpr);
|
|
186
184
|
cv.style.width = `${w}px`;
|
|
@@ -36,10 +36,9 @@ const NavMenu = ({
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const moveWithGSAP = () => {
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const width = rect.width;
|
|
39
|
+
// Layout offsets (not screen rects), so it stays right inside zoomed or scaled parents.
|
|
40
|
+
const left = el.offsetLeft;
|
|
41
|
+
const width = el.offsetWidth;
|
|
43
42
|
|
|
44
43
|
gsap.to(indicator, {
|
|
45
44
|
x: left,
|
|
@@ -59,10 +58,8 @@ const NavMenu = ({
|
|
|
59
58
|
const handleResize = () => {
|
|
60
59
|
const el = navItemsRef.current[activeIdx];
|
|
61
60
|
if (el) {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const left = rect.left - parentRect.left;
|
|
65
|
-
const width = rect.width;
|
|
61
|
+
const left = el.offsetLeft;
|
|
62
|
+
const width = el.offsetWidth;
|
|
66
63
|
|
|
67
64
|
gsap.set(indicatorRef.current, {
|
|
68
65
|
x: left,
|
|
@@ -1,154 +1,159 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (isScrolling) {
|
|
27
|
-
x.set(0);
|
|
28
|
-
y.set(0);
|
|
29
|
-
}
|
|
30
|
-
}, [isScrolling, x, y]);
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<motion.div
|
|
34
|
-
ref={cardRef}
|
|
35
|
-
className="w-full h-full bg-slate-900 rounded-2xl group"
|
|
36
|
-
style={{
|
|
37
|
-
transformStyle: 'preserve-3d',
|
|
38
|
-
perspective: '1500px',
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<img src={slide.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
<p className="text-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
animatingRef.current
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
animatingRef.current
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
scrollTimeoutRef.current
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { motion, useMotionValue, useTransform } from 'framer-motion';
|
|
3
|
+
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
4
|
+
const CarouselCard = ({ slide, isCenter, isScrolling }) => {
|
|
5
|
+
const cardRef = useRef(null);
|
|
6
|
+
const x = useMotionValue(0);
|
|
7
|
+
const y = useMotionValue(0);
|
|
8
|
+
|
|
9
|
+
const rotateX = useTransform(y, [-200, 200], [15, -15]);
|
|
10
|
+
const rotateY = useTransform(x, [-200, 200], [-15, 15]);
|
|
11
|
+
|
|
12
|
+
const handleMouseMove = (event) => {
|
|
13
|
+
if (!cardRef.current || !isCenter || isScrolling) return;
|
|
14
|
+
const rect = cardRef.current.getBoundingClientRect();
|
|
15
|
+
x.set(event.clientX - rect.left - rect.width / 2);
|
|
16
|
+
y.set(event.clientY - rect.top - rect.height / 2);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const handleMouseLeave = () => {
|
|
20
|
+
x.set(0);
|
|
21
|
+
y.set(0);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Level the card when it leaves the centre or the page scrolls.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isScrolling || !isCenter) {
|
|
27
|
+
x.set(0);
|
|
28
|
+
y.set(0);
|
|
29
|
+
}
|
|
30
|
+
}, [isScrolling, isCenter, x, y]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<motion.div
|
|
34
|
+
ref={cardRef}
|
|
35
|
+
className="w-full h-full bg-slate-900 rounded-2xl group"
|
|
36
|
+
style={{
|
|
37
|
+
transformStyle: 'preserve-3d',
|
|
38
|
+
perspective: '1500px',
|
|
39
|
+
// Always the motion values (swapping them for 0 made the card jump).
|
|
40
|
+
rotateX,
|
|
41
|
+
rotateY,
|
|
42
|
+
}}
|
|
43
|
+
onMouseMove={handleMouseMove}
|
|
44
|
+
onMouseLeave={handleMouseLeave}
|
|
45
|
+
>
|
|
46
|
+
{/* Slide Content */}
|
|
47
|
+
<div className="absolute inset-0 w-full h-full" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
|
|
48
|
+
<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" />
|
|
49
|
+
<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" />
|
|
50
|
+
</div>
|
|
51
|
+
<div className="relative flex flex-col justify-end h-full text-white p-5 sm:p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
|
|
52
|
+
<h2 className="text-2xl sm:text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
|
|
53
|
+
<p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
|
|
54
|
+
<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>
|
|
55
|
+
</div>
|
|
56
|
+
</motion.div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// --- Custom Parallax Carousel Component ---
|
|
62
|
+
const ParallaxCarousel = ({ slides }) => {
|
|
63
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
64
|
+
const animatingRef = useRef(false);
|
|
65
|
+
const animTimeoutRef = useRef(null);
|
|
66
|
+
|
|
67
|
+
const [isScrolling, setIsScrolling] = useState(false);
|
|
68
|
+
const scrollTimeoutRef = useRef(null);
|
|
69
|
+
|
|
70
|
+
// ✅ REMOVED: isHovered state hata diya gaya hai.
|
|
71
|
+
|
|
72
|
+
const NAV_ANIM_MS = 600;
|
|
73
|
+
const handleNext = () => {
|
|
74
|
+
if (animatingRef.current) return;
|
|
75
|
+
animatingRef.current = true;
|
|
76
|
+
clearTimeout(animTimeoutRef.current);
|
|
77
|
+
setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
|
|
78
|
+
animTimeoutRef.current = setTimeout(() => {
|
|
79
|
+
animatingRef.current = false;
|
|
80
|
+
}, NAV_ANIM_MS);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handlePrev = () => {
|
|
84
|
+
if (animatingRef.current) return;
|
|
85
|
+
animatingRef.current = true;
|
|
86
|
+
clearTimeout(animTimeoutRef.current);
|
|
87
|
+
setActiveIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length);
|
|
88
|
+
animTimeoutRef.current = setTimeout(() => {
|
|
89
|
+
animatingRef.current = false;
|
|
90
|
+
}, NAV_ANIM_MS);
|
|
91
|
+
};
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const handleScroll = () => {
|
|
94
|
+
setIsScrolling(true);
|
|
95
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
96
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
97
|
+
setIsScrolling(false);
|
|
98
|
+
}, 150);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
window.removeEventListener('scroll', handleScroll);
|
|
105
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
106
|
+
};
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="relative w-full flex flex-col items-center px-2">
|
|
112
|
+
<div
|
|
113
|
+
className="relative w-full h-[380px] sm:h-[480px]"
|
|
114
|
+
style={{ perspective: '1200px' }}
|
|
115
|
+
>
|
|
116
|
+
{slides.map((slide, index) => {
|
|
117
|
+
let positionOffset = index - activeIndex;
|
|
118
|
+
if (positionOffset > slides.length / 2) {
|
|
119
|
+
positionOffset -= slides.length;
|
|
120
|
+
} else if (positionOffset < -slides.length / 2) {
|
|
121
|
+
positionOffset += slides.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const isVisible = Math.abs(positionOffset) <= 2;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
// Separate x / z / scale (not one transform string) and no preserve-3d
|
|
128
|
+
// here: opacity below 1 flattens a 3D layer, which made cards pop
|
|
129
|
+
// while they moved. The stacking order is set at once, not animated.
|
|
130
|
+
<motion.div
|
|
131
|
+
key={index}
|
|
132
|
+
className={`absolute left-1/2 top-1/2 aspect-[25/26] w-[min(80%,25rem)] sm:w-[min(88%,25rem)] ${positionOffset === 0 ? '' : 'pointer-events-none'}`}
|
|
133
|
+
initial={false}
|
|
134
|
+
style={{ y: '-50%', zIndex: slides.length - Math.abs(positionOffset), willChange: 'transform, opacity' }}
|
|
135
|
+
animate={{
|
|
136
|
+
x: `${-50 + positionOffset * 50}%`,
|
|
137
|
+
z: -Math.abs(positionOffset) * 250,
|
|
138
|
+
scale: 1 - Math.abs(positionOffset) * 0.15,
|
|
139
|
+
opacity: isVisible ? 1 - Math.abs(positionOffset) * 0.45 : 0,
|
|
140
|
+
}}
|
|
141
|
+
transition={{ type: 'spring', stiffness: 160, damping: 22 }}
|
|
142
|
+
>
|
|
143
|
+
<CarouselCard slide={slide} isCenter={positionOffset === 0} isScrolling={isScrolling} />
|
|
144
|
+
</motion.div>
|
|
145
|
+
);
|
|
146
|
+
})}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<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 }}>
|
|
150
|
+
<ArrowLeft size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
|
|
151
|
+
</motion.button>
|
|
152
|
+
<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 }}>
|
|
153
|
+
<ArrowRight size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
|
|
154
|
+
</motion.button>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
154
159
|
export default ParallaxCarousel;
|
|
@@ -87,13 +87,18 @@ export function useTheme(themes = defaultThemes, { storageKey = "theme", default
|
|
|
87
87
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
88
|
}, [themes, store, id]);
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// onCommit runs in the same update as the theme change, so anything it
|
|
91
|
+
// changes (like closing a menu) lands in the same view-transition frame.
|
|
92
|
+
const setTheme = (next, origin, onCommit) => {
|
|
91
93
|
const theme = themes.find((x) => x.id === next);
|
|
92
|
-
if (!theme || next === store.get()) return;
|
|
94
|
+
if (!theme || next === store.get()) return onCommit?.();
|
|
93
95
|
write(storageKey, next);
|
|
94
96
|
const commit = () => {
|
|
95
97
|
applyTheme(theme, rootEl());
|
|
96
|
-
flushSync(() =>
|
|
98
|
+
flushSync(() => {
|
|
99
|
+
store.set(next);
|
|
100
|
+
onCommit?.();
|
|
101
|
+
});
|
|
97
102
|
};
|
|
98
103
|
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
99
104
|
if (!document.startViewTransition || reduced) {
|
|
@@ -130,6 +135,9 @@ const Palette = () => (
|
|
|
130
135
|
const ThemeSwitcher = ({ themes = defaultThemes, placement = "down", align = "right", label, storageKey = "theme", target, className = "", buttonClassName = "" }) => {
|
|
131
136
|
const [id, setTheme] = useTheme(themes, { storageKey, target });
|
|
132
137
|
const [open, setOpen] = useState(false);
|
|
138
|
+
// Closed by picking a theme: hide at once (an exit animation would be caught
|
|
139
|
+
// half-way by the theme transition snapshot and flicker).
|
|
140
|
+
const [picked, setPicked] = useState(false);
|
|
133
141
|
const root = useRef(null);
|
|
134
142
|
const current = themes.find((x) => x.id === id) ?? themes[0];
|
|
135
143
|
const up = placement === "up";
|
|
@@ -153,21 +161,27 @@ const ThemeSwitcher = ({ themes = defaultThemes, placement = "down", align = "ri
|
|
|
153
161
|
aria-label="Choose colour theme"
|
|
154
162
|
aria-haspopup="true"
|
|
155
163
|
aria-expanded={open}
|
|
156
|
-
onClick={() =>
|
|
164
|
+
onClick={() => {
|
|
165
|
+
setPicked(false);
|
|
166
|
+
setOpen((o) => !o);
|
|
167
|
+
}}
|
|
157
168
|
className={`relative inline-flex items-center justify-center gap-2 border border-[var(--line)] bg-[var(--surface)] text-[var(--ink)] transition-colors hover:border-[var(--accent)] ${label ? "h-9 rounded-full px-3.5 text-sm font-medium" : "h-9 w-9 rounded-full"} ${buttonClassName}`}
|
|
158
169
|
>
|
|
159
170
|
<Palette />
|
|
160
171
|
{label && <span>{label}</span>}
|
|
161
172
|
<span aria-hidden="true" className="absolute right-0.5 top-0.5 h-2 w-2 rounded-full ring-2 ring-[var(--surface)]" style={{ background: current?.vars["--accent"] }} />
|
|
162
173
|
</button>
|
|
163
|
-
|
|
174
|
+
{/* custom passes the latest "picked" to the panel that is leaving. */}
|
|
175
|
+
<AnimatePresence custom={picked}>
|
|
164
176
|
{open && (
|
|
165
177
|
<motion.div
|
|
166
178
|
role="dialog"
|
|
167
179
|
aria-label="Colour themes"
|
|
168
180
|
initial={{ opacity: 0, y: up ? 8 : -8, scale: 0.96 }}
|
|
169
181
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
170
|
-
|
|
182
|
+
custom={picked}
|
|
183
|
+
variants={{ leave: (p) => (p ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: up ? 8 : -8, scale: 0.96 }) }}
|
|
184
|
+
exit="leave"
|
|
171
185
|
transition={{ type: "spring", stiffness: 380, damping: 28 }}
|
|
172
186
|
className={`absolute z-50 w-[17.5rem] max-w-[calc(100vw-2rem)] rounded-2xl border border-[var(--line)] bg-[var(--surface)] p-3 text-[var(--ink)] shadow-[0_24px_60px_-16px_rgba(0,0,0,0.35)] ${up ? "bottom-full mb-2" : "top-full mt-2"} ${align === "left" ? "left-0" : "right-0"}`}
|
|
173
187
|
style={{ transformOrigin: `${up ? "bottom" : "top"} ${align === "left" ? "left" : "right"}` }}
|
|
@@ -185,8 +199,10 @@ const ThemeSwitcher = ({ themes = defaultThemes, placement = "down", align = "ri
|
|
|
185
199
|
animate={{ y: 0 }}
|
|
186
200
|
transition={{ delay: 0.03 * i, type: "spring", stiffness: 380, damping: 28 }}
|
|
187
201
|
onClick={(e) => {
|
|
188
|
-
setTheme(theme.id, e.currentTarget.querySelector("[data-swatch]"))
|
|
189
|
-
|
|
202
|
+
setTheme(theme.id, e.currentTarget.querySelector("[data-swatch]"), () => {
|
|
203
|
+
setPicked(true);
|
|
204
|
+
setOpen(false);
|
|
205
|
+
});
|
|
190
206
|
}}
|
|
191
207
|
className={`relative flex items-center gap-2.5 rounded-xl py-2 pl-2 pr-5 text-left text-[0.82rem] font-medium transition-colors hover:bg-[var(--bg)] ${active ? "bg-[var(--bg)]" : ""}`}
|
|
192
208
|
style={{ animation: `apex-theme-in 240ms ease-out ${30 * i}ms backwards` }}
|
package/dist/cli/index.js
CHANGED
|
@@ -111,7 +111,6 @@ const componentMap = {
|
|
|
111
111
|
"pixel-transition": "PixelTransition",
|
|
112
112
|
"folder": "Folder",
|
|
113
113
|
"bounce-cards": "BounceCards",
|
|
114
|
-
"card-swap-3d": "CardSwap3D",
|
|
115
114
|
"gooey-nav": "GooeyNav",
|
|
116
115
|
"liquid-blob-3d": "LiquidBlob3D",
|
|
117
116
|
"particle-morph": "ParticleMorph",
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apex-ui-kit",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.2",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "116 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
|
|
6
6
|
"homepage": "https://apex-ui.in",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
-
import { motion } from "framer-motion";
|
|
3
|
-
import { Code2, Layers, Sparkles, Zap } from "lucide-react";
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
* Card Swap 3D: cards stacked in real 3D, turned toward the viewer and
|
|
7
|
-
* receding into the distance. On a timer the front card drops away and
|
|
8
|
-
* returns to the back while the rest step forward. Pauses while hovered;
|
|
9
|
-
* click to swap now. The whole stack scales down to fit narrow containers.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const DEFAULT_CARDS = [
|
|
13
|
-
{ icon: Sparkles, title: "Smooth", body: "Springs tuned to feel natural, never floaty." },
|
|
14
|
-
{ icon: Layers, title: "Composable", body: "Drop components in and style them with Tailwind." },
|
|
15
|
-
{ icon: Code2, title: "Yours", body: "Copy the source or add it with one CLI command." },
|
|
16
|
-
{ icon: Zap, title: "Fast", body: "Transforms and opacity only, 60fps on phones." },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, distanceY = 3.2, depth = 70, tilt = 18, skew = 0, pauseOnHover = true, accentColor = "#a3e635", theme = "dark", width = 18, className = "" }) => {
|
|
20
|
-
// order[0] is the card in front.
|
|
21
|
-
const [order, setOrder] = useState(() => cards.map((_, i) => i));
|
|
22
|
-
const [dropping, setDropping] = useState(null);
|
|
23
|
-
const [paused, setPaused] = useState(false);
|
|
24
|
-
const [fit, setFit] = useState(1);
|
|
25
|
-
const outer = useRef(null);
|
|
26
|
-
const dark = theme !== "light";
|
|
27
|
-
const n = cards.length;
|
|
28
|
-
// Footprint of the whole stack in rem (card plus the steps behind it).
|
|
29
|
-
// (+5: the 3D turn makes the cards project a little wider than they are.)
|
|
30
|
-
const footW = width + distanceX * (n - 1) + 5;
|
|
31
|
-
const footH = width * 0.68 + distanceY * (n - 1) + 2;
|
|
32
|
-
|
|
33
|
-
const swap = () => {
|
|
34
|
-
if (dropping !== null) return;
|
|
35
|
-
setDropping(order[0]);
|
|
36
|
-
setTimeout(() => {
|
|
37
|
-
setOrder((o) => [...o.slice(1), o[0]]);
|
|
38
|
-
setDropping(null);
|
|
39
|
-
}, 520);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (paused) return undefined;
|
|
44
|
-
const id = setInterval(swap, delay);
|
|
45
|
-
return () => clearInterval(id);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Scale the stack down when its container is narrower than its footprint.
|
|
49
|
-
useLayoutEffect(() => {
|
|
50
|
-
const el = outer.current?.parentElement;
|
|
51
|
-
if (!el) return undefined;
|
|
52
|
-
const ro = new ResizeObserver(() => {
|
|
53
|
-
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
54
|
-
setFit(Math.min(1, el.clientWidth / (footW * rem)));
|
|
55
|
-
});
|
|
56
|
-
ro.observe(el);
|
|
57
|
-
return () => ro.disconnect();
|
|
58
|
-
}, [footW]);
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div ref={outer} className={`relative mx-auto ${className}`} style={{ width: `${footW * fit}rem`, height: `${footH * fit}rem` }}>
|
|
62
|
-
<div
|
|
63
|
-
className="absolute left-0 top-0 origin-top-left [perspective:1200px]"
|
|
64
|
-
style={{ width: `${footW}rem`, height: `${footH}rem`, transform: `scale(${fit})` }}
|
|
65
|
-
onPointerEnter={() => pauseOnHover && setPaused(true)}
|
|
66
|
-
onPointerLeave={() => setPaused(false)}
|
|
67
|
-
onClick={swap}
|
|
68
|
-
>
|
|
69
|
-
<div className="absolute [transform-style:preserve-3d]" style={{ left: "2.5rem", bottom: "1rem", width: `${width}rem`, height: `${width * 0.68}rem`, transform: `rotateY(${-tilt}deg) rotateX(${tilt * 0.35}deg)` }}>
|
|
70
|
-
{cards.map((card, i) => {
|
|
71
|
-
const slot = order.indexOf(i);
|
|
72
|
-
const isDropping = dropping === i;
|
|
73
|
-
const Icon = card.icon ?? Sparkles;
|
|
74
|
-
return (
|
|
75
|
-
<motion.div
|
|
76
|
-
key={i}
|
|
77
|
-
className={`absolute inset-0 cursor-pointer overflow-hidden rounded-2xl border p-6 ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"}`}
|
|
78
|
-
style={{ zIndex: isDropping ? 60 : n - slot, boxShadow: "0 30px 60px -28px rgba(0,0,0,0.65)" }}
|
|
79
|
-
initial={false}
|
|
80
|
-
animate={
|
|
81
|
-
isDropping
|
|
82
|
-
? { x: "-2rem", y: "16rem", z: 120, rotateZ: -8, skewY: skew, opacity: 0 }
|
|
83
|
-
: { x: `${slot * distanceX}rem`, y: `${-slot * distanceY}rem`, z: -slot * depth, rotateZ: 0, skewY: skew, opacity: slot > 4 ? 0 : 1 - slot * 0.12 }
|
|
84
|
-
}
|
|
85
|
-
transition={isDropping ? { duration: 0.5, ease: [0.55, 0, 0.8, 0.2] } : { type: "spring", stiffness: 150, damping: 21 }}
|
|
86
|
-
>
|
|
87
|
-
<div className="flex items-center gap-2.5">
|
|
88
|
-
<span className="grid h-9 w-9 place-items-center rounded-xl text-black" style={{ background: accentColor }}>
|
|
89
|
-
<Icon className="h-[18px] w-[18px]" />
|
|
90
|
-
</span>
|
|
91
|
-
<p className="text-lg font-semibold">{card.title}</p>
|
|
92
|
-
</div>
|
|
93
|
-
<p className={`mt-3 text-[0.92rem] leading-relaxed ${dark ? "text-white/60" : "text-neutral-500"}`}>{card.body}</p>
|
|
94
|
-
<div className="pointer-events-none absolute -bottom-10 -right-10 h-32 w-32 rounded-full opacity-20 blur-2xl" style={{ background: accentColor }} />
|
|
95
|
-
</motion.div>
|
|
96
|
-
);
|
|
97
|
-
})}
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export default CardSwap3D;
|