Corrige o fundo tintado (gradiente do body herdado de :root) com um layout de segmento `data-theme="editorial"` + scroll interno, espelhando `[type]/layout.tsx`, e porta o gate aprovado: vídeo herói, título à esquerda, meta em relógio, prosa medida, kit WhatsApp-first responsivo, secundárias em grade no mobile e estado indisponível redesenhado. Extrai os botões do share kit no próprio dono (acervo/biblioteca inalterados) e adiciona o guard e2e `data-theme="editorial"`.
17 KiB
Impl: Página pública do corte: revisão de design
Status: em execução Atualizado em: 2026-09-16 Issue: #1086 Intenção: docs/plans/corte-pagina-publica-design.md Appetite restante: herdado (~1–1,5 dia eng, uma página). Sem inflar.
Leitura da intenção
- Outcome: a página
/corte/<id>passa a ler como peça cuidada e compartilhável — vídeo herói, título alinhado à esquerda, meta/descrição em superfície neutra (zero fundo tintado), WhatsApp-first no celular, e o estado indisponível igualmente bonito — sem tocar rota,noindexou metadata (C167). - O que NÃO negociar: rota
/corte/<id>erobots: { index: false, follow: false }inalterados; MP4 armazenado como player (nunca YouTube/VOD); créditoFonte: Câmara dos Deputados · CC BY 4.0; mesma tela para id desconhecido e despublicado (não revela existência); comportamento do acervo/biblioteca que reusaSpeechCutShareActionsidêntico. - O que reavaliar (hipóteses da intenção):
- "fundo colorido por theme/
editorial-bg/gradiente" — confirmado: não éeditorial-bg; é o gradiente global dobody(src/app/(frontend)/styles.css:1025-1026) +:root --background:#ae1603(styles.css:124) que/corteherda por não terdata-themenem layout. O conserto é local, não na paleta global. - "SiteHeader pode precisar mexer" — não precisa:
src/components/SiteHeader.tsxjá éstickye pronto para viver dentro de um scroll container ([type]/layout.tsxé o precedente). Reusar como está.
- "fundo colorido por theme/
Abordagem recomendada
flowchart LR
L["corte/layout.tsx (novo)<br/>data-theme=editorial<br/>h-dvh overflow-y-auto bg-background"] --> P["corte/[id]/page.tsx<br/>composição responsiva"]
L --> N["corte/not-found.tsx<br/>estado indisponível"]
P --> S["SpeechCutShareActions.tsx<br/>exporta WhatsApp + Download button"]
S --> C["CopyLinkButton (reuso)"]
P --> SC["buildSpeechCutShare / speechCutPublicPath (reuso)"]
Opções consideradas: A) layout de segmento corte/layout.tsx + composição responsiva na page reusando botões extraídos do dono; B) editar o gradiente do body em styles.css; C) wrapper data-theme só dentro de page.tsx.
Recomendação: A — espelha src/app/(frontend)/[type]/layout.tsx:12-20 (mesma classe data-theme="editorial" h-dvh w-full overflow-y-auto bg-background text-foreground), conserta de uma vez a page e corte/not-found.tsx (o not-found do segmento é envolvido pelo layout do segmento), usa os tokens neutros de styles.css:143-162 (--background:#f6f4f3), e mantém o conserto escopado à rota. O h-screen/overflow-hidden do root ((frontend)/layout.tsx:76,80) já exige que a página dona do viewport forneça o scroll interno — hoje /corte não fornece.
Rejeitadas: B porque o gradiente do body sustenta as páginas públicas escuras (home/petition/campaign-site) — mexer nele vaza para fora do escopo; C porque um wrapper na page não cobre corte/not-found.tsx e duplica a responsabilidade de shell que o App Router já oferece no layout; ambas trocam um conserto de 1 arquivo por risco de regressão global.
Decisão: ordem responsiva do share kit (WhatsApp acima da descrição no mobile, kit inline abaixo no desktop)
Opções consideradas: A) extrair/exportar SpeechCutWhatsAppButton (+SpeechCutDownloadButton) de src/components/SpeechCutShareActions.tsx e o próprio componente passar a consumi-los, compondo os marcadores responsivos na page; B) duplicar o kit inteiro em hidden lg:flex / lg:hidden sem extração; C) reordenar via utilitários order-* com um único kit.
Recomendação: A — mantém um único dono do URL/rótulo do WhatsApp (buildSpeechCutShare, já em SpeechCutShareActions.tsx:37) e do download do MP4, com a page só posicionando blocos: sm:hidden WhatsApp h-12 w-full logo após a meta → descrição (instância única) → sm:hidden grade 2 colunas (CopyLinkButton + SpeechCutDownloadButton) → hidden sm:flex SpeechCutShareActions primary="whatsapp" → YouTube inline na meta no desktop (hidden sm:inline) e link avulso no mobile (sm:hidden). O limite é sm (640 px) porque o gate tem exatamente duas cenas (390 e 1280): uma única fronteira realiza as duas sem inventar um estado intermédio não desenhado (o plano dizia lg; ajuste de execução). A API pública do componente e o markup do acervo/biblioteca permanecem idênticos (o acervo usa primary="copy"; nada muda em SpeechCutResultCard.tsx:49 e SpeechCutLibraryShareActions.tsx:25).
Rejeitadas: B porque duplica fonte de verdade do CTA (label/URL) em dois lugares — DRY de conhecimento; C porque o agrupamento difere entre viewports (mobile = WhatsApp primário + grade de secundários; desktop = kit de 3 inline) e não há como "fatiar" um componente ao redor da descrição com order-*; D (prop nova variant="mobile" no componente) porque cria um gêmeo dentro do dono e arrisca o acervo — contra "edit the owner, don't twin". Não criar arquivo novo src/components/*.tsx: um componente novo fica fora do manifest e cai no fallback smoke (scripts/lib/e2e-affected-manifest.mjs:320-325).
Decisão: mapa de tokens do artefato → tokens do app (sob data-theme="editorial")
Opções consideradas: A) portar classes literais do artefato (text-stone-*, bg-white, border-stone-300); B) portar para tokens semânticos do app.
Recomendação: B, token-level (o artefato já usa text-muted-foreground/border-border na meta/footer, então o port é o que ele mesmo declara):
text-stone-950 → text-foreground; text-stone-700/text-stone-800 → text-foreground/90; text-stone-600/text-stone-500 → text-muted-foreground; bg-white em controles → bg-card; superfície da página (<main class="bg-white">) → bg-background; border-stone-300 → border-border; bg-stone-100 text-stone-600 (chip indisponível) → bg-muted text-muted-foreground. O <video> continua bg-black.
Divergência registrada: o artefato declara --background:#ffffff no editorial (linha 30); a produção tem --background:#f6f4f3 (neutro quente, styles.css:144). bg-background é o port correto porque (1) o aceite exige "superfície neutra do site público (background/card)", não branco puro; (2) usar bg-white cru divergiria da página de artigo irmã e furaria o contrato de tokens. bg-card (#fff) fica para os controles outline, como no artefato.
Rejeitadas: A porque fixa a paleta do mock e ignora o tema do site (contraste/tema divergiriam em campaign-site/petition caso a página fosse reusada).
Decisão: copy/ícone/botão do estado indisponível
Opções consideradas: A) manter a copy atual ("O link pode ter sido despublicado ou o endereço está incorreto." / botão "Ir para jorgesolla1313.com.br"); B) adotar a do gate ("O conteúdo pode ter sido removido ou o endereço não está correto." / chip de ícone / botão "Voltar ao início" → /).
Recomendação: B — trata como literal aprovada pelo design; a intenção fixou só o título Este corte não está disponível e o gate é o artefato aprovado. Voltar ao início é mais limpo que um rótulo com domínio hardcoded.
Assumido — validar com produto: o parágrafo de apoio e o rótulo do botão são literal do gate, não da intenção.
Rejeitadas: A porque mantém copy mais fraca e um botão com URL de domínio embutida, destoando da peça redesenhada. Semântica: o artefato usa h3, mas nessa rota é o heading principal → usar h1 com as classes de tamanho do artefato (text-2xl sm:text-3xl font-bold), mantendo o text-center (a base global styles.css:1029 já centraliza — aqui é o desejado).
Decisão: guard de teste da regressão do fundo tintado
Opções consideradas: A) asserção barata no spec existente; B) teste visual/screenshot; C) nenhum guard.
Recomendação: A — no teste "serves the published cut…" (tests/e2e/campaignSpeechCut.e2e.spec.ts:127-157) acrescentar expect(html).toContain('data-theme="editorial"'): o HTML servido prova que o segmento herdou o tema neutro e, portanto, que o gradiente dark-red não é mais a superfície. É o guard exato do bug.
Rejeitadas: B porque screenshot e2e é caro/instável e o appetite não comporta; C porque o bug é justamente uma regressão silenciosa de superfície. Nenhuma asserção existente quebra: título, /api/media/file/, crédito, noindex, thumbnail do YouTube e Ver sessão no YouTube permanecem; o título no HTML de not-found continua presente e o rótulo antigo do botão não é asserido.
Componentes / mudanças
src/app/(frontend)/corte/layout.tsx(NOVO):<div data-theme="editorial" className="h-dvh w-full overflow-y-auto bg-background text-foreground">{children}</div>— espelho desrc/app/(frontend)/[type]/layout.tsx:12-20. Cobre[id]/page.tsxenot-found.tsx; conserta o fundo tintado e fornece o scroll interno (sticky doSiteHeader).src/app/(frontend)/corte/[id]/page.tsx(EDIT): mantémgenerateMetadata,loadPublishedCut,noindex,CREDIT; muda só o JSX deSpeechCutPage(L138-196) — colunamax-w-4xl px-8 py-12,<video>aspect-video w-full rounded-xl border bg-black(mobile full-bleed comrounded-none border-x-0/sm:rounded-xl),h1comtext-left(a base global centraliza), meta com YouTubehidden lg:inline, descriçãomax-w-[68ch] text-base leading-7 text-foreground/90, blocos responsivos descritos acima, footerborder-border.src/app/(frontend)/corte/not-found.tsx(EDIT): bloco centrado (chipbg-muted text-muted-foregroundcom íconeXCircledolucide-reactjá disponível),h1título, parágrafo eLink"Voltar ao início"border-border bg-cardpara/. SemSiteHeaderduplicado — o layout não o renderiza, então a page/not-found continuam com o seu.src/components/SpeechCutShareActions.tsx(EDIT): exportarSpeechCutWhatsAppButtoneSpeechCutDownloadButton; oSpeechCutShareActionspassa a consumi-los internamente (mesmo markup/labels/data-slot). API pública e comportamento do acervo inalterados.tests/e2e/campaignSpeechCut.e2e.spec.ts(EDIT): guarddata-theme="editorial"(acima).- Migration: sem migration (nenhuma mudança de schema/collection/global).
- Access / Consent: nenhum (leitura pública read-only; sem Consent, sem write path).
- UI: Impeccable C (superfície pública existente redesenhada). Não é novo shape: o port é fiel ao gate já aprovado (
docs/plans/corte-pagina-publica-design-ui-design.html); shape→craft→critique→polish já ocorreram no gate — aqui é port class-for-class ajustado a tokens. O overlay de player do artefato (botão fake/progresso/NEEDS ASSET) é mock — NÃO portar; manter<video controls poster>nativo, só portando sizing/rounding/bleed.
Dados → forma (se aplicável)
N/A — a intenção declara explicitamente "não apresento dados"; nenhum KPI/gráfico/visualização. Nada a decidir em data-presentation.
Notas de execução (pós-crítica do designer)
- Duração em relógio (
formatSpeechClock, ex.02:14): literal do gate/intenção (2:14) ao lado do player; difere doformatSpeechSpan(2min14s) dos view models do acervo — decisão registrada em comentário nopage.tsx. Não mexer no dono compartilhado. - Breakpoint único
sm: o gate tem exatamente duas cenas (390/1280);smrealiza as duas sem estado intermédio não desenhado (o rascunho dizialg). - Poster 16:9:
object-coverno<video>faz o thumbnail 4:3 (hqdefault) preencher o quadro; o MP4 é 16:9, então o enquadramento da reprodução não muda. - Controles em
min-h-*: kit desktopmin-h-11, primário mobilemin-h-12, secundáriasmin-h-11— convergência emmin-h(semh-*fixo) para não cortar texto. - Débito deferido — shell editorial duplicado:
corte/layout.tsxé idêntico a[type]/layout.tsx(6 linhas). Extrair um shell compartilhado agora seria abstração para 2 call sites (eprivacidade/layout.tsxtem classes diferentes). Gatilho: um terceiro layout com o mesmo shell verbatim → extrairEditorialScrollShell. - Débito deferido —
controlClassNameno share kit: sizing livre serve só ao 1º consumidor (/corte/<id>); Gatilho: um 2º consumidor fora do C176 precisar de sizing por controle → trocar por variante de densidade (size) em vez de estender o free-form.
Fases verificáveis
- Tracer / superfície (≈0,3 dia) — criar
corte/layout.tsx; redesenharnot-found.tsx. Verificar:/corte/999999999responde 404 com superfície neutra (sem vermelho) edata-theme="editorial"no HTML; sem scroll duplo;SiteHeadersticky funciona. - UI da página publicada (≈0,7 dia) — extrair botões do dono; recompor
page.tsx(desktop 1280 + mobile 390) conforme o gate; preservar metadata/OG/noindex/crédito. Verificar manualmente com um corte semeado no dev local (pnpm dev, DB local) com e semyoutubeUrl, em 1280 e 390. - Gates (≈0,2 dia) —
pnpm gate:fast; rodartests/e2e/campaignSpeechCut.e2e.spec.ts(ecampaignSpeechAcervolocalmente como guard do refactor do share kit); push viapnpm push.
Rabbit holes / Não escopo (engenharia)
- Editar o gradiente do
body/:rootdestyles.css— vaza para as páginas escuras; corte explícito: conserto só na rota via layout. - Redesenhar
SiteHeader, home ou paleta global — a intenção já corta; o header é reusado como está. - Player custom/branded, autoplay, controles próprios — proibido pela intenção; o overlay do artefato é mock. Só
<video controls poster>. - Arquivo novo
src/components/*.tsx— ficaria sem mapping no manifest e2e (fallback smoke); a extração vai para dentro deSpeechCutShareActions.tsx, que já é prefixo mapeado. - Criar collection/Consent/migration/transação — não há write path; qualquer proposta nesse sentido é escopo inflado.
- Galeria pública, transcrição, embed de YouTube, slug amigável — fora de escopo por desenho (unlisted).
Riscos e mitigação
- Refactor de
SpeechCutShareActionsmexer no acervo/biblioteca → manter a API e o markup renderizado (mesmos rótulos,data-slot, variantes); rodarcampaignSpeechAcervo+campaignSpeechCutlocalmente; a spec de acervo mapeiasrc/components/campaign/speeche cobre o card que o consome. - Duplicação de nós no DOM (WhatsApp/Copiar/Baixar/YouTube existem em bloco mobile e desktop, ocultos por
sm:hidden/hidden sm:flex) → visualmente só um de cada; asserções e2e são por texto, não contagem; aceitável para página estática. - Scroll/altura duplicados no root (
h-screen overflow-hidden) → o layout novo é a única fonte de scroll do segmento; testar que mobile rola até o footer e que o sticky cola no topo do container (não do viewport). h1continua centralizado →text-leftexplícito na page publicada (utilities vencem@layer base); not-found mantém centralizado de propósito.- Meta com
·vazando no mobile → separador e link do YouTube comhidden sm:inline; link avulso sósm:hidden. - Manifest e2e →
corte/layout.tsxcai no prefixosrc/app/(frontend)/corte(scripts/lib/e2e-affected-manifest.mjs:321), então o spec roda; nenhum prefixo novo necessário.
Aceite de engenharia
- Aceite de produto da intenção ainda coberto: vídeo herói; superfície neutra (zero fundo tintado); WhatsApp-first acima da dobra no mobile; leitura AA; publicado (com/sem YouTube) e indisponível coerentes.
- Invariantes AGENTS/engineering-standards: rota e
noindexintocados; C167 congelado; copy pt-BR / identificadores em inglês; sem migration; sem collection/Consent; escopo só em(frontend)/corte+ o dono do share kit. - Testes: guard
data-theme="editorial"no spec C167;campaignSpeechCutecampaignSpeechAcervoverdes;pnpm gate:fastverde.
Self-score (decision-quality: 5/5)
- Decisões caras com rejeitadas? Sim — superfície (rejeitadas B/C), composição responsiva (B/C/D), tokens (A) e copy do indisponível (A) têm opções + rejeitadas nomeadas. (5/5)
- Cabe no appetite (~1–1,5 dia, uma página)? Sim — 1 arquivo novo + 3 edits + 1 asserção; sem schema, sem dependência nova. (5/5)
- Rabbit holes nomeados? Sim — gradiente global, header/paleta, player custom, arquivo de componente novo, collection/migration. (5/5)
- Depth check — reusa shells/helpers? Sim —
[type]/layout.tsx(shell+theme),SpeechCutShareActions/CopyLinkButton(dono do share),buildSpeechCutShare/speechCutPublicPath(URL), tokenseditorial; nenhum pass-through raso criado. (5/5) - Intenção preservada? Sim — a engenharia não reescreveu o outcome; cada corte remete ao aceite de produto. (5/5)