apex-ui-kit 2.10.0 → 2.12.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/ReadMe.md +2 -3
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +17 -3
- package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +157 -35
- package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +134 -145
- package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +64 -40
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +6 -3
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +1 -1
- package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +58 -36
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +16 -2
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +20 -10
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +1 -1
- package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +28 -10
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +8 -6
- package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +5 -5
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +175 -158
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +34 -33
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +237 -235
- package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +140 -120
- package/dist/cli/index.js +40 -21
- package/package.json +8 -2
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +0 -243
|
@@ -1,120 +1,140 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
1
|
+
import React, { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
3
|
+
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// --- Testimonials Component ---
|
|
7
|
+
const Testimonials = ({ testimonials }) => {
|
|
8
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
9
|
+
// The layout follows the card's own width (not the screen), so it also
|
|
10
|
+
// fits sidebars, preview frames and narrow columns.
|
|
11
|
+
const root = useRef(null);
|
|
12
|
+
const [size, setSize] = useState("lg");
|
|
13
|
+
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
const el = root.current;
|
|
16
|
+
if (!el) return undefined;
|
|
17
|
+
const ro = new ResizeObserver(() => {
|
|
18
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
19
|
+
const w = el.clientWidth / rem;
|
|
20
|
+
setSize(w < 26 ? "sm" : w < 44 ? "md" : "lg");
|
|
21
|
+
});
|
|
22
|
+
ro.observe(el);
|
|
23
|
+
return () => ro.disconnect();
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const wide = size === "lg";
|
|
27
|
+
const small = size === "sm";
|
|
28
|
+
|
|
29
|
+
const handleNext = () => {
|
|
30
|
+
setActiveIndex((prevIndex) => (prevIndex + 1) % testimonials.length);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handlePrev = () => {
|
|
34
|
+
setActiveIndex((prevIndex) => (prevIndex - 1 + testimonials.length) % testimonials.length);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Animation variants for the text content
|
|
38
|
+
const textVariants = {
|
|
39
|
+
enter: { opacity: 0, y: 20, filter: "blur(5px)" },
|
|
40
|
+
center: { opacity: 1, y: 0, filter: "blur(0px)" },
|
|
41
|
+
exit: { opacity: 0, y: -20, filter: "blur(5px)" },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div ref={root} className={`w-full max-w-4xl mx-auto bg-slate-950 rounded-2xl border border-slate-800 grid items-center overflow-hidden ${wide ? "grid-cols-2 gap-12 p-12" : small ? "gap-6 p-5" : "gap-8 p-8"}`}>
|
|
46
|
+
|
|
47
|
+
{/* Left Part: Text Content and Navigation */}
|
|
48
|
+
<div className={`flex flex-col justify-center h-full ${wide ? "text-left order-1" : "text-center order-2"}`}>
|
|
49
|
+
{/* Title, subtitle and quote with animation; the min height keeps the buttons from jumping. */}
|
|
50
|
+
<div className={`relative ${small ? "min-h-[15rem]" : "min-h-[12rem]"}`}>
|
|
51
|
+
<AnimatePresence initial={false} mode="wait">
|
|
52
|
+
<motion.div
|
|
53
|
+
key={activeIndex}
|
|
54
|
+
variants={textVariants}
|
|
55
|
+
initial="enter"
|
|
56
|
+
animate="center"
|
|
57
|
+
exit="exit"
|
|
58
|
+
transition={{ duration: 0.4, ease: "easeInOut" }}
|
|
59
|
+
className="w-full"
|
|
60
|
+
>
|
|
61
|
+
<h2 className={`${small ? "text-2xl" : "text-3xl"} font-bold text-white tracking-tight`}>{testimonials[activeIndex].author}</h2>
|
|
62
|
+
<p className="mt-2 text-lime-400 font-medium">{testimonials[activeIndex].title}</p>
|
|
63
|
+
<p className="mt-4 text-slate-400 text-sm leading-relaxed">"{testimonials[activeIndex].text}"</p>
|
|
64
|
+
</motion.div>
|
|
65
|
+
</AnimatePresence>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Navigation Buttons with Microinteractions */}
|
|
69
|
+
<div className={`flex items-center gap-4 ${wide ? "justify-start mt-8" : "justify-center mt-5"}`}>
|
|
70
|
+
<motion.button
|
|
71
|
+
onClick={handlePrev}
|
|
72
|
+
aria-label="Previous testimonial"
|
|
73
|
+
className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
|
|
74
|
+
whileHover={{ scale: 1.1 }}
|
|
75
|
+
whileTap={{ scale: 0.95 }}
|
|
76
|
+
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
|
77
|
+
>
|
|
78
|
+
<ArrowLeft size={20} />
|
|
79
|
+
</motion.button>
|
|
80
|
+
<motion.button
|
|
81
|
+
onClick={handleNext}
|
|
82
|
+
aria-label="Next testimonial"
|
|
83
|
+
className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
|
|
84
|
+
whileHover={{ scale: 1.1 }}
|
|
85
|
+
whileTap={{ scale: 0.95 }}
|
|
86
|
+
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
|
87
|
+
>
|
|
88
|
+
<ArrowRight size={20} />
|
|
89
|
+
</motion.button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Right Part: Stacked Images */}
|
|
94
|
+
<div className={`flex w-full items-center justify-center ${wide ? "order-2" : "order-1"}`}>
|
|
95
|
+
{/* Sized to the card plus the fan, so the stack stays centred and inside the panel. */}
|
|
96
|
+
<div className={`relative ${small ? "h-[calc(14rem+30px)] w-[calc(10.5rem+60px)]" : "h-[calc(20rem+30px)] w-[calc(16rem+60px)]"}`}>
|
|
97
|
+
{testimonials.map((testimonial, index) => {
|
|
98
|
+
// Determine the position of the card in the stack
|
|
99
|
+
const position = (index - activeIndex + testimonials.length) % testimonials.length;
|
|
100
|
+
|
|
101
|
+
// Only render the top 4 cards for performance and visual clarity
|
|
102
|
+
if (position > 3) return null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<motion.div
|
|
106
|
+
key={testimonial.id || index}
|
|
107
|
+
className={`absolute ${small ? "w-[10.5rem] h-56" : "w-64 h-80"} bg-slate-800 rounded-xl shadow-2xl overflow-hidden border-2 border-slate-700`}
|
|
108
|
+
initial={false}
|
|
109
|
+
animate={{
|
|
110
|
+
scale: 1 - position * 0.08,
|
|
111
|
+
rotate: -4 + position * 4, // Add a fanned rotation effect
|
|
112
|
+
left: `${position * 20}px`, // Fan out horizontally
|
|
113
|
+
top: `${position * 10}px`, // Stagger vertically
|
|
114
|
+
zIndex: testimonials.length - position,
|
|
115
|
+
opacity: position === 0 ? 1 : 0.5, // Only top image is fully opaque
|
|
116
|
+
}}
|
|
117
|
+
transition={{
|
|
118
|
+
type: 'spring',
|
|
119
|
+
stiffness: 260,
|
|
120
|
+
damping: 25,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<img
|
|
124
|
+
src={testimonial.image}
|
|
125
|
+
alt={testimonial.author}
|
|
126
|
+
className="w-full h-full object-cover"
|
|
127
|
+
onError={(e) => { e.target.onerror = null; e.target.src = `https://placehold.co/256x320/0f172a/a3e635?text=${testimonial.author.split(' ')[0]}`; }}
|
|
128
|
+
/>
|
|
129
|
+
</motion.div>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
export default Testimonials;
|
package/dist/cli/index.js
CHANGED
|
@@ -40,7 +40,6 @@ const componentMap = {
|
|
|
40
40
|
"rain-background": "RainBackground",
|
|
41
41
|
"ripple-background": "RippleBackground",
|
|
42
42
|
"scroll-marquee": "ScrollMarquee",
|
|
43
|
-
"smart-grid-card": "SmartGridCard",
|
|
44
43
|
"smoke-background": "SmokeBackground",
|
|
45
44
|
"tooltip": "ToolTip",
|
|
46
45
|
"tool-tip": "ToolTip",
|
|
@@ -153,6 +152,31 @@ const extraDeps = {
|
|
|
153
152
|
WebGLCarousel: ["ogl"],
|
|
154
153
|
CrossedMarquee: ["simple-icons"],
|
|
155
154
|
};
|
|
155
|
+
// Packages the app already has (or that ship with Node), never installed by us.
|
|
156
|
+
const BUILTIN = new Set(["react", "react-dom", "path", "fs", "url"]);
|
|
157
|
+
// The npm packages a component folder imports, read from its source files.
|
|
158
|
+
async function importedPackages(dir) {
|
|
159
|
+
const found = new Set();
|
|
160
|
+
const walk = async (d) => {
|
|
161
|
+
for (const entry of await fs.readdir(d, { withFileTypes: true })) {
|
|
162
|
+
const full = path.join(d, entry.name);
|
|
163
|
+
if (entry.isDirectory())
|
|
164
|
+
await walk(full);
|
|
165
|
+
else if (/\.(jsx?|tsx?|mjs)$/.test(entry.name)) {
|
|
166
|
+
const code = await fs.readFile(full, "utf8");
|
|
167
|
+
const re = /(?:from\s+|import\s*\(\s*|import\s+)["']([^"'.\/][^"']*)["']/g;
|
|
168
|
+
for (let m = re.exec(code); m; m = re.exec(code)) {
|
|
169
|
+
const spec = m[1];
|
|
170
|
+
const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
|
|
171
|
+
if (!BUILTIN.has(name))
|
|
172
|
+
found.add(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
await walk(dir);
|
|
178
|
+
return [...found];
|
|
179
|
+
}
|
|
156
180
|
async function main() {
|
|
157
181
|
const args = process.argv.slice(2);
|
|
158
182
|
const command = args[0];
|
|
@@ -220,26 +244,6 @@ async function main() {
|
|
|
220
244
|
{ title: "TypeScript (Coming Soon)", value: "ts", disabled: true },
|
|
221
245
|
],
|
|
222
246
|
});
|
|
223
|
-
// Install dependencies
|
|
224
|
-
console.log(chalk.cyan("📦 Installing dependencies..."));
|
|
225
|
-
const deps = [
|
|
226
|
-
"gsap",
|
|
227
|
-
"framer-motion",
|
|
228
|
-
"three",
|
|
229
|
-
"@fortawesome/fontawesome-svg-core",
|
|
230
|
-
"@fortawesome/free-solid-svg-icons",
|
|
231
|
-
"@fortawesome/react-fontawesome",
|
|
232
|
-
"react-icons",
|
|
233
|
-
"lucide-react",
|
|
234
|
-
...(extraDeps[matchedName] ?? []),
|
|
235
|
-
];
|
|
236
|
-
try {
|
|
237
|
-
execSync(`npm install ${deps.join(" ")}`, { stdio: "inherit" });
|
|
238
|
-
}
|
|
239
|
-
catch (err) {
|
|
240
|
-
console.log(chalk.red("❌ Failed to install dependencies."));
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
247
|
// Prepare destination
|
|
244
248
|
const apexKitPath = path.join(basePath, "ApexUI-Kit");
|
|
245
249
|
const dest = path.join(apexKitPath, matchedName);
|
|
@@ -250,6 +254,21 @@ async function main() {
|
|
|
250
254
|
console.log(chalk.red(`❌ Internal error: Component folder '${matchedName}' not found.`));
|
|
251
255
|
return;
|
|
252
256
|
}
|
|
257
|
+
// Install only what this component imports, so the app stays light.
|
|
258
|
+
const deps = [...new Set([...(await importedPackages(source)), ...(extraDeps[matchedName] ?? [])])];
|
|
259
|
+
if (deps.length) {
|
|
260
|
+
console.log(chalk.cyan(`📦 Installing ${deps.join(", ")}...`));
|
|
261
|
+
try {
|
|
262
|
+
execSync(`npm install ${deps.join(" ")}`, { stdio: "inherit" });
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
console.log(chalk.red("❌ Failed to install dependencies."));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
console.log(chalk.cyan("📦 No extra packages needed."));
|
|
271
|
+
}
|
|
253
272
|
await fs.ensureDir(dest);
|
|
254
273
|
await fs.copy(source, dest);
|
|
255
274
|
console.log(chalk.green(`✅ '${matchedName}' added to ${dest}`));
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apex-ui-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "117 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",
|
|
@@ -60,5 +60,11 @@
|
|
|
60
60
|
"@types/node": "^20.4.0",
|
|
61
61
|
"copyfiles": "^2.4.1",
|
|
62
62
|
"typescript": "^5.4.0"
|
|
63
|
+
},
|
|
64
|
+
"sideEffects": [
|
|
65
|
+
"*.css"
|
|
66
|
+
],
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18"
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { gsap } from "gsap";
|
|
3
|
-
import {
|
|
4
|
-
Box,
|
|
5
|
-
Settings,
|
|
6
|
-
Lock,
|
|
7
|
-
Sparkles,
|
|
8
|
-
Search,
|
|
9
|
-
LaptopMinimal,
|
|
10
|
-
} from "lucide-react";
|
|
11
|
-
|
|
12
|
-
const CARD_HEIGHT = 264;
|
|
13
|
-
const CARD_EXPANDED_HEIGHT = 2 * CARD_HEIGHT + 10; // two rows plus the grid gap
|
|
14
|
-
|
|
15
|
-
const SmartGridCard = ({
|
|
16
|
-
cards = defaultCards(),
|
|
17
|
-
mergeMap = {}, // Example: { 5: 2 }
|
|
18
|
-
borderColor = "cyan"
|
|
19
|
-
}) => {
|
|
20
|
-
// Track screen width for responsive mergeMap logic
|
|
21
|
-
const [screenWidth, setScreenWidth] = useState(() => typeof window !== 'undefined' ? window.innerWidth : 1200);
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
const handleResize = () => setScreenWidth(window.innerWidth);
|
|
24
|
-
window.addEventListener('resize', handleResize);
|
|
25
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
26
|
-
}, []);
|
|
27
|
-
const isLargeScreen = screenWidth >= 1080;
|
|
28
|
-
const isThreeCol = screenWidth >= 1024;
|
|
29
|
-
const isTwoCol = screenWidth >= 768 && screenWidth < 1024;
|
|
30
|
-
const cardRefs = useRef({});
|
|
31
|
-
const [mousePos, setMousePos] = useState({});
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (!isLargeScreen) return; // Only run mergeMap effect on large screens
|
|
35
|
-
// Defensive: never trigger a reload, only animate
|
|
36
|
-
if (Object.keys(mergeMap).length === 0) return; // No merges to process
|
|
37
|
-
// Animate cards based on mergeMap
|
|
38
|
-
Object.entries(mergeMap).forEach(([sourceId, targetId]) => {
|
|
39
|
-
const sourceEl = cardRefs.current[sourceId];
|
|
40
|
-
const targetEl = cardRefs.current[targetId];
|
|
41
|
-
|
|
42
|
-
if (targetEl) {
|
|
43
|
-
gsap.to(targetEl, {
|
|
44
|
-
height: CARD_EXPANDED_HEIGHT,
|
|
45
|
-
duration: 0.5,
|
|
46
|
-
ease: "power2.out",
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (sourceEl) {
|
|
51
|
-
gsap.to(sourceEl, {
|
|
52
|
-
opacity: 0,
|
|
53
|
-
scale: 0.9,
|
|
54
|
-
duration: 0.3,
|
|
55
|
-
onComplete: () => {
|
|
56
|
-
if (sourceEl) sourceEl.style.display = "none";
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
// No reload, no navigation, no window.location
|
|
62
|
-
return () => { };
|
|
63
|
-
}, [mergeMap, isLargeScreen]);
|
|
64
|
-
|
|
65
|
-
const handleMouseMove = (e, id) => {
|
|
66
|
-
const bounds = e.currentTarget.getBoundingClientRect();
|
|
67
|
-
const x = e.clientX - bounds.left;
|
|
68
|
-
const y = e.clientY - bounds.top;
|
|
69
|
-
setMousePos((prev) => ({
|
|
70
|
-
...prev,
|
|
71
|
-
[id]: { x, y },
|
|
72
|
-
}));
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const getCardStyle = (id) => {
|
|
76
|
-
// <768px: 1 column
|
|
77
|
-
if (screenWidth < 768) {
|
|
78
|
-
return {
|
|
79
|
-
display: 'block',
|
|
80
|
-
height: CARD_HEIGHT,
|
|
81
|
-
gridColumnStart: 'auto',
|
|
82
|
-
gridRowStart: 'auto',
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// 768px - 1023px: 2 columns
|
|
86
|
-
if (isTwoCol) {
|
|
87
|
-
const col = ((id - 1) % 2) + 1;
|
|
88
|
-
const row = Math.floor((id - 1) / 2) + 1;
|
|
89
|
-
return {
|
|
90
|
-
display: 'block',
|
|
91
|
-
height: CARD_HEIGHT,
|
|
92
|
-
gridColumnStart: col,
|
|
93
|
-
gridRowStart: row,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
// 1024px and above: 3 columns (mergeMap only for >=1080px)
|
|
97
|
-
if (isThreeCol) {
|
|
98
|
-
if (screenWidth < 1080) {
|
|
99
|
-
const col = ((id - 1) % 3) + 1;
|
|
100
|
-
const row = Math.floor((id - 1) / 3) + 1;
|
|
101
|
-
return {
|
|
102
|
-
display: 'block',
|
|
103
|
-
height: CARD_HEIGHT,
|
|
104
|
-
gridColumnStart: col,
|
|
105
|
-
gridRowStart: row,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
// >=1080px: 3 columns, merging enabled
|
|
109
|
-
const isMerged = mergeMap[id];
|
|
110
|
-
const isTarget = Object.values(mergeMap).includes(id);
|
|
111
|
-
if (isMerged) return { display: "none" };
|
|
112
|
-
const col = ((id - 1) % 3) + 1;
|
|
113
|
-
const row = Math.floor((id - 1) / 3) + 1;
|
|
114
|
-
return {
|
|
115
|
-
gridColumnStart: col,
|
|
116
|
-
gridRowStart: row,
|
|
117
|
-
height: isTarget ? CARD_EXPANDED_HEIGHT : CARD_HEIGHT,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div
|
|
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
|
-
style={screenWidth >= 1024 ? {
|
|
126
|
-
// minmax(0, …): columns share the container's width instead of overflowing it.
|
|
127
|
-
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
|
128
|
-
gridTemplateRows: `repeat(2, ${CARD_HEIGHT}px)`
|
|
129
|
-
} : screenWidth >= 768 ? {
|
|
130
|
-
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
131
|
-
gridTemplateRows: `repeat(3, ${CARD_HEIGHT}px)`
|
|
132
|
-
} : {}}
|
|
133
|
-
>
|
|
134
|
-
{cards.map((card) => {
|
|
135
|
-
const style = getCardStyle(card.id);
|
|
136
|
-
if (style.display === "none") return null;
|
|
137
|
-
|
|
138
|
-
const pos = mousePos[card.id] || { x: 0, y: 0 };
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<div
|
|
142
|
-
key={card.id}
|
|
143
|
-
ref={(el) => (cardRefs.current[card.id] = el)}
|
|
144
|
-
style={style}
|
|
145
|
-
onMouseMove={(e) => handleMouseMove(e, card.id)}
|
|
146
|
-
className="group relative overflow-hidden p-[1px] rounded-2xl bg-zinc-900 border border-zinc-700 shadow-2xl transition-all duration-500"
|
|
147
|
-
>
|
|
148
|
-
{/* Mouse Glow Border */}
|
|
149
|
-
<div
|
|
150
|
-
className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition duration-300"
|
|
151
|
-
style={{
|
|
152
|
-
maskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
|
|
153
|
-
WebkitMaskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
|
|
154
|
-
background: `radial-gradient(circle at ${pos.x}px ${pos.y}px, ${borderColor}, transparent 80%)`,
|
|
155
|
-
borderRadius: "1rem",
|
|
156
|
-
}}
|
|
157
|
-
/>
|
|
158
|
-
|
|
159
|
-
{/* Card Content */}
|
|
160
|
-
{/* Compact enough that the button always fits inside the fixed card height */}
|
|
161
|
-
<div className="relative z-10 flex flex-col items-start h-full w-full p-5 rounded-2xl bg-zinc-900 text-white gap-3">
|
|
162
|
-
<div className="flex h-11 w-11 shrink-0 items-center justify-center border border-dashed rounded-lg border-purple-600">{card.icon}</div>
|
|
163
|
-
<div className="min-h-0">
|
|
164
|
-
<h3 className="text-lg font-bold mb-1.5 break-words leading-tight">{card.title}</h3>
|
|
165
|
-
<p className="text-sm leading-relaxed w-full text-white/70 break-words line-clamp-3">{card.description}</p>
|
|
166
|
-
</div>
|
|
167
|
-
<button
|
|
168
|
-
type="button"
|
|
169
|
-
className="mt-auto shrink-0 relative overflow-hidden whitespace-nowrap px-4 py-2 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-sm"
|
|
170
|
-
>
|
|
171
|
-
<span className="relative z-10 flex items-center gap-2">
|
|
172
|
-
{card.buttonText}
|
|
173
|
-
<span className="arrow-icon transition-transform duration-200 group-hover:translate-x-2 flex items-center">
|
|
174
|
-
<svg width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
175
|
-
<path d="M5 12h14M13 6l6 6-6 6" />
|
|
176
|
-
</svg>
|
|
177
|
-
</span>
|
|
178
|
-
</span>
|
|
179
|
-
<span
|
|
180
|
-
className="absolute bottom-0 left-0 w-full h-full opacity-0 group-hover:opacity-100 origin-bottom transition-all ease-in-out duration-500 rounded-lg"
|
|
181
|
-
style={{
|
|
182
|
-
background: "radial-gradient(circle at bottom, purple, transparent 69%)",
|
|
183
|
-
mixBlendMode: "screen",
|
|
184
|
-
}}
|
|
185
|
-
/>
|
|
186
|
-
</button>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
);
|
|
190
|
-
})}
|
|
191
|
-
</div>
|
|
192
|
-
);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// Default Cards
|
|
196
|
-
function defaultCards() {
|
|
197
|
-
return [
|
|
198
|
-
{
|
|
199
|
-
id: 1,
|
|
200
|
-
icon: <LaptopMinimal className="w-6 h-6 text-white" />,
|
|
201
|
-
title: "Responsive Layout",
|
|
202
|
-
description: "Works across all screen sizes with smooth animation.",
|
|
203
|
-
buttonText: "Learn More",
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
id: 2,
|
|
207
|
-
icon: <Box className="w-6 h-6 text-white" />,
|
|
208
|
-
title: "Composable Components",
|
|
209
|
-
description: "Each card is modular and easy to customize.",
|
|
210
|
-
buttonText: "Explore",
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
id: 3,
|
|
214
|
-
icon: <Settings className="w-6 h-6 text-white" />,
|
|
215
|
-
title: "Easy Config",
|
|
216
|
-
description: "Use props like mergeMap to control logic.",
|
|
217
|
-
buttonText: "Settings",
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
id: 4,
|
|
221
|
-
icon: <Lock className="w-6 h-6 text-white" />,
|
|
222
|
-
title: "Secure UI",
|
|
223
|
-
description: "Perfect for dashboards and secure apps.",
|
|
224
|
-
buttonText: "Secure Now",
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
id: 5,
|
|
228
|
-
icon: <Sparkles className="w-6 h-6 text-white" />,
|
|
229
|
-
title: "Edge Effects",
|
|
230
|
-
description: "Glowing borders that follow your mouse.",
|
|
231
|
-
buttonText: "Try It",
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
id: 6,
|
|
235
|
-
icon: <Search className="w-6 h-6 text-white" />,
|
|
236
|
-
title: "Fast Search",
|
|
237
|
-
description: "Lightning-fast component navigation.",
|
|
238
|
-
buttonText: "Search",
|
|
239
|
-
},
|
|
240
|
-
];
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export default SmartGridCard;
|