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 ( {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) => ( ))}
); }); export const SystemPanelMetaBar = memo(function SystemPanelMetaBar({ children, trailing, }: { children: ReactNode; trailing?: ReactNode; }) { return (
{children}
{trailing}
); }); 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 && ( )}
); }); export const SystemPanelInlineError = memo(function SystemPanelInlineError({ message, onRetry, retryLabel, loading, }: { message: string; onRetry?: () => void; retryLabel?: string; loading?: boolean; }) { return (
{message} {onRetry && 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 ); }); export const SystemPanelMiniButton = memo(function SystemPanelMiniButton({ title, onClick, disabled, children, }: { title: string; onClick: () => void; disabled?: boolean; children: ReactNode; }) { return ( {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 ( {title} ); }); /** Small uppercase section divider inside expanded details. */ export const SystemPanelSectionHeader = memo(function SystemPanelSectionHeader({ children, trailing, }: { children: ReactNode; trailing?: ReactNode; }) { return (
{children}
{trailing}
); }); export const SystemPanelInspectBlock = memo(function SystemPanelInspectBlock({ title, data, onClose, closeLabel, }: { title: string; data: Record; onClose?: () => void; closeLabel?: string; }) { return (
{title} {onClose && closeLabel && ( )}
        {JSON.stringify(data, null, 2)}
      
); });