From 84365ddf54f63dbb770e9cc61ad09d9caf4091cd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 18:36:28 +0000 Subject: [PATCH 1/4] =?UTF-8?q?B106=20=E2=80=94=20Voltar=20no=20header=20m?= =?UTF-8?q?obile,=20Android=20back=20e=20skeleton=20das=20sugest=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CampaignHomeSearchChromeContext + publisher no Início - CampaignMobileTopBar troca SidebarTrigger por Voltar quando uiFocused - useHomeSearchFocusHistory com pushState/popstate para back do SO - HomeSearchSuggestSkeleton enquanto isFetching sem hits - Unit tests para history helper, top bar e skeleton Co-authored-by: Francisco Solla --- src/app/(campaign)/campanha/(app)/layout.tsx | 7 +- .../CampaignHomeSearchChromePublisher.tsx | 31 ++++ .../dashboard/CampaignHomeStaffChrome.tsx | 2 + .../dashboard/HomeSearchResultsShell.tsx | 10 +- .../dashboard/HomeSearchSuggestSkeleton.tsx | 28 ++++ .../dashboard/useHomeSearchFocusHistory.ts | 70 +++++++++ .../shell/CampaignHomeSearchChromeContext.tsx | 65 +++++++++ .../campaign/shell/CampaignMobileTopBar.tsx | 20 ++- src/lib/campaignHomeSearchContract.ts | 3 + src/lib/homeSearchFocusHistory.ts | 21 +++ tests/unit/campaignHomeSearch.unit.spec.tsx | 37 ++++- tests/unit/campaignMobileTopBar.unit.spec.tsx | 133 +++++++++++------- .../unit/homeSearchFocusHistory.unit.spec.ts | 38 +++++ 13 files changed, 407 insertions(+), 58 deletions(-) create mode 100644 src/components/campaign/dashboard/CampaignHomeSearchChromePublisher.tsx create mode 100644 src/components/campaign/dashboard/HomeSearchSuggestSkeleton.tsx create mode 100644 src/components/campaign/dashboard/useHomeSearchFocusHistory.ts create mode 100644 src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx create mode 100644 src/lib/homeSearchFocusHistory.ts create mode 100644 tests/unit/homeSearchFocusHistory.unit.spec.ts diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index e23233e9..ee35e237 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -10,6 +10,7 @@ import { type BiometricEnrollmentOffer, } from '@/components/campaign/shell/BiometricEnrollmentToast' import { CampaignAppScrollChrome } from '@/components/campaign/shell/CampaignAppScrollChrome' +import { CampaignHomeSearchChromeProvider } from '@/components/campaign/shell/CampaignHomeSearchChromeContext' import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar' import { CampaignNotificationBellSlot } from '@/components/campaign/shell/CampaignNotificationBellSlot' import { CampaignQuickActionContextProvider } from '@/components/campaign/shell/CampaignQuickActionContext' @@ -75,7 +76,8 @@ export default async function CampaignAppLayout({ children }: { children: React. - + + } @@ -100,7 +102,8 @@ export default async function CampaignAppLayout({ children }: { children: React. {biometricEnrollment ? : null} - + + diff --git a/src/components/campaign/dashboard/CampaignHomeSearchChromePublisher.tsx b/src/components/campaign/dashboard/CampaignHomeSearchChromePublisher.tsx new file mode 100644 index 00000000..cdfd5325 --- /dev/null +++ b/src/components/campaign/dashboard/CampaignHomeSearchChromePublisher.tsx @@ -0,0 +1,31 @@ +'use client' + +import { useCallback, useMemo } from 'react' + +import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext' +import { useHomeSearchFocusHistory } from '@/components/campaign/dashboard/useHomeSearchFocusHistory' +import { + useSetCampaignHomeSearchChrome, + type CampaignHomeSearchChromeState, +} from '@/components/campaign/shell/CampaignHomeSearchChromeContext' + +/** Publishes Início search chrome to the layout top bar (B106). */ +export const CampaignHomeSearchChromePublisher = () => { + const { clear, uiFocused } = useHomeSearch() + + const collapse = useCallback(() => { + clear() + }, [clear]) + + const chrome = useMemo((): CampaignHomeSearchChromeState | null => { + if (!uiFocused) { + return null + } + return { focused: true, collapse } + }, [collapse, uiFocused]) + + useSetCampaignHomeSearchChrome(chrome) + useHomeSearchFocusHistory({ focused: uiFocused, onCollapse: collapse }) + + return null +} diff --git a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx index 3bc0d4ce..a54094c0 100644 --- a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx +++ b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx @@ -9,6 +9,7 @@ import { import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout' import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch' import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext' +import { CampaignHomeSearchChromePublisher } from '@/components/campaign/dashboard/CampaignHomeSearchChromePublisher' import { HomeSearchResultsProvider, InitialHomeSearchSuggestProvider, @@ -42,6 +43,7 @@ const CampaignHomeStaffChromeInner = ({ return (
+ { const { query } = useHomeSearch() - const { results, resultKind } = useHomeSearchResults() + const { results, resultKind, isFetching } = useHomeSearchResults() + + const hasRenderableHits = + results.status === 'success' && homeSearchHasAnyHits(results.data) + const showSkeleton = isFetching && !hasRenderableHits && results.status !== 'error' const showEmpty = query.isActive && @@ -18,7 +23,8 @@ export const HomeSearchResultsShell = ({ children }: { children: ReactNode }) => return ( <> - {children} + {showSkeleton ? : null} + {!showSkeleton ? children : null} {results.status === 'error' ? (

{results.message} diff --git a/src/components/campaign/dashboard/HomeSearchSuggestSkeleton.tsx b/src/components/campaign/dashboard/HomeSearchSuggestSkeleton.tsx new file mode 100644 index 00000000..666fd549 --- /dev/null +++ b/src/components/campaign/dashboard/HomeSearchSuggestSkeleton.tsx @@ -0,0 +1,28 @@ +import { Skeleton } from '@/components/ui/skeleton' +import { HOME_SEARCH_HIT_ROW_WRAPPER_CLASS } from '@/lib/homeSearchUi' +import { cn } from '@/lib/utils' + +const HOME_SEARCH_SUGGEST_SKELETON_ROW_COUNT = 3 + +const HomeSearchSuggestSkeletonRow = () => ( +

+
+ + +
+
+) + +/** B106 — placeholder rows while suggest/search hits are still loading. */ +export const HomeSearchSuggestSkeleton = () => ( +
+ {Array.from({ length: HOME_SEARCH_SUGGEST_SKELETON_ROW_COUNT }, (_, index) => ( + + ))} +
+) diff --git a/src/components/campaign/dashboard/useHomeSearchFocusHistory.ts b/src/components/campaign/dashboard/useHomeSearchFocusHistory.ts new file mode 100644 index 00000000..86e45879 --- /dev/null +++ b/src/components/campaign/dashboard/useHomeSearchFocusHistory.ts @@ -0,0 +1,70 @@ +'use client' + +import { useEffect, useRef } from 'react' + +import { HOME_SEARCH_FOCUS_HISTORY_KEY } from '@/lib/homeSearchFocusHistory' + +/** + * B106 — Android / browser back collapses Início search without leaving `/campanha`. + * Only active while mounted on the home page publisher. + */ +export const useHomeSearchFocusHistory = (input: { + focused: boolean + onCollapse: () => void +}): void => { + const { focused, onCollapse } = input + const historyPushedRef = useRef(false) + const closingProgrammaticallyRef = useRef(false) + const onCollapseRef = useRef(onCollapse) + + useEffect(() => { + onCollapseRef.current = onCollapse + }, [onCollapse]) + + useEffect(() => { + if (!focused || typeof window === 'undefined' || historyPushedRef.current) { + return + } + + window.history.pushState({ [HOME_SEARCH_FOCUS_HISTORY_KEY]: true }, '') + historyPushedRef.current = true + }, [focused]) + + useEffect(() => { + if (focused || !historyPushedRef.current || typeof window === 'undefined') { + return + } + + if (window.history.state?.[HOME_SEARCH_FOCUS_HISTORY_KEY]) { + closingProgrammaticallyRef.current = true + window.history.back() + } + historyPushedRef.current = false + }, [focused]) + + useEffect(() => { + if (typeof window === 'undefined') { + return + } + + const handlePopState = () => { + if (closingProgrammaticallyRef.current) { + closingProgrammaticallyRef.current = false + historyPushedRef.current = false + return + } + + if (!historyPushedRef.current) { + return + } + + historyPushedRef.current = false + onCollapseRef.current() + } + + window.addEventListener('popstate', handlePopState) + return () => { + window.removeEventListener('popstate', handlePopState) + } + }, []) +} diff --git a/src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx b/src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx new file mode 100644 index 00000000..909dc5f8 --- /dev/null +++ b/src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx @@ -0,0 +1,65 @@ +'use client' + +import { + createContext, + useContext, + useLayoutEffect, + useMemo, + useState, + type Dispatch, + type ReactNode, + type SetStateAction, +} from 'react' + +export type CampaignHomeSearchChromeState = { + focused: boolean + collapse: () => void +} + +type CampaignHomeSearchChromeContextValue = { + chrome: CampaignHomeSearchChromeState | null + setChrome: Dispatch> +} + +const CampaignHomeSearchChromeContext = createContext( + null, +) + +export const CampaignHomeSearchChromeProvider = ({ children }: { children: ReactNode }) => { + const [chrome, setChrome] = useState(null) + const value = useMemo(() => ({ chrome, setChrome }), [chrome]) + + return ( + + {children} + + ) +} + +const useCampaignHomeSearchChromeContext = (): CampaignHomeSearchChromeContextValue => { + const value = useContext(CampaignHomeSearchChromeContext) + if (!value) { + throw new Error('CampaignHomeSearchChromeProvider is required') + } + return value +} + +export const useCampaignHomeSearchChrome = (): CampaignHomeSearchChromeState | null => + useCampaignHomeSearchChromeContext().chrome + +export const useSetCampaignHomeSearchChrome = ( + state: CampaignHomeSearchChromeState | null, +): void => { + const { setChrome } = useCampaignHomeSearchChromeContext() + + useLayoutEffect(() => { + setChrome(state) + }, [setChrome, state]) + + useLayoutEffect( + () => () => { + setChrome(null) + }, + [setChrome], + ) +} diff --git a/src/components/campaign/shell/CampaignMobileTopBar.tsx b/src/components/campaign/shell/CampaignMobileTopBar.tsx index fc096119..c25cbd8c 100644 --- a/src/components/campaign/shell/CampaignMobileTopBar.tsx +++ b/src/components/campaign/shell/CampaignMobileTopBar.tsx @@ -5,6 +5,7 @@ import type { ReactNode } from 'react' import { useCampaignListTransition } from '@/components/campaign/shared/CampaignListPending' import { CampaignWizardNavLink } from '@/components/campaign/shared/CampaignWizardNavLink' +import { useCampaignHomeSearchChrome } from '@/components/campaign/shell/CampaignHomeSearchChromeContext' import { useCampaignWizardChrome } from '@/components/campaign/shell/CampaignWizardChromeContext' import { Button } from '@/components/ui/button' import { SidebarTrigger } from '@/components/ui/Sidebar' @@ -15,6 +16,7 @@ import { wizardFlowChromeAriaLabel, wizardMunicipalityChromeAriaLabel, } from '@/lib/campaignWizardCopy' +import { HOME_SEARCH_COLLAPSE_ARIA_LABEL } from '@/lib/campaignHomeSearchContract' import { cn } from '@/lib/utils' const wizardNavButtonClass = @@ -30,6 +32,7 @@ const wizardTitleSkeletonClass = 'mx-auto motion-reduce:animate-none bg-primary- export const CampaignMobileTopBar = ({ notificationBell }: { notificationBell?: ReactNode }) => { const chrome = useCampaignWizardChrome() + const homeSearchChrome = useCampaignHomeSearchChrome() const { isPending } = useCampaignListTransition() if (chrome) { @@ -123,13 +126,28 @@ export const CampaignMobileTopBar = ({ notificationBell }: { notificationBell?:
- + {homeSearchChrome?.focused ? ( + + ) : ( + + )}
Jorge Solla Campanha · Bahia diff --git a/src/lib/campaignHomeSearchContract.ts b/src/lib/campaignHomeSearchContract.ts index 67e3ec55..d5fd4d11 100644 --- a/src/lib/campaignHomeSearchContract.ts +++ b/src/lib/campaignHomeSearchContract.ts @@ -24,3 +24,6 @@ export const normalizeHomeSearchRaw = (raw: string): string => raw.trim() */ export const homeSearchUiFocused = (input: { inputFocused: boolean; isActive: boolean }): boolean => input.inputFocused || input.isActive + +/** B106 — mobile top bar back control when Início search is expanded. */ +export const HOME_SEARCH_COLLAPSE_ARIA_LABEL = 'Fechar busca' as const diff --git a/src/lib/homeSearchFocusHistory.ts b/src/lib/homeSearchFocusHistory.ts new file mode 100644 index 00000000..72e674a6 --- /dev/null +++ b/src/lib/homeSearchFocusHistory.ts @@ -0,0 +1,21 @@ +/** History state mark for Início mobile search focus (B106). */ +export const HOME_SEARCH_FOCUS_HISTORY_KEY = 'teqoHomeSearchFocus' as const + +export type HomeSearchFocusHistoryState = { + [HOME_SEARCH_FOCUS_HISTORY_KEY]?: true +} + +export const isHomeSearchFocusHistoryState = (state: unknown): state is HomeSearchFocusHistoryState => + typeof state === 'object' && + state !== null && + HOME_SEARCH_FOCUS_HISTORY_KEY in state && + (state as HomeSearchFocusHistoryState)[HOME_SEARCH_FOCUS_HISTORY_KEY] === true + +/** + * After popstate, the active entry may no longer carry our mark — collapse when + * focus mode was open and the browser popped our synthetic entry. + */ +export const shouldCollapseHomeSearchOnPopstate = (input: { + wasHistoryPushed: boolean + closingProgrammatically: boolean +}): boolean => input.wasHistoryPushed && !input.closingProgrammatically diff --git a/tests/unit/campaignHomeSearch.unit.spec.tsx b/tests/unit/campaignHomeSearch.unit.spec.tsx index 1f40f99f..492668b1 100644 --- a/tests/unit/campaignHomeSearch.unit.spec.tsx +++ b/tests/unit/campaignHomeSearch.unit.spec.tsx @@ -1,8 +1,10 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch' import { HomeSearchProvider } from '@/components/campaign/dashboard/HomeSearchContext' +import { HomeSearchResultsProvider } from '@/components/campaign/dashboard/HomeSearchResultsContext' +import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell' const providerValue = { query: { raw: 'ca', debounced: 'ca', isActive: true }, @@ -15,6 +17,10 @@ const providerValue = { } describe('CampaignHomeSearch', () => { + afterEach(() => { + cleanup() + }) + it('renders search input and results region', () => { render( @@ -29,3 +35,30 @@ describe('CampaignHomeSearch', () => { expect(screen.getByText('Group stub')).toBeTruthy() }) }) + +describe('HomeSearchResultsShell', () => { + afterEach(() => { + cleanup() + }) + + it('shows skeleton rows while fetching without hits (B106)', () => { + render( + + + +

Group stub

+
+
+
, + ) + + expect(screen.getByTestId('home-search-suggest-skeleton')).toBeTruthy() + expect(screen.queryByText('Group stub')).toBeNull() + }) +}) diff --git a/tests/unit/campaignMobileTopBar.unit.spec.tsx b/tests/unit/campaignMobileTopBar.unit.spec.tsx index 841070e2..76c92d31 100644 --- a/tests/unit/campaignMobileTopBar.unit.spec.tsx +++ b/tests/unit/campaignMobileTopBar.unit.spec.tsx @@ -1,4 +1,4 @@ -import { cleanup, render, screen } from '@testing-library/react' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('next/navigation', () => ({ @@ -9,6 +9,11 @@ vi.mock('next/navigation', () => ({ })) import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar' +import { + CampaignHomeSearchChromeProvider, + useSetCampaignHomeSearchChrome, + type CampaignHomeSearchChromeState, +} from '@/components/campaign/shell/CampaignHomeSearchChromeContext' import { CampaignWizardChromeProvider, useSetCampaignWizardChrome, @@ -17,27 +22,28 @@ import { import { SidebarProvider } from '@/components/ui/Sidebar' import { stubMatchMedia } from '../helpers/matchMedia' -const ChromeProbe = ({ chrome }: { chrome: CampaignWizardChromeState | null }) => { +const WizardChromeProbe = ({ chrome }: { chrome: CampaignWizardChromeState | null }) => { useSetCampaignWizardChrome(chrome) return null } -const renderTopBar = (chrome: CampaignWizardChromeState | null) => - render( - - - - - - , - ) +const HomeSearchChromeProbe = ({ chrome }: { chrome: CampaignHomeSearchChromeState | null }) => { + useSetCampaignHomeSearchChrome(chrome) + return null +} -const renderWizardTopBar = (chrome: CampaignWizardChromeState) => +const renderTopBar = (input: { + wizardChrome?: CampaignWizardChromeState | null + homeSearchChrome?: CampaignHomeSearchChromeState | null +}) => render( - - + + + + + , ) @@ -53,20 +59,37 @@ describe('CampaignMobileTopBar', () => { }) it('renders app mode with sidebar trigger when chrome is null', () => { - const { container } = renderTopBar(null) + const { container } = renderTopBar({}) const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]') expect(topBar?.getAttribute('data-mode')).toBe('app') expect(screen.getByText('Jorge Solla')).toBeTruthy() expect(screen.queryByRole('link', { name: /Voltar/ })).toBeNull() + expect(screen.queryByRole('button', { name: 'Fechar busca' })).toBeNull() expect(screen.queryByRole('link', { name: 'Sair da ação' })).toBeNull() }) + it('renders app mode with Voltar when home search is focused (B106)', () => { + const collapse = vi.fn() + const { container } = renderTopBar({ + homeSearchChrome: { focused: true, collapse }, + }) + + const topBar = container.querySelector('[data-slot="campaign-mobile-top-bar"]') + expect(topBar?.getAttribute('data-home-search-focused')).toBe('true') + expect(screen.queryByRole('button', { name: 'Abrir ou fechar menu da campanha' })).toBeNull() + + fireEvent.click(screen.getByRole('button', { name: 'Fechar busca' })) + expect(collapse).toHaveBeenCalledTimes(1) + }) + it('renders wizard entry mode with dismiss control and flow title', () => { - renderWizardTopBar({ - flowTitle: 'Ajustar votos', - stepKind: 'entry', - dismissHref: '/campanha', + renderTopBar({ + wizardChrome: { + flowTitle: 'Ajustar votos', + stepKind: 'entry', + dismissHref: '/campanha', + }, }) expect(screen.getByText('Ajustar votos')).toBeTruthy() @@ -77,12 +100,14 @@ describe('CampaignMobileTopBar', () => { }) it('renders wizard continue mode with back link and municipality subtitle', () => { - renderWizardTopBar({ - flowTitle: 'Ajustar votos', - municipalityLabel: 'Cairu', - stepKind: 'continue', - previousHref: '/campanha/acoes/atualizar-votos', - dismissHref: '/campanha', + renderTopBar({ + wizardChrome: { + flowTitle: 'Ajustar votos', + municipalityLabel: 'Cairu', + stepKind: 'continue', + previousHref: '/campanha/acoes/atualizar-votos', + dismissHref: '/campanha', + }, }) expect(screen.getByRole('link', { name: /Voltar/ }).getAttribute('href')).toBe( @@ -93,14 +118,16 @@ describe('CampaignMobileTopBar', () => { }) it('renders skip link instead of dismiss when chained flow defines skip', () => { - renderWizardTopBar({ - flowTitle: 'Registrar sinal', - stepKind: 'continue', - previousHref: '/campanha/acoes/atualizar-votos?municipio=cairu', - dismissHref: '/campanha', - skip: { - label: 'Pular', - href: '/campanha/acoes/mudar-tendencia?municipio=cairu', + renderTopBar({ + wizardChrome: { + flowTitle: 'Registrar sinal', + stepKind: 'continue', + previousHref: '/campanha/acoes/atualizar-votos?municipio=cairu', + dismissHref: '/campanha', + skip: { + label: 'Pular', + href: '/campanha/acoes/mudar-tendencia?municipio=cairu', + }, }, }) @@ -114,14 +141,16 @@ describe('CampaignMobileTopBar', () => { const { rerender } = render( - - + + + + , ) @@ -131,16 +160,18 @@ describe('CampaignMobileTopBar', () => { rerender( - - + + + + , ) diff --git a/tests/unit/homeSearchFocusHistory.unit.spec.ts b/tests/unit/homeSearchFocusHistory.unit.spec.ts new file mode 100644 index 00000000..925928b5 --- /dev/null +++ b/tests/unit/homeSearchFocusHistory.unit.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' + +import { + HOME_SEARCH_FOCUS_HISTORY_KEY, + isHomeSearchFocusHistoryState, + shouldCollapseHomeSearchOnPopstate, +} from '@/lib/homeSearchFocusHistory' + +describe('homeSearchFocusHistory', () => { + it('recognizes the focus history mark', () => { + expect(isHomeSearchFocusHistoryState({ [HOME_SEARCH_FOCUS_HISTORY_KEY]: true })).toBe(true) + expect(isHomeSearchFocusHistoryState({})).toBe(false) + expect(isHomeSearchFocusHistoryState(null)).toBe(false) + }) + + it('collapses on popstate only when focus history was pushed by us', () => { + expect( + shouldCollapseHomeSearchOnPopstate({ + wasHistoryPushed: true, + closingProgrammatically: false, + }), + ).toBe(true) + + expect( + shouldCollapseHomeSearchOnPopstate({ + wasHistoryPushed: true, + closingProgrammatically: true, + }), + ).toBe(false) + + expect( + shouldCollapseHomeSearchOnPopstate({ + wasHistoryPushed: false, + closingProgrammatically: false, + }), + ).toBe(false) + }) +}) From 50aa83ea8f36605849f43be5afa1649b97ae6850 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 18:38:04 +0000 Subject: [PATCH 2/4] =?UTF-8?q?B106=20=E2=80=94=20optional=20home=20search?= =?UTF-8?q?=20chrome=20read=20for=20wizard=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Solla --- .../campaign/shell/CampaignHomeSearchChromeContext.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx b/src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx index 909dc5f8..61480db5 100644 --- a/src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx +++ b/src/components/campaign/shell/CampaignHomeSearchChromeContext.tsx @@ -36,6 +36,11 @@ export const CampaignHomeSearchChromeProvider = ({ children }: { children: React ) } +export const useCampaignHomeSearchChrome = (): CampaignHomeSearchChromeState | null => { + const value = useContext(CampaignHomeSearchChromeContext) + return value?.chrome ?? null +} + const useCampaignHomeSearchChromeContext = (): CampaignHomeSearchChromeContextValue => { const value = useContext(CampaignHomeSearchChromeContext) if (!value) { @@ -44,9 +49,6 @@ const useCampaignHomeSearchChromeContext = (): CampaignHomeSearchChromeContextVa return value } -export const useCampaignHomeSearchChrome = (): CampaignHomeSearchChromeState | null => - useCampaignHomeSearchChromeContext().chrome - export const useSetCampaignHomeSearchChrome = ( state: CampaignHomeSearchChromeState | null, ): void => { From d4a7b448adccee5ffd617836fb769f7a4d752ee3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 18:38:07 +0000 Subject: [PATCH 3/4] docs(plans): mark B106 inicio-busca-voltar-skeleton done Co-authored-by: Francisco Solla --- docs/plans/inicio-busca-voltar-skeleton.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/inicio-busca-voltar-skeleton.md b/docs/plans/inicio-busca-voltar-skeleton.md index a7300031..e4940ef5 100644 --- a/docs/plans/inicio-busca-voltar-skeleton.md +++ b/docs/plans/inicio-busca-voltar-skeleton.md @@ -1,7 +1,7 @@ # Início mobile — Voltar no header + Android back + skeleton das sugestões -Status: registrado -Atualizado em: 2026-08-01 +Status: done +Atualizado em: 2026-08-01 — as-built: `CampaignHomeSearchChromeContext` + publisher no Início; `CampaignMobileTopBar` troca `SidebarTrigger` por Voltar quando `uiFocused`; `useHomeSearchFocusHistory` com `pushState`/`popstate` (marca `teqoHomeSearchFocus`); `HomeSearchSuggestSkeleton` (3 rows) enquanto `isFetching` sem hits; unit `homeSearchFocusHistory`, `campaignMobileTopBar`, `campaignHomeSearch`. Sem migration. Issue: #141 Priority: P1 Model: composer-2.5 From f80f6727327ecf90cfa4b2e6e3df8f036bc56c65 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 18:38:50 +0000 Subject: [PATCH 4/4] =?UTF-8?q?B106=20=E2=80=94=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(campaign)/campanha/(app)/layout.tsx | 36 +++++++++---------- .../dashboard/CampaignHomeStaffChrome.tsx | 2 +- .../dashboard/HomeSearchResultsShell.tsx | 5 ++- .../campaign/shell/CampaignMobileTopBar.tsx | 2 +- src/lib/homeSearchFocusHistory.ts | 4 ++- tests/unit/campaignMobileTopBar.unit.spec.tsx | 2 +- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/app/(campaign)/campanha/(app)/layout.tsx b/src/app/(campaign)/campanha/(app)/layout.tsx index ee35e237..82ef42a0 100644 --- a/src/app/(campaign)/campanha/(app)/layout.tsx +++ b/src/app/(campaign)/campanha/(app)/layout.tsx @@ -78,30 +78,30 @@ export default async function CampaignAppLayout({ children }: { children: React. - - } - /> -
- -
- -
-
- {/* + + } + /> +
+ +
+ +
+
+ {/* Provider must wrap CampaignAppScrollChrome, not only page children: the mobile quick-actions drawer (B91/B100) mounts as a sibling of the scrollport and renders search hits with CampaignHoverTooltip (priority flag). Nested only around {children} left focus→suggest without a provider and crashed the page (B102). */} - - {children} - - - - {biometricEnrollment ? : null} -
+ + {children} + + + + {biometricEnrollment ? : null} +
diff --git a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx index a54094c0..fb91e0ad 100644 --- a/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx +++ b/src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx @@ -8,8 +8,8 @@ import { } from '@/components/campaign/dashboard/CampaignGlobalSearchMount' import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHomeLayout' import { CampaignHomeSearch } from '@/components/campaign/dashboard/CampaignHomeSearch' -import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext' import { CampaignHomeSearchChromePublisher } from '@/components/campaign/dashboard/CampaignHomeSearchChromePublisher' +import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext' import { HomeSearchResultsProvider, InitialHomeSearchSuggestProvider, diff --git a/src/components/campaign/dashboard/HomeSearchResultsShell.tsx b/src/components/campaign/dashboard/HomeSearchResultsShell.tsx index 9d999122..4e0297a6 100644 --- a/src/components/campaign/dashboard/HomeSearchResultsShell.tsx +++ b/src/components/campaign/dashboard/HomeSearchResultsShell.tsx @@ -3,16 +3,15 @@ import type { ReactNode } from 'react' import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext' -import { HomeSearchSuggestSkeleton } from '@/components/campaign/dashboard/HomeSearchSuggestSkeleton' import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext' +import { HomeSearchSuggestSkeleton } from '@/components/campaign/dashboard/HomeSearchSuggestSkeleton' import { homeSearchHasAnyHits } from '@/lib/campaignHomeSearchHits' export const HomeSearchResultsShell = ({ children }: { children: ReactNode }) => { const { query } = useHomeSearch() const { results, resultKind, isFetching } = useHomeSearchResults() - const hasRenderableHits = - results.status === 'success' && homeSearchHasAnyHits(results.data) + const hasRenderableHits = results.status === 'success' && homeSearchHasAnyHits(results.data) const showSkeleton = isFetching && !hasRenderableHits && results.status !== 'error' const showEmpty = diff --git a/src/components/campaign/shell/CampaignMobileTopBar.tsx b/src/components/campaign/shell/CampaignMobileTopBar.tsx index c25cbd8c..9f3b37c7 100644 --- a/src/components/campaign/shell/CampaignMobileTopBar.tsx +++ b/src/components/campaign/shell/CampaignMobileTopBar.tsx @@ -10,13 +10,13 @@ import { useCampaignWizardChrome } from '@/components/campaign/shell/CampaignWiz import { Button } from '@/components/ui/button' import { SidebarTrigger } from '@/components/ui/Sidebar' import { Skeleton } from '@/components/ui/skeleton' +import { HOME_SEARCH_COLLAPSE_ARIA_LABEL } from '@/lib/campaignHomeSearchContract' import { WIZARD_APP_TOP_BAR_ARIA_LABEL, WIZARD_DISMISS_ARIA_LABEL, wizardFlowChromeAriaLabel, wizardMunicipalityChromeAriaLabel, } from '@/lib/campaignWizardCopy' -import { HOME_SEARCH_COLLAPSE_ARIA_LABEL } from '@/lib/campaignHomeSearchContract' import { cn } from '@/lib/utils' const wizardNavButtonClass = diff --git a/src/lib/homeSearchFocusHistory.ts b/src/lib/homeSearchFocusHistory.ts index 72e674a6..7921fb47 100644 --- a/src/lib/homeSearchFocusHistory.ts +++ b/src/lib/homeSearchFocusHistory.ts @@ -5,7 +5,9 @@ export type HomeSearchFocusHistoryState = { [HOME_SEARCH_FOCUS_HISTORY_KEY]?: true } -export const isHomeSearchFocusHistoryState = (state: unknown): state is HomeSearchFocusHistoryState => +export const isHomeSearchFocusHistoryState = ( + state: unknown, +): state is HomeSearchFocusHistoryState => typeof state === 'object' && state !== null && HOME_SEARCH_FOCUS_HISTORY_KEY in state && diff --git a/tests/unit/campaignMobileTopBar.unit.spec.tsx b/tests/unit/campaignMobileTopBar.unit.spec.tsx index 76c92d31..56d61633 100644 --- a/tests/unit/campaignMobileTopBar.unit.spec.tsx +++ b/tests/unit/campaignMobileTopBar.unit.spec.tsx @@ -8,12 +8,12 @@ vi.mock('next/navigation', () => ({ }), })) -import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar' import { CampaignHomeSearchChromeProvider, useSetCampaignHomeSearchChrome, type CampaignHomeSearchChromeState, } from '@/components/campaign/shell/CampaignHomeSearchChromeContext' +import { CampaignMobileTopBar } from '@/components/campaign/shell/CampaignMobileTopBar' import { CampaignWizardChromeProvider, useSetCampaignWizardChrome,