import { createContext, useContext, useEffect, useState } from "react"; import { cn } from "@/utils/ui"; import { HugeiconsIcon } from "@hugeicons/react"; import { ArrowDownIcon } from "@hugeicons/core-free-icons"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; const sectionExpandedCache = new Map(); const mountedSectionKeys = new Set(); interface SectionContext { isOpen: boolean; toggle: () => void; collapsible: boolean; } const SectionCtx = createContext(null); function useSectionContext() { return useContext(SectionCtx); } interface SectionProps { children: React.ReactNode; collapsible?: boolean; defaultOpen?: boolean; sectionKey?: string; className?: string; showTopBorder?: boolean; showBottomBorder?: boolean; } export function Section({ children, collapsible = false, defaultOpen = true, sectionKey, className, showTopBorder = false, showBottomBorder = true, }: SectionProps) { const cached = sectionKey ? sectionExpandedCache.get(sectionKey) : undefined; const [isOpen, setIsOpen] = useState(cached ?? defaultOpen); useEffect(() => { if (!sectionKey) return; if ( process.env.NODE_ENV !== "production" && mountedSectionKeys.has(sectionKey) ) { console.error( `[Section] duplicate sectionKey mounted simultaneously: "${sectionKey}"`, ); } mountedSectionKeys.add(sectionKey); return () => { mountedSectionKeys.delete(sectionKey); }; }, [sectionKey]); const toggle = () => { const next = !isOpen; setIsOpen(next); if (sectionKey) sectionExpandedCache.set(sectionKey, next); }; return (
{children}
); } interface SectionHeaderProps { children?: React.ReactNode; trailing?: React.ReactNode; leading?: React.ReactNode; actions?: React.ReactNode; onClick?: () => void; className?: string; } export function SectionHeader({ children, trailing, leading, actions, onClick, className, }: SectionHeaderProps) { const ctx = useSectionContext(); const isCollapsible = ctx?.collapsible ?? false; const isOpen = ctx?.isOpen ?? true; const isInteractive = isCollapsible || !!onClick; const handleClick = isCollapsible ? ctx?.toggle : onClick; const chevronIcon = ( ); const trailingArea = trailing || isCollapsible ? (
{trailing} {isCollapsible && ( )}
) : null; const innerContent = isInteractive ? ( ) : ( <> {leading}
{children}
); return (
{innerContent} {trailingArea} {actions}
); } export function SectionTitle({ children, className, onClick, }: { children: React.ReactNode; className?: string; onClick?: () => void; }) { const ctx = useSectionContext(); const isCollapsible = ctx?.collapsible ?? false; const isOpen = ctx?.isOpen ?? true; const titleClass = cn( "text-sm font-medium", isCollapsible && isOpen ? "text-foreground" : "text-muted-foreground", className, ); if (onClick) { return ( ); } return {children}; } export function SectionFields({ children, className, }: { children: React.ReactNode; className?: string; }) { return (
{children}
); } export function SectionField({ label, beforeLabel, children, className, }: { label: string; beforeLabel?: React.ReactNode; children: React.ReactNode; className?: string; }) { return (
{beforeLabel}
{children}
); } export function SectionContent({ children, className, }: { children: React.ReactNode; className?: string; }) { const ctx = useSectionContext(); const isCollapsible = ctx?.collapsible ?? false; const isOpen = ctx?.isOpen ?? true; const content =
{children}
; if (isCollapsible) { return (
{content}
); } return content; }