"use client"; import { AlertTriangle, Check, ChevronDown, Circle, Info, LoaderCircle, X, type LucideIcon, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion, type HTMLMotionProps, type Transition, type Variants, } from "motion/react"; import { useCallback, useMemo, useState, type ReactNode, } from "react"; import { cn } from "@/lib/utils"; export type InlineStatusRowStatus = | "idle" | "queued" | "running" | "success" | "warning" | "error"; export type InlineStatusRowClassNames = { root?: string; button?: string; iconWrap?: string; content?: string; title?: string; description?: string; meta?: string; progressTrack?: string; progressBar?: string; details?: string; chevron?: string; }; export interface InlineStatusRowProps extends Omit, "children" | "title"> { status?: InlineStatusRowStatus; title: ReactNode; description?: ReactNode; meta?: ReactNode; progress?: number; details?: ReactNode; expanded?: boolean; defaultExpanded?: boolean; onExpandedChange?: (expanded: boolean) => void; disabled?: boolean; icon?: ReactNode; icons?: Partial>; classNames?: InlineStatusRowClassNames; renderDetails?: (state: { status: InlineStatusRowStatus; expanded: boolean }) => ReactNode; } const STATUS_ICON: Record = { idle: Circle, queued: Info, running: LoaderCircle, success: Check, warning: AlertTriangle, error: X, }; const STATUS_CLASS: Record = { idle: "bg-(--color-fg)/[0.05] text-(--color-fg-muted)", queued: "bg-(--color-accent)/10 text-(--color-accent)", running: "bg-(--color-violet)/10 text-(--color-violet)", success: "bg-(--color-success)/10 text-(--color-success)", warning: "bg-(--color-warning)/10 text-(--color-warning)", error: "bg-(--color-danger)/10 text-(--color-danger)", }; const BAR_CLASS: Record = { idle: "bg-(--color-fg-muted)", queued: "bg-(--color-accent)", running: "bg-(--color-violet)", success: "bg-(--color-success)", warning: "bg-(--color-warning)", error: "bg-(--color-danger)", }; const ROW_TRANSITION: Transition = { type: "spring", stiffness: 430, damping: 34, mass: 0.72, }; const CONTENT_TRANSITION = { duration: 0.28, ease: [0.16, 1, 0.3, 1], } as const; const ICON_VARIANTS: Variants = { initial: { opacity: 0.6, y: 10, scale: 0.86, filter: "blur(6px)" }, animate: { opacity: 1, y: 0, scale: 1, filter: "blur(0px)", transition: CONTENT_TRANSITION, }, exit: { opacity: 0.45, y: -10, scale: 0.9, filter: "blur(6px)", transition: { duration: 0.2, ease: [0.16, 1, 0.3, 1] }, }, }; function clampProgress(progress?: number) { if (progress == null) return undefined; return Math.min(Math.max(progress, 0), 100); } function useControllableExpanded({ expanded, defaultExpanded, onExpandedChange, }: { expanded?: boolean; defaultExpanded?: boolean; onExpandedChange?: (expanded: boolean) => void; }) { const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? false); const value = expanded ?? internalExpanded; const isControlled = expanded !== undefined; const setValue = useCallback( (next: boolean) => { if (!isControlled) setInternalExpanded(next); onExpandedChange?.(next); }, [isControlled, onExpandedChange], ); return [value, setValue] as const; } export function InlineStatusRow({ status = "idle", title, description, meta, progress, details, expanded, defaultExpanded, onExpandedChange, disabled = false, icon, icons, className, classNames, renderDetails, ...rest }: InlineStatusRowProps) { const reduce = useReducedMotion(); const Icon = STATUS_ICON[status]; const [isExpanded, setIsExpanded] = useControllableExpanded({ expanded, defaultExpanded, onExpandedChange, }); const resolvedProgress = clampProgress(progress); const canExpand = Boolean(details || renderDetails); const detailsContent = useMemo( () => renderDetails?.({ status, expanded: isExpanded }) ?? details, [details, isExpanded, renderDetails, status], ); return ( {isExpanded && detailsContent ? (
{detailsContent}
) : null}
); }