mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Merge branch 'cursor/b28b985c'
This commit is contained in:
commit
853e486747
7 changed files with 66 additions and 34 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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 (<3 call sites) — sem helper shared prematuro.
|
||||
Depth check: reusa `CampaignHoverTooltip` e Lucide; island local (<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 (<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
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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']
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue