feat(S30): card Time do estadual — 5º modelo com as 53 dobradinhas (Closes #1271)

This commit is contained in:
Francisco Solla 2026-09-23 09:28:10 -03:00
parent c832e51ed3
commit 8151cba1cb
129 changed files with 1942 additions and 94 deletions

View file

@ -22,7 +22,7 @@ Conceitos e decisões: `docs/ARCHITECTURE.md`. Operação (setup/seeds/deploy/se
- `src/components/campaign/<domínio>/` — 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`).
- `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/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`).

View file

@ -0,0 +1 @@
**S30 — Card "Time do estadual": quinto modelo do funil público, com as 53 dobradinhas (2026-09-23):** o estúdio de cards (`#cards` da home + `/cards` + `?model=time-do-estadual`) ganha o quinto modelo no MESMO composer: o visitante escolhe um dos 53 estaduais, digita como é chamado e envia a foto de busto; o recorte roda no aparelho (esteira do S15 inteira: envio, progresso/retry, harmonização, pan/zoom) e o card final é `TIME DE <nome>` com a arte oficial do escolhido, na ordem medida FOTOS do estadual → recorte na janela `{286,439,592,577}` → BASE do estadual → banners (`TEAM_CARD_LABEL` + banner azul dinâmico do S16). Catálogo estático derivado dos lockups da arte (`src/lib/stateDeputyCatalog.ts`: slug, nome de exibição, número de 5 dígitos e os dois caminhos de asset; 53 entradas imutáveis com snapshot `tests/fixtures/state-deputy-catalog.snapshot.json` + sha256 e busca por nome sem acento ou pela pasta entregue), com os 106 derivativos WebP otimizados (~6,8MB, q82) commitados em `public/cards/estaduais/<slug>-{fotos,base}.webp` — re-deriváveis por `pnpm build:state-deputy-card-assets -- --from <origem>` (sharp, falha fechado em par ausente/duplicado/dimensão fora de 1080×1440) — e o jpeg exato aprovado pelo humano copiado byte-a-byte para o tile/placeholder (`/cards/modelo-time-de-voce-com-estadual.jpeg`). Seletor inline portado do gate (trigger com nome+número+"Trocar", painel aberto por padrão sem escolha, busca, lista rolável `role="listbox"`, ↑↓/Enter, seleção por linha inteira com check); pré-escolha mostra a arte de exemplo, pós-escolha sem foto desenha a silhueta do visitante (`#001a42`, geometria do design normalizada na janela) e trocar de estadual não perde o nome nem o recorte (par de arte em estado próprio, sem flicker; harmonia congelada na referência do recorte). Galeria: 5 tiles numa fileira no desktop (`lg:grid-cols-5`), 5 dots no mobile seguindo o scroll e dica "Deslize para ver os cinco modelos"; o badge `NOVO` passa para o modelo novo. Shell do dialog endurecido (`flex flex-col` sobre o `grid` do kit) para o corpo rolar e o rodapé ficar pinado com o seletor aberto. Sem migration, sem collection/CMS, sem Consent, sem segundo cadastro: roster e artes são estáticos, 100% no aparelho e nenhum byte da foto sai. Verificação: unit (catálogo 53/snapshot/sha256/dimensões dos 106 arquivos via sharp, silhueta/`subject` no recorder falso, pins do 5º modelo), `pnpm gate:fast` e e2e do `frontend` (35 verdes: seleção por busca/teclado, fluxo completo com download real do PNG, troca preservando nome/recorte, dots mobile).

View file

@ -0,0 +1,184 @@
# Impl: Card personalizado com o seu estadual (dobradinha)
Status: aprovado (modo --auto)
Atualizado em: 2026-09-23
Issue: #1271
Intenção: docs/plans/cards-estadual-dobradinha.md
Appetite restante: herdado — ~1–1,5 dia eng (3 fases somam ≤1,3 dia; sem migration; o único peso novo é o commit dos ~8MB de derivativos)
## Leitura da intenção
- **Outcome:** o estúdio de cards ganha o quinto modelo `Time do estadual`: o visitante escolhe um dos 53 estaduais, digita como é chamado e envia a foto de busto; o recorte roda no aparelho e o card final é o `TIME DE <NOME>` com a arte oficial do estadual (FOTOS + visitante na janela + BASE + banners), baixável em PNG — sem conta e sem nenhum byte da foto saindo do aparelho.
- **O que NÃO negociar:**
- 100% client-side: sem upload, sem persistência, sem analytics de PII; nome/foto nunca saem do aparelho; os quatro modelos existentes (e suas artes) intocados além do badge.
- Um único editor/fluxo: quinto item do catálogo no MESMO `CardsStudio`/`CardComposer`; `/cards` e `?model=time-do-estadual` continuam.
- Fluxo de foto do S15 inteiro (envio, recorte no aparelho com progresso/retry, harmonização, arrasto/zoom); nenhum controle novo nasce aqui; geometria literal do S15: janela `{286,439,592,577}`.
- Lista dos 53 navegável por toque e teclado no celular; trocar de estadual não perde o nome nem a foto já recortada.
- Nome em uma linha com `fitCardName`/banner azul do S16; fora do cap mínimo → pedir nome mais curto, nunca corte silencioso.
- Catálogo derivado da arte (nome completo + número de urna), imutável e commitado; nenhum schema/migration/CMS; nenhuma leitura de `stateDeputy`/`Contact`.
- **O que reavaliar (hipóteses da intenção):**
- **“Catálogo estático no padrão do `municipalityCatalog`”**: confirmado — `src/lib/stateDeputyCatalog.ts` client-safe + snapshot + sha256; os números saem dos lockups da arte (a base não tem número 2026).
- **“O composer ramifica por kind”**: para não mexer no discriminante dos 4 modelos, a ramificação nova é uma flag (`stateDeputyPicker`) na 5ª entrada `kind:'team'` — registro na Decisão 1.
- **“A arte de exemplo por estadual segue a receita”**: o tile e o placeholder pré-escolha usam o arquivo exato aprovado pelo humano (jpeg), copiado byte-a-byte; só a arte escolhida compõe o card.
## Abordagem recomendada
```mermaid
flowchart LR
H["home #cards / rota /cards"] --> S["CardsStudio — ilha única"]
S --> G["CardModelGallery — 5 tiles + dots (mobile)"]
G -->|"tile Time do estadual"| C["CardComposer — editor único"]
C --> SEL["StateDeputySelect — painel inline dos 53"]
SEL --> CAT["stateDeputyCatalog — tabela imutável + snapshot"]
CAT --> AST["public/cards/estaduais/<slug>-{fotos,base}.webp"]
C -->|"estadual, sem foto"| SIL["renderTeamCard — subject silhouette"]
C -->|"foto recortada"| RT["renderTeamCard — subject photo"]
AST --> RT
AST --> SIL
SIL --> PV["prévia canvas"]
RT --> PV
PV --> DL["Baixar PNG no aparelho"]
SRC["build-state-deputy-card-assets.mjs --from <origem>"] -.->|"deriva e commita"| AST
```
**Opções consideradas:** A) estender o estúdio existente com o quinto modelo + catálogo estático dos 53 + seletor inline no mesmo composer | B) rota/editor novo por estadual | C) roster em CMS/migration com assets dinâmicos.
**Recomendação:** A — o outcome é um modelo no estúdio existente; catálogo estático + derivativos commitados mantêm 100% no aparelho, dispensam schema e reusam a esteira do S15 (recorte, harmonização, banners, download); cabe no appetite.
**Rejeitadas:** B porque é o anti-goal "segundo editor"/rota por estadual e duplicaria shells/estado; C porque schema/migration está fora do escopo, a base não tem número de 2026 e o catálogo deve ser derivado da arte.
### Decisões de engenharia
1. **Quinto modelo — flag na entrada `kind:'team'`, sem novo `kind`.**
Opções: A) 5ª entrada `time-do-estadual` (`kind:'team'`) + flag nova `stateDeputyPicker: true` ramificando o composer | B) novo valor de `kind` (ex. `'team-state'`) | C) 53 entradas no `CARD_MODELS` (uma por estadual) | D) editor/render dedicado por id.
Recomendação: A — o fluxo é idêntico ao time (mesma janela, esteira de recorte, harmonização e banners); a flag marca só o eixo novo (seletor + arte variável) e deixa `isCardModelId`/`?model=` cobrirem o id de graça. `assetSrc`/`overlaySrc` default = par do JULIO (`/cards/estaduais/julio-fotos.webp` + `julio-base.webp`), `previewSrc` = jpeg aprovado; `photoWindow` idêntico ao S15; `badge: 'NOVO'` move de `time-de-voce` para o novo (design cena 1).
Rejeitadas: B porque ramifica o discriminante usado pelos 4 modelos para algo que a flag expressa; C porque o estadual viraria "modelo" (galeria/pins/URL explodem) e contraria o anti-goal; D porque duplica composer/render.
2. **Catálogo e assets — módulo puro + derivativos WebP commitados + script re-derivável.**
Opções: A) `src/lib/stateDeputyCatalog.ts` (client-safe, sem imports de `utilities/`) com `stateDeputyCatalog`/`getStateDeputyCard`/`filterStateDeputyCards`, snapshot `tests/fixtures/state-deputy-catalog.snapshot.json` (padrão `municipalityCatalog`), 106 WebP em `public/cards/estaduais/<slug>-{fotos,base}.webp` gerados por `scripts/build-state-deputy-card-assets.mjs --from <origem>` | B) collection Payload/migration | C) commitar os PNG originais (~569MB) | D) importar do diretório de origem no build/runtime.
Recomendação: A — lib pura como o resto de `src/lib`; q≈82 (medido: FOTOS≈95KB + BASE≈51KB por estadual → ~8MB no total) porque o canvas carrega a URL crua (fora do otimizador do `next/image`) e o deploy não pode depender de diretório externo (escada de cache #3: artefato commitado via script re-executável); o script pareia `FOTOS*`/`BASE*` case-insensitive por pasta, aplica `slugify` no nome e falha se faltar par ou se a dimensão não for 1080×1440.
Rejeitadas: B (fora de escopo; sem número 2026 na base); C (peso no repo/imagem sem ganho); D (deploy refém de pasta local e fora da escada de cache).
3. **Seletor — componente novo com painel inline do design; Base UI Combobox rejeitado.**
Opções: A) `src/components/cards/StateDeputySelect.tsx` portando a cena 3 (trigger h-11 com nome+número+"Trocar" ou placeholder, painel inline aberto por default sem escolha, busca, lista rolável `max-h-52` com `role="listbox"`/opções, teclado ↑↓ Enter, seleção por linha inteira com check) | B) `src/components/ui/combobox.tsx` (Base UI, já no bundle público) | C) `<select>`/datalist nativo.
Recomendação: A — o design é painel inline (busca + lista + check + teclado); o combobox renderiza popup flutuante e teria que ser reconfigurado para um visual que não é o aprovado.
Rejeitadas: B porque popup ≠ painel inline e não entrega busca/rolagem/check do design; C porque não comporta busca/check/rolagem com o visual aprovado no mobile com 53 itens.
4. **Composição — estender o dono (`renderTeamCard`) com sujeito discriminado + silhueta pura.**
Opções: A) `renderTeamCard` recebe `subject` (`{kind:'photo', photo, photoSize, transform}` | `{kind:'silhouette'}`) e desenha base → sujeito → overlay → banners; `drawCardVisitorSilhouette(ctx, window)` nova no `cardRender.ts`, `CardDrawContext` ganha `beginPath/arc/roundRect/fill` | B) renderizador de placeholder separado | C) silhueta desenhada ad hoc no composer.
Recomendação: A — a ordem é a mesma do time; a silhueta é geometria pura normalizada no `photoWindow` (cor `#001a42`, cabeça circular + ombros arredondados) testável no recorder falso; `subject` discriminado evita `photoSize`/`transform` mentirosos quando não há foto; os dois call sites (prévia com foto e sem foto) ficam num caminho só.
Rejeitadas: B porque duplicaria a composição de banners (twin do dono); C porque joga geometria para fora do módulo puro.
5. **Troca de estadual — par de arte em estado próprio (sem flicker) e harmonia congelada no recorte.**
Opções: A) carregar `photosSrc`/`baseSrc` do selecionado num estado paralelo (`deputyImages`) mantendo o par anterior pintado até o novo decodificar; `loadState` só reflete a 1ª carga; referência de harmonia = FOTOS do estadual selecionado no momento do `start()` do recorte (trocar depois não re-harmoniza) | B) remontar o composer por estadual (`key`) | C) re-harmonizar a cada troca.
Recomendação: A — o efeito de carga atual (`CardComposer.tsx:207-237`) zera imagens e volta `loading` a cada mudança de src (flicker); para o modelo novo a troca usa um efeito próprio que só commita o par quando os dois decodificam, sem tocar `loadState` nem re-anunciar a live region; nome e recorte sobrevivem por ficarem fora desse par. A harmonia usa a referência no `start()` (hook já congela via deps) e a troca posterior não re-harmoniza — diferença sutil entre artes da mesma receita; gatilho: UAT reprovar o tom ao trocar depois do recorte.
Rejeitadas: B porque perde nome/recorte (viola o aceite) e pisca; C porque adiciona processamento assíncrono + guarda de corrida para ganho não comprovado.
6. **Galeria — cinco colunas no desktop e dots portados no mobile.**
Opções: A) `lg:grid-cols-5` com gap menor (`lg:gap-4`) + dots (5 indicadores, ativo segue o scroll, `aria-label="Ir para o modelo N de 5"`) dentro do `CardModelGallery` (`'use client'`), dica "Deslize para ver os cinco modelos" | B) extrair um hook/componente genérico de dots compartilhado com `CampaignContentCarousel` | C) manter 4 colunas e esconder o 5º atrás do scroll.
Recomendação: A — a galeria é um track de 5 tiles fixos; o padrão de dots do `CampaignContentCarousel.tsx:101-119` é pequeno e a extração tocaria uma superfície pública já entregue (que tem teclado/inert/live region próprios) para um ganho de 1 call site. Gatilho de revisitação: um 3º carrossel ou a galeria ganhar navegação por teclado.
Rejeitadas: B (abstração prematura com risco em superfície entregue); C (viola o design: cinco tiles na mesma fileira).
7. **Gatilho do S15 (5º modelo → fatiar o composer) — adjudicado com dívida nomeada, não silenciosa.**
Opções: A) manter o composer único neste PR e registrar a dívida com Issue específica ("extrair processing/error/ready do `CardComposer`") | B) fatiar os subcomponentes de estado agora.
Recomendação: A — o S30 acrescenta um ramo de seletor/copy, não um novo estado de editor; fatiar agora mexe nos 4 modelos e consome folga (~0,5 dia) que o appetite não tem. A dívida entra no `capture-review-debts` no mesmo PR.
Rejeitadas: B porque estoura o appetite e adiciona risco de plumbing num PR que já toca 4 superfícies.
8. **Testes — estender os pins e o describe do `frontend.e2e.spec.ts`; somar o prefixo do catálogo ao manifesto.**
Opções: A) atualizar unit pins (`cardModels`, `cardRender`) e o e2e existente (loop dos 5 tiles, badge `NOVO` no novo), acrescentar o describe S30 (seleção por busca/teclado, fluxo completo com stub, troca preservando nome/recorte, placeholder pré-escolha) e adicionar `src/lib/stateDeputyCatalog.ts` ao entry de cards do `scripts/lib/e2e-affected-manifest.mjs` | B) spec e2e novo separado | C) só unit.
Recomendação: A — o harness do S15 (stub `__cardsCutoutStub`, `TEST_PHOTO_PNG`, probes de canvas) já vive nesse spec e a superfície é a mesma; o manifest casa `src/components/cards` e `src/lib/card`, mas o catálogo novo não casa — prefixo explícito.
Rejeitadas: B porque fragmenta o harness e duplica setup; C porque o aceite é de fluxo visual no browser (troca, seleção, download).
### Componentes / mudanças
- **`src/lib/cardModels.ts`** (editar): 5ª entrada `time-do-estadual` (`kind:'team'`, `label:'Time do estadual'`, `stateDeputyPicker: true`, `assetSrc:'/cards/estaduais/julio-fotos.webp'`, `overlaySrc:'/cards/estaduais/julio-base.webp'`, `previewSrc:'/cards/modelo-time-de-voce-com-estadual.jpeg'`, 1080×1440, `photoWindow {286,439,592,577}`, `badge:'NOVO'`); remover o badge de `time-de-voce`; tipo `CardModel` ganha `stateDeputyPicker?: boolean`.
- **`src/lib/stateDeputyCatalog.ts`** (novo, client-safe): `StateDeputyCatalogEntry { slug, name, ballotNumber, photosSrc, baseSrc }`, tabela `stateDeputyCatalog` (53, ordem alfabética da pasta/origem), `getStateDeputyCard`, `filterStateDeputyCards(query)` pura (sem acento/caixa) — sem importar `utilities/`.
- **`src/lib/cardRender.ts`** (editar): `CardDrawContext` ganha `beginPath/arc/roundRect/fill`; `drawCardVisitorSilhouette(ctx, window)` (cabeça circular 34% da largura, topo 0, esquerda 33%; ombros com a largura da janela, 72% da altura, base +3%, raios 48%/48%/10%/10%; `#001a42`); `renderTeamCard` com `subject` discriminado e retorno `{ fit, transform: CardPhotoTransform | null }`.
- **`src/components/cards/StateDeputySelect.tsx`** (novo): trigger h-11 (selecionado: nome + `número · Trocar`; vazio: `Escolha um nome`), painel inline aberto por default sem escolha, busca (`Buscar por nome`), hint `53 estaduais · use ↑ ↓ e Enter para escolher`, lista `max-h-52` com `role="listbox"`/opções, ↑↓/Enter e seleção por linha inteira com check na linha selecionada.
- **`src/components/cards/CardComposer.tsx`** (editar): estado `selectedDeputySlug`; ramo `isStateDeputyModel = isTeamModel && model.stateDeputyPicker === true` com prévia pré-escolha = jpeg aprovado; pós-escolha sem foto = `renderTeamCard` com `subject:{kind:'silhouette'}`; com recorte = `subject` photo (nome/recorte sobrevivem à troca); efeito próprio de swap do par (`deputyImages`) sem mexer em `loadState`; referência de harmonia = `getStateDeputyCard(slug)?.photosSrc`; CTA só com estadual + recorte + nome que cabe; copy nova (eyebrows `Time do estadual`/`Estadual escolhido`, títulos `Escolha sua dobradinha`/`Agora coloque seu nome`, nota `Escolha um estadual para continuar.`, legenda `Nome · número` sob a prévia pronta); falha ao carregar o par mantém o anterior + `role="alert"` recuperável.
- **`src/components/cards/CardModelGallery.tsx`** (editar): `'use client'`; `lg:grid-cols-5 lg:gap-4`; dots mobile (5, ativo segue o scroll, clique rola); dica `Deslize para ver os cinco modelos`; comentários "quatro" → "cinco".
- **`src/components/cards/CardModelTile.tsx`** (editar): `sizes` para 5 colunas (`(min-width: 1024px) 200px, 78vw`).
- **`src/app/(frontend)/(home)/cards/page.tsx`** (editar): intro/metadata `'Escolha um dos cinco modelos, personalize e baixe para compartilhar.'`.
- **`scripts/build-state-deputy-card-assets.mjs`** (novo; usa `dieWithLabel`/`sha256Hex` de `scripts/lib/cli.mjs` e `sharp`): `--from <dir>` pareia `FOTOS*`/`BASE*` case-insensitive por pasta, valida 1080×1440, `slugify` da pasta, WebP q≈82 em `public/cards/estaduais/`; falha fechado se faltar par ou duplicar slug. Script `build:state-deputy-card-assets` no `package.json`.
- **`public/cards/estaduais/*.webp`** (novo, 106 arquivos) e **`public/cards/modelo-time-de-voce-com-estadual.jpeg`** (novo, cópia byte-a-byte do asset aprovado em `docs/plans/cards-estadual-dobradinha-ui-design-assets/`).
- **Testes** (editar/novo): `tests/unit/cardModels.unit.spec.ts` (5 ids, pin do modelo novo, badge), `tests/unit/cardRender.unit.spec.ts` (subject/silhueta no recorder falso), `tests/unit/stateDeputyCatalog.unit.spec.ts` (novo, `// @vitest-environment node`; 53 entradas, slugs/5 dígitos/únicos, 106 assets em disco com 1080×1440 via sharp, snapshot + sha256 e o sha da cópia do jpeg aprovado), `tests/fixtures/state-deputy-catalog.snapshot.json` (novo), `tests/e2e/frontend.e2e.spec.ts` (loop dos 5 tiles + badge movido + describe novo), `scripts/lib/e2e-affected-manifest.mjs` (prefixo do catálogo no entry de cards → spec `frontend`).
- **`docs/changelog/2026-09-23-s30-card-time-do-estadual.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 novo; catálogo é conteúdo estático e o fluxo é 100% client-side.
- **UI:** Impeccable B — shape (artefato aprovado, cenas 1–5) → craft (port class-by-class: grid 5, seletor, silhueta, dots) → critique (a11y teclado/toque, mobile 390, copy) → polish. Shells/tokens reusados: `CardsStudio`, `ui/dialog`, `ui/Drawer`, `CardPreviewCanvas`, `--pt-red`/`--campaign-*`/`--team-blue`; alvos ≥44px, `role=listbox`/`option`, `role=status`/`role=alert`, foco preso e retorno ao tile.
### 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 e o progresso do recorte. Nome, foto e escolha são insumos locais da imagem — nunca dados coletados, apresentados ou enviados.
## Fases verificáveis
1. **Catálogo + assets + 5º modelo** — quota ~0,4 dia. `stateDeputyCatalog.ts` + snapshot + unit node (dimensões via sharp); script de assets + `package.json` + 106 WebP gerados e commitados + cópia byte-a-byte do jpeg aprovado; `cardModels.ts` com a 5ª entrada, flag e badge; pins de unit atualizados.
Prova: `pnpm gate:fast` verde; `pnpm build:state-deputy-card-assets -- --from '<origem>'` re-executado deixa `git status public/cards/estaduais` limpo (byte-idêntico); unit do catálogo verde (53, sha256, 1080×1440).
2. **Seletor + composer + render + galeria** — quota ~0,6 dia. `drawCardVisitorSilhouette` + `subject` no `renderTeamCard` (unit no recorder falso); `StateDeputySelect`; ramo `stateDeputyPicker` no composer (swap sem flicker, silhueta, harmonia na referência do estadual, copy das cenas 3–4); galeria 5 colunas + dots + dica; intro `/cards`.
Prova: `pnpm gate:fast` verde; `?model=time-do-estadual` no browser: pré-escolha = jpeg + CTA desabilitado + nota; seleção por busca/↑↓/Enter; pós-seleção sem foto = FOTOS + silhueta + BASE + `TIME DE`; com foto = recorte em primeiro plano; trocar estadual mantém nome e recorte sem piscar; nome longo = erro do S16 com CTA off.
3. **Gates + e2e + changelog** — quota ~0,3 dia. e2e do describe S30 + loop de 5 tiles + badge; prefixo do manifesto; changelog; `pnpm gate:fast`; e2e da superfície (`pnpm test:e2e --no-deps -- tests/e2e/frontend.e2e.spec.ts -g "Time do estadual"` e o `Cards personalizados (S14)`); `pnpm push` (pre-push roda `gate:ci`).
Prova: e2e verde com stub (seleção por teclado, download real `card-jorge-solla-time-do-estadual.png` com assinatura PNG, troca preservando nome/recorte, placeholder pré-escolha); fixture de console-error intacta.
## Rabbit holes / Não escopo (engenharia)
- 53 entradas em `CARD_MODELS` ou query nova (`?estadual=`) — o estadual não vira modelo nem muda o contrato de URL (`#cards`, `/cards`, `?model=`).
- Re-harmonização/worker/motion na troca de estadual (Decisão 5; gatilho UAT).
- Calibrar janela/harmonia/pixel por estadual — a régua do S15 vale para as 53 (corte do plano de intenção).
- CMS/collection/migration do roster; segundo cadastro de pessoas; ler `stateDeputy`/`Contact`; número de urna vindo da base.
- Página/rota por estadual, ranking/"força", métrica, analytics (S32).
- Fatiar o `CardComposer` (Decisão 7 — dívida nomeada, não neste PR).
- Otimização em build/prebuild ou `next/image` para os assets do canvas — o script é manual, idempotente e commitado; o canvas carrega a URL crua.
- Silhueta/avatar por estadual; segundo estilo de card; compartilhamento direto em redes.
## Riscos e mitigação
- **e2e instável com 5 tiles/assets novos:** stub determinístico do S15 + waits por texto/heading + probes de canvas (`toDataURL` antes/depois da troca); assets locais same-origin.
- **Catálogo divergindo da arte:** snapshot + sha256 + contagem 53 + `ballotNumber` 5 dígitos + existência/dimensão dos 106 arquivos no unit node (falha fechado).
- **~8MB a mais no repo/imagem:** aceito (derivativos q≈82); nada no bundle JS; sem dependência do diretório de origem no build.
- **Silhueta desalinhada da arte:** geometria normalizada no `photoWindow` (a BASE cobre a base em +3%); unit da geometria + craft com o estadual real.
- **Troca de estadual mexer no recorte/harmonia:** par de arte em estado próprio; recorte/transform/name intocados; e2e "troca preserva nome e recorte"; harmonia congelada registrada como assumida.
- **Falha ao decodificar o par de um estadual:** mantém o par anterior pintado, `role=alert` recuperável e CTA bloqueado enquanto o selecionado não estiver pronto; escolher outro recupera.
- **Badge `NOVO` movido quebra pins:** unit + e2e atualizados no mesmo PR (literal assumido).
- **`CardModelGallery` client:** já era consumido apenas pela ilha client (`CardsStudio`); sem payload novo no servidor.
## Aceite de engenharia
- [ ] Aceite de produto da intenção ainda coberto: 5º modelo `Time do estadual` na home e `/cards` (`#cards`, `/cards`, `?model=time-do-estadual`); 53 estaduais por toque/teclado; recorte no aparelho com o fluxo do S15; `TIME DE <nome>` com a arte do estadual; troca sem perder nome/recorte; nome nunca cortado; derivativos commitados; 100% no aparelho; os 4 modelos intocados além do badge.
- [ ] Invariantes AGENTS/engineering-standards: sem migration/collection/Consent; `src/lib` puro/client-safe (`stateDeputyCatalog` sem `utilities/`); copy pt-BR/identificadores em inglês; sem `as never`; knip sem órfãos (script e helpers com call site); artes-mestre intocadas; `pnpm gate:fast` + e2e da superfície + `pnpm push` verdes.
- [ ] Testes de domínio previstos: unit dos módulos puros (catálogo 5 ids/badge, snapshot dos 53 + assets, silhueta/subject do render) obrigatório; e2e no spec `frontend` com stub (pré-escolha, seleção por teclado, fluxo completo e download, troca preservando nome/recorte); **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 `time-do-estadual`, label `Time do estadual`, `kind:'team'`, `stateDeputyPicker:true`, `photoWindow {286,439,592,577}`, assets default do JULIO, `previewSrc` = jpeg aprovado; **`NOVO` move de `time-de-voce` para o novo** (design cena 1) e o assert e2e acompanha.
- **Catálogo (arte, ordem alfabética da pasta):** `slug|Nome exibido|Número` — fiel aos lockups, inclusive ADRIANA→`Coletivo de Enfermagem`, JAMILLE→`Jamile da Saúde`, LENINHA→`Lenínha`:
```text
adriana|Coletivo de Enfermagem|13763 · andre|André Fidalgo|13131 · andrea|Andréa Castro|55670 · angelo|Angelo Almeida|13100
arthur|Artur Barachisio Lisbôa|55255 · bobo|Bobô|65888 · carol|Carol da Pitanguinha|13110 · cicero|Cícero Monteiro|70133
denise|Denise Menezes|55111 · elane|Dra. Elaine|55100 · euclides|Euclides Fernandes|13123 · fabiola|Dra. Fabíola Mansur|43123
fatima|Fátima Nunes|13567 · felipe|Felipe Duarte|70111 · geane|Geane Vasconcelos|12120 · hilton|Hilton|50150
ivana|Ivana Bastos|55555 · jaco|Jacó|13130 · jamille|Jamile da Saúde|45456 · josafa|Josafá Marinho|12222
juliete|Juliete Barreto|65100 · julio|Julio Pinheiro|13999 · junior|Júnior Muniz|13000 · kleber|Kleber Rosa|50500
leninha|Lenínha Valente|13111 · leo|Leo de Neco|70700 · ludmilla|Ludmilla|55444 · magno|Magno|18000
marcos|Dr. Marcos Adriano|12999 · marlene|Marlene do Sindicato|18111 · mestre|Mestre Reginaldo|18273 · neusa|Neusa Cadore|13690
niltinho|Niltinho|55999 · osni|Osni|13013 · pablo|Pablo Barrozo|70900 · patrick|Patrick Lopes|70123
pinheiro|Pinheiro|13700 · radiovaldo|Radiovaldo|13050 · roberto|Roberto Carlos|43333 · robinson|Robinson|13500
rogerio|Rogério Andrade|15444 · rosemberg|Rosemberg|13444 · rosival|Rosival Leite|13333 · rowenna|Rowenna|13456
silva|Silva Neto|70444 · silvio|Silvio Dias|13113 · thiago|Thiago Gilleno|10000 · vilma|Vilma Reis|13033
vitor|Vitor Azevedo|70333 · welligton|Wellington Oliveira|40888 · wenceslau|Wenceslau|65111 · yulo|Yulo|13234
ze-raimundo|Zé Raimundo|13222
```
Origem da arte: `/home/fsolla/Downloads/DOBRADINHAS SITE-20260923T033020Z-1-001/DOBRADINHAS SITE/` (originais fora do repo).
- **Caminhos:** `public/cards/estaduais/<slug>-fotos.webp` e `<slug>-base.webp` (derivativos q≈82); `public/cards/modelo-time-de-voce-com-estadual.jpeg` = cópia byte-a-byte do asset aprovado (sha256 pinado).
- **Silhueta:** normalizada no `photoWindow` com a geometria CSS do design (cabeça circular 34% da largura/esquerda 33%/topo 0; ombros largura cheia, 72% da altura, base +3%, raios 48%/48%/10%/10%; cor `#001a42`).
- **Harmonia na troca:** a referência de tom é a FOTOS do estadual selecionado no momento do recorte; **trocar de estadual depois não re-harmoniza** (nome e recorte continuam) — gatilho: UAT reprovar o tom (Decisão 5).
- **Copy:** portados os literais novos das cenas 3–4 (`Escolha sua dobradinha`, `Estadual escolhido`, `Agora coloque seu nome`, `Escolha um estadual para continuar.`, `A prévia e o download usam a arte oficial da dobradinha escolhida.`, `A silhueta marca o lugar da sua foto. O card final usa a arte do estadual escolhido.`, `Seu estadual`, `Escolha um nome`, `Buscar por nome`, `53 estaduais · use ↑ ↓ e Enter para escolher`, `Trocar`, `Deslize para ver os cinco modelos`, legenda `Nome · número`); o resto permanece o existente (ex.: resultado `Seu card está pronto para compartilhar.` — frases alternativas do mock não são portadas).
## Self-score decision-quality: 5/5
1. **Decisões caras com rejeitadas:** modelo/flag, catálogo+assets, seletor, composição/silhueta, swap/harmonia, galeria/dots, gatilho do composer 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 (~8MB de derivativos) já estava no appetite da intenção.
3. **Rabbit holes nomeados:** 53 modelos/URL, CMS, re-harmonização, calibração por estadual, fatiar o composer, otimização em build, métricas.
4. **Depth check:** reusa `CardsStudio`/shells, `CardComposer`/`useCardCutout`/`cardRender`/`fitCardName`/banners, `cardCanvas`, o padrão de dots do `CampaignContentCarousel` e o padrão snapshot do `municipalityCatalog`; nada de abstração especulativa.
5. **Intenção preservada:** aceite de produto intacto (5º modelo, 53, recorte no aparelho, nome nunca cortado, troca preservando nome/recorte, 100% local, 4 modelos intocados); a engenharia só resolveu forma (flag, catálogo, seletor, swap).

View file

@ -0,0 +1,66 @@
# Escala/DRY pós-S30 — fatiar o CardComposer + resíduo do script de assets
Status: rascunho
Atualizado em: 2026-09-23
Issue: #1271 (débito do S30; a Issue própria nasce `depends: [1271]`)
Priority: P3
Impeccable: A — refactor interno do estúdio de cards, sem mudança visual (o e2e da superfície é a prova)
Appetite: ~0,5–1 dia eng (fill-in; três fases pequenas, sem migration)
Responsável: —
## Contexto
A entrega S30 ([cards-estadual-dobradinha-impl.md](cards-estadual-dobradinha-impl.md), Issue #1271) acrescentou o 5º modelo do estúdio de cards. O gatilho do S15 para fatiar o `CardComposer` ("um 5º modelo OU folga de ~0,5 dia no próximo item do funil", [cards-time-de-voce-impl.md](cards-time-de-voce-impl.md) § Adiados) disparou; a Decisão 7 do S30 adjudicou manter o composer único no PR e registrar a dívida aqui — o arquivo saiu de ~830 para ~1100 linhas, com os ramos `processing`/`error`/`ready` inline e ternários aninhados de 3–4 níveis (`canAdvance`/`adjustable`/`eyebrow`/`title`). A passagem `/simplify` (2 revisores) aplicou ~20 correções na própria sessão (R1–R8 abaixo); o que sobrou são débitos maiores que o cleanup e do mesmo lote (mesma superfície: estúdio de cards + script de assets do S30). Sem este item: (a) cada modelo novo (o S31 já está na fila) paga o custo de mexer num arquivo de 1100 linhas com derivações sem teste; (b) o `build-state-deputy-card-assets.mjs` segue com dois passes e predicados gêmeos que podem divergir.
**Coordenação:** o S31 (#1272, colinha) também toca `CardComposer.tsx`/`StateDeputySelect.tsx`; executar este lote **depois** do S31 evita rebase (a Issue depende só de #1271, que destrava sozinho).
## Objetivos
- Os blocos de estado do composer (`processing`/`error`/`ready`, campo de nome, avisos de privacidade) vivem em componentes presentacionais com props tipadas; o `CardComposer` fica com a orquestração (estado, efeitos, canvas).
- `canAdvance`/`adjustable`/`eyebrow`/`title`/estágios de prévia viram funções nomeadas e testáveis (fim dos ternários aninhados por `kind`/estado/seleção).
- `scripts/build-state-deputy-card-assets.mjs` faz um passe único: um `Map` slug→par montado na validação, sem o 2º `readdir` nem o `pick`/`findOne` gêmeos.
- Guardrails: sem migration; nenhuma mudança de DOM/a11y/copy/URL; os 4 modelos antigos intocados; `pnpm gate:fast` + e2e `Cards personalizados` (S14/S15/S30) verdes.
## Fases
1. **F1 (S3, maior ROI) — subcomponentes de estado.** Mover os blocos `step === 'compose' && teamProcessing|teamError|teamReady` (e o campo de nome/avisos que os acompanha) para `src/components/cards/CardComposerStates.tsx`, com props explícitas e sem estado próprio, preservando literais, roles e labels. Opções: A) componentes no mesmo diretório com props explícitas | B) manter inline no arquivo | C) render-props. Recomendação: A — é o dono natural do que hoje é JSX condicional sem dono; rejeitadas B (não reduz o arquivo) e C (cerimônia sem 2º consumidor). Prova: e2e S30/S15 + unit existentes verdes, sem snapshot/DOM novo; `pnpm gate:fast`.
2. **F2 (S3) — derivações puras.** Extrair `canAdvance`, `adjustable`, `eyebrow`, `title` e os estágios de prévia para funções nomeadas (ex. `src/components/cards/cardComposerState.ts`) com unit `// @vitest-environment node` cobrindo a matriz `kind` (name/photo/team/state-deputy) × estado do recorte × seleção × erro de nome. O composer só consome o resultado. Prova: unit novo verde + e2e da superfície.
3. **F3 (S2, carona) — passe único do script de assets.** Montar `Map<slug, { dir, photos, base }>` no 1º passe (validação) e derivar do Map no 2º, eliminando o `readdir` repetido e o par `pick`/`findOne`. Prova: re-executar `pnpm build:state-deputy-card-assets -- --from '<origem>'` deixa `git status public/cards/estaduais` limpo (byte-idêntico).
## Já resolvido no simplify (não reabrir)
- **R1** `renderTeamCard` num ramo só com `subject` discriminado (fim da duplicação de ramo).
- **R2** trava de scroll programático nos dots da galeria (`PROGRAMMATIC_SCROLL_LOCK_MS`).
- **R3** opções do listbox com `tabIndex={-1}` (fim dos 53 tab stops).
- **R4** alvo dos dots 6px→24px (WCAG 2.5.8) + ordem dica→dots do design.
- **R5** picker: scroll da opção ativa ao digitar, mensagem vazia fora do listbox, autofocus só em abertura do usuário, clamp do `activeIndex`, retry ao reescolher o mesmo estadual após falha.
- **R6** `deputyPairReady` exige o par carregado; fallback morto de `photoWindow`; casts do `querySelectorAll`; `.map((file) => file)` morto no script.
- **R7** e2e: comentário dos pixels mágicos, clique nos dots via `aria-current`, nome longo do modelo novo, estado vazio da busca.
- **R8** divergências do designer (destaque do tile novo + escala da prévia S30) portadas e certificadas.
## Explicitamente fora (descartes e defers com gatilho)
- **P2 — 3ª cópia do padrão listbox inline** (`StateDeputySelect` vs `CampaignListOmnibox` vs `RelationChipCell`): **defer**, gatilho já documentado no próprio componente — 4º consumidor com implementação própria (reuso de `StateDeputySelect` não conta) ou um fix de a11y que precise valer nos três.
- **P4 — `nome · número` em 2 lugares**: **descartar** — não é o mesmo formato (legenda `nome · número`; trigger `número · Trocar` + nome em span) e são 2 call sites < 3.
- **P6 + P10 — par JULIO default morto/duplicado** (`cardModels.ts` × `stateDeputyCatalog.ts`; o composer nunca carrega os defaults e `previewSrc` cobre tile/pré-escolha): **defer**, gatilho — o S31 (#1272) acrescenta o 6º modelo e revisita o contrato `CardModel`, ou surge um 2º consumidor do par default.
- **P5 — `window` como nome de parâmetro em `cardRender.ts`**: **descartar** — pureza herdada do S15, sem bug; rename amplo (score 1) por score ≤2.
- **P7 — comentários/mensagens pt-BR no script novo**: **descartar** — família `build:*` mista (`build-radio-artes.mjs` é pt-BR) e comentário não é identificador.
- **P8 — `identitySha256` redundante após o `toEqual`**: **descartar** — precedente idêntico no `municipalityCatalog`; o hash é o guard de regeneração do fixture.
- **P9 — `!` em `tests/unit/cardRender.unit.spec.ts`**: **descartar** — convenção do próprio arquivo (test-only, pré-existente).
## Rabbit holes / Não escopo
- Extrair o listbox compartilhado agora (P2) — três superfícies divergentes (admin/omnibox, chips de relação, estúdio público) e gatilho documentado; abstração prematura.
- Mexer no contrato `CardModel`/defaults (P6) — o S31 já vai tocá-lo.
- Redesenho de prévia/segunda gramática visual, re-harmonização na troca de estadual (assumida no S30), 53 modelos/URL, CMS.
- Mudar copy/literais ou o fluxo dos 4 modelos antigos.
## Self-score (decision-quality)
1. Decisões caras com rejeitadas? **Sim** — F1/F2 com opções e rejeitadas; nada de access/LGPD/schema/unicidade entra (piso 4–5 não se aplica) e descartes/defers estão nomeados com gatilho.
2. Cabe no appetite? **Sim** (~0,5–1 dia, três fases pequenas, sem migration).
3. Rabbit holes nomeados? **Sim** — listbox compartilhado, contrato `CardModel`, re-harmonização.
4. Depth check? **Sim** — reusa composer/script existentes; F2 nasce com unit real, sem módulo de pureza especulativo.
5. Outcome do S30 intocado? **Sim** — comportamento/DOM/copy/URL idênticos; o e2e da superfície é a prova.
**Score: 5/5.**

View file

@ -50,6 +50,7 @@
"build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs",
"build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs",
"build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs",
"build:state-deputy-card-assets": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-state-deputy-card-assets.mjs",
"build:municipality-zone-geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-municipality-zone-geometries.mjs",
"build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs",
"reels:build": "node scripts/build-reel.mjs",

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Some files were not shown because too many files have changed in this diff Show more