mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
style: fix portfolio image distortion, nebula animation timing and add parallax to computer
This commit is contained in:
@@ -3,65 +3,80 @@ import Image from "next/image";
|
|||||||
|
|
||||||
export const PortfolioGrid = () => {
|
export const PortfolioGrid = () => {
|
||||||
return (
|
return (
|
||||||
<section className="grid grid-cols-1 md:grid-cols-4 auto-rows-[300px] gap-4 max-w-7xl mx-auto p-4 z-10 relative">
|
<section className="grid grid-cols-1 md:grid-cols-4 auto-rows-[300px] gap-6 w-full max-w-7xl mx-auto p-4 z-10 relative">
|
||||||
{/* Hero Project: r/CryptoCurrency */}
|
{/* Hero Project: r/CryptoCurrency */}
|
||||||
<Card className="md:col-span-2 md:row-span-2 relative group overflow-hidden border-0 bg-slate-900/70 backdrop-blur-sm">
|
<Card className="md:col-span-2 md:row-span-2 relative group overflow-hidden border-0 bg-slate-900/70 backdrop-blur-sm min-h-[300px]">
|
||||||
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/80 to-transparent p-6 flex flex-col justify-end">
|
<div className="absolute inset-0 z-20 bg-gradient-to-t from-black/90 via-black/20 to-transparent p-8 flex flex-col justify-end pointer-events-none">
|
||||||
<h3 className="text-3xl font-bold text-white">r/CryptoCurrency</h3>
|
<h3 className="text-3xl font-bold text-white shadow-black drop-shadow-xl">r/CryptoCurrency</h3>
|
||||||
<p className="text-gray-200">Governance dashboard for 10M+ members. Real-time DAO metrics.</p>
|
<p className="text-gray-200 shadow-black drop-shadow-md max-w-sm mt-2 font-medium">Governance dashboard for 10M+ members. Real-time DAO metrics.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute inset-x-4 inset-y-4 rounded-lg overflow-hidden z-10">
|
||||||
<Image
|
<Image
|
||||||
src="/images/r-cryptocurrency.png"
|
src="/images/r-cryptocurrency.png"
|
||||||
alt="r/CryptoCurrency Governance Dashboard"
|
alt="r/CryptoCurrency"
|
||||||
fill
|
fill
|
||||||
className="object-cover opacity-60 transition-transform duration-500 group-hover:scale-105"
|
className="object-cover object-top opacity-60 transition-transform duration-1000 group-hover:scale-110"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Secondary: Occupy Wallets */}
|
{/* Secondary: Occupy Wallets */}
|
||||||
<Card className="md:col-span-2 relative group overflow-hidden bg-zinc-100/70 backdrop-blur-sm">
|
<Card className="md:col-span-2 relative group overflow-hidden bg-zinc-100/70 backdrop-blur-sm border-0 min-h-[300px]">
|
||||||
<div className="absolute inset-0 z-10 p-6 flex flex-col justify-center items-center text-center">
|
<div className="absolute inset-0 z-20 p-8 flex flex-col justify-center items-center text-center pointer-events-none">
|
||||||
<h3 className="text-2xl font-bold text-zinc-900 drop-shadow-sm">Occupy Wallets</h3>
|
<h3 className="text-2xl font-bold text-zinc-900 drop-shadow-sm bg-white/60 backdrop-blur-md rounded-xl px-6 py-2">Occupy Wallets</h3>
|
||||||
<p className="text-zinc-800 font-medium drop-shadow-sm">High-fidelity gallery for fiscal graffiti.</p>
|
<p className="text-zinc-900 font-bold drop-shadow-sm bg-white/60 backdrop-blur-md rounded-xl px-6 py-2 mt-4">High-fidelity gallery for fiscal graffiti.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute inset-x-4 inset-y-4 rounded-lg overflow-hidden z-10">
|
||||||
<Image
|
<Image
|
||||||
src="/images/occupy-wallets.png"
|
src="/images/occupy-wallets.png"
|
||||||
alt="Occupy Wallets Gallery"
|
alt="Occupy Wallets"
|
||||||
fill
|
fill
|
||||||
className="object-cover opacity-40 transition-transform duration-500 group-hover:scale-105"
|
className="object-cover object-top opacity-50 transition-transform duration-1000 group-hover:scale-110"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
|
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
|
||||||
<Card className="md:col-span-1 relative group overflow-hidden bg-orange-50/70 dark:bg-orange-950/70 border-orange-500/20 backdrop-blur-sm">
|
<Card className="md:col-span-1 relative group overflow-hidden bg-orange-50/70 dark:bg-orange-950/70 border-orange-500/20 backdrop-blur-sm min-h-[300px]">
|
||||||
<div className="absolute inset-0 z-10 p-6 bg-gradient-to-br from-background/90 to-transparent">
|
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
|
||||||
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
|
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
|
||||||
<ul className="mt-2 space-y-2 text-sm text-foreground">
|
<ul className="mt-2 space-y-1 text-sm text-foreground font-semibold">
|
||||||
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-50">(Game)</span></li>
|
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-60">(Game)</span></li>
|
||||||
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-50">(WASM)</span></li>
|
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-60">(WASM)</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 border border-orange-500/20 shadow-lg">
|
||||||
<Image
|
<Image
|
||||||
src="/images/privacy-labs.png"
|
src="/images/privacy-labs.png"
|
||||||
alt="Privacy Labs Projects"
|
alt="Privacy Labs"
|
||||||
fill
|
fill
|
||||||
className="object-cover opacity-30 transition-transform duration-500 group-hover:scale-105"
|
className="object-cover object-top opacity-40 transition-transform duration-1000 group-hover:scale-110"
|
||||||
|
sizes="(max-width: 768px) 100vw, 25vw"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Quaternary: MoonPlace */}
|
{/* Quaternary: MoonPlace */}
|
||||||
<Card className="md:col-span-1 relative group overflow-hidden bg-indigo-50/70 dark:bg-indigo-950/70 border-indigo-500/20 backdrop-blur-sm">
|
<Card className="md:col-span-1 relative group overflow-hidden bg-indigo-50/70 dark:bg-indigo-950/70 border-indigo-500/20 backdrop-blur-sm min-h-[300px]">
|
||||||
<div className="absolute inset-0 z-10 p-6 bg-gradient-to-br from-background/90 to-transparent">
|
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
|
||||||
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
|
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
|
||||||
<p className="text-sm mt-2 text-foreground">Collaborative pixel canvas engine.</p>
|
<p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 border border-indigo-500/20 shadow-lg">
|
||||||
<Image
|
<Image
|
||||||
src="/images/moonplace.png"
|
src="/images/moonplace.png"
|
||||||
alt="MoonPlace Pixel Canvas"
|
alt="MoonPlace"
|
||||||
fill
|
fill
|
||||||
className="object-cover opacity-30 transition-transform duration-500 group-hover:scale-105"
|
className="object-cover object-top opacity-40 transition-transform duration-1000 group-hover:scale-110"
|
||||||
|
sizes="(max-width: 768px) 100vw, 25vw"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
+5
-2
@@ -9,13 +9,16 @@
|
|||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"turbo": "latest",
|
|
||||||
"prettier": "^3.2.0",
|
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
|
"prettier": "^3.2.0",
|
||||||
|
"turbo": "latest",
|
||||||
"typescript": "^5.4.0"
|
"typescript": "^5.4.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.0.0",
|
"packageManager": "pnpm@9.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"framer-motion": "^11.0.0",
|
"framer-motion": "^11.0.0",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion, useAnimation } from "framer-motion";
|
import { motion, useAnimation, useScroll, useTransform } from "framer-motion";
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { Monitor } from "lucide-react";
|
import { Monitor } from "lucide-react";
|
||||||
|
|
||||||
interface NebulaProps {
|
interface NebulaProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
density?: number;
|
density?: number;
|
||||||
themeMode?: 'light' | 'dark'; // customized prop
|
themeMode?: 'light' | 'dark';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StreamLineProps {
|
interface StreamLineProps {
|
||||||
@@ -24,27 +24,35 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
const animate = async () => {
|
const animate = async () => {
|
||||||
// Random initial delay
|
// Staggered start delay (0-4s)
|
||||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 4000));
|
await new Promise(resolve => setTimeout(resolve, Math.random() * 4000));
|
||||||
|
|
||||||
while (isMounted) {
|
while (isMounted) {
|
||||||
const duration = 2 + Math.random() * 4;
|
const duration = 10 + Math.random() * 8; // Slow streamers
|
||||||
|
|
||||||
controls.set({ pathLength: 0, opacity: 0 });
|
// Ensure starting state is definitely 0
|
||||||
|
controls.set({ pathLength: 0, pathOffset: 0, opacity: 0 });
|
||||||
|
|
||||||
|
// Trigger onHit when the tip reaches the end (at 75% of duration)
|
||||||
|
const hitTimeout = setTimeout(() => {
|
||||||
|
if (isMounted) onHit();
|
||||||
|
}, duration * 750);
|
||||||
|
|
||||||
await controls.start({
|
await controls.start({
|
||||||
pathLength: [0, 1],
|
pathLength: [0, 0.25, 0.25, 0],
|
||||||
opacity: [0, 1, 1, 0],
|
pathOffset: [0, 0, 0.75, 1],
|
||||||
|
opacity: [0, 0.5, 0.5, 0],
|
||||||
transition: {
|
transition: {
|
||||||
duration,
|
duration,
|
||||||
ease: "easeInOut",
|
ease: "linear",
|
||||||
times: [0, 0.1, 0.9, 1]
|
times: [0, 0.25, 0.75, 1]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMounted) onHit();
|
clearTimeout(hitTimeout);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 2000));
|
// Continuous flow with small pause
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
animate();
|
animate();
|
||||||
@@ -57,57 +65,74 @@ const StreamLine = ({ d, themeMode, onHit, width }: StreamLineProps) => {
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={width}
|
strokeWidth={width}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
initial={{ pathLength: 0, pathOffset: 0, opacity: 0 }}
|
||||||
animate={controls}
|
animate={controls}
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-colors duration-500",
|
"transition-colors duration-1000",
|
||||||
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
|
themeMode === 'light' ? "text-slate-300" : "text-emerald-500/50"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: NebulaProps) => {
|
export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: NebulaProps) => {
|
||||||
const [paths, setPaths] = useState<string[]>([]);
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const monitorControls = useAnimation();
|
const monitorControls = useAnimation();
|
||||||
|
const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-slate-400" : "text-emerald-500");
|
||||||
|
|
||||||
// Configuration for convergence point (percent of container)
|
// Sync color with theme if no impacts have happened yet
|
||||||
const targetX = 25; // moved right
|
|
||||||
const targetY = 20; // moved up
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMonitorColor(themeMode === 'light' ? "text-slate-300" : "text-emerald-500/50");
|
||||||
const newPaths = Array.from({ length: density }).map(() => {
|
}, [themeMode]);
|
||||||
// Determine random edge start point
|
|
||||||
const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
|
// Parallax Scroll for Computer Icon
|
||||||
|
const { scrollY } = useScroll();
|
||||||
|
const yRange = useTransform(scrollY, [0, 1000], [0, 200]);
|
||||||
|
|
||||||
|
// Configuration for convergence point
|
||||||
|
const targetX = 25;
|
||||||
|
const targetY = 20;
|
||||||
|
|
||||||
|
const paths = useMemo(() => {
|
||||||
|
return Array.from({ length: density }).map(() => {
|
||||||
|
const edge = Math.floor(Math.random() * 4);
|
||||||
let startX = 50, startY = 50;
|
let startX = 50, startY = 50;
|
||||||
|
|
||||||
// Push start point well outside viewbox to ensure full coverage
|
|
||||||
switch(edge) {
|
switch(edge) {
|
||||||
case 0: startX = Math.random() * 100; startY = -20; break; // Top
|
case 0: startX = Math.random() * 100; startY = -20; break;
|
||||||
case 1: startX = 120; startY = Math.random() * 100; break; // Right
|
case 1: startX = 120; startY = Math.random() * 100; break;
|
||||||
case 2: startX = Math.random() * 100; startY = 120; break; // Bottom
|
case 2: startX = Math.random() * 100; startY = 120; break;
|
||||||
case 3: startX = -20; startY = Math.random() * 100; break; // Left
|
case 3: startX = -20; startY = Math.random() * 100; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End at target point with small variance
|
const endX = targetX + (Math.random() - 0.5) * 4;
|
||||||
const endX = targetX + (Math.random() - 0.5) * 5;
|
const endY = targetY + (Math.random() - 0.5) * 4;
|
||||||
const endY = targetY + (Math.random() - 0.5) * 5;
|
|
||||||
|
|
||||||
// Control point for curve
|
|
||||||
const cpX = (startX + endX) / 2 + (Math.random() - 0.5) * 50;
|
const cpX = (startX + endX) / 2 + (Math.random() - 0.5) * 50;
|
||||||
const cpY = (startY + endY) / 2 + (Math.random() - 0.5) * 50;
|
const cpY = (startY + endY) / 2 + (Math.random() - 0.5) * 50;
|
||||||
|
|
||||||
return `M ${startX} ${startY} Q ${cpX} ${cpY} ${endX} ${endY}`;
|
return `M ${startX} ${startY} Q ${cpX} ${cpY} ${endX} ${endY}`;
|
||||||
});
|
});
|
||||||
setPaths(newPaths);
|
}, [density, themeMode]); // Regenerate on theme switch
|
||||||
}, [density]); // Removed targetX/Y dep as constant
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleHit = useCallback(() => {
|
const handleHit = useCallback(() => {
|
||||||
|
const colors = [
|
||||||
|
"text-emerald-500", "text-blue-500", "text-purple-500",
|
||||||
|
"text-indigo-500", "text-cyan-500", "text-rose-500", "text-amber-500"
|
||||||
|
];
|
||||||
|
setMonitorColor(colors[Math.floor(Math.random() * colors.length)]);
|
||||||
|
|
||||||
monitorControls.start({
|
monitorControls.start({
|
||||||
scale: [1, 1.15, 1],
|
scale: [1, 1.2, 1],
|
||||||
filter: ["brightness(1)", "brightness(1.5)", "brightness(1)"],
|
filter: [
|
||||||
transition: { duration: 0.3 }
|
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))",
|
||||||
|
"brightness(1.5) drop-shadow(0 0 20px currentColor)",
|
||||||
|
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))"
|
||||||
|
],
|
||||||
|
transition: { duration: 4, ease: "easeInOut" }
|
||||||
});
|
});
|
||||||
}, [monitorControls]);
|
}, [monitorControls]);
|
||||||
|
|
||||||
@@ -116,17 +141,20 @@ export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: Neb
|
|||||||
return (
|
return (
|
||||||
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
|
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
|
||||||
|
|
||||||
{/* "Computer" Icon positioned via inline styles to match physics target exactly */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute -translate-x-1/2 -translate-y-1/2 z-10 opacity-30 dark:opacity-50"
|
className="absolute -translate-x-1/2 -translate-y-1/2 z-20 opacity-40 dark:opacity-60"
|
||||||
style={{ left: `${targetX}%`, top: `${targetY}%` }}
|
style={{
|
||||||
|
left: `${targetX}%`,
|
||||||
|
top: `${targetY}%`,
|
||||||
|
y: yRange
|
||||||
|
}}
|
||||||
animate={monitorControls}
|
animate={monitorControls}
|
||||||
>
|
>
|
||||||
<Monitor
|
<Monitor
|
||||||
size={64}
|
size={64}
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-colors duration-1000",
|
"transition-colors duration-2000",
|
||||||
themeMode === 'light' ? "text-slate-400" : "text-emerald-500"
|
monitorColor
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -134,15 +162,15 @@ export const NebulaField = ({ className, density = 20, themeMode = 'dark' }: Neb
|
|||||||
<svg
|
<svg
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
className="w-full h-full opacity-60 dark:opacity-80 absolute inset-0"
|
className="w-full h-full opacity-40 dark:opacity-70 absolute inset-0"
|
||||||
>
|
>
|
||||||
{paths.map((d, i) => (
|
{paths.map((d: string, i: number) => (
|
||||||
<StreamLine
|
<StreamLine
|
||||||
key={i}
|
key={`${themeMode}-${i}`}
|
||||||
d={d}
|
d={d}
|
||||||
themeMode={themeMode}
|
themeMode={themeMode}
|
||||||
onHit={handleHit}
|
onHit={handleHit}
|
||||||
width={themeMode === 'light' ? 0.1 : 0.2}
|
width={themeMode === 'light' ? 0.05 : 0.15}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
Generated
+7
@@ -7,6 +7,10 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^18.0.0
|
||||||
|
version: 18.3.27
|
||||||
devDependencies:
|
devDependencies:
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
@@ -105,6 +109,9 @@ importers:
|
|||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4(@types/react@18.3.27)(react@18.3.1)
|
version: 1.2.4(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@types/react':
|
||||||
|
specifier: ^18.0.0
|
||||||
|
version: 18.3.27
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
|
|||||||
Reference in New Issue
Block a user