Merge pull request #136 from fsolla/cursor/pool-i128-b91ce9e1-0a68

B101 — Strip de ações: bleed sem faixa branca + hit area via padding
This commit is contained in:
Francisco Solla 2026-08-01 15:08:42 -03:00 committed by GitHub
commit aae788a64c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 82 additions and 20 deletions

View file

@ -27,8 +27,8 @@ export type CampaignHomeActionButtonProps = {
disabled?: boolean
}
const actionControlClassName =
'group flex min-h-11 w-[4.75rem] 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 actionControlClassName =
'group box-content flex min-h-11 w-[4.75rem] shrink-0 snap-start flex-col items-center gap-2 rounded-md px-2 text-center outline-none focus-visible:ring-2 focus-visible:ring-ring active:opacity-90 disabled:pointer-events-none disabled:opacity-50'
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'

View file

@ -140,7 +140,7 @@ export const CampaignHomeActionStrip = ({
>
<ul
role="list"
className="m-0 flex min-w-max list-none snap-x snap-proximity gap-2 p-0 pb-1"
className="m-0 flex min-w-max list-none snap-x snap-proximity gap-0 px-4 pb-1 md:px-0"
>
{actions?.map(({ id, ...button }) => (
<li key={id ?? button.href ?? button.label} className="m-0 list-none p-0">

View file

@ -78,16 +78,18 @@ export const CampaignQuickActionsDrawer = ({
aria-hidden={!isDock}
>
{showActions ? (
<CampaignHomeActionStrip
actions={actions.map((action) => ({
id: action.id,
label: action.label,
icon: action.icon,
description: action.description,
href: action.href,
}))}
className="w-full"
/>
<div className="-mx-4 w-[calc(100%+2rem)]">
<CampaignHomeActionStrip
actions={actions.map((action) => ({
id: action.id,
label: action.label,
icon: action.icon,
description: action.description,
href: action.href,
}))}
className="w-full"
/>
</div>
) : null}
<CampaignGlobalSearchBody />
</div>

View file

@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
CampaignHomeActionButton,
actionControlClassName,
type CampaignHomeActionButtonProps,
} from '@/components/campaign/dashboard/CampaignHomeActionButton'
import {
@ -90,6 +91,16 @@ describe('CampaignHomeActionButton', () => {
expect(screen.getByLabelText('Em breve').getAttribute('aria-disabled')).toBe('true')
})
it('uses horizontal padding for hit area without shrinking the visual column (B101)', () => {
renderActionButton({ label: 'Registrar', icon: BarChart3 })
const control = screen.getByRole('button', { name: 'Registrar' })
expect(control.className).toContain('px-2')
expect(control.className).toContain('box-content')
expect(control.className).toContain('w-[4.75rem]')
expect(actionControlClassName).toContain('px-2')
expect(actionControlClassName).toContain('box-content')
})
it('opens the description drawer on long-press when the pointer is coarse', () => {
resetCampaignCoarsePointerForTests()
matchMediaMock.mockImplementation((query: string) => ({
@ -180,7 +191,12 @@ describe('CampaignHomeActionStrip', () => {
const scroller = getScroller(container)
expect(scroller?.className).toContain('overflow-x-auto')
expect(scroller?.className).toContain('scrollbar-width:none')
expect(scroller?.querySelector('ul[role="list"]')).toBeTruthy()
const list = scroller?.querySelector('ul[role="list"]')
expect(list).toBeTruthy()
expect(list?.className).toContain('gap-0')
expect(list?.className).toContain('px-4')
expect(list?.className).toContain('md:px-0')
expect(list?.className).not.toContain('gap-2')
})
it('scrolls horizontally on pointer-fine drag past the threshold', () => {

View file

@ -1,6 +1,7 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { BarChart3 } from 'lucide-react'
import type { ReactNode } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CampaignGlobalSearchProvider } from '@/components/campaign/dashboard/CampaignGlobalSearchMount'
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
@ -37,6 +38,27 @@ const idleSuggestPayload = {
demands: [],
}
const matchMediaMock = vi.fn()
beforeEach(() => {
matchMediaMock.mockImplementation((query: string) => ({
matches: false,
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
}))
vi.stubGlobal('matchMedia', matchMediaMock)
})
afterEach(() => {
cleanup()
vi.mocked(postCampaignJson).mockReset()
vi.unstubAllGlobals()
})
const renderQuickActionsChrome = (ui: ReactNode) =>
render(
<CampaignQuickActionsSnapProvider>
@ -49,11 +71,6 @@ const SnapReadout = () => {
return <div data-testid="snap">{snapPoint ?? 'null'}</div>
}
afterEach(() => {
cleanup()
vi.mocked(postCampaignJson).mockReset()
})
describe('CampaignQuickActionsDrawer (B100)', () => {
it('loads in dock snap with global search visible', () => {
vi.mocked(postCampaignJson).mockResolvedValue({
@ -71,6 +88,33 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
expect(screen.getByRole('button', { name: 'Ocultar ações rápidas' })).toBeTruthy()
})
it('bleeds the action strip edge-to-edge inside the drawer gutter (B101)', () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: idleSuggestPayload,
})
renderQuickActionsChrome(
<CampaignQuickActionsDrawer
actions={[
{
id: 'test',
label: 'Registrar',
icon: BarChart3,
description: 'Teste.',
href: '/campanha',
},
]}
/>,
)
const context = document.getElementById('quickActionContext')
const stripBleed = context?.firstElementChild
expect(stripBleed?.className).toContain('-mx-4')
expect(stripBleed?.className).toContain('w-[calc(100%+2rem)]')
expect(stripBleed?.querySelector('[aria-label="Ações rápidas"]')).not.toBeNull()
})
it('posts home-search when the drawer query is active', async () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,