138 lines
3.6 KiB
TypeScript
138 lines
3.6 KiB
TypeScript
import React, { forwardRef } from 'react';
|
|
import { clsx } from 'clsx';
|
|
|
|
export interface ListItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
title: string;
|
|
subtitle?: string;
|
|
description?: string;
|
|
leadingContent?: React.ReactNode;
|
|
trailingContent?: React.ReactNode;
|
|
variant?: 'default' | 'compact' | 'detailed';
|
|
interactive?: boolean;
|
|
active?: boolean;
|
|
disabled?: boolean;
|
|
divider?: boolean;
|
|
}
|
|
|
|
const ListItem = forwardRef<HTMLDivElement, ListItemProps>(({
|
|
title,
|
|
subtitle,
|
|
description,
|
|
leadingContent,
|
|
trailingContent,
|
|
variant = 'default',
|
|
interactive = false,
|
|
active = false,
|
|
disabled = false,
|
|
divider = false,
|
|
className,
|
|
children,
|
|
onClick,
|
|
...props
|
|
}, ref) => {
|
|
const baseClasses = [
|
|
'flex items-center w-full',
|
|
'transition-colors duration-200',
|
|
];
|
|
|
|
const variantClasses = {
|
|
default: 'py-3 px-4',
|
|
compact: 'py-2 px-3',
|
|
detailed: 'py-4 px-4',
|
|
};
|
|
|
|
const stateClasses = {
|
|
interactive: interactive ? [
|
|
'cursor-pointer',
|
|
'hover:bg-[var(--bg-secondary)]',
|
|
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20',
|
|
] : [],
|
|
active: active ? 'bg-[var(--color-primary)]/10 border-r-2 border-[var(--color-primary)]' : '',
|
|
disabled: disabled ? 'opacity-50 cursor-not-allowed' : '',
|
|
};
|
|
|
|
const Component = interactive ? 'button' : 'div';
|
|
|
|
return (
|
|
<>
|
|
<Component
|
|
ref={ref}
|
|
className={clsx(
|
|
baseClasses,
|
|
variantClasses[variant],
|
|
stateClasses.interactive,
|
|
stateClasses.active,
|
|
stateClasses.disabled,
|
|
className
|
|
)}
|
|
onClick={disabled ? undefined : onClick}
|
|
disabled={disabled}
|
|
{...props}
|
|
>
|
|
{/* Leading content */}
|
|
{leadingContent && (
|
|
<div className="flex-shrink-0 mr-3">
|
|
{leadingContent}
|
|
</div>
|
|
)}
|
|
|
|
{/* Main content */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between">
|
|
<div className="min-w-0 flex-1">
|
|
<p className={clsx(
|
|
'font-medium text-[var(--text-primary)] truncate',
|
|
{
|
|
'text-sm': variant === 'compact',
|
|
'text-base': variant === 'default',
|
|
'text-lg': variant === 'detailed',
|
|
}
|
|
)}>
|
|
{title}
|
|
</p>
|
|
|
|
{subtitle && (
|
|
<p className={clsx(
|
|
'text-[var(--text-secondary)] truncate mt-1',
|
|
{
|
|
'text-xs': variant === 'compact',
|
|
'text-sm': variant === 'default' || variant === 'detailed',
|
|
}
|
|
)}>
|
|
{subtitle}
|
|
</p>
|
|
)}
|
|
|
|
{description && variant === 'detailed' && (
|
|
<p className="text-sm text-[var(--text-tertiary)] mt-1 line-clamp-2">
|
|
{description}
|
|
</p>
|
|
)}
|
|
|
|
{children && (
|
|
<div className="mt-2">
|
|
{children}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Trailing content */}
|
|
{trailingContent && (
|
|
<div className="flex-shrink-0 ml-3">
|
|
{trailingContent}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Component>
|
|
|
|
{divider && (
|
|
<div className="border-b border-[var(--border-primary)] mx-4" />
|
|
)}
|
|
</>
|
|
);
|
|
});
|
|
|
|
ListItem.displayName = 'ListItem';
|
|
|
|
export default ListItem; |