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 = true, 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 = isCollapsible ? ( ) : null; const headerContent = ( <> {leading}
{children}
{(trailing || chevronIcon) && (
{trailing} {chevronIcon && ( )}
)} {actions} ); if (!isInteractive) { return (
{headerContent}
); } return ( // biome-ignore lint/a11y/useSemanticElements: outer div intentionally wraps a nested ); } 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; if (isCollapsible) { return (
{children}
); } return
{children}
; }