From 1b6de775a0cc74d10a17512dbeb1d29b00a4ed9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sun, 19 Jul 2026 13:02:47 -0300 Subject: [PATCH] Unify campaign auth pages with shared shell and improve login UX. Extract CampaignAuthPageShell and shared copy tokens across login, forgot-password, and reset-password flows; add adaptive inputMode, credential error a11y, and roadmap skill updates from the FD2 critique pass. --- .cursor/skills/capture-review-debts/SKILL.md | 143 ++++++++++ .cursor/skills/compile-roadmap/SKILL.md | 257 ++++++++++++++++++ .../skills/implement-roadmap-item/SKILL.md | 4 +- .cursor/skills/roadmap-item/SKILL.md | 63 ++++- .cursor/skills/roadmap-item/plan-template.md | 25 +- .../suggest-next-roadmap-items/SKILL.md | 152 +++++++++++ ...10-58Z__src-app-campaign-campanha-login.md | 101 +++++++ ...55-17Z__src-app-campaign-campanha-login.md | 99 +++++++ .../esqueci-senha/ForgotPasswordForm.tsx | 16 +- .../campanha/esqueci-senha/page.tsx | 16 +- .../(campaign)/campanha/login/LoginForm.tsx | 62 ++++- src/app/(campaign)/campanha/login/page.tsx | 16 +- .../redefinir-senha/ResetPasswordForm.tsx | 10 +- .../campanha/redefinir-senha/page.tsx | 55 ++-- .../campaign/CampaignAuthPageShell.tsx | 33 +++ src/lib/campaignAuthCopy.ts | 10 + .../campaignCompositionCleanup.unit.spec.ts | 52 +++- 17 files changed, 1017 insertions(+), 97 deletions(-) create mode 100644 .cursor/skills/capture-review-debts/SKILL.md create mode 100644 .cursor/skills/compile-roadmap/SKILL.md create mode 100644 .cursor/skills/suggest-next-roadmap-items/SKILL.md create mode 100644 .impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md create mode 100644 .impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md create mode 100644 src/components/campaign/CampaignAuthPageShell.tsx create mode 100644 src/lib/campaignAuthCopy.ts diff --git a/.cursor/skills/capture-review-debts/SKILL.md b/.cursor/skills/capture-review-debts/SKILL.md new file mode 100644 index 00000000..6b52f2bf --- /dev/null +++ b/.cursor/skills/capture-review-debts/SKILL.md @@ -0,0 +1,143 @@ +--- +name: capture-review-debts +description: Use when the user asks to register leftover /simplify or /impeccable findings on the Teqo roadmap, harvest session review debts, triage post-simplify or post-critique follow-ups, or says "registra os débitos", "o que ficou do simplify", "coloca no roadmap o que o critique apontou", "harvest debts". +--- + +# Capturar débitos de /simplify e /impeccable no roadmap + +Após um ciclo de entrega, `/simplify` e `/impeccable` deixam achados **maiores que o cleanup da sessão**. Esta skill **triageia** esses achados a partir do contexto da sessão, decide o que vira roadmap (e o que não), **mescla** o que for do mesmo lote, e só então registra via **`roadmap-item`**. Não implementa código. + +**REQUIRED SUB-SKILL:** Use `roadmap-item` para qualquer inclusão/absorção no `docs/roadmap.md` + `docs/plans/`. Nunca edite roadmap/planos ad hoc neste fluxo. + +Precedente canônico: um plano `escala-dry-pos-*` (engenharia pós-simplify) e/ou um plano UX pós-critique (ex.: FD+ vs FD2) — tipos **separados**, fases internas mescladas. + +## Checklist + +``` +- [ ] 1. Colher candidatos da sessão (simplify + impeccable) +- [ ] 2. Deduplicar contra código, roadmap e planos +- [ ] 3. Pontuar importância e classificar destino +- [ ] 4. Mesclar relacionados (mesmo lote / mesma superfície) +- [ ] 5. Apresentar tabela de triage e obter confirmação +- [ ] 6. Registrar só o aprovado via roadmap-item (1 invocação por item/plano) +``` + +## Passo 1 — Colher candidatos + +Fontes (nesta ordem; não invente achados que não apareceram): + +1. **Mensagens da sessão atual** — resumos finais de `/simplify` ("skipped", "recommend", "larger than cleanup"), P0–P3 de `/impeccable critique`, notas de polish não feitos. +2. **Snapshots** — `.impeccable/critique/*.md` do alvo da sessão (mais recente por superfície). +3. **Diff local** — só para verificar se um achado já foi aplicado; não para minerar débitos novos. +4. **Transcripts** — só se a sessão atual for curta demais e o usuário apontar um chat anterior. + +Para cada candidato, registre uma linha bruta: + +| Campo | Conteúdo | +| ----------- | --------------------------------------------------- | +| `id` | S1, S2… (simplify) / I1, I2… (impeccable) | +| `origem` | simplify quality\|perf\|reuse **ou** critique P0–P3 | +| `resumo` | uma frase | +| `evidência` | quote curto / path / heurística | +| `já_feito?` | sim se o diff/código já resolve | + +**Ignore na colheita:** elogios, sugestões hipotéticas sem achado, itens que o próprio simplify marcou como "fixed". + +## Passo 2 — Deduplicar + +Contra o repositório **antes** de pontuar: + +| Check | Ação se verdadeiro | +| ------------------------------------------------------ | --------------------------------------------------------------- | +| Já no diff / código da sessão | → balde **já_resolvido** (não reabrir) | +| Já em `docs/plans/*.md` ou roadmap (grep termos/paths) | → **absorver** no plano existente ou **descartar** como coberto | +| Pré-existente em `main`, fora do escopo da entrega | → **descartar** deste lote (bug separado só se o usuário pedir) | +| Intentional em `DESIGN.md` / decisão travada | → **descartar** (ex.: field-mode mobile documentado) | + +## Passo 3 — Pontuar e decidir destino + +Para cada candidato restante, atribua **importância** (1–5) com estes âncoras Teqo: + +| Score | Quando | +| ----- | ------------------------------------------------------------------------------------------- | +| 5 | Bloqueia feature dependente, hot path em produção, P0/P1 critique aberto, risco access/LGPD | +| 4 | Perf/DRY com ≥3 call sites ou custo herdado por A5/B3/E4/lista de núcleos | +| 3 | UX P2 acionável (Alex/Casey/Lia), DRY claro 2 call sites, a11y outline | +| 2 | Higiene/naming, polish cosmético, P3 critique | +| 1 | Preferência de estilo, rename de pureza, micro-otimização sem evidência | + +**Destino** (escolha exatamente um): + +| Destino | Critério | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| **registrar** | Score ≥3 **e** não coberto; cabe num plano próprio ou lote mesclado | +| **absorver** | Score ≥3 mas é fase natural de um plano/`escala-dry-pos-*`/UX já existente | +| **descartar** | Score ≤2 **ou** rename de pureza **ou** "nice to have" sem dono de produto **ou** revisores do simplify pediram skip explícito | +| **já_resolvido** | Feito no cleanup da sessão | + +**Separação de tipos (não misturar num único plano):** + +- **Engenharia / escala / DRY** → família `escala-dry-pos-.md`, IDs tipo `A7`, `C11`, `FD+`, `RS+`, `O0+` (fill-in ou próximo ID da trilha). +- **UX / produto pós-critique** → plano tipo `*-ux-pos-critique.md` ou fases num FD2 existente (`Impeccable: B|C|D` no `roadmap-item`). +- Nunca fundir N+1 SQL com "glossário de engajado" no mesmo item (precedente FD+ ≠ FD2). + +**Impeccable no registro:** ao chamar `roadmap-item`, a classificação A–D dele vale — débitos só-backend são **A**; UX pós-critique são **B** (encaixe) ou **C** se fluxo novo. + +## Passo 4 — Mesclar + +Dentro do **mesmo tipo** (engenharia **ou** UX): + +- Mesclar se compartilham **superfície** (mesma rota/loader/shell) **ou** **pai** (mesmo item do roadmap que acabou de ser entregue). +- Um plano, várias **fases** ordenadas por ROI (perf/access antes de DRY cosmético; P2 produto antes de P3 motion). +- Não mesclar across pais não relacionados (débito do C3 com débito do reset de senha → dois itens). +- Se sobrar um único achado score ≥3, ainda assim um plano curto é melhor que linha órfã no roadmap sem plano. + +Alvo de merge: **1 plano engenharia + no máximo 1 plano UX** por sessão de entrega, salvo pais distintos. + +## Passo 5 — Tabela de triage + confirmação + +Mostre ao usuário **antes** de editar docs: + +```markdown +| ID | Resumo | Origem | Score | Destino | Lote mesclado | +| --- | ------------- | ---------------- | ----- | -------------- | --------------- | +| S1 | … | simplify/perf | 4 | registrar | Escala pós-X F1 | +| I2 | … | critique P2 | 3 | absorver → FD2 | — | +| S3 | rename pureza | simplify/quality | 2 | descartar | — | +``` + +Inclua baldes **já_resolvido** e **descartar** com uma linha de racional cada (transparência > silêncio). + +**Pare e confirme.** Só avance ao Passo 6 com aprovação explícita (ou ajuste pedido). Sem confirmação = não toca `docs/roadmap.md` / `docs/plans/`. + +## Passo 6 — Registrar via `roadmap-item` + +Para cada lote com destino **registrar** ou **absorver**: + +1. Invoque **`roadmap-item`** com o escopo do lote (não um item por achado micro). +2. Se **absorver**: peça à skill para editar o plano existente (nova fase/seção), não criar ID paralelo — mesmo precedente "Gap vs 2022". +3. Se **registrar**: slug `escala-dry-pos-` ou `-ux-pos-critique`; posicione como fill-in (paralelo) salvo dependência dura de trilha. +4. No plano, seções obrigatórias além do template: + - **Já resolvido no simplify/critique (não reabrir)** + - **Explicitamente fora** (skips dos revisores + descartes deste triage) +5. Classe Impeccable A–D conforme `roadmap-item` Passo 4. + +Não implemente as fases aqui. Apontar `implement-roadmap-item` só se o usuário pedir em seguida. + +## Anti-padrões (baseline) + +| Desculpa | Realidade | +| ---------------------------------------- | --------------------------------------------------------------------- | +| "Virou um item por achado" | Mesclar no lote; roadmap poluído é pior que débito omitido de score 2 | +| "Edito o roadmap na mão, é só um bullet" | Sempre `roadmap-item` (grafo, janela, plano, cortes) | +| "Rename PascalCase / pureza merece ID" | Score ≤2 → descartar, a menos que desbloqueie reuso real já pedido | +| "Junto DRY e UX num FD+" | Quebra o precedente FD+/FD2; tipos separados | +| "Não li o plano existente" | Grep primeiro; absorver > duplicar | +| "Registro sem perguntar" | Passo 5 é gate; docs de produto não são scratchpad | + +## Resumo ao usuário + +1. Contagem: colhidos / já_resolvidos / descartados / absorvidos / a registrar +2. Tabela de triage (final pós-confirmação) +3. IDs/slugs criados ou planos estendidos (links) +4. O que ficou de fora de propósito (e por quê) diff --git a/.cursor/skills/compile-roadmap/SKILL.md b/.cursor/skills/compile-roadmap/SKILL.md new file mode 100644 index 00000000..8e193964 --- /dev/null +++ b/.cursor/skills/compile-roadmap/SKILL.md @@ -0,0 +1,257 @@ +--- +name: compile-roadmap +description: >- + Lê docs/roadmap.md e fontes relacionadas, classifica o que já foi entregue, + comprime o histórico em um resumo curto e reescreve o roadmap focando nos + próximos passos (janelas, grafo, fill-ins, bloqueadores). Usar quando o + usuário pedir para "compilar o roadmap", "limpar o roadmap", "atualizar + roadmap para próximos passos", "consolidar entregas no roadmap", "enxugar + docs/roadmap.md", ou quando o arquivo estiver inchado com ciclos já + mesclados em main. +--- + +# Compilar e limpar o roadmap + +Esta skill **reescreve** `docs/roadmap.md` para voltar a ser um documento de **próximos passos**, não um diário de entregas. O histórico detalhado permanece nos planos (`docs/plans/`) e no notebook do projeto — o roadmap só aponta o que ainda importa operar. + +**Não implementa código.** Não cria itens novos (isso é `roadmap-item`). Não escolhe o próximo a implementar (isso é `suggest-next-roadmap-items`). + +**Divisão com as skills irmãs:** + +| Skill | Papel | +| ---------------------------- | ----------------------------------------------------- | +| `compile-roadmap` | **Enxugar** o roadmap: resumir feito, destacar aberto | +| `suggest-next-roadmap-items` | Escolher candidatos entre o que o roadmap já lista | +| `roadmap-item` | Registrar ideia nova / criar plano | +| `implement-roadmap-item` | Auditar plano + implementar o ID escolhido | +| `capture-review-debts` | Triagear débitos de `/simplify` / critique | + +**Escopo padrão:** só `docs/roadmap.md`. Planos em `docs/plans/` **não** são movidos nem apagados. Só atualize/arquive planos se o usuário pedir explicitamente. + +## Checklist + +``` +- [ ] 1. Ler fontes canônicas (roadmap + notebook + gaps) +- [ ] 2. Inventariar entregues vs abertos (com evidência) +- [ ] 3. Mostrar inventário + proposta de corte ao usuário e pedir confirmação +- [ ] 4. Reescrever docs/roadmap.md no template limpo +- [ ] 5. Verificar links, mermaid, IDs e consistência cruzada +- [ ] 6. Entregar diff resumido + o que foi cortado +``` + +## Passo 1 — Ler as fontes canônicas + +Leia **nesta ordem** (não edite antes de inventariar): + +1. **`docs/roadmap.md` inteiro** — linha "Atualizado em", Onda 0, seções Ciclo/Trilha, grafo mermaid, tabelas por janela, Fill-ins, Cortes, Bloqueadores, Site, Admin, Fora de escopo, Fontes. +2. **`.cursor/rules/projects/nucleos-eleitorais.mdc`** (ou notebook do projeto ativo) — status operacional que pode estar à frente do roadmap. +3. **`AGENTS.md`** — só as seções Known Gaps / Recently resolved / checklist de campanha (para alinhar bloqueadores e não contradizer decisões travadas). +4. **Data de hoje** (contexto da sessão) vs âncoras do calendário — define a janela vigente no cabeçalho limpo. +5. **Planos** — só sob demanda: abra `docs/plans/.md` se o status no roadmap estiver ambíguo (ex.: "implementado" vs "registrado" vs "em branch"). Não leia todos os planos de uma vez. + +Fontes **fora do repo** (`plano-arquitetura-campanha-2026.md`, Cowork) só se o inventário precisar resolver um conflito; cite e não invente. + +## Passo 2 — Inventariar entregues vs abertos + +Monte duas listas. Sinais de **entregue** (qualquer um basta): + +- `✓` no nó mermaid +- "(entregue …)" / "implementado e mesclado" / "engenharia pronta e mesclada" / "MVP entregue" +- Notebook ou AGENTS "Recently resolved" alinhado ao mesmo ID + +Sinais de **ainda aberto** (manter em destaque): + +- Sem marca de entrega +- "registrado" / "proposto" / "futuro" / "validar com produto" +- Engenharia em `main` mas **produção bloqueada** (ex.: C2 dados reais ↔ Onda 0) — trate como **bloqueio operacional**, não como feature a implementar de novo +- Débitos `escala-dry-pos-*` / `*+` sem ✓ +- Fill-ins sem ✓ +- Itens do site/admin/white-label ainda listados como próximos + +Para cada ID, uma linha: + +| ID | Estado | Evidência (1 frase) | Plano linkado | Ação no compile | +| --- | ------------------------------ | ------------------- | ------------- | ------------------------ | +| A4 | entregue | mesclado 2026-07-18 | baseline-… | comprimir em Já entregue | +| E4 | aberto | janela 3 | mapa-proj… | manter em Próximos | +| C2 | entregue eng. / bloqueio prod. | Onda 0 Consent | cadastro-… | 1 linha em Já + Onda 0 | + +**Não invente status.** Se roadmap e notebook divergirem, prefira o notebook + grep rápido no código só para desempate; reporte a divergência ao usuário. + +## Passo 3 — Confirmar antes de escrever + +Mostre ao usuário (conciso): + +1. Contagem: N entregues a comprimir / M abertos a manter / K bloqueios operacionais. +2. O que **sai** do corpo detalhado (seções Ciclo 1/2/2+ verbosas, linhas de janela já ✓, design-refs só de itens feitos, prosa de migration IDs já aplicados). +3. O que **fica** (calendário, princípios, Onda 0 pendente, grafo dos abertos, janelas só com pendentes, fill-ins abertos, cortes ainda relevantes, bloqueadores, fora de escopo). +4. Pergunta: "Posso reescrever `docs/roadmap.md` assim?" + +**Pare aqui até confirmação**, salvo se o usuário já tiver dito explicitamente "compila/limpa agora" neste turno. + +Opcional (só se pedido): marcar `Status: Done` no topo de planos entregues, ou mover para `docs/plans/archive/` — nunca como default desta skill. + +## Passo 4 — Reescrever no template limpo + +Substitua o arquivo inteiro (não faça um diff cirúrgico em 40 lugares — o risco de inconsistência é maior que um rewrite controlado). Preserve: + +- IDs estáveis (`A5`, `C10`, `E4`, `O0+`, …) +- Links para planos ainda relevantes +- Datas de âncora do calendário TSE +- Decisões travadas e Fora de escopo (podem enxugar prosa, não o sentido) +- Tabela de bloqueadores atualizada + +Use **este esqueleto** (adicione/remova subseções só se o inventário exigir): + +```markdown +# Roadmap — Teqo + +Atualizado em: YYYY-MM-DD (janela N vigente; foco: …; último compile) + +Registro canônico dos **próximos** planos e débitos. Histórico de entregas: +resumo abaixo + planos em [`docs/plans/`](plans/) + notebook +[`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). + +## Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026) + +[tabela de datas — manter; enxugar coluna Consequência se redundante] + +## Princípios e decisões travadas + +[bullets curtos; sem repetir AGENTS inteiro] + +## Onda 0 — caminho crítico para dados reais + +[só o que AINDA falta: lote jurídico, smoke, onboarding, holds. +Não recontar migrations/features já em main — 1 frase "engenharia de +núcleos/C2/C3/… já em produção de código".] + +## Já entregue (resumo) + +Agrupar por trilha/ciclo em **1–3 linhas por grupo**, não por parágrafo de +implementation notes. Exemplo de densidade alvo: + +- **Núcleos MVP + Ciclo 2** — auth/`campaignUser`, território A1/A2, baseline + A3/A4, overview B1, share C1, PWA D1, geometrias B2. +- **Operação** — C2 (eng. pronta; prod. ↔ Onda 0), C3 agenda, C6–C9 escala. +- **Trilha E (parcial)** — E1+E3 metas/estratégia; E2 série/tendência; faltam E4… +- **Fill-ins** — visitados, reset senha/perfil, … (só os ✓) + +Linkar o plano **só** se ainda for referência útil; não listar migration IDs +nem fases `/simplify` no resumo. + +## Próximos — Campanha (`/campanha`) + +### Por trilha (só abertos) + +- **A** — A5, A6, A7, A8, … +- **B** — B3, B4, B5, … +- **C** — C4, C5, C10, C11, … +- **D** — D2, … +- **E** — E4, E5, E6, E7, … +- **Débitos / fill-ins abertos** — O0+, VR+, RS+, FD+, FD2, … + +Cada bullet: `ID — nome` + meia frase + link do plano. Sem changelog. + +### Referências de design (só itens abertos com ref) + +[tabela Item | Design | Plano — remover linhas de itens já entregues] + +### Grafo de dependências (abertos + predecessores mínimos) + +Mermaid: incluir nós **abertos** e só os entregues que ainda são +dependência dura/suave citada. Marcar entregues com `✓` no label se +precisarem aparecer. Remover subgraphs inteiros se todos os nós estão ✓ +e nenhum aberto depende deles. + +### Sequência por janela (só pendentes) + +Tabelas Janela 1–4: **apagar linhas já entregues**. Renumerar "Ordem" +como sequência contínua dos pendentes (ou manter números históricos só +se o usuário pedir continuidade — default = renumerar limpo). + +Colunas: Ordem | Item | Plano | Depende de | Paralelizável com. + +### Fill-ins abertos + +Só itens sem ✓. Uma linha cada + link. + +### Cortes seguros / não cortáveis + +Reescrever a lista para citar **apenas IDs ainda abertos**. Remover +conselhos sobre fases já mescladas (ex.: "preferir C6 fases 1–2" se C6 ✓). + +## Bloqueadores atuais + +[tabela atualizada; remover resolvidos] + +## Site público / Admin / White-label / Fora de escopo / Fontes + +[manter seções; Site: separar Já entregue (1 linha) vs Próximos] +``` + +**Regras de corte (obrigatórias):** + +| Cortar do roadmap compilado | Manter | +| -------------------------------------------------------------------------- | -------------------------------------- | +| Seções "Ciclo 1/2/2+" com notas de implementação | Resumo "Já entregue" | +| Linha "Atualizado em" enciclopédica | 1 linha: data + janela + foco | +| Migration IDs / nomes de fases `/simplify` no corpo | Links para planos abertos | +| Design-refs de itens ✓ | Design-refs de abertos | +| "Itens consolidados/removidos" antigo (salvo se ainda explicar um ID vivo) | Fora de escopo atual | +| Prosa duplicada AGENTS/notebook | Ponteiros para AGENTS/notebook | +| Janelas cheias de "(entregue …)" | Só pendentes na janela vigente+futuras | + +**Densidade alvo:** o arquivo compilado deve caber com folga na leitura de uma sessão de priorização — tipicamente **bem menor** que a versão diário. Se após o rewrite ainda parecer um changelog, corte mais o "Já entregue". + +## Passo 5 — Verificar + +Antes de declarar pronto: + +- [ ] Todo ID **aberto** do inventário aparece em Próximos **ou** Bloqueadores **ou** Fill-ins +- [ ] Nenhum ID entregue reaparece como tarefa a fazer (exceto bloqueio operacional explícito, ex. "C2 dados reais") +- [ ] Links `plans/…` dos itens abertos resolvem (grep / existência do arquivo) +- [ ] Mermaid parseia (sem nós órfãos óbvios; setas só entre IDs que existem no diagrama) +- [ ] Âncoras de calendário e "não cortáveis" ainda cobrem Onda 0 / base nominal / agenda se forem política vigente +- [ ] Skills irmãs continuam válidas: `suggest-next-roadmap-items` e `roadmap-item` devem conseguir operar no arquivo limpo + +Se achar item **aberto sem plano**, não invente o plano — liste no resumo final e ofereça `roadmap-item`. + +## Passo 6 — Entregar + +Resposta ao usuário: + +```markdown +## Roadmap compilado + +- Arquivo: `docs/roadmap.md` +- Comprimidos: N itens → seção Já entregue +- Em foco: M abertos (janela vigente: …) +- Bloqueios operacionais: … + +## Principais cortes + +- … + +## Próximo passo sugerido + +Rodar `suggest-next-roadmap-items` na versão limpa? +``` + +**Proibido neste fluxo:** + +- Implementar código, abrir PR, ou marcar entregas que não estavam evidenciadas +- Apagar ou mover `docs/plans/*` sem pedido explícito +- Inventar IDs / reabrir itens "Fora de escopo" +- Expandir o roadmap com ideias novas (use `roadmap-item`) +- Deixar a linha "Atualizado em" como parágrafo de changelog de novo + +## Exemplo de compressão + +**Antes (ruim no roadmap compilado):** + +> **C6 Escala e DRY pós-C2 — implementado e mesclado em `main` (2026-07-19)** — import bulk drizzle, token HMAC + `supporterImportBatch`, KPI aggregate, shells… Duas passagens `/simplify`; débitos maiores → C8. + +**Depois (resumo):** + +> **Operação / escala** — C2–C3 + C6–C9 em `main` (C2 prod. ↔ Onda 0). Débitos abertos: C10, C11. diff --git a/.cursor/skills/implement-roadmap-item/SKILL.md b/.cursor/skills/implement-roadmap-item/SKILL.md index a72c0deb..d5b8204d 100644 --- a/.cursor/skills/implement-roadmap-item/SKILL.md +++ b/.cursor/skills/implement-roadmap-item/SKILL.md @@ -78,7 +78,7 @@ Leia, nesta ordem: 5. **AGENTS.md** — seções relevantes ao domínio do item (Campaign auth, Posts & Tags, migrations, convenções de naming). 6. **Planos vizinhos** citados nas seções "Dependências" e "Não escopo" — para saber onde o escopo deste item termina e o dos outros começa. -Item sem plano detalhado (ex.: só design, como C5; ou fill-in "sem plano detalhado ainda"): primeiro crie o plano seguindo a skill `roadmap-item` (template em `.cursor/skills/roadmap-item/plan-template.md`), depois continue esta skill a partir do Passo 4. +Item sem plano detalhado (ex.: só design, como C5; ou fill-in "sem plano detalhado ainda"): primeiro crie o plano seguindo a skill `roadmap-item` (template em `.cursor/skills/roadmap-item/plan-template.md` — inclui classificação Impeccable A–D e brief compacto se B/C/D), depois continue esta skill a partir do Passo 4. Se o plano já trouxer classe + brief, reutilize no Passo 8 em vez de reclassificar do zero; só reabra se o código/roadmap mostrar que a classe mudou. ## Passo 4 — Auditar o plano contra o repositório @@ -197,3 +197,5 @@ Checklist AGENTS.md completo contra banco **local**; scan Aikido nos arquivos fi **Após planejamento (Passos 1–8):** (1) item e estado no roadmap (janela, dependências, bloqueios); (2) veredito da auditoria — o que confirmou, o que estava defasado, o que conflitava; (3) o plano de implementação em fases, com fases Impeccable marcadas; (4) classificação A/B/C/D e decisões assumidas que merecem validação de produto. **Após implementação (Passo 10):** o que entrou em cada fase, resultado do critique/polish (score/P0–P1 se houver), checklist AGENTS.md + Aikido, e o que ficou de fora. + +Se `/simplify` ou `/impeccable critique` deixaram follow-ups **maiores que o cleanup da sessão**, ofereça a skill `capture-review-debts` (triage → confirmação → `roadmap-item`) em vez de registrar débitos ad hoc ou abandoná-los no chat. diff --git a/.cursor/skills/roadmap-item/SKILL.md b/.cursor/skills/roadmap-item/SKILL.md index c1c03dfd..1ad1b59d 100644 --- a/.cursor/skills/roadmap-item/SKILL.md +++ b/.cursor/skills/roadmap-item/SKILL.md @@ -7,16 +7,21 @@ description: Adiciona uma ideia, tarefa ou funcionalidade ao docs/roadmap.md do Esta skill transforma uma ideia solta em: (1) um item posicionado corretamente no `docs/roadmap.md` e (2) um arquivo de plano em `docs/plans/.md` linkado a partir do item. O roadmap é o registro canônico **em ordem de execução, com dependências e paralelismo explícitos** — um item mal posicionado é pior que um item ausente. +**Divisão com `/impeccable`, `implement-roadmap-item` e `compile-roadmap`:** aqui se **classifica** a superfície UI e se **semeia** âncoras de design no plano (`PRODUCT.md` / `DESIGN.md` / design-refs / brief compacto). **Não** se implementa UI nem se roda craft/critique/polish — isso pertence a `implement-roadmap-item` (Passos 8–10). Rodar Impeccable completo neste fluxo atrasa o registro e duplica trabalho. Se o `docs/roadmap.md` estiver inchado com ciclos já entregues, rode `compile-roadmap` **antes** de posicionar um item novo — senão o grafo/janelas ficam inconsistentes. + +**Entrada desde reviews:** débitos de `/simplify` / `/impeccable` triageados por `capture-review-debts` chegam aqui já mesclados em lotes — não re-expanda um lote em um item por achado micro. + ## Checklist do fluxo ``` - [ ] 1. Ler docs/roadmap.md inteiro (nunca editar sem ler a versão atual) - [ ] 2. Classificar a ideia e checar duplicidade/absorção - [ ] 3. Explorar o código relevante para fundamentar o plano -- [ ] 4. Decidir posicionamento: seção, ID, dependências, janela, paralelismo -- [ ] 5. Criar docs/plans/.md a partir do template -- [ ] 6. Editar o roadmap em TODOS os pontos de consistência -- [ ] 7. Verificar links, mermaid e consistência cruzada +- [ ] 4. Classificar superfície UI (A–D) e semear Impeccable no plano se B/C/D +- [ ] 5. Decidir posicionamento: seção, ID, dependências, janela, paralelismo +- [ ] 6. Criar docs/plans/.md a partir do template +- [ ] 7. Editar o roadmap em TODOS os pontos de consistência +- [ ] 8. Verificar links, mermaid e consistência cruzada ``` ## Passo 1 — Ler o roadmap atual @@ -59,7 +64,43 @@ Um plano competente cita arquivos, utilities e padrões **reais** — não abstr - Se toca pessoa → é join com `Contact`, nunca cadastro paralelo. Se toca opt-in/PII → `Consent` por chave estável, falhando fechado. - Precedente análogo: encontre o plano existente em `docs/plans/` mais parecido e espelhe o nível de detalhe dele. -## Passo 4 — Decidir o posicionamento +## Passo 4 — Classificar superfície UI e semear Impeccable + +Classifique o item **antes** de travar a abordagem do plano. Use a mesma tabela que `implement-roadmap-item` (Passo 8) — divergência aqui vira plano que a implementação precisa reescrever. + +| Classe | Critério | Neste fluxo (`roadmap-item`) | +| ---------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | +| **A — Só backend** | Migration, seed, utility, access, aggregate SQL; zero rota/componente UI | Declare `Impeccable: N/A` no plano; **não** leia DESIGN.md só por formalidade | +| **B — UI encaixada** | Edita tela/componente existente sob o tema `campaign` / site público | Semear âncoras + decisões de UX no plano; shape completo fica para a implementação | +| **C — UI nova / ambígua** | Rota nova, fluxo multi-tela, ou sem design-ref claro | Semear âncoras + **brief compacto** (ver abaixo); marcar shape obrigatório na implementação | +| **D — Design-ref já existe** | Par em `docs/design-refs/latest/` (tabela do roadmap ou pasta) | Linkar ref no plano; estrutura vale, paleta NÃO; shape compacto na implementação | + +**Se A:** pule o restante deste passo e vá ao Passo 5. + +**Se B, C ou D — âncoras obrigatórias (ler, não reinventar):** + +1. `PRODUCT.md` + `DESIGN.md` na raiz (register `product` → Field Desk / Signal Red para `/campanha`; `brand` só se for marketing do site público). +2. Design-ref em `docs/design-refs/latest/` se existir (grep na tabela "Referências de design" do roadmap). +3. Critique recente em `.impeccable/critique/` só se o item **nasce** de um débito de critique (precedente: FD2 / `field-desk-ux-pos-critique.md`) — cite o snapshot; não rode critique novo aqui. +4. Componentes/shells já no produto (`CampaignPageShell`, `CampaignMetricStrip`, shadcn em `src/components/ui`) — o plano deve reusar, não propor segundo design system. + +**Brief compacto (obrigatório em C; opcional em B se a direção ainda for ambígua):** + +Não rode a entrevista longa de `/impeccable shape` nem craft. No plano (seção "Design (Impeccable)" do template), registre em ≤½ página: + +- Persona / estado de espírito na tela (Alex, Casey, Lia — ou quem o PRODUCT.md nomear). +- Um job principal da superfície (uma frase). +- Estratégia de cor desta superfície: Restrained (default de produto) salvo exceção justificada. +- Anti-goals (ex.: não hero-metric SaaS; não segundo sistema de cards). +- Na implementação: `Impeccable: shape → craft → critique → polish` (C) ou `craft compacto → critique → polish` (B/D). + +**Proibido neste passo:** + +- `/impeccable craft`, `critique`, `polish`, `live` — pertencem a `implement-roadmap-item`. +- Entrevista shape de várias rodadas que bloqueia o registro no roadmap. Se C estiver **demasiado** ambíguo para até um brief compacto, pergunte **2–3** perguntas ao usuário (assert-then-confirm), registre as respostas como decisões travadas, e siga — não abra um ciclo Impeccable completo. +- Inventar paleta ou tipografia novas quando `DESIGN.md` / tokens `data-theme='campaign'` já existem. + +## Passo 5 — Decidir o posicionamento Responda explicitamente, nesta ordem: @@ -73,7 +114,7 @@ Responda explicitamente, nesta ordem: Se a priorização depender de decisão de produto que você não tem como inferir, posicione com a sua melhor recomendação e marque o item como _(proposto — validar com produto)_, como foi feito com C5. -## Passo 5 — Criar o plano em `docs/plans/` +## Passo 6 — Criar o plano em `docs/plans/` - **Slug**: kebab-case em português, descritivo, curto (padrão existente: `cadastro-nominal-apoiadores.md`, `zonas-por-municipio.md`). Idioma do conteúdo: pt-BR; identificadores de código citados: inglês. - **Estrutura**: siga [plan-template.md](plan-template.md) à risca — mesmas seções, mesma ordem. Leia o template antes de escrever. @@ -83,9 +124,10 @@ Se a priorização depender de decisão de produto que você não tem como infer - "Abordagem proposta" tem diagrama mermaid + lista de componentes com caminhos de arquivo reais e assinaturas concretas. - "Não escopo" é explícito e cita para qual outro plano/item cada exclusão vai. - "Referências" lista os arquivos-fonte reais que o implementador vai abrir. -- Se existir design em `docs/design-refs/latest/`, inclua a seção "Referência visual (UX Pilot)" com o aviso padrão de paleta (a estrutura vale, a paleta não — usar tokens do tema `data-theme='campaign'`). + - Classe Impeccable **A/B/C/D** aparece no plano (cabeçalho ou seção Design); se B/C/D, a seção "Design (Impeccable)" está preenchida. +- Se existir design em `docs/design-refs/latest/`, inclua a subseção de referência visual (UX Pilot) com o aviso padrão de paleta (a estrutura vale, a paleta não — usar tokens do tema `data-theme='campaign'`). -## Passo 6 — Editar o roadmap (todos os pontos de consistência) +## Passo 7 — Editar o roadmap (todos os pontos de consistência) Um item novo de trilha toca **até 7 lugares** no roadmap. Pule apenas os que não se aplicam: @@ -99,12 +141,13 @@ Um item novo de trilha toca **até 7 lugares** no roadmap. Pule apenas os que n Para itens fora das trilhas (site público, admin, fill-in, bloqueador, fora de escopo): edite só a lista/tabela da seção correspondente + `Atualizado em`, mantendo o padrão da seção — bullets com _(fonte)_ em itálico no final, ou linha de tabela com Status e Fonte. -## Passo 7 — Verificação final +## Passo 8 — Verificação final - Todos os links relativos resolvem (`plans/.md` a partir de `docs/`, `../roadmap.md` a partir de `docs/plans/`). - O mermaid continua válido (nomes de nó únicos, subgraphs fechadas — renderize mentalmente ou cole num validador se a edição foi grande). - Consistência tripla: o que a **tabela de janela** diz em "Depende de" = setas do **grafo** = seção "Dependências" do **plano**. Divergência aqui é o bug mais comum. - O plano referencia o roadmap (`Item do roadmap:` no cabeçalho) e o roadmap referencia o plano (link na tabela). - Não inventou ID duplicado nem quebrou a numeração de "Ordem". +- Classe Impeccable do Passo 4 está no plano; se B/C/D, há âncoras (`PRODUCT.md`/`DESIGN.md` e/ou design-ref) citadas — não só "fazer uma tela bonita". -Ao final, resuma para o usuário: ID atribuído, janela, dependências assumidas, e as decisões de posicionamento que merecem validação de produto. +Ao final, resuma para o usuário: ID atribuído, janela, dependências assumidas, **classe Impeccable (A–D)**, e as decisões de posicionamento que merecem validação de produto. diff --git a/.cursor/skills/roadmap-item/plan-template.md b/.cursor/skills/roadmap-item/plan-template.md index c214b6ba..2bf67e3c 100644 --- a/.cursor/skills/roadmap-item/plan-template.md +++ b/.cursor/skills/roadmap-item/plan-template.md @@ -1,6 +1,6 @@ # Template de plano (`docs/plans/.md`) -Estrutura extraída dos planos existentes (`overview-lista-nucleos.md`, `visitados-recentemente.md`, etc.). Manter as seções nesta ordem; omitir "Referência visual" apenas quando não houver design. +Estrutura extraída dos planos existentes (`overview-lista-nucleos.md`, `visitados-recentemente.md`, etc.). Manter as seções nesta ordem; omitir "Design (Impeccable)" apenas na classe **A** (só backend); omitir a subseção "Referência visual (UX Pilot)" quando não houver design-ref. ````markdown # @@ -8,9 +8,25 @@ Estrutura extraída dos planos existentes (`overview-lista-nucleos.md`, `visitad Status: rascunho Atualizado em: Item do roadmap: [docs/roadmap.md](../roadmap.md) () +Impeccable: — Responsável: — -## Referência visual (UX Pilot) +## Design (Impeccable) + + + +Âncoras: `PRODUCT.md` / `DESIGN.md` (register ) · tema `data-theme='campaign'` (ou tokens do site público). + +Na implementação (`implement-roadmap-item`): . + +Brief compacto (obrigatório em C; se B ambíguo): + +- **Persona / contexto:** +- **Job principal:** +- **Estratégia de cor:** Restrained (default) | +- **Anti-goals:** + +### Referência visual (UX Pilot) @@ -24,6 +40,8 @@ Como usar: - **Fora deste plano:** . - **Ajustar cores e código:** o HTML/PNG usa a paleta antiga (vermelho escuro `#8E0E23`, navy `#1B2B4B`, dourado `#C8874B`) e Tailwind via CDN. Implementar com os componentes shadcn existentes (`src/components/ui`) e os tokens do tema `data-theme='campaign'` (`src/app/(frontend)/styles.css`): fundo branco, primário `#C51414`, superfícies neutras claras. + + ## Contexto ) - - AGENTS.md — +- `PRODUCT.md` / `DESIGN.md` — ```` Notas: - No bloco mermaid acima, remover as barras invertidas dos fences internos (`\``` → ` ``` `). - Nível de detalhe alvo: ~100–130 linhas, como os planos existentes. Menos que isso costuma significar que o Passo 3 (exploração do código) foi pulado. +- Classe **A**: omita a seção "Design (Impeccable)" e use no cabeçalho `Impeccable: A — N/A (sem superfície UI)`. +- Não rode `/impeccable craft|critique|polish` ao criar o plano — só classifique e semeie; a skill `implement-roadmap-item` executa o ciclo. diff --git a/.cursor/skills/suggest-next-roadmap-items/SKILL.md b/.cursor/skills/suggest-next-roadmap-items/SKILL.md new file mode 100644 index 00000000..09da8d41 --- /dev/null +++ b/.cursor/skills/suggest-next-roadmap-items/SKILL.md @@ -0,0 +1,152 @@ +--- +name: suggest-next-roadmap-items +description: >- + Lê docs/roadmap.md (e planos/notebook/calendário necessários), filtra o que + já foi entregue e o que ainda está bloqueado por dependência, e sugere 3–5 + itens candidatos a serem o próximo a implementar no Teqo. Usar quando o + usuário perguntar "o que fazer agora", "próximo item do roadmap", "o que + priorizar", "sugere o próximo", "what's next", "qual item implementar", ou + pedir recomendações de priorização a partir do roadmap. +--- + +# Sugerir próximos itens do roadmap + +Esta skill **só recomenda** — não implementa, não edita o roadmap e não cria planos. O deliverable é uma shortlist ranqueada com racional, para o usuário escolher. Depois disso, o fluxo natural é `implement-roadmap-item` no ID escolhido (ou `roadmap-item` se a sugestão revelar um gap de registro). + +**Divisão com as skills irmãs:** + +| Skill | Papel | +| ---------------------------- | ------------------------------------------------------ | +| `compile-roadmap` | Enxugar roadmap: resumir feito, destacar abertos | +| `suggest-next-roadmap-items` | **Escolher candidatos** entre o que o roadmap já lista | +| `roadmap-item` | Registrar ideia nova / criar plano | +| `implement-roadmap-item` | Auditar plano + implementar o ID escolhido | +| `capture-review-debts` | Triagear débitos de `/simplify` / critique | + +## Checklist + +``` +- [ ] 1. Ler docs/roadmap.md inteiro + notebook do projeto +- [ ] 2. Extrair pool de itens ainda abertos (sem ✓ / sem "entregue") +- [ ] 3. Verificar dependências duras e bloqueios externos +- [ ] 4. Pontuar candidatos (calendário × valor × prontidão × risco) +- [ ] 5. Ler planos só dos top candidatos (progressive disclosure) +- [ ] 6. Entregar shortlist 3–5 + próximo passo; parar +``` + +## Passo 1 — Ler as fontes canônicas + +Leia **nesta ordem** (não pule o roadmap): + +1. **`docs/roadmap.md` inteiro** — âncoras do calendário, Onda 0, grafo mermaid, tabelas por janela, Fill-ins, Cortes seguros / não cortáveis, Bloqueadores, Fora de escopo. +2. **`.cursor/rules/projects/nucleos-eleitorais.mdc`** (ou notebook do projeto ativo) — status operacional posterior à linha "Atualizado em" do roadmap. +3. **Data de hoje** (contexto da sessão) vs âncoras do calendário eleitoral — define a janela vigente (1–4). +4. **Contexto da conversa** — se o usuário já pediu foco (ex.: "só mapa", "só DRY", "nada de fill-in"), aplique como filtro explícito. + +**Não** leia todos os `docs/plans/*.md` de uma vez. Planos entram só no Passo 5, para os candidatos que sobrarem no top. + +## Passo 2 — Montar o pool de abertos + +Um item está **aberto** se **não** estiver marcado entregue. Sinais de entregue (qualquer um basta): + +- `✓` no nó do grafo mermaid +- "(entregue YYYY-MM-DD)" / "implementado e mesclado" / "engenharia pronta e mesclada" na tabela ou seção Ciclo +- Fill-in com ✓ explícito + +Exclua do pool: + +- Seção **Fora de escopo** e **Itens consolidados/removidos** +- Itens cuja engenharia já está em `main` e só falta ativação externa (ex.: C2 produção = lote jurídico) — marque-os como **bloqueio operacional**, não como candidato de implementação de código +- Itens em hold explícito (ex.: E5 "futuro"; C5 "proposto — validar com produto" só entra se o usuário pedir features de produto) + +Inclua Fill-ins e débitos `escala-dry-pos-*` / `*+` — eles competem na shortlist, mas com peso menor que features de caminho crítico (Passo 4). + +## Passo 3 — Filtrar por prontidão + +Para cada aberto, classifique: + +| Estado | Critério | Ação na shortlist | +| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------ | +| **Pronto** | Todas as dependências **duras** (setas `-->` / coluna "Depende de") entregues | Candidato normal | +| **Quase** | Só falta dependência **suave** (`-.->`) | Candidato; note o que degrada se seguir sem a suave | +| **Bloqueado duro** | Dependência dura aberta | Fora da shortlist; cite o predecessor a fazer antes | +| **Bloqueado externo** | Jurídico (Onda 0), TSE após 15/08, validação de produto | Fora da shortlist de código; mencione em "Bloqueios" | +| **Cortável agora** | Está em "Cortes seguros" **e** o prazo da janela aperta | Só sugerir se for quick win barato ou se o usuário pedir DRY | + +Confirme no notebook/código só quando o roadmap e o notebook **divergirem** (ex.: roadmap sem ✓ mas notebook diz entregue). Não faça auditoria profunda de código aqui — isso é `implement-roadmap-item`. + +## Passo 4 — Pontuar e ranquear + +Atribua a cada **Pronto** / **Quase** uma nota 1–5 (soma ponderada mental; não precisa ser fórmula rígida). Âncoras Teqo: + +| Peso | Critério | Preferir | +| ----- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Alto | **Calendário** | Serve a janela vigente (hoje → próxima âncora). Janela 1–2: produção, base nominal, agenda, fundação de dados. Janela 3: inteligência, mapa, engajamento. Janela 4: só o que falta p/ dia D + estabilização; nada de migration arriscada perto de ~20/09. | +| Alto | **Caminho crítico** | Onda 0 / não cortáveis > operação de campo > inteligência > plataforma/engajamento > fill-in DRY | +| Médio | **Paralelismo / desbloqueio** | Item que desbloqueia vários outros (ex.: A7 antes de A5/B3/E\*) ou fecha um lote (E4 após E1–E3) | +| Médio | **Prontidão do plano** | Tem `docs/plans/.md` acionável > só design-ref > sem plano (C5) | +| Médio | **Custo vs valor** | Quick win de campo / insight barato pode furar fila; cadeia `escala-dry-pos-*` só se a superfície já dói (volume, lista lenta) ou se for Fase 1 de alto ROI citada em "Cortes seguros" | +| Baixo | **Risco** | Migration + Consent novo = mais caro; item só utility/seed = mais barato | + +**Regras de desempate (travadas):** + +1. Não sugerir como #1 um fill-in DRY se houver feature **Pronta** da janela vigente que desbloqueie operação (ex.: E4, A5 fatia, B3, A8, C4). +2. Preferir **uma fatia** de A5 (um insight) a "fazer A5 inteiro" — diga qual sub-plano. +3. Se várias cadeias DRY competem (C10, C11, E6, E7, A7, B5, O0+, …), escolha no máximo **1–2** na shortlist, priorizando as Fases que o próprio roadmap marca como "mais valiosas" em Cortes seguros. +4. Itens _(proposto — validar com produto)_ (ex.: C5) só entram se o usuário pedir exploração de produto; senão liste em "Depois / validar". +5. Trabalho externo (lote jurídico) **nunca** substitui um candidato de engenharia — reporte em seção própria. + +## Passo 5 — Aprofundar só o top + +Pegue os **3–5** melhores do Passo 4. Para cada um: + +1. Abra o plano linkado (`docs/plans/...`) — leia cabeçalho, Status, Dependências, Não escopo, e o tamanho das fases (1 parágrafo basta). +2. Se houver design-ref na tabela do roadmap, cite o nome do par em `docs/design-refs/latest/` (não precisa abrir o HTML). +3. Classifique superficialmente Impeccable A/B/C/D (mesma tabela de `implement-roadmap-item`) — uma letra + meia frase. + +Se dois candidatos forem quase equivalentes, prefira o de menor risco de migration / o que o calendário pune mais se atrasar. + +## Passo 6 — Entregar e parar + +Formato obrigatório da resposta (conciso; sem reescrever o roadmap): + +```markdown +## Contexto + +- Janela vigente: N (âncora → data) +- Bloqueios externos: … +- Foco do usuário (se houver): … + +## Recomendação principal + +**ID — Nome** — uma frase do porquê agora. + +- Plano: `docs/plans/…` +- Dependências: ok / suave pendente: … +- Impeccable: A|B|C|D +- Próximo passo: skill `implement-roadmap-item` neste ID + +## Alternativas (2–4) + +| # | ID | Por que agora | Por que não #1 | Plano | +| --- | --- | ------------- | -------------- | ----- | +| 2 | … | … | … | … | + +## Deixados de fora (amostra) + +- **Bloqueados:** ID — falta X +- **Cortáveis / depois:** ID — racional curto +- **Validar com produto:** … + +Quer que eu rode `implement-roadmap-item` em algum destes? +``` + +**Proibido neste fluxo:** + +- Implementar código, abrir PR, ou marcar item como entregue +- Editar `docs/roadmap.md` / `docs/plans/` (se achar inconsistência material, **reporte** e ofereça `roadmap-item` / correção via `implement-roadmap-item` Passo 7) +- Inventar IDs ou itens que não estão no roadmap +- Shortlist > 5 candidatos "principais" (alternativas contam no total 3–5) +- Auditoria afirmação-a-afirmação do plano (isso é `implement-roadmap-item`) + +Se o usuário já disser "faz o X" / "implementa o segundo", pare esta skill e invoque `implement-roadmap-item` no ID escolhido. diff --git a/.impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md new file mode 100644 index 00000000..93022d5c --- /dev/null +++ b/.impeccable/critique/2026-07-19T15-10-58Z__src-app-campaign-campanha-login.md @@ -0,0 +1,101 @@ +--- +target: /campanha/login +total_score: 28 +p0_count: 0 +p1_count: 2 +timestamp: 2026-07-19T15-10-58Z +slug: src-app-campaign-campanha-login +--- +Method: dual-agent (A: cd378fde-c091-463a-8e89-ca6cecac8904 · B: 44f7668c-1143-41ec-a147-a2dc2f4d7f9c) + +# Critique: /campanha/login + +## Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 3 | Pending state is clear (Spinner + “Entrando…”); auth failure is a single generic FieldError not tied to fields | +| 2 | Match System / Real World | 3 | “E-mail ou celular” matches dual auth, but leadership phone path vs email-only reset is invisible on this screen | +| 3 | User Control and Freedom | 3 | Forgot-password link exists; no show-password, no first-access/invite escape | +| 4 | Consistency and Standards | 3 | Auth shell duplicated across login/esqueci/redefinir; Entrar lacks `w-full` unlike sibling forms | +| 5 | Error Prevention | 2 | `inputMode="email"` on a dual identifier; validation authoritative only on server with generic messages | +| 6 | Recognition Rather Than Recall | 4 | Standard login IA; placeholder shows both formats | +| 7 | Flexibility and Efficiency | 2 | Single rigid credential path; no returning-user / PWA shortcut | +| 8 | Aesthetic and Minimalist Design | 4 | Restrained Field Desk card on muted; Signal Red budget respected | +| 9 | Error Recovery | 3 | Generic security error is correct but offers no next-step branch for phone-only / invite users | +| 10 | Help and Documentation | 1 | Zero first-timer guidance on login; forgot-password already explains phone→coordenador invite | +| **Total** | | **28/40** | **Good** | + +## Anti-Patterns Verdict + +**LLM assessment**: Not AI slop. Centered card on `bg-muted`, Megaphone-in-primary square, Inter/stone tokens — reads as restrained shadcn Field Desk, not SaaS purple or campaign brochure. Absolute bans (side-stripe, gradient text, glassmorphism, hero-metric, eyebrow scaffolding) absent. Weakness is emotional flatness and leadership-path ambiguity, not template aesthetics. + +**Deterministic scan**: `detect.mjs --json` on `src/app/(campaign)/campanha/login` → exit 0, **0 findings** (`[]`). Per-file scans of `page.tsx` and `LoginForm.tsx` also clean. Detector and LLM agree: no automated slop families. + +**Visual overlays**: No reliable user-visible overlay. Browser MCP (`plugin-browse-browser`) failed on navigate/snapshot; live-server injection skipped. Dev server was serving `http://localhost:3000/campanha/login` (200). Layout confirmed from source + served HTML: muted full-viewport center, brand row, card with identifier/password, “Esqueceu a senha?”, primary “Entrar”. + +## Overall Impression + +A clean, trustworthy ops gate that earns familiarity — then stalls for the people who need it most under pressure. Biggest opportunity: make the leadership (phone / invite) path as discoverable as the staff (email) path without cluttering the desk. + +## What's Working + +1. **Touch targets**: Inputs and submit use `min-h-11` (44px) — meets field/leadership phone bar. +2. **Signal Red discipline**: Red only on mark, primary CTA, focus ring, and forgot link — not washed across the canvas. +3. **Pending feedback**: `disabled={pending}`, Spinner, `aria-live="polite"` label swap to “Entrando…”. + +## Priority Issues + +### [P1] Missing document heading landmark +- **What**: `CardTitle` is a `
`; page has no `

