diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 2c83c0a0..3564e0f2 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -785,7 +785,11 @@ const plans = [
{p.summary}
/ month · billed yearly @@ -1052,10 +1056,34 @@ const plans = [ }); // ── Pricing billing toggle (Monthly ↔ Annual, annual default) ─── + // Thumb still slides with motion.dev. Prices swap with the same + // slide-up rebound animation /pricing uses on its plan numbers. const toggle = document.getElementById('billing-toggle'); const thumb = document.getElementById('toggle-thumb'); if (toggle && thumb) { const btns = toggle.querySelectorAll('button[data-billing]'); + + // Slide-up swap on a single price number. Phase 1 lifts and fades + // the old value out; phase 2 drops the new value in from below with + // a subtle overshoot. + const animatePrice = (el, newValue) => { + const inner = el.querySelector('.plan-price-inner'); + if (!inner) return; + if (inner.textContent === newValue) return; + inner.style.transition = 'transform 220ms cubic-bezier(0.4,0,0.2,1), opacity 220ms cubic-bezier(0.4,0,0.2,1)'; + inner.style.transform = 'translateY(-100%)'; + inner.style.opacity = '0'; + setTimeout(() => { + inner.style.transition = 'none'; + inner.style.transform = 'translateY(100%)'; + inner.textContent = newValue; + inner.offsetHeight; // reflow + inner.style.transition = 'transform 320ms cubic-bezier(0.34,1.56,0.64,1), opacity 220ms ease-out'; + inner.style.transform = 'translateY(0)'; + inner.style.opacity = '1'; + }, 220); + }; + const apply = (mode, animated = true) => { const btn = toggle.querySelector(`button[data-billing="${mode}"]`); if (!btn) return; @@ -1076,17 +1104,27 @@ const plans = [ b.classList.remove('text-white'); } }); - document.querySelectorAll('.price-amount').forEach((el) => { + document.querySelectorAll('.plan-price').forEach((el) => { + const next = el.getAttribute(`data-${mode}`) || ''; + if (!next) return; + if (!animated) { + const inner = el.querySelector('.plan-price-inner'); + if (inner) inner.textContent = next; + return; + } + animatePrice(el, next); + }); + // Cadence text gets a soft fade-swap so it never just snaps. + document.querySelectorAll('.cadence').forEach((el) => { const next = el.getAttribute(`data-${mode}`) || ''; if (!next) return; if (!animated) { el.textContent = next; return; } - animate(el, { opacity: [1, 0] }, { duration: 0.12 }).then(() => { + el.style.transition = 'opacity 180ms ease-out'; + el.style.opacity = '0'; + setTimeout(() => { el.textContent = next; - animate(el, { opacity: [0, 1] }, { duration: 0.18 }); - }); - }); - document.querySelectorAll('.cadence').forEach((el) => { - el.textContent = el.getAttribute(`data-${mode}`) || ''; + el.style.opacity = '1'; + }, 180); }); }; btns.forEach((b) => b.addEventListener('click', () => apply(b.getAttribute('data-billing'))));