mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
Merge pull request #442 from fsolla/B169-estabilizar-a-lista-de-dobradinhas-parar-o-flicker-de-l
B169 — Estabilizar a lista de Dobradinhas (parar o flicker de linhas/colunas)
This commit is contained in:
commit
031521b4f5
4 changed files with 148 additions and 3 deletions
|
|
@ -10,6 +10,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
**Recently resolved (2026-08-08):** **C90** (Issue #426) — os três campos de responsável da Atividade (`responsible` Contact, `advisors` staff, `leadership`) viraram **um campo `responsible` polimórfico multi-valor** (`campaignUser` staff · `leadership` · `stateDeputy`), removendo os dois antigos do modelo/UI; assessor continua vendo/curando o compromisso onde está listado como responsável via notação-objeto `equals {relationTo,value}` no access (provado por int test com `payload.find` real); seletor `ResponsibleMultiSelect` (chips tipados) + busca única `searchActivityResponsibleOptions` (escopo de leitura do ator, liderança engajada) — reusado pelo C91 na agenda; migração esquema+dados (advisors/leadership → `responsible`, Contact antigo descartado, contagem via NOTICE; SQL de reconciliação ensaiado contra cenário misto); busca `q` da lista virou título-only (regressão assumida no gate); notificação de atenção notifica só staff em `responsible`. Intenção [`remodelar-responsaveis-atividade.md`](docs/plans/remodelar-responsaveis-atividade.md); impl [`remodelar-responsaveis-atividade-impl.md`](docs/plans/remodelar-responsaveis-atividade-impl.md).
|
||||
|
||||
**Recently resolved (2026-08-08):** **B169** (Issue #425) — fim do flicker de linhas/colunas na lista de Dobradinhas (e nas demais listas com células de chips: Municípios, Lideranças, Assessores): o ciclo "medir → recolher" de `RelationChipCell` passou de `useEffect` (pós-pintura) para `useLayoutEffect` (antes da paint). Antes, cada remount (navegação RSC, paginação, ordenação, filtro) pintava o clamp `max-h-18` (72px) e só então colapsava para o slice de 3 linhas — a tabela ia 1122→682px numa onda de LayoutShift por célula (medido `0.021×6` por navegação). Agora a primeira pintura já é a altura final; medido durante ordenação: tabela estável em 682px, zero LayoutShift das células. Resíduo documentado: o load SSR inicial ainda pinta o estado medindo (pré-hidratação) — `useLayoutEffect` não alcança essa paint; estimar largura no servidor foi rejeitado (reintroduziria a estimativa por label que o código evita). Sem migration, dados, access ou mudança visual. Impl [`estabilizar-lista-dobradinhas-flicker-impl.md`](docs/plans/estabilizar-lista-dobradinhas-flicker-impl.md).
|
||||
|
||||
**Recently resolved (2026-08-08):** **worktree next provisiona ambiente isolado por worktree** — agentes em paralelo não disputam mais porta 3000, o `teqo_test` compartilhado nem um segundo Postgres no 5432: `pnpm worktree next [--go] [--no-migrate]` deriva deterministicamente slot do código da Issue (C15→15; hash sem dígitos; bump em colisão entre worktrees vivos) → dev server `3100+slot`, bancos `teqo_wt<slot>`/`teqo_wt<slot>_test` criados no container compartilhado com migrations aplicadas; escreve `.env.local` + `.env.test.local` (gitignored, marcador gerado; envs manuais nunca sobrescritos) e copia PAYLOAD*SECRET/Blob/VAPID do main. `kill` dropa os bancos gerados (best-effort, WITH FORCE). Suites leem `.env.test.local` depois de `.env.test` (vitest.setup/playwright/db-doctor/gate-ci espelham o layering); `assertTestDatabase` aceita `teqo_test` ou `teqo*<worktree>\_test`em localhost só (fail-closed preservado).`db:start`/`db:stop`fixam`-p teqo`(um container, qualquer diretório); max_connections 200→400. Sem Docker (Cursor Cloud): degrada ao compartilhado com aviso. Derivação pura`scripts/lib/worktree-env.mjs` (unit-tested).
|
||||
|
||||
**Recently resolved (2026-08-08):** **B164** (Issue #400) — barra de navegação inferior fixa no mobile para staff, com 5 itens (Início, Municípios, Atualizações, Agenda, Mais) e drawer overflow "Mais" para destinos secundários (Quadro, Territórios, Organizações, Líderanças, etc.) + rodapé Perfil/Sair. Leader e desktop mantêm sidebar-only (lockdown intacto). `CampaignContentScroll` e `CampaignQuickActionsHost` ajustados para safe-area e z-index do FAB sobre a barra. `/campanha/atualizacoes` é placeholder honesto (âncora C89). Impl [`barra-navegacao-inferior-mobile-impl.md`](docs/plans/barra-navegacao-inferior-mobile-impl.md).
|
||||
|
|
|
|||
92
docs/plans/estabilizar-lista-dobradinhas-flicker-impl.md
Normal file
92
docs/plans/estabilizar-lista-dobradinhas-flicker-impl.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Impl: Estabilizar a lista de Dobradinhas (parar o flicker de linhas/colunas)
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-08
|
||||
Issue: #425
|
||||
Intenção: docs/plans/estabilizar-lista-dobradinhas-flicker.md
|
||||
Appetite restante: ~0,5–1 dia eng (herdado)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** a lista de Doradrinhas (`/campanha/dobradinhas`) deixa de "piscar": linhas (e a sensação de colunas) mudam de tamanho em onda após a primeira pintura, a cada carga, paginação, ordenação ou filtro. A tabela deve aparecer já no tamanho final; "Ver mais…" mantém o recolhimento/expansão sob demanda.
|
||||
- **O que NÃO negociar:** não redesenhar as células de chips; não mudar a regra de recolher em 3 linhas; não mexer em dados/permissões/URLs/schema; corrigir no dono compartilhado das células de chips (`RelationChipCell`) para valer nas demais listas (Municípios, Lideranças, Assessores) sem regressão.
|
||||
- **O que reavaliar:** a intenção assumia que o ciclo "medir → recolher" era o culpado e que a forma de corrigir estava "no dono compartilhado". Investigação confirmou a causa raiz empírica — mas a correção tem um resíduo no load SSR que a intenção não previu (ver "Abordagem recomendada" e "Riscos").
|
||||
|
||||
## Causa raiz (confirmada por medição no browser, porto 3269 / DB `teqo_wt169`)
|
||||
|
||||
`RelationChipCell` (src/components/campaign/shared/RelationChipCell.tsx) resolve o overflow fazendo a **mediação num `useEffect` (pós-pintura)**:
|
||||
|
||||
1. Estado inicial/SSR: `visibleChipCount === null` → `measuring = true` → monta **TODOS os chips** (até 192 no seed sintético) com `max-h-18 overflow-hidden` (clamp de 72px). A linha nasce com ~98px e a tabela com ~1122px.
|
||||
2. `useEffect` roda **depois da paint** → mede quantos cabem em 3 linhas → `setVisibleChipCount`.
|
||||
3. Re-render: `visibleChips = chips.slice(0, visibleChipCount)`, `clamping = false` → linha colapsa para ~58px, tabela para ~682px. **Reflow + LayoutShift real.**
|
||||
|
||||
Medições (PerformanceObserver `layout-shift`, `hadRecentInput=false`):
|
||||
|
||||
- **Load original:** shifts `[0.139, 0.215, 0.129, 0.021×6]`; tabela `1122→682px`; linhas `98→58px`; 192 chips montados no estado medindo.
|
||||
- **Navegação original (ordenar por partido):** shifts `[0.215, 0.128, 0.021×6]` — a "fila" de 0.021 é **uma onda por célula** ajeitando a altura.
|
||||
- **Probe `useLayoutEffect` (navegação):** a fila `0.021×6` **some**; frames da tabela ficam estáveis em `682px`/`58px` — nenhum frame intermediário de 1122px. (Os shifts residuais 0.193/0.043/0.021 são do shell, não da tabela.)
|
||||
- **Probe `useLayoutEffect` (load/reload):** ainda `1122→682px` — porque o **SSR envia o HTML no estado medindo** (todos os chips) e o browser pinta isso antes da hidratação; o `useLayoutEffect` só roda após.
|
||||
|
||||
Conclusões:
|
||||
|
||||
- **Navegações client-side (paginação, ordenação, filtros)** — o sintoma que "se repete a cada…" — são 100% resolvidas movendo a mediação para `useLayoutEffect` (medição síncrona antes da paint no remount RSC).
|
||||
- **Load SSR inicial** ainda produz um único shift da paint pré-hidratação com o fix apenas de `useLayoutEffect`.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Medição em useLayoutEffect<br/>antes da paint no cliente] --> B[Remount RSC pinta colapsado<br/>navegações 100% estáveis]
|
||||
C[SSR medindo envia todos os chips] --> D{Load inicial}
|
||||
D -->|aceitável| E[1 shift único pré-hidratação]
|
||||
```
|
||||
|
||||
**Opções consideradas:**
|
||||
|
||||
- **A — `useLayoutEffect` no ciclo de mediação** (os efeitos de measurar/invalidar de `RelationChipCell`). Simples, dono compartilhado, resolve o caso dominante (paginação/ordenação/filtro) por completo.
|
||||
- **B — `useLayoutEffect` + pré-render do estado colapsado no servidor** (estimativa determinística do número de chips que cabem em 3 linhas, usando a largura máxima conhecida das colunas `max-w-64/72` e comprimento dos labels). Zeraria também a paint SSR, mas reintroduz **exatamente** a estimativa por largura que o código deliberadamente evita ("read from the real layout instead of estimated from label lengths") — risco de divergência visual na última linha.
|
||||
- **C — reserva de altura estável no estado medindo** (clamp igual ao colapsado). Testada empiricamente como variação de probe (`clamping` persistente com `hasHiddenChips`): **não** estabilizou o load (o `max-h` é teto, não piso — o slice colapsado é naturalmente mais baixo).
|
||||
|
||||
**Recomendação: A**, com o resíduo do load SSR documentado e tratado pelo `useLayoutEffect` + a medição já existente no `ResizeObserver` cobrindo ajustes pós-layout. A se o produto exigir (ver "Riscos") pode ir como follow-up, **não** nesta entrega (appetite).
|
||||
|
||||
**Rejeitadas:** B (estimativa de texto), C (empiricamente falhou), e qualquer mudança de layout/regra de colapso (anti-goal da intenção).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`RelationChipCell`** (src/components/campaign/shared/RelationChipCell.tsx): trocar os `useEffect` de medição e invalidação (chipsKey, measuring, ResizeObserver de largura) por `useLayoutEffect`, para que a mediação aconteça **antes** da primeira paint do componente no cliente. Mantém-se `useEffect` para effects não-layout (blur debounce, reconciliações de estado puro). Adição mínima: import `useLayoutEffect`; troca de `useEffect` → `useLayoutEffect` nos 3 effects de medição (385–403 e 410–449, linhas de referência atuais).
|
||||
- Detalhe de correção: como hoje o `useEffect` de `invalidateMeasurement` (chipsKey) roda pós-paint, mover para layout effect garante que a re-medida provocada por mudança de conteúdo também aconteça antes da paint no remount.
|
||||
- **Migration:** sem migration (nenhuma mudança de schema/dados).
|
||||
- **Access / Consent:** toca nenhum.
|
||||
- **UI:** Impeccable B — estabilidade de lista existente; estados visuais e copy preservados; shells de lista (`CampaignTable`) intocados.
|
||||
|
||||
### Dados → forma
|
||||
|
||||
- N/A — não há dados novos; aceite mensurável por observação (CLS da carga/navegação ≈ 0 atribuível às células, validado via PerformanceObserver no browser durante a execução).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Tracer / server+UI** — mudança pontual em `RelationChipCell` (useLayoutEffect na medição). Quota do appetite: ~30 min.
|
||||
2. **Verificação no browser** — dev server do worktree; logar como coordenador seed; `PerformanceObserver('layout-shift')` em `/campanha/dobradinhas`:
|
||||
- Navegação client-side (ordenar por partido, paginar, filtrar): nenhuma mudança de altura da tabela (frames estáveis 682px/58px) — **é o gate principal**.
|
||||
- Load/reload: shift residual documentado (único, pré-hidratação).
|
||||
- Confirmar nas outras listas que usam as células (Municípios, Lideranças, Assessores) sem regressão visual.
|
||||
3. **Gates** — `pnpm gate:fast`; `pnpm test` (unitários das células não devem quebrar: `measureOverflow` já é stubável); `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`; `pnpm build` local. Push via `pnpm push`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Zerar a paint SSR inicial com estimativa de largura (opção B) — postergado como follow-up; o `useLayoutEffect` + `ResizeObserver` existente já cobrem o layout real.
|
||||
- Ajustar `COLLAPSED_CHIP_ROWS` / `max-h-18` — não muda a regra de colapso (anti-goal).
|
||||
- Tratar fonts-cls / sidebar / Sollinha — outras fontes de shift de layout, fora do escopo (o plano de intenção já as separa).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **`useLayoutEffect` em SSR:** é inofensivo (não roda no servidor); o componente já é `'use client'`. Os testes unitários existentes (`municipalityPortfolioCell`, `leadershipStateDeputyRelationCell`, `stateDeputyAdvisorRelationCell`) stubbam `ResizeObserver` e não medem layout real — não devem quebrar; confirmar no gate.
|
||||
- **Medição antes da paint pode custar um layout sync em massa** (centenas de chips) no primeiro render. O código atual já mede com `getBoundingClientRect` de todos os chips igualmente; a mudança só move o _timing_ — sem custo novo.
|
||||
- **Divergência da intenção:** a intenção assumia que o ciclo medir→recolher "acontecia depois da primeira pintura" e que resolver no dono bastaria para tudo; a medição mostrou que no **load SSR** há uma pintura do estado medindo antes da hidratação que `useLayoutEffect` não alcança. Mantemos a correção no dono (cobre o que se repete) e documentamos o resíduo do load; não inventamos produto novo.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto (onda de linhas/colunas em paginação/ordenação/filtro eliminada; "Ver mais…" intacto; demais listas sem regressão)
|
||||
- [ ] Invariantes AGENTS/engineering-standards (nada de schema/access/URLs; identificadores em inglês)
|
||||
- [ ] Testes de domínio previstos: unitários das células continuam verdes; verificação CLS por PerformanceObserver no browser registrada no diff/entrega
|
||||
- [ ] Cobertura multi-lista validada (Municípios, Lideranças, Assessores) no browser
|
||||
43
docs/plans/estabilizar-lista-dobradinhas-followup.md
Normal file
43
docs/plans/estabilizar-lista-dobradinhas-followup.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# B170 — Follow-up B169: contrato do colapso de chips + shift único no load SSR
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-08-08
|
||||
Issue: #441
|
||||
Priority: P2
|
||||
Model: composer-2.5
|
||||
Model-local: deepseek-v4-flash-high
|
||||
Impeccable: A — engenharia (teste de contrato + resíduo de first-paint), sem mudança de superfície
|
||||
Appetite: ~0,5–1 dia eng
|
||||
Responsável: —
|
||||
|
||||
## Intenção
|
||||
|
||||
Follow-up do B169 (#425, entregue). Duas lacunas registradas na triage de débitos daquela entrega, na mesma superfície (`RelationChipCell` / listas de chips de `/campanha`):
|
||||
|
||||
1. **Contrato do colapso sem teste** — nenhum spec cobre "Ver mais…"/"Ver menos"/`visibleChipCount` parcial com layout real (specs atuais usam `measureOverflow: false` ou colapsam para 1 chip). A mudança de timing do B169 (medição em `useLayoutEffect`) não tem regressão unitária do comportamento de colapso.
|
||||
2. **Shift único no load SSR** — `useLayoutEffect` elimina a onda de LayoutShift nas navegações client-side (caso dominante), mas o carregamento inicial (SSR) ainda pinta o estado medindo (todos os chips, clamp `max-h-18`, ~1122px) antes da hidratação e só então colapsa (~682px) — um LayoutShift único pré-hidratação.
|
||||
|
||||
## Objetivo e aceite
|
||||
|
||||
- F1: um spec unitário pinando o contrato do colapso (recolher para N chips/3 linhas, "Ver mais…" expande, "Ver menos" recolhe) com `getBoundingClientRect` stubado para múltiplos chips.
|
||||
- F2: avaliar e, se possível, eliminar o shift único do load SSR, **sem** reintroduzir a estimativa de largura por label (razão da rejeição na entrega B169: o dono mede layout real de propósito).
|
||||
- Sem mudança de layout, regra de colapso (3 linhas), dados, access ou schema.
|
||||
|
||||
## Direção no codebase
|
||||
|
||||
- F1: `tests/unit/` — spec que renderiza `RelationChipCell` (ou wrapper) com > N chips e `Element.prototype.getBoundingClientRect` stubado (precedente: stubs de `scrollIntoView`/`ResizeObserver` nos specs existentes).
|
||||
- F2: `src/components/campaign/shared/RelationChipCell.tsx` — única alternativa ao `useLayoutEffect` atual seria o servidor conhecer o slice antes da paint; caminho barato a investigar primeiro: garantir que o estado SSR inicial use o clamp de 3 linhas de forma **estável** (sem ondulação por célula), não a estimativa de largura.
|
||||
|
||||
## Rabbit holes / Não escopo
|
||||
|
||||
- Redesign das células de chips ou da tabela (anti-goal já travado).
|
||||
- Outras fontes de deslocamento de layout na shell (sidebar, Sollinha, agenda).
|
||||
- Migrations, dados, access.
|
||||
|
||||
## Dependências
|
||||
|
||||
- `depends: [425]` — destrava quando o B169 flipar done (Issue #441 nasceu blocked; `pnpm agent:ready -- --issue 441` após o plano chegar a main).
|
||||
|
||||
## Questões em aberto
|
||||
|
||||
- O shift único do load (0,13–0,26) é aceitável em produção? Se sim, F2 vira só o teste de contrato e o shift documentado no CHANGELOG (com medição). Validar com medição no browser; sem evidência de impacto, F2 é defer.
|
||||
|
|
@ -6,6 +6,7 @@ import {
|
|||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
|
|
@ -382,12 +383,19 @@ export const RelationChipCell = ({
|
|||
// chips must not throw away a measurement that is still valid. The remove
|
||||
// button is absolutely positioned, so revealing it never changes a chip's box.
|
||||
const chipsKey = chips.map((chip) => chip.key).join('|')
|
||||
useEffect(() => {
|
||||
|
||||
// B169 — the whole measure→collapse cycle runs in `useLayoutEffect`, not
|
||||
// `useEffect`: measuring after paint made the clamp height (`max-h-18`, 72px)
|
||||
// render for a frame and only then collapse to the computed 3-row slice (rows
|
||||
// that fit ~2 lines shift visibly) — a LayoutShift wave down the table on every
|
||||
// remount (RSC navigation, pagination, sorting, filtering). Measuring
|
||||
// synchronously before paint keeps the first frame already at final height.
|
||||
useLayoutEffect(() => {
|
||||
if (!effectiveMeasureOverflow) return
|
||||
invalidateMeasurement()
|
||||
}, [chipsKey, effectiveMeasureOverflow, invalidateMeasurement])
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!effectiveMeasureOverflow) return
|
||||
const row = chipRowRef.current
|
||||
if (!row) return
|
||||
|
|
@ -407,7 +415,7 @@ export const RelationChipCell = ({
|
|||
* three visible rows — and the room the toggle needs on the third one — is
|
||||
* read from the real layout instead of estimated from label lengths.
|
||||
*/
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!effectiveMeasureOverflow) return
|
||||
if (visibleChipCount !== null) return
|
||||
const row = chipRowRef.current
|
||||
|
|
|
|||
Loading…
Reference in a new issue