From 5b6af56b2230f52fa01141fab1b86246fe34cab0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 17:53:39 +0000 Subject: [PATCH 1/4] =?UTF-8?q?B122=20=E2=80=94=20In=C3=ADcio=20mobile:=20?= =?UTF-8?q?a=C3=A7=C3=B5es=20em=20grade=202=C3=973=20sem=20scroll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CampaignHomeActionStrip: variant responsive (grid lt-md, strip md+) - CampaignHomeActionButton: layout strip/grid/responsive class contracts - CampaignHomeLayout: remove mobile bleed for grid gutter - CampaignQuickActionsDrawer: explicit variant=strip - Plans: inicio-acoes-grid-2x3-mobile + impl - Tests: unit grid/strip + e2e mobile visibility Co-authored-by: Francisco Solla --- .../inicio-acoes-grid-2x3-mobile-impl.md | 65 +++++++++ docs/plans/inicio-acoes-grid-2x3-mobile.md | 123 ++++++++++++++++++ .../dashboard/CampaignHomeActionButton.tsx | 22 +++- .../dashboard/CampaignHomeActionStrip.tsx | 37 ++++-- .../dashboard/CampaignHomeActions.tsx | 2 +- .../campaign/dashboard/CampaignHomeLayout.tsx | 2 +- .../shell/CampaignQuickActionsDrawer.tsx | 1 + tests/e2e/campaignHomeActions.e2e.spec.ts | 26 ++++ .../campaignHomeActionButton.unit.spec.tsx | 35 ++++- tests/unit/campaignHomeLayout.unit.spec.tsx | 5 +- 10 files changed, 300 insertions(+), 18 deletions(-) create mode 100644 docs/plans/inicio-acoes-grid-2x3-mobile-impl.md create mode 100644 docs/plans/inicio-acoes-grid-2x3-mobile.md diff --git a/docs/plans/inicio-acoes-grid-2x3-mobile-impl.md b/docs/plans/inicio-acoes-grid-2x3-mobile-impl.md new file mode 100644 index 00000000..ad4a0698 --- /dev/null +++ b/docs/plans/inicio-acoes-grid-2x3-mobile-impl.md @@ -0,0 +1,65 @@ +# Impl: Início mobile — ações em grade 2×3 (sem scroll) + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #249 +Intenção: docs/plans/inicio-acoes-grid-2x3-mobile.md +Appetite restante: ~0,5d (layout + tests) + +## Leitura da intenção + +- **Outcome:** No Início mobile, as 6 ações staff aparecem numa grade 2×3 sem scroll horizontal; desktop mantém strip. +- **O que NÃO negociar:** drawer de outras rotas continua strip; catálogo/lockdown leader; long-press/tooltip. +- **O que reavaliar:** bleed `-mx-4` do B99 — removido no mobile porque a grade cabe no gutter. + +## Abordagem recomendada + +```mermaid +flowchart LR + Strip["CampaignHomeActionStrip"] + Strip -->|variant=responsive| R["grid lt-md / flex md+"] + Strip -->|variant=strip| S["flex + overflow-x"] +``` + +**Opções consideradas:** A) CSS só no strip (`grid md:flex`) | B) componente `CampaignHomeActionGrid` | C) duplicar markup no `CampaignHomeActions` +**Recomendação:** **A** — edita o dono; `variant` explícito para o drawer. +**Rejeitadas:** B (twin); C (duplica map de ações). + +### Componentes / mudanças + +- **`CampaignHomeActionStrip`** (`src/components/campaign/dashboard/CampaignHomeActionStrip.tsx`): prop `variant?: 'strip' | 'responsive'` (default `'responsive'`). Responsive: `grid grid-cols-2 gap-3` + sem overflow no mobile; `md:flex md:overflow-x-auto` + pan handlers. Strip: comportamento atual. +- **`CampaignHomeActionButton`** (`CampaignHomeActionButton.tsx`): `actionControlClassName` ganha `w-full md:w-[5.5rem]` quando pai é responsive (via prop `layout` ou classe compartilhada exportada). +- **`CampaignHomeActions`**: passa `variant="responsive"` (explícito). +- **`CampaignQuickActionsDrawer`**: passa `variant="strip"`. +- **`CampaignHomeLayout`**: `home-actions` perde `-mx-4 w-[calc(100%+2rem)]` no mobile; bleed só se strip voltar (não neste slice). +- **Migration:** sem migration. +- **UI:** Impeccable B — craft; sem novo token. + +### Dados → forma + +N/A — launcher estático. + +## Fases verificáveis + +1. **Strip + button** — `variant` + classes grid/strip; drawer `variant="strip"`. +2. **Layout + tests** — remover bleed mobile; unit layout/strip; e2e 6 ações visíveis em 390px. +3. **Gates** — `pnpm gate:fast`; `pnpm push`. + +## Rabbit holes / Não escopo (engenharia) + +- Extrair tile genérico wizard/home. +- Grid no drawer. +- Mudar tamanho do círculo sem pedido de produto. + +## Riscos e mitigação + +- **Pan handlers em grid mobile:** inofensivos (sem overflow); não montar scroller ref em grid se simplificar. +- **Teste bleed antigo:** atualizar spec `campaignHomeLayout` para refletir gutter no mobile. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes de domínio previstos (unit/int) onde access/write paths mudam + +Self-score decision-quality: 5/5 diff --git a/docs/plans/inicio-acoes-grid-2x3-mobile.md b/docs/plans/inicio-acoes-grid-2x3-mobile.md new file mode 100644 index 00000000..b2872fe7 --- /dev/null +++ b/docs/plans/inicio-acoes-grid-2x3-mobile.md @@ -0,0 +1,123 @@ +# Início mobile — ações em grade 2×3 (sem scroll) + +Status: registrado +Atualizado em: 2026-08-02 +Issue: #249 +Priority: P1 +Model: composer-2.5 +Impeccable: B — `CampaignHomeActionStrip` / `CampaignHomeLayout` (Início staff/leader) +Appetite: ~0,5d eng; layout responsivo; sem migration +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (Clarity under pressure; Feel the action) / `DESIGN.md` · B44/B45 strip · B63 grid 2×3 wizard · tema `campaign`. + +Na implementação: craft compacto → critique → polish (iPhone 390px; 6 ações staff). + +Brief: + +- **Persona / contexto:** CG/assessor no celular — polegar na thumb zone; hoje precisa pan horizontal para ver as 6 ações. +- **Job principal:** ver **todas** as ações staff (6) de uma vez, sem scroll horizontal, em grade 2 colunas × 3 linhas. +- **Estratégia de cor:** herda B44 (círculo `bg-muted` + label). +- **Edit where you see:** não — launchers. +- **Anti-goals:** card com borda envolvendo a grade; scroll horizontal no mobile do Início; mudar o drawer de outras rotas (continua strip). + +### Wireframe (texto) + +```text +┌─ /campanha mobile (< md) ──────────────────────┐ +│ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ Ajustar │ │Registrar │ │ +│ │ votos │ │ sinal │ │ +│ └──────────┘ └──────────┘ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ Mudar │ │Atualizar │ │ +│ │tendência │ │liderança │ │ +│ └──────────┘ └──────────┘ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │Registrar │ │ Ver │ │ +│ │ pedido │ │esquecidos│ │ +│ └──────────┘ └──────────┘ │ +│ │ +│ ┌ Buscar na campanha ────────────────────────┐│ +└────────────────────────────────────────────────┘ + +md+: strip horizontal atual (B44/B67) — inalterada. +``` + +## Dados → decisão → apresentação + +Dados: N/A — chrome de launcher; catálogo B45 inalterado. + +## Contexto + +**B44–B74 / B99** iteraram na strip horizontal (gap, bleed, pan). Com 6 ações staff, o mobile ainda exige pan — produto pede **grade 2×3 sem scroll** no Início. Precedente visual: `WizardSignalTypeStep` (`grid-cols-2 gap-3`). O **drawer** de ações rápidas (B105) permanece strip horizontal no peek. + +## Objetivos + +- **< `md` (Início):** `CampaignHomeActions` renderiza grade `grid-cols-2 gap-3`; 6 ações staff visíveis sem `overflow-x`; liderança (2 ações) em 1 linha. +- **`md+`:** strip horizontal atual (scroll, pan fine, tooltip). +- `CampaignQuickActionsDrawer`: continua `variant="strip"` (sem grade). +- Remover bleed `-mx-4` do slot `home-actions` no mobile (grade respeita gutter `p-4`). +- Long-press Drawer / Tooltip fine inalterados. +- Unit + e2e smoke: grade no mobile; strip no drawer; ações staff todas visíveis em viewport 390. +- Sem migration / collection / server action. + +## Decisões travadas + +- **Grade só no Início mobile; strip no drawer e no `md+`.** **Rejeitado:** grade global (drawer peek não cabe 3 linhas); manter strip no mobile “com gap menor” (B99/B74 esgotaram). +- **`variant` no dono `CampaignHomeActionStrip`.** `responsive` (default no Início) vs `strip` (drawer). **Rejeitado:** componente paralelo `CampaignHomeActionGrid`. +- **Botão com classes responsivas** (`w-full md:w-[5.5rem]`). **Rejeitado:** segundo primitivo de botão. +- **i18n:** ids/classes em inglês; copy B58 intacta. + +## Questões em aberto + +- **Círculo menor na grade?** **Opções:** A) manter `size-14` | B) `size-12` na grade. **Recomendação:** **A** — célula larga o suficiente; critique mede. _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart TB + Home["CampaignHomeActions"] --> Strip["CampaignHomeActionStrip variant=responsive"] + Drawer["CampaignQuickActionsDrawer"] --> Strip2["CampaignHomeActionStrip variant=strip"] + Strip --> Grid["lt md: grid 2×3"] + Strip --> HStrip["md+: horizontal strip"] +``` + +Componentes: + +- **`CampaignHomeActionStrip.tsx`:** prop `variant`; lista `grid grid-cols-2 gap-3` vs `flex` + overflow. +- **`CampaignHomeActionButton.tsx`:** `w-full` na grade / largura fixa na strip. +- **`CampaignHomeLayout.tsx`:** remover bleed mobile do slot `home-actions`. +- **Testes:** unit strip/grid classes; e2e 6 ações visíveis em 390px. + +## Dependências + +- Dura: B45 ✓ (catálogo), B44 ✓ (primitivo). Soft: B99 bleed (revertido no mobile para grade). + +## Não escopo + +- Catálogo / labels / wizards. +- Grade no drawer B105. +- Mudar ordem thumb-zone (B46). + +## Rabbit holes + +- **Unificar grade wizard + home num tile genérico.** Mitigação: classes Tailwind compartilhadas só; sem extrair até 3º call site. +- **CSS container queries.** Mitigação: breakpoint `md` como o resto do Início. + +## Adiado com gatilho + +- **Grade no drawer** se peek crescer. Revisitar com redesign B105. +- **3 colunas em tablet portrait.** Revisitar se `md` grid pedido. + +## Referências + +- Issue #249 · `WizardSignalTypeStep.tsx` (grid 2×3) +- `CampaignHomeActionStrip.tsx` · `CampaignHomeLayout.tsx` · `CampaignQuickActionsDrawer.tsx` +- [botao-acao-inicio-strip.md](botao-acao-inicio-strip.md) · [strip-acoes-edge-gap-inicio.md](strip-acoes-edge-gap-inicio.md) +- `PRODUCT.md` / `DESIGN.md` + +Qualidade de decisão: 5/5 diff --git a/src/components/campaign/dashboard/CampaignHomeActionButton.tsx b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx index 44273173..d927bdc1 100644 --- a/src/components/campaign/dashboard/CampaignHomeActionButton.tsx +++ b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx @@ -25,11 +25,21 @@ export type CampaignHomeActionButtonProps = { href?: string onClick?: () => void disabled?: boolean + layout?: 'strip' | 'grid' | 'responsive' } -export const actionControlClassName = +export const actionStripControlClassName = 'group box-content flex min-h-11 w-[5.5rem] shrink-0 snap-start flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50' +export const actionGridControlClassName = + 'group box-content flex min-h-11 w-full flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50' + +export const actionResponsiveControlClassName = + 'group box-content flex min-h-11 w-full flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50 md:w-[5.5rem] md:shrink-0 md:snap-start' + +/** @deprecated Use actionStripControlClassName or actionGridControlClassName */ +export const actionControlClassName = actionStripControlClassName + const circleClassName = 'flex size-14 items-center justify-center rounded-full bg-muted text-foreground transition-[transform,colors] duration-150 ease-out motion-reduce:transition-none pointer-fine:group-hover:scale-[1.05] pointer-coarse:group-data-[pressing=true]:scale-[1.05] motion-reduce:scale-100 group-hover:bg-muted/80 group-focus-visible:bg-muted/80' @@ -51,6 +61,7 @@ export const CampaignHomeActionButton = ({ href, onClick, disabled = false, + layout = 'strip', }: CampaignHomeActionButtonProps) => { const isCoarsePointer = useCoarsePointer() const [drawerOpen, setDrawerOpen] = useState(false) @@ -74,7 +85,14 @@ export const CampaignHomeActionButton = ({ }, }) - const className = cn(actionControlClassName, isInert && 'cursor-default') + const className = cn( + layout === 'grid' + ? actionGridControlClassName + : layout === 'responsive' + ? actionResponsiveControlClassName + : actionStripControlClassName, + isInert && 'cursor-default', + ) const aria = { 'aria-label': label, 'aria-describedby': drawerOpen && longPressEnabled ? descriptionId : undefined, diff --git a/src/components/campaign/dashboard/CampaignHomeActionStrip.tsx b/src/components/campaign/dashboard/CampaignHomeActionStrip.tsx index 35f41959..99c3d2d5 100644 --- a/src/components/campaign/dashboard/CampaignHomeActionStrip.tsx +++ b/src/components/campaign/dashboard/CampaignHomeActionStrip.tsx @@ -13,6 +13,8 @@ import { cn } from '@/lib/utils' /** Horizontal pan threshold (px) — below this, child link/button clicks still fire. */ export const HOME_ACTION_STRIP_DRAG_THRESHOLD_PX = 8 +export type CampaignHomeActionStripVariant = 'strip' | 'responsive' + type DragSession = { pointerId: number startX: number @@ -33,15 +35,19 @@ export const CampaignHomeActionStrip = ({ actions, ariaLabel = 'Ações rápidas', className, + variant = 'responsive', }: { actions?: readonly (CampaignHomeActionButtonProps & { id?: string })[] ariaLabel?: string className?: string + /** `responsive`: 2×3 grid on mobile, horizontal strip on md+. `strip`: always horizontal. */ + variant?: CampaignHomeActionStripVariant }) => { const isCoarsePointer = useCoarsePointer() const scrollerRef = useRef(null) const dragRef = useRef(null) const suppressClickRef = useRef(false) + const isStripLayout = variant === 'strip' const endDragSession = useCallback((pointerId: number) => { const session = dragRef.current @@ -126,25 +132,36 @@ export const CampaignHomeActionStrip = ({ onClickCapture, } + const listClassName = cn( + 'm-0 list-none p-0', + isStripLayout + ? 'flex min-w-max snap-x snap-proximity gap-0 px-4 pb-1 md:px-0' + : 'grid grid-cols-2 gap-3 md:flex md:min-w-max md:snap-x md:snap-proximity md:gap-0 md:px-0 md:pb-1', + ) + + const scrollerClassName = cn( + 'min-w-0', + isStripLayout + ? 'overflow-x-auto overflow-y-hidden overscroll-x-contain [touch-action:pan-x] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden pointer-fine:cursor-grab pointer-fine:data-[dragging=true]:cursor-grabbing pointer-fine:data-[dragging=true]:select-none' + : 'md:overflow-x-auto md:overflow-y-hidden md:overscroll-x-contain md:[touch-action:pan-x] md:[scrollbar-width:none] md:[&::-webkit-scrollbar]:hidden md:pointer-fine:cursor-grab md:pointer-fine:data-[dragging=true]:cursor-grabbing md:pointer-fine:data-[dragging=true]:select-none', + className, + ) + return (
-
    +
      {actions?.map(({ id, ...button }) => (
    • - +
    • ))}
    diff --git a/src/components/campaign/dashboard/CampaignHomeActions.tsx b/src/components/campaign/dashboard/CampaignHomeActions.tsx index bbd4bbf3..3323e1c4 100644 --- a/src/components/campaign/dashboard/CampaignHomeActions.tsx +++ b/src/components/campaign/dashboard/CampaignHomeActions.tsx @@ -19,5 +19,5 @@ export const CampaignHomeActions = ({ if (actions.length === 0) return null - return + return } diff --git a/src/components/campaign/dashboard/CampaignHomeLayout.tsx b/src/components/campaign/dashboard/CampaignHomeLayout.tsx index 40f45bdc..7345cf1c 100644 --- a/src/components/campaign/dashboard/CampaignHomeLayout.tsx +++ b/src/components/campaign/dashboard/CampaignHomeLayout.tsx @@ -75,7 +75,7 @@ export const CampaignHomeLayout = ({ >
    {actions}
    diff --git a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx index b5137564..5a337443 100644 --- a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx @@ -136,6 +136,7 @@ export const CampaignQuickActionsDrawer = ({ {showActions ? (
    ({ id: action.id, label: action.label, diff --git a/tests/e2e/campaignHomeActions.e2e.spec.ts b/tests/e2e/campaignHomeActions.e2e.spec.ts index 655b1a41..35e6de86 100644 --- a/tests/e2e/campaignHomeActions.e2e.spec.ts +++ b/tests/e2e/campaignHomeActions.e2e.spec.ts @@ -102,6 +102,32 @@ test.describe('Início — busca global (B47)', () => { }) test.describe('Início — catálogo de ações (B45)', () => { + test.use({ viewport: { width: 390, height: 844 } }) + + test('staff sees all six home actions in the mobile grid without horizontal scroll', async ({ + campaign, + page, + }) => { + const { fixtures } = campaign + const coordinator = await fixtures.createCampaignUser('coordinator', { + name: fixtures.value('Coordenadora Geral'), + }) + + await campaign.login(page, coordinator.email!, coordinator.password) + const actionsRegion = page.getByLabel('Ações rápidas') + await expect(actionsRegion).toBeVisible() + + for (const label of staffActionLabels) { + await expect(page.getByRole('link', { name: label, exact: true }).or( + page.getByRole('button', { name: label, exact: true }), + )).toBeVisible() + } + + const overflowX = await actionsRegion.evaluate((el) => getComputedStyle(el).overflowX) + expect(overflowX).not.toBe('auto') + expect(overflowX).not.toBe('scroll') + }) + test('staff sees six home actions and can open municipalities without coverage', async ({ campaign, page, diff --git a/tests/unit/campaignHomeActionButton.unit.spec.tsx b/tests/unit/campaignHomeActionButton.unit.spec.tsx index 0bd85135..49c0b821 100644 --- a/tests/unit/campaignHomeActionButton.unit.spec.tsx +++ b/tests/unit/campaignHomeActionButton.unit.spec.tsx @@ -178,6 +178,7 @@ describe('CampaignHomeActionStrip', () => { it('renders a horizontal scroller with list semantics', () => { const { container } = render( { expect(scroller).toBeTruthy() const list = scroller?.querySelector('ul[role="list"]') expect(list).toBeTruthy() + expect(list?.className).toMatch(/flex/) + }) + + it('renders a 2-column grid on mobile when variant is responsive', () => { + const { container } = render( + , + ) + + const region = container.querySelector('[aria-label="Ações rápidas"]') + expect(region).toBeTruthy() + const list = region?.querySelector('ul[role="list"]') + expect(list).toBeTruthy() + expect(list?.className).toMatch(/grid-cols-2/) + expect(list?.className).toMatch(/md:flex/) + expect(region?.className).not.toMatch(/overflow-x-auto/) + expect(region?.className).toMatch(/md:overflow-x-auto/) }) it('scrolls horizontally on pointer-fine drag past the threshold', () => { - const { container } = render() + const { container } = render( + , + ) const scroller = getScroller(container) mockScrollerOverflow(scroller) @@ -227,6 +252,7 @@ describe('CampaignHomeActionStrip', () => { const onClick = vi.fn() const { container } = render( , ) @@ -246,6 +272,7 @@ describe('CampaignHomeActionStrip', () => { const onClick = vi.fn() const { container } = render( , ) @@ -263,7 +290,10 @@ describe('CampaignHomeActionStrip', () => { it('allows child click when movement stays below the drag threshold', () => { const onClick = vi.fn() const { container } = render( - , + , ) const scroller = getScroller(container) mockScrollerOverflow(scroller) @@ -285,6 +315,7 @@ describe('CampaignHomeActionStrip', () => { const onLinkClick = vi.fn() const { container } = render( { expect(actions?.closest('[data-slot="home-dock"]')).not.toBeNull() }) - it('bleeds home-actions edge-to-edge on mobile (compensates scroll p-4)', () => { + it('keeps home-actions within page gutter on mobile (B122 grid)', () => { const { container } = render(Actions block

    } />) const actions = container.querySelector('[data-slot="home-actions"]') expect(actions).not.toBeNull() - expect(actions?.closest('[data-slot="home-actions-chrome"]')).not.toBeNull() + expect(actions?.className).not.toMatch(/-mx-4/) + expect(actions?.className).not.toMatch(/calc\(100%\+2rem\)/) }) it('does not clip horizontal bleed on the actions retraction shell', () => { From 8a64e330e600ab2377651c22b16871c1b733825b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 17:55:13 +0000 Subject: [PATCH 2/4] =?UTF-8?q?B122=20=E2=80=94=20fix=20unit=20assertion?= =?UTF-8?q?=20for=20responsive=20overflow=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Solla --- tests/unit/campaignHomeActionButton.unit.spec.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/campaignHomeActionButton.unit.spec.tsx b/tests/unit/campaignHomeActionButton.unit.spec.tsx index 49c0b821..f361e3d2 100644 --- a/tests/unit/campaignHomeActionButton.unit.spec.tsx +++ b/tests/unit/campaignHomeActionButton.unit.spec.tsx @@ -210,8 +210,9 @@ describe('CampaignHomeActionStrip', () => { expect(list).toBeTruthy() expect(list?.className).toMatch(/grid-cols-2/) expect(list?.className).toMatch(/md:flex/) - expect(region?.className).not.toMatch(/overflow-x-auto/) - expect(region?.className).toMatch(/md:overflow-x-auto/) + const regionClasses = region?.className.split(/\s+/) ?? [] + expect(regionClasses).not.toContain('overflow-x-auto') + expect(regionClasses.some((token) => token.includes('md:overflow-x-auto'))).toBe(true) }) it('scrolls horizontally on pointer-fine drag past the threshold', () => { From 74c9c3a6e5699ef8d5f9fd2cfb2206bb84c2e9de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 17:57:40 +0000 Subject: [PATCH 3/4] =?UTF-8?q?B122=20=E2=80=94=20simplify:=20private=20bu?= =?UTF-8?q?tton=20classes,=20drop=20bleed=20shell=20prop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Solla --- .../dashboard/CampaignHomeActionButton.tsx | 27 +++++++++---------- .../campaign/dashboard/CampaignHomeLayout.tsx | 1 - tests/e2e/campaignHomeActions.e2e.spec.ts | 27 +++---------------- .../campaignHomeActionButton.unit.spec.tsx | 4 +-- tests/unit/campaignHomeLayout.unit.spec.tsx | 10 +------ 5 files changed, 19 insertions(+), 50 deletions(-) diff --git a/src/components/campaign/dashboard/CampaignHomeActionButton.tsx b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx index d927bdc1..1421e4ce 100644 --- a/src/components/campaign/dashboard/CampaignHomeActionButton.tsx +++ b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx @@ -25,20 +25,21 @@ export type CampaignHomeActionButtonProps = { href?: string onClick?: () => void disabled?: boolean - layout?: 'strip' | 'grid' | 'responsive' + layout?: 'strip' | 'responsive' } -export const actionStripControlClassName = - 'group box-content flex min-h-11 w-[5.5rem] shrink-0 snap-start flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50' +const actionControlBaseClassName = + 'group box-content flex min-h-11 flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50' -export const actionGridControlClassName = - 'group box-content flex min-h-11 w-full flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50' +const actionStripControlClassName = cn( + actionControlBaseClassName, + 'w-[5.5rem] shrink-0 snap-start', +) -export const actionResponsiveControlClassName = - 'group box-content flex min-h-11 w-full flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50 md:w-[5.5rem] md:shrink-0 md:snap-start' - -/** @deprecated Use actionStripControlClassName or actionGridControlClassName */ -export const actionControlClassName = actionStripControlClassName +const actionResponsiveControlClassName = cn( + actionControlBaseClassName, + 'w-full md:w-[5.5rem] md:shrink-0 md:snap-start', +) const circleClassName = 'flex size-14 items-center justify-center rounded-full bg-muted text-foreground transition-[transform,colors] duration-150 ease-out motion-reduce:transition-none pointer-fine:group-hover:scale-[1.05] pointer-coarse:group-data-[pressing=true]:scale-[1.05] motion-reduce:scale-100 group-hover:bg-muted/80 group-focus-visible:bg-muted/80' @@ -86,11 +87,7 @@ export const CampaignHomeActionButton = ({ }) const className = cn( - layout === 'grid' - ? actionGridControlClassName - : layout === 'responsive' - ? actionResponsiveControlClassName - : actionStripControlClassName, + layout === 'responsive' ? actionResponsiveControlClassName : actionStripControlClassName, isInert && 'cursor-default', ) const aria = { diff --git a/src/components/campaign/dashboard/CampaignHomeLayout.tsx b/src/components/campaign/dashboard/CampaignHomeLayout.tsx index 7345cf1c..3c039a88 100644 --- a/src/components/campaign/dashboard/CampaignHomeLayout.tsx +++ b/src/components/campaign/dashboard/CampaignHomeLayout.tsx @@ -71,7 +71,6 @@ export const CampaignHomeLayout = ({ retracted={focused} slot="home-actions-chrome" className="order-1 md:order-2" - allowHorizontalBleed >
    { test.describe('Início — catálogo de ações (B45)', () => { test.use({ viewport: { width: 390, height: 844 } }) - test('staff sees all six home actions in the mobile grid without horizontal scroll', async ({ + test('staff sees six home actions and can open municipalities without coverage', async ({ campaign, page, }) => { @@ -118,33 +118,14 @@ test.describe('Início — catálogo de ações (B45)', () => { await expect(actionsRegion).toBeVisible() for (const label of staffActionLabels) { - await expect(page.getByRole('link', { name: label, exact: true }).or( - page.getByRole('button', { name: label, exact: true }), - )).toBeVisible() + const link = page.getByRole('link', { name: label, exact: true }) + const button = page.getByRole('button', { name: label, exact: true }) + await expect(link.or(button)).toBeVisible() } const overflowX = await actionsRegion.evaluate((el) => getComputedStyle(el).overflowX) expect(overflowX).not.toBe('auto') expect(overflowX).not.toBe('scroll') - }) - - test('staff sees six home actions and can open municipalities without coverage', async ({ - campaign, - page, - }) => { - const { fixtures } = campaign - const coordinator = await fixtures.createCampaignUser('coordinator', { - name: fixtures.value('Coordenadora Geral'), - }) - - await campaign.login(page, coordinator.email!, coordinator.password) - await expect(page.getByLabel('Ações rápidas')).toBeVisible() - - for (const label of staffActionLabels) { - const link = page.getByRole('link', { name: label, exact: true }) - const button = page.getByRole('button', { name: label, exact: true }) - await expect(link.or(button)).toBeVisible() - } await page.getByRole('link', { name: 'Ajustar votos', exact: true }).click() await page.waitForURL(/\/campanha\/acoes\/atualizar-votos/) diff --git a/tests/unit/campaignHomeActionButton.unit.spec.tsx b/tests/unit/campaignHomeActionButton.unit.spec.tsx index f361e3d2..36ecadb2 100644 --- a/tests/unit/campaignHomeActionButton.unit.spec.tsx +++ b/tests/unit/campaignHomeActionButton.unit.spec.tsx @@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { CampaignHomeActionButton, - actionControlClassName, type CampaignHomeActionButtonProps, } from '@/components/campaign/dashboard/CampaignHomeActionButton' import { @@ -95,7 +94,8 @@ describe('CampaignHomeActionButton', () => { const onClick = vi.fn() renderActionButton({ label: 'Registrar', icon: BarChart3, onClick }) const control = screen.getByRole('button', { name: 'Registrar' }) - expect(control.className).toBe(actionControlClassName) + expect(control.className).toContain('min-h-11') + expect(control.className).toContain('w-[5.5rem]') }) it('opens the description drawer on long-press when the pointer is coarse', () => { diff --git a/tests/unit/campaignHomeLayout.unit.spec.tsx b/tests/unit/campaignHomeLayout.unit.spec.tsx index ede8822e..6e957943 100644 --- a/tests/unit/campaignHomeLayout.unit.spec.tsx +++ b/tests/unit/campaignHomeLayout.unit.spec.tsx @@ -19,7 +19,7 @@ describe('CampaignHomeLayout', () => { expect(actions?.closest('[data-slot="home-dock"]')).not.toBeNull() }) - it('keeps home-actions within page gutter on mobile (B122 grid)', () => { + it('does not bleed home-actions on mobile (B122 grid uses page gutter)', () => { const { container } = render(Actions block

    } />) const actions = container.querySelector('[data-slot="home-actions"]') @@ -28,14 +28,6 @@ describe('CampaignHomeLayout', () => { expect(actions?.className).not.toMatch(/calc\(100%\+2rem\)/) }) - it('does not clip horizontal bleed on the actions retraction shell', () => { - const { container } = render(Actions block

    } />) - - const actionsChrome = container.querySelector('[data-slot="home-actions-chrome"]') - const bleedWrapper = actionsChrome?.querySelector('[data-allow-horizontal-bleed="true"]') - expect(bleedWrapper).not.toBeNull() - }) - it('renders search slot inside home-dock when provided', () => { render( Date: Sun, 2 Aug 2026 17:58:24 +0000 Subject: [PATCH 4/4] =?UTF-8?q?B122=20=E2=80=94=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/CampaignHomeActionButton.tsx | 5 +---- .../campaign/dashboard/CampaignHomeLayout.tsx | 15 ++------------- tests/unit/campaignHomeActionButton.unit.spec.tsx | 4 +--- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/components/campaign/dashboard/CampaignHomeActionButton.tsx b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx index 1421e4ce..ebc00249 100644 --- a/src/components/campaign/dashboard/CampaignHomeActionButton.tsx +++ b/src/components/campaign/dashboard/CampaignHomeActionButton.tsx @@ -31,10 +31,7 @@ export type CampaignHomeActionButtonProps = { const actionControlBaseClassName = 'group box-content flex min-h-11 flex-col items-center gap-2 rounded-md text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50' -const actionStripControlClassName = cn( - actionControlBaseClassName, - 'w-[5.5rem] shrink-0 snap-start', -) +const actionStripControlClassName = cn(actionControlBaseClassName, 'w-[5.5rem] shrink-0 snap-start') const actionResponsiveControlClassName = cn( actionControlBaseClassName, diff --git a/src/components/campaign/dashboard/CampaignHomeLayout.tsx b/src/components/campaign/dashboard/CampaignHomeLayout.tsx index 3c039a88..4e2d37e5 100644 --- a/src/components/campaign/dashboard/CampaignHomeLayout.tsx +++ b/src/components/campaign/dashboard/CampaignHomeLayout.tsx @@ -15,14 +15,11 @@ const HomeChromeRetractionShell = ({ slot, className, children, - allowHorizontalBleed = false, }: { retracted: boolean slot?: string className?: string children: ReactNode - /** B111 — keep Y clip for grid-rows retraction without clipping -mx bleed on X. */ - allowHorizontalBleed?: boolean }) => (
    -
    - {children} -
    +
    {children}
    ) @@ -72,10 +64,7 @@ export const CampaignHomeLayout = ({ slot="home-actions-chrome" className="order-1 md:order-2" > -
    +
    {actions}
    diff --git a/tests/unit/campaignHomeActionButton.unit.spec.tsx b/tests/unit/campaignHomeActionButton.unit.spec.tsx index 36ecadb2..8630dba7 100644 --- a/tests/unit/campaignHomeActionButton.unit.spec.tsx +++ b/tests/unit/campaignHomeActionButton.unit.spec.tsx @@ -216,9 +216,7 @@ describe('CampaignHomeActionStrip', () => { }) it('scrolls horizontally on pointer-fine drag past the threshold', () => { - const { container } = render( - , - ) + const { container } = render() const scroller = getScroller(container) mockScrollerOverflow(scroller)