feat(equipa): o cartão de detalhe nasce da foto, estica e segue o rato
O cartão abre por cima da própria foto (mesmo sítio, tamanho e inclinação), endireita-se e estica para o lado — para a esquerda nas fotos da ponta direita — com o texto a entrar por trás. Aberto, inclina-se em 3D e desliza uns píxeis na direção do rato; ao sair volta a dobrar-se para dentro da foto. No telemóvel voa da foto para o centro (FLIP) e volta ao fechar. - clip-path em px calculados no JS: o Chrome não interpola inset() com calc(100% - …) e o recorte saltava em vez de esticar. - O cartão é posicionado sem transição antes de abrir; senão a transição do estado fechado arrancava primeiro e a abertura partia de valores antigos. - Tira o «Saber mais» do cartão da equipa. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1614bf327b
commit
90ee2a93bc
3 files changed
+237
-84
No files matched your search
+134
-35
@@ -224,36 +224,105 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
|
||||
|
||||
/* ── Equipa: horizontal detail card ─────────────────────────────────────────
|
||||
Desktop (fine pointer) opens it on hover purely in CSS; this handles tap /
|
||||
click / keyboard (.is-open, one at a time; Esc, the × and outside clicks
|
||||
close) and nudges the absolutely-positioned card sideways (--td-shift) so an
|
||||
edge column's card stays inside the viewport. On small / touch screens the
|
||||
CSS turns it into a centred fixed card, so no shift is applied there. */
|
||||
/* ── Equipa: detail card that grows out of the tile ─────────────────────────
|
||||
Desktop: hover (or keyboard focus) opens. The card is laid exactly over the
|
||||
tile photo, tilted like it, and the CSS unclips/straightens it sideways —
|
||||
to the right, or to the left (.td-flip) when there is no room. While open,
|
||||
the mouse drives a small 3D tilt + drift so it handles like a real card.
|
||||
Touch / narrow screens: tap opens a centred fixed card; a FLIP animation
|
||||
makes it fly out of (and back into) the tile photo. Esc, ×, outside tap close. */
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var items = document.querySelectorAll('.trainer-item.has-detail');
|
||||
if (!items.length) return;
|
||||
var floating = window.matchMedia('(max-width: 767px), (hover: none)');
|
||||
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
var section = document.getElementById('equipa');
|
||||
var CARD_W = 560, GUTTER = 16;
|
||||
|
||||
function tileRot(item) {
|
||||
// #powerRotation tilts odd tiles -3deg and even ones +3deg (style.css).
|
||||
var col = item.parentElement, i = 0;
|
||||
while (col && (col = col.previousElementSibling)) i++;
|
||||
return (i % 2 === 1) ? 3 : -3;
|
||||
}
|
||||
|
||||
// Desktop placement: photo column on top of the tile photo.
|
||||
function place(item) {
|
||||
var card = item.querySelector('.trainer-detail');
|
||||
if (!card) return;
|
||||
card.style.setProperty('--td-shift', '0px');
|
||||
if (floating.matches) return;
|
||||
var r = card.getBoundingClientRect();
|
||||
var itemR = item.getBoundingClientRect();
|
||||
// Where the card would sit centred on the item (ignoring its current transform).
|
||||
var left = itemR.left + itemR.width / 2 - r.width / 2;
|
||||
var right = left + r.width;
|
||||
var gutter = 16, vw = document.documentElement.clientWidth, shift = 0;
|
||||
if (left < gutter) shift = gutter - left;
|
||||
else if (right > vw - gutter) shift = (vw - gutter) - right;
|
||||
card.style.setProperty('--td-shift', Math.round(shift) + 'px');
|
||||
var pic = item.querySelector('.trainer-pic');
|
||||
if (!card || !pic || floating.matches) return;
|
||||
var vw = document.documentElement.clientWidth;
|
||||
var w = Math.min(CARD_W, vw - GUTTER * 2);
|
||||
var pw = pic.offsetWidth, ph = pic.offsetHeight;
|
||||
var picLeft = item.getBoundingClientRect().left + pic.offsetLeft; // untransformed
|
||||
var flip = picLeft + w > vw - GUTTER; // no room on the right
|
||||
var x = flip ? pic.offsetLeft + pw - w : pic.offsetLeft;
|
||||
card.classList.toggle('td-flip', flip);
|
||||
card.style.setProperty('--td-w', w + 'px');
|
||||
card.style.setProperty('--td-pw', pw + 'px');
|
||||
card.style.setProperty('--td-ph', ph + 'px');
|
||||
card.style.setProperty('--td-x', x + 'px');
|
||||
card.style.setProperty('--td-y', pic.offsetTop + 'px');
|
||||
card.style.setProperty('--td-rot', tileRot(item) + 'deg');
|
||||
// Closed clip = just the photo square, in px (see style.css).
|
||||
card.style.setProperty('--td-cx', Math.max(0, w - pw) + 'px');
|
||||
card.style.setProperty('--td-cy', Math.max(0, card.offsetHeight - ph) + 'px');
|
||||
}
|
||||
var section = document.getElementById('equipa');
|
||||
|
||||
function resetTilt(card) {
|
||||
['--td-rx', '--td-ry', '--td-mx', '--td-my'].forEach(function (p) { card.style.removeProperty(p); });
|
||||
card.classList.remove('td-tracking');
|
||||
}
|
||||
|
||||
// Touch: fly the fixed card out of / back into the tile photo.
|
||||
function flip(item, opening) {
|
||||
var card = item.querySelector('.trainer-detail');
|
||||
var pic = item.querySelector('.trainer-pic');
|
||||
if (!card || !pic || !card.animate || reduced.matches) return null;
|
||||
var p = pic.getBoundingClientRect(), c = card.getBoundingClientRect();
|
||||
if (!c.width || !c.height) return null;
|
||||
var from = 'translate(' + (p.left - c.left) + 'px,' + (p.top - c.top) + 'px) scale(' +
|
||||
(p.width / c.width) + ',' + (p.height / c.height) + ') rotate(' + tileRot(item) + 'deg)';
|
||||
var frames = [{ transform: from, opacity: .4 }, { transform: 'none', opacity: 1 }];
|
||||
return card.animate(opening ? frames : frames.reverse(), {
|
||||
duration: opening ? 420 : 280,
|
||||
easing: opening ? 'cubic-bezier(.2,.9,.25,1)' : 'cubic-bezier(.4,0,1,1)'
|
||||
});
|
||||
}
|
||||
|
||||
function setOpen(item, open) {
|
||||
item.classList.toggle('is-open', open);
|
||||
var card = item.querySelector('.trainer-detail');
|
||||
var wasOpen = item.classList.contains('is-open');
|
||||
if (open === wasOpen) return;
|
||||
clearTimeout(item._tdTrack);
|
||||
if (open) {
|
||||
if (floating.matches) {
|
||||
item.classList.add('is-open');
|
||||
card.style.setProperty('--td-top', Math.max(24, (window.innerHeight - card.offsetHeight) / 2) + 'px');
|
||||
flip(item, true);
|
||||
} else {
|
||||
// Jump (no transition) to the closed pose on top of the tile, commit
|
||||
// it, then open — otherwise the closed state's own transition would
|
||||
// start moving from the previous pose and the opening would begin
|
||||
// from that half-way value instead of from the photo.
|
||||
card.style.transition = 'none';
|
||||
place(item);
|
||||
void card.offsetWidth;
|
||||
card.style.transition = '';
|
||||
void card.offsetWidth;
|
||||
item.classList.add('is-open');
|
||||
item._tdTrack = setTimeout(function () { card.classList.add('td-tracking'); }, 600);
|
||||
}
|
||||
} else {
|
||||
resetTilt(card);
|
||||
var anim = floating.matches ? flip(item, false) : null;
|
||||
if (anim) anim.onfinish = function () { item.classList.remove('is-open'); syncSection(); };
|
||||
else item.classList.remove('is-open');
|
||||
}
|
||||
item.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
syncSection();
|
||||
}
|
||||
function syncSection() {
|
||||
if (section) section.classList.toggle('has-open-detail', !!section.querySelector('.trainer-item.is-open'));
|
||||
}
|
||||
function closeAll(except) {
|
||||
@@ -261,35 +330,65 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
|
||||
items.forEach(function (item) {
|
||||
item.addEventListener('mouseenter', function () { place(item); });
|
||||
item.addEventListener('focus', function () { place(item); });
|
||||
var card = item.querySelector('.trainer-detail');
|
||||
|
||||
// Desktop hover: short intent delay so sweeping across the grid
|
||||
// doesn't fire every card.
|
||||
item.addEventListener('mouseenter', function () {
|
||||
if (floating.matches) return;
|
||||
clearTimeout(item._tdHover);
|
||||
item._tdHover = setTimeout(function () { closeAll(item); setOpen(item, true); }, 90);
|
||||
});
|
||||
item.addEventListener('mouseleave', function () {
|
||||
if (floating.matches) return;
|
||||
clearTimeout(item._tdHover);
|
||||
setOpen(item, false);
|
||||
});
|
||||
|
||||
// Tilt + drift toward the pointer while open (desktop only).
|
||||
item.addEventListener('mousemove', function (e) {
|
||||
if (floating.matches || reduced.matches || !item.classList.contains('is-open')) return;
|
||||
var r = card.getBoundingClientRect();
|
||||
var dx = Math.max(-1, Math.min(1, (e.clientX - (r.left + r.width / 2)) / (r.width / 2)));
|
||||
var dy = Math.max(-1, Math.min(1, (e.clientY - (r.top + r.height / 2)) / (r.height / 2)));
|
||||
card.style.setProperty('--td-ry', (dx * 5).toFixed(2) + 'deg');
|
||||
card.style.setProperty('--td-rx', (-dy * 5).toFixed(2) + 'deg');
|
||||
card.style.setProperty('--td-mx', (dx * 6).toFixed(1) + 'px');
|
||||
card.style.setProperty('--td-my', (dy * 6).toFixed(1) + 'px');
|
||||
});
|
||||
|
||||
item.addEventListener('focus', function () {
|
||||
if (!floating.matches) { closeAll(item); setOpen(item, true); }
|
||||
});
|
||||
item.addEventListener('blur', function (e) {
|
||||
if (!floating.matches && !item.contains(e.relatedTarget)) setOpen(item, false);
|
||||
});
|
||||
|
||||
// Tap / click (touch, or a desktop click on a closed tile).
|
||||
item.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (e.target.closest('.td-close')) { setOpen(item, false); return; }
|
||||
// Clicks inside an open card (e.g. scrolling / selecting text) keep it open.
|
||||
if (e.target.closest('.trainer-detail') && item.classList.contains('is-open')) return;
|
||||
// Tapping the dimmed backdrop (the item's ::after) closes it.
|
||||
if (floating.matches && item.classList.contains('is-open') && !e.target.closest('.trainer-detail')) {
|
||||
setOpen(item, false); return;
|
||||
var inCard = !!e.target.closest('.trainer-detail');
|
||||
if (item.classList.contains('is-open')) {
|
||||
// Tapping the dimmed backdrop (the item's ::after) closes it;
|
||||
// taps inside the card (scroll, select) keep it open.
|
||||
if (floating.matches && !inCard) setOpen(item, false);
|
||||
return;
|
||||
}
|
||||
closeAll(item);
|
||||
place(item);
|
||||
setOpen(item, !item.classList.contains('is-open'));
|
||||
setOpen(item, true);
|
||||
});
|
||||
item.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
|
||||
if (e.target !== item) return;
|
||||
if (e.key === 'Escape') { setOpen(item, false); item.blur(); }
|
||||
else if ((e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') && e.target === item) {
|
||||
e.preventDefault();
|
||||
closeAll(item); place(item);
|
||||
setOpen(item, !item.classList.contains('is-open'));
|
||||
} else if (e.key === 'Escape') {
|
||||
setOpen(item, false);
|
||||
item.blur();
|
||||
}
|
||||
});
|
||||
});
|
||||
document.addEventListener('click', function () { closeAll(null); });
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(null); });
|
||||
window.addEventListener('resize', function () { closeAll(null); }, { passive: true });
|
||||
});
|
||||
|
||||
/* ── Mensalidades: pre-select the clicked card in the sign-up modal ───────── */
|
||||
|
||||
Reference in new issue
Block a user