diff --git a/docs/plans/bottom-drawer-busca-fullscreen-polimento.md b/docs/plans/bottom-drawer-busca-fullscreen-polimento.md new file mode 100644 index 00000000..e14427fd --- /dev/null +++ b/docs/plans/bottom-drawer-busca-fullscreen-polimento.md @@ -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 diff --git a/docs/plans/inicio-strip-bleed-delta-flat.md b/docs/plans/inicio-strip-bleed-delta-flat.md new file mode 100644 index 00000000..90c58431 --- /dev/null +++ b/docs/plans/inicio-strip-bleed-delta-flat.md @@ -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` diff --git a/docs/plans/wizard-retorno-pagina-origem.md b/docs/plans/wizard-retorno-pagina-origem.md new file mode 100644 index 00000000..a6204289 --- /dev/null +++ b/docs/plans/wizard-retorno-pagina-origem.md @@ -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