From a2eabaccba9997f0fdd2c94d40fbb442babf4862 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 01:38:10 +0000 Subject: [PATCH] =?UTF-8?q?B79=20=E2=80=94=20chassis=20do=20bottom=20drawe?= =?UTF-8?q?r=20de=20a=C3=A7=C3=B5es=20r=C3=A1pidas=20+=20busca=20global=20?= =?UTF-8?q?(mobile)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monta CampaignQuickActionsDrawer não-modal no layout (app) para staff fora do Início e de /acoes, com snap collapsed/expanded, padding de peek no scroll, registry vazio para catálogos B80+, e busca global reutilizada via CampaignStaffGlobalSearch. Closes #15 Co-authored-by: Francisco Solla --- .../chassis-bottom-drawer-acoes-rapidas.md | 2 +- src/app/(campaign)/campanha/(app)/layout.tsx | 37 +++--- src/app/(campaign)/campanha/(app)/page.tsx | 25 +--- .../dashboard/CampaignHomeStaffChrome.tsx | 5 +- .../dashboard/CampaignStaffGlobalSearch.tsx | 52 +++++++++ .../shell/CampaignAppScrollChrome.tsx | 27 +++++ .../shell/CampaignQuickActionContext.tsx | 55 +++++++++ .../shell/CampaignQuickActionsDrawer.tsx | 110 ++++++++++++++++++ .../shell/CampaignQuickActionsHost.tsx | 55 +++++++++ src/lib/campaignQuickActionContext.ts | 13 +++ src/lib/campaignQuickActionMount.ts | 27 +++++ src/lib/campaignQuickActionRegistry.ts | 14 +++ src/lib/campaignQuickActionSnap.ts | 15 +++ tests/unit/campaignComponents.unit.spec.ts | 8 +- tests/unit/campaignQuickAction.unit.spec.ts | 63 ++++++++++ 15 files changed, 461 insertions(+), 47 deletions(-) create mode 100644 src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx create mode 100644 src/components/campaign/shell/CampaignAppScrollChrome.tsx create mode 100644 src/components/campaign/shell/CampaignQuickActionContext.tsx create mode 100644 src/components/campaign/shell/CampaignQuickActionsDrawer.tsx create mode 100644 src/components/campaign/shell/CampaignQuickActionsHost.tsx create mode 100644 src/lib/campaignQuickActionContext.ts create mode 100644 src/lib/campaignQuickActionMount.ts create mode 100644 src/lib/campaignQuickActionRegistry.ts create mode 100644 src/lib/campaignQuickActionSnap.ts create mode 100644 tests/unit/campaignQuickAction.unit.spec.ts diff --git a/docs/plans/chassis-bottom-drawer-acoes-rapidas.md b/docs/plans/chassis-bottom-drawer-acoes-rapidas.md index a45e5b75..e5ce0b98 100644 --- a/docs/plans/chassis-bottom-drawer-acoes-rapidas.md +++ b/docs/plans/chassis-bottom-drawer-acoes-rapidas.md @@ -1,6 +1,6 @@ # Chassis do bottom drawer de ações rápidas + busca (mobile) -Status: in-progress (Issue #15) — **fatiado 2026-07-31:** busca embutida → **B91** +Status: entregue (B79 chassis — busca → B91) Atualizado em: 2026-07-31 Issue: #15 (B79) Priority: P1 diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index fdbf1cb8..fdd8c821 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -9,8 +9,10 @@ import { BiometricEnrollmentToast, type BiometricEnrollmentOffer, } from '@/components/campaign/shell/BiometricEnrollmentToast' +import { CampaignAppScrollChrome } from '@/components/campaign/shell/CampaignAppScrollChrome' import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar' import { CampaignNotificationBellSlot } from '@/components/campaign/shell/CampaignNotificationBellSlot' +import { CampaignQuickActionContextProvider } from '@/components/campaign/shell/CampaignQuickActionContext' import { CampaignSidebar } from '@/components/campaign/shell/CampaignSidebar' import { CampaignSidebarViewportDefault } from '@/components/campaign/shell/CampaignSidebarViewportDefault' import { CampaignWizardChromeProvider } from '@/components/campaign/shell/CampaignWizardChromeContext' @@ -73,24 +75,23 @@ export default async function CampaignAppLayout({ children }: { children: React. - - } /> -
- -
- -
-
-
- {children} -
- - - {biometricEnrollment ? : null} -
+ + + } /> +
+ +
+ +
+
+ + {children} + + + + {biometricEnrollment ? : null} +
+
diff --git a/src/app/(campaign)/campanha/(app)/page.tsx b/src/app/(campaign)/campanha/(app)/page.tsx index 58b5e968..31d65982 100644 --- a/src/app/(campaign)/campanha/(app)/page.tsx +++ b/src/app/(campaign)/campanha/(app)/page.tsx @@ -6,14 +6,6 @@ import { CampaignHomeActions } from '@/components/campaign/dashboard/CampaignHom import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout' import { CampaignHomeStaffChrome } from '@/components/campaign/dashboard/CampaignHomeStaffChrome' import { CampaignHomeSummary } from '@/components/campaign/dashboard/CampaignHomeSummary' -import { HomeSearchActivityGroup } from '@/components/campaign/dashboard/HomeSearchActivityGroup' -import { HomeSearchAdvisorGroup } from '@/components/campaign/dashboard/HomeSearchAdvisorGroup' -import { HomeSearchDemandGroup } from '@/components/campaign/dashboard/HomeSearchDemandGroup' -import { HomeSearchLeadershipGroup } from '@/components/campaign/dashboard/HomeSearchLeadershipGroup' -import { HomeSearchMunicipalityGroup } from '@/components/campaign/dashboard/HomeSearchMunicipalityGroup' -import { HomeSearchResultsLayout } from '@/components/campaign/dashboard/HomeSearchResultsLayout' -import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell' -import { HomeSearchStateDeputyGroup } from '@/components/campaign/dashboard/HomeSearchStateDeputyGroup' import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { isStaffCampaignRole } from '@/lib/campaignRoles' import { loadCampaignHomeSummary } from '@/utilities/campaignDashboardData' @@ -47,22 +39,7 @@ export default async function CampaignHomePage() { return ( {staff ? ( - - - - - - - - - - - } - summarySlot={summarySlot} - /> + ) : ( )} diff --git a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx index 76138861..ab17d8ec 100644 --- a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx +++ b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx @@ -4,6 +4,7 @@ import type { ReactNode } from 'react' import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout' import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch' +import { CampaignStaffGlobalSearchBody } from '@/components/campaign/dashboard/CampaignStaffGlobalSearch' import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext' import { HomeSearchResultsProvider, @@ -29,7 +30,7 @@ export const CampaignHomeStaffChrome = ({ summarySlot, }: { actions: ReactNode - /** Result groups plug in here from B48+. */ + /** Result groups plug in here from B48+. Defaults to global search when omitted. */ searchResults?: ReactNode summarySlot?: ReactNode }) => { @@ -46,7 +47,7 @@ export const CampaignHomeStaffChrome = ({ searchResults ? ( ) : ( - + ) } summarySlot={summarySlot} diff --git a/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx new file mode 100644 index 00000000..59c904ba --- /dev/null +++ b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx @@ -0,0 +1,52 @@ +'use client' + +import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch' +import { HomeSearchActivityGroup } from '@/components/campaign/dashboard/HomeSearchActivityGroup' +import { HomeSearchAdvisorGroup } from '@/components/campaign/dashboard/HomeSearchAdvisorGroup' +import { HomeSearchDemandGroup } from '@/components/campaign/dashboard/HomeSearchDemandGroup' +import { HomeSearchLeadershipGroup } from '@/components/campaign/dashboard/HomeSearchLeadershipGroup' +import { HomeSearchMunicipalityGroup } from '@/components/campaign/dashboard/HomeSearchMunicipalityGroup' +import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext' +import { HomeSearchResultsLayout } from '@/components/campaign/dashboard/HomeSearchResultsLayout' +import { + HomeSearchResultsProvider, + useHomeSearchResultsState, +} from '@/components/campaign/dashboard/HomeSearchResultsContext' +import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell' +import { HomeSearchStateDeputyGroup } from '@/components/campaign/dashboard/HomeSearchStateDeputyGroup' +import { useHomeSearchQuery } from '@/components/campaign/dashboard/useHomeSearchQuery' + +const CampaignStaffGlobalSearchResults = () => { + const searchResultsState = useHomeSearchResultsState() + + return ( + + + + + + + + + + + + + + + ) +} + +/** Result groups + input — requires an ancestor `HomeSearchProvider`. */ +export const CampaignStaffGlobalSearchBody = CampaignStaffGlobalSearchResults + +/** Staff global search — same contract as Início (`POST /campanha/home-search`). */ +export const CampaignStaffGlobalSearch = () => { + const searchState = useHomeSearchQuery() + + return ( + + + + ) +} diff --git a/src/components/campaign/shell/CampaignAppScrollChrome.tsx b/src/components/campaign/shell/CampaignAppScrollChrome.tsx new file mode 100644 index 00000000..8750e805 --- /dev/null +++ b/src/components/campaign/shell/CampaignAppScrollChrome.tsx @@ -0,0 +1,27 @@ +'use client' + +import type { ReactNode } from 'react' + +import { + CampaignContentScroll, + CampaignQuickActionsHost, + useQuickActionsChromeActive, +} from '@/components/campaign/shell/CampaignQuickActionsHost' +import type { CampaignRole } from '@/lib/campaignRoles' + +export const CampaignAppScrollChrome = ({ + role, + children, +}: { + role: CampaignRole + children: ReactNode +}) => { + const quickActionsPeek = useQuickActionsChromeActive(role) + + return ( + <> + {children} + + + ) +} diff --git a/src/components/campaign/shell/CampaignQuickActionContext.tsx b/src/components/campaign/shell/CampaignQuickActionContext.tsx new file mode 100644 index 00000000..078efb8a --- /dev/null +++ b/src/components/campaign/shell/CampaignQuickActionContext.tsx @@ -0,0 +1,55 @@ +'use client' + +import { + createContext, + useContext, + useLayoutEffect, + useMemo, + useState, + type Dispatch, + type ReactNode, + type SetStateAction, +} from 'react' + +import { + emptyCampaignQuickActionContext, + type CampaignQuickActionContext, +} from '@/lib/campaignQuickActionContext' + +type CampaignQuickActionContextValue = { + context: CampaignQuickActionContext + setContext: Dispatch> +} + +const QuickActionContext = createContext(null) + +export const CampaignQuickActionContextProvider = ({ children }: { children: ReactNode }) => { + const [context, setContext] = useState(emptyCampaignQuickActionContext) + const value = useMemo(() => ({ context, setContext }), [context]) + + return {children} +} + +export const useCampaignQuickActionContext = (): CampaignQuickActionContextValue => { + const value = useContext(QuickActionContext) + if (!value) { + throw new Error('CampaignQuickActionContextProvider is required') + } + return value +} + +/** B80+ — set page context for the current route (cleared on unmount). */ +export const useSetCampaignQuickActionContext = (next: CampaignQuickActionContext): void => { + const { setContext } = useCampaignQuickActionContext() + + useLayoutEffect(() => { + setContext(next) + }, [next, setContext]) + + useLayoutEffect( + () => () => { + setContext(emptyCampaignQuickActionContext()) + }, + [setContext], + ) +} diff --git a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx new file mode 100644 index 00000000..a3421ebe --- /dev/null +++ b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx @@ -0,0 +1,110 @@ +'use client' + +import { useCallback, useState } from 'react' + +import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip' +import { + Drawer, + DrawerContent, + DrawerTitle, +} from '@/components/ui/Drawer' +import type { ResolvedCampaignHomeAction } from '@/lib/campaignHomeActions' +import { + QUICK_ACTIONS_SNAP_COLLAPSED, + QUICK_ACTIONS_SNAP_EXPANDED, + QUICK_ACTIONS_SNAP_POINTS, + quickActionsSnapIsExpanded, + type QuickActionsSnapPoint, +} from '@/lib/campaignQuickActionSnap' +import type { QuickActionsSnapPoint, +} from '@/lib/campaignQuickActionSnap' + +const SNAP_POINTS = [...QUICK_ACTIONS_SNAP_POINTS] + +export const CampaignQuickActionsDrawer = ({ + actions, +}: { + actions: readonly ResolvedCampaignHomeAction[] +}) => { + const [snapPoint, setSnapPoint] = useState( + QUICK_ACTIONS_SNAP_COLLAPSED, + ) + const expanded = quickActionsSnapIsExpanded(snapPoint) + const showActions = actions.length > 0 + + const handleSnapPointChange = useCallback((next: QuickActionsSnapPoint | string | number | null) => { + if (next === QUICK_ACTIONS_SNAP_COLLAPSED || next === QUICK_ACTIONS_SNAP_EXPANDED) { + setSnapPoint(next) + return + } + setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED) + }, []) + + const toggleSnap = useCallback(() => { + setSnapPoint((current) => + quickActionsSnapIsExpanded(current) ? QUICK_ACTIONS_SNAP_COLLAPSED : QUICK_ACTIONS_SNAP_EXPANDED, + ) + }, []) + + const handleOpenChange = useCallback((open: boolean) => { + if (!open) { + setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED) + } + }, []) + + return ( + + + Ações rápidas + +
+ {showActions ? ( + ({ + id: action.id, + label: action.label, + icon: action.icon, + description: action.description, + href: action.href, + }))} + className="w-full" + /> + ) : ( +

Nenhuma ação rápida nesta página.

+ )} +
+
+
+ ) +} diff --git a/src/components/campaign/shell/CampaignQuickActionsHost.tsx b/src/components/campaign/shell/CampaignQuickActionsHost.tsx new file mode 100644 index 00000000..b5b1b555 --- /dev/null +++ b/src/components/campaign/shell/CampaignQuickActionsHost.tsx @@ -0,0 +1,55 @@ +'use client' + +import { usePathname } from 'next/navigation' +import type { ReactNode } from 'react' + +import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer' +import { useCampaignQuickActionContext } from '@/components/campaign/shell/CampaignQuickActionContext' +import { useIsMobile } from '@/hooks/use-mobile' +import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry' +import { shouldMountQuickActionsDrawer } from '@/lib/campaignQuickActionMount' +import { QUICK_ACTIONS_SNAP_COLLAPSED } from '@/lib/campaignQuickActionSnap' +import type { CampaignRole } from '@/lib/campaignRoles' +import { cn } from '@/lib/utils' + +export const CampaignContentScroll = ({ + children, + quickActionsPeek, +}: { + children: ReactNode + quickActionsPeek: boolean +}) => ( +
+ {children} +
+) + +export const CampaignQuickActionsHost = ({ role }: { role: CampaignRole }) => { + const pathname = usePathname() + const isMobile = useIsMobile() + const { context } = useCampaignQuickActionContext() + + const mounted = isMobile && shouldMountQuickActionsDrawer(pathname, role) + if (!mounted) return null + + const actions = resolveQuickActionsForPath(pathname, role, context) + + return +} + +export const useQuickActionsChromeActive = (role: CampaignRole): boolean => { + const pathname = usePathname() + const isMobile = useIsMobile() + return isMobile && shouldMountQuickActionsDrawer(pathname, role) +} diff --git a/src/lib/campaignQuickActionContext.ts b/src/lib/campaignQuickActionContext.ts new file mode 100644 index 00000000..1d0db468 --- /dev/null +++ b/src/lib/campaignQuickActionContext.ts @@ -0,0 +1,13 @@ +/** + * Client-safe quick-action context — routes and B80+ providers fill this so + * `resolveQuickActionsForPath` can prefill wizard/list hrefs. + */ +export type CampaignQuickActionContext = { + municipalitySlug?: string + leadershipId?: number + organizationSlug?: string + activitySlug?: string + demandSlug?: string +} + +export const emptyCampaignQuickActionContext = (): CampaignQuickActionContext => ({}) diff --git a/src/lib/campaignQuickActionMount.ts b/src/lib/campaignQuickActionMount.ts new file mode 100644 index 00000000..d2ff99fe --- /dev/null +++ b/src/lib/campaignQuickActionMount.ts @@ -0,0 +1,27 @@ +import { CAMPAIGN_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths' +import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes' +import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles' + +/** Início matches only exactly — same rule as sidebar nav. */ +export const isCampaignHomePath = (pathname: string): boolean => + pathname === CAMPAIGN_HOME || pathname === `${CAMPAIGN_HOME}/` + +export const isCampaignActionsPath = (pathname: string): boolean => + pathname === CAMPAIGN_ACTIONS_HOME || pathname.startsWith(`${CAMPAIGN_ACTIONS_HOME}/`) + +export const isLeaderContactsPath = (pathname: string): boolean => + pathname === LEADER_CONTACTS_HOME || pathname.startsWith(`${LEADER_CONTACTS_HOME}/`) + +/** + * Whether the mobile quick-actions drawer should mount on this navigation. + * Desktop (`md+`) is gated in the host via `useIsMobile`. + */ +export const shouldMountQuickActionsDrawer = ( + pathname: string, + role: CampaignRole, +): boolean => { + if (isCampaignHomePath(pathname)) return false + if (isCampaignActionsPath(pathname)) return false + if (role === 'leader') return isLeaderContactsPath(pathname) + return isStaffCampaignRole(role) +} diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts new file mode 100644 index 00000000..ff2f3eec --- /dev/null +++ b/src/lib/campaignQuickActionRegistry.ts @@ -0,0 +1,14 @@ +import type { ResolvedCampaignHomeAction } from '@/lib/campaignHomeActions' +import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' +import type { CampaignRole } from '@/lib/campaignRoles' + +/** + * Pathname + role + page context → contextual quick actions. + * Catalogs per vertical land in B80–B90; the B79 chassis renders whatever + * this helper returns (empty until those items register providers). + */ +export const resolveQuickActionsForPath = ( + _pathname: string, + _role: CampaignRole, + _context: CampaignQuickActionContext, +): readonly ResolvedCampaignHomeAction[] => [] diff --git a/src/lib/campaignQuickActionSnap.ts b/src/lib/campaignQuickActionSnap.ts new file mode 100644 index 00000000..7329c97e --- /dev/null +++ b/src/lib/campaignQuickActionSnap.ts @@ -0,0 +1,15 @@ +/** Collapsed peek — swipe handle only (replaces legacy bottom-nav `pb-24`). */ +export const QUICK_ACTIONS_SNAP_COLLAPSED = '3rem' as const + +/** Expanded — actions strip + global search inside the drawer. */ +export const QUICK_ACTIONS_SNAP_EXPANDED = 0.55 as const + +export const QUICK_ACTIONS_SNAP_POINTS = [ + QUICK_ACTIONS_SNAP_COLLAPSED, + QUICK_ACTIONS_SNAP_EXPANDED, +] as const + +export type QuickActionsSnapPoint = (typeof QUICK_ACTIONS_SNAP_POINTS)[number] + +export const quickActionsSnapIsExpanded = (snap: QuickActionsSnapPoint | null): boolean => + snap === QUICK_ACTIONS_SNAP_EXPANDED diff --git a/tests/unit/campaignComponents.unit.spec.ts b/tests/unit/campaignComponents.unit.spec.ts index ce2ea586..1f761b7f 100644 --- a/tests/unit/campaignComponents.unit.spec.ts +++ b/tests/unit/campaignComponents.unit.spec.ts @@ -124,6 +124,10 @@ describe('campaign visual foundation', () => { resolve(process.cwd(), 'src/app/(campaign)/campanha/(app)/layout.tsx'), 'utf8', ) + const scrollChromeSource = readFileSync( + resolve(process.cwd(), 'src/components/campaign/shell/CampaignQuickActionsHost.tsx'), + 'utf8', + ) const sidebarSource = readFileSync( resolve(process.cwd(), 'src/components/campaign/shell/CampaignSidebar.tsx'), 'utf8', @@ -132,8 +136,8 @@ describe('campaign visual foundation', () => { expect(layoutSource).toContain( 'className="h-svh min-h-0 overflow-hidden print:h-auto print:overflow-visible"', ) - expect(layoutSource).toContain('data-slot="campaign-content-scroll"') - expect(layoutSource).toContain('min-h-0 flex-1 overflow-y-auto overscroll-contain') + expect(scrollChromeSource).toContain('data-slot="campaign-content-scroll"') + expect(scrollChromeSource).toContain('min-h-0 flex-1 overflow-y-auto overscroll-contain') expect(layoutSource).toContain( 'hidden min-h-11 shrink-0 items-center gap-2 border-b border-border px-4 md:flex print:hidden', ) diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts new file mode 100644 index 00000000..9447fcaa --- /dev/null +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest' + +import { + isCampaignActionsPath, + isCampaignHomePath, + isLeaderContactsPath, + shouldMountQuickActionsDrawer, +} from '@/lib/campaignQuickActionMount' +import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry' +import { + QUICK_ACTIONS_SNAP_COLLAPSED, + QUICK_ACTIONS_SNAP_EXPANDED, + quickActionsSnapIsExpanded, +} from '@/lib/campaignQuickActionSnap' + +describe('campaignQuickActionMount', () => { + it('treats Início as exact match only', () => { + expect(isCampaignHomePath('/campanha')).toBe(true) + expect(isCampaignHomePath('/campanha/')).toBe(true) + expect(isCampaignHomePath('/campanha/municipios')).toBe(false) + }) + + it('matches action wizard subtree', () => { + expect(isCampaignActionsPath('/campanha/acoes')).toBe(true) + expect(isCampaignActionsPath('/campanha/acoes/atualizar-votos')).toBe(true) + expect(isCampaignActionsPath('/campanha/municipios')).toBe(false) + }) + + it('matches leader contacts subtree', () => { + expect(isLeaderContactsPath('/campanha/contatos')).toBe(true) + expect(isLeaderContactsPath('/campanha/contatos/novo')).toBe(true) + expect(isLeaderContactsPath('/campanha')).toBe(false) + }) + + it('mounts for staff outside Início and acoes', () => { + expect(shouldMountQuickActionsDrawer('/campanha/municipios', 'coordinator')).toBe(true) + expect(shouldMountQuickActionsDrawer('/campanha', 'coordinator')).toBe(false) + expect(shouldMountQuickActionsDrawer('/campanha/acoes/registrar-sinal', 'advisor')).toBe(false) + }) + + it('mounts for leader only on contacts', () => { + expect(shouldMountQuickActionsDrawer('/campanha/contatos', 'leader')).toBe(true) + expect(shouldMountQuickActionsDrawer('/campanha/municipios', 'leader')).toBe(false) + }) +}) + +describe('campaignQuickActionRegistry', () => { + it('returns empty catalog until B80+ register providers', () => { + expect( + resolveQuickActionsForPath('/campanha/municipios/foo', 'coordinator', { + municipalitySlug: 'foo', + }), + ).toEqual([]) + }) +}) + +describe('campaignQuickActionSnap', () => { + it('detects expanded snap', () => { + expect(quickActionsSnapIsExpanded(QUICK_ACTIONS_SNAP_EXPANDED)).toBe(true) + expect(quickActionsSnapIsExpanded(QUICK_ACTIONS_SNAP_COLLAPSED)).toBe(false) + expect(quickActionsSnapIsExpanded(null)).toBe(false) + }) +})