mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
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.
This commit is contained in:
parent
7fb688ab95
commit
1b6de775a0
17 changed files with 1017 additions and 97 deletions
143
.cursor/skills/capture-review-debts/SKILL.md
Normal file
143
.cursor/skills/capture-review-debts/SKILL.md
Normal file
|
|
@ -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-<slug>.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-<pai>` ou `<surface>-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ê)
|
||||
257
.cursor/skills/compile-roadmap/SKILL.md
Normal file
257
.cursor/skills/compile-roadmap/SKILL.md
Normal file
|
|
@ -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/<slug>.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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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/<slug>.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/<slug>.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/<slug>.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/<slug>.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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Template de plano (`docs/plans/<slug>.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
|
||||
# <Título do item em pt-BR>
|
||||
|
|
@ -8,9 +8,25 @@ Estrutura extraída dos planos existentes (`overview-lista-nucleos.md`, `visitad
|
|||
Status: rascunho
|
||||
Atualizado em: <YYYY-MM-DD>
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (<seção e/ou ID, ex.: "Trilha C, item C6">)
|
||||
Impeccable: <A | B | C | D> — <uma linha: N/A sem UI | encaixe em tela X | UI nova em rota Y | ref Nome.png>
|
||||
Responsável: —
|
||||
|
||||
## Referência visual (UX Pilot)
|
||||
## Design (Impeccable)
|
||||
|
||||
<!-- Obrigatório se Impeccable = B, C ou D. Omitir inteiro se A. -->
|
||||
|
||||
Âncoras: `PRODUCT.md` / `DESIGN.md` (register <product|brand>) · tema `data-theme='campaign'` (ou tokens do site público).
|
||||
|
||||
Na implementação (`implement-roadmap-item`): <shape → craft → critique → polish | craft compacto → critique → polish | shape compacto (ref) → craft → …>.
|
||||
|
||||
Brief compacto (obrigatório em C; se B ambíguo):
|
||||
|
||||
- **Persona / contexto:** <quem, onde, estado de espírito>
|
||||
- **Job principal:** <uma frase>
|
||||
- **Estratégia de cor:** Restrained (default) | <exceção justificada>
|
||||
- **Anti-goals:** <o que esta superfície não deve ser>
|
||||
|
||||
### Referência visual (UX Pilot)
|
||||
|
||||
<!-- Só se existir design em docs/design-refs/latest/. -->
|
||||
|
||||
|
|
@ -24,6 +40,8 @@ Como usar:
|
|||
- **Fora deste plano:** <partes do design que pertencem a outros planos, com links>.
|
||||
- **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.
|
||||
|
||||
<!-- Se o item nasce de critique: cite `.impeccable/critique/<snapshot>.md` aqui; não rode critique novo neste fluxo. -->
|
||||
|
||||
## Contexto
|
||||
|
||||
<Estado atual do sistema (com caminhos de arquivo reais), o problema/oportunidade, e a decisão
|
||||
|
|
@ -76,9 +94,12 @@ Componentes:
|
|||
- `docs/roadmap.md` (<seção/linhas>)
|
||||
- <arquivos-fonte reais que o implementador vai abrir, um por bullet, com o porquê>
|
||||
- AGENTS.md — <quais convenções se aplicam: Campaign auth, naming, overrideAccess, etc.>
|
||||
- <!-- Se B/C/D: --> `PRODUCT.md` / `DESIGN.md` — <o que deste item herda do Field Desk / register>
|
||||
````
|
||||
|
||||
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.
|
||||
|
|
|
|||
152
.cursor/skills/suggest-next-roadmap-items/SKILL.md
Normal file
152
.cursor/skills/suggest-next-roadmap-items/SKILL.md
Normal file
|
|
@ -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/<slug>.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.
|
||||
|
|
@ -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 `<div>`; page has no `<h1>`. 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 `<h1>`.
|
||||
- **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 `<h1>`; 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?
|
||||
|
|
@ -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 `<h1>` + 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?
|
||||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Esqueceu a senha?</CardTitle>
|
||||
<h1 className={campaignAuthHeadingClassName}>Esqueceu a senha?</h1>
|
||||
<CardDescription>
|
||||
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}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -56,7 +59,10 @@ export const ForgotPasswordForm = () => {
|
|||
<span aria-live="polite">{pending ? 'Enviando...' : 'Enviar link'}</span>
|
||||
</Button>
|
||||
<p className="text-center text-sm">
|
||||
<Link href="/campanha/login" className="text-primary underline-offset-4 hover:underline">
|
||||
<Link
|
||||
href="/campanha/login"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
Voltar ao login
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<div className="flex items-center gap-2 self-center font-semibold">
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<Megaphone className="size-4" />
|
||||
</div>
|
||||
Campanha
|
||||
</div>
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
</main>
|
||||
<CampaignAuthPageShell>
|
||||
<ForgotPasswordForm />
|
||||
</CampaignAuthPageShell>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Acessar painel</CardTitle>
|
||||
<CardDescription>Entre com seu e-mail ou celular para continuar.</CardDescription>
|
||||
<h1 className={campaignAuthHeadingClassName}>Entrar na campanha</h1>
|
||||
<CardDescription>Use o e-mail da equipe ou o celular da liderança.</CardDescription>
|
||||
<p className="text-sm text-muted-foreground text-pretty">
|
||||
Primeiro acesso? Peça um convite no WhatsApp ao coordenador do seu núcleo.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={formAction}>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<Field data-invalid={hasAuthError || undefined}>
|
||||
<FieldLabel htmlFor="identifier">E-mail ou celular</FieldLabel>
|
||||
<Input
|
||||
id="identifier"
|
||||
name="identifier"
|
||||
type="text"
|
||||
inputMode="email"
|
||||
inputMode={identifierInputMode(identifier)}
|
||||
autoComplete="username"
|
||||
enterKeyHint="next"
|
||||
placeholder="voce@exemplo.com ou (71) 99999-1234"
|
||||
placeholder="(71) 99999-1234 ou voce@exemplo.com"
|
||||
required
|
||||
className="min-h-11"
|
||||
value={identifier}
|
||||
onChange={(event) => setIdentifier(event.target.value)}
|
||||
aria-invalid={hasAuthError || undefined}
|
||||
aria-describedby={hasAuthError ? LOGIN_ERROR_ID : undefined}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Field data-invalid={hasAuthError || undefined}>
|
||||
<FieldLabel htmlFor="current-password">Senha</FieldLabel>
|
||||
<Input
|
||||
id="current-password"
|
||||
|
|
@ -46,19 +73,26 @@ export const LoginForm = () => {
|
|||
enterKeyHint="done"
|
||||
required
|
||||
className="min-h-11"
|
||||
aria-invalid={hasAuthError || undefined}
|
||||
aria-describedby={hasAuthError ? LOGIN_ERROR_ID : undefined}
|
||||
/>
|
||||
</Field>
|
||||
<p className="text-sm">
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<Link
|
||||
href="/campanha/esqueci-senha"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
Esqueceu a senha?
|
||||
</Link>
|
||||
</p>
|
||||
{hasAuthError ? (
|
||||
<p className="text-muted-foreground text-pretty">
|
||||
{CAMPAIGN_LEADERSHIP_LOGIN_RECOVERY_HINT}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Field>
|
||||
{state.error ? <FieldError>{state.error}</FieldError> : null}
|
||||
<Button type="submit" className="min-h-11" disabled={pending}>
|
||||
{state.error ? <FieldError id={LOGIN_ERROR_ID}>{state.error}</FieldError> : null}
|
||||
<Button type="submit" className="min-h-11 w-full" disabled={pending}>
|
||||
{pending ? <Spinner data-icon="inline-start" aria-hidden="true" /> : null}
|
||||
<span aria-live="polite">{pending ? 'Entrando...' : 'Entrar'}</span>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<div className="flex items-center gap-2 self-center font-semibold">
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<Megaphone className="size-4" />
|
||||
</div>
|
||||
Campanha
|
||||
</div>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</main>
|
||||
<CampaignAuthPageShell>
|
||||
<LoginForm />
|
||||
</CampaignAuthPageShell>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Redefinir senha</CardTitle>
|
||||
<h1 className={campaignAuthHeadingClassName}>Redefinir senha</h1>
|
||||
<CardDescription>Escolha uma nova senha com pelo menos 8 caracteres.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
|
@ -63,7 +64,10 @@ export const ResetPasswordForm = ({ token }: { token: string }) => {
|
|||
<span aria-live="polite">{pending ? 'Salvando...' : 'Salvar nova senha'}</span>
|
||||
</Button>
|
||||
<p className="text-center text-sm">
|
||||
<Link href="/campanha/login" className="text-primary underline-offset-4 hover:underline">
|
||||
<Link
|
||||
href="/campanha/login"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
Voltar ao login
|
||||
</Link>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl">Link inválido</CardTitle>
|
||||
<CardDescription>{CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<Button asChild className="min-h-11 w-full">
|
||||
<Link href="/campanha/esqueci-senha">Solicitar novo link</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="min-h-11 w-full">
|
||||
<Link href="/campanha/login">Voltar ao login</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
<CampaignAuthPageShell>
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<h1 className={campaignAuthHeadingClassName}>Link inválido</h1>
|
||||
<CardDescription>{CAMPAIGN_PASSWORD_RESET_INVALID_TOKEN_MESSAGE}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<Button asChild className="min-h-11 w-full">
|
||||
<Link href="/campanha/esqueci-senha">Solicitar novo link</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="min-h-11 w-full">
|
||||
<Link href="/campanha/login">Voltar ao login</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CampaignAuthPageShell>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<div className="flex items-center gap-2 self-center font-semibold">
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<Megaphone className="size-4" />
|
||||
</div>
|
||||
Campanha
|
||||
</div>
|
||||
<ResetPasswordForm token={token} />
|
||||
</div>
|
||||
</main>
|
||||
<CampaignAuthPageShell>
|
||||
<ResetPasswordForm token={token} />
|
||||
</CampaignAuthPageShell>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
33
src/components/campaign/CampaignAuthPageShell.tsx
Normal file
33
src/components/campaign/CampaignAuthPageShell.tsx
Normal file
|
|
@ -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<ComponentProps<'main'>, 'children' | 'className'>
|
||||
|
||||
export const CampaignAuthPageShell = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: CampaignAuthPageShellProps) => (
|
||||
<main
|
||||
className={cn(
|
||||
'flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||
<div className="flex items-center gap-2 self-center font-semibold">
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<Megaphone className="size-4" aria-hidden="true" />
|
||||
</div>
|
||||
Campanha
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
10
src/lib/campaignAuthCopy.ts
Normal file
10
src/lib/campaignAuthCopy.ts
Normal file
|
|
@ -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'
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue