feat: rediseño 3 secciones

DifferenceCards: fix align-items:start + clearProps GSAP
ServicesSection: aplanar grupos, grid 2×3 sin subtítulos de categoría
ServiceCard: 3D tilt on hover (rotateX/Y), translateZ icon+cta, sombra dinámica
ProcessSteps: círculos horizontales asimétricos + SVG path orgánico que se dibuja al scroll, flotación sutil, mobile vertical
This commit is contained in:
Mauricio López Coria
2026-07-02 01:55:07 -03:00
parent 9ef05f8eea
commit 07165ee94e
4 changed files with 396 additions and 176 deletions
+2
View File
@@ -76,6 +76,7 @@ const items = [
duration: 0.7, duration: 0.7,
stagger: 0.12, stagger: 0.12,
ease: 'power3.out', ease: 'power3.out',
clearProps: 'transform,opacity',
}); });
} }
</script> </script>
@@ -91,6 +92,7 @@ const items = [
.diff-grid { .diff-grid {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 1.5rem; gap: 1.5rem;
align-items: start;
} }
} }
+220 -85
View File
@@ -1,12 +1,7 @@
--- ---
// "Cómo trabajamos" — Timeline with GSAP ScrollTrigger progress bar // "Cómo trabajamos" — Horizontal asymmetric circles with organic SVG connecting line
// Phase 4 — Organic Fluidity Premium
import SectionEyebrow from './SectionEyebrow.astro'; import SectionEyebrow from './SectionEyebrow.astro';
import type { TranslationKey } from '@/i18n/ui'; import type { TranslationKey } from '@/i18n/ui';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
interface Props { interface Props {
t: (key: TranslationKey) => string; t: (key: TranslationKey) => string;
@@ -14,10 +9,10 @@ interface Props {
const { t } = Astro.props; const { t } = Astro.props;
const steps = [ const steps = [
{ num: '01', titleKey: 'process.step1.title' as TranslationKey, bodyKey: 'process.step1.body' as TranslationKey }, { num: '01', titleKey: 'process.step1.title' as TranslationKey, bodyKey: 'process.step1.body' as TranslationKey, yPct: 35 },
{ num: '02', titleKey: 'process.step2.title' as TranslationKey, bodyKey: 'process.step2.body' as TranslationKey }, { num: '02', titleKey: 'process.step2.title' as TranslationKey, bodyKey: 'process.step2.body' as TranslationKey, yPct: 58 },
{ num: '03', titleKey: 'process.step3.title' as TranslationKey, bodyKey: 'process.step3.body' as TranslationKey }, { num: '03', titleKey: 'process.step3.title' as TranslationKey, bodyKey: 'process.step3.body' as TranslationKey, yPct: 28 },
{ num: '04', titleKey: 'process.step4.title' as TranslationKey, bodyKey: 'process.step4.body' as TranslationKey }, { num: '04', titleKey: 'process.step4.title' as TranslationKey, bodyKey: 'process.step4.body' as TranslationKey, yPct: 50 },
]; ];
--- ---
<section class="py-24 md:py-32 relative overflow-hidden" style="background: var(--hds-bg-soft);" id="como-trabajamos"> <section class="py-24 md:py-32 relative overflow-hidden" style="background: var(--hds-bg-soft);" id="como-trabajamos">
@@ -31,31 +26,61 @@ const steps = [
</h2> </h2>
</div> </div>
<!-- Timeline with progress bar --> <!-- Desktop: horizontal asymmetric circles -->
<div class="process-timeline"> <div class="process-desktop hidden md:block">
<!-- Vertical progress bar track --> <svg class="process-line-svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
<div class="process-progress-track" aria-hidden="true"> <path
<div id="process-progress-fill" class="process-progress-fill"></div> class="process-line-path"
</div> d="M 12.5 35 C 22 35, 28 58, 37.5 58 C 47 58, 53 28, 62.5 28 C 72 28, 78 50, 87.5 50"
fill="none"
stroke="var(--color-hds-naranja)"
stroke-width="2"
stroke-linecap="round"
vector-effect="non-scaling-stroke"
stroke-dasharray="200"
stroke-dashoffset="200"
opacity="0.5"
/>
</svg>
<div class="process-steps"> <div class="process-circles-row">
{steps.map((step, i) => ( {steps.map((step, i) => (
<div id={`process-step-${i}`} class="process-step-wrap"> <div class="process-circle-col" style={`top: ${step.yPct}%;`}>
<article class="glass-card process-step-card p-8 md:p-10"> <div class="process-circle glass-card" data-step-index={i}>
<div class="flex items-start gap-6"> <span class="font-display process-circle-num">{step.num}</span>
<div class="font-display leading-none flex-shrink-0" style="font-size: 3.5rem; color: var(--color-hds-naranja); font-weight: 500; opacity: 0.9;"> </div>
{step.num} <div class="process-circle-text">
</div> <h3 class="text-lg font-semibold mb-1.5" style="color: var(--hds-fg);">
<div> {t(step.titleKey)}
<h3 class="text-xl md:text-2xl font-semibold mb-3" style="color: var(--hds-fg);"> </h3>
{t(step.titleKey)} <p class="text-sm leading-relaxed" style="color: var(--hds-fg-soft);">
</h3> {t(step.bodyKey)}
<p class="text-[15px] md:text-base leading-relaxed" style="color: var(--hds-fg-soft);"> </p>
{t(step.bodyKey)} </div>
</p> </div>
</div> ))}
</div> </div>
</article> </div>
<!-- Mobile: vertical layout with connecting line -->
<div class="process-mobile md:hidden">
<div class="process-mobile-track" aria-hidden="true">
<div class="process-mobile-line" id="process-mobile-line"></div>
</div>
<div class="process-mobile-steps">
{steps.map((step, i) => (
<div class="process-mobile-row" data-step-index={i}>
<div class="process-circle process-circle-mobile glass-card">
<span class="font-display process-circle-num">{step.num}</span>
</div>
<div class="flex-1">
<h3 class="text-base font-semibold mb-1" style="color: var(--hds-fg);">
{t(step.titleKey)}
</h3>
<p class="text-sm leading-relaxed" style="color: var(--hds-fg-soft);">
{t(step.bodyKey)}
</p>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -70,83 +95,193 @@ const steps = [
gsap.registerPlugin(ScrollTrigger); gsap.registerPlugin(ScrollTrigger);
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReduced) { if (!prefersReduced) {
const steps = document.querySelectorAll('.process-step-card'); // Desktop: draw SVG line on scroll
const progressFill = document.getElementById('process-progress-fill'); const path = document.querySelector<SVGPathElement>('.process-line-path');
if (path) {
const pathLength = path.getTotalLength();
path.style.strokeDasharray = String(pathLength);
path.style.strokeDashoffset = String(pathLength);
if (steps.length && progressFill) { gsap.to(path, {
// Animate progress bar as user scrolls through the section
ScrollTrigger.create({
trigger: '.process-steps',
start: 'top 70%',
end: 'bottom bottom',
onUpdate: (self) => {
progressFill.style.setProperty('--progress', `${self.progress * 100}%`);
},
});
// Stagger reveal each step card
gsap.from('.process-step-card', {
scrollTrigger: { scrollTrigger: {
trigger: '.process-steps', trigger: '.process-desktop',
start: 'top 75%', start: 'top 75%',
toggleActions: 'play none none none', end: 'bottom 60%',
scrub: 0.8,
}, },
opacity: 0, strokeDashoffset: 0,
x: -30, ease: 'none',
duration: 0.7,
stagger: 0.15,
ease: 'power3.out',
}); });
} }
// Desktop: scale-in circles with stagger
gsap.from('.process-circle-col', {
scrollTrigger: {
trigger: '.process-desktop',
start: 'top 70%',
toggleActions: 'play none none none',
},
opacity: 0,
scale: 0.6,
duration: 0.6,
stagger: 0.18,
ease: 'back.out(1.7)',
clearProps: 'transform,opacity',
});
// Desktop: subtle floating animation on circles
document.querySelectorAll<HTMLElement>('.process-circle-col').forEach((el, i) => {
gsap.to(el, {
y: '+=12',
duration: 2.5 + i * 0.4,
repeat: -1,
yoyo: true,
ease: 'sine.inOut',
delay: i * 0.3,
});
});
// Mobile: line fill on scroll
const mobileLine = document.getElementById('process-mobile-line');
if (mobileLine) {
gsap.to(mobileLine, {
scrollTrigger: {
trigger: '.process-mobile-steps',
start: 'top 75%',
end: 'bottom 60%',
scrub: 0.8,
},
scaleY: 1,
ease: 'none',
});
}
// Mobile: fade-in rows
gsap.from('.process-mobile-row', {
scrollTrigger: {
trigger: '.process-mobile-steps',
start: 'top 75%',
toggleActions: 'play none none none',
},
opacity: 0,
x: -20,
duration: 0.5,
stagger: 0.15,
ease: 'power2.out',
clearProps: 'transform,opacity',
});
} }
</script> </script>
<style> <style>
.process-timeline { /* Desktop layout */
display: flex; .process-desktop {
gap: 2rem;
max-width: 48rem;
margin: 0 auto;
}
.process-progress-track {
flex-shrink: 0;
width: 3px;
border-radius: 3px;
background: var(--hds-line);
position: relative; position: relative;
margin-top: 0; height: 420px;
} }
.process-progress-fill { .process-line-svg {
position: sticky; position: absolute;
top: 0; inset: 0;
width: 100%; width: 100%;
height: 40vh; height: 100%;
background: var(--color-hds-naranja); z-index: 0;
border-radius: inherit;
clip-path: inset(0 0 calc(100% - var(--progress, 0%)) 0);
transition: clip-path 0.1s linear;
} }
.process-steps { .process-circles-row {
position: relative;
display: grid;
grid-template-columns: repeat(4, 1fr);
height: 100%;
z-index: 1;
}
.process-circle-col {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; align-items: center;
flex-grow: 1; padding: 0 0.75rem;
} }
.process-step-card { .process-circle {
display: flex;
align-items: center;
justify-content: center;
width: 76px;
height: 76px;
border-radius: 50%;
flex-shrink: 0;
}
.process-circle-num {
font-size: 1.6rem;
font-weight: 500;
color: var(--color-hds-naranja);
line-height: 1;
}
.process-circle-text {
margin-top: 1.25rem;
text-align: center;
max-width: 200px;
}
/* Mobile layout */
.process-mobile {
position: relative;
padding-left: 2.5rem;
}
.process-mobile-track {
position: absolute;
left: 37px;
top: 38px;
bottom: 38px;
width: 2px;
background: var(--hds-line);
border-radius: 2px;
overflow: hidden;
}
.process-mobile-line {
width: 100%; width: 100%;
height: 100%;
background: var(--color-hds-naranja);
transform: scaleY(0);
transform-origin: top;
} }
@media (max-width: 767px) { .process-mobile-steps {
.process-timeline { display: flex;
gap: 1rem; flex-direction: column;
gap: 2rem;
}
.process-mobile-row {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.process-circle-mobile {
width: 56px;
height: 56px;
flex-shrink: 0;
}
.process-circle-mobile .process-circle-num {
font-size: 1.2rem;
}
@media (prefers-reduced-motion: reduce) {
.process-line-path {
stroke-dashoffset: 0 !important;
} }
.process-progress-track { .process-mobile-line {
width: 2px; transform: scaleY(1) !important;
} }
} }
</style> </style>
+38 -4
View File
@@ -13,10 +13,10 @@ const { href, title, description, Icon, borderColor = 'var(--color-hds-naranja)'
--- ---
<a <a
href={href} href={href}
class="glass-card group flex flex-col h-full p-6 md:p-7" class="glass-card service-tilt-card group flex flex-col h-full p-6 md:p-7"
style={`border-top: 3px solid ${borderColor};`} style={`border-top: 3px solid ${borderColor}; transform-style: preserve-3d;`}
> >
<div class="icon-circle mb-5" style="width: 48px; height: 48px;"> <div class="service-icon-3d mb-5" style="width: 52px; height: 52px;">
<Icon class="w-6 h-6" /> <Icon class="w-6 h-6" />
</div> </div>
<h3 class="text-xl font-semibold mb-2 transition-colors" style="color: var(--hds-fg);"> <h3 class="text-xl font-semibold mb-2 transition-colors" style="color: var(--hds-fg);">
@@ -25,7 +25,7 @@ const { href, title, description, Icon, borderColor = 'var(--color-hds-naranja)'
<p class="text-sm leading-relaxed mb-4 flex-grow" style="color: var(--hds-fg-soft);"> <p class="text-sm leading-relaxed mb-4 flex-grow" style="color: var(--hds-fg-soft);">
{description} {description}
</p> </p>
<span class="btn-ghost mt-auto"> <span class="btn-ghost mt-auto service-cta-3d">
Ver detalles Ver detalles
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="5" y1="12" x2="19" y2="12"/> <line x1="5" y1="12" x2="19" y2="12"/>
@@ -33,3 +33,37 @@ const { href, title, description, Icon, borderColor = 'var(--color-hds-naranja)'
</svg> </svg>
</span> </span>
</a> </a>
<style>
.service-icon-3d {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid var(--glass-border);
color: var(--color-hds-naranja);
transform: translateZ(0);
transition: transform 0.4s var(--ease-spring, cubic-bezier(0.16, 1.2, 0.3, 1));
}
.group:hover .service-icon-3d {
transform: translateZ(40px);
}
.service-cta-3d {
transform: translateZ(0);
transition: transform 0.4s var(--ease-spring, cubic-bezier(0.16, 1.2, 0.3, 1));
}
.group:hover .service-cta-3d {
transform: translateZ(25px);
}
@media (prefers-reduced-motion: reduce) {
.service-icon-3d,
.service-cta-3d {
transform: none !important;
transition: none !important;
}
}
</style>
+136 -87
View File
@@ -17,75 +17,60 @@ interface Props {
const { t, lang } = Astro.props; const { t, lang } = Astro.props;
const base = lang === 'en' ? '/en' : ''; const base = lang === 'en' ? '/en' : '';
const groups = [ const services = [
{ {
title: t('services.cat.hosting'), slug: 'hosting-correo-web',
items: [ title: lang === 'en' ? 'Hosting for email & web' : 'Hosting correo y web',
{ desc: lang === 'en'
slug: 'hosting-correo-web', ? 'Optimized hosting with cPanel for WordPress, PHP and MySQL.'
title: lang === 'en' ? 'Hosting for email & web' : 'Hosting correo y web', : 'Alojamiento optimizado con cPanel para WordPress, PHP y MySQL.',
desc: lang === 'en' Icon: MailIcon,
? 'Optimized hosting with cPanel for WordPress, PHP and MySQL.' border: 'var(--color-hds-naranja)',
: 'Alojamiento optimizado con cPanel para WordPress, PHP y MySQL.',
Icon: MailIcon,
border: 'var(--color-hds-naranja)',
},
{
slug: 'mantenimiento-wordpress',
title: lang === 'en' ? 'WordPress maintenance' : 'Mantenimiento WordPress',
desc: lang === 'en'
? 'Security patches and updates by official WordPress ecosystem experts.'
: 'Parches de seguridad y actualizaciones por expertos del ecosistema WP.',
Icon: WordpressIcon,
border: 'var(--color-hds-naranja)',
},
],
}, },
{ {
title: t('services.cat.design'), slug: 'mantenimiento-wordpress',
items: [ title: lang === 'en' ? 'WordPress maintenance' : 'Mantenimiento WordPress',
{ desc: lang === 'en'
slug: 'desarrollo-webs-institucionales', ? 'Security patches and updates by official WordPress ecosystem experts.'
title: lang === 'en' ? 'Institutional websites' : 'Desarrollo webs institucionales', : 'Parches de seguridad y actualizaciones por expertos del ecosistema WP.',
desc: lang === 'en' Icon: WordpressIcon,
? 'Corporate sites tailored to your business and identity.' border: 'var(--color-hds-naranja)',
: 'Sitios corporativos a medida de tu negocio e identidad.',
Icon: CodeIcon,
border: 'var(--color-hds-coral)',
},
{
slug: 'e-commerce',
title: lang === 'en' ? 'E-commerce websites' : 'Sitios web con e-commerce',
desc: lang === 'en'
? 'WooCommerce stores with secure payments and inventory.'
: 'Tiendas WooCommerce con pagos seguros y gestión de stock.',
Icon: ShopIcon,
border: 'var(--color-hds-coral)',
},
{
slug: 'gestion-contenido-traducciones',
title: lang === 'en' ? 'Content & translations' : 'Gestión de contenido y traducciones',
desc: lang === 'en'
? 'Recurring editorial: copy, updates, blog and translations.'
: 'Servicio editorial recurrente: redacción, blog, traducciones.',
Icon: TranslateIcon,
border: 'var(--color-hds-coral)',
},
],
}, },
{ {
title: t('services.cat.infra'), slug: 'desarrollo-webs-institucionales',
items: [ title: lang === 'en' ? 'Institutional websites' : 'Desarrollo webs institucionales',
{ desc: lang === 'en'
slug: 'servidores-linux-virtualizacion', ? 'Corporate sites tailored to your business and identity.'
title: lang === 'en' ? 'Linux servers & virtualization' : 'Servidores Linux y virtualización', : 'Sitios corporativos a medida de tu negocio e identidad.',
desc: lang === 'en' Icon: CodeIcon,
? 'Linux servers, KVM virtualization, private clouds.' border: 'var(--color-hds-coral)',
: 'Servidores Linux, virtualización KVM, nubes privadas.', },
Icon: ServerIcon, {
border: 'var(--color-hds-durazno)', slug: 'e-commerce',
}, title: lang === 'en' ? 'E-commerce websites' : 'Sitios web con e-commerce',
], desc: lang === 'en'
? 'WooCommerce stores with secure payments and inventory.'
: 'Tiendas WooCommerce con pagos seguros y gestión de stock.',
Icon: ShopIcon,
border: 'var(--color-hds-coral)',
},
{
slug: 'gestion-contenido-traducciones',
title: lang === 'en' ? 'Content & translations' : 'Gestión de contenido y traducciones',
desc: lang === 'en'
? 'Recurring editorial: copy, updates, blog and translations.'
: 'Servicio editorial recurrente: redacción, blog, traducciones.',
Icon: TranslateIcon,
border: 'var(--color-hds-coral)',
},
{
slug: 'servidores-linux-virtualizacion',
title: lang === 'en' ? 'Linux servers & virtualization' : 'Servidores Linux y virtualización',
desc: lang === 'en'
? 'Linux servers, KVM virtualization, private clouds.'
: 'Servidores Linux, virtualización KVM, nubes privadas.',
Icon: ServerIcon,
border: 'var(--color-hds-durazno)',
}, },
]; ];
--- ---
@@ -101,35 +86,23 @@ const groups = [
</p> </p>
</div> </div>
<div class="space-y-14"> <div class="services-grid">
{groups.map((group) => ( {services.map((item, i) => (
<div> <div class="service-tilt-wrap" data-service-index={i}>
<div class="flex items-baseline gap-4 mb-6"> <ServiceCard
<h3 class="text-2xl md:text-3xl font-display" style="color: var(--hds-fg);"> href={`${base}/servicios/${item.slug}/`}
{group.title} title={item.title}
</h3> description={item.desc}
<span class="text-xs font-semibold tracking-[0.25em] uppercase" style="color: var(--color-hds-naranja);"> Icon={item.Icon}
{group.items.length} {group.items.length === 1 ? 'servicio' : 'servicios'} borderColor={item.border}
</span> />
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
{group.items.map((item) => (
<ServiceCard
href={`${base}/servicios/${item.slug}/`}
title={item.title}
description={item.desc}
Icon={item.Icon}
borderColor={item.border}
/>
))}
</div>
</div> </div>
))} ))}
</div> </div>
<div class="text-center mt-14"> <div class="text-center mt-14">
<a href={`${base}/servicios/`} class="btn-secondary"> <a href={`${base}/servicios/`} class="btn-secondary">
Ver todos los servicios {lang === 'en' ? 'View all services' : 'Ver todos los servicios'}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="5" y1="12" x2="19" y2="12"/> <line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12 5 19 12 12 19"/> <polyline points="12 5 19 12 12 19"/>
@@ -138,3 +111,79 @@ const groups = [
</div> </div>
</div> </div>
</section> </section>
<script>
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const canHover = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
if (!prefersReduced) {
// Stagger scroll reveal
gsap.from('.service-tilt-wrap', {
scrollTrigger: {
trigger: '.services-grid',
start: 'top 80%',
toggleActions: 'play none none none',
},
opacity: 0,
y: 50,
duration: 0.7,
stagger: 0.1,
ease: 'power3.out',
clearProps: 'transform,opacity',
});
// 3D tilt on hover
if (canHover) {
document.querySelectorAll<HTMLElement>('.service-tilt-wrap').forEach((wrap) => {
const card = wrap.querySelector<HTMLElement>('.service-tilt-card');
if (!card) return;
const rotateXTo = gsap.quickTo(card, 'rotationX', { duration: 0.4, ease: 'power2.out' });
const rotateYTo = gsap.quickTo(card, 'rotationY', { duration: 0.4, ease: 'power2.out' });
wrap.addEventListener('mousemove', (e) => {
const rect = wrap.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
rotateYTo(x * 12);
rotateXTo(-y * 12);
});
wrap.addEventListener('mouseleave', () => {
rotateXTo(0);
rotateYTo(0);
});
});
}
}
</script>
<style>
.services-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
align-items: start;
}
@media (min-width: 640px) {
.services-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.services-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.service-tilt-wrap {
perspective: 1000px;
}
</style>