import { Loader2, RefreshCw, Search, Unplug } from 'lucide-react';
import React, { memo, useEffect, useRef, useState, type ReactNode } from 'react';
import { cn } from '../../lib/utils';
import { Input } from '../ui/input';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
function splitPanelMessage(message: string): string[] {
return message.match(/[^。.!?]+[。.!?]?/g)?.map((line) => line.trim()).filter(Boolean) ?? [message];
}
function SystemPanelMessage({
message,
className,
}: {
message: string;
className?: string;
}) {
const lines = splitPanelMessage(message);
if (lines.length <= 1) {
return {message} ;
}
return (
{lines.map((line, index) => (
{line}
))}
);
}
export const SystemPanelShell = memo(function SystemPanelShell({
children,
section,
className,
}: {
children: ReactNode;
section?: string;
className?: string;
}) {
return (
{children}
);
});
export const SystemPanelToolbar = memo(function SystemPanelToolbar({
children,
trailing,
}: {
children?: ReactNode;
trailing?: ReactNode;
}) {
return (
{children}
{trailing &&
{trailing}
}
);
});
export const SystemPanelSearch = memo(function SystemPanelSearch({
value,
onChange,
placeholder,
onEnter,
}: {
value: string;
onChange: (value: string) => void;
placeholder: string;
onEnter?: () => void;
}) {
return (
onChange(e.target.value)}
placeholder={placeholder}
className="h-7 pl-7 text-xs bg-muted/30 border-none focus-visible:ring-1 focus-visible:ring-primary/50 focus-visible:bg-muted/50 transition-all"
onKeyDown={(e) => { if (e.key === 'Enter') onEnter?.(); }}
/>
);
});
export const SystemPanelIconButton = memo(function SystemPanelIconButton({
title,
onClick,
disabled,
destructive,
children,
}: {
title: string;
onClick?: () => void;
disabled?: boolean;
destructive?: boolean;
children: ReactNode;
}) {
return (
{children}
{title}
);
});
export const SystemPanelRefreshButton = memo(function SystemPanelRefreshButton({
title,
loading,
onClick,
}: {
title: string;
loading?: boolean;
onClick: () => void;
}) {
return (
);
});
export const SystemPanelSegmented = memo(function SystemPanelSegmented({
value,
options,
onChange,
}: {
value: T;
options: { id: T; label: string }[];
onChange: (value: T) => void;
}) {
return (
{options.map((option) => (
onChange(option.id)}
className={cn(
'shrink-0 px-2.5 py-0.5 rounded-md text-[10px] transition-all duration-200 whitespace-nowrap',
value === option.id
? 'bg-primary/15 text-primary font-medium shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50',
)}
>
{option.label}
))}
);
});
export const SystemPanelMetaBar = memo(function SystemPanelMetaBar({
children,
trailing,
}: {
children: ReactNode;
trailing?: ReactNode;
}) {
return (
);
});
export const SystemPanelEmpty = memo(function SystemPanelEmpty({
icon: Icon,
message,
}: {
icon: React.ComponentType<{ size?: number; className?: string }>;
message: string;
}) {
return (
);
});
export const SystemPanelLoading = memo(function SystemPanelLoading({
message,
}: {
message: string;
}) {
return (
{message}
);
});
export const SystemPanelError = memo(function SystemPanelError({
message,
onRetry,
retryLabel,
loading,
}: {
message: string;
onRetry?: () => void;
retryLabel?: string;
loading?: boolean;
}) {
return (
{onRetry && retryLabel && (
{retryLabel}
)}
);
});
export const SystemPanelInlineError = memo(function SystemPanelInlineError({
message,
onRetry,
retryLabel,
loading,
}: {
message: string;
onRetry?: () => void;
retryLabel?: string;
loading?: boolean;
}) {
return (
{message}
{onRetry && retryLabel && (
{retryLabel}
)}
);
});
export const SystemPanelList = memo(function SystemPanelList({
children,
}: {
children: ReactNode;
}) {
// No divide-y here: the collapsible wrapper stays mounted at zero height
// during its exit animation, and a divider on it would add a moving 1px
// seam. Rows carry their own border-b instead.
return (
{children}
);
});
export const SystemPanelRow = memo(function SystemPanelRow({
selected,
onClick,
depth = 0,
leading,
title,
subtitle,
trailing,
actions,
className,
}: {
selected?: boolean;
onClick?: () => void;
depth?: number;
leading?: ReactNode;
title: ReactNode;
subtitle?: ReactNode;
trailing?: ReactNode;
actions?: ReactNode;
className?: string;
}) {
const content = (
<>
{leading}
{title}
{subtitle && (
{subtitle}
)}
{trailing}
{actions && (
e.stopPropagation()}
>
{actions}
)}
>
);
const rowClassName = cn(
'group flex items-center gap-2.5 pr-2.5 py-2.5 min-h-[44px] border-b border-border/30 relative',
'transition-colors duration-150',
selected && 'bg-accent/30',
selected && 'before:absolute before:left-0 before:top-0 before:bottom-0 before:w-0.5 before:bg-primary',
onClick && 'cursor-pointer hover:bg-accent/40',
className,
);
const style = { paddingLeft: 12 + depth * 14 };
if (onClick) {
// Not a : trailing/actions hold real buttons, and interactive
// content may not nest inside a button element.
return (
{
if (e.target !== e.currentTarget) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
{content}
);
}
return (
{content}
);
});
export const SystemPanelDetailStrip = memo(function SystemPanelDetailStrip({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
{children}
);
});
const COLLAPSE_MS = 180;
/**
* Expand/collapse with a height animation (grid-template-rows 0fr→1fr, no
* measuring). Children mount on open and unmount after the exit transition;
* the last rendered children are kept during exit so collapse animates even
* when the parent clears them together with the open flag.
*/
export function SystemPanelCollapsible({
open,
children,
}: {
open: boolean;
children: ReactNode;
}) {
const [mounted, setMounted] = useState(open);
const [expanded, setExpanded] = useState(open);
const lastChildrenRef = useRef(children);
if (open) lastChildrenRef.current = children;
useEffect(() => {
if (open) {
setMounted(true);
// Two frames so the 0fr state paints before transitioning to 1fr.
let raf2 = 0;
const raf1 = requestAnimationFrame(() => {
raf2 = requestAnimationFrame(() => setExpanded(true));
});
return () => {
cancelAnimationFrame(raf1);
cancelAnimationFrame(raf2);
};
}
setExpanded(false);
const timer = setTimeout(() => {
setMounted(false);
lastChildrenRef.current = null;
}, COLLAPSE_MS);
return () => clearTimeout(timer);
}, [open]);
if (!mounted) return null;
return (
{open ? children : lastChildrenRef.current}
);
}
export const SystemPanelActionChip = memo(function SystemPanelActionChip({
title,
onClick,
destructive,
disabled,
children,
}: {
title: string;
onClick: () => void;
destructive?: boolean;
disabled?: boolean;
children: ReactNode;
}) {
return (
{children}
);
});
export const SystemPanelMiniButton = memo(function SystemPanelMiniButton({
title,
onClick,
disabled,
children,
}: {
title: string;
onClick: () => void;
disabled?: boolean;
children: ReactNode;
}) {
return (
{children}
{title}
);
});
/** Solid bright status pill — same palette as the vault entity icons. */
export const SystemPanelStatusBadge = memo(function SystemPanelStatusBadge({
tone,
children,
}: {
tone: 'success' | 'warning' | 'muted';
children: ReactNode;
}) {
return (
{/* Subtle top highlight sheen */}
{children}
);
});
/** Always-visible round icon button for list-row quick actions. */
export const SystemPanelRoundButton = memo(function SystemPanelRoundButton({
title,
onClick,
disabled,
destructive,
loading,
children,
}: {
title: string;
onClick: () => void;
disabled?: boolean;
destructive?: boolean;
/** Shows a spinner instead of the icon and disables the button. */
loading?: boolean;
children: ReactNode;
}) {
const isDisabled = disabled || loading;
return (
{
e.stopPropagation();
onClick();
}}
className={cn(
'h-6 w-6 shrink-0 rounded-full flex items-center justify-center bg-muted/60 text-muted-foreground transition-colors disabled:opacity-40',
destructive
? 'hover:bg-destructive/20 hover:text-destructive'
: 'hover:bg-muted hover:text-foreground',
)}
>
{loading ? : children}
{title}
);
});
/** Small uppercase section divider inside expanded details. */
export const SystemPanelSectionHeader = memo(function SystemPanelSectionHeader({
children,
trailing,
}: {
children: ReactNode;
trailing?: ReactNode;
}) {
return (
);
});
export const SystemPanelInspectBlock = memo(function SystemPanelInspectBlock({
title,
data,
onClose,
closeLabel,
}: {
title: string;
data: Record;
onClose?: () => void;
closeLabel?: string;
}) {
return (
{title}
{onClose && closeLabel && (
{closeLabel}
)}
{JSON.stringify(data, null, 2)}
);
});