forked from emre/www_projectmycelium_com
feat: add animated transitions to cloud features tabs
- Implemented slide and fade animations when switching between feature tabs using Framer Motion - Added animated background indicator that follows the selected tab - Enhanced hover states with scale transitions and outline effects for better interactivity
This commit is contained in:
@@ -130,27 +130,111 @@ const features = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
interface CustomAnimationProps {
|
||||||
|
isForwards: boolean
|
||||||
|
changeCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxZIndex = 2147483647
|
||||||
|
|
||||||
|
const bodyVariantBackwards: Variant = {
|
||||||
|
opacity: 0.4,
|
||||||
|
scale: 0.8,
|
||||||
|
zIndex: 0,
|
||||||
|
filter: 'blur(4px)',
|
||||||
|
transition: { duration: 0.4 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyAnimation: MotionProps = {
|
||||||
|
initial: 'initial',
|
||||||
|
animate: 'animate',
|
||||||
|
exit: 'exit',
|
||||||
|
variants: {
|
||||||
|
initial: (custom: CustomAnimationProps) =>
|
||||||
|
custom.isForwards
|
||||||
|
? {
|
||||||
|
y: '100%',
|
||||||
|
zIndex: maxZIndex - custom.changeCount,
|
||||||
|
transition: { duration: 0.4 },
|
||||||
|
}
|
||||||
|
: bodyVariantBackwards,
|
||||||
|
animate: (custom: CustomAnimationProps) => ({
|
||||||
|
y: '0%',
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
zIndex: maxZIndex / 2 - custom.changeCount,
|
||||||
|
filter: 'blur(0px)',
|
||||||
|
transition: { duration: 0.4 },
|
||||||
|
}),
|
||||||
|
exit: (custom: CustomAnimationProps) =>
|
||||||
|
custom.isForwards
|
||||||
|
? bodyVariantBackwards
|
||||||
|
: {
|
||||||
|
y: '100%',
|
||||||
|
zIndex: maxZIndex - custom.changeCount,
|
||||||
|
transition: { duration: 0.4 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePrevious<T>(value: T) {
|
||||||
|
const ref = useRef<T>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return ref.current
|
||||||
|
}
|
||||||
|
|
||||||
/* Desktop Component */
|
/* Desktop Component */
|
||||||
function CloudFeaturesDesktop() {
|
function CloudFeaturesDesktop() {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
let [changeCount, setChangeCount] = useState(0)
|
||||||
|
let [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
let prevIndex = usePrevious(selectedIndex)
|
||||||
|
let isForwards = prevIndex === undefined ? true : selectedIndex > prevIndex
|
||||||
|
|
||||||
|
let onChange = useDebouncedCallback(
|
||||||
|
(selectedIndex: number) => {
|
||||||
|
setSelectedIndex(selectedIndex)
|
||||||
|
setChangeCount((changeCount) => changeCount + 1)
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
{ leading: true },
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabGroup vertical className="grid grid-cols-12 gap-10">
|
<TabGroup
|
||||||
|
vertical
|
||||||
|
className="grid grid-cols-12 items-start gap-10"
|
||||||
|
selectedIndex={selectedIndex}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
<TabList className="col-span-6 space-y-6 pl-4 sm:pl-6 lg:pl-8">
|
<TabList className="col-span-6 space-y-6 pl-4 sm:pl-6 lg:pl-8">
|
||||||
{features.map((feature, i) => (
|
{features.map((feature, featureIndex) => (
|
||||||
<div
|
<div
|
||||||
key={feature.name}
|
key={feature.name}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative rounded-2xl transition-all hover:scale-[1.02] hover:bg-gray-100',
|
'relative rounded-2xl outline-2 transition-all duration-300 ease-in-out hover:scale-105 hover:bg-gray-100',
|
||||||
selectedIndex === i
|
selectedIndex === featureIndex
|
||||||
? 'ring-2 ring-cyan-500 bg-white shadow'
|
? 'outline-cyan-500'
|
||||||
: 'ring-1 ring-transparent',
|
: 'outline-transparent hover:outline-cyan-500',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-8">
|
{featureIndex === selectedIndex && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="activeBackground"
|
||||||
|
className="absolute inset-0 bg-white shadow"
|
||||||
|
initial={{ borderRadius: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative z-10 p-8">
|
||||||
<feature.icon className="h-8 w-8" />
|
<feature.icon className="h-8 w-8" />
|
||||||
<FeatureTitle as="h3" className="mt-6 text-gray-900">
|
<FeatureTitle as="h3" className="mt-6 text-gray-900">
|
||||||
<Tab>{feature.name}</Tab>
|
<Tab className="text-left data-selected:not-data-focus:outline-hidden">
|
||||||
|
<span className="absolute inset-0 rounded-2xl" />
|
||||||
|
{feature.name}
|
||||||
|
</Tab>
|
||||||
</FeatureTitle>
|
</FeatureTitle>
|
||||||
<FeatureDescription className="mt-2 text-gray-600">
|
<FeatureDescription className="mt-2 text-gray-600">
|
||||||
{feature.description}
|
{feature.description}
|
||||||
@@ -160,15 +244,30 @@ function CloudFeaturesDesktop() {
|
|||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<div className="col-span-6">
|
<div className="relative col-span-6">
|
||||||
<TabPanels as={Fragment}>
|
<TabPanels as={Fragment}>
|
||||||
{features.map((feature, i) => (
|
<AnimatePresence
|
||||||
<TabPanel key={feature.name}>
|
initial={false}
|
||||||
<div className="w-full flex justify-center">
|
custom={{ isForwards, changeCount }}
|
||||||
|
>
|
||||||
|
{features.map((feature, featureIndex) =>
|
||||||
|
selectedIndex === featureIndex ? (
|
||||||
|
<TabPanel
|
||||||
|
static
|
||||||
|
key={feature.name + changeCount}
|
||||||
|
className="col-start-1 row-start-1 flex focus:outline-offset-32 data-selected:not-data-focus:outline-hidden"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
{...bodyAnimation}
|
||||||
|
custom={{ isForwards, changeCount }}
|
||||||
|
className="w-full flex justify-center"
|
||||||
|
>
|
||||||
<feature.screen />
|
<feature.screen />
|
||||||
</div>
|
</motion.div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
) : null,
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</div>
|
</div>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user