mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:45:18 +00:00
fix(S36-FOLLOWUP): logo do header da Central sem corte — janela na razão da tinta
This commit is contained in:
parent
c472c5c0b3
commit
c013de13d6
5 changed files with 156 additions and 4 deletions
1
docs/changelog/2026-09-25-central-logo-cortado.md
Normal file
1
docs/changelog/2026-09-25-central-logo-cortado.md
Normal file
|
|
@ -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`.
|
||||
78
docs/postmortems/2026-09-25-central-logo-cortado.md
Normal file
78
docs/postmortems/2026-09-25-central-logo-cortado.md
Normal file
|
|
@ -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/<slug>` 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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<header className="bg-[#ae1603]">
|
||||
<div className="mx-auto flex h-14 w-full max-w-6xl items-center justify-between gap-3 px-5 sm:h-16 sm:px-8">
|
||||
<Link href="/" aria-label="Início — Jorge Solla 1313" className={CONTENT_PIECE_FOCUS}>
|
||||
<span className="relative block h-[39px] w-[142px] shrink-0 overflow-hidden sm:h-[50px] sm:w-[188px]">
|
||||
<span className="relative block h-[46px] w-[128px] shrink-0 overflow-hidden sm:h-[52px] sm:w-[144px]">
|
||||
<Image
|
||||
src="/campaign-kit/jorge-solla-negativo.png"
|
||||
alt="Jorge Solla"
|
||||
width={1037}
|
||||
height={595}
|
||||
priority
|
||||
className="absolute top-[-29px] left-[-22px] h-auto w-[186px] max-w-none sm:top-[-38px] sm:left-[-29px] sm:w-[246px]"
|
||||
className="absolute top-[-26.1px] left-[-19.8px] h-auto w-[167px] max-w-none sm:top-[-29.5px] sm:left-[-22.4px] sm:w-[189px]"
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue