docs(roadmap): B66 busca no focus; município sticky no B59
Registra emenda ao B47 (limpeza e animação ao focar a busca) e absorve o lembrete de município visível no topo dos wizards no chassis B59.
This commit is contained in:
parent
47c693ab0c
commit
7dcfd2989f
6 changed files with 151 additions and 23 deletions
|
|
@ -43,7 +43,7 @@ Em scrollport com altura definida pelo flex do `SidebarInset`, filhos com só `m
|
|||
|
||||
- No viewport **< `md`**, com Início **não** focado (query B47 inativa): o **grupo** strip de ações + barra de busca fica colado ao limite inferior do conteúdo útil (acima da bottom nav / `pb-24` do scroll), sem gap morto entre o grupo e esse limite.
|
||||
- Em **`md+`**: comportamento visual inalterado (busca acima das ações, no topo do conteúdo).
|
||||
- Modo focado **B47** intacto: spacer/strip ocultos; input sobe; resultados abaixo.
|
||||
- Modo focado **B47/B66** intacto: spacer/strip ocultos; input sobe; resultados abaixo. **B66** (quando landar) dispara a limpeza no **focus** e anima a retração — este item só ancora o dock no idle.
|
||||
- Leader (só strip, sem busca) também ancora a strip no inferior no mobile.
|
||||
- Sem migration / collection / Consent / server action / mudança de URL.
|
||||
- Pin em unit (cadeia de classes / presença do dock) + checklist visual mobile (viewport ~390) no e2e existente ou asserção de layout leve se couber sem flake.
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Início blank (**B43**) + ações (**B44/B45**) + posição (**B46**). Pedido (2
|
|||
|
||||
- **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).
|
||||
- **Expand = query não vazia**, não focus-only. **Rejeitado:** sumir a strip no focus vazio (rouba o ritual de ações). **Supersedido 2026-07-29 por B66** ([modo-focado-busca-no-focus.md](modo-focado-busca-no-focus.md)): produto pediu limpeza **no focus** + animação de expansão; hits continuam gated por query.
|
||||
- **i18n:** `CampaignHomeSearch`, `homeSearchQuery`; label pt-BR “Buscar na campanha” (ou copy curta a fechar no craft).
|
||||
|
||||
## Questões em aberto
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# Chassis do wizard de campanha (shell + voltar)
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-07-29
|
||||
Atualizado em: 2026-07-29 — absorve lembrete de produto: município em atualização sempre visível no topo (centro, semi-discreto)
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B59 — UX-1 wizards)
|
||||
Impeccable: C — UI nova (shell de fluxo multi-passo sob `/campanha`)
|
||||
Appetite: ~0,75 dia eng; layout de passo + header mobile + slot de conteúdo; sem migration
|
||||
Appetite: ~0,75–1 dia eng; layout de passo + header mobile + slot de conteúdo + caption sticky do município; sem migration
|
||||
Responsável: —
|
||||
|
||||
## Design (Impeccable)
|
||||
|
|
@ -15,11 +15,11 @@ Na implementação (`implement-roadmap-item`): shape → craft → critique →
|
|||
|
||||
Brief compacto:
|
||||
|
||||
- **Persona / contexto:** CG/assessor no celular entre Zaps; um passo por vez, sem chrome de lista.
|
||||
- **Job principal:** enquadrar qualquer etapa de wizard (busca, votos, sinal…) com volta óbvia no mobile e conteúdo focável.
|
||||
- **Persona / contexto:** CG/assessor no celular entre Zaps; um passo por vez, sem chrome de lista — e sem esquecer **qual** município está atualizando enquanto edita votos/sinal/tendência.
|
||||
- **Job principal:** enquadrar qualquer etapa de wizard (busca, votos, sinal…) com volta óbvia no mobile, conteúdo focável e **contexto de município sempre legível** após a escolha.
|
||||
- **Estratégia de cor:** Restrained.
|
||||
- **Edit where you see:** não — fluxo linear; escrita nas etapas filhas.
|
||||
- **Anti-goals:** sidebar/nav do app competindo com o passo; seta “voltar” no desktop (lá é browser); segundo design system de wizard.
|
||||
- **Anti-goals:** sidebar/nav do app competindo com o passo; seta “voltar” no desktop (lá é browser); segundo design system de wizard; badge/chip gritante de município no meio do passo; repetir o nome do município no título de cada pergunta.
|
||||
|
||||
## Dados → decisão → apresentação
|
||||
|
||||
|
|
@ -34,18 +34,20 @@ Este item é o **frame** compartilhado — não a busca, nem o ajuste de votos,
|
|||
## Objetivos
|
||||
|
||||
- Componente shell (nome final no PR, ex. `CampaignWizardShell`) com: título/pergunta do passo, slot `children`, slot opcional de ação no topo direito (ex. “Pular…” do B63), **back control só em viewport mobile** (`md:hidden` ou equivalente) que navega ao passo anterior / Início conforme o fluxo.
|
||||
- **Contexto de município (pedido 2026-07-29):** quando o fluxo já tem município escolhido, o **nome** permanece visível no **limite superior central**, semi-discreto (`text-sm` / `muted-foreground` — craft), **sticky** no scroll do passo (não some ao rolar formulário longo). Ausente na etapa **B60** (ainda não há município). Não substitui o título/pergunta do passo.
|
||||
- Desktop: sem seta; documentar que `history.back` / rota anterior cobre o gesto.
|
||||
- Layout: uma coluna; conteúdo alinhado ao bottom em mobile quando a etapa pedir (B63 tipifica), top em `md+` — o shell expõe um prop de alinhamento (`contentAlign: 'start' | 'end'`) sem hardcodar o grid de sinais.
|
||||
- Rotas sob `/campanha/…` (recomendação UX-1 travada: rota, não Sheet — sobrevive a refresh/Zap). Este item define o layout e o header; as rotas concretas nascem com B60/B61/B63 ou com o wiring A1.
|
||||
- Sem migration / Consent / action de escrita.
|
||||
- a11y: seta com nome acessível contendo o verbo visível (“Voltar”); foco gerenciado ao trocar de passo (mínimo: foco no heading do passo).
|
||||
- a11y: seta com nome acessível contendo o verbo visível (“Voltar”); caption do município anunciável (`aria`-nomeada, não só cor); foco gerenciado ao trocar de passo (mínimo: foco no heading do passo).
|
||||
|
||||
## Decisões travadas
|
||||
|
||||
- **Voltar mobile = chrome do wizard; desktop = browser.** **Rejeitado:** seta também em `md+` (ruído; duplica gesto nativo); só `Link` “Cancelar” no rodapé (longe do polegar no topo).
|
||||
- **Wizard = rota**, não Drawer sobre o Início. **Rejeitado:** Sheet full-screen (refresh/Zap perde contexto; URL não compartilha passo). Fonte: gate UX-1 2026-07-29 + rascunho.
|
||||
- **Shell não grava estado de domínio** — só navegação/chrome. Estado de município/votos/sinal fica nas etapas ou num store mínimo do fluxo (Adiado com gatilho se 3º wizard pedir).
|
||||
- **i18n:** ids em inglês (`WizardShell`, `onBack`, `trailingAction`); copy pt-BR.
|
||||
- **Shell não grava estado de domínio** — só navegação/chrome. Estado de município/votos/sinal fica nas etapas ou num store mínimo do fluxo (Adiado com gatilho se 3º wizard pedir). O shell **recebe** o rótulo (`municipalityLabel?: string` / slot) — não resolve slug→nome sozinho.
|
||||
- **Município no topo = caption sticky semi-discreta, centro superior.** **Rejeitado:** A) só no título do passo (some da hierarquia quando a pergunta muda); B) chip/badge primário gritante (compete com CTA); C) link obrigatório para `/municipios/[slug]` na v1 (abandona o wizard no meio do ritual — Adiado com gatilho). Fonte: produto 2026-07-29.
|
||||
- **i18n:** ids em inglês (`WizardShell`, `onBack`, `trailingAction`, `municipalityLabel` / `contextCaption`); copy pt-BR.
|
||||
|
||||
## Questões em aberto
|
||||
|
||||
|
|
@ -58,13 +60,14 @@ flowchart TB
|
|||
Home["/campanha Início"] --> Route["rota de passo"]
|
||||
Route --> Shell["CampaignWizardShell"]
|
||||
Shell --> Back["Voltar md:hidden"]
|
||||
Shell --> Ctx["municipalityLabel sticky centro"]
|
||||
Shell --> Trail["trailingAction?"]
|
||||
Shell --> Body["children = etapa"]
|
||||
```
|
||||
|
||||
Componentes:
|
||||
|
||||
- **`CampaignWizardShell`** em `src/components/campaign/shared/` (ou `shell/`): header + main; reusa tokens/`Button` ghost para a seta (`ArrowLeft`).
|
||||
- **`CampaignWizardShell`** em `src/components/campaign/shared/` (ou `shell/`): header (back | caption município | trailing) + main; reusa tokens/`Button` ghost para a seta (`ArrowLeft`); caption com `sticky top-0` no scroller do passo (respeitar safe-area / não cobrir a seta).
|
||||
- **Hook/helper de passo** (opcional neste item): `useWizardBack({ previousHref })` — se <2 call sites, inline no shell.
|
||||
- **Migration:** Sem migration, sem collection, sem server action.
|
||||
|
||||
|
|
@ -75,18 +78,21 @@ Componentes:
|
|||
|
||||
## Não escopo
|
||||
|
||||
- Busca de município → **B60**. Ajuste de votos → **B61**. Modelo/UI de sinal → **B62**/ **B63**.
|
||||
- Busca de município → **B60** (é quem **escolhe**; o caption só aparece **depois**). Ajuste de votos → **B61**. Modelo/UI de sinal → **B62**/ **B63**.
|
||||
- Resumo final + CTA “Registrar atualização” do A1 → wiring do wizard A1 (após estas etapas).
|
||||
- Rascunho persistente cross-session → Adiado.
|
||||
- Modo focado da busca do Início no focus → **B66**.
|
||||
|
||||
## Rabbit holes
|
||||
|
||||
- **State machine genérica de wizard (XState / multi-route orchestrator).** **Mitigação:** shell burro + hrefs/query de passo por fluxo; extrair só no 3º wizard.
|
||||
- **App chrome (`CampaignSidebar`) dentro do wizard.** **Mitigação:** layout de `(app)` pode permanecer; visualmente o conteúdo é full-bleed de passo — não inventar segundo layout group sem medição.
|
||||
- **Segundo header sticky competindo com `CampaignPageShell`.** **Mitigação:** um único sticky no chrome do wizard; não sticky o título do passo também.
|
||||
|
||||
## Adiado com gatilho
|
||||
|
||||
- **Persistência de rascunho (município + ação) ao reabrir.** Revisitar quando a 1ª sessão real for interrompida por Zap e o CG reclamar (UX-1 § contrato #8).
|
||||
- **Caption do município como `Link` para a ficha.** Revisitar se a mesa pedir “abrir município” sem sair do fluxo (ex. Sheet de leitura) — v1 é texto só.
|
||||
|
||||
## Referências
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ Copy: zero “broker”, “campo sem captura”, “LQ” nos passos. Se precis
|
|||
| 2 | Que tipo? | Grid de tiles grandes + Drawer de info por tipo (**B63**); “Pular registro de sinal” no topo direito **só se** a origem ≠ ação `register-signal` |
|
||||
| 3 | Detalhe? | Texto livre; Skip (mesma regra); **Salvar** inferior direito (**B63**) |
|
||||
|
||||
Chrome de toda etapa: seta **Voltar** no topo esquerdo **só mobile**; desktop = browser (**B59**).
|
||||
Chrome de toda etapa: seta **Voltar** no topo esquerdo **só mobile**; desktop = browser (**B59**). Após escolher o município: **nome do município sticky no limite superior central**, semi-discreto (**B59** — absorvido 2026-07-29).
|
||||
|
||||
Opcional ao fim: “Isso muda a projeção de votos?” → entra em A1 / **B61**.
|
||||
|
||||
|
|
|
|||
116
docs/plans/modo-focado-busca-no-focus.md
Normal file
116
docs/plans/modo-focado-busca-no-focus.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Modo focado da busca no focus (não na digitação)
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-07-29
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B66 — UX-1 busca Início)
|
||||
Impeccable: B — encaixe de motion/estado no chrome do Início (`CampaignHomeLayout` / `CampaignHomeStaffChrome`) sob tema `campaign`
|
||||
Appetite: ~0,5–0,75 dia eng; estado `focused` ≠ `query.isActive` + transição CSS; sem migration
|
||||
Responsável: —
|
||||
|
||||
## Design (Impeccable)
|
||||
|
||||
Âncoras: `PRODUCT.md` (Clarity under pressure; Feel the action) / `DESIGN.md` · B47 `CampaignHomeSearch` / B46 layout · tema `data-theme='campaign'`.
|
||||
|
||||
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish.
|
||||
|
||||
Brief compacto:
|
||||
|
||||
- **Persona / contexto:** CG/assessor toca a busca no Início para achar um município; ainda não digitou — quer a tela limpa **já**, sem esperar a 1ª letra.
|
||||
- **Job principal:** ao focar o input, o resto do Início cede espaço com animação (como se a barra expandisse para ocupar a tela); resultados só quando a query passar o limiar B47/B48.
|
||||
- **Estratégia de cor:** Restrained — motion de layout/opacity, sem overlay escuro tipo modal.
|
||||
- **Edit where you see:** não — chrome de descoberta.
|
||||
- **Anti-goals:** modo focado só após digitar (decisão B47 supersedida); sumir o input; command palette ⌘K; `position:fixed` full-screen que compete com bottom nav / teclado.
|
||||
|
||||
## Dados → decisão → apresentação
|
||||
|
||||
Dados: N/A — chrome + motion; hits continuam em **B48–B55**.
|
||||
|
||||
## Contexto
|
||||
|
||||
**B47 ✓** entregou input + modo focado, com decisão travada: **expand = query não vazia** (rejeitou focus-only para “não roubar o ritual de ações”). Em produção visual, quem foca a barra ainda vê strip/resumo/dock até digitar — atrito no gesto de busca.
|
||||
|
||||
**Pedido de produto (2026-07-29):** limpar a tela **ao focar** a barra (não ao começar a digitar) e **animar** a redução do resto do conteúdo, como se a busca expandisse para tomar a tela.
|
||||
|
||||
Isto **emenda** B47 (não reabre B48+). Hits, debounce 250 ms e limiar ≥2 chars permanecem.
|
||||
|
||||
Código vivo: `focused = searchState.query.isActive` em [`CampaignHomeStaffChrome.tsx`](../../src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx); `CampaignHomeLayout` esconde spacer/strip com `focused`; contrato em [`campaignHomeSearchContract.ts`](../../src/lib/campaignHomeSearchContract.ts).
|
||||
|
||||
## Objetivos
|
||||
|
||||
- Separar **`uiFocused`** (input com foco **ou** query ativa) de **`query.isActive`** (debounced ≥ limiar — dispara fetch B48).
|
||||
- Ao **focus** do input: entrar em modo focado imediatamente — strip de ações, spacer B46/B65, resumo B56 (quando landar) e demais chrome do Início fora da região busca+resultados **retraem com transição** (altura/opacity/transform — “expandir a busca”), não sumir em hard cut.
|
||||
- Ao **blur** com query vazia (após trim): sair do modo focado e restaurar o chrome (mesma transição inversa).
|
||||
- Com query ativa: permanecer focado mesmo se o foco for para um hit (Tab / toque na lista) — igual espírito atual, sem reaparecer strip no meio da leitura.
|
||||
- Escape: limpa query (**B47**) e, se ficar vazia, sai do modo focado (blur ou `uiFocused=false`).
|
||||
- `prefers-reduced-motion: reduce` → sem animação (só estado final).
|
||||
- Soft: se **B65** já ancorou o dock no inferior, a animação parte dessa posição; se não, anima a partir do layout atual.
|
||||
- Soft: etapa **B60** (busca só município) herda o mesmo critério se houver chrome residual no shell — senão N/A.
|
||||
- Sem migration / collection / Consent / mudança de contrato JSON de busca.
|
||||
- Pins: unit no predicado `uiFocused` / saídas blur+Escape; e2e leve “focus esconde strip antes de digitar” se estável.
|
||||
|
||||
## Decisões travadas
|
||||
|
||||
- **Focus dispara limpeza; digitação dispara só hits.** Opções rejeitadas: A) manter B47 “só query” (pedido explícito anula); B) focus **e** fetch com 0 chars (ruído / custo). **Escolhido:** `uiFocused = inputFocused || query.isActive`; fetch continua gated por `isActive`.
|
||||
- **Animação = retração do chrome, não overlay modal.** **Rejeitado:** scrim escuro / Sheet full-screen / `fixed inset-0` que tapa bottom nav; teleportar o input para outro nó DOM (quebra foco/teclado).
|
||||
- **Blur vazio restaura; blur com query ativa mantém focado.** **Rejeitado:** sticky forever até Escape (rouba Início se o usuário toca fora sem limpar).
|
||||
- **Emenda B47, não novo input.** Reusa `CampaignHomeSearch` + layout; só o predicado e a transição mudam. Fonte: produto 2026-07-29.
|
||||
- **i18n:** ids `uiFocused` / `home-search-expanding`; copy intacta.
|
||||
|
||||
## Questões em aberto
|
||||
|
||||
- **Duração / easing da transição?** **Opções:** A ~200–250 ms `ease-out` (paridade Feel the action) | B ~150 ms | C só opacity sem height. **Recomendação:** A com height/opacity no grupo chrome; medir jank no mobile — se layout thrash, cair para C. _(assumido — craft)_
|
||||
- **Pointer coarse: focus via tap já abre teclado — animar em paralelo ao teclado?** **Opções:** A animar sempre | B adiar animação até `visualViewport` estabilizar. **Recomendação:** A na v1; B só se critique mostrar salto. _(assumido)_
|
||||
|
||||
## Abordagem proposta
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Focus["input focus"] --> UI["uiFocused=true"]
|
||||
UI --> Anim["transição: strip/spacer/resumo retraem"]
|
||||
Type["debounce ≥ limiar"] --> Hits["query.isActive → B48 fetch"]
|
||||
BlurEmpty["blur + raw vazio"] --> Restore["uiFocused=false"]
|
||||
Esc["Escape"] --> Clear["clear query"] --> Restore
|
||||
```
|
||||
|
||||
Componentes:
|
||||
|
||||
- **`useHomeSearchQuery` / `HomeSearchController`**: expor `inputFocused` + setters, ou mover `uiFocused` para `CampaignHomeStaffChrome` com `onFocus`/`onBlur` no input — depth: um predicado documentado, sem segundo hook.
|
||||
- **`CampaignHomeSearch`**: encaminhar `onFocus`/`onBlur` (cuidado com blur→click em hit: `relatedTarget` / `pointerdown` prevent — padrão lista; não fechar antes do click no resultado).
|
||||
- **`CampaignHomeLayout`**: `focused` passa a significar `uiFocused`; classes de transição (`transition-[…]`, `data-home-focused`) no spacer/actions; results slot inalterado.
|
||||
- **`campaignHomeSearchContract`**: helper puro `homeSearchUiFocused({ inputFocused, isActive })` + unit spec; JSDoc atualiza a semântica supersedida de B47.
|
||||
- **Migration:** Sem migration, sem collection, sem server action.
|
||||
|
||||
## Dependências
|
||||
|
||||
- Dura: **B47 ✓** (chrome existe).
|
||||
- Soft: **B65** (âncora inferior — animação parte do dock certo); **B56** (resumo some no focus como o resto); **B60** (paridade visual se couber).
|
||||
|
||||
## Não escopo
|
||||
|
||||
- Providers / layout de hits → **B48–B55**.
|
||||
- Âncora inferior idle → **B65**.
|
||||
- Chassis wizard / município sticky → **B59**.
|
||||
- Atalho ⌘K / busca na sidebar.
|
||||
|
||||
## Rabbit holes
|
||||
|
||||
- **Framer Motion / lib de spring.** **Mitigação:** CSS transitions + tokens; lib só se 3º call site de motion complexo.
|
||||
- **Sincronizar pixel-perfect com teclado iOS.** **Mitigação:** não amarrar a `visualViewport` na v1.
|
||||
- **Reescrever B47 plan como se nunca tivesse rejeitado focus.** **Mitigação:** as-built B47 fica; este item documenta a emenda.
|
||||
|
||||
## Adiado com gatilho
|
||||
|
||||
- **Persistir `?q=` na URL do Início.** Já adiado em B47 — reabrir só com pedido de shareable search.
|
||||
- **Gesture “puxar para baixo para fechar” no modo focado.** Revisitar se mesa pedir dismiss sem Escape/blur.
|
||||
|
||||
## Referências
|
||||
|
||||
- [busca-global-inicio-input.md](busca-global-inicio-input.md) — decisão supersedida (“expand = query”)
|
||||
- [ancorar-busca-acoes-inicio-mobile.md](ancorar-busca-acoes-inicio-mobile.md) — dock idle
|
||||
- `src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx`
|
||||
- `src/components/campaign/dashboard/CampaignHomeLayout.tsx`
|
||||
- `src/components/campaign/dashboard/CampaignHomeSearch.tsx`
|
||||
- `src/components/campaign/dashboard/useHomeSearchQuery.ts`
|
||||
- `src/lib/campaignHomeSearchContract.ts`
|
||||
- `PRODUCT.md` / `DESIGN.md` — Field Desk / Feel the action
|
||||
- AGENTS.md — naming; sem Consent
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue