"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
export type CollapseProps = React.ComponentProps<"details"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
variant?: "default" | "soft" | "ghost"
size?: "sm" | "md" | "lg"
}
function Collapse({
open,
defaultOpen,
onOpenChange,
onToggle,
className,
children,
variant = "default",
size = "md",
...props
}: CollapseProps) {
const isControlled = open !== undefined
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false)
const currentOpen = isControlled ? open : internalOpen
return (
{
onToggle?.(event)
const nextOpen = event.currentTarget.open
if (!isControlled) {
setInternalOpen(nextOpen)
} else if (nextOpen !== currentOpen) {
event.currentTarget.open = currentOpen
}
onOpenChange?.(nextOpen)
}}
open={currentOpen}
{...props}
>
{children}
)
}
export type CollapseTriggerProps = React.ComponentProps<"summary"> & {
icon?: React.ReactNode
hideIcon?: boolean
size?: "sm" | "md" | "lg"
inset?: boolean
indicatorPosition?: "start" | "end"
}
function CollapseTrigger({
icon,
hideIcon = false,
size = "md",
inset = false,
indicatorPosition = "end",
className,
children,
onClick,
...props
}: CollapseTriggerProps) {
const indicator = !hideIcon ? (
{icon ?? }
) : null
return (
{
if (props["aria-disabled"]) {
event.preventDefault()
return
}
onClick?.(event)
}}
{...props}
>
{indicatorPosition === "start" ? indicator : null}
{children}
{indicatorPosition === "end" ? indicator : null}
)
}
function CollapseContent({ className, ...props }: React.ComponentProps<"div">) {
return (
)
}
export type CollapseItem = {
key: string
title: React.ReactNode
content: React.ReactNode
description?: React.ReactNode
meta?: React.ReactNode
badge?: React.ReactNode
disabled?: boolean
disabledReason?: React.ReactNode
icon?: React.ReactNode
indicatorPosition?: CollapseTriggerProps["indicatorPosition"]
triggerClassName?: string
contentClassName?: string
}
export type CollapseGroupProps = React.ComponentProps<"div"> & {
items: CollapseItem[]
type?: "single" | "multiple"
value?: string | string[]
defaultValue?: string | string[]
onValueChange?: (value: string | string[]) => void
variant?: CollapseProps["variant"]
size?: CollapseTriggerProps["size"]
}
function CollapseGroup({
items,
type = "multiple",
value,
defaultValue,
onValueChange,
variant = "default",
size = "md",
className,
...props
}: CollapseGroupProps) {
const initialValue = React.useMemo(() => {
if (value !== undefined) return value
if (defaultValue !== undefined) return defaultValue
return type === "single" ? "" : []
}, [defaultValue, type, value])
const [internalValue, setInternalValue] = React.useState(initialValue)
const currentValue = value ?? internalValue
const isOpen = (key: string) => Array.isArray(currentValue) ? currentValue.includes(key) : currentValue === key
const updateValue = (key: string, nextOpen: boolean) => {
const nextValue = type === "single"
? nextOpen ? key : ""
: nextOpen
? Array.from(new Set([...(Array.isArray(currentValue) ? currentValue : []), key]))
: (Array.isArray(currentValue) ? currentValue.filter((item) => item !== key) : [])
if (value === undefined) setInternalValue(nextValue)
onValueChange?.(nextValue)
}
return (
{items.map((item) => (
{
if (item.disabled) return
updateValue(item.key, open)
}}
variant={variant}
size={size}
className={cn(item.disabled && "pointer-events-none opacity-60")}
>
{item.title}
{item.badge ? {item.badge} : null}
{item.meta ? {item.meta} : null}
{item.description && {item.description}}
{item.disabled && item.disabledReason ? (
{item.disabledReason}
) : null}
{item.content}
))}
)
}
export { Collapse, CollapseContent, CollapseGroup, CollapseTrigger }