Replace all project-local .cursor/ path references with .agents/ across rules, skills, environment config, hooks, and ignore files. Keep Cursor editor paths (~/.cursor/, window.cursor, CURSOR_API_KEY) and vendored Impeccable editor-hook paths unchanged.
9.6 KiB
Dimir o resultado no compositor de giro (feedback pendente)
Status: entregue (2026-07-28)
Atualizado em: 2026-07-28
Item do roadmap: docs/roadmap.md (Fill-ins abertos — riscado)
Impeccable: B — encaixe em /campanha/atividades/giros (rota entregue no E13 ✓); nenhuma superfície nova
Appetite: ~0,25 dia eng; três pontos de edição, sem migration/action/Consent
Responsável: —
Revisão 2026-07-28 (auditoria pré-implementação): questões em aberto fechadas (copy aria-live reutilizada; spinner do select permanece). CalendarPhaseNote hoje renderiza acima do picker — a entrega move a nota para dentro de CampaignListResults (abaixo do seletor), como a abordagem já descrevia.
As-built 2026-07-28: CampaignListPendingBoundary + CampaignListResults em giros/page.tsx; TourRegionPicker passou a useCampaignListTransition; CalendarPhaseNote dentro de Results; unit tourRegionPending.unit.spec.ts pina aria-busy/data-pending com push Promise pendente (React 19). Critique: sem P0/P1; polish sem mudança de código. Gate tsc/lint/format/cycles; knip P3 pré-existente (payload.config.ts); Aikido 0 findings nos first-party.
Design (Impeccable)
Âncoras: PRODUCT.md (clareza sob pressão) / DESIGN.md (register product, Field Desk) · tema data-theme='campaign' · precedente engineering-standards.mdc § Loading feedback e as 10 listas de /campanha que já usam CampaignListPendingBoundary + CampaignListResults.
Na implementação (implement-roadmap-item): craft compacto → critique → polish. Sem redesign.
Brief compacto:
- Persona / contexto: o Assessor troca o Território de Identidade no seletor e o compositor abaixo é recomposto pelo servidor (âncora, satélites, semente, fila). Enquanto a navegação corre, a proposta antiga continua nítida na tela — nada diz que ela é do território anterior.
- Job principal: ao trocar de território, saber que a proposta visível está sendo substituída.
- Estratégia de cor: Restrained — o idioma já existe (
opacity-60+aria-busy+aria-livedoCampaignListResults); nada de spinner novo nem skeleton próprio. - Anti-goals: transformar o compositor numa lista (ele não é); segundo idioma de pending;
loading.tsxpara a rota (a troca de território é uma transição, não uma entrada).
Dados → decisão → apresentação
Dados: N/A — nenhuma métrica, série ou escala muda. O item é feedback de transição sobre a composição que o E13 ✓ já entrega.
Contexto
O /simplify do E13 (2026-07-27, três revisores) marcou este achado como "too involved — the composer isn't a list structure" e o deixou passar. A auditoria de fechamento (capture-review-debts, mesma data) mediu o contrário: as peças de pending não são acopladas a lista nenhuma —
CampaignListPendingBoundary(src/components/campaign/shared/CampaignListPending.tsx:30) é umContext.Providerem volta dechildren, sem uma linha sobre tabelas;CampaignListResults(:107) é umadivque aplicadata-pending→opacity-60,aria-busye a regiãoaria-live"Atualizando resultados…";useCampaignListTransition(:45) já cai numuseTransitionlocal quando não há boundary acima.
Ou seja, o que falta é ligar três fios, não portar uma abstração. Hoje TourRegionPicker (src/components/campaign/tour/TourRegionPicker.tsx:35) instancia useTransition() à mão e gasta o isPending só no seu próprio FieldDescription (spinner + "Montando a proposta de giro…"), com o select desabilitado. É exatamente o padrão que o engineering-standards.mdc nomeia como defeito: "a região de RESULTADOS dima, não só o controle".
Dois efeitos concretos, além da inconsistência com as dez listas vizinhas:
- A proposta obsoleta continua legível e clicável durante a transição — inclusive as checkboxes de parada e o campo de nome do giro, cujo default (
Giro <TI> dd/mm) é do território que está saindo. - Leitor de tela perde o evento. As listas anunciam "Atualizando resultados…" numa região
aria-liveque envolve o resultado; aqui o único sinal vive na descrição do próprio select, que o usuário de teclado acabou de sair.
Objetivos
- Trocar de território em
/campanha/atividades/girosdima a região de resultado (proposta do giro / empty state), não apenas o seletor. TourRegionPickerconsomeuseCampaignListTransitionem vez de instanciar o seu própriouseTransition— mesma peça que os outros controles navegantes de/campanha.- A transição anuncia-se por
aria-liveuma vez (a doCampaignListResults), sem criar uma segunda região viva na página. - Guardrails: sem migration, sem collection, sem server action, sem
Consent; contrato de URL (?region=) intacto; o comportamento do formulário (useActionState, caps, validação) intacto.
Decisões travadas
- Reusar
CampaignListPendingBoundary/CampaignListResultsem vez de um par próprio para o compositor. As duas peças não sabem o que é uma lista e o hook já tem fallback local; um par paralelo seria o segundo idioma de pending em/campanha. (2026-07-27, medição do fechamento do E13.) Rejeitado: componentesTourComposerPending*próprios (duplicaopacity/aria-busy/aria-livepor zero ganho); renomear as peças paraCampaignPending*neste fill-in (toca 10 rotas por cosmética — só se um 3º domínio não-lista aparecer). - A fronteira do "resultado" é tudo o que depende de
?region=: oCalendarPhaseNote, os empty states de "escolha um território"/"nenhum candidato" e oTourComposerForm. O header, o seletor e o botão "Voltar para Atividades" ficam fora. Rejeitado: dimir a página inteira (o controle que o usuário está operando não deve desaparecer sob ele). - Nada de
loading.tsxna rota. A navegação érouter.pushdentro de uma transição, então o segmento não remonta; umloading.tsxsó apareceria na entrada direta, que já é rápida. Rejeitado: skeleton do compositor (a proposta anterior dimada é mais informativa que um esqueleto). - i18n e naming (AGENTS.md): identificadores em inglês; a copy visível reusa "Atualizando resultados…" já existente, sem nova string.
Questões resolvidas (2026-07-28)
- Copy
aria-live: reusar "Atualizando resultados…" (sem prop de mensagem noCampaignListResults). Revisitar no critique se soar errado em voz alta. - Spinner do
FieldDescription: permanece — feedback imediato no gesto; o dim informa o escopo.
Abordagem proposta
flowchart LR
boundary["CampaignListPendingBoundary<br/>(page.tsx)"] --> picker["TourRegionPicker<br/>useCampaignListTransition"]
boundary --> results["CampaignListResults"]
results --> phase["CalendarPhaseNote"]
results --> empty["CampaignListEmptyState"]
results --> form["TourComposerForm"]
Componentes:
src/app/(campaign)/campanha/(app)/atividades/giros/page.tsx: envolve o seletor + o bloco de resultado numCampaignListPendingBoundary(o provider é client, o conteúdo continua RSC como nas dez listas) e o bloco condicional (CalendarPhaseNote+ a escada de empty states +TourComposerForm) numCampaignListResults.src/components/campaign/tour/TourRegionPicker.tsx(:35):useTransition()→useCampaignListTransition(); nada mais muda (ohrefjá vem serializado pelo RSC).- Teste: caso jsdom em
tests/unit/no molde decampaignListFilterNavigation.unit.spec.ts(router mockado) provando que a troca de território marcaaria-busyna região de resultado; odata-pendingé o mesmo atributo que as listas já pinam. - Migration: nenhuma.
Depth check: nenhum módulo novo — três edições em dois arquivos que já donam a superfície.
Dependências
- Dura: E13 ✓ (a rota existe). Suave: B33+ ✓ (
useCampaignListTransitionnasceu ali como fallback compartilhado). - Reusa:
CampaignListPending.tsxinteiro.
Não escopo
- Feedback de submissão do formulário (gerar rascunhos) — já coberto pelo
useActionState+SpinnerdoTourComposerForm. - Pending nas demais superfícies do E13 (card "Elegibilidade para visita" no detalhe) — já streama atrás de
<Suspense>. - Renomear a família
CampaignList*para algo neutro — gatilho no plano do B32+ se um 3º consumidor não-lista aparecer.
Rabbit holes
- "Já que estou aqui, transformo o compositor numa lista canônica." Se alguém "só completar": nasce paginação, filtros e footer numa tela de três interações. Mitigação: o item toca duas linhas de import e dois wrappers.
- "Generalizo o pending para toda a
/campanha." Vira o rename de 10 rotas que esta decisão explicitamente adiou. Mitigação: boundary = a rota de giros.
Adiado com gatilho
- Renomear
CampaignList*→CampaignPending*. Revisitar quando um 3º consumidor não-lista usar as peças (o compositor é o 2º, contando o wizard de import como parcial).
Referências
docs/roadmap.md(Fill-ins abertos)- planejador-de-giros.md — E13 ✓, o pai do achado
src/components/campaign/shared/CampaignListPending.tsx(:30boundary,:45hook,:107results)src/components/campaign/tour/TourRegionPicker.tsx(:35)src/app/(campaign)/campanha/(app)/atividades/giros/page.tsxsrc/app/(campaign)/campanha/(app)/municipios/page.tsx— precedente de montagem boundary + resultstests/unit/campaignListFilterNavigation.unit.spec.ts— molde do teste.agents/rules/engineering-standards.mdc— § Loading feedback ("a região de RESULTADOS dima")PRODUCT.md/DESIGN.md— Field Desk