From 02abd7de899d62d83b23557e949669d31db27657 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 11 Aug 2026 07:37:34 -0300 Subject: [PATCH] feat(B196): omnibox colado ao header e card mobile mais denso (Closes #607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chassis de lista (vale para todas as listas, gate A): - scrollport: max-md:has-[.campaign-list-omnibox-form]:pt-0 — o containing block do sticky passa a começar no topo; a barra cola no header (sem vão de 16px, nada passa por cima no scroll) - barra densa: campo min-h-8 py-0 + X size-8 → ~40px no total; anel de foco escopado a md (mobile sem anel de cor; caret é o indicador) - trailing cluster desktop-only por padrão (edgeToEdge removido) - pt-4 md:pt-0 nas 6 listas com linha de ações antes do form (incl. organizacoes) Card mobile (B193 densificado): - nome 18px leading-tight com território a ~2px - chips como blocos label-acima (Classe/Tendência/Nível, gate B) via ChipBlock - avatares numa linha por grupo com overlap proporcional (flex-1 cells, sem cap/wrap/…, overflow-hidden nas bordas) — prop wrap → overlapRow - MissingAdvisorBadge com wrap para não clipar na célula do grupo - densidade vertical gap-2.5/py-3; toques min-h-11 preservados e2e B196 (6 testes) + regressão B184/B183/B193 verde. --- ...ios-mobile-polimento-omnibox-cards-impl.md | 82 +++++++ .../campanha/(app)/apoiadores/page.tsx | 2 +- .../campanha/(app)/atividades/page.tsx | 2 +- .../campanha/(app)/demandas/page.tsx | 2 +- .../campanha/(app)/dobradinhas/page.tsx | 2 +- .../campanha/(app)/liderancas/page.tsx | 2 +- .../campanha/(app)/organizacoes/page.tsx | 2 +- .../MunicipalityAdvisorAvatarStack.tsx | 20 +- .../municipality/MunicipalityMobileCard.tsx | 70 ++++-- .../campaign/people/PeopleFilters.tsx | 7 +- .../campaign/shared/CampaignListOmnibox.tsx | 48 ++-- .../MunicipalityRelationAvatarStack.tsx | 48 +++- .../shell/CampaignQuickActionsHost.tsx | 2 +- tests/e2e/campaignMunicipalities.e2e.spec.ts | 214 ++++++++++++++++++ 14 files changed, 426 insertions(+), 77 deletions(-) create mode 100644 docs/plans/municipios-mobile-polimento-omnibox-cards-impl.md diff --git a/docs/plans/municipios-mobile-polimento-omnibox-cards-impl.md b/docs/plans/municipios-mobile-polimento-omnibox-cards-impl.md new file mode 100644 index 00000000..4934b76a --- /dev/null +++ b/docs/plans/municipios-mobile-polimento-omnibox-cards-impl.md @@ -0,0 +1,82 @@ +# Impl: Municípios mobile: omnibox colado ao header e card mais denso (pós-B193/B184) + +Status: aprovado +Atualizado em: 2026-08-10 +Issue: #607 +Intenção: docs/plans/municipios-mobile-polimento-omnibox-cards.md +Appetite restante: herdado (~0,5–1 dia eng) — sem estouro: mudanças de superfície em 2 shared + 1 card + 1 avatar + 5 pages + e2e + +## Leitura da intenção + +- **Outcome:** em `/campanha/municipios` mobile (< md), a barra de filtro cola no header (sem vão, nada passa por cima no scroll), o campo fica ~40px no total, foco sem anel de cor, nome 18px com território colado, chips Classe/Tendência/Nível com label acima, avatares numa linha por grupo com sobreposição dinâmica — padrão do chassis vale para TODAS as listas (gate A); chip de classe ganha label "Classe" (gate B). +- **O que NÃO negociar:** desktop (`md+`) intacto; sheets/edit-where-you-see intactos; labels/dados dos chips intactos; nenhum limite de avatares com "…"; área de toque dos controles do card não encolhe. +- **O que reavaliar:** a hipótese da intenção ("encurtar o vão no contexto da barra — não o padding global do scrollport") é correta e foi confirmada: o sticky não pode subir acima do containing block (o shell), que começa 16px abaixo do scrollport por causa do `p-4` do `CampaignContentScroll`. Qualquer fix no form isolado falha (clamp do containing block). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[CampaignContentScroll
max-md:has-[marcador]:pt-0] --> B[form sticky ancora em y=0
colado ao header] + B --> C[omnibox denso: min-h-8 py-1
X size-8 · ring só md] + C --> D[card: nome 18px tight · chips label-acima
avatares overlap row] + D --> E[e2e B196 + gates] +``` + +**Opções consideradas:** A | B | C +**Recomendação:** A — resolver o vão via `:has()` no scrollport (o scrollport "sabe" quando hospeda a barra sticky; o form carrega um marcador de classe estável). É a única correção que move o containing block do sticky para y=0 sem tocar o padding global nem as 11 páginas de lista. +**Rejeitadas:** + +- **B — `-mt-4` no form:** o sticky não sobe acima do containing block; com a borda do shell a 16px, o clamp continua em 16 (e o natural position vira 0 ≠ posição colada no scroll → barra "pula"). Medido na exploração: `formY=72` = header 56 + 16. +- **C — `pt-0` global no scrollport / remover `p-4`:** rabbit hole nomeado na intenção; destrói a folga do topo de todas as telas (dashboard, wizard…). O `:has()` escopa o `pt-0` só para páginas com o form de lista. + +### Componentes / mudanças + +- **`campaignListOmniboxFormClassName`** (`src/components/campaign/shared/CampaignListOmnibox.tsx`): adiciona o marcador `campaign-list-omnibox-form` ao bloco de classes existente (contrato do `:has()`). Barra: `py-2` → `py-1` no mobile (`md:py-0` preservado). Nada mais muda aqui — sticky/`-mx-4`/border-b ficam. +- **Campo do omnibox** (mesmo arquivo): `min-h-11` → `min-h-8`, `py-1.5` → `py-1` no mobile (`md:min-h-11 md:py-1.5` restauram o desktop); botão "Limpar" X `size-11` → `size-8` (span interno `size-7` → `size-6`, ícone `size-4` → `size-3.5`; `md:hidden` já é mobile-only); anel de foco `focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50` → escopado a `md:` (mobile sem anel/contorno de cor — caret é o indicador; contrato B184 borderless preservado). `text-sm` do input INTOCADO (contrato B183: 16px touch). + - Total da barra mobile: 32 (min-h-8) + 8 (py-1×2) + 1 (border-b) = **41px ≈ 40** ✓. Área de toque do X = 32px — aceitável porque o alvo real é o campo (o X vive dentro dele). +- **`CampaignContentScroll`** (`src/components/campaign/shell/CampaignQuickActionsHost.tsx`): `p-4` → adiciona `max-md:has-[.campaign-list-omnibox-form]:pt-0` (desktop `md:p-6` intocado). Efeito: só páginas com o form de lista perdem o padding-top no mobile; o shell (containing block do sticky) passa a começar em y=0 → barra cola no header e cards nunca passam por cima. Fallback se o arbitrary variant do Tailwind v4 não compilar: regra CSS pura em `styles.css` sob `@media (width < 48rem)` com o mesmo seletor. +- **Compensação das 6 listas com linha de ações antes do form** (`atividades`, `liderancas`, `apoiadores`, `demandas`, `dobradinhas`, `organizacoes` — verificadas na exploração): `pt-4 md:pt-0` no `div.flex.justify-end` que precede o form (respiro no topo que o `pt-0` removeu; desktop inalterado). `municipios`, `territorios`, `atualizacoes`, `pessoas`, `assessores` têm o form como primeiro filho visível — sem compensação. +- **`MunicipalityMobileCard`** (`src/components/campaign/municipality/MunicipalityMobileCard.tsx`): + - Nome: `

