diff --git a/.agents/rules/codebase-map.mdc b/.agents/rules/codebase-map.mdc index cd96e027..1d7f0bd6 100644 --- a/.agents/rules/codebase-map.mdc +++ b/.agents/rules/codebase-map.mdc @@ -22,7 +22,7 @@ Conceitos e decisões: `docs/ARCHITECTURE.md`. Operação (setup/seeds/deploy/se - `src/components/campaign//` — municipality, activity, supporter, leadership, advisor, demand, organization, stateDeputy, votePledge, invite, map, dashboard, auth, shell (sidebar/nav/PWA), shared (sistema de listas + widgets genéricos). - `src/components/cards/` — estúdio público de cards personalizados (S13): `CardsStudio` (seleção + shell dialog/drawer), `CardComposer`, `CardModelGallery`/`CardModelTile`, `CardPreviewCanvas`, adapters em `cardCanvas.ts`/`cardCutout.ts`/`cardPhotoHarmonyCanvas.ts` (S17). Geometria/catálogo/tom puros em `src/lib/card{Models,NameFit,PhotoTransform,Render,Harmony}.ts`. - `src/components/ui/` — kit shadcn da casa (sem peças mortas; `shadcn add` re-adiciona). -- `src/lib/schemas/` — zod de entrada; `src/lib/` também guarda catálogos estáticos e o artefato TSE (`electionAggregates/`), além dos módulos puros do estúdio de cards públicos (`cardModels`, `cardNameFit`, `cardPhotoTransform`, `cardRender`, `cardPhotoHarmony`) e do roster estático das 53 dobradinhas (`stateDeputyCatalog`, S30 — snapshot em `tests/fixtures/state-deputy-catalog.snapshot.json`; artes derivadas por `scripts/build-state-deputy-card-assets.mjs`). +- `src/lib/schemas/` — zod de entrada; `src/lib/` também guarda catálogos estáticos e o artefato TSE (`electionAggregates/`), além dos módulos puros do estúdio de cards públicos (`cardModels`, `cardNameFit`, `cardPhotoTransform`, `cardRender`, `cardPhotoHarmony`) e do roster estático das 53 dobradinhas (`stateDeputyCatalog`, S30 — snapshot em `tests/fixtures/state-deputy-catalog.snapshot.json`; artes derivadas por `scripts/build-state-deputy-card-assets.mjs`), além da geometria/linhas da colinha (`cardColinha`, S31 — topo/linha legal/corpo medidos do gate, `colinhaVoteRows` e o fit do candidato). - `src/migrations/` — história congelada; nunca editar retroativamente. - `tests/` — `unit` primeiro (lógica pura, DATABASE_URL inválida), depois `int` para fronteiras Payload/DB (Payload real em `teqo_test`, fixtures em `tests/helpers/campaignFixtures.ts`), e `e2e` só com benefício real por fluxo que as camadas inferiores não cobrem (Playwright smoke; dono da definição: skill `test-driven-development`). diff --git a/docs/changelog/2026-09-23-s31-minha-colinha.md b/docs/changelog/2026-09-23-s31-minha-colinha.md new file mode 100644 index 00000000..cd674ac7 --- /dev/null +++ b/docs/changelog/2026-09-23-s31-minha-colinha.md @@ -0,0 +1 @@ +**S31 — "Minha colinha": o estadual escolhido na colinha de voto, sexto modelo do estúdio (2026-09-23):** o funil de cards (`#cards` da home + `/cards` + `?model=minha-colinha`) ganha a colinha de voto no MESMO estúdio e sem segundo editor: o visitante escolhe um estadual na MESMA lista de S30 (`stateDeputyCatalog`, 53 entradas; `StateDeputySelect` reusado intocado) e baixa um PNG 1080×1920 (9:16) com a linha do escolhido preenchida — nome de urna em caixa alta e os cinco dígitos, um por caixa — em volta das cinco escolhas fixas (`DEPUTADO FEDERAL Jorge Solla 1313`, `SENADOR Jaques Wagner 130`, `SENADOR Rui Costa 133`, `GOVERNADOR Jerônimo 13`, `PRESIDENTE Lula 13`), cada linha com o selo verde `CONFIRMA`. O render novo é uma composição canvas pura portada classe-a-classe do gate em 1080×1920: `src/lib/cardColinha.ts` guarda a geometria `COLINHA_*` medida no browser (topo de 710,4 com a foto do grupo em cover, caixa da marca no gradiente 125° `#e4102f`→`#184e92` com a `marca-negativa-completa`, faixa vermelha recortada de `team-card-front`, linha legal vertical de 15,66 e o corpo `30% | 1fr | auto` com linhas de 108) + a tabela das cinco linhas fixas + `colinhaVoteRows(deputy)` (estadual preenchido ou vazio com cinco caixas e `CONFIRMA` esmaecido) + `fitColinhaCandidate` (encolhe em uma linha e, no piso de 18, quebra em duas por palavra — o ritmo fixo das seis linhas nunca reflui); `renderColinhaCard` mora no dono único (`src/lib/cardRender.ts`, cujo `CardDrawContext` ganhou membros reais `roundRect`/`clip`/`createLinearGradient`/`letterSpacing`/`globalAlpha` e um peso opcional no `createCardMeasure`, com S13–S30 intocados). O `CardComposer` ganhou o branch `kind:'colinha'` de passo único — CTA `Baixar minha colinha` baixa direto da prévia compartilhada, `Cancelar` + primário no dialog e só o primário full-width no drawer (cenas 2/5 do gate) — com os literais aprovados (`Escolha seu estadual` / `Pronta para levar com você` / `Pronta para baixar`, aviso de escolha, caixa `Linha conferida`, nota nova `A escolha é processada neste aparelho…`) e prévias compacta (145) e grande (405/300). A tipografia do slip é a do gate (`Arial, Helvetica, sans-serif` via `COLINHA_FONT_FAMILY`, no desenho e na medição — não a display da campanha usada no resto do estúdio) e a descrição da colinha vive só no dialog (o cabeçalho do drawer mobile segue a cena 5, com o seletor na primeira viewport); os dois ajustes saíram da crítica final do `designer` (1ª passada DEGRADED, 2ª certificada). O tile usa o arquivo exato entregue pelo humano copiado byte-a-byte (`public/cards/modelo-colinha.jpeg`, 900×1600, sha256 pinado); a galeria passa a seis tiles numa fileira (`lg:grid-cols-6`), seis dots no mobile e a dica "Deslize para ver os seis modelos"; o badge `NOVO` passa para o modelo novo e o `?model=`/`isCardModelId` cobrem o id de graça. Sem migration, collection, CMS, Consent ou upload: a escolha não sai do aparelho e nada do visitante é solicitado. Verificação: unit (`cardColinha` com pins de geometria/linhas/fit e sha do tile, `cardModels` com o 6º modelo e o badge movido, `renderColinhaCard` no recorder falso com `roundRect`/clip/gradiente), `pnpm gate:fast` e e2e do `frontend` (6 tiles + 6 dots, seleção por busca/teclado, prévia vazia com CTA desabilitado e probe de pixel, linha preenchida, download real `card-jorge-solla-minha-colinha.png` com assinatura PNG e troca do estadual). diff --git a/docs/plans/cards-colinha-impl.md b/docs/plans/cards-colinha-impl.md new file mode 100644 index 00000000..77238957 --- /dev/null +++ b/docs/plans/cards-colinha-impl.md @@ -0,0 +1,203 @@ +# Impl: Minha colinha — o estadual escolhido na colinha de voto + +Status: aprovado (modo --auto) +Atualizado em: 2026-09-23 +Issue: #1272 +Intenção: docs/plans/cards-colinha.md +Appetite restante: herdado — ~1–1,5 dia eng (3 fases somam ≤1,3 dia; sem migration; o único peso novo é 1 asset copiado byte-a-byte) + +## Leitura da intenção + +- **Outcome:** o estúdio de cards ganha o sexto modelo `Minha colinha`: o visitante escolhe um estadual na MESMA lista de S30 e recebe uma colinha 9:16 (PNG 1080×1920) com o topo aprovado (foto do grupo + lockup + faixa `JORGE SOLLA 1313` e as marcas menores), a linha legal vertical na borda esquerda e seis linhas de voto — cinco fixas (`DEPUTADO FEDERAL Jorge Solla 1313`, `SENADOR Jaques Wagner 130`, `SENADOR Rui Costa 133`, `GOVERNADOR Jerônimo 13`, `PRESIDENTE Lula 13`) e a do estadual preenchida com nome de urna + cinco dígitos, um por caixa — cada uma com o selo verde `CONFIRMA`. +- **O que NÃO negociar:** + - 100% no aparelho: sem nome do visitante, sem foto, sem upload, sem persistência/analytics; os cinco modelos existentes intocados além do badge. + - Um único editor/fluxo: sexto item do MESMO `CardsStudio`/`CardComposer`; `#cards`, `/cards` e `?model=minha-colinha` continuam. + - Catálogo de estaduais = o de S30 (`src/lib/stateDeputyCatalog.ts`, 53 entradas): um estadual, uma linha; sem escolha não há download; o seletor S30 é reusado sem tocar na superfície entregue. + - Fidelidade ao gate: topo, linha legal vertical na borda esquerda e corpo de linhas seguem `docs/plans/cards-colinha-ui-design.html` (cenas 1–5). + - Escolha por toque e teclado; sem página/rota por colinha; a nota de privacidade precisa permanecer verdadeira (a existente fala de nome/foto). +- **O que reavaliar (hipóteses da intenção):** + - **“O `CardComposer` ramifica por `kind`”**: confirmado (`CardComposer.tsx:206-208` e ternários ao longo do arquivo); a colinha é o primeiro `kind` novo desde S13 e o branch fica isolado — sem flag extra dentro do `kind:'team'`. + - **“Catálogo e assets são os de S30”**: confirmado — reuso puro; nenhuma entrada/asset de estadual nasce aqui. + - **“Pipeline `renderX → preview canvas → PNG download` reaproveitado”**: confirmado — o render novo (`renderColinhaCard`) mora no dono único (`cardRender.ts`) e usa `CardPreviewCanvas`/`cardCanvas`/`cardFileName`. + - **“NEEDS ASSET do topo”**: resolvido no gate — o topo é composição medida a partir do kit (`team-card-base.png` + `marca-negativa-completa.png` + `team-card-front.png`); não há asset-mestre novo nem script. + - **“`CardModelTile`/galeria”**: o tile já cai no `Image` com `previewSrc` para `kind` desconhecido (só o `sizes` acompanha as seis colunas); a galeria passa a seis colunas. + +## Abordagem recomendada + +```mermaid +flowchart LR + H["home #cards / rota /cards"] --> S["CardsStudio — ilha única"] + S --> G["CardModelGallery — 6 tiles + 6 dots"] + G -->|"tile Minha colinha"| C["CardComposer — branch kind:'colinha' (passo único)"] + C --> SEL["StateDeputySelect — S30, intocado"] + SEL --> CAT["stateDeputyCatalog — 53 entradas (S30)"] + C --> RC["renderColinhaCard — cardRender.ts (dono único)"] + MOD["cardColinha.ts — COLINHA_* + colinhaVoteRows"] --> RC + AST["team-card-base + team-card-front + marca-negativa (kit)"] --> RC + RC --> PV["prévia canvas compartilhada"] + PV --> DL["Baixar minha colinha — PNG 1080×1920"] + TILE["public/cards/modelo-colinha.jpeg — cópia byte-a-byte"] --> G +``` + +**Opções consideradas:** A) sexto modelo no estúdio existente com composição canvas portada do gate + seletor de S30 reusado | B) usar o `modelo-colinha.jpeg` como fundo e sobrepor só a linha do estadual | C) asset-mestre novo gerado por script sharp + render de camadas. +**Recomendação:** A — o outcome é um modelo no estúdio existente; portar o gate classe-a-classe mantém a fidelidade aprovada, usa só o kit oficial e reaproveita catálogo/seletor/preview/download; cabe no appetite (um render novo, nenhuma infra). +**Rejeitadas:** B porque o modelo aprovado não tem slot para o nome dinâmico (a linha do estadual está queimada e o rótulo não comporta nome variável); C porque cria mestre novo não aprovado e um script para um asset quando o gate já define a composição. + +### Decisões de engenharia + +1. **Sexto modelo — `kind:'colinha'` novo + flag `stateDeputyPicker: true` de S30.** + Opções: A) novo `kind:'colinha'` + `stateDeputyPicker:true` | B) reusar `kind:'team'` | C) uma entrada por estadual (53 modelos). + Recomendação: A — o fluxo não tem foto/recorte/nome (nada da esteira S15), mas reusa o eixo do catálogo de S30; o `kind` discrimina os branches de composer/render e `isCardModelId`/`?model=` cobrem o id de graça. + Rejeitadas: B porque arrastaria janela de foto/recorte/harmonia/nome para um fluxo que não tem nenhum (branch por flag extra dentro do `team`); C porque explode galeria/pins/URL. + +2. **Render — composição canvas pura portada classe-a-classe do gate (não o `modelo-colinha.jpeg` como fundo).** + Opções: A) portar a composição do gate (medidas em px, canvas puro) | B) `modelo-colinha.jpeg` como fundo + linha do estadual desenhada por cima | C) mestre novo por script sharp + camadas. + Recomendação: A — a doutrina do estúdio porta o artefato aprovado e a intenção diz que o topo é “definida no design hi-fi e montada a partir do kit oficial”; o `modelo` não tem slot para o nome dinâmico (rótulo queimado). + Rejeitadas: B porque o nome/dígitos não têm onde entrar; C porque é mestre novo não aprovado + script, quando o gate já define a composição. + +3. **Módulos — `src/lib/cardColinha.ts` puro + `renderColinhaCard` no dono do render.** + Opções: A) módulo puro novo (`COLINHA_*`, tabela fixa, `colinhaVoteRows`) + renderer em `src/lib/cardRender.ts` | B) tudo dentro do `cardRender.ts` | C) renderer standalone em módulo próprio. + Recomendação: A — o layout/tabela é conhecimento próprio, testável sem canvas; o renderer fica no dono único de prévia/export (um call site), como S13–S30. + Rejeitadas: B porque espalha a tabela de linhas no arquivo de desenho; C porque seria um segundo dono de render (twin). + +4. **Geometria — px medidos do gate em 1080×1920, constantes `COLINHA_*` pinadas em unit; candidato shrink-to-fit em uma linha.** + Opções: A) portar os valores medidos do gate em px | B) recalcular percentuais/`cqw` em runtime | C) desenhar na escala 900×1600 do tile. + Recomendação: A — canvas não tem CSS; os valores do gate em 1080×1920 são a régua (re-medir no browser se algo divergir ao portar). Valores: + - **topo:** y `0 → 710,4` (37% de 1920), fundo `#148fc2`; foto do grupo `team-card-base.png` (1080×1440) em cover, desenhada em `(0, −335,62, 1080, 1440)` com clip no bloco. + - **caixa da marca:** `(32,4; 21,29; 529,19×134,97)`; fundo gradiente linear 125° `#e4102f` 0→52% e `#184e92` 52→100% (linha CSS: dir `(sin125°, −cos125°)`, comprimento `|w·sinθ| + |h·cosθ|`, stops via `createLinearGradient`); lockup `public/campaign-kit/marca-negativa-completa.png` (1037×595) contido → rect desenhado `(226,4; 48,29; 141,1×80,97)` (o padding percentual do gate resolve contra o bloco de 1080 = 27px, não contra a caixa; não recalcular o contain a partir de “13,23”). + - **faixa vermelha:** `(0; 504,39; 1080×206,0)`, fundo `#e4102f`; arte `public/cards/team-card-front.png` (1080×1440) recortada nas linhas de origem `1232,36 → 1438,36`, desenhada com clip em `dy = 504,39 − 1232,36 = −727,97`. + - **linha legal vertical:** box `(7,55; 748,79; 15,66×1132,81)`; texto `FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL (PT-PC DO B - PV) | CNPJ CANDIDATO: 68.430.467/0001-05`; `700 15,66px`, letter-spacing ~0,31, cor `#333`, rotacionada −90° (lê de baixo para cima). + - **corpo:** `(51,83; 710,39; 1028,17×1209,61)`; padding topo 34,56 / lados 35,64 / base 32,4; gap entre linhas 34,56; centralizado verticalmente; cada uma das 6 linhas min-height 108; grid da linha = `30% | 1fr | auto` com gap 21,6 (coluna do rótulo ≈287,07). Derivado: primeira linha em y ≈ 905,88 e passo 142,56 (6×108 + 5×34,56 = 820,8 no miolo de 1142,65). + - **rótulo do cargo:** `900 27px` (line-height 1,02), caixa alta, `#171717`, quebras `DEPUTADO\nFEDERAL` e `DEPUTADO\nESTADUAL`, uma linha nos outros quatro. + - **candidato:** `900 26,46px`, `#e4102f`, gap ~9,6 acima; shrink-to-fit em UMA linha dentro da coluna de 287px, piso 18px — rejeitadas a quebra em duas linhas com altura variável (o CSS do gate quebra; o ritmo fixo das 6 linhas é melhor no canvas) e candidatos de duas linhas fixas. + - **caixas de dígito:** `59,39×73,44`, borda `5,94` `#111`, raio `6,48`, fundo branco, dígito `900 46,44px` centrado; gap 8,1; vazia = borda `#9a9a9a`, fundo `#fafafa`, dígito invisível. + - **pill CONFIRMA:** raio cheio, fundo `#009647`, padding `17,28×21,6`, `900 21,6px` branco; vazia = 30% de opacidade (fill `#0096474d`, sem `globalAlpha` no subset). + - **posições:** CONFIRMA na borda direita da linha (coluna `auto`); dígitos alinhados à esquerda na coluna do meio. + Rejeitadas: B porque canvas não interpreta CSS e a conversão runtime reintroduz erro de medição; C porque a régua aprovada é 1080×1920 (o tile 900×1600 é só arte de galeria). + +5. **`CardDrawContext` ganha `roundRect` (fill-only).** + Opções: A) adicionar `roundRect` real e compor bordas com dois preenchimentos (externo na cor da borda, interno no fill) | B) `stroke`/`lineWidth` no subset | C) cantos com arcos manuais. + Recomendação: A — método real, subset mínimo e fill-only; caixas (borda 5,94), pill e estados vazios saem de dois `roundRect` preenchidos; o recorder falso registra a op. Pela geometria medida o subset ganha também os membros reais `clip` (recortes do topo/faixa), `createLinearGradient` (caixa da marca) e `letterSpacing` (linha legal). + Rejeitadas: B porque stroke centrado exigiria meia-borda na geometria e polui o subset; C porque reinventa `roundRect`. + +6. **Composer — passo único, CTA baixa direto da prévia compartilhada.** + Opções: A) sem `result`: CTA `Baixar minha colinha` baixa direto do canvas da prévia; desabilitado até estadual + imagens prontas; `Gerando…` durante | B) dois passos compose→result como os outros modelos | C) dois CTAs (criar + baixar). + Recomendação: A — não há nome/foto para confirmar; as cenas 2 e 5 do gate mostram um único CTA; evita estado novo. Rodapé fiel ao gate: dialog = `Cancelar` + primário (cena 2); drawer = só o primário full-width (cena 5). Prévias: compacta pré-escolha (cena 2, 145px) e grande pós-escolha (cena 3 desktop 405px / cena 5 mobile 300px) — no branch da colinha o `max-h-[38dvh]` do S15 não se aplica (o 9:16 precisa da largura da cena 3 e rola com o corpo); os demais modelos ficam intocados. + Rejeitadas: B porque adiciona tela de confirmação sem conteúdo novo; C porque não existe no design. + +7. **Tile — cópia byte-a-byte do asset aprovado; badge `NOVO` muda de modelo.** + Opções: A) copiar `docs/plans/cards-colinha-ui-design-assets/modelo-colinha.jpeg` para `public/cards/modelo-colinha.jpeg` (900×1600, sha256 pinado) | B) derivativo via script sharp | C) referenciar o arquivo de `docs/plans/` no `src`. + Recomendação: A — precedente S30; `docs/` não é servido em produção; um asset não justifica script. O badge `NOVO` sai de `time-do-estadual` e entra em `minha-colinha` (literal assumido, gate cena 1). + Rejeitadas: B (script para 1 arquivo; mestre novo não aprovado); C (não é servido pelo Next). + +8. **Galeria — seis tiles na mesma fileira.** + Opções: A) `lg:grid-cols-6` mantendo `lg:gap-4` + dica `Deslize para ver os seis modelos` (os dots já derivam de `CARD_MODELS.length`) | B) manter 5 colunas e esconder o 6º no scroll | C) extrair carrossel/dots genérico. + Recomendação: A — o gate cena 1 desenha seis colunas; dots e contagem já são derivados; nada de abstração nova. `/cards` passa a `Escolha um dos seis modelos, personalize e baixe para compartilhar.` e o doc-comment da home (`CampaignCardsSection.tsx:5`) “five”→“six”. + Rejeitadas: B viola o gate; C abstração sem segundo consumidor. + +9. **Entrada do modelo.** `id:'minha-colinha'`, `kind:'colinha'`, `label:'Minha colinha'`, `badge:'NOVO'`, `stateDeputyPicker:true`, `assetSrc:'/cards/team-card-base.png'` (foto do grupo), `overlaySrc:'/cards/team-card-front.png'` (arte da faixa), `lockupSrc:'/campaign-kit/marca-negativa-completa.png'` (novo campo opcional de `CardModel`), `previewSrc:'/cards/modelo-colinha.jpeg'`, `width:1080`, `height:1920`, sem `photoWindow`. + +10. **Copy do composer — literais do gate + nota nova; seletor S30 intocado.** + Opções: A) portar os literais das cenas 2/3/5 e criar constante nova em `cardCopy.ts` | B) reusar `CARD_PHOTO_PRIVACY_NOTE` | C) reescrever a copy. + Recomendação: A — a nota existente fala de “nome e foto” (falsa aqui: nada é digitado/enviado); a nova é o literal do gate. `StateDeputySelect` e sua dica seguem intocados (superfície S30 não é tocada). + Rejeitadas: B porque a frase não corresponde ao fluxo sem foto; C porque o gate é a fonte. + +11. **Case dos textos.** Opções: A) rótulos UPPERCASE; fixos em Title Case como o gate/modelo; estadual UPPERCASE; vazio `Escolha abaixo` | B) tudo UPPERCASE | C) estadual em Title Case como no catálogo. + Recomendação: A — fidelidade literal (cena 3/4: `JULIO PINHEIRO`; fixos `Jorge Solla`, `Jaques Wagner`, `Rui Costa`, `Jerônimo`, `Lula`); o catálogo guarda o nome de exibição e a colinha aplica `toLocaleUpperCase('pt-BR')`. + Rejeitadas: B muda os fixos aprovados; C diverge do gate. + +12. **Testes — estender os pins e o spec `frontend`; manifesto sem mudança.** + Opções: A) atualizar `cardModels`/`cardRender`, criar `cardColinha.unit.spec.ts` e estender `tests/e2e/frontend.e2e.spec.ts` | B) spec e2e novo separado | C) só unit. + Recomendação: A — o harness/probes do estúdio já vivem no spec `frontend`; `scripts/lib/e2e-affected-manifest.mjs` NÃO muda (o prefixo `src/lib/card` já casa `cardColinha.ts`, `src/components/cards` casa o composer e `public/` é ignorado); **int não se aplica** (sem Payload/DB/access/escrita multi-collection). + Rejeitadas: B fragmenta o harness e duplica setup; C não cobre o aceite visual (prévia/download/troca). + +### Componentes / mudanças + +- **`src/lib/cardColinha.ts`** (novo, puro, client-safe): geometria `COLINHA_*` (topo, marca, faixa, legal, corpo, linha, rótulo, candidato, dígito, confirmar; cores e pesos), `COLINHA_LEGAL_TEXT`, tabela fixa das 5 linhas e `colinhaVoteRows(deputy: StateDeputyCatalogEntry | null)` → 6 linhas na ordem do gate (federal, estadual, 2× senador, governador, presidente), com `officeLines`, `candidate`, `digits` (por caractere) e o marcador de estado vazio (5 slots + `Escolha abaixo` + CONFIRMA esmaecido). +- **`src/lib/cardRender.ts`** (editar): `CardDrawContext` ganha `roundRect` (+ `clip`, `createLinearGradient`, `letterSpacing` — membros reais exigidos pela geometria medida); `renderColinhaCard(ctx, model, args)` desenha na ordem medida — fundo branco → topo (fundo, foto recortada, caixa da marca com gradiente, faixa recortada) → linha legal rotacionada → 6 linhas (rótulo, candidato shrink-to-fit, caixas de dígito, pill) — reusando `CardMeasureText`; `createCardMeasure` ganha peso opcional com default 700 (S13–S30 intocados) para medir o candidato em 900. +- **`src/lib/cardModels.ts`** (editar): `CARD_MODEL_IDS` com 6 ids; `kind` ganha `'colinha'`; `CardModel` ganha `lockupSrc?: string`; entrada de S31 (Decisão 9) e remoção do `badge` de `time-do-estadual`. +- **`src/components/cards/CardComposer.tsx`** (editar): `isColinhaModel`; branch de carregamento (foto do grupo + faixa + lockup; `previewSrc` fica só no tile) e de render no canvas; sem `useCardCutout`/nome/harmonia no fluxo; título/eyebrow/descrição por estado; seletor S30 reusado; caixa `Linha conferida`; nota nova; CTA único (Decisão 6) chamando o `handleDownload` existente. +- **`src/components/cards/cardCopy.ts`** (editar): constante nova da nota de privacidade da colinha (`A escolha é processada neste aparelho. Nenhum nome, foto ou cadastro é solicitado.`). +- **`src/components/cards/CardModelGallery.tsx`** (editar): `lg:grid-cols-6` + `lg:gap-4`, dica `Deslize para ver os seis modelos`, doc-comment “five”→“six”. +- **`src/components/cards/CardModelTile.tsx`** (editar só o `sizes`): o `kind:'colinha'` cai no `Image` com `previewSrc` (fall-through já existente); o `sizes` de 5 para 6 colunas (`167px`). +- **`src/app/(frontend)/(home)/cards/page.tsx`** (editar): intro `Escolha um dos seis modelos, personalize e baixe para compartilhar.`. +- **`src/app/(frontend)/(home)/CampaignCardsSection.tsx`** (editar): doc-comment “five”→“six”. +- **`public/cards/modelo-colinha.jpeg`** (novo): cópia byte-a-byte do asset aprovado (900×1600, sha256 `76505492fb243c67313d237e58926afbd4d0847c00ab3bb43c090859cbdf18c8`). +- **Testes** (editar/novo): `tests/unit/cardModels.unit.spec.ts` (6 ids, pin do modelo novo + `lockupSrc`, badge movido), `tests/unit/cardRender.unit.spec.ts` (recorder falso com `roundRect`/`clip`/gradiente; ordem e medidas do `renderColinhaCard`; estado vazio), `tests/unit/cardColinha.unit.spec.ts` (novo, `// @vitest-environment node`: pins de `COLINHA_*`, `colinhaVoteRows` fixo/preenchido/vazio, sha256 + 900×1600 do tile via `sharp`), `tests/e2e/frontend.e2e.spec.ts` (loop de 6 tiles + badge em `minha-colinha`; 6 dots + `seis modelos`; describe novo `Cards personalizados (S31 — Minha colinha)`). +- **`scripts/lib/e2e-affected-manifest.mjs`**: **sem mudança** — `src/lib/card` já casa `cardColinha.ts`, `src/components/cards` cobre composer/galeria e assets em `public/` são ignorados pelo manifesto. +- **`docs/changelog/2026-09-23-s31-minha-colinha.md`** (novo). +- **Migration:** sem migration — nenhuma collection/global/field toca o schema; `push:false` intocado. +- **Access / Consent:** não se aplica — nenhuma superfície Payload, nenhum opt-in; o fluxo é 100% client-side e a escolha não sai do aparelho. +- **UI:** Impeccable B — shape (gate cenas 1–5) → craft (port classe-a-classe das medidas) → critique (a11y toque/teclado, mobile 390, contraste das caixas, copy) → polish. Shells/tokens reusados: `CardsStudio`, `ui/dialog`, `ui/Drawer`, `CardPreviewCanvas`, `StateDeputySelect`, `--pt-red`/`--campaign-*`; alvos ≥44px; `role=status`/`role=alert` herdados. **Crítica final (c) do `designer`:** 1ª passada DEGRADED com 2 ajustes — tipografia do canvas devia ser o stack do gate, não a display da campanha, e a descrição não devia ocupar o cabeçalho do drawer mobile (cena 5) — ambos aplicados (`COLINHA_FONT_FAMILY` no desenho/medição; descrição só no dialog) e **certificados** na 2ª passada (`Design tier: openai/gpt-5.6-sol`). + +### Dados → forma (se aplicável) + +Não se aplica (data-presentation Q3): nenhum KPI, mapa ou série; a única “forma” é a geometria do card. A escolha do estadual é insumo local da imagem — nunca dado coletado, apresentado ou enviado (medição de uso é S32). + +## Fases verificáveis + +1. **Núcleo puro + modelo + tile** — quota ~0,5 dia. `cardColinha.ts` (geometria + tabela + builder); `renderColinhaCard` + membros novos do subset no `cardRender.ts`; `cardModels.ts` (6º modelo, `lockupSrc`, badge movido); cópia byte-a-byte do jpeg; unit pins (`cardModels`, `cardColinha`, `cardRender`). + Prova: `pnpm gate:fast` verde; `sha256sum public/cards/modelo-colinha.jpeg docs/plans/cards-colinha-ui-design-assets/modelo-colinha.jpeg` idênticos; unit da colinha verde (geometria, linhas, 900×1600). +2. **Composer + galeria + copy** — quota ~0,5 dia. Branch `kind:'colinha'` no `CardComposer` (passo único, CTA direto, prévias 145/300/405, `Linha conferida`, nota nova); galeria 6 colunas/dica/dots; intro `/cards` e doc-comment da home. + Prova: `pnpm gate:fast` verde; `?model=minha-colinha` no browser: estado vazio (`Escolha abaixo`, 5 caixas vazias, CONFIRMA esmaecido, CTA desabilitado), escolha por busca/↑↓/Enter, prévia com nome + 5 dígitos, CTA habilitado, `Baixar minha colinha`; `/cards` com 6 tiles e dica dos seis. +3. **Gates + e2e + changelog** — quota ~0,3 dia. e2e (loop 6 tiles + badge; 6 dots + `seis modelos`; describe S31); changelog; `pnpm gate:fast`; e2e da superfície; `pnpm push` (pre-push roda `gate:ci`). + Prova: `pnpm test:e2e --no-deps -- tests/e2e/frontend.e2e.spec.ts -g "Minha colinha"` verde (prévia vazia + CTA off, seleção por teclado, prévia preenchida, download real `card-jorge-solla-minha-colinha.png` com assinatura PNG, troca mantendo a escolha); fixture de console-error intacta. + +## Rabbit holes / Não escopo (engenharia) + +- PDF/A4 ou impressão em casa (intenção, Questão 1) — só PNG 9:16; A4 vira item próprio se o produto pedir. +- Nome do visitante/“feito por você” (Questão 2) — a colinha é o produto. +- Segundo editor/estúdio, rota/página por colinha, editor de linhas (reordenar/remover cargos), `?estadual=`. +- Analytics de uso/abandono (S32); qualquer medição da escolha. +- CMS/collection/migration; ler `stateDeputy`/`Contact`; qualquer superfície Payload. +- Re-harmonização/segundo estilo de card; 53 modelos (a colinha é uma só); compartilhamento direto em redes. +- Redesenhar o topo/arte-mestre fora do gate; tipografia nova; otimização/prebuild de assets ou script de build (um asset = cópia byte-a-byte). +- Tocar nas artes-mestre (`team-card-base.png`, `team-card-front.png`, kit) e nos cinco modelos existentes além do badge; mexer no `StateDeputySelect`/dica de S30. + +## Explicitamente fora (descartes e defers com gatilho do simplify) + +- **Descartado — ternários por `kind` no composer** (revisores 1/2): a dívida é a F2 do #1277 (`escala-dry-pos-s30.md`), que já absorve `canAdvance`/`adjustable`/`eyebrow`/`title`/estágios de prévia; nada a duplicar aqui. +- **Descartado — identificadores PT (`colinha`/`isColinhaModel`/`kind:'colinha'`)**: literal do gate auto-aprovado no plano (palavra do domínio, como os enums de `post.type`). +- **Descartado — pins de geometria no unit/e2e e `sizes` 167px**: duplo pin intencional da composição (as constantes já são pinadas em `cardColinha.unit`); o `sizes` foi medido (166,7px para 6 colunas do contêiner de 1080). +- **Descartado — pureza ≤2** (`colinhaVoteRows` mutável/refs compartilhadas, `Record` inline, `drawColinhaTop` recebendo `CardModel`): sem bug e sem 2º consumidor. +- **Defer — `lockupSrc` opcional**: gatilho = 2º campo required por `kind` ou 2º consumidor de `lockupSrc` (entra junto com a união discriminada de `CardModel`). +- **Defer — dígitos/`ballotNumber` (5 fixo)**: gatilho = catálogo ganhar código ≠5 dígitos ou layout parametrizado pela contagem. +- **Defer — teste do caminho de falha do download**: gatilho = próxima mexida no caminho de download/erro do estúdio. +- **Re-defer — par JULIO default morto/duplicado (S30 P6+P10, #1277)**: o gatilho do S30 (“o S31 revisita o contrato”) disparou, mas a condição não se cumpriu — os defaults seguem mortos e sem 2º consumidor; o S31 só acrescentou `lockupSrc`. Novo gatilho: 2º consumidor do par default ou modelo que precise de default real (não ilustrativo). +- **Já resolvido no simplify (não reabrir):** `downloadError` renderizado no compose da colinha (alert novo), doc de `previewSrc`, plano/codebase-map, `Linha conferida` derivada de `colinhaVoteRows` (fim do rótulo/case duplicados), splice de classe, guard `width <= 0`, `drawColinhaDigit(empty)`, altura da pill calculada 1×, `capAt`, `pickJulioByKeyboard` içado, nota de privacidade asserida no e2e. + +## Riscos e mitigação + +- **Fidelidade do topo vs `modelo-colinha.jpeg`:** o port segue as medidas do gate + crítica do designer (c) + UAT; os números `COLINHA_*` ficam pinados em unit e a divergência gate × modelo é literal assumido (fonte canônica = gate). Gatilho: crítica do designer reprovar → ajustar só as constantes. +- **Nomes longos do catálogo (53):** shrink-to-fit em uma linha com piso 18px; unit pina o builder/geometria e o UAT cobre o pior caso (`Artur Barachisio Lisbôa`, `Coletivo de Enfermagem`); se um nome ainda estourar no piso, o candidato quebra em duas linhas apenas nessa linha (revisitar a Decisão 4). +- **Badge `NOVO` movido quebra pins:** unit + e2e atualizados no mesmo PR (literal assumido). +- **Medida do candidato em 900:** `createCardMeasure` mede em 700; o peso opcional (default 700) entra junto para o shrink medir o que desenha — sem tocar S13–S30. +- **Regressão nos modelos S13–S30:** subset só ganha membros; nenhum call site antigo muda; unit/e2e dos cinco modelos seguem verdes; o `max-h-[38dvh]` do S15 fica intocado (o branch da colinha tem o próprio enquadramento). +- **Prévia 9:16 no dialog:** sem o teto de altura do S15 a prévia usa a largura da cena 3 e rola com o corpo; se o UAT achar o dialog alto demais, reduzir a largura pós-escolha (constante da Decisão 6), nunca a fidelidade das linhas. +- **Peso do asset novo (~250KB):** aceito; cópia byte-a-byte sem script e sem tocar no bundle JS. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto: 6º modelo `Minha colinha` na home e `/cards` (`#cards`, `/cards`, `?model=minha-colinha`); escolha do estadual na lista de S30 por toque/teclado; prévia com nome de urna + 5 dígitos (um por caixa) e as 5 linhas fixas intocadas; PNG 1080×1920 baixado do mesmo estúdio; 100% no aparelho (sem nome/foto/upload); fidelidade ao gate (topo, linha legal vertical, corpo); sem página/rota por colinha. +- [ ] Invariantes AGENTS/engineering-standards: sem migration/collection/Consent; `src/lib` puro/client-safe (`cardColinha` sem `utilities/`); copy pt-BR/identificadores em inglês; sem `as never`; knip sem órfãos; artes-mestre intocadas; `pnpm gate:fast` + e2e da superfície + `pnpm push` verdes. +- [ ] Testes de domínio previstos: unit dos módulos puros (6 ids/badge/`lockupSrc`, pins de `COLINHA_*`, `colinhaVoteRows` preenchido/vazio, render no recorder falso com `roundRect`/`clip`, sha256 + 900×1600 do tile) obrigatório; e2e no spec `frontend` (6 tiles + badge, 6 dots + `seis modelos`, describe S31 com prévia vazia + CTA desabilitado, busca/teclado, prévia preenchida, download PNG, troca mantendo a escolha); **int não se aplica** — sem Payload/DB/access nem escrita multi-collection neste fluxo (client-side + assets estáticos). + +## Literais assumidos (auto-aprovados) + +- **Modelo:** id `minha-colinha`, label `Minha colinha`, `kind:'colinha'`, `stateDeputyPicker:true`, `badge:'NOVO'` (**movido** de `time-do-estadual`), `assetSrc:'/cards/team-card-base.png'`, `overlaySrc:'/cards/team-card-front.png'`, `lockupSrc:'/campaign-kit/marca-negativa-completa.png'`, `previewSrc:'/cards/modelo-colinha.jpeg'`, `1080×1920`, sem `photoWindow`. +- **Tile:** `public/cards/modelo-colinha.jpeg` = cópia byte-a-byte de `docs/plans/cards-colinha-ui-design-assets/modelo-colinha.jpeg` (900×1600; sha256 `76505492fb243c67313d237e58926afbd4d0847c00ab3bb43c090859cbdf18c8`). +- **Linhas fixas (ordem e literais):** `DEPUTADO FEDERAL · Jorge Solla · 1313` (4 caixas) · `DEPUTADO ESTADUAL · · <5 dígitos>` (5 caixas) · `SENADOR · Jaques Wagner · 130` · `SENADOR · Rui Costa · 133` · `GOVERNADOR · Jerônimo · 13` · `PRESIDENTE · Lula · 13`; todas com `CONFIRMA`. +- **Legal:** `FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL (PT-PC DO B - PV) | CNPJ CANDIDATO: 68.430.467/0001-05`. +- **Case:** rótulos UPPERCASE; fixos em Title Case (`Jorge Solla`, `Jaques Wagner`, `Rui Costa`, `Jerônimo`, `Lula`); estadual UPPERCASE (gate `JULIO PINHEIRO`); vazio `Escolha abaixo`. +- **Copy do composer:** eyebrow `Minha colinha`; pré-escolha título `Escolha seu estadual` + descrição `A linha de deputado estadual será preenchida na hora.`; pós-escolha título dialog `Pronta para levar com você` / drawer `Pronta para baixar` + descrição `Confira o estadual escolhido. As demais escolhas já vêm preenchidas no modelo oficial.`; aviso pré-escolha `Escolha um estadual para baixar.` / `A linha vazia será preenchida com o nome e os cinco dígitos.`; pós-escolha `Linha conferida` + `DEPUTADO ESTADUAL · · <5 dígitos>`; CTA `Baixar minha colinha` / `Gerando…`; nota nova `A escolha é processada neste aparelho. Nenhum nome, foto ou cadastro é solicitado.` +- **Galeria:** `lg:grid-cols-6` com `lg:gap-4`; dica `Deslize para ver os seis modelos`; intro `/cards` `Escolha um dos seis modelos, personalize e baixe para compartilhar.`; badge `NOVO` no modelo novo (assert e2e acompanha). +- **Geometria:** constantes `COLINHA_*` com os valores da Decisão 4 (fonte canônica = gate em 1080×1920; o gate × `modelo` divergente fica sinalizado para a crítica do designer). +- **Prévias:** 145px pré-escolha; 300px mobile / 405px desktop pós-escolha; rodapé dialog `Cancelar` + primário, drawer só primário full-width. + +## Self-score decision-quality: 5/5 + +1. **Decisões caras com rejeitadas:** modelo/kind, estratégia de render, módulos, geometria (e o candidato em uma linha), subset do contexto, fluxo do composer, tile/badge, galeria, entrada, copy, case e harness de testes — todas com Opções/Recomendação/Rejeitadas. +2. **Cabe no appetite:** 3 fases ≤1,3 dia, sem migration/infra; o único peso novo é 1 asset copiado byte-a-byte (~250KB), já previsto na intenção. +3. **Rabbit holes nomeados:** PDF/A4, nome do visitante, segundo editor/rota, editor de linhas, analytics, CMS, 53 modelos, redesenho do topo, script de asset, tocar nas artes-mestre. +4. **Depth check:** reusa `CardsStudio`/shells, `CardComposer`, `StateDeputySelect`/`stateDeputyCatalog`, `CardPreviewCanvas`/`cardCanvas`/`cardFileName`, o dono `cardRender` e o padrão de cópia byte-a-byte + sha256 de S30; nada de abstração especulativa. +5. **Intenção preservada:** aceite de produto intacto (6º modelo, escolha S30, prévia preenchida, PNG 1080×1920, 100% local, 5 modelos intocados além do badge); a engenharia só resolveu forma (kind novo, render portado, módulo puro, branch single-step). diff --git a/public/cards/modelo-colinha.jpeg b/public/cards/modelo-colinha.jpeg new file mode 100644 index 00000000..da706cb7 Binary files /dev/null and b/public/cards/modelo-colinha.jpeg differ diff --git a/src/app/(frontend)/(home)/CampaignCardsSection.tsx b/src/app/(frontend)/(home)/CampaignCardsSection.tsx index a79b8221..938bc468 100644 --- a/src/app/(frontend)/(home)/CampaignCardsSection.tsx +++ b/src/app/(frontend)/(home)/CampaignCardsSection.tsx @@ -2,8 +2,8 @@ import { brexterBold } from '@/app/(frontend)/fonts' import { CardsStudio } from '@/components/cards/CardsStudio' /** - * S14/S30 — the home invitation to the card funnel: short copy + the five model - * tiles, positioned right before the newsletter capture (which stays the last + * S14/S30/S31 — the home invitation to the card funnel: short copy + the six + * model tiles, positioned right before the newsletter capture (which stays the last * conversion block). Tapping a tile opens the SAME composer island used by * `/cards` right here (dialog on desktop, drawer on mobile) — no navigation and * no separate CTA. `brexterBold.variable` includes the display face in the page diff --git a/src/app/(frontend)/(home)/cards/page.tsx b/src/app/(frontend)/(home)/cards/page.tsx index 77c3b197..c5c751be 100644 --- a/src/app/(frontend)/(home)/cards/page.tsx +++ b/src/app/(frontend)/(home)/cards/page.tsx @@ -10,7 +10,7 @@ import { hasPublishedContentPieces } from '@/utilities/content/contentPieceReads import { hasPublishedJingles } from '@/utilities/jingleReads' const intro = - 'Escolha um dos cinco modelos, personalize com seu nome ou sua foto e baixe para compartilhar.' + 'Escolha um dos seis modelos, personalize com seu nome ou sua foto e baixe para compartilhar.' export const metadata: Metadata = { title: 'Crie seu card de apoio | Jorge Solla', diff --git a/src/components/cards/CardComposer.tsx b/src/components/cards/CardComposer.tsx index 558f4c7e..017a84cb 100644 --- a/src/components/cards/CardComposer.tsx +++ b/src/components/cards/CardComposer.tsx @@ -20,6 +20,7 @@ import { loadCardPhoto, } from '@/components/cards/cardCanvas' import { + CARD_COLINHA_PRIVACY_NOTE, CARD_HARMONY_HELP, CARD_HARMONY_LABEL, CARD_PHOTO_PRIVACY_NOTE, @@ -30,6 +31,7 @@ import { StateDeputySelect } from '@/components/cards/StateDeputySelect' import { useCardCutout } from '@/components/cards/useCardCutout' import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { DrawerDescription, DrawerHeader, DrawerTitle } from '@/components/ui/Drawer' +import { COLINHA_FONT_FAMILY, colinhaVoteRows } from '@/lib/cardColinha' import { TEAM_CARD_NAME_SLOT, type CardModel, type CardRect } from '@/lib/cardModels' import { fitCardName, type CardNameFit } from '@/lib/cardNameFit' import { @@ -44,6 +46,7 @@ import { } from '@/lib/cardPhotoTransform' import { createCardMeasure, + renderColinhaCard, renderNameCard, renderPhotoCard, renderTeamCard, @@ -85,6 +88,16 @@ const STATE_DEPUTY_PREVIEW_WIDTH: Record = { selected: 'max-w-[12.8125rem] sm:max-w-[14.375rem]', } +/** + * S31 — the colinha stages of the design gate: compact while the estadual is + * unchosen (scene 2, 145px) and the scene 3/5 stage after the pick (405px + * desktop, 300px mobile). + */ +const COLINHA_PREVIEW_WIDTH: Record<'idle' | 'selected', string> = { + idle: 'max-w-[9.0625rem]', + selected: 'max-w-[18.75rem] sm:max-w-[25.3125rem]', +} + const TeamNameField = ({ id, value, @@ -180,6 +193,8 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading') const [baseImage, setBaseImage] = useState(null) const [overlayImage, setOverlayImage] = useState(null) + /** S31 — the colinha brand lockup of the top box (`lockupSrc`). */ + const [lockupImage, setLockupImage] = useState(null) const [previewImage, setPreviewImage] = useState(null) const [downloadError, setDownloadError] = useState(null) const [isDownloading, setIsDownloading] = useState(false) @@ -205,10 +220,13 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer const photoWindow = model.photoWindow const isNameModel = model.kind === 'name' const isTeamModel = model.kind === 'team' + const isColinhaModel = model.kind === 'colinha' const isStateDeputyModel = isTeamModel && model.stateDeputyPicker === true const selectedDeputy = selectedDeputySlug ? (getStateDeputyCard(selectedDeputySlug) ?? null) : null + /** S31 — the estadual row owns the label/case of the `Linha conferida` copy. */ + const estadualRow = colinhaVoteRows(selectedDeputy)[1] const deputyPairReady = selectedDeputy !== null && deputyImages?.slug === selectedDeputy.slug // S30 — the tone reference is the selected deputy's group art, frozen when the // cutout starts (switching the deputy later keeps the processed photo). @@ -247,6 +265,7 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer setLoadState('loading') setBaseImage(null) setOverlayImage(null) + setLockupImage(null) setPreviewImage(null) const load = async () => { @@ -255,17 +274,22 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer if (!fontReady) throw new Error('card-font-unavailable') // S30 — the dobradinha model loads its art pair per selection (below); // the static defaults stay unused so no illustrative JULIO art flashes - // before the choice. - const [image, overlay, preview] = await Promise.all([ + // before the choice. S31 — the colinha composes the top from its three + // official assets and never draws the tile art (`previewSrc`). + const [image, overlay, lockup, preview] = await Promise.all([ isStateDeputyModel ? Promise.resolve(null) : loadCardImage(model.assetSrc), !isStateDeputyModel && model.overlaySrc ? loadCardImage(model.overlaySrc) : Promise.resolve(null), - model.previewSrc ? loadCardImage(model.previewSrc) : Promise.resolve(null), + model.lockupSrc ? loadCardImage(model.lockupSrc) : Promise.resolve(null), + model.previewSrc && !isColinhaModel + ? loadCardImage(model.previewSrc) + : Promise.resolve(null), ]) if (cancelled) return setBaseImage(image) setOverlayImage(overlay) + setLockupImage(lockup) setPreviewImage(preview) setLoadState('ready') } catch { @@ -277,7 +301,15 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer return () => { cancelled = true } - }, [isStateDeputyModel, model.assetSrc, model.overlaySrc, model.previewSrc, fontFamily]) + }, [ + isStateDeputyModel, + isColinhaModel, + model.assetSrc, + model.overlaySrc, + model.lockupSrc, + model.previewSrc, + fontFamily, + ]) /** * S30 — the selected deputy's pair swaps in place: the previous art stays @@ -322,6 +354,23 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer return } + if (isColinhaModel) { + // S31 — the slip: official top composition + legal + the six rows; the + // estadual row follows the shared selection and the fixed rows never + // change. No name, photo or cutout enters this flow. + if (!baseImage || !overlayImage || !lockupImage) return + ctx.clearRect(0, 0, model.width, model.height) + renderColinhaCard(ctx, model, { + group: baseImage, + lockup: lockupImage, + band: overlayImage, + deputy: selectedDeputy, + fontFamily: COLINHA_FONT_FAMILY, + measure: createCardMeasure(ctx, COLINHA_FONT_FAMILY, 900), + }) + return + } + if (isTeamModel) { // S30 — the dobradinha pair comes from the selected catalog entry: `photos` // is the FOTOS background (drawn under the cutout) and `base` is the front @@ -415,10 +464,12 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer }, [ baseImage, overlayImage, + lockupImage, previewImage, loadState, isNameModel, isTeamModel, + isColinhaModel, isStateDeputyModel, selectedDeputy, deputyImages, @@ -539,13 +590,15 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer } const nameCanAdvance = nameFit?.ok === true - const canAdvance = isNameModel - ? nameCanAdvance - : isStateDeputyModel - ? selectedDeputy !== null && deputyPairReady && teamReady !== null && nameCanAdvance - : isTeamModel - ? teamReady !== null && nameCanAdvance - : photo !== null + const canAdvance = isColinhaModel + ? selectedDeputy !== null + : isNameModel + ? nameCanAdvance + : isStateDeputyModel + ? selectedDeputy !== null && deputyPairReady && teamReady !== null && nameCanAdvance + : isTeamModel + ? teamReady !== null && nameCanAdvance + : photo !== null const nameError = (isNameModel || isTeamModel) && name.trim().length > 0 && nameFit !== null && !nameFit.ok const adjustable = isStateDeputyModel @@ -556,6 +609,10 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer const title = (() => { if (step === 'result') return 'Seu card está pronto para compartilhar.' if (isNameModel) return 'Personalize com seu nome' + if (isColinhaModel) { + if (!selectedDeputy) return 'Escolha seu estadual' + return shell === 'dialog' ? 'Pronta para levar com você' : 'Pronta para baixar' + } if (isStateDeputyModel && !selectedDeputy) return 'Escolha sua dobradinha' if (!isTeamModel) return 'Enquadre sua foto' if (teamProcessing) return 'Preparando sua foto' @@ -566,32 +623,46 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer return 'Entre para o time' })() const photoHeaderDescription = - step === 'compose' && !isNameModel && !isTeamModel + step === 'compose' && !isNameModel && !isTeamModel && !isColinhaModel ? 'Arraste para posicionar e use os controles para aproximar ou ajustar.' : null const stateDeputyHeaderDescription = isStateDeputyModel && step === 'compose' && !selectedDeputy ? 'Escolha seu estadual da dobradinha.' : null - const headerDescription = photoHeaderDescription ?? stateDeputyHeaderDescription + /** + * S31 — the colinha description lives only in the dialog: the gate's mobile + * drawer (scene 5) carries eyebrow + title only, so the compact preview keeps + * the selector in the first viewport. + */ + const colinhaHeaderDescription = + isColinhaModel && step === 'compose' && shell === 'dialog' + ? selectedDeputy + ? 'Confira o estadual escolhido. As demais escolhas já vêm preenchidas no modelo oficial.' + : 'A linha de deputado estadual será preenchida na hora.' + : null + const headerDescription = + photoHeaderDescription ?? stateDeputyHeaderDescription ?? colinhaHeaderDescription const teamBodyNotice = isTeamModel && step === 'compose' && teamReady && !nameError ? 'O recorte já foi centralizado. Se precisar, arraste a foto ou use os controles.' : null const ShellDescription = shell === 'dialog' ? DialogDescription : DrawerDescription - const eyebrow = isTeamModel - ? step === 'result' - ? 'Tudo certo' - : teamError - ? 'Falha no recorte' - : nameError - ? 'Nome muito longo' - : isStateDeputyModel - ? selectedDeputy - ? 'Estadual escolhido' - : 'Time do estadual' - : 'Time de você' - : null + const eyebrow = isColinhaModel + ? 'Minha colinha' + : isTeamModel + ? step === 'result' + ? 'Tudo certo' + : teamError + ? 'Falha no recorte' + : nameError + ? 'Nome muito longo' + : isStateDeputyModel + ? selectedDeputy + ? 'Estadual escolhido' + : 'Time do estadual' + : 'Time de você' + : null const eyebrowNode = eyebrow ? (

{eyebrow} @@ -624,11 +695,15 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer step === 'result' ? 'result' : teamReady ? 'ready' : teamProcessing ? 'processing' : 'idle' const stateDeputyPreviewStage: StateDeputyPreviewStage = teamPreviewStage === 'idle' && selectedDeputy && !nameError ? 'selected' : teamPreviewStage - const previewWidthClassName = isStateDeputyModel - ? STATE_DEPUTY_PREVIEW_WIDTH[stateDeputyPreviewStage] - : isTeamModel - ? TEAM_PREVIEW_WIDTH[teamPreviewStage] - : 'max-w-[22rem]' + const previewWidthClassName = isColinhaModel + ? selectedDeputy + ? COLINHA_PREVIEW_WIDTH.selected + : COLINHA_PREVIEW_WIDTH.idle + : isStateDeputyModel + ? STATE_DEPUTY_PREVIEW_WIDTH[stateDeputyPreviewStage] + : isTeamModel + ? TEAM_PREVIEW_WIDTH[teamPreviewStage] + : 'max-w-[22rem]' const nameInputClassName = `mt-2 h-11 w-full rounded-lg bg-white px-3 text-base text-(--campaign-ink) focus-visible:ring-2 focus-visible:ring-(--pt-red) focus-visible:outline-none ${ nameError @@ -771,9 +846,9 @@ export const CardComposer = ({ model, shell, fontFamily, onClose }: CardComposer ) : null} + {step === 'compose' && isColinhaModel ? ( +

+ + {selectedDeputy ? ( +
+

Linha conferida

+

+ {estadualRow.officeLines.join(' ')} · {estadualRow.candidate} ·{' '} + {selectedDeputy.ballotNumber} +

+
+ ) : ( +

+ + Escolha um estadual para baixar. + + A linha vazia será preenchida com o nome e os cinco dígitos. +

+ )} +

+ {CARD_COLINHA_PRIVACY_NOTE} +

+ {downloadError ? ( +

+ {downloadError} +

+ ) : null} +
+ ) : null} + {step === 'compose' && isNameModel ? (