`. Brand row is plain `font-semibold` text. +- **Why it matters**: Screen-reader users get no clear “where am I” on a critical entry screen; weak WCAG structure. +- **Fix**: Promote “Acessar painel” (or a sr-only “Entrar — Campanha”) to a real `

`. +- **Suggested command**: `/impeccable harden /campanha/login` + +### [P1] Phone-first leadership mismatch +- **What**: Identifier uses `inputMode="email"` while leadership auth is phone-as-username; “Esqueceu a senha?” leads to email-only reset that already tells phone users to ask a coordinator — but login itself is silent. +- **Why it matters**: Field leadership gets the wrong keyboard, fails auth, then hits an email form that abandons them. +- **Fix**: Prefer `inputMode="tel"` or pattern-aware mode; add one line of login copy mirroring `ForgotPasswordForm` (“Liderança: celular cadastrado. Sem senha? Peça convite ao coordenador.”). +- **Suggested command**: `/impeccable clarify /campanha/login` + +### [P2] Monolithic auth error with no recovery branches +- **What**: `state.error` (“E-mail, celular ou senha inválidos.” / “Dados inválidos.”) sits in one `FieldError` above submit, not linked via `aria-describedby` / `aria-invalid`. +- **Why it matters**: Under pressure users retype everything; no path to invite vs reset. +- **Fix**: Keep message generic for security; add `aria-invalid` + recovery links (esqueci-senha / first-access invite guidance). +- **Suggested command**: `/impeccable harden /campanha/login` + +### [P2] No first-timer guidance on the login surface +- **What**: “Campanha” + Megaphone + “Acessar painel” assumes prior context; invite flow undiscoverable. +- **Why it matters**: Jordan abandons; leadership thinks the product is broken when they need a WhatsApp invite. +- **Fix**: One sentence under CardDescription: who this is for + what to do on first access. +- **Suggested command**: `/impeccable onboard /campanha/login` or `/impeccable clarify /campanha/login` + +### [P3] Submit width inconsistent with sibling auth forms +- **What**: Login `Button` is `min-h-11` without `w-full`; forgot/reset use `min-h-11 w-full`. +- **Why it matters**: Thumb-target inconsistency on phone; visual drift across auth chain. +- **Fix**: Add `w-full`; extract shared `CampaignAuthPageShell` (already on RS+ backlog). +- **Suggested command**: `/impeccable polish /campanha/login` + +## Persona Red Flags + +**Jordan (First-Timer)**: No product/first-access explanation; dual “E-mail ou celular” forces a format guess; after error, no “try next” guidance; invite path invisible. + +**Sam (Accessibility)**: No `

