redesign(homepage): elegant editorial redesign with scroll animations

- HeroSection: full-viewport split layout with staggered entrance animations,
  editorial typography, tech stack tags, animated scroll indicator
- WorksSection: refined header with section label, scroll-reveal on entry
- ProjectCard: per-card IntersectionObserver reveal, numbered projects,
  enhanced hover states (scale + brightness), improved CTA
- FeatureSection: card grid with border/glass treatment and staggered reveals
- AboutSection: animated timeline progress line, scroll-triggered card entrances
- FeatureColumn: icon container with hover treatment
- globals.css: fadeUp/lineGrow/arrowBounce keyframes, .reveal utility,
  prefers-reduced-motion support, brand-accent color token
- lib/useReveal.ts: IntersectionObserver hook for scroll reveals

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 08:39:54 +02:00
parent b717abb7de
commit 87efea3cc1
9 changed files with 600 additions and 178 deletions
+49 -11
View File
@@ -1,3 +1,6 @@
'use client';
import { useRef, useEffect } from 'react';
import { Project } from "@/lib/projects/types";
import { ProjectCard } from "../ui/ProjectCard";
@@ -7,18 +10,53 @@ type props = {
};
export default function WorksSection({ projects, locale }: props) {
return (
<section id="works" className="w-full flex flex-col items-start justify-start">
<h2 className="text-[clamp(80px,10vw,128px)] font-serif text-brand-brown mt-6 mb-4">Travaux</h2>
const sectionRef = useRef<HTMLElement>(null);
{projects.map((project, index) => (
<ProjectCard
key={project.slug}
project={project}
locale={locale}
isTheLast={index === projects.length - 1}
/>
))}
useEffect(() => {
const section = sectionRef.current;
if (!section) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.querySelectorAll('.reveal').forEach((el, i) => {
setTimeout(() => el.classList.add('visible'), i * 120);
});
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.08 }
);
observer.observe(section);
return () => observer.disconnect();
}, []);
return (
<section id="works" ref={sectionRef} className="w-full flex flex-col items-start justify-start pt-20 md:pt-28">
{/* Section label */}
<div className="reveal flex items-center gap-3 mb-4">
<span className="w-8 h-px bg-brand-brown/40" />
<span className="font-sans text-[11px] uppercase tracking-[0.2em] text-brand-brown/45">Sélection de projets</span>
</div>
<h2 className="reveal font-serif text-[clamp(56px,8vw,112px)] text-brand-brown leading-none mb-12 md:mb-16">
Travaux
</h2>
<div className="w-full">
{projects.map((project, index) => (
<ProjectCard
key={project.slug}
project={project}
locale={locale}
isTheLast={index === projects.length - 1}
index={index}
/>
))}
</div>
</section>
);
}