feat(B165): território colado ao nome na célula da lista de municípios

A célula de nome lia 'nome + território' como bloco: a altura mínima da
linha passa a viver no wrapper da célula (preservando o alinhamento dos
controles), o link do nome perde o espaço vazio forçado abaixo do texto
e o TerritoryLink ganha a variante compact na tabela desktop (o card
mobile mantém os 44px de alvo de toque).

Verificado geometricamente no browser (1 e 2 linhas): vão texto→texto
de 2–4px, sem sobreposição, links intactos, linha com nome curto sem
desalinhar as demais colunas.

Closes #412
This commit is contained in:
Francisco Solla 2026-08-08 12:38:04 -03:00
parent 9447686edf
commit 59948ea1ef
3 changed files with 94 additions and 6 deletions

View file

@ -0,0 +1,73 @@
# Impl: Território mais perto do nome na lista de municípios
Status: aprovado
Atualizado em: 2026-08-08
Issue: #412
Intenção: docs/plans/territorio-perto-nome-lista-municipios.md
Appetite restante: ~0,25–0,5 dia — mantém; ajuste de célula, sem migration.
## Leitura da intenção
- **Outcome:** na célula de nome da tabela desktop de `/campanha/municipios`, o território fica colado ao nome (~2–4 px entre os dois textos), com nome em 1 ou 2 linhas, sem sobrepor, sem mudar nenhuma navegação e sem desalinhar os controles das outras colunas.
- **O que NÃO negociar:** navegação (nome → município, território → âncora da página de territórios, B25); altura mínima da linha preservada; mobile cards fora de escopo; nada de densificar a lista inteira.
- **O que reavaliar:** a hipótese "a culpa é do vão `gap-0.5`" está **errada** — o `gap` já é 2 px. O vão real vem de duas caixas que engordam a célula por baixo: o `min-h-11` do link do território (`items-center` centraliza o texto xs em 44 px) e o `min-h-11` + `py-2` do link do nome (texto alinhado ao topo de 44 px → ~24 px vazios abaixo quando o nome tem 1 linha). Resultado hoje: ~40 px de vão texto→texto em nome de 1 linha.
## Abordagem recomendada
```mermaid
flowchart LR
Cell["name cell<br/>MunicipalityList.tsx"]
Link["name Link<br/>remove min-h-11 + pb; min-h vai p/ o wrapper"]
Terr["TerritoryLink <b>compact</b><br/>sem min-h-11; mobile mantém 44px"]
Cell --> Link
Cell --> Terr
```
**Opções consideradas:** A | B | C
**Recomendação:** A — célula compacta sem perder a altura mínima de linha, com variante `compact` nova só para a tabela desktop.
**Rejeitadas:**
- **B (mexer no `TerritoryLink` em globo, sem variante):** encolhe o alvo de toque no card mobile (touch), que está fora de escopo e hoje usa os mesmos 44 px de forma legítima.
- **C (componente paralelo para a célula desktop):** gêmeo do dono — viola "edit the owner, don't twin"; o `TerritoryLink` compartilhado tem 2 call sites, uma variante cabe.
- **Mover o vão para 0 px (`gap-0`/colado de verdade):** aproxima descidas/ascendentes de fontes diferentes (nome base vs território xs); a intenção já fixou 2–4 px.
### Componentes / mudanças
- **`TerritoryLink`** (`src/components/campaign/municipality/TerritoryLink.tsx`): ganha prop opcional `compact` (default `false`). Em `compact`, remove `min-h-11` (mantém `inline-flex items-center ...`). Assinatura inglesa; comportamento atual intacto por omissão — mobile não muda.
- **Célula `name`** (`src/components/campaign/municipality/MunicipalityList.tsx`, ~linhas 278–297):
- wrapper externo `flex flex-col gap-0.5` → ganha `min-h-11` (a altura mínima da linha muda do link do nome para a célula).
- link do nome: remove `min-h-11` e o `py-2`; mantém `line-clamp-2 min-w-0 flex-1`, com `pt-1` (respiro de 4 px acima do texto; sem padding inferior → gap texto→texto = `gap-0.5` = 2 px).
- indicador de prioridade: ajuste fino de `mt-2` → `mt-1` (alinha o ícone `size-4` com a 1ª linha do nome após a perda do `py-2`).
- território: `TerritoryLink` com `compact`.
- **Migration:** não há.
- **Access / Consent:** não toca.
- **UI:** Impeccable A — encaixe de 2 arquivos, sem shape novo. Verificação visual no browser (dev server do worktree, porta 3265) com nomes de 1 e 2 linhas.
### Dados → forma (se aplicável)
- N/A — ajuste de espaçamento em célula existente, sem número novo (intenção, "Dados" = Não).
## Fases verificáveis
1. **Schema+server → N/A** (sem schema). Direto para a célula: ajustar `MunicipalityList.tsx` + variante `TerritoryLink`.
2. **UI/verificação** — `pnpm dev` na porta 3265; login staff; `/campanha/municipios`; checar nome 1 linha, nome 2 linhas (e nome + território longo truncado): vão ~2–4 px, sem sobreposição, links OK (nome → município; território → âncora `#ti-…` em `/campanha/territorios`), controles das outras colunas alinhados em linha de nome curto.
3. **Gates** — `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm format:check`, `gate:fast` (não depende de DB para este diff; rodar igual para o invariante). `pnpm push` na entrega. Sem testes de domínio novos (não há mudança de lógica/access/write).
## Rabbit holes / Não escopo (engenharia)
- **"Já que vou mexer, densifico a lista toda"** — cortado na intenção: só o vão nome↔território da célula `name`.
- **"Aproveito e ajusto o card mobile"** — fora de escopo (intenção), e a variante evita tocá-lo.
- **Tune de `px` por guess** — o exato pode variar (ex. `pt-1` vs `pt-0`, `mt-1` vs `mt-2`); resolver no browser, não por tentativa às cegas.
- Nada de helper/util nova: contato com nada de `utilities`/`lib`.
## Riscos e mitigação
- **Sobreposição em caso extremo** — impossível por construção: `line-clamp-2` fixa o nome em máx. 2 linhas e o território entra 2 px abaixo da última linha; os dois textos nunca dividem a mesma caixa.
- **Nome com descida (ç, g, p) encostando no território** — 2 px de `gap` + descida natural da fonte mantêm separação; confirmar no browser com "Camaçari" (uma linha) e um nome de 2 linhas.
- **Alvo de toque do território menor na tabela** — aceite da intenção (hit-zone menor em troca da leitura colada; registrar se virar reclamação). O nome segue sendo a navegação primária da célula com alvo maior.
## Aceite de engenharia
- [x] Aceite de produto da intenção ainda coberto (2–4 px, sem sobrepor, links intactos, altura mínima preservada)
- [x] Invariantes AGENTS/engineering-standards (naming inglês; copy pt-BR mantida; sem migration/round; sem tocar access)
- [x] Testes de domínio previstos (unit/int) onde access/write paths mudam — N/A, sem mudança de lógica; verificação visual + gates

