All files / src/components/ui collapsible.tsx

100% Statements 6/6
100% Branches 18/18
100% Functions 4/4
100% Lines 6/6

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116                                            44x   44x       6x                                                                                           1359x   1359x       3x                                                                      
import { useState, type ReactNode } from "react";
import { ChevronDown } from "lucide-react";
 
type CollapsibleProps = {
	title: ReactNode;
	description?: ReactNode;
	icon?: ReactNode;
	defaultOpen?: boolean;
	children: ReactNode;
	className?: string;
	headerClassName?: string;
};
 
export function Collapsible({
	title,
	description,
	icon,
	defaultOpen = false,
	children,
	className = "",
	headerClassName = "",
}: CollapsibleProps) {
	const [isOpen, setIsOpen] = useState(defaultOpen);
 
	return (
		<div className={className}>
			<button
				type="button"
				onClick={() => setIsOpen(!isOpen)}
				className={`w-full flex items-center justify-between text-left ${headerClassName}`}
			>
				<div className="flex items-center gap-2">
					{icon}
					<div>
						<div className="font-semibold text-foreground-default">{title}</div>
						{description && (
							<div className="text-sm text-foreground-muted">{description}</div>
						)}
					</div>
				</div>
				<ChevronDown
					className={`h-5 w-5 text-foreground-muted transition-transform duration-200 ${
						isOpen ? "rotate-180" : ""
					}`}
				/>
			</button>
			<div
				className={`overflow-hidden transition-all duration-200 ${
					isOpen ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0"
				}`}
			>
				{children}
			</div>
		</div>
	);
}
 
type CollapsibleCardProps = {
	title: ReactNode;
	description?: ReactNode;
	icon?: ReactNode;
	defaultOpen?: boolean;
	children: ReactNode;
	badge?: ReactNode;
};
 
export function CollapsibleCard({
	title,
	description,
	icon,
	defaultOpen = false,
	children,
	badge,
}: CollapsibleCardProps) {
	const [isOpen, setIsOpen] = useState(defaultOpen);
 
	return (
		<div className="rounded-lg border border-border-default bg-background-elevated shadow-card">
			<button
				type="button"
				onClick={() => setIsOpen(!isOpen)}
				className="w-full flex items-center justify-between p-6 text-left hover:bg-background-subtle transition-colors rounded-t-lg"
			>
				<div className="flex items-center gap-3">
					{icon && <span className="text-foreground-muted">{icon}</span>}
					<div>
						<div className="flex items-center gap-2">
							<span className="text-lg font-semibold text-foreground-default">
								{title}
							</span>
							{badge}
						</div>
						{description && (
							<p className="text-sm text-foreground-muted">{description}</p>
						)}
					</div>
				</div>
				<ChevronDown
					className={`h-5 w-5 text-foreground-subtle transition-transform duration-200 flex-shrink-0 ${
						isOpen ? "rotate-180" : ""
					}`}
				/>
			</button>
			<div
				className={`overflow-hidden transition-all duration-200 ${
					isOpen ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0"
				}`}
			>
				<div className="p-6 pt-0 border-t border-border-default">
					{children}
				</div>
			</div>
		</div>
	);
}