Merge branch 'cursor/b28b985c'

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-28 15:50:19 -03:00
commit 853e486747
7 changed files with 66 additions and 34 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,17 @@
# Ícone de prioridade na lista de Municípios
Status: rascunho
Atualizado em: 2026-07-24
Status: entregue
Atualizado em: 2026-07-28
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins)
Impeccable: B — encaixe em `MunicipalityList` (`/campanha/municipios`); sem rota nova
Appetite: ~0,25 dia eng; troca Badge→ícone+tooltip na coluna do nome; sem migration
Responsável: —
Revisão 2026-07-28: paths Pass 2 W2 (`municipality/MunicipalityList.tsx`, `municipalityLabels.ts`); primitivo de tooltip = `CampaignHoverTooltip` (B23 ✓), não Tooltip cru isolado; touch via tap no primitivo; card mobile exige `relative` no trigger (B42 link esticado).
## Design (Impeccable)
Âncoras: `PRODUCT.md` (princípio 2 — clareza sob pressão) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · shell `MunicipalityList` + shadcn `Tooltip` (`src/components/ui/tooltip.tsx`).
Âncoras: `PRODUCT.md` (princípio 2 — clareza sob pressão) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · shell `MunicipalityList` + `CampaignHoverTooltip` (`src/components/campaign/shared/CampaignHoverTooltip.tsx`).
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (só affordance de prioridade na lista; sem redesign da tabela).
@ -27,11 +29,11 @@ Dados: N/A — este item **não** muda métricas, agregação nem o significado
## Contexto
Em `/campanha/municipios` (staff), `MunicipalityList` (`src/components/campaign/MunicipalityList.tsx`) mostra, ao lado do nome na coluna da tabela desktop **e** no card mobile, um `Badge variant="destructive"` com o texto de `municipalityPriorityLabels.alta` (“Prioritária”) quando `municipality.priority === 'alta'`. A lista é Server Component; o chip é só leitura.
Em `/campanha/municipios` (staff), `MunicipalityList` (`src/components/campaign/municipality/MunicipalityList.tsx`) mostrava, ao lado do nome na coluna da tabela desktop **e** no card mobile, um `Badge variant="destructive"` com o texto de `municipalityPriorityLabels.alta` (“Prioritária”) quando `municipality.priority === 'alta'`. A lista é Server Component; o sinal é só leitura.
Pedido de produto (2026-07-24): na coluna do município da tabela, trocar o chip por um **ícone** que indique prioridade + **tooltip no hover** com o texto **“Município prioritário”** (ortografia correta; tipografia do pedido tinha “Muncípio”).
Pedido de produto (2026-07-24): na coluna do município da tabela, trocar o chip por um **ícone** que indique prioridade + **tooltip** com o texto **“Município prioritário”** (ortografia correta; tipografia do pedido tinha “Muncípio”).
`MunicipalityList` já importa ícones Lucide (`CircleAlertIcon` / `CircleCheckIcon` na cobertura). Existe `Tooltip` shadcn client-only; o único uso de produto hoje é o `Sidebar`. `CampaignInfoHint` é Popover touch-friendly para “?” — padrão errado para flag binário.
`CampaignHoverTooltip` (B23) cobre hover, foco e toque; `CampaignInfoHint` permanece padrão errado para flag binário.
## Objetivos
@ -43,15 +45,16 @@ Pedido de produto (2026-07-24): na coluna do município da tabela, trocar o chip
- **Fill-in com plano próprio (não ID B novo; não absorver só em R6).** Polish cosmético, ~¼ dia, paralelizável, cortável. (2026-07-24, classificação roadmap-item.) **Rejeitado:** B16 de trilha (infla grafo); só R6 (atrasa quick win no critique largo); fase informal sem plano (precedente fill-in: Cenário junto aos filtros).
- **Ícone Lucide `Flag` (ou `FlagIcon`) ao lado do link do nome.** Sinal de prioridade sem competir com “favorito” / alerta de cobertura. **Rejeitado:** `Star` (lê como favorito); `CircleAlert` (já usado na coluna Cobertura); manter Badge de texto (pedido explícito); `title=` nativo sem Tooltip (UX inconsistente com shadcn).
- **Client island mínima** (ex. `MunicipalityPriorityIndicator.tsx` `'use client'`) usada nos 2 loci de `MunicipalityList` (tabela + card). Lista permanece Server Component. **Rejeitado:** `'use client'` em todo `MunicipalityList`; `title` só no SSR; abstrair shared `PriorityChip` genérico (<3 call sites fora desta lista).
- **Client island mínima** (`MunicipalityPriorityIndicator.tsx` `'use client'`) usada nos 2 loci de `MunicipalityList` (tabela + card). Lista permanece Server Component. **Rejeitado:** `'use client'` em todo `MunicipalityList`; `title` só no SSR; abstrair shared `PriorityChip` genérico (<3 call sites fora desta lista).
- **Copy fixa: “Município prioritário”.** Não reusar o label curto “Prioritária” do Badge/filtro. **Rejeitado:** “Prioritária” no tooltip (pedido de produto); tipografia “Muncípio”.
- **Escopo = só lista (`MunicipalityList`).** Card de estratégia no detalhe (`MunicipalityStrategyCard`) mantém Badge por enquanto. **Rejeitado:** unificar todas as superfícies de `priority` neste item (estoura appetite e mistura leitura de lista com card de estratégia).
- **i18n e naming** (AGENTS.md): identificador inglês (`MunicipalityPriorityIndicator`); string visível pt-BR “Município prioritário”.
- **Tooltip via `CampaignHoverTooltip`** (2026-07-28). **Rejeitado:** Tooltip shadcn cru com provider local (B23 já padronizou touch + dismiss).
## Questões em aberto
- **Cor do ícone: `text-primary` (Signal Red do tema) vs. herdar o tom do `Badge destructive`?** **Opções:** A) `text-primary` | B) classes alinhadas ao destructive | C) `text-muted-foreground`. **Recomendação:** **A** — ênfase Field Desk sem reintroduzir um chip vermelho cheio. _(assumido — validar no craft/critique)_
- **Touch / mobile: tooltip Radix é hover — basta `aria-label` no ícone no card?** **Opções:** A) só aria-label no mobile | B) Popover on tap | C) texto curto visível no card. **Recomendação:** **A** — pedido é hover na tabela; card já é denso; Popover seria overkill. _(assumido)_
- **Cor do ícone:** **Fechado** — `text-primary` (recomendação A).
- **Touch / mobile:** **Fechado** — `CampaignHoverTooltip` com tap; `aria-label` no trigger; no card, `relative` + alvo 44px para ficar acima do link esticado (B42).
## Abordagem proposta
@ -60,21 +63,21 @@ flowchart LR
list["MunicipalityList RSC"] --> name["Link nome"]
list --> island["MunicipalityPriorityIndicator client"]
island --> icon["Flag Lucide"]
island --> tip["Tooltip shadcn"]
island --> tip["CampaignHoverTooltip"]
tip --> copy["Município prioritário"]
```
Componentes:
- **`MunicipalityPriorityIndicator`** (novo, `src/components/campaign/MunicipalityPriorityIndicator.tsx`, `'use client'`): renderiza trigger (botão ou `span` com `asChild` conforme padrão shadcn) com ícone `Flag`, `aria-label="Município prioritário"`, envolvido em `TooltipProvider` local (ou provider pai se já houver) + `Tooltip` / `TooltipTrigger` / `TooltipContent` com o mesmo texto. Props mínimas (sem view-model).
- **`MunicipalityList`** (`src/components/campaign/MunicipalityList.tsx`): nos dois ramos `priority === 'alta' && isStaffView`, trocar `<Badge…>` por `<MunicipalityPriorityIndicator />`; remover import de `Badge` / `municipalityPriorityLabels` se ficarem órfãos neste arquivo.
- **`MunicipalityPriorityIndicator`** (`src/components/campaign/municipality/MunicipalityPriorityIndicator.tsx`, `'use client'`): `FlagIcon`, `aria-label="Município prioritário"`, `CampaignHoverTooltip` com o mesmo texto. Props: `className` opcional (card passa `relative size-11`).
- **`MunicipalityList`**: nos dois ramos `priority === 'alta' && isStaffView`, `<MunicipalityPriorityIndicator />`.
- **Migration**: Sem migration, sem collection, sem server action.
Depth check: reusa `tooltip.tsx` e Lucide já no produto; um island local (&lt;3 call sites) — sem helper shared prematuro.
Depth check: reusa `CampaignHoverTooltip` e Lucide; island local (&lt;3 call sites) — sem helper shared prematuro.
## Dependências
- Nenhuma de outro plano aberto. Reusa `priority` / `isStaffView` já no loader/VM e o `Tooltip` shadcn existente.
- Nenhuma de outro plano aberto. Reusa `priority` / `isStaffView` já no loader/VM e `CampaignHoverTooltip` (B23).
## Não escopo
@ -85,22 +88,21 @@ Depth check: reusa `tooltip.tsx` e Lucide já no produto; um island local (&lt;3
## Rabbit holes
- **Popover touch “completo” / `CampaignInfoHint`.** Explode um “?” genérico para flag binário. **Mitigação:** Tooltip + `aria-label`; defer Popover só se critique R6 mostrar falha de descoberta no mobile.
- **Popover touch “completo” / `CampaignInfoHint`.** Explode um “?” genérico para flag binário. **Mitigação:** `CampaignHoverTooltip` + `aria-label`.
- **Componente shared de prioridade em todo `/campanha`.** Classitis com 1–2 call sites. **Mitigação:** island só na lista; unificar quando o 3º call site aparecer (ex. se StrategyCard pedir o mesmo ícone).
- **Tooltip no link inteiro do nome.** Atrapalha clique/navegação e a11y. **Mitigação:** trigger só no ícone.
## Adiado com gatilho
- **Mesmo ícone+tooltip no `MunicipalityStrategyCard`.** Revisitar quando: R6 critique o card de estratégia ou produto pedir paridade lista↔detalhe.
- **Popover on-tap no card mobile.** Revisitar quando: evidência de campo de que staff não entende o Flag sem hover (improvável no desktop-first da lista).
## Referências
- `docs/roadmap.md` (Fill-ins abertos)
- `src/components/campaign/MunicipalityList.tsx` — Badge atual (mobile ~L80–82; desktop ~L171–173)
- `src/components/ui/tooltip.tsx` — primitivo a reusar
- `src/utilities/municipalityUi.ts` — `municipalityPriorityLabels` (forms/filtro; não necessariamente o tooltip)
- `src/components/campaign/MunicipalityStrategyCard.tsx` — Badge fora de escopo
- `docs/roadmap.md` (Fill-ins)
- `src/components/campaign/municipality/MunicipalityList.tsx`
- `src/components/campaign/shared/CampaignHoverTooltip.tsx`
- `src/utilities/municipalityLabels.ts` — `municipalityPriorityLabels` (forms/filtro)
- `src/components/campaign/municipality/MunicipalityStrategyCard.tsx` — Badge fora de escopo
- [cenario-junto-filtros-municipios.md](cenario-junto-filtros-municipios.md) — precedente de fill-in Impeccable B na mesma vertical
- AGENTS.md — naming; staff vs leader
- `PRODUCT.md` / `DESIGN.md` — Field Desk, clareza sob pressão

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,7 @@ import { MunicipalityListGoalCoverageCell } from '@/components/campaign/municipa
import { MunicipalityListLevelControl } from '@/components/campaign/municipality/MunicipalityListLevelControl'
import { MunicipalityListSignalControl } from '@/components/campaign/municipality/MunicipalityListSignalControl'
import { MunicipalityListTrendControl } from '@/components/campaign/municipality/MunicipalityListTrendControl'
import { MunicipalityPriorityIndicator } from '@/components/campaign/municipality/MunicipalityPriorityIndicator'
import { MunicipalitySortableHead } from '@/components/campaign/municipality/MunicipalitySortableHead'
import { TerritoryLink } from '@/components/campaign/municipality/TerritoryLink'
import { CampaignHoverTooltip } from '@/components/campaign/shared/CampaignHoverTooltip'
@ -46,7 +47,6 @@ import {
municipalityColumnDescriptions,
municipalityGeographyParts,
municipalityKindLabels,
municipalityPriorityLabels,
territorialClassBadgeVariant,
territorialClassLabels,
type MunicipalityListColumnId,
@ -172,8 +172,8 @@ const TerritorialClassReadout = ({ municipality }: { municipality: MunicipalityL
* Cold reads in the warning amber, NOT destructive: early in the campaign
* most municípios are past the 21-day threshold, and painting them all red
* would drown the one state that really is an error in this list — a priority
* município with nobody answering for it. Same reason it stays text with an
* icon instead of a third badge: the row already carries the priority pill and,
* município with nobody answering for it. Same reason the priority flag stays
* an icon instead of a third badge: the row already carries the priority signal and,
* when it applies, the missing-advisor one.
*/
const SignalAgeReadout = ({
@ -250,10 +250,9 @@ const advisorNames = (
advisorNamesById: ReadonlyMap<number, MunicipalityAdvisorSummary>,
): string[] => advisorEntries(municipality, advisorNamesById).map((advisor) => advisor.name)
/** The desktop table as column definitions (Pass 2 W1 list system). */
/**
* The id is the picker's key, the cookie's key and the key of the B22
* description record, so it is typed instead of widening to `string`.
* Desktop table columns (Pass 2 W1). `id` is the picker key, cookie key, and B22
* description key — typed instead of widening to `string`.
*/
type MunicipalityColumn = CampaignTableColumn<MunicipalityListViewModel> & {
id: MunicipalityListColumnId
@ -293,9 +292,7 @@ const municipalityListColumns = ({
>
{municipality.name}
</Link>
{municipality.priority === 'alta' && isStaffView ? (
<Badge variant="destructive">{municipalityPriorityLabels.alta}</Badge>
) : null}
{municipality.priority === 'alta' && isStaffView ? <MunicipalityPriorityIndicator /> : null}
</div>
),
},
@ -611,7 +608,7 @@ export const MunicipalityList = (props: MunicipalityListProps) => {
{position ? <VotePositionReadout position={position} layout="card" /> : null}
</div>
{isPriority && isStaffView ? (
<Badge variant="destructive">{municipalityPriorityLabels.alta}</Badge>
<MunicipalityPriorityIndicator className="relative size-11" />
) : null}
</div>
{isStaffView ? (

View file

@ -0,0 +1,25 @@
'use client'
import { FlagIcon } from 'lucide-react'
import { CampaignHoverTooltip } from '@/components/campaign/shared/CampaignHoverTooltip'
import { cn } from '@/lib/utils'
import { municipalityPriorityIndicatorLabel } from '@/utilities/municipalityLabels'
type MunicipalityPriorityIndicatorProps = {
className?: string
}
/** Staff-only `priority === 'alta'` readout on the municipality list (icon + tooltip). */
export const MunicipalityPriorityIndicator = ({
className,
}: MunicipalityPriorityIndicatorProps) => (
<CampaignHoverTooltip content={municipalityPriorityIndicatorLabel} align="start">
<span
className={cn('inline-flex shrink-0 items-center justify-center text-primary', className)}
aria-label={municipalityPriorityIndicatorLabel}
>
<FlagIcon className="size-4" aria-hidden />
</span>
</CampaignHoverTooltip>
)

View file

@ -32,6 +32,9 @@ export const municipalityPriorityLabels: Record<NonNullable<Municipality['priori
normal: 'Normal',
}
/** List name-column flag tooltip / `aria-label` — not the short filter label above. */
export const municipalityPriorityIndicatorLabel = 'Município prioritário' as const
export type PoliticalTrendStatus = NonNullable<
NonNullable<Municipality['politicalTrend']>['status']
>

View file

@ -20,6 +20,7 @@ import type { CampaignColumnVisibility } from '@/lib/campaignColumnVisibility'
import { toVoteEstimateScenarioViewModel } from '@/lib/voteEstimate'
import type { CampaignFormActionState } from '@/utilities/campaignFormActionError'
import { createEmptyGoalCoverageByScenario } from '@/utilities/goalCoverage'
import { municipalityPriorityIndicatorLabel } from '@/utilities/municipalityLabels'
import type {
MunicipalityAdvisorSummary,
MunicipalityListViewModel,
@ -249,7 +250,9 @@ describe('campaign visual foundation', () => {
expect(html).toContain('href="/campanha/municipios/seabra"')
expect(html).toContain('1.500')
expect(html).toContain('Cenários de estimativa')
expect(html).toContain('Prioritária')
expect(html).toContain(`aria-label="${municipalityPriorityIndicatorLabel}"`)
expect(html).toContain('lucide-flag')
expect(html).not.toMatch(/>\s*Prioritária\s*<\/span>/)
// The advisor column carries coverage on its own — no separate "Assessoria" column.
expect(html).toContain('Assessores')
expect(html).toContain('Ana Bastos')
@ -514,6 +517,7 @@ describe('campaign visual foundation', () => {
expect(html).not.toContain('Cenários de estimativa')
expect(html).not.toContain('Faltam')
expect(html).not.toContain('Prioritária')
expect(html).not.toContain(`aria-label="${municipalityPriorityIndicatorLabel}"`)
// B22: the leader view's 5 columns (name/region/kind/votos/lastUpdateAt)
// also carry header explanations.
const [theadHtml] = html.split('<tbody')