mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
B46: anchor home actions in thumb zone via CampaignHomeLayout.
Mobile flex spacer keeps the B44/B45 strip in the lower viewport above bottom nav; md+ keeps actions at the top and reserves searchSlot order for B47.
This commit is contained in:
parent
b660d5adc0
commit
4658cdd686
7 changed files with 87 additions and 19 deletions
|
|
@ -7,11 +7,12 @@ alwaysApply: false
|
|||
|
||||
## Status
|
||||
|
||||
- **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). Próximo: **B46** thumb-zone → **B47–B55** busca global.
|
||||
- **B46–B55 registrados no roadmap (2026-07-29, sem código):** thumb-zone do Início (**B46**) + 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; clique = rotas de detalhe existentes; TIs no grupo 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). Próximo: **B47** busca global.
|
||||
- **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).
|
||||
- **B43 entregue em código (2026-07-29):** chassis UX-1 — `/campanha` fica só shell (`CampaignPageShell` vazio); dashboard staff move para `/campanha/quadro` (corpo idêntico ao antigo Início); `LeaderContactsPanel` em `/campanha/contatos`; nav staff ganha **Quadro** (sidebar; fora do bottom nav mobile); liderança **Início** + **Meus contatos**; `requireCampaignPageActor` redireciona leader bloqueado para `/campanha/contatos` (não para o Início blank). Sem migration. Plano: [`inicio-em-branco-quadro.md`](../../../docs/plans/inicio-em-branco-quadro.md). Desbloqueia **B44** / **B45**.
|
||||
- **Sessão observada CG 2026-07-29 (Bloco B):** 1º hands-on ~54 min. Achados U1–U8 / O9–O11 — não rola; mental model **Ação→Local→Quem**; confirmações invisíveis; forms precisam ser contínuos; hit = lista sem assessor × votos 2022; mapa ≠ mesa de trabalho. Pivot: tabelas=bulk; operação=fluxos lineares tipo Nubank. Snapshot [`sessao-observada-coordenador-2026-07-29-snapshot.md`](../../../docs/plans/sessao-observada-coordenador-2026-07-29-snapshot.md); CUSTOMER.md/OST atualizados. Prazo soft: manejável até reunião assessoria **03/08**. **Chassis:** ~~B43~~ ✓ → ~~B44~~ ✓ → ~~B45~~ ✓ → **B46** thumb-zone → **B47–B55** busca global (wizards depois).
|
||||
- **Sessão observada CG 2026-07-29 (Bloco B):** 1º hands-on ~54 min. Achados U1–U8 / O9–O11 — não rola; mental model **Ação→Local→Quem**; confirmações invisíveis; forms precisam ser contínuos; hit = lista sem assessor × votos 2022; mapa ≠ mesa de trabalho. Pivot: tabelas=bulk; operação=fluxos lineares tipo Nubank. Snapshot [`sessao-observada-coordenador-2026-07-29-snapshot.md`](../../../docs/plans/sessao-observada-coordenador-2026-07-29-snapshot.md); CUSTOMER.md/OST atualizados. Prazo soft: manejável até reunião assessoria **03/08**. **Chassis:** ~~B43~~ ✓ → ~~B44~~ ✓ → ~~B45~~ ✓ → ~~B46~~ ✓ → **B47–B55** busca global (wizards depois).
|
||||
- **E12+ entregue em código (2026-07-29):** escala/DRY pós-E12 — F1 forçada: `src/lib/metropolitanoTerritoryPeers.ts` unifica constantes, filtros Salvador vs demais RMS e `catalogPeersForSlug` (rollup E17/E12, `slugsForMetropolitanoSubRow`, benchmark T4). F2 não entrou — `benchmarkBySlug` já memoiza por slug. Spec `metropolitanoTerritoryPeers.unit.spec.ts`. Sem migration/UI. Plano: [`escala-dry-pos-e12.md`](../../../docs/plans/escala-dry-pos-e12.md).
|
||||
- **B17+ entregue em código (2026-07-29):** Escala/DRY pós-B17 — F1: `readCampaignColumnVisibility` antes do `Promise.all` e `loadLeadershipOptions` / `loadStateDeputyOptions` só quando a coluna de relação está visível (`resolveVisibleColumns`). F2: `CampaignListEmptyState` na prop `empty` de `CampaignTable` (e `SupporterList`) em demandas, organizações e apoiadores, para o botão "Colunas" sobreviver com zero linhas. Plano: [`escala-dry-pos-b17.md`](../../../docs/plans/escala-dry-pos-b17.md).
|
||||
- **Fill-in remover coluna Tipo entregue em código (2026-07-29):** em `/campanha/municipios`, sumiram a coluna "Tipo", o filtro single-select no header (B16), o ramo mobile em `MunicipalityFilters`, a sort key `kind` e o param `?kind=` em `municipalityListUrl` / `buildMunicipalityListWhere` / `payloadSortFieldByKey` — bookmarks antigos ignoram `kind` no parse. `municipalityKindLabels` permanece para detalhe, mapa e dossiê. `MunicipalityHeaderFilter` ficou só com filtros multi (`singleRows`/`applyMunicipalityKindFilter` deletados). B17: 11 colunas staff; e2e do picker usa Território; B18 usa `?priority=alta`. Sem migration/action/Consent. Plano: [`remover-coluna-tipo-municipios.md`](../../../docs/plans/remover-coluna-tipo-municipios.md).
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ Início blank (**B43**) + ações (**B44/B45**) + posição (**B46**). Pedido (2
|
|||
|
||||
## Questões em aberto
|
||||
|
||||
- **Ordem de foco no `md+`:** com `searchSlot`, o visual fica busca→ações (`order-*`), mas o DOM hoje é ações→busca. No craft do B47, colocar o nó de busca **antes** de `home-actions` no markup e usar `order-*` só no mobile, **ou** aceitar exceção documentada se produto priorizar o markup atual.
|
||||
- **Transporte dos hits: `POST` JSON vs server action vs fetch RSC?** **Opções:** A rota `campaignJsonMutationRoute`-like GET/POST search | B server action | C debounce + `router` query. **Recomendação:** A — `GET`/`POST` same-origin com body/query curta, access no loader, sem sujar a URL do Início com `?q=` (Início não é lista). _(assumido)_
|
||||
- **Limiar mínimo de caracteres?** **Opções:** 1 | 2. **Recomendação:** 2 para reduzir ruído em nomes curtos; 1 se critique pedir. _(assumido)_
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Posição dos botões de ação do Início (thumb zone)
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-07-29
|
||||
Status: entregue (2026-07-29)
|
||||
Atualizado em: 2026-07-29 — as-built: `CampaignHomeLayout` (`min-h-full` + spacer `flex-1` só em mobile; `data-slot="home-actions"` / `home-search`; ordem flex `order-*` — mobile ações→busca, `md+` busca→ações); `/campanha` usa `CampaignPageShell` `min-h-full` + layout; decisão sticky vs coluna = coluna (opção B). Unit `campaignHomeLayout.unit.spec.ts`; e2e B45 intacto. Sem migration.
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B46 — chassis UX-1)
|
||||
Impeccable: B — encaixe de layout no Início sob tema `campaign` (strip B44/B45 já montada)
|
||||
Appetite: ~0,5 dia eng; CSS/layout do shell do Início; sem migration
|
||||
|
|
@ -46,7 +46,7 @@ Com **B43** o Início está blank; **B44/B45** colocam a strip de ações. Sem r
|
|||
|
||||
## Questões em aberto
|
||||
|
||||
- **`position: sticky` vs flex column com `mt-auto` no mobile?** **Opções:** A sticky na base do scroller | B coluna full-height com strip no fim. **Recomendação:** B se o Início continuar sem scroll de conteúdo útil (blank + strip + busca); A se a 2ª dobra voltar cedo. _(assumido — medir no PR com o shell real)_
|
||||
- **`position: sticky` vs flex column com `mt-auto` no mobile?** **Resolvido (2026-07-29):** opção B — coluna full-height com spacer `flex-1` (`CampaignHomeLayout`); sticky não foi necessário com Início blank.
|
||||
|
||||
## Abordagem proposta
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,5 @@
|
|||
import { CampaignHomeActions } from '@/components/campaign/dashboard/CampaignHomeActions'
|
||||
import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout'
|
||||
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
|
||||
import { isStaffCampaignRole } from '@/lib/campaignRoles'
|
||||
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
|
||||
|
|
@ -14,10 +15,14 @@ export default async function CampaignHomePage() {
|
|||
: undefined
|
||||
|
||||
return (
|
||||
<CampaignPageShell aria-label="Início">
|
||||
<CampaignHomeActions
|
||||
role={user.role}
|
||||
uncoveredMunicipalitiesHref={uncoveredMunicipalitiesHref}
|
||||
<CampaignPageShell aria-label="Início" className="min-h-full">
|
||||
<CampaignHomeLayout
|
||||
actions={
|
||||
<CampaignHomeActions
|
||||
role={user.role}
|
||||
uncoveredMunicipalitiesHref={uncoveredMunicipalitiesHref}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</CampaignPageShell>
|
||||
)
|
||||
|
|
|
|||
21
src/components/campaign/dashboard/CampaignHomeLayout.tsx
Normal file
21
src/components/campaign/dashboard/CampaignHomeLayout.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { ReactNode } from 'react'
|
||||
|
||||
export const CampaignHomeLayout = ({
|
||||
actions,
|
||||
searchSlot,
|
||||
}: {
|
||||
actions: ReactNode
|
||||
searchSlot?: ReactNode
|
||||
}) => (
|
||||
<div className="flex min-h-full w-full flex-col md:min-h-0">
|
||||
<div aria-hidden className="min-h-0 flex-1 md:hidden" />
|
||||
<div data-slot="home-actions" className="order-1 min-w-0 md:order-2">
|
||||
{actions}
|
||||
</div>
|
||||
{searchSlot ? (
|
||||
<div data-slot="home-search" className="order-3 mt-4 min-w-0 md:order-1 md:mt-0 md:mb-6">
|
||||
{searchSlot}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
40
tests/unit/campaignHomeLayout.unit.spec.tsx
Normal file
40
tests/unit/campaignHomeLayout.unit.spec.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
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>} />)
|
||||
|
||||
expect(screen.getByText('Actions block').closest('[data-slot="home-actions"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders search slot when provided', () => {
|
||||
render(
|
||||
<CampaignHomeLayout
|
||||
actions={<p>Actions block</p>}
|
||||
searchSlot={<p data-testid="home-search-stub">Search stub</p>}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByTestId('home-search-stub').closest('[data-slot="home-search"]'),
|
||||
).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()
|
||||
})
|
||||
|
||||
it('renders mobile thumb-zone spacer above actions', () => {
|
||||
const { container } = render(<CampaignHomeLayout actions={<p>Actions block</p>} />)
|
||||
|
||||
const spacer = container.querySelector('[data-slot="home-actions"]')?.previousElementSibling
|
||||
expect(spacer?.getAttribute('aria-hidden')).toBe('true')
|
||||
expect(spacer?.className).toContain('flex-1')
|
||||
expect(spacer?.className).toContain('md:hidden')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue