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:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;">
|
|
||||||
{step.num}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="process-circle-text">
|
||||||
<h3 class="text-xl md:text-2xl font-semibold mb-3" style="color: var(--hds-fg);">
|
<h3 class="text-lg font-semibold mb-1.5" style="color: var(--hds-fg);">
|
||||||
{t(step.titleKey)}
|
{t(step.titleKey)}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-[15px] md:text-base leading-relaxed" style="color: var(--hds-fg-soft);">
|
<p class="text-sm leading-relaxed" style="color: var(--hds-fg-soft);">
|
||||||
{t(step.bodyKey)}
|
{t(step.bodyKey)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
))}
|
||||||
|
</div>
|
||||||
|
</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) {
|
|
||||||
const steps = document.querySelectorAll('.process-step-card');
|
|
||||||
const progressFill = document.getElementById('process-progress-fill');
|
|
||||||
|
|
||||||
if (steps.length && progressFill) {
|
if (!prefersReduced) {
|
||||||
// Animate progress bar as user scrolls through the section
|
// Desktop: draw SVG line on scroll
|
||||||
ScrollTrigger.create({
|
const path = document.querySelector<SVGPathElement>('.process-line-path');
|
||||||
trigger: '.process-steps',
|
if (path) {
|
||||||
start: 'top 70%',
|
const pathLength = path.getTotalLength();
|
||||||
end: 'bottom bottom',
|
path.style.strokeDasharray = String(pathLength);
|
||||||
onUpdate: (self) => {
|
path.style.strokeDashoffset = String(pathLength);
|
||||||
progressFill.style.setProperty('--progress', `${self.progress * 100}%`);
|
|
||||||
|
gsap.to(path, {
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: '.process-desktop',
|
||||||
|
start: 'top 75%',
|
||||||
|
end: 'bottom 60%',
|
||||||
|
scrub: 0.8,
|
||||||
},
|
},
|
||||||
|
strokeDashoffset: 0,
|
||||||
|
ease: 'none',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stagger reveal each step card
|
// Desktop: subtle floating animation on circles
|
||||||
gsap.from('.process-step-card', {
|
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: {
|
scrollTrigger: {
|
||||||
trigger: '.process-steps',
|
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%',
|
start: 'top 75%',
|
||||||
toggleActions: 'play none none none',
|
toggleActions: 'play none none none',
|
||||||
},
|
},
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
x: -30,
|
x: -20,
|
||||||
duration: 0.7,
|
duration: 0.5,
|
||||||
stagger: 0.15,
|
stagger: 0.15,
|
||||||
ease: 'power3.out',
|
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;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-mobile-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
.process-progress-track {
|
|
||||||
width: 2px;
|
.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-mobile-line {
|
||||||
|
transform: scaleY(1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ 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'),
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
slug: 'hosting-correo-web',
|
slug: 'hosting-correo-web',
|
||||||
title: lang === 'en' ? 'Hosting for email & web' : 'Hosting correo y web',
|
title: lang === 'en' ? 'Hosting for email & web' : 'Hosting correo y web',
|
||||||
@@ -39,11 +36,6 @@ const groups = [
|
|||||||
Icon: WordpressIcon,
|
Icon: WordpressIcon,
|
||||||
border: 'var(--color-hds-naranja)',
|
border: 'var(--color-hds-naranja)',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('services.cat.design'),
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
slug: 'desarrollo-webs-institucionales',
|
slug: 'desarrollo-webs-institucionales',
|
||||||
title: lang === 'en' ? 'Institutional websites' : 'Desarrollo webs institucionales',
|
title: lang === 'en' ? 'Institutional websites' : 'Desarrollo webs institucionales',
|
||||||
@@ -71,11 +63,6 @@ const groups = [
|
|||||||
Icon: TranslateIcon,
|
Icon: TranslateIcon,
|
||||||
border: 'var(--color-hds-coral)',
|
border: 'var(--color-hds-coral)',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('services.cat.infra'),
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
slug: 'servidores-linux-virtualizacion',
|
slug: 'servidores-linux-virtualizacion',
|
||||||
title: lang === 'en' ? 'Linux servers & virtualization' : 'Servidores Linux y virtualización',
|
title: lang === 'en' ? 'Linux servers & virtualization' : 'Servidores Linux y virtualización',
|
||||||
@@ -85,8 +72,6 @@ const groups = [
|
|||||||
Icon: ServerIcon,
|
Icon: ServerIcon,
|
||||||
border: 'var(--color-hds-durazno)',
|
border: 'var(--color-hds-durazno)',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
<section id="servicios" class="py-20 md:py-28" style="background: var(--hds-bg);">
|
<section id="servicios" class="py-20 md:py-28" style="background: var(--hds-bg);">
|
||||||
@@ -101,19 +86,9 @@ 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">
|
|
||||||
<h3 class="text-2xl md:text-3xl font-display" style="color: var(--hds-fg);">
|
|
||||||
{group.title}
|
|
||||||
</h3>
|
|
||||||
<span class="text-xs font-semibold tracking-[0.25em] uppercase" style="color: var(--color-hds-naranja);">
|
|
||||||
{group.items.length} {group.items.length === 1 ? 'servicio' : 'servicios'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
|
||||||
{group.items.map((item) => (
|
|
||||||
<ServiceCard
|
<ServiceCard
|
||||||
href={`${base}/servicios/${item.slug}/`}
|
href={`${base}/servicios/${item.slug}/`}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
@@ -121,15 +96,13 @@ const groups = [
|
|||||||
Icon={item.Icon}
|
Icon={item.Icon}
|
||||||
borderColor={item.border}
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user