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 (
{error}
);
}
if (disabled && disabledReason) {
return (
{disabledReason}
);
}
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 (