diff --git a/docs/plans/colunas-responsivas-municipios.md b/docs/plans/colunas-responsivas-municipios.md index ed09c416..732f3285 100644 --- a/docs/plans/colunas-responsivas-municipios.md +++ b/docs/plans/colunas-responsivas-municipios.md @@ -1,191 +1,270 @@ -# Colunas responsivas na tabela de Municipios (sem scroll horizontal) +# B158 — Colunas adaptativas na tabela de Municípios -Status: rascunho -Atualizado em: 2026-08-04 +Status: rascunho revisado +Atualizado em: 2026-08-05 Issue: #364 Priority: P1 Model: composer-2.5 -Impeccable: B — encaixe em `CampaignTable` / `MunicipalityList` (desktop/tablet); sem rota nova -Canvas UI: /Users/francisco.solla/.cursor/projects/Users-francisco-solla-cursor-worktrees-teqo-5hoy/canvases/plan-b158-ui-draft.canvas.tsx -Appetite: ~1-1,5 dia eng; um outcome verificavel -Responsavel: -- +Impeccable: Adapt / modo Operate — densidade progressiva dentro do painel de conteúdo +Canvas UI: `/Users/francisco.solla/.cursor/projects/Users-francisco-solla-cursor-worktrees-teqo-5hoy/canvases/plan-b158-ui-draft.canvas.tsx` (referência histórica; o contrato abaixo prevalece) +Appetite: ~1,5–2 dias eng; um outcome verificável +Responsável: — -## Intencao +## Intenção -A tabela de Municipios em `/campanha/municipios` tem 10+ colunas no modo staff. Em tablets e laptops de campo (resolucao ~1366x768 ou menor, comum na mesa do assessor), a tabela **estoura a viewport** e forca scroll horizontal. Coordenadores e assessores relatam que o scroll horizontal atrapalha a leitura da fila -- diferentemente do scroll vertical, que e natural, o horizontal quebra a varredura rapida de "quem esta no topo / o que atacar". +A tabela de Municípios em `/campanha/municipios` tem 10+ colunas no modo staff. O espaço útil não depende mais apenas da viewport: a página pode dividir a largura entre **sidebar, painel de conteúdo e chat**, cada um aberto ou fechado. Breakpoints de tela não representam o espaço que a tabela realmente recebeu. -O seletor manual de colunas (B17) existe, mas exige que o ator **decida** quais colunas esconder -- uma decisao que a maioria nao toma, ou toma uma vez e esquece. A tela deveria se adaptar sozinha. +A lista deve responder à largura do **painel de conteúdo**, mostrar primeiro as colunas que sustentam a decisão e acrescentar as demais uma a uma conforme houver espaço — sem scroll horizontal e sem exigir configuração prévia do ator. -Alem disso, varias colunas tem largura ditada pelo **titulo do header** (ex.: "Votos estimados" ocupa mais espaco que os numeros da coluna), e nao pelo conteudo. Em telas estreitas, cada pixel conta. +O seletor manual de colunas (B17) continua existindo. Ele expressa preferência; as container queries expressam capacidade física do painel. -**Decisoes de produto acumuladas (gate 2026-08-04):** +## Decisões de produto consolidadas -1. **Territorio na 2a linha do nome** -- a coluna Territorio deixa de existir como coluna independente. O territorio aparece como segunda linha discreta abaixo do nome do municipio, na mesma celula. -2. **"Sem nivel" vira "--"** -- mesmo padrao das colunas 2022 e Classe para ausencia de dado. -3. **Headers no singular + renomeios:** "Assessores" -> "Assessor", "Liderancas" -> "Lideranca", "Dobradinhas" -> "Dobradinha", "Votos estimados" -> "2026" (com tooltip explicando: "Estimativa de votos do candidato neste municipio em 2026"), "Ultimo sinal" -> "Sinal". -4. **Cobertura e Sinal ocultos por padrao** -- colunas de leitura derivada. Comecam pre-povoadas no `hiddenColumnIds` do cookie B17; o ator que quiser ve-las marca no seletor. -5. **Prioridade de colunas por breakpoint** -- CSS-only, sem JS de resize. Mapa de prioridade P0 (sempre visivel) a P5 (opt-in). -6. **Coluna Acoes dinamica** -- nao aparece no seletor B17. Visivel apenas quando **Tendencia OU Sinal** estao ocultos (por breakpoint ou escolha manual). Se ambos estao visiveis, a coluna Acoes some completamente. - - Tendencia oculta -> icone de Tendencia na coluna Acoes (cor do campo Tendencia: favoravel/neutra/desfavoravel) - - Sinal oculto -> icone de adicionar sinal na coluna Acoes - - Ambos ocultos -> ambos os icones +1. **Container queries, não media queries.** O wrapper da lista vira um named container. Abrir ou fechar sidebar/chat recalcula as colunas sem JavaScript de resize. +2. **Território na segunda linha do nome.** A coluna `region` deixa de existir. `TerritoryLink` aparece sob o nome do município, em texto secundário e truncado. +3. **Filtro de território preservado no omnibox.** `MunicipalityFilters` já oferece sugestões e chips de território; essa passa a ser a entrada canônica. Remover a coluna não remove `regionFilterOptions`, `?region=` nem o estado do filtro. +4. **Sort legado por território preservado.** `sort=region` continua válido por compatibilidade de URL e aparece no resumo quando ativo, mas deixa de ter um header clicável. Não será criada uma segunda affordance de sort neste item. +5. **Ausência de nível vira `—`.** O filtro `sem_nivel` continua sendo uma resposta operacional real; somente a célula usa o padrão visual de ausência. +6. **Headers curtos:** `Assessor`, `Liderança`, `Dobradinha`, `2026`, `Sinal`. O header `2026` explica a estimativa no tooltip B22. +7. **Cobertura e Sinal ocultos por padrão.** Novos estados de preferência começam com `goalCoverage` e `lastSignal` ocultos; ambos continuam disponíveis no seletor. +8. **Densidade progressiva.** Colunas entram individualmente conforme o container cresce. `Tendência` e `Sinal` são adaptativas: usam ícone compacto quando não cabe o readout completo, sem montar um segundo editor. +9. **Ações cobre apenas ocultação manual.** Se o ator ocultar `trend` ou `lastSignal`, a coluna interna `actions` monta o editor correspondente. Container query nunca duplica o controle: uma coluna adaptativa compacta no próprio lugar. +10. **Sem fallback horizontal.** A tabela não usa `overflow-x-auto`; cards continuam sendo a superfície do container realmente estreito. -## Persona e fluxo +## Persona e job -- **Persona / contexto:** Coordenador-geral ou assessor em tablet/laptop de campo (~1366x768), no calor da operacao, alternando entre a lista e o detalhe de municipio. Precisa escanear rapidamente, nao configurar tabela. -- **Job principal:** Ver as colunas de decisao (nome+territorio, 2022, 2026, nivel, classe) sem scroll horizontal. Quando uma coluna recolhe por falta de espaco, o icone de acao correspondente aparece na coluna Acoes para manter o caminho de escrita acessivel. -- **Fluxo desejado:** - 1. Abre `/campanha/municipios` em qualquer tela >= 1024px. Tabela sem scroll horizontal. - 2. Coluna Acoes aparece no extremo direito **apenas se** Tendencia ou Sinal estiverem ocultos. - 3. Em tela larga com ambas visiveis: coluna Acoes nao renderiza. - 4. Ao estreitar a tela e Tendencia recolher (P3): icone de Tendencia aparece na coluna Acoes, com a cor do status atual. - 5. Ao estreitar mais e Sinal recolher (P5): icone de Sinal aparece na coluna Acoes. - 6. Ambos os icones abrem o mesmo popover que as colunas correspondentes usam hoje. - 7. Nomes longos quebram em ate 2 linhas; territorio em 2a linha truncada. - 8. Headers quebram em 2 linhas quando reduz a largura da coluna (so em whitespace). -- **Anti-goals de produto:** - - Nao e substituir o seletor manual de colunas (B17) -- ele continua disponivel. - - Nao e "table-fixed" com larguras CSS arbitrarias. - - Nao e truncar dados numericos ou badges. - - Nao e voltar ao scroll horizontal como fallback. - - Acoes nao aparece no seletor B17 (nao e uma coluna que o ator controla). +- **Persona:** coordenador, candidato ou assessor usando tablet/laptop de campo, com sidebar e/ou chat alternando o espaço disponível. +- **Job:** escanear nome, base de 2022, estimativa de 2026, nível e classe; reconhecer tendência e manter ações rápidas acessíveis; receber mais contexto de rede conforme o painel cresce. +- **Princípio:** adaptar ao espaço que o conteúdo recebeu, não ao dispositivo que o hospeda. -### Esboco de fluxo +## Fluxo desejado + +1. O painel de conteúdo abre estreito: a lista usa cards. +2. Ao ganhar largura suficiente, entra a tabela essencial. +3. Cada faixa adicional introduz uma coluna, sem salto de três ou quatro colunas de uma vez. +4. Abrir o chat ou a sidebar faz o processo inverso imediatamente via CSS. +5. `Tendência` e `Sinal`, quando escolhidos no B17, encolhem para um trigger de ícone antes de desaparecerem. +6. Se o ator os ocultar manualmente, seus triggers migram no render do servidor para `Ações`, no extremo direito. +7. O filtro de território continua disponível no omnibox e seus chips continuam refletindo `?region=`. + +## Contrato entre preferência e capacidade + +A regra efetiva é: ```text -Desktop largo (>=1400px) -- default: Tendencia visivel, Sinal oculto -> Acoes visivel (icone Sinal) - [Nome+Territorio | 2022 | 2026 | Assessor | Lideranca | Dobradinha | Tendencia | Nivel | Classe | (S)] - -> 10 colunas; (S) = icone de Sinal na coluna Acoes - -Desktop largo (>=1400px) -- com Sinal habilitado no seletor, Tendencia visivel -> Acoes some - [Nome+Territorio | 2022 | 2026 | Assessor | Lideranca | Dobradinha | Tendencia | Nivel | Classe | Sinal] - -> 10 colunas; Acoes oculta (Tendencia e Sinal ambos visiveis) - -Tablet (~1200px) -- Tendencia recolhe (P3), Sinal oculto -> Acoes visivel (icones Tendencia + Sinal) - [Nome+Territorio | 2022 | 2026 | Assessor | Lideranca | Dobradinha | Nivel | Classe | (T)(S)] - -> 9 colunas; (T) = icone Tendencia colorido, (S) = icone Sinal - -Laptop estreito (~1024px) -- P1 e P3 recolhem, P0 + P4 visiveis - [Nome+Territorio | 2022 | 2026 | Nivel | Classe | (T)(S)] - -> 6 colunas - -Mobile (< 1024px) - -> Cards (inalterado) +coluna renderizada = permitida pelo papel + AND não ocultada manualmente + AND faixa do container permite a coluna ``` +Exceções: + +- `name` é obrigatória. +- `trend` e `lastSignal`, quando manualmente visíveis, permanecem renderizados na tabela e alternam **compacto ↔ completo** conforme o container. +- Se `trend` ou `lastSignal` estiver manualmente oculto, seu editor é renderizado uma única vez dentro de `actions`. +- Mostrar manualmente uma coluna não força o painel a ultrapassar sua capacidade. O automático vence apenas na dimensão de espaço; a preferência continua guardada para quando houver largura. + +## Matriz canônica por largura do container + +Os valores abaixo são **breakpoints iniciais do painel de conteúdo**, não da viewport. Devem ser calibrados no craft contra o conteúdo real, mas a ordem de entrada é contrato de produto. Uma calibração pode mover um corte; não pode agrupar colunas nem trocar a prioridade sem atualizar este plano. + +| Faixa do container | Superfície / coluna que entra | Estado acumulado | +| ------------------- | ----------------------------- | ----------------------------------------------------------------------------------- | +| `< 48rem` (< 768px) | Cards | Cards existentes; tabela ausente | +| `C0 >= 48rem` | Tabela essencial | Nome+Território, 2022, 2026, Nível, Classe, Tendência compacta, Ações se necessária | +| `C1 >= 54rem` | Assessor | C0 + Assessor | +| `C2 >= 60rem` | Tendência completa | C1; trigger troca ícone por badge/readout no mesmo controle | +| `C3 >= 66rem` | Liderança | C2 + Liderança | +| `C4 >= 72rem` | Dobradinha | C3 + Dobradinha, somente coordinator/candidate | +| `C5 >= 78rem` | Cobertura opt-in | C4 + Cobertura, se habilitada no B17 | +| `C6 >= 84rem` | Sinal completo opt-in | C5; Sinal, se habilitado, troca ícone por readout de idade | + +### Ordem visual estável + +Quando todas estão disponíveis: + +1. `name` — Município + Território +2. `votos` — 2022 +3. `expectedVotes` — 2026 +4. `level` — Nível +5. `classe` — Classe +6. `advisors` — Assessor +7. `trend` — Tendência +8. `leaderships` — Liderança +9. `stateDeputies` — Dobradinha, quando o papel permite +10. `goalCoverage` — Cobertura, opt-in +11. `lastSignal` — Sinal, opt-in +12. `actions` — somente quando Tendência e/ou Sinal foram ocultos manualmente + +A ordem prioriza diagnóstico/decisão, depois responsabilidade e rede. A tabela não muda a ordem ao cruzar faixas; apenas acrescenta ou compacta células. + +## Colunas adaptativas e montagem única + +### Tendência + +- Manualmente visível: + - C0–C1: um trigger de ícone com cor semântica do status atual. + - C2+: o mesmo `MunicipalityListTrendControl` mostra o badge completo. +- Manualmente oculta: a coluna `trend` não é renderizada e uma única instância do controle aparece em `actions`. +- Estado sem tendência usa ícone neutro e nome acessível “Tendência não registrada”. + +### Sinal + +- Manualmente visível: + - C0–C5: o mesmo `MunicipalityListSignalControl` usa trigger de adicionar/atualizar sinal. + - C6+: troca para `SignalAgeReadout` no mesmo controle. +- Manualmente oculto: uma única instância aparece em `actions`. + +### Ações + +- ID interno: `actions`. +- Não entra em `MunicipalityListColumnId`, `municipalityColumnLabels`, `municipalityColumnDescriptions` nem no picker B17. +- Usa um tipo local, por exemplo `MunicipalityTableColumnId = MunicipalityListColumnId | 'actions'`. +- Possui header visualmente oculto “Ações” para manter a semântica da tabela. +- Só entra no DOM quando `trend` ou `lastSignal` está manualmente oculto. +- Os triggers têm nome acessível, tooltip redundante e alvo mínimo de 44×44px. +- Nunca existem duas instâncias do mesmo editor na mesma linha. + +## Defaults do B17 sem ambiguidade + +O cookie atual omite uma lista cujo conjunto de ocultas é vazio. Portanto, “nunca configurou” e “restaurou todas” são indistinguíveis. B158 precisa evoluir o formato antes de aplicar defaults. + +### Contrato escolhido + +- O formato passa a representar explicitamente uma lista configurada com zero ocultas, usando um sentinel reservado e versionado pelo parser/serializer. +- Exemplo conceitual: `municipios:__none__` significa “configurado; todas visíveis”. O sentinel nunca é exposto como column ID. +- Ausência da entrada `municipios` significa “usar defaults atuais da lista”. +- Defaults de Municípios: `['goalCoverage', 'lastSignal']`. +- `Restaurar todas` persiste o estado explícito vazio; não volta aos defaults no refresh. +- Cookies legados sem entrada de Municípios adotam o novo preset compacto uma vez. Após a primeira interação, o estado fica explícito. +- As outras listas mantêm default vazio e comportamento existente. +- O controle continua sendo preferência por dispositivo/browser, não por usuário autenticado. + ## Objetivo e aceite -- **Tabela sem scroll horizontal:** em qualquer viewport >= 1024px, `containerClassName` deixa de ser `overflow-x-auto` e a tabela nao produz barra de rolagem horizontal. -- **Largura segue o conteudo:** colunas com dados curtos (ex.: "2022" com numero de 1-3 digitos + badge, "Nivel" com badge N0-N4) ocupam ~o espaco do dado, nao do titulo do header. -- **Headers com quebra de linha:** onde o titulo do header for mais largo que os dados, o header quebra em 2+ linhas (so em whitespace, nunca no meio de palavra). -- **"Sem nivel" vira "--"** na coluna Nivel -- mesmo padrao das colunas 2022 e Classe para ausencia de dado. -- **Headers no singular + renomeios:** "Assessor", "Lideranca", "Dobradinha", "2026", "Sinal". O header "2026" tem tooltip (B22): "Estimativa de votos do candidato neste municipio em 2026." -- **Territorio na 2a linha do nome:** a coluna `region` deixa de existir. Na celula `name`, abaixo do nome do municipio, o territorio aparece em texto menor e tom secundario. O link para o territorio (`TerritoryLink`) migra para essa segunda linha. -- **Cobertura e Sinal ocultos por padrao:** pre-povoados no `hiddenColumnIds` do cookie B17. O ator habilita manualmente no seletor. -- **Coluna Acoes dinamica (id: `actions`):** - - NAO aparece no seletor B17 (nao e uma coluna que o ator controla). - - Renderiza apenas quando **Tendencia ou Sinal** estao ocultos (por breakpoint ou escolha manual B17). - - Se ambos estao visiveis, a coluna Acoes nao renderiza (nem aparece no DOM). - - Icone de Tendencia: mesma cor do status atual (verde = favoravel, cinza = neutra, vermelho = desfavoravel). Abre o popover de `MunicipalityListTrendControl`. - - Icone de Sinal: icone padrao de adicionar. Abre o popover de `MunicipalityListSignalControl`. - - Ambos os icones coexistem na mesma celula quando ambos estao ocultos. -- **Prioridade de colunas por breakpoint:** CSS-only. Mapa P0-P5 (ver tabela abaixo). -- **Nome com largura maxima:** coluna `name` com `max-w-52` (~208px); nomes quebram em 2 linhas. Territorio truncado em 1 linha. -- **Coluna `name` mantem sticky left.** -- **Seletor B17 preservado:** o manual vence o automatico. Coluna Acoes nao aparece no seletor. -- **Sem regressao no mobile:** cards `< md` seguem inalterados. -- **Guardrails:** sem migration, sem collection, sem Consent, sem server action; sem mudanca no contrato de URL; sem novo cookie. +- A tabela responde à largura do named container do painel de conteúdo, inclusive quando sidebar/chat abrem ou fecham sem mudar a viewport. +- Não há barra de rolagem horizontal nem overflow do shell em nenhuma faixa de tabela. +- A transição cards ↔ tabela é baseada no container em 48rem. +- As colunas entram uma por vez na ordem C0–C6. +- `name` continua sticky left e passa a ter largura máxima aproximada de 13rem; nome quebra em até duas linhas e território trunca em uma. +- Headers podem quebrar apenas em whitespace; números e badges não truncam. +- Território continua filtrável no omnibox; chips e URLs `?region=` permanecem intactos. +- URLs legadas com `sort=region` continuam válidas. +- `Nível` sem valor renderiza `—`. +- Headers e labels usam singular; `2026` tem tooltip: “Estimativa de votos do candidato neste município em 2026. Ordena pelo cenário central, independente do cenário selecionado acima.” +- Cobertura e Sinal começam ocultos nos defaults de Municípios. +- `actions` não aparece no picker e só existe no DOM por ocultação manual de Tendência/Sinal. +- Cada editor de Tendência/Sinal é montado no máximo uma vez por linha. +- Cards mantêm conteúdo e interação atuais; somente o gatilho cards/tabela muda de media query para container query. +- Sem migration, collection, Consent, server action ou mudança de contrato público de URL. -## Dados (intencao) +## Direção no codebase -- **Vou apresentar dados?** Nao -- **Dados: N/A**. Layout/CSS de colunas existentes. -- **Decisoes desbloqueadas:** Nenhuma decisao eleitoral nova. -- **Forma:** _adiada ao plano de implementacao_. +### `MunicipalityList.tsx` -## Mapa de prioridade das colunas (canonico) +- Envolver cards + tabela em `@container/municipality-list`. +- Trocar `md:hidden` / `md:block` por variantes do named container. +- Remover a definição `region`; mover `TerritoryLink` para a segunda linha de `name`. +- Reordenar as definições conforme a ordem visual canônica. +- Aplicar classes de container query simétricas ao `