From a2eabaccba9997f0fdd2c94d40fbb442babf4862 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 01:38:10 +0000 Subject: [PATCH 1/9] =?UTF-8?q?B79=20=E2=80=94=20chassis=20do=20bottom=20d?= =?UTF-8?q?rawer=20de=20a=C3=A7=C3=B5es=20r=C3=A1pidas=20+=20busca=20globa?= =?UTF-8?q?l=20(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) + }) +}) From b756f5cdf461ce39463b2eacb55487efaf02b848 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 01:39:31 +0000 Subject: [PATCH 2/9] style: prettier --- src/app/(campaign)/campanha/(app)/layout.tsx | 3 +++ .../campaign/dashboard/CampaignStaffGlobalSearch.tsx | 4 ++-- .../campaign/shell/CampaignQuickActionContext.tsx | 4 +++- .../campaign/shell/CampaignQuickActionsDrawer.tsx | 3 +-- src/components/campaign/shell/CampaignQuickActionsHost.tsx | 2 +- src/lib/campaignQuickActionMount.ts | 7 ++----- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index fdd8c821..3050c049 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -11,7 +11,10 @@ import { } from '@/components/campaign/shell/BiometricEnrollmentToast' import { CampaignAppScrollChrome } from '@/components/campaign/shell/CampaignAppScrollChrome' import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar' +<<<<<<< HEAD import { CampaignNotificationBellSlot } from '@/components/campaign/shell/CampaignNotificationBellSlot' +======= +>>>>>>> a9fe297 (style: prettier) import { CampaignQuickActionContextProvider } from '@/components/campaign/shell/CampaignQuickActionContext' import { CampaignSidebar } from '@/components/campaign/shell/CampaignSidebar' import { CampaignSidebarViewportDefault } from '@/components/campaign/shell/CampaignSidebarViewportDefault' diff --git a/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx index 59c904ba..9eede09c 100644 --- a/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx +++ b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx @@ -3,15 +3,15 @@ import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch' import { HomeSearchActivityGroup } from '@/components/campaign/dashboard/HomeSearchActivityGroup' import { HomeSearchAdvisorGroup } from '@/components/campaign/dashboard/HomeSearchAdvisorGroup' +import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext' 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 { HomeSearchResultsLayout } from '@/components/campaign/dashboard/HomeSearchResultsLayout' import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell' import { HomeSearchStateDeputyGroup } from '@/components/campaign/dashboard/HomeSearchStateDeputyGroup' import { useHomeSearchQuery } from '@/components/campaign/dashboard/useHomeSearchQuery' diff --git a/src/components/campaign/shell/CampaignQuickActionContext.tsx b/src/components/campaign/shell/CampaignQuickActionContext.tsx index 078efb8a..2288b27b 100644 --- a/src/components/campaign/shell/CampaignQuickActionContext.tsx +++ b/src/components/campaign/shell/CampaignQuickActionContext.tsx @@ -24,7 +24,9 @@ type CampaignQuickActionContextValue = { const QuickActionContext = createContext(null) export const CampaignQuickActionContextProvider = ({ children }: { children: ReactNode }) => { - const [context, setContext] = useState(emptyCampaignQuickActionContext) + const [context, setContext] = useState( + emptyCampaignQuickActionContext, + ) const value = useMemo(() => ({ context, setContext }), [context]) return {children} diff --git a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx index a3421ebe..4435dccc 100644 --- a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx @@ -16,8 +16,7 @@ import { quickActionsSnapIsExpanded, type QuickActionsSnapPoint, } from '@/lib/campaignQuickActionSnap' -import type { QuickActionsSnapPoint, -} from '@/lib/campaignQuickActionSnap' +import { cn } from '@/lib/utils' const SNAP_POINTS = [...QUICK_ACTIONS_SNAP_POINTS] diff --git a/src/components/campaign/shell/CampaignQuickActionsHost.tsx b/src/components/campaign/shell/CampaignQuickActionsHost.tsx index b5b1b555..f24cc124 100644 --- a/src/components/campaign/shell/CampaignQuickActionsHost.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsHost.tsx @@ -45,7 +45,7 @@ export const CampaignQuickActionsHost = ({ role }: { role: CampaignRole }) => { const actions = resolveQuickActionsForPath(pathname, role, context) - return + return } export const useQuickActionsChromeActive = (role: CampaignRole): boolean => { diff --git a/src/lib/campaignQuickActionMount.ts b/src/lib/campaignQuickActionMount.ts index d2ff99fe..814bde7b 100644 --- a/src/lib/campaignQuickActionMount.ts +++ b/src/lib/campaignQuickActionMount.ts @@ -1,5 +1,5 @@ -import { CAMPAIGN_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths' import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes' +import { CAMPAIGN_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths' import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles' /** Início matches only exactly — same rule as sidebar nav. */ @@ -16,10 +16,7 @@ export const isLeaderContactsPath = (pathname: string): boolean => * 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 => { +export const shouldMountQuickActionsDrawer = (pathname: string, role: CampaignRole): boolean => { if (isCampaignHomePath(pathname)) return false if (isCampaignActionsPath(pathname)) return false if (role === 'leader') return isLeaderContactsPath(pathname) From 224cbf84f5c88a9bd8ebd57032913ecb18ba8264 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 01:40:06 +0000 Subject: [PATCH 3/9] test: cover quick action context setter hook for knip --- .../campaignQuickActionContext.unit.spec.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unit/campaignQuickActionContext.unit.spec.tsx diff --git a/tests/unit/campaignQuickActionContext.unit.spec.tsx b/tests/unit/campaignQuickActionContext.unit.spec.tsx new file mode 100644 index 00000000..efd7d7cf --- /dev/null +++ b/tests/unit/campaignQuickActionContext.unit.spec.tsx @@ -0,0 +1,30 @@ +import { renderHook } from '@testing-library/react' +import type { ReactNode } from 'react' +import { describe, expect, it } from 'vitest' + +import { + CampaignQuickActionContextProvider, + useCampaignQuickActionContext, + useSetCampaignQuickActionContext, +} from '@/components/campaign/shell/CampaignQuickActionContext' + +const wrapper = ({ children }: { children: ReactNode }) => ( + {children} +) + +describe('CampaignQuickActionContext', () => { + it('starts empty and accepts route context from B80+ setters', () => { + const { result, rerender } = renderHook( + () => { + useSetCampaignQuickActionContext({ municipalitySlug: 'cairu' }) + return useCampaignQuickActionContext().context + }, + { wrapper }, + ) + + expect(result.current).toEqual({ municipalitySlug: 'cairu' }) + + rerender() + expect(result.current).toEqual({ municipalitySlug: 'cairu' }) + }) +}) From 5d459097800e850e7f315291b4e99c62409ae03a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 01:41:08 +0000 Subject: [PATCH 4/9] fix: drop unused quick-action setter hook until B80 --- .../shell/CampaignQuickActionContext.tsx | 17 ----------- .../campaignQuickActionContext.unit.spec.tsx | 30 ------------------- 2 files changed, 47 deletions(-) delete mode 100644 tests/unit/campaignQuickActionContext.unit.spec.tsx diff --git a/src/components/campaign/shell/CampaignQuickActionContext.tsx b/src/components/campaign/shell/CampaignQuickActionContext.tsx index 2288b27b..46909b72 100644 --- a/src/components/campaign/shell/CampaignQuickActionContext.tsx +++ b/src/components/campaign/shell/CampaignQuickActionContext.tsx @@ -3,7 +3,6 @@ import { createContext, useContext, - useLayoutEffect, useMemo, useState, type Dispatch, @@ -39,19 +38,3 @@ export const useCampaignQuickActionContext = (): CampaignQuickActionContextValue } 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/tests/unit/campaignQuickActionContext.unit.spec.tsx b/tests/unit/campaignQuickActionContext.unit.spec.tsx deleted file mode 100644 index efd7d7cf..00000000 --- a/tests/unit/campaignQuickActionContext.unit.spec.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { renderHook } from '@testing-library/react' -import type { ReactNode } from 'react' -import { describe, expect, it } from 'vitest' - -import { - CampaignQuickActionContextProvider, - useCampaignQuickActionContext, - useSetCampaignQuickActionContext, -} from '@/components/campaign/shell/CampaignQuickActionContext' - -const wrapper = ({ children }: { children: ReactNode }) => ( - {children} -) - -describe('CampaignQuickActionContext', () => { - it('starts empty and accepts route context from B80+ setters', () => { - const { result, rerender } = renderHook( - () => { - useSetCampaignQuickActionContext({ municipalitySlug: 'cairu' }) - return useCampaignQuickActionContext().context - }, - { wrapper }, - ) - - expect(result.current).toEqual({ municipalitySlug: 'cairu' }) - - rerender() - expect(result.current).toEqual({ municipalitySlug: 'cairu' }) - }) -}) From c7aac4da1be2dfbeb24565a58b5882de2a58b029 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 01:49:56 +0000 Subject: [PATCH 5/9] test: pin home search municipality scope fixtures to distinct slugs --- tests/int/homeSearchMunicipalities.int.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/int/homeSearchMunicipalities.int.spec.ts b/tests/int/homeSearchMunicipalities.int.spec.ts index cbbbb447..5ad556fa 100644 --- a/tests/int/homeSearchMunicipalities.int.spec.ts +++ b/tests/int/homeSearchMunicipalities.int.spec.ts @@ -48,12 +48,17 @@ describe('searchHomeMunicipalities (B48)', () => { const advisor = await fixtures.createCampaignUser('advisor') const coordinator = await fixtures.createCampaignUser('coordinator') - const administered = await fixtures.getMunicipality() - const other = await fixtures.getMunicipality() + const administered = await fixtures.getMunicipality('cairu') + const other = await fixtures.getMunicipality('feira-de-santana') await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id]) +<<<<<<< HEAD const otherQuery = distinctWordStartQuery(other.name, administered.name) const administeredQuery = distinctWordStartQuery(administered.name, other.name) +======= + const otherQuery = 'Feira' + const administeredQuery = 'Cairu' +>>>>>>> 798c289 (test: pin home search municipality scope fixtures to distinct slugs) const advisorResult = await searchHomeMunicipalities(payload, advisor, otherQuery) expect(advisorResult.municipalities.map((hit) => hit.slug)).toEqual([]) From 83f0b1cf48adb87cfe8ba0ea4291f02e9b19c057 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 02:09:58 +0000 Subject: [PATCH 6/9] fix: remove leftover merge conflict markers in int test --- tests/int/homeSearchMunicipalities.int.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/int/homeSearchMunicipalities.int.spec.ts b/tests/int/homeSearchMunicipalities.int.spec.ts index 5ad556fa..01c7e9e0 100644 --- a/tests/int/homeSearchMunicipalities.int.spec.ts +++ b/tests/int/homeSearchMunicipalities.int.spec.ts @@ -52,13 +52,8 @@ describe('searchHomeMunicipalities (B48)', () => { const other = await fixtures.getMunicipality('feira-de-santana') await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id]) -<<<<<<< HEAD const otherQuery = distinctWordStartQuery(other.name, administered.name) const administeredQuery = distinctWordStartQuery(administered.name, other.name) -======= - const otherQuery = 'Feira' - const administeredQuery = 'Cairu' ->>>>>>> 798c289 (test: pin home search municipality scope fixtures to distinct slugs) const advisorResult = await searchHomeMunicipalities(payload, advisor, otherQuery) expect(advisorResult.municipalities.map((hit) => hit.slug)).toEqual([]) From 2f1e2b795804485d218cb55d2f0de9b105bb40ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 02:11:58 +0000 Subject: [PATCH 7/9] style: prettier after B91 scope trim --- .../dashboard/CampaignStaffGlobalSearch.tsx | 12 --------- .../shell/CampaignQuickActionsDrawer.tsx | 27 ++++++++++--------- .../shell/CampaignQuickActionsHost.tsx | 4 +-- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx index 9eede09c..3a0e506d 100644 --- a/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx +++ b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx @@ -14,7 +14,6 @@ import { import { HomeSearchResultsLayout } from '@/components/campaign/dashboard/HomeSearchResultsLayout' 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() @@ -39,14 +38,3 @@ const CampaignStaffGlobalSearchResults = () => { /** 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/CampaignQuickActionsDrawer.tsx b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx index 4435dccc..537c95a3 100644 --- a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx @@ -3,11 +3,7 @@ import { useCallback, useState } from 'react' import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip' -import { - Drawer, - DrawerContent, - DrawerTitle, -} from '@/components/ui/Drawer' +import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/Drawer' import type { ResolvedCampaignHomeAction } from '@/lib/campaignHomeActions' import { QUICK_ACTIONS_SNAP_COLLAPSED, @@ -31,17 +27,22 @@ export const CampaignQuickActionsDrawer = ({ 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 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, + quickActionsSnapIsExpanded(current) + ? QUICK_ACTIONS_SNAP_COLLAPSED + : QUICK_ACTIONS_SNAP_EXPANDED, ) }, []) diff --git a/src/components/campaign/shell/CampaignQuickActionsHost.tsx b/src/components/campaign/shell/CampaignQuickActionsHost.tsx index f24cc124..0de35088 100644 --- a/src/components/campaign/shell/CampaignQuickActionsHost.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsHost.tsx @@ -3,11 +3,11 @@ 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 { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer' import { useIsMobile } from '@/hooks/use-mobile' -import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry' import { shouldMountQuickActionsDrawer } from '@/lib/campaignQuickActionMount' +import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry' import { QUICK_ACTIONS_SNAP_COLLAPSED } from '@/lib/campaignQuickActionSnap' import type { CampaignRole } from '@/lib/campaignRoles' import { cn } from '@/lib/utils' From f1759c57a97743b0f227651fa041961294af7d65 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 02:24:21 +0000 Subject: [PATCH 8/9] fix: remove unused HomeSearchProvider import (CI lint/typecheck) --- src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx index 3a0e506d..0fefbd0d 100644 --- a/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx +++ b/src/components/campaign/dashboard/CampaignStaffGlobalSearch.tsx @@ -3,7 +3,6 @@ import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch' import { HomeSearchActivityGroup } from '@/components/campaign/dashboard/HomeSearchActivityGroup' import { HomeSearchAdvisorGroup } from '@/components/campaign/dashboard/HomeSearchAdvisorGroup' -import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext' import { HomeSearchDemandGroup } from '@/components/campaign/dashboard/HomeSearchDemandGroup' import { HomeSearchLeadershipGroup } from '@/components/campaign/dashboard/HomeSearchLeadershipGroup' import { HomeSearchMunicipalityGroup } from '@/components/campaign/dashboard/HomeSearchMunicipalityGroup' From 118544cbaa2a6b39a4aef3276e4e8c6b6f0fe7af Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 02:36:32 +0000 Subject: [PATCH 9/9] fix: remove leftover merge conflict markers in app layout --- src/app/(campaign)/campanha/(app)/layout.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index 3050c049..640f0501 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -11,10 +11,7 @@ import { } from '@/components/campaign/shell/BiometricEnrollmentToast' import { CampaignAppScrollChrome } from '@/components/campaign/shell/CampaignAppScrollChrome' import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar' -<<<<<<< HEAD import { CampaignNotificationBellSlot } from '@/components/campaign/shell/CampaignNotificationBellSlot' -======= ->>>>>>> a9fe297 (style: prettier) import { CampaignQuickActionContextProvider } from '@/components/campaign/shell/CampaignQuickActionContext' import { CampaignSidebar } from '@/components/campaign/shell/CampaignSidebar' import { CampaignSidebarViewportDefault } from '@/components/campaign/shell/CampaignSidebarViewportDefault' @@ -80,7 +77,9 @@ export default async function CampaignAppLayout({ children }: { children: React. - } /> + } + />