docs(plans): register B109–B111 mobile drawer and Início polish

Capture confirmed product feedback as trackable plans linked to #148–#150.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-08-01 17:07:04 -03:00
parent 97f7423f81
commit 1a4b5b6273
3 changed files with 383 additions and 0 deletions

View file

@ -0,0 +1,150 @@
# Bottom drawer — polimento dock + busca fullscreen + excluir entidade atual
Status: registrado
Atualizado em: 2026-08-01
Issue: #148
Priority: P1
Model: composer-2.5
Impeccable: C — chrome `CampaignQuickActionsDrawer` (geometria dock + modo busca)
Appetite: ~1–1,5d eng; snap + CSS botões + filtro de hits; sem migration
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (Clarity under pressure; Feel the action; Continuity) / `DESIGN.md` · B79/B91/B100/B105 · Início `focused` (B65) · tema `data-theme='campaign'`.
Na implementação (`work-issue`): craft compacto → critique → polish (viewport ~390; detalhe município + lista).
Brief compacto:
- **Persona / contexto:** CG/assessor no celular **fora do Início** — dock para ações + busca; ao digitar/focar a busca, precisa de tela inteira sem o strip competindo.
- **Job principal:** (1) ler rótulos das ações sem corte; (2) busca com dock apertado na base; (3) ao focar a busca, drawer sobe cobrindo o header, só handle + input + resultados; (4) não sugerir/listar a entidade da página atual.
- **Estratégia de cor:** Restrained.
- **Edit where you see:** não — launchers / navegação.
- **Anti-goals:** scrim modal no dock idle; segundo FAB; redesenhar catálogos B80–B90; wizard return (→ **B110**); polish do Início (→ **B111**).
### Wireframe (texto)
**1) Dock idle (não-Início):**
```text
┌─ /campanha/municipios/foo ─────────────────────┐
│ header + conteúdo │
├─ drawer DOCK ──────────────────────────────────┤
│ ═ handle │
│ [● Ajustar] [● Mudar] [● …] ← labels 2 linhas │
│ tendência cabe inteira; botão um pouco +largo│
│ ┌ Buscar na campanha ────────────────────────┐ │
│ └────────────────────────────────────────────┘ │
│ ← gap curto até safe-area / bottom │
└────────────────────────────────────────────────┘
```
**2) Busca focada / ativa (fullscreen):**
```text
┌─ drawer FULL (cobre header) ───────────────────┐
│ ═ handle │
│ ┌ Buscar na campanha ────────────────────────┐ │
│ └────────────────────────────────────────────┘ │
│ Sugestões / resultados (sem a entidade atual) │
│ … │
│ … │
└────────────────────────────────────────────────┘
Fora: strip de ações oculto; página atrás opaca
(não-modal continua — sem scrim que bloqueie? ver Decisões).
```
## Dados → decisão → apresentação
Dados: N/A como métrica nova — hits de `POST /campanha/home-search` já existentes; só **filtro de exclusão** da entidade do contexto.
## Contexto
Pós **B100/B105** o drawer tem dock + collapsed + labels `line-clamp-2`. Feedback de campo (2026-08-01):
1. **Gap** entre a barra de busca e o fundo da tela **grande demais** no dock.
2. Labels cortados na horizontal (ex. “Mudar tendênci”) — `CampaignHomeActionButton` fixa `w-[4.75rem]`.
3. Foco na busca deve espelhar o ritual do Início (`CampaignHomeLayout` `focused`): strip some, superfície sobe — mas no drawer precisa **cobrir o header** também.
4. Empty state / resultados ainda incluem a entidade da página atual (município/liderança/assessor/…), o que é ruído quando já se está nela.
## Objetivos
- Reduzir o espaçamento vertical **busca → bottom** no snap dock (gap do stack + padding do handle / safe-area); dock continua a caber strip + busca.
- Largura dos botões da strip **o suficiente** para o maior rótulo do catálogo em **duas linhas**, centralizado; retirar padding horizontal residual do controle se ainda existir; sem encolher hit target do círculo (<44 px).
- Ao **focus** do input de busca (e enquanto `uiFocused` / query ativa): snap **fullscreen** (`1` / `100dvh` — pin no craft); **ocultar** a strip de ações; sheet cobre o viewport inclusive `CampaignMobileTopBar`.
- Ao blur com query vazia / clear: voltar ao dock (ou collapsed se o scroll da página já tiver recolhido — ver Questões).
- Filtrar sugestões e resultados da busca para **excluir** a entidade do `CampaignQuickActionContext` da rota atual (slug/id por tipo).
- Guardrails: sem migration / Consent / escrita; Início fora deste drawer; leader lockdown intacto.
- **Tracer bullet:** e2e mobile em `/campanha/municipios/[slug]` — dock com label “Mudar tendência” legível → focus busca → strip ausente + handle no topo do viewport → hit do município atual **não** aparece nas sugestões.
## Decisões travadas
- **3º snap `full` só sob busca focada/ativa; idle continua dock|collapsed (B100).** Espelha Início `focused`. **Rejeitado:** sempre full; crescer só o dock sem snap novo (não cobre header de forma confiável).
- **Fullscreen não-modal (sem bloquear pointer na página) no appetite v1 — sheet cobre visualmente o chrome.** Overlay `modal={false}` já é o contrato B79; se o critique exigir scrim no full, Adiado. **Rejeitado:** tornar o drawer modal só no focus (muda a família de gesto / dismiss).
- **Exclusão client-side nos grupos de resultado + payload suggest, keyed pelo contexto da página** (`municipalitySlug` / `leadershipId` / `organizationSlug` / `activitySlug` / `demandSlug` / advisor id se houver). **Rejeitado:** mudar o ranking server-side do home-search neste item (blast maior; Início não precisa excluir “si mesmo”).
- **Botão: aumentar `w-*` (ou `min-w`) o mínimo que caiba “Mudar tendência” / “Atualizar liderança” em 2 linhas; não multi-width por label.** **Rejeitado:** `w-auto` solto (quebra snap/pan da strip); fonte menor.
- **Item novo (B109), não reabrir B105/B100.** Continuação de polimento. **Rejeitado:** editar só as-built antigo sem Issue.
- **i18n:** ids `full` snap / `excludeCurrentEntity`; copy pt-BR intacta.
## Questões em aberto
- **Após blur da busca com query vazia, voltar a qual snap?** **Opções:** A) sempre dock | B) dock se `scrollTop` baixo, senão collapsed. **Recomendação:** **B** — respeita o ritual B100 de leitura. _(assumido)_
- **Excluir também visitados recentes / geo suggest quando forem a entidade atual?** **Opções:** A) sim, qualquer hit com o mesmo id/slug | B) só grupo “Sugestões” server. **Recomendação:** **A**. _(assumido)_
## Abordagem proposta
```mermaid
flowchart TB
Focus["input focus / uiFocused"] --> Full["snap FULL"]
Full --> Hide["strip hidden"]
Full --> Cover["sheet cobre header"]
Ctx["QuickActionContext"] --> Filter["excludeCurrentEntity(hits)"]
Filter --> Groups["HomeSearch*Group"]
Dock["snap DOCK"] --> Gap["menos gap busca↔bottom"]
Dock --> Width["ActionButton w- maior"]
```
Componentes:
- **`campaignQuickActionSnap.ts`**: adicionar `QUICK_ACTIONS_SNAP_FULL`; helpers `quickActionsSnapIsFull` / `isDock`.
- **`CampaignQuickActionsDrawer.tsx`**: reagir a `uiFocused` (já em `HomeSearchContext` no host) → `setSnapPoint(FULL)`; ocultar strip; ordem visual handle → busca → resultados no full; apertar padding/gap no dock.
- **`CampaignQuickActionsHost` / scroll peek**: peek height no full = full viewport (ou não reservar padding absurdo no content scroll enquanto full).
- **`CampaignHomeActionButton.tsx`**: largura do controle; pin visual/unit se houver snapshot de classe.
- **Filtro:** helper puro `excludeHomeSearchHitForContext(hit, context)` em `lib/`; aplicar nos grupos ou no merge do `HomeSearchResultsContext` quando montado sob o drawer (prop/`data-exclude` via provider leve). Evitar poluir o Início.
- **Migration:** Sem migration, sem collection, sem server action nova.
## Dependências
- Soft: B100 ✓, B105 ✓, B91 ✓, B79 ✓. Nenhuma dura.
- Paralelo: **B110** (return path wizard), **B111** (Início strip/delta) — sem `depends` duro.
## Não escopo
- Redirect pós-wizard → **B110**.
- Faixas brancas / delta flat do Início → **B111**.
- Redesign do catálogo de ações / novos verbos.
- Busca leader / rotas fora do drawer staff.
## Rabbit holes
- **Reescrever o primitivo `Drawer.tsx` para todos os sheets.** Mitigação: override só no chassis quick-actions; Base UI snap já usado.
- **Segundo `HomeSearchProvider` / fork do suggest API.** Mitigação: filtro no client com contexto já sincronizado.
- **Animar header aparte do snap.** Mitigação: cobrir com o sheet; não esconder o top bar via layout.
## Adiado com gatilho
- **Scrim no snap full.** Revisitar se critique de campo disser que o conteúdo atrás distrai durante a busca.
- **Exclusão server-side no `home-search`.** Revisitar se o payload vier grande demais para filtrar no client.
## Referências
- GitHub Issue #148 (spec + frontmatter `id/depends/priority/model`)
- `src/components/campaign/shell/CampaignQuickActionsDrawer.tsx` — chassis
- `src/lib/campaignQuickActionSnap.ts` — snaps
- `src/components/campaign/dashboard/CampaignHomeActionButton.tsx` — `w-[4.75rem]`
- `src/components/campaign/dashboard/CampaignHomeLayout.tsx` — precedente `focused`
- `src/lib/campaignQuickActionContext.ts` — entidade atual
- `docs/plans/bottom-drawer-peek-acoes-busca.md` (B100), `bottom-drawer-handle-scroll-peek.md` (B105)
- AGENTS.md — Campaign auth; naming EN / copy pt-BR
- `PRODUCT.md` / `DESIGN.md` — Field Desk / Continuity

