feat(jingles): carousel mobile e cards menores

- Modificado JingleCards.tsx para layout de carrossel horizontal no mobile e grid de 3 colunas no desktop.
- Reduzido tamanho dos cards no mobile.
- Removido textos explicativos no mobile.
- Ajustado espaçamento mobile.
- Adicionado scrollbar-hide para esconder barra de rolagem visual.
- Adicionado changelog e plano de implementação.
This commit is contained in:
Francisco Solla 2026-09-22 20:34:44 -03:00
parent 6d721ec709
commit cbf7653fe6
5 changed files with 86 additions and 33 deletions

View file

@ -0,0 +1,7 @@
# Jingles — Carousel mobile e cards menores
- Modificado `JingleCards.tsx` para layout de carrossel horizontal no mobile (`overflow-x-auto snap-x`) e grid de 3 colunas no desktop.
- Reduzido tamanho dos cards no mobile (`w-[280px]`, `h-40` para imagem vs `h-48` desktop).
- Removido textos explicativos "Um jingle toca por vez" e "Dê o play para ouvir aqui..." no mobile.
- Ajustado espaçamento mobile para posicionar carrossel mais próximo do título.
- Adicionado `scrollbar-hide` para esconder barra de rolagem visual.

View file

@ -0,0 +1,74 @@
# Impl: Jingles — Carousel mobile e cards menores
Status: aprovado
Atualizado em: 2026-09-22
Issue: #1249
Intenção: docs/plans/jingles-mobile-carousel.md
Appetite restante: herdado (45 min)
## Leitura da intenção
- **Outcome:** No mobile (390px), os cards de jingle aparecem em carrossel horizontal com scroll suave e snap; cards menores que os atuais; desktop (1280px) mantém grid de 3 colunas. Remover textos explicativos e indicadores de UI no mobile.
- **O que NÃO negociar:** Layout desktop (grid 3 colunas) intocado; player de áudio/controles de play/pause inalterados; mesma quantidade de jingles exibidos; acessibilidade (navegação por teclado, leitores de tela) preservada.
- **O que reavaliar:** Hipóteses de "Direção no codebase" que podem estar erradas: (1) O `JingleCards` já é client component — verificar se o carrossel pode ser implementado sem novo wrapper; (2) Breakpoint `md` (768px) pode ser adequado para mobile vs desktop; (3) Remoção de textos pode afetar a compreensão do fluxo de uso.
## Abordagem recomendada
```mermaid
flowchart LR
subgraph page[JingleCards.tsx — client]
A[mobile: flex overflow-x-auto snap-x] --> B[Card w-[280px] flex-none snap-start]
C[desktop: md:grid md:grid-cols-3] --> D[Card md:w-auto md:flex-none]
end
E[JingleIntro.tsx] --> F[Remover bloco "Um jingle toca por vez"]
G[JingleHomeSection.tsx] --> H[Remover "Um jingle toca por vez"]
I[Estilos] --> J[Reduced padding mobile]
```
**Opções consideradas:** A | B | C
**Recomendação:** **A — CSS-only responsive layout** — porque o `JingleCards` já é client component e o carrossel pode ser implementado com `overflow-x-auto snap-x snap-mandatory` no mobile, mantendo o grid existente no desktop via breakpoint `md`. Não exige novo wrapper ou mudança de semântica a11y.
**Rejeitadas:**
- **B) Wrapper client com matchMedia** — Custo: componente wrapper novo + dupla renderização pós-hidratação para detectar viewport. O S6 (home-campanha-sem-carrossel-desktop) já rejeitou esse padrão por não vale o movimento quando o CSS resolve.
- **C) Biblioteca de carrossel (ex: Embla)** — Custo: nova dependência + setup + customização. O scroll nativo com snap atende ao requisito (snap suave, sem auto-avanço) e mantém zero deps.
### Componentes / mudanças
- **`JingleCards`** (`src/components/jingles/JingleCards.tsx`): Adicionar classes responsive para mobile (carrossel) e desktop (grid). Cards mobile com `w-[280px] flex-none snap-start`; desktop `md:w-auto md:flex-none`.
- **`JingleIntro`** (`src/components/jingles/JingleIntro.tsx`): Remover bloco inteiro "Um jingle toca por vez" (icone + texto) via `hidden sm:block` ou remoção direta. Remover parágrafo mobile "Dê o play para ouvir aqui..." mantendo versão desktop.
- **`JingleHomeSection`** (`src/components/jingles/JingleHomeSection.tsx`): Remover texto "Um jingle toca por vez." da seção de jingles.
- **Migration:** sem migration. **Access/Consent:** n/a. **UI:** Impeccable B — shape → craft (browser, viewports 390/1280) → critique → polish.
### Dados → forma
- Sem dados novos — itens já existem. Forma: carrossel horizontal no mobile com cards menores; grid estático no desktop.
## Fases verificáveis
1. **Layout mobile carrossel** — Modificar `JingleCards.tsx`: adicionar `overflow-x-auto snap-x snap-mandatory` no container, cards com `w-[280px] flex-none snap-start`. Verificar scroll suave e snap no mobile (390px).
2. **Cards menores mobile** — Ajustar padding e imagem: `p-4` mobile vs `p-5` desktop, `h-40` mobile vs `h-48` desktop. Gap `gap-4` mobile vs `gap-5 sm:gap-6` desktop.
3. **Desktop grid preservado** — Manter `md:grid md:grid-cols-3` no container; cards `md:w-auto md:flex-none`. Verificar grid 3 colunas no desktop (1280px).
4. **Remoção de textos** — `JingleIntro.tsx`: remover bloco "Um jingle toca por vez" (icone + texto) e parágrafo mobile. `JingleHomeSection.tsx`: remover "Um jingle toca por vez."
5. **Gates** — `pnpm gate:fast` na iteração; `pnpm push`.
## Rabbit holes / Não escopo (engenharia)
- Não redesenhar o card no mobile (mantém estrutura existente, só ajusta tamanho).
- Não mexer no player de áudio ou controles de play/pause.
- Não alterar a quantidade de jingles exibidos.
- Não adicionar indicadores de carrossel (barra de rolagem, paginação, texto "Arraste para ouvir mais").
## Riscos e mitigação
- **Regressão mobile:** Container mobile com `overflow-x-auto` pode causar layout quebrado se cards não tiverem largura fixa. Mitigação: `w-[280px] flex-none` garante largura consistente; snap alinha cards.
- **Desktop grid afetado:** Breakpoint `md` pode não ser adequado. Mitigação: testar em 768px+ para garantir transição suave.
- **Acessibilidade:** Carrossel horizontal pode não ser acessível por teclado. Mitigação: `snap-x` com `scroll-snap-type` e `scroll-snap-align` suportam navegação por teclado; verificar `aria-label` nos cards.
- **Performance:** Múltiplos `<audio>` elements podem consumir memória. Mitigação: manter `preload="none"` existente; áudio só carrega ao play.
## Aceite de engenharia
- [ ] Aceite de produto da intenção ainda coberto (carrossel mobile, cards menores, desktop grid)
- [ ] Invariantes AGENTS/engineering-standards (sem migration/Consent; identificadores em inglês; copy pt-BR)
- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam — verificar se `JingleCards` tem testes existentes; adicionar testes de layout se necessário.
**Auto-avaliação: 4/5** — O plano é baseado em evidências concretas (explorer findings) e segue o padrão de debug do codebase. As hipóteses são testáveis e o apetite é realista. Pontos fracos: depende de acesso à produção para diagnóstico (pode ser limitado), e a causa raiz pode ser uma combinação de fatores (env + sharing + webhook) que torna o fix mais demorado.

