feat(timeline): خط روی دایره تمام میشود و کارت پایانی زیر آن مینشیند — لبهٔ بالای کارت دقیقاً روی نقطهٔ پایان خط (اندازهگیری DOM برای هر محتوا/اندازه)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react'
|
||||
import { motion, useScroll, useTransform, useInView } from 'framer-motion'
|
||||
import { useSite, type TimelineItem } from '../../api'
|
||||
import { useLang } from '../../i18n/lang'
|
||||
@@ -9,17 +9,17 @@ const ease = [0.23, 1, 0.32, 1] as const
|
||||
|
||||
type Milestone = TimelineItem
|
||||
|
||||
function Item({ item, index, featured = false }: { item: Milestone; index: number; featured?: boolean }) {
|
||||
const ref = useRef<HTMLLIElement>(null)
|
||||
function Item({ item, index, featured = false, onLi }: { item: Milestone; index: number; featured?: boolean; onLi?: (el: HTMLLIElement | null) => void }) {
|
||||
const ref = useRef<HTMLLIElement | null>(null)
|
||||
const active = useInView(ref, { once: true, amount: 0.6 })
|
||||
const onRight = index % 2 === 0
|
||||
|
||||
return (
|
||||
<li ref={ref} className={`relative md:grid md:grid-cols-2 md:gap-16 ${featured ? 'mt-10 md:mt-16' : ''}`}>
|
||||
<li ref={(el) => { ref.current = el; onLi?.(el) }} className={`relative md:grid md:grid-cols-2 md:gap-16 ${featured ? 'mt-10 md:mt-16' : ''}`}>
|
||||
{/* دایرهها: مرکز دایره در موبایل و دسکتاپ دقیقاً روی مرکز خط 2px است */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`absolute right-[-5px] top-1/2 z-10 -translate-y-1/2 h-3 w-3 rounded-full transition-all duration-500 ease-lux md:right-1/2 md:translate-x-1/2 ${
|
||||
className={`absolute right-[-5px] z-10 -translate-y-1/2 h-3 w-3 rounded-full transition-all duration-500 ease-lux md:right-1/2 md:translate-x-1/2 ${featured ? 'top-0' : 'top-1/2'} ${
|
||||
active
|
||||
? 'bg-copper shadow-[0_0_15px_rgba(197,143,92,0.6),0_0_0_6px_rgba(213,135,76,0.15)]'
|
||||
: 'bg-ivory/20'
|
||||
@@ -56,12 +56,34 @@ export function Timeline() {
|
||||
const { timeline } = useSite()
|
||||
const { t } = useLang()
|
||||
const ref = useRef<HTMLOListElement>(null)
|
||||
const lastLiRef = useRef<HTMLLIElement | null>(null)
|
||||
// فاصلهٔ انتهای فهرست تا بالای کارت پایانی — خط دقیقاً روی بالای کارت تمام میشود
|
||||
const [lineBottom, setLineBottom] = useState(0)
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: ref,
|
||||
offset: ['start 70%', 'end 100%'],
|
||||
offset: ['start 70%', 'end 110%'],
|
||||
})
|
||||
const scaleY = useTransform(scrollYProgress, [0, 1], [0, 1])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const measure = () => {
|
||||
const ol = ref.current
|
||||
const li = lastLiRef.current
|
||||
if (!ol || !li) return
|
||||
const gap = ol.getBoundingClientRect().bottom - li.getBoundingClientRect().top
|
||||
setLineBottom(Math.max(0, Math.round(gap)))
|
||||
}
|
||||
measure()
|
||||
const observer = new ResizeObserver(measure)
|
||||
if (ref.current) observer.observe(ref.current)
|
||||
if (lastLiRef.current) observer.observe(lastLiRef.current)
|
||||
window.addEventListener('resize', measure)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
window.removeEventListener('resize', measure)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className="bg-char py-24 md:py-32" aria-labelledby="timeline-title">
|
||||
<div className="mx-auto w-full max-w-site px-6 lg:px-10">
|
||||
@@ -82,15 +104,16 @@ export function Timeline() {
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-y-0 right-0 w-[2px] bg-ivory/10 md:right-1/2 md:translate-x-1/2"
|
||||
style={{ bottom: lineBottom }}
|
||||
/>
|
||||
{/* خط متحرک - عرض 2px و همراستایی دقیق با مرکز */}
|
||||
<motion.span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-y-0 right-0 w-[2px] origin-top bg-gradient-to-b from-copper/80 via-copper to-copper/55 shadow-[0_0_10px_rgba(197,143,92,0.5)] md:right-1/2 md:translate-x-1/2"
|
||||
style={{ scaleY }}
|
||||
style={{ scaleY, bottom: lineBottom }}
|
||||
/>
|
||||
{timeline.map((item, i) => (
|
||||
<Item key={item.id} item={item} index={i} featured={i === timeline.length - 1} />
|
||||
<Item key={item.id} item={item} index={i} featured={i === timeline.length - 1} onLi={i === timeline.length - 1 ? (el) => { lastLiRef.current = el } : undefined} />
|
||||
))}
|
||||
</ol>
|
||||
<div className="mt-12">
|
||||
|
||||
Reference in New Issue
Block a user