diff --git a/docs/changelog/2026-09-25-central-logo-cortado.md b/docs/changelog/2026-09-25-central-logo-cortado.md new file mode 100644 index 00000000..6c72b5ce --- /dev/null +++ b/docs/changelog/2026-09-25-central-logo-cortado.md @@ -0,0 +1 @@ +**Central de Conteúdos pública — logo do header sem corte (2026-09-25):** o S36 (`772a5cf0`) trocou o `Image` direto `h-7 sm:h-9` do header da Central por uma janela `overflow-hidden` de recorte, mas o artefato de design trazia os números errados (188×50 desktop / 142×39 mobile, img 246px/186px, offsets −29/−38 e −22/−29) e a implementação os portou fielmente: a janela tinha razão 188/50 ≈ 3,76 (mobile 3,64) contra a razão da tinta real do PNG oficial — canvas 1037×595, alpha bbox 790×285 em (123, 162), razão ≈ 2,77 — então a tinta renderizada (187,4×67,6 no desktop; 141,7×51,1 no mobile) ficava mais alta que a janela e o `overflow-hidden` decapitava a base de "SOLLA" (18px/12,2px cortados; reproduzido read-only em produção nos dois breakpoints, nas três superfícies do header, catálogo/peça/não-encontrado). O dono único `src/components/conteudos/ContentPiecePageHeader.tsx` passa a derivar a janela da caixa da tinta no peso da referência `/jingles` — mobile `h-[46px] w-[128px]` (img 167px, offsets −19,8/−26,1) e desktop `sm:h-[52px] sm:w-[144px]` (img 189px, offsets −22,4/−29,5) — com o comentário reescrito (invariante + receita de recálculo via `sharp trim`); `CampaignPageHeader` (`/jingles`) intocado, sem migration/access/Consent. Regressão e2e nova em `frontendConteudos` ("keeps the whole brand ink inside the header crop, both breakpoints (S36)") mede o alpha bbox real do PNG com sharp e asserta a tinta renderizada dentro da janela em 1280 e 390 — RED provado sem o fix (`ink bottom clipped @1280px`, esperado ≤57,5, recebido 75,04) e verde com; e2e `frontendConteudos` 12/12, `frontendJingles` 5/5, `pnpm gate:fast` verde (unit 4685 em 423 arquivos), `format:check`/`knip`/`check:cycles` verdes; nota de prevenção no `public/campaign-kit/README.md` §"Lockups do nome"; CI e prod pendentes (nunca declarar corrigido antes do approve humano). Post-mortem: `docs/postmortems/2026-09-25-central-logo-cortado.md`. diff --git a/docs/postmortems/2026-09-25-central-logo-cortado.md b/docs/postmortems/2026-09-25-central-logo-cortado.md new file mode 100644 index 00000000..ce116604 --- /dev/null +++ b/docs/postmortems/2026-09-25-central-logo-cortado.md @@ -0,0 +1,78 @@ +# Post-mortem: logo do header da Central de Conteúdos cortado — janela de recorte sem a razão da tinta + +> Template do `/bug-fix`. Preencha com fatos apurados; o que não for apurado fica "não apurado" — nunca invente. + +## Registro + +| Campo | Valor | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Data do post-mortem | 2026-09-25 | +| Severidade | baixa (branding no header público da Central; sem perda de dados) | +| Ambiente | prod (sintoma, repro read-only) + dev/worktree (diagnóstico e fix) | +| Issue(s) | sem Issue — fluxo `/bug-fix` do worktree `fix/12` (o post-mortem é o registro); o S36 que introduziu o bug fechou a Issue #1301 | +| PR do fix | pendente (este PR) | +| Detectado por | humano (relato que abriu a sessão `/bug-fix` do worktree `fix/12`) | + +## Timeline + +| Momento | Data/hora | Evento | +| ------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Início provável | 2026-09-24 02:20 BRT | Commit `772a5cf0` "fix(S36): logo do header da Central no tamanho da marca (Closes #1301)" troca o `Image` direto `h-7 sm:h-9` pela janela `overflow-hidden` de recorte, portando os números do artefato de design | +| Detecção | 2026-09-25 ~00:07 BRT | Reprodução read-only em produção nos dois breakpoints (1280px: janela 188×50 e tinta real 187,4×67,6 → 18px cortados; 390px: janela 142×39 e tinta 141,7×51,1 → 12,2px cortados); hora exata do relato original não apurada | +| Correção mergeada | pendente (este PR) | — | +| Deploy | pendente | merge em `main` dispara deploy; produção só com approve humano no environment `production` (não feito ainda) | +| Verificado em prod | pendente | aguardando confirmação do humano (nunca declarar corrigido antes) | + +## O bug + +No header público da Central de Conteúdos (`/conteudos`), a marca aparecia "muito grande e cortada": a base de "SOLLA" sumia. Afetava as três superfícies do header — catálogo `/conteudos`, página da peça `/conteudos/` e não-encontrado — em desktop e mobile. + +A reprodução read-only em produção (2026-09-25 ~00:07 BRT) mediu os dois breakpoints: a 1280px a janela de recorte era 188×50 e a tinta real 187,4×67,6 (18px cortados); a 390px a janela era 142×39 e a tinta 141,7×51,1 (12,2px cortados). Sem perda de dados; branding/marca no header da Central na reta final. + +## Causa-raiz + +5 whys: + +1. A base de "SOLLA" sumia porque a janela de 188×50 cobria uma tinta de 67,6px de altura. +2. A janela foi dimensionada "no olho" como o lockup visível, sem derivar da caixa da tinta medida. +3. O design hand-authored os crops sem passo de `trim`/alpha bbox do ativo. +4. O design foi lido como spec, e o e2e `frontendConteudos` só checava texto/heading — nenhum teste pinava geometria. +5. `scripts/lib/campaignKitAssets.mjs` é só leitor de data URI (zero validação de geometria) e o risco do S36 ficou em prosa. + +O PNG oficial `public/campaign-kit/jorge-solla-negativo.png` tem canvas 1037×595 com tinta visível (alpha bbox) 790×285 nos offsets esquerda 123 / topo 162 (medido com `sharp(...).trim()`); a razão da tinta é 790/285 ≈ 2,77. A janela do S36 tinha razão 188/50 ≈ 3,76 (mobile 142/39 ≈ 3,64) — incompatível. Escalando a imagem para 246px de largura (mobile 186px), a tinta ficava 187,4×67,6 (mobile 141,7×51,1), mais alta que a janela, e o `overflow-hidden` decapitava a base. + +Origem: `src/components/conteudos/ContentPiecePageHeader.tsx:21` (janela) + `:28` (imagem/offsets), no estado pré-fix. O artefato de design aprovado `docs/plans/central-conteudos-header-logo-ui-design.html:52-73` já trazia os números errados (`.trimmed-lockup` 188×50 / img 246px / -29/-38; mobile 142×39 / 186px / -22/-29) e a implementação os portou fielmente. O peso-alvo declarado no S36 era a referência `/jingles` (`CampaignPageHeader.tsx:26`, tinta 51,6px desktop / 46,0px mobile) — a janela nunca teve a proporção da tinta. + +## Correção + +Em `src/components/conteudos/ContentPiecePageHeader.tsx`, a janela passa a ser a caixa da tinta no peso da referência `/jingles`: mobile `h-[46px] w-[128px]` (img 167px, offsets -19,8/-26,1) e desktop `sm:h-[52px] sm:w-[144px]` (img 189px, offsets -22,4/-29,5). O comentário do componente foi reescrito com a invariante (a janela tem a mesma razão da tinta; a moldura não) e a receita de recálculo (`sharp(asset).trim()` → caixa + trimOffset; imgWidth = inkHeight × 1037/285; offsets proporcionais). + +Nenhum outro consumidor foi tocado; `CampaignPageHeader` (`/jingles`) intocado; sem migration/access/Consent. + +## Verificação + +- Teste de regressão: `tests/e2e/frontendConteudos.e2e.spec.ts` — "keeps the whole brand ink inside the header crop, both breakpoints (S36)" — mede o alpha bbox real do PNG com sharp, calcula a caixa da tinta renderizada por bounding boxes e asserta que ela cabe na janela de recorte em 1280 e 390. Provado **RED** sem o fix (falha `ink bottom clipped @1280px`, esperado ≤57,5, recebido 75,04) e **GREEN** com o fix. +- Suíte: `pnpm test:e2e --no-deps --project=frontendConteudos` 12/12; `--project=frontendJingles` 5/5; `pnpm gate:fast` verde (lint + typecheck + unit 4685 em 423 arquivos); `pnpm format:check`, `pnpm knip` e `pnpm check:cycles` verdes. +- CI: pendente (PR ainda não aberto). +- Prod: pendente de approve humano e confirmação (nunca declarar corrigido antes). + +## Prevenção + +| Estratégia | Custo | Estado | +| ---------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------- | +| Teste de regressão e2e que mede o alpha bbox real do PNG e a tinta renderizada contra a janela nos dois breakpoints | barata | implementada agora (este PR) | +| Nota em `public/campaign-kit/README.md` §"Lockups do nome": derivar a janela da caixa da tinta medida, nunca do canvas | barata | implementada agora (este PR) | +| Comentário do dono com a invariante e a receita de recálculo (`sharp trim`) | barata | implementada agora (este PR) | +| Asset trimmed versionado no kit + pipeline de preparo | cara | documentada — não implementada neste fluxo (muda o ativo dono e todos os consumidores; Issue própria) | +| Passo de medição obrigatório no fluxo de design (`plan-issue` ui-design) | cara | documentada — não implementada neste fluxo | +| Harness de CI que acorde specs ao mudar `public/**` + baseline visual do header | cara | documentada — não implementada neste fluxo | + +**Estratégia implementada:** as três baratas acima — o teste e2e que mede o ativo real (não a intenção), a nota no README do kit e o comentário do dono com a receita de recálculo. + +**Estratégia documentada (cara):** as três caras acima — asset trimmed versionado com pipeline de preparo (candidata a Issue própria, pois muda o ativo dono e todos os consumidores), medição obrigatória no fluxo de design e harness de CI com baseline visual do header. + +## Lições + +- Design hand-authored como spec sem medição do ativo transfere erro geométrico para o port fiel: o S36 implementou os números exatos do artefato e o artefato estava errado. +- e2e de página que só checa texto/heading não guarda layout: nenhum teste media geometria, então a janela errada passou verde. +- "Fiel ao design" é fidelidade à intenção (peso da referência `/jingles`), não aos números do artefato — quando o número não fecha com a medição do ativo, a medição vence. diff --git a/public/campaign-kit/README.md b/public/campaign-kit/README.md index 51de5677..fc91f250 100644 --- a/public/campaign-kit/README.md +++ b/public/campaign-kit/README.md @@ -36,6 +36,13 @@ nos reels. **fundos claros** (plate do chrome sobre o site capturado). - **`jorge-solla-negativo.png`** — lockup do nome em branco, para fundos escuros. +- **Moldura transparente:** os lockups têm uma moldura grande no canvas e a + tinta visível é menor que o arquivo. Quem recorta por janela CSS + (`overflow-hidden`) deriva a janela da **caixa da tinta**, nunca do canvas: + meça com `sharp(asset).trim()` (`trimOffsetLeft/Top` + tamanho) e trave a + janela com o teste e2e `frontendConteudos` ("keeps the whole brand ink inside + the header crop"). O `jorge-solla-negativo.png` é canvas 1037×595 com tinta + 790×285 em (123, 162) — razão 2,77, não a da moldura. ### Número diff --git a/src/components/conteudos/ContentPiecePageHeader.tsx b/src/components/conteudos/ContentPiecePageHeader.tsx index d3800edd..0602e930 100644 --- a/src/components/conteudos/ContentPiecePageHeader.tsx +++ b/src/components/conteudos/ContentPiecePageHeader.tsx @@ -11,21 +11,27 @@ import { CONTENT_PIECE_FOCUS } from './contentPieceClasses' * S36 — the official lockup ships with a large transparent frame (canvas * 1037x595, visible ink 790x285: top 162, bottom 148, left 123, right 124), * so the header crops the frame with a fixed window instead of scaling the - * whole canvas: 142x39 mobile (ink 186px, left -22 / top -29) and 188x50 - * desktop (ink 246px, left -29 / top -38), per docs/plans/central-conteudos-header-logo-ui-design.html. + * whole canvas. The window must have the SAME aspect ratio as the ink (the + * frame does not); a mismatched window clips the base of "SOLLA" (production + * report 2026-09-25). Window = ink box at the /jingles reference weight: + * mobile 128x46 (img 167px -> ink 127.2x45.9, left -19.8 / top -26.1) and + * desktop 144x52 (img 189px -> ink 144x51.9, left -22.4 / top -29.5). + * To recompute after a kit PNG change: `sharp(asset).trim()` gives the ink box + * (size + trimOffset); imgWidth = inkHeight x 1037/285 and the offsets are the + * trim offsets x imgWidth/1037. The e2e spec asserts the fitted ink. */ export const ContentPiecePageHeader = ({ backHref }: { backHref?: string }) => (
- + Jorge Solla diff --git a/tests/e2e/frontendConteudos.e2e.spec.ts b/tests/e2e/frontendConteudos.e2e.spec.ts index f7fabfd6..6ebf888f 100644 --- a/tests/e2e/frontendConteudos.e2e.spec.ts +++ b/tests/e2e/frontendConteudos.e2e.spec.ts @@ -1,6 +1,8 @@ import { randomUUID } from 'node:crypto' +import { join } from 'node:path' import type { APIRequestContext, Page } from '@playwright/test' +import sharp from 'sharp' import { loadMunicipalityGeometryModule } from '../../src/lib/bahiaGeometries.js' import { getMunicipalityCatalogEntry } from '../../src/lib/municipalityCatalog.js' @@ -798,4 +800,62 @@ test.describe('Frontend Central de Conteúdos (S27)', () => { await waitForSettledPage(page) await expect(page.locator('summary').filter({ hasText: 'Lideranças' })).toHaveCount(0) }) + + /** + * S36 regression (production report 2026-09-25): the negative lockup ships + * with a transparent frame and the header crops it with a fixed window. A + * window with a different aspect ratio than the ink clips the base of + * "SOLLA" (that was the shipped S36: a 188x50 window over a 187.4x67.6 ink, + * 18px lost at the bottom). The real asset is measured with sharp and the + * rendered ink box must fit inside the crop window at both breakpoints. + */ + test('keeps the whole brand ink inside the header crop, both breakpoints (S36)', async ({ + page, + }) => { + const assetPath = join(process.cwd(), 'public/campaign-kit/jorge-solla-negativo.png') + const canvas = await sharp(assetPath).metadata() + const { info: ink } = await sharp(assetPath).trim().toBuffer({ resolveWithObject: true }) + const assetInk = { + canvasWidth: canvas.width!, + left: -ink.trimOffsetLeft!, + top: -ink.trimOffsetTop!, + width: ink.width, + height: ink.height, + } + + const measureAt = async (viewport: { width: number; height: number }) => { + await page.setViewportSize(viewport) + await page.goto('/conteudos') + await waitForSettledPage(page) + const logo = page.locator('header img[alt="Jorge Solla"]') + await expect(logo).toBeVisible() + return logo.evaluate((node, box) => { + const image = node as HTMLImageElement + const crop = image.parentElement!.getBoundingClientRect() + const rendered = image.getBoundingClientRect() + const scale = rendered.width / box.canvasWidth + return { + crop: { top: crop.top, right: crop.right, bottom: crop.bottom, left: crop.left }, + ink: { + top: rendered.top + box.top * scale, + right: rendered.left + (box.left + box.width) * scale, + bottom: rendered.top + (box.top + box.height) * scale, + left: rendered.left + box.left * scale, + }, + } + }, assetInk) + } + + for (const viewport of [ + { width: 1280, height: 900 }, + { width: 390, height: 844 }, + ]) { + const { crop, ink: renderedInk } = await measureAt(viewport) + const at = `@${viewport.width}px` + expect(renderedInk.top, `ink top clipped ${at}`).toBeGreaterThanOrEqual(crop.top - 0.5) + expect(renderedInk.left, `ink left clipped ${at}`).toBeGreaterThanOrEqual(crop.left - 0.5) + expect(renderedInk.bottom, `ink bottom clipped ${at}`).toBeLessThanOrEqual(crop.bottom + 0.5) + expect(renderedInk.right, `ink right clipped ${at}`).toBeLessThanOrEqual(crop.right + 0.5) + } + }) })