From a18bbc67d7d172d92ff37b68adf5a5d981f5d3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sat, 1 Aug 2026 15:32:26 -0300 Subject: [PATCH] =?UTF-8?q?docs(plans):=20B106=E2=80=93B108=20polish=20mob?= =?UTF-8?q?ile=20busca=20e=20wizard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Planos para Voltar/Android back/skeleton no Início, município sem título + sticky + skeleton, e autofocus/destaque de votos no wizard. --- docs/plans/inicio-busca-voltar-skeleton.md | 133 +++++++++++++++++ docs/plans/wizard-foco-input-e-votos.md | 136 ++++++++++++++++++ .../wizard-municipio-sem-titulo-sticky.md | 131 +++++++++++++++++ 3 files changed, 400 insertions(+) create mode 100644 docs/plans/inicio-busca-voltar-skeleton.md create mode 100644 docs/plans/wizard-foco-input-e-votos.md create mode 100644 docs/plans/wizard-municipio-sem-titulo-sticky.md diff --git a/docs/plans/inicio-busca-voltar-skeleton.md b/docs/plans/inicio-busca-voltar-skeleton.md new file mode 100644 index 00000000..a7300031 --- /dev/null +++ b/docs/plans/inicio-busca-voltar-skeleton.md @@ -0,0 +1,133 @@ +# Início mobile — Voltar no header + Android back + skeleton das sugestões + +Status: registrado +Atualizado em: 2026-08-01 +Issue: #141 +Priority: P1 +Model: composer-2.5 +Impeccable: B — encaixe em `CampaignMobileTopBar` + chrome da busca do Início (`uiFocused`) +Appetite: ~0,75–1d eng; contexto de chrome + history layer + skeleton; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Clarity under pressure; Feel the action) / `DESIGN.md` · B66 modo focado · B68 sugestões · tema `data-theme='campaign'`. + +Na implementação (`work-issue`): craft compacto → critique → polish (Android WebView / Chrome back + iOS Safari). + +Brief compacto: + +- **Persona / contexto:** assessor/CG no Início mobile toca a busca; chrome retraí (B66); hoje o hamburger do header ainda sugere “abrir menu”, não “sair da busca”. +- **Job principal:** no modo focado, o canto esquerdo do header vira **Voltar** (fecha a expansão); o botão Voltar do Android faz o mesmo — não navega para a rota anterior; enquanto as sugestões carregam, a região mostra skeleton shimmer. +- **Estratégia de cor:** Restrained — ícone/botão ghost no Mandate Red (paridade wizard B75); skeleton `bg-muted` / `animate-pulse` do kit. +- **Edit where you see:** não — chrome de descoberta. +- **Anti-goals:** teleportar o input para o header; Sheet full-screen; aplicar o mesmo history layer ao bottom drawer (B91/B105) neste item; redesenhar sugestões. + +### Wireframe (texto) + +```text +┌─ header app (Mandate Red) — busca NÃO focada ───────┐ +│ [☰] Jorge Solla / Campanha · Bahia [🔔] │ +└─────────────────────────────────────────────────────┘ +┌─ header app — busca focada (uiFocused) ─────────────┐ +│ [←] Jorge Solla / Campanha · Bahia [🔔] │ +└─────────────────────────────────────────────────────┘ +┌─ dock busca + região resultados ────────────────────┐ +│ ┌─ Buscar… ──────────────────────────────────────┐ │ +│ │ … │ │ +│ └────────────────────────────────────────────────┘ │ +│ ░░░░░ skeleton row ░░░░░ (enquanto suggest load) │ +│ ░░░░░ skeleton row ░░░░░ │ +│ ░░░░░ skeleton row ░░░░░ │ +│ (depois) hits B68 / busca tipada │ +└─────────────────────────────────────────────────────┘ + Fora do frame: strip/resumo retraídos (B66); drawer B91 N/A no Início. +``` + +## Dados → decisão → apresentação + +Dados: N/A — chrome + pending visual; hits/sugestões já existem (B68). Sem métrica nova. + +## Contexto + +**B66 ✓** retrai strip/resumo ao `uiFocused` (`homeSearchUiFocused`), mas `CampaignMobileTopBar` em modo `app` continua com `SidebarTrigger`. **B68 ✓** carrega sugestões no empty; a região fica vazia até `success` — só `aria-busy`, sem skeleton. + +O provider da busca no Início vive **dentro** da página (`CampaignHomeStaffChrome` → `CampaignGlobalSearchProvider`); o top bar está no layout `(app)` **acima** do scroll. Não há `pushState`/`popstate` no `src/` hoje — o Voltar do Android sai da rota. + +Pedido de produto (2026-08-01): no modo expandido, Voltar à esquerda no header (no lugar do menu); Android back recolhe a busca; skeleton shimmer no empty enquanto sugestões carregam. + +## Objetivos + +- Com `uiFocused` no Início mobile: substituir `SidebarTrigger` por botão **Voltar** (`ArrowLeft` + aria “Fechar busca” / “Voltar”) que chama o mesmo collapse que blur+Escape (clear focus; se query vazia, sai do modo focado — alinhado B66). +- Ao entrar em `uiFocused`, empilhar um entry de history (`pushState` com marca estável); `popstate` / back do SO **só** recolhe a busca se o entry for nosso; não dispara navegação Next para a rota anterior. +- Enquanto suggest/search estiver `loading`/`isFetching` sem hits ainda renderizados: 3–5 rows skeleton (altura ~hit row), `aria-busy` mantido; sumir ao `success`/`error`. +- Desktop (`md+`): top bar já hidden — comportamento inalterado; history layer só mobile ou só quando top bar troca o trigger. +- Sem migration / Consent / mudança de contrato JSON de busca. +- Pins: unit no helper de history (push/pop/idempotência); unit/RTL do top bar app-mode com chrome “search-focused”; e2e leve opcional se estável. + +## Decisões travadas + +- **Contexto leve de chrome no layout (irmã do wizard chrome), não subir o `HomeSearchProvider` inteiro.** O top bar só precisa de `{ focused, collapse }`. Home (e só Início) publica; top bar consome. **Rejeitado:** prop-drill via layout RSC; ler DOM `data-home-focused`; duplicar estado de foco no top bar. +- **History = camada só do modo focado da busca do Início.** Marca tipo `teqo:home-search-focus` (nome final no craft). Collapse via back **não** limpa query tipada se produto preferir só blur focus — **recomendação:** igual Escape/`clear` do B66 (query some e chrome restaura) para um gesto único “sair da busca”. **Rejeitado:** history também no drawer B91 neste item; `beforeunload` hacks. +- **Skeleton reutiliza `Skeleton` shadcn + shape das hit rows; sem lib de shimmer.** Extrair helper/componente mínimo se **B107** (wizard) for o 2º call site no mesmo ciclo — gate 2026-08-01 pediu skeleton no wizard também. **Rejeitado:** spinner centrado; skeleton só no input. +- **Escopo de Voltar/history = Início (`/campanha`), não drawer global.** Drawer tem snap/peek próprios (B100/B105). **Rejeitado:** unificar “qualquer busca focada” neste PR. +- **i18n:** ids `homeSearchChrome` / `collapseHomeSearch`; copy pt-BR no aria do Voltar. + +## Questões em aberto + +- **Voltar com query tipada: limpa query ou só blur?** **Opções:** A limpa (paridade Escape) | B só tira foco e mantém texto (chrome pode continuar focado via `query.isActive`). **Recomendação:** **A** — um gesto = sair da busca. _(assumido — validar com produto)_ +- **Quantas rows de skeleton?** **Opções:** A 3 | B 5. **Recomendação:** **A** (viewport curto com teclado). _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + Focus["input focus / uiFocused"] --> Pub["HomeSearchChrome publish"] + Pub --> Bar["CampaignMobileTopBar: ☰ → Voltar"] + Focus --> Hist["pushState marca"] + Back["Android back / Voltar"] --> Pop["popstate ou collapse()"] + Pop --> Clear["clear + uiFocused=false"] + Fetch["suggest loading"] --> Skel["HomeSearchSuggestSkeleton"] + Skel --> Hits["success → HomeSearchHitRow"] +``` + +Componentes: + +- **`CampaignHomeSearchChromeContext`** (ou nome alinhado ao wizard): provider no `(app)/layout` junto aos outros chromes; `useSetHomeSearchChrome` / `useHomeSearchChrome` espelhando o padrão B75. +- **`CampaignGlobalSearchMount` / `CampaignHomeSearch` / controller:** ao mudar `uiFocused`, publish + sync history (push ao true, replace/back ao false sem loop). +- **`CampaignMobileTopBar` (modo app):** se chrome.searchFocused → botão Voltar; senão `SidebarTrigger`. +- **`HomeSearchSuggestSkeleton`** (inline no grupo de resultados / results shell): rows quando `isFetching && !hits`; reuso em Início; wizard município **fora** deste item (pode citar como soft follow-up). +- **Helper puro** `homeSearchFocusHistory.ts` em `lib/`: serialize marca, `shouldHandlePopstate`, unit-pinned. +- **Migration:** Sem migration. + +## Dependências + +- Duras: nenhuma. Soft: B66 ✓, B68 ✓. Paralelo a B102/B103/B105 (drawer) — não serializa schema; evitar editar os mesmos arquivos do drawer no mesmo PR se outro agente estiver neles. + +## Não escopo + +- Bottom drawer busca (B91/B100/B105) — Voltar/history/skeleton do drawer. +- Remover título “Sugestões” → **B103**. +- Wizard município sticky/título/**skeleton do passo** → **B107** (pode reusar o bloco de rows deste item). Autofocus/votos → **B108**. + +## Rabbit holes + +- **“Já que o top bar lê foco, movo a busca para o header.”** **Mitigação:** só swap do trigger; input fica no dock (B65/B66). +- **History API × App Router (bfcache, soft nav).** **Mitigação:** marca + guard idempotente; testes unit do helper; não `router.back()` para collapse. +- **Skeleton shared com API inchada.** **Mitigação:** um componente de N rows pinado; sem tema/variant factory. + +## Adiado com gatilho + +- **Mesmo Voltar/history no drawer.** Revisitar quando B105 estiver estável e produto pedir paridade. + +## Referências + +- GitHub Issue #141 +- `src/components/campaign/shell/CampaignMobileTopBar.tsx` +- `src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx` +- `src/components/campaign/dashboard/CampaignGlobalSearchMount.tsx` +- `src/lib/campaignHomeSearchContract.ts` +- [modo-focado-busca-no-focus.md](modo-focado-busca-no-focus.md) (B66) · [sugestoes-busca-vazia-inicio.md](sugestoes-busca-vazia-inicio.md) (B68) +- `PRODUCT.md` / `DESIGN.md` + +Qualidade de decisão: 5/5 diff --git a/docs/plans/wizard-foco-input-e-votos.md b/docs/plans/wizard-foco-input-e-votos.md new file mode 100644 index 00000000..ee846479 --- /dev/null +++ b/docs/plans/wizard-foco-input-e-votos.md @@ -0,0 +1,136 @@ +# Wizard — autofocus do primeiro input + destaque do último cenário focado + +Status: registrado +Atualizado em: 2026-08-01 +Issue: #143 +Priority: P1 +Model: composer-2.5 +Impeccable: B — encaixe em `CampaignWizardShell` + steps com input + `VoteEstimateScenarioInputs` compact +Appetite: ~0,5–0,75d eng; foco/a11y + CSS de active scenario; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Feel the action; Clarity under pressure) / `DESIGN.md` · B59 shell · B61 votos · tema `campaign`. + +Na implementação: craft compacto → critique → polish (teclado mobile ao abrir cada step). + +Brief compacto: + +- **Persona / contexto:** assessor avança steps do wizard no celular; espera digitar na hora sem tocar de novo no campo. +- **Job principal:** ao abrir um step **com input**, o primeiro campo já está focado (e selecionado se numérico); no ajuste de votos, o input **visualmente** destacado é o **último focado**, não o central fixo — alinhado aos atalhos (+50, 2×) que já seguem `focusedScenario`. +- **Estratégia de cor:** Restrained — reusar `border-primary/50 bg-primary/5 ring-primary/15` no ativo; muted nos demais. +- **Edit where you see:** sim no sentido de affordance do cenário ativo (já existe strip); não abre editor novo. +- **Anti-goals:** autofocus em steps só de escolha (grid de tendência/tipo) sem input de texto; mudar validação `getWizardVoteViolation`; spreadsheet mode. + +### Wireframe (texto) + +```text +┌─ step município (após B107 sem h1) ─────────────────┐ +│ ┌─ Nome do município… │← foco + teclado ao abrir │ +│ hits… │ +└─────────────────────────────────────────────────────┘ + +┌─ step votos estimados ──────────────────────────────┐ +│ [strip marker sob o ativo] │ +│ ┌─────┐ ┌─────┐ ┌─────┐ │ +│ │ 80 │ │ 100 │ │ 120 │ ← destaque = last focused │ +│ └─────┘ └─────┘ └─────┘ │ +│ (ex.: foco em otimista → anel primary nele, │ +│ não no central) │ +│ [+50] [2×] … aplicam ao focusedScenario │ +└─────────────────────────────────────────────────────┘ +``` + +## Dados → decisão → apresentação + +Dados: N/A — affordance de edição; números já existentes. + +## Contexto + +`CampaignWizardShell` faz `titleRef.focus()` em todo `stepTitle` change — útil para screen readers em steps de escolha, mas **rouba** o foco do primeiro input. + +`WizardExpectedVotesStep` já mantém `focusedScenario` (default `central`) e passa `activeScenario` / `onFocusScenario` para atalhos e strip (`markerMode: 'active-only'`). Porém em `VoteEstimateScenarioInputs` **compact**, a classe primary está hardcoded em `scenario === 'central'`, então o meio parece sempre “o” ativo mesmo quando o foco (e os botões) estão noutro cenário. + +Pedido de produto (2026-08-01): autofocus do primeiro input em cada step com input; no votos, destaque = último focado. + +## Objetivos + +- Steps com `` / textarea / search: ao montar (ou ao mudar de step), focar o primeiro campo editável; em campos numéricos de votos, manter `select()` no focus (já existe). +- Shell **não** foca o `h1` quando o step declara que vai autofocusar um input (ou quando `stepTitle` é null — B107). +- Compact votes: highlight primary segue `activeScenario` (último `onFocus`), não `central` fixo; erro (`errorScenarios`) continua a ganhar da highlight de foco. +- Default inicial inalterado: monta com `central` focado + `autoFocusScenario="central"` → meio destacado até o usuário focar outro. +- Sem migration / mudança de schema de pledge. +- Pins: unit na regra de classe compact (`activeScenario` vs central); RTL leve de autofocus no municipality step se barato. + +## Decisões travadas + +- **Um item para autofocus + highlight de votos.** Mesmo eixo “foco = sinal do controle ativo”; appetite cabe. **Rejeitado:** duas Issues cosméticas. +- **Opt-in por step (`autoFocusFieldId` / `focusMode: 'title' | 'first-input'`), não heurística DOM genérica no shell.** Cada step sabe seu primeiro input. **Rejeitado:** `querySelector('input')` no shell (frágil com hidden fields). +- **Highlight compact = `activeScenario`, em todos os call sites compact (wizard + lista B42).** Consistência; lista já passa foco. **Rejeitado:** prop `highlightCentralAlways` legacy. +- **Steps só de botões/grid (tendência, tipo de sinal): mantêm foco no título (ou primeiro radio/button se já existir).** Fora do “com input”. **Rejeitado:** forçar foco no primeiro botão do grid neste item (pode ser follow-up a11y). +- **i18n:** ids existentes; sem copy nova obrigatória. + +## Questões em aberto + +- **Município: `autoFocus` nativo vs `useEffect` + ref após paint?** **Opções:** A `autoFocus` no `CampaignSearchInput` | B effect no step. **Recomendação:** **A** se o shell parar de focar o h1; B se race com navegação pending. _(assumido — craft)_ +- **Labeled variant (não compact) também destaca por foco?** **Opções:** A só compact | B ambos. **Recomendação:** **A** — pedido é o wizard/strip compact; labeled já tem labels por campo. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + Shell["CampaignWizardShell focusMode"] + Mun["MunicipalitySearch autoFocus"] + Votes["ExpectedVotes autoFocus central"] + Note["TrendNote / SignalBody / Leadership autoFocus"] + Compact["VoteEstimateScenarioInputs highlight=activeScenario"] + Shell -->|"title ou none"| Mun + Shell --> Votes --> Compact + Shell --> Note +``` + +Componentes: + +- **`CampaignWizardShell`:** prop `contentFocus?: 'title' | 'none'` (default `'title'` para não regredir grids); quando `'none'`, skip `titleRef.focus()`. Steps com input passam `'none'`. +- **`WizardMunicipalitySearchStep`:** `contentFocus="none"` + `autoFocus` no search input (coordena com B107 sem h1). +- **`WizardExpectedVotesStep` / `WizardTrendNoteStep` / `WizardSignalBodyStep` / `WizardLeadershipStep`:** `contentFocus="none"` + autofocus no primeiro campo. +- **`VoteEstimateScenarioInputs` compact:** trocar `scenario === 'central'` por `scenario === activeScenario` na classe primary (erro destrutivo prevalece). +- **Testes:** unit da classe compact; smoke se houver spec do wizard votos. +- **Migration:** Sem migration. + +## Dependências + +- Soft: B59 ✓, ajuste votos wizard ✓. Coordena com **B107** (shell sem h1 no município) — se B107 landar antes, `contentFocus` no município é trivial; se este landar antes, funciona com h1 ainda presente desde que shell não foque o título. +- `serializes`: nenhum schema; preferir não editar `CampaignWizardShell` em paralelo com B107 no mesmo instante — se ambos claimed, **B107 primeiro** (título) ou um PR único se o mesmo agente pegar os dois. + +## Não escopo + +- Remover título / sticky busca → **B107**. +- Voltar/skeleton Início → **B106**. +- Mudar defaults de cenário (pessimista vs central) ou atalhos. + +## Rabbit holes + +- **Autofocus + leitores de tela (WCAG 2.4.3).** **Mitigação:** steps de escolha continuam no título; só steps de formulário movem foco para o campo — padrão de wizards mobile. +- **Autofocus em toda navegação client do wizard dispara teclado em loop.** **Mitigação:** focus só no mount do step / mudança de `stepTitle`+slug, não a cada re-render de pending. + +## Adiado com gatilho + +- **Foco no primeiro controle de steps só-grid.** Revisitar com auditoria a11y se VoiceOver reclamar do h1 sem atalho para o grid. +- **Sincronizar highlight da variant labeled.** Se a lista desktop compactar tudo em compact. + +## Referências + +- GitHub Issue #143 +- `src/components/campaign/shared/CampaignWizardShell.tsx` +- `src/components/campaign/shared/WizardMunicipalitySearchStep.tsx` +- `src/components/campaign/shared/WizardExpectedVotesStep.tsx` +- `src/components/campaign/votePledge/VoteEstimateScenarioInputs.tsx` +- `src/components/campaign/municipality/WizardTrendNoteStep.tsx` +- `src/components/campaign/municipality/WizardSignalBodyStep.tsx` +- `src/components/campaign/leadership/WizardLeadershipStep.tsx` +- [chassis-wizard-campanha.md](chassis-wizard-campanha.md) (B59) · [ajuste-votos-wizard.md](ajuste-votos-wizard.md) +- `PRODUCT.md` / `DESIGN.md` + +Qualidade de decisão: 5/5 diff --git a/docs/plans/wizard-municipio-sem-titulo-sticky.md b/docs/plans/wizard-municipio-sem-titulo-sticky.md new file mode 100644 index 00000000..c6caea81 --- /dev/null +++ b/docs/plans/wizard-municipio-sem-titulo-sticky.md @@ -0,0 +1,131 @@ +# Wizard município — sem título + busca sticky + skeleton + +Status: registrado +Atualizado em: 2026-08-01 +Issue: #142 +Priority: P1 +Model: composer-2.5 +Impeccable: B — encaixe em `WizardMunicipalitySearchStep` + `CampaignWizardShell` (título opcional) +Appetite: ~0,5–0,75d eng; markup/CSS + shell opt-out do h1 + skeleton idle; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Clarity under pressure) / `DESIGN.md` · B60/B75/B95 · tema `campaign`. + +Na implementação: craft compacto → critique → polish (teclado aberto + lista longa). + +Brief compacto: + +- **Persona / contexto:** CG/assessor no passo 1 do wizard no celular; o header vermelho já diz o fluxo (“Ajustar votos”); o placeholder é “Nome do município…”. +- **Job principal:** cair direto na busca (sem h1 “Em qual município?”); ao rolar hits, a barra de busca **permanece** logo abaixo do header; enquanto suggest/search carrega, skeleton shimmer na região de resultados. +- **Estratégia de cor:** inalterada; sticky com `bg-background` para não transparizar hits; skeleton `bg-muted` / `animate-pulse`. +- **Edit where you see:** não — layout do passo. +- **Anti-goals:** sticky do header vermelho (já fora do scroll); redesign da hit row; mudar ranking B92–B94. + +### Wireframe (texto) + +```text +┌─ header wizard (Mandate Red) ───────────────────────┐ +│ Ajustar votos [X] │ +└─────────────────────────────────────────────────────┘ +┌─ scroll content ────────────────────────────────────┐ +│ ┌─ Buscar município (sticky) ────────────────────┐ │ ← cola sob o header +│ │ Nome do município… │ │ +│ └────────────────────────────────────────────────┘ │ +│ ░░░ skeleton · skeleton · skeleton ░░░ (loading) │ +│ Cairu · Baixo Sul (após success) │ +│ (rola; busca sticky fica) │ +└─────────────────────────────────────────────────────┘ + Sem h1 “Em qual município?”. Gap shell→busca mínimo no mobile. +``` + +## Dados → decisão → apresentação + +Dados: N/A — layout/chrome; hits inalterados. + +## Contexto + +**B95 ✓** aproxima o título do header e remove bullets; o h1 **“Em qual município?”** (`WIZARD_MUNICIPALITY_STEP_TITLE`) permanece. `CampaignWizardShell` sempre renderiza o `h1` e foca nele no mount. + +No mobile, a busca + lista rolam juntos dentro de `CampaignContentScroll` — a barra sobe e some sob o header ao escanear sugestões/hits. + +Pedido de produto (2026-08-01): remover o título; diminuir gap ao header; busca sticky/fixed sempre sob o header; skeleton enquanto o empty/suggest (ou search) carrega — se tem load, tem load indicator. + +## Objetivos + +- Passo município **sem** `h1` “Em qual município?” (copy pode permanecer no módulo para sr-only/`aria-label` da região se útil; UI visível some). +- Gap visual header→busca no mobile menor que o stack atual (sem h1 + `pt` já `pt-3` do B95; critique pode `pt-2` / `gap` só neste passo). +- Barra de busca **sticky** no scrollport do conteúdo (`sticky top-0 z-10` + fundo opaco + padding-bottom curto), sempre visível sob o top bar vermelho. +- Lista de resultados rola por baixo da busca sticky. +- Enquanto `results.status === 'loading'` / `resultsBusy` sem hits: 3 rows skeleton (paridade B106), `aria-busy` mantido; sumir em `success`/`error`. +- Sem migration / mudança de ranking / autofocus (autofocus → **B108**). +- Pin: unit/RTL se classes sticky/`stepTitle` opcional forem pináveis; e2e não obrigatório. + +## Decisões travadas + +- **`stepTitle` opcional no shell (`string | null`): omitir `h1` quando vazio/null.** Passo município passa `null`. Outros passos inalterados. **Rejeitado:** CSS `hidden` no h1 (ainda no a11y tree / foco); fork de shell só para município. +- **Sticky da busca, não `position: fixed` no viewport.** Sticky relativo ao `CampaignContentScroll` alinha sob o header sem calcular `env(safe-area)` + altura do top bar à mão. **Rejeitado:** `fixed` + spacer mágico (frágil com teclado). +- **Skeleton no idle/loading deste passo (produto 2026-08-01).** Paridade com B106; reutilizar o mesmo bloco de rows se B106 extrair helper — 2º call site justifica shared mínimo. **Rejeitado:** só `aria-busy` sem visual; spinner centrado. +- **Item novo (B107), não reabrir B95.** B95 manteve o título de propósito; produto agora remove. **Rejeitado:** editar só o as-built B95 sem Issue. +- **i18n:** `WIZARD_MUNICIPALITY_STEP_TITLE` pode virar sr-only / aria da região; identificadores ingleses intactos. + +## Questões em aberto + +- **Título some só no mobile ou em todo breakpoint?** **Opções:** A some sempre | B some só `md:hidden` (desktop mantém h1). **Recomendação:** **A** — contexto do fluxo já está no chrome/desktop trailing; placeholder basta. _(assumido — gate: aprovado implícito no lote)_ + +## Abordagem proposta + +```mermaid +flowchart TB + Shell["CampaignWizardShell stepTitle=null → sem h1"] + Step["WizardMunicipalitySearchStep"] + Stick["wrapper sticky top-0 bg-background"] + Input["CampaignSearchInput"] + Skel["skeleton rows se loading"] + List["ul hits / suggest"] + Shell --> Step + Step --> Stick --> Input + Step --> Skel + Step --> List +``` + +Componentes: + +- **`CampaignWizardShell.tsx`:** `stepTitle?: string | null`; se ausente, não renderiza `h1` / não foca título; `aria-labelledby` cai para id do flow no chrome ou `aria-label` no `main` via `flowTitle`. +- **`WizardMunicipalitySearchStep.tsx`:** `stepTitle={null}`; envolver input (e label) em container sticky; lista fora do sticky; render skeleton quando busy sem hits. +- **Skeleton rows:** reusar helper/componente nascido em **B106** (`HomeSearchSuggestSkeleton` ou equivalente em `shared`/`dashboard`) — se B107 landar antes, inline 3× `Skeleton` e B106 extrai no polish. +- **`campaignWizardCopy.ts`:** título pode permanecer exportado para aria; documentar. +- **Migration:** Sem migration. + +## Dependências + +- Soft: B60 ✓, B75 ✓, B95 ✓. Nenhuma dura. +- Coordenar com **B108** (autofocus): sem h1, o shell não deve “roubar” foco — B108 formaliza; neste item, ao omitir h1, **não** chamar `titleRef.focus()` (já coberto pela ausência do nó). + +## Não escopo + +- Autofocus do input → **B108**. +- Destaque de cenários de votos → **B108**. +- Voltar + skeleton do **Início** → **B106** (shared de rows ok). +- Ranking/geo/continuity → B92–B94. + +## Rabbit holes + +- **Sticky + teclado virtual (iOS) empurra visualViewport.** **Mitigação:** sticky simples v1; se critique mostrar jump, medir — não `visualViewport` polyfill neste appetite. +- **Tornar todo passo “title-less”.** **Mitigação:** só município; outros passos mantêm pergunta no h1. + +## Adiado com gatilho + +- **`position: sticky` no dock do Início.** Só se produto pedir paridade (hoje a busca já está no fundo). + +## Referências + +- GitHub Issue #142 +- `src/components/campaign/shared/WizardMunicipalitySearchStep.tsx` +- `src/components/campaign/shared/CampaignWizardShell.tsx` +- `src/lib/campaignWizardCopy.ts` +- [polimento-busca-municipio-wizard.md](polimento-busca-municipio-wizard.md) (B95) · [busca-municipio-wizard.md](busca-municipio-wizard.md) (B60) · [header-mobile-wizard-campanha.md](header-mobile-wizard-campanha.md) (B75) +- `PRODUCT.md` / `DESIGN.md` + +Qualidade de decisão: 5/5