teqo/docs/plans/hover-mapa-pracas.md
Francisco Ignácio Fontoura Solla adc0f31f5b Sync all project docs with the deployed remodel and hardening; compile roadmap
- Compile docs/roadmap.md into a next-steps document (277→206 lines): R0–R5/M1–M5
  tables and the delivered map/list item paragraphs compressed into 'Já entregue';
  janelas, grafo and cortes list only open IDs; C10/C11/C8 fill-ins keep their
  original triggers; C5 moved under 'A validar'.
- Record the production deploy of the destructive remodel chain (2026-07-23 Vercel
  builds) across roadmap, AGENTS.md, the project notebook and plans — supersedes
  every 'deploy pendente'; smoke pós-deploy + onboarding remain the open blockers.
- Fix AGENTS.md internal contradictions: Município locked-decisions bullet (435,
  Camaçari whole, candidate role), campaignUser role enum, deploy-checklist item 7
  as historical, access/* structure note, election-aggregates artifact +
  election-tse revalidate runbook.
- Sync open plans to real identifiers (plaza*/nucleus* → municipality*): the
  intelligence program (master + E8–E15/B13/C12 with artifact/caching context and
  satisfied deps), B8 rescoped to Salvador ZE 1–19 only, WhatsApp D3–D5, D2, A6
  (now the insight layer over stateDeputy), RS+, onda-0 smoke step.
- Close items verified done in code: C8 F4 (DRY de forms) complete, filtros-auto
  shared pending shell, petition generateStaticParams select/depth; ledger the
  surviving user-visible 'Praça' copy in src as new P2 debt.
- Banner delivered praça-era plans (production note + rename pointer); mark
  remodelagem-pracas.md superseded and remodelagem-municipios.md delivered.
- TECH-DEBT smell statuses, TESTING RBAC row, README quality gates, campanha
  UI rules updated to current component names.
2026-07-24 02:04:45 -03:00

115 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Hover no Mapa das Praças (destaque + total de votos + navegação)
> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md).
Status: entregue 2026-07-21 (em produção desde 2026-07-23)
Atualizado em: 2026-07-21 (navegação: desktop click / mobile 2º tap; B6 ✓ setStyle incremental)
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B10 — entregue 2026-07-21)
Impeccable: B — encaixe em `PlazaMapPanel` / `BahiaMap` (sem rota nova)
Appetite: ~0,5–1 dia eng; handlers Leaflet + faixa + mapa ibge→slug + teste/checklist
Responsável: —
_Revisão 2026-07-21 (pós-implementação + `/simplify`): `BahiaMap` com `onFeatureSelect` / `onFeatureActivate`; `MapFeatureReadout`; `plazasByIbgeCode` + `resolvePlazaMapNavigation`; SSA/CMS N>1 → scroll `#plaza-zone-breakdown`. Cleanup: dedup hover, merge effects, `emphasizeFeature`. Débito hot path O(n) `eachLayer` → **B6 ✓** ([escala-dry-pos-b3.md](escala-dry-pos-b3.md))._
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`PlazaMapPanel`, `BahiaMap`, `ChoroplethLegend`).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (superfície já existe; interação nova, sem shape longo).
Brief compacto:
- **Persona / contexto:** Assessor/Coordenador Geral (Alex) inspeciona o coroplético, lê o número sob o cursor e, quando quiser, **abre a Praça** sem voltar à lista.
- **Job principal:** destacar o polígono ativo, mostrar nome + total da métrica corrente, e navegar para `/campanha/pracas/[slug]` com o gesto certo (click desktop / 2º tap mobile).
- **Estratégia de cor:** Restrained — borda/weight de destaque com primário `#C51414`; fill do coroplético inalterado.
- **Anti-goals:** não navegar no hover; não tooltip SaaS ornamentado; não segunda legenda; não redesenhar o painel; não exigir `matchMedia` separado se o modelo selected+click unificar desktop/mobile.
## Contexto
O **Mapa das Praças** (`PlazaMapPanel` em `/campanha/pracas`) já pinta municípios pelo valor do ano selecionado (TSE 2014/2018/2022 ou estimativas 2026) e no modo comparar pelo diff Solla − outro. O renderer `BahiaMap` monta `L.geoJSON` com estilo estático e `highlightKeys` **só** para zoom/destaque programático — **não** há `mouseover` / readout / click→Praça.
Pedido de produto (2026-07-21): hover destaca + mostra votos; **click navega**. Em mobile (sem hover): **primeiro toque seleciona** (mesmo readout); **segundo toque na já selecionada** navega.
Não cobre: filtro URL (**B7**), polígonos por zona (**B8**). ~~`setStyle` incremental ao trocar ano~~ → **B6 ✓**.
## Objetivos
- Hover (desktop) sobre um município: borda/weight mais forte + readout com **nome** + **valor formatado** da métrica corrente; **não** navega.
- Click (desktop) no polígono sob o hover: `router.push` / `Link` para a Praça correspondente.
- Touch: 1º tap = seleciona (destaque + readout); 2º tap na **mesma** seleção = navega; tap noutro polígono = troca a seleção.
- O valor exibido é o mesmo de `values` já passado ao mapa — sem fetch por hover/click.
- Guardrails: sem migration, sem collection, sem Consent, sem server action; `leader` continua sem mapa; pan/zoom do Leaflet não devem competir com o 2º tap (ver abordagem).
## Decisões travadas
- **Item próprio B10 (não absorver em B6, B7 nem R6).** Interação de inspeção+atalho ≠ filtro URL, ≠ perf `setStyle`, ≠ polish genérico. (2026-07-21.) **Rejeitado:** fill-in sem plano; fase de B7.
- **Modelo unificado selected + click (não forks `pointer: coarse` obrigatórios).** Estado `selectedKey`: hover (desktop) e 1º tap (mobile) **selecionam**; click/tap com `key === selectedKey` **navega**; hover não navega. Em desktop o hover já deixa `selectedKey` igual ao polígono, então o 1º click navega. Em mobile o 1º tap só seleciona. (Produto, 2026-07-21.) **Rejeitado:** só highlight sem navegação; navegação no hover; `matchMedia` mobile/desktop com dois comportamentos divergentes sem o mesmo selected.
- **Cliente-only; bundle traz `plazasByIbgeCode`.** Nome no readout pode vir do GeoJSON; navegação precisa de `slug`(s) por `codarea` — incluir no `PlazaMapBundle` um mapa `ibgeCode → { slug, name }[]` (já filtrado pelo escopo de access / futuro B7). **Rejeitado:** Server Action por click; adivinhar slug a partir do nome do município no client.
- **Faixa de leitura sob o mapa + destaque Leaflet.** Touch-friendly e `aria-live="polite"`. Tooltip Leaflet opcional só no polish. **Rejeitado:** só tooltip hover-only; painel inspect lateral.
- **Mesma métrica do seletor Ano / Comparar.** **Rejeitado:** segundo número no hover v1.
- **Salvador / Camaçari (N>1 Praças no mesmo `ibgeCode`):** seleção mostra agregado municipal + readout; **navegação no 2º gesto não escolhe zona às cegas** — faz scroll/focus na lista `zoneBreakdown` (ou na faixa, com hint “ver Praças por zona abaixo”). Se o escopo do usuário tiver **exatamente 1** Praça-zona naquele município, aí sim navega para o único slug. (2026-07-21.) **Rejeitado:** sempre ir à ZE 1 / primeira do catálogo; inventar hit-test por zona (= B8).
- **i18n e naming** (AGENTS.md): `onFeatureSelect`, `selectedFeature`, `plazasByIbgeCode`, `MapFeatureReadout`; strings em pt-BR.
## Questões em aberto
- **Polígono sem valor (0 / ausente no `values`)?** **Opções:** A) ainda seleciona e mostra “0” / “Sem dados”; navegação só se houver slug(s) no mapa | B) ignora interação. **Recomendação:** A — “Sem dados” se chave ausente; “0” se presente e zero; click-nav segue a regra de slugs (município sem Praça no escopo → sem navegação, só readout).
- **Faixa clicável como atalho de navegação?** **Opções:** A) só o polígono | B) faixa também é link/`button` para o mesmo destino. **Recomendação:** B quando há exatamente 1 slug (a11y / alvo 44px); com N>1 a faixa não navega (só o scroll hint). _(assumido — validar no craft)_
## Abordagem proposta
```mermaid
flowchart LR
bundle["PlazaMapBundle values + plazasByIbgeCode"] --> panel["PlazaMapPanel"]
panel --> map["BahiaMap onFeaturePointer"]
map -->|"hover / 1º tap"| select["selectedKey + destaque + readout"]
map -->|"click / 2º tap same key"| nav{"slugs.length?"}
nav -->|"1"| push["router.push /pracas/slug"]
nav -->|"N>1"| zones["scroll zoneBreakdown"]
map -->|"mouseout desktop"| clear["limpa selected se não pinned touch"]
```
Componentes:
- **`loadPlazaMapBundle` / `PlazaMapBundle`** (`src/utilities/plazaMapData.ts`): acrescentar `plazasByIbgeCode: Record<string, { slug: string; name: string }[]>` a partir das Praças já carregadas (município = 1 entrada; SSA/CMS = N zonas no escopo). Sem query extra.
- **`BahiaMap`** (`src/components/campaign/BahiaMap.tsx`): `onEachFeature` com `mouseover` / `mouseout` / `click`. Hover → callback `onFeatureSelect(info)`; mouseout → `onFeatureSelect(null)` (só se o ponteiro for fino / não após tap recente — ref `pointerType` ou “touch pin” até tap fora). Click → callback `onFeatureActivate(key)` (o painel decide select vs navigate). Destaque via `setStyle` + `bringToFront`; mouseout restaura estilo do coroplético. Depth check: sem wrapper novo.
- **`PlazaMapPanel`**: estado `selected`; readout; em `onFeatureActivate`: se `key !== selected` → select; se igual → `navigateForIbge(key)` (1 slug → `router.push`; N>1 → `scrollIntoView` na seção zonas). Opcional: link na faixa quando `slugs.length === 1`.
- **Alinhamento com B6:** mesma função de estilo no reset pós-hover. Não bloqueia B10.
- **Teste:** unit do resolver `navigateForIbge` (0 / 1 / N slugs); checklist manual desktop (hover→readout, click→ficha) e mobile (tap1 select, tap2 navigate, tap outro troca).
- **Migration:** Sem migration, sem collection, sem server action.
## Dependências
- **Dura:** R2 (mapa Praças) — entregue.
- **Suave:** A9 (métrica 2026); ~~B6 (`setStyle`)~~ entregue; B7 (filtro → `plazasByIbgeCode` só do conjunto filtrado); B8 (aí o 2º tap em SSA/CMS pode passar a ir à Praça-zona).
- Reusa: `PlazaMapPanel`, `BahiaMap`, `plazaMapData`, formatters existentes.
## Não escopo
- Filtrar mapa pela URL da lista → **B7** ([mapa-pracas-filtrado.md](mapa-pracas-filtrado.md)).
- ~~Rebuild/perf do layer ao trocar ano~~ → **B6 ✓** ([escala-dry-pos-b3.md](escala-dry-pos-b3.md)).
- Polígonos / hover por Praça-zona SSA/CMS → **B8** ([poligonos-pracas-zona.md](poligonos-pracas-zona.md)).
- Redesenho do painel / mover filtros → **R6**.
## Rabbit holes
- **`react-leaflet` / Popup Radix.** **Mitigação:** Leaflet nativo + faixa HTML.
- **Hit-test de zonas sem polígono.** **Mitigação:** agregado + scroll para `zoneBreakdown` até B8.
- **Detectores `isMobile` frágeis.** **Mitigação:** modelo selected+activate; no máximo `pointerType` no evento para não limpar seleção no “mouseout” sintético pós-tap.
- **Conflito pan vs 2º tap.** **Mitigação:** só navegar em `click` Leaflet do path (não em `moveend`); se pan arrastar, Leaflet não dispara click no feature.
## Adiado com gatilho
- **2º tap em SSA/CMS → Praça-zona específica.** Revisitar quando **B8** entregar polígonos (hit-test real) ou produto pedir picker de zona no readout.
- **Tooltip Leaflet além da faixa.** Revisitar no polish se critique pedir reforço desktop.
## Referências
- `docs/roadmap.md` (Trilha B, item B10 — entregue)
- `src/components/campaign/BahiaMap.tsx` — `onFeatureSelect` / `onFeatureActivate` / highlight
- `src/components/campaign/PlazaMapPanel.tsx` — selected + readout + nav
- `src/utilities/plazaMapData.ts` / `plazaMapNavigation.ts` — `plazasByIbgeCode`
- `docs/plans/mapa-pracas-filtrado.md` — B7
- `docs/plans/escala-dry-pos-b3.md` — B6
- `docs/plans/poligonos-pracas-zona.md` — B8
- AGENTS.md — naming, mapa dinâmico `/campanha`, access assessor
- `PRODUCT.md` / `DESIGN.md` — Field Desk