feat(ui): Liquid Glass + Aceternity 컴포넌트 도입 (clsx·framer-motion·tailwind-merge)
- LiquidGlass: GlassButton·GlassFilter (Apple Liquid Glass 효과) - 3d-card-effect: 마우스 추적 3D 카드 래퍼 - sparkles-text: SparklesText·SparklesOverlay - lib/utils.ts: cn() (clsx + tailwind-merge) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
app/components/LiquidGlass.tsx
Normal file
161
app/components/LiquidGlass.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface GlassEffectProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
href?: string;
|
||||
external?: boolean;
|
||||
target?: string;
|
||||
onClick?: () => void;
|
||||
tint?: string;
|
||||
}
|
||||
|
||||
export const GlassEffect: React.FC<GlassEffectProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
style = {},
|
||||
href,
|
||||
external,
|
||||
target,
|
||||
onClick,
|
||||
tint = 'rgba(255, 255, 255, 0.18)',
|
||||
}) => {
|
||||
const glassStyle: React.CSSProperties = {
|
||||
boxShadow: '0 6px 6px rgba(0, 0, 0, 0.2), 0 0 20px rgba(0, 0, 0, 0.1)',
|
||||
transitionTimingFunction: 'cubic-bezier(0.175, 0.885, 0.32, 2.2)',
|
||||
...style,
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={`relative flex font-semibold overflow-hidden text-white cursor-pointer transition-all duration-700 ${className}`}
|
||||
style={glassStyle}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 z-0 overflow-hidden"
|
||||
style={{
|
||||
borderRadius: 'inherit',
|
||||
backdropFilter: 'blur(3px)',
|
||||
WebkitBackdropFilter: 'blur(3px)',
|
||||
filter: 'url(#glass-distortion)',
|
||||
isolation: 'isolate',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{ borderRadius: 'inherit', background: tint }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 z-20 overflow-hidden"
|
||||
style={{
|
||||
borderRadius: 'inherit',
|
||||
boxShadow:
|
||||
'inset 2px 2px 1px 0 rgba(255,255,255,0.5), inset -1px -1px 1px 1px rgba(255,255,255,0.5)',
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-30 w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!href) return content;
|
||||
if (external) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={target ?? '_blank'}
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link href={href} className="inline-block" style={{ textDecoration: 'none' }}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const GlassButton: React.FC<{
|
||||
children: React.ReactNode;
|
||||
href?: string;
|
||||
external?: boolean;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
tint?: string;
|
||||
}> = ({ children, href, external, onClick, className = '', tint }) => (
|
||||
<GlassEffect
|
||||
href={href}
|
||||
external={external}
|
||||
onClick={onClick}
|
||||
tint={tint}
|
||||
className={`rounded-2xl px-7 py-4 hover:px-8 ${className}`}
|
||||
>
|
||||
<div
|
||||
className="transition-all duration-700 hover:scale-[0.98] whitespace-nowrap"
|
||||
style={{ transitionTimingFunction: 'cubic-bezier(0.175, 0.885, 0.32, 2.2)' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</GlassEffect>
|
||||
);
|
||||
|
||||
export const GlassFilter: React.FC = () => (
|
||||
<svg style={{ display: 'none' }} aria-hidden>
|
||||
<filter
|
||||
id="glass-distortion"
|
||||
x="0%"
|
||||
y="0%"
|
||||
width="100%"
|
||||
height="100%"
|
||||
filterUnits="objectBoundingBox"
|
||||
>
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.001 0.005"
|
||||
numOctaves="1"
|
||||
seed="17"
|
||||
result="turbulence"
|
||||
/>
|
||||
<feComponentTransfer in="turbulence" result="mapped">
|
||||
<feFuncR type="gamma" amplitude="1" exponent="10" offset="0.5" />
|
||||
<feFuncG type="gamma" amplitude="0" exponent="1" offset="0" />
|
||||
<feFuncB type="gamma" amplitude="0" exponent="1" offset="0.5" />
|
||||
</feComponentTransfer>
|
||||
<feGaussianBlur in="turbulence" stdDeviation="3" result="softMap" />
|
||||
<feSpecularLighting
|
||||
in="softMap"
|
||||
surfaceScale="5"
|
||||
specularConstant="1"
|
||||
specularExponent="100"
|
||||
lightingColor="white"
|
||||
result="specLight"
|
||||
>
|
||||
<fePointLight x="-200" y="-200" z="300" />
|
||||
</feSpecularLighting>
|
||||
<feComposite
|
||||
in="specLight"
|
||||
operator="arithmetic"
|
||||
k1="0"
|
||||
k2="1"
|
||||
k3="1"
|
||||
k4="0"
|
||||
result="litImage"
|
||||
/>
|
||||
<feDisplacementMap
|
||||
in="SourceGraphic"
|
||||
in2="softMap"
|
||||
scale="200"
|
||||
xChannelSelector="R"
|
||||
yChannelSelector="G"
|
||||
/>
|
||||
</filter>
|
||||
</svg>
|
||||
);
|
||||
139
components/ui/3d-card-effect.tsx
Normal file
139
components/ui/3d-card-effect.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useContext,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
const MouseEnterContext = createContext<
|
||||
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
|
||||
>(undefined);
|
||||
|
||||
export const CardContainer = ({
|
||||
children,
|
||||
className,
|
||||
containerClassName,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isMouseEntered, setIsMouseEntered] = useState(false);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!containerRef.current) return;
|
||||
const { left, top, width, height } = containerRef.current.getBoundingClientRect();
|
||||
const x = (e.clientX - left - width / 2) / 25;
|
||||
const y = (e.clientY - top - height / 2) / 25;
|
||||
containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`;
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsMouseEntered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!containerRef.current) return;
|
||||
setIsMouseEntered(false);
|
||||
containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`;
|
||||
};
|
||||
|
||||
return (
|
||||
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
|
||||
<div
|
||||
className={cn('flex items-center justify-center', containerClassName)}
|
||||
style={{ perspective: '1000px' }}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={cn(
|
||||
'flex items-center justify-center relative transition-all duration-200 ease-linear w-full',
|
||||
className,
|
||||
)}
|
||||
style={{ transformStyle: 'preserve-3d' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</MouseEnterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardBody = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'[transform-style:preserve-3d] [&>*]:[transform-style:preserve-3d]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type CardItemProps = {
|
||||
as?: React.ElementType;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
translateX?: number | string;
|
||||
translateY?: number | string;
|
||||
translateZ?: number | string;
|
||||
rotateX?: number | string;
|
||||
rotateY?: number | string;
|
||||
rotateZ?: number | string;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export const CardItem = ({
|
||||
as: Tag = 'div',
|
||||
children,
|
||||
className,
|
||||
translateX = 0,
|
||||
translateY = 0,
|
||||
translateZ = 0,
|
||||
rotateX = 0,
|
||||
rotateY = 0,
|
||||
rotateZ = 0,
|
||||
...rest
|
||||
}: CardItemProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [isMouseEntered] = useMouseEnter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
if (isMouseEntered) {
|
||||
ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`;
|
||||
} else {
|
||||
ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`;
|
||||
}
|
||||
}, [isMouseEntered, translateX, translateY, translateZ, rotateX, rotateY, rotateZ]);
|
||||
|
||||
return (
|
||||
<Tag ref={ref} className={cn('transition duration-200 ease-linear', className)} {...rest}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMouseEnter = () => {
|
||||
const context = useContext(MouseEnterContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useMouseEnter must be used within a MouseEnterProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
183
components/ui/sparkles-text.tsx
Normal file
183
components/ui/sparkles-text.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
'use client';
|
||||
|
||||
import { CSSProperties, useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Sparkle {
|
||||
id: string;
|
||||
x: string;
|
||||
y: string;
|
||||
color: string;
|
||||
delay: number;
|
||||
scale: number;
|
||||
lifespan: number;
|
||||
}
|
||||
|
||||
interface SparklesTextProps {
|
||||
className?: string;
|
||||
text: string;
|
||||
sparklesCount?: number;
|
||||
colors?: {
|
||||
first: string;
|
||||
second: string;
|
||||
};
|
||||
}
|
||||
|
||||
const SparklesText: React.FC<SparklesTextProps> = ({
|
||||
text,
|
||||
colors = { first: '#9E7AFF', second: '#FE8BBB' },
|
||||
className,
|
||||
sparklesCount = 10,
|
||||
...props
|
||||
}) => {
|
||||
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generateStar = (): Sparkle => {
|
||||
const starX = `${Math.random() * 100}%`;
|
||||
const starY = `${Math.random() * 100}%`;
|
||||
const color = Math.random() > 0.5 ? colors.first : colors.second;
|
||||
const delay = Math.random() * 2;
|
||||
const scale = Math.random() * 1 + 0.3;
|
||||
const lifespan = Math.random() * 10 + 5;
|
||||
const id = `${starX}-${starY}-${Date.now()}-${Math.random()}`;
|
||||
return { id, x: starX, y: starY, color, delay, scale, lifespan };
|
||||
};
|
||||
|
||||
const initializeStars = () => {
|
||||
const newSparkles = Array.from({ length: sparklesCount }, generateStar);
|
||||
setSparkles(newSparkles);
|
||||
};
|
||||
|
||||
const updateStars = () => {
|
||||
setSparkles((currentSparkles) =>
|
||||
currentSparkles.map((star) => {
|
||||
if (star.lifespan <= 0) {
|
||||
return generateStar();
|
||||
}
|
||||
return { ...star, lifespan: star.lifespan - 0.1 };
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
initializeStars();
|
||||
const interval = setInterval(updateStars, 100);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [colors.first, colors.second, sparklesCount]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('text-6xl font-bold', className)}
|
||||
{...props}
|
||||
style={
|
||||
{
|
||||
'--sparkles-first-color': `${colors.first}`,
|
||||
'--sparkles-second-color': `${colors.second}`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="relative inline-block">
|
||||
{sparkles.map((sparkle) => (
|
||||
<SparkleEl key={sparkle.id} {...sparkle} />
|
||||
))}
|
||||
<strong>{text}</strong>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SparkleEl: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||
return (
|
||||
<motion.svg
|
||||
key={id}
|
||||
className="pointer-events-none absolute z-20"
|
||||
initial={{ opacity: 0, left: x, top: y }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0],
|
||||
scale: [0, scale, 0],
|
||||
rotate: [75, 120, 150],
|
||||
}}
|
||||
transition={{ duration: 0.8, repeat: Infinity, delay }}
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
>
|
||||
<path
|
||||
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
|
||||
fill={color}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
|
||||
interface SparklesOverlayProps {
|
||||
className?: string;
|
||||
sparklesCount?: number;
|
||||
colors?: { first: string; second: string };
|
||||
}
|
||||
|
||||
const SparklesOverlay: React.FC<SparklesOverlayProps> = ({
|
||||
className,
|
||||
sparklesCount = 18,
|
||||
colors = { first: '#9E7AFF', second: '#FE8BBB' },
|
||||
}) => {
|
||||
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generateStar = (): Sparkle => {
|
||||
const starX = `${Math.random() * 100}%`;
|
||||
const starY = `${Math.random() * 100}%`;
|
||||
const color = Math.random() > 0.5 ? colors.first : colors.second;
|
||||
const delay = Math.random() * 4;
|
||||
const scale = Math.random() * 0.9 + 0.4;
|
||||
const lifespan = Math.random() * 14 + 8;
|
||||
const id = `${starX}-${starY}-${Date.now()}-${Math.random()}`;
|
||||
return { id, x: starX, y: starY, color, delay, scale, lifespan };
|
||||
};
|
||||
|
||||
setSparkles(Array.from({ length: sparklesCount }, generateStar));
|
||||
const interval = setInterval(() => {
|
||||
setSparkles((cur) =>
|
||||
cur.map((s) => (s.lifespan <= 0 ? generateStar() : { ...s, lifespan: s.lifespan - 0.1 })),
|
||||
);
|
||||
}, 100);
|
||||
return () => clearInterval(interval);
|
||||
}, [colors.first, colors.second, sparklesCount]);
|
||||
|
||||
return (
|
||||
<div className={cn('pointer-events-none absolute inset-0 overflow-hidden', className)} aria-hidden>
|
||||
{sparkles.map((s) => (
|
||||
<SlowSparkle key={s.id} {...s} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SlowSparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||
return (
|
||||
<motion.svg
|
||||
key={id}
|
||||
className="pointer-events-none absolute z-20"
|
||||
initial={{ opacity: 0, left: x, top: y }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0],
|
||||
scale: [0, scale, 0],
|
||||
rotate: [75, 120, 150],
|
||||
}}
|
||||
transition={{ duration: 2.6, repeat: Infinity, delay, ease: 'easeInOut' }}
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
>
|
||||
<path
|
||||
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
|
||||
fill={color}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
|
||||
export { SparklesText, SparklesOverlay };
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
66
package-lock.json
generated
66
package-lock.json
generated
@@ -15,7 +15,9 @@
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.99.0",
|
||||
"cheerio": "^1.2.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"lunar-javascript": "^1.7.7",
|
||||
"next": "16.1.6",
|
||||
"openai": "^6.21.0",
|
||||
@@ -24,7 +26,8 @@
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"resend": "^6.9.1",
|
||||
"solarlunar": "^2.0.7"
|
||||
"solarlunar": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -3237,6 +3240,15 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -4561,6 +4573,33 @@
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.38.0",
|
||||
"motion-utils": "^12.36.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -7249,6 +7288,21 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.38.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.36.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.36.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -9049,6 +9103,16 @@
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.99.0",
|
||||
"cheerio": "^1.2.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"lunar-javascript": "^1.7.7",
|
||||
"next": "16.1.6",
|
||||
"openai": "^6.21.0",
|
||||
@@ -25,7 +27,8 @@
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"resend": "^6.9.1",
|
||||
"solarlunar": "^2.0.7"
|
||||
"solarlunar": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
Reference in New Issue
Block a user