Merge branch 'cursor/7cf91b92' — B65 âncora inferior mobile no Início

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-29 22:41:45 -03:00
commit 27678a58c5
7 changed files with 77 additions and 41 deletions

View file

@ -7,6 +7,7 @@ alwaysApply: false
## Status
- **B65 entregue em código (2026-07-29):** âncora inferior mobile no Início — cadeia `h-full min-h-0` em `CampaignPageShell` + `CampaignHomeStaffChrome` + `CampaignHomeLayout`; wrapper `data-slot="home-dock"` agrupa strip B45 + busca B47; spacer `flex-1` empurra o dock acima da bottom nav (`pb-24` intacto); `md+` inalterado; modo focado B47/B66 intacto; sem `fixed`/`mt-auto`. Unit `campaignHomeLayout.unit.spec.tsx` (6 casos). Plano: [`ancorar-busca-acoes-inicio-mobile.md`](../../../docs/plans/ancorar-busca-acoes-inicio-mobile.md). Desbloqueia animação idle de **B66**. Débito adiado: ajuste fino `pb-24` vs altura real da bottom nav se gap >~8 px em device real.
- **B59 entregue em código (2026-07-29):** chassis UX-1 dos wizards de ação — `CampaignWizardShell` (`shared/`: Voltar só mobile com rótulo visível, caption sticky central de município, `contentAlign`, foco no `h1` ao trocar passo); rotas staff `/campanha/acoes/<slug>` (cinco entradas espelhando o catálogo B45, passo ponte "Em qual município?" até **B60**); `campaignActionRoutes.ts` + hrefs em `toHomeActionButtonProps`; layout `acoes/` com gate `staff`. Sem migration/action/Consent. Unit `campaignActionRoutes`, `campaignWizardShell`, `campaignHomeActions` atualizado; e2e Início → `atualizar-votos`. Plano: [`chassis-wizard-campanha.md`](../../../docs/plans/chassis-wizard-campanha.md). Desbloqueia **B60** / **B61** / **B63** / **B64**.
- **B56 entregue em código (2026-07-29):** resumo da campanha no Início staff — `loadCampaignHomeSummary` + `CampaignHomeSummary` (total central `staffVoteTotal` + cobertura E8 com `Progress`); `summarySlot` em `CampaignHomeLayout`/`CampaignHomeStaffChrome`, oculto no modo focado B47; sem saudação; leader fora. Loader em `campaignDashboardData.ts` (não módulo top-level novo). Unit `campaignHomeSummary.unit.spec.tsx`. Sem migration. Plano: [`resumo-campanha-inicio.md`](../../../docs/plans/resumo-campanha-inicio.md). Desbloqueia slot visual de **B57** (delta 7d, futuro).
- **B67 entregue em código (2026-07-29):** pan na strip de ações do Início (UX-1) — `CampaignHomeActionStrip` com drag-to-scroll em `pointer-fine` (`HOME_ACTION_STRIP_DRAG_THRESHOLD_PX` 8, `cursor-grab`/`grabbing`); scrollbar oculta (B58) mantida; touch `pan-x` nativo; `onClickCapture` suprime link após arraste. Unit em `campaignHomeActionButton.unit.spec.tsx`. Sem migration. Plano: [`restaurar-pan-strip-acoes-inicio.md`](../../../docs/plans/restaurar-pan-strip-acoes-inicio.md). Fecha Adiado drag desktop de B44.
@ -16,7 +17,7 @@ alwaysApply: false
- **B50 entregue em código (2026-07-29):** grupo **Assessores** na busca global do Início — `searchHomeAdvisors` (só CG/candidato; assessor recebe `[]`); match word-start no nome; secundário = N municípios; `HomeSearchAdvisorGroup` após Lideranças; `municipalityIdsByAdvisorIds` exportado de `advisorData`. Int `homeSearchAdvisors.int.spec.ts`. WA = **B55**. Plano: [`busca-global-resultados-assessores.md`](../../../docs/plans/busca-global-resultados-assessores.md).
- **B48 entregue em código (2026-07-29):** primeiro provider da busca global do Início — grupo **Municípios** (municípios scoped + TIs no mesmo bloco) via `POST /campanha/home-search`, `searchHomeMunicipalities`, `HomeSearchMunicipalityGroup` no slot B47; readout 2022 alinhado à lista (`MunicipalityVotePositionReadout`). **B54** grid/cap. Plano: [`busca-global-resultados-municipios.md`](../../../docs/plans/busca-global-resultados-municipios.md).
- **B47 entregue em código (2026-07-29):** busca global do Início (UX-1) — input staff-only com debounce 250 ms (`HOME_SEARCH_DEBOUNCE_MS`), sem botão Pesquisar; modo focado quando query debounced trim ≥2 chars esconde strip B45 e spacer thumb-zone (`CampaignHomeLayout` `focused`). Ilha `CampaignHomeStaffChrome` + `CampaignHomeSearch` + `HomeSearchContext` / `useHomeSearch` para grupos B48+; contrato em `src/lib/campaignHomeSearchContract.ts`. Leader continua sem omnibox (`CampaignHomeLayout` seco). Escape limpa. Sem migration. Plano: [`busca-global-inicio-input.md`](../../../docs/plans/busca-global-inicio-input.md). ~~Próximo: **B48** resultados Municípios.~~
- **B46 entregue em código (2026-07-29):** thumb-zone do Início (UX-1 chassis) — `CampaignHomeLayout` ancora `CampaignHomeActions` na metade inferior do viewport em mobile (`min-h-full` no shell + spacer `flex-1` acima da strip; clearance existente `pb-24` + bottom nav fixa, sem `position: fixed` na strip) e no topo em `md+` (`md:min-h-0`). Slots `data-slot="home-actions"` e opcional `searchSlot` → `data-slot="home-search"` (ordem documento mobile ações→busca; desktop busca→ações via `order-1`/`order-2`/`order-3`) para **B47** ✓. Wire em `campanha/(app)/page.tsx`. Unit `campaignHomeLayout.unit.spec.ts`; e2e `campaignHomeActions` verde. Sem migration. Plano: [`posicao-botoes-acao-inicio-thumb-zone.md`](../../../docs/plans/posicao-botoes-acao-inicio-thumb-zone.md).
- **B46 entregue em código (2026-07-29):** thumb-zone do Início (UX-1 chassis) — `CampaignHomeLayout` com slots `data-slot="home-actions"` / `home-search` (ordem mobile ações→busca; desktop busca→ações). Âncora inferior corrigida por **B65** ✓. Plano: [`posicao-botoes-acao-inicio-thumb-zone.md`](../../../docs/plans/posicao-botoes-acao-inicio-thumb-zone.md).
- **B45 entregue em código (2026-07-29):** catálogo de ações do Início por persona (UX-1 chassis) — `src/lib/campaignHomeActions.ts` (`homeActionsForRole`, `toHomeActionButtonProps`) + ilha client `CampaignHomeActions` (ícones Lucide no client; RSC passa `role` + `uncoveredMunicipalitiesHref` de `buildMunicipalityListHref`). Staff (coordinator/candidate/advisor): 6 ações; `uncovered-municipalities` → `?coverage=sem_assessor&sort=votos`; demais inertes (`cursor-default`). Leader: 2 ações; `my-contacts` → `/campanha/contatos`. Seção "O que você quer fazer?" acima da strip B44. Unit `campaignHomeActions.unit.spec.ts`; e2e `campaignHomeActions.e2e.spec.ts`. Sem migration. Plano: [`catalogo-acoes-inicio-por-persona.md`](../../../docs/plans/catalogo-acoes-inicio-por-persona.md).
- **B46–B55 registrados no roadmap (2026-07-29):** ~~thumb-zone do Início (**B46**)~~ entregue; 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 na busca; 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`). Montado no Início por **B45** ✓. Specs: `campaignLongPress.unit.spec.tsx`, `campaignHomeActionButton.unit.spec.tsx`. Plano: [`botao-acao-inicio-strip.md`](../../../docs/plans/botao-acao-inicio-strip.md).

View file

@ -1,7 +1,7 @@
# Ancorar busca + ações do Início no limite inferior (mobile)
Status: rascunho
Atualizado em: 2026-07-29
Status: entregue (2026-07-29)
Atualizado em: 2026-07-29 — as-built: cadeia `h-full min-h-0` em `CampaignPageShell` + `CampaignHomeStaffChrome` + `CampaignHomeLayout`; wrapper `data-slot="home-dock"` agrupa strip + busca; spacer `flex-1` volta a empurrar o dock no mobile; `mt-auto` não foi necessário; modo focado B47/B66 intacto; unit `campaignHomeLayout` estendido (6 casos). Sem migration.
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B — **B65**; follow-up do chassis UX-1 / B46)
Impeccable: B — encaixe de layout em `CampaignHomeLayout` / shell do Início sob tema `campaign`
Appetite: ~0,25–0,5 dia eng; CSS/altura do chrome do Início; sem migration

File diff suppressed because one or more lines are too long

View file

@ -41,7 +41,7 @@ export default async function CampaignHomePage() {
}
return (
<CampaignPageShell aria-label="Início" className="min-h-full">
<CampaignPageShell aria-label="Início" className="h-full min-h-0">
{staff ? (
<CampaignHomeStaffChrome
actions={actions}

View file

@ -11,28 +11,35 @@ export const CampaignHomeLayout = ({
actions: ReactNode
searchSlot?: ReactNode
summarySlot?: ReactNode
/** When true (trimmed debounced query length ≥ min), hide thumb-zone spacer and action strip. */
/** When true (input focused or query active), hide thumb-zone spacer and action strip. */
focused?: boolean
}) => (
<div className="flex min-h-full w-full flex-col md:min-h-0">
<div className="flex h-full min-h-0 w-full flex-col">
{summarySlot ? (
<div className={cn('order-0 min-w-0', focused && 'hidden')} data-slot="home-summary">
{summarySlot}
</div>
) : null}
{focused ? null : (
<div aria-hidden className="min-h-0 flex-1 md:hidden" data-slot="home-thumb-spacer" />
)}
<div data-slot="home-actions" className={cn('order-1 min-w-0 md:order-2', focused && 'hidden')}>
{actions}
</div>
{searchSlot ? (
<div
aria-hidden
className={cn('min-h-0 flex-1 md:hidden', focused && 'hidden')}
data-slot="home-thumb-spacer"
/>
<div data-slot="home-dock" className="flex min-w-0 flex-col">
<div
data-slot="home-search"
className={cn('order-3 min-w-0 md:order-1', focused ? 'mt-0' : 'mt-4 md:mt-0 md:mb-6')}
data-slot="home-actions"
className={cn('order-1 min-w-0 md:order-2', focused && 'hidden')}
>
{searchSlot}
{actions}
</div>
) : null}
{searchSlot ? (
<div
data-slot="home-search"
className={cn('order-2 min-w-0 md:order-1', focused ? 'mt-0' : 'mt-4 md:mt-0 md:mb-6')}
>
{searchSlot}
</div>
) : null}
</div>
</div>
)

View file

@ -10,7 +10,6 @@ import {
useHomeSearchResultsState,
} from '@/components/campaign/dashboard/HomeSearchResultsContext'
import { useHomeSearchQuery } from '@/components/campaign/dashboard/useHomeSearchQuery'
import { cn } from '@/lib/utils'
const CampaignHomeStaffSearchSlot = ({ searchResults }: { searchResults: ReactNode }) => {
const searchResultsState = useHomeSearchResultsState()
@ -39,10 +38,7 @@ export const CampaignHomeStaffChrome = ({
return (
<HomeSearchProvider value={searchState}>
<div
className={cn('flex min-h-full w-full flex-col', focused && 'md:min-h-0')}
data-home-focused={focused || undefined}
>
<div className="h-full min-h-0 w-full" data-home-focused={focused || undefined}>
<CampaignHomeLayout
actions={actions}
focused={focused}

View file

@ -4,13 +4,23 @@ import { describe, expect, it } from 'vitest'
import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout'
describe('CampaignHomeLayout', () => {
it('renders actions in the home-actions slot', () => {
render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
it('fills the scrollport height on mobile', () => {
const { container } = render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
expect(screen.getByText('Actions block').closest('[data-slot="home-actions"]')).not.toBeNull()
const root = container.firstElementChild
expect(root?.className).toContain('h-full')
expect(root?.className).toContain('min-h-0')
})
it('renders search slot when provided', () => {
it('renders actions in the home-actions slot inside home-dock', () => {
const { container } = render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
const actions = container.querySelector('[data-slot="home-actions"]')
expect(actions).not.toBeNull()
expect(actions?.closest('[data-slot="home-dock"]')).not.toBeNull()
})
it('renders search slot inside home-dock when provided', () => {
render(
<CampaignHomeLayout
actions={<p>Actions block</p>}
@ -18,23 +28,45 @@ describe('CampaignHomeLayout', () => {
/>,
)
expect(
screen.getByTestId('home-search-stub').closest('[data-slot="home-search"]'),
).not.toBeNull()
const search = screen.getByTestId('home-search-stub').closest('[data-slot="home-search"]')
expect(search).not.toBeNull()
expect(search?.closest('[data-slot="home-dock"]')).not.toBeNull()
})
it('omits search slot when not provided', () => {
const { container } = render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
expect(container.querySelector('[data-slot="home-search"]')).toBeNull()
expect(container.querySelector('[data-slot="home-dock"]')).not.toBeNull()
})
it('renders mobile thumb-zone spacer above actions', () => {
it('renders mobile thumb-zone spacer above home-dock', () => {
const { container } = render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
const spacer = container.querySelector('[data-slot="home-actions"]')?.previousElementSibling
const dock = container.querySelector('[data-slot="home-dock"]')
const spacer = dock?.previousElementSibling
expect(spacer?.getAttribute('data-slot')).toBe('home-thumb-spacer')
expect(spacer?.getAttribute('aria-hidden')).toBe('true')
expect(spacer?.className).toContain('flex-1')
expect(spacer?.className).toContain('md:hidden')
})
it('hides spacer, summary, and actions when focused but keeps search', () => {
const { container } = render(
<CampaignHomeLayout
actions={<p>Actions block</p>}
focused
searchSlot={<p data-testid="home-search-stub">Search stub</p>}
summarySlot={<p data-testid="home-summary-stub">Summary stub</p>}
/>,
)
expect(container.querySelector('[data-slot="home-thumb-spacer"]')?.className).toContain(
'hidden',
)
expect(container.querySelector('[data-slot="home-summary"]')?.className).toContain('hidden')
expect(container.querySelector('[data-slot="home-actions"]')?.className).toContain('hidden')
expect(container.querySelector('[data-slot="home-search"]')).not.toBeNull()
expect(container.querySelector('[data-slot="home-dock"]')).not.toBeNull()
})
})