import { ChevronDownIcon, ChevronRightIcon, CircleAlertIcon, LockIcon } from "lucide-react"; import { useCallback, useId, useState, type ReactNode } from "react"; import { Button } from "./button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible"; import { DetailBody, DetailFooter, DetailHeader, DetailSection, DetailSheet, DetailSheetContent, DetailSheetTrigger, } from "./detail-sheet"; import { SheetClose } from "./sheet"; import { cn } from "../lib/cn"; /** * - `row` (default): a full-width row with a left chevron that turns 90°, the * title and a summary of the current values. Opens in place. * - `inline`: summary on the left, "Change" / "Hide" and a chevron on the right, * between two hairlines. Opens in place. * - `sheet`: nothing opens in place. The row has a right chevron (it opens * something) and the options live in a DetailSheet. */ export type DisclosureVariant = "row" | "inline" | "sheet"; export interface DisclosureProps { variant?: DisclosureVariant | undefined; /** "Advanced", "Technical details". */ title: ReactNode; /** The current values, so people know what is inside before opening it. */ summary?: ReactNode | undefined; open?: boolean | undefined; defaultOpen?: boolean | undefined; onOpenChange?: ((open: boolean) => void) | undefined; disabled?: boolean | undefined; /** Why it can't be opened and who can change that. Replaces the summary. */ disabledReason?: ReactNode | undefined; /** * Something inside needs attention: "Data notebooks was deleted. Choose * where it runs." Replaces the summary, so a closed section never hides an * error. */ error?: ReactNode | undefined; /** The summary is still loading. */ loading?: boolean | undefined; /** Sheet variant: the sheet's title. Defaults to `title`. */ sheetTitle?: ReactNode | undefined; /** Sheet variant: one line under the sheet's title, usually the object's name. */ sheetDescription?: ReactNode | undefined; /** Sheet variant: footer actions. Defaults to one "Done" button. */ sheetFooter?: ReactNode | undefined; className?: string | undefined; /** Classes on the row that opens it. */ triggerClassName?: string | undefined; /** Classes on the revealed content. */ contentClassName?: string | undefined; children: ReactNode; } function useOpenState( open: boolean | undefined, defaultOpen: boolean | undefined, onOpenChange: ((open: boolean) => void) | undefined, ) { const [inner, setInner] = useState(defaultOpen ?? false); const controlled = open !== undefined; const current = controlled ? open : inner; const setOpen = useCallback( (next: boolean) => { if (!controlled) setInner(next); onOpenChange?.(next); }, [controlled, onOpenChange], ); return [current, setOpen] as const; } /** * The line under (or next to) the title: the summary, or what blocks it or * needs attention. `clampClassName` limits only the summary text; an error or * a reason always shows in full. */ function SummaryLine({ id, summary, disabled, disabledReason, error, loading, className, clampClassName = "line-clamp-2", }: { id: string; summary?: ReactNode | undefined; disabled?: boolean | undefined; disabledReason?: ReactNode | undefined; error?: ReactNode | undefined; loading?: boolean | undefined; className?: string | undefined; clampClassName?: string | undefined; }) { if (error) { return ( ); } if (disabled && disabledReason) { return ( ); } if (loading) { return ( Loading ); } if (!summary) return null; return ( {summary} ); } /** * Hides secondary options of the same object, one level only. Never nest one * inside another, never put the primary action inside, and never use it for * a sheet's main content. */ export function Disclosure({ variant = "row", title, summary, open: openProp, defaultOpen, onOpenChange, disabled, disabledReason, error, loading, sheetTitle, sheetDescription, sheetFooter, className, triggerClassName, contentClassName, children, }: DisclosureProps) { const [open, setOpen] = useOpenState(openProp, defaultOpen, onOpenChange); const summaryId = useId(); const contentId = useId(); const hasLine = Boolean(error || (disabled && disabledReason) || loading || summary); const line = (lineClassName?: string, clampClassName?: string) => hasLine ? ( ) : null; if (variant === "sheet") { return (
{children} {sheetFooter ?? ( )}
); } if (variant === "inline") { return ( {children} ); } return ( {children} ); }