From 2ad36d09cf81d72fc5926204e77f129c344725de Mon Sep 17 00:00:00 2001
From: Francisco Solla
Date: Sat, 12 Sep 2026 02:31:49 -0300
Subject: [PATCH] =?UTF-8?q?refactor(S13):=20endurece=20o=20compositor=20ap?=
=?UTF-8?q?=C3=B3s=20review?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- font: espera a família primária (check falhava pela fallback do next/font)
- load de assets/fonte separado do redraw (sem flicker/live-region por tecla)
- pan/zoom com updates funcionais; photoSize memoizado
- foco volta ao tile do catálogo ao fechar (trigger da home desmonta)
- copy de privacidade centralizada; tile deriva dims/cor do modelo
- aria do input de arquivo sem colidir com o botão; +2 unit tests
---
.../cards-personalizados-campanha-impl.md | 12 +-
src/app/(frontend)/(home)/cards/page.tsx | 5 +-
src/app/(payload)/admin/importMap.js | 2 -
src/components/cards/CardComposer.tsx | 150 ++++++++++--------
src/components/cards/CardModelGallery.tsx | 2 +
src/components/cards/CardModelTile.tsx | 12 +-
src/components/cards/CardsStudio.tsx | 14 ++
src/components/cards/cardCanvas.ts | 13 +-
src/components/cards/cardCopy.ts | 8 +
src/lib/cardNameFit.ts | 2 +-
src/lib/cardRender.ts | 9 +-
tests/e2e/frontend.e2e.spec.ts | 6 +-
tests/unit/cardNameFit.unit.spec.ts | 24 +++
tests/unit/cardPhotoTransform.unit.spec.ts | 7 +
14 files changed, 173 insertions(+), 93 deletions(-)
create mode 100644 src/components/cards/cardCopy.ts
diff --git a/docs/plans/cards-personalizados-campanha-impl.md b/docs/plans/cards-personalizados-campanha-impl.md
index 7c487727..fd258dde 100644
--- a/docs/plans/cards-personalizados-campanha-impl.md
+++ b/docs/plans/cards-personalizados-campanha-impl.md
@@ -101,7 +101,7 @@ flowchart LR
- **`src/app/(frontend)/(home)/CampaignCardsSection.tsx`** (novo): seção server da home no molde de `CampaignNewsletterSection.tsx` — `id="cards"`, `aria-labelledby`, `data-home-section="cards"`, `mx-auto max-w-[1160px] px-5 py-12 sm:px-8 lg:px-10 lg:py-16`, eyebrow/título/corpo com `.campaign-section-*`, CTA `--pt-red`; usa `CardModelGallery variant="link"` (tiles levam a `/cards?model=`).
- **`src/app/(frontend)/(home)/page.tsx`** (editar): `` logo após ``, dentro do ``, antes de ``/`CampaignFooter`.
- **`src/app/(frontend)/fonts.ts`** + **`src/app/(frontend)/fonts/Brexter-Bold.ttf`** (novos): `export const brexterBold = localFont({ src: './fonts/Brexter-Bold.ttf', weight: '700', style: 'normal', display: 'swap' })`; só a família (string) cruza para os componentes.
-- **`src/lib/cardModels.ts`** (novo, puro): `CardModelId`, `CardModel`, `CARD_MODELS`, `isCardModelId`, `NAME_SLOT` (centerX 570 / capHeight 106 / maxInkWidth 714 / bandTop 430 / bandBottom 536 / ink `#ffec01`), `PHOTO_WINDOWS` (quadrada 1000×1000 janela y[0..740]; vertical 1000×1440 janela y[0..1044]), `OUTPUT` dims e paths ASCII. Fonte única do catálogo (home + `/cards` + render).
+- **`src/lib/cardModels.ts`** (novo, puro): `CardModelId`, `CardModel`, `CARD_MODELS`, `isCardModelId`, `NAME_CARD_SLOT` (centerX 570 / capTop 430 / capHeight 106 / maxInkWidth 714 / maxBlockBottom 630 para 2 linhas / minCapHeight 48 / ink `#ffec01`), janelas de foto em `CARD_MODELS[i].photoWindow` (quadrada 1000×1000 janela y[0..740]; vertical 1000×1440 janela y[0..1044]), dims de saída nos próprios modelos e paths ASCII. Fonte única do catálogo (home + `/cards` + render).
- **`src/lib/cardNameFit.ts`** (novo, puro): `fitCardName(name, measure, slot)` + `resolveFontSizeForCapHeight`; retorno `{ ok: true; lines; fontSize; lineHeight } | { ok: false; reason: 'too-long' }`; `MIN_CAP_HEIGHT` fill-in (~48px, validado no craft); nunca trunca.
- **`src/lib/cardPhotoTransform.ts`** (novo, puro): cover scale na janela, `clampPhotoTransform`, `photoDrawRect`; zoom 1–4; pan clampado para nunca abrir gap na janela (inclui a contra-forma transparente dentro dos limites da janela).
- **`src/lib/cardRender.ts`** (novo, puro): interfaces estruturais mínimas `CardRenderContext`/`CardTextMetrics` (o `CanvasRenderingContext2D` real as satisfaz); `renderNameCard` (base → nome) e `renderPhotoCard` (foto → máscara, nessa ordem); sem dependência de DOM/Next.
@@ -155,7 +155,6 @@ Não se aplica (data-presentation Q3): nenhum KPI, mapa ou série; a única “f
4. **Craft/Critique/Polish + e2e + gates** — quota ~0,5 dia. Impeccable C; e2e novo + prewarm + manifest; changelog; `pnpm gate:fast`; e2e local do describe (`pnpm test:e2e --no-deps -- tests/e2e/frontend.e2e.spec.ts -g "Cards personalizados"`); `pnpm push`.
## Rabbit holes / Não escopo (engenharia)
-
- Não criar collection/global/migration/CMS para modelos (o catálogo é código).
- Não criar um segundo compositor na home: o tile da home é link e só `/cards` edita.
- Não recriar faixas/contra-formas das artes em CSS/SVG; overlay é o PNG mestre.
@@ -167,7 +166,7 @@ Não se aplica (data-presentation Q3): nenhum KPI, mapa ou série; a única “f
## Riscos e mitigação
-- **Fonte não pronta no draw** (medida com fallback desalinha o fit): só desenhar após `document.fonts.load` e `document.fonts.check` na família primária; estado de carregamento no preview.
+- **Fonte não pronta no draw** (medida com fallback desalinha o fit): só desenhar após `document.fonts.load` da família primária (`check` falha pela fallback gerada do next/font); estado de carregamento no preview.
- **Canvas tainted/cross-origin**: assets same-origin em `public/cards/`; nunca URL externa.
- **Foto HEIC/grande/corrompida**: decode em try/catch com erro recuperável (`Escolher outra foto`); formatos suportados pelo browser seguem funcionando; limite defensivo de tamanho sem bloquear caso comum.
- **Gap no pan/zoom**: clamp puro testado; a foto é desenhada antes do overlay, então nem uma borda vazaria.
@@ -177,6 +176,13 @@ Não se aplica (data-presentation Q3): nenhum KPI, mapa ou série; a única “f
- **Cold compile do `/cards` no e2e dev**: entrada no prewarm do `setup.e2e.spec.ts`.
- **Formato TTF no `next/font/local`**: se o build reclamar, converter para WOFF2 na mesma fase (família/permissão inalteradas).
+## Débitos diferidos (triage pós-review, 2026-09-12)
+
+- **Extrair `PhotoControls`/`NameField` do `CardComposer`** (cheap_polish, score 2) — gatilho: próximo toque funcional no composer (novo passo/controle) ou regressão de interação no bloco de foto.
+- **Helper único do idioma download Blob → object URL → ``** (2 call sites hoje) — gatilho: 3º call site.
+- **Cobertura de pan por teclado no e2e e dos re-checks do fit de duas linhas no unit** — gatilho: próximo toque em `cardNameFit`/`CardComposer` ou regressão de teclado/duas linhas em UAT.
+- Descartados na triagem: união discriminada do `CardModelGallery` e inline do `CardPreviewCanvas` (polish sem gatilho; invariantes já garantidas pelos call sites atuais).
+
## Aceite de engenharia
- [ ] Aceite de produto da intenção ainda coberto: seção na home após a newsletter; 3 modelos (grid/carrossel com peek); CTA e tiles levam a `/cards`; composer modal desktop/bottom drawer mobile; nome só na banda do mestre, sem corte silencioso; foto com pan/zoom sobre a máscara; aviso de privacidade; resultado + download `PNG`; erro recuperável; toque e teclado.
diff --git a/src/app/(frontend)/(home)/cards/page.tsx b/src/app/(frontend)/(home)/cards/page.tsx
index 8a83b6b6..c0e16153 100644
--- a/src/app/(frontend)/(home)/cards/page.tsx
+++ b/src/app/(frontend)/(home)/cards/page.tsx
@@ -3,13 +3,12 @@ import Link from 'next/link'
import { brexterBold } from '@/app/(frontend)/fonts'
import { CampaignFooter } from '@/components/CampaignFooter'
+import { CARD_PRIVACY_NOTE } from '@/components/cards/cardCopy'
import { CardsStudio } from '@/components/cards/CardsStudio'
import { isCardModelId } from '@/lib/cardModels'
const intro =
'Escolha um dos três modelos, personalize com seu nome ou sua foto e baixe para compartilhar.'
-const privacyNote =
- 'Seu nome e sua foto são processados apenas no seu aparelho e não são enviados para nós.'
export const metadata: Metadata = {
title: 'Crie seu card de apoio | Jorge Solla',
@@ -52,7 +51,7 @@ export default async function CardsPage({ searchParams }: CardsPageProps) {
Crie seu card de apoio