`; CardTitle/Description are divs; error alert not associated with inputs; pending state is announced but field-level invalid state is not. + +**Casey (Distracted Mobile)**: Touch targets pass and CTA is low on the card (good); `inputMode="email"` hurts phone typing; no show-password for one-handed re-entry. + +**Field leadership on phone (project)**: Phone-as-username is core product behavior, but copy leads with e-mail, reset abandons them, and tone (“painel”) reads admin portal not peer field tool. + +## Minor Observations + +- Auth shell triplicated (`login` / `esqueci-senha` / `redefinir-senha`); invalid-token reset can drop brand header — shell would prevent drift. +- Placeholder `voce@exemplo.com ou (71) 99999-1234` helps staff more than leadership. +- `robots: noindex` and campaign theme tokens correctly applied. +- Cognitive load: **2 failures (moderate)** — working memory (email vs phone mental model) and progressive disclosure (invite path hidden until forgot-password). + +## Questions to Consider + +1. Split “Sou equipe” / “Sou liderança” vs keep one identifier field? +2. Is “Acessar painel” the right frame, or “Entrar na campanha” / field-desk language? +3. Why educate phone users only on forgot-password — intentional funnel or debt? +4. Would one trust line (data used only for campaign org) reduce abandonment without kitsch? diff --git a/.impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md b/.impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md new file mode 100644 index 00000000..c63cf729 --- /dev/null +++ b/.impeccable/critique/2026-07-19T15-55-17Z__src-app-campaign-campanha-login.md @@ -0,0 +1,99 @@ +--- +target: /campanha/login +total_score: 32 +p0_count: 0 +p1_count: 0 +p2_count: 2 +timestamp: 2026-07-19T15-55-17Z +slug: src-app-campaign-campanha-login +--- +Method: dual-agent (A: 103923c2-0d00-49d5-9902-eee91515fbc5 · B: c71afcf4-ae05-432e-900f-a93610cb29a4) + +# Critique: /campanha/login (post-polish) + +## Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 4 | Spinner + aria-live; FieldError alert + aria-invalid/describedby on both fields | +| 2 | Match System / Real World | 3 | Dual-path copy clear; description still leads with email before phone; forgot remains email-only | +| 3 | User Control and Freedom | 3 | Forgot link exists; no show-password; no staff/liderança switch | +| 4 | Consistency and Standards | 4 | CampaignAuthPageShell + shared heading class; w-full matches siblings | +| 5 | Error Prevention | 3 | Dynamic inputMode after typing; empty field still inputMode=text | +| 6 | Recognition Rather Than Recall | 4 | Phone-first placeholder; standard login IA | +| 7 | Flexibility and Efficiency | 2 | Single rigid credential path | +| 8 | Aesthetic and Minimalist Design | 4 | Restrained Field Desk; Signal Red budget OK; header slightly dense | +| 9 | Error Recovery | 3 | Generic error + leadership recovery hint; hint not in aria-describedby | +| 10 | Help and Documentation | 3 | First-access WhatsApp tip onboarded; phone recovery still partly error-gated | +| **Total** | | **32/40** | **Good** (+4 vs prior 28) | + +## Anti-Patterns Verdict + +**LLM:** Not AI slop. Field Desk card on muted; Megaphone mark; Mandate Red as signal only. Residual dual-path ambiguity and emotional flatness, not template aesthetics. + +**Deterministic scan:** detect.mjs exit 0, **0 findings** on login dir + shell + campaignAuthCopy. + +**Visual overlays:** No reliable overlay. Browser MCP failed; live-server/inject infra worked (port 8400) but detect.js never ran. Curl confirmed 200 + structure (h1, min-h-11, w-full, a11y attrs). + +## Overall Impression + +Prior P1/P2 gaps closed: real h1, mobilizing title, phone-first placeholder, first-access tip, field-linked errors, recovery hint, shared shell. Biggest remaining opportunity: make the empty-field mobile keyboard and forgot-password funnel as phone-first as the rest of the screen. + +## What's Working + +1. Real `

