diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 861eba8e..8b0a07ab 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,9 +7,10 @@ alwaysApply: false ## Status +- **B46–B55 registrados no roadmap (2026-07-29, sem código):** thumb-zone do Início (**B46**) + busca global fatiada (**B47** input → **B48** Municípios/TIs → **B49–B53** outros grupos → **B54** grid/cap → **B55** WA/share). Cadeia: B45 → B46 → B47 → …. Planos em `docs/plans/posicao-botoes-*` e `busca-global-*`. Assumido: staff-only; clique = rotas de detalhe existentes; TIs no grupo Municípios. - **B44 entregue em código (2026-07-29):** primitivo UX-1 — `CampaignHomeActionButton` (círculo `bg-muted` + ícone + label; um só controle focável) e `CampaignHomeActionStrip` (`overflow-x-auto`, `touch-pan-x`, snap). Descrição: Tooltip shadcn em ponteiro fino; long-press (~450ms) abre Drawer em ponteiro grosso (`useSyncExternalStore` + `matchMedia`, um alvo no DOM) sem disparar click (`src/lib/campaignLongPress.ts`). **Sem** montar catálogo em `/campanha` — B45. Specs: `campaignLongPress.unit.spec.tsx`, `campaignHomeActionButton.unit.spec.tsx`. Plano: [`botao-acao-inicio-strip.md`](../../../docs/plans/botao-acao-inicio-strip.md). Desbloqueia **B45**. - **B43 entregue em código (2026-07-29):** chassis UX-1 — `/campanha` fica só shell (`CampaignPageShell` vazio); dashboard staff move para `/campanha/quadro` (corpo idêntico ao antigo Início); `LeaderContactsPanel` em `/campanha/contatos`; nav staff ganha **Quadro** (sidebar; fora do bottom nav mobile); liderança **Início** + **Meus contatos**; `requireCampaignPageActor` redireciona leader bloqueado para `/campanha/contatos` (não para o Início blank). Sem migration. Plano: [`inicio-em-branco-quadro.md`](../../../docs/plans/inicio-em-branco-quadro.md). Desbloqueia **B44** / **B45**. -- **Sessão observada CG 2026-07-29 (Bloco B):** 1º hands-on ~54 min. Achados U1–U8 / O9–O11 — não rola; mental model **Ação→Local→Quem**; confirmações invisíveis; forms precisam ser contínuos; hit = lista sem assessor × votos 2022; mapa ≠ mesa de trabalho. Pivot: tabelas=bulk; operação=fluxos lineares tipo Nubank. Snapshot [`sessao-observada-coordenador-2026-07-29-snapshot.md`](../../../docs/plans/sessao-observada-coordenador-2026-07-29-snapshot.md); CUSTOMER.md/OST atualizados. Prazo soft: manejável até reunião assessoria **03/08**. **Chassis:** ~~B43~~ ✓ → **B44** botão/strip → **B45** catálogo por persona (wizards depois). +- **Sessão observada CG 2026-07-29 (Bloco B):** 1º hands-on ~54 min. Achados U1–U8 / O9–O11 — não rola; mental model **Ação→Local→Quem**; confirmações invisíveis; forms precisam ser contínuos; hit = lista sem assessor × votos 2022; mapa ≠ mesa de trabalho. Pivot: tabelas=bulk; operação=fluxos lineares tipo Nubank. Snapshot [`sessao-observada-coordenador-2026-07-29-snapshot.md`](../../../docs/plans/sessao-observada-coordenador-2026-07-29-snapshot.md); CUSTOMER.md/OST atualizados. Prazo soft: manejável até reunião assessoria **03/08**. **Chassis:** ~~B43~~ ✓ → ~~B44~~ ✓ → **B45** catálogo → **B46** thumb-zone → **B47–B55** busca global (wizards depois). - **E12+ entregue em código (2026-07-29):** escala/DRY pós-E12 — F1 forçada: `src/lib/metropolitanoTerritoryPeers.ts` unifica constantes, filtros Salvador vs demais RMS e `catalogPeersForSlug` (rollup E17/E12, `slugsForMetropolitanoSubRow`, benchmark T4). F2 não entrou — `benchmarkBySlug` já memoiza por slug. Spec `metropolitanoTerritoryPeers.unit.spec.ts`. Sem migration/UI. Plano: [`escala-dry-pos-e12.md`](../../../docs/plans/escala-dry-pos-e12.md). - **B17+ entregue em código (2026-07-29):** Escala/DRY pós-B17 — F1: `readCampaignColumnVisibility` antes do `Promise.all` e `loadLeadershipOptions` / `loadStateDeputyOptions` só quando a coluna de relação está visível (`resolveVisibleColumns`). F2: `CampaignListEmptyState` na prop `empty` de `CampaignTable` (e `SupporterList`) em demandas, organizações e apoiadores, para o botão "Colunas" sobreviver com zero linhas. Plano: [`escala-dry-pos-b17.md`](../../../docs/plans/escala-dry-pos-b17.md). - **Fill-in remover coluna Tipo entregue em código (2026-07-29):** em `/campanha/municipios`, sumiram a coluna "Tipo", o filtro single-select no header (B16), o ramo mobile em `MunicipalityFilters`, a sort key `kind` e o param `?kind=` em `municipalityListUrl` / `buildMunicipalityListWhere` / `payloadSortFieldByKey` — bookmarks antigos ignoram `kind` no parse. `municipalityKindLabels` permanece para detalhe, mapa e dossiê. `MunicipalityHeaderFilter` ficou só com filtros multi (`singleRows`/`applyMunicipalityKindFilter` deletados). B17: 11 colunas staff; e2e do picker usa Território; B18 usa `?priority=alta`. Sem migration/action/Consent. Plano: [`remover-coluna-tipo-municipios.md`](../../../docs/plans/remover-coluna-tipo-municipios.md). @@ -53,7 +54,8 @@ alwaysApply: false Peças: `src/lib/municipalityProximity.ts` (puro, recebe a malha já carregada — `features` **e** o índice memoizado `getMunicipalityFeature`: haversine, ray casting com buracos, centroide ponderado por área, união discriminada `inScope | zoneCity | outOfScope | outsideBahia`), `src/utilities/campaignGeolocation.ts` (irmão de `recentVisits`: flag `teqo:campaign:geo-prompted-session`, permissão guardada — Safari sem descriptor devolve `unknown`, que é resposta normal —, `requestCurrentPosition` com falha tipada), `NearestMunicipalityCard` ao lado de `RecentlyVisitedCard` numa linha escrita direto em `CampaignDashboard` (linha inteira escondida quando os dois estão vazios — **em CSS** (`has-[>*]:grid`), decisão do `/simplify`: a primeira versão pedia aos filhos que reportassem visibilidade para cima e pagava dois slots de estado, um callback, um effect e um _layout shift_ por load, porque o servidor renderizava a linha `hidden` e o effect só a revelava depois do primeiro paint; a regra compilada entra depois de `.hidden` na cascata, verificado no CSS do build. Sem esse estado o wrapper `DashboardShortcuts` era um `div` com nome, e foi inlinado). Estado `starting` no mount para não mostrar um botão que viraria spinner milissegundos depois (um toque ali dispararia um segundo pedido de posição); guarda de `useRef` contra o Strict Mode; fix com raio acima de 10 km mostra o resultado **com** a ressalva de precisão em vez de fingir certeza; `aria-live` `sr-only` montado desde o início. Sem migration, sem collection, sem server action, sem `Consent` — a posição nunca sai do aparelho. Payload RSC das 435 entradas: ~6,6 KB gzip (teto do plano era ~10 KB, então ficaram objetos legíveis em vez de tuplas). Gate verde (tsc, lint, format, knip com o P3 pré-existente, check:cycles, 510 unit / 397 int, 4 e2e novos, build); Aikido 0 achados. - **O `/simplify` mediu o que a revisão só suspeitava** (três revisores paralelos: reuso, qualidade, performance). Números reais, para ninguém re-derivar: a malha decodifica em **417 features, todas `Polygon` simples — 417 anéis, zero buracos, zero MultiPolygon, 15.111 pares de coordenadas**, então os ramos de buraco/MultiPolygon são seguro contra rebuild, não caminho quente; a varredura point-in-polygon completa custa **0,106 ms** e o fallback inteiro **0,32 ms** (81% disso são os 417 centroides), ~2,6 ms com penalidade de 6× de celular médio — ou seja **não havia problema de CPU**, e construir um `Map` novo por chamada mediu **mais lento** (0,337 ms). O índice `getMunicipalityFeature` entrou porque **já existe** (o módulo o constrói para o mapa), não porque a varredura doía. Um **bug real** apareceu aqui: `loadMunicipalityGeometryModule` memoiza a *rejeição* do `import()` dinâmico, então um ChunkLoadError deixava o card girando para sempre com um "Tentar de novo" incapaz de recuperar — agora há `catch` → estado acionável, com a dica de recarregar (só o reload recupera). Também morreu o `noScope`: a política "carteira vazia não pede posição" já vive no componente (que nem renderiza, nem chama `locate`), e repeti-la no módulo puro custava 7 ramos inalcançáveis em dois arquivos. **Débito registrado, não corrigido:** as 435 linhas de `accessibleMunicipalities` (~6,6 KB gzip) duplicam o `municipalitiesByIbgeCode` que o bundle do mapa já manda na mesma página — unificar mexe na fronteira de Suspense do mapa, não é limpeza. **Débito de teste confirmado, não introduzido:** `campaignMunicipalities.e2e.spec.ts` falha 3/6 na suíte cheia com 2 workers e passa 6/6 serial — o mesmo flake que o B13 já registrou no ledger. Plano: [`municipio-mais-proximo.md`](../../../docs/plans/municipio-mais-proximo.md). + **O `/simplify` mediu o que a revisão só suspeitava** (três revisores paralelos: reuso, qualidade, performance). Números reais, para ninguém re-derivar: a malha decodifica em **417 features, todas `Polygon` simples — 417 anéis, zero buracos, zero MultiPolygon, 15.111 pares de coordenadas**, então os ramos de buraco/MultiPolygon são seguro contra rebuild, não caminho quente; a varredura point-in-polygon completa custa **0,106 ms** e o fallback inteiro **0,32 ms** (81% disso são os 417 centroides), ~2,6 ms com penalidade de 6× de celular médio — ou seja **não havia problema de CPU**, e construir um `Map` novo por chamada mediu **mais lento** (0,337 ms). O índice `getMunicipalityFeature` entrou porque **já existe** (o módulo o constrói para o mapa), não porque a varredura doía. Um **bug real** apareceu aqui: `loadMunicipalityGeometryModule` memoiza a _rejeição_ do `import()` dinâmico, então um ChunkLoadError deixava o card girando para sempre com um "Tentar de novo" incapaz de recuperar — agora há `catch` → estado acionável, com a dica de recarregar (só o reload recupera). Também morreu o `noScope`: a política "carteira vazia não pede posição" já vive no componente (que nem renderiza, nem chama `locate`), e repeti-la no módulo puro custava 7 ramos inalcançáveis em dois arquivos. **Débito registrado, não corrigido:** as 435 linhas de `accessibleMunicipalities` (~6,6 KB gzip) duplicam o `municipalitiesByIbgeCode` que o bundle do mapa já manda na mesma página — unificar mexe na fronteira de Suspense do mapa, não é limpeza. **Débito de teste confirmado, não introduzido:** `campaignMunicipalities.e2e.spec.ts` falha 3/6 na suíte cheia com 2 workers e passa 6/6 serial — o mesmo flake que o B13 já registrou no ledger. Plano: [`municipio-mais-proximo.md`](../../../docs/plans/municipio-mais-proximo.md). + - **B13 entregue em código (2026-07-26):** escala relativa no mapa do Início. O coropleto era quase monocromático porque 0–100% dos válidos comprime uma disputa cujo topo estadual é 4,71% — três escalas **relativas ao próprio candidato**, em **classes discretas** (nunca rampa contínua): **quantis** (novo default), **padrão próprio (LQ)** e **posição no município**, ao lado de "% dos válidos" (B11) e "Total (votos)". **Auditoria do plano antes de codar** achou três defasagens, corrigidas em [`escala-relativa-mapa.md`](../../../docs/plans/escala-relativa-mapa.md) § Revisão na entrega: caminhos pós-Pass 2 W2 (`components/campaign/map/`), o nome inexistente `computeDfTerritorialClass` (é `computeMunicipalityTerritorialClass`) e a premissa "o mapa acompanha a lista" (desde a remodelagem o mapa só existe no Início) — resolvida como **quantis sobre o escopo de acesso do usuário**, decisão do usuário nesta sessão. Resolvi também a **contradição interna do plano** sobre LQ: os Objetivos pediam diverging em torno de 1 e as Decisões travadas reservavam diverging ao compare — ficou com as Decisões travadas, LQ como 5 classes nomeadas nas âncoras do E10, porque vermelho × azul significa "Solla × adversário" e não pode passar a significar duas coisas. **Artefato v3 (decisão cara do usuário: construir).** "Posição no município" não existia nem no artefato nem em query reaproveitável, e computá-la em request seria varrer ~100 mil linhas por ano para um dado imutável. Foi para o artefato commitado (regra 3 da escada de cache, como o E8 na v2): `federalRankByIbgeCode: Record>` — colocação entre **todos** os candidatos com votos > 0 na geografia, 2014/2018/2022, alimentada por `loadFederalVotesByCityZoneAndCandidate` (irmão CLI-only de `loadCampoFederalVotesByCityZone`). Chaveado por `ibgeCode` e não por slug porque é o que o mapa desenha (as 19 zonas de Salvador somam a cidade inteira); rank por slug entra quando dossiê/E11 pedirem. 534 → 623 KB, dentro do orçamento de 700 KB. Rank fica indisponível em 2026 (dado TSE), mesmo precedente do compare; LQ em 2026 reusa o padrão estadual de 2022. @@ -63,12 +65,13 @@ alwaysApply: false **UI.** Legenda discreta (`MapScaleLegend`) com as faixas reais, do mais fraco para o mais forte, e a nota ligada ao seletor por `aria-describedby`; o readout **nunca** mostra a classe sozinha (research §6.4), soletra o porquê em texto real ("4ª de 5 faixas", "1,8× o padrão estadual do candidato", "12º entre 663 candidatos votados aqui"). Bolhas opcionais por válidos projetados (área ∝ valor, `interactive: false` para não empilhar dois alvos de hover no mesmo município, desenhadas da menor para a maior); **medido antes de otimizar** — 417 marcadores não derrubaram o frame, então o corte para top-K não foi feito. Dois conceitos novos em `/campanha/conceitos` (E18) com "Saiba mais" inline na nota; LQ já estava coberto por `dominancia-relativa`. Sem migration, sem collection, sem server action; o **First Load JS** de `/campanha` ficou byte a byte igual (9,46 kB / 265 kB) porque o painel do mapa já carrega em chunk próprio (`ssr: false`) — o chunk em si cresceu, e o payload RSC subiu ~64 KB não comprimidos (~10 KB gzip), quase tudo `competitiveRankByYear`. Gate verde (tsc, lint, format, knip, check:cycles, 467 unit / 387 int, build); Aikido sem achado. **Débito de teste registrado:** `campaignMunicipalities.e2e.spec.ts` é flaky com 2 workers **em `main` também** — verificado por stash: com as mudanças passa 5/5 isolado e falha na suíte cheia; sem as mudanças passou 15/15 numa rodada e falhou na seguinte. Duas causas visíveis: o checkbox de consentimento do `LeaderContactsPanel` clicado antes da hidratação ("Clicking the checkbox did not change its state") e colisão de regex quando o fixture sorteia "Conde" e o `getByRole` casa também "Condeúba". + - **B33+ entregue em código (2026-07-27):** `useCampaignListFilterNavigation` (`src/components/campaign/shared/`) absorve o scaffold de navegação/debounce que `StateDeputyFilters`, `TerritoryFilters` e `MunicipalityFilters` reimplementavam — 216 linhas removidas contra 35 adicionadas, sem uma única linha de markup (`className`/`id`/`aria-*`/`htmlFor`) alterada nos três. **A auditoria pré-implementação derrubou a premissa central do plano:** ele rejeitava um callback único porque "os três domínios têm assinaturas de canonicalização diferentes", mas `buildStateDeputyListHref`/`buildTerritoryListHref`/`buildMunicipalityListHref` **já canonicalizam internamente** (`parseXListParams(xListStateToRawParams(...))` dentro do próprio builder) e `buildMunicipalityFiltersKey` é só `buildMunicipalityListSearchParams(state).toString()` — a mesma query string canônica. Logo o `parse(...)` explícito de dobradinhas/territórios era trabalho redundante, não diferença de domínio, e um só `toHref` cobre os três; o guardrail de fundo (o hook recebe callback e nunca importa os módulos de URL por domínio) ficou de pé. **Achado de comportamento, não de estrutura:** tocar um filtro/sort no meio da digitação **descartava** a busca pendente em dobradinhas e territórios (`navigateTo` chamava `clearDebounce()` primeiro) enquanto o input seguia exibindo o texto — só municípios carregava o rascunho. Unificado na semântica de municípios por `navigateWithSearch`, que é a **terceira** função exposta (a "questão em aberto" do plano fecha com três nomes, não dois); `clearDebounce` não é exposto porque `navigateTo` limpa antes de qualquer guarda, o que também matou o `router.replace` solto do "Limpar" de municípios. `shouldUpdateMunicipalitySearchUrl` orfanou (era `normalizedText(input) !== currentQ`, com o componente e o próprio spec como únicos consumidores) e foi deletado com seu `describe`. **Rede de segurança onde não havia nenhuma:** `tests/unit/campaignListFilterNavigation.unit.spec.ts` renderiza os três filtros em jsdom (`next/navigation` mockado, `vi.useFakeTimers({ toFake: ['setTimeout','clearTimeout'] })` — o scheduler do React precisa de microtasks reais), 5 casos × 3 listas; escrito antes da migração, o caso do rascunho nasceu **vermelho exatamente em dobradinhas e territórios**. `CampaignSearchForm` (lideranças/assessores/organizações) ficou fora por decisão explícita: 4º irmão, mas sem debounce, com submit explícito, e B29 provavelmente o substitui. Gate verde e bare (tsc, lint, format, knip, check:cycles, 555 unit + 413 int, build local); Aikido 0 achados. **e2e não serviu de veredito nesta sessão:** máquina com load average ~58 (dois outros worktrees com dev server), `campaignMunicipalities` falhando de forma instável na árvore nova **e** na árvore limpa em stash (3 → 6 → 4 falhas em rodadas idênticas, e verde isolado) — o flake pré-existente já ledgerizado, amplificado por timeout; `campaignTerritories` passou em todas as rodadas, e nenhum teste de municípios exercita o formulário de filtros (navegam direto por `?q=`), então o e2e nunca cobriu esta superfície. **Rodada `/simplify` (mesmo dia) mudou números e achou um bug:** o fallback `useCampaignListPending() ?? useTransition()` estava copiado em 5 lugares e o hook novo virou o 6º — virou `useCampaignListTransition()` em `CampaignListPending.tsx` (e `useCampaignListPending` deixou de ser exportado); `draftQ` saiu para o hook; `MunicipalityFilters` perdeu um `aria-live` na **terceira** região viva da mesma página (anunciava o resumo de filtros a cada tecla) e um `div` de um filho só. O bug: o `setTimeout` do debounce capturava o `state` do render que o agendou, então em territórios/dobradinhas digitar + clicar num sort dentro de 1s **revertia o sort** um segundo depois (municípios escapava pelo `key=` da página, que remonta e mata o timer) — corrigido com `stateRef`, com caso de spec verificado vermelho nas três listas sem o fix. Números finais: 281 linhas removidas contra 75 em todo o `src`, 8 casos × 3 listas, 562 unit + 413 int. **2ª rodada de `/simplify` (após o rebase) fechou a questão que ficara parada** — os dois revisores tinham proposto direções opostas para ressincronizar o input (remontar as três páginas × ressincronizar no hook e remover o `key=`), e a saída foi o terceiro caminho: o hook segue o `q` commitado **só quando não há debounce pendente**, então Back/Forward e o "Limpar busca e filtros" do estado vazio atualizam a caixa sem nunca atropelar quem está a digitar. Com isso o `key={buildMunicipalityFiltersKey(state)}` de municípios saiu (era a única razão da divergência entre as três, e remontar a cada navegação tornava a guarda de dedup do `stateRef` código morto justamente ali) e `buildMunicipalityFiltersKey` foi deletado. Mais quatro correções de reuso: `setSearch`+`scheduleSearch` eram sempre chamados em par (em duas grafias) e viraram `onSearchChange`, o que baixou a superfície do hook de 8 para 6 nomes; `MunicipalityFilters` reimplementava inline o `buildMunicipalityFilterHref` que `municipalityListFilters.ts` já exporta com 4 consumidores; nasceu o irmão `buildStateDeputyFilterHref` (4 sítios pinavam `page: 1` à mão); e municípios ganhou o resumo de filtros ativos que faltava no bloco `md:hidden` — no telemóvel não havia indicação nenhuma de quais filtros estavam ligados. A normalização que a 1ª rodada tinha metido em `navigateWithSearch` foi **revertida** (era a segunda política que ela dizia estar a remover: `scheduleSearch` sempre entregou a tecla crua ao serializador). Números finais: **325 linhas removidas contra 228 em `src`** (138 delas o hook), 10 casos × 3 listas, 586 unit. A review de performance mediu a entrega como neutra e o seu único achado é pré-existente: `/campanha/municipios` embarca ~10 kB gzip do catálogo no cliente por um `isMunicipalitySlug` — no roadmap, junto com as outras deferidas (shell de `
`, `CampaignListSortSelect`, `serialize`/`parseXSortValue` triplicado). Plano: [`escala-dry-pos-b33.md`](../../../docs/plans/escala-dry-pos-b33.md). - **B33 entregue em código (2026-07-26):** ordenação e filtro no header de `/campanha/dobradinhas` — `stateDeputyListUrl.ts`/`stateDeputyListFilters.ts` (contrato de URL/filtro próprio, sentinela `NO_PARTY_FILTER_VALUE` para "Sem partido") espelhando `municipalityListUrl.ts` (paginação/`resolveListUrl`) e `territoryListUrl.ts` (chaves/labels de sort) — não `leadershipListUrl.ts`, que o plano mandava espelhar mas **não existe** (B29 não entregou). `StateDeputySortableHead`/`StateDeputyHeaderFilter` consomem o chrome já compartilhado pelo **B21 ✓** sem extrair de novo (a corrida de ordem de chegada com B29 está encerrada); `loadStateDeputyPartyFacet` segue o contrato de faceta de `loadMunicipalityListFilterFacets` (respeita a busca, ignora o próprio filtro do popover). Ordem dos nulos de `party` **pinada por int test** em vez de assumida — Postgres/Payload ordena nulls por último em `asc` e primeiro em `desc`; "Sem partido" continua sendo o caminho direto via `party: { exists: false }`. Paridade mobile (`StateDeputyFilters`, espelho de `TerritoryFilters`) entrou por decisão desta sessão, fora do appetite original. Redirect canônico em duas fases (`resolveStateDeputyListUrl` → `totalPages` → resolve de novo) no molde de `municipios/page.tsx`. Sort v1 = Nome (default) e Partido; filtro v1 = Partido (multi + "Sem partido"); `municipalityCount`/`leadershipCount` seguem sem sort/filtro, como travado no plano. Gate verde (tsc, lint, format, check:cycles, 396 testes, build); Aikido 0 achados. **Correção pós-entrega no mesmo dia (design review independente):** mobile não tinha a opção "Sem partido" que o popover desktop já injetava (`hasNoParty` não chegava a `StateDeputyFilters`) e a tabela não passava `caption` ao `CampaignTable`, ao contrário dos dois precedentes citados no plano — ambos corrigidos no mesmo molde; copy do empty state também virou plural. **`/simplify` no mesmo dia:** a correção acima tinha copiado a montagem da linha "Sem partido" em vez de compartilhá-la — extraído `buildStateDeputyPartyOptions` em `stateDeputyListFilters.ts`, consumido por `StateDeputyFilters` e `StateDeputyHeaderFilter`; `StateDeputySortableHead` perdeu o discriminante `filterParam: 'party'` (só há uma coluna filtrável, `filterOptions` já basta); `parsePartiesParam` parou de reimplementar o dedupe de `allParamValues`; um `if` sempre-verdadeiro saiu de `buildStateDeputyListWhere`; `loadStateDeputyListPageData` ganhou `select` na query principal. Gate reverificado verde. **`/simplify` pós-`/rebase-on-main` (2ª rodada, mesmo dia):** rodou de novo sobre o commit squashado — removeu tipo+cast morto `StateDeputyPartyRow` (`stateDeputyData.ts`, `select` só afeta runtime, não a checagem estática), `stateDeputyColumns` (`page.tsx`) passou a importar `StateDeputyFilterOption` em vez de redeclarar a forma, `parsePartiesParam` foi inlineado (helper de uma linha usado uma vez). Achado maior: `firstPartyNamesLabel` era a 3ª cópia byte-idêntica da mesma função de truncar nomes (`firstTerritoryNamesLabel` em `territoryListFilters.ts`, `firstNamesLabel` em `municipalityListFilters.ts`) — cruzou o limiar de "3+ call sites"; extraído `truncatedNamesLabel` em `campaignListUrl.ts`, os três domínios migrados. Recomendação de hook compartilhado para o scaffold de busca debounced (`StateDeputyFilters`/`TerritoryFilters`/`MunicipalityFilters` reimplementam o mesmo ~50 linhas) registrada mas não perseguida — tocaria os três domínios, fora do escopo deste item; capturada no fechamento como **B33+** (fill-in de engenharia, `useCampaignListFilterNavigation`, ~0,5d). Gate reverificado verde (tsc, lint, format, check:cycles, 396 testes, build); Aikido 0 achados. Agregado SQL para o facet de partido também adiado com gatilho (só compensa em escala). Plano: [`ordenacao-filtros-lista-dobradinhas.md`](../../../docs/plans/ordenacao-filtros-lista-dobradinhas.md) · fill-in: [`escala-dry-pos-b33.md`](../../../docs/plans/escala-dry-pos-b33.md). - **B27 entregue em código (2026-07-26):** combobox + chips + auto-save no popover de Assessores de `/campanha/municipios` — trocou a lista de checkboxes + botão "Salvar assessores" por chips removíveis (`Badge`+`XIcon`, `aria-label="Remover "`) acima de um `Command` (`shouldFilter={false}` + `matchesAtWordStart`, acento-insensível), com gravação **por delta** (um toggle por vez, não replace do array). Fase 1 — `nextAdvisorIdsAfterMembership` subiu de `actions/advisor.ts` para `src/lib/municipalityAdvisorMembership.ts` (compartilhado com a carteira do B19); `setMunicipalityAdvisorMembershipRecord`/`setMunicipalityAdvisorMembership` em `actions/municipality.ts` (mesma transação + lock `municipality-advisors:{id}` + `reloadUnrestrictedActor`, mas sem `assertTargetAdvisor` — o alvo pode ser coordenador/candidato, legal nesta tela). Fase 2 — endpoint `POST /campanha/municipios/advisors` espelhando `expected-votes/route.ts` (`isSameOriginRequest` local, zod, `mapCampaignFormActionError`). Fase 3 — `MunicipalityListAdvisorsControl.tsx` reescrito: estado otimista + contador de pendências (reconcilia só quando toda entrega em voo assentou, nunca aborta um delta anterior), `Alert` de erro, `aria-live` `sr-only`; polish do `/impeccable` alinhou o gatilho (`aria-expanded`/`aria-haspopup`/realce `bg-muted/60` ao abrir) ao padrão do `MunicipalityListExpectedVotesControl` irmão (B24). `assignMunicipalityAdvisorsRecord` (replace do array) sobrevive só para `/editar`. Achado de infraestrutura no caminho: o Playwright `webServer` (`pnpm dev`) recompila uma rota POST-only no primeiro hit e o Fast Refresh resultante aborta o fetch em voo do teste (`ERR_ABORTED`) — `tests/e2e/setup.e2e.spec.ts` ganhou um prewarm dummy de `/campanha/municipios/advisors` (e retroativamente de `expected-votes/`, mesmo padrão) antes dos specs reais. Gate verde: tsc/lint/format/`check:cycles` limpos, 393 unit+int (6 novos), e2e do fluxo (isolado e em suíte) verde — falhas observadas em rodadas full-suite eram flakiness de ambiente sob carga sustentada (testes não relacionados falhando de forma rotativa, sempre verdes isolados), não regressão do B27; build produção ok; Aikido 0 achados nos 9 arquivos novos/editados. Plano: [`combobox-assessores-lista-municipios.md`](../../../docs/plans/combobox-assessores-lista-municipios.md). - **B23 entregue em código (2026-07-26):** tooltip de conteúdo nas células das listas — a mecânica genérica (`cellTooltip` em `CampaignTableColumn`, `CampaignCellTooltip`, `CampaignHoverTooltip` promovido para `shared/`) já tinha saído de graça no **E10 ✓**; este item fechou o que faltava: `openOnTouch`/`disabled` em `CampaignHoverTooltip` (default `true`/`false`, sem mudança nos 4 call sites pré-B23) para conviver com um Popover no mesmo gatilho sem roubar o tap nem sobreviver atrás dele — e, no `/simplify`, ganhou um terceiro comportamento: `content` falsy renderiza o `children` sem wrapper, então os dois consumidores abaixo não precisam mais de um ternário `content ? … : children` cada; `formatAdvisorNamesTooltip` + `advisorEntriesFromIds` (dedupe do id→nome entre a lista e o controle) junto de `MunicipalityAdvisorAvatarStack.tsx`; e os dois consumidores de `/campanha/municipios` — célula "Assessores" (não-coordenador via `cellTooltip`; coordenador envolvendo o próprio `PopoverTrigger`, `openOnTouch={false}`/`disabled={open}`, nunca os dois ao mesmo tempo) e célula "Tendência" (tooltip só quando há `politicalTrendNote`; em toque a nota aparece como leitura dentro do próprio Popover, acima do select). Verificado manualmente em `/campanha/municipios` como coordenador e assessor: hover mostra nomes/justificativa, foco de teclado abre a mesma tooltip, clique/tap abre o Popover sem tooltip órfã atrás. Gate verde (tsc/lint/format/knip — P3 pré-existente/check:cycles/443 unit/387 int/build); Aikido 0 findings. Plano: [`tooltip-celulas-listas.md`](../../../docs/plans/tooltip-celulas-listas.md). - **B22 entregue em código (2026-07-26):** explicação por coluna no header de `/campanha/municipios` — `description?: ReactNode` em `CampaignTableHead` (`shared/CampaignTable.tsx`) e rename `tooltip`→`description` em `MunicipalitySortableHead`, ambos com o mesmo sublinhado pontilhado no label e o mesmo `CampaignHoverTooltip` compartilhado (já promovido para `shared/` pelo **E10 ✓**) — sem componente novo. Auditoria pré-implementação achou o escopo já reduzido: o primitivo de tooltip e o `cellTooltip` genérico já existiam (E10), então só faltava a prop em `CampaignTableHead` e o rename. Novo helper `campaignConceptOneLiner(id)` ao lado de `campaignConceptHref` em `campaignIntelligenceConcepts.ts` — fonte única para as colunas cobertas pelo **E18 ✓** (`expectedVotes` lê `oneLiner('meta')`, `goalCoverage` lê `oneLiner('cobertura-da-meta')` + a nota de cenário). Novo `MunicipalityListColumnId` + `municipalityColumnDescriptions` (`Record` de 11 entradas) em `municipalityLabels.ts` — a coluna "Classe" mantém o texto mais longo (`CLASS_COLUMN_DESCRIPTION`, movido de `MunicipalityList.tsx`) em vez do `oneLiner` terso de `classe-territorial`, decisão assumida porque uma classificação de 5 vias não cabe numa frase. `MunicipalityList.tsx` perdeu os três `*Hint` locais e a intersecção de tipo extra em `municipalityListColumns()`. Testes: pin do `campaignConceptOneLiner`, `describe('CampaignTableHead description')` (sem prop = render idêntico; com prop = 1 trigger, conteúdo fora do markup do servidor, mesmo contrato do `cellTooltip`), e contagem de `data-slot="tooltip-trigger"` no `` (10 staff / 5 leader) em `campaignComponents.unit.spec.ts`. Gate verde (tsc, lint, format, check:cycles, 442 unit / 387 int, build); `knip` segue com o erro pré-existente ao carregar `payload.config.ts` (ledgerado P3, confirmado idêntico fora desta mudança); Aikido sem achado. Fora do escopo (mantido do plano): copy nas outras 5 listas, tooltip nos cards mobile, variante Popover no touch. Plano: [`explicacao-colunas-header-listas.md`](../../../docs/plans/explicacao-colunas-header-listas.md). -- **C13 entregue em código (2026-07-25):** "Plano de Ação" → **"Atividade"** em toda a vertical. Collection `actionPlan` → `activity` (`src/collections/Activity.ts`, labels "Atividade"/"Atividades", hooks internos renomeados), campo `campaignDemand.actionPlan` → `activity`, 7 utilities `actionPlan*` → `activity*`, `access/actionPlans.ts` → `access/activities.ts`, `lib/schemas/actionPlan.ts` → `activity.ts`, `actions/actionPlan.ts` → `activity.ts`, `components/campaign/actionPlan/` → `activity/` (13 componentes), rota `(app)/planos/` → `(app)/atividades/` (criação em `atividades/nova`, feminino como `demandas/nova`) e sidebar "Atividades". **Migration data-preserving escrita à mão** `20260725_213000_rename_action_plan_to_activity`: 4 tabelas, 3 enums, 2 colunas, 39 índices (4 pkeys inclusos), 14 FKs, 2 sequences — lista canônica tirada de `pg_indexes`/`pg_constraint`/`pg_type`/`pg_sequences` porque o snapshot do drizzle não conhece o índice parcial `action_plan_upcoming_start_at_idx` (escrito à mão no C8) e porque o gerador do Payload emitiria DROP+CREATE. O `.json` foi derivado do snapshot anterior por transformação textual (o `migrate:create` é interativo para renames) e validado pelo teste que importa: `pnpm migrate:create __probe` responde "No schema changes detected". **Bônus aprovado pelo usuário nesta sessão:** os **8 índices fósseis `%plaza%`** da remodelagem de 23/07 (que renomeou as colunas `plaza_id` mas não os índices, deixando banco e snapshot em desacordo) foram alinhados na mesma migration; `down()` os restaura, para não inventar um estado que nunca existiu. Decisões de vocabulário: sidebar **"Atividades"** (uma palavra só, a da entidade) e rótulos de campo alinhados — **"Tipo de atividade"**, **"Origem da atividade"**, **"Resultado da atividade"** — porque "ação" ao lado de "atividade" reintroduzia a tradução mental que o item existe para matar; valores de enum `kind`/`status`/`origin` **inalterados**. **Guard:** o describe "banned campaign terminology" de `codebaseConventions.unit.spec.ts` virou tabela de termos varrendo `src`+`tests`+`scripts` (migrations fora — história congelada), banindo os compostos `actionPlan`, a copy "plano de ação" e a rota velha, mas **não** `plan`/`plano` isolado (colidiria com os planos de assinatura da Fase 2 white-label); allowlist: o próprio spec, `scripts/generate-remodel-municipalities-migration.mjs` e o comentário histórico em `campaignMigrationReconciliation.int.spec.ts`. Estendê-lo a `tests/` derrubou 8 fósseis "Praça" em fixtures, limpos no mesmo PR. Gate verde (tsc, lint, format, knip, check:cycles, 46 unit / 48 int / 7 e2e, build); Aikido sem achado. **Deploy:** o `pnpm build` da Vercel aplica a migration enquanto o deployment antigo ainda serve — deployar fora do horário de mesa (mesmo perfil da remodelagem de 23/07). **Pós-`/simplify`:** a varredura mecânica comeu uma interpolação — `tests/unit/recentVisits.unit.spec.ts` ficou com `label: \`Município \`` para todas as entradas, e passava porque o label não era asserido (restaurado, com assertion nova para fechar a brecha, e os slugs `praca-` do fixture alinhados). O guard ganhou tolerância a espaço (`action[\s_-]?plan`), que era o buraco por onde `access/shared.ts` mantinha "action plan" em prosa; com isso a allowlist encolheu — o comentário de `campaignMigrationReconciliation.int.spec.ts` passou a citar `activity` e o arquivo saiu da exceção (uma spec que cresce não fica fora do guard por causa de um comentário). Deliberadamente **não** ampliei o padrão de "Praça" para a forma sem cedilha: cobraria três entradas novas de allowlist (nomes reais de organização na planilha, o teste de entradas legadas `kind: 'nucleus'`) por um fóssil já corrigido. Também: três resíduos do find-and-replace (banner `// Action activities`, `DOSSIER_*_PLAN_LIMIT`, "a activity" em dois comentários), concordância em pt-BR ("Criada por", "atividades apoiadas" minúsculo), o `??` impossível em `ActivityForm`, e as duas buscas de FK da migration agora usam `to_regclass` em vez de `::regclass` (que lança em vez de retornar NULL, contradizendo a invariante do próprio cabeçalho). A entrada morta `action_plan_municipality_idx` saiu da lista de índices da migration (nenhuma migration jamais criou esse nome — só os snapshots o afirmam, e com `push: false` nenhum banco pode carregá-lo), deixando `up()` e `down()` com os mesmos 48 pares; e o detalhe de organização parou de derivar o rótulo de status à mão e passou a reusar `ActivityStatusBadge`, com `OrganizationDetailViewModel.activities[].status` tipado como `ActivityStatus` em vez de `string` (a origem que forçava o re-narrowing no consumidor). Plano: [`renomear-plano-acao-para-atividade.md`](../../../docs/plans/renomear-plano-acao-para-atividade.md). +- **C13 entregue em código (2026-07-25):** "Plano de Ação" → **"Atividade"** em toda a vertical. Collection `actionPlan` → `activity` (`src/collections/Activity.ts`, labels "Atividade"/"Atividades", hooks internos renomeados), campo `campaignDemand.actionPlan` → `activity`, 7 utilities `actionPlan*` → `activity*`, `access/actionPlans.ts` → `access/activities.ts`, `lib/schemas/actionPlan.ts` → `activity.ts`, `actions/actionPlan.ts` → `activity.ts`, `components/campaign/actionPlan/` → `activity/` (13 componentes), rota `(app)/planos/` → `(app)/atividades/` (criação em `atividades/nova`, feminino como `demandas/nova`) e sidebar "Atividades". **Migration data-preserving escrita à mão** `20260725_213000_rename_action_plan_to_activity`: 4 tabelas, 3 enums, 2 colunas, 39 índices (4 pkeys inclusos), 14 FKs, 2 sequences — lista canônica tirada de `pg_indexes`/`pg_constraint`/`pg_type`/`pg_sequences` porque o snapshot do drizzle não conhece o índice parcial `action_plan_upcoming_start_at_idx` (escrito à mão no C8) e porque o gerador do Payload emitiria DROP+CREATE. O `.json` foi derivado do snapshot anterior por transformação textual (o `migrate:create` é interativo para renames) e validado pelo teste que importa: `pnpm migrate:create __probe` responde "No schema changes detected". **Bônus aprovado pelo usuário nesta sessão:** os **8 índices fósseis `%plaza%`** da remodelagem de 23/07 (que renomeou as colunas `plaza_id` mas não os índices, deixando banco e snapshot em desacordo) foram alinhados na mesma migration; `down()` os restaura, para não inventar um estado que nunca existiu. Decisões de vocabulário: sidebar **"Atividades"** (uma palavra só, a da entidade) e rótulos de campo alinhados — **"Tipo de atividade"**, **"Origem da atividade"**, **"Resultado da atividade"** — porque "ação" ao lado de "atividade" reintroduzia a tradução mental que o item existe para matar; valores de enum `kind`/`status`/`origin` **inalterados**. **Guard:** o describe "banned campaign terminology" de `codebaseConventions.unit.spec.ts` virou tabela de termos varrendo `src`+`tests`+`scripts` (migrations fora — história congelada), banindo os compostos `actionPlan`, a copy "plano de ação" e a rota velha, mas **não** `plan`/`plano` isolado (colidiria com os planos de assinatura da Fase 2 white-label); allowlist: o próprio spec, `scripts/generate-remodel-municipalities-migration.mjs` e o comentário histórico em `campaignMigrationReconciliation.int.spec.ts`. Estendê-lo a `tests/` derrubou 8 fósseis "Praça" em fixtures, limpos no mesmo PR. Gate verde (tsc, lint, format, knip, check:cycles, 46 unit / 48 int / 7 e2e, build); Aikido sem achado. **Deploy:** o `pnpm build` da Vercel aplica a migration enquanto o deployment antigo ainda serve — deployar fora do horário de mesa (mesmo perfil da remodelagem de 23/07). **Pós-`/simplify`:** a varredura mecânica comeu uma interpolação — `tests/unit/recentVisits.unit.spec.ts` ficou com `label: \`Município \``para todas as entradas, e passava porque o label não era asserido (restaurado, com assertion nova para fechar a brecha, e os slugs`praca-` do fixture alinhados). O guard ganhou tolerância a espaço (`action[\s_-]?plan`), que era o buraco por onde `access/shared.ts`mantinha "action plan" em prosa; com isso a allowlist encolheu — o comentário de`campaignMigrationReconciliation.int.spec.ts`passou a citar`activity`e o arquivo saiu da exceção (uma spec que cresce não fica fora do guard por causa de um comentário). Deliberadamente **não** ampliei o padrão de "Praça" para a forma sem cedilha: cobraria três entradas novas de allowlist (nomes reais de organização na planilha, o teste de entradas legadas`kind: 'nucleus'`) por um fóssil já corrigido. Também: três resíduos do find-and-replace (banner `// Action activities`, `DOSSIER\_\*\_PLAN_LIMIT`, "a activity" em dois comentários), concordância em pt-BR ("Criada por", "atividades apoiadas" minúsculo), o `??`impossível em`ActivityForm`, e as duas buscas de FK da migration agora usam `to_regclass`em vez de`::regclass`(que lança em vez de retornar NULL, contradizendo a invariante do próprio cabeçalho). A entrada morta`action_plan_municipality_idx`saiu da lista de índices da migration (nenhuma migration jamais criou esse nome — só os snapshots o afirmam, e com`push: false`nenhum banco pode carregá-lo), deixando`up()`e`down()`com os mesmos 48 pares; e o detalhe de organização parou de derivar o rótulo de status à mão e passou a reusar`ActivityStatusBadge`, com `OrganizationDetailViewModel.activities[].status`tipado como`ActivityStatus`em vez de`string` (a origem que forçava o re-narrowing no consumidor). Plano: [`renomear-plano-acao-para-atividade.md`](../../../docs/plans/renomear-plano-acao-para-atividade.md). - **B26 entregue em código (2026-07-25):** registrar sinal a partir da célula "Último sinal" de `/campanha/municipios` — Popover (desktop) / Drawer bottom sheet (mobile) cria `municipalityUpdate` `kind: 'sinal'` (C12) sem sair da fila; submit explícito "Registrar sinal" (exceção do princípio 4); `revalidateMunicipalityListPaths`; sem migration. Plano: [`registrar-sinal-lista-municipios.md`](../../../docs/plans/registrar-sinal-lista-municipios.md). - **B20 entregue (2026-07-25):** Municípios prioritários no Início — `pickDashboardPriorityMunicipalities` (`src/utilities/dashboardPriorityMunicipalities.ts`) ordena `priority=alta` por déficit E9/`central`, amostra 8; `CampaignDashboard` exibe Badge com `highPriorityCount`, CTA “Ver todas” → `?priority=alta`, chips com `list-none`. Unit `dashboardPriorityMunicipalities.unit.spec.ts`. Plano: [`municipios-prioritarios-inicio.md`](../../../docs/plans/municipios-prioritarios-inicio.md). - **B21 entregue (2026-07-25):** nova rota staff `/campanha/territorios` com os 27 Territórios de Identidade no sistema de listas — sort/filtros/busca canônicos na URL, sub-linhas do Metropolitano achatadas sem perder a hierarquia, tabela mobile com primeira coluna sticky e drill para `/campanha/municipios?region=`. O chrome de header rico foi extraído para `shared/CampaignSortableHead` + `CampaignHeaderFilterPopover`, com os wrappers de municípios migrados sem mudança de contrato; B29/B33 agora só consomem essa peça. O painel de TI saiu do Início por decisão desta sessão. Sem migration/collection/action. Plano: [`pagina-territorios-identidade.md`](../../../docs/plans/pagina-territorios-identidade.md). diff --git a/docs/plans/busca-global-acoes-linha.md b/docs/plans/busca-global-acoes-linha.md new file mode 100644 index 00000000..bd6b55a3 --- /dev/null +++ b/docs/plans/busca-global-acoes-linha.md @@ -0,0 +1,85 @@ +# Busca global — ações de linha (WhatsApp / compartilhar) + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B55 — busca global) +Impeccable: B — trailing actions nas linhas de resultado +Appetite: ~0,5–0,75 dia eng; botões + Web Share / wa.me; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (atalhos WhatsApp ok; soberania no horizonte) / `DESIGN.md` · `whatsAppHrefForPhone` (B28/B19) · tema `campaign`. + +Na implementação: craft compacto → critique → polish. + +Brief: + +- **Persona:** staff achou a pessoa/pedido e quer ZAP ou share sem abrir a ficha. +- **Job:** trailing control à direita da linha; não rouba o click da linha (stopPropagation). +- **Anti-goals:** share que grava PII em analytics; segundo botão de copy; WA em município/TI. + +## Dados → decisão → apresentação + +Dados: N/A — ações de saída; texto de share = título + URL canônica do detalhe. + +## Contexto + +Pedido (2026-07-29): + +- **Lideranças, assessores, dobradinhas:** ação abrir WhatsApp à direita. +- **Demandas e atividades:** compartilhar à direita — **mobile** = `navigator.share` do device; **desktop** = delegar para WhatsApp (`wa.me` com texto/url). + +## Objetivos + +- Primitivo(s) `HomeSearchWhatsAppAction` / `HomeSearchShareAction` (ou um com `variant`). +- WA: reusar `whatsAppHrefForPhone`; ocultar se null; `target`/`rel` seguros; `aria-label` com nome. +- Share: se `navigator.share` disponível **e** viewport/pointer mobile (definir: `navigator.share` + `!matchMedia('(pointer: fine)')` ou UA — **recomendação:** feature-detect `share` primeiro; fallback WA no desktop). +- Texto/url de share: título do hit + absolute URL do detalhe (`getCampaignInviteBaseURL` / site URL já usados na campanha). +- `pointer-events` / `onClick` stop na ação para o `Link` da linha não navegar. +- Sem migration / Consent novo (não é canal D3; é `wa.me` / share nativo). + +## Decisões travadas + +- **stopPropagation na ação.** **Rejeitado:** ação = único hit da linha (mata navegação). +- **Desktop share → WhatsApp**, não Twitter/e-mail picker. **Rejeitado:** `mailto:` genérico. +- **Sem Consent** — não armazena biometria nem canal; só abre app externo. **Rejeitado:** abrir Onda 0 por isso. +- **i18n:** labels “Abrir no WhatsApp” / “Compartilhar”. + +## Questões em aberto + +- **Dobradinha sem telefone:** esconder ícone (já em B52). Confirmado. + +## Abordagem proposta + +```mermaid +flowchart LR + Row["result row Link"] --> WA["wa.me"] + Row --> Share["navigator.share | wa.me fallback"] +``` + +- Integrar nos grupos **B49–B53** via prop `trailingAction` ou composição. +- Precedente share: `SupporterShareKit.tsx`; WA: `whatsAppHrefForPhone` / `buildWhatsAppUrl` em `lib/phone.ts`. +- Unit: share fallback; click não navega. + +## Dependências + +- Dura: **B47** + pelo menos um de **B49–B53**. Soft: todos os cinco grupos. +- Soft: B28 ✓ / B19 ✓ (`whatsAppHrefForPhone`). + +## Não escopo + +Bridge D3–D5. Preview de mensagem rica. Municípios/TIs. + +## Rabbit holes + +**Web Share Level 2 files.** **Mitigação:** só text+url. + +## Adiado com gatilho + +- **Copy link** ao lado do share. Revisitar se mesa pedir no critique. + +## Referências + +- [busca-global-resultados-liderancas.md](busca-global-resultados-liderancas.md) · assessores/atividades/dobradinhas/demandas · `src/lib/phone.ts` · share kits de apoiadores (precedente copy/`wa.me` se existir) +- AGENTS.md — naming; sem WABA diff --git a/docs/plans/busca-global-inicio-input.md b/docs/plans/busca-global-inicio-input.md new file mode 100644 index 00000000..6598a891 --- /dev/null +++ b/docs/plans/busca-global-inicio-input.md @@ -0,0 +1,100 @@ +# Busca global do Início — input e modo resultados + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B47 — busca global) +Impeccable: C — superfície nova de busca no Início (expand/collapse + empty results region) +Appetite: ~1–1,5 dia eng; ilha client + debounce + chrome de modo focado; sem provider de hits ainda +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Clarity under pressure; Feel the action) / `DESIGN.md` · `CampaignSearchInput` · tema `campaign`. + +Na implementação: **shape → craft → critique → polish** (classe C). + +Brief compacto: + +- **Persona / contexto:** staff no Início quer achar um município/pessoa/atividade sem abrir a lista certa na sidebar. +- **Job principal:** digitar → ver resultados; no mobile o input sobe e o resto some. +- **Estratégia de cor:** Restrained; sem card chrome nos resultados (grupos chegam em B48+). +- **Edit where you see:** não — navegação/descoberta. +- **Anti-goals:** botão “Pesquisar”; command palette estilo Raycast com atalho global nesta fatia; busca na sidebar; leader com omnibox estadual _(assumido — lockdown)_. + +## Dados → decisão → apresentação + +Dados: N/A neste item — só chrome; hits = **B48–B53**. + +## Contexto + +Início blank (**B43**) + ações (**B44/B45**) + posição (**B46**). Pedido (2026-07-29): input de busca geral **sem botão**, debounce na digitação; no mobile fica **abaixo** das ações e, ao digitar, **desliza para cima**, some o resto, ficam input + resultados; no tablet/desktop o input fica **acima** das ações e a visualização de resultados é semelhante (grid = **B54**). + +## Objetivos + +- Ilha **`CampaignHomeSearch`** (nome final no PR) no Início **staff** (`coordinator` / `candidate` / `advisor`); **não** montar para `leader` _(assumido)_. +- Reusar visual de `CampaignSearchInput` (ícone, `min-h-11`); **sem** submit button. +- Debounce ~200–300 ms (**não** reusar `SEARCH_DEBOUNCE_MS = 1000` de `useCampaignListFilterNavigation` / `CampaignSearchForm` — esse valor é para filtro de lista com round-trip RSC; omnibox precisa resposta mais curta); query vazia / só espaços → sair do modo focado e limpar região de resultados. +- **Modo focado (mobile):** ao ter query não vazia, transição: input sobe; strip de ações e demais chrome do Início ocultos; região de resultados abaixo do input. +- **Modo focado (`md+`):** mesma semântica de “só input + resultados”, sem empurrar a strip para baixo da dobra de forma confusa — strip some enquanto há query (paridade com mobile). +- Slot/região `aria-live` / `role="region"` para grupos (**B48+**); empty state “Nenhum resultado.” só quando query ≥ limiar e todos os grupos vazios (após ≥1 provider). +- Endpoint ou RSC search: **não** neste item — expor contrato (`query`, `onResults`, registry de grupos) para B48+. +- Sem migration / Consent. + +## Decisões travadas + +- **Debounce no cliente; sem botão Pesquisar.** **Rejeitado:** submit form; busca a cada keystroke sem debounce. +- **Staff-only.** Leader continua lockdown (contatos). **Rejeitado:** omnibox para leader com só apoiadores nesta fatia (outro job). +- **Expand = query não vazia**, não focus-only. **Rejeitado:** sumir a strip no focus vazio (rouba o ritual de ações). +- **i18n:** `CampaignHomeSearch`, `homeSearchQuery`; label pt-BR “Buscar na campanha” (ou copy curta a fechar no craft). + +## Questões em aberto + +- **Transporte dos hits: `POST` JSON vs server action vs fetch RSC?** **Opções:** A rota `campaignJsonMutationRoute`-like GET/POST search | B server action | C debounce + `router` query. **Recomendação:** A — `GET`/`POST` same-origin com body/query curta, access no loader, sem sujar a URL do Início com `?q=` (Início não é lista). _(assumido)_ +- **Limiar mínimo de caracteres?** **Opções:** 1 | 2. **Recomendação:** 2 para reduzir ruído em nomes curtos; 1 se critique pedir. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + Input["CampaignHomeSearch input"] + Deb["debounce"] + Mode["focused mode UI"] + Slot["results slot"] + Input --> Deb --> Mode + Deb --> Slot +``` + +Componentes: + +- **`CampaignHomeSearch.tsx`** (`'use client'`, `components/campaign/dashboard/`): estado local da query; debounce; classes de modo focado; renderiza `children` / registry de grupos quando B48+ plugar. +- **Wire-up** em `page.tsx` (RSC) só para staff; passar escopo mínimo se necessário depois. +- **Não** importar `buildMunicipalityListHref` / catálogo no client strip — hits serializados pelo server nos itens seguintes. +- **Migration:** Sem migration. Rota JSON de busca pode nascer em **B48** com o primeiro provider (evitar endpoint vazio). + +## Dependências + +- Dura: **B46** (ordem relativa ações↔busca). Soft: **B45**. +- Desbloqueia **B48–B55**. + +## Não escopo + +- Providers de hits → **B48–B53**. +- Grid 2/3 colunas e cap de viewport → **B54**. +- WhatsApp/share na linha → **B55**. +- Páginas de detalhe novas → rotas existentes (cada grupo só `Link`). + +## Rabbit holes + +- **cmdk / Command palette global com ⌘K.** **Mitigação:** input no Início só; atalho = fill-in se pedido ≥2×. +- **Index único tipo Algolia.** **Mitigação:** loaders Payload/`wordStartFilter` por domínio. + +## Adiado com gatilho + +- **`?q=` na URL do Início** (shareable). Revisitar se mesa pedir mandar link da busca. +- **Busca na liderança (apoiadores).** Revisitar se leader pedir achar contato sem rolar a lista. + +## Referências + +- [posicao-botoes-acao-inicio-thumb-zone.md](posicao-botoes-acao-inicio-thumb-zone.md) · [fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md) (U11 busca de município nos wizards — reusar matcher) +- `CampaignSearchInput.tsx` · `lib/wordStartFilter.ts` · `campaignJsonMutationRoute.ts` (precedente CSRF se POST) +- `PRODUCT.md` / `DESIGN.md` diff --git a/docs/plans/busca-global-layout-resultados.md b/docs/plans/busca-global-layout-resultados.md new file mode 100644 index 00000000..96a8a3fc --- /dev/null +++ b/docs/plans/busca-global-layout-resultados.md @@ -0,0 +1,80 @@ +# Busca global — layout dos resultados (grid + cap de viewport) + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B54 — busca global) +Impeccable: B — layout responsivo da região de resultados B47 +Appetite: ~0,5–0,75 dia eng; CSS grid + política de cap; sem provider novo +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Clarity under pressure) / `DESIGN.md` · chrome **B47** · tema `campaign`. + +Na implementação: craft compacto → critique → polish. + +Brief compacto: + +- **Persona:** staff no tablet/notebook com vários grupos; no mobile empilha. +- **Job principal:** ver grupos lado a lado sem scroll infinito de hits. +- **Anti-goals:** masonry irregular; virtualização prematura; cap por grupo fixo que esconde Municípios (grupo principal). + +## Dados → decisão → apresentação + +Dados: N/A — só layout/quantidade de linhas renderizadas; números vêm dos grupos. + +## Contexto + +Pedido (2026-07-29): mobile = grupos empilhados; tablet = **2 colunas**; desktop = **3 colunas**; total de resultados limitado ao **preenchimento da tela** pelo conjunto dos grupos. Conteúdo das linhas = **B48–B53**; este item só organiza e corta. + +## Objetivos + +- Container de resultados: `flex flex-col` (<`md`) → `md:grid md:grid-cols-2` → `lg:grid-cols-3` (breakpoints alinhados ao shell da campanha). +- **Cap de viewport:** limitar o número total de hits renderizados (soma dos grupos) ao que cabe na área útil sob o input — medir via `ResizeObserver` / estimativa de row height, ou política simples documentada (ex. max 12–18 hits totais redistribuídos com prioridade ao grupo Municípios). +- Grupo Municípios (**B48**) tem prioridade de slots no cap; demais grupos compartilham o resto. +- Grupos vazios continuam não montados (regra dos providers). +- Sem migration. + +## Decisões travadas + +- **Grid por viewport, não por pointer.** **Rejeitado:** 1 coluna em touch tablet paisagem. +- **Cap no conjunto, não scroll infinito.** **Rejeitado:** “mostrar todos” com página interna. +- **Prioridade de slots: Municípios primeiro.** **Rejeitado:** round-robin que deixa Cairu fora e enche de demandas. +- **i18n:** `HomeSearchResultsLayout`. + +## Questões em aberto + +- **Cap: medir DOM vs constante?** **Opções:** A ResizeObserver + row estimate | B constante por breakpoint (ex. 8/12/15). **Recomendação:** B no v1 (barato, testável); A se critique achar corte cego. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart TB + Slot["B47 results region"] --> Layout["HomeSearchResultsLayout"] + Layout --> G1["Municípios"] + Layout --> G2["outros grupos"] +``` + +- Componente de layout envolvendo os grupos já registrados; API `allocateHitBudget(groups, budget)`. +- **Migration:** nenhuma. + +## Dependências + +- Dura: **B47**. Soft: **B48** (precisa ≥1 grupo para validar cap); melhor após 2+ grupos. + +## Não escopo + +Conteúdo das linhas → **B48–B53**. Ações → **B55**. + +## Rabbit holes + +**Virtualização (`react-virtual`).** **Mitigação:** cap pequeno; virtualizar só se budget > 50. + +## Adiado com gatilho + +- **“Ver todos em /municipios?q=”** por grupo. Revisitar se cap gerar reclamação na sessão. + +## Referências + +- [busca-global-inicio-input.md](busca-global-inicio-input.md) · [busca-global-resultados-municipios.md](busca-global-resultados-municipios.md) +- `CampaignPageShell` breakpoints · `PRODUCT.md` diff --git a/docs/plans/busca-global-resultados-assessores.md b/docs/plans/busca-global-resultados-assessores.md new file mode 100644 index 00000000..1fbe515c --- /dev/null +++ b/docs/plans/busca-global-resultados-assessores.md @@ -0,0 +1,63 @@ +# Busca global — resultados Assessores + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B50 — busca global) +Impeccable: B — grupo de hits no slot B47 +Appetite: ~0,5 dia eng; loader + grupo; sem WA (B55) +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` / `DESIGN.md` · `/campanha/assessores` (B19) · tema `campaign`. + +Brief: CG/candidato acha assessor pelo nome; assessor **não** precisa ver o grupo inteiro se access negar — falhar fechado com a mesma regra de `isCampaignUnrestricted` da lista (se advisor não lê assessores, grupo sempre vazio / não registrado). + +## Dados → decisão → apresentação + +Dados: N/A (identificação). Lista simples. + +## Contexto + +Grupo “Assessores”. Clique → `/campanha/assessores/[id]`. WhatsApp = **B55**. + +## Objetivos + +- `searchHomeAdvisors(query, user)` respeitando access B19 (unrestricted only para a vertical de assessores — se o ator não pode listar, não montar provider). +- Título **“Assessores”**; oculto se vazio. +- Linha: nome; secundário discreto (e-mail mascarado ou contagem de municípios — barato). +- Sem migration. + +## Decisões travadas + +- **Mesmo access da lista de assessores.** **Rejeitado:** advisor buscando colegas (não é o modelo atual). +- **Detalhe existente.** **Rejeitado:** página nova. +- **i18n:** `HomeSearchAdvisorHit`; “Assessores”. + +## Questões em aberto + +- **Advisor vê o grupo?** **Opções:** A nunca | B só a si. **Recomendação:** A — consistente com nav. _(assumido)_ + +## Abordagem proposta + +Loader espelhando `advisorData`; grupo UI; pluga no registry B47. + +## Dependências + +- Dura: **B47**. Soft: B19 ✓; **B55** para WA. + +## Não escopo + +WA → **B55**. Outros grupos / grid → B48–B54. + +## Rabbit holes + +**Expor e-mail `@planilha.invalid`.** **Mitigação:** não mostrar e-mail inválido; preferir nome + N municípios. + +## Adiado com gatilho + +Nenhum neste item. + +## Referências + +- [busca-global-inicio-input.md](busca-global-inicio-input.md) · `gerenciar-assessores.md` · `utilities/advisorData.ts` (`loadAdvisorListPageData` / `loadAdvisorDetail`) · `/campanha/assessores/[id]` diff --git a/docs/plans/busca-global-resultados-atividades.md b/docs/plans/busca-global-resultados-atividades.md new file mode 100644 index 00000000..6ec9bf0f --- /dev/null +++ b/docs/plans/busca-global-resultados-atividades.md @@ -0,0 +1,59 @@ +# Busca global — resultados Atividades + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B51 — busca global) +Impeccable: B — grupo de hits no slot B47 +Appetite: ~0,5–0,75 dia eng; loader + grupo; share = B55 +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` / `DESIGN.md` · `/campanha/atividades` · tema `campaign`. + +Brief: achar atividade por título; secundário = município + data. Clique → `/campanha/atividades/[slug]`. + +## Dados → decisão → apresentação + +Dados: N/A (agenda). Lista com data legível pt-BR. + +## Objetivos + +- `searchHomeActivities(query, user)` com access de `activity`; match no `title` (word-start). +- Grupo **“Atividades”**; oculto se vazio. +- Linha: título; secundário município + quando; slot para share (**B55**). +- Sem migration / Consent. + +## Decisões travadas + +- **Detalhe existente por slug.** **Rejeitado:** abrir compositor de giro. +- **Share não neste item** → **B55**. +- **i18n:** `HomeSearchActivityHit`; “Atividades”. + +## Questões em aberto + +- **Incluir rascunhos?** **Opções:** A sim no escopo do ator | B só próximos/realizados. **Recomendação:** A — busca é achar o que eu criei. _(assumido)_ + +## Abordagem proposta + +Loader perto de activity list data; grupo UI. + +## Dependências + +- Dura: **B47**. Soft: C13 ✓; **B55**. + +## Não escopo + +Share → **B55**. Grid → **B54**. + +## Rabbit holes + +**Full-text no `resultSummary`.** **Mitigação:** só título no v1. + +## Adiado com gatilho + +Nenhum neste item. + +## Referências + +- [busca-global-inicio-input.md](busca-global-inicio-input.md) · `utilities/activityPageData.ts` / `activityDetailPageData.ts` · `/campanha/atividades/[slug]` diff --git a/docs/plans/busca-global-resultados-demandas.md b/docs/plans/busca-global-resultados-demandas.md new file mode 100644 index 00000000..b8d971a3 --- /dev/null +++ b/docs/plans/busca-global-resultados-demandas.md @@ -0,0 +1,58 @@ +# Busca global — resultados Demandas + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B53 — busca global) +Impeccable: B — grupo de hits no slot B47 +Appetite: ~0,5 dia eng; loader + grupo; share = B55 +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` / `DESIGN.md` · `/campanha/demandas` · tema `campaign`. + +Brief: achar pedido pelo título/assunto; secundário município + status; clique → `/campanha/demandas/[slug]`. + +## Dados → decisão → apresentação + +Dados: N/A. Lista. + +## Objetivos + +- `searchHomeDemands(query, user)` com access staff de demandas; match no título (e campos de texto baratos já selecionados na lista). +- Grupo **“Demandas”**; oculto se vazio. +- Slot share (**B55**). +- Sem migration / Consent. + +## Decisões travadas + +- **Detalhe existente.** **Rejeitado:** criar demanda a partir do empty state neste item. +- **i18n:** `HomeSearchDemandHit`; “Demandas”. + +## Questões em aberto + +- **Buscar descrição longa?** **Opções:** A só título | B título+resumo. **Recomendação:** A no v1. _(assumido)_ + +## Abordagem proposta + +Loader demand list; grupo UI. + +## Dependências + +- Dura: **B47**. Soft: **B55**. + +## Não escopo + +Share → **B55**. Leader nunca vê (já sem access). + +## Rabbit holes + +**Filtro por status na busca.** **Mitigação:** query de nome só. + +## Adiado com gatilho + +Nenhum neste item. + +## Referências + +- [busca-global-inicio-input.md](busca-global-inicio-input.md) · `utilities/campaignDemandData.ts` (`loadDemandListPageData`) · `/campanha/demandas/[slug]` diff --git a/docs/plans/busca-global-resultados-dobradinhas.md b/docs/plans/busca-global-resultados-dobradinhas.md new file mode 100644 index 00000000..21e772ee --- /dev/null +++ b/docs/plans/busca-global-resultados-dobradinhas.md @@ -0,0 +1,58 @@ +# Busca global — resultados Dobradinhas + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B52 — busca global) +Impeccable: B — grupo de hits no slot B47 +Appetite: ~0,5 dia eng; loader + grupo; WA = B55 +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` / `DESIGN.md` · `/campanha/dobradinhas` · tema `campaign`. + +Brief: achar deputado estadual / dobradinha por nome ou partido; clique → `/campanha/dobradinhas/[slug]`. + +## Dados → decisão → apresentação + +Dados: N/A. Secundário = partido + contagem de municípios se barato. + +## Objetivos + +- `searchHomeStateDeputies(query, user)`; match nome (e partido se couber sem ruído). +- Grupo **“Dobradinhas”**; oculto se vazio. +- Slot trailing para WhatsApp (**B55**) quando houver telefone no modelo — se não houver telefone na entidade, B55 no-op nessa linha. +- Sem migration. + +## Decisões travadas + +- **Detalhe existente.** **Rejeitado:** abrir município da dobradinha. +- **i18n:** `HomeSearchStateDeputyHit`; “Dobradinhas”. + +## Questões em aberto + +- **Telefone na dobradinha existe?** **Opções:** A WA só se campo existir | B omitir WA neste grupo. **Recomendação:** A — B55 esconde ícone se `whatsAppHrefForPhone` for null. _(assumido)_ + +## Abordagem proposta + +Loader `stateDeputy` list; grupo UI. + +## Dependências + +- Dura: **B47**. Soft: **B55**; B33 ✓. + +## Não escopo + +WA → **B55**. + +## Rabbit holes + +**Buscar por município da dobradinha.** **Mitigação:** nome/partido só; município já está em B48. + +## Adiado com gatilho + +Nenhum neste item. + +## Referências + +- [busca-global-inicio-input.md](busca-global-inicio-input.md) · `utilities/stateDeputyData.ts` (`loadStateDeputyListPageData`) · `/campanha/dobradinhas/[slug]` diff --git a/docs/plans/busca-global-resultados-liderancas.md b/docs/plans/busca-global-resultados-liderancas.md new file mode 100644 index 00000000..3ff2d00b --- /dev/null +++ b/docs/plans/busca-global-resultados-liderancas.md @@ -0,0 +1,71 @@ +# Busca global — resultados Lideranças + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B49 — busca global) +Impeccable: B — grupo de hits no slot B47 +Appetite: ~0,5–0,75 dia eng; loader + grupo UI; sem WA (B55) +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` / `DESIGN.md` · lista `/campanha/liderancas` · tema `campaign`. + +Na implementação: craft compacto → critique → polish. + +Brief: staff acha uma liderança pelo nome; toque abre a ficha. Sem card. Grupo “Lideranças” só se houver hit. + +## Dados → decisão → apresentação + +Dados: N/A como KPI — identificação (nome + contexto curto: município principal ou contagem). Forma: lista. Sem chart. + +## Contexto + +Slot **B47**; grupo secundário abaixo de Municípios (**B48**). Clique → `/campanha/liderancas/[id]` (rota existente). Ação WhatsApp à direita = **B55**. + +## Objetivos + +- `searchHomeLeaderships(query, user)` com access existente (`overrideAccess: false`); match em `contact.name` (mesmo espírito da busca da lista / `wordStartFilter`). +- Grupo título **“Lideranças”**; oculto se vazio. +- Linha: nome em destaque; linha secundária discreta (ex. municípios resumidos ou setor — o que o loader já tiver barato); **sem** trailing WA neste item (reserva slot/`actions` opcional para B55). +- Sem migration / Consent. + +## Decisões travadas + +- **Navega para detalhe existente.** **Rejeitado:** drawer de preview nesta fatia. +- **Sem WhatsApp aqui** — **B55**. **Rejeitado:** copiar o ícone do B28 agora e divergir depois. +- **i18n:** `HomeSearchLeadershipHit`; título “Lideranças”. + +## Questões em aberto + +- **Secundário: lista de municípios vs status de apoio?** **Opções:** A municípios (curto) | B status. **Recomendação:** A — alinha ao job “quem é de onde”. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + Q --> L["searchHomeLeaderships"] --> G["HomeSearchLeadershipGroup"] --> D["/liderancas/id"] +``` + +- Loader perto de `leadershipData.ts` / list where; UI no dashboard/search. +- **Migration:** nenhuma. + +## Dependências + +- Dura: **B47**. Soft: **B48** (ordem visual — Municípios acima); B28 ✓ (telefone existirá para B55). + +## Não escopo + +- WA → **B55**. Assessores/outros grupos → **B50–B53**. Layout grid → **B54**. + +## Rabbit holes + +- **Buscar por telefone.** **Mitigação:** só nome no v1; gatilho se campo pedir. + +## Adiado com gatilho + +Nenhum neste item. + +## Referências + +- [busca-global-inicio-input.md](busca-global-inicio-input.md) · `utilities/leadership/leadershipData.ts` (`loadLeadershipListPageData`) · `/campanha/liderancas/[id]` · B28 (`whatsAppHrefForPhone` em `lib/phone.ts`) diff --git a/docs/plans/busca-global-resultados-municipios.md b/docs/plans/busca-global-resultados-municipios.md new file mode 100644 index 00000000..46a83d3f --- /dev/null +++ b/docs/plans/busca-global-resultados-municipios.md @@ -0,0 +1,92 @@ +# Busca global — resultados Municípios (+ Territórios) + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B48 — busca global) +Impeccable: B — grupo de resultados no chrome B47; linhas sem card +Appetite: ~1–1,5 dia eng; loader scoped + UI do grupo; 1º provider da busca +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` / `DESIGN.md` · `MunicipalityPriorityIndicator` · coluna “2022” (A11) · tema `campaign`. + +Na implementação: craft compacto → critique → polish. + +Brief compacto: + +- **Persona:** staff digita “Cairu” / “Recôncavo” e quer o hit certo em 1 toque. +- **Job principal:** listar municípios (e TIs) que casam a query, com prioridade e votos 2022 legíveis. +- **Anti-goals:** card com borda por linha; grupo vazio visível; desalinhamento dos nomes quando um tem flag e outro não. + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Sim — votos 2022 (município) e agregado do território (TI) à direita. +- **Decisões:** abrir o município/TI certo; ler magnitude relativa (mesma linguagem da coluna “2022” / rollup E17). +- **Forma:** lista ranqueada por relevância de nome (word-start), não chart. **Rejeitado:** mapa nos resultados; % estadual absoluto. +- **Profile:** ~até N hits por grupo (cap fino em **B54**); scoped ao access do ator. +- **Anti-goals de dado:** sem inventar métrica nova; reusar A11 / rollup TI. + +## Contexto + +**B47** entrega o input e o slot. Este item é o **grupo principal** (“Municípios”), sempre no topo quando há hits. Inclui **territórios de identidade** no mesmo grupo (pedido 2026-07-29): nome do TI + valores agregados à direita. Clique → detalhe existente (`/campanha/municipios/[slug]` ou `/campanha/territorios#ti-…` via `territoryAnchor`). + +## Objetivos + +- Loader/server path `searchHomeMunicipalities(query, user)` (nome final) com `overrideAccess: false`; match via `matchesAtWordStart` / `normalizeSearchPhrase` (`lib/wordStartFilter.ts`). +- Grupo com título discreto **“Municípios”**; **só renderiza se `hits.length > 0`**. +- Linha município: flag de prioridade (`MunicipalityPriorityIndicator`) **só se `priority === 'alta'`**, com **coluna de ícone de largura fixa** para alinhar o início dos nomes; nome em destaque; território (TI) abaixo, menor/`muted`; à direita a mesma informação da coluna **“2022”** da lista (votos + contexto A11 — reusar helper de célula / `formatElectionNumber`, sem rank longo se estourar densidade — preferir o compacto da lista). +- Linha TI: sem flag; nome do território; à direita agregado já usado em `/campanha/territorios` / `territoryOverview` (o mesmo número que a mesa já lê — tipicamente votos/rollup 2022 do E17/E12, documentar no PR qual campo). +- Sem borda/card; padding vertical suficiente; hit target ≥ 44 px no mobile. +- Clique = `Link` para detalhe existente — **sem** item de roadmap de “página nova”. +- Sem migration / Consent. + +## Decisões travadas + +- **TIs no grupo Municípios**, não grupo separado. **Rejeitado:** “Territórios” como 7º grupo (pedido explícito). +- **Coluna de ícone reservada** (vazia se não prioritário). **Rejeitado:** flag inline que desloca o texto. +- **Primeiro provider nasce com a rota de busca** (se B47 adiou o endpoint). **Rejeitado:** client-only scan do catálogo sem access (assessor veria fora da carteira). +- **i18n:** `HomeSearchMunicipalityHit`, título pt-BR “Municípios”. + +## Questões em aberto + +- **Ordenação: word-start primeiro vs votos 2022 desc?** **Opções:** A relevância de nome | B votos. **Recomendação:** A (busca é achar pelo nome); empate → votos desc. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + Q["query B47"] --> L["searchHomeMunicipalities"] + L --> G["HomeSearchMunicipalityGroup"] + G --> Mun["/municipios/slug"] + G --> TI["/territorios#ti-"] +``` + +Componentes: + +- **Loader** em `utilities/` (domínio municipality ou `dashboard/` se multi — preferir municipality + territory helpers existentes: `loadTerritoryOverview` / rollup puro). +- **`HomeSearchMunicipalityGroup.tsx`**: título + lista de linhas; registra-se no slot B47. +- Reuso: `MunicipalityPriorityIndicator`, `mapKey`/catalog names, `TerritoryLink`/`territoryAnchor` se couber. +- **Migration:** Sem migration. + +## Dependências + +- Dura: **B47**. Soft: A11 ✓, E17/E12 ✓, B20 ✓ (priority), B25 ✓ (âncora TI). + +## Não escopo + +- Outros grupos → **B49–B53**. Grid/cap → **B54**. Ações WA → **B55** (não aplica a município/TI). + +## Rabbit holes + +- **Fuzzy/typo tolerance.** **Mitigação:** word-start como listas. +- **Salvador ZE vs cidade.** **Mitigação:** hits são as 19 unidades operacionais + TIs; não agregar Salvador num hit único. + +## Adiado com gatilho + +- **Hit “sem responsável” / deficit na linha.** Revisitar se a busca virar fila E9. + +## Referências + +- [busca-global-inicio-input.md](busca-global-inicio-input.md) · `MunicipalityList.tsx` (`VotePositionReadout` / coluna `votos` → header “2022”) · `MunicipalityPriorityIndicator.tsx` · `lib/municipalityVoteRank.ts` · `lib/wordStartFilter.ts` · `utilities/territory/loadTerritoryOverview.ts` · `utilities/territory/territoryOverview.ts` (`TerritoryOverviewRow.votesByYear` / `pctPropriaVotacao`) · `lib/territoryAnchor.ts` · `municipalityListUrl.ts` (lista usa `name.contains` — busca home preferir word-start no índice/loader próprio) +- AGENTS.md — `overrideAccess: false`; access advisor diff --git a/docs/plans/posicao-botoes-acao-inicio-thumb-zone.md b/docs/plans/posicao-botoes-acao-inicio-thumb-zone.md new file mode 100644 index 00000000..406bf282 --- /dev/null +++ b/docs/plans/posicao-botoes-acao-inicio-thumb-zone.md @@ -0,0 +1,97 @@ +# Posição dos botões de ação do Início (thumb zone) + +Status: rascunho +Atualizado em: 2026-07-29 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B46 — chassis UX-1) +Impeccable: B — encaixe de layout no Início sob tema `campaign` (strip B44/B45 já montada) +Appetite: ~0,5 dia eng; CSS/layout do shell do Início; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Clarity under pressure; Feel the action; alvos de toque no campo) / `DESIGN.md` · primitivo **B44** + catálogo **B45** · tema `campaign`. + +Na implementação: craft compacto → critique → polish. + +Brief compacto: + +- **Persona / contexto:** CG/assessor no celular (polegar na metade inferior); no notebook a faixa de ações é a primeira coisa acima da dobra. +- **Job principal:** alcançar a strip de ações sem alongar o polegar no mobile; no `md+` manter as ações no topo do conteúdo. +- **Estratégia de cor:** herda B44 (Restrained). +- **Edit where you see:** não — chrome de launcher. +- **Anti-goals:** fixed overlay que tapa a sidebar Sheet; segundo FAB flutuante; “sticky” que empurra o conteúdo sem reserva de espaço; misturar viewport (`md`) com política de pointer (aqui é **viewport**: thumb zone ≠ fine/coarse). + +## Dados → decisão → apresentação + +Dados: N/A — posicionamento de chrome; sem KPI. + +## Contexto + +Com **B43** o Início está blank; **B44/B45** colocam a strip de ações. Sem regra de zona, a strip tende a nascer no fluxo normal do `CampaignPageShell` (topo do conteúdo) — bom no desktop, ruim no celular (polegar longe). Pedido de produto (2026-07-29): botões **próximos do limite inferior** no mobile e **próximos do limite superior** em tablet/desktop. A busca global (**B47+**) se ancora nesta composição (abaixo da strip no mobile; acima no `md+`). + +## Objetivos + +- Layout do Início staff (e leader, se a strip B45 existir) com: + - **< `md`:** strip de ações ancorada na região inferior do viewport útil (acima da bottom nav, se houver), com safe-area. + - **`md+`:** strip no topo do conteúdo do Início (abaixo do header/inset usual). +- Reservar espaço para o input de busca (**B47**) na ordem relativa: mobile = ações → busca; `md+` = busca → ações. +- Sem migration / collection / Consent / action. + +## Decisões travadas + +- **Breakpoint = viewport (`md`), não pointer.** Thumb zone é anatomia de mão + tela pequena; tablet com mouse ainda quer ações no topo. **Rejeitado:** `pointer-coarse` → bottom (um touch laptop colocaria a strip embaixo no desktop widescreen). +- **Âncora acima da bottom nav** no mobile (não cobrir `getCampaignBottomNav` / `CampaignBottomNav` — já usa `pb-[env(safe-area-inset-bottom)]`). **Rejeitado:** `fixed bottom-0` por cima da barra. +- **Sem FAB circular solto** — continua a strip B44. **Rejeitado:** botão único flutuante “Ações”. +- **i18n:** classes/ids em inglês (`homeActionDock`, …); copy intacta do B45. + +## Questões em aberto + +- **`position: sticky` vs flex column com `mt-auto` no mobile?** **Opções:** A sticky na base do scroller | B coluna full-height com strip no fim. **Recomendação:** B se o Início continuar sem scroll de conteúdo útil (blank + strip + busca); A se a 2ª dobra voltar cedo. _(assumido — medir no PR com o shell real)_ + +## Abordagem proposta + +```mermaid +flowchart TB + Page["/campanha page.tsx"] + Shell["CampaignPageShell / home layout"] + SearchSlot["slot busca B47"] + Strip["CampaignHomeActionStrip B44/B45"] + Page --> Shell + Shell -->|"md+: SearchSlot → Strip"| SearchSlot + Shell -->|"lt md: Strip → SearchSlot"| Strip +``` + +Componentes: + +- **Layout no Início** (`src/app/(campaign)/campanha/(app)/page.tsx` + wrapper em `components/campaign/dashboard/` se o markup crescer): ordenação responsiva (`flex-col-reverse` / `md:flex-col` ou dois slots CSS `order-*`). +- **Safe-area + offset da bottom nav** — medir altura real em `CampaignBottomNav.tsx` (fixed + safe-area); não hardcodar sem checar o shell. +- **Migration:** Sem migration, sem collection, sem server action. + +## Dependências + +- Dura: **B45** (strip montada no Início — senão não há o quê ancorar). Soft: **B44** ✓ (primitivo). +- Soft: **B47** consome a ordem relativa (não bloqueia B46). + +## Não escopo + +- Input de busca e expand → **B47**. +- Resultados / grid → **B48–B54**. +- Wizards das ações → fatias UX-1 seguintes. +- 2ª dobra (mapa/KPI) no Início → adiado do B43. + +## Rabbit holes + +- **Redesenhar `CampaignPageShell` global.** **Mitigação:** wrapper só do Início. +- **`dvh` / teclado virtual.** **Mitigação:** não fixar altura frágil; preferir fluxo + padding-bottom. + +## Adiado com gatilho + +- **Reposicionar 2ª dobra sob a busca no `md+`.** Revisitar quando B45+B47 estáveis e CG pedir briefing no mesmo viewport (soft 03/08). + +## Referências + +- `docs/roadmap.md` (B46) · [botao-acao-inicio-strip.md](botao-acao-inicio-strip.md) · [catalogo-acoes-inicio-por-persona.md](catalogo-acoes-inicio-por-persona.md) · [fluxos-acao-primeiro-inicio.md](fluxos-acao-primeiro-inicio.md) +- `src/app/(campaign)/campanha/(app)/page.tsx` · `CampaignPageShell` · `CampaignBottomNav.tsx` · `nav.ts` (`getCampaignBottomNav`) +- `CampaignHomeActionStrip.tsx` / `CampaignHomeActionButton.tsx` (B44 ✓) +- `PRODUCT.md` / `DESIGN.md` +- AGENTS.md — Feel the action; naming diff --git a/docs/roadmap.md b/docs/roadmap.md index da5984aa..7f239fff 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-29 — B44 entregue (botão/strip UX-1); B45 pendente. +Atualizado em: 2026-07-29 — B46–B55 registrados (thumb-zone + busca global fatiada); B44 ✓; B45 pendente. Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). ## Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026) @@ -83,10 +83,20 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re ### Demais itens abertos -- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis:** ~~**B43**~~ ✓ → ~~**B44**~~ ✓ → **B45** (catálogo por persona; wizards com escrita = fatias seguintes). +- **UX-1 fluxos ação-primeiro (prioridade pós-sessão 29/07)** — evidência: [snapshot](plans/sessao-observada-coordenador-2026-07-29-snapshot.md) · O9–O11 em CUSTOMER.md · **rascunho:** [fluxos-acao-primeiro-inicio.md](plans/fluxos-acao-primeiro-inicio.md). Soft deadline: **03/08**. **Chassis:** ~~**B43**~~ ✓ → ~~**B44**~~ ✓ → **B45** → **B46** (thumb-zone) → **B47–B55** (busca global fatiada); wizards com escrita = fatias seguintes. - ~~**B43** Início em branco + Quadro~~ — **entregue 2026-07-29** (`/campanha` blank; staff dashboard em `/campanha/quadro`; liderança em `/campanha/contatos`; nav Início+Quadro / Início+Meus contatos; `requireCampaignPageActor` manda leader bloqueado → `/campanha/contatos`; sem migration) · [plano](plans/inicio-em-branco-quadro.md) - ~~**B44** Botão de ação do Início + lista horizontal~~ — **entregue 2026-07-29** (`CampaignHomeActionButton` + `CampaignHomeActionStrip`; Tooltip fine / long-press Drawer coarse; sem mount no Início — B45) · ~1d · Janela 1 · Impeccable C · [plano](plans/botao-acao-inicio-strip.md) -- **B45** Catálogo de ações por persona — CG/candidato e assessor (6 ações; `uncovered-municipalities` já navega); liderança (2; `my-contacts` → `/campanha/contatos`); resto inerte até wizards. Depende de B44 · ~0,5d · Janela 1 · Impeccable B · [plano](plans/catalogo-acoes-inicio-por-persona.md) +- **B45** Catálogo de ações por persona — CG/candidato e assessor (6 ações; `uncovered-municipalities` já navega); liderança (2; `my-contacts` → `/campanha/contatos`); resto inerte até wizards. Depende de B44 ✓ · ~0,5d · Janela 1 · Impeccable B · [plano](plans/catalogo-acoes-inicio-por-persona.md) +- **B46** Posição dos botões de ação (thumb zone) — mobile: strip perto do limite inferior (acima da bottom nav); `md+`: perto do topo; ordem relativa com a busca (mobile ações→busca; desktop busca→ações). Depende de B45 · ~0,5d · Janela 1 · Impeccable B · [plano](plans/posicao-botoes-acao-inicio-thumb-zone.md) +- **B47** Busca global — input (debounce, sem botão; modo focado: sobe e esconde o resto) · staff-only · Depende de B46 · ~1–1,5d · Janela 1 · Impeccable C · [plano](plans/busca-global-inicio-input.md) +- **B48** Busca — resultados Municípios (+ TIs no mesmo grupo; flag de prioridade alinhada; “2022” / agregado à direita; sem card) · Depende de B47 · ~1–1,5d · Janela 1 · Impeccable B · [plano](plans/busca-global-resultados-municipios.md) +- **B49** Busca — resultados Lideranças · Depende de B47 · ~0,5–0,75d · Janela 1 · Impeccable B · [plano](plans/busca-global-resultados-liderancas.md) +- **B50** Busca — resultados Assessores (access B19) · Depende de B47 · ~0,5d · Janela 1 · Impeccable B · [plano](plans/busca-global-resultados-assessores.md) +- **B51** Busca — resultados Atividades · Depende de B47 · ~0,5–0,75d · Janela 1 · Impeccable B · [plano](plans/busca-global-resultados-atividades.md) +- **B52** Busca — resultados Dobradinhas · Depende de B47 · ~0,5d · Janela 1 · Impeccable B · [plano](plans/busca-global-resultados-dobradinhas.md) +- **B53** Busca — resultados Demandas · Depende de B47 · ~0,5d · Janela 1 · Impeccable B · [plano](plans/busca-global-resultados-demandas.md) +- **B54** Busca — layout resultados (mobile empilhado; tablet 2 col; desktop 3 col; cap ao viewport; prioridade Municípios) · Depende de B47 · soft B48 · ~0,5–0,75d · Janela 1 · Impeccable B · [plano](plans/busca-global-layout-resultados.md) +- **B55** Busca — ações de linha (WA em lideranças/assessores/dobradinhas; share nativo mobile / WA no desktop em demandas/atividades) · Depende de B47 + ≥1 de B49–B53 · ~0,5–0,75d · Janela 1 · Impeccable B · [plano](plans/busca-global-acoes-linha.md) - ~~**E4R** import único da planilha de projeção~~ — **entregue 2026-07-24** (`pnpm db:seed:projecao`; overwrite-always; [plano](plans/import-planilha-projecao.md)) - ~~**A11** posição em votos do município (rank absoluto + % da própria votação) + ordenação da lista por votação~~ — **entregue 2026-07-24** (helper `municipalityVoteRank`; baseline + coluna lista; default `?sort=votos` desc; header=`2022`; consome contrato B15) · [plano](plans/ranking-votos-municipio.md) - ~~**E17** tabela comparativa dos Territórios de Identidade no Início (pedido do candidato) · somas/razões apenas, Metropolitano decomposto (salvaguardas MAUP) · ~1d · Janela 1–2 · primeira fatia de E12 · [plano](plans/tabela-ti-inicio.md)~~ — **entregue 2026-07-24** (`territoryOverview.ts` + `loadTerritoryOverview.ts` + `TerritoryOverviewTable.tsx`; rollup puro client-safe, loader `overrideAccess: true`, Metropolitano decomposto, ordenação client-side; sem migration) @@ -102,7 +112,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re - **D3** fundação multi-sessão (QR por `campaignUser` + log + Consent `campanha-whatsapp-canal`) · [plano](plans/whatsapp-canal-fundacao.md) - **D4** envio 1:1 pela sessão do ator · depende de D3 · [plano](plans/whatsapp-envio-liderancas.md) - **D5** inbox da própria sessão → rascunhos (`municipalityUpdate`/demanda) com humano no loop · depende de D3 · [plano](plans/whatsapp-sugestao-atualizacoes.md) -- **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · absorve débitos FD2 ([field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md)): glossário (O3 **confirmada 29/07**), triagem em lote, empty states · **subordinado a UX-1 / B43–B45** (não polish células se a porta de entrada continuar entidade-primeiro) · gatilho: antes de 16/08; soft 03/08 +- **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · absorve débitos FD2 ([field-desk-ux-pos-critique.md](plans/field-desk-ux-pos-critique.md)): glossário (O3 **confirmada 29/07**), triagem em lote, empty states · **subordinado a UX-1 / B43–B48** (não polish células se a porta de entrada continuar entidade-primeiro) · gatilho: antes de 16/08; soft 03/08 - ~~**B38** sidebar recolhido em tablet~~ — **entregue 2026-07-26** (`collapsible="offcanvas"` no `CampaignSidebar`; `SidebarTrigger` no inset `md+`; `defaultOpen` lê cookie `sidebar_state`; `CampaignSidebarViewportDefault` colapsa uma vez em 768–1023 sem cookie; mobile Sheet inalterado; sem migration) · [plano](plans/sidebar-recolhido-tablet.md) - ~~**B14**~~ ✓ município mais próximo (geolocalização → card "Onde estou" no Início staff, entregue 2026-07-26; **débito B8 F2-D1** ZE exata em Salvador fechado 2026-07-29) · pede permissão **1× por sessão** se ainda não concedida; matching client-side **por point-in-polygon** (malha municipal + malha das 19 ZE em Salvador; centroides ordenam fallback de carteira e ZE mais próxima) · [plano](plans/municipio-mais-proximo.md) - ~~**B20** Municípios prioritários no Início~~ — **entregue 2026-07-25** (`pickDashboardPriorityMunicipalities` + amostra 8 por déficit `central`; Badge com total; “Ver todas” → `?priority=alta`; `list-none` na strip) · [plano](plans/municipios-prioritarios-inicio.md) @@ -190,6 +200,16 @@ flowchart TD B43n["B43 ✓ Início em branco
+ Quadro / Contatos"] B44n["B44 ✓ Botão ação
+ strip horizontal"] B45n["B45 Catálogo ações
por persona"] + B46n["B46 Thumb zone
ações Início"] + B47n["B47 Busca global
input + modo focado"] + B48n["B48 Busca
Municípios + TIs"] + B49n["B49 Busca Lideranças"] + B50n["B50 Busca Assessores"] + B51n["B51 Busca Atividades"] + B52n["B52 Busca Dobradinhas"] + B53n["B53 Busca Demandas"] + B54n["B54 Busca layout
grid + cap"] + B55n["B55 Busca ações
WA / share"] B15n["B15 ✓ Ordenar lista
por coluna"] B16n["B16 ✓ Filtros no header
da lista"] B17n["B17 ✓ Seletor de colunas
das 7 listas"] @@ -252,7 +272,20 @@ flowchart TD B26n -.Drawer mobile.-> B42n B24n -.Trend control.-> B42n B27n -.Advisors control.-> B42n - B43n --> B44n --> B45n + B43n --> B44n --> B45n --> B46n --> B47n + B47n --> B48n + B47n --> B49n + B47n --> B50n + B47n --> B51n + B47n --> B52n + B47n --> B53n + B47n --> B54n + B48n -.cap prioridade.-> B54n + B49n --> B55n + B50n --> B55n + B51n --> B55n + B52n --> B55n + B53n --> B55n B16n -.densidade do th.-> B22n E18n -.oneLiner dos conceitos.-> B22n B22n -.colunas se explicam.-> B17n @@ -344,11 +377,11 @@ flowchart TD B8F2 -.rank por ZE = débito aberto.-> B14n ``` -Paralelizáveis agora: ~~**B43**~~ ✓ (chassis UX-1 — entregue 2026-07-29), ~~**B42**~~ ✓ (polimento mobile da mesma lista — entregue 2026-07-27), **B21** (página dos TIs; sem dep dura — E17 ✓/B15 ✓/B16 ✓ já entregues; E12 pousa nela depois; **B25** é o único item que espera B21 — o link do território na lista só existe depois do destino), ~~**B17**~~ ✓ (seletor de colunas; soft B15 ✓/B16 ✓ — pousa no header de coluna atual), ~~**B18**~~ ✓ (filtros salvos + submenu Municípios; soft B16 ✓/B17 — botão na barra), **B22** (explicação por coluna no header; sem dep dura — B15 ✓/B16 ✓/E18 ✓), **B23** (tooltip nas células; a mecânica genérica e a promoção do primitivo já saíram no E10 ✓ — restam os consumidores Assessores/Tendência e a convivência com o Popover), ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência; entregue 2026-07-26), **B26** (registrar sinal na célula "Último sinal"; sem dep dura — E9 ✓/C12 ✓ já entregues; soft B24/B23 só pela coerência de contrato na mesma linha), **B27** (combobox + chips + auto-save no popover de Assessores; sem dep dura — B19 ✓ já deu o delta+lock e o padrão de chips; soft B24 pelo endpoint e B23 pelo gatilho), **B28** (e-mail + celular na lista de lideranças; sem dep dura — B19 ✓ é só o padrão soft de copy/WhatsApp; `phone` já está no view model), **B29** (sort + filtro no header da lista de lideranças; sem dep dura — B15 ✓/B16 ✓ deram o contrato e o popover; soft B21 e B28 só pela ordem de chegada: quem entrar depois herda o head compartilhado / encaixa as colunas do outro), fill-ins (renomear coluna Cobertura → quick win de densidade no header; remover coluna Tipo — quick win de densidade; remover lista de zonas do Início — pós-B8 F2 ✓; ícone de prioridade na lista; Cenário junto aos filtros — encaixa na barra slim de B16 ✓ —; selecionados no topo nos filtros do header — helper puro que B21 reusa —; O0+, RS+). **B30** (convite WhatsApp na lista de lideranças; sem dep dura — reusa integralmente a action/collection existentes; soft B28 só pelo ícone na mesma linha, qualquer um pode ir primeiro — **entregue 2026-07-26**), **B31** (coluna de dobradinhas na lista de lideranças; sem dep dura — o dado já está no view model e o catálogo já tem loader; soft B27 pela peça de célula, que só vira extração compartilhada no 3º call site, e B28/B29/B30 pela ordem de chegada na mesma tabela), ~~**B32**~~ ✓ (editar Status de apoio na célula da lista de lideranças; entregue 2026-07-26), **B33** (sort + filtro no header da lista de dobradinhas; sem dep dura — B15 ✓/B16 ✓ deram o contrato; soft B29/B21 só pela ordem de chegada na mesma corrida de extração do chrome compartilhado, quem entrar primeiro paga a peça e o outro só consome), **B34** (chips editáveis de municípios na lista de lideranças; sem dep dura — B19 ✓ já deu o delta+lock, o padrão de chips e o loader do catálogo a promover; soft B27/B31 pela mesma peça de célula editável — quem chegar depois herda o combobox/Popover em vez de reinventar —, e B28/B30/B32 só pela mesma tabela; _decisão a validar com produto_: Popover por célula em vez do toggle "Editar" pedido literalmente, mesmo precedente do B31/B32), **B37** (chips editáveis de municípios na lista de dobradinhas; sem dep dura — B19 ✓/B34 já dão a lib a promover e o desenho do Popover+combobox; soft B34 só pela ordem de chegada em quem faz a promoção da lib, B31 pela mesma família de pedido em outra tabela, B33 pela mesma tabela; _mesma decisão a validar com produto_ do B31/B32/B34: Popover por célula, não toggle). **B36** (coluna de lideranças na lista de dobradinhas; sem dep dura — reusa integralmente a mutação/schema/lock que o B31 desenha para a mesma relação, do lado oposto; soft B31 pela corrida de extração da célula/action compartilhadas, B19 ✓ pelo padrão de origem, B33 pela mesma tabela), **C13** (renomear Plano de Ação → Atividade; sem dep dura — não toca nenhuma das listas em curso (B26–B37 são municípios/lideranças/dobradinhas), então roda em paralelo a elas; a única ordem que importa é **antes de E13**, que constrói dentro da vertical renomeada, e antes do congelamento ~20/09 por carregar migration; PR mecânico e isolado, não misturar com feature). **D3** só após smoke + folga e só se o fluxo sede-digita (C12 ✓) não absorver os deltas do ZAP (não compete com E9). +Paralelizáveis agora: **B45** (próximo do chassis UX-1 — desbloqueia B46–B55); após **B47**, **B49–B53** em paralelo entre si (soft ordem visual sob B48). Demais já listados: ~~**B43**~~ ✓ (chassis UX-1 — entregue 2026-07-29), ~~**B42**~~ ✓ (polimento mobile da mesma lista — entregue 2026-07-27), **B21** (página dos TIs; sem dep dura — E17 ✓/B15 ✓/B16 ✓ já entregues; E12 pousa nela depois; **B25** é o único item que espera B21 — o link do território na lista só existe depois do destino), ~~**B17**~~ ✓ (seletor de colunas; soft B15 ✓/B16 ✓ — pousa no header de coluna atual), ~~**B18**~~ ✓ (filtros salvos + submenu Municípios; soft B16 ✓/B17 — botão na barra), **B22** (explicação por coluna no header; sem dep dura — B15 ✓/B16 ✓/E18 ✓), **B23** (tooltip nas células; a mecânica genérica e a promoção do primitivo já saíram no E10 ✓ — restam os consumidores Assessores/Tendência e a convivência com o Popover), ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência; entregue 2026-07-26), **B26** (registrar sinal na célula "Último sinal"; sem dep dura — E9 ✓/C12 ✓ já entregues; soft B24/B23 só pela coerência de contrato na mesma linha), **B27** (combobox + chips + auto-save no popover de Assessores; sem dep dura — B19 ✓ já deu o delta+lock e o padrão de chips; soft B24 pelo endpoint e B23 pelo gatilho), **B28** (e-mail + celular na lista de lideranças; sem dep dura — B19 ✓ é só o padrão soft de copy/WhatsApp; `phone` já está no view model), **B29** (sort + filtro no header da lista de lideranças; sem dep dura — B15 ✓/B16 ✓ deram o contrato e o popover; soft B21 e B28 só pela ordem de chegada: quem entrar depois herda o head compartilhado / encaixa as colunas do outro), fill-ins (renomear coluna Cobertura → quick win de densidade no header; remover coluna Tipo — quick win de densidade; remover lista de zonas do Início — pós-B8 F2 ✓; ícone de prioridade na lista; Cenário junto aos filtros — encaixa na barra slim de B16 ✓ —; selecionados no topo nos filtros do header — helper puro que B21 reusa —; O0+, RS+). **B30** (convite WhatsApp na lista de lideranças; sem dep dura — reusa integralmente a action/collection existentes; soft B28 só pelo ícone na mesma linha, qualquer um pode ir primeiro — **entregue 2026-07-26**), **B31** (coluna de dobradinhas na lista de lideranças; sem dep dura — o dado já está no view model e o catálogo já tem loader; soft B27 pela peça de célula, que só vira extração compartilhada no 3º call site, e B28/B29/B30 pela ordem de chegada na mesma tabela), ~~**B32**~~ ✓ (editar Status de apoio na célula da lista de lideranças; entregue 2026-07-26), **B33** (sort + filtro no header da lista de dobradinhas; sem dep dura — B15 ✓/B16 ✓ deram o contrato; soft B29/B21 só pela ordem de chegada na mesma corrida de extração do chrome compartilhado, quem entrar primeiro paga a peça e o outro só consome), **B34** (chips editáveis de municípios na lista de lideranças; sem dep dura — B19 ✓ já deu o delta+lock, o padrão de chips e o loader do catálogo a promover; soft B27/B31 pela mesma peça de célula editável — quem chegar depois herda o combobox/Popover em vez de reinventar —, e B28/B30/B32 só pela mesma tabela; _decisão a validar com produto_: Popover por célula em vez do toggle "Editar" pedido literalmente, mesmo precedente do B31/B32), **B37** (chips editáveis de municípios na lista de dobradinhas; sem dep dura — B19 ✓/B34 já dão a lib a promover e o desenho do Popover+combobox; soft B34 só pela ordem de chegada em quem faz a promoção da lib, B31 pela mesma família de pedido em outra tabela, B33 pela mesma tabela; _mesma decisão a validar com produto_ do B31/B32/B34: Popover por célula, não toggle). **B36** (coluna de lideranças na lista de dobradinhas; sem dep dura — reusa integralmente a mutação/schema/lock que o B31 desenha para a mesma relação, do lado oposto; soft B31 pela corrida de extração da célula/action compartilhadas, B19 ✓ pelo padrão de origem, B33 pela mesma tabela), **C13** (renomear Plano de Ação → Atividade; sem dep dura — não toca nenhuma das listas em curso (B26–B37 são municípios/lideranças/dobradinhas), então roda em paralelo a elas; a única ordem que importa é **antes de E13**, que constrói dentro da vertical renomeada, e antes do congelamento ~20/09 por carregar migration; PR mecânico e isolado, não misturar com feature). **D3** só após smoke + folga e só se o fluxo sede-digita (C12 ✓) não absorver os deltas do ZAP (não compete com E9). ### Sequência por janela (só pendentes) -**Janela 1 — agora → 05/08 (convenções):** **prioridade pós-29/07 — ~~B43~~ ✓ → B44 → B45** (chassis UX-1: ~~Início blank + Quadro/Contatos~~ entregue 2026-07-29; botão/strip + catálogo por persona; soft 03/08) · [fluxos](plans/fluxos-acao-primeiro-inicio.md); smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); **E4R ✓** em código (aplicar `pnpm db:seed:projecao` em produção após smoke); **E8 ✓** conta da cadeira entregue; **E17 ✓** tabela TI no Início; **B15 ✓** (sort por coluna) e **A11 ✓** (posição em votos + `sort=votos`; o default do staff passou a `deficit` no E9 ✓) entregues 2026-07-24; **E18 ✓** documentação de conceitos (`/campanha/conceitos`, v1 só E8) entregue 2026-07-24; **B19 ✓** gerenciar assessores (caminho do onboarding sem `/admin` — criar/ativar contas e carteiras) entregue 2026-07-24; **B16 ✓** filtros no header entregue 2026-07-25; **B20 ✓** Municípios prioritários no Início entregue 2026-07-25; ~~**B38**~~ ✓ sidebar recolhido em tablet (entregue 2026-07-26 — `offcanvas` + trigger `md+` + cookie/`matchMedia`); **B39** (lembrar de mim / sessão longa — o default de 2 h está derrubando sessão no onboarding; ~0,5 dia, entra cedo) e, logo atrás se folga, ~~**B40**~~ ✓ (biometria WebAuthn — entregue 2026-07-28); ~~**B41**~~ ✓ (scroll horizontal + sticky na lista de municípios — entregue 2026-07-27) e ~~**B42**~~ ✓ (polimento mobile da mesma lista — Drawer em Tendência/Assessores/Votos estimados + card compacto e clicável; entregue 2026-07-27); **B21** (página dos Territórios de Identidade — a leitura regional sai do painel do Início e ganha tela própria com sort/filtro; útil já no onboarding para dividir carteiras) se folga — e, logo atrás dela, **B25** (link do território na célula da lista: quarto de dia, é o que faz a página nova ser encontrada de dentro do trabalho); **B22** (explicação por coluna no header — a tabela deixa de exigir o autor do E8/E9 ao lado para ser lida; quick win de onboarding, meio dia); **B23** (tooltip nas células — nomes dos assessores no hover da coluna, hoje só iniciais, e a justificativa da tendência, hoje invisível na lista; capacidade genérica da célula, meio dia, casa com B22 pelo mesmo primitivo); ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência — entregue 2026-07-26; a mesa corrige em massa as notas semeadas pelo E4R ✓ sem abrir 435 telas de edição); **B26** (registrar sinal na própria célula "Último sinal" — fecha o laço do E9 ✓: hoje a fila diz onde está frio e obriga a sair dela para registrar qualquer coisa; ~0,75 dia, e é o item que mais aumenta a chance do gate de adoção abaixo, porque o registro passa a caber no gesto de varrer a lista, inclusive no celular); **B27** (combobox + chips + auto-save nos Assessores — é literalmente a tela em que o CG divide as carteiras no onboarding: hoje procura o nome numa coluna de checkboxes e confirma num botão; ~0,75 dia, casa com B24 pelo mesmo endpoint e com B23 pelo mesmo gatilho); **B28** (e-mail + celular na lista de lideranças — copy + WhatsApp como em assessores, sem abrir a ficha; ~0,5–0,75 dia, quick win de campo no onboarding da rede); **B30** (convidar para completar cadastro por WhatsApp na mesma lista — reusa o convite que já existe no detalhe, sem abrir a ficha; ~0,25–0,5 dia, o quick win mais barato desta leva porque não toca action/collection nenhuma; casa com B28 pela mesma linha, ícone distinto); **B31** (coluna de dobradinhas na mesma lista — é o preenchimento em lote que o onboarding exige: o E4R ✓ deixou dobradinha só no município e as fichas vazias, e completar uma a uma pelo detalhe é o gesto que trava a mesa; ~1 dia, casa com B27 pela mesma peça de célula — quem for primeiro define o padrão do chip+combobox in-list); ~~**B32**~~ ✓ (editar Status de apoio na célula da mesma lista — entregue 2026-07-26; fecha o adiado do próprio B29); **B34** (chips editáveis de municípios na mesma lista — a coluna "Municípios" ganha o mesmo tratamento da coluna homônima em Assessores (B19 ✓): chips com link + Popover de edição com combobox, em vez do texto morto atual; ~1–1,25 dia, casa com B27/B31 pela mesma peça de célula editável — quem entrar antes define o padrão do combobox+chip in-list que os outros herdam); **B37** (o mesmo tratamento de chips editáveis na coluna "Municípios" de `/campanha/dobradinhas` — hoje um número morto, `municipalityCount`; ~0,75–1 dia, herda a lib promovida pelo B34 se ele já tiver landado, senão promove ela mesma); **B36** (a coluna "Lideranças" da mesma lista ganha o mesmo tratamento — chips nomeados + Popover + busca + auto-save por delta, reusando sem alteração a mutação que o B31 desenha para o lado oposto da mesma relação; ~0,5–1,5 dia dependendo de quem entre B31/B36 chega primeiro na corrida de extração da célula compartilhada); **B33** (sort + filtro no header de `/campanha/dobradinhas` — pedido explícito de reuso: Nome/Partido no header e filtro por Partido, sem duplicar o chrome de `/campanha/municipios`; ~0,5–1,5 dia dependendo de quem chega primeiro na corrida de extração do chrome compartilhado com B29/B21; entra perto de B29 por reusar a mesma peça); **B29** (sort + filtro no header da mesma lista — é a tela onde o assessor recebe a carteira e precisa recortar "quem ainda está a abordar aqui" / "quem ainda não tem acesso ao app" antes de convidar; ~1,5–2 dias porque paga também a extração do head compartilhado, então entra depois dos quick wins — mas antes se B21 for adiado, para o head nascer compartilhado); ~~**B17**~~ ✓ (seletor de colunas) / ~~**B18**~~ ✓ (filtros salvos + atalho sidebar) entregues 2026-07-28; fill-in **renomear coluna Cobertura** (header curto na fila — quarto de dia; glossário/KPI intactos); fill-in **remover coluna Tipo** (densidade morta na fila — quarto/meio dia; Salvador via slugs cobre ZE); ~~fill-in **remover lista de zonas do Início**~~ ✓ (entregue 2026-07-28); fill-in **selecionados no topo nos filtros do header** (desmarcar sem caçar checkbox; helper puro que B21 herda — quarto de dia); **C13 ✓** (renomeado "Plano de Ação" → "Atividade" em toda a vertical, incluindo a migration data-preserving — entregue 2026-07-25); gate de adoção: sinal Little Hire — ≥1 update espontâneo até 30/07, cobrança da tabela dispensada (planilhas já em `docs/sheets/`) — acompanhamento em [IMPROVE-APP-PLAN.md](IMPROVE-APP-PLAN.md); **R6** critique/polish; **E9 ✓** entregue no mesmo dia; **B14 ✓** (atalho geo entregue 2026-07-26); Onda 0 jurídica em paralelo (externa). +**Janela 1 — agora → 05/08 (convenções):** **prioridade pós-29/07 — ~~B43~~ ✓ → ~~B44~~ ✓ → B45 → B46 → B47 → B48** (e em paralelo B49–B53; depois B54/B55) — chassis UX-1 + busca global; soft 03/08 · [fluxos](plans/fluxos-acao-primeiro-inicio.md); smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); **E4R ✓** em código (aplicar `pnpm db:seed:projecao` em produção após smoke); **E8 ✓** conta da cadeira entregue; **E17 ✓** tabela TI no Início; **B15 ✓** (sort por coluna) e **A11 ✓** (posição em votos + `sort=votos`; o default do staff passou a `deficit` no E9 ✓) entregues 2026-07-24; **E18 ✓** documentação de conceitos (`/campanha/conceitos`, v1 só E8) entregue 2026-07-24; **B19 ✓** gerenciar assessores (caminho do onboarding sem `/admin` — criar/ativar contas e carteiras) entregue 2026-07-24; **B16 ✓** filtros no header entregue 2026-07-25; **B20 ✓** Municípios prioritários no Início entregue 2026-07-25; ~~**B38**~~ ✓ sidebar recolhido em tablet (entregue 2026-07-26 — `offcanvas` + trigger `md+` + cookie/`matchMedia`); **B39** (lembrar de mim / sessão longa — o default de 2 h está derrubando sessão no onboarding; ~0,5 dia, entra cedo) e, logo atrás se folga, ~~**B40**~~ ✓ (biometria WebAuthn — entregue 2026-07-28); ~~**B41**~~ ✓ (scroll horizontal + sticky na lista de municípios — entregue 2026-07-27) e ~~**B42**~~ ✓ (polimento mobile da mesma lista — Drawer em Tendência/Assessores/Votos estimados + card compacto e clicável; entregue 2026-07-27); **B21** (página dos Territórios de Identidade — a leitura regional sai do painel do Início e ganha tela própria com sort/filtro; útil já no onboarding para dividir carteiras) se folga — e, logo atrás dela, **B25** (link do território na célula da lista: quarto de dia, é o que faz a página nova ser encontrada de dentro do trabalho); **B22** (explicação por coluna no header — a tabela deixa de exigir o autor do E8/E9 ao lado para ser lida; quick win de onboarding, meio dia); **B23** (tooltip nas células — nomes dos assessores no hover da coluna, hoje só iniciais, e a justificativa da tendência, hoje invisível na lista; capacidade genérica da célula, meio dia, casa com B22 pelo mesmo primitivo); ~~**B24**~~ ✓ (auto-save + justificativa no popover de Tendência — entregue 2026-07-26; a mesa corrige em massa as notas semeadas pelo E4R ✓ sem abrir 435 telas de edição); **B26** (registrar sinal na própria célula "Último sinal" — fecha o laço do E9 ✓: hoje a fila diz onde está frio e obriga a sair dela para registrar qualquer coisa; ~0,75 dia, e é o item que mais aumenta a chance do gate de adoção abaixo, porque o registro passa a caber no gesto de varrer a lista, inclusive no celular); **B27** (combobox + chips + auto-save nos Assessores — é literalmente a tela em que o CG divide as carteiras no onboarding: hoje procura o nome numa coluna de checkboxes e confirma num botão; ~0,75 dia, casa com B24 pelo mesmo endpoint e com B23 pelo mesmo gatilho); **B28** (e-mail + celular na lista de lideranças — copy + WhatsApp como em assessores, sem abrir a ficha; ~0,5–0,75 dia, quick win de campo no onboarding da rede); **B30** (convidar para completar cadastro por WhatsApp na mesma lista — reusa o convite que já existe no detalhe, sem abrir a ficha; ~0,25–0,5 dia, o quick win mais barato desta leva porque não toca action/collection nenhuma; casa com B28 pela mesma linha, ícone distinto); **B31** (coluna de dobradinhas na mesma lista — é o preenchimento em lote que o onboarding exige: o E4R ✓ deixou dobradinha só no município e as fichas vazias, e completar uma a uma pelo detalhe é o gesto que trava a mesa; ~1 dia, casa com B27 pela mesma peça de célula — quem for primeiro define o padrão do chip+combobox in-list); ~~**B32**~~ ✓ (editar Status de apoio na célula da mesma lista — entregue 2026-07-26; fecha o adiado do próprio B29); **B34** (chips editáveis de municípios na mesma lista — a coluna "Municípios" ganha o mesmo tratamento da coluna homônima em Assessores (B19 ✓): chips com link + Popover de edição com combobox, em vez do texto morto atual; ~1–1,25 dia, casa com B27/B31 pela mesma peça de célula editável — quem entrar antes define o padrão do combobox+chip in-list que os outros herdam); **B37** (o mesmo tratamento de chips editáveis na coluna "Municípios" de `/campanha/dobradinhas` — hoje um número morto, `municipalityCount`; ~0,75–1 dia, herda a lib promovida pelo B34 se ele já tiver landado, senão promove ela mesma); **B36** (a coluna "Lideranças" da mesma lista ganha o mesmo tratamento — chips nomeados + Popover + busca + auto-save por delta, reusando sem alteração a mutação que o B31 desenha para o lado oposto da mesma relação; ~0,5–1,5 dia dependendo de quem entre B31/B36 chega primeiro na corrida de extração da célula compartilhada); **B33** (sort + filtro no header de `/campanha/dobradinhas` — pedido explícito de reuso: Nome/Partido no header e filtro por Partido, sem duplicar o chrome de `/campanha/municipios`; ~0,5–1,5 dia dependendo de quem chega primeiro na corrida de extração do chrome compartilhado com B29/B21; entra perto de B29 por reusar a mesma peça); **B29** (sort + filtro no header da mesma lista — é a tela onde o assessor recebe a carteira e precisa recortar "quem ainda está a abordar aqui" / "quem ainda não tem acesso ao app" antes de convidar; ~1,5–2 dias porque paga também a extração do head compartilhado, então entra depois dos quick wins — mas antes se B21 for adiado, para o head nascer compartilhado); ~~**B17**~~ ✓ (seletor de colunas) / ~~**B18**~~ ✓ (filtros salvos + atalho sidebar) entregues 2026-07-28; fill-in **renomear coluna Cobertura** (header curto na fila — quarto de dia; glossário/KPI intactos); fill-in **remover coluna Tipo** (densidade morta na fila — quarto/meio dia; Salvador via slugs cobre ZE); ~~fill-in **remover lista de zonas do Início**~~ ✓ (entregue 2026-07-28); fill-in **selecionados no topo nos filtros do header** (desmarcar sem caçar checkbox; helper puro que B21 herda — quarto de dia); **C13 ✓** (renomeado "Plano de Ação" → "Atividade" em toda a vertical, incluindo a migration data-preserving — entregue 2026-07-25); gate de adoção: sinal Little Hire — ≥1 update espontâneo até 30/07, cobrança da tabela dispensada (planilhas já em `docs/sheets/`) — acompanhamento em [IMPROVE-APP-PLAN.md](IMPROVE-APP-PLAN.md); **R6** critique/polish; **E9 ✓** entregue no mesmo dia; **B14 ✓** (atalho geo entregue 2026-07-26); Onda 0 jurídica em paralelo (externa). **Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; ~~**E9** fila de alocação~~ (entregue 2026-07-24, antecipada da janela 2); ~~**E16** dossiê do município~~ (entregue 2026-07-25, antecipado da janela 2); D2 se sobrar folga. @@ -358,9 +391,9 @@ Paralelizáveis agora: ~~**B43**~~ ✓ (chassis UX-1 — entregue 2026-07-29), ~ ### Cortes seguros / não cortáveis -**Não cortáveis:** Onda 0 (jurídico/Consent); **B44–B45** chassis UX-1 (porta de entrada ação-primeiro — cortar = continuar sem botões no Início até 03/08); ~~**B43**~~ ✓ (entregue 2026-07-29); ~~**E4R** seed da planilha~~ (entregue 2026-07-24 — ainda aplicar em produção após smoke); ~~**E8** conta da cadeira~~ (entregue 2026-07-24); ~~**C12** registro-fundação~~ (entregue 2026-07-24); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**E9** fila de alocação~~ (entregue 2026-07-24 — era o mínimo de "inteligência, não planilha"); ~~**B19** gerenciar assessores~~ (entregue 2026-07-24 — CG/candidato não entram em `/admin`); **B39** lembrar de mim / sessão longa (default de 2 h derruba sessão no onboarding/campo — atrito diário de adoção). +**Não cortáveis:** Onda 0 (jurídico/Consent); **B45–B48** chassis UX-1 + busca mínima (catálogo → thumb-zone → input → Municípios — cortar = Início sem ações montadas / sem achar município até 03/08); ~~**B44**~~ ✓; ~~**B43**~~ ✓ (entregue 2026-07-29); ~~**E4R** seed da planilha~~ (entregue 2026-07-24 — ainda aplicar em produção após smoke); ~~**E8** conta da cadeira~~ (entregue 2026-07-24); ~~**C12** registro-fundação~~ (entregue 2026-07-24); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**E9** fila de alocação~~ (entregue 2026-07-24 — era o mínimo de "inteligência, não planilha"); ~~**B19** gerenciar assessores~~ (entregue 2026-07-24 — CG/candidato não entram em `/admin`); **B39** lembrar de mim / sessão longa (default de 2 h derruba sessão no onboarding/campo — atrito diário de adoção). -**Cortes seguros** (se o prazo apertar, nesta ordem): **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E12** camada TI (rollup manual por lista; **E17** já dá a leitura regional básica no Início); **E13** planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); **E15** backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter a fila E9 ✓ sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); ~~**E14** níveis~~ (entregue 2026-07-27 — `priority` continua, intocado); ~~**B8 F2** polígonos~~ (entregue 2026-07-26 — o mapa não é mais agregado em Salvador); **D2** push (adiar); **A6**; ~~**B14** município mais próximo~~ (entregue 2026-07-26); ~~**B38** sidebar recolhido em tablet~~ (entregue 2026-07-26); ~~**B40** login biométrico~~ (entregue 2026-07-28 — senha + B39 ✓ continuam, e cortar teria custado só o atalho de digital/Face ID; era o mais caro e o primeiro a cair desta leva B39–B42); **B42** polimento mobile da lista de municípios (Popover e card longos continuam usáveis — cortar custa conforto de teclado e densidade no celular); ~~**B41** scroll horizontal + sticky~~ (entregue 2026-07-27) (a tabela continua legível com zoom/pan da página — cortar custa o overflow em tablet; cai **depois** de B42 porque o bug de layout em mesa é mais caro que o polish mobile); ~~**B18** filtros salvos~~ ✓ entregue (lista + filtros manuais continuam; Visitados cobrem “voltar”); **B21** página dos Territórios de Identidade (o painel do E17 ✓ no Início continua entregando a leitura regional) — **B25** cai junto por definição (sem destino, sem link); ~~**B17** seletor de colunas~~ ✓ entregue (tabela completa continua; scroll horizontal); ~~**B22** explicação no header~~ (entregue 2026-07-26); ~~**B23** tooltip nas células~~ (entregue 2026-07-26); ~~**B24** auto-save + justificativa na Tendência~~ (entregue 2026-07-26); **B27** combobox + chips nos Assessores (a lista de checkboxes com "Salvar" continua atribuindo — cortar custa a mesma coerência do B24 mais o tempo do CG procurando nomes numa coluna durante o onboarding; corte junto com B24, já que dividem o endpoint); **B28** e-mail + celular na lista de lideranças (a ficha `/liderancas/[id]` continua mostrando o contato — cortar custa o gesto de copiar/abrir ZAP na varredura; quick win barato de cortar e de fazer); ~~**B30** convite de completar cadastro por WhatsApp na lista~~ (entregue 2026-07-26); **B31** coluna de dobradinhas nas lideranças (o `RelationMultiSelect` da ficha interna continua vinculando — cortar custa o preenchimento em lote do onboarding, uma ficha aberta por liderança, e mantém o vínculo invisível na varredura; corte junto com B27, já que dividem a peça de célula, e antes de B29 só se a mesa já tiver preenchido as dobradinhas à mão); ~~**B32** editar Status de apoio na célula~~ (entregue 2026-07-26); **B34** chips editáveis de municípios na lista de lideranças (o texto simples `row.municipalityNames.join(', ')` continua legível e a ficha `/liderancas/[id]` continua vinculando município via formulário — cortar custa o link direto ao município, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma peça de célula editável, e antes de B29 pelo mesmo motivo de coerência); **B37** chips editáveis de municípios na lista de dobradinhas (o número `municipalityCount` continua legível e a ficha de município continua vinculando dobradinha pelo `MunicipalityStrategyForm` — cortar custa o link direto, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha do município; corte junto com B34, já que dividem a mesma lib e a mesma decisão de container); ~~**B36** coluna de lideranças na lista de dobradinhas~~ (entregue 2026-07-26; a contagem `leadershipCount` era o fallback e a ficha `/liderancas/[id]` continua vinculando dobradinha via formulário — cortar custa o link direto à liderança, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma mutação/schema/lock); ~~**B33** sort + filtro no header das dobradinhas~~ (entregue 2026-07-26); **B29** sort + filtro no header das lideranças (a busca por nome continua funcionando e o escopo do assessor já reduz a lista — cortar custa o recorte por status/setor/município no onboarding da rede e mantém o header rico como cópia única de municípios; por ser o item mais caro desta faixa (~1,5–2 dias com a extração do head), cai **antes** do B26, que é o que sustenta o gate de adoção); **B26** registrar sinal na célula (o formulário completo em `/[slug]?tab=updates` continua sendo o caminho — cortar custa o laço da fila do E9 ✓ e o registro em campo pelo celular, que é justamente onde o gate de adoção se decide; por isso cai **depois** de B22/B23/B24); ~~**C13** renomear Plano de Ação → Atividade~~ (entregue 2026-07-25); **B20** prioritários no Início (lista `?priority=alta` continua; só perde o atalho — mas a seção atual mente, então cortar = conviver com o bug); fill-in **renomear coluna Cobertura** (a lista continua legível com o rótulo longo — corte = header mais largo); fill-in **remover coluna Tipo** (a lista continua completa; só mantém densidade morta — corte = conviver com a coluna); ~~fill-in **remover lista de zonas do Início**~~ ✓ (entregue 2026-07-28 — o mapa B8 F2 ✓ continua abrindo cada ZE); ~~**B16** filtros no header~~ (entregue 2026-07-25); ~~**B15** sort por coluna~~ (entregue 2026-07-24); ~~**A11**/**E17**~~ (entregues 2026-07-24); ~~**E18** documentação de conceitos~~ (entregue 2026-07-24); ~~**E16** dossiê~~ (entregue 2026-07-25); demais débitos/fill-ins. +**Cortes seguros** (se o prazo apertar, nesta ordem): **B55** ações WA/share na busca (linha continua clicável); **B54** grid/cap (empilhar sempre); **B51**/**B53**/**B52**/**B50**/**B49** grupos secundários da busca (nessa ordem — Municípios B48 fica); **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E12** camada TI (rollup manual por lista; **E17** já dá a leitura regional básica no Início); **E13** planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); **E15** backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter a fila E9 ✓ sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); ~~**E14** níveis~~ (entregue 2026-07-27 — `priority` continua, intocado); ~~**B8 F2** polígonos~~ (entregue 2026-07-26 — o mapa não é mais agregado em Salvador); **D2** push (adiar); **A6**; ~~**B14** município mais próximo~~ (entregue 2026-07-26); ~~**B38** sidebar recolhido em tablet~~ (entregue 2026-07-26); ~~**B40** login biométrico~~ (entregue 2026-07-28 — senha + B39 ✓ continuam, e cortar teria custado só o atalho de digital/Face ID; era o mais caro e o primeiro a cair desta leva B39–B42); **B42** polimento mobile da lista de municípios (Popover e card longos continuam usáveis — cortar custa conforto de teclado e densidade no celular); ~~**B41** scroll horizontal + sticky~~ (entregue 2026-07-27) (a tabela continua legível com zoom/pan da página — cortar custa o overflow em tablet; cai **depois** de B42 porque o bug de layout em mesa é mais caro que o polish mobile); ~~**B18** filtros salvos~~ ✓ entregue (lista + filtros manuais continuam; Visitados cobrem “voltar”); **B21** página dos Territórios de Identidade (o painel do E17 ✓ no Início continua entregando a leitura regional) — **B25** cai junto por definição (sem destino, sem link); ~~**B17** seletor de colunas~~ ✓ entregue (tabela completa continua; scroll horizontal); ~~**B22** explicação no header~~ (entregue 2026-07-26); ~~**B23** tooltip nas células~~ (entregue 2026-07-26); ~~**B24** auto-save + justificativa na Tendência~~ (entregue 2026-07-26); **B27** combobox + chips nos Assessores (a lista de checkboxes com "Salvar" continua atribuindo — cortar custa a mesma coerência do B24 mais o tempo do CG procurando nomes numa coluna durante o onboarding; corte junto com B24, já que dividem o endpoint); **B28** e-mail + celular na lista de lideranças (a ficha `/liderancas/[id]` continua mostrando o contato — cortar custa o gesto de copiar/abrir ZAP na varredura; quick win barato de cortar e de fazer); ~~**B30** convite de completar cadastro por WhatsApp na lista~~ (entregue 2026-07-26); **B31** coluna de dobradinhas nas lideranças (o `RelationMultiSelect` da ficha interna continua vinculando — cortar custa o preenchimento em lote do onboarding, uma ficha aberta por liderança, e mantém o vínculo invisível na varredura; corte junto com B27, já que dividem a peça de célula, e antes de B29 só se a mesa já tiver preenchido as dobradinhas à mão); ~~**B32** editar Status de apoio na célula~~ (entregue 2026-07-26); **B34** chips editáveis de municípios na lista de lideranças (o texto simples `row.municipalityNames.join(', ')` continua legível e a ficha `/liderancas/[id]` continua vinculando município via formulário — cortar custa o link direto ao município, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma peça de célula editável, e antes de B29 pelo mesmo motivo de coerência); **B37** chips editáveis de municípios na lista de dobradinhas (o número `municipalityCount` continua legível e a ficha de município continua vinculando dobradinha pelo `MunicipalityStrategyForm` — cortar custa o link direto, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha do município; corte junto com B34, já que dividem a mesma lib e a mesma decisão de container); ~~**B36** coluna de lideranças na lista de dobradinhas~~ (entregue 2026-07-26; a contagem `leadershipCount` era o fallback e a ficha `/liderancas/[id]` continua vinculando dobradinha via formulário — cortar custa o link direto à liderança, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma mutação/schema/lock); ~~**B33** sort + filtro no header das dobradinhas~~ (entregue 2026-07-26); **B29** sort + filtro no header das lideranças (a busca por nome continua funcionando e o escopo do assessor já reduz a lista — cortar custa o recorte por status/setor/município no onboarding da rede e mantém o header rico como cópia única de municípios; por ser o item mais caro desta faixa (~1,5–2 dias com a extração do head), cai **antes** do B26, que é o que sustenta o gate de adoção); **B26** registrar sinal na célula (o formulário completo em `/[slug]?tab=updates` continua sendo o caminho — cortar custa o laço da fila do E9 ✓ e o registro em campo pelo celular, que é justamente onde o gate de adoção se decide; por isso cai **depois** de B22/B23/B24); ~~**C13** renomear Plano de Ação → Atividade~~ (entregue 2026-07-25); **B20** prioritários no Início (lista `?priority=alta` continua; só perde o atalho — mas a seção atual mente, então cortar = conviver com o bug); fill-in **renomear coluna Cobertura** (a lista continua legível com o rótulo longo — corte = header mais largo); fill-in **remover coluna Tipo** (a lista continua completa; só mantém densidade morta — corte = conviver com a coluna); ~~fill-in **remover lista de zonas do Início**~~ ✓ (entregue 2026-07-28 — o mapa B8 F2 ✓ continua abrindo cada ZE); ~~**B16** filtros no header~~ (entregue 2026-07-25); ~~**B15** sort por coluna~~ (entregue 2026-07-24); ~~**A11**/**E17**~~ (entregues 2026-07-24); ~~**E18** documentação de conceitos~~ (entregue 2026-07-24); ~~**E16** dossiê~~ (entregue 2026-07-25); demais débitos/fill-ins. ## Bloqueadores atuais