teqo-backup/src/components/campaign/shell/CampaignQuickActionsHost.tsx
Cursor Agent 490d5deeb7
B109 — bottom drawer: fullscreen search, wider labels, exclude current entity
- Add QUICK_ACTIONS_SNAP_FULL; drawer expands on search focus and hides action strip
- Tighten dock bottom gap; restore dock/collapsed on blur per scroll position (B100)
- Widen CampaignHomeActionButton to w-[5.5rem] for two-line labels
- Client-side filter excludes current page entity from drawer search hits
- Unit + e2e coverage for full snap, exclusion, and label readability
2026-08-01 20:38:10 +00:00

116 lines
3.8 KiB
TypeScript

'use client'
import { usePathname } from 'next/navigation'
import { useCallback, useEffect, useRef, type ReactNode } from 'react'
import { useHomeSearch } from '@/components/campaign/dashboard/HomeSearchContext'
import { useCampaignQuickActionContext } from '@/components/campaign/shell/CampaignQuickActionContext'
import { CampaignQuickActionsDrawer } from '@/components/campaign/shell/CampaignQuickActionsDrawer'
import { useCampaignQuickActionsSnap } from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import { useIsMobile } from '@/hooks/use-mobile'
import { shouldMountQuickActionsDrawer } from '@/lib/campaignQuickActionMount'
import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry'
import {
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_DOCK,
quickActionsScrollDirection,
} from '@/lib/campaignQuickActionSnap'
import type { CampaignRole } from '@/lib/campaignRoles'
import { cn } from '@/lib/utils'
const CampaignContentScrollWithPeek = ({ children }: { children: ReactNode }) => {
const scrollRef = useRef<HTMLDivElement>(null)
const lastScrollTopRef = useRef(0)
const { isDock, isFull, setSnapPoint } = useCampaignQuickActionsSnap()
const { uiFocused } = useHomeSearch()
const peekHeight = isFull
? '0px'
: isDock
? QUICK_ACTIONS_SNAP_DOCK
: QUICK_ACTIONS_SNAP_COLLAPSED
const syncSnapFromScroll = useCallback(() => {
const scrollport = scrollRef.current
if (!scrollport) return
const nextTop = scrollport.scrollTop
const direction = quickActionsScrollDirection(lastScrollTopRef.current, nextTop)
lastScrollTopRef.current = nextTop
// Keep the scroll baseline fresh while search is focused; only gate snap writes.
if (uiFocused) return
if (direction === 'down') {
setSnapPoint(QUICK_ACTIONS_SNAP_COLLAPSED)
return
}
if (direction === 'up') {
setSnapPoint(QUICK_ACTIONS_SNAP_DOCK)
}
}, [setSnapPoint, uiFocused])
useEffect(() => {
const scrollport = scrollRef.current
if (!scrollport) return
lastScrollTopRef.current = scrollport.scrollTop
scrollport.addEventListener('scroll', syncSnapFromScroll, { passive: true })
return () => {
scrollport.removeEventListener('scroll', syncSnapFromScroll)
}
}, [syncSnapFromScroll])
return (
<div
ref={scrollRef}
data-slot="campaign-content-scroll"
className={cn(
'min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 md:p-6 print:h-auto print:overflow-visible print:p-0',
'pb-[calc(1rem+var(--campaign-quick-actions-peek))] md:pb-6',
)}
style={{ '--campaign-quick-actions-peek': peekHeight } as React.CSSProperties}
>
{children}
</div>
)
}
export const CampaignContentScroll = ({
children,
quickActionsPeek,
}: {
children: ReactNode
quickActionsPeek: boolean
}) => {
if (!quickActionsPeek) {
return (
<div
data-slot="campaign-content-scroll"
className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 md:p-6 print:h-auto print:overflow-visible print:p-0"
>
{children}
</div>
)
}
return <CampaignContentScrollWithPeek>{children}</CampaignContentScrollWithPeek>
}
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 <CampaignQuickActionsDrawer actions={actions} />
}
export const useQuickActionsChromeActive = (role: CampaignRole): boolean => {
const pathname = usePathname()
const isMobile = useIsMobile()
return isMobile && shouldMountQuickActionsDrawer(pathname, role)
}