233 lines
6.0 KiB
TypeScript
233 lines
6.0 KiB
TypeScript
import { ReactNode, FormHTMLAttributes } from 'react';
|
||
import { Form as RemixForm } from '@remix-run/react';
|
||
import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
|
||
import { Button } from './Button';
|
||
import { Text } from './Text';
|
||
|
||
interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'className'> {
|
||
children: ReactNode;
|
||
className?: string;
|
||
config?: Partial<LayoutConfig>;
|
||
title?: string;
|
||
description?: string;
|
||
loading?: boolean;
|
||
error?: string;
|
||
success?: string;
|
||
actions?: ReactNode;
|
||
spacing?: 'sm' | 'md' | 'lg';
|
||
}
|
||
|
||
export function Form({
|
||
children,
|
||
className = '',
|
||
config = {},
|
||
title,
|
||
description,
|
||
loading = false,
|
||
error,
|
||
success,
|
||
actions,
|
||
spacing = 'md',
|
||
...props
|
||
}: FormProps) {
|
||
const layoutConfig = { ...defaultLayoutConfig, ...config };
|
||
|
||
const spacingClasses = {
|
||
sm: 'space-y-4',
|
||
md: 'space-y-6',
|
||
lg: 'space-y-8',
|
||
};
|
||
|
||
return (
|
||
<div className={`${className}`} dir={layoutConfig.direction}>
|
||
{/* Form Header */}
|
||
{(title || description) && (
|
||
<div className="mb-6">
|
||
{title && (
|
||
<Text as="h2" size="xl" weight="semibold" className="mb-2">
|
||
{title}
|
||
</Text>
|
||
)}
|
||
{description && (
|
||
<Text color="secondary">
|
||
{description}
|
||
</Text>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Success Message */}
|
||
{success && (
|
||
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||
<div className="flex items-center">
|
||
<svg className="h-5 w-5 text-green-400 ml-2" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
<Text color="success" size="sm">
|
||
{success}
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error Message */}
|
||
{error && (
|
||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||
<div className="flex items-center">
|
||
<svg className="h-5 w-5 text-red-400 ml-2" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||
</svg>
|
||
<Text color="error" size="sm">
|
||
{error}
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Form Content */}
|
||
<RemixForm className={spacingClasses[spacing]} {...props}>
|
||
{children}
|
||
|
||
{/* Form Actions */}
|
||
{actions && (
|
||
<div className="pt-4 border-t border-gray-200">
|
||
{actions}
|
||
</div>
|
||
)}
|
||
</RemixForm>
|
||
|
||
{/* Loading Overlay */}
|
||
{loading && (
|
||
<div className="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded-lg">
|
||
<div className="flex items-center space-x-2 space-x-reverse">
|
||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||
<Text color="secondary">جاري المعالجة...</Text>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Form Actions Component
|
||
interface FormActionsProps {
|
||
children: ReactNode;
|
||
className?: string;
|
||
config?: Partial<LayoutConfig>;
|
||
justify?: 'start' | 'center' | 'end' | 'between';
|
||
spacing?: 'sm' | 'md' | 'lg';
|
||
}
|
||
|
||
export function FormActions({
|
||
children,
|
||
className = '',
|
||
config = {},
|
||
justify = 'end',
|
||
spacing = 'md',
|
||
}: FormActionsProps) {
|
||
const layoutConfig = { ...defaultLayoutConfig, ...config };
|
||
|
||
const justifyClasses = {
|
||
start: 'justify-start',
|
||
center: 'justify-center',
|
||
end: 'justify-end',
|
||
between: 'justify-between',
|
||
};
|
||
|
||
const spacingClasses = {
|
||
sm: 'space-x-2 space-x-reverse',
|
||
md: 'space-x-4 space-x-reverse',
|
||
lg: 'space-x-6 space-x-reverse',
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`flex ${justifyClasses[justify]} ${spacingClasses[spacing]} ${className}`}
|
||
dir={layoutConfig.direction}
|
||
>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Form Section Component
|
||
interface FormSectionProps {
|
||
children: ReactNode;
|
||
title?: string;
|
||
description?: string;
|
||
className?: string;
|
||
config?: Partial<LayoutConfig>;
|
||
}
|
||
|
||
export function FormSection({
|
||
children,
|
||
title,
|
||
description,
|
||
className = '',
|
||
config = {},
|
||
}: FormSectionProps) {
|
||
const layoutConfig = { ...defaultLayoutConfig, ...config };
|
||
|
||
return (
|
||
<div className={`${className}`} dir={layoutConfig.direction}>
|
||
{(title || description) && (
|
||
<div className="mb-4">
|
||
{title && (
|
||
<Text as="h3" size="lg" weight="medium" className="mb-1">
|
||
{title}
|
||
</Text>
|
||
)}
|
||
{description && (
|
||
<Text color="secondary" size="sm">
|
||
{description}
|
||
</Text>
|
||
)}
|
||
</div>
|
||
)}
|
||
<div className="space-y-4">
|
||
{children}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Form Grid Component
|
||
interface FormGridProps {
|
||
children: ReactNode;
|
||
columns?: 1 | 2 | 3 | 4;
|
||
className?: string;
|
||
config?: Partial<LayoutConfig>;
|
||
gap?: 'sm' | 'md' | 'lg';
|
||
}
|
||
|
||
export function FormGrid({
|
||
children,
|
||
columns = 2,
|
||
className = '',
|
||
config = {},
|
||
gap = 'md',
|
||
}: FormGridProps) {
|
||
const layoutConfig = { ...defaultLayoutConfig, ...config };
|
||
|
||
const columnClasses = {
|
||
1: 'grid-cols-1',
|
||
2: 'grid-cols-1 md:grid-cols-2',
|
||
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
||
};
|
||
|
||
const gapClasses = {
|
||
sm: 'gap-4',
|
||
md: 'gap-6',
|
||
lg: 'gap-8',
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className={`grid ${columnClasses[columns]} ${gapClasses[gap]} ${className}`}
|
||
dir={layoutConfig.direction}
|
||
>
|
||
{children}
|
||
</div>
|
||
);
|
||
} |