markup.html · 62 linhas · 3,1 KB
<!-- ==========================================================================
GALERIA EM PASTA — pilha de fotos que abre em leque
Prefixo: fold-
Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js
Sem lib. O leque de hover é CSS puro; o JS só abre, fecha e arrasta.
Cada foto declara --i (posição em relação ao centro, negativa à esquerda)
e --a (o mesmo em módulo, para a escala). São os dois números que o CSS
usa para montar pilha, leque e linha aberta — mude a quantidade de fotos
e só o --i/--a precisa acompanhar.
Ímpar fica melhor: com 5, o --i vai de -2 a 2 e o centro é o 0.
========================================================================== -->
<section class="sec-dark sec-pad" id="galeria">
<div class="wrap">
<span class="eyebrow" data-rv>Nossos escritórios</span>
<h2 class="sec-title ttl">Onde o trabalho <em>acontece</em></h2>
<p class="sec-lead" data-rv data-d="1">Uma frase de apoio situando o que a pasta guarda: o lugar, o time, o ano. Duas linhas no máximo — a atenção aqui é da imagem, não do texto.</p>
<div class="fold" data-fold data-rv data-d="2">
<div class="fold-stage">
<div class="fold-body" aria-hidden="true">
<span class="fold-body-tab"></span>
<span class="fold-body-back"></span>
</div>
<ul class="fold-items" id="fold-items">
<li class="fold-item" style="--i:-2;--a:2">
<img class="fold-img" src="../../../showroom/demo/ph-6.webp" alt="Globo terrestre antigo sobre mesa de biblioteca">
<span class="fold-cap">Biblioteca</span>
</li>
<li class="fold-item" style="--i:-1;--a:1">
<img class="fold-img" src="../../../showroom/demo/ph-7.webp" alt="Reunião de quatro pessoas em sala de vidro">
<span class="fold-cap">Sala de reunião</span>
</li>
<li class="fold-item" style="--i:0;--a:0">
<img class="fold-img" src="../../../showroom/demo/ph-4.webp" alt="Três pessoas conversando em escritório com vista para a cidade">
<span class="fold-cap">Escritório, andar 14</span>
</li>
<li class="fold-item" style="--i:1;--a:1">
<img class="fold-img" src="../../../showroom/demo/ph-8.webp" alt="Mesa de trabalho com xícara de café, caderno e óculos">
<span class="fold-cap">Mesa de trabalho</span>
</li>
<li class="fold-item" style="--i:2;--a:2">
<img class="fold-img" src="../../../showroom/demo/ph-3.webp" alt="Duas pessoas conversando junto a janela com vista da cidade">
<span class="fold-cap">Vista norte</span>
</li>
</ul>
<button class="fold-front" type="button" data-fold-open aria-expanded="false" aria-controls="fold-items">
<span class="fold-label">Escritórios.2026</span>
</button>
</div>
<p class="fold-hint" data-fold-hint aria-hidden="true">Arraste uma foto para baixo para fechar</p>
<button class="fold-close" type="button" data-fold-close tabindex="-1">Fechar a pasta</button>
</div>
</div>
</section>
estilo.css · 159 linhas · 9,3 KB
/* ==========================================================================
GALERIA EM PASTA — prefixo fold-
Pilha de fotos dentro de uma pasta. Clique abre em linha, arrastar uma
foto para baixo fecha.
HOVER NÃO ABRE O LEQUE, desde 2026-09-29 (CLAUDE.md §6). Até então as
fotos saltavam da pasta em arco sob o ponteiro, e cada foto aberta subia
e crescia ao passar o mouse. Saiu tudo: imagem que se mexe no hover é o
que quebra a página quando o componente é colado noutro lugar. Sob o
ponteiro a pasta só acende a borda e o rótulo. Cada foto lê --i (posição
em relação ao centro) e --a (o mesmo em módulo) do próprio style inline;
sem JS a pilha continua legível e parada.
========================================================================== */
/* --dur do core é .9s: certo para entrada de seção, lento para o leque
responder ao ponteiro. A duração de interação é local e fica aqui, à
mão de quem for ajustar. A curva continua sendo a do projeto. */
.fold{--fold-dur:.42s;--fold-dur-lento:.6s;--fold-spread:132px;
margin-top:clamp(34px,5vh,56px);display:flex;flex-direction:column;align-items:center}
/* O palco reserva a altura toda: as fotos são absolutas e não empurram nada.
A perspectiva é do palco, senão a aba tomba achatada. */
.fold-stage{position:relative;width:100%;max-width:420px;height:460px;
display:flex;justify-content:center;perspective:1400px}
/* ---------------------------------------------------------------- a pasta */
/* Duas peças: a abinha de cima (o "dente" da pasta suspensa) e o corpo.
Ficam atrás de tudo e somem quando a pasta abre. */
.fold-body{position:absolute;bottom:26px;width:min(320px,84%);height:224px;
transition:opacity var(--fold-dur) var(--ease),transform var(--fold-dur) var(--ease)}
.fold-body-tab{position:absolute;top:0;left:0;width:128px;height:38px;
background:linear-gradient(to top,var(--dark-2),color-mix(in srgb,var(--fg) 12%,var(--dark-2)));
border:1px solid var(--rule);border-bottom:0;
border-radius:var(--radius-media) var(--radius-media) 0 0}
.fold-body-back{position:absolute;top:32px;left:0;right:0;bottom:0;
background:linear-gradient(to bottom,var(--dark-2),var(--dark));
border:1px solid var(--rule);border-radius:0 var(--radius-media) var(--radius-media) var(--radius-media);
box-shadow:inset 0 0 40px rgba(var(--dark-rgb),.8)}
/* ---------------------------------------------------------------- aba da frente */
/* É um <button> de verdade: foco, Enter e Espaço vêm de graça, e o leitor de
tela anuncia o estado por aria-expanded. O original era uma <div> clicável. */
.fold-front{position:absolute;bottom:0;width:min(340px,90%);height:176px;z-index:20;
background:linear-gradient(to bottom,color-mix(in srgb,var(--fg) 12%,var(--dark-2)),var(--dark));
border:1px solid var(--rule-2);border-radius:var(--radius-media);
box-shadow:inset 0 2px 10px color-mix(in srgb,var(--fg) 8%,transparent),0 -20px 40px rgba(var(--dark-rgb),.8);
transform-origin:bottom;cursor:pointer;overflow:hidden;
display:flex;align-items:flex-end;justify-content:center;padding-bottom:30px;
transition:transform var(--fold-dur) var(--ease),opacity var(--fold-dur) var(--ease)}
/* o fio de luz na quina de cima, que dá volume à aba */
.fold-front::before{content:"";position:absolute;top:0;left:0;right:0;height:1px;
background:linear-gradient(to right,transparent,color-mix(in srgb,var(--fg) 40%,transparent),transparent)}
.fold-front:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.fold-label{font-family:var(--display);font-weight:700;font-size:13.5px;
letter-spacing:.02em;color:var(--fg);background:var(--dark);
border:1px solid rgba(var(--dark-rgb),.8);border-radius:var(--radius-media);
padding:10px 20px;box-shadow:inset 0 1px 6px rgba(var(--dark-rgb),.9)}
/* hover: a pasta avisa que é clicável sem se mexer — borda e rótulo na cor
de marca. A aba tombava e as fotos saltavam em leque; ver o cabeçalho. */
.fold-stage:hover .fold-front,
.fold-front:focus-visible{border-color:color-mix(in srgb,var(--accent) 60%,var(--rule-2))}
.fold-stage:hover .fold-label,
.fold-front:focus-visible .fold-label{color:var(--accent)}
/* aberta: pasta e aba saem de cena */
.fold.is-open .fold-front{opacity:0;transform:none;pointer-events:none}
.fold.is-open .fold-body{opacity:0;transform:scale(.9);transition-duration:var(--fold-dur-lento)}
/* ---------------------------------------------------------------- as fotos */
.fold-items{position:absolute;bottom:40px;z-index:10;
display:flex;justify-content:center;list-style:none;margin:0;padding:0}
/* --dx/--dy/--dr são o canal do arraste: o script só escreve estes três
valores e o CSS recompõe o transform inteiro. Assim o JS não precisa
conhecer a fórmula da posição, que continua morando aqui. */
.fold-item{--dx:0px;--dy:0px;--dr:0deg;
position:absolute;bottom:0;width:224px;height:288px;
border:1px solid var(--rule-2);border-radius:var(--radius-media);overflow:hidden;
box-shadow:0 20px 40px rgba(var(--dark-rgb),.5);transform-origin:bottom;
pointer-events:none;
/* z-index sobe com a posição para a foto da direita ficar na frente */
z-index:calc(10 + var(--i));
transition:transform var(--fold-dur) var(--ease),box-shadow var(--fold-dur) var(--ease);
/* PILHA — estado de repouso: quase alinhadas, leque mínimo */
transform:translateX(calc(var(--i) * 3px)) translateY(calc(var(--i) * -5px))
rotate(calc(var(--i) * 3deg)) scale(calc(1 - var(--a) * .03))}
.fold-img{width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}
/* ABERTA — linha reta, todas do mesmo tamanho, sem rotação */
.fold.is-open .fold-item{
pointer-events:auto;cursor:grab;z-index:50;
transform:translateX(calc(var(--i) * var(--fold-spread) + var(--dx)))
translateY(calc(-130px + var(--dy))) rotate(var(--dr)) scale(1.05)}
.fold.is-open .fold-item:focus-visible{outline:2px solid var(--accent);outline-offset:3px;z-index:100}
/* durante o arraste o JS assume o transform e a transição sai da frente */
.fold-item.is-dragging{cursor:grabbing;transition:none;z-index:150;
box-shadow:0 30px 60px rgba(var(--dark-rgb),.7)}
/* soltou sem fechar: volta para o lugar com transição de novo */
.fold-item.is-returning{transition:transform var(--fold-dur) var(--ease)}
/* ---------------------------------------------------------------- legenda */
/* Dentro da foto, ancorada embaixo, com véu em gradiente mascarado — véu em
caixa reta lê chapado (contrato §6). Só aparece com a pasta aberta: na
pilha as fotos estão sobrepostas e a legenda viraria ruído. */
.fold-cap{position:absolute;left:0;right:0;bottom:0;padding:34px 15px 13px;
font-size:11.5px;letter-spacing:.05em;text-transform:uppercase;
font-family:var(--display);font-weight:700;color:var(--fg);
background:linear-gradient(to top,rgba(var(--dark-rgb),.92),transparent);
backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);
mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 44%,transparent 100%);
-webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 44%,transparent 100%);
opacity:0;transition:opacity var(--fold-dur) var(--ease)}
.fold.is-open .fold-cap{opacity:1}
/* ---------------------------------------------------------------- apoio */
.fold-hint{margin-top:20px;font-size:11.5px;letter-spacing:.11em;text-transform:uppercase;
font-family:var(--display);font-weight:700;color:var(--fg-muted);
border:1px solid var(--rule);border-radius:var(--radius-pill);padding:10px 22px;
opacity:0;transform:translateY(14px);pointer-events:none;
transition:opacity var(--fold-dur) var(--ease),transform var(--fold-dur) var(--ease)}
.fold.is-open .fold-hint{opacity:1;transform:none}
/* Saída por teclado. O arraste é a graça do componente, mas não pode ser o
único jeito de fechar: fica fora do alcance de quem navega por teclado ou
usa leitor de tela. Some do fluxo enquanto a pasta está fechada. */
.fold-close{margin-top:14px;font-family:var(--display);font-weight:700;font-size:11px;
letter-spacing:.1em;text-transform:uppercase;color:var(--fg-2);background:none;
border:0;padding:8px 4px;cursor:pointer;transition:color .25s;
position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
.fold-close:hover{color:var(--accent)}
.fold.is-open .fold-close{position:static;width:auto;height:auto;clip-path:none}
.fold-close:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
/* ---------------------------------------------------------------- movimento reduzido */
/* Sem deslocamento nem rotação: a pasta abre trocando opacidade e as fotos
viram uma grade quieta. O componente continua inteiro, só não se mexe. */
@media(prefers-reduced-motion:reduce){
.fold-item,.fold-front,.fold-body,.fold-cap,.fold-hint{transition-duration:.01ms}
}
/* ---------------------------------------------------------------- mobile */
/* Aberta, cinco fotos de 224px não cabem em tela estreita. O espalhamento
encolhe e as fotos passam a se sobrepor de propósito — vira um leque
apertado em vez de uma linha, que é o que a largura permite. */
@media(max-width:860px){
.fold{--fold-spread:74px}
.fold-item{width:180px;height:232px}
.fold-stage{height:420px}
}
@media(max-width:520px){
.fold{--fold-spread:46px}
.fold-item{width:150px;height:196px}
.fold-stage{height:380px}
.fold-hint{font-size:10.5px;padding:9px 16px;text-align:center}
}
script.js · 140 linhas · 4,6 KB
/* ==========================================================================
GALERIA EM PASTA — abrir, fechar e arrastar.
O leque de hover é CSS e não passa por aqui. Este script só cuida do que
o CSS não alcança: o estado aberto, as saídas e o arraste.
O arraste escreve --dx/--dy/--dr na foto; quem recompõe o transform é o
estilo.css. Assim mudar a posição aberta não exige mexer no script.
========================================================================== */
(function () {
var raizes = document.querySelectorAll('[data-fold]');
if (!raizes.length) return;
var LIMITE = 100; /* px para baixo que fecham a pasta */
var GIRO = 0.04; /* graus por px de arraste lateral */
/* A flag vem do core, que ja' resolve o ?motion=on. Ler matchMedia aqui
duplicaria a regra e perderia esse escape. */
function reduzido() {
return !!(window.LIB && window.LIB.reduce);
}
function iniciar(raiz) {
var abrir = raiz.querySelector('[data-fold-open]');
var fechar = raiz.querySelector('[data-fold-close]');
var itens = raiz.querySelectorAll('.fold-item');
if (!abrir || !itens.length) return;
function estado(aberta) {
raiz.classList.toggle('is-open', aberta);
abrir.setAttribute('aria-expanded', aberta ? 'true' : 'false');
for (var i = 0; i < itens.length; i++) {
itens[i].setAttribute('tabindex', aberta ? '0' : '-1');
zerar(itens[i]);
}
if (fechar) fechar.tabIndex = aberta ? 0 : -1;
}
function zerar(item) {
item.style.removeProperty('--dx');
item.style.removeProperty('--dy');
item.style.removeProperty('--dr');
item.classList.remove('is-dragging');
}
abrir.addEventListener('click', function () {
estado(true);
itens[Math.floor(itens.length / 2)].focus();
});
if (fechar) {
fechar.addEventListener('click', function () {
estado(false);
abrir.focus();
});
}
/* Esc fecha, mas só com o foco dentro do componente: a tecla é disputada
por drawer, modal e busca, e quem está longe daqui não espera isto. */
raiz.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && raiz.classList.contains('is-open')) {
estado(false);
abrir.focus();
}
});
/* clique fora fecha */
document.addEventListener('pointerdown', function (e) {
if (raiz.classList.contains('is-open') && !raiz.contains(e.target)) estado(false);
}, { passive: true });
/* ------------------------------------------------------------ arraste */
for (var i = 0; i < itens.length; i++) {
ligarArraste(itens[i]);
}
function ligarArraste(item) {
var x0 = 0, y0 = 0, dx = 0, dy = 0, arrastando = false, pedido = 0;
function pintar() {
pedido = 0;
item.style.setProperty('--dx', dx + 'px');
item.style.setProperty('--dy', dy + 'px');
item.style.setProperty('--dr', (dx * GIRO) + 'deg');
}
item.addEventListener('pointerdown', function (e) {
if (!raiz.classList.contains('is-open') || reduzido()) return;
arrastando = true;
x0 = e.clientX; y0 = e.clientY; dx = 0; dy = 0;
item.classList.remove('is-returning');
item.classList.add('is-dragging');
item.setPointerCapture(e.pointerId);
});
item.addEventListener('pointermove', function (e) {
if (!arrastando) return;
dx = e.clientX - x0;
dy = e.clientY - y0;
/* preso ao ponteiro: pinta no frame, nunca no listener */
if (!pedido) pedido = requestAnimationFrame(pintar);
});
function soltar() {
if (!arrastando) return;
arrastando = false;
if (pedido) { cancelAnimationFrame(pedido); pedido = 0; }
item.classList.remove('is-dragging');
if (dy > LIMITE) {
estado(false);
abrir.focus();
return;
}
/* não passou do limite: volta sozinha para o lugar */
item.classList.add('is-returning');
zerar(item);
setTimeout(function () { item.classList.remove('is-returning'); }, 500);
}
item.addEventListener('pointerup', soltar);
item.addEventListener('pointercancel', soltar);
/* teclado: seta para baixo faz o papel do arraste */
item.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown' || e.key === 'Escape') {
e.preventDefault();
estado(false);
abrir.focus();
}
});
}
estado(false);
}
for (var r = 0; r < raizes.length; r++) {
iniciar(raizes[r]);
}
})();