View file

@ -276,22 +276,25 @@ const municipalityListColumns = ({
),
cellClassName: 'sticky left-0 z-[5] w-52 max-w-52 whitespace-normal bg-background',
cell: (municipality) => (
<div className="flex min-w-0 max-w-52 flex-col gap-0.5">
// B165 — the name+territory read as one block: the row's minimum height
// lives on the cell, so a 1-line name keeps the other columns aligned
// while the territory sits ~2px under the name with no forced box below.
<div className="flex min-h-11 min-w-0 max-w-52 flex-col gap-0.5">
<div className="flex min-w-0 items-start gap-1.5">
<Link
href={`/campanha/municipios/${municipality.slug}`}
className="line-clamp-2 min-h-11 min-w-0 flex-1 py-2 font-medium text-primary underline-offset-4 hover:underline"
className="line-clamp-2 min-w-0 flex-1 pt-1 font-medium text-primary underline-offset-4 hover:underline"
>
{municipality.name}
</Link>
{municipality.priority === 'alta' && isStaffView ? (
<span className="mt-2 shrink-0">
<span className="mt-[5px] shrink-0">
<MunicipalityPriorityIndicator />
</span>
) : null}
</div>
<span className="block truncate text-xs text-muted-foreground">
<TerritoryLink region={municipality.region} />
<TerritoryLink region={municipality.region} compact />
</span>
</div>
),

View file

@ -1,11 +1,23 @@
import Link from 'next/link'
import { buildTerritoryPageHref } from '@/lib/territoryAnchor'
import { cn } from '@/lib/utils'
export const TerritoryLink = ({ region }: { region: string }) => (
export const TerritoryLink = ({
region,
compact = false,
}: {
region: string
/** B165 — the desktop name cell reads name+territory as one block, so the
* territory drops its 44px touch-min there; the mobile card keeps it. */
compact?: boolean
}) => (
<Link
href={buildTerritoryPageHref(region)}
className="inline-flex min-h-11 items-center text-muted-foreground underline-offset-4 hover:underline"
className={cn(
'inline-flex items-center text-muted-foreground underline-offset-4 hover:underline',
compact ? 'min-h-0' : 'min-h-11',
)}
>
{region}
</Link>