View file

@ -0,0 +1,117 @@
# Início — strip edge-to-edge de verdade + delta flat silencioso
Status: registrado
Atualizado em: 2026-08-01
Issue: #150
Priority: P1
Model: composer-2.5
Impeccable: B — `CampaignHomeLayout` / `CampaignHomeSummary`
Appetite: ~0,25–0,5d eng; CSS overflow + conditional delta; sem migration
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` · B57 delta · B99/B101 bleed · tema `campaign`.
Na implementação: craft compacto → critique → polish (iPhone estreito).
Brief:
- **Persona:** CG no Início mobile — strip deve nascer na borda; Δ só quando houve movimento.
- **Job principal:** (1) zero “trilho” branco nas laterais da strip; (2) sem traço/zero quando Δ = 0 nos últimos 7 dias.
- **Anti-goals:** mexer no drawer não-Início (**B109**); redesenhar o bloco de cobertura.
### Wireframe (texto)
```text
┌─ /campanha mobile ─────────────────────────────┐
│ Votos estimados │
│ 12.345 ← sem “−” / “0” / — │
│ quando Δ flat │
│ (se Δ≠0: ↑ 120 / ↓ 80 + “nos últimos 7 dias”)│
│ │
│[●][●][●][●]… →→→ ← cola nas bordas L/R │
│ ▲ sem faixa branca lateral │
│ ┌ Buscar … ──────────────────────────────────┐ │
└────────────────────────────────────────────────┘
```
## Dados → decisão → apresentação
- **Vou apresentar dados?** Sim — o Δ 7d (**B57**) já existe; este item só muda a **forma no estado flat**.
- **Decisões desbloqueadas:** CG: “houve movimento na semana?” — se não, não ocupar o campo visual com ruído.
- **Forma escolhida:** seta + magnitude **somente** se `delta !== 0 && delta !== null`; flat e unavailable → **omitir** o chip inteiro (e a linha “nos últimos 7 dias”). **Rejeitado:** manter `MinusIcon` / “0” / em-dash como “sinal de estabilidade” (produto pediu vazio).
- **Profile:** escalar assinado já carregado em `homeSummaryDelta`.
- **Anti-goals de dado:** não inventar sparkline; não mudar a janela de 7 dias.
## Contexto
**Bleed (B99/B101):** `CampaignHomeLayout` já aplica `-mx-4 w-[calc(100%+2rem)]` em `home-actions`, mas o wrapper `HomeChromeRetractionShell` usa **`overflow-hidden`** no filho interno (necessário à animação de retraction). Isso **clipa** o bleed e deixa as faixas do `p-4` do `CampaignContentScroll` visíveis como “barras brancas” laterais. Diagnóstico preferido vs “margin na strip” ou `overflow: visible` solto no scroller.
**Delta (B57):** `HomeSummaryDelta` renderiza `MinusIcon` + magnitude `"0"` quando `delta === 0`, e `"—"` quando `null`. Produto (2026-08-01): no estado **sem mudança**, **não mostrar nada**.
## Objetivos
- Strip de ações no Início mobile **visualmente edge-to-edge** (primeiro/último círculo na borda ou com peek de pan, sem gutter branco do padding da página).
- Quando `homeSummaryDelta === 0` **ou** `null`: não renderizar o span do Δ nem o caption do período; manter só o total.
- Quando `delta > 0` / `< 0`: setas + magnitude + caption inalterados.
- Unit: pins em `campaignHomeSummaryDelta` / render helper; layout pin se existir spec de classes.
- Guardrails: sem migration; desktop `md:` pode manter gutter; pan B67 intacto.
## Decisões travadas
- **Corrigir o clip do bleed (overflow na shell de retraction), não remover `p-4` da página.** **Rejeitado:** `p-0` global; `overflow: visible` cego no `campaign-content-scroll` (risco de spill horizontal da página).
- **Técnica recomendada:** tirar `overflow-hidden` do wrapper interno **só** do slot `home-actions-chrome` (ou estrutura em duas camadas: clip só na animação de altura, não no eixo X do bleed); validar que a retraction `grid-rows` continua sem flash. **Rejeitado:** duplicar a strip fora do scroll.
- **Flat/unavailable = omitir UI do Δ** (não `Minus` / não `—` / não `0`). **Rejeitado:** traço como “estável”; sr-only com magnitude zero (desnecessário se não há chip).
- **Item novo (B111), continuação de B99/B57 — não reabrir Issues fechadas.**
- **i18n:** helpers existentes; copy do período só quando Δ ≠ 0.
## Questões em aberto
- **Unavailable (`null`, sem histórico 7d): também omite?** **Opções:** A) omitir igual ao flat | B) mostrar “—” só nesse caso. **Recomendação:** **A** — produto pediu vazio na ausência de mudança útil; buraco de histórico ≠ seta. _(assumido)_
## Abordagem proposta
```mermaid
flowchart TB
Shell["home-actions-chrome"] --> Bleed["-mx-4 sem clip X"]
Bleed --> Strip["CampaignHomeActionStrip"]
Delta["homeSummaryDelta"] -->|!==0| Chip["seta + mag + caption"]
Delta -->|0 ou null| Hide["não renderiza"]
```
Componentes:
- **`CampaignHomeLayout.tsx`**: ajustar `HomeChromeRetractionShell` / slot actions para permitir bleed horizontal.
- **`CampaignHomeSummary.tsx`**: early-return em `HomeSummaryDelta` quando flat/unavailable; condicionar caption.
- **`campaignHomeSummaryDelta.ts`**: opcional helper `shouldShowHomeSummaryDelta(delta)`; atualizar aria tests.
- **Migration:** Sem migration.
## Dependências
- Soft: B57 ✓, B99 ✓, B101 ✓. Nenhuma dura.
## Não escopo
- Drawer não-Início → **B109**.
- Wizard return → **B110**.
- Mudar fórmula do snapshot / janela 7d.
## Rabbit holes
- **Refatorar retraction para Framer/WAAPI.** Mitigação: CSS grid rows já funciona; só isolar overflow.
- **Recalcular Δ no client.** Mitigação: só apresentação.
## Adiado com gatilho
- **Caption “sem variação” em tooltip no total.** Revisitar se acessibilidade pedir explicação sem chip visível.
## Referências
- GitHub Issue #150 (spec + frontmatter `id/depends/priority/model`)
- `src/components/campaign/dashboard/CampaignHomeLayout.tsx` — `-mx-4` + `overflow-hidden`
- `src/components/campaign/dashboard/CampaignHomeSummary.tsx` — `MinusIcon` / `—`
- `src/lib/campaignHomeSummaryDelta.ts`
- `docs/plans/strip-acoes-edge-gap-inicio.md` (B99), `delta-7-dias-estimativa-inicio.md` (B57)
- AGENTS.md — naming
- `PRODUCT.md` / `DESIGN.md`

View file

@ -0,0 +1,116 @@
# Wizard — ao terminar, voltar à página de origem (não ao Início)
Status: registrado
Atualizado em: 2026-08-01
Issue: #149
Priority: P1
Model: composer-2.5
Impeccable: B — navegação pós-cadeia nos wizards `/campanha/acoes`
Appetite: ~0,5–1d eng; query `from` allowlisted + `wizardChainContinueHref`; sem migration
Responsável: —
## Design (Impeccable)
Âncoras: `PRODUCT.md` (Continuity; Ação → Local → Quem) / `DESIGN.md` · B98 cadeia · B75/B96 chrome · tema `campaign`.
Na implementação: craft compacto → critique → polish (fluxo município + um fluxo liderança).
Brief:
- **Persona:** staff dispara ação rápida **na página em que está** (detalhe de município, liderança, lista…).
- **Job principal:** ao concluir (ou esgotar a cadeia B98), **voltar para essa página**, não para `/campanha`.
- **Anti-goals:** segundo sistema de deep-link; gravar origem em cookie; mudar a matriz de encadeamento B98.
### Wireframe (texto)
```text
/campanha/municipios/foo
→ strip: Mudar tendência
→ /acoes/mudar-tendencia?municipio=foo&from=/campanha/municipios/foo
→ [cadeia B98…]
→ fim → /campanha/municipios/foo ← não /campanha
/campanha (Início)
→ mesma ação sem from (ou from=/campanha)
→ fim → /campanha
```
## Dados → decisão → apresentação
Dados: N/A — só navegação.
## Contexto
**B98** fechou a cadeia de ajustes e travou o fim em **Início** (`wizardChainContinueHref` → `CAMPAIGN_HOME`). Correto quando a entrada é o dock do Início. Incorreto quando a entrada é o **bottom drawer** (ou qualquer CTA) numa página de entidade: o usuário perde o contexto e precisa reencontrar o município.
Pedido de produto (2026-08-01): o fluxo de ação deve **sempre** redirecionar, ao final, para a página da qual foi disparado — município, liderança, assessor, lista, etc.
## Objetivos
- Propagar um **return path** allowlisted desde o href da ação rápida (e CTAs equivalentes) até o fim da cadeia / dismiss X da principal.
- `wizardChainContinueHref` (e pontos que hoje hardcodam `CAMPAIGN_HOME` ao esgotar a fila) usam esse path; default permanece Início quando ausente/inválido.
- X / cancel da **principal** também volta à origem (não só “sucesso”).
- Unit pins: fim com `from` → origem; sem `from` → Início; `from` fora da allowlist → Início (fail-closed).
- Guardrails: sem migration; access intacto; open-redirect impossível (só paths internos `/campanha/...`).
## Decisões travadas
- **Query `from` (path absoluto interno allowlisted), não `sessionStorage`.** Sobrevive a refresh e é auditável na URL; B98 já usa query (`entry`, `municipio`). **Rejeitado:** só “se tem `municipio=` voltar ao detalhe” (falha em liderança/lista/assessor sem município); `document.referrer` (frágil cross-flow).
- **Allowlist:** pathname sob `/campanha` que **não** seja `/campanha/acoes/*` nem auth (`login`, `convite`, …) — helper puro `parseWizardReturnPath`. **Rejeitado:** qualquer string; URLs absolutas externas.
- **Supersede o “Fim: Início” do B98 quando `from` presente; Início continua o default.** **Rejeitado:** reabrir a matriz de elos; tela intermediária de sucesso obrigatória.
- **Quick actions / registry:** ao montar `href` com contexto de página, anexar `from=pathname` (pathname atual). Entrada pelo Início omite ou seta `/campanha`.
- **i18n:** `WIZARD_RETURN_PATH_QUERY_KEY = 'from'`; ids EN.
## Questões em aberto
- **Pular o último elo da cadeia: ainda honra `from`?** **Opções:** A) sim | B) Início. **Recomendação:** **A** — “terminar o ritual” = origem. _(assumido)_
- **Demandas / fluxos fora da cadeia B98?** **Opções:** A) mesmo `from` no redirect final | B) só cadeia. **Recomendação:** **A** onde já redirecionam para Início. _(assumido)_
## Abordagem proposta
```mermaid
flowchart LR
Page["pathname atual"] --> Href["action href + from"]
Href --> Chain["elos B98 (propagam from)"]
Chain -->|fila vazia| Ret["parseWizardReturnPath(from) ?? HOME"]
X["dismiss X principal"] --> Ret
```
Componentes:
- **`src/lib/campaignActionRoutes.ts`**: constante da query; `appendWizardReturnPath` / `parseWizardReturnPath` (allowlist).
- **`src/lib/wizardActionChain.ts`**: `wizardChainContinueHref(..., returnPath?)` — fim = returnPath ?? HOME; propagar `from` nos hrefs dos elos.
- **Registry / quick action builders** (`campaignQuickAction*.ts`): incluir `from` a partir do pathname (passado pelo host/registry).
- **Chrome wizard (X / skip último):** ler `from` da URL.
- **Unit:** `wizardActionChain` + parser allowlist.
- **Migration:** Sem migration.
## Dependências
- Soft: B98 ✓. Nenhuma dura.
- Paralelo a **B109** / **B111**.
## Não escopo
- Polimento visual do drawer → **B109**.
- Mudar ordem dos elos B98.
- Deep link pós-login.
## Rabbit holes
- **Histórico `router.back()` em vez de `from`.** Mitigação: back pode sair do app / pular elos; URL explícita.
- **Allowlist por enum de rotas.** Mitigação: prefixo `/campanha` + denylist `acoes`/auth basta neste appetite.
## Adiado com gatilho
- **Toast “Salvo — voltando a …”.** Revisitar se campo pedir confirmação verbal do destino.
## Referências
- GitHub Issue #149 (spec + frontmatter `id/depends/priority/model`)
- `src/lib/wizardActionChain.ts` — `CAMPAIGN_HOME` no fim
- `src/lib/campaignActionRoutes.ts` — builders de href
- `src/lib/campaignQuickActionRegistry.ts` + providers B80–B90
- `docs/plans/encadear-ajustes-wizard.md` (B98)
- AGENTS.md — naming; Campaign auth
- `PRODUCT.md` — Continuity