diff --git a/src/components/Collapse.tsx b/src/components/Collapse.tsx new file mode 100644 index 00000000..d076888d --- /dev/null +++ b/src/components/Collapse.tsx @@ -0,0 +1,132 @@ +import React, { useCallback, useId, useMemo, useState } from 'react'; +import { cn } from '@/lib/helper'; + +export type CollapseVariant = 'default' | 'arrow' | 'plus'; + +export type CollapseProps = { + /** Unique name used when `asRadio` is true (Accordion single-open). */ + name?: string; + /** If provided, component is controlled. */ + open?: boolean; + /** Initial open state for uncontrolled usage. */ + defaultOpen?: boolean; + /** Callback when open state changes. */ + onOpenChange?: (open: boolean) => void; + /** Title row content. Accepts string or custom node. */ + title?: React.ReactNode; + /** Optional secondary text displayed under/next to title. */ + subtitle?: React.ReactNode; + /** Content of the panel. */ + children?: React.ReactNode; + /** Visual variant: default / arrow / plus */ + variant?: CollapseVariant; + /** Add a bordered look */ + bordered?: boolean; + /** Disable interactions */ + disabled?: boolean; + /** Allow only one open at a time by switching to radio input */ + asRadio?: boolean; + /** Extra classnames */ + className?: string; + titleClassName?: string; + contentClassName?: string; +}; + +export const Collapse = ({ + name, + open, + defaultOpen, + onOpenChange, + title, + subtitle, + children, + variant = 'default', + bordered, + disabled, + asRadio = false, + className, + titleClassName, + contentClassName, +}: CollapseProps) => { + const inputId = useId(); + const isControlled = typeof open === 'boolean'; + const [internalOpen, setInternalOpen] = useState(!!defaultOpen); + const isOpen = isControlled ? !!open : internalOpen; + + // Manage change from checkbox/radio + const handleChange = useCallback( + (next: boolean) => { + if (!isControlled) setInternalOpen(next); + onOpenChange?.(next); + }, + [isControlled, onOpenChange] + ); + + const inputType = asRadio ? 'radio' : 'checkbox'; + + const rootClass = cn( + 'collapse', + variant === 'arrow' && 'collapse-arrow', + variant === 'plus' && 'collapse-plus', + bordered && 'border base-content/20 border-opacity-20 rounded-box', + disabled && 'opacity-60 pointer-events-none', + className + ); + + const titleNode = useMemo(() => { + if (subtitle) { + return ( +