` ganha `text-lg leading-tight` (18px/22.5px vs 24px/32px atuais do `h3` global em `styles.css:371`); `gap-0.5` existente → território a ~2–4px do texto (linha-box enxuta é o que fecha o vão medido de ~13px). Badge "Cidade" e link stretch intactos. + - Chips: bloco `flex flex-col gap-0.5` por chip com `ChipLabel` acima do valor (gate B), substituindo o `inline-flex items-center` atual; linha de chips `items-start gap-x-3 gap-y-2` (1–2 linhas aceitas pelo gate); valores com `whitespace-nowrap` (nenhum chip quebra no meio). `chipTriggerClassName` vira o estilo de bloco (sem pílula de hover: `hover:bg-transparent`; `min-h-11` mantido para a área de toque; label+valor são o alvo do tap). A label "Classe" é renderizada no CALL SITE do card (``), não dentro de `TerritorialClassCardReadout` — o readout continua compartilhado com a tabela desktop intocado. Caso cidade: Tendência/Nível/Classe como blocos com os mesmos valores ("Não registrada", dash). `MissingAdvisorBadge` ganha wrap (`whitespace-normal h-auto`) para o pill não clipar na célula de ~110px do grupo (desktop segue uma linha). + - Densidade: article `gap-3` → `gap-2.5` e `p-4` → `px-4 py-3` (vertical denso; horizontal mantém o edge-to-edge B184; toques `min-h-11` dos controles intactos). +- **`MunicipalityRelationAvatarStack`** (`src/components/campaign/shared/MunicipalityRelationAvatarStack.tsx`): o modo `wrap` (B193) vira **overlap row dinâmica** — células `flex-1` com avatar fixo centralizado: poucos avatares → sem sobreposição (célula > 28px), muitos → sobreposição proporcional à contagem, sem wrap, sem cap, sem "…", todos visíveis (a linha ocupa exatamente a largura do grupo; casos patológicos de dezenas de avatares degradam para pilha — sr-only continua com todos os nomes). Renomear a prop `wrap` → `overlapRow` (honestidade da semântica; 2 call sites no card + 1 em `MunicipalityAdvisorAvatarStack`). Adicionar `data-view="relation-avatars"` na linha (hook de teste, precedente `data-view="mobile-cards"`). Desktop (`-space-x-2`, cap 3, tooltip) intocado. +- **`MunicipalityAdvisorAvatarStack`** (mesmo arquivo): passa `wrap` → `overlapRow` no modo denso (call site do card não-coordenador). +- **Migration:** sem migration (nenhuma mudança de schema/collection/global). +- **Access / Consent:** nenhum (zero paths de escrita tocados). +- **UI:** Impeccable B — encaixe na tela existente; shape (classes acima) → craft (browser, 390px) → critique (contrastes/vão/overlap) → polish. + +### Dados → forma + +N/A — nenhuma métrica nova; é densidade e alinhamento de superfície existente (intenção: "Vou apresentar dados? Não"). + +## Fases verificáveis + +1. **Tracer — chassis da barra** (maioria do appetite): marcador de classe + `:has()` no scrollport + densidade do campo/X/anel + compensação das 5 páginas. Verificar: `pnpm exec tsc --noEmit` + e2e B184/B183 existentes + novos e2e B196 (barra colada, altura ≤44, sem anel no foco). +2. **UI — card denso**: nome/território, chips label-acima, overlap row de avatares, densidade vertical. Verificar: e2e B193 existentes + novos e2e B196 (nome 18px, território colado, label acima do badge, avatares numa linha com overlap e sem overlap quando poucos). +3. **Gates**: `pnpm gate:fast` completo (tsc, lint, format, knip, cycles, unit+int, e2e do conjunto afetado) e `pnpm build`. + +## Rabbit holes / Não escopo (engenharia) + +- Não tocar `p-4`/`pb-*` do scrollport fora do `:has()` — o `pt-0` é escopado e é O fix do containing block. +- Não mudar `text-sm` do input (contrato B183), nem o desktop do omnibox (`md:*` restaura tudo). +- Não criar sistema de avatares novo — só o modo denso do componente existente. +- **Alinhamento dos avatares no grupo (adiado):** com poucos avatares a pilha fica centralizada no grupo (label à esquerda) — ancorar à esquerda mudaria o perfil de overflow para o grupo vizinho com 4+ avatares; revisitável se o product pedir (gatilho: review Impeccable do /simplify). +- Não polir o rodapé de atualização nem outras arestas do card (item separado na intenção). +- Não mexer em sheets, tooltips de desktop, busca/sugestões do omnibox. + +## Riscos e mitigação + +- **Tailwind v4 não compilar `max-md:has-[.campaign-list-omnibox-form]:pt-0`:** verificar no build da Fase 1; fallback = regra CSS pura em `styles.css` (`@media (width < 48rem)` + `.campaign-content-scroll:has(.campaign-list-omnibox-form)`). +- **e2e B193 regressar:** `card.getByText('Nível', { exact: true }).click()` — a label continua DENTRO do trigger (bloco label-acima), então o clique continua abrindo o sheet; `[data-view="mobile-cards"]` e classes de borda intactas. +- **Janela estreita-desktop (container < 48rem) vê o card densificado:** aceito — é o MESMO componente mobile (B193 já o estiliza com variantes `md:` no article); o desktop de verdade (tabela) não é tocado. +- **Listas com linha de ações antes do form** perdem o respiro no topo com o `pt-0`: mitigado pelo `pt-4 md:pt-0` nas 5 páginas; e2e de regressão no `/campanha/atividades` (barra colada após scroll). +- **`has-` casar páginas além das listas:** o marcador só existe em `campaignListOmniboxFormClassName`, usado exclusivamente pelos 11 `*Filters` (verificado por grep); wizard usa forms sem `role="search"`/marcador. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (vão zero, barra ~40px, sem anel, nome 18px+território colado, chips label-acima, avatares 1 linha sem cap/wrap/"…"; desktop intacto) +- [x] Invariantes AGENTS/engineering-standards (sem schema, sem access, sem Consent; nomes de props em inglês; copy pt-BR preservada) +- [x] Testes previstos: e2e B196 (barra colada + altura + anel + nome/território + labels + overlap de avatares) + regressão B184/B183/B193 existentes; sem unit/int novos (mudança puramente de superfície) + +Self-score decision-quality: 5/5 (decisões caras com rejeitadas; cabe no appetite; rabbit holes nomeados; reusa chassis existente — zero módulos novos; intenção preservada) diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx index e8d8ec62..1fab1eef 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx +++ b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx @@ -84,7 +84,7 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro return ( -
+
{isCampaignCoordinator(user) ? ( ) : null} @@ -365,12 +368,11 @@ export const CampaignListOmnibox = ({ ) : null}
-
+ {/* C100 — the trailing cluster is desktop-only: below `md` the page + registers its own header controls, so the cluster never joins the + mobile sticky bar (an empty container would also inflate its height + — B196 keeps the bar ≈ 40px). */} +
{trailing} {onClearAll && hasChips ? (