` + FieldError association (Sam P1 closed). +2. First-access WhatsApp invite line without branching UI. +3. Auth-chain consistency via CampaignAuthPageShell + full-width touch targets. + +## Priority Issues + +### [P2] Empty-field keyboard not phone-first +- **What:** identifierInputMode('') → 'text'; tel only after ≥2 digits. +- **Why:** Leadership first tap still gets QWERTY. +- **Fix:** Default inputMode=tel when empty; switch to email on @. +- **Suggested command:** `/impeccable adapt /campanha/login` + +### [P2] No show-password toggle +- **What:** password type only. +- **Why:** One-handed field re-entry under glare. +- **Fix:** Reveal toggle with aria-pressed, keep min-h-11. +- **Suggested command:** `/impeccable harden /campanha/login` + +### [P3] Recovery hint outside aria-describedby +- **What:** Only login-credentials-error is described; CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT is a sibling p. +- **Why:** SR may miss phone-only guidance. +- **Fix:** Append hint id to aria-describedby. +- **Suggested command:** `/impeccable harden /campanha/login` + +### [P3] “Esqueceu a senha?” still email-first funnel +- **What:** Link unlabeled as email-only; phone users hit forgot then discover coordinator path. +- **Why:** Reflex after one failed attempt. +- **Fix:** Sub-label or always-visible CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT near link. +- **Suggested command:** `/impeccable clarify /campanha/login` + +### [P3] Emotional register still flat vs “inspiring” +- **What:** Clear ops gate; no trust/solidarity line. +- **Why:** Field Desk asks for socialist/clear/inspiring. +- **Fix:** One grounded trust line without kitsch. +- **Suggested command:** `/impeccable delight /campanha/login` or `/impeccable clarify` + +## Persona Red Flags + +**Jordan:** Improved; still assumes “núcleo”/“coordenador” literacy. +**Sam:** h1 fixed; recovery hint not in describedby. +**Casey:** Targets pass; empty inputMode=text; no show-password. +**Field leadership:** Best-improved; still at risk from unqualified forgot link + email-first description ordering. + +## Minor Observations + +- Three header text blocks — purposeful but dense on narrow screens. +- Brand row not in heading hierarchy (OK with card h1). +- Cognitive load still 2 failures (moderate): dual credential model; phone recovery partly progressive. + +## Questions to Consider + +1. Default empty identifier to tel? +2. Always-visible leadership recovery near forgot link? +3. Split Sou equipe / Sou liderança still worth it? +4. One inspiring trust line on a login gate — yes or overreach? +5. Relabel “Esqueceu a senha?” for email-only accounts? diff --git a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx index f6fb88b8..f33b31e0 100644 --- a/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx +++ b/src/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm.tsx @@ -5,10 +5,14 @@ import { useActionState } from 'react' import { requestCampaignPasswordResetFormAction } from '@/app/(campaign)/campanha/actions/password' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' +import { + CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT, + campaignAuthHeadingClassName, +} from '@/lib/campaignAuthCopy' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' import { fieldError } from '@/utilities/campaignFormFields' @@ -21,10 +25,9 @@ export const ForgotPasswordForm = () => { return ( - Esqueceu a senha? +

Esqueceu a senha?

- Informe o e-mail cadastrado na sua conta. Se você acessa só com celular, peça um novo - convite ao coordenador. + Informe o e-mail cadastrado na sua conta. {CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT}
@@ -56,7 +59,10 @@ export const ForgotPasswordForm = () => { {pending ? 'Enviando...' : 'Enviar link'}

- + Voltar ao login

diff --git a/src/app/(campaign)/campanha/esqueci-senha/page.tsx b/src/app/(campaign)/campanha/esqueci-senha/page.tsx index 73e600e0..80148927 100644 --- a/src/app/(campaign)/campanha/esqueci-senha/page.tsx +++ b/src/app/(campaign)/campanha/esqueci-senha/page.tsx @@ -1,8 +1,8 @@ -import { Megaphone } from 'lucide-react' import type { Metadata } from 'next' import { redirect } from 'next/navigation' import { ForgotPasswordForm } from '@/app/(campaign)/campanha/esqueci-senha/ForgotPasswordForm' +import { CampaignAuthPageShell } from '@/components/campaign/CampaignAuthPageShell' import { getCampaignUser } from '@/utilities/campaignAuth' export const metadata: Metadata = { @@ -21,16 +21,8 @@ export default async function CampaignForgotPasswordPage() { } return ( -
-
-
-
- -
- Campanha -
- -
-
+ + + ) } diff --git a/src/app/(campaign)/campanha/login/LoginForm.tsx b/src/app/(campaign)/campanha/login/LoginForm.tsx index 00bb06d9..8b1a689e 100644 --- a/src/app/(campaign)/campanha/login/LoginForm.tsx +++ b/src/app/(campaign)/campanha/login/LoginForm.tsx @@ -1,42 +1,69 @@ 'use client' import Link from 'next/link' -import { useActionState } from 'react' +import { useActionState, useState } from 'react' -import { loginCampaignFormAction } from '@/app/(campaign)/campanha/actions/auth' +import { loginCampaignFormAction, type LoginResult } from '@/app/(campaign)/campanha/actions/auth' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' +import { + CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT, + campaignAuthHeadingClassName, +} from '@/lib/campaignAuthCopy' + +const LOGIN_ERROR_ID = 'login-credentials-error' + +const identifierInputMode = (value: string): 'tel' | 'email' | 'text' => { + const trimmed = value.trim() + if (!trimmed) return 'text' + if (trimmed.includes('@')) return 'email' + const digits = trimmed.replace(/\D/g, '') + if (digits.length >= 2 && !/[a-zA-Z]/.test(trimmed)) return 'tel' + return 'text' +} export const LoginForm = () => { - const [state, formAction, pending] = useActionState(loginCampaignFormAction, {}) + const [state, formAction, pending] = useActionState( + loginCampaignFormAction, + {} satisfies LoginResult, + ) + const [identifier, setIdentifier] = useState('') + const hasAuthError = Boolean(state.error) return ( - Acessar painel - Entre com seu e-mail ou celular para continuar. +

Entrar na campanha

+ Use o e-mail da equipe ou o celular da liderança. +

+ Primeiro acesso? Peça um convite no WhatsApp ao coordenador do seu núcleo. +

- + E-mail ou celular setIdentifier(event.target.value)} + aria-invalid={hasAuthError || undefined} + aria-describedby={hasAuthError ? LOGIN_ERROR_ID : undefined} /> - + Senha { enterKeyHint="done" required className="min-h-11" + aria-invalid={hasAuthError || undefined} + aria-describedby={hasAuthError ? LOGIN_ERROR_ID : undefined} /> -

+

Esqueceu a senha? -

+ {hasAuthError ? ( +

+ {CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT} +

+ ) : null} +
- {state.error ? {state.error} : null} - diff --git a/src/app/(campaign)/campanha/login/page.tsx b/src/app/(campaign)/campanha/login/page.tsx index ab14f765..913a25c7 100644 --- a/src/app/(campaign)/campanha/login/page.tsx +++ b/src/app/(campaign)/campanha/login/page.tsx @@ -1,7 +1,7 @@ -import { Megaphone } from 'lucide-react' import type { Metadata } from 'next' import { redirect } from 'next/navigation' +import { CampaignAuthPageShell } from '@/components/campaign/CampaignAuthPageShell' import { getCampaignUser } from '@/utilities/campaignAuth' import { LoginForm } from './LoginForm' @@ -21,16 +21,8 @@ export default async function CampaignLoginPage() { } return ( -
-
-
-
- -
- Campanha -
- -
-
+ + + ) } diff --git a/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx b/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx index 75a8441b..a75213fc 100644 --- a/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx +++ b/src/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm.tsx @@ -5,10 +5,11 @@ import { useActionState } from 'react' import { resetCampaignPasswordFormAction } from '@/app/(campaign)/campanha/actions/password' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Spinner } from '@/components/ui/Spinner' +import { campaignAuthHeadingClassName } from '@/lib/campaignAuthCopy' import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' import { fieldError } from '@/utilities/campaignFormFields' @@ -21,7 +22,7 @@ export const ResetPasswordForm = ({ token }: { token: string }) => { return ( - Redefinir senha +

Redefinir senha

Escolha uma nova senha com pelo menos 8 caracteres.
@@ -63,7 +64,10 @@ export const ResetPasswordForm = ({ token }: { token: string }) => { {pending ? 'Salvando...' : 'Salvar nova senha'}

- + Voltar ao login

diff --git a/src/app/(campaign)/campanha/redefinir-senha/page.tsx b/src/app/(campaign)/campanha/redefinir-senha/page.tsx index 73948989..6efb1004 100644 --- a/src/app/(campaign)/campanha/redefinir-senha/page.tsx +++ b/src/app/(campaign)/campanha/redefinir-senha/page.tsx @@ -1,12 +1,13 @@ -import { Megaphone } from 'lucide-react' import type { Metadata } from 'next' import Link from 'next/link' import { redirect } from 'next/navigation' -import { CAMPAIGN_PASSWORD_RESET_TOKEN_MIN_LENGTH } from '@/lib/schemas/campaignPassword' import { ResetPasswordForm } from '@/app/(campaign)/campanha/redefinir-senha/ResetPasswordForm' +import { CampaignAuthPageShell } from '@/components/campaign/CampaignAuthPageShell' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card' +import { campaignAuthHeadingClassName } from '@/lib/campaignAuthCopy' +import { CAMPAIGN_PASSWORD_RESET_TOKEN_MIN_LENGTH } from '@/lib/schemas/campaignPassword' import { getCampaignUser } from '@/utilities/campaignAuth' import { CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE } from '@/utilities/campaignPasswordReset' @@ -34,38 +35,28 @@ export default async function CampaignResetPasswordPage({ if (!token || token.length < CAMPAIGN_PASSWORD_RESET_TOKEN_MIN_LENGTH) { return ( -
-
- - - Link inválido - {CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE} - - - - - - -
-
+ + + +

Link inválido

+ {CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE} +
+ + + + +
+
) } return ( -
-
-
-
- -
- Campanha -
- -
-
+ + + ) } diff --git a/src/components/campaign/CampaignAuthPageShell.tsx b/src/components/campaign/CampaignAuthPageShell.tsx new file mode 100644 index 00000000..fb400e78 --- /dev/null +++ b/src/components/campaign/CampaignAuthPageShell.tsx @@ -0,0 +1,33 @@ +import { Megaphone } from 'lucide-react' +import type { ComponentProps, ReactNode } from 'react' + +import { cn } from '@/lib/utils' + +type CampaignAuthPageShellProps = { + children: ReactNode + className?: string +} & Omit, 'children' | 'className'> + +export const CampaignAuthPageShell = ({ + children, + className, + ...props +}: CampaignAuthPageShellProps) => ( +
+
+
+
+
+ Campanha +
+ {children} +
+
+) diff --git a/src/lib/campaignAuthCopy.ts b/src/lib/campaignAuthCopy.ts new file mode 100644 index 00000000..0013721b --- /dev/null +++ b/src/lib/campaignAuthCopy.ts @@ -0,0 +1,10 @@ +/** Client-safe copy and shared auth UI class tokens (not `server-only`). */ + +export const CAMPAIGN_LEADERSHIP_PHONE_ACCESS_HINT = + 'Se você acessa só com celular, peça um novo convite ao coordenador.' + +export const CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT = + 'Conta só com celular? Peça um novo convite ao coordenador — a recuperação por e-mail não funciona sem e-mail cadastrado.' + +/** Card titles on public campaign auth screens (login / forgot / reset). */ +export const campaignAuthHeadingClassName = 'text-balance text-xl leading-snug font-medium' diff --git a/tests/unit/campaignCompositionCleanup.unit.spec.ts b/tests/unit/campaignCompositionCleanup.unit.spec.ts index aedfdbd7..02032dcc 100644 --- a/tests/unit/campaignCompositionCleanup.unit.spec.ts +++ b/tests/unit/campaignCompositionCleanup.unit.spec.ts @@ -1,9 +1,9 @@ +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { existsSync, readFileSync, readdirSync } from 'node:fs' import { dirname, extname, resolve } from 'node:path' import { act, createElement } from 'react' import { hydrateRoot } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ @@ -97,14 +97,55 @@ describe('campaign login composition', () => { const password = screen.getByLabelText('Senha') const submit = screen.getByRole('button', { name: 'Entrar' }) + expect(screen.getByRole('heading', { level: 1, name: 'Entrar na campanha' })).toBeTruthy() expect(identifier.getAttribute('name')).toBe('identifier') expect(identifier.hasAttribute('required')).toBe(true) expect(identifier.getAttribute('autocomplete')).toBe('username') - expect(identifier.getAttribute('inputmode')).toBe('email') + expect(identifier.getAttribute('inputmode')).toBe('text') expect(password.getAttribute('name')).toBe('password') expect(password.hasAttribute('required')).toBe(true) expect(password.getAttribute('autocomplete')).toBe('current-password') expect(submit.getAttribute('type')).toBe('submit') + expect(submit.className).toContain('w-full') + }) + + it('switches identifier inputMode toward phone or email as the user types', () => { + render(createElement(LoginForm)) + const identifier = screen.getByLabelText('E-mail ou celular') + + fireEvent.change(identifier, { target: { value: '(71) 9' } }) + expect(identifier.getAttribute('inputmode')).toBe('tel') + + fireEvent.change(identifier, { target: { value: 'staff@' } }) + expect(identifier.getAttribute('inputmode')).toBe('email') + }) + + it('links auth errors to both credential fields and shows leadership recovery copy', async () => { + mocks.loginAction.mockResolvedValue({ error: 'E-mail, celular ou senha inválidos.' }) + render(createElement(LoginForm)) + + fireEvent.change(screen.getByLabelText('E-mail ou celular'), { + target: { value: '71' }, + }) + fireEvent.change(screen.getByLabelText('Senha'), { + target: { value: 'wrong' }, + }) + fireEvent.submit(screen.getByRole('button', { name: 'Entrar' }).closest('form')!) + + const alert = await screen.findByRole('alert') + expect(alert.id).toBe('login-credentials-error') + expect(alert.textContent).toContain('E-mail, celular ou senha inválidos.') + expect(screen.getByLabelText('E-mail ou celular').getAttribute('aria-invalid')).toBe('true') + expect(screen.getByLabelText('Senha').getAttribute('aria-invalid')).toBe('true') + expect(screen.getByLabelText('E-mail ou celular').getAttribute('aria-describedby')).toBe( + 'login-credentials-error', + ) + expect(screen.getByLabelText('Senha').getAttribute('aria-describedby')).toBe( + 'login-credentials-error', + ) + expect( + screen.getByText(/Conta só com celular\? Peça um novo convite ao coordenador/), + ).toBeTruthy() }) it('shows the pending spinner and announces an action error', async () => { @@ -166,9 +207,9 @@ describe('campaign client module boundaries', () => { it('keeps the TSE input on the browser-safe parser boundary', () => { const source = readFileSync(resolve(sourceRoot, 'components/campaign/TseZoneInput.tsx'), 'utf8') - expect(source).toContain("@/utilities/tseZone") - expect(source).not.toContain("@/lib/formData") - expect(source).not.toContain("@/utilities/nucleusUi") + expect(source).toContain('@/utilities/tseZone') + expect(source).not.toContain('@/lib/formData') + expect(source).not.toContain('@/utilities/nucleusUi') }) }) @@ -215,4 +256,3 @@ describe('static table composition', () => { consoleError.mockRestore() }) }) -