feat: page transitions, scroll progress bar, and staggered value cards

This commit is contained in:
Manuel
2026-05-10 15:41:03 +02:00
parent 97c05ff620
commit f1c1f54ef9
3 changed files with 113 additions and 12 deletions
+45 -9
View File
@@ -42,7 +42,11 @@ const {
rel="stylesheet" rel="stylesheet"
/> />
</head> </head>
<body> <body class="page-ready">
<!-- Scroll progress bar -->
<div class="scroll-progress" id="scrollProgress" role="progressbar" aria-label="Progreso de lectura" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<SiteHeader activeNav={activeNav} /> <SiteHeader activeNav={activeNav} />
<main> <main>
<slot /> <slot />
@@ -68,17 +72,49 @@ const {
{ threshold: 0.1, rootMargin: '0px 0px -48px 0px' } { threshold: 0.1, rootMargin: '0px 0px -48px 0px' }
); );
document.querySelectorAll('.reveal, .reveal-left, .reveal-scale, .gold-line, .section-eyebrow').forEach((el) => { document.querySelectorAll('.reveal, .reveal-left, .reveal-scale, .gold-line, .section-eyebrow, .value-card-animate').forEach((el) => {
io.observe(el); io.observe(el);
}); });
}
// ─── HEADER SCROLL ELEVATION ───────────────── // ─── SCROLL PROGRESS ────────────────────────
const header = document.querySelector('.site-header') as HTMLElement | null; const progressBar = document.getElementById('scrollProgress') as HTMLElement | null;
if (header) { if (progressBar) {
window.addEventListener('scroll', () => { const updateProgress = () => {
header.classList.toggle('is-scrolled', window.scrollY > 16); const scrollTop = window.scrollY;
}, { passive: true }); const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
progressBar.style.width = `${progress}%`;
progressBar.setAttribute('aria-valuenow', String(Math.round(progress)));
};
window.addEventListener('scroll', updateProgress, { passive: true });
updateProgress();
}
// ─── PAGE TRANSITIONS ───────────────────────
// Fade IN: page loads → body gets .page-ready → CSS handles fade-in
document.body.classList.add('page-ready');
// Fade OUT: intercept internal navigation links
const handleNavClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const anchor = target.closest('a');
if (!anchor) return;
const href = anchor.getAttribute('href');
if (!href || href.startsWith('#') || href.startsWith('http') || href.startsWith('mailto') || href.startsWith('tel') || anchor.target === '_blank') return;
e.preventDefault();
document.body.classList.remove('page-ready');
setTimeout(() => {
window.location.href = href;
}, 300);
};
document.addEventListener('click', handleNavClick);
} else {
// Reduced motion: show immediately, no transitions
document.body.classList.add('page-ready');
} }
</script> </script>
</body> </body>
+38 -3
View File
@@ -63,17 +63,17 @@ import MainLayout from '../layouts/MainLayout.astro';
<h2 class="text-headline-lg reveal" style="--reveal-delay: 150;">Valores que nos definen</h2> <h2 class="text-headline-lg reveal" style="--reveal-delay: 150;">Valores que nos definen</h2>
</div> </div>
<div class="grid-3"> <div class="grid-3">
<div class="value-card"> <div class="value-card value-card-animate" data-value-delay="0">
<span class="material-symbols-outlined text-primary icon-lg">verified</span> <span class="material-symbols-outlined text-primary icon-lg">verified</span>
<h3 class="text-headline-md">Calidad</h3> <h3 class="text-headline-md">Calidad</h3>
<p class="text-body-sm text-muted leading-relaxed">Innegociable. Desde la frescura del producto hasta el último detalle del servicio de sala.</p> <p class="text-body-sm text-muted leading-relaxed">Innegociable. Desde la frescura del producto hasta el último detalle del servicio de sala.</p>
</div> </div>
<div class="value-card"> <div class="value-card value-card-animate" data-value-delay="150">
<span class="material-symbols-outlined text-primary icon-lg">history_edu</span> <span class="material-symbols-outlined text-primary icon-lg">history_edu</span>
<h3 class="text-headline-md">Tradición</h3> <h3 class="text-headline-md">Tradición</h3>
<p class="text-body-sm text-muted leading-relaxed">Honramos las técnicas clásicas que han forjado la gran cocina, aplicándolas con respeto.</p> <p class="text-body-sm text-muted leading-relaxed">Honramos las técnicas clásicas que han forjado la gran cocina, aplicándolas con respeto.</p>
</div> </div>
<div class="value-card"> <div class="value-card value-card-animate" data-value-delay="300">
<span class="material-symbols-outlined text-primary icon-lg">architecture</span> <span class="material-symbols-outlined text-primary icon-lg">architecture</span>
<h3 class="text-headline-md">Vanguardia</h3> <h3 class="text-headline-md">Vanguardia</h3>
<p class="text-body-sm text-muted leading-relaxed">Innovamos en presentaciones y texturas para sorprender a los paladares contemporáneos.</p> <p class="text-body-sm text-muted leading-relaxed">Innovamos en presentaciones y texturas para sorprender a los paladares contemporáneos.</p>
@@ -147,4 +147,39 @@ import MainLayout from '../layouts/MainLayout.astro';
} }
.quote-card-desktop { display: block !important; } .quote-card-desktop { display: block !important; }
} }
/* ─── VALUE CARD TIMELINE ANIMATION ──────────── */
.value-card-animate {
opacity: 0;
transform: translateY(20px) rotate(var(--card-tilt, 0deg));
transition:
opacity 600ms cubic-bezier(0.22, 1, 0.36, 1),
transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
transition-delay: calc(var(--value-delay, 0) * 1ms);
}
.value-card-animate.visible {
opacity: 1;
transform: translateY(0) rotate(0deg);
}
.value-card-animate:nth-child(1) { --card-tilt: 0deg; }
.value-card-animate:nth-child(2) { --card-tilt: 0deg; }
.value-card-animate:nth-child(3) { --card-tilt: 0deg; }
/* subtle stacking effect */
@media (min-width: 48rem) {
.value-card-animate:nth-child(1) { --card-tilt: -1.5deg; }
.value-card-animate:nth-child(2) { --card-tilt: 0deg; }
.value-card-animate:nth-child(3) { --card-tilt: 1.5deg; }
}
</style> </style>
<script>
// Override IntersectionObserver to also handle value-card timing
const valueCards = document.querySelectorAll('.value-card-animate');
valueCards.forEach((card) => {
const delay = (card as HTMLElement).dataset.valueDelay || '0';
(card as HTMLElement).style.setProperty('--value-delay', delay);
});
</script>
+30
View File
@@ -255,3 +255,33 @@
transition-duration: 0.01ms !important; transition-duration: 0.01ms !important;
} }
} }
/* ─── PAGE TRANSITIONS ────────────────────────── */
body {
opacity: 0;
transition: opacity 400ms cubic-bezier(0.22, 1, 0.36, 1);
}
body.page-ready {
opacity: 1;
}
/* ─── SCROLL PROGRESS ────────────────────────── */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 2px;
width: 0%;
background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-container) 100%);
z-index: 9999;
transition: width 100ms linear;
}
@media (prefers-reduced-motion: reduce) {
.scroll-progress {
display: none;
}
}