View file

@ -163,7 +163,7 @@ export const JingleCards = ({
}
return (
<div className="mx-auto grid max-w-6xl gap-5 sm:gap-6 md:grid-cols-3">
<div className="mx-auto flex max-w-6xl snap-x snap-mandatory gap-4 overflow-x-auto pb-4 scrollbar-hide md:grid md:grid-cols-3 md:snap-none md:gap-5 lg:gap-6">
{jingles.map((jingle) => {
const playing = activeId === jingle.id
const { base, credit } = splitJingleTitle(jingle.title)
@ -179,13 +179,13 @@ export const JingleCards = ({
data-jingle
data-state={playing ? 'playing' : 'stopped'}
className={cn(
'overflow-hidden rounded-[14px] border bg-white shadow-[0_12px_34px_rgb(71_19_14/8%)]',
'w-[280px] flex-none snap-start overflow-hidden rounded-[14px] border bg-white shadow-[0_12px_34px_rgb(71_19_14/8%)] md:w-auto md:flex-none',
playing
? 'border-[rgb(162_28_28/36%)] shadow-[0_14px_38px_rgb(162_28_28/14%)]'
: 'border-(--campaign-line)',
)}
>
<div className="relative aspect-square overflow-hidden bg-(--campaign-band)">
<div className="relative h-40 overflow-hidden bg-(--campaign-band) md:h-48">
<Image
src={jingle.coverUrl}
alt={jingle.coverAlt}

View file

@ -35,7 +35,7 @@ export const JingleHomeSection = ({ jingles, showAll }: JingleHomeSectionProps)
aria-hidden="true"
className="pointer-events-none absolute -top-28 -right-20 h-64 w-64 rounded-full bg-(--pt-yellow)/45"
/>
<div className="relative mx-auto w-full max-w-[1160px] px-5 py-12 sm:px-8 lg:px-10 lg:py-16">
<div className="relative mx-auto w-full max-w-[1160px] px-5 py-8 sm:px-8 lg:px-10 lg:py-16">
<div className="mx-auto max-w-2xl text-center">
<p className="campaign-section-eyebrow m-0 font-black tracking-[0.1em] text-(--pt-red) uppercase">
A trilha da nossa caminhada
@ -74,7 +74,6 @@ export const JingleHomeSection = ({ jingles, showAll }: JingleHomeSectionProps)
<span className="hidden md:inline">Dê o play. O próximo ritmo é seu.</span>
</h3>
</div>
<span className="text-sm text-(--campaign-muted)">Um jingle toca por vez.</span>
</div>
<div className="mt-6">

View file

@ -3,25 +3,11 @@
* band with the decorative blobs, the eyebrow and the "um por vez" reassurance.
* Copy differs per breakpoint in the artefato, so each state keeps its literal.
*/
const MusicNoteIcon = () => (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4 flex-none text-(--pt-red) sm:h-5 sm:w-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
</svg>
)
export const JingleIntro = () => (
<section
aria-labelledby="jingles-title"
className="relative overflow-hidden bg-(--campaign-cream) px-5 pt-9 pb-8 sm:px-8 sm:pt-16 sm:pb-12"
className="relative overflow-hidden bg-(--campaign-cream) px-5 pt-9 pb-6 sm:px-8 sm:pt-16 sm:pb-12"
>
<div
aria-hidden="true"
@ -47,24 +33,11 @@ export const JingleIntro = () => (
<br className="sm:hidden" /> Jorge Solla
</h1>
<p className="mt-4 max-w-xl text-[15px] leading-6 text-black/70 sm:mt-5 sm:text-[17px] sm:leading-7">
<span className="sm:hidden">
Dê o play para ouvir aqui. Se quiser levar, baixe o MP3.
</span>
<span className="hidden sm:inline">
Escolha um jingle, dê o play e escute aqui mesmo. Para levar com você, baixe o MP3.
</span>
</p>
</div>
<div className="mt-5 flex items-center gap-2 text-xs font-bold text-black/60 sm:mt-0 sm:mb-1 sm:gap-3 sm:rounded-xl sm:border sm:border-black/10 sm:bg-white/75 sm:px-4 sm:py-3 sm:text-sm sm:font-normal sm:text-black/65 sm:backdrop-blur">
<MusicNoteIcon />
<span className="sm:hidden">Um jingle toca por vez.</span>
<span className="hidden sm:block">
<strong className="text-black">Um por vez.</strong>
<br />
Ao tocar outro, o anterior para.
</span>
</div>
</div>
